From 5c716de97340b56f6e112fd766189c4deef6420e Mon Sep 17 00:00:00 2001 From: MrMha <114929652+MrMha@users.noreply.github.com> Date: Mon, 12 Oct 2026 01:26:48 +0400 Subject: [PATCH 0001/1000] perf(shader): rewrite high-fan-out IR uses in linear time --- CMakeLists.txt | 11 + .../src/IrValue.cpp | 26 +- .../tests/UseRewrite.cpp | 244 ++++++++++++++++++ .../tests/UseRewriteAllocation.cpp | 20 ++ 4 files changed, 298 insertions(+), 3 deletions(-) create mode 100644 core/shader/recompiler/IntermediateRepresentation/tests/UseRewrite.cpp create mode 100644 core/shader/recompiler/IntermediateRepresentation/tests/UseRewriteAllocation.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 5cb4fb85c8..dc6dbf6924 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -38,6 +38,17 @@ endif() if(BUILD_TESTING) find_package(Python3 QUIET COMPONENTS Interpreter) + add_test_executable(shader_ir_use_rewrite_tests + core/shader/recompiler/IntermediateRepresentation/tests/UseRewrite.cpp + core/shader/recompiler/IntermediateRepresentation/tests/UseRewriteAllocation.cpp + core/shader/recompiler/IntermediateRepresentation/src/IrValue.cpp + core/shader/recompiler/IntermediateRepresentation/src/IrBlock.cpp + core/shader/recompiler/IntermediateRepresentation/src/IrOpcode.cpp + core/shader/recompiler/IntermediateRepresentation/src/GuestRegister.cpp) + target_include_directories(shader_ir_use_rewrite_tests PRIVATE + core/shader/recompiler/IntermediateRepresentation/include) + add_test(NAME shader_ir_use_rewrite COMMAND shader_ir_use_rewrite_tests) + set_tests_properties(shader_ir_use_rewrite PROPERTIES TIMEOUT 20) if(Python3_Interpreter_FOUND) add_test(NAME pr_overlap_encoding COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_pr_overlap_encoding.py) diff --git a/core/shader/recompiler/IntermediateRepresentation/src/IrValue.cpp b/core/shader/recompiler/IntermediateRepresentation/src/IrValue.cpp index 4a1a662b69..33273410cf 100644 --- a/core/shader/recompiler/IntermediateRepresentation/src/IrValue.cpp +++ b/core/shader/recompiler/IntermediateRepresentation/src/IrValue.cpp @@ -65,10 +65,30 @@ void IrValue::ReplaceAllUsesWith(IrValue* replacement) { if (replacement == this) { return; } - while (!operandUses.empty()) { - const IrUse use = operandUses.back(); - use.user->ReplaceArgument(use.operand, replacement); + if (replacement != nullptr) { + const auto reserveAdditional = [count = operandUses.size()](auto& list) { + const auto maximum = list.max_size(); + if (count > maximum - list.size()) { + throw std::length_error("IrValue::ReplaceAllUsesWith use list is too large"); + } + const auto required = list.size() + count; + if (required > list.capacity()) { + const auto grown = list.capacity() > maximum / 2 ? maximum : list.capacity() * 2; + list.reserve(std::max(required, grown)); + } + }; + reserveAdditional(replacement->uses); + reserveAdditional(replacement->operandUses); } + for (auto use = operandUses.rbegin(); use != operandUses.rend(); ++use) { + use->user->arguments[use->operand] = replacement; + if (replacement != nullptr) { + replacement->uses.push_back(use->user); + replacement->operandUses.push_back(*use); + } + } + uses.clear(); + operandUses.clear(); } void IrValue::SetImmediateU32(std::uint32_t value) { diff --git a/core/shader/recompiler/IntermediateRepresentation/tests/UseRewrite.cpp b/core/shader/recompiler/IntermediateRepresentation/tests/UseRewrite.cpp new file mode 100644 index 0000000000..3308434316 --- /dev/null +++ b/core/shader/recompiler/IntermediateRepresentation/tests/UseRewrite.cpp @@ -0,0 +1,244 @@ +#include "IntermediateRepresentation/IrBlock.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace ShaderRecompiler; + +namespace IrRewriteAllocation { +extern std::ptrdiff_t failureAfter; +extern std::size_t allocations; +extern bool countAllocations; +} +using namespace IrRewriteAllocation; + +void Require(bool condition) { + if (!condition) throw std::runtime_error("IR use rewrite assertion failed"); +} + +struct Graph { + IrBlock block{1}; + std::vector> values; + + explicit Graph(std::size_t count) { + for (std::size_t index = 0; index < count; ++index) { + auto value = std::make_unique(IrOpcode::IAdd32, IrType::U32, static_cast(index)); + value->SetFlags(static_cast(index * 17)); + block.AppendInstruction(value.get()); + values.push_back(std::move(value)); + } + } + + IrValue* Value(std::size_t index) const { return values.at(index).get(); } +}; + +void ReferenceReplace(IrValue* source, IrValue* replacement) { + if (source == replacement) return; + while (!source->OperandUses().empty()) { + const IrUse use = source->OperandUses().back(); + use.user->ReplaceArgument(use.operand, replacement); + } +} + +void EqualGraphs(const Graph& first, const Graph& second) { + Require(first.values.size() == second.values.size()); + for (std::size_t index = 0; index < first.values.size(); ++index) { + const auto* left = first.Value(index); + const auto* right = second.Value(index); + Require(left->Opcode() == right->Opcode() && left->Type() == right->Type()); + Require(left->Id() == right->Id() && left->Flags() == right->Flags()); + Require((left->Parent() == nullptr) == (right->Parent() == nullptr)); + if (left->Parent()) Require(left->Parent()->Id() == right->Parent()->Id()); + Require(left->ArgumentCount() == right->ArgumentCount()); + for (std::size_t operand = 0; operand < left->ArgumentCount(); ++operand) { + const auto* a = left->Argument(operand); + const auto* b = right->Argument(operand); + Require((a == nullptr) == (b == nullptr)); + if (a) Require(a->Id() == b->Id()); + } + Require(left->Uses().size() == right->Uses().size()); + for (std::size_t use = 0; use < left->Uses().size(); ++use) { + Require(left->Uses()[use]->Id() == right->Uses()[use]->Id()); + } + Require(left->OperandUses().size() == right->OperandUses().size()); + for (std::size_t use = 0; use < left->OperandUses().size(); ++use) { + const auto& a = left->OperandUses()[use]; + const auto& b = right->OperandUses()[use]; + Require(a.user->Id() == b.user->Id() && a.operand == b.operand); + } + } +} + +void CheckSimpleReplacement() { + Graph graph(5); + auto* source = graph.Value(0); + auto* target = graph.Value(1); + auto* first = graph.Value(2); + auto* second = graph.Value(3); + auto* existing = graph.Value(4); + existing->AddArgument(target); + first->AddArgument(source); + second->AddArgument(source); + first->AddArgument(source); + first->AddArgument(target); + source->ReplaceAllUsesWith(target); + Require(!source->HasUses() && source->OperandUses().empty()); + Require(first->Argument(0) == target && first->Argument(1) == target && first->Argument(2) == target); + Require(second->Argument(0) == target); + const std::vector expected{{existing, 0}, {first, 2}, {first, 1}, {second, 0}, {first, 0}}; + Require(target->OperandUses() == expected); + Require(target->Uses() == std::vector({existing, first, first, second, first})); + first->ReplaceArgument(1, source); + Require(source->UseCount() == 1); + source->ReplaceAllUsesWith(nullptr); + Require(first->Argument(1) == nullptr && target->UseCount() == 4); + Require(source->Opcode() == IrOpcode::IAdd32 && source->Parent() == &graph.block); +} + +void CheckSelfAndEmpty() { + Graph graph(3); + graph.Value(2)->AddArgument(graph.Value(0)); + graph.Value(0)->ReplaceAllUsesWith(graph.Value(0)); + Require(graph.Value(0)->UseCount() == 1 && graph.Value(2)->Argument(0) == graph.Value(0)); + graph.Value(1)->ReplaceAllUsesWith(graph.Value(0)); + Require(graph.Value(0)->UseCount() == 1); + graph.Value(1)->ReplaceAllUsesWith(nullptr); + Require(!graph.Value(1)->HasUses()); +} + +void CheckPhiAndIdentity() { + Graph graph(4); + auto* source = graph.Value(0); + auto* target = graph.Value(1); + auto* phi = graph.Value(2); + phi->ReplaceOpcode(IrOpcode::Phi); + phi->AddPhiOperand(&graph.block, source); + phi->AddPhiOperand(&graph.block, source); + graph.Value(3)->ReplaceOpcode(IrOpcode::Identity); + graph.Value(3)->AddArgument(source); + source->ReplaceUsesWith(target, false); + Require(source->Opcode() == IrOpcode::Unreachable); + Require(phi->Argument(0) == target && phi->Argument(1) == target); + Require(phi->PhiBlockCount() == 2 && phi->PhiBlock(0) == &graph.block && phi->PhiBlock(1) == &graph.block); + Require(graph.Value(3)->Resolve() == target); + Require(target->UseCount() == 3); +} + +void CheckAllocationFailure() { + for (std::ptrdiff_t failure = 0; failure < 2; ++failure) { + Graph graph(12); + for (std::size_t index = 2; index < 12; ++index) { + graph.Value(index)->AddArgument(graph.Value(0)); + } + bool caught = false; + failureAfter = failure; + try { + graph.Value(0)->ReplaceAllUsesWith(graph.Value(1)); + } catch (const std::bad_alloc&) { + caught = true; + } catch (...) { + failureAfter = -1; + throw; + } + failureAfter = -1; + Require(caught && graph.Value(0)->UseCount() == 10); + Require(graph.Value(0)->OperandUses().size() == 10); + Require(!graph.Value(1)->HasUses() && graph.Value(1)->OperandUses().empty()); + for (std::size_t index = 2; index < 12; ++index) { + Require(graph.Value(index)->Argument(0) == graph.Value(0)); + } + graph.Value(0)->ReplaceAllUsesWith(graph.Value(1)); + Require(graph.Value(1)->UseCount() == 10); + } +} + +void CheckGeometricGrowth() { + constexpr std::size_t count = 4096; + Graph graph(count + 2); + std::vector> sources; + for (std::size_t index = 2; index < count + 2; ++index) { + sources.push_back(std::make_unique(IrOpcode::UndefU32, IrType::U32, index + count + 2)); + graph.Value(index)->AddArgument(sources.back().get()); + } + allocations = 0; + countAllocations = true; + for (auto& source : sources) source->ReplaceAllUsesWith(graph.Value(1)); + countAllocations = false; + Require(allocations <= 2 * (std::bit_width(count) + 1)); + Require(graph.Value(1)->UseCount() == count); + for (std::size_t index = 2; index < count + 2; ++index) Require(graph.Value(index)->Argument(0) == graph.Value(1)); +} + +void CheckLargeFanOut() { + constexpr std::size_t count = 100000; + Graph graph(count + 2); + for (std::size_t index = 2; index < count + 2; ++index) { + graph.Value(index)->AddArgument(graph.Value(0)); + graph.Value(index)->AddArgument(graph.Value(0)); + } + graph.Value(0)->ReplaceAllUsesWith(graph.Value(1)); + Require(graph.Value(1)->UseCount() == count * 2 && !graph.Value(0)->HasUses()); + for (std::size_t index = 2; index < count + 2; ++index) { + Require(graph.Value(index)->Argument(0) == graph.Value(1) && graph.Value(index)->Argument(1) == graph.Value(1)); + } +} + +void CheckDifferentialPrograms() { + std::mt19937 random(0x4f23a771); + constexpr std::size_t count = 32; + for (std::size_t trial = 0; trial < 2000; ++trial) { + Graph actual(count), reference(count); + for (std::size_t index = 0; index < count; ++index) { + const auto operands = random() % 5; + for (std::size_t operand = 0; operand < operands; ++operand) { + const auto argument = random() % (count + 1); + actual.Value(index)->AddArgument(argument == count ? nullptr : actual.Value(argument)); + reference.Value(index)->AddArgument(argument == count ? nullptr : reference.Value(argument)); + } + } + for (std::size_t step = 0; step < 24; ++step) { + const auto source = random() % count; + const auto replacement = random() % (count + 1); + if (random() % 4 == 0 && actual.Value(source)->ArgumentCount() > 0) { + const auto operand = random() % actual.Value(source)->ArgumentCount(); + actual.Value(source)->ReplaceArgument(operand, replacement == count ? nullptr : actual.Value(replacement)); + reference.Value(source)->ReplaceArgument(operand, replacement == count ? nullptr : reference.Value(replacement)); + } else { + actual.Value(source)->ReplaceAllUsesWith(replacement == count ? nullptr : actual.Value(replacement)); + ReferenceReplace(reference.Value(source), replacement == count ? nullptr : reference.Value(replacement)); + } + EqualGraphs(actual, reference); + } + } +} + +int main() { + const char* current = "setup"; + try { + const std::pair cases[] = { + {"ordered duplicate operands", CheckSimpleReplacement}, + {"self and empty replacement", CheckSelfAndEmpty}, + {"phi and identity rewriting", CheckPhiAndIdentity}, + {"allocation failure integrity", CheckAllocationFailure}, + {"geometric use-list growth", CheckGeometricGrowth}, + {"large fan-out", CheckLargeFanOut}, + {"differential use graphs", CheckDifferentialPrograms} + }; + for (const auto& [name, test] : cases) { + current = name; + test(); + } + std::puts("7 IR rewrite suites passed; 2000 differential use graphs and 48000 mutation comparisons"); + return 0; + } catch (const std::exception& error) { + failureAfter = -1; + countAllocations = false; + std::fprintf(stderr, "%s: %s\n", current, error.what()); + return 1; + } +} diff --git a/core/shader/recompiler/IntermediateRepresentation/tests/UseRewriteAllocation.cpp b/core/shader/recompiler/IntermediateRepresentation/tests/UseRewriteAllocation.cpp new file mode 100644 index 0000000000..4681d4b223 --- /dev/null +++ b/core/shader/recompiler/IntermediateRepresentation/tests/UseRewriteAllocation.cpp @@ -0,0 +1,20 @@ +#include +#include +#include + +namespace IrRewriteAllocation { +std::ptrdiff_t failureAfter = -1; +std::size_t allocations = 0; +bool countAllocations = false; +} + +void* operator new(std::size_t size) { + if (IrRewriteAllocation::failureAfter == 0) throw std::bad_alloc(); + if (IrRewriteAllocation::failureAfter > 0) --IrRewriteAllocation::failureAfter; + if (IrRewriteAllocation::countAllocations) ++IrRewriteAllocation::allocations; + if (void* memory = std::malloc(size == 0 ? 1 : size)) return memory; + throw std::bad_alloc(); +} + +void operator delete(void* memory) noexcept { std::free(memory); } +void operator delete(void* memory, std::size_t) noexcept { std::free(memory); } From 05f2dc4c91cbde3ee286be3dff627babb02f9257 Mon Sep 17 00:00:00 2001 From: JJenkx Date: Mon, 12 Oct 2026 01:18:07 -0400 Subject: [PATCH 0002/1000] feat(libkernel): support process-private mutex sharing attributes --- .../prx/libkernel/Pthread/Posix/Mutex.cpp | 16 +++++++++ core/libs/tests/GuestPthreadMutexattr.cpp | 35 +++++++++++++++++++ docs/dev/TechnicalDebt.md | 2 ++ 3 files changed, 53 insertions(+) diff --git a/core/libs/prx/libkernel/Pthread/Posix/Mutex.cpp b/core/libs/prx/libkernel/Pthread/Posix/Mutex.cpp index 7ab97118fb..3782f0d12d 100644 --- a/core/libs/prx/libkernel/Pthread/Posix/Mutex.cpp +++ b/core/libs/prx/libkernel/Pthread/Posix/Mutex.cpp @@ -12,6 +12,9 @@ namespace { constexpr int POSIX_EINVAL = 22; +constexpr int POSIX_ENOTSUP = 45; +constexpr int POSIX_PROCESS_PRIVATE = 0; +constexpr int POSIX_PROCESS_SHARED = 1; constexpr int GUEST_REALTIME_CLOCK = 0; constexpr int POSIX_PRIO_PROTECT = 2; constexpr int POSIX_SCHED_RR = 3; @@ -88,6 +91,19 @@ int APS5_VABI pthread_mutexattr_init_nid_postfix(PthreadMutexattr* attr) { return PosixThread::ToErrno(result); } +int APS5_VABI pthread_mutexattr_getpshared_nid_postfix(const PthreadMutexattr* attr, int* pshared) { + if (!attr || !*attr) return POSIX_EINVAL; + if (!pshared) throw std::runtime_error("pthread_mutexattr_getpshared: null pshared"); + *pshared = POSIX_PROCESS_PRIVATE; + return 0; +} + +int APS5_VABI pthread_mutexattr_setpshared_nid_postfix(PthreadMutexattr* attr, int pshared) { + if (!attr || !*attr) return POSIX_EINVAL; + if (pshared == POSIX_PROCESS_PRIVATE) return 0; + return pshared == POSIX_PROCESS_SHARED ? POSIX_ENOTSUP : POSIX_EINVAL; +} + int APS5_VABI pthread_mutexattr_setprotocol_nid_postfix(PthreadMutexattr* attr, int protocol) { if (!attr || !*attr) throw std::runtime_error("pthread_mutexattr_setprotocol: null attr"); if (protocol < 0 || protocol > POSIX_PRIO_PROTECT) return POSIX_EINVAL; diff --git a/core/libs/tests/GuestPthreadMutexattr.cpp b/core/libs/tests/GuestPthreadMutexattr.cpp index 410aa02ba0..8de61a9233 100644 --- a/core/libs/tests/GuestPthreadMutexattr.cpp +++ b/core/libs/tests/GuestPthreadMutexattr.cpp @@ -6,6 +6,8 @@ extern "C" { int APS5_VABI pthread_mutexattr_init_nid_postfix(PthreadMutexattr* attr); int APS5_VABI pthread_mutexattr_destroy_nid_postfix(PthreadMutexattr* attr); +int APS5_VABI pthread_mutexattr_getpshared_nid_postfix(const PthreadMutexattr* attr, int* pshared); +int APS5_VABI pthread_mutexattr_setpshared_nid_postfix(PthreadMutexattr* attr, int pshared); int APS5_VABI pthread_mutexattr_setprotocol_nid_postfix(PthreadMutexattr* attr, int protocol); int APS5_VABI pthread_mutexattr_setprioceiling_nid_postfix(PthreadMutexattr* attr, int prioceiling); int APS5_VABI pthread_mutexattr_getprioceiling_nid_postfix(const PthreadMutexattr* attr, int* prioceiling); @@ -22,6 +24,7 @@ int APS5_VABI sched_get_priority_max_nid_postfix(int policy); } static constexpr int GUEST_EINVAL = 22; +static constexpr int GUEST_ENOTSUP = 45; static constexpr int PRIO_NONE = 0; static constexpr int PRIO_INHERIT = 1; static constexpr int PRIO_PROTECT = 2; @@ -76,8 +79,40 @@ static void CheckType() { Require(pthread_mutexattr_gettype_nid_postfix(nullptr, &type) == GUEST_EINVAL && type == -1); } +static void CheckProcessSharing() { + PthreadMutexattr attr = nullptr; + int sharing = -1; + Require(pthread_mutexattr_getpshared_nid_postfix(nullptr, &sharing) == GUEST_EINVAL && sharing == -1); + Require(pthread_mutexattr_getpshared_nid_postfix(&attr, &sharing) == GUEST_EINVAL && sharing == -1); + Require(pthread_mutexattr_setpshared_nid_postfix(nullptr, 0) == GUEST_EINVAL); + Require(pthread_mutexattr_setpshared_nid_postfix(&attr, 0) == GUEST_EINVAL); + Require(pthread_mutexattr_init_nid_postfix(&attr) == 0); + Require(pthread_mutexattr_getpshared_nid_postfix(&attr, &sharing) == 0 && sharing == 0); + Require(pthread_mutexattr_settype_nid_postfix(&attr, 2) == 0); + Require(pthread_mutexattr_setpshared_nid_postfix(&attr, 0) == 0); + Require(pthread_mutexattr_setpshared_nid_postfix(&attr, 1) == GUEST_ENOTSUP); + for (const int invalid : {-1, 2, 2147483647}) + Require(pthread_mutexattr_setpshared_nid_postfix(&attr, invalid) == GUEST_EINVAL); + Require(pthread_mutexattr_getpshared_nid_postfix(&attr, &sharing) == 0 && sharing == 0); + int type = -1; + Require(pthread_mutexattr_gettype_nid_postfix(&attr, &type) == 0 && type == 2); + Require(Throws([&] { pthread_mutexattr_getpshared_nid_postfix(&attr, nullptr); })); + PthreadMutex mutex = nullptr; + Require(pthread_mutex_init_nid_postfix(&mutex, &attr) == 0); + Require(pthread_mutex_lock_nid_postfix(&mutex) == 0); + Require(pthread_mutex_lock_nid_postfix(&mutex) == 0); + Require(pthread_mutex_unlock_nid_postfix(&mutex) == 0); + Require(pthread_mutex_unlock_nid_postfix(&mutex) == 0); + Require(pthread_mutex_destroy_nid_postfix(&mutex) == 0); + Require(pthread_mutexattr_destroy_nid_postfix(&attr) == 0); + sharing = -1; + Require(pthread_mutexattr_getpshared_nid_postfix(&attr, &sharing) == GUEST_EINVAL && sharing == -1); + Require(pthread_mutexattr_setpshared_nid_postfix(&attr, 0) == GUEST_EINVAL); +} + int main() { CheckType(); + CheckProcessSharing(); CheckTimedlockInvalidDeadline(); PthreadMutexattr attr = nullptr; Require(pthread_mutexattr_init_nid_postfix(&attr) == 0); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index b2f878ee68..ec92990439 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -3,6 +3,7 @@ ### Build - Building on Windows requires a specific version of mingw - MinGW-w64 GCC 15.2.0 (`winlibs-gcc15`, `x86_64-ucrt-posix-seh`) + - Even compiled prx libraries on Windows require nearby (static linking of these dependencies causes conflicts): - libgcc_s_seh-1.dll - libstdc++-6.dll @@ -401,6 +402,7 @@ Throughout the project, every function at every stage either **does exactly what ### Functional - [Reused depth storage](../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp): a storage view with another extent or a format that cannot hold the cached depth plane uses guest memory. Depth rendering is not written back to those guest bytes; this admission rule does not prove allocation reuse or preserve a depth writer through an incompatible view. Matching depth/stencil storage access still throws. +- [pthread_mutexattr_getpshared and pthread_mutexattr_setpshared](../../core/libs/prx/libkernel/Pthread/Posix/Mutex.cpp) (libkernel) support process-private mutexes. Setting `PTHREAD_PROCESS_SHARED` returns `ENOTSUP` because mutexes use process-local host objects; no attribute is changed on failure. Values 0 and 1 and guest error 45 follow FreeBSD's [pthread.h](https://github.com/freebsd/freebsd-src/blob/main/include/pthread.h) and [errno.h](https://github.com/freebsd/freebsd-src/blob/main/sys/sys/errno.h); the unsupported-sharing error follows [pthread_mutexattr_setpshared](https://man7.org/linux/man-pages/man3/pthread_mutexattr_getpshared.3.html). PS5 shared-mutex behavior and invalid-attribute error precedence are unverified. - [sceAudioInHqOpen, sceAudioInAsyncOpen](../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) open the same port as `sceAudioInOpen`, so `sceAudioInInput`, `sceAudioInGetSilentState` and `sceAudioInClose` accept their handles. The signatures agree across [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/fc5d2cc219c43c8c8f354153e7b3d0c04a5d506e/src/core/libraries/audio/audioin.cpp), [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Audio/AudioIn.cs) and [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/c86c8b4d9333a766cc8c4495f89f893bf35e6199/src/libs/audio.cpp). HqOpen is limited to 128 samples at 48 kHz; AsyncOpen rejects sizes above 384 and throws for currently unverified inputs. Host capture pacing and console behavior remain unverified. From e306204c582e807658656e7911f84d22b4653140 Mon Sep 17 00:00:00 2001 From: JJenkx Date: Sun, 11 Oct 2026 23:07:27 -0400 Subject: [PATCH 0003/1000] fix(relinker): clear Linux guest finalizer arguments --- core/relinker/CMakeLists.txt | 3 + .../src/linux/LinuxGuestModuleWriter.cpp | 8 +- .../elfpatcher/tests/GuestModuleArgsHost.cpp | 7 ++ .../relinker/tests/test_guest_module_fini.py | 81 +++++++++++++++++++ 4 files changed, 95 insertions(+), 4 deletions(-) create mode 100644 core/relinker/relinker/tests/test_guest_module_fini.py diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index 1f710b3fd8..cf48ed56a2 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -446,6 +446,9 @@ if(BUILD_TESTING) add_test(NAME guest_module_args COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_module_args.py $ $) + add_test(NAME guest_module_fini COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_module_fini.py + $ $) add_test(NAME guest_absolute_symbols COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_absolute_symbols.py $) add_test(NAME guest_module_identity COMMAND ${Python3_EXECUTABLE} diff --git a/core/relinker/elfpatcher/src/linux/LinuxGuestModuleWriter.cpp b/core/relinker/elfpatcher/src/linux/LinuxGuestModuleWriter.cpp index 76598e35ae..b64e126fec 100644 --- a/core/relinker/elfpatcher/src/linux/LinuxGuestModuleWriter.cpp +++ b/core/relinker/elfpatcher/src/linux/LinuxGuestModuleWriter.cpp @@ -141,18 +141,18 @@ std::vector GuestModuleWriter::WriteLinux(const Relinker::GuestIma bytes.insert(bytes.end(), stub.begin(), stub.end()); return start; }; - const auto callThrough = [&](std::uint64_t target) { + const auto finalizer = [&](std::uint64_t target) { if (target == 0) return std::uint64_t{}; const auto start = address(); - const auto distance = static_cast(target) - static_cast(start + 5); + const auto distance = static_cast(target) - static_cast(start + 11); if (distance < std::numeric_limits::min() || distance > std::numeric_limits::max()) throw Domain::RelinkerException("Guest finaliser exceeds relative branch range"); - bytes.push_back(0xe9); + bytes.insert(bytes.end(), {0x31, 0xff, 0x31, 0xf6, 0x31, 0xd2, 0xe9}); Io::AppendU32(bytes, static_cast(distance)); return start; }; const auto init = lifecycle(image.Init); - const auto fini = callThrough(image.Fini); + const auto fini = finalizer(image.Fini); Io::AlignBuffer(bytes, 8); const auto dynamicOffset = bytes.size(); const auto dynamicAddress = address(); diff --git a/core/relinker/elfpatcher/tests/GuestModuleArgsHost.cpp b/core/relinker/elfpatcher/tests/GuestModuleArgsHost.cpp index 708d39dc7b..b99a198578 100644 --- a/core/relinker/elfpatcher/tests/GuestModuleArgsHost.cpp +++ b/core/relinker/elfpatcher/tests/GuestModuleArgsHost.cpp @@ -45,6 +45,13 @@ extern "C" void SetPendingArgs(std::size_t args, const void* argp) { pending.argp = argp; } +extern "C" void APS5_VABI RecordFini(std::size_t args, const void* argp, const void* callback) { + char line[112]; + std::snprintf(line, sizeof(line), "fini=%016zx argp=%016zx callback=%016zx\n", args, + reinterpret_cast(argp), reinterpret_cast(callback)); + Write(line); +} + extern "C" const void* __aps5_get_pending_module_args_nid_no_patch() { return &pending; } diff --git a/core/relinker/relinker/tests/test_guest_module_fini.py b/core/relinker/relinker/tests/test_guest_module_fini.py new file mode 100644 index 0000000000..aa79a1fc69 --- /dev/null +++ b/core/relinker/relinker/tests/test_guest_module_fini.py @@ -0,0 +1,81 @@ +from pathlib import Path +import os +import shutil +import struct +import subprocess +import sys +import tempfile + +from test_guest_module_args import ARGS, ARGP, args_module +from test_guest_symbol_names import dynamic_tags +from test_windows_import_modules import executable + + +def fini_module(with_init): + image = args_module() + tags = dynamic_tags(image) + name = tags[10] + image[0x2600 + name:0x2600 + name + 11] = b'RecordFini\0' + entries = [(tag, value + 11 if tag == 10 else 48 if tag == 8 else value) + for tag, value in tags.items() if with_init or tag != 12] + entries += [(13, 0x1040), (0, 0)] + for index, entry in enumerate(entries): + struct.pack_into(' Date: Mon, 12 Oct 2026 19:52:22 -0400 Subject: [PATCH 0004/1000] feat(libc): implement x87 long-double finite classification --- core/libs/prx/libc/src/Math.cpp | 7 +++++++ core/libs/tests/GuestMath.cpp | 16 ++++++++++++++++ docs/dev/debt/libc.md | 2 ++ 3 files changed, 25 insertions(+) diff --git a/core/libs/prx/libc/src/Math.cpp b/core/libs/prx/libc/src/Math.cpp index 9ecf79e147..59d1c925aa 100644 --- a/core/libs/prx/libc/src/Math.cpp +++ b/core/libs/prx/libc/src/Math.cpp @@ -389,6 +389,13 @@ int APS5_VABI __signbitl_nid_postfix(long double x) { return std::signbit(x) ? 1 : 0; } +int APS5_VABI __isfinitel_nid_postfix(long double x) { + static_assert(sizeof(long double) == 16 && std::numeric_limits::digits == 64 && std::numeric_limits::max_exponent == 16384 && std::endian::native == std::endian::little, "guest long double requires little-endian x87 extended precision storage"); + std::uint16_t exponent; + std::memcpy(&exponent, reinterpret_cast(&x) + 8, sizeof(exponent)); + return (exponent & 0x7fffu) != 0x7fffu; +} + static std::mutex g_randLock; static std::uint32_t g_randState = 1; diff --git a/core/libs/tests/GuestMath.cpp b/core/libs/tests/GuestMath.cpp index 5a8e85f5ff..155b9ca92f 100644 --- a/core/libs/tests/GuestMath.cpp +++ b/core/libs/tests/GuestMath.cpp @@ -84,6 +84,7 @@ std::int64_t APS5_VABI lroundf_nid_postfix(float); std::int64_t APS5_VABI llround_nid_postfix(double); std::int64_t APS5_VABI llroundf_nid_postfix(float); int APS5_VABI __isfinitef_nid_postfix(float); +int APS5_VABI __isfinitel_nid_postfix(long double); int APS5_VABI __isnormal_nid_postfix(double); int APS5_VABI __isnormalf_nid_postfix(float); int APS5_VABI __isinff_nid_postfix(float); @@ -722,6 +723,21 @@ int main() { Require(__isinf_nid_postfix(0.) == 0 && __isinf_nid_postfix(1e308 * 10) == 1); Require(__isfinitef_nid_postfix(0.f) == 1 && __isfinitef_nid_postfix(infinity) == 0); Require(__isfinitef_nid_postfix(nan) == 0); + std::feclearexcept(FE_ALL_EXCEPT); + std::feraiseexcept(FE_DIVBYZERO); + for (const long double value : {0.0L, -0.0L, 1.0L, -1.0L, std::numeric_limits::denorm_min(), + -std::numeric_limits::denorm_min(), std::numeric_limits::min(), + std::numeric_limits::max(), std::numeric_limits::lowest(), + 0x1p1024L, -0x1p1024L, 0x1p-16382L}) { + Require(__isfinitel_nid_postfix(value) == 1); + } + for (const long double value : {std::numeric_limits::infinity(), -std::numeric_limits::infinity(), + std::numeric_limits::quiet_NaN(), -std::numeric_limits::quiet_NaN(), + std::numeric_limits::signaling_NaN(), -std::numeric_limits::signaling_NaN()}) { + Require(__isfinitel_nid_postfix(value) == 0); + } + Require(std::fetestexcept(FE_ALL_EXCEPT) == FE_DIVBYZERO); + std::feclearexcept(FE_ALL_EXCEPT); Require(__isnormalf_nid_postfix(1.f) == 1 && __isnormalf_nid_postfix(0.f) == 0); Require(__isnormalf_nid_postfix(std::numeric_limits::denorm_min()) == 0); Require(__isnormal_nid_postfix(1.) == 1 && __isnormal_nid_postfix(0.) == 0); diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index dfc540a6d5..8981ae0c18 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -26,6 +26,8 @@ ### Functional +- [__isfinitel](../../../core/libs/prx/libc/src/Math.cpp) - classifies the x87 80-bit exponent without narrowing to double or performing floating-point comparisons, following [FreeBSD msun](https://github.com/freebsd/freebsd-src/blob/main/lib/msun/src/s_isfinite.c). The signature and classification follow FreeBSD; console behaviour is unmeasured. Requires little-endian x87 long double stored in 16 bytes, as the existing guest long-double ABI does. + - [getc_unlocked, putc_unlocked](../../../core/libs/prx/libc/src/StandardStreams.cpp) (libc) - byte I/O uses the host unlocked operations; guest EOF/error flags and macro counters follow `FileStream`. Linux status queries use `feof_unlocked`/`ferror_unlocked` because [glibc `feof`](https://github.com/bminor/glibc/blob/d2097651cc57834dbfcaa102ddfacae0d86cfb66/libio/feof.c) and [`ferror`](https://github.com/bminor/glibc/blob/d2097651cc57834dbfcaa102ddfacae0d86cfb66/libio/ferror.c) can acquire the stream lock. Windows uses [the CRT no-lock byte operations](https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/fgetc-nolock-fgetwc-nolock) and `feof`/`ferror`, verified under a held stream lock on UCRT. Console behavior and other host CRTs remain unverified. - [ftrylockfile](../../../core/libs/prx/libc/src/RuntimeSupport.cpp) (libc) - tries the host CRT stream lock that `flockfile`, `funlockfile` and each locking host stdio call on the stream take, so it returns -1 while another thread holds the stream through `flockfile` or is inside such a call, and 0 when it acquires the lock or already owns it (recursively, released by as many `funlockfile` calls), as FreeBSD 11 [`_ftrylockfile`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/stdio/_flock_stub.c#L89-L104) does. A libc export holds the lock only for each host call it makes, not across several host calls or while it updates the guest `FILE` prefix. Linux uses glibc `ftrylockfile`. UCRT has no try variant of `_lock_file`, so Windows calls `TryEnterCriticalSection` on the `CRITICAL_SECTION` that `_lock_file` enters, found through the private UCRT stream layout (`__crt_stdio_stream_data` in `ucrt/inc/corecrt_internal_stdio.h`, unchanged from Windows SDK 10.0.19041 to 10.0.26100); another CRT or a change of that layout breaks it. A null or closed stream throws where FreeBSD dereferences it - [Wide-character conversions](../../../core/libs/prx/libc/src/Multibyte.cpp) (libc) - The wide `wcsto*` functions parse the ASCII prefix of the guest string, as the C locale accepts only ASCII white space and digits. [`wcsrtombs`](../../../core/libs/prx/libc/src/Multibyte.cpp) uses the guest's single-byte C locale through `wcrtomb`; other encodings and shift states are not supported From 6571007d635b4335e7dcd9eb9c2b1e8a62b29388 Mon Sep 17 00:00:00 2001 From: QRMOE Date: Tue, 13 Oct 2026 06:35:54 +0200 Subject: [PATCH 0005/1000] fix(libc): ignore getcwd size when allocating a buffer --- core/libs/prx/libc/src/General.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libc/src/General.cpp b/core/libs/prx/libc/src/General.cpp index 5eab2d9d3f..23efa2a0b4 100644 --- a/core/libs/prx/libc/src/General.cpp +++ b/core/libs/prx/libc/src/General.cpp @@ -426,8 +426,8 @@ extern "C" char* APS5_VABI getcwd_nid_postfix(char* buffer, std::size_t size) { const auto relative = state.current.lexically_relative(state.root); const auto path = relative == "." ? std::string("/") : "/" + relative.generic_string(); const auto required = path.size() + 1; - if ((buffer || size) && size < required) { errno = 34; return nullptr; } - if (!buffer) buffer = static_cast(ApplicationHeapAllocate_nid_no_patch(size ? size : required)); + if (buffer && size < required) { errno = 34; return nullptr; } + if (!buffer) buffer = static_cast(ApplicationHeapAllocate_nid_no_patch(required)); if (!buffer) { errno = 12; return nullptr; } std::memcpy(buffer, path.c_str(), required); return buffer; From 2db1b6041f4ade8c3d9ecffe6a1402458102acc3 Mon Sep 17 00:00:00 2001 From: QRMOE Date: Tue, 13 Oct 2026 06:36:26 +0200 Subject: [PATCH 0006/1000] test(libc): cover ignored getcwd allocation sizes --- core/libs/tests/GuestWorkingDirectory.cpp | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/core/libs/tests/GuestWorkingDirectory.cpp b/core/libs/tests/GuestWorkingDirectory.cpp index df874768e8..de3ea75fc1 100644 --- a/core/libs/tests/GuestWorkingDirectory.cpp +++ b/core/libs/tests/GuestWorkingDirectory.cpp @@ -3,9 +3,11 @@ #include "prx/libc/include/GuestHeap.hpp" #include #include +#include #include #include #include +#include extern "C" { int APS5_VABI chdir_nid_postfix(const char*); char* APS5_VABI getcwd_nid_postfix(char*, std::size_t); @@ -41,6 +43,14 @@ int main() { Require(allocated && std::strcmp(allocated, path) == 0 && allocations == 1); ApplicationHeapFree_nid_no_patch(allocated); Require(releases == 1); + for (const auto size : {std::size_t{1}, std::size_t{2}, std::size_t{1024}, std::numeric_limits::max()}) { + const int before = allocations; + allocated = getcwd_nid_postfix(nullptr, size); + if (!allocated) std::fputs("getcwd(NULL, size) must ignore size when allocating its result\n", stderr); + Require(allocated && std::strcmp(allocated, path) == 0 && allocations == before + 1); + ApplicationHeapFree_nid_no_patch(allocated); + Require(releases == allocations); + } Require(chdir_nid_postfix("sample.txt") == -1 && *__error_nid_postfix() == 20); Require(chdir_nid_postfix("missing") == -1 && *__error_nid_postfix() == 2); Require(chdir_nid_postfix("..") == 0); From bb95e44e9cd40ddf3c4957b054bf8505ccbbc494 Mon Sep 17 00:00:00 2001 From: Rick <99503318+0xRickx@users.noreply.github.com> Date: Tue, 13 Oct 2026 17:25:30 +0200 Subject: [PATCH 0007/1000] fix(libSceAudiodec): decode HE-AAC as the console does ui_enable_heaac was ignored and FFmpeg applied Parametric Stereo, so an HE-AAC v2 stream decoded to 48 kHz stereo whatever the flag: 2048 samples in 2 channels per frame, and ERROR_INVALID_PCM_SIZE for a PCM buffer sized for the console's output. The console returns the 24 kHz mono core with HE-AAC disabled, and SBR output at 48 kHz in one channel with it enabled. AAC access units now reach FFmpeg without their ADTS header, with an AudioSpecificConfig built from the header or from the raw parameters. With HE-AAC disabled it signals SBR as absent. With HE-AAC enabled SBR stays implicit, and a mono stream that FFmpeg decodes to two channels is reopened with SBR present and Parametric Stereo absent. The new test decodes fdk-aac HE-AAC v2 and HE-AAC stereo streams, ADTS and raw, with HE-AAC on and off; it aborts on main. Co-Authored-By: Claude Opus 5.5 (1M context) --- core/libs/prx/libSceAudiodec/Export.cpp | 2 +- core/libs/prx/libSceAudiodec/src/Codecs.cpp | 120 +++++++++++++++----- core/libs/prx/libSceAudiodec/src/Codecs.hpp | 2 +- core/libs/tests/GuestAudiodec.cpp | 101 ++++++++++++++++ docs/dev/debt/libSceAudiodec.md | 2 + 5 files changed, 197 insertions(+), 30 deletions(-) diff --git a/core/libs/prx/libSceAudiodec/Export.cpp b/core/libs/prx/libSceAudiodec/Export.cpp index 76722d3554..fc1a113e4f 100644 --- a/core/libs/prx/libSceAudiodec/Export.cpp +++ b/core/libs/prx/libSceAudiodec/Export.cpp @@ -139,7 +139,7 @@ std::int32_t createAac(const AudiodecParamM4aac* param, AudiodecM4aacInfo* info, if (param->ui_max_channels > 8) return ERROR_M4AAC_INVALID_MAX_CHANNELS; if (param->ui_enable_heaac > 1) return ERROR_M4AAC_INVALID_ENABLE_HEAAC; const std::uint32_t channels = param->ui_max_channels == 0 ? 2 : param->ui_max_channels; - instance.decoder = Audiodec::CreateAac(param->i_bw_pcm, param->ui_config_number == M4AAC_CONFIG_ADTS, param->ui_sampling_freq_index, channels); + instance.decoder = Audiodec::CreateAac(param->i_bw_pcm, param->ui_config_number == M4AAC_CONFIG_ADTS, param->ui_sampling_freq_index, channels, param->ui_enable_heaac == 1); instance.channels = channels; instance.sampleRate = sampleRate; info->ui_sampling_freq = sampleRate; diff --git a/core/libs/prx/libSceAudiodec/src/Codecs.cpp b/core/libs/prx/libSceAudiodec/src/Codecs.cpp index aca46d88d1..c3324c3b1a 100644 --- a/core/libs/prx/libSceAudiodec/src/Codecs.cpp +++ b/core/libs/prx/libSceAudiodec/src/Codecs.cpp @@ -122,35 +122,103 @@ public: } }; +struct AacLayout { + std::uint32_t objectType; + std::uint32_t samplingIndex; + std::uint32_t channelConfiguration; + bool operator==(const AacLayout&) const = default; +}; + +enum class Sbr { + Implicit, + Absent, + WithoutParametricStereo, +}; + +std::vector audioSpecificConfig(const AacLayout& layout, Sbr sbr, std::uint32_t sbrSampleRate) { + std::vector bytes; + std::size_t bits = 0; + const auto put = [&](std::uint32_t value, int width) { + for (int bit = width - 1; bit >= 0; --bit, ++bits) { + if (bits % 8 == 0) bytes.push_back(0); + bytes.back() |= static_cast((value >> bit & 1) << (7 - bits % 8)); + } + }; + put(layout.objectType, 5); + put(layout.samplingIndex, 4); + put(layout.channelConfiguration, 4); + put(0, 3); + if (sbr == Sbr::Implicit) return bytes; + put(0x2B7, 11); + put(5, 5); + put(sbr == Sbr::WithoutParametricStereo, 1); + if (sbr == Sbr::WithoutParametricStereo) { + const auto* rate = std::find(std::begin(AAC_SAMPLE_RATES), std::end(AAC_SAMPLE_RATES), sbrSampleRate); + if (rate == std::end(AAC_SAMPLE_RATES)) { + put(15, 4); + put(sbrSampleRate, 24); + } else { + put(static_cast(rate - std::begin(AAC_SAMPLE_RATES)), 4); + } + put(0x548, 11); + put(0, 1); + } + return bytes; +} + class AacDecoder final : public FfmpegDecoder { public: - AacDecoder(std::int32_t wordSize, bool adts, std::vector> configs) - : FfmpegDecoder(AV_CODEC_ID_AAC, wordSize, configs.empty() ? std::vector{} : configs.front()), adts(adts), configs(std::move(configs)) {} + AacDecoder(std::int32_t wordSize, bool adts, bool highEfficiency, std::vector layouts) + : FfmpegDecoder(AV_CODEC_ID_AAC, wordSize, layouts.empty() ? std::vector{} : audioSpecificConfig(layouts.front(), highEfficiency ? Sbr::Implicit : Sbr::Absent, 0)), + adts(adts), highEfficiency(highEfficiency), sbr(highEfficiency ? Sbr::Implicit : Sbr::Absent), layouts(std::move(layouts)) {} DecodeResult Decode(const std::uint8_t* data, std::size_t size, std::uint8_t* pcm, std::size_t pcmSize) override { - std::size_t frameBytes = size; - if (adts) { - if (size < 7 || data[0] != 0xFF || (data[1] & 0xF6) != 0xF0) return {DecodeStatus::InvalidData}; - frameBytes = static_cast((data[3] & 3) << 11 | data[4] << 3 | data[5] >> 5); - if (frameBytes < 7) return {DecodeStatus::InvalidData}; - if (frameBytes > size) return {DecodeStatus::PartialInput}; - } - if (configs.size() <= 1) return decodePacket(data, frameBytes, pcm, pcmSize); - for (std::size_t index = 0; index < configs.size(); ++index) { - if (index != 0) reopen(configs[index]); - const DecodeResult result = decodePacket(data, frameBytes, pcm, pcmSize); - if (result.status != DecodeStatus::InvalidData) { - configs = {configs[index]}; - return result; + if (!adts) return decodeAccessUnit(data, size, pcm, pcmSize); + if (size < 7 || data[0] != 0xFF || (data[1] & 0xF6) != 0xF0) return {DecodeStatus::InvalidData}; + const auto frameBytes = static_cast((data[3] & 3) << 11 | data[4] << 3 | data[5] >> 5); + const std::size_t headerBytes = (data[1] & 1) != 0 ? 7 : 9; + if (frameBytes <= headerBytes) return {DecodeStatus::InvalidData}; + if (frameBytes > size) return {DecodeStatus::PartialInput}; + const AacLayout layout{(data[2] >> 6) + 1u, data[2] >> 2 & 15u, (data[2] & 1u) << 2 | data[3] >> 6}; + if (layout.channelConfiguration == 0) { + if (!layouts.empty()) { + layouts.clear(); + reopen({}); } + return decodePacket(data, frameBytes, pcm, pcmSize); } - reopen(configs.front()); - return {DecodeStatus::InvalidData}; + if (layouts.empty() || layouts.front() != layout) { + layouts = {layout}; + sbr = highEfficiency ? Sbr::Implicit : Sbr::Absent; + reopen(audioSpecificConfig(layout, sbr, 0)); + } + DecodeResult result = decodeAccessUnit(data + headerBytes, frameBytes - headerBytes, pcm, pcmSize); + result.consumed = frameBytes; + return result; } private: + DecodeResult decodeAccessUnit(const std::uint8_t* data, std::size_t size, std::uint8_t* pcm, std::size_t pcmSize) { + for (std::size_t index = 0; index < layouts.size(); ++index) { + if (index != 0) reopen(audioSpecificConfig(layouts[index], sbr, 0)); + DecodeResult result = decodePacket(data, size, pcm, pcmSize); + if (result.status == DecodeStatus::InvalidData) continue; + layouts = {layouts[index]}; + if (sbr == Sbr::Implicit && layouts.front().channelConfiguration == 1 && result.channels > 1) { + sbr = Sbr::WithoutParametricStereo; + reopen(audioSpecificConfig(layouts.front(), sbr, result.sampleRate)); + result = decodePacket(data, size, pcm, pcmSize); + } + return result; + } + if (layouts.size() > 1) reopen(audioSpecificConfig(layouts.front(), sbr, 0)); + return {DecodeStatus::InvalidData}; + } + bool adts; - std::vector> configs; + bool highEfficiency; + Sbr sbr; + std::vector layouts; }; class At9Decoder final : public Decoder { @@ -239,20 +307,16 @@ std::unique_ptr CreateMp3(std::int32_t wordSize) { return std::make_unique(wordSize); } -std::unique_ptr CreateAac(std::int32_t wordSize, bool adts, std::uint32_t samplingFreqIndex, std::uint32_t channels) { - std::vector> configs; +std::unique_ptr CreateAac(std::int32_t wordSize, bool adts, std::uint32_t samplingFreqIndex, std::uint32_t channels, bool highEfficiency) { + std::vector layouts; if (!adts) { const std::uint32_t lowComplexity = 2; - const auto config = [&](std::uint32_t channelConfiguration) { - return std::vector{static_cast(lowComplexity << 3 | samplingFreqIndex >> 1), - static_cast((samplingFreqIndex & 1) << 7 | channelConfiguration << 3)}; - }; - configs.push_back(config(channels < 2 ? 1 : 2)); + layouts.push_back({lowComplexity, samplingFreqIndex, channels < 2 ? 1u : 2u}); for (std::uint32_t channelConfiguration = 3; channelConfiguration <= 7; ++channelConfiguration) { - if ((channelConfiguration == 7 ? 8 : channelConfiguration) <= channels) configs.push_back(config(channelConfiguration)); + if ((channelConfiguration == 7 ? 8 : channelConfiguration) <= channels) layouts.push_back({lowComplexity, samplingFreqIndex, channelConfiguration}); } } - return std::make_unique(wordSize, adts, std::move(configs)); + return std::make_unique(wordSize, adts, highEfficiency, std::move(layouts)); } std::unique_ptr CreateAt9(std::int32_t wordSize, const std::uint8_t (&config)[4], At9Format& format) { diff --git a/core/libs/prx/libSceAudiodec/src/Codecs.hpp b/core/libs/prx/libSceAudiodec/src/Codecs.hpp index ebf8434eb2..6d43c67e6b 100644 --- a/core/libs/prx/libSceAudiodec/src/Codecs.hpp +++ b/core/libs/prx/libSceAudiodec/src/Codecs.hpp @@ -57,7 +57,7 @@ bool ParseMp3Header(const std::uint8_t* data, std::size_t size, Mp3Header& heade std::uint32_t AacSampleRate(std::uint32_t index); bool ValidAt9Config(const std::uint8_t (&config)[4]); std::unique_ptr CreateMp3(std::int32_t wordSize); -std::unique_ptr CreateAac(std::int32_t wordSize, bool adts, std::uint32_t samplingFreqIndex, std::uint32_t channels); +std::unique_ptr CreateAac(std::int32_t wordSize, bool adts, std::uint32_t samplingFreqIndex, std::uint32_t channels, bool highEfficiency); std::unique_ptr CreateAt9(std::int32_t wordSize, const std::uint8_t (&config)[4], At9Format& format); } diff --git a/core/libs/tests/GuestAudiodec.cpp b/core/libs/tests/GuestAudiodec.cpp index 2e6af3008a..5e9ee471f1 100644 --- a/core/libs/tests/GuestAudiodec.cpp +++ b/core/libs/tests/GuestAudiodec.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include extern "C" { @@ -143,6 +144,60 @@ const std::uint8_t MP3_MONO[] = { 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, }; +const std::uint8_t HEAAC_V2_MONO[] = { + 0xFF, 0xF1, 0x58, 0x40, 0x10, 0x02, 0x80, 0x01, 0x40, 0x22, 0x80, 0xA3, 0x6E, 0xEF, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4A, 0x80, + 0x00, 0x06, 0xF5, 0x61, 0x0A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, + 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, + 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, + 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, + 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5E, 0xFF, 0xF1, 0x58, 0x40, 0x10, 0x02, 0x80, 0x01, 0x40, 0x42, 0x80, 0xA3, 0x78, 0x46, 0x81, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x38, 0x76, 0xE4, 0x89, 0xB5, 0x65, 0xE5, 0xDF, 0xFE, 0xB7, 0x32, 0x7C, 0x7F, 0xF0, 0x00, 0x06, 0xF4, 0xC1, 0x0A, 0x5A, + 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, + 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, + 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, + 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5E, 0xFF, 0xF1, 0x58, 0x40, 0x18, 0xA2, 0x38, 0x01, + 0x0A, 0x97, 0x98, 0xD8, 0x73, 0x75, 0x18, 0xBB, 0x6D, 0xCD, 0xA6, 0xE8, 0x7F, 0x09, 0xD5, 0xA0, 0x7E, 0x99, 0xC7, 0x42, 0xF2, 0x57, 0xBB, 0xC8, + 0xAB, 0x51, 0x57, 0x0A, 0xBF, 0x56, 0x8A, 0x40, 0x48, 0xFB, 0xCC, 0xF1, 0xFF, 0x4E, 0x4C, 0x21, 0xEE, 0x7F, 0x10, 0xFD, 0xA6, 0xB4, 0xE2, 0xCC, + 0x52, 0xA9, 0x38, 0x00, 0x00, 0xC4, 0x1A, 0x9A, 0xD2, 0x05, 0xE8, 0x94, 0xDB, 0xA5, 0x47, 0xFE, 0x92, 0xFC, 0x12, 0x41, 0x11, 0x08, 0x01, 0x22, + 0x65, 0x54, 0x28, 0x24, 0x16, 0x72, 0xE9, 0xAA, 0xC7, 0x45, 0x33, 0xEC, 0x74, 0x13, 0xAC, 0x84, 0xD5, 0xD4, 0x95, 0xCB, 0x41, 0x9B, 0x55, 0x2D, + 0xD5, 0xAC, 0x5C, 0xEF, 0xD4, 0x75, 0xF8, 0xD5, 0xCE, 0xC9, 0x64, 0xD9, 0xF5, 0x80, 0xD3, 0xD9, 0xAA, 0x5F, 0x1E, 0x45, 0x4F, 0xEA, 0x5F, 0xF8, + 0xBF, 0x48, 0x1F, 0xAD, 0xDF, 0x0C, 0xE5, 0xF1, 0x7F, 0x65, 0xC0, 0x12, 0xFA, 0xBF, 0xE6, 0x40, 0x0D, 0xAA, 0xFD, 0x40, 0x02, 0x58, 0x64, 0xA4, + 0x03, 0x2A, 0xC1, 0xAA, 0x79, 0x01, 0x6D, 0x37, 0x60, 0x78, 0x7B, 0x2B, 0x1B, 0x62, 0x6C, 0x87, 0xA7, 0xA2, 0xDB, 0x65, 0x00, 0xDE, 0x0F, 0xA0, + 0x50, 0x00, 0x00, 0x00, 0x00, 0x01, 0xDB, 0x92, 0x26, 0xD5, 0x97, 0x97, 0x7F, 0xFA, 0xDC, 0xC9, 0xF1, 0xFF, 0xC0, 0x01, 0xC0, 0xFF, 0xF1, 0x58, + 0x40, 0x15, 0x02, 0x10, 0x01, 0x3C, 0xF3, 0x08, 0xB6, 0xA1, 0x99, 0x0C, 0x42, 0x0A, 0xA1, 0x40, 0xF6, 0x05, 0x0A, 0x6C, 0x0B, 0xC0, 0x1F, 0x1F, + 0x19, 0x9D, 0x15, 0x2A, 0x56, 0x97, 0x6C, 0x63, 0x88, 0xF4, 0x89, 0x13, 0x53, 0xA1, 0x9D, 0x8E, 0x9A, 0x93, 0x0C, 0x6A, 0x18, 0x0F, 0x2C, 0xCB, + 0x53, 0xB3, 0x93, 0xA9, 0xA9, 0x32, 0x63, 0xC3, 0x0C, 0x07, 0xF0, 0xF1, 0x3C, 0x98, 0xB9, 0x6B, 0x75, 0xB0, 0x95, 0xAD, 0x62, 0xDC, 0xA4, 0xB4, + 0x75, 0xE1, 0xAD, 0xE9, 0x64, 0x31, 0x51, 0x24, 0xAE, 0x6E, 0x20, 0xC0, 0x9D, 0x70, 0x25, 0x47, 0x1D, 0x67, 0x4D, 0x5C, 0xE1, 0xDA, 0x4E, 0x8A, + 0x9E, 0xF9, 0xFA, 0x62, 0x16, 0xDA, 0x99, 0x9F, 0xD0, 0xDC, 0x7D, 0xBD, 0x5F, 0x31, 0x8B, 0xC2, 0xE8, 0xD0, 0xB8, 0xE5, 0xC6, 0xC0, 0x37, 0x8F, + 0x68, 0xBC, 0x92, 0x4C, 0xD0, 0x00, 0x01, 0xFC, 0xFD, 0x43, 0xF2, 0xF5, 0xFF, 0x9F, 0xBF, 0xAD, 0x6D, 0x56, 0x0D, 0x3F, 0xF4, 0xA5, 0xD9, 0x17, + 0xFF, 0x20, 0x02, 0x7C, 0x02, 0xCC, 0x8B, 0x26, 0x49, 0xFF, 0xFB, 0x9E, 0xA2, 0x00, 0x0F, 0xE4, 0xDF, 0xF0, 0x00, 0x00, 0x70, +}; + +const std::uint8_t HEAAC_STEREO[] = { + 0xFF, 0xF1, 0x58, 0x80, 0x10, 0x02, 0xC0, 0x21, 0x21, 0x45, 0x00, 0x14, 0x50, 0x01, 0x46, 0xAD, 0xD5, 0x81, 0x75, 0x00, 0x00, 0x00, 0x1C, 0x60, + 0x18, 0x0D, 0xEA, 0xC2, 0x14, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, + 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, + 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, + 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, + 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xB4, 0xBC, 0xFF, 0xF1, 0x58, 0x80, 0x1B, 0x62, 0x90, 0x21, 0x4A, 0xCC, 0x80, 0x1F, 0xEA, 0xFE, 0xBF, 0xC8, + 0x87, 0x77, 0x07, 0x5B, 0x6B, 0x0A, 0xDA, 0x70, 0x6A, 0xC6, 0x8F, 0xE1, 0xE6, 0xC4, 0x36, 0x01, 0x48, 0xB1, 0x94, 0xF0, 0x67, 0x71, 0x13, 0x21, + 0x28, 0xB0, 0x74, 0xC1, 0x02, 0x0B, 0xED, 0x1B, 0x75, 0xF2, 0x1D, 0x89, 0x8A, 0xFA, 0xCB, 0x17, 0x0D, 0x51, 0xAE, 0x81, 0x2D, 0xB3, 0x27, 0x07, + 0x00, 0x69, 0x11, 0x1E, 0x51, 0x2E, 0x56, 0x4C, 0xD1, 0x9C, 0x39, 0x65, 0xED, 0x43, 0x53, 0x4E, 0x4E, 0x22, 0x8D, 0x5D, 0x48, 0x56, 0x50, 0x66, + 0x75, 0x4B, 0x3D, 0x6B, 0x10, 0xE5, 0x38, 0x97, 0x8D, 0x5C, 0xEC, 0xCC, 0x6F, 0xDA, 0x1D, 0xD5, 0x99, 0x0C, 0xBD, 0x50, 0xDB, 0x92, 0xDE, 0x06, + 0xD1, 0x0A, 0xE2, 0xD5, 0x32, 0x9C, 0xEA, 0x82, 0x1A, 0x30, 0x72, 0x92, 0xC1, 0x30, 0x88, 0xAC, 0x38, 0x15, 0xD9, 0xE7, 0x22, 0x5A, 0x97, 0x94, + 0x0D, 0xE4, 0xFF, 0xFF, 0xFC, 0xC8, 0xD0, 0xEE, 0xDB, 0x60, 0x26, 0x6D, 0x11, 0x0D, 0x69, 0x10, 0x61, 0x63, 0xA7, 0xE7, 0xB7, 0x58, 0x11, 0x63, + 0x29, 0xE0, 0xFB, 0xBD, 0x8E, 0x3C, 0x98, 0xBC, 0x01, 0x7E, 0x8F, 0x7B, 0xF7, 0xCB, 0x3A, 0xC2, 0xC0, 0xC2, 0xC2, 0xCB, 0x01, 0x1F, 0xA3, 0x94, + 0xE8, 0x28, 0x44, 0x40, 0x52, 0x10, 0x42, 0x48, 0x0A, 0x94, 0x53, 0x9D, 0x57, 0xB0, 0x4B, 0x3E, 0x50, 0x80, 0xDF, 0xD2, 0x70, 0x30, 0x95, 0x33, + 0x92, 0x2C, 0xAD, 0x9D, 0x59, 0x74, 0x83, 0xC0, 0x00, 0x00, 0x38, 0xFF, 0xF1, 0x58, 0x80, 0x16, 0xC2, 0x74, 0x21, 0x79, 0x08, 0x00, 0x87, 0xFF, + 0xFF, 0xCF, 0x11, 0x6C, 0xCC, 0x21, 0x92, 0x14, 0x02, 0x80, 0xE8, 0x05, 0x50, 0x10, 0x50, 0x3F, 0xC7, 0xC6, 0x67, 0x50, 0xB0, 0xBB, 0x63, 0x1A, + 0x77, 0x48, 0xF3, 0x0E, 0x0A, 0x02, 0x2F, 0xC6, 0xDA, 0xD2, 0xFA, 0xDA, 0x2D, 0x32, 0x15, 0x6E, 0xEE, 0xF0, 0x11, 0x7F, 0x0F, 0x13, 0xC9, 0x48, + 0xEB, 0x84, 0x2D, 0x63, 0xAD, 0xAD, 0x61, 0xCA, 0x43, 0x5E, 0x1A, 0xDC, 0x31, 0x76, 0x4B, 0x9C, 0xBB, 0xD1, 0xF7, 0xD4, 0x84, 0xF0, 0x35, 0x83, + 0x78, 0xEE, 0x95, 0x05, 0x2E, 0x61, 0x76, 0xDB, 0xE1, 0x68, 0xC9, 0x23, 0xA4, 0x1E, 0xAA, 0x71, 0xCA, 0x9A, 0xA5, 0xEE, 0x88, 0x0D, 0x0F, 0xE7, + 0xFE, 0x55, 0x6D, 0x0F, 0x61, 0xA3, 0xAE, 0xEE, 0xF3, 0x08, 0xCB, 0x13, 0x08, 0x28, 0x0A, 0x1E, 0x28, 0x40, 0x70, 0xA0, 0xA7, 0x84, 0x14, 0x15, + 0xF5, 0xD0, 0x50, 0x53, 0x51, 0x5F, 0x32, 0x89, 0xA8, 0xAC, 0x1F, 0x73, 0x62, 0xD1, 0x67, 0x53, 0x57, 0x86, 0x2C, 0x80, 0x44, 0xFB, 0xC2, 0xF4, + 0xAF, 0x24, 0x93, 0x1D, 0x40, 0x00, 0xFF, 0x89, 0x36, 0xFF, 0xC6, 0x45, 0x23, 0x7F, 0xF0, 0x5B, 0xCC, 0x00, 0x00, 0x00, 0x03, 0x20, 0x0C, 0x01, + 0x38, +}; constexpr std::int32_t errorApiFail = static_cast(0x807F0000); constexpr std::int32_t errorInvalidType = static_cast(0x807F0001); constexpr std::int32_t errorArg = static_cast(0x807F0002); @@ -225,6 +280,46 @@ void TestAacRaw51(std::uint32_t maxChannels) { Require(sceAudiodecDeleteDecoder(handle) == 0); } +double UpperHalfEnergyShare(const std::int16_t* samples, std::size_t count, std::size_t stride) { + double upper = 0; + double total = 0; + for (std::size_t bin = 1; bin < count / 2; ++bin) { + double real = 0; + double imaginary = 0; + for (std::size_t i = 0; i < count; ++i) { + const double phase = 2 * std::numbers::pi * static_cast(bin * i % count) / static_cast(count); + real += samples[i * stride] * std::cos(phase); + imaginary += samples[i * stride] * std::sin(phase); + } + total += real * real + imaginary * imaginary; + if (bin >= count / 4) upper += real * real + imaginary * imaginary; + } + return upper / total; +} + +void TestHeAac(const std::uint8_t* stream, std::size_t streamSize, std::uint32_t channels, bool adts, bool highEfficiency) { + AudiodecParamM4aac param{sizeof(param), 1, adts ? 1u : 2u, 6, channels, highEfficiency ? 1u : 0u}; + AudiodecM4aacInfo info{sizeof(info), 0, 0, 0, 0}; + AudiodecCtrl ctrl{¶m, &info, nullptr, nullptr}; + const std::int32_t handle = sceAudiodecCreateDecoder(&ctrl, 3); + Require(handle > 0); + Streams streams; + const std::uint32_t samples = highEfficiency ? 2048 : 1024; + const std::uint32_t room = samples * channels * 2; + for (std::size_t offset = 0; offset < streamSize; offset += AdtsLength(stream + offset)) { + const std::size_t length = AdtsLength(stream + offset); + Require(Decode(handle, ctrl, streams, adts ? stream + offset : stream + offset + 7, adts ? length : length - 7, room) == 0); + Require(streams.au.ui_au_size == (adts ? length : length - 7)); + Require(streams.pcm.ui_pcm_size == room && info.i_result == 0); + Require(info.ui_sampling_freq == (highEfficiency ? 48000u : 24000u) && info.ui_number_of_channels == channels); + } + if (highEfficiency) { + Require(info.ui_heaac == 1); + Require(UpperHalfEnergyShare(reinterpret_cast(streams.output.data()), samples, channels) > 0.02); + } + Require(sceAudiodecDeleteDecoder(handle) == 0); +} + void TestMp3() { AudiodecParamMp3 param{sizeof(param), 2}; AudiodecMp3Info info{}; @@ -315,6 +410,12 @@ int main() { TestAac(false, 8); TestAacRaw51(6); TestAacRaw51(8); + for (const bool adts : {true, false}) { + for (const bool highEfficiency : {true, false}) { + TestHeAac(HEAAC_V2_MONO, sizeof(HEAAC_V2_MONO), 1, adts, highEfficiency); + TestHeAac(HEAAC_STEREO, sizeof(HEAAC_STEREO), 2, adts, highEfficiency); + } + } TestMp3(); TestAt9(); for (std::uint32_t type = 1; type <= 3; ++type) Require(sceAudiodecTermLibrary(type) == 0); diff --git a/docs/dev/debt/libSceAudiodec.md b/docs/dev/debt/libSceAudiodec.md index 6e1bcb974e..0e37f5e79f 100644 --- a/docs/dev/debt/libSceAudiodec.md +++ b/docs/dev/debt/libSceAudiodec.md @@ -3,7 +3,9 @@ ### Unknown function info - [sceAudiodecDecode](../../../core/libs/prx/libSceAudiodec/src/Codecs.cpp) (libSceAudiodec) - a raw M4AAC stream carries no channel configuration, so the decoder tries the mono/stereo configuration and then configurations 3 to 7 within `ui_max_channels` until one decodes the first access unit, and keeps that one. No reference documents how the library picks the layout (shadPS4 maps the channel count straight to the configuration; SharpProspero only describes the maximum). With the stereo configuration a mono raw stream decodes as 2 channels, so 2 is reported, not 1 +- [sceAudiodecDecode](../../../core/libs/prx/libSceAudiodec/src/Codecs.cpp) (libSceAudiodec) - the HE-AAC handling follows one console measurement of an HE-AAC v2 ADTS stream: SBR output at 48 kHz in one channel with `ui_enable_heaac` 1, the 24 kHz mono core with 0. Not measured: whether the console, like FFmpeg, decodes a stream without SBR data in its first access unit as AAC-LC with `ui_enable_heaac` 1, and what it reports in `ui_sampling_freq` and `ui_heaac` with `ui_enable_heaac` 0 (here the rate of the returned PCM and 0) ### Functional +- [sceAudiodecDecode](../../../core/libs/prx/libSceAudiodec/src/Codecs.cpp) (libSceAudiodec) - an ADTS stream with channel configuration 0 (layout in a program config element) is still parsed by FFmpeg, so `ui_enable_heaac` 0 does not remove its SBR and a mono one with Parametric Stereo comes out as stereo. - [libSceAudiodec](../../../core/libs/prx/libSceAudiodec/Export.cpp) throws for the 24-bit PCM word size (`iBwPcm` 0): its sample layout is unknown. ATRAC9 decoding is covered only by configuration and error tests, as no ATRAC9 encoder is available for a fixture. From 89e70ab80ddc4e35aed3300bc4a33327970f9c45 Mon Sep 17 00:00:00 2001 From: Karan Vora <52519664+karanvora2599@users.noreply.github.com> Date: Sat, 10 Oct 2026 18:54:42 -0400 Subject: [PATCH 0008/1000] perf(shader): compute scalar f16 add, sub and mul with native f16 math v_add_f16, v_sub_f16, v_subrev_f16 and v_mul_f16 converted their sources to f32, computed in f32 and converted the result back. When the device has native f16 that rounds to nearest even and keeps f16 denormals, and FLOAT_MODE does the same for f16 (or the shader has no FLOAT_MODE), they are now one f16 add or mul on the source bits (sub adds the negated subtrahend), followed by the FP16_OVFL and clamp rules on the result bits. Other f16 modes, including the f16 denormal flushes, keep the f32 path. Results other than NaN payloads don't change, for the same reason as for the packed instructions: rounding an f16 sum or product to f32 and then to f16 gives the correctly rounded f16 result, which Vulkan requires of f16 OpFAdd and OpFMul. A NaN result keeps the payload the driver gives, as before (TechnicalDebt lists the NaN payloads of scalar f16 arithmetic as the driver's); it is now the payload of the driver's f16 arithmetic rather than of its f32 arithmetic. v_fma_f16, v_fmac_f16, v_fmamk_f16 and v_fmaak_f16 keep their exact f32 path, since a native f16 Fma is not required to be fused. New test agc_driver_float16_native compiles one shader with these instructions and the fma forms (abs, neg, op_sel, clamp, inline constant and literal variants) with and without Float16, checks that only add and mul use f16 math, and compares the results bit for bit in ten float modes, accepting another NaN payload only where both results are NaN, and checks that a mode that flushes f16 denormals keeps the f32 path. --- .../tests/execution/Float16Native.cpp | 229 ++++++++++++++++++ .../Translation/TranslationContext.hpp | 2 + .../Translation/src/FloatInstructions.cpp | 20 ++ 3 files changed, 251 insertions(+) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/Float16Native.cpp diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/Float16Native.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/Float16Native.cpp new file mode 100644 index 0000000000..bc1ffc04fb --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/Float16Native.cpp @@ -0,0 +1,229 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 64; +constexpr std::uint32_t Inputs = 4; +constexpr std::uint32_t Results = 16; +constexpr std::uint32_t Stride = 16; +constexpr std::uint32_t Batches = 32; +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; + +alignas(256) constexpr std::array Code{ + 0x34020084, 0x34060086, 0xe0381000, 0x80000401, 0xbf8c3f70, + 0x64140b04, + 0x66160b04, + 0x68180b04, + 0x6a1a0b04, + 0xd74b000e, 0x041a0b04, + 0xd74b980f, 0x041a0b04, + 0xd74b3110, 0xc4160d04, + 0x7e220306, 0x6c220b04, + 0x6e240b04, 0x00003d55, + 0x70260b04, 0x00000001, + 0xd74ba214, 0x241a0b04, + 0x662a08f2, + 0xd5328116, 0x20020b04, + 0xd5350217, 0x40020d04, + 0xd5338018, 0x00020b04, + 0xd5340019, 0x20020b04, + 0xe0781000, 0x80010a03, 0xe0781010, 0x80010e03, 0xe0781020, 0x80011203, 0xe0781030, 0x80011603, 0xbf810000, +}; + +constexpr const char* Names[Results] = { + "v_add_f16 v10, v4, v5", + "v_sub_f16 v11, v4, v5", + "v_subrev_f16 v12, v4, v5", + "v_mul_f16 v13, v4, v5", + "v_fma_f16 v14, v4, v5, v6", + "v_fma_f16 v15, v4, v5, v6 op_sel:[1,1,0,0] clamp", + "v_fma_f16 v16, |v4|, -v6, -v5 op_sel:[0,1,1,0]", + "v_fmac_f16 v17, v4, v5", + "v_fmamk_f16 v18, v4, 0x3d55, v5", + "v_fmaak_f16 v19, v4, v5, 0x0001", + "v_fma_f16 v20, -v4, |v5|, v6 op_sel:[0,0,1,0] clamp", + "v_sub_f16 v21, 1.0, v4", + "v_add_f16 v22, -|v4|, v5 clamp", + "v_mul_f16 v23, v4, -|v6|", + "v_sub_f16 v24, v4, v5 clamp", + "v_subrev_f16 v25, -v4, v5", +}; + +struct Mode { + const char* name; + std::optional floatMode; + bool native = true; +}; + +const std::array Modes{{ + {"no FLOAT_MODE", std::nullopt}, + {"dx10 ieee ovfl", ShaderRecompiler::ShaderFloatMode{0xc0u, true, true, true}}, + {"dx10 ieee", ShaderRecompiler::ShaderFloatMode{0xc0u, true, true, false}}, + {"dx10 ovfl", ShaderRecompiler::ShaderFloatMode{0xc0u, true, false, true}}, + {"dx10", ShaderRecompiler::ShaderFloatMode{0xc0u, true, false, false}}, + {"ieee ovfl", ShaderRecompiler::ShaderFloatMode{0xc0u, false, true, true}}, + {"ieee", ShaderRecompiler::ShaderFloatMode{0xc0u, false, true, false}}, + {"ovfl", ShaderRecompiler::ShaderFloatMode{0xc0u, false, false, true}}, + {"none", ShaderRecompiler::ShaderFloatMode{0xc0u, false, false, false}}, + {"f16 denormals flushed", ShaderRecompiler::ShaderFloatMode{0x30u, true, false, false}, false}, +}}; + +std::uint64_t Seed = 0x9e3779b97f4a7c15ull; + +std::uint32_t Next() { + Seed += 0x9e3779b97f4a7c15ull; + std::uint64_t z = Seed; + z = (z ^ (z >> 30u)) * 0xbf58476d1ce4e5b9ull; + z = (z ^ (z >> 27u)) * 0x94d049bb133111ebull; + return static_cast(z ^ (z >> 31u)); +} + +std::uint32_t Half() { + const std::uint32_t bits = Next(); + const std::uint32_t sign = (bits >> 31u) << 15u; + const std::uint32_t mantissa = bits & 0x3ffu; + switch ((bits >> 16u) % 10u) { + case 0u: return sign; + case 1u: return sign | std::max(mantissa, 1u); + case 2u: return sign | 0x7c00u; + case 3u: return sign | 0x7c00u | std::max(mantissa, 1u); + case 4u: return sign | ((0x1du + (bits >> 20u) % 2u) << 10u) | mantissa; + case 5u: return sign | ((1u + (bits >> 20u) % 4u) << 10u) | mantissa; + case 6u: return sign | ((0x0du + (bits >> 20u) % 5u) << 10u) | mantissa; + default: return bits & 0xffffu; + } +} + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + +struct F16x2Math { + bool add = false; + bool mul = false; + bool fma = false; +}; + +F16x2Math FindF16x2Math(std::span spirv) { + std::optional half; + std::optional pair; + F16x2Math math; + for (std::size_t offset = 5u; offset < spirv.size();) { + const std::uint32_t words = spirv[offset] >> 16u; + const std::uint32_t opcode = spirv[offset] & 0xffffu; + Require(words != 0u && offset + words <= spirv.size(), "f16 native: malformed SPIR-V"); + if (opcode == spv::OpTypeFloat && spirv[offset + 2u] == 16u) half = spirv[offset + 1u]; + if (opcode == spv::OpTypeVector && half && spirv[offset + 2u] == *half && spirv[offset + 3u] == 2u) pair = spirv[offset + 1u]; + if (pair && spirv[offset + 1u] == *pair) { + math.add = math.add || opcode == spv::OpFAdd; + math.mul = math.mul || opcode == spv::OpFMul; + math.fma = math.fma || opcode == spv::OpExtInst; + } + offset += words; + } + return math; +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + const auto target = device->Target(); + const auto supports = [&](spv::Capability capability) { + return std::ranges::find(target.supportedCapabilities, static_cast(capability)) != target.supportedCapabilities.end(); + }; + if (!supports(spv::CapabilityFloat16) || !supports(spv::CapabilityRoundingModeRTE) || !supports(spv::CapabilityDenormPreserve)) { + std::puts("f16 native: the device has no native f16 with RTE rounding and kept denormals, skipped"); + return VulkanTestSkipped; + } + std::vector withoutFloat16; + std::ranges::copy_if(target.supportedCapabilities, std::back_inserter(withoutFloat16), [](std::uint32_t capability) { return capability != spv::CapabilityFloat16; }); + auto emulatedTarget = target; + emulatedTarget.supportedCapabilities = withoutFloat16; + + const std::span code(Code); + std::vector userData(8, 0u); + const auto input = BufferDescriptor(Input.data(), static_cast(Input.size() * 4u)); + const auto output = BufferDescriptor(Output.data(), static_cast(Output.size() * 4u)); + std::copy(input.begin(), input.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + const auto compile = [&](const ShaderRecompiler::SpirvTarget& shaderTarget, const Mode& mode) { + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + shaderTarget, + {0, 0, 0, 128} + }; + request.useCache = false; + request.context.floatMode = mode.floatMode; + return ShaderRecompiler::Recompile(request); + }; + const auto run = [&](const auto& result) { + Output.fill(0xdeadbeefu); + device->Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device->WaitIdle(); + return Output; + }; + + std::uint64_t compared = 0u; + std::uint64_t nanPayloads = 0u; + const auto isNan16 = [](std::uint32_t value) { return (value & 0x7fffu) > 0x7c00u; }; + for (const auto& mode : Modes) { + const auto native = compile(target, mode); + const auto emulated = compile(emulatedTarget, mode); + const F16x2Math nativeMath = FindF16x2Math(native.spirv.Words()); + const F16x2Math emulatedMath = FindF16x2Math(emulated.spirv.Words()); + Require(nativeMath.add == mode.native && nativeMath.mul == mode.native, std::string("f16 native: ") + mode.name + (mode.native ? " does not use" : " uses") + " f16vec2 add and mul"); + Require(!nativeMath.fma, std::string("f16 native: ") + mode.name + " uses an f16vec2 Fma, which Vulkan does not require to be fused"); + Require(!emulatedMath.add && !emulatedMath.mul && !emulatedMath.fma, std::string("f16 native: ") + mode.name + " uses f16vec2 math without Float16"); + for (std::uint32_t batch = 0; batch < Batches; ++batch) { + for (auto& word : Input) word = Half() | (Half() << 16u); + const auto nativeOutput = run(native); + const auto emulatedOutput = run(emulated); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + for (std::uint32_t index = 0; index < Results; ++index) { + const std::uint32_t actual = nativeOutput[tid * Stride + index]; + const std::uint32_t expected = emulatedOutput[tid * Stride + index]; + const std::uint32_t* row = &Input[tid * Inputs]; + const bool bothNan = (actual >> 16u) == (expected >> 16u) && isNan16(actual) && isNan16(expected); + if (actual != expected && bothNan) ++nanPayloads; + Require(actual == expected || bothNan, std::string("f16 native: ") + mode.name + ", " + Names[index] + " with v4=" + Hex(row[0]) + " v5=" + Hex(row[1]) + " v6=" + Hex(row[2]) + " v7=" + Hex(row[3]) + " is " + Hex(actual) + ", f32 path gives " + Hex(expected)); + } + } + compared += Threads * Results; + } + } + std::printf("f16 native tests passed (%llu results, %llu NaN results with another payload)\n", static_cast(compared), static_cast(nanPayloads)); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp b/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp index bc79d3fd18..51ce060d8c 100644 --- a/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp +++ b/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp @@ -53,6 +53,7 @@ private: bool outputModifierApplies(std::uint32_t denormalShift) const; bool dx10Clamp() const { return !floatMode.has_value() || floatMode->dx10Clamp; } bool fp16Overflow() const { return floatMode.has_value() && floatMode->fp16Overflow; } + bool nativeFloat16Mode() const { return nativeFloat16 && (!floatMode.has_value() || (floatMode->floatMode & 0xccu) == 0xc0u); } IrF32 clampF16Overflow(IrF32 value, std::initializer_list sources); void rejectHalfOrDoubleOutputModifier(const RdnaOperand& operand) const; IrU32 clampF16Bits(const RdnaOperand& operand, IrU32 bits); @@ -197,6 +198,7 @@ private: bool vDivFixupF16(const RdnaInstruction& inst); bool float16Binary(const RdnaInstruction& inst, IrOpcode opcode, bool reverse); bool float16Ternary(const RdnaInstruction& inst, IrOpcode opcode, bool accumulator, bool mix); + bool float16Native(const RdnaInstruction& inst, IrOpcode opcode, const RdnaOperand& lhsOperand, const RdnaOperand& rhsOperand); IrF32 fmaF16RoundedToOdd(IrF32 lhs, IrF32 rhs, IrF32 addend); bool floatUnary(const RdnaInstruction& inst, IrOpcode opcode); bool floatBinary(const RdnaInstruction& inst, IrOpcode opcode, bool reverse); diff --git a/core/shader/recompiler/Translation/src/FloatInstructions.cpp b/core/shader/recompiler/Translation/src/FloatInstructions.cpp index e09885cb3f..3707b478df 100644 --- a/core/shader/recompiler/Translation/src/FloatInstructions.cpp +++ b/core/shader/recompiler/Translation/src/FloatInstructions.cpp @@ -228,6 +228,7 @@ bool TranslationContext::vDivFixupF16(const RdnaInstruction& inst) { } bool TranslationContext::float16Binary(const RdnaInstruction& inst, IrOpcode opcode, bool reverse) { + if (nativeFloat16Mode()) return float16Native(inst, opcode, sourceAt(inst, reverse ? 1u : 0u), sourceAt(inst, reverse ? 0u : 1u)); const bool measured = (opcode == IrOpcode::FPAdd32 || opcode == IrOpcode::FPSub32 || opcode == IrOpcode::FPMul32) && floatMode.has_value() && (floatMode->floatMode & 0x0cu) == 0u; const auto flush = [&](IrF32 value, std::uint32_t threshold) { const IrU32 bits(ir.BitCastU32(value.Value())); @@ -268,6 +269,25 @@ bool TranslationContext::float16Ternary(const RdnaInstruction& inst, IrOpcode op return true; } +bool TranslationContext::float16Native(const RdnaInstruction& inst, IrOpcode opcode, const RdnaOperand& lhsOperand, const RdnaOperand& rhsOperand) { + const IrU32 lhs = readF16Bits(lhsOperand); + const IrU32 rhs = readF16Bits(rhsOperand); + IrU32 result; + if (opcode == IrOpcode::FPMul32) { + result = IrU32(ir.Emit(IrOpcode::FPMul16x2, IrType::U32, {&lhs.Value(), &rhs.Value()})); + } else { + const IrU32 addend = opcode == IrOpcode::FPSub32 ? IrU32(ir.BitwiseXor(rhs.Value(), ir.Constant(0x8000u))) : rhs; + result = IrU32(ir.Emit(IrOpcode::FPAdd16x2, IrType::U32, {&lhs.Value(), &addend.Value()})); + } + if (fp16Overflow()) { + const auto infinite = [&](const IrU32& value) { return IrU1(ir.IEqual(ir.BitwiseAnd(value.Value(), ir.Constant(0x7fffu)), ir.Constant(0x7c00u))); }; + const IrU1 overflow(ir.LogicalAnd(infinite(result).Value(), ir.LogicalNot(ir.LogicalOr(infinite(lhs).Value(), infinite(rhs).Value())))); + result = IrU32(ir.Select(overflow.Value(), ir.BitwiseOr(ir.BitwiseAnd(result.Value(), ir.Constant(0x8000u)), ir.Constant(0x7bffu)), result.Value())); + } + write16Bits(inst.destination, clampF16Bits(inst.destination, result)); + return true; +} + IrF32 TranslationContext::fmaF16RoundedToOdd(IrF32 lhs, IrF32 rhs, IrF32 addend) { IrValue& product = ir.Emit(IrOpcode::FPMul32, IrType::F32, {&lhs.Value(), &rhs.Value()}); IrValue& sum = ir.Emit(IrOpcode::FPAdd32, IrType::F32, {&product, &addend.Value()}); From b60f89cf73bb6b4771ab3251d530e83963676110 Mon Sep 17 00:00:00 2001 From: jweidenh <38249793+jweidenh@users.noreply.github.com> Date: Sun, 11 Oct 2026 12:35:32 -0400 Subject: [PATCH 0009/1000] test(agc): bound native f16 SPIR-V scanner reads FindF16x2Math read spirv[offset + 1] for every instruction once the f16 vector type was known, so a final one-word OpFunctionEnd read past the module. Check the opcode and its minimum word count before reading operands. --- .../tests/execution/Float16Native.cpp | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/Float16Native.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/Float16Native.cpp index bc1ffc04fb..aca6e3b7fb 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/Float16Native.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/Float16Native.cpp @@ -135,12 +135,21 @@ F16x2Math FindF16x2Math(std::span spirv) { const std::uint32_t words = spirv[offset] >> 16u; const std::uint32_t opcode = spirv[offset] & 0xffffu; Require(words != 0u && offset + words <= spirv.size(), "f16 native: malformed SPIR-V"); - if (opcode == spv::OpTypeFloat && spirv[offset + 2u] == 16u) half = spirv[offset + 1u]; - if (opcode == spv::OpTypeVector && half && spirv[offset + 2u] == *half && spirv[offset + 3u] == 2u) pair = spirv[offset + 1u]; - if (pair && spirv[offset + 1u] == *pair) { - math.add = math.add || opcode == spv::OpFAdd; - math.mul = math.mul || opcode == spv::OpFMul; - math.fma = math.fma || opcode == spv::OpExtInst; + if (opcode == spv::OpTypeFloat) { + Require(words >= 3u, "f16 native: malformed float type"); + if (spirv[offset + 2u] == 16u) half = spirv[offset + 1u]; + } + if (opcode == spv::OpTypeVector) { + Require(words >= 4u, "f16 native: malformed vector type"); + if (half && spirv[offset + 2u] == *half && spirv[offset + 3u] == 2u) pair = spirv[offset + 1u]; + } + if (opcode == spv::OpFAdd || opcode == spv::OpFMul || opcode == spv::OpExtInst) { + Require(words >= 5u, "f16 native: malformed arithmetic instruction"); + if (pair && spirv[offset + 1u] == *pair) { + math.add = math.add || opcode == spv::OpFAdd; + math.mul = math.mul || opcode == spv::OpFMul; + math.fma = math.fma || opcode == spv::OpExtInst; + } } offset += words; } From 05a2999a2a71389cda2aa6ef6eb9e0638029f3f2 Mon Sep 17 00:00:00 2001 From: Karan Vora <52519664+karanvora2599@users.noreply.github.com> Date: Tue, 13 Oct 2026 17:56:02 -0400 Subject: [PATCH 0010/1000] test(agc): word the native f16 skip as a device limitation check_skips.py accepts a skip only when its message reads "skipped, the device has no ...". Float16Native printed the reason before "skipped", so the Linux job rejected its expected skip on llvmpipe. Use the wording PackedFloat16Native already has. --- core/libs/prx/libSceAgcDriver/tests/execution/Float16Native.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/Float16Native.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/Float16Native.cpp index aca6e3b7fb..959ceebe7b 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/Float16Native.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/Float16Native.cpp @@ -167,7 +167,7 @@ int main() { return std::ranges::find(target.supportedCapabilities, static_cast(capability)) != target.supportedCapabilities.end(); }; if (!supports(spv::CapabilityFloat16) || !supports(spv::CapabilityRoundingModeRTE) || !supports(spv::CapabilityDenormPreserve)) { - std::puts("f16 native: the device has no native f16 with RTE rounding and kept denormals, skipped"); + std::puts("f16 native: skipped, the device has no native f16 with RTE rounding and kept denormals"); return VulkanTestSkipped; } std::vector withoutFloat16; From 1b0eba7550b02bf5a0aa52364e8f52bd3d9b791b Mon Sep 17 00:00:00 2001 From: JJenkx Date: Tue, 13 Oct 2026 20:37:24 -0400 Subject: [PATCH 0011/1000] test: separate independent PR additions --- docs/dev/debt/libkernel.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 59d6198e49..0168a6e104 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -93,8 +93,6 @@ ### Functional -- [pthread_mutexattr_getpshared and pthread_mutexattr_setpshared](../../../core/libs/prx/libkernel/Pthread/Posix/Mutex.cpp) (libkernel) support process-private mutexes. Setting `PTHREAD_PROCESS_SHARED` returns `ENOTSUP` because mutexes use process-local host objects; no attribute is changed on failure. Values 0 and 1 and guest error 45 follow FreeBSD's [pthread.h](https://github.com/freebsd/freebsd-src/blob/main/include/pthread.h) and [errno.h](https://github.com/freebsd/freebsd-src/blob/main/sys/sys/errno.h); the unsupported-sharing error follows [pthread_mutexattr_setpshared](https://man7.org/linux/man-pages/man3/pthread_mutexattr_getpshared.3.html). PS5 shared-mutex behavior and invalid-attribute error precedence are unverified. - - [JIT shared memory](../../../core/libs/prx/libkernel/DirectMemory/Jit.cpp) (libkernel) - `sceKernelJitCreateSharedMemory`, `sceKernelJitCreateAliasOfSharedMemory` and `sceKernelJitMapSharedMemory` take the signatures of [Orbital](https://github.com/AlexAltea/orbital)'s `tools/ida/db_types.json` (name, length, maximum protection, descriptor out; descriptor, maximum protection, descriptor out; descriptor, protection, address in and out), which ps4sdk, ps5-jar-loader and the call sites in RetroArch PPSA99169, XPSemu PPSA97358 and VLC PPSA85300 match; SharpProspero declares the alias as returning the descriptor. The object is a host anonymous file shared by its descriptors (a `memfd` on Linux, a delete-on-close temporary file mapped as an executable section on Windows), the alias is a duplicate descriptor, and every map maps the whole object, so writes through one view show through the others; a non-null address is a hint, as in `mmap` without `MAP_FIXED`. Not measured on the console: a length that is zero or not a multiple of 16 KiB and protection bits other than CPU read, write and execute are `EINVAL`; an alias or a map asking for more than the descriptor's maximum protection (write and execute counting as read) is `EACCES`, as FreeBSD 11's `vm_map_protect` treats a protection past `max_protection`; a descriptor that is not a JIT object is `EBADF`. The name is ignored, the objects do not count against the flexible memory budget, `mprotect` of a view is not limited to the maximum protection, and `mmap` of the descriptors is unsupported, as for `shm_open`. On Linux the `memfd` is created without `MFD_EXEC`, which Linux 6.3 added and older kernels reject with `EINVAL`, and relies on the protection of the mapping: the flag only sets the file's execute permission bits, and the direct memory `memfd` is mapped executable the same way; a host with `vm.memfd_noexec` set to 2 refuses the creation and the call throws - [kqueue and kevent](../../../core/libs/prx/libkernel/Equeue/src/Kqueue.cpp) (libkernel) follow FreeBSD 11's [`kern_event.c`](https://github.com/freebsd/freebsd-src/blob/34bd34750187bae02a0086402c78c031c59dd8b3/sys/kern/kern_event.c) (`kqueue_kevent`, `kqueue_register`, `kqueue_scan`, the timer and user filters) over the event queue; nothing is measured on a console. The descriptor is a host null device registered as the kqueue: `close` and `sceKernelClose` close the queue and `dup`/`dup2` of it throw, but `read`, `write` and `fstat` act on the null device instead of failing with `EOPNOTSUPP` or reporting `S_IFIFO`. Read and write events are supported on guest sockets only, plus the timer, user and [vnode](../../../core/libs/prx/libkernel/Equeue/src/FileEvent.cpp) filters; other filters, `EV_DISABLE`, `EV_DISPATCH`, `EV_FORCEONESHOT`, `NOTE_LOWAT`, `EV_CLEAR` on a socket, `EVFILT_READ` on another kqueue, a `NOTE_NSECONDS` period above one second with a fraction (11.0 converts the fraction with the microsecond macro), any change to an existing timer (11.0 keeps the armed expiry and applies the new period after it, 11.4's `filt_timertouch` restarts it) and a negative `nevents` throw. A new user event keeps its `fflags` unmasked and is not triggered by `NOTE_TRIGGER`, because `filt_userattach` reads the already cleared `kn_fflags`. The timer limit `kq_calloutmax` (`ENOMEM`) is not enforced, and closing a kqueue wakes a thread waiting on it in `kevent` with `EBADF`, where `kqueue_drain` makes the close wait for that thread. A level-triggered event is reported once per call and moved behind the other events, as in `kqueue_scan`; the `sceKernel*Equeue` calls keep their own collection loop - [EVFILT_VNODE, sceKernelAddFileEvent and sceKernelDeleteFileEvent](../../../core/libs/prx/libkernel/Equeue/src/FileEvent.cpp) (libkernel) watch the host file or directory behind the descriptor and turn host changes into FreeBSD 11 vnode hints, which [`filt_vfsvnode`](https://github.com/freebsd/freebsd-src/blob/34bd34750187bae02a0086402c78c031c59dd8b3/sys/kern/vfs_subr.c#L5032) adds whole when they share a bit with the requested `fflags`: a write that grows a file is `NOTE_WRITE | NOTE_EXTEND`, another write `NOTE_WRITE`, a shrinking truncate or an attribute change `NOTE_ATTRIB`, a higher link count `NOTE_LINK`, a lower one or a pending delete `NOTE_DELETE`, a new path `NOTE_RENAME`; in a directory, an entry created, removed or renamed is `NOTE_WRITE`, with `NOTE_LINK` for a subdirectory, and on Linux an entry moved from or to another directory adds `NOTE_EXTEND`; inotify queues the two halves of a rename one after the other, so a watcher woken by the first can read it alone, and a half read without the other is classified only after a `getdents64` on the watched directory, which takes the directory lock that `rename` holds until both halves are queued ([`do_renameat2`](https://github.com/torvalds/linux/blob/v6.8/fs/namei.c#L4975) around `vfs_rename` and its `fsnotify_move`, [`iterate_dir`](https://github.com/torvalds/linux/blob/v6.8/fs/readdir.c#L103)), an ordering taken from the kernel source, not from inotify(7), which promises none; a directory registered with `NOTE_EXTEND` on Windows is accepted, as `vfs_kqfilter` does not check it, but never receives it, because the directory handle cannot tell a move between directories from a removal plus an addition. FreeBSD's hints depend on the filesystem and the console's filesystems are not known: a growing `ftruncate` reports `NOTE_WRITE | NOTE_EXTEND` where `vop_setattr_post` reports `NOTE_ATTRIB`, and on Windows `utimes` on a file reports `NOTE_WRITE`. Windows uses `FindFirstChangeNotification` on the parent directory (and on a watched directory itself) and compares `GetFileInformationByHandleEx` snapshots at least every 50 ms, because a write through an open handle is reported only when the handle is flushed or closed, so file hints can arrive 50 ms late; Linux uses inotify on the file and is not run. `NOTE_REVOKE`, `NOTE_OPEN`, `NOTE_CLOSE`, `NOTE_CLOSE_WRITE`, `NOTE_READ`, unknown bits and descriptors other than files, directories, pipes and sockets throw. `sceKernelAddFileEvent(eq, fd, watch, udata)` and `sceKernelDeleteFileEvent(eq, fd)` take the [OpenOrbis](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain/blob/0a1aaf9d/include/orbis/libkernel.h#L35) and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0/src/SharpProspero/Interop/Kernel/KernelEqueue.cs#L164) signatures; `watch` is taken as the `fflags` and the event is added with `EV_CLEAR`, neither confirmed on a title @@ -108,6 +106,7 @@ - [sceKernelWriteThrottlingStatus](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) reports no write throttling: the host SSD has no write budget, so the first qword of the 32-byte status is set to `UINT32_MAX` and the rest is zeroed. PPSA17221 reads only that qword: it multiplies it by 64 KiB and compares the product with byte thresholds up to 0x4AF00000 to pick one of five tiers. When the call fails the title assumes 0x4B000001 bytes, which selects the same top tier as `UINT32_MAX`. What the value measures on hardware (a remaining budget or a rate) and the meaning of the other three qwords are unknown - [Windows directory descriptors](../../../core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp) (libkernel) take one snapshot of the directory, on the first `getdents`, `getdirentries` or non-zero `lseek` (`SEEK_SET` or `SEEK_CUR`), and keep it until it is discarded; `lseek` to offset 0 only discards the snapshot, and the next read lists the directory again, so every rewind costs O(entries) on the next read. `lseek` takes the offset as an entry index and `SEEK_CUR` with offset 0 only reports the index. `SEEK_END` throws, where the NUL descriptor used to return 0, because FreeBSD defines it relative to the directory size, which is not modelled. A negative result returns `EINVAL` and a `SEEK_CUR` offset that overflows returns `EOVERFLOW`, as FreeBSD `vn_seek` reports them. Names whose UTF-8 encoding exceeds the one-byte `d_namlen` limit are not representable in this guest directory-record layout: `getdents` returns `ENAMETOOLONG` without consuming the entry. Repeated calls therefore report the same error and entries after that name cannot be reached through this descriptor; FreeBSD's `getdents` behavior for host filenames that exceed its `MAXNAMLEN` is not verified. `getdirentries` still reports 0 through `basep` instead of the position of the block it read, so only 0 and values returned by `lseek` are valid offsets; FreeBSD offsets are filesystem cookies - [/dev/random and /dev/urandom](../../../core/libs/prx/libkernel/File/src/RandomDevice.cpp) (libkernel) opened for reading return a descriptor backed by the host null device whose reads are filled with host OS entropy (`getentropy`, `rand_s` on Windows); both paths behave the same, never block, and positioned reads ignore the offset (FreeBSD `random(4)`). Only the exact paths are recognised, `fstat`, `lseek` and `mmap` see the null device, and opening them for writing throws +- [pthread_mutexattr_getpshared and pthread_mutexattr_setpshared](../../../core/libs/prx/libkernel/Pthread/Posix/Mutex.cpp) (libkernel) support process-private mutexes. Setting `PTHREAD_PROCESS_SHARED` returns `ENOTSUP` because mutexes use process-local host objects; no attribute is changed on failure. Values 0 and 1 and guest error 45 follow FreeBSD's [pthread.h](https://github.com/freebsd/freebsd-src/blob/main/include/pthread.h) and [errno.h](https://github.com/freebsd/freebsd-src/blob/main/sys/sys/errno.h); the unsupported-sharing error follows [pthread_mutexattr_setpshared](https://man7.org/linux/man-pages/man3/pthread_mutexattr_getpshared.3.html). PS5 shared-mutex behavior and invalid-attribute error precedence are unverified. - [pthread_attr_getinheritsched, pthread_attr_setinheritsched, pthread_attr_getscope, pthread_attr_setscope, pthread_attr_getstackaddr](../../../core/libs/prx/libkernel/Pthread/Posix/Attr.cpp) (libkernel) follow FreeBSD 11 libthr ([thr_attr.c](https://github.com/freebsd/freebsd-src/blob/5ed7eb0d97ba4436218e810f61bd059acba984c2/lib/libthr/thread/thr_attr.c), [thr_create.c](https://github.com/freebsd/freebsd-src/blob/5ed7eb0d97ba4436218e810f61bd059acba984c2/lib/libthr/thread/thr_create.c)), not measured on the console: the scope defaults to `PTHREAD_SCOPE_SYSTEM` (2), only it and `PTHREAD_SCOPE_PROCESS` (0) are accepted and other values return `EINVAL`, and an inherit value other than `PTHREAD_INHERIT_SCHED` (4) or `PTHREAD_EXPLICIT_SCHED` (0) returns `ENOTSUP`. `pthread_attr_getstackaddr` returns the configured stack address pointer (null by default). A null output of `pthread_attr_getinheritsched` throws, since libthr writes through it unchecked. A thread keeps the scope it was created with, taken from the creating thread under `PTHREAD_INHERIT_SCHED`, and `pthread_attr_get_np` and `scePthreadAttrGet` report it, but every guest thread is a host thread at system scope, so process scope changes nothing - [Shared memory](../../../core/libs/prx/libkernel/File/src/SharedMemory.cpp) (libkernel) on Linux implements anonymous and named objects, bounded descriptor writes and seeks, and `shm_unlink` with effective read/write permission checks from FreeBSD 11. Named paths up to 1023 bytes use `/anyps5-shm-` plus SHA-1; collisions are not detected, and the namespace is shared across processes in the host mount namespace. Named creation applies the guest umask without changing the host mask. Linux procfs and `/dev/shm` are required. Operations use an advisory directory lock; external host operations bypass it, unrelated objects serialize, and descriptor operations require two spare host descriptors and compare device/inode with the initial descriptor identity on both the pinned-object and ordinary-file fallback paths. [Guest mappings](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) support shared and private mappings of these descriptors through the allocation tracker and pin a duplicate descriptor during validation and mapping; mapping requires one spare host descriptor; file-backed mappings use the write-watch scan fallback because aliases and descriptor writes bypass per-mapping tracking. Named objects and file-backed mappings on Windows remain unsupported. Invalid non-null name pointers, console behaviour and title compatibility remain unverified. - [sceKernelAddReadEvent and sceKernelAddWriteEvent](../../../core/libs/prx/libkernel/Equeue/Equeue.cpp) (libkernel): a write event reports the socket's send buffer size (`SO_SNDBUF`) in `data`, not the free space left in it, as Windows has no query for that space. Only guest sockets are supported; other descriptors and a low watermark above 1 throw From e5deca76d9ca5dedfb4d200eed161a7dd1f62793 Mon Sep 17 00:00:00 2001 From: JJenkx Date: Tue, 13 Oct 2026 20:37:25 -0400 Subject: [PATCH 0012/1000] test: separate independent PR additions --- core/relinker/CMakeLists.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index b2b4ef506e..467ac79bf0 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -486,9 +486,6 @@ if(BUILD_TESTING) add_test(NAME guest_module_args COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_module_args.py $ $) - add_test(NAME guest_module_fini COMMAND ${Python3_EXECUTABLE} - ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_module_fini.py - $ $) add_test(NAME guest_absolute_symbols COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_absolute_symbols.py $) add_test(NAME guest_module_identity COMMAND ${Python3_EXECUTABLE} @@ -496,6 +493,9 @@ if(BUILD_TESTING) add_test(NAME guest_export_lookup COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_export_lookup.py $) set_tests_properties(guest_export_lookup PROPERTIES TIMEOUT 30) + add_test(NAME guest_module_fini COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_module_fini.py + $ $) add_test(NAME guest_needed_nested_modules COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_needed_nested_modules.py $) set(windows_stdio_host_arguments) From bfa0de72cbef86ecdb92568e66e76b77c591d772 Mon Sep 17 00:00:00 2001 From: JJenkx Date: Tue, 13 Oct 2026 23:13:51 -0400 Subject: [PATCH 0013/1000] fix(libkernel): retain shared mutex attributes --- core/libs/prx/libkernel/Pthread/Posix/Mutex.cpp | 8 ++++---- core/libs/prx/libkernel/Pthread/include/Pthread.hpp | 1 + core/libs/tests/GuestPthreadMutexattr.cpp | 8 +++++--- docs/dev/debt/libkernel.md | 2 +- 4 files changed, 11 insertions(+), 8 deletions(-) diff --git a/core/libs/prx/libkernel/Pthread/Posix/Mutex.cpp b/core/libs/prx/libkernel/Pthread/Posix/Mutex.cpp index 3782f0d12d..fbe5145666 100644 --- a/core/libs/prx/libkernel/Pthread/Posix/Mutex.cpp +++ b/core/libs/prx/libkernel/Pthread/Posix/Mutex.cpp @@ -12,7 +12,6 @@ namespace { constexpr int POSIX_EINVAL = 22; -constexpr int POSIX_ENOTSUP = 45; constexpr int POSIX_PROCESS_PRIVATE = 0; constexpr int POSIX_PROCESS_SHARED = 1; constexpr int GUEST_REALTIME_CLOCK = 0; @@ -94,14 +93,15 @@ int APS5_VABI pthread_mutexattr_init_nid_postfix(PthreadMutexattr* attr) { int APS5_VABI pthread_mutexattr_getpshared_nid_postfix(const PthreadMutexattr* attr, int* pshared) { if (!attr || !*attr) return POSIX_EINVAL; if (!pshared) throw std::runtime_error("pthread_mutexattr_getpshared: null pshared"); - *pshared = POSIX_PROCESS_PRIVATE; + *pshared = (*attr)->pshared; return 0; } int APS5_VABI pthread_mutexattr_setpshared_nid_postfix(PthreadMutexattr* attr, int pshared) { if (!attr || !*attr) return POSIX_EINVAL; - if (pshared == POSIX_PROCESS_PRIVATE) return 0; - return pshared == POSIX_PROCESS_SHARED ? POSIX_ENOTSUP : POSIX_EINVAL; + if (pshared != POSIX_PROCESS_PRIVATE && pshared != POSIX_PROCESS_SHARED) return POSIX_EINVAL; + (*attr)->pshared = pshared; + return 0; } int APS5_VABI pthread_mutexattr_setprotocol_nid_postfix(PthreadMutexattr* attr, int protocol) { diff --git a/core/libs/prx/libkernel/Pthread/include/Pthread.hpp b/core/libs/prx/libkernel/Pthread/include/Pthread.hpp index e22426757f..1252760c57 100644 --- a/core/libs/prx/libkernel/Pthread/include/Pthread.hpp +++ b/core/libs/prx/libkernel/Pthread/include/Pthread.hpp @@ -32,6 +32,7 @@ struct PthreadMutexattrPrivate { MutexType type; int protocol = 0; int ceiling = 0; + int pshared = 0; }; struct PthreadMutexPrivate { diff --git a/core/libs/tests/GuestPthreadMutexattr.cpp b/core/libs/tests/GuestPthreadMutexattr.cpp index 8de61a9233..297f8006df 100644 --- a/core/libs/tests/GuestPthreadMutexattr.cpp +++ b/core/libs/tests/GuestPthreadMutexattr.cpp @@ -24,7 +24,6 @@ int APS5_VABI sched_get_priority_max_nid_postfix(int policy); } static constexpr int GUEST_EINVAL = 22; -static constexpr int GUEST_ENOTSUP = 45; static constexpr int PRIO_NONE = 0; static constexpr int PRIO_INHERIT = 1; static constexpr int PRIO_PROTECT = 2; @@ -90,10 +89,11 @@ static void CheckProcessSharing() { Require(pthread_mutexattr_getpshared_nid_postfix(&attr, &sharing) == 0 && sharing == 0); Require(pthread_mutexattr_settype_nid_postfix(&attr, 2) == 0); Require(pthread_mutexattr_setpshared_nid_postfix(&attr, 0) == 0); - Require(pthread_mutexattr_setpshared_nid_postfix(&attr, 1) == GUEST_ENOTSUP); + Require(pthread_mutexattr_setpshared_nid_postfix(&attr, 1) == 0); + Require(pthread_mutexattr_getpshared_nid_postfix(&attr, &sharing) == 0 && sharing == 1); for (const int invalid : {-1, 2, 2147483647}) Require(pthread_mutexattr_setpshared_nid_postfix(&attr, invalid) == GUEST_EINVAL); - Require(pthread_mutexattr_getpshared_nid_postfix(&attr, &sharing) == 0 && sharing == 0); + Require(pthread_mutexattr_getpshared_nid_postfix(&attr, &sharing) == 0 && sharing == 1); int type = -1; Require(pthread_mutexattr_gettype_nid_postfix(&attr, &type) == 0 && type == 2); Require(Throws([&] { pthread_mutexattr_getpshared_nid_postfix(&attr, nullptr); })); @@ -104,6 +104,8 @@ static void CheckProcessSharing() { Require(pthread_mutex_unlock_nid_postfix(&mutex) == 0); Require(pthread_mutex_unlock_nid_postfix(&mutex) == 0); Require(pthread_mutex_destroy_nid_postfix(&mutex) == 0); + Require(pthread_mutexattr_setpshared_nid_postfix(&attr, 0) == 0); + Require(pthread_mutexattr_getpshared_nid_postfix(&attr, &sharing) == 0 && sharing == 0); Require(pthread_mutexattr_destroy_nid_postfix(&attr) == 0); sharing = -1; Require(pthread_mutexattr_getpshared_nid_postfix(&attr, &sharing) == GUEST_EINVAL && sharing == -1); diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index f94b8a7c27..9e166b141c 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -106,7 +106,7 @@ - [sceKernelWriteThrottlingStatus](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) reports no write throttling: the host SSD has no write budget, so the first qword of the 32-byte status is set to `UINT32_MAX` and the rest is zeroed. PPSA17221 reads only that qword: it multiplies it by 64 KiB and compares the product with byte thresholds up to 0x4AF00000 to pick one of five tiers. When the call fails the title assumes 0x4B000001 bytes, which selects the same top tier as `UINT32_MAX`. What the value measures on hardware (a remaining budget or a rate) and the meaning of the other three qwords are unknown - [Windows directory descriptors](../../../core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp) (libkernel) take one snapshot of the directory, on the first `getdents`, `getdirentries` or non-zero `lseek` (`SEEK_SET` or `SEEK_CUR`), and keep it until it is discarded; `lseek` to offset 0 only discards the snapshot, and the next read lists the directory again, so every rewind costs O(entries) on the next read. `lseek` takes the offset as an entry index and `SEEK_CUR` with offset 0 only reports the index. `SEEK_END` throws, where the NUL descriptor used to return 0, because FreeBSD defines it relative to the directory size, which is not modelled. A negative result returns `EINVAL` and a `SEEK_CUR` offset that overflows returns `EOVERFLOW`, as FreeBSD `vn_seek` reports them. Names whose UTF-8 encoding exceeds the one-byte `d_namlen` limit are not representable in this guest directory-record layout: `getdents` returns `ENAMETOOLONG` without consuming the entry. Repeated calls therefore report the same error and entries after that name cannot be reached through this descriptor; FreeBSD's `getdents` behavior for host filenames that exceed its `MAXNAMLEN` is not verified. `getdirentries` still reports 0 through `basep` instead of the position of the block it read, so only 0 and values returned by `lseek` are valid offsets; FreeBSD offsets are filesystem cookies - [/dev/random and /dev/urandom](../../../core/libs/prx/libkernel/File/src/RandomDevice.cpp) (libkernel) opened for reading return a descriptor backed by the host null device whose reads are filled with host OS entropy (`getentropy`, `rand_s` on Windows); both paths behave the same, never block, and positioned reads ignore the offset (FreeBSD `random(4)`). Only the exact paths are recognised, `fstat`, `lseek` and `mmap` see the null device, and opening them for writing throws -- [pthread_mutexattr_getpshared and pthread_mutexattr_setpshared](../../../core/libs/prx/libkernel/Pthread/Posix/Mutex.cpp) (libkernel) support process-private mutexes. Setting `PTHREAD_PROCESS_SHARED` returns `ENOTSUP` because mutexes use process-local host objects; no attribute is changed on failure. Values 0 and 1 and guest error 45 follow FreeBSD's [pthread.h](https://github.com/freebsd/freebsd-src/blob/main/include/pthread.h) and [errno.h](https://github.com/freebsd/freebsd-src/blob/main/sys/sys/errno.h); the unsupported-sharing error follows [pthread_mutexattr_setpshared](https://man7.org/linux/man-pages/man3/pthread_mutexattr_getpshared.3.html). PS5 shared-mutex behavior and invalid-attribute error precedence are unverified. +- [pthread_mutexattr_getpshared and pthread_mutexattr_setpshared](../../../core/libs/prx/libkernel/Pthread/Posix/Mutex.cpp) (libkernel) store and report private or shared attributes, following FreeBSD 11 libthr [thr_mutexattr.c](https://github.com/freebsd/freebsd-src/blob/stable/11/lib/libthr/thread/thr_mutexattr.c). Invalid values return `EINVAL` without changing the attribute. Guest threads execute in one host process; sharing mutexes across processes is not modeled. Console behavior and invalid-attribute error precedence are unverified. - [pthread_attr_getinheritsched, pthread_attr_setinheritsched, pthread_attr_getscope, pthread_attr_setscope, pthread_attr_getstackaddr](../../../core/libs/prx/libkernel/Pthread/Posix/Attr.cpp) (libkernel) follow FreeBSD 11 libthr ([thr_attr.c](https://github.com/freebsd/freebsd-src/blob/5ed7eb0d97ba4436218e810f61bd059acba984c2/lib/libthr/thread/thr_attr.c), [thr_create.c](https://github.com/freebsd/freebsd-src/blob/5ed7eb0d97ba4436218e810f61bd059acba984c2/lib/libthr/thread/thr_create.c)), not measured on the console: the scope defaults to `PTHREAD_SCOPE_SYSTEM` (2), only it and `PTHREAD_SCOPE_PROCESS` (0) are accepted and other values return `EINVAL`, and an inherit value other than `PTHREAD_INHERIT_SCHED` (4) or `PTHREAD_EXPLICIT_SCHED` (0) returns `ENOTSUP`. `pthread_attr_getstackaddr` returns the configured stack address pointer (null by default). A null output of `pthread_attr_getinheritsched` throws, since libthr writes through it unchecked. A thread keeps the scope it was created with, taken from the creating thread under `PTHREAD_INHERIT_SCHED`, and `pthread_attr_get_np` and `scePthreadAttrGet` report it, but every guest thread is a host thread at system scope, so process scope changes nothing - [Shared memory](../../../core/libs/prx/libkernel/File/src/SharedMemory.cpp) (libkernel) on Linux implements anonymous and named objects, bounded descriptor writes and seeks, and `shm_unlink` with effective read/write permission checks from FreeBSD 11. Named paths up to 1023 bytes use `/anyps5-shm-` plus SHA-1; collisions are not detected, and the namespace is shared across processes in the host mount namespace. Named creation applies the guest umask without changing the host mask. Linux procfs and `/dev/shm` are required. Operations use an advisory directory lock; external host operations bypass it, unrelated objects serialize, and descriptor operations require two spare host descriptors and compare device/inode with the initial descriptor identity on both the pinned-object and ordinary-file fallback paths. [Guest mappings](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) support shared and private mappings of these descriptors through the allocation tracker and pin a duplicate descriptor during validation and mapping; mapping requires one spare host descriptor; file-backed mappings use the write-watch scan fallback because aliases and descriptor writes bypass per-mapping tracking. Named objects and file-backed mappings on Windows remain unsupported. Invalid non-null name pointers, console behaviour and title compatibility remain unverified. - [sceKernelAddReadEvent and sceKernelAddWriteEvent](../../../core/libs/prx/libkernel/Equeue/Equeue.cpp) (libkernel): a write event reports the socket's send buffer size (`SO_SNDBUF`) in `data`, not the free space left in it, as Windows has no query for that space. Only guest sockets are supported; other descriptors and a low watermark above 1 throw From 2caefdb8ed3ea1e470413bee4e671677f6d2a542 Mon Sep 17 00:00:00 2001 From: Rishit Vora Date: Wed, 14 Oct 2026 04:19:49 +0000 Subject: [PATCH 0014/1000] fix(libkernel): resolve renamed guest exports in the default scope --- core/libs/prx/libkernel/Module/src/DynamicLoader.cpp | 2 ++ core/libs/tests/GuestDynamicLoader.cpp | 11 +++++++++++ 2 files changed, 13 insertions(+) diff --git a/core/libs/prx/libkernel/Module/src/DynamicLoader.cpp b/core/libs/prx/libkernel/Module/src/DynamicLoader.cpp index 1dc372c387..b135e50b31 100644 --- a/core/libs/prx/libkernel/Module/src/DynamicLoader.cpp +++ b/core/libs/prx/libkernel/Module/src/DynamicLoader.cpp @@ -217,6 +217,8 @@ void* APS5_VABI dlsym_nid_postfix(void* handle, const char* name) { if (auto* result = DefaultScopeSymbol(name)) return result; const auto nid = Nid::ComputeNid(name, ""); if (auto* result = DefaultScopeSymbol(nid.c_str())) return result; + if (auto* result = DefaultScopeSymbol((std::string(name) + "#guest").c_str())) return result; + if (auto* result = DefaultScopeSymbol((nid + "#guest").c_str())) return result; } #endif Error("dlsym: symbol not found in supported module scope"); diff --git a/core/libs/tests/GuestDynamicLoader.cpp b/core/libs/tests/GuestDynamicLoader.cpp index 1eb0a5fb45..6ad61bd721 100644 --- a/core/libs/tests/GuestDynamicLoader.cpp +++ b/core/libs/tests/GuestDynamicLoader.cpp @@ -1,4 +1,5 @@ #include "prx/libc/include/general/VabiMacros.hpp" +#include #include #include #include @@ -122,6 +123,16 @@ int main(int argc, char** argv) { void* hostAdd = ::dlsym(host, "3qcG+yBRS+s"); Require(hostAdd != nullptr); Require(dlsym_nid_postfix(nullptr, "GuestModuleAdd") == hostAdd); + void* hostMul = ::dlsym(host, "GuestModuleMul#guest"); + void* hostSub = ::dlsym(host, "BOyBJaKwOa8#guest"); + Require(hostMul != nullptr && hostSub != nullptr); + for (void* scope : std::array{nullptr, reinterpret_cast(-2)}) { + auto defaultMul = reinterpret_cast(dlsym_nid_postfix(scope, "GuestModuleMul")); + auto defaultSub = reinterpret_cast(dlsym_nid_postfix(scope, "GuestModuleSub")); + Require(reinterpret_cast(defaultMul) == hostMul); + Require(reinterpret_cast(defaultSub) == hostSub); + Require(defaultMul(6, 7) == 42 && defaultSub(50, 8) == 42); + } Require(dlsym_nid_postfix(nullptr, "malloc") == nullptr); Require(::dlclose(host) == 0); #endif From e41f7f292e2a106858f3e5f8acd99ee2cadc2e33 Mon Sep 17 00:00:00 2001 From: Rujul Talekar <113801876+Roojool@users.noreply.github.com> Date: Mon, 12 Oct 2026 16:45:16 +0530 Subject: [PATCH 0015/1000] fix(libkernel): answer a second scePthreadJoin with ESRCH --- core/libs/prx/libkernel/CMakeLists.txt | 10 +++ .../prx/libkernel/Pthread/include/Pthread.hpp | 2 + .../libs/prx/libkernel/Pthread/src/Thread.cpp | 32 ++++++++- core/libs/tests/GuestPthreadSecondJoin.cpp | 65 +++++++++++++++++++ docs/dev/debt/libkernel.md | 2 +- 5 files changed, 107 insertions(+), 4 deletions(-) create mode 100644 core/libs/tests/GuestPthreadSecondJoin.cpp diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 6d9e5fbdc6..077ed8d23d 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -594,6 +594,16 @@ if(BUILD_TESTING) set_tests_properties(guest_pthread_join_self PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_pthread_second_join_tests ${LIBS_INCLUDE_DIR}/tests/GuestPthreadSecondJoin.cpp) + target_include_directories(guest_pthread_second_join_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_pthread_second_join_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_pthread_second_join_tests) + add_test(NAME guest_pthread_second_join COMMAND guest_pthread_second_join_tests) + set_tests_properties(guest_pthread_second_join PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_pthread_second_join PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_pthread_key_tests ${LIBS_INCLUDE_DIR}/tests/GuestPthreadKey.cpp) target_include_directories(guest_pthread_key_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_pthread_key_tests PRIVATE libkernel libc) diff --git a/core/libs/prx/libkernel/Pthread/include/Pthread.hpp b/core/libs/prx/libkernel/Pthread/include/Pthread.hpp index e22426757f..696defa42d 100644 --- a/core/libs/prx/libkernel/Pthread/include/Pthread.hpp +++ b/core/libs/prx/libkernel/Pthread/include/Pthread.hpp @@ -139,6 +139,8 @@ struct PthreadPrivate { void* _retval; bool _detached; bool _adopted; + Pthread* _handleStorage; + std::atomic _joined; std::mutex _join_mtx; TimedWait::Condition _join_cv; std::atomic cancelPending{false}; diff --git a/core/libs/prx/libkernel/Pthread/src/Thread.cpp b/core/libs/prx/libkernel/Pthread/src/Thread.cpp index caeb5c291d..11c0fc776b 100644 --- a/core/libs/prx/libkernel/Pthread/src/Thread.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Thread.cpp @@ -303,7 +303,7 @@ static CpuClockRegistry& CpuClocks() { return *registry; } -PthreadPrivate::PthreadPrivate(const PthreadAttrPrivate* attr) : _finished(false), _retval(nullptr), _detached(false), _adopted(false) { +PthreadPrivate::PthreadPrivate(const PthreadAttrPrivate* attr) : _finished(false), _retval(nullptr), _detached(false), _adopted(false), _handleStorage(nullptr), _joined(false) { auto& registry = CpuClocks(); std::lock_guard lock(registry.lock); const auto* parent = CurrentGuestState().current; @@ -554,6 +554,26 @@ static void ReleaseThread(PthreadPrivate* thread) { delete thread; } +static std::mutex joinedLock; +static std::map joinedThreads; + +static void RetainJoinedThread(PthreadPrivate* thread) { + std::lock_guard lock(joinedLock); + Pthread* const storage = thread->_handleStorage; + const auto found = joinedThreads.find(storage); + if (found != joinedThreads.end() && found->second != thread) ReleaseThread(found->second); + joinedThreads[storage] = thread; +} + +static void RecycleJoinedThread(Pthread* storage) { + std::lock_guard lock(joinedLock); + const auto found = joinedThreads.find(storage); + if (found == joinedThreads.end()) return; + PthreadPrivate* joined = found->second; + joinedThreads.erase(found); + ReleaseThread(joined); +} + #ifdef _WIN32 // The title's job workers ("BPE JobWorkerThread CPU0".."CPU12") spin at full load on one core each. // Pinning them to the efficiency cores of a hybrid CPU (APS5_JOB_AFFINITY=1, with the driver's @@ -716,8 +736,10 @@ int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, Pthread throw; } auto* published = p.release(); + published->_handleStorage = thread; *thread = published; start.set_value(true); + RecycleJoinedThread(thread); if (detached) ReleaseThread(published); #else @@ -772,8 +794,10 @@ int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, Pthread throw; } auto* published = p.release(); + published->_handleStorage = thread; *thread = published; start.set_value(true); + RecycleJoinedThread(thread); if (detached) ReleaseThread(published); #endif @@ -783,30 +807,32 @@ int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, Pthread int APS5_VABI scePthreadJoin(Pthread thread, void** retval) { if (!thread) return SCE_KERNEL_ERROR_EINVAL; if (thread->_detached) return SCE_KERNEL_ERROR_EINVAL; + if (thread->_joined.load(std::memory_order_acquire)) return SCE_KERNEL_ERROR_ESRCH; if (thread == CurrentGuestState().current) return SCE_KERNEL_ERROR_EDEADLK; ThreadCancel::Check(); { std::unique_lock lock(thread->_join_mtx); ThreadCancel::WaitUntil(thread->_join_cv, lock, std::nullopt, [&] { return thread->_finished.load(); }); } + if (thread->_joined.exchange(true, std::memory_order_acq_rel)) return SCE_KERNEL_ERROR_ESRCH; #ifdef _WIN32 if (WaitForSingleObject(thread->nativeHandle, INFINITE) != WAIT_OBJECT_0) throw std::system_error(GetLastError(), std::system_category(), "Joining guest thread"); if (retval) *retval = thread->_retval; - ReleaseThread(thread); #else if (const int result = pthread_join(thread->hostThread, nullptr); result != 0) throw std::system_error(result, std::generic_category(), "Joining guest thread"); FreeGuestStack(thread); if (retval) *retval = thread->_retval; - ReleaseThread(thread); #endif + RetainJoinedThread(thread); return SCE_OK; } int APS5_VABI scePthreadDetach(Pthread thread) { if (!thread) return SCE_KERNEL_ERROR_EINVAL; if (thread->_detached) return SCE_KERNEL_ERROR_EINVAL; + if (thread->_joined.load(std::memory_order_acquire)) return SCE_KERNEL_ERROR_EINVAL; thread->_detached = true; #ifdef _WIN32 ReleaseThread(thread); diff --git a/core/libs/tests/GuestPthreadSecondJoin.cpp b/core/libs/tests/GuestPthreadSecondJoin.cpp new file mode 100644 index 0000000000..4db4d57034 --- /dev/null +++ b/core/libs/tests/GuestPthreadSecondJoin.cpp @@ -0,0 +1,65 @@ +#include "SceTypes.hpp" +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, PthreadEntry entry, void* arg, const char* name); +int APS5_VABI scePthreadJoin(Pthread thread, void** retval); +int APS5_VABI scePthreadDetach(Pthread thread); +int APS5_VABI pthread_join_nid_postfix(Pthread thread, void** retval); +} + +static constexpr int SCE_OK = 0; +static constexpr int SCE_KERNEL_ERROR_ESRCH = static_cast(0x80020003u); +static constexpr int SCE_KERNEL_ERROR_EINVAL = static_cast(0x80020016u); +static constexpr int GUEST_ESRCH = 3; +static constexpr std::size_t REUSED_SLOT_ROUNDS = 64; + +static void Require(bool value) { if (!value) std::abort(); } + +static void* APS5_VABI Finish(void*) { + return reinterpret_cast(std::uintptr_t{0x1234}); +} + +static void* APS5_VABI WaitForRelease(void* arg) { + auto* release = static_cast*>(arg); + while (!release->load()) std::this_thread::yield(); + return nullptr; +} + +int main() { + Pthread thread = nullptr; + void* result = nullptr; + Require(scePthreadCreate(&thread, nullptr, Finish, nullptr, "second-join") == SCE_OK); + Require(scePthreadJoin(thread, &result) == SCE_OK); + Require(result == reinterpret_cast(std::uintptr_t{0x1234})); + + result = reinterpret_cast(std::uintptr_t{0x55}); + Require(scePthreadJoin(thread, &result) == SCE_KERNEL_ERROR_ESRCH); + Require(result == reinterpret_cast(std::uintptr_t{0x55})); + Require(scePthreadDetach(thread) == SCE_KERNEL_ERROR_EINVAL); + Require(pthread_join_nid_postfix(thread, &result) == GUEST_ESRCH); + Require(result == reinterpret_cast(std::uintptr_t{0x55})); + + for (std::size_t round = 0; round < REUSED_SLOT_ROUNDS; ++round) { + thread = nullptr; + result = nullptr; + Require(scePthreadCreate(&thread, nullptr, Finish, nullptr, "reused-slot") == SCE_OK); + Require(scePthreadJoin(thread, &result) == SCE_OK); + Require(result == reinterpret_cast(std::uintptr_t{0x1234})); + Require(scePthreadJoin(thread, nullptr) == SCE_KERNEL_ERROR_ESRCH); + } + + static std::atomic release{false}; + Pthread detached = nullptr; + Require(scePthreadCreate(&detached, nullptr, WaitForRelease, &release, "detached") == SCE_OK); + Require(scePthreadDetach(detached) == SCE_OK); + result = reinterpret_cast(std::uintptr_t{0x55}); + Require(scePthreadJoin(detached, &result) == SCE_KERNEL_ERROR_EINVAL); + Require(result == reinterpret_cast(std::uintptr_t{0x55})); + Require(scePthreadDetach(detached) == SCE_KERNEL_ERROR_EINVAL); + release = true; +} diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index cfdc0680d8..0adf5cffff 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -131,7 +131,7 @@ - [sceKernelRaiseException](../../../core/libs/prx/libkernel/System/src/Exception.cpp) on Windows runs the handler wherever the target thread was interrupted. It waits only while the thread is inside winpthreads or restoring a context through `NtContinue`, and throws if that lasts 1 s, so a handler that calls into other host code the thread was interrupted in, such as the C runtime's heap or a lock of the emulator, can deadlock on it or reenter it - [sigaltstack](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) records each thread's alternate signal stack and reports it back, following FreeBSD's `kern_sigaltstack` (`EINVAL` for flags other than `SS_DISABLE`, `ENOMEM` below `MINSIGSTKSZ` 2048), but handlers never run on it, so `SS_ONSTACK` and `EPERM` are never reported - [sigpending](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) reports the intersection of the process-wide pending set of `sigtimedwait` and the blocked mask of `_sigprocmask`, as FreeBSD's `sys_sigpending` intersects the pending list with `td_sigmask`, and returns -1 with `EFAULT` for a null set. Because `_sigprocmask` keeps only the first 32 signals, signals 33 to 128 are never blocked and never appear in the set, and setting `SIG_IGN` (or `SIG_DFL` on a signal whose default action is to ignore) does not clear a signal that was already marked pending. PPSA99420 imports it -- [scePthreadJoin, scePthreadCreate](../../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) - on a PS5 (firmware 13.60) joining a thread a second time returns `ESRCH` (`0x80020003`); here a joined thread is released, so a second join reads freed memory. A null entry crashed the payload and still throws. Joining the calling thread returns `EDEADLK` and a zeroed attribute creates a default thread, as on the console +- [scePthreadJoin, scePthreadCreate](../../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) - on a PS5 (firmware 13.60) joining a thread a second time returns `ESRCH` (`0x80020003`), as it does here, but a joined thread is kept until `scePthreadCreate` publishes over the same handle storage, so one host object and its handle are retained per distinct guest handle storage location for the lifetime of the process; a guest that joins into a fresh storage location every time keeps every one of them. A null entry crashed the payload and still throws. Joining the calling thread returns `EDEADLK` and a zeroed attribute creates a default thread, as on the console. `scePthreadDetach` on a joined thread answers `EINVAL`, as FreeBSD does on a thread that is not joinable; the console answer for a detach after a join is unverified. A detached thread is still released when it ends, so joining it after it ended reads freed memory. A handle value copied to a second storage and joined there retains the object under the original storage key, so a later `scePthreadCreate` over that original storage releases an object the copy still points at, and the copy reads freed memory - [Thread cancellation](../../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) follows FreeBSD libthr (states and types 0/1 and 0/2, `PTHREAD_CANCELED` 1, the `_pthread_cleanup_info` node layout), not measured on the console. A request is acted on only in `scePthreadTestcancel`, the condition variable, `sem` and `scePthreadSem` waits, `scePthreadJoin` and the sleeps, and their POSIX names; file and socket calls are not cancellation points. A sleep is not interrupted: the thread exits when it ends. The asynchronous type acts like the deferred one, except at the two set calls. - [cpuset_setaffinity](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - only the calling thread or the calling process is handled; other levels (ROOT/CPUSET), other scopes (CPUSET/JAIL) and other threads throw as unmodelled, invalid levels or scopes return `EINVAL` and unknown thread or process IDs return `ESRCH`, following FreeBSD's `cpuset_setaffinity`. Bits outside `sceKernelGetAvailableCpumask` are dropped, and a mask with none of those bits fails with `EDEADLK`, as FreeBSD does when the new set has no CPU in common with the parent set - [cpuset_getaffinity](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - only the calling thread or the calling process is handled; other levels (ROOT/CPUSET), other scopes (CPUSET/JAIL) and other threads throw as unmodelled, invalid levels or scopes return `EINVAL` and unknown thread or process IDs return `ESRCH`, following FreeBSD's `cpuset_getaffinity`. For the process the mask is the union of the affinities of the live guest threads, as [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/sys/kern/kern_cpuset.c#L1150-L1156) ORs the mask of every thread in the process; threads the guest did not create through libkernel are not included, and the console result is unverified From 7ef0931ffd197a718290916d798c6eb8c3f5a525 Mon Sep 17 00:00:00 2001 From: Rujul Talekar <113801876+Roojool@users.noreply.github.com> Date: Mon, 12 Oct 2026 19:47:44 +0530 Subject: [PATCH 0016/1000] fix(libkernel): answer pthread_getname_np for a joined thread with ESRCH --- .../prx/libkernel/Pthread/Posix/Thread.cpp | 3 +++ core/libs/tests/GuestPthreadGetname.cpp | 20 +++++++++++++++++++ docs/dev/debt/libkernel.md | 2 +- 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libkernel/Pthread/Posix/Thread.cpp b/core/libs/prx/libkernel/Pthread/Posix/Thread.cpp index 90ad038f81..a98f6e946b 100644 --- a/core/libs/prx/libkernel/Pthread/Posix/Thread.cpp +++ b/core/libs/prx/libkernel/Pthread/Posix/Thread.cpp @@ -190,6 +190,9 @@ int APS5_VABI pthread_rename_np_nid_postfix(Pthread thread, const char* name) { int APS5_VABI pthread_getname_np_nid_postfix(Pthread thread, char* name) { if (!thread) return PosixThread::GUEST_ESRCH; if (!name) return PosixThread::GUEST_EFAULT; + // A joined thread answers ESRCH on the console (PS5 firmware 13.60); the object is retained + // after scePthreadJoin, so _joined tells it apart from a live thread. + if (thread->_joined.load(std::memory_order_acquire)) return PosixThread::GUEST_ESRCH; return PosixThread::ToErrno(scePthreadGetname(thread, name)); } diff --git a/core/libs/tests/GuestPthreadGetname.cpp b/core/libs/tests/GuestPthreadGetname.cpp index 4943093901..f8545f6078 100644 --- a/core/libs/tests/GuestPthreadGetname.cpp +++ b/core/libs/tests/GuestPthreadGetname.cpp @@ -10,6 +10,7 @@ int APS5_VABI pthread_rename_np_nid_postfix(Pthread thread, const char* name); int APS5_VABI pthread_getname_np_nid_postfix(Pthread thread, char* name); int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, PthreadEntry entry, void* arg, const char* name); int APS5_VABI scePthreadJoin(Pthread thread, void** retval); +int APS5_VABI scePthreadGetname(Pthread thread, char* name); } static constexpr int GUEST_ESRCH = 3; @@ -35,6 +36,10 @@ static void* APS5_VABI Unnamed(void*) { return nullptr; } +static void* APS5_VABI ExitNow(void*) { + return nullptr; +} + int main() { const Pthread self = pthread_self_nid_postfix(); for (const char* name : {"", "A", "AnyPS5Probe", "0123456789012345678901234567890"}) { @@ -45,6 +50,21 @@ int main() { Require(scePthreadCreate(&worker, nullptr, Unnamed, nullptr, nullptr) == 0); Require(scePthreadJoin(worker, nullptr) == 0); + // A joined thread answers pthread_getname_np with ESRCH (PS5 firmware 13.60) and leaves the + // output buffer untouched, so the posix wrapper must not read the retained object's name. + Pthread joined = nullptr; + Require(scePthreadCreate(&joined, nullptr, ExitNow, nullptr, "JoinedWorker") == 0); + Require(scePthreadJoin(joined, nullptr) == 0); + const auto afterJoin = Read(joined, GUEST_ESRCH); + for (unsigned char byte : afterJoin) Require(byte == 0xAA); + + // The SCE sibling has no measured joined-thread behaviour on the console; it still reads the + // retained object's name, which only pins today's behaviour of scePthreadGetname. + std::array sibling{}; + sibling.fill(0x5A); + Require(scePthreadGetname(joined, sibling.data()) == 0); + Require(std::memcmp(sibling.data(), "JoinedWorker", sizeof("JoinedWorker")) == 0); + const auto untouched = Read(nullptr, GUEST_ESRCH); for (unsigned char byte : untouched) Require(byte == 0xAA); Require(pthread_getname_np_nid_postfix(self, nullptr) == GUEST_EFAULT); diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 0adf5cffff..2c62ccbcce 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -65,7 +65,7 @@ - [environ](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - data export pointing to a fixed empty environment so titles that import it load; it is not the environment libc's `getenv` reads, so it lists none of those variables and `setenv`/`putenv` changes do not appear in it - [seteuid, setegid, setuid, setreuid, setregid, setgroups, getgroups](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - the guest process is assumed to run as root, with real, effective and saved user and group ids 0. As in FreeBSD's `sys_seteuid`, `sys_setegid`, `sys_setuid`, `sys_setreuid` and `sys_setregid`, id 0 (or -1 for an unchanged `setreuid`/`setregid` id) is then accepted without a privilege check and changes no credential, so 0 is returned; any other id would change an id, which is not modelled, and throws. `setgroups` keeps the supplementary group list as `kern_setgroups` does (`NGROUPS_MAX` 1023, an empty list leaves the effective group as the only entry, a first entry other than 0 would change the effective group and throws) and `getgroups` reports it. The ids and privileges a title has on a console are unverified - [pthread_barrierattr_setpshared](../../../core/libs/prx/libkernel/Pthread/Posix/Barrier.cpp) (libkernel) - PTHREAD_PROCESS_SHARED is accepted on the assumption that the console follows FreeBSD 11.0 libthr, which accepts it (9.0 rejects it with EINVAL); unverified on hardware. Guest code runs in one process, so a shared barrier behaves as a private one -- [pthread_getname_np](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - a thread that has already been joined returns `ESRCH` (3) on a PS5 (firmware 13.60), but its object is freed after the join here, so the pointer cannot be told apart from a live thread and its name is read as for a live one +- [pthread_getname_np](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - a thread that has already been joined returns `ESRCH` (3) and its output buffer is left untouched, as measured on a PS5 (firmware 13.60) by the author of this entry; since [#4481](https://github.com/boykopovar/AnyPS5/pull/4481) the joined thread's object is retained, so the posix wrapper answers `ESRCH` for it instead of reading the retained name. A null name buffer is answered with `EFAULT` before that, an order the console table does not measure. The SCE sibling `scePthreadGetname` has no measured joined-thread behaviour on the console and still reads the retained object's name, so it is unchanged, as is the pre-existing dangling case of a thread that ended detached (its object is released at exit; see the scePthreadJoin entry) - [sceKernelGetOperationMode](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - the signature `(int* mode, int* submode)` comes from PPSA12544, which logs both values after the call and ignores the result. A homebrew payload on a PS5 (firmware 13.60) reads mode 2 and submode 0, nothing else is written, and the result register holds no status, so the function is taken to return nothing. Whether a title process reads the same values is unverified. Null outputs throw - [sceCoredumpGetStopInfoCpu](../../../core/libs/prx/libkernel/System/src/Coredump.cpp) (libkernel) takes `(void* info, size_t size)` as in the [Orbital type database](https://github.com/AlexAltea/orbital/blob/cc408367da6a6ebbf5ac1ac559fefeadc73b2831/tools/ida/db_types.json); the 16-byte, 8-byte-aligned block follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Kernel/KernelSystem.cs), which places coredump reads inside the handler. The handler is never invoked here, so a well-formed request returns `NOT_IN_COREDUMP_HANDLER` without reading or writing the block. Its value `0x81180003` follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/e0a466753a1aacbc8db8253be685bf7ab4d9daae/src/core/libraries/kernel/coredump/coredump_error.h) and the NHL 27 (PPSA34063) call site reported in [#4275](https://github.com/boykopovar/AnyPS5/discussions/4275), not a PS5 measurement. The console validation order is unverified: null, unaligned or wrapping buffers and sizes other than 16 throw instead of guessing an argument error. Information from an active coredump handler remains unsupported - [sceCoredumpAttachMemoryRegion, sceCoredumpAttachUserMemoryFile](../../../core/libs/prx/libkernel/System/src/Coredump.cpp) (libkernel) - both take `(uint32_t userValue, const void* mem, size_t size)`: for the first, the IDA type database of [Orbital](https://github.com/AlexAltea/orbital/blob/master/tools/ida/db_types.json) and the [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Kernel/KernelSystem.cs) bindings agree; the second has only SharpProspero (the PS4 5.05 firmware does not export it, so Orbital has no entry) and is described there as attaching the region as an unnamed user file. Not confirmed on a title; the RED engine's PS4 crash handler ([Vangaurd](https://github.com/vurimad/Vangaurd/blob/main/source/imported/common/redSystem/src/errorHandlerImplErrorHooksOrbis.cpp)) calls the sibling `sceCoredumpAttachMemoryRegionAsUserFile(999, buffer, bytes, name)` from inside its handler. Attaching belongs inside the core dump handler, which the system runs on its own thread after the process stops (SharpProspero's remark, the RED engine's usage and `SCE_COREDUMP_ERROR_NOT_IN_COREDUMP_HANDLER`, "function was not called from registered core dump handler", in the Vita's SceCoredump error table, [RivetDB](https://github.com/CreepNT/RivetDB/blob/master/rivetdb.json)). The handler is never invoked here, so every call is outside it and returns that error; nothing is recorded, as there is no dump to attach to. Its value `0x81180003` comes from shadPS4 (PS4), not confirmed on a PS5: [fpPS4](https://github.com/red-prig/fpPS4/blob/master/kernel/ps4_libkernel.pas) has `NOT_REGISTERED` 0x81180001 and `ALREADY_REGISTERED` 0x81180002, shadPS4's `coredump_error.h` has `NOT_IN_COREDUMP_HANDLER` = 0x81180003, and the Vita table numbers `PARAM` 0, `NOT_REGISTERED` 1, `ALREADY_REGISTERED` 2 and `NOT_IN_COREDUMP_HANDLER` 3. Whether the console checks the region before the calling context is unknown, so a null, empty or wrapping region throws. `userValue` is not interpreted; SharpProspero names its bits 31 and 30 as truncation flags for the user file variants From 9dc2ce8acfb23fa85e6dc72e04475320b47b31d4 Mon Sep 17 00:00:00 2001 From: fogydes Date: Tue, 13 Oct 2026 21:16:40 +0530 Subject: [PATCH 0017/1000] fix(relinker): prevent address overflow and signed UB in CFG, GOT access, and entry point analysis --- .../UnusedNidFilter/ControlFlowGraph.cpp | 23 ++++++++-------- .../UnusedNidFilter/EntryPointCollector.cpp | 5 +++- .../UnusedNidFilter/GotAccessIndex.cpp | 10 ++++--- .../relinker/tests/ControlFlowGraphTests.cpp | 27 +++++++++++++++++++ 4 files changed, 48 insertions(+), 17 deletions(-) diff --git a/core/relinker/relinker/src/analysis/UnusedNidFilter/ControlFlowGraph.cpp b/core/relinker/relinker/src/analysis/UnusedNidFilter/ControlFlowGraph.cpp index bf4c1f76d9..fcbcc21b15 100644 --- a/core/relinker/relinker/src/analysis/UnusedNidFilter/ControlFlowGraph.cpp +++ b/core/relinker/relinker/src/analysis/UnusedNidFilter/ControlFlowGraph.cpp @@ -4,6 +4,7 @@ #include #include #include +#include namespace Relinker::UnusedNidFilter { @@ -30,6 +31,8 @@ private: namespace { void buildGraph(std::span text, VirtualAddress textVaddr, VirtualAddress entryVaddr, const std::vector& extraEntries, const IRelativeRelocationIndex& relativeRelocations, const std::vector* data, bool followCodeAddresses, std::unordered_set& reachable, const std::function& progress) { + if (text.size() > std::numeric_limits::max() - textVaddr) + throw RelinkerException("Text section range exceeds the address space"); const Codegen::X64InstructionDecoder decoder; std::unordered_set visited; std::queue worklist; @@ -38,7 +41,7 @@ void buildGraph(std::span text, VirtualAddress textVaddr, Vi auto enqueue = [&](VirtualAddress va) { if (reachable.count(va) > 0) return; - if (va < textVaddr || va >= textVaddr + static_cast(text.size())) return; + if (va < textVaddr || va - textVaddr >= text.size()) return; if (!visited.insert(va).second) return; worklist.push(va); }; @@ -89,13 +92,13 @@ void buildGraph(std::span text, VirtualAddress textVaddr, Vi if (followCodeAddresses && info.HasRipRelativeDisp && text[bufOff + info.OpcodeOffset] == 0x8d && (info.RexPrefix & 8)) { std::int32_t displacement = 0; std::memcpy(&displacement, text.data() + bufOff + info.RipRelativeDispOffset, 4); - enqueue(static_cast(static_cast(nextVaddr) + displacement)); + enqueue(nextVaddr + static_cast(static_cast(displacement))); } if (data != nullptr && info.HasRipRelativeDisp && !info.IsTwoByteOpcode && info.Opcode == 0x8D) { std::int32_t displacement = 0; std::memcpy(&displacement, text.data() + bufOff + info.RipRelativeDispOffset, 4); - const auto base = static_cast(static_cast(nextVaddr) + displacement); + const auto base = nextVaddr + static_cast(static_cast(displacement)); takeAddress(base); for (VirtualAddress target : ReadRelativeTableTargets(*data, base, textVaddr, text.size())) takeAddress(target); } @@ -107,35 +110,31 @@ void buildGraph(std::span text, VirtualAddress textVaddr, Vi break; case ConditionalBranch: if (info.HasBranchTarget) - enqueue(static_cast(static_cast(nextVaddr) + info.BranchDisp)); + enqueue(nextVaddr + static_cast(info.BranchDisp)); break; case UnconditionalJump: fallThrough = false; if (info.HasRipRelativeDisp) { std::int32_t disp = 0; std::memcpy(&disp, text.data() + bufOff + info.RipRelativeDispOffset, 4); - VirtualAddress slotVaddr = static_cast( - static_cast(nextVaddr) + disp - ); + VirtualAddress slotVaddr = nextVaddr + static_cast(static_cast(disp)); auto resolved = relativeRelocations.TargetOfSlot(slotVaddr); if (resolved.has_value()) enqueue(*resolved); } else if (info.HasBranchTarget) { - enqueue(static_cast(static_cast(nextVaddr) + info.BranchDisp)); + enqueue(nextVaddr + static_cast(info.BranchDisp)); } break; case Call: if (info.HasRipRelativeDisp) { std::int32_t disp = 0; std::memcpy(&disp, text.data() + bufOff + info.RipRelativeDispOffset, 4); - VirtualAddress slotVaddr = static_cast( - static_cast(nextVaddr) + disp - ); + VirtualAddress slotVaddr = nextVaddr + static_cast(static_cast(disp)); auto resolved = relativeRelocations.TargetOfSlot(slotVaddr); if (resolved.has_value()) enqueue(*resolved); } else if (info.HasBranchTarget) { - enqueue(static_cast(static_cast(nextVaddr) + info.BranchDisp)); + enqueue(nextVaddr + static_cast(info.BranchDisp)); } break; case IndirectCall: diff --git a/core/relinker/relinker/src/analysis/UnusedNidFilter/EntryPointCollector.cpp b/core/relinker/relinker/src/analysis/UnusedNidFilter/EntryPointCollector.cpp index e95afa383b..947521c429 100644 --- a/core/relinker/relinker/src/analysis/UnusedNidFilter/EntryPointCollector.cpp +++ b/core/relinker/relinker/src/analysis/UnusedNidFilter/EntryPointCollector.cpp @@ -39,7 +39,7 @@ struct LoadSegment { }; bool inText(VirtualAddress va, VirtualAddress textVaddr, std::size_t textSize) { - return va >= textVaddr && va < textVaddr + static_cast(textSize); + return va >= textVaddr && va - textVaddr < static_cast(textSize); } } @@ -52,6 +52,9 @@ public: std::size_t textSize, const IRelativeRelocationIndex& relativeRelocations ) const override { + if (textSize > std::numeric_limits::max() - textVaddr) { + throw RelinkerException("Text section range exceeds the address space"); + } if (elfBytes.size() < 64) throw RelinkerException("ELF too small for header"); if (elfBytes[0] != 0x7f || elfBytes[1] != 'E' || elfBytes[2] != 'L' || elfBytes[3] != 'F') { diff --git a/core/relinker/relinker/src/analysis/UnusedNidFilter/GotAccessIndex.cpp b/core/relinker/relinker/src/analysis/UnusedNidFilter/GotAccessIndex.cpp index cf9f0b94d5..e9405ca433 100644 --- a/core/relinker/relinker/src/analysis/UnusedNidFilter/GotAccessIndex.cpp +++ b/core/relinker/relinker/src/analysis/UnusedNidFilter/GotAccessIndex.cpp @@ -2,6 +2,7 @@ #include #include #include +#include namespace Relinker::UnusedNidFilter { @@ -23,11 +24,14 @@ std::unique_ptr BuildGotAccessIndex( const std::vector& text, VirtualAddress textVaddr ) { + if (text.size() > std::numeric_limits::max() - textVaddr) { + throw RelinkerException("Text section range exceeds the address space"); + } const Codegen::X64InstructionDecoder decoder; std::unordered_set accessed; for (VirtualAddress va : cfg.ReachableVaddrs()) { - if (va < textVaddr || va >= textVaddr + static_cast(text.size())) + if (va < textVaddr || va - textVaddr >= text.size()) continue; std::size_t bufOff = static_cast(va - textVaddr); std::size_t available = text.size() - bufOff; @@ -39,9 +43,7 @@ std::unique_ptr BuildGotAccessIndex( VirtualAddress nextVaddr = va + static_cast(info.Length); std::int32_t disp = 0; std::memcpy(&disp, text.data() + bufOff + info.RipRelativeDispOffset, 4); - VirtualAddress target = static_cast( - static_cast(nextVaddr) + disp - ); + VirtualAddress target = nextVaddr + static_cast(static_cast(disp)); accessed.insert(target); } diff --git a/core/relinker/relinker/tests/ControlFlowGraphTests.cpp b/core/relinker/relinker/tests/ControlFlowGraphTests.cpp index 5870e3c5e7..aa724babbe 100644 --- a/core/relinker/relinker/tests/ControlFlowGraphTests.cpp +++ b/core/relinker/relinker/tests/ControlFlowGraphTests.cpp @@ -6,11 +6,13 @@ #include #include #include +#include namespace { using namespace Relinker::UnusedNidFilter; using Relinker::VirtualAddress; +using Relinker::RelinkerException; using Bytes = std::vector; constexpr VirtualAddress TextAddress = 0xec00001000; @@ -151,6 +153,29 @@ void CheckBorrowedReachability() { RequireOffsets(*first, {0, 1, 3, 4}); } +void CheckAddressOverflow() { + const Pointers pointers; + const Bytes text{0x90, 0xc3}; + bool threw = false; + try { + BuildControlFlowGraph(text, std::numeric_limits::max(), std::numeric_limits::max(), {}, pointers); + } catch (const RelinkerException&) { + threw = true; + } + Require(threw, "Text section address space overflow was not rejected"); +} + +void CheckHighVirtualAddress() { + constexpr VirtualAddress highVaddr = 0xf00000001000; + const Pointers pointers; + Bytes text{0xeb, 0x02, 0x90, 0x90, 0xc3}; + const auto graph = BuildControlFlowGraph(text, highVaddr, highVaddr, {}, pointers); + std::unordered_set expected{highVaddr, highVaddr + 4}; + Require(graph->ReachableVaddrs() == expected, "Unexpected reachable instruction set at high virtual address"); + Require(graph->IsReachable(highVaddr), "Missing high entry address"); + Require(graph->IsReachable(highVaddr + 4), "Missing high branch target address"); +} + } int main() { @@ -162,6 +187,8 @@ int main() { CheckCodeAddresses(); CheckSpeculativeRoots(); CheckBorrowedReachability(); + CheckAddressOverflow(); + CheckHighVirtualAddress(); std::cout << "Control-flow graph tests passed\n"; return 0; } catch (const std::exception& error) { From d41a3e65f63788f3227d97a968d1309870baa704 Mon Sep 17 00:00:00 2001 From: fogydes Date: Tue, 13 Oct 2026 21:20:18 +0530 Subject: [PATCH 0018/1000] test(relinker): suppress Windows error dialog in occupied base collision check --- core/relinker/relinker/tests/test_windows_fixed_base.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/relinker/relinker/tests/test_windows_fixed_base.py b/core/relinker/relinker/tests/test_windows_fixed_base.py index 85a0429cea..3d04fb54f9 100644 --- a/core/relinker/relinker/tests/test_windows_fixed_base.py +++ b/core/relinker/relinker/tests/test_windows_fixed_base.py @@ -38,6 +38,8 @@ def run(output, expected=42): def check_occupied_base(output): if os.name != "nt": return + import ctypes + ctypes.windll.kernel32.SetErrorMode(0x8003) try: result = subprocess.run([str(output)], capture_output=True, timeout=5) except OSError as error: From 8b400afc5d5635e5af803c10b5725d7f4405ede7 Mon Sep 17 00:00:00 2001 From: Leandro Soares Date: Wed, 14 Oct 2026 00:07:13 -0300 Subject: [PATCH 0019/1000] fix(relinker): move hazard scalar compares out of line for Rosetta --- core/relinker/CMakeLists.txt | 3 + .../x86/Amd64OnlySubstitutionTable.hpp | 1 + .../codegen/x86/ScalarCompareOperands.hpp | 19 ++++++ .../src/x86/Amd64OnlyInstructionMatcher.cpp | 15 +++++ .../codegen/src/x86/ScalarCompareOperands.cpp | 63 +++++++++++++++++++ .../codegen/tests/Amd64OnlyConverterTests.cpp | 57 +++++++++++++++++ .../relinker/src/pipeline/Conversion.cpp | 2 +- 7 files changed, 159 insertions(+), 1 deletion(-) create mode 100644 core/relinker/codegen/include/codegen/x86/ScalarCompareOperands.hpp create mode 100644 core/relinker/codegen/src/x86/ScalarCompareOperands.cpp diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index 2a01d4bee2..5eddd14cc2 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -50,6 +50,7 @@ add_library(relinker_core STATIC ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/ReciprocalLowering.cpp ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/WideStoreOperands.cpp ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/WideStoreLowering.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/ScalarCompareOperands.cpp ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/parsing/ElfReader.cpp ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/analysis/SyscallScanner.cpp ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/analysis/CallSiteResolver.cpp @@ -167,6 +168,7 @@ if(WIN32) ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/ReciprocalLowering.cpp ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/WideStoreOperands.cpp ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/WideStoreLowering.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/ScalarCompareOperands.cpp ) target_include_directories(amd64_only_windows_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/include ${CMAKE_CURRENT_SOURCE_DIR}/domain/include ${CMAKE_CURRENT_SOURCE_DIR}/io/include ${CMAKE_CURRENT_SOURCE_DIR}/codegen/include ${CMAKE_CURRENT_SOURCE_DIR}/../libs/prx/libc/src/specifics/windows) if(BUILD_TESTING) @@ -345,6 +347,7 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/ReciprocalLowering.cpp ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/WideStoreOperands.cpp ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/WideStoreLowering.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/ScalarCompareOperands.cpp ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/src/linux/LinuxElfPatcher.cpp ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/src/general/SegmentFilter.cpp ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/src/general/EntryStubBuilder.cpp diff --git a/core/relinker/codegen/include/codegen/x86/Amd64OnlySubstitutionTable.hpp b/core/relinker/codegen/include/codegen/x86/Amd64OnlySubstitutionTable.hpp index 7f09e114ad..41948aec85 100644 --- a/core/relinker/codegen/include/codegen/x86/Amd64OnlySubstitutionTable.hpp +++ b/core/relinker/codegen/include/codegen/x86/Amd64OnlySubstitutionTable.hpp @@ -42,6 +42,7 @@ inline constexpr Entry kRdseed = {"RDSEED", nullptr, 0}; inline constexpr Entry kRdpid = {"RDPID", nullptr, 0}; inline constexpr Entry kClwb = {"CLWB", nullptr, 0}; inline constexpr Entry kWideStore = {"256-bit store", nullptr, 0}; +inline constexpr Entry kScalarCompare = {"scalar compare", nullptr, 0}; inline constexpr Entry kExtrq = {"EXTRQ", nullptr, 0}; inline constexpr Entry kInsertq = {"INSERTQ", nullptr, 0}; inline constexpr Entry kExtrqRegisterForm = {"EXTRQ register form", nullptr, 0}; diff --git a/core/relinker/codegen/include/codegen/x86/ScalarCompareOperands.hpp b/core/relinker/codegen/include/codegen/x86/ScalarCompareOperands.hpp new file mode 100644 index 0000000000..458f799ce4 --- /dev/null +++ b/core/relinker/codegen/include/codegen/x86/ScalarCompareOperands.hpp @@ -0,0 +1,19 @@ +#ifndef CODEGEN_X86_SCALARCOMPAREOPERANDS_HPP +#define CODEGEN_X86_SCALARCOMPAREOPERANDS_HPP + +#include +#include +#include + +namespace Codegen { + +struct ScalarCompareOperands { + std::optional RipDisplacementOffset; + std::int64_t RipTarget = 0; +}; + +[[nodiscard]] std::optional DecodeScalarCompare(const std::uint8_t* data, std::size_t length); + +} + +#endif diff --git a/core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp b/core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp index edf59c514c..112fed6de7 100644 --- a/core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp +++ b/core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -106,6 +107,14 @@ Amd64OnlyMatch _inPlace(const Entry& entry, const std::size_t length, std::vecto return Amd64OnlyMatch{entry.Name, length, Amd64OnlyLowering::InPlace, std::move(replacement), {}, 0}; } +void _emitScalarCompare(StubBodyBuilder& body, const DecodedInstruction& instr, const ScalarCompareOperands& operands) { + const std::span bytes(instr.Data, instr.Length); + if (operands.RipDisplacementOffset) + body.RipRelative(bytes, *operands.RipDisplacementOffset, operands.RipTarget); + else + body.Raw(bytes); +} + bool _isClzeroOpcode(const DecodedInstruction& instr) { const auto pos = instr.OpcodeOffset(); return pos + 2 < instr.Length && instr.Data[pos] == X64OpcodeConstants::TwoByteOpcodeEscape && instr.Data[pos + 1] == X64OpcodeConstants::TwoByteGrp7 && instr.Data[pos + 2] == 0xFC; @@ -219,6 +228,8 @@ std::optional Amd64OnlyInstructionMatcher::_matchRosetta(const D return _trampoline(kRdpid.Name, instr.Length, _outOfLine(instr.Length, trailing, [&](StubBodyBuilder& body) { body.Raw(_rdpidReplacement(instr)); }), true); if (const auto wide = DecodeVexWideStore(instr.Data, instr.Length)) return _trampoline(kWideStore.Name, instr.Length, _outOfLine(instr.Length, trailing, [&](StubBodyBuilder& body) { _wideStoreLowering.EmitOutOfLine(body, *wide); }), false, true); + if (const auto compare = DecodeScalarCompare(instr.Data, instr.Length)) + return _trampoline(kScalarCompare.Name, instr.Length, _outOfLine(instr.Length, trailing, [&](StubBodyBuilder& body) { _emitScalarCompare(body, instr, *compare); }), true); return std::nullopt; } @@ -254,6 +265,10 @@ const char* Amd64OnlyInstructionMatcher::_emitOutOfLine(StubBodyBuilder& body, c _wideStoreLowering.EmitOutOfLine(body, *wide); return kWideStore.Name; } + if (const auto compare = _target == Amd64OnlyTarget::Rosetta ? DecodeScalarCompare(instr.Data, instr.Length) : std::nullopt) { + _emitScalarCompare(body, instr, *compare); + return kScalarCompare.Name; + } return nullptr; } diff --git a/core/relinker/codegen/src/x86/ScalarCompareOperands.cpp b/core/relinker/codegen/src/x86/ScalarCompareOperands.cpp new file mode 100644 index 0000000000..c0c31a92e6 --- /dev/null +++ b/core/relinker/codegen/src/x86/ScalarCompareOperands.cpp @@ -0,0 +1,63 @@ +#include +#include + +namespace Codegen { + +namespace { + +constexpr std::uint8_t kVexMap0F = 0x01; +constexpr std::uint8_t kPpF3 = 0x02; +constexpr std::uint8_t kCompareOpcode = 0xC2; +constexpr std::uint8_t kPredicateLeOs = 0x02; + +bool _segmentPrefix(const std::uint8_t prefix) { + using namespace X64OpcodeConstants; + return prefix == PrefixSegCs || prefix == PrefixSegSs || prefix == PrefixSegDs || prefix == PrefixSegEs || prefix == PrefixSegFs || prefix == PrefixSegGs; +} + +} + +std::optional DecodeScalarCompare(const std::uint8_t* data, const std::size_t length) { + using namespace X64OpcodeConstants; + std::size_t pos = 0; + while (pos < length && _segmentPrefix(data[pos])) + ++pos; + std::size_t modRmOffset = 0; + if (pos < length && data[pos] == OneByteVex2) { + if (pos + 4 > length || (data[pos + 1] & 0x03) < kPpF3 || data[pos + 2] != kCompareOpcode) + return std::nullopt; + modRmOffset = pos + 3; + } else if (pos < length && data[pos] == OneByteVex3) { + if (pos + 5 > length || (data[pos + 1] & Vex3MapMask) != kVexMap0F || (data[pos + 2] & 0x03) < kPpF3 || data[pos + 3] != kCompareOpcode) + return std::nullopt; + modRmOffset = pos + 4; + } else { + return std::nullopt; + } + const auto modrm = data[modRmOffset]; + const auto mod = static_cast((modrm >> ModRmModShift) & ModRmModMask); + const auto rm = static_cast(modrm & ModRmRmMask); + auto cursor = modRmOffset + 1; + std::optional sib; + if (mod != ModRmModRegister && rm == ModRmRmSibPresent) { + if (cursor >= length) + return std::nullopt; + sib = data[cursor++]; + } + const bool ripRelative = mod == ModRmModIndirect && rm == ModRmRmRipRelative; + const bool absolute = mod == ModRmModIndirect && sib.has_value() && (*sib & SibBaseMask) == SibBaseDisp32; + const std::size_t displacementSize = mod == ModRmModDisp8 ? Disp8Size : (mod == ModRmModDisp32 || ripRelative || absolute) ? Disp32Size : 0; + if (cursor + displacementSize + ImmSize8 != length || data[length - 1] <= kPredicateLeOs) + return std::nullopt; + ScalarCompareOperands operands; + if (ripRelative) { + std::uint32_t displacement = 0; + for (std::size_t index = 0; index < Disp32Size; ++index) + displacement |= static_cast(data[cursor + index]) << (index * 8); + operands.RipDisplacementOffset = cursor; + operands.RipTarget = static_cast(length) + static_cast(displacement); + } + return operands; +} + +} diff --git a/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp b/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp index 8c4e4e0090..dcb82e697d 100644 --- a/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp +++ b/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp @@ -952,6 +952,61 @@ void converterRosetta() { require(intel.Reports.empty() && intel.Bytes == file, "The Intel target converted RDSEED, RDPID or CLWB"); } +void rosettaScalarCompares() { + const auto rosetta = Codegen::MakeAmd64OnlyInstructionMatcher(Codegen::Amd64OnlyTarget::Rosetta); + const auto match = [&](const Bytes& bytes) { return rosetta->Match(bytes.data(), bytes.size()); }; + const Bytes compare = {0xC5, 0xEA, 0xC2, 0xCA, 0x03}; + const auto lowered = match(compare); + require(lowered && lowered->Lowering == Codegen::Amd64OnlyLowering::Trampoline && lowered->Optional && !lowered->RelocatableWindow && lowered->InstructionName == "scalar compare", "A Rosetta-hazard scalar compare was not lowered through an optional stub"); + require(lowered->ReturnBranchOffset == 5 && lowered->StubBody.size() == 10 && Bytes(lowered->StubBody.begin(), lowered->StubBody.begin() + 5) == compare && lowered->StubBody[5] == 0xE9 && lowered->Relocations.empty(), "The scalar compare stub does not run the compare unchanged before its return jump"); + const auto unaligned = match(Bytes{0xC5, 0xEA, 0xC2, 0x47, 0x68, 0x03}); + require(unaligned && Bytes(unaligned->StubBody.begin(), unaligned->StubBody.begin() + 6) == Bytes{0xC5, 0xEA, 0xC2, 0x47, 0x68, 0x03} && unaligned->Relocations.empty(), "A memory-form scalar compare does not move unchanged"); + const Bytes rip = {0xC5, 0xEA, 0xC2, 0x0D, 0x00, 0x01, 0x00, 0x00, 0x03}; + const auto ripLowered = match(rip); + require(ripLowered && ripLowered->Relocations.size() == 1 && ripLowered->Relocations[0].DisplacementOffset == 4 && ripLowered->Relocations[0].InstructionEnd == 9 && ripLowered->Relocations[0].SiteTarget == 9 + 0x100, "A RIP-relative scalar compare was not relocated from its place in the site"); + require(Bytes(ripLowered->StubBody.begin() + 4, ripLowered->StubBody.begin() + 8) == Bytes(4, 0), "The RIP-relative scalar compare keeps its displacement in the stub"); + const auto vex3 = match(Bytes{0xC4, 0xE1, 0xEB, 0xC2, 0xCB, 0x03}); + require(vex3 && Bytes(vex3->StubBody.begin(), vex3->StubBody.begin() + 6) == Bytes{0xC4, 0xE1, 0xEB, 0xC2, 0xCB, 0x03}, "A VEX3 VCMPSD with a hazard predicate does not move unchanged"); + for (const Bytes& kept : {Bytes{0xC5, 0xEA, 0xC2, 0xCA, 0x00}, Bytes{0xC5, 0xEA, 0xC2, 0xCA, 0x01}, Bytes{0xC5, 0xEA, 0xC2, 0xCA, 0x02}, Bytes{0xC4, 0xE1, 0xEB, 0xC2, 0xCB, 0x01}}) + require(!match(kept).has_value(), "An EQ_OQ, LT_OS or LE_OS scalar compare must stay inline"); + for (const Bytes& other : {Bytes{0xC5, 0xE9, 0xC2, 0xCA, 0x03}, Bytes{0xC5, 0xE8, 0xC2, 0xCA, 0x03}, Bytes{0xC5, 0xEA, 0x2E, 0xC8}, Bytes{0xC5, 0xEA, 0xC2, 0xCA}, Bytes{0xC4, 0xE2, 0xEB, 0xC2, 0xCB, 0x03}, Bytes{0x62, 0xD1, 0x76, 0x08, 0xC2, 0xCB, 0x03}, Bytes{0xF3, 0x0F, 0xC2, 0xCA, 0x03}}) + require(!match(other).has_value(), "An instruction other than a Rosetta-hazard scalar compare was moved"); + require(!Codegen::MakeAmd64OnlyInstructionMatcher()->Match(compare.data(), compare.size()).has_value(), "A scalar compare was moved for an Intel host"); + const std::vector> pair = {std::span(compare), std::span(rip)}; + const auto sequence = rosetta->MatchSequence(pair, {}); + require(sequence && sequence->InstructionName == "scalar compare" && sequence->ReturnBranchOffset == 14 && sequence->Relocations.size() == 1 && sequence->Relocations[0].SiteTarget == 5 + 9 + 0x100, "A scalar compare after another is not emitted out of line in one stub"); +} + +void converterRosettaScalarCompare() { + const auto converter = Codegen::MakeAmd64OnlyConverter(Codegen::Amd64OnlyTarget::Rosetta); + Bytes file(0x300, 0xCC); + const Bytes text = { + 0xC5, 0xFA, 0x10, 0x15, 0x10, 0x00, 0x00, 0x00, + 0xC5, 0xF8, 0x28, 0xC2, + 0xC5, 0xEA, 0xC2, 0xCA, 0x03, + 0xC5, 0xF8, 0x28, 0x0D, 0x20, 0x00, 0x00, 0x00, + 0xC5, 0xF8, 0x2E, 0xC8, + 0xC3}; + std::copy(text.begin(), text.end(), file.begin() + 0x200); + const auto result = converter->Convert(file, {segmentHeader(text.size())}); + require(result.ReplacedCount == 0 && result.KeptCount == 0 && result.Trampolines.size() == 1 && result.Reports.size() == 1 && result.Reports[0].InstructionName == "scalar compare" && result.Reports[0].Offset == 0x20C && result.Reports[0].Lowering == Codegen::Amd64OnlyLowering::Trampoline, "The Rosetta-hazard compare in the fmax shape was not the only moved instruction"); + const auto& site = result.Trampolines[0]; + require(site.Offset == 0x20C && site.Length == 5 && site.ReturnBranchOffset == 5 && Bytes(site.Body.begin(), site.Body.begin() + 5) == Bytes(text.begin() + 12, text.begin() + 17) && Bytes(site.Body.begin() + 5, site.Body.begin() + 10) == Bytes{0xE9, 0, 0, 0, 0} && site.Relocations.empty(), "The compare stub does not run the compare unchanged and return"); + require(result.Bytes == file, "Moving the compare changed the file image"); + auto branchInside = file; + const Bytes jumped = {0xEB, 0x02, 0xC5, 0xEA, 0xC2, 0xCA, 0x03, 0xC3}; + std::copy(jumped.begin(), jumped.end(), branchInside.begin() + 0x200); + const auto kept = converter->Convert(branchInside, {segmentHeader(jumped.size())}); + require(kept.Trampolines.empty() && kept.KeptCount == 1 && kept.Reports.size() == 1 && kept.Reports[0].Lowering == Codegen::Amd64OnlyLowering::Kept && kept.Bytes == branchInside, "A branch into the compare did not keep it native"); + auto ripSource = file; + const Bytes ripText = {0xC5, 0xEA, 0xC2, 0x0D, 0x00, 0x01, 0x00, 0x00, 0x03, 0xC3}; + std::copy(ripText.begin(), ripText.end(), ripSource.begin() + 0x200); + const auto ripMoved = converter->Convert(ripSource, {segmentHeader(ripText.size())}); + require(ripMoved.Trampolines.size() == 1 && ripMoved.Trampolines[0].Relocations.size() == 1 && ripMoved.Trampolines[0].Relocations[0].SiteTarget == 9 + 0x100, "A RIP-relative compare source was not relocated"); + const auto intel = Codegen::MakeAmd64OnlyConverter()->Convert(file, {segmentHeader(text.size())}); + require(intel.Reports.empty() && intel.Bytes == file, "The Intel target moved a scalar compare"); +} + void reciprocalOperands() { const auto vex2 = Codegen::DecodeReciprocal(Bytes{0xC5, 0xF8, 0x52, 0xD5}.data(), 4); require(vex2 && vex2->Operation == Codegen::ReciprocalOperation::ReciprocalSquareRoot && vex2->Destination == 2 && vex2->Source == 5, "VEX2 VRSQRTPS was not decoded"); @@ -1672,6 +1727,8 @@ int main() { converterClzero(); rosettaSubstitutions(); converterRosetta(); + rosettaScalarCompares(); + converterRosettaScalarCompare(); wideStoreSplit(); converterWideStoreWindows(); converterJumpTableTargets(); diff --git a/core/relinker/relinker/src/pipeline/Conversion.cpp b/core/relinker/relinker/src/pipeline/Conversion.cpp index 798d086efb..28923e1e2e 100644 --- a/core/relinker/relinker/src/pipeline/Conversion.cpp +++ b/core/relinker/relinker/src/pipeline/Conversion.cpp @@ -55,7 +55,7 @@ ConversionOutput Conversion::Convert(ConversionInput input, const ConversionOpti sourceBytes = std::move(converted.Bytes); trampolines = std::move(converted.Trampolines); std::map stubsByName; - const auto counted = [](const std::string& name) { return name == "VRSQRTPS" || name == "VRCPPS" || name == "256-bit store"; }; + const auto counted = [](const std::string& name) { return name == "VRSQRTPS" || name == "VRCPPS" || name == "256-bit store" || name == "scalar compare"; }; for (const auto& report : converted.Reports) { if (report.Lowering == Codegen::Amd64OnlyLowering::Kept) { std::ostringstream message; message << "Intel substitution: " << report.InstructionName << " at 0x" << std::hex << report.Offset << std::dec << " (" << report.OriginalLength << " bytes) kept: no room for a jump\n"; inputProgress.Message(message.str()); } From 292752ff5a73e57ff61a6474efbeb85f8228073b Mon Sep 17 00:00:00 2001 From: babyhuey <7751607+babyhuey@users.noreply.github.com> Date: Tue, 13 Oct 2026 21:35:56 -0400 Subject: [PATCH 0020/1000] fix(libc): fail stream calls on a stream that a failed freopen closed --- core/libs/prx/libc/CMakeLists.txt | 3 +- core/libs/prx/libc/include/FileStream.hpp | 7 +++ core/libs/prx/libc/src/FileAndHeap.cpp | 11 +++++ core/libs/prx/libc/src/Formatting.cpp | 8 ++++ core/libs/prx/libc/src/StandardStreams.cpp | 8 ++++ core/libs/tests/FileStreams.cpp | 2 +- core/libs/tests/GuestStandardStreams.cpp | 54 +++++++++++++++++++++- 7 files changed, 90 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libc/CMakeLists.txt b/core/libs/prx/libc/CMakeLists.txt index 62f3b7a43b..953fa7e0c5 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -390,8 +390,9 @@ if(BUILD_TESTING) target_link_libraries(guest_standard_stream_tests PRIVATE libc) configure_windows_unwind(guest_standard_stream_tests) add_test(NAME guest_standard_streams COMMAND guest_standard_stream_tests) + add_test(NAME guest_standard_streams_closed_stdout COMMAND guest_standard_stream_tests --closed-stdout) if(WIN32) - set_tests_properties(guest_standard_streams PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(guest_standard_streams guest_standard_streams_closed_stdout PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() add_test_executable(file_streams_tests ${LIBS_INCLUDE_DIR}/tests/FileStreams.cpp) diff --git a/core/libs/prx/libc/include/FileStream.hpp b/core/libs/prx/libc/include/FileStream.hpp index 4ad9fa00b8..0f51bc7d6d 100644 --- a/core/libs/prx/libc/include/FileStream.hpp +++ b/core/libs/prx/libc/include/FileStream.hpp @@ -71,6 +71,10 @@ public: return _handle; } + bool IsOpen() const { + return _handle != nullptr; + } + bool IsDynamic() const { return _dynamic; } @@ -103,6 +107,7 @@ public: return true; } void SyncStatus() { + if (!_handle) return; _guest.readRemaining = 0; _guest.writeRemaining = 0; _guest.flags = static_cast((_guest.flags & ~0x60) | @@ -110,6 +115,7 @@ public: } void SyncUnlockedStatus() { + if (!_handle) return; _guest.readRemaining = 0; _guest.writeRemaining = 0; #ifdef _WIN32 @@ -129,6 +135,7 @@ public: } void ClearError() { + if (!_handle) return; std::clearerr(GetHandle()); guestError = false; SyncStatus(); diff --git a/core/libs/prx/libc/src/FileAndHeap.cpp b/core/libs/prx/libc/src/FileAndHeap.cpp index 69e7f8d0fe..16025ecbb9 100644 --- a/core/libs/prx/libc/src/FileAndHeap.cpp +++ b/core/libs/prx/libc/src/FileAndHeap.cpp @@ -324,6 +324,11 @@ int APS5_VABI fopen_s_nid_postfix(FileStream** result, const char* filename, con } int APS5_VABI fclose_nid_postfix(FileStream* stream) { + if (stream && !stream->IsOpen()) { + std::unique_ptr owner(stream->IsDynamic() ? stream : nullptr); + errno = 9; + return -1; + } GetNativeStream(stream); std::unique_ptr owner(stream->IsDynamic() ? stream : nullptr); return stream->Close(); @@ -357,6 +362,7 @@ FileStream* APS5_VABI _ZSt7_FiopenPKcNSt5_IosbIiE9_OpenmodeEi_nid_postfix(const } size_t APS5_VABI fread_nid_postfix(void* buffer, size_t size, size_t count, FileStream* stream) { + if (stream && !stream->IsOpen()) { errno = 9; return 0; } auto* handle = GetNativeStream(stream); if (size == 0 || count == 0) return 0; if (!buffer) throw std::runtime_error("fread: null buffer"); @@ -371,6 +377,7 @@ size_t APS5_VABI fread_nid_postfix(void* buffer, size_t size, size_t count, File } size_t APS5_VABI fwrite_nid_postfix(const void* buffer, size_t size, size_t count, FileStream* stream) { + if (stream && !stream->IsOpen()) { errno = 9; return 0; } auto* handle = GetNativeStream(stream); if (size == 0 || count == 0) return 0; if (!buffer) throw std::runtime_error("fwrite: null buffer"); @@ -380,6 +387,7 @@ size_t APS5_VABI fwrite_nid_postfix(const void* buffer, size_t size, size_t coun } int APS5_VABI fseeko_nid_postfix(FileStream* stream, std::int64_t offset, int origin) { + if (stream && !stream->IsOpen()) { errno = 9; return -1; } if (origin != SEEK_SET && origin != SEEK_CUR && origin != SEEK_END) { errno = 22; return -1; } #ifdef _WIN32 auto* native = GetNativeStream(stream); @@ -396,6 +404,7 @@ int APS5_VABI fseeko_nid_postfix(FileStream* stream, std::int64_t offset, int or } std::int64_t APS5_VABI ftello_nid_postfix(FileStream* stream) { + if (stream && !stream->IsOpen()) { errno = 9; return -1; } #ifdef _WIN32 auto* native = GetNativeStream(stream); if (IsPipeStream(native)) { errno = 29; return -1; } @@ -435,12 +444,14 @@ void APS5_VABI rewind_nid_postfix(FileStream* stream) { int APS5_VABI fputs_nid_postfix(const char* str, FileStream* stream) { if (!str) throw std::runtime_error("fputs: null string"); + if (stream && !stream->IsOpen()) { errno = 9; return -1; } const int result = std::fputs(str, GetNativeStream(stream)); stream->SyncStatus(); return result; } int APS5_VABI fflush_nid_postfix(FileStream* stream) { + if (stream && !stream->IsOpen()) { errno = 9; return -1; } std::FILE* native = stream ? GetNativeStream(stream) : nullptr; #ifdef __linux__ if (native && !__fwriting(native)) return 0; diff --git a/core/libs/prx/libc/src/Formatting.cpp b/core/libs/prx/libc/src/Formatting.cpp index bc08460a33..cb1d0d5798 100644 --- a/core/libs/prx/libc/src/Formatting.cpp +++ b/core/libs/prx/libc/src/Formatting.cpp @@ -239,6 +239,7 @@ int APS5_VABI vasprintf_nid_postfix(char** destination, const char* format, VaLi } int APS5_VABI vfprintf_nid_postfix(FileStream* stream, const char* format, VaList* args) { + if (stream && !stream->IsOpen()) { errno = 9; return -1; } auto* native = GetNativeStream(stream); #ifdef _WIN32 std::string buffer; @@ -280,6 +281,7 @@ int APS5_VABI fprintf_nid_postfix(FileStream* stream, const char* format, ...) { } int APS5_VABI fscanf_nid_postfix(FileStream* stream, const char* format, ...) { + if (stream && !stream->IsOpen()) { errno = 9; return -1; } auto* native = GetNativeStream(stream); #ifdef _WIN32 __builtin_sysv_va_list args; @@ -300,6 +302,7 @@ int APS5_VABI fscanf_nid_postfix(FileStream* stream, const char* format, ...) { #ifdef _WIN32 int APS5_VABI printf_nid_postfix(const char* format, ...) { + if (!_Stdout_nid_postfix.IsOpen()) { errno = 9; return -1; } __builtin_sysv_va_list args; __builtin_sysv_va_start(args, format); const int result = LibcDetail::PrintWindows(format, args); @@ -308,6 +311,7 @@ int APS5_VABI printf_nid_postfix(const char* format, ...) { } int APS5_VABI libc_printf_nid_postfix(const char* format, ...) { + if (!_Stdout_nid_postfix.IsOpen()) { errno = 9; return -1; } __builtin_sysv_va_list args; __builtin_sysv_va_start(args, format); const int result = LibcDetail::PrintWindows(format, args); @@ -334,6 +338,7 @@ int APS5_VABI sprintf_nid_postfix(char* buffer, const char* format, ...) { #else int APS5_VABI printf_nid_postfix(const char* format, ...) { + if (!_Stdout_nid_postfix.IsOpen()) { errno = 9; return -1; } std::va_list args; va_start(args, format); const int result = HasGuestWideFormat(format) ? LibcDetail::PrintWindows(format, args) : std::vprintf(format, args); @@ -342,6 +347,7 @@ int APS5_VABI printf_nid_postfix(const char* format, ...) { } int APS5_VABI libc_printf_nid_postfix(const char* format, ...) { + if (!_Stdout_nid_postfix.IsOpen()) { errno = 9; return -1; } std::va_list args; va_start(args, format); const int result = HasGuestWideFormat(format) ? LibcDetail::PrintWindows(format, args) : std::vprintf(format, args); @@ -423,6 +429,7 @@ int APS5_VABI sscanf_s_nid_postfix(const char* buffer, const char* format, ...) #endif int APS5_VABI vprintf_nid_postfix(const char* str, VaList* c) { + if (!_Stdout_nid_postfix.IsOpen()) { errno = 9; return -1; } #ifdef _WIN32 return LibcDetail::PrintWindows(str, c); #else @@ -541,6 +548,7 @@ int APS5_VABI printf_s_nid_postfix(const char* format, ...) { #endif int APS5_VABI puts_nid_postfix(const char* s) { + if (!_Stdout_nid_postfix.IsOpen()) { errno = 9; return -1; } return std::puts(s); } diff --git a/core/libs/prx/libc/src/StandardStreams.cpp b/core/libs/prx/libc/src/StandardStreams.cpp index 69e24de20c..dc79864a59 100644 --- a/core/libs/prx/libc/src/StandardStreams.cpp +++ b/core/libs/prx/libc/src/StandardStreams.cpp @@ -15,6 +15,7 @@ FileStream* __stderrp_nid_postfix = &_Stderr_nid_postfix; int __isthreaded_nid_postfix = 1; int APS5_VABI fgetc_nid_postfix(FileStream* stream) { + if (stream && !stream->IsOpen()) { errno = 9; return -1; } const int result = std::fgetc(GetNativeStream(stream)); stream->SyncStatus(); return result; @@ -36,6 +37,7 @@ std::int32_t APS5_VABI ungetwc_nid_postfix(std::int32_t value, FileStream* strea } int APS5_VABI getc_nid_postfix(FileStream* stream) { return fgetc_nid_postfix(stream); } int APS5_VABI getc_unlocked_nid_postfix(FileStream* stream) { + if (stream && !stream->IsOpen()) { errno = 9; return -1; } #ifdef _WIN32 const int result = _fgetc_nolock(GetNativeStream(stream)); #else @@ -47,12 +49,14 @@ int APS5_VABI getc_unlocked_nid_postfix(FileStream* stream) { int APS5_VABI __srget_nid_postfix(FileStream* stream) { return fgetc_nid_postfix(stream); } int APS5_VABI getchar_nid_postfix() { return fgetc_nid_postfix(__stdinp_nid_postfix); } int APS5_VABI fputc_nid_postfix(int value, FileStream* stream) { + if (stream && !stream->IsOpen()) { errno = 9; return -1; } const int result = std::fputc(value, GetNativeStream(stream)); stream->SyncStatus(); return result; } int APS5_VABI putc_nid_postfix(int value, FileStream* stream) { return fputc_nid_postfix(value, stream); } int APS5_VABI putc_unlocked_nid_postfix(int value, FileStream* stream) { + if (stream && !stream->IsOpen()) { errno = 9; return -1; } #ifdef _WIN32 const int result = _fputc_nolock(value, GetNativeStream(stream)); #else @@ -64,11 +68,13 @@ int APS5_VABI putc_unlocked_nid_postfix(int value, FileStream* stream) { int APS5_VABI __swbuf_nid_postfix(int value, FileStream* stream) { return fputc_nid_postfix(value, stream); } int APS5_VABI putchar_nid_postfix(int value) { return fputc_nid_postfix(value, __stdoutp_nid_postfix); } int APS5_VABI ungetc_nid_postfix(int value, FileStream* stream) { + if (stream && !stream->IsOpen()) { errno = 9; return -1; } const int result = std::ungetc(value, GetNativeStream(stream)); stream->SyncStatus(); return result; } char* APS5_VABI fgets_nid_postfix(char* buffer, int size, FileStream* stream) { + if (stream && !stream->IsOpen()) { errno = 9; return nullptr; } if (size <= 0) { stream->SetError(); errno = 22; @@ -90,6 +96,7 @@ void APS5_VABI clearerr_nid_postfix(FileStream* stream) { stream->ClearError(); } int APS5_VABI fileno_nid_postfix(FileStream* stream) { + if (stream && !stream->IsOpen()) { errno = 9; return -1; } #ifdef _WIN32 const int descriptor = _fileno(GetNativeStream(stream)); #else @@ -99,6 +106,7 @@ int APS5_VABI fileno_nid_postfix(FileStream* stream) { return descriptor; } int APS5_VABI setvbuf_nid_postfix(FileStream* stream, char* buffer, int mode, std::size_t size) { + if (stream && !stream->IsOpen()) { errno = 9; return -1; } if (mode < 0 || mode > 2) return -1; const int native = mode == 0 ? _IOFBF : mode == 1 ? _IOLBF : _IONBF; if (native != _IONBF && size == 0) { diff --git a/core/libs/tests/FileStreams.cpp b/core/libs/tests/FileStreams.cpp index ff80543263..bb1361dd5c 100644 --- a/core/libs/tests/FileStreams.cpp +++ b/core/libs/tests/FileStreams.cpp @@ -88,7 +88,7 @@ int main(int argc, char** argv) { ExpectException([] { fopen_nid_postfix(nullptr, "r"); }); FileStream closed(std::tmpfile()); closed.Close(); - ExpectException([&] { fflush_nid_postfix(&closed); }); + Require(fflush_nid_postfix(&closed) == -1 && errno == 9); Require(std::remove(argv[1]) == 0); errno = 0; Require(fopen_nid_postfix(argv[1], "rb") == nullptr); diff --git a/core/libs/tests/GuestStandardStreams.cpp b/core/libs/tests/GuestStandardStreams.cpp index 83b97d5ecd..f1fc3f943e 100644 --- a/core/libs/tests/GuestStandardStreams.cpp +++ b/core/libs/tests/GuestStandardStreams.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -34,6 +35,10 @@ extern FileStream* __stdoutp_nid_postfix; extern FileStream* __stderrp_nid_postfix; extern int __isthreaded_nid_postfix; int APS5_VABI fprintf_nid_postfix(FileStream*, const char*, ...); +int APS5_VABI fputs_nid_postfix(const char*, FileStream*); +int APS5_VABI printf_nid_postfix(const char*, ...); +int APS5_VABI vprintf_nid_postfix(const char*, void*); +int APS5_VABI puts_nid_postfix(const char*); int APS5_VABI vfprintf_nid_postfix(FileStream*, const char*, void*); int APS5_VABI vsprintf_nid_postfix(char*, const char*, void*); int APS5_VABI fgetc_nid_postfix(FileStream*); @@ -80,6 +85,22 @@ static int APS5_VABI WriteFormatted(FileStream* stream, const char* format, ...) #endif return result; } +static int APS5_VABI PrintStandard(const char* format, ...) { +#ifdef _WIN32 + __builtin_sysv_va_list args; + __builtin_sysv_va_start(args, format); +#else + std::va_list args; + va_start(args, format); +#endif + const int result = vprintf_nid_postfix(format, args); +#ifdef _WIN32 + __builtin_sysv_va_end(args); +#else + va_end(args); +#endif + return result; +} static int APS5_VABI FormatString(char* buffer, const char* format, ...) { #ifdef _WIN32 __builtin_sysv_va_list args; @@ -493,7 +514,38 @@ static void CheckDescriptorAccessModes() { Require(std::filesystem::remove(filename)); } -int main() { +static void CheckClosedStreamWrites() { + FileStream stream(std::tmpfile()); + Require(freopen_nid_postfix("missing-closed-stream-dir/out.log", "w", &stream) == nullptr); + Require(fputs_nid_postfix("text", &stream) == -1 && *__error_nid_postfix() == 9); + Require(fprintf_nid_postfix(&stream, "%d", 1) == -1 && *__error_nid_postfix() == 9); + Require(fwrite_nid_postfix("text", 1, 4, &stream) == 0 && *__error_nid_postfix() == 9); + Require(fputc_nid_postfix('t', &stream) == -1 && *__error_nid_postfix() == 9); + Require(putc_unlocked_nid_postfix('t', &stream) == -1 && *__error_nid_postfix() == 9); + Require(fflush_nid_postfix(&stream) == -1 && *__error_nid_postfix() == 9); + char buffer[8] = {}; + Require(fread_nid_postfix(buffer, 1, sizeof(buffer), &stream) == 0 && *__error_nid_postfix() == 9); + Require(fgetc_nid_postfix(&stream) == -1 && *__error_nid_postfix() == 9); + Require(fgets_nid_postfix(buffer, sizeof(buffer), &stream) == nullptr && *__error_nid_postfix() == 9); + Require(fseeko_nid_postfix(&stream, 0, SEEK_SET) == -1 && *__error_nid_postfix() == 9); + Require(ftello_nid_postfix(&stream) == -1 && *__error_nid_postfix() == 9); + Require(fileno_nid_postfix(&stream) == -1 && *__error_nid_postfix() == 9); + Require(ferror_nid_postfix(&stream) == 0 && feof_nid_postfix(&stream) == 0); + Require(fclose_nid_postfix(&stream) == -1 && *__error_nid_postfix() == 9); +} + +static int CheckClosedStandardOutput() { + Require(freopen_nid_postfix("missing-closed-stream-dir/out.log", "w", __stdoutp_nid_postfix) == nullptr); + Require(printf_nid_postfix("%d", 1) == -1 && *__error_nid_postfix() == 9); + Require(puts_nid_postfix("text") == -1 && *__error_nid_postfix() == 9); + Require(PrintStandard("%d", 1) == -1 && *__error_nid_postfix() == 9); + Require(fputs_nid_postfix("text", __stdoutp_nid_postfix) == -1 && *__error_nid_postfix() == 9); + return 0; +} + +int main(int argc, char** argv) { + if (argc > 1 && std::string_view(argv[1]) == "--closed-stdout") return CheckClosedStandardOutput(); + CheckClosedStreamWrites(); CheckDescriptorAccessModes(); Require(_Getmbcurmax_nid_postfix() == 1 && _Getmbcurmax_nid_postfix() == ___mb_cur_max_nid_postfix()); CheckFlushWhileReading(); From a1c56b59e1510c2c2466ceb97aaa9096b57dffef Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Wed, 14 Oct 2026 08:12:03 +0200 Subject: [PATCH 0021/1000] fix(libSceVideoOut): return VIDEO_OUT_ERROR_INVALID_VALUE for a flip rate outside 0..2 Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libSceVideoOut/src/Output.cpp | 4 +--- core/libs/prx/libSceVideoOut/tests/Flip.cpp | 5 ++++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceVideoOut/src/Output.cpp b/core/libs/prx/libSceVideoOut/src/Output.cpp index 35c3228916..4a64220ffd 100644 --- a/core/libs/prx/libSceVideoOut/src/Output.cpp +++ b/core/libs/prx/libSceVideoOut/src/Output.cpp @@ -114,9 +114,7 @@ int APS5_VABI sceVideoOutSetFlipRate(int handle, int rate) try { if (cfg == nullptr) { throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_HANDLE"); } - if (rate < 0 || rate > 2) { - throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_VALUE"); - } + if (rate < 0 || rate > 2) return VIDEO_OUT_ERROR_INVALID_VALUE; std::lock_guard lock(cfg->mutex); cfg->Check(); cfg->flipRate = rate; diff --git a/core/libs/prx/libSceVideoOut/tests/Flip.cpp b/core/libs/prx/libSceVideoOut/tests/Flip.cpp index b90bbc768a..04437790db 100644 --- a/core/libs/prx/libSceVideoOut/tests/Flip.cpp +++ b/core/libs/prx/libSceVideoOut/tests/Flip.cpp @@ -315,7 +315,10 @@ void testControls() { for (int rate = 0; rate <= 2; ++rate) { check(sceVideoOutSetFlipRate(handle, rate) == 0 && cfg->flipRate == rate, "flip rate was not applied"); } - expectFailure([&] { sceVideoOutSetFlipRate(handle, 3); }); + for (const int rate : {3, 60, -1}) { + check(sceVideoOutSetFlipRate(handle, rate) == VIDEO_OUT_ERROR_INVALID_VALUE, "flip rate outside 0..2 was accepted"); + check(cfg->flipRate == 2, "a rejected flip rate changed the stored rate"); + } VideoOutColorSettings settings{}; expectFailure([&] { sceVideoOutColorSettingsSetGamma(&settings, std::numeric_limits::quiet_NaN()); }); KernelEqueue queue = 0; From 76e5edcacdd0d4767cb7f2a7fc41b6a07ba1510b Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Wed, 14 Oct 2026 08:31:29 +0200 Subject: [PATCH 0022/1000] docs(debt): note the unverified flip rate 60 of PPSA13356 Co-Authored-By: Claude Sonnet 5.5 --- docs/dev/debt/libSceVideoOut.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/dev/debt/libSceVideoOut.md b/docs/dev/debt/libSceVideoOut.md index ce195aa306..546c5f5d32 100644 --- a/docs/dev/debt/libSceVideoOut.md +++ b/docs/dev/debt/libSceVideoOut.md @@ -2,6 +2,7 @@ ### Silent stubs +- [sceVideoOutSetFlipRate](../../../core/libs/prx/libSceVideoOut/src/Output.cpp) stores a rate of 0, 1 or 2 and returns `VIDEO_OUT_ERROR_INVALID_VALUE` for any other rate. PPSA13356 calls it with 60; whether the console accepts that value or returns this error is unverified - [sceVideoOutOpen](../../../core/libs/prx/libSceVideoOut/src/Output.cpp) validates the priority and CPU affinity that the open param requests for the VideoOut service thread but does not apply them: the port's present and vblank threads are host threads, and guest priorities and affinities do not reach host scheduling - [sceVideoOutAddVrrStatusFlagsPrivilege](../../../core/libs/prx/libSceVideoOut/src/Output.cpp) returns 0 and does nothing: the host output has no VRR state to report. The name comes from an assert message in a title that requires `SCE_VIDEO_OUT_OK`; the NID hashes to it, the arguments are unknown (it is called without any) - [sceVideoOutVrrPegToFixedRate, sceVideoOutVrrUnpegFromFixedRate](../../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - PPSA05684 passes the handle to both, two further zero arguments to `sceVideoOutVrrPegToFixedRate`, and ignores the results. The meaning of those two arguments is unknown, so only zeros are accepted. VRR is not modelled and the output always runs at a fixed rate, so both calls only validate the handle and return 0 From c73e9720aecc224bdb0fbeb9007e043c61cd4e85 Mon Sep 17 00:00:00 2001 From: SandstromPL Date: Wed, 14 Oct 2026 12:08:08 +0530 Subject: [PATCH 0023/1000] fix(savedata): return a parameter error from sceSaveDataMount3 for an invalid directory name An unterminated or empty directory name, "." or "..", or a name containing "/", "\" or ":" now returns SAVE_DATA_ERROR_PARAMETER and leaves the mount result unchanged. The SDK 12.000 reference lists that code for an invalid parameter of sceSaveDataMount3, KytyPS5's SaveDataMount3 returns it for an invalid name, and sceSaveDataDelete and sceSaveDataConvert already return it here for the same check. It threw before, which stopped Minecraft Preview (PPSA19639) on entering a world. The Mount3 trace now prints at most the 32 bytes of the directory name and the 16 bytes of the mount point, since both can be unterminated when the mount fails. Co-Authored-By: Claude Opus 5.5 --- .../libs/prx/libSceSaveData.native/Export.cpp | 6 +-- core/libs/tests/GuestSaveDataMountStatus.cpp | 37 +++++++++++++++++++ docs/dev/debt/libSceSaveData.native.md | 1 + 3 files changed, 41 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceSaveData.native/Export.cpp b/core/libs/prx/libSceSaveData.native/Export.cpp index 00529793b7..4712333c0d 100644 --- a/core/libs/prx/libSceSaveData.native/Export.cpp +++ b/core/libs/prx/libSceSaveData.native/Export.cpp @@ -529,11 +529,11 @@ static int mount3(const SaveDataMount3* mount, SaveDataMountResult* mount_result } const auto* nameEnd = static_cast(std::memchr(mount->dir_name->data, '\0', sizeof(mount->dir_name->data))); if (nameEnd == nullptr) { - throw std::runtime_error("sceSaveDataMount3: unterminated directory name"); + return SAVE_DATA_ERROR_PARAMETER; } const std::string dirName(mount->dir_name->data, static_cast(nameEnd - mount->dir_name->data)); if (dirName.empty() || dirName == "." || dirName == ".." || dirName.find_first_of("/\\:") != std::string::npos) { - throw std::runtime_error("sceSaveDataMount3: invalid directory name"); + return SAVE_DATA_ERROR_PARAMETER; } const std::string real_path = save_root() + "/" + dirName; std::lock_guard lock(g_slots_mutex); @@ -576,7 +576,7 @@ static int mount3(const SaveDataMount3* mount, SaveDataMountResult* mount_result int APS5_VABI sceSaveDataMount3(const SaveDataMount3* mount, SaveDataMountResult* mount_result) { const int rc = mount3(mount, mount_result); - SAVEDATA_TRACE("mount3 user=%d dir=%s mode=0x%x blocks=%llu -> 0x%08x point=%s status=%u", mount != nullptr ? mount->user_id : -1, mount != nullptr && mount->dir_name != nullptr ? mount->dir_name->data : "(null)", mount != nullptr ? mount->mount_mode : 0u, mount != nullptr ? static_cast(mount->blocks) : 0ull, static_cast(rc), mount_result != nullptr ? mount_result->mount_point.data : "", mount_result != nullptr ? mount_result->mount_status : 0u); + SAVEDATA_TRACE("mount3 user=%d dir=%.*s mode=0x%x blocks=%llu -> 0x%08x point=%.*s status=%u", mount != nullptr ? mount->user_id : -1, static_cast(sizeof(SceSaveDataDirName::data)), mount != nullptr && mount->dir_name != nullptr ? mount->dir_name->data : "(null)", mount != nullptr ? mount->mount_mode : 0u, mount != nullptr ? static_cast(mount->blocks) : 0ull, static_cast(rc), static_cast(sizeof(SaveDataMountPoint::data)), mount_result != nullptr ? mount_result->mount_point.data : "", mount_result != nullptr ? mount_result->mount_status : 0u); return rc; } diff --git a/core/libs/tests/GuestSaveDataMountStatus.cpp b/core/libs/tests/GuestSaveDataMountStatus.cpp index 9249ab3838..abdbe40aef 100644 --- a/core/libs/tests/GuestSaveDataMountStatus.cpp +++ b/core/libs/tests/GuestSaveDataMountStatus.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include @@ -18,6 +19,7 @@ namespace { constexpr std::uint32_t MountReadWrite = 2; constexpr std::uint32_t MountCreate = 4; constexpr std::uint32_t MountCreate2 = 32; +constexpr int SaveDataErrorParameter = -2137063424; constexpr int SaveDataErrorBusy = -2137063421; constexpr int SaveDataErrorNotFound = -2137063416; @@ -63,6 +65,40 @@ void CheckFailedMountKeepsResult() { Check(sceSaveDataUmount2(0, &result.mount_point) == 0, "the kept mount point unmounts the save"); } +std::set Tree() { + std::set paths; + for (const auto& entry : std::filesystem::recursive_directory_iterator(std::filesystem::current_path())) paths.insert(entry.path()); + return paths; +} + +void CheckInvalidNameIsRejected(const char* data, std::size_t size, const std::string& label) { + for (const std::uint32_t mode : {MountReadWrite | MountCreate2, MountReadWrite | MountCreate, MountReadWrite}) { + SceSaveDataDirName dirName{}; + std::memcpy(dirName.data, data, size); + SaveDataMount3 mount{}; + mount.dir_name = &dirName; + mount.mount_mode = mode; + SaveDataMountResult result; + std::memset(&result, 0xCD, sizeof(result)); + const SaveDataMountResult before = result; + const std::string what = label + " with mount mode " + std::to_string(mode); + Check(sceSaveDataMount3(&mount, &result) == SaveDataErrorParameter, what + " is a parameter error"); + Check(std::memcmp(&result, &before, sizeof(result)) == 0, what + " leaves the result as it was"); + } +} + +void CheckInvalidNamesAreRejected() { + const auto before = Tree(); + for (const char* invalid : {"", ".", "..", "a/b", "a\\b", "c:d"}) { + CheckInvalidNameIsRejected(invalid, std::strlen(invalid) + 1, std::string("\"") + invalid + "\""); + } + char unterminated[sizeof(SceSaveDataDirName::data)]; + std::memset(unterminated, 'a', sizeof(unterminated)); + CheckInvalidNameIsRejected(unterminated, sizeof(unterminated), "an unterminated name"); + Check(Tree() == before, "rejected names create nothing"); + MountAndCheck("after", MountReadWrite | MountCreate2, 1u, "CREATE2 after the rejected names"); +} + } int main() { @@ -76,6 +112,7 @@ int main() { MountAndCheck("made", MountReadWrite | MountCreate, 1u, "CREATE of a missing save"); MountAndCheck("made", MountReadWrite, 0u, "open of an existing save"); CheckFailedMountKeepsResult(); + CheckInvalidNamesAreRejected(); std::filesystem::current_path(previous); std::error_code error; std::filesystem::remove_all(root, error); diff --git a/docs/dev/debt/libSceSaveData.native.md b/docs/dev/debt/libSceSaveData.native.md index 6759fd2a9e..fbdf136d12 100644 --- a/docs/dev/debt/libSceSaveData.native.md +++ b/docs/dev/debt/libSceSaveData.native.md @@ -15,4 +15,5 @@ - [sceSaveDataDirNameSearch, sceSaveDataDirNameSearch2](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - only the running title's saves are kept, so a search with another title's `title_id` reports zero hits, as [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/ec334904604ba41f5d4753f4a2498ec82540e5d6/src/core/libraries/save_data/savedata.cpp#L791-L802) and [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/569ec04d1a77127c601eed522ad5bba187173f9d/src/libs/libSaveData.cpp#L592-L596) do when that title has no save directory; what a console returns for another title's saves is not verified. The running title's ID is the `titleId` in its `param.json` - [sceSaveDataDirNameSearch2](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) runs the same search as `sceSaveDataDirNameSearch` with the same condition and result structures, as [SharpEmu](https://github.com/sharpemu/sharpemu/blob/main/src/SharpEmu.Libs/SaveData/SaveDataExports.cs) does, its only source; what distinguishes it on the console is unknown. - [sceSaveDataMount3](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - a failed mount (BUSY, EXISTS, NOT_FOUND, MOUNT_FULL) leaves the result as it was, as shadPS4 does; not checked on hardware. Animal Well (PPSA22520) depends on it: it checks for its save with a mount it never unmounts, then saves with a second mount of the same directory, ignores its BUSY and keeps using the first mount point for `sceSaveDataPrepare`, its file and `sceSaveDataUmount2` +- [sceSaveDataMount3](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - an unterminated or empty directory name, `.`, `..`, or a name containing `/`, `\` or `:` returns `SAVE_DATA_ERROR_PARAMETER`, which the [SDK 12.000 reference](https://github.com/peterpuppy/bm/blob/3f9d1ba6b896a53080460c903f5c15af8affb80d/features/ps5/document/output/psn_12/System/Save_Data/SaveData-Reference/sce-save-data-mount3.md#L196-L204) lists for an invalid parameter, and leaves the result as it was, as [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/569ec04d1a77127c601eed522ad5bba187173f9d/src/libs/libSaveData.cpp#L652-L658) does. The [SDK](https://github.com/peterpuppy/bm/blob/3f9d1ba6b896a53080460c903f5c15af8affb80d/features/ps5/document/output/psn_12/System/Save_Data/SaveData-Reference/sce-save-data-dir-name.md#L32-L36) lets an application use only letters, digits, `-`, `.` and `@` in a name, and KytyPS5 also accepts `_` ([`valid_path_component`](https://github.com/KytyPS5/KytyPS5/blob/569ec04d1a77127c601eed522ad5bba187173f9d/src/libs/libSaveData.cpp#L276-L287)); which other names the console's mount rejects is not checked on hardware From 187e5fc0ac213e6f2cdceb08749b1db4d71cbfee Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Tue, 13 Oct 2026 03:25:11 -0700 Subject: [PATCH 0024/1000] fix(libkernel): commit a reserved host stack before protecting it Guest threads run on host stacks that Windows reserves (0x210000 bytes by default) and commits on demand. Resident Evil Village protects one thread's whole host stack with 0xc2 at startup while its lowest 0xd000 bytes are still only reserved, so the Windows mprotect emulation threw. When such a stack sat inside the arena bounds as a host region, the call took the arena commit path and failed to split a placeholder that was never one. Commit the reserved pages of a host range before protecting it, and only use the arena commit for ranges the arena owns. --- .../libkernel/DirectMemory/DirectMemory.cpp | 20 +++++++++++- core/libs/tests/GuestKernelStackProtect.cpp | 31 +++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp b/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp index dc3a1bbcfd..b5a28af3a9 100644 --- a/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp +++ b/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp @@ -57,6 +57,7 @@ struct KernelArena { return arena; } bool Contains(const void* pointer, size_t len) const { return GuestArena::GuestArenaContains_nid_postfix(pointer, len); } + bool Owns(const void* pointer, size_t len) const { return Contains(pointer, len) && !GuestArena::GuestArenaHostRegionOverlaps_nid_postfix(reinterpret_cast(pointer), len); } void* Allocate(size_t len, size_t alignment) { return GuestArena::GuestArenaAllocate_nid_postfix(len, alignment); } void MarkUsed(const void* pointer, size_t len) { GuestArena::GuestArenaMarkUsed_nid_postfix(pointer, len); } void Release(const void* pointer, size_t len) { GuestArena::GuestArenaRelease_nid_postfix(pointer, len); } @@ -117,9 +118,23 @@ static int munmap_release(void* addr) { return 0; } +static void CommitReservedPages(void* addr, size_t len) { + auto cursor = reinterpret_cast(addr); + const auto end = cursor + len; + while (cursor < end) { + MEMORY_BASIC_INFORMATION memory{}; + if (VirtualQuery(reinterpret_cast(cursor), &memory, sizeof(memory)) != sizeof(memory)) + throw std::system_error(static_cast(GetLastError()), std::system_category(), "VirtualQuery failed"); + const auto regionEnd = std::min(end, reinterpret_cast(memory.BaseAddress) + memory.RegionSize); + if (memory.State == MEM_RESERVE && !VirtualAlloc(reinterpret_cast(cursor), regionEnd - cursor, MEM_COMMIT, PAGE_READWRITE)) + throw std::system_error(static_cast(GetLastError()), std::system_category(), "VirtualAlloc stack commit failed"); + cursor = regionEnd; + } +} + static int mprotect(void* addr, size_t len, int prot) { GuestArena::GuestArenaSetProtection_nid_postfix(reinterpret_cast(addr), len, WinProtFromPosix(prot)); - if (prot != PROT_NONE && KernelArena::Get().Contains(addr, len)) CommitArenaRange(addr, len, WinProtFromPosix(prot)); + if (prot != PROT_NONE && KernelArena::Get().Owns(addr, len)) CommitArenaRange(addr, len, WinProtFromPosix(prot)); auto cursor = reinterpret_cast(addr); const auto end = cursor + len; while (cursor < end) { @@ -1029,6 +1044,9 @@ int DoMprotect(const void* addr, size_t len, int prot) { if (inside >= insideEnd) return 0; auto* insidePointer = reinterpret_cast(inside); const auto insideBytes = static_cast(insideEnd - inside); +#ifdef _WIN32 + if (nativeProtection != PROT_NONE && !KernelArena::Get().Owns(insidePointer, insideBytes)) CommitReservedPages(insidePointer, insideBytes); +#endif if (mprotect(insidePointer, insideBytes, nativeProtection) != 0) throw std::system_error(errno, std::generic_category(), "mprotect failed"); NoteGuestStackProtection(inside, insideBytes, (prot & 3) == 3); RecordProtection(insidePointer, insideBytes, prot); diff --git a/core/libs/tests/GuestKernelStackProtect.cpp b/core/libs/tests/GuestKernelStackProtect.cpp index f946e321ae..8df5bb6381 100644 --- a/core/libs/tests/GuestKernelStackProtect.cpp +++ b/core/libs/tests/GuestKernelStackProtect.cpp @@ -154,7 +154,38 @@ static void* APS5_VABI WriteLeftGuard(void*) { return nullptr; } +#ifdef _WIN32 +static void ProtectWholeStack() { + int local = 0; + void* start = nullptr; + void* end = nullptr; + Require(sceKernelIsStack(&local, &start, &end) == SCE_OK); + const auto stackBytes = reinterpret_cast(end) - reinterpret_cast(start); + Require(mprotect_nid_postfix(start, stackBytes, 0xc2) == 0); + Require(QueryPage(reinterpret_cast(start)).writable); + static_cast(start)[0] = 6; +} + +static DWORD WINAPI ProtectReservedStack(void*) { + ProtectWholeStack(); + return 0; +} + +static void ProtectHostStacks() { + ProtectWholeStack(); + HANDLE thread = CreateThread(nullptr, HostBytes * 4, ProtectReservedStack, nullptr, STACK_SIZE_PARAM_IS_A_RESERVATION, nullptr); + Require(thread != nullptr); + Require(WaitForSingleObject(thread, INFINITE) == WAIT_OBJECT_0); + DWORD exitCode = 1; + Require(GetExitCodeThread(thread, &exitCode) != 0 && exitCode == 0); + Require(CloseHandle(thread) != 0); +} +#endif + int main() { +#ifdef _WIN32 + ProtectHostStacks(); +#endif std::atomic passed{false}; Pthread thread = nullptr; Require(scePthreadCreate(&thread, nullptr, Worker, &passed, nullptr) == SCE_OK); From a648b558847369c62226fbf9434cc4044d6c0490 Mon Sep 17 00:00:00 2001 From: Nithishvelan M Date: Wed, 14 Oct 2026 13:17:01 +0530 Subject: [PATCH 0025/1000] fix(libSceWebBrowserDialog): return error codes instead of throwing in initialize and terminate --- core/libs/prx/libSceWebBrowserDialog/Export.cpp | 13 +++++++------ core/libs/tests/GuestWebBrowserDialog.cpp | 5 +++++ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libSceWebBrowserDialog/Export.cpp b/core/libs/prx/libSceWebBrowserDialog/Export.cpp index 45939e99e8..cf61a32659 100644 --- a/core/libs/prx/libSceWebBrowserDialog/Export.cpp +++ b/core/libs/prx/libSceWebBrowserDialog/Export.cpp @@ -1,6 +1,5 @@ #include #include -#include #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" @@ -13,6 +12,7 @@ constexpr int COMMON_DIALOG_STATUS_RUNNING = 2; constexpr int COMMON_DIALOG_STATUS_FINISHED = 3; constexpr int COMMON_DIALOG_RESULT_USER_CANCELED = 1; constexpr int COMMON_DIALOG_ERROR_NOT_INITIALIZED = static_cast(0x80B80003u); +constexpr int COMMON_DIALOG_ERROR_ALREADY_INITIALIZED = static_cast(0x80B80004u); constexpr int COMMON_DIALOG_ERROR_NOT_FINISHED = static_cast(0x80B80005u); constexpr int COMMON_DIALOG_ERROR_BUSY = static_cast(0x80B80007u); constexpr int COMMON_DIALOG_ERROR_ARG_NULL = static_cast(0x80B8000Du); @@ -21,17 +21,18 @@ constexpr int COMMON_DIALOG_ERROR_ARG_NULL = static_cast(0x80B8000Du); extern "C" { int APS5_VABI sceWebBrowserDialogInitialize(void) { - int expected = 0; - if (!g_status.compare_exchange_strong(expected, 1)) throw std::logic_error("sceWebBrowserDialogInitialize: already initialized"); + int expected = COMMON_DIALOG_STATUS_NONE; + if (!g_status.compare_exchange_strong(expected, 1)) return COMMON_DIALOG_ERROR_ALREADY_INITIALIZED; return 0; } int APS5_VABI sceWebBrowserDialogTerminate(void) { int expected = 1; - if (g_status.compare_exchange_strong(expected, 0)) return 0; + if (g_status.compare_exchange_strong(expected, COMMON_DIALOG_STATUS_NONE)) return 0; expected = COMMON_DIALOG_STATUS_FINISHED; - if (!g_status.compare_exchange_strong(expected, 0)) throw std::logic_error("sceWebBrowserDialogTerminate: not initialized or still running"); - return 0; + if (g_status.compare_exchange_strong(expected, COMMON_DIALOG_STATUS_NONE)) return 0; + if (g_status.load() == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + return COMMON_DIALOG_ERROR_BUSY; } int APS5_VABI sceWebBrowserDialogClose(void) { diff --git a/core/libs/tests/GuestWebBrowserDialog.cpp b/core/libs/tests/GuestWebBrowserDialog.cpp index 9c5ef4b12b..b20ec63097 100644 --- a/core/libs/tests/GuestWebBrowserDialog.cpp +++ b/core/libs/tests/GuestWebBrowserDialog.cpp @@ -19,6 +19,7 @@ constexpr int COMMON_DIALOG_STATUS_INITIALIZED = 1; constexpr int COMMON_DIALOG_STATUS_FINISHED = 3; constexpr int COMMON_DIALOG_RESULT_USER_CANCELED = 1; constexpr int COMMON_DIALOG_ERROR_NOT_INITIALIZED = static_cast(0x80B80003u); +constexpr int COMMON_DIALOG_ERROR_ALREADY_INITIALIZED = static_cast(0x80B80004u); constexpr int COMMON_DIALOG_ERROR_NOT_FINISHED = static_cast(0x80B80005u); constexpr int COMMON_DIALOG_ERROR_ARG_NULL = static_cast(0x80B8000Du); @@ -30,6 +31,7 @@ int main() { std::uint8_t param[64] = {}; std::int32_t result = 0x5a5a5a5a; + Require(sceWebBrowserDialogTerminate() == COMMON_DIALOG_ERROR_NOT_INITIALIZED); Require(sceWebBrowserDialogGetStatus() == COMMON_DIALOG_STATUS_NONE); Require(sceWebBrowserDialogClose() == COMMON_DIALOG_ERROR_NOT_INITIALIZED); Require(sceWebBrowserDialogOpen(param) == COMMON_DIALOG_ERROR_NOT_INITIALIZED); @@ -37,12 +39,14 @@ int main() { Require(result == 0x5a5a5a5a); Require(sceWebBrowserDialogInitialize() == 0); + Require(sceWebBrowserDialogInitialize() == COMMON_DIALOG_ERROR_ALREADY_INITIALIZED); Require(sceWebBrowserDialogGetStatus() == COMMON_DIALOG_STATUS_INITIALIZED); Require(sceWebBrowserDialogUpdateStatus() == COMMON_DIALOG_STATUS_INITIALIZED); Require(sceWebBrowserDialogGetResult(&result) == COMMON_DIALOG_ERROR_NOT_FINISHED); Require(result == 0x5a5a5a5a); Require(sceWebBrowserDialogTerminate() == 0); + Require(sceWebBrowserDialogTerminate() == COMMON_DIALOG_ERROR_NOT_INITIALIZED); Require(sceWebBrowserDialogGetStatus() == COMMON_DIALOG_STATUS_NONE); Require(sceWebBrowserDialogInitialize() == 0); @@ -57,6 +61,7 @@ int main() { Require(sceWebBrowserDialogClose() == 0); Require(sceWebBrowserDialogTerminate() == 0); + Require(sceWebBrowserDialogTerminate() == COMMON_DIALOG_ERROR_NOT_INITIALIZED); Require(sceWebBrowserDialogGetStatus() == COMMON_DIALOG_STATUS_NONE); return 0; From 62b2c30baa55d71771f222cc074583a7f4a1a48c Mon Sep 17 00:00:00 2001 From: GalaxyGamingFR <153150716+GalaxyGamingFR@users.noreply.github.com> Date: Wed, 14 Oct 2026 00:54:37 -0700 Subject: [PATCH 0026/1000] fix(ci): count only earlier open PRs in the intake limit pr-intake runs once per opened PR, and each run lists every open PR by the author. When an author opens several PRs within seconds, every run sees all of them and closes its own, so a newcomer opening two PRs at once loses both, and an author with one merge opening 21 loses all 21. On opened events the limit now counts only lower-numbered PRs, so the earliest ones stay open and only the PRs over the limit are closed. Reopened events still count every open PR, as before. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_012zyXkUzTZGh3nBwNMtrLaW --- .github/workflows/pr-intake.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pr-intake.yml b/.github/workflows/pr-intake.yml index 6ae30f8b57..949a8cb717 100644 --- a/.github/workflows/pr-intake.yml +++ b/.github/workflows/pr-intake.yml @@ -42,7 +42,10 @@ jobs: const openPRs = await github.paginate(github.rest.pulls.list, { owner, repo, state: 'open', base: 'main', per_page: 100 }); - const open = new Set(openPRs.filter((p) => p.user.login === pr.user.login).map((p) => p.number)); + const reopened = context.payload.action === 'reopened'; + const open = new Set(openPRs + .filter((p) => p.user.login === pr.user.login && (reopened || p.number < pull_number)) + .map((p) => p.number)); open.add(pull_number); if (open.size <= allowedOpen) { return; From 8e498e3f7669352d258f1c7bed754108a00e36d7 Mon Sep 17 00:00:00 2001 From: GalaxyGamingFR <153150716+GalaxyGamingFR@users.noreply.github.com> Date: Wed, 14 Oct 2026 00:55:49 -0700 Subject: [PATCH 0027/1000] fix(ci): clear the stale label once a PR is rebased Stale conflicts runs actions/stale with only-pr-labels: needs rebase. actions/stale skips any PR without all only-labels before it looks at remove-pr-stale-when-updated, so after a rebase removes needs rebase the PR is never processed again and keeps its stale label for good. A step before actions/stale now removes stale from open PRs that no longer carry needs rebase. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_012zyXkUzTZGh3nBwNMtrLaW --- .github/workflows/stale-conflicts.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/stale-conflicts.yml b/.github/workflows/stale-conflicts.yml index 4eee09a40e..00150e1b70 100644 --- a/.github/workflows/stale-conflicts.yml +++ b/.github/workflows/stale-conflicts.yml @@ -14,6 +14,14 @@ jobs: issues: write pull-requests: write steps: + - env: + GH_TOKEN: ${{ github.token }} + run: | + gh pr list -R "$GITHUB_REPOSITORY" --label stale -L 1000 --json number,labels \ + --jq '.[] | select(all(.labels[]; .name != "needs rebase")) | .number' | + while read -r number; do + gh api -X DELETE "repos/$GITHUB_REPOSITORY/issues/$number/labels/stale" --silent + done - uses: actions/stale@v11 with: only-pr-labels: needs rebase From be9fbd5371c7c61cae41e5988d134b8791bcd7f3 Mon Sep 17 00:00:00 2001 From: Ondra97 <47064087+Ondra97@users.noreply.github.com> Date: Wed, 14 Oct 2026 10:12:29 +0200 Subject: [PATCH 0028/1000] fix(release): include the enabled Tracy runtime --- tools/package_release.py | 17 ++++++++++ tools/tests/test_package_release.py | 50 +++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+) diff --git a/tools/package_release.py b/tools/package_release.py index c825539e1d..fb9baec6c2 100644 --- a/tools/package_release.py +++ b/tools/package_release.py @@ -18,6 +18,22 @@ def windows_runtime(build): raise RuntimeError(f"CMAKE_CXX_COMPILER is not configured in {build / 'CMakeCache.txt'}") +def tracy_runtime(platform, build): + cache = build / "CMakeCache.txt" + if not cache.is_file(): + return [] + lines = cache.read_text(encoding="utf-8").splitlines() + enabled = next((line.partition("=")[2].upper() for line in lines if line.startswith("APS5_ENABLE_TRACY:")), "OFF") + if enabled in ("", "0", "OFF", "NO", "FALSE", "N", "IGNORE", "NOTFOUND") or enabled.endswith("-NOTFOUND"): + return [] + pattern = "libTracyClient.dll" if platform == "windows" else "libTracyClient.so.*" + directory = build / "core/libs/libs" + clients = sorted(directory.glob(pattern)) + if len(clients) != 1: + raise RuntimeError(f"Missing or ambiguous Tracy runtime for APS5_ENABLE_TRACY: {directory / pattern}") + return clients + + def package(platform, build, output, version): if not re.fullmatch(r"v[0-9A-Za-z][0-9A-Za-z._-]*", version) or version.endswith("."): raise ValueError(f"Invalid release tag for asset filenames: {version}") @@ -31,6 +47,7 @@ def package(platform, build, output, version): if platform == "windows": runtime = windows_runtime(build) files.extend(runtime / name for name in ("libgcc_s_seh-1.dll", "libstdc++-6.dll", "libwinpthread-1.dll")) + files.extend(tracy_runtime(platform, build)) binary = build / "core/relinker" / executable for file in [*files, binary]: if not file.is_file() or file.stat().st_size == 0: diff --git a/tools/tests/test_package_release.py b/tools/tests/test_package_release.py index ce95a7bcd0..77fd19fbce 100644 --- a/tools/tests/test_package_release.py +++ b/tools/tests/test_package_release.py @@ -74,6 +74,56 @@ class ReleasePackageTests(unittest.TestCase): self.assertEqual(list(self.output.iterdir()), [previous]) self.assertEqual(previous.read_bytes(), b"existing output") + def test_tracy_packages_include_only_the_target_platform_client(self): + directory = self.build / "core/libs/libs" + for platform, name in (("linux", "libTracyClient.so.0.14.1"), ("windows", "libTracyClient.dll")): + with self.subTest(platform=platform): + client = directory / name + client.write_bytes(platform.encode() + b" Tracy client") + cache = "APS5_ENABLE_TRACY:BOOL=ON\n" + expected = {"libs/libc.prx", "libs/" + name} + if platform == "windows": + runtime = self.root / "toolchain" + runtime.mkdir() + for runtime_name in ("libgcc_s_seh-1.dll", "libstdc++-6.dll", "libwinpthread-1.dll"): + (runtime / runtime_name).write_bytes(runtime_name.encode()) + expected.add("libs/" + runtime_name) + cache += "CMAKE_CXX_COMPILER:FILEPATH=" + (runtime / "g++.exe").as_posix() + "\n" + (self.build / "core/relinker/relinker.exe").write_bytes(b"Windows executable") + (self.build / "CMakeCache.txt").write_text(cache, encoding="utf-8") + package_release.package(platform, self.build, self.output, "v1") + with zipfile.ZipFile(self.output / f"prx-{platform}-v1.zip") as archive: + self.assertEqual(set(archive.namelist()), expected) + self.assertEqual(archive.read("libs/" + name), client.read_bytes()) + with tarfile.open(self.output / f"prx-{platform}-v1.tar.gz") as archive: + self.assertEqual(set(archive.getnames()), expected) + self.assertEqual(archive.extractfile("libs/" + name).read(), client.read_bytes()) + + def test_disabled_tracy_does_not_publish_stale_clients(self): + for name in ("libTracyClient.dll", "libTracyClient.so.0.14.1"): + (self.library.parent / name).write_bytes(b"old client") + for index, value in enumerate(("OFF", "0", "NO", "FALSE", "N", "IGNORE", "NOTFOUND", "TRACY-NOTFOUND", "")): + with self.subTest(value=value): + (self.build / "CMakeCache.txt").write_text(f"APS5_ENABLE_TRACY:BOOL={value}\n", encoding="utf-8") + output = self.output / str(index) + package_release.package("linux", self.build, output, "v1") + with zipfile.ZipFile(output / "prx-linux-v1.zip") as archive: + self.assertEqual(archive.namelist(), ["libs/libc.prx"]) + + def test_missing_or_ambiguous_tracy_client_preserves_existing_assets(self): + (self.build / "CMakeCache.txt").write_text("APS5_ENABLE_TRACY:BOOL=ON\n", encoding="utf-8") + previous = {"prx-linux-v1.zip": b"old zip", "prx-linux-v1.tar.gz": b"old tar", "relinker-v1": b"old binary"} + self.output.mkdir() + for name, data in previous.items(): + (self.output / name).write_bytes(data) + for clients in ((), ("libTracyClient.so.0.13.1", "libTracyClient.so.0.14.1")): + with self.subTest(clients=clients): + for name in clients: + (self.library.parent / name).write_bytes(b"client bytes") + with self.assertRaisesRegex(RuntimeError, "Tracy"): + package_release.package("linux", self.build, self.output, "v1") + self.assertEqual({file.name: file.read_bytes() for file in self.output.iterdir()}, previous) + def test_source_discovery_does_not_depend_on_working_directory(self): with working_directory(self.root): package_release.package("linux", self.build, self.output, "v1.0") From 14e62d45d3abb905f9000680e0a5b4fb03d683ee Mon Sep 17 00:00:00 2001 From: mugurc Date: Wed, 14 Oct 2026 11:32:59 +0300 Subject: [PATCH 0029/1000] fix(libSceFiber): map fiber host stacks on hosts without MAP_STACK AllocateHostStack passes MAP_STACK to mmap. The macOS SDK defines MAP_NORESERVE but not MAP_STACK, so Export.cpp does not compile there. Build the flags in a local variable and add MAP_STACK only where the host defines it. Linux keeps the same flags. --- core/libs/prx/libSceFiber/Export.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceFiber/Export.cpp b/core/libs/prx/libSceFiber/Export.cpp index a727830404..7c3a53dc03 100644 --- a/core/libs/prx/libSceFiber/Export.cpp +++ b/core/libs/prx/libSceFiber/Export.cpp @@ -297,7 +297,11 @@ static void ReleaseHostStack(Fiber* fiber) { static void AllocateHostStack(Fiber* fiber) { const auto page = static_cast(sysconf(_SC_PAGESIZE)); const auto bytes = (fiber->contextSize + FIBER_HOST_STACK_MARGIN + page - 1) / page * page + page; - void* mapping = mmap(nullptr, bytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE | MAP_STACK, -1, 0); + int flags = MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE; +#ifdef MAP_STACK + flags |= MAP_STACK; +#endif + void* mapping = mmap(nullptr, bytes, PROT_READ | PROT_WRITE, flags, -1, 0); if (mapping == MAP_FAILED) throw std::runtime_error(std::string("sceFiberInitialize: no host stack of ") + std::to_string(bytes) + " bytes for fiber '" + fiber->name + "' (errno " + std::to_string(errno) + ")"); if (mprotect(mapping, page, PROT_NONE) != 0) { const int error = errno; From c53c349095b62c9a937369d7789e8954b4bd0ab3 Mon Sep 17 00:00:00 2001 From: Muhammad Wasif Ali Wasif <76695420+wachi35676@users.noreply.github.com> Date: Tue, 13 Oct 2026 23:29:22 +0500 Subject: [PATCH 0030/1000] fix(tools): count tests registered per configuration as run Ninja Multi-Config writes one add_test per configuration and a final NOT_AVAILABLE one for the same name. The audit kept only the last, so every test executable was reported as never run. Co-Authored-By: Claude Opus 5.5 --- tools/check_test_registration.py | 6 +++--- tools/tests/test_test_registration.py | 13 +++++++++++++ 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/tools/check_test_registration.py b/tools/check_test_registration.py index 7b00d94d3e..07f952dcde 100644 --- a/tools/check_test_registration.py +++ b/tools/check_test_registration.py @@ -81,12 +81,12 @@ def registered(build): if directory in visited or not file.is_file(): continue visited.add(directory) - tests, disabled = {}, {} + tests, disabled = [], {} for command, arguments in commands(file.read_text(encoding="utf-8", errors="replace")): if command == "subdirs": pending.extend(directory / argument for argument in arguments) elif command == "add_test" and arguments: - tests[arguments[0]] = arguments[1:] + tests.append((arguments[0], arguments[1:])) elif command == "set_tests_properties" and "PROPERTIES" in arguments: properties = arguments.index("PROPERTIES") values = arguments[properties + 1:] @@ -94,7 +94,7 @@ def registered(build): if key == "DISABLED": for name in arguments[:properties]: disabled[name] = value.upper() in ("1", "ON", "YES", "TRUE", "Y") - for test, arguments in tests.items(): + for test, arguments in tests: if disabled.get(test, False): continue for index, argument in enumerate(arguments): diff --git a/tools/tests/test_test_registration.py b/tools/tests/test_test_registration.py index e9862c2bc3..73f0d9a721 100644 --- a/tools/tests/test_test_registration.py +++ b/tools/tests/test_test_registration.py @@ -66,6 +66,19 @@ class RegistrationTests(unittest.TestCase): names = self.registered('add_test("bounds"\n "/build/tests/buffer_bounds_tests"\n)\n') self.assertIn("buffer_bounds_tests", names) + def test_multi_config_branches_register_the_executable(self): + names = self.registered( + 'if(CTEST_CONFIGURATION_TYPE MATCHES "^([Dd][Ee][Bb][Uu][Gg])$")\n' + ' add_test([=[bounds]=] "/build/tests/Debug/buffer_bounds_tests")\n' + ' set_tests_properties([=[bounds]=] PROPERTIES TIMEOUT "20")\n' + 'elseif(CTEST_CONFIGURATION_TYPE MATCHES "^([Rr][Ee][Ll][Ee][Aa][Ss][Ee])$")\n' + ' add_test([=[bounds]=] "/build/tests/Release/buffer_bounds_tests")\n' + ' set_tests_properties([=[bounds]=] PROPERTIES TIMEOUT "20")\n' + 'else()\n' + ' add_test([=[bounds]=] NOT_AVAILABLE)\n' + 'endif()\n') + self.assertIn("buffer_bounds_tests", names) + def test_disabled_test_is_not_counted_as_run(self): names = self.registered('add_test("bounds" "/build/tests/buffer_bounds_tests")\nset_tests_properties("bounds" PROPERTIES DISABLED TRUE)\n') self.assertNotIn("buffer_bounds_tests", names) From d4709592e76a98d3b4618efe4bd4ce9927e87bd7 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Wed, 14 Oct 2026 01:40:04 -0700 Subject: [PATCH 0031/1000] fix(agc): bind a read-only V# over the cached address space through the region that holds it Since the cached address space keeps a read-only V# that only partly overlaps its base regions (readsOverBase), a build's own regions can overlap base regions. UploadPrepare merges overlapping own views, so a merged region can begin before the base region that holds a view's first byte. owner() still took whichever candidate began later, so Descriptor bound such a view through one base region it does not fit: guest buffer view 0x1f0494592c+0x1b2190 exceeds its GPU owner 0x1f04940000-0x1f04950000 (buffer 0, direct 1, mirror 0) owner() now takes the range's end and prefers the candidate that holds the whole range. When both or neither do, it keeps the previous rule. Every caller already knew the end it passes. --- .../Graphics/include/GuestBufferMemory.hpp | 2 +- .../Graphics/src/GuestBufferMemory.cpp | 20 ++++---- .../libSceAgcDriver/tests/BdaResources.cpp | 47 +++++++++++++++++++ 3 files changed, 58 insertions(+), 11 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp index 2bcdbbbca0..9e71901ebd 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp @@ -335,7 +335,7 @@ private: void dissolveSpace(bool resolve); // The region containing `address`: this build's own regions (sorted after UploadPrepare) or // the space's base regions, or null. - const Region* owner(std::uint64_t address) const; + const Region* owner(std::uint64_t address, std::uint64_t end) const; // Builds this build's regions from a lease as the per-build path always did: `base` gets the // ranges served in place (imports, mirrors), `copied` the ones copied per build. struct CopiedRange { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index 78669afd45..b4771f1a6a 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -2596,15 +2596,15 @@ void GuestBufferMemory::dissolveSpace(bool resolve) { space.reset(); } -const GuestBufferMemory::Region* GuestBufferMemory::owner(std::uint64_t address) const { - // The region with the greatest begin at or below the address, as the one merged list gave; - // the caller checks the end, as before. +const GuestBufferMemory::Region* GuestBufferMemory::owner(std::uint64_t address, std::uint64_t end) const { const auto candidate = [address](const std::vector& list) -> const Region* { const auto found = std::upper_bound(list.begin(), list.end(), address, [](std::uint64_t value, const Region& region) { return value < region.begin; }); return found == list.begin() ? nullptr : &*std::prev(found); }; const auto* own = candidate(regions); const auto* shared = space != nullptr ? candidate(space->base) : nullptr; + const auto holds = [end](const Region* region) { return region != nullptr && end <= region->end; }; + if (holds(own) != holds(shared)) return holds(own) ? own : shared; if (own == nullptr || (shared != nullptr && shared->begin > own->begin)) return shared; return own; } @@ -3603,7 +3603,7 @@ void GuestBufferMemory::RecordCopyBacks(Recorder& recorder) { VkDescriptorBufferInfo GuestBufferMemory::Descriptor(std::uint64_t address, std::size_t bytes, std::uint32_t& adjustment) const { Require(uploaded && !committed, "guest GPU memory is not available"); Require(bytes != 0 && bytes <= std::numeric_limits::max() - address, "invalid guest buffer view"); - const auto* found = owner(address); + const auto* found = owner(address, address + bytes); Require(found != nullptr, "guest buffer has no GPU owner"); const auto& region = *found; const auto describe = [&](const char* what) { @@ -3683,7 +3683,7 @@ std::vector GuestBufferMemory::AddressRanges() part.end = end; part.deviceAddress += cursor - range.begin; if (written) { - const auto* region = owner(cursor); + const auto* region = owner(cursor, end); Require(region != nullptr && region->writable && (region->mirror == nullptr || region->mirror->writable), "runtime buffer write has no writable GPU owner"); part.permissions |= ShaderRecompiler::BdaAbi::Write; } @@ -3720,7 +3720,7 @@ std::optional GuestBufferMemory::CachedAddressTa bool GuestBufferMemory::HasCopiedWrites() const { for (const auto& [begin, end] : writes) { - const auto* found = owner(begin); + const auto* found = owner(begin, end); if (found == nullptr) continue; const auto& region = *found; if (region.direct != nullptr) continue; @@ -3740,7 +3740,7 @@ bool GuestBufferMemory::CopiedWritesOverlap(std::uint64_t address, std::size_t b if (bytes == 0) return false; for (const auto& [begin, end] : writes) { if (!(address < end && begin < address + bytes)) continue; - const auto* found = owner(begin); + const auto* found = owner(begin, end); if (found == nullptr) continue; const auto& region = *found; if (region.direct != nullptr) continue; @@ -3753,7 +3753,7 @@ bool GuestBufferMemory::CopiedWritesOverlap(std::uint64_t address, std::size_t b void GuestBufferMemory::MarkDirectWrites() const { for (const auto& [begin, end] : writes) { - const auto* found = owner(begin); + const auto* found = owner(begin, end); if (found == nullptr) continue; const auto& region = *found; if (region.direct != nullptr || (region.gpuCopy && region.copiedBack)) GuestMemory::MarkWritten(begin, static_cast(end - begin)); @@ -3820,7 +3820,7 @@ std::vector> mergeWrites(std::vectormirror == nullptr || !found->mirror->heap || !found->mirror->writable) continue; Require(end <= found->end, "written range exceeds its heap mirror"); const auto& mirror = *found->mirror; @@ -3837,7 +3837,7 @@ void GuestBufferMemory::WriteBack() { // The CPU keeps running while the GPU works, so storing whole ranges would roll back its writes to // bytes the shader never touched. Only runs that differ from the uploaded copy are stored. for (const auto& [begin, end] : merged) { - const auto* found = owner(begin); + const auto* found = owner(begin, end); Require(found != nullptr, "write-back range has no GPU owner"); const auto& region = *found; // Every branch below but the direct, copied-back and staged ones stores from the CPU. diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp index 356fe0d4ff..e056c5abb4 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp @@ -557,6 +557,52 @@ void spaceReadAcrossImportsWrittenTests(const Context& context) { ::operator delete(block, std::align_val_t{window}); } +void spaceReadsAcrossImportsMergedTests(const Context& context, const BdaTestAccess& access) { + constexpr std::size_t piece = 65536; + constexpr std::size_t window = 2 * piece; + constexpr std::size_t pieces = 6; + void* block = ::operator new(pieces * piece, std::align_val_t{window}); + auto* guest = static_cast(block); + const auto first = reinterpret_cast(block); + for (std::size_t index = 0; index < pieces; ++index) std::memset(guest + index * piece, static_cast(0x10 + index), piece); + auto importing = context; + importing.hostImportAlignment = piece; + SetHostImportWindow(window); + const auto change = [&](std::uint8_t* range, bool add) { + auto* mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix(); + if (add) GuestAllocations::GuestAllocationsAdd_nid_postfix(mutation, range, piece, true, false, true); + else GuestAllocations::GuestAllocationsRemove_nid_postfix(mutation, range); + GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation); + }; + for (std::size_t index = 0; index < pieces; ++index) change(guest + index * piece, true); + const auto second = first + window; + const auto third = first + 2 * window; + const std::uint64_t lower = second - 256; + const std::uint64_t lowerBytes = 512; + const std::uint64_t upper = second + 128; + const std::uint64_t upperBytes = third + 256 - upper; + for (int build = 0; build < 2; ++build) { + GuestBufferMemory leased(importing); + leased.AcquireRegistered(); + leased.AddReadable(lower, lowerBytes); + leased.AddReadable(upper, upperBytes); + leased.Upload(true); + for (const auto& [address, bytes] : {std::pair{lower, lowerBytes}, std::pair{upper, upperBytes}}) { + std::uint32_t adjustment = 0; + const auto view = leased.Descriptor(address, bytes, adjustment); + Require(view.buffer != VK_NULL_HANDLE && view.range == bytes + adjustment, "a read-only V# over imports, merged with another one, has no descriptor of its size"); + const auto mapped = access.bytes(view.buffer); + const auto at = static_cast(view.offset + adjustment); + const auto expected = [&](std::uint64_t guestAddress) { return std::byte{static_cast(0x10 + (guestAddress - first) / piece)}; }; + Require(at + bytes <= mapped.size() && mapped[at] == expected(address) && mapped[at + bytes - 1] == expected(address + bytes - 1), "a read-only V# over imports, merged with another one, maps other bytes than its own"); + } + leased.WriteBack(); + } + for (std::size_t index = 0; index < pieces; ++index) change(guest + index * piece, false); + SetHostImportWindow(0); + ::operator delete(block, std::align_val_t{window}); +} + ShaderRecompiler::DescriptorBinding binding(Role role, std::uint32_t slot) { return {ShaderRecompiler::DescriptorKind::StorageBuffer, role, 0, slot, 1, {}, false}; } @@ -1556,6 +1602,7 @@ void RunBdaResourceTests(const Context& context, const BdaTestAccess& access) { importWindowTests(context); spaceReadAcrossImportsTests(context, access); spaceReadAcrossImportsWrittenTests(context); + spaceReadsAcrossImportsMergedTests(context, access); importJoinTests(context); importWholeRangeOverWindowTests(context); importRefusedWholeRangeTests(context); From a30f700630952f255a5eb01b203580f682daf037 Mon Sep 17 00:00:00 2001 From: GalaxyGamingFR <153150716+GalaxyGamingFR@users.noreply.github.com> Date: Wed, 14 Oct 2026 01:44:11 -0700 Subject: [PATCH 0032/1000] fix(tools): accept skip reasons on their own line in check_skips Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_012zyXkUzTZGh3nBwNMtrLaW --- tools/check_skips.py | 2 +- tools/tests/test_check_skips.py | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/tools/check_skips.py b/tools/check_skips.py index 0cc51cacb3..af4db8a61c 100644 --- a/tools/check_skips.py +++ b/tools/check_skips.py @@ -33,7 +33,7 @@ def skips(path): name = case.get("name") if not name: raise ValueError(f"{path}: skipped testcase has no name") - output = ["skipped, " + reason for reason in (skipped.get("message", ""), skipped.text or "") if reason] + output = ["skipped, " + reason.strip() for reason in (skipped.get("message", ""), skipped.text or "") if reason.strip()] output += [case.findtext(channel) or "" for channel in ("system-out", "system-err")] yield name, "\n".join(output) diff --git a/tools/tests/test_check_skips.py b/tools/tests/test_check_skips.py index c2a4c33b79..5ec9c41943 100644 --- a/tools/tests/test_check_skips.py +++ b/tools/tests/test_check_skips.py @@ -143,6 +143,15 @@ class CheckSkipsTests(unittest.TestCase): self.assertIn("error: odd skipped", result.stdout) self.assertNotIn("error: geometry skipped", result.stdout) + def test_reason_on_its_own_line_inside_skipped_is_expected(self): + for reason, job in (("the device lacks geometryShader", "linux"), ("no display", "windows")): + with self.subTest(job=job), tempfile.TemporaryDirectory() as directory: + junit = Path(directory) / "junit.xml" + junit.write_text(f'\n{reason}\n', encoding="utf-8") + result = subprocess.run([sys.executable, "-I", str(TOOL), str(junit), "--job", job], capture_output=True, text=True) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertIn("all expected", result.stdout) + From 23b8a3a48ada9535689ad3121f150c28a5a4bd40 Mon Sep 17 00:00:00 2001 From: NaitikJoshiPro <190247830+NaitikJoshiPro@users.noreply.github.com> Date: Wed, 14 Oct 2026 14:43:25 +0530 Subject: [PATCH 0033/1000] fix(agc): queue 0 waits instead of spinning when every queue is idle With every queue idle, no completions pending and no storage results pending, queue 0 took the idle branch, found nothing to store (idleResultsPending() is false since #4410 added the AnyPending() check) and re-checked at once: the condition kept for "nothing was stored" continued the loop whenever storeIdleResults was false. Queue 0 never reached its wait and spun on the driver mutex, a full host core for as long as the GPU sat idle. Queue 0 now re-checks only when storage results became pending since the check or a queue finished work (idle epoch changed); otherwise it waits for a submission or the next idle epoch as before. Test: agc_driver_queue_zero_idle_wait submits a NOP on queue 0 and requires queue 0 to go dormant with nothing pending. Co-Authored-By: Claude Opus 5.5 --- .../src/Driver/Queues/QueueWorker.cpp | 2 +- .../tests/execution/QueueZeroIdleWait.cpp | 40 +++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/QueueZeroIdleWait.cpp diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/QueueWorker.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/QueueWorker.cpp index 56db0c1f68..cfde878be1 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/QueueWorker.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/QueueWorker.cpp @@ -159,7 +159,7 @@ void Driver::run(std::uint32_t id) noexcept { lock.lock(); if (ready()) break; if (id == 0 && allQueuesIdle() && !completionsPending() - && (!storeIdleResults || idleEpoch.load(std::memory_order_acquire) != idleEpochAtCheck)) continue; + && ((!storeIdleResults && Graphics::StorageTexture::AnyPending()) || idleEpoch.load(std::memory_order_acquire) != idleEpochAtCheck)) continue; if (id == 0) queue0Dormant.store(true, std::memory_order_relaxed); const auto idleEpochAtWait = idleEpoch.load(std::memory_order_acquire); changed.wait(lock, [&] { return ready() || (id == 0 && idleEpoch.load(std::memory_order_acquire) != idleEpochAtWait); }); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/QueueZeroIdleWait.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/QueueZeroIdleWait.cpp new file mode 100644 index 0000000000..07f2a58a6b --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/QueueZeroIdleWait.cpp @@ -0,0 +1,40 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Synchronization/SynchronizationStatistics.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include + +int main() { + try { + { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + } + struct ShutdownGuard { + ~ShutdownGuard() { try { AgcDriverShutdown_nid_postfix(); } catch (...) {} } + } shutdown; + + std::array nop{0xc0001000u, 0u}; + Packet packet{nop.data(), static_cast(nop.size()), 0, {}}; + AgcDriver::Submit(&packet, 0); + + const auto dormant = [] { return AgcDriver::DriverDetail::queue0Dormant.load(std::memory_order_relaxed); }; + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(2); + while (!dormant() && std::chrono::steady_clock::now() < deadline) std::this_thread::sleep_for(std::chrono::milliseconds(1)); + AgcDriver::Graphics::Require(!AgcDriver::Graphics::StorageTexture::AnyPending(), "a no-op submission left storage results pending"); + AgcDriver::Graphics::Require(dormant(), "queue 0 kept polling with every queue idle and no storage results to store instead of waiting"); + + AgcDriverShutdown_nid_postfix(); + std::puts("queue zero idle wait tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} From 70ef3710f33efeb32f8a048666564ffb947aa015 Mon Sep 17 00:00:00 2001 From: David Date: Wed, 14 Oct 2026 13:14:58 +0400 Subject: [PATCH 0034/1000] fix(libc): start the guest environment empty instead of copying the host's --- core/libs/prx/libc/src/Environment.cpp | 19 +------------------ core/libs/tests/GuestEnvironment.cpp | 9 +++++---- 2 files changed, 6 insertions(+), 22 deletions(-) diff --git a/core/libs/prx/libc/src/Environment.cpp b/core/libs/prx/libc/src/Environment.cpp index 03996644bf..cbb171729b 100644 --- a/core/libs/prx/libc/src/Environment.cpp +++ b/core/libs/prx/libc/src/Environment.cpp @@ -9,10 +9,6 @@ #include #include "prx/libc/include/general/VabiMacros.hpp" -#ifndef _WIN32 -extern char** environ; -#endif - namespace { struct EnvironmentEntry { std::unique_ptr owned; @@ -26,20 +22,7 @@ struct EnvironmentEntry { }; std::mutex environmentMutex; std::vector& Environment() { - // Seed once from the host. Guest mutations remain local to the guest runtime. - static std::vector entries = [] { - std::vector result; -#ifdef _WIN32 - char** source = _environ; -#else - char** source = environ; -#endif - if (source) for (; *source; ++source) { - if (**source && **source != '=' && std::strchr(*source, '=')) - result.push_back({std::make_unique(*source), nullptr}); - } - return result; - }(); + static std::vector entries; return entries; } bool ValidEnvironmentName(const char* name) { diff --git a/core/libs/tests/GuestEnvironment.cpp b/core/libs/tests/GuestEnvironment.cpp index 7bd0c7fb88..3156c6a760 100644 --- a/core/libs/tests/GuestEnvironment.cpp +++ b/core/libs/tests/GuestEnvironment.cpp @@ -13,18 +13,19 @@ static void Require(bool value) { if (!value) std::abort(); } int main() { const char* key = "ANYPS5_GUEST_ENV_TEST_4C27"; #ifdef _WIN32 - Require(_putenv_s(key, "inherited") == 0); + Require(_putenv_s(key, "host") == 0); #else - Require(::setenv(key, "inherited", 1) == 0); + Require(::setenv(key, "host", 1) == 0); #endif - Require(std::strcmp(getenv_nid_postfix(key), "inherited") == 0); + Require(getenv_nid_postfix(key) == nullptr); + Require(setenv_nid_postfix(key, "inherited", 0) == 0); Require(setenv_nid_postfix(key, "kept out", 0) == 0); Require(std::strcmp(getenv_nid_postfix(key), "inherited") == 0); char value[] = "copied"; Require(setenv_nid_postfix(key, value, 1) == 0); value[0] = 'X'; Require(std::strcmp(getenv_nid_postfix(key), "copied") == 0); - Require(std::strcmp(std::getenv(key), "inherited") == 0); + Require(std::strcmp(std::getenv(key), "host") == 0); Require(setenv_nid_postfix(key, "", 1) == 0); Require(getenv_nid_postfix(key) && *getenv_nid_postfix(key) == 0); char borrowed[] = "ANYPS5_GUEST_ENV_TEST_4C27=one"; From 713a23888c3036cd5be3dfd945f518bcdb42d9b0 Mon Sep 17 00:00:00 2001 From: David Date: Wed, 14 Oct 2026 13:15:07 +0400 Subject: [PATCH 0035/1000] test(libc): keep the host's HOME and PATH out of the guest environment --- core/libs/tests/GuestEnvironment.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/core/libs/tests/GuestEnvironment.cpp b/core/libs/tests/GuestEnvironment.cpp index 3156c6a760..46e2b2b11b 100644 --- a/core/libs/tests/GuestEnvironment.cpp +++ b/core/libs/tests/GuestEnvironment.cpp @@ -18,6 +18,12 @@ int main() { Require(::setenv(key, "host", 1) == 0); #endif Require(getenv_nid_postfix(key) == nullptr); +#ifdef _WIN32 + Require(_putenv_s("HOME", "C:\\Users\\host") == 0); +#else + Require(::setenv("HOME", "/home/host", 1) == 0); +#endif + for (const char* host : {"HOME", "PATH"}) Require(std::getenv(host) != nullptr && getenv_nid_postfix(host) == nullptr); Require(setenv_nid_postfix(key, "inherited", 0) == 0); Require(setenv_nid_postfix(key, "kept out", 0) == 0); Require(std::strcmp(getenv_nid_postfix(key), "inherited") == 0); From 69715e8393561172d0e35173e2ba0d8072978059 Mon Sep 17 00:00:00 2001 From: David Date: Wed, 14 Oct 2026 13:15:53 +0400 Subject: [PATCH 0036/1000] docs(debt): record that the guest environment starts empty --- docs/dev/debt/libc.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 3f8c51d2fb..90f4f90359 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -41,6 +41,7 @@ - [sinhf](../../../core/libs/prx/libc/src/Math.cpp) (libc) follows the `sinhf` of the `libc.prx` bundled with PPSA28319 (disassembly): FreeBSD msun's [`e_sinhf.c`](https://github.com/freebsd/freebsd-src/blob/5aa554c7e5b6fd9042986df07f12ac5687e41392/lib/msun/src/e_sinhf.c) from before `__ldexp_expf`, which squares `expf(|x| / 2)` between `logf(FLT_MAX)` and the overflow threshold, calling msun's [`expm1f`](https://github.com/freebsd/freebsd-src/blob/59bc2f095289a10b8cfa55cbb2b45c3bd6c2d4ae/lib/msun/src/s_expm1f.c) and [`expf`](https://github.com/freebsd/freebsd-src/blob/59bc2f095289a10b8cfa55cbb2b45c3bd6c2d4ae/lib/msun/src/e_expf.c), whose paths for the arguments it passes are inlined here. Past the threshold Sony's error helper sets `errno` to `ERANGE` before returning `x * 1e37`; msun itself sets no `errno`. That code, run on a Windows host, gives the same bits as this implementation for every finite argument up to the threshold, of both signs; it was not run on Linux. That the system `libSceLibcInternal`, which Ruffle (PPSA68091) and RetroArch (PPSA99169) import while bundling a zero-return `libc.prx`, has the same code is assumed - [asinl](../../../core/libs/prx/libc/src/Math.cpp) (libc) follows the `asinl` of the `libc.prx` bundled with PPSA28319 (disassembly), FreeBSD msun's [`e_asinl.c`](https://github.com/freebsd/freebsd-src/blob/9f0432041b8fb69832f10c32da50a67f34feeb61/lib/msun/src/e_asinl.c) with the ld80 [`invtrig.c`](https://github.com/freebsd/freebsd-src/blob/9f0432041b8fb69832f10c32da50a67f34feeb61/lib/msun/ld80/invtrig.c) coefficients, which the binary holds as the same 80-bit constants. For |x| > 1, the infinities and NaN, Sony's error helper raises `FE_INVALID` and sets `errno` to `EDOM` before returning `(x - x) / (x - x)`, so a NaN argument also sets `EDOM`; msun itself sets no `errno`. 20 million random arguments in (-1, 1) give the same bits as that code run on a Windows host. The result needs the x87 precision control at 64 bits, as FreeBSD amd64 starts threads (control word `0x37f`), but guest threads do not set it: on Windows a thread started by `_beginthreadex` runs at 53 bits (`0x27f`), where `asinl(0.75)` returns mantissa `0xd91a98ae3406e000` instead of `0xd91a98ae3406e041`. The system `libSceLibcInternal` that RetroArch (PPSA99169) imports is assumed to have the same code - [atanhf](../../../core/libs/prx/libc/src/Math.cpp) (libc) follows the `atanhf` of the `libc.prx` bundled with PPSA28319 (disassembly): FreeBSD msun's [`e_atanhf.c`](https://github.com/freebsd/freebsd-src/blob/59bc2f095289a10b8cfa55cbb2b45c3bd6c2d4ae/lib/msun/src/e_atanhf.c) calling msun's [`log1pf`](https://github.com/freebsd/freebsd-src/blob/59bc2f095289a10b8cfa55cbb2b45c3bd6c2d4ae/lib/msun/src/s_log1pf.c), whose paths for the positive arguments it passes are inlined here; the exported `log1pf` is still the host's. For |x| > 1, the infinities and NaN, Sony's error helper raises `FE_INVALID` and sets `errno` to `EDOM` before returning `(x - x) / (x - x)`, and for |x| = 1 it raises `FE_DIVBYZERO` and sets `errno` to `ERANGE` before returning `x / 0`; msun itself sets no `errno`. That code, run on a Windows host, gives the same bits, `errno` and floating-point exceptions as this implementation for all 2^32 arguments; it was not run on Linux. That the system `libSceLibcInternal`, which Dusklight (PPSA99305) imports while bundling a zero-return `libc.prx`, has the same code is assumed +- [getenv, setenv, unsetenv, putenv](../../../core/libs/prx/libc/src/Environment.cpp) (libc) keep one guest environment that starts empty, like the `environ` libkernel exports, and never reads or changes the host's. A title sees only the variables it sets itself; host variables such as `HOME` and `PATH` name host paths a guest cannot use (PS5MC, PPSA99420, exits with `XBMC_Run returned -1` when it gets the host's `HOME`). Whether the console starts a title with any variables set is not measured - On macOS the [write watch](../../../core/libs/prx/libc/src/GuestWriteWatch.cpp) write-protects watched pages and records the first write to each from a SIGBUS/SIGSEGV handler, because macOS has no counterpart to `userfaultfd` write-protect or `MEM_WRITE_WATCH`. The protection is real, so: - a system call that writes into a protected page fails with `EFAULT` instead of faulting. `GuestArena::HostWrite` lifts the protection for the calls it brackets (file reads and socket receives); a host call that writes guest memory without it (`clock_gettime` and the like into a watched page) fails; - code that changes the protection of watched pages, or maps over them, has to call `GuestWriteWatchProtectionChanged`, and code that asks the kernel for a page's protection has to ask `GuestWriteWatchProtection` first, since a watched page is read-only between a collection and the next write. Nothing calls either yet: the macOS memory paths are not in the tree; From 84d2bb93e3a29687e1315bfffb25586041c066bd Mon Sep 17 00:00:00 2001 From: GalaxyGamingFR <153150716+GalaxyGamingFR@users.noreply.github.com> Date: Wed, 14 Oct 2026 02:23:19 -0700 Subject: [PATCH 0037/1000] fix(ci): label all open PRs, not only the newest 200 The area-labels workflow listed open PRs with -L 200, so with more than 200 open PRs the older ones never entered the todo list and never got their area labels (#3295 changes libkernel and has no libkernel label). List up to 1000; the head -n 40 cap still bounds labeler work per run. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_012zyXkUzTZGh3nBwNMtrLaW --- .github/workflows/area-labels.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/area-labels.yml b/.github/workflows/area-labels.yml index bd3d6a370d..f76171b143 100644 --- a/.github/workflows/area-labels.yml +++ b/.github/workflows/area-labels.yml @@ -26,7 +26,7 @@ jobs: state=$RUNNER_TEMP/area-state.json [ -s "$state" ] || echo '{}' > "$state" config=$(gh api "repos/$GITHUB_REPOSITORY/contents/.github/labeler.yml" --jq .sha) - gh pr list -R "$GITHUB_REPOSITORY" --base "${{ github.event.repository.default_branch }}" -L 200 --json number,headRefOid \ + gh pr list -R "$GITHUB_REPOSITORY" --base "${{ github.event.repository.default_branch }}" -L 1000 --json number,headRefOid \ --jq "map({key: (.number | tostring), value: (.headRefOid + \"-$config\")}) | from_entries" > "$RUNNER_TEMP/area-open.json" jq -r --slurpfile old "$state" 'to_entries[] | select($old[0][.key] != .value) | .key' "$RUNNER_TEMP/area-open.json" | head -n 40 > "$RUNNER_TEMP/area-todo" jq -c --slurpfile open "$RUNNER_TEMP/area-open.json" --rawfile todo "$RUNNER_TEMP/area-todo" \ From 87e090452d6d2730badbbd3de8aa9131a5f79e0a Mon Sep 17 00:00:00 2001 From: Tiberon Kuiper Date: Wed, 14 Oct 2026 11:18:48 +0200 Subject: [PATCH 0038/1000] fix(libc): ignore empty macOS write-watch ranges --- core/libs/prx/libc/src/GuestWriteWatch.cpp | 15 +++++--- core/libs/tests/WriteWatch.cpp | 40 +++++++++++++++++++++- core/libs/tests/write_watch/CMakeLists.txt | 2 +- 3 files changed, 50 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libc/src/GuestWriteWatch.cpp b/core/libs/prx/libc/src/GuestWriteWatch.cpp index 55f991f047..b86781c280 100644 --- a/core/libs/prx/libc/src/GuestWriteWatch.cpp +++ b/core/libs/prx/libc/src/GuestWriteWatch.cpp @@ -330,9 +330,10 @@ public: } void Register(std::uintptr_t begin, std::uintptr_t end) { + if (!_available || end <= begin) return; begin = pageStart(begin); end = pageEnd(end); - if (!_available || end <= begin) return; + if (end <= begin) return; std::vector pages((end - begin) / _page, Written | Fresh); std::vector queried(pages.size(), false); const Lock lock(_lock); @@ -364,9 +365,10 @@ public: } bool Unregister(std::uintptr_t begin, std::uintptr_t end) { + if (!_available || end <= begin) return false; begin = pageStart(begin); end = pageEnd(end); - if (!_available || end <= begin) return false; + if (end <= begin) return false; const Lock lock(_lock); return erase(begin, end); } @@ -407,9 +409,10 @@ public: } void ProtectionChanged(std::uintptr_t begin, std::uintptr_t end, int protection) { + if (!_available || end <= begin) return; begin = pageStart(begin); end = pageEnd(end); - if (!_available || end <= begin) return; + if (end <= begin) return; const Lock lock(_lock); forEachPage(begin, end, [&](std::uintptr_t, std::uint8_t& state) { state = static_cast(Written | (state & Fresh) | (protection & ProtectionMask)); @@ -417,9 +420,10 @@ public: } void BeginHostWrite(std::uintptr_t begin, std::uintptr_t end) { + if (!_available || end <= begin) return; begin = pageStart(begin); end = pageEnd(end); - if (!_available || end <= begin) return; + if (end <= begin) return; const Lock lock(_lock); _hostWrites.emplace_back(begin, end); forEachPage(begin, end, [&](std::uintptr_t page, std::uint8_t& state) { @@ -429,9 +433,10 @@ public: } void EndHostWrite(std::uintptr_t begin, std::uintptr_t end) { + if (!_available || end <= begin) return; begin = pageStart(begin); end = pageEnd(end); - if (!_available || end <= begin) return; + if (end <= begin) return; const Lock lock(_lock); const auto found = std::find(_hostWrites.begin(), _hostWrites.end(), std::pair{begin, end}); if (found != _hostWrites.end()) _hostWrites.erase(found); diff --git a/core/libs/tests/WriteWatch.cpp b/core/libs/tests/WriteWatch.cpp index 045b6c10b0..2093126aa4 100644 --- a/core/libs/tests/WriteWatch.cpp +++ b/core/libs/tests/WriteWatch.cpp @@ -170,6 +170,43 @@ void CheckHostWrite() { ::close(descriptors[1]); } +void CheckEmptyRanges() { + Region region(1); + for (const auto offset : {std::size_t{0}, std::size_t{1}, region.page - 1}) { + auto* pointer = region.base + offset; + GuestWriteWatchRegister_nid_postfix(pointer, 0); + Require(!GuestWriteWatchCovers_nid_postfix(region.Address(0), region.bytes), "an empty registration watches a page"); + GuestWriteWatchRegister_nid_postfix(region.base, region.bytes); + Require(region.Collect() == All(1), "a new range is not reported in full"); + + Require(!GuestWriteWatchUnregister_nid_postfix(pointer, 0), "an empty unregister removes a page"); + Require(GuestWriteWatchCovers_nid_postfix(region.Address(0), region.bytes), "an empty unregister changes coverage"); + GuestWriteWatchRegister_nid_postfix(pointer, 0); + Require(region.Collect().empty(), "an empty registration marks a watched page fresh"); + + GuestWriteWatchBeginHostWrite_nid_postfix(pointer, 0); + Require(KernelProtection(region.base) == PROT_READ, "an empty host write unprotects a page"); + Require(region.Collect().empty(), "an empty host write reports a dirty page"); + GuestWriteWatchEndHostWrite_nid_postfix(pointer, 0); + + GuestWriteWatchBeginHostWrite_nid_postfix(pointer, 1); + GuestWriteWatchEndHostWrite_nid_postfix(pointer, 0); + Require(region.Collect() == All(1), "an active host write is not reported"); + Require(KernelProtection(region.base) == (PROT_READ | PROT_WRITE), "an empty end host write releases an active host write"); + GuestWriteWatchEndHostWrite_nid_postfix(pointer, 1); + Require(region.Collect() == All(1), "a finished host write is not reported"); + Require(region.Collect().empty(), "a finished host write is reported again"); + + GuestWriteWatchProtectionChanged_nid_postfix(pointer, 0, PROT_NONE); + std::uintptr_t runEnd = 0; + Require(GuestWriteWatchProtection_nid_postfix(region.Address(0), region.Address(1), &runEnd) == (PROT_READ | PROT_WRITE), "an empty protection change changes the recorded protection"); + Require(region.Collect().empty(), "an empty protection change reports a dirty page"); + region.base[offset] = 1; + Require(region.Collect() == All(1), "an empty range operation prevents a later write from being tracked"); + Require(GuestWriteWatchUnregister_nid_postfix(region.base, region.bytes), "cannot unregister the test region"); + } +} + void CheckProtection() { Region region(6); std::uintptr_t runEnd = 0; @@ -232,7 +269,7 @@ void CheckThreads() { int main(int argc, char** argv) { try { - Require(argc == 2, "expected collect, fresh, armed, register, mapped, host, protection or threads"); + Require(argc == 2, "expected collect, fresh, armed, register, mapped, host, empty, protection or threads"); const std::string_view mode(argv[1]); if (mode == "collect") CheckCollect(); else if (mode == "fresh") CheckFresh(); @@ -240,6 +277,7 @@ int main(int argc, char** argv) { else if (mode == "register") CheckRegisterAgain(); else if (mode == "mapped") CheckRegisterMapped(); else if (mode == "host") CheckHostWrite(); + else if (mode == "empty") CheckEmptyRanges(); else if (mode == "protection") CheckProtection(); else if (mode == "threads") CheckThreads(); else throw std::runtime_error("unknown write watch test"); diff --git a/core/libs/tests/write_watch/CMakeLists.txt b/core/libs/tests/write_watch/CMakeLists.txt index 0824ee5009..7e55f2bb41 100644 --- a/core/libs/tests/write_watch/CMakeLists.txt +++ b/core/libs/tests/write_watch/CMakeLists.txt @@ -15,7 +15,7 @@ set_target_properties(write_watch_tests PROPERTIES CXX_EXTENSIONS OFF RUNTIME_OUTPUT_DIRECTORY "$<1:${ANYPS5_TEST_OUTPUT_DIRECTORY}>") -foreach(mode collect fresh armed register mapped host protection threads) +foreach(mode collect fresh armed register mapped host empty protection threads) add_test(NAME write_watch_${mode} COMMAND write_watch_tests ${mode}) set_tests_properties(write_watch_${mode} PROPERTIES TIMEOUT 30) endforeach() From efde691a45078714f100ddd96bce8560c45668ed Mon Sep 17 00:00:00 2001 From: GalaxyGamingFR <153150716+GalaxyGamingFR@users.noreply.github.com> Date: Wed, 14 Oct 2026 02:32:58 -0700 Subject: [PATCH 0039/1000] fix(conventions): accept Depends on answered as a list The pr-template check only accepted "Depends on: #N" or "none" on the same line, so a checklist answering it as a bulleted list failed CI although pr_overlap reads exactly that form (test_pr_overlap_dependencies covers "Depends on: - #2 - #3"). Allow one list marker before the first answer; an empty answer is still reported. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_012zyXkUzTZGh3nBwNMtrLaW --- tools/check_conventions.py | 2 +- tools/tests/test_check_conventions_comments.py | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/tools/check_conventions.py b/tools/check_conventions.py index 61b187c984..4f60f34740 100644 --- a/tools/check_conventions.py +++ b/tools/check_conventions.py @@ -219,7 +219,7 @@ class Check: self.report("pr-template", "", 0, f"unchecked: {item.strip()}") if checklist and not re.search(r"AI-assisted:\s*(yes|no)\b", checklist, re.I): self.report("pr-template", "", 0, "answer AI-assisted with yes or no") - if checklist and not re.search(r"Depends on:\s*(#\d+|none)\b", checklist, re.I): + if checklist and not re.search(r"Depends on:\s*(?:[-*+]\s+)?(#\d+|none)\b", checklist, re.I): self.report("pr-template", "", 0, "fill Depends on with #N or none") diff --git a/tools/tests/test_check_conventions_comments.py b/tools/tests/test_check_conventions_comments.py index 25e0b1f871..1c084ad5b0 100644 --- a/tools/tests/test_check_conventions_comments.py +++ b/tools/tests/test_check_conventions_comments.py @@ -381,6 +381,16 @@ class PullRequestTemplateTests(unittest.TestCase): def test_checked_item_is_accepted(self): self.assertEqual(self.findings("- [x] all"), []) + def test_depends_on_answered_as_a_list_is_accepted(self): + for answer in ("\n - #3\n - #4", "\r\n - #3\r\n - #4", "\n- #3", "\n * none"): + with self.subTest(answer=answer): + body = f"### What\nx\n### Tested\nLinux\n### Checklist\n- [x] Depends on:{answer}\n- [x] AI-assisted: no\n" + self.assertEqual(self.body_findings(body), []) + + def test_depends_on_left_empty_before_the_next_item_is_reported(self): + body = "### What\nx\n### Tested\nLinux\n### Checklist\n- [x] Depends on:\n- [x] AI-assisted: no\n" + self.assertEqual(self.body_findings(body), [("pr-template", "", 0, "fill Depends on with #N or none")]) + def test_subsections_stay_inside_their_section(self): body = "### What\nx\n### Tested\n#### Linux\nctest\n#### Windows\nctest\n### Checklist\n- [x] Depends on: none\n- [x] AI-assisted: no\n" self.assertEqual(self.body_findings(body), []) From 8e812f4b738672aaed5509b5d8a28efaa50e008f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexej=20Szpev=C3=A1k?= Date: Wed, 14 Oct 2026 11:32:05 +0200 Subject: [PATCH 0040/1000] fix(relinker): report RIP-relative operands of far indirect CALL and JMP DecodeInstruction returned before reading the ModRM of FF /3 and FF /5, so a far CALL or JMP through a RIP-relative pointer had no RIP-relative displacement. A stub that moved such a JMP copied the displacement without relocating it. The decoder was compared with iced-x86 (AMD decoding) over 3.99 million encodings: every opcode of the legacy, 0F, 0F38 and 0F3A maps under 34 prefix combinations and the VEX 0F, 0F38 and 0F3A maps, each with 48 ModRM forms. Apart from this, it agrees on length, RIP-relative offset, control flow kind and branch target for every valid Zen 2 instruction except near branches with a 66 prefix, recorded as technical debt, and kernel-only returns, which it treats as sequential. Add the comparison's metadata as cases: one RIP-relative encoding per opcode, map and immediate size, every FF /r form, and the branch forms with their flow kind and target. The existing cases check lengths only; a loop or jrcxz decoded as an unconditional jump passed the whole suite. --- .../codegen/src/x86/X64InstructionDecoder.cpp | 12 +- .../codegen/tests/DecoderLengthTests.cpp | 22 + .../codegen/tests/DecoderMetadataCases.hpp | 792 ++++++++++++++++++ docs/dev/debt/relinker.md | 1 + 4 files changed, 821 insertions(+), 6 deletions(-) create mode 100644 core/relinker/codegen/tests/DecoderMetadataCases.hpp diff --git a/core/relinker/codegen/src/x86/X64InstructionDecoder.cpp b/core/relinker/codegen/src/x86/X64InstructionDecoder.cpp index f905d2df43..a3f7f3962d 100644 --- a/core/relinker/codegen/src/x86/X64InstructionDecoder.cpp +++ b/core/relinker/codegen/src/x86/X64InstructionDecoder.cpp @@ -488,12 +488,12 @@ DecodedInstructionInfo X64InstructionDecoder::DecodeInstruction( } return info; } - if (reg == 3) { - info.FlowKind = ControlFlowKind::IndirectCall; - return info; - } - if (reg == 5) { - info.FlowKind = ControlFlowKind::IndirectJump; + if (reg == 3 || reg == 5) { + if (mod == ModRmModIndirect && rm == ModRmRmRipRelative) { + info.HasRipRelativeDisp = true; + info.RipRelativeDispOffset = pos + 1; + } + info.FlowKind = reg == 3 ? ControlFlowKind::IndirectCall : ControlFlowKind::IndirectJump; return info; } } diff --git a/core/relinker/codegen/tests/DecoderLengthTests.cpp b/core/relinker/codegen/tests/DecoderLengthTests.cpp index e141e1539e..54dd90df2b 100644 --- a/core/relinker/codegen/tests/DecoderLengthTests.cpp +++ b/core/relinker/codegen/tests/DecoderLengthTests.cpp @@ -1,4 +1,5 @@ #include "DecoderLengthCases.hpp" +#include "DecoderMetadataCases.hpp" #include #include #include @@ -77,6 +78,27 @@ int main() { } catch (const Codegen::CodegenException&) { } } + for (const auto& testCase : kDecoderRipRelativeCases) { + const std::string name = testCase.Bytes; + auto bytes = parse(testCase.Bytes); + require(bytes.size() == testCase.Length, "RIP-relative case " + name + " does not hold its own length"); + bytes.insert(bytes.end(), 15, 0x90); + require(decodedLength(decoder, bytes, name) == testCase.Length, "Wrong length for " + name); + const auto info = decoder.DecodeInstruction(bytes.data(), bytes.size()); + require(info.HasRipRelativeDisp, "RIP-relative operand of " + name + " was not reported"); + require(info.RipRelativeDispOffset == testCase.DisplacementOffset, "Wrong RIP-relative displacement offset for " + name); + } + for (const auto& testCase : kDecoderBranchCases) { + const std::string name = testCase.Bytes; + auto bytes = parse(testCase.Bytes); + require(bytes.size() == testCase.Length, "Branch case " + name + " does not hold its own length"); + bytes.insert(bytes.end(), 15, 0x90); + const auto info = decoder.DecodeInstruction(bytes.data(), bytes.size()); + require(info.Length == testCase.Length, "Wrong length for " + name); + require(info.FlowKind == testCase.FlowKind, "Wrong control flow kind for " + name); + require(info.HasBranchTarget == testCase.HasBranchTarget, "Wrong branch target presence for " + name); + require(!testCase.HasBranchTarget || info.BranchDisp == testCase.BranchDisp, "Wrong branch displacement for " + name); + } const auto threeDNow = parse("0f0f0d11223344a6"); const auto threeDNowInfo = decoder.DecodeInstruction(threeDNow.data(), threeDNow.size()); require(threeDNowInfo.HasModRm && threeDNowInfo.ModRmByte == 0x0d, "3DNow! ModRM was not reported"); diff --git a/core/relinker/codegen/tests/DecoderMetadataCases.hpp b/core/relinker/codegen/tests/DecoderMetadataCases.hpp new file mode 100644 index 0000000000..c685b184ea --- /dev/null +++ b/core/relinker/codegen/tests/DecoderMetadataCases.hpp @@ -0,0 +1,792 @@ +#ifndef CODEGEN_TESTS_DECODERMETADATACASES_HPP +#define CODEGEN_TESTS_DECODERMETADATACASES_HPP + +#include +#include +#include + +struct DecoderRipRelativeCase { + const char* Bytes; + std::size_t Length; + std::size_t DisplacementOffset; +}; + +struct DecoderBranchCase { + const char* Bytes; + std::size_t Length; + Codegen::ControlFlowKind FlowKind; + bool HasBranchTarget; + std::int64_t BranchDisp; +}; + +inline constexpr DecoderRipRelativeCase kDecoderRipRelativeCases[] = { + {"ff0511111111", 6, 2}, + {"ff0d11111111", 6, 2}, + {"ff1511111111", 6, 2}, + {"ff1d11111111", 6, 2}, + {"ff2511111111", 6, 2}, + {"ff2d11111111", 6, 2}, + {"ff3511111111", 6, 2}, + {"000511111111", 6, 2}, + {"010511111111", 6, 2}, + {"020511111111", 6, 2}, + {"030511111111", 6, 2}, + {"080511111111", 6, 2}, + {"090511111111", 6, 2}, + {"0a0511111111", 6, 2}, + {"0b0511111111", 6, 2}, + {"100511111111", 6, 2}, + {"110511111111", 6, 2}, + {"120511111111", 6, 2}, + {"130511111111", 6, 2}, + {"180511111111", 6, 2}, + {"190511111111", 6, 2}, + {"1a0511111111", 6, 2}, + {"1b0511111111", 6, 2}, + {"200511111111", 6, 2}, + {"210511111111", 6, 2}, + {"220511111111", 6, 2}, + {"230511111111", 6, 2}, + {"280511111111", 6, 2}, + {"290511111111", 6, 2}, + {"2a0511111111", 6, 2}, + {"2b0511111111", 6, 2}, + {"300511111111", 6, 2}, + {"310511111111", 6, 2}, + {"320511111111", 6, 2}, + {"330511111111", 6, 2}, + {"380511111111", 6, 2}, + {"390511111111", 6, 2}, + {"3a0511111111", 6, 2}, + {"3b0511111111", 6, 2}, + {"630511111111", 6, 2}, + {"666905111111111111", 9, 3}, + {"69051111111111111111", 10, 2}, + {"6b051111111111", 7, 2}, + {"80051111111111", 7, 2}, + {"668105111111111111", 9, 3}, + {"81051111111111111111", 10, 2}, + {"83051111111111", 7, 2}, + {"840511111111", 6, 2}, + {"850511111111", 6, 2}, + {"860511111111", 6, 2}, + {"870511111111", 6, 2}, + {"880511111111", 6, 2}, + {"890511111111", 6, 2}, + {"8a0511111111", 6, 2}, + {"8b0511111111", 6, 2}, + {"8c0511111111", 6, 2}, + {"8d0511111111", 6, 2}, + {"8e0511111111", 6, 2}, + {"c0051111111111", 7, 2}, + {"c1051111111111", 7, 2}, + {"c6051111111111", 7, 2}, + {"66c705111111111111", 9, 3}, + {"c7051111111111111111", 10, 2}, + {"d00511111111", 6, 2}, + {"d10511111111", 6, 2}, + {"d20511111111", 6, 2}, + {"d30511111111", 6, 2}, + {"d80511111111", 6, 2}, + {"d90511111111", 6, 2}, + {"da0511111111", 6, 2}, + {"db0511111111", 6, 2}, + {"dc0511111111", 6, 2}, + {"dd0511111111", 6, 2}, + {"de0511111111", 6, 2}, + {"df0511111111", 6, 2}, + {"f61511111111", 6, 2}, + {"f6051111111111", 7, 2}, + {"f71511111111", 6, 2}, + {"66f705111111111111", 9, 3}, + {"f7051111111111111111", 10, 2}, + {"fe0511111111", 6, 2}, + {"0f000511111111", 7, 3}, + {"0f010511111111", 7, 3}, + {"0f020511111111", 7, 3}, + {"0f030511111111", 7, 3}, + {"0f0d0511111111", 7, 3}, + {"0f100511111111", 7, 3}, + {"0f110511111111", 7, 3}, + {"0f120511111111", 7, 3}, + {"0f130511111111", 7, 3}, + {"0f140511111111", 7, 3}, + {"0f150511111111", 7, 3}, + {"0f160511111111", 7, 3}, + {"0f170511111111", 7, 3}, + {"0f180511111111", 7, 3}, + {"0f190511111111", 7, 3}, + {"0f1a0511111111", 7, 3}, + {"0f1b0511111111", 7, 3}, + {"0f1c0511111111", 7, 3}, + {"0f1d0511111111", 7, 3}, + {"0f1e0511111111", 7, 3}, + {"0f1f0511111111", 7, 3}, + {"0f280511111111", 7, 3}, + {"0f290511111111", 7, 3}, + {"0f2a0511111111", 7, 3}, + {"0f2b0511111111", 7, 3}, + {"0f2c0511111111", 7, 3}, + {"0f2d0511111111", 7, 3}, + {"0f2e0511111111", 7, 3}, + {"0f2f0511111111", 7, 3}, + {"0f400511111111", 7, 3}, + {"0f410511111111", 7, 3}, + {"0f420511111111", 7, 3}, + {"0f430511111111", 7, 3}, + {"0f440511111111", 7, 3}, + {"0f450511111111", 7, 3}, + {"0f460511111111", 7, 3}, + {"0f470511111111", 7, 3}, + {"0f480511111111", 7, 3}, + {"0f490511111111", 7, 3}, + {"0f4a0511111111", 7, 3}, + {"0f4b0511111111", 7, 3}, + {"0f4c0511111111", 7, 3}, + {"0f4d0511111111", 7, 3}, + {"0f4e0511111111", 7, 3}, + {"0f4f0511111111", 7, 3}, + {"0f510511111111", 7, 3}, + {"0f520511111111", 7, 3}, + {"0f530511111111", 7, 3}, + {"0f540511111111", 7, 3}, + {"0f550511111111", 7, 3}, + {"0f560511111111", 7, 3}, + {"0f570511111111", 7, 3}, + {"0f580511111111", 7, 3}, + {"0f590511111111", 7, 3}, + {"0f5a0511111111", 7, 3}, + {"0f5b0511111111", 7, 3}, + {"0f5c0511111111", 7, 3}, + {"0f5d0511111111", 7, 3}, + {"0f5e0511111111", 7, 3}, + {"0f5f0511111111", 7, 3}, + {"0f600511111111", 7, 3}, + {"0f610511111111", 7, 3}, + {"0f620511111111", 7, 3}, + {"0f630511111111", 7, 3}, + {"0f640511111111", 7, 3}, + {"0f650511111111", 7, 3}, + {"0f660511111111", 7, 3}, + {"0f670511111111", 7, 3}, + {"0f680511111111", 7, 3}, + {"0f690511111111", 7, 3}, + {"0f6a0511111111", 7, 3}, + {"0f6b0511111111", 7, 3}, + {"660f6c0511111111", 8, 4}, + {"660f6d0511111111", 8, 4}, + {"0f6e0511111111", 7, 3}, + {"0f6f0511111111", 7, 3}, + {"0f70051111111111", 8, 3}, + {"0f740511111111", 7, 3}, + {"0f750511111111", 7, 3}, + {"0f760511111111", 7, 3}, + {"660f7c0511111111", 8, 4}, + {"660f7d0511111111", 8, 4}, + {"0f7e0511111111", 7, 3}, + {"0f7f0511111111", 7, 3}, + {"0f900511111111", 7, 3}, + {"0f910511111111", 7, 3}, + {"0f920511111111", 7, 3}, + {"0f930511111111", 7, 3}, + {"0f940511111111", 7, 3}, + {"0f950511111111", 7, 3}, + {"0f960511111111", 7, 3}, + {"0f970511111111", 7, 3}, + {"0f980511111111", 7, 3}, + {"0f990511111111", 7, 3}, + {"0f9a0511111111", 7, 3}, + {"0f9b0511111111", 7, 3}, + {"0f9c0511111111", 7, 3}, + {"0f9d0511111111", 7, 3}, + {"0f9e0511111111", 7, 3}, + {"0f9f0511111111", 7, 3}, + {"0fa30511111111", 7, 3}, + {"0fa4051111111111", 8, 3}, + {"0fa50511111111", 7, 3}, + {"0fab0511111111", 7, 3}, + {"0fac051111111111", 8, 3}, + {"0fad0511111111", 7, 3}, + {"0fae0511111111", 7, 3}, + {"0faf0511111111", 7, 3}, + {"0fb00511111111", 7, 3}, + {"0fb10511111111", 7, 3}, + {"0fb20511111111", 7, 3}, + {"0fb30511111111", 7, 3}, + {"0fb40511111111", 7, 3}, + {"0fb50511111111", 7, 3}, + {"0fb60511111111", 7, 3}, + {"0fb70511111111", 7, 3}, + {"f30fb80511111111", 8, 4}, + {"0fb90511111111", 7, 3}, + {"0fba251111111111", 8, 3}, + {"0fbb0511111111", 7, 3}, + {"0fbc0511111111", 7, 3}, + {"0fbd0511111111", 7, 3}, + {"0fbe0511111111", 7, 3}, + {"0fbf0511111111", 7, 3}, + {"0fc00511111111", 7, 3}, + {"0fc10511111111", 7, 3}, + {"0fc2051111111111", 8, 3}, + {"0fc30511111111", 7, 3}, + {"0fc4051111111111", 8, 3}, + {"0fc6051111111111", 8, 3}, + {"0fc70d11111111", 7, 3}, + {"660fd00511111111", 8, 4}, + {"0fd10511111111", 7, 3}, + {"0fd20511111111", 7, 3}, + {"0fd30511111111", 7, 3}, + {"0fd40511111111", 7, 3}, + {"0fd50511111111", 7, 3}, + {"660fd60511111111", 8, 4}, + {"0fd80511111111", 7, 3}, + {"0fd90511111111", 7, 3}, + {"0fda0511111111", 7, 3}, + {"0fdb0511111111", 7, 3}, + {"0fdc0511111111", 7, 3}, + {"0fdd0511111111", 7, 3}, + {"0fde0511111111", 7, 3}, + {"0fdf0511111111", 7, 3}, + {"0fe00511111111", 7, 3}, + {"0fe10511111111", 7, 3}, + {"0fe20511111111", 7, 3}, + {"0fe30511111111", 7, 3}, + {"0fe40511111111", 7, 3}, + {"0fe50511111111", 7, 3}, + {"660fe60511111111", 8, 4}, + {"0fe70511111111", 7, 3}, + {"0fe80511111111", 7, 3}, + {"0fe90511111111", 7, 3}, + {"0fea0511111111", 7, 3}, + {"0feb0511111111", 7, 3}, + {"0fec0511111111", 7, 3}, + {"0fed0511111111", 7, 3}, + {"0fee0511111111", 7, 3}, + {"0fef0511111111", 7, 3}, + {"f20ff00511111111", 8, 4}, + {"0ff10511111111", 7, 3}, + {"0ff20511111111", 7, 3}, + {"0ff30511111111", 7, 3}, + {"0ff40511111111", 7, 3}, + {"0ff50511111111", 7, 3}, + {"0ff60511111111", 7, 3}, + {"0ff80511111111", 7, 3}, + {"0ff90511111111", 7, 3}, + {"0ffa0511111111", 7, 3}, + {"0ffb0511111111", 7, 3}, + {"0ffc0511111111", 7, 3}, + {"0ffd0511111111", 7, 3}, + {"0ffe0511111111", 7, 3}, + {"0f38000511111111", 8, 4}, + {"0f38010511111111", 8, 4}, + {"0f38020511111111", 8, 4}, + {"0f38030511111111", 8, 4}, + {"0f38040511111111", 8, 4}, + {"0f38050511111111", 8, 4}, + {"0f38060511111111", 8, 4}, + {"0f38070511111111", 8, 4}, + {"0f38080511111111", 8, 4}, + {"0f38090511111111", 8, 4}, + {"0f380a0511111111", 8, 4}, + {"0f380b0511111111", 8, 4}, + {"660f38100511111111", 9, 5}, + {"660f38140511111111", 9, 5}, + {"660f38150511111111", 9, 5}, + {"660f38170511111111", 9, 5}, + {"0f381c0511111111", 8, 4}, + {"0f381d0511111111", 8, 4}, + {"0f381e0511111111", 8, 4}, + {"660f38200511111111", 9, 5}, + {"660f38210511111111", 9, 5}, + {"660f38220511111111", 9, 5}, + {"660f38230511111111", 9, 5}, + {"660f38240511111111", 9, 5}, + {"660f38250511111111", 9, 5}, + {"660f38280511111111", 9, 5}, + {"660f38290511111111", 9, 5}, + {"660f382a0511111111", 9, 5}, + {"660f382b0511111111", 9, 5}, + {"660f38300511111111", 9, 5}, + {"660f38310511111111", 9, 5}, + {"660f38320511111111", 9, 5}, + {"660f38330511111111", 9, 5}, + {"660f38340511111111", 9, 5}, + {"660f38350511111111", 9, 5}, + {"660f38370511111111", 9, 5}, + {"660f38380511111111", 9, 5}, + {"660f38390511111111", 9, 5}, + {"660f383a0511111111", 9, 5}, + {"660f383b0511111111", 9, 5}, + {"660f383c0511111111", 9, 5}, + {"660f383d0511111111", 9, 5}, + {"660f383e0511111111", 9, 5}, + {"660f383f0511111111", 9, 5}, + {"660f38400511111111", 9, 5}, + {"660f38410511111111", 9, 5}, + {"660f38820511111111", 9, 5}, + {"0f38c80511111111", 8, 4}, + {"0f38c90511111111", 8, 4}, + {"0f38ca0511111111", 8, 4}, + {"0f38cb0511111111", 8, 4}, + {"0f38cc0511111111", 8, 4}, + {"0f38cd0511111111", 8, 4}, + {"f30f38d80511111111", 9, 5}, + {"660f38db0511111111", 9, 5}, + {"660f38dc0511111111", 9, 5}, + {"660f38dd0511111111", 9, 5}, + {"660f38de0511111111", 9, 5}, + {"660f38df0511111111", 9, 5}, + {"0f38f00511111111", 8, 4}, + {"0f38f10511111111", 8, 4}, + {"660f38f60511111111", 9, 5}, + {"660f3a08051111111111", 10, 5}, + {"660f3a09051111111111", 10, 5}, + {"660f3a0a051111111111", 10, 5}, + {"660f3a0b051111111111", 10, 5}, + {"660f3a0c051111111111", 10, 5}, + {"660f3a0d051111111111", 10, 5}, + {"660f3a0e051111111111", 10, 5}, + {"0f3a0f051111111111", 9, 4}, + {"660f3a14051111111111", 10, 5}, + {"660f3a15051111111111", 10, 5}, + {"660f3a16051111111111", 10, 5}, + {"660f3a17051111111111", 10, 5}, + {"660f3a20051111111111", 10, 5}, + {"660f3a21051111111111", 10, 5}, + {"660f3a22051111111111", 10, 5}, + {"660f3a40051111111111", 10, 5}, + {"660f3a41051111111111", 10, 5}, + {"660f3a42051111111111", 10, 5}, + {"660f3a44051111111111", 10, 5}, + {"660f3a60051111111111", 10, 5}, + {"660f3a61051111111111", 10, 5}, + {"660f3a62051111111111", 10, 5}, + {"660f3a63051111111111", 10, 5}, + {"0f3acc051111111111", 9, 4}, + {"660f3adf051111111111", 10, 5}, + {"c5f8100511111111", 8, 4}, + {"c5f8110511111111", 8, 4}, + {"c5f8120511111111", 8, 4}, + {"c5f8130511111111", 8, 4}, + {"c5f8140511111111", 8, 4}, + {"c5f8150511111111", 8, 4}, + {"c5f8160511111111", 8, 4}, + {"c5f8170511111111", 8, 4}, + {"c5f8280511111111", 8, 4}, + {"c5f8290511111111", 8, 4}, + {"c5fa2a0511111111", 8, 4}, + {"c5f82b0511111111", 8, 4}, + {"c5fa2c0511111111", 8, 4}, + {"c5fa2d0511111111", 8, 4}, + {"c5f82e0511111111", 8, 4}, + {"c5f82f0511111111", 8, 4}, + {"c5f8510511111111", 8, 4}, + {"c5f8520511111111", 8, 4}, + {"c5f8530511111111", 8, 4}, + {"c5f8540511111111", 8, 4}, + {"c5f8550511111111", 8, 4}, + {"c5f8560511111111", 8, 4}, + {"c5f8570511111111", 8, 4}, + {"c5f8580511111111", 8, 4}, + {"c5f8590511111111", 8, 4}, + {"c5f85a0511111111", 8, 4}, + {"c5f85b0511111111", 8, 4}, + {"c5f85c0511111111", 8, 4}, + {"c5f85d0511111111", 8, 4}, + {"c5f85e0511111111", 8, 4}, + {"c5f85f0511111111", 8, 4}, + {"c5f9600511111111", 8, 4}, + {"c5f9610511111111", 8, 4}, + {"c5f9620511111111", 8, 4}, + {"c5f9630511111111", 8, 4}, + {"c5f9640511111111", 8, 4}, + {"c5f9650511111111", 8, 4}, + {"c5f9660511111111", 8, 4}, + {"c5f9670511111111", 8, 4}, + {"c5f9680511111111", 8, 4}, + {"c5f9690511111111", 8, 4}, + {"c5f96a0511111111", 8, 4}, + {"c5f96b0511111111", 8, 4}, + {"c5f96c0511111111", 8, 4}, + {"c5f96d0511111111", 8, 4}, + {"c5f96e0511111111", 8, 4}, + {"c5f96f0511111111", 8, 4}, + {"c5f970051111111111", 9, 4}, + {"c5f9740511111111", 8, 4}, + {"c5f9750511111111", 8, 4}, + {"c5f9760511111111", 8, 4}, + {"c5f97c0511111111", 8, 4}, + {"c5f97d0511111111", 8, 4}, + {"c5f97e0511111111", 8, 4}, + {"c5f97f0511111111", 8, 4}, + {"c5f8ae1511111111", 8, 4}, + {"c5f8c2051111111111", 9, 4}, + {"c5f9c4051111111111", 9, 4}, + {"c5f8c6051111111111", 9, 4}, + {"c5f9d00511111111", 8, 4}, + {"c5f9d10511111111", 8, 4}, + {"c5f9d20511111111", 8, 4}, + {"c5f9d30511111111", 8, 4}, + {"c5f9d40511111111", 8, 4}, + {"c5f9d50511111111", 8, 4}, + {"c5f9d60511111111", 8, 4}, + {"c5f9d80511111111", 8, 4}, + {"c5f9d90511111111", 8, 4}, + {"c5f9da0511111111", 8, 4}, + {"c5f9db0511111111", 8, 4}, + {"c5f9dc0511111111", 8, 4}, + {"c5f9dd0511111111", 8, 4}, + {"c5f9de0511111111", 8, 4}, + {"c5f9df0511111111", 8, 4}, + {"c5f9e00511111111", 8, 4}, + {"c5f9e10511111111", 8, 4}, + {"c5f9e20511111111", 8, 4}, + {"c5f9e30511111111", 8, 4}, + {"c5f9e40511111111", 8, 4}, + {"c5f9e50511111111", 8, 4}, + {"c5f9e60511111111", 8, 4}, + {"c5f9e70511111111", 8, 4}, + {"c5f9e80511111111", 8, 4}, + {"c5f9e90511111111", 8, 4}, + {"c5f9ea0511111111", 8, 4}, + {"c5f9eb0511111111", 8, 4}, + {"c5f9ec0511111111", 8, 4}, + {"c5f9ed0511111111", 8, 4}, + {"c5f9ee0511111111", 8, 4}, + {"c5f9ef0511111111", 8, 4}, + {"c5fbf00511111111", 8, 4}, + {"c5f9f10511111111", 8, 4}, + {"c5f9f20511111111", 8, 4}, + {"c5f9f30511111111", 8, 4}, + {"c5f9f40511111111", 8, 4}, + {"c5f9f50511111111", 8, 4}, + {"c5f9f60511111111", 8, 4}, + {"c5f9f80511111111", 8, 4}, + {"c5f9f90511111111", 8, 4}, + {"c5f9fa0511111111", 8, 4}, + {"c5f9fb0511111111", 8, 4}, + {"c5f9fc0511111111", 8, 4}, + {"c5f9fd0511111111", 8, 4}, + {"c5f9fe0511111111", 8, 4}, + {"c4e178100511111111", 9, 5}, + {"c4e178110511111111", 9, 5}, + {"c4e178120511111111", 9, 5}, + {"c4e178130511111111", 9, 5}, + {"c4e178140511111111", 9, 5}, + {"c4e178150511111111", 9, 5}, + {"c4e178160511111111", 9, 5}, + {"c4e178170511111111", 9, 5}, + {"c4e178280511111111", 9, 5}, + {"c4e178290511111111", 9, 5}, + {"c4e17a2a0511111111", 9, 5}, + {"c4e1782b0511111111", 9, 5}, + {"c4e17a2c0511111111", 9, 5}, + {"c4e17a2d0511111111", 9, 5}, + {"c4e1782e0511111111", 9, 5}, + {"c4e1782f0511111111", 9, 5}, + {"c4e178510511111111", 9, 5}, + {"c4e178520511111111", 9, 5}, + {"c4e178530511111111", 9, 5}, + {"c4e178540511111111", 9, 5}, + {"c4e178550511111111", 9, 5}, + {"c4e178560511111111", 9, 5}, + {"c4e178570511111111", 9, 5}, + {"c4e178580511111111", 9, 5}, + {"c4e178590511111111", 9, 5}, + {"c4e1785a0511111111", 9, 5}, + {"c4e1785b0511111111", 9, 5}, + {"c4e1785c0511111111", 9, 5}, + {"c4e1785d0511111111", 9, 5}, + {"c4e1785e0511111111", 9, 5}, + {"c4e1785f0511111111", 9, 5}, + {"c4e179600511111111", 9, 5}, + {"c4e179610511111111", 9, 5}, + {"c4e179620511111111", 9, 5}, + {"c4e179630511111111", 9, 5}, + {"c4e179640511111111", 9, 5}, + {"c4e179650511111111", 9, 5}, + {"c4e179660511111111", 9, 5}, + {"c4e179670511111111", 9, 5}, + {"c4e179680511111111", 9, 5}, + {"c4e179690511111111", 9, 5}, + {"c4e1796a0511111111", 9, 5}, + {"c4e1796b0511111111", 9, 5}, + {"c4e1796c0511111111", 9, 5}, + {"c4e1796d0511111111", 9, 5}, + {"c4e1796e0511111111", 9, 5}, + {"c4e1796f0511111111", 9, 5}, + {"c4e17970051111111111", 10, 5}, + {"c4e179740511111111", 9, 5}, + {"c4e179750511111111", 9, 5}, + {"c4e179760511111111", 9, 5}, + {"c4e1797c0511111111", 9, 5}, + {"c4e1797d0511111111", 9, 5}, + {"c4e1797e0511111111", 9, 5}, + {"c4e1797f0511111111", 9, 5}, + {"c4e178ae1511111111", 9, 5}, + {"c4e178c2051111111111", 10, 5}, + {"c4e179c4051111111111", 10, 5}, + {"c4e178c6051111111111", 10, 5}, + {"c4e179d00511111111", 9, 5}, + {"c4e179d10511111111", 9, 5}, + {"c4e179d20511111111", 9, 5}, + {"c4e179d30511111111", 9, 5}, + {"c4e179d40511111111", 9, 5}, + {"c4e179d50511111111", 9, 5}, + {"c4e179d60511111111", 9, 5}, + {"c4e179d80511111111", 9, 5}, + {"c4e179d90511111111", 9, 5}, + {"c4e179da0511111111", 9, 5}, + {"c4e179db0511111111", 9, 5}, + {"c4e179dc0511111111", 9, 5}, + {"c4e179dd0511111111", 9, 5}, + {"c4e179de0511111111", 9, 5}, + {"c4e179df0511111111", 9, 5}, + {"c4e179e00511111111", 9, 5}, + {"c4e179e10511111111", 9, 5}, + {"c4e179e20511111111", 9, 5}, + {"c4e179e30511111111", 9, 5}, + {"c4e179e40511111111", 9, 5}, + {"c4e179e50511111111", 9, 5}, + {"c4e179e60511111111", 9, 5}, + {"c4e179e70511111111", 9, 5}, + {"c4e179e80511111111", 9, 5}, + {"c4e179e90511111111", 9, 5}, + {"c4e179ea0511111111", 9, 5}, + {"c4e179eb0511111111", 9, 5}, + {"c4e179ec0511111111", 9, 5}, + {"c4e179ed0511111111", 9, 5}, + {"c4e179ee0511111111", 9, 5}, + {"c4e179ef0511111111", 9, 5}, + {"c4e17bf00511111111", 9, 5}, + {"c4e179f10511111111", 9, 5}, + {"c4e179f20511111111", 9, 5}, + {"c4e179f30511111111", 9, 5}, + {"c4e179f40511111111", 9, 5}, + {"c4e179f50511111111", 9, 5}, + {"c4e179f60511111111", 9, 5}, + {"c4e179f80511111111", 9, 5}, + {"c4e179f90511111111", 9, 5}, + {"c4e179fa0511111111", 9, 5}, + {"c4e179fb0511111111", 9, 5}, + {"c4e179fc0511111111", 9, 5}, + {"c4e179fd0511111111", 9, 5}, + {"c4e179fe0511111111", 9, 5}, + {"c4e279000511111111", 9, 5}, + {"c4e279010511111111", 9, 5}, + {"c4e279020511111111", 9, 5}, + {"c4e279030511111111", 9, 5}, + {"c4e279040511111111", 9, 5}, + {"c4e279050511111111", 9, 5}, + {"c4e279060511111111", 9, 5}, + {"c4e279070511111111", 9, 5}, + {"c4e279080511111111", 9, 5}, + {"c4e279090511111111", 9, 5}, + {"c4e2790a0511111111", 9, 5}, + {"c4e2790b0511111111", 9, 5}, + {"c4e2790c0511111111", 9, 5}, + {"c4e2790d0511111111", 9, 5}, + {"c4e2790e0511111111", 9, 5}, + {"c4e2790f0511111111", 9, 5}, + {"c4e279130511111111", 9, 5}, + {"c4e27d160511111111", 9, 5}, + {"c4e279170511111111", 9, 5}, + {"c4e279180511111111", 9, 5}, + {"c4e27d190511111111", 9, 5}, + {"c4e27d1a0511111111", 9, 5}, + {"c4e2791c0511111111", 9, 5}, + {"c4e2791d0511111111", 9, 5}, + {"c4e2791e0511111111", 9, 5}, + {"c4e279200511111111", 9, 5}, + {"c4e279210511111111", 9, 5}, + {"c4e279220511111111", 9, 5}, + {"c4e279230511111111", 9, 5}, + {"c4e279240511111111", 9, 5}, + {"c4e279250511111111", 9, 5}, + {"c4e279280511111111", 9, 5}, + {"c4e279290511111111", 9, 5}, + {"c4e2792a0511111111", 9, 5}, + {"c4e2792b0511111111", 9, 5}, + {"c4e2792c0511111111", 9, 5}, + {"c4e2792d0511111111", 9, 5}, + {"c4e2792e0511111111", 9, 5}, + {"c4e2792f0511111111", 9, 5}, + {"c4e279300511111111", 9, 5}, + {"c4e279310511111111", 9, 5}, + {"c4e279320511111111", 9, 5}, + {"c4e279330511111111", 9, 5}, + {"c4e279340511111111", 9, 5}, + {"c4e279350511111111", 9, 5}, + {"c4e27d360511111111", 9, 5}, + {"c4e279370511111111", 9, 5}, + {"c4e279380511111111", 9, 5}, + {"c4e279390511111111", 9, 5}, + {"c4e2793a0511111111", 9, 5}, + {"c4e2793b0511111111", 9, 5}, + {"c4e2793c0511111111", 9, 5}, + {"c4e2793d0511111111", 9, 5}, + {"c4e2793e0511111111", 9, 5}, + {"c4e2793f0511111111", 9, 5}, + {"c4e279400511111111", 9, 5}, + {"c4e279410511111111", 9, 5}, + {"c4e279450511111111", 9, 5}, + {"c4e279460511111111", 9, 5}, + {"c4e279470511111111", 9, 5}, + {"c4e279580511111111", 9, 5}, + {"c4e279590511111111", 9, 5}, + {"c4e27d5a0511111111", 9, 5}, + {"c4e279780511111111", 9, 5}, + {"c4e279790511111111", 9, 5}, + {"c4e2798c0511111111", 9, 5}, + {"c4e2798e0511111111", 9, 5}, + {"c4e279960511111111", 9, 5}, + {"c4e279970511111111", 9, 5}, + {"c4e279980511111111", 9, 5}, + {"c4e279990511111111", 9, 5}, + {"c4e2799a0511111111", 9, 5}, + {"c4e2799b0511111111", 9, 5}, + {"c4e2799c0511111111", 9, 5}, + {"c4e2799d0511111111", 9, 5}, + {"c4e2799e0511111111", 9, 5}, + {"c4e2799f0511111111", 9, 5}, + {"c4e279a60511111111", 9, 5}, + {"c4e279a70511111111", 9, 5}, + {"c4e279a80511111111", 9, 5}, + {"c4e279a90511111111", 9, 5}, + {"c4e279aa0511111111", 9, 5}, + {"c4e279ab0511111111", 9, 5}, + {"c4e279ac0511111111", 9, 5}, + {"c4e279ad0511111111", 9, 5}, + {"c4e279ae0511111111", 9, 5}, + {"c4e279af0511111111", 9, 5}, + {"c4e279b60511111111", 9, 5}, + {"c4e279b70511111111", 9, 5}, + {"c4e279b80511111111", 9, 5}, + {"c4e279b90511111111", 9, 5}, + {"c4e279ba0511111111", 9, 5}, + {"c4e279bb0511111111", 9, 5}, + {"c4e279bc0511111111", 9, 5}, + {"c4e279bd0511111111", 9, 5}, + {"c4e279be0511111111", 9, 5}, + {"c4e279bf0511111111", 9, 5}, + {"c4e279db0511111111", 9, 5}, + {"c4e279dc0511111111", 9, 5}, + {"c4e279dd0511111111", 9, 5}, + {"c4e279de0511111111", 9, 5}, + {"c4e279df0511111111", 9, 5}, + {"c4e278f20511111111", 9, 5}, + {"c4e278f30d11111111", 9, 5}, + {"c4e278f50511111111", 9, 5}, + {"c4e27bf60511111111", 9, 5}, + {"c4e278f70511111111", 9, 5}, + {"c4e3fd00051111111111", 10, 5}, + {"c4e3fd01051111111111", 10, 5}, + {"c4e37902051111111111", 10, 5}, + {"c4e37904051111111111", 10, 5}, + {"c4e37905051111111111", 10, 5}, + {"c4e37d06051111111111", 10, 5}, + {"c4e37908051111111111", 10, 5}, + {"c4e37909051111111111", 10, 5}, + {"c4e3790a051111111111", 10, 5}, + {"c4e3790b051111111111", 10, 5}, + {"c4e3790c051111111111", 10, 5}, + {"c4e3790d051111111111", 10, 5}, + {"c4e3790e051111111111", 10, 5}, + {"c4e3790f051111111111", 10, 5}, + {"c4e37914051111111111", 10, 5}, + {"c4e37915051111111111", 10, 5}, + {"c4e37916051111111111", 10, 5}, + {"c4e37917051111111111", 10, 5}, + {"c4e37d18051111111111", 10, 5}, + {"c4e37d19051111111111", 10, 5}, + {"c4e3791d051111111111", 10, 5}, + {"c4e37920051111111111", 10, 5}, + {"c4e37921051111111111", 10, 5}, + {"c4e37922051111111111", 10, 5}, + {"c4e37d38051111111111", 10, 5}, + {"c4e37d39051111111111", 10, 5}, + {"c4e37940051111111111", 10, 5}, + {"c4e37941051111111111", 10, 5}, + {"c4e37942051111111111", 10, 5}, + {"c4e37944051111111111", 10, 5}, + {"c4e37d46051111111111", 10, 5}, + {"c4e3794a051111111111", 10, 5}, + {"c4e3794b051111111111", 10, 5}, + {"c4e3794c051111111111", 10, 5}, + {"c4e37960051111111111", 10, 5}, + {"c4e37961051111111111", 10, 5}, + {"c4e37962051111111111", 10, 5}, + {"c4e37963051111111111", 10, 5}, + {"c4e379df051111111111", 10, 5}, + {"c4e37bf0051111111111", 10, 5}, +}; + +inline constexpr DecoderBranchCase kDecoderBranchCases[] = { + {"0f0b", 2, Codegen::ControlFlowKind::Trap, false, 0}, + {"0f80c0111111", 6, Codegen::ControlFlowKind::ConditionalBranch, true, 286331328}, + {"0f81c0111111", 6, Codegen::ControlFlowKind::ConditionalBranch, true, 286331328}, + {"0f82c0111111", 6, Codegen::ControlFlowKind::ConditionalBranch, true, 286331328}, + {"0f83c0111111", 6, Codegen::ControlFlowKind::ConditionalBranch, true, 286331328}, + {"0f84c0111111", 6, Codegen::ControlFlowKind::ConditionalBranch, true, 286331328}, + {"0f85c0111111", 6, Codegen::ControlFlowKind::ConditionalBranch, true, 286331328}, + {"0f86c0111111", 6, Codegen::ControlFlowKind::ConditionalBranch, true, 286331328}, + {"0f87c0111111", 6, Codegen::ControlFlowKind::ConditionalBranch, true, 286331328}, + {"0f88c0111111", 6, Codegen::ControlFlowKind::ConditionalBranch, true, 286331328}, + {"0f89c0111111", 6, Codegen::ControlFlowKind::ConditionalBranch, true, 286331328}, + {"0f8ac0111111", 6, Codegen::ControlFlowKind::ConditionalBranch, true, 286331328}, + {"0f8bc0111111", 6, Codegen::ControlFlowKind::ConditionalBranch, true, 286331328}, + {"0f8cc0111111", 6, Codegen::ControlFlowKind::ConditionalBranch, true, 286331328}, + {"0f8dc0111111", 6, Codegen::ControlFlowKind::ConditionalBranch, true, 286331328}, + {"0f8ec0111111", 6, Codegen::ControlFlowKind::ConditionalBranch, true, 286331328}, + {"0f8fc0111111", 6, Codegen::ControlFlowKind::ConditionalBranch, true, 286331328}, + {"0fb9c0", 3, Codegen::ControlFlowKind::Trap, false, 0}, + {"480fb9c0", 4, Codegen::ControlFlowKind::Trap, false, 0}, + {"48cac011", 4, Codegen::ControlFlowKind::Return, false, 0}, + {"48cb", 2, Codegen::ControlFlowKind::Return, false, 0}, + {"660fb9c0", 4, Codegen::ControlFlowKind::Trap, false, 0}, + {"66c2c011", 4, Codegen::ControlFlowKind::Return, false, 0}, + {"66c3", 2, Codegen::ControlFlowKind::Return, false, 0}, + {"66cac011", 4, Codegen::ControlFlowKind::Return, false, 0}, + {"66cb", 2, Codegen::ControlFlowKind::Return, false, 0}, + {"66ff18", 3, Codegen::ControlFlowKind::IndirectCall, false, 0}, + {"66ff28", 3, Codegen::ControlFlowKind::IndirectJump, false, 0}, + {"66ffd0", 3, Codegen::ControlFlowKind::IndirectCall, false, 0}, + {"66ffe0", 3, Codegen::ControlFlowKind::IndirectJump, false, 0}, + {"70c0", 2, Codegen::ControlFlowKind::ConditionalBranch, true, -64}, + {"71c0", 2, Codegen::ControlFlowKind::ConditionalBranch, true, -64}, + {"72c0", 2, Codegen::ControlFlowKind::ConditionalBranch, true, -64}, + {"73c0", 2, Codegen::ControlFlowKind::ConditionalBranch, true, -64}, + {"74c0", 2, Codegen::ControlFlowKind::ConditionalBranch, true, -64}, + {"75c0", 2, Codegen::ControlFlowKind::ConditionalBranch, true, -64}, + {"76c0", 2, Codegen::ControlFlowKind::ConditionalBranch, true, -64}, + {"77c0", 2, Codegen::ControlFlowKind::ConditionalBranch, true, -64}, + {"78c0", 2, Codegen::ControlFlowKind::ConditionalBranch, true, -64}, + {"79c0", 2, Codegen::ControlFlowKind::ConditionalBranch, true, -64}, + {"7ac0", 2, Codegen::ControlFlowKind::ConditionalBranch, true, -64}, + {"7bc0", 2, Codegen::ControlFlowKind::ConditionalBranch, true, -64}, + {"7cc0", 2, Codegen::ControlFlowKind::ConditionalBranch, true, -64}, + {"7dc0", 2, Codegen::ControlFlowKind::ConditionalBranch, true, -64}, + {"7ec0", 2, Codegen::ControlFlowKind::ConditionalBranch, true, -64}, + {"7fc0", 2, Codegen::ControlFlowKind::ConditionalBranch, true, -64}, + {"c2c011", 3, Codegen::ControlFlowKind::Return, false, 0}, + {"c3", 1, Codegen::ControlFlowKind::Return, false, 0}, + {"cac011", 3, Codegen::ControlFlowKind::Return, false, 0}, + {"cb", 1, Codegen::ControlFlowKind::Return, false, 0}, + {"e0c0", 2, Codegen::ControlFlowKind::ConditionalBranch, true, -64}, + {"e1c0", 2, Codegen::ControlFlowKind::ConditionalBranch, true, -64}, + {"e2c0", 2, Codegen::ControlFlowKind::ConditionalBranch, true, -64}, + {"e3c0", 2, Codegen::ControlFlowKind::ConditionalBranch, true, -64}, + {"e8c0111111", 5, Codegen::ControlFlowKind::Call, true, 286331328}, + {"e9c0111111", 5, Codegen::ControlFlowKind::UnconditionalJump, true, 286331328}, + {"ebc0", 2, Codegen::ControlFlowKind::UnconditionalJump, true, -64}, + {"ff1511111111", 6, Codegen::ControlFlowKind::Call, false, 0}, + {"ff1d11111111", 6, Codegen::ControlFlowKind::IndirectCall, false, 0}, + {"ff2511111111", 6, Codegen::ControlFlowKind::UnconditionalJump, true, 286331153}, + {"ff2d11111111", 6, Codegen::ControlFlowKind::IndirectJump, false, 0}, + {"ff18", 2, Codegen::ControlFlowKind::IndirectCall, false, 0}, + {"ff28", 2, Codegen::ControlFlowKind::IndirectJump, false, 0}, + {"ffd0", 2, Codegen::ControlFlowKind::IndirectCall, false, 0}, + {"ffe0", 2, Codegen::ControlFlowKind::IndirectJump, false, 0}, +}; + +#endif diff --git a/docs/dev/debt/relinker.md b/docs/dev/debt/relinker.md index e5841ca308..2ce4cc5356 100644 --- a/docs/dev/debt/relinker.md +++ b/docs/dev/debt/relinker.md @@ -6,6 +6,7 @@ - The executable file that [relinker](../../../core/relinker/elfpatcher/src/windows/WindowsPeWriter.cpp) generates opens a console when launched, unless it is relinked with `--windows-gui`, which selects the GUI subsystem. A GUI build started from Explorer has no standard output or error handle, so everything it prints is lost, including the entry stub's errors and crash reports (#3282). - Converted Windows main executables [omit `DYNAMIC_BASE`](../../../core/relinker/elfpatcher/src/windows/WindowsPeWriter.cpp) by default, not only with `--windows-fixed-base`: on the console the executable lies inside the 40-bit window that the title's own `sceLibcMspaceCreate` checks (#3043). Relocations are kept, so if `0x140000000` is taken Windows relocates the image, possibly above that window. Verified only on Windows 10. - [SDK memory-management syscall traps](../../../core/relinker/relinker/src/analysis/SyscallScanner.cpp) under `--trap-sdk-syscall-stubs` recognize three exact instruction sequences from [ps5-payload-dev SDK libc/mman.c](https://github.com/ps5-payload-dev/sdk/blob/12cf3dda40/libc/mman.c), observed in Nuvio Maxd 0.5.15 (PPSA99288): syscall 74 and the two syscall 477 paths, with their argument preparation and carry-flag error handling. Matching requires a decoded instruction boundary and the complete sequence inside the scanned code range. Other SDK/compiler output remains rejected. The matched syscall becomes `UD2`; this does not implement mmap or mprotect and a call still faults. +- The [instruction decoder](../../../core/relinker/codegen/src/x86/X64InstructionDecoder.cpp) decodes a near CALL, JMP or Jcc with an operand-size prefix (`66`) as Intel does, with a 32-bit or 8-bit displacement and a 64-bit target. AMD processors, the console included, read a 16-bit displacement for `66 E8`, `66 E9` and `66 0F 80`-`8F` and truncate the target of every near branch with that prefix, rel8 forms included, to 16 bits. Compilers do not emit these forms; the relinker's length and target for them do not match the console. - `--to-intel` does not lower RDPRU/MCOMMIT; the [matcher](../../../core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp) fails the relink instead. - The length-changing path of the [instruction rewriter](../../../core/relinker/codegen/src/x86/X64InstructionRewriter.cpp) is not used by the [converter](../../../core/relinker/codegen/src/Amd64OnlyConverter.cpp): it does not adjust data-to-code references (relocations, FDEs, jump tables) or segment sizes, so every substitution keeps the instruction length. - A `PT_SCE_RELRO` segment that no `PT_LOAD` covers, the PS4 layout, is [loaded](../../../core/relinker/relinker/src/parsing/ElfReader.cpp) as a read-write `PT_LOAD` and stays writable after relocation; FreeBSD rtld makes `PT_GNU_RELRO` read-only once relocation is done, and what the console does with `PT_SCE_RELRO` is not checked. From 30fab0777ee34632b6a038d2f9d65e7be0fd5d68 Mon Sep 17 00:00:00 2001 From: hypermist Date: Wed, 14 Oct 2026 19:05:25 +1300 Subject: [PATCH 0041/1000] perf(recompiler): keep the vector window's unindexed slots out of the array A shader with any v_movreld kept its whole relative-addressing window (167 registers in a PPSA08427 compute shader) in one Function-storage array, which the device puts in local memory. The window array now holds only the slots the relative accesses likely reach (from a bound analysis of their indices); every other slot is a variable of its own the device can keep in a register. A relative access whose index lands past the array at run time switches over those variables, so the guess only affects speed. APS5_NO_WINDOW_NARROWING=1 turns it off. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Uzy15wqYTt6iWyxYXEzUWo --- .../tests/execution/VectorWindowNarrowing.cpp | 120 ++++++++++++ .../SpirvBackend/SpirvEmitterState.hpp | 2 + .../src/SpirvAlu/SpirvAluEmitterRegisters.cpp | 85 ++++++++- .../src/SpirvMemory/SpirvModuleSetup.cpp | 176 +++++++++++++++++- .../SpirvBackend/src/SpirvModuleEmitter.cpp | 6 +- 5 files changed, 380 insertions(+), 9 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/VectorWindowNarrowing.cpp diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/VectorWindowNarrowing.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/VectorWindowNarrowing.cpp new file mode 100644 index 0000000000..73b15eb93a --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/VectorWindowNarrowing.cpp @@ -0,0 +1,120 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t MaxThreads = 64; +constexpr std::uint32_t Results = 16; +constexpr std::uint32_t Checked = 6; +alignas(256) std::array Output{}; + +alignas(256) constexpr std::array Code{ + 0x34020086, 0x34060088, 0x4a140680, 0x4a160681, 0x4a180682, 0x4a1a0683, 0x4a1c0684, 0x4a1e0685, + 0x4a200686, 0x4a220687, 0x4a240688, 0x4a260689, 0x4a28068a, 0x4a2a068b, 0x4a2c068c, 0x4a2e068d, + 0x4a30068e, 0x4a32068f, 0x4a340690, 0x4a360691, 0x4a380692, 0x4a3a0693, 0x4a3c0694, 0x4a3e0695, + 0x4a400696, 0x4a420697, 0x4a440698, 0x4a460699, 0x4a48069a, 0x4a4a069b, 0x4a4c069c, 0x4a4e069d, + 0x4a50069e, 0x4a52069f, 0x877c8308, 0x7e08870a, 0xbefc0309, 0x7e0a870a, 0x7e148504, 0x877c8308, + 0x7e148505, 0xe0701000, 0x80010401, 0xe0701004, 0x80010501, 0xe0701008, 0x80010c01, 0xe070100c, + 0x80010d01, 0xe0701010, 0x80011e01, 0xe0701014, 0x80012901, 0xbf810000, +}; + +constexpr const char* Names[Checked] = {"v4", "v5", "v12", "v13", "v30", "v41"}; + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + +std::uint32_t FunctionVariables(std::span words) { + std::uint32_t count = 0; + for (std::size_t at = 5; at < words.size();) { + const auto length = words[at] >> 16u; + if (length == 0u) break; + if ((words[at] & 0xffffu) == spv::OpVariable && at + 3u < words.size() && words[at + 3u] == spv::StorageClassFunction) ++count; + at += length; + } + return count; +} + +void Run(AgcDriver::VulkanDevice& device, std::uint32_t waveSize, std::uint32_t threads, std::uint32_t bounded, std::uint32_t data) { + Output.fill(0xdeadbeefu); + std::vector userData(10, 0u); + const auto output = BufferDescriptor(Output.data(), static_cast(Output.size() * 4u)); + std::copy(output.begin(), output.end(), userData.begin() + 4); + userData[8] = bounded; + userData[9] = data; + const std::span code(Code); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {waveSize, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + const auto result = ShaderRecompiler::Recompile(request); + Require(FunctionVariables(result.spirv.Words()) > 8u, "vector window narrowing: wave" + std::to_string(waveSize) + " window was not split into separate slot variables"); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); +} + +void Check(std::uint32_t waveSize, std::uint32_t threads, std::uint32_t bounded, std::uint32_t data) { + for (std::uint32_t tid = 0; tid < threads; ++tid) { + std::array window{}; + for (std::uint32_t k = 0; k < window.size(); ++k) window[k] = (tid << 8u) + k; + const std::uint32_t v4 = window[bounded & 3u]; + const std::uint32_t v5 = window[data]; + window[data] = v4; + window[bounded & 3u] = v5; + const std::array expected{v4, v5, window[2], window[3], window[20], window[31]}; + const std::uint32_t* out = &Output[tid * Results]; + for (std::uint32_t index = 0; index < Checked; ++index) { + Require(out[index] == expected[index], "vector window narrowing: wave" + std::to_string(waveSize) + " data index " + std::to_string(data) + " lane " + std::to_string(tid) + " " + Names[index] + " is " + Hex(out[index]) + ", expected " + Hex(expected[index])); + } + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + if (device->Target().subgroupSize < 32u) { + std::printf("skipped, subgroup size %u cannot hold a wave32\n", device->Target().subgroupSize); + return VulkanTestSkipped; + } + for (const std::uint32_t data : {20u, 2u, 31u}) { + Run(*device, 32, 32, 7u, data); + Check(32, 32, 7u, data); + Run(*device, 64, 64, 7u, data); + Check(64, 64, 7u, data); + } + std::puts("vector window narrowing tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp index f5d6a95d9e..30676e608e 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp @@ -153,6 +153,8 @@ struct SpirvEmitterState { std::uint32_t numWorkgroupsVariable = 0; std::array scratchVariable {}; std::array vectorWindowVariable {}; + std::uint32_t vectorWindowIndexed = 0; + std::array, 2> vectorWindowSlots {}; std::array imageVariables {}; std::uint32_t samplerVariable = 0; std::uint32_t mainFunc = 0; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterRegisters.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterRegisters.cpp index f24270d401..f38c0fe047 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterRegisters.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterRegisters.cpp @@ -1,7 +1,10 @@ #include "SpirvBackend/SpirvAluEmitter.hpp" #include "SpirvBackend/SpirvEmitterHelpers.hpp" #include "SpirvBackend/SpirvEmitterInstructions.hpp" +#include +#include #include +#include namespace ShaderRecompiler { @@ -45,21 +48,89 @@ void EmitSetVectorRegister(SpirvValueEmitContext& ctx, const IrValue& inst) { EmitUnreachable(ctx, inst); } -std::uint32_t VectorWindowElement(SpirvValueEmitContext& ctx, const IrValue& inst) { - auto& state = ctx.state; +namespace { + +std::uint32_t VectorWindowElement(SpirvEmitterState& state, std::uint32_t index) { const auto pointer = state.module.AllocateId(); - state.module.AddFunction(spv::OpAccessChain, TypePointer(state, spv::StorageClassFunction, TypeU32(state)), pointer, state.vectorWindowVariable.at(state.laneHalf), ctx.Arg(inst, 0u)); + state.module.AddFunction(spv::OpAccessChain, TypePointer(state, spv::StorageClassFunction, TypeU32(state)), pointer, state.vectorWindowVariable.at(state.laneHalf), index); return pointer; } +std::uint32_t AccessNarrowedWindow(SpirvEmitterState& state, std::uint32_t index, const std::function& access) { + const auto& slots = state.vectorWindowSlots.at(state.laneHalf); + const auto arrayLabel = state.module.AllocateId(); + const auto slotsLabel = state.module.AllocateId(); + const auto slotsMerge = state.module.AllocateId(); + const auto merge = state.module.AllocateId(); + const auto inArray = Binary(state, spv::OpULessThan, TypeBool(state), index, ConstantU32(state, state.vectorWindowIndexed)); + state.module.AddFunction(spv::OpSelectionMerge, merge, spv::SelectionControlMaskNone); + state.module.AddFunction(spv::OpBranchConditional, inArray, arrayLabel, slotsLabel); + EmitLabel(state, arrayLabel); + const auto fromArray = access(VectorWindowElement(state, index)); + const auto arrayExit = state.currentLabel; + state.module.AddFunction(spv::OpBranch, merge); + EmitLabel(state, slotsLabel); + std::vector labels(slots.size()); + std::vector words{spv::OpSwitch, index, slotsMerge}; + for (std::size_t slot = 0; slot < slots.size(); ++slot) { + labels[slot] = state.module.AllocateId(); + words.push_back(state.vectorWindowIndexed + static_cast(slot)); + words.push_back(labels[slot]); + } + state.module.AddFunction(spv::OpSelectionMerge, slotsMerge, spv::SelectionControlMaskNone); + state.module.AddFunction(std::span(words)); + std::vector incoming; + for (std::size_t slot = 0; slot < slots.size(); ++slot) { + EmitLabel(state, labels[slot]); + incoming.push_back(access(slots[slot])); + incoming.push_back(labels[slot]); + state.module.AddFunction(spv::OpBranch, slotsMerge); + } + incoming.push_back(ConstantU32(state, 0u)); + incoming.push_back(slotsLabel); + EmitLabel(state, slotsMerge); + const auto fromSlots = state.module.AllocateId(); + std::vector phi{spv::OpPhi, TypeU32(state), fromSlots}; + phi.insert(phi.end(), incoming.begin(), incoming.end()); + state.module.AddFunction(std::span(phi)); + state.module.AddFunction(spv::OpBranch, merge); + EmitLabel(state, merge); + const auto value = state.module.AllocateId(); + state.module.AddFunction(spv::OpPhi, TypeU32(state), value, fromArray, arrayExit, fromSlots, slotsMerge); + return value; +} + +std::uint32_t DirectWindowElement(SpirvValueEmitContext& ctx, const IrValue& inst) { + auto& state = ctx.state; + const IrValue* index = inst.Argument(0)->Resolve(); + if (state.vectorWindowIndexed == 0u) return VectorWindowElement(state, ctx.Arg(inst, 0u)); + if (!index->HasImmediate()) return 0u; + if (index->ImmediateU32() < state.vectorWindowIndexed) return VectorWindowElement(state, ctx.Arg(inst, 0u)); + return state.vectorWindowSlots.at(state.laneHalf).at(index->ImmediateU32() - state.vectorWindowIndexed); +} + +} + void EmitLoadVectorWindow(SpirvValueEmitContext& ctx, const IrValue& inst) { - const auto value = ctx.state.module.AllocateId(); - ctx.state.module.AddFunction(spv::OpLoad, TypeU32(ctx.state), value, VectorWindowElement(ctx, inst)); - ctx.Define(inst, value); + auto& state = ctx.state; + const auto load = [&state](std::uint32_t pointer) { + const auto value = state.module.AllocateId(); + state.module.AddFunction(spv::OpLoad, TypeU32(state), value, pointer); + return value; + }; + const auto pointer = DirectWindowElement(ctx, inst); + ctx.Define(inst, pointer != 0u ? load(pointer) : AccessNarrowedWindow(state, ctx.Arg(inst, 0u), load)); } void EmitStoreVectorWindow(SpirvValueEmitContext& ctx, const IrValue& inst) { - ctx.state.module.AddFunction(spv::OpStore, VectorWindowElement(ctx, inst), ctx.Arg(inst, 1u)); + auto& state = ctx.state; + const auto value = ctx.Arg(inst, 1u); + const auto store = [&state, value](std::uint32_t pointer) { + state.module.AddFunction(spv::OpStore, pointer, value); + return ConstantU32(state, 0u); + }; + if (const auto pointer = DirectWindowElement(ctx, inst); pointer != 0u) (void)store(pointer); + else (void)AccessNarrowedWindow(state, ctx.Arg(inst, 0u), store); } void EmitGetGotoVariable(SpirvValueEmitContext& ctx, const IrValue& inst) { diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp index f4c1da3f2b..641413c954 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp @@ -5,6 +5,11 @@ #include "SpirvBackend/SpirvEmitterHelpers.hpp" #include #include +#include +#include +#include +#include +#include #include #include @@ -41,6 +46,170 @@ std::uint32_t DefineInterfaceVariable(SpirvEmitterState& state, std::uint32_t ty return variable; } +namespace { + +class WindowIndexBounds { +public: + std::optional Of(const IrValue& input) { + const IrValue* value = input.Resolve(); + if (value->HasImmediate()) return value->ImmediateU32(); + if (const auto found = memo.find(value); found != memo.end()) return found->second; + if (!active.insert(value).second || active.size() > 256u) return std::nullopt; + const auto bound = compute(*value); + active.erase(value); + memo.emplace(value, bound); + return bound; + } + +private: + using Bound = std::optional; + + Bound maxOf(std::initializer_list bounds) { + std::uint64_t result = 0; + for (const auto& bound : bounds) { + if (!bound) return std::nullopt; + result = std::max(result, *bound); + } + return result; + } + + static std::vector reachingStores(const IrValue& load, std::uint32_t slot) { + std::vector values; + std::unordered_set visited; + std::vector> pending {{load.Parent(), &load}}; + while (!pending.empty()) { + const auto [block, before] = pending.back(); + pending.pop_back(); + const auto& instructions = block->Instructions(); + auto it = instructions.rbegin(); + if (before != nullptr) it = std::next(std::find(instructions.rbegin(), instructions.rend(), before)); + bool stored = false; + for (; it != instructions.rend() && !stored; ++it) { + if ((*it)->Opcode() != IrOpcode::StoreVectorWindow) continue; + const IrValue* index = (*it)->Argument(0)->Resolve(); + if (!index->HasImmediate() || index->ImmediateU32() != slot) continue; + values.push_back((*it)->Argument(1)); + stored = true; + } + if (stored) continue; + for (IrBlock* predecessor : block->Predecessors()) { + if (visited.insert(predecessor).second) pending.emplace_back(predecessor, nullptr); + } + } + return values; + } + + Bound compute(const IrValue& value) { + const auto arg = [&](std::size_t index) { return Of(*value.Argument(index)); }; + const auto immediate = [&](std::size_t index) -> std::optional { + const IrValue* operand = value.Argument(index)->Resolve(); + if (!operand->HasImmediate()) return std::nullopt; + return operand->ImmediateU32(); + }; + const auto fits = [](std::uint64_t bound) -> Bound { return bound <= 0xffffffffull ? Bound(bound) : std::nullopt; }; + switch (value.Opcode()) { + case IrOpcode::BitFieldUExtract: { + const auto count = immediate(2); + if (count && *count < 32u) return (std::uint64_t {1} << *count) - 1u; + return arg(0); + } + case IrOpcode::BitwiseAnd32: { + const auto lhs = arg(0); + const auto rhs = arg(1); + if (lhs && rhs) return std::min(*lhs, *rhs); + return lhs ? lhs : rhs; + } + case IrOpcode::UMin32: { + const auto lhs = arg(0); + const auto rhs = arg(1); + if (!lhs || !rhs) return std::nullopt; + return std::min(*lhs, *rhs); + } + case IrOpcode::UMax32: + return maxOf({arg(0), arg(1)}); + case IrOpcode::BitwiseOr32: { + const auto both = maxOf({arg(0), arg(1)}); + if (!both) return std::nullopt; + std::uint64_t mask = 0; + while (mask < *both) mask = mask << 1u | 1u; + return mask; + } + case IrOpcode::IAdd32: { + const auto lhs = arg(0); + const auto rhs = arg(1); + if (!lhs || !rhs) return std::nullopt; + return fits(*lhs + *rhs); + } + case IrOpcode::ShiftRightLogical32: { + const auto shift = immediate(1); + const auto bound = arg(0); + if (!bound) return std::nullopt; + return shift ? *bound >> (*shift & 31u) : *bound; + } + case IrOpcode::ShiftLeftLogical32: { + const auto shift = immediate(1); + const auto bound = arg(0); + if (!bound || !shift) return std::nullopt; + return fits(*bound << (*shift & 31u)); + } + case IrOpcode::ConvertU32U16: + return 0xffffu; + case IrOpcode::Select: + case IrOpcode::SelectU32: + { + if (value.Argument(0)->Resolve()->Opcode() == IrOpcode::GetExec || value.Argument(2)->Resolve()->Opcode() == IrOpcode::LoadVectorWindow) return arg(1); + const auto lhs = arg(1); + const auto rhs = arg(2); + if (lhs && rhs) return std::max(*lhs, *rhs); + return lhs ? lhs : rhs; + } + case IrOpcode::ReadFirstLane: + return arg(0); + case IrOpcode::Identity: + case IrOpcode::Phi: { + Bound bound; + for (std::size_t index = 0; index < value.ArgumentCount(); ++index) { + if (const auto incoming = arg(index)) bound = std::max(bound.value_or(0u), *incoming); + } + return bound; + } + case IrOpcode::LoadVectorWindow: { + const IrValue* index = value.Argument(0)->Resolve(); + if (!index->HasImmediate()) return std::nullopt; + Bound bound = 0u; + for (const IrValue* stored : reachingStores(value, index->ImmediateU32())) { + bound = maxOf({bound, Of(*stored)}); + if (!bound) return std::nullopt; + } + return bound; + } + default: + return std::nullopt; + } + } + + std::unordered_map memo; + std::unordered_set active; +}; + +std::uint32_t IndexedWindowSlots(const IrProgram& program) { + const auto window = program.Metadata().vectorWindowSize; + WindowIndexBounds bounds; + std::uint64_t reached = 0; + for (const auto& block : program.Blocks()) { + for (const IrValue* inst : block->Instructions()) { + if (inst->Opcode() != IrOpcode::LoadVectorWindow && inst->Opcode() != IrOpcode::StoreVectorWindow) continue; + const IrValue* index = inst->Argument(0)->Resolve(); + if (index->HasImmediate()) continue; + const auto bound = bounds.Of(*index); + if (bound && *bound < window / 2u) reached = std::max(reached, *bound + 1u); + } + } + return static_cast(reached); +} + +} + void DefineModule(SpirvEmitterState& state) { state.interfaceVariables.reserve(state.program.Info().inputs.size() + state.program.Info().outputs.size()); DefineInputs(state); @@ -55,9 +224,14 @@ void DefineModule(SpirvEmitterState& state) { state.scratchVariable.at(half) = state.module.AllocateId(); } } - if (state.program.Metadata().vectorWindowSize != 0u) { + if (const auto window = state.program.Metadata().vectorWindowSize; window != 0u) { + static const bool narrowWindow = std::getenv("APS5_NO_WINDOW_NARROWING") == nullptr; + state.vectorWindowIndexed = narrowWindow ? IndexedWindowSlots(state.program) : 0u; for (std::uint32_t half = 0; half < state.laneCount; half++) { state.vectorWindowVariable.at(half) = state.module.AllocateId(); + if (state.vectorWindowIndexed == 0u) continue; + auto& slots = state.vectorWindowSlots.at(half); + for (std::uint32_t slot = state.vectorWindowIndexed; slot < window; slot++) slots.push_back(state.module.AllocateId()); } } state.mainFunc = state.module.AllocateId(); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp index 410f5b1580..752082de85 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp @@ -1053,10 +1053,14 @@ void EmitProgram(SpirvEmitterState& state) { } } if (const auto window = state.program.Metadata().vectorWindowSize; window != 0u) { - const auto array = state.module.Type(spv::OpTypeArray, TypeU32(state), ConstantU32(state, window)); + const auto indexed = state.vectorWindowIndexed != 0u ? state.vectorWindowIndexed : window; + const auto array = state.module.Type(spv::OpTypeArray, TypeU32(state), ConstantU32(state, indexed)); const auto zero = state.module.Constant(spv::OpConstantNull, array); for (std::uint32_t half = 0; half < state.laneCount; half++) { state.module.AddFunction(spv::OpVariable, TypePointer(state, spv::StorageClassFunction, array), state.vectorWindowVariable.at(half), spv::StorageClassFunction, zero); + for (const auto slot : state.vectorWindowSlots.at(half)) { + state.module.AddFunction(spv::OpVariable, TypePointer(state, spv::StorageClassFunction, TypeU32(state)), slot, spv::StorageClassFunction, ConstantU32(state, 0u)); + } } } if (state.pixelValidMaskVariable != 0u) { From c54854b2bdd997be1ebbc224dbdacfa3d8942f9e Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Wed, 14 Oct 2026 02:38:14 -0700 Subject: [PATCH 0042/1000] fix(agc): find a view's region when the cached address space dissolves after UploadPrepare UploadFinish dissolves the cached address space when an import was retired after the space was built. The space's base regions are then merged with the build's own regions, which since readsOverBase may overlap them. owner() searched the merged list for the region with the greatest begin, a base region of one mapping, so a read-only V# merged into an own region that began earlier was bound through it again: guest buffer view 0x1f0390c1c4+0x10c1cc exceeds its GPU owner 0x1f03900000-0x1f03910000 (buffer 0, direct 1, mirror 0) The dissolve now notes when the merged list overlaps, and owner() then looks back for a region that holds the whole range. The test's third build retires the imports between UploadPrepare and UploadFinish. --- .../Graphics/include/GuestBufferMemory.hpp | 1 + .../Graphics/src/GuestBufferMemory.cpp | 20 +++++++++++++++---- .../libSceAgcDriver/tests/BdaResources.cpp | 6 ++++-- 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp index 9e71901ebd..d2fedc76af 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp @@ -389,6 +389,7 @@ private: // Whether `regions` is in ascending address order (true right after AcquireRegistered, whose // regions follow the registry's order), so AddSnapshot can search instead of scanning. bool regionsSorted = false; + bool regionsOverlap = false; std::vector> writes; std::vector> readsOverBase; std::vector>> heapReferences; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index b4771f1a6a..d91dc92ba1 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -2580,6 +2580,11 @@ void GuestBufferMemory::dissolveSpace(bool resolve) { // Both sorted and disjoint (UploadPrepare merged this build's regions; the space's are // registered ranges), so the merge keeps the order Descriptor searches. std::merge(space->base.begin(), space->base.end(), std::make_move_iterator(regions.begin()), std::make_move_iterator(regions.end()), std::back_inserter(merged), [](const Region& left, const Region& right) { return left.begin < right.begin; }); + std::uint64_t reached = 0; + for (const auto& region : merged) { + regionsOverlap = regionsOverlap || region.begin < reached; + reached = std::max(reached, region.end); + } } else { merged.assign(space->base.begin(), space->base.end()); merged.insert(merged.end(), std::make_move_iterator(regions.begin()), std::make_move_iterator(regions.end())); @@ -2597,12 +2602,19 @@ void GuestBufferMemory::dissolveSpace(bool resolve) { } const GuestBufferMemory::Region* GuestBufferMemory::owner(std::uint64_t address, std::uint64_t end) const { - const auto candidate = [address](const std::vector& list) -> const Region* { + const auto candidate = [address, end](const std::vector& list, bool overlapping) -> const Region* { const auto found = std::upper_bound(list.begin(), list.end(), address, [](std::uint64_t value, const Region& region) { return value < region.begin; }); - return found == list.begin() ? nullptr : &*std::prev(found); + if (found == list.begin()) return nullptr; + if (overlapping) { + for (auto it = found; it != list.begin();) { + --it; + if (end <= it->end) return &*it; + } + } + return &*std::prev(found); }; - const auto* own = candidate(regions); - const auto* shared = space != nullptr ? candidate(space->base) : nullptr; + const auto* own = candidate(regions, regionsOverlap); + const auto* shared = space != nullptr ? candidate(space->base, false) : nullptr; const auto holds = [end](const Region* region) { return region != nullptr && end <= region->end; }; if (holds(own) != holds(shared)) return holds(own) ? own : shared; if (own == nullptr || (shared != nullptr && shared->begin > own->begin)) return shared; diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp index e056c5abb4..ab710def2b 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp @@ -581,12 +581,14 @@ void spaceReadsAcrossImportsMergedTests(const Context& context, const BdaTestAcc const std::uint64_t lowerBytes = 512; const std::uint64_t upper = second + 128; const std::uint64_t upperBytes = third + 256 - upper; - for (int build = 0; build < 2; ++build) { + for (int build = 0; build < 3; ++build) { GuestBufferMemory leased(importing); leased.AcquireRegistered(); leased.AddReadable(lower, lowerBytes); leased.AddReadable(upper, upperBytes); - leased.Upload(true); + leased.UploadPrepare(true); + if (build == 2) ClearHostImports(importing.device); + leased.UploadFinish(true); for (const auto& [address, bytes] : {std::pair{lower, lowerBytes}, std::pair{upper, upperBytes}}) { std::uint32_t adjustment = 0; const auto view = leased.Descriptor(address, bytes, adjustment); From ae111b9c07f4f63f28cb8f84724ae01fddc3c74e Mon Sep 17 00:00:00 2001 From: Yossi Yadgar Date: Wed, 14 Oct 2026 12:42:28 +0300 Subject: [PATCH 0043/1000] test(agc): drop a shared range's backing when the range goes away Each SharedRange in agc_host_import_sparse registered a pointer to its Backing in the global list Resolve searches, and never removed it. The ranges live on the stack of each check, so by the last check the list held pointers into stack frames that had been reused. On this machine Resolve matched one of them for the fourth check's range and returned an unrelated descriptor (st_size 0, offset 46330441829925), the sparse path was not chosen, and the check failed with "a failure at vkQueueBindSparse after the sparse path was chosen did not throw". Whether it passes depends on what the compiler leaves in those frames. The range now removes its Backing when it is destroyed. The test passes 20 of 20 here, and fails again when a vkQueueBindSparse failure no longer throws, which it could not show before. --- .../libs/prx/libSceAgcDriver/tests/HostImportSparseDmaBuf.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/HostImportSparseDmaBuf.cpp b/core/libs/prx/libSceAgcDriver/tests/HostImportSparseDmaBuf.cpp index a6d05d4dc7..de92d1672f 100644 --- a/core/libs/prx/libSceAgcDriver/tests/HostImportSparseDmaBuf.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/HostImportSparseDmaBuf.cpp @@ -214,6 +214,10 @@ struct SharedRange { mutation.Add(backing.base, static_cast(bytes), true, true, true); } + ~SharedRange() { + std::erase(backings, &backing); + } + std::uint64_t Address() const { return reinterpret_cast(backing.base); } From ba4b2db2386739ba69cb6539074bc75de59bf066 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexej=20Szpev=C3=A1k?= Date: Wed, 14 Oct 2026 11:48:11 +0200 Subject: [PATCH 0044/1000] fix(relinker): move legacy CMPSS and CMPSD out of line for Rosetta The Rosetta split covered only VEX scalar compares. #5642 measured the same lost load after legacy CMPSS and CMPSD, and the decoder already hands them to the matcher with the right length and RIP offset. Recognise F3 or F2, segment overrides and an optional REX before 0F C2 /r ib, with the same predicate rule as the VEX forms. A 66, 67 or LOCK prefix, or both F2 and F3, keeps the instruction inline as before. --- .../codegen/src/x86/ScalarCompareOperands.cpp | 18 +++++++- .../codegen/tests/Amd64OnlyConverterTests.cpp | 46 ++++++++++++++++++- 2 files changed, 62 insertions(+), 2 deletions(-) diff --git a/core/relinker/codegen/src/x86/ScalarCompareOperands.cpp b/core/relinker/codegen/src/x86/ScalarCompareOperands.cpp index c0c31a92e6..0f1e217d5c 100644 --- a/core/relinker/codegen/src/x86/ScalarCompareOperands.cpp +++ b/core/relinker/codegen/src/x86/ScalarCompareOperands.cpp @@ -32,7 +32,23 @@ std::optional DecodeScalarCompare(const std::uint8_t* dat return std::nullopt; modRmOffset = pos + 4; } else { - return std::nullopt; + bool singlePrecision = false; + bool doublePrecision = false; + for (; pos < length; ++pos) { + if (data[pos] == PrefixRep) + singlePrecision = true; + else if (data[pos] == PrefixRepne) + doublePrecision = true; + else if (!_segmentPrefix(data[pos])) + break; + } + if (singlePrecision == doublePrecision) + return std::nullopt; + if (pos < length && data[pos] >= RexMin && data[pos] <= RexMax) + ++pos; + if (pos + 3 > length || data[pos] != TwoByteOpcodeEscape || data[pos + 1] != kCompareOpcode) + return std::nullopt; + modRmOffset = pos + 2; } const auto modrm = data[modRmOffset]; const auto mod = static_cast((modrm >> ModRmModShift) & ModRmModMask); diff --git a/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp b/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp index dcb82e697d..c97cea6a0b 100644 --- a/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp +++ b/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp @@ -969,7 +969,7 @@ void rosettaScalarCompares() { require(vex3 && Bytes(vex3->StubBody.begin(), vex3->StubBody.begin() + 6) == Bytes{0xC4, 0xE1, 0xEB, 0xC2, 0xCB, 0x03}, "A VEX3 VCMPSD with a hazard predicate does not move unchanged"); for (const Bytes& kept : {Bytes{0xC5, 0xEA, 0xC2, 0xCA, 0x00}, Bytes{0xC5, 0xEA, 0xC2, 0xCA, 0x01}, Bytes{0xC5, 0xEA, 0xC2, 0xCA, 0x02}, Bytes{0xC4, 0xE1, 0xEB, 0xC2, 0xCB, 0x01}}) require(!match(kept).has_value(), "An EQ_OQ, LT_OS or LE_OS scalar compare must stay inline"); - for (const Bytes& other : {Bytes{0xC5, 0xE9, 0xC2, 0xCA, 0x03}, Bytes{0xC5, 0xE8, 0xC2, 0xCA, 0x03}, Bytes{0xC5, 0xEA, 0x2E, 0xC8}, Bytes{0xC5, 0xEA, 0xC2, 0xCA}, Bytes{0xC4, 0xE2, 0xEB, 0xC2, 0xCB, 0x03}, Bytes{0x62, 0xD1, 0x76, 0x08, 0xC2, 0xCB, 0x03}, Bytes{0xF3, 0x0F, 0xC2, 0xCA, 0x03}}) + for (const Bytes& other : {Bytes{0xC5, 0xE9, 0xC2, 0xCA, 0x03}, Bytes{0xC5, 0xE8, 0xC2, 0xCA, 0x03}, Bytes{0xC5, 0xEA, 0x2E, 0xC8}, Bytes{0xC5, 0xEA, 0xC2, 0xCA}, Bytes{0xC4, 0xE2, 0xEB, 0xC2, 0xCB, 0x03}, Bytes{0x62, 0xD1, 0x76, 0x08, 0xC2, 0xCB, 0x03}}) require(!match(other).has_value(), "An instruction other than a Rosetta-hazard scalar compare was moved"); require(!Codegen::MakeAmd64OnlyInstructionMatcher()->Match(compare.data(), compare.size()).has_value(), "A scalar compare was moved for an Intel host"); const std::vector> pair = {std::span(compare), std::span(rip)}; @@ -977,6 +977,48 @@ void rosettaScalarCompares() { require(sequence && sequence->InstructionName == "scalar compare" && sequence->ReturnBranchOffset == 14 && sequence->Relocations.size() == 1 && sequence->Relocations[0].SiteTarget == 5 + 9 + 0x100, "A scalar compare after another is not emitted out of line in one stub"); } +void rosettaLegacyScalarCompares() { + const auto rosetta = Codegen::MakeAmd64OnlyInstructionMatcher(Codegen::Amd64OnlyTarget::Rosetta); + const auto match = [&](const Bytes& bytes) { return rosetta->Match(bytes.data(), bytes.size()); }; + for (const Bytes& moved : {Bytes{0xF3, 0x0F, 0xC2, 0xCA, 0x03}, Bytes{0xF2, 0x0F, 0xC2, 0xCA, 0x04}, Bytes{0xF2, 0x41, 0x0F, 0xC2, 0xC9, 0x05}, + Bytes{0x64, 0xF3, 0x0F, 0xC2, 0x4F, 0x68, 0x06}, Bytes{0xF3, 0x2E, 0x0F, 0xC2, 0x44, 0x24, 0x08, 0x07}, Bytes{0xF3, 0x0F, 0xC2, 0xCA, 0x08}}) { + const auto lowered = match(moved); + require(lowered && lowered->Lowering == Codegen::Amd64OnlyLowering::Trampoline && lowered->Optional && lowered->InstructionName == "scalar compare", "A legacy Rosetta-hazard scalar compare was not lowered through an optional stub"); + require(lowered->ReturnBranchOffset == moved.size() && lowered->StubBody.size() == moved.size() + 5 && Bytes(lowered->StubBody.begin(), lowered->StubBody.begin() + static_cast(moved.size())) == moved && lowered->StubBody[moved.size()] == 0xE9 && lowered->Relocations.empty(), "A legacy scalar compare does not run unchanged before its return jump"); + } + const auto rip = match(Bytes{0xF3, 0x0F, 0xC2, 0x0D, 0x00, 0x01, 0x00, 0x00, 0x06}); + require(rip && rip->Relocations.size() == 1 && rip->Relocations[0].DisplacementOffset == 4 && rip->Relocations[0].InstructionEnd == 9 && rip->Relocations[0].SiteTarget == 9 + 0x100, "A RIP-relative legacy CMPSS was not relocated from its place in the site"); + const auto ripRex = match(Bytes{0xF2, 0x48, 0x0F, 0xC2, 0x05, 0x00, 0x01, 0x00, 0x00, 0x07}); + require(ripRex && ripRex->Relocations.size() == 1 && ripRex->Relocations[0].DisplacementOffset == 5 && ripRex->Relocations[0].InstructionEnd == 10 && ripRex->Relocations[0].SiteTarget == 10 + 0x100, "A RIP-relative legacy CMPSD with REX was not relocated"); + for (const Bytes& kept : {Bytes{0xF3, 0x0F, 0xC2, 0xCA, 0x00}, Bytes{0xF3, 0x0F, 0xC2, 0xCA, 0x01}, Bytes{0xF2, 0x0F, 0xC2, 0xCA, 0x02}}) + require(!match(kept).has_value(), "A legacy EQ_OQ, LT_OS or LE_OS scalar compare must stay inline"); + for (const Bytes& other : {Bytes{0x0F, 0xC2, 0xCA, 0x03}, Bytes{0x66, 0x0F, 0xC2, 0xCA, 0x03}, Bytes{0x66, 0xF3, 0x0F, 0xC2, 0xCA, 0x03}, Bytes{0xF2, 0xF3, 0x0F, 0xC2, 0xCA, 0x03}, + Bytes{0x67, 0xF3, 0x0F, 0xC2, 0x0D, 0x00, 0x01, 0x00, 0x00, 0x03}, Bytes{0xF0, 0xF3, 0x0F, 0xC2, 0xCA, 0x03}, Bytes{0x48, 0xF3, 0x0F, 0xC2, 0xCA, 0x03}, + Bytes{0xF3, 0x0F, 0xC2, 0xCA}, Bytes{0xF3, 0x0F, 0xC3, 0xCA, 0x03}, Bytes{0xF3, 0x0F, 0x10, 0xCA}}) + require(!match(other).has_value(), "An instruction other than a legacy Rosetta-hazard scalar compare was moved"); + const Bytes compare = {0xF3, 0x0F, 0xC2, 0xCA, 0x03}; + require(!Codegen::MakeAmd64OnlyInstructionMatcher()->Match(compare.data(), compare.size()).has_value(), "A legacy scalar compare was moved for an Intel host"); +} + +void converterRosettaLegacyScalarCompare() { + const auto converter = Codegen::MakeAmd64OnlyConverter(Codegen::Amd64OnlyTarget::Rosetta); + Bytes file(0x300, 0xCC); + const Bytes text = { + 0xF3, 0x0F, 0x10, 0x15, 0x10, 0x00, 0x00, 0x00, + 0x0F, 0x28, 0xC2, + 0xF3, 0x0F, 0xC2, 0xCA, 0x03, + 0xC5, 0xF8, 0x28, 0x0D, 0x20, 0x00, 0x00, 0x00, + 0xC5, 0xF8, 0x2E, 0xC8, + 0xC3}; + std::copy(text.begin(), text.end(), file.begin() + 0x200); + const auto result = converter->Convert(file, {segmentHeader(text.size())}); + require(result.Trampolines.size() == 1 && result.Reports.size() == 1 && result.Reports[0].InstructionName == "scalar compare" && result.Reports[0].Offset == 0x20B && result.Reports[0].Lowering == Codegen::Amd64OnlyLowering::Trampoline, "The legacy compare in the fmax shape was not the only moved instruction"); + const auto& site = result.Trampolines[0]; + require(site.Offset == 0x20B && site.Length == 5 && Bytes(site.Body.begin(), site.Body.begin() + 5) == Bytes(text.begin() + 11, text.begin() + 16) && site.Relocations.empty(), "The legacy compare stub does not run the compare unchanged"); + const auto intel = Codegen::MakeAmd64OnlyConverter()->Convert(file, {segmentHeader(text.size())}); + require(intel.Reports.empty() && intel.Bytes == file, "The Intel target moved a legacy scalar compare"); +} + void converterRosettaScalarCompare() { const auto converter = Codegen::MakeAmd64OnlyConverter(Codegen::Amd64OnlyTarget::Rosetta); Bytes file(0x300, 0xCC); @@ -1729,6 +1771,8 @@ int main() { converterRosetta(); rosettaScalarCompares(); converterRosettaScalarCompare(); + rosettaLegacyScalarCompares(); + converterRosettaLegacyScalarCompare(); wideStoreSplit(); converterWideStoreWindows(); converterJumpTableTargets(); From 25527f97a0d9221f7c4e121d250248c0f8078bee Mon Sep 17 00:00:00 2001 From: GalaxyGamingFR <153150716+GalaxyGamingFR@users.noreply.github.com> Date: Wed, 14 Oct 2026 02:50:53 -0700 Subject: [PATCH 0045/1000] fix(stubgen): report named stubs by __func__ CONTRIBUTING.md says unimplemented exports call NotImplemented_nid_no_patch(__func__), and 362 calls in core/ do. A stub generated for a NID with a known name passed the NID string instead, so its log line showed tNn5WBkta60 rather than sceAmprCommandBufferNop and the output had to be edited by hand. Named stubs now use __func__; stubs with an unknown name keep the NID, the only identifier they have. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_012zyXkUzTZGh3nBwNMtrLaW --- tools/stubgen.py | 2 +- tools/tests/test_stubgen.py | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/tools/stubgen.py b/tools/stubgen.py index 2c296b9298..8b8a8d6829 100644 --- a/tools/stubgen.py +++ b/tools/stubgen.py @@ -146,7 +146,7 @@ def generate(module, nids, db, td_path=None, td_lib=None): if display is not None: chunks.append("") chunks.append("int APS5_VABI %s(void) {" % func) - chunks.append(' NotImplemented_nid_no_patch("%s");' % nid) + chunks.append(" NotImplemented_nid_no_patch(__func__);") chunks.append(" return 0;") chunks.append("}") td.append("- [%s](%s) (%s) - unknown signature" % (display, path, lib)) diff --git a/tools/tests/test_stubgen.py b/tools/tests/test_stubgen.py index 66ab9bd1b3..cb77d2c1e2 100644 --- a/tools/tests/test_stubgen.py +++ b/tools/tests/test_stubgen.py @@ -105,6 +105,12 @@ class StubGeneratorTests(unittest.TestCase): path.write_text("# imports\nAAAAAAAAAAA#A#B\nAAAAAAAAAAA\nBBBBBBBBBBB\n", encoding="utf-8-sig") self.assertEqual(stubgen.parse_nids(path), ["AAAAAAAAAAA", "BBBBBBBBBBB"]) + def test_named_stub_reports_its_function_name(self): + code = stubgen.generate("libSceAmpr", ["tNn5WBkta60", "AAAAAAAAAAA"], {"tNn5WBkta60": "sceAmprCommandBufferNop"})[0] + named = code.split("sceAmprCommandBufferNop(void) {\n", 1)[1].split("\n", 1)[0] + self.assertEqual(named, " NotImplemented_nid_no_patch(__func__);") + self.assertIn('NotImplemented_nid_no_patch("AAAAAAAAAAA");', code) + def test_valid_generation_keeps_the_code_and_debt(self): with tempfile.TemporaryDirectory() as directory: root = Path(directory) From 97bb32d5161333c3d9abbdb8c849848ff7d9c946 Mon Sep 17 00:00:00 2001 From: ShinMalphur Date: Wed, 14 Oct 2026 16:21:35 +0630 Subject: [PATCH 0046/1000] fix(tools): classify GPU mutex trace slices as waits --- tools/trace_digest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/trace_digest.py b/tools/trace_digest.py index a375a776b1..4d88042f10 100644 --- a/tools/trace_digest.py +++ b/tools/trace_digest.py @@ -14,7 +14,7 @@ unless --summary names another file.""" def is_wait(name): - return "wait" in name.rsplit(".", 1)[-1].lower() + return name.startswith("GpuMutex.wait.") or "wait" in name.rsplit(".", 1)[-1].lower() class Trace: From 2507b84f16abb7204cc2e22625b61a1464d04841 Mon Sep 17 00:00:00 2001 From: ShinMalphur Date: Wed, 14 Oct 2026 16:22:30 +0630 Subject: [PATCH 0047/1000] test(tools): cover GPU mutex wait accounting --- tools/tests/test_trace_digest.py | 35 ++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/tools/tests/test_trace_digest.py b/tools/tests/test_trace_digest.py index 7917bc864a..5521006427 100644 --- a/tools/tests/test_trace_digest.py +++ b/tools/tests/test_trace_digest.py @@ -114,6 +114,41 @@ class TraceDigestTests(unittest.TestCase): self.assertAlmostEqual(waits[0]["ms"], 14.5) self.assertEqual(waits[1]["name"], "Driver.WorkerWait") + def test_gpu_mutex_waits_with_site_names(self): + for site in ("other", "dispatch", "indirect", "draw", "hook", "wait", "label", "flush", "present", "fill", "copy", "end", "try"): + with self.subTest(site=site): + name = f"GpuMutex.wait.{site}" + events = [ + complete(1, "Driver.Packet", 0, 10000), + complete(1, name, 1000, 4000), + complete(1, "Driver.Packet.memory_wait", 2000, 1000), + {"name": "present", "ph": "i", "ts": 0, "tid": 1, "args": {"present": 600}}, + {"name": "present", "ph": "i", "ts": 10000, "tid": 1, "args": {"present": 601}}, + ] + result = trace_digest.digest(trace_digest.Trace({"traceEvents": events})) + thread = result["threads"][0] + self.assertAlmostEqual(thread["wait_ms_per_frame"], 4.0) + self.assertAlmostEqual(thread["busy_ms_per_frame"], 6.0) + self.assertAlmostEqual(thread["wait_pct"], 40.0) + self.assertAlmostEqual(thread["busy_pct"], 60.0) + self.assertEqual(result["longest_waits"], [ + {"name": name, "thread": "tid 1", "ms": 4.0, "at_ms": 1.0, "present": 600}, + ]) + self.assertIn(f"- {name} (tid 1) 4.0 ms", trace_digest.markdown(result)) + + def test_top_level_gpu_mutex_wait_is_not_busy_work(self): + events = [ + complete(1, "GpuMutex.wait.draw", 0, 4000), + complete(1, "GpuMutex.hold.draw", 4000, 6000), + {"name": "present", "ph": "i", "ts": 0, "tid": 1, "args": {"present": 600}}, + {"name": "present", "ph": "i", "ts": 10000, "tid": 1, "args": {"present": 601}}, + ] + result = trace_digest.digest(trace_digest.Trace({"traceEvents": events})) + thread = result["threads"][0] + self.assertAlmostEqual(thread["busy_ms_per_frame"], 6.0) + self.assertAlmostEqual(thread["wait_ms_per_frame"], 4.0) + self.assertEqual(thread["top"], [{"name": "GpuMutex.hold.draw", "ms_per_frame": 6.0}]) + def test_stacks_self_and_total(self): stacks = self.result()["stacks"] self.assertEqual(stacks["samples"], 5) From c177bde3168787b3156b35a8a2b3745935252398 Mon Sep 17 00:00:00 2001 From: GalaxyGamingFR <153150716+GalaxyGamingFR@users.noreply.github.com> Date: Wed, 14 Oct 2026 02:52:39 -0700 Subject: [PATCH 0048/1000] fix(trace_digest): print non-ASCII names on a legacy console On Windows, piped or redirected output uses the ANSI code page, so a trace whose thread or slice names are not in cp1252 (a Japanese thread name, for example) made the markdown digest stop with UnicodeEncodeError after the work was done. stdout is now set to UTF-8 when the tool runs from the command line, as check_conventions.py does. The --json output was not affected because json.dumps escapes non-ASCII. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_012zyXkUzTZGh3nBwNMtrLaW --- tools/tests/test_trace_digest.py | 12 ++++++++++++ tools/trace_digest.py | 2 ++ 2 files changed, 14 insertions(+) diff --git a/tools/tests/test_trace_digest.py b/tools/tests/test_trace_digest.py index 7917bc864a..53c8815be4 100644 --- a/tools/tests/test_trace_digest.py +++ b/tools/tests/test_trace_digest.py @@ -2,6 +2,8 @@ import contextlib import gzip import io import json +import os +import subprocess import sys import tempfile import unittest @@ -82,6 +84,16 @@ class TraceDigestTests(unittest.TestCase): self.assertEqual(result["frames"], 3) self.assertAlmostEqual(result["window_ms"], 40.0) + def test_command_line_prints_non_ascii_names_on_a_legacy_console(self): + events = [dict(event, args={"name": "メイン"}) if event["name"] == "thread_name" and event["tid"] == 1 else event for event in EVENTS] + with gzip.open(self.trace, "wt", encoding="utf-8") as handle: + json.dump({"traceEvents": events}, handle) + tool = Path(trace_digest.__file__).resolve() + result = subprocess.run([sys.executable, str(tool), str(self.trace)], capture_output=True, + env=dict(os.environ, PYTHONIOENCODING="cp1252"), timeout=60) + self.assertEqual(result.returncode, 0, result.stderr.decode("utf-8", "replace")) + self.assertIn("メイン", result.stdout.decode("utf-8")) + def test_slowest_frame_lists_its_slices(self): slowest = self.result()["slowest_frames"] self.assertEqual([frame["present"] for frame in slowest], [601, 600]) diff --git a/tools/trace_digest.py b/tools/trace_digest.py index a375a776b1..b521280309 100644 --- a/tools/trace_digest.py +++ b/tools/trace_digest.py @@ -219,4 +219,6 @@ def main(arguments=None): if __name__ == "__main__": + if hasattr(sys.stdout, "reconfigure"): + sys.stdout.reconfigure(encoding="utf-8", errors="replace") sys.exit(main()) From 378ac61788f1ee2065909a3360e43f12cd9a0cda Mon Sep 17 00:00:00 2001 From: simonbronk <275178677+simonbronk@users.noreply.github.com> Date: Wed, 14 Oct 2026 12:02:20 +0200 Subject: [PATCH 0049/1000] fix(libSceConvertKeycode): ignore the state bit 0x4000000 RE Engine titles pass Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceConvertKeycode/Export.cpp | 3 ++- core/libs/tests/GuestConvertKeycode.cpp | 11 +++++++++++ docs/dev/debt/libSceConvertKeycode.md | 2 +- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceConvertKeycode/Export.cpp b/core/libs/prx/libSceConvertKeycode/Export.cpp index 29c849a7e1..0fa5a89b58 100644 --- a/core/libs/prx/libSceConvertKeycode/Export.cpp +++ b/core/libs/prx/libSceConvertKeycode/Export.cpp @@ -8,6 +8,7 @@ namespace { constexpr int kErrInvalidAddress = static_cast(0x80BC0031u); constexpr int kErrInvalidUserId = static_cast(0x80BC0010u); +constexpr std::uint32_t kIgnoredStateBits = 0x4000000u; char16_t UsCharacter(std::uint16_t keycode) { static constexpr char16_t kPunctuation[] = u"-=[]\\\0;'`,./"; @@ -43,7 +44,7 @@ int APS5_VABI sceConvertKeycodeGetCharacterFromKeyboardData(void) { int APS5_VABI sceConvertKeycodeGetCharacter(std::uint16_t keycode, std::uint32_t state, std::uint32_t type, char16_t* character) { if (character == nullptr) return kErrInvalidAddress; - if (state != 0) throw std::runtime_error("sceConvertKeycodeGetCharacter: unsupported state " + std::to_string(state)); + if ((state & ~kIgnoredStateBits) != 0) throw std::runtime_error("sceConvertKeycodeGetCharacter: unsupported state " + std::to_string(state)); if (type != 0) throw std::runtime_error("sceConvertKeycodeGetCharacter: unsupported keyboard type " + std::to_string(type)); *character = UsCharacter(keycode); return 0; diff --git a/core/libs/tests/GuestConvertKeycode.cpp b/core/libs/tests/GuestConvertKeycode.cpp index c25a9f31bd..ec8968222b 100644 --- a/core/libs/tests/GuestConvertKeycode.cpp +++ b/core/libs/tests/GuestConvertKeycode.cpp @@ -2,9 +2,11 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include #include +#include extern "C" { int APS5_VABI sceConvertKeycodeGetImeKeyboardType(std::int32_t, std::uint32_t*); +int APS5_VABI sceConvertKeycodeGetCharacter(std::uint16_t, std::uint32_t, std::uint32_t, char16_t*); } static void Require(bool value) { if (!value) std::abort(); } @@ -26,5 +28,14 @@ int main() { type = 0xFFFFFFFFu; Require(sceConvertKeycodeGetImeKeyboardType(1, &type) == 0); Require(type == 0); + for (std::uint32_t state : {0u, 0x4000000u}) { + char16_t character[3] = {u'?', 0, 0}; + Require(sceConvertKeycodeGetCharacter(0x04, state, 0, character) == 0); + Require(character[0] == u'a'); + Require(sceConvertKeycodeGetCharacter(0x1E, state, 0, character) == 0); + Require(character[0] == u'1'); + Require(sceConvertKeycodeGetCharacter(0x3A, state, 0, character) == 0); + Require(character[0] == 0); + } return 0; } diff --git a/docs/dev/debt/libSceConvertKeycode.md b/docs/dev/debt/libSceConvertKeycode.md index 1f1b491f21..25d15eaa60 100644 --- a/docs/dev/debt/libSceConvertKeycode.md +++ b/docs/dev/debt/libSceConvertKeycode.md @@ -2,7 +2,7 @@ ### Unknown function info -- [sceConvertKeycodeGetCharacter](../../../core/libs/prx/libSceConvertKeycode/Export.cpp) (libSceConvertKeycode) - the signature `(uint16_t keycode, uint32_t, uint32_t keyboardType, char16_t* character)` is inferred from PPSA12361, which passes a 16-bit keycode, 0, the type from `sceConvertKeycodeGetImeKeyboardType` and a pointer it reads one UTF-16 unit from when the result is 0. The meaning of the second argument is unknown; a value other than 0 throws, and so does a keyboard type other than 0. +- [sceConvertKeycodeGetCharacter](../../../core/libs/prx/libSceConvertKeycode/Export.cpp) (libSceConvertKeycode) - the signature `(uint16_t keycode, uint32_t, uint32_t keyboardType, char16_t* character)` is inferred from PPSA12361, which passes a 16-bit keycode, 0, the type from `sceConvertKeycodeGetImeKeyboardType` and a pointer it reads one UTF-16 unit from when the result is 0. The meaning of the second argument is unknown. PRAGMATA (PPSA02530, #5726) and Onimusha: Way of the Sword (PPSA27836) pass `0x4000000` as a constant: Onimusha calls it for every keycode from 0 to 0x8F to build a table of key names, upper-cases `a` to `z` itself and skips keycodes that fail or return 0, so that bit is ignored and the unshifted character is returned as for 0. Any other bit throws, and so does a keyboard type other than 0. ### Functional From 9d9554948cff94bfbb928dbc0f746d767b37e182 Mon Sep 17 00:00:00 2001 From: Muhammad Wasif Ali Wasif <76695420+wachi35676@users.noreply.github.com> Date: Wed, 14 Oct 2026 00:47:37 +0500 Subject: [PATCH 0050/1000] fix(libc): ignore the space and + flags for unsigned printf conversions The guest formatter passed the flags of %o, %u, %x and %X to the host, and MinGW's printf prefixes a space to %u when the space flag is set: "% u" of 5 gave " 5". C17 7.21.6.1 applies both flags to signed conversions only, and FreeBSD 11.4 vfprintf clears the sign for every unsigned conversion (nosign). Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libc/include/WindowsFormatting.hpp | 4 ++++ core/libs/tests/Formatting.cpp | 2 ++ 2 files changed, 6 insertions(+) diff --git a/core/libs/prx/libc/include/WindowsFormatting.hpp b/core/libs/prx/libc/include/WindowsFormatting.hpp index f3deb658df..436549ba2c 100644 --- a/core/libs/prx/libc/include/WindowsFormatting.hpp +++ b/core/libs/prx/libc/include/WindowsFormatting.hpp @@ -327,6 +327,10 @@ inline int FormatWindows(char* buffer, size_t size, const char* format, const vo output.Value(spec + "ll" + conversion, value); } else if (std::strchr("ouxX", conversion)) { if (!integerLength) throw std::invalid_argument("Invalid integer length"); + for (size_t flag = 1; flag < spec.size() && std::strchr("-+ #0", spec[flag]);) { + if (spec[flag] == ' ' || spec[flag] == '+') spec.erase(flag, 1); + else ++flag; + } unsigned long long value; if (length.empty() || length == "h" || length == "hh") { value = args.Next(); diff --git a/core/libs/tests/Formatting.cpp b/core/libs/tests/Formatting.cpp index 8b2260536d..e8693b984e 100644 --- a/core/libs/tests/Formatting.cpp +++ b/core/libs/tests/Formatting.cpp @@ -102,6 +102,8 @@ static bool CheckWidePrecision() { Require(snprintf_nid_postfix(output, sizeof(output), "[%5.1ls]", u"\u00e9") == 7 && std::strcmp(output, "[ ]") == 0); Require(snprintf_nid_postfix(output, sizeof(output), "[%-5.2ls]", u"\u00e9") == 7 && std::strcmp(output, "[\xc3\xa9 ]") == 0); Require(FormatList(output, sizeof(output), "%.3ls", u"\U0001f600") == 0 && output[0] == 0); + Require(FormatList(output, sizeof(output), "%ls[% u][% 4u][%+ lu][%- 3hhu][% x][%+o]", u"", 5u, 5u, 5ULL, 5u, 255u, 8u) == 25 + && std::strcmp(output, "[5][ 5][5][5 ][ff][10]") == 0); Require(FormatList(output, sizeof(output), "%.*ls:%d", -1, u"\u00e9", 7) == 4 && std::strcmp(output, "\xc3\xa9:7") == 0); const char16_t bounded[] = {u'A', u'B'}; Require(snprintf_nid_postfix(output, sizeof(output), "%.2ls", bounded) == 2 && std::strcmp(output, "AB") == 0); From e671332d2ea2bdba23f2cf1356eb2640e02cbf82 Mon Sep 17 00:00:00 2001 From: Muhammad Wasif Ali Wasif <76695420+wachi35676@users.noreply.github.com> Date: Wed, 14 Oct 2026 01:21:14 +0500 Subject: [PATCH 0051/1000] fix(libc): keep lgamma finite for subnormal arguments on Windows The MinGW-w64 lgamma overflows to infinity for nonzero |x| at or below 2^-1024. FreeBSD msun e_lgamma_r.c returns -log(|x|) for every nonzero |x| below 2^-56, so take that branch before the host function. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libc/src/Math.cpp | 6 +++++- core/libs/tests/GuestMath.cpp | 8 ++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libc/src/Math.cpp b/core/libs/prx/libc/src/Math.cpp index ac5bcac781..44b05312d5 100644 --- a/core/libs/prx/libc/src/Math.cpp +++ b/core/libs/prx/libc/src/Math.cpp @@ -300,7 +300,11 @@ float APS5_VABI log1pf_nid_postfix(float x) { return std::log1p(x); } double APS5_VABI expm1_nid_postfix(double x) { return std::expm1(x); } float APS5_VABI expm1f_nid_postfix(float x) { return std::expm1(x); } float APS5_VABI erff_nid_postfix(float x) { return std::erf(x); } -double APS5_VABI lgamma_nid_postfix(double x) { return std::lgamma(x); } +double APS5_VABI lgamma_nid_postfix(double x) { + const std::uint64_t magnitude = std::bit_cast(x) & 0x7fffffffffffffffull; + if (magnitude != 0 && magnitude < 0x3c70000000000000ull) return -std::log(std::fabs(x)); + return std::lgamma(x); +} float APS5_VABI sinf_nid_postfix(float x) { return std::sin(x); } float APS5_VABI cosf_nid_postfix(float x) { return std::cos(x); } diff --git a/core/libs/tests/GuestMath.cpp b/core/libs/tests/GuestMath.cpp index 23c9d20d5d..0697528eef 100644 --- a/core/libs/tests/GuestMath.cpp +++ b/core/libs/tests/GuestMath.cpp @@ -867,6 +867,14 @@ int main() { Require(lgamma_nid_postfix(1.) == 0. && lgamma_nid_postfix(2.) == 0. && std::isinf(lgamma_nid_postfix(0.))); Require(std::abs(lgamma_nid_postfix(0.5) - 0.5723649429247001) < 1e-15 && std::abs(lgamma_nid_postfix(10.) - 12.801827480081469) < 1e-13); Require(std::abs(lgamma_nid_postfix(-0.5) - 1.2655121234846454) < 1e-15); + for (const double sign : {1.0, -1.0}) { + std::feclearexcept(FE_ALL_EXCEPT); + Require(std::abs(lgamma_nid_postfix(sign * std::numeric_limits::denorm_min()) - 744.44007192138122) < 1e-12); + Require(std::abs(lgamma_nid_postfix(sign * 0x1p-1025) - 710.47586007394398) < 1e-12); + Require(std::abs(lgamma_nid_postfix(sign * 0x1p-1022) - 708.39641853226408) < 1e-12); + Require(std::abs(lgamma_nid_postfix(sign * 0x1p-57) - 39.509389291916882) < 1e-13); + Require(std::fetestexcept(FE_OVERFLOW | FE_DIVBYZERO | FE_INVALID) == 0); + } Require(llroundl_nid_postfix(2.5L) == 3 && llroundl_nid_postfix(-2.5L) == -3 && llroundl_nid_postfix(0.49999999999999999L) == 0); Require(llroundl_nid_postfix(9007199254740993.0L) == 9007199254740993LL && llroundl_nid_postfix(-1e30L) == INT64_MIN); for (const int mode : {FE_TONEAREST, FE_DOWNWARD, FE_UPWARD, FE_TOWARDZERO}) { From e9c98900e7c6c466381a3d99ed8d558b6d9f053f Mon Sep 17 00:00:00 2001 From: Mehran Date: Wed, 14 Oct 2026 11:34:47 +0300 Subject: [PATCH 0052/1000] fix(agc): bind a V# past the range limit by the memory behind it --- .../Graphics/src/ShaderResources.cpp | 7 ++- .../prx/libSceAgcDriver/tests/Graphics.cpp | 44 +++++++++++++++++-- docs/dev/debt/libSceAgcDriver.md | 1 + 3 files changed, 48 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index f09d2cbcb6..2c93158d0b 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -2900,7 +2900,12 @@ std::size_t ShaderResources::addGuestBuffer(std::span words const ShaderRecompiler::ShaderBufferResource descriptor{{words[0], words[1], words[2], words[3]}}; Require(descriptor.Type() == 0u, "buffer descriptor uses an unsupported type"); const auto address = descriptor.Base48(); - const auto byteSize = descriptor.GetSize(); + auto byteSize = descriptor.GetSize(); + if (byteSize > context.limits.maxStorageBufferRange) { + const auto mappedEnd = RegisteredReadableEnd(address); + if (mappedEnd <= address) byteSize = 0; + else if (mappedEnd - address < context.limits.maxStorageBufferRange) byteSize = mappedEnd - address; + } if (byteSize == 0 || address == 0) { allocations.push_back({0, EmptyBufferBytes, false, nullptr, ShaderRecompiler::DescriptorRole::GuestBuffers, false}); return allocations.size() - 1; diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 66467a9cef..e465127dff 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -3147,12 +3147,49 @@ void expectSingleAccepted(const ShaderRecompiler::DescriptorBinding& binding, st ShaderRecompiler::RecompileResult fragment; vertex.bindings.push_back(binding); mock = MockVulkan{}; - const auto context = mockContext(); + auto context = mockContext(); + context.emptyBuffer = reinterpret_cast(std::uintptr_t{0x1000}); const auto color = AgcDriver::Graphics::DecodeState(makeState()).color; { AgcDriver::Graphics::ShaderResources resources(context, vertex, fragment, color, 0, 0); } Require(mock.live == 0, std::string(what) + " leaked Vulkan objects"); } +void oversizedDescriptorTests() { + constexpr std::size_t blockBytes = 65536; + constexpr std::size_t tailBytes = 8192; + void* block = ::operator new(blockBytes, std::align_val_t{blockBytes}); + std::memset(block, 0x5a, blockBytes); + { + auto* mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix(); + GuestAllocations::GuestAllocationsAdd_nid_postfix(mutation, block, blockBytes, true, true, true); + GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation); + } + const auto oversized = [](const void* pointer) { return makeBinding(Role::GuestBuffers, 0, 1, vsharp(pointer, 0xffffffffu)); }; + const auto* tail = static_cast(block) + blockBytes - tailBytes; + { + ShaderRecompiler::RecompileResult vertex; + ShaderRecompiler::RecompileResult fragment; + vertex.bindings.push_back(oversized(tail)); + mock = MockVulkan{}; + const auto context = mockContext(); + const auto color = AgcDriver::Graphics::DecodeState(makeState()).color; + { + AgcDriver::Graphics::ShaderResources resources(context, vertex, fragment, color, 0, 0); + const auto& write = findWrite(0); + Require(write.buffers.size() == 1 && write.buffers[0].range == tailBytes && sameBytes(bufferBytes(write.buffers[0].buffer), tail, tailBytes), "a V# past the range limit was not bound up to the end of its registered allocation"); + } + Require(mock.live == 0, "a V# bound up to its registered allocation leaked Vulkan objects"); + } + expectSingleFailure(oversized(block), "descriptor range limit"); + expectSingleAccepted(oversized(reinterpret_cast(0x1000)), "an unregistered V# past the range limit"); + { + auto* mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix(); + GuestAllocations::GuestAllocationsRemove_nid_postfix(mutation, block); + GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation); + } + ::operator delete(block, std::align_val_t{blockBytes}); +} + void descriptorLayoutFlagTests() { constexpr VkDescriptorBindingFlags indexed = VK_DESCRIPTOR_BINDING_PARTIALLY_BOUND_BIT | VK_DESCRIPTOR_BINDING_VARIABLE_DESCRIPTOR_COUNT_BIT; mock = MockVulkan{}; @@ -3450,8 +3487,8 @@ void resourceTests() { expectSingleFailure(changed([](auto& binding) { binding.role = Role::FlattenedSrt; binding.guestDescriptor.clear(); }), "empty shader data descriptor"); expectSingleFailure(changed([](auto& binding) { binding.guestDescriptor[1] |= 0x40000000u; }), "reserved bits"); expectSingleFailure(changed([](auto& binding) { binding.guestDescriptor[3] |= 0x40000000u; }), "unsupported type"); - expectSingleFailure(changed([](auto& binding) { binding.guestDescriptor[1] |= 0x3fffu << 16u; binding.guestDescriptor[2] = 0xffffffffu; }), "descriptor range limit"); - expectSingleFailure(changed([](auto& binding) { binding.guestDescriptor[2] = 32768; }), "descriptor range limit"); + expectSingleAccepted(changed([](auto& binding) { binding.guestDescriptor[1] |= 0x3fffu << 16u; binding.guestDescriptor[2] = 0xffffffffu; }), "an unregistered V# past the range limit"); + expectSingleAccepted(changed([](auto& binding) { binding.guestDescriptor[2] = 32768; }), "an unregistered V# past the range limit"); expectSingleAccepted(changed([](auto& binding) { binding.guestDescriptor = vsharp(reinterpret_cast(0x1000), 8); }), "an unmapped V#"); expectSingleFailure(changed([&](auto& binding) { binding.guestDescriptor = vsharp(reinterpret_cast(state.color.address), 64); }), "aliases the render target"); expectSingleAccepted(changed([](auto& binding) { binding.count = 3; binding.guestDescriptor = join(join(vsharp(guestFirst.data(), 16), vsharp(guestSecond.data(), 32)), vsharp(reinterpret_cast(0x1000), 8)); }), "an unmapped V# element"); @@ -5905,6 +5942,7 @@ int main(int argc, char** argv) { descriptorPoolTests(); pushConstantTests(); resourceTests(); + oversizedDescriptorTests(); descriptorSnapshotTests(); descriptorCacheTests(); misalignedShaderDataTests(); diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 821e37ade6..eacae38315 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -14,6 +14,7 @@ - [SET_PREDICATION](../../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (AGC driver) - boolean predication follows the AMD PM4 encoding [Mesa](https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/src/amd/common/sid.h) uses: operation 3 reads 64 bits and 4 reads 32 bits, and bit 8 runs predicated packets when the value is non-zero (clear: when it is zero); the bit 12 hint is ignored. The address must have its low 4 bits clear, as `sceAgcDcbSetPredication` writes it. Query predication (Z-pass, primitive count), a predicated COND_EXEC, predicated command buffer chains and flips in predicated command buffers throw - [NGG vertex programs on the mesh path](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - an unregistered vertex program with `PRIMGEN_EN` that uses LDS or `s_barrier` runs on the geometry (mesh) path. Each input primitive is assumed to yield at most one output primitive, the subgroup is bounded by `GE_MAX_OUTPUT_PER_SUBGROUP` as a vertex budget, and the GS/ES VGPR count checks are skipped, because the merged inputs keep their positions. Taken from the code Mesa's ACO emits for NGG culling and from PPSA59322 and PPSA99010 draws, not measured on hardware. Registered programs keep the vertex path - [Strided buffer overhang](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - a strided buffer descriptor that ends less than one stride past the end of the readable registered allocation holding its base is bound only up to that allocation's end, even when another readable allocation starts right there. This assumes the bytes past the end are the rounded-up `num_records` of the last record (RADV rounds it up), which no fetch inside the buffer reads; a guest that really keeps a vertex split across two adjacent allocations would read zeros for the cut tail. Taken from the RetroArch Dolphin core case in #2912, not measured on hardware. +- [Buffer descriptors past the range limit](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - a buffer V# whose size is over `maxStorageBufferRange` is bound only up to the end of the readable registered allocation holding its base when that allocation ends before the limit, and as the empty placeholder buffer, like a null V#, when no registered allocation holds its base; a V# over an allocation that reaches the limit still throws. Titles leave such words in descriptor slots their shaders do not reach on the path taken (PPSA10528 binds `c8010002 2c6bec20 c0053c00 04000013`, about 33 TB from an address above the user address space). What the console returns for an access past the bound range is not known; here the device's robust buffer access decides. Follows the review of #3202, not measured on hardware. - [Zero-stride vertex fetch range](../../../core/libs/prx/libSceAgcDriver/Graphics/include/VertexInput.hpp) (libSceAgcDriver) - for a stride-0 V# with OOB_SELECT 2 the whole fetch reads memory unless NUM_RECORDS is 0; with OOB_SELECT 3 it reads zero when NUM_RECORDS is 0 and throws when the fetch only partly fits. Rules taken from the GFX10 OOB_SELECT table in Mesa [ac_descriptors.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/src/amd/common/ac_descriptors.c), not measured on hardware. Whether OOB_SELECT 3 checks each dword of a multi-dword fetch separately is unknown. OOB_SELECT 0 and 1 keep the structured record checks. - [Resource registration](../../../core/libs/prx/libSceAgcDriver/Resource/src/Registration.cpp) (libSceAgcDriver) - registration always fails with `RESOURCE_REGISTRATION_UNAVAILABLE`, so the owner and resource queries, `sceAgcDriverSetResourceUserData`, `sceAgcDriverFindResourcesPublic` and `sceAgcDriverUnregisterAllResourcesForOwner` return the same error without reading their arguments. Their parameter lists follow the libSceGnmDriver counterparts and are unverified. `sceAgcDriverGetResourceRegistrationMaxNameLength` writes 0xfc and returns 0, as [prosper](https://github.com/mattias800/prosper) does from a title's registration configuration; the `uint32_t*` parameter matches the SDK wrapper `sce::Agc::ResourceRegistration::getMaxNameLength(unsigned int*)`. sharpemu writes 256 and KytyPS5 returns `RESOURCE_REGISTRATION_UNAVAILABLE`, so the value is unverified; a null output throws - [sceAgcDriverRegisterMultipleResources](../../../core/libs/prx/libSceAgcDriver/Resource/src/Registration.cpp) uses the existing unavailable-registration state and leaves caller memory untouched. Its four arguments (uint32 handle output, uint32 owner, descriptor pointer, uint32 count) follow the original PPSA34502 / 01.010.000 codzm call. The caller builds 40-byte descriptors and explicitly accepts 0x8A6C9018 with an invalid output handle. Enabled registration and console argument-validation precedence are unverified. From 5a7f183a3da29a68d23c16d13377584fb1aa6556 Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Wed, 14 Oct 2026 13:14:47 +0200 Subject: [PATCH 0053/1000] fix(libSceAgcDriver): return 0 from sceAgcDriverRegisterDefaultOwner Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libSceAgcDriver/Unimplemented.cpp | 6 +++--- core/libs/tests/GuestAgcDriverResources.cpp | 3 +++ docs/dev/debt/libSceAgcDriver.md | 4 ++++ 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Unimplemented.cpp b/core/libs/prx/libSceAgcDriver/Unimplemented.cpp index 1a2d0f5c58..ddb13df0a1 100644 --- a/core/libs/prx/libSceAgcDriver/Unimplemented.cpp +++ b/core/libs/prx/libSceAgcDriver/Unimplemented.cpp @@ -15,9 +15,9 @@ int APS5_VABI sceAgcDriverGetShaderDebuggingStatus() { return 1; } -int APS5_VABI sceAgcDriverRegisterDefaultOwner(uint32_t* owner_handle) { - (void)owner_handle; - return static_cast(0x8A6C9018); +int APS5_VABI sceAgcDriverRegisterDefaultOwner(uint32_t owner) { + (void)owner; + return 0; } int APS5_VABI sceAgcDriverSetValidationErrorOutputFrequency(uint32_t frequency) { diff --git a/core/libs/tests/GuestAgcDriverResources.cpp b/core/libs/tests/GuestAgcDriverResources.cpp index df3981cfb8..0406b512c3 100644 --- a/core/libs/tests/GuestAgcDriverResources.cpp +++ b/core/libs/tests/GuestAgcDriverResources.cpp @@ -7,6 +7,7 @@ #include extern "C" { +int APS5_VABI sceAgcDriverRegisterDefaultOwner(std::uint32_t); int APS5_VABI sceAgcDriverRegisterOwner(std::uint32_t*, const char*); int APS5_VABI sceAgcDriverRegisterResource(std::uint32_t*, std::uint32_t, const void*, std::size_t, const char*, std::uint32_t, std::uint64_t); int APS5_VABI sceAgcDriverRegisterMultipleResources(std::uint32_t*, std::uint32_t, const void*, std::uint32_t); @@ -47,6 +48,8 @@ int main() { std::uint32_t owner = 7u; std::uint32_t resource = 9u; const std::uint32_t memory[4]{}; + Require(sceAgcDriverRegisterDefaultOwner(0u) == 0); + Require(sceAgcDriverRegisterDefaultOwner(owner) == 0); Require(sceAgcDriverRegisterOwner(&owner, "owner") == Unavailable); Require(sceAgcDriverRegisterResource(&resource, owner, memory, sizeof(memory), "resource", 1u, 2u) == Unavailable); Require(sceAgcDriverRegisterGdsResource(&resource, owner, 0u, 64u, "gds", 1u, 2u) == Unavailable); diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 111c9e39ff..09a7bf9e19 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -1,5 +1,9 @@ # libSceAgcDriver technical debt +### Silent stubs + +- [sceAgcDriverRegisterDefaultOwner](../../../core/libs/prx/libSceAgcDriver/Unimplemented.cpp) (libSceAgcDriver) - takes one uint32 owner by value (OpenAGC `include/agcdriver.h`, SharpEmu `AgcExports.DriverResources.cs`) and returns 0 without recording it. PPSA21480 treats the code 0x8A6C9018 it returned before as fatal. Console behaviour is unverified: OpenAGC describes the function as a firmware 5.50 stub that returns 0x8A6C9018, SharpEmu returns 0 + ### Unknown function info - [Inactive vertex point size](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) - `PA_CL_VS_OUT_CNTL.USE_VTX_POINT_SIZE` is accepted for lines, triangles and rectangles with `GS_EN` and `HS_EN` clear, when polygon mode is disabled or both faces are filled. Points, patches and absent routing or rasterization registers still throw. Bit 16 selects the misc export's X component as PointSize ([AMD CIK register reference](https://www.amd.com/content/dam/amd/en/documents/radeon-tech-docs/programmer-references/CIK_3D_registers_v2.pdf), PA_CL_VS_OUT_CNTL); ignoring it for non-point rasterization follows [Vulkan PointSize](https://docs.vulkan.org/refpages/latest/refpages/source/PointSize.html). This is inferred, not measured on console hardware. XashPS5 (PPSA19111 v1.3) sets `0x01210000` on a filled triangle draw. From 3c81a744bdf7f7898b64d0516c03cc1ac2697e1e Mon Sep 17 00:00:00 2001 From: billythemenace2 Date: Wed, 14 Oct 2026 14:02:14 +0100 Subject: [PATCH 0054/1000] fix(kernel): read a zero flexibleMemorySize in param.json as the default budget SONIC SUPERSTARS (PPSA06888) ships "kernel": {"flexibleMemorySize": 0}, which the parser rejected, so the title stopped before its first module loaded. A zero size now leaves the default budget, like an absent one. Co-Authored-By: Claude Opus 5.5 (1M context) --- core/libs/prx/libkernel/AppMetadata/src/ParamJsonParser.cpp | 6 +++--- core/libs/tests/ParamJsonParser.cpp | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libkernel/AppMetadata/src/ParamJsonParser.cpp b/core/libs/prx/libkernel/AppMetadata/src/ParamJsonParser.cpp index 8cde56681f..e8ac7d212a 100644 --- a/core/libs/prx/libkernel/AppMetadata/src/ParamJsonParser.cpp +++ b/core/libs/prx/libkernel/AppMetadata/src/ParamJsonParser.cpp @@ -337,9 +337,9 @@ ParsedParamJson parseParamJson(const std::filesystem::path& paramJsonPath) { const auto parsed = std::from_chars(number.data(), number.data() + number.size(), bytes); if (parsed.ec != std::errc{} || parsed.ptr != number.data() + number.size()) throw std::runtime_error("param.json kernel.flexibleMemorySize exceeds uint64 range"); - if (bytes == 0 || (bytes & (PS5_PAGE_SIZE - 1)) != 0) - throw std::runtime_error("param.json kernel.flexibleMemorySize must be a positive multiple of 16 KiB"); - result.flexibleMemorySizeBytes = bytes; + if ((bytes & (PS5_PAGE_SIZE - 1)) != 0) + throw std::runtime_error("param.json kernel.flexibleMemorySize must be a multiple of 16 KiB"); + if (bytes != 0) result.flexibleMemorySizeBytes = bytes; } } if (const JsonValue* downloadData = findObjectMember(root, "downloadDataSize")) { diff --git a/core/libs/tests/ParamJsonParser.cpp b/core/libs/tests/ParamJsonParser.cpp index 719a0852ff..8d9a2ffbd9 100644 --- a/core/libs/tests/ParamJsonParser.cpp +++ b/core/libs/tests/ParamJsonParser.cpp @@ -102,11 +102,12 @@ static void CheckFlexibleMemorySize(const std::filesystem::path& path) { const std::string prefix = R"({"titleId":"PPSA00000","localizedParameters":{"en-US":{"titleName":"Example"}})"; Require(!ParseText(path, prefix + "}").flexibleMemorySizeBytes, "Absent kernel changed the default budget"); Require(!ParseText(path, prefix + R"(,"kernel":{}})").flexibleMemorySizeBytes, "Absent flexible size changed the default budget"); + Require(!ParseText(path, prefix + R"(,"kernel":{"flexibleMemorySize":0}})").flexibleMemorySizeBytes, "A zero flexible size changed the default budget"); for (const auto bytes : {std::uint64_t{16384}, std::uint64_t{838860800}, std::uint64_t{18446744073709535232ull}}) { const auto parsed = ParseText(path, prefix + R"(,"kernel":{"flexibleMemorySize":)" + std::to_string(bytes) + "}}"); Require(parsed.flexibleMemorySizeBytes == bytes, "Incorrect flexible memory size in bytes"); } - for (const auto* value : {"0", "-0", "-16384", "1", "16385", "838860800.0", "8e8", "true", "null", "\"838860800\"", "18446744073709551616"}) { + for (const auto* value : {"-0", "-16384", "1", "16385", "838860800.0", "8e8", "true", "null", "\"838860800\"", "18446744073709551616"}) { bool rejected = false; try { ParseText(path, prefix + R"(,"kernel":{"flexibleMemorySize":)" + value + "}}"); } catch (const std::runtime_error&) { rejected = true; } From 1f7bf7413f4355b302d41621c5f34e0b82e0e76b Mon Sep 17 00:00:00 2001 From: Piglit09 Date: Wed, 14 Oct 2026 07:25:12 -0500 Subject: [PATCH 0055/1000] fix(relinker): protect Windows TLS code entry roots --- core/relinker/CMakeLists.txt | 2 + .../src/windows/WindowsTlsBuilder.cpp | 10 +- .../analysis/CodeInstructionCollector.hpp | 2 +- .../src/analysis/CodeInstructionCollector.cpp | 4 +- .../test_windows_tls_protected_entries.py | 96 +++++++++++++++++++ 5 files changed, 111 insertions(+), 3 deletions(-) create mode 100644 core/relinker/relinker/tests/test_windows_tls_protected_entries.py diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index 0b57570996..f7774e3e3b 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -407,6 +407,8 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_optional_plt.py $) add_test(NAME empty_tls COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_empty_tls.py $) + add_test(NAME windows_tls_protected_entries COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_windows_tls_protected_entries.py $) set(tlsCoverageGroups metadata padding register_loads displacement_bounds alu_execution register_address_loads moved_branch rejected) foreach(register IN ITEMS 0 1 2 3 5 6 7 8 9 10 11 12 13 14 15) list(APPEND tlsCoverageGroups displacement_r${register}) diff --git a/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp b/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp index 2a017013c4..a0033f242d 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp @@ -99,7 +99,8 @@ PeDirectory WindowsTlsBuilder::Build(const std::vector& source, co std::vector accesses; std::set branchTargets; - const auto instructions = Relinker::CodeInstructionCollector().Collect(source, headers, [&](auto completed, auto total) { report(7000 * completed / total); }); + std::set entryRoots; + const auto instructions = Relinker::CodeInstructionCollector().Collect(source, headers, [&](auto completed, auto total) { report(7000 * completed / total); }, &entryRoots); std::uint64_t scanned = 0; for (const auto& header : headers) { @@ -120,6 +121,11 @@ PeDirectory WindowsTlsBuilder::Build(const std::vector& source, co const auto rva = image.GetRva(header.MappedAddress + offset, info.Length); const bool insidePrevious = *instruction < decodedEnd; decodedEnd = std::max(decodedEnd, *instruction + info.Length); + const auto checkProtectedEntry = [&](const std::size_t length) { + const auto entry = entryRoots.upper_bound(*instruction); + if (entry != entryRoots.end() && *entry - *instruction < length) + throw Domain::RelinkerException("TLS rewrite would overwrite an executable entry point", header.Offset + *entry - header.MappedAddress); + }; if (info.HasBranchTarget && !info.HasRipRelativeDisp) { const auto target = static_cast(rva) + static_cast(info.Length) + info.BranchDisp; @@ -175,6 +181,7 @@ PeDirectory WindowsTlsBuilder::Build(const std::vector& source, co moved.push_back({CheckedRva(rva + length), std::move(movedBytes), next.HasRipRelativeDisp ? std::optional(next.RipRelativeDispOffset) : std::nullopt, branchTarget, branchDisplacementOffset}); length += next.Length; } + checkProtectedEntry(length); accesses.push_back({rva, header.Offset + offset, length, false, 0, addressRegister, 0, isAlu ? bytes[position] : std::uint8_t{0}, *address, wide, std::move(moved)}); continue; } @@ -186,6 +193,7 @@ PeDirectory WindowsTlsBuilder::Build(const std::vector& source, co message << ')'; throw Domain::RelinkerException(message.str(), header.Offset + offset); } + checkProtectedEntry(info.Length); accesses.push_back({rva, header.Offset + offset, info.Length, storeImmediate, storeImmediate ? Io::ReadU32(source, header.Offset + offset + position + 7) : 0, storeImmediate ? std::uint8_t{0} : loadRegister, storeImmediate ? 0 : Io::ReadU32(source, header.Offset + offset + position + 3), aluRead ? bytes[position] : std::uint8_t{0}}); } } diff --git a/core/relinker/relinker/include/relinker/analysis/CodeInstructionCollector.hpp b/core/relinker/relinker/include/relinker/analysis/CodeInstructionCollector.hpp index 9169186da7..de93097012 100644 --- a/core/relinker/relinker/include/relinker/analysis/CodeInstructionCollector.hpp +++ b/core/relinker/relinker/include/relinker/analysis/CodeInstructionCollector.hpp @@ -9,7 +9,7 @@ namespace Relinker { class CodeInstructionCollector { public: - std::set Collect(const std::vector& bytes, const std::vector& headers, const std::function& progress = {}) const; + std::set Collect(const std::vector& bytes, const std::vector& headers, const std::function& progress = {}, std::set* entryRoots = nullptr) const; }; } diff --git a/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp b/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp index c5ffda7b73..8b83ab475e 100644 --- a/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp +++ b/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp @@ -13,6 +13,7 @@ #include #include #include +#include namespace Relinker { @@ -35,7 +36,7 @@ public: } -std::set CodeInstructionCollector::Collect(const std::vector& bytes, const std::vector& headers, const std::function& progress) const { +std::set CodeInstructionCollector::Collect(const std::vector& bytes, const std::vector& headers, const std::function& progress, std::set* entryRoots) const { std::uint64_t lastProgress = 0; const auto report = [&](std::uint64_t completed) { if (completed <= lastProgress) return; @@ -438,6 +439,7 @@ std::set CodeInstructionCollector::Collect(const std::ve } } report(10000); + if (entryRoots) *entryRoots = std::move(roots); return instructions; } diff --git a/core/relinker/relinker/tests/test_windows_tls_protected_entries.py b/core/relinker/relinker/tests/test_windows_tls_protected_entries.py new file mode 100644 index 0000000000..4b7f2bd674 --- /dev/null +++ b/core/relinker/relinker/tests/test_windows_tls_protected_entries.py @@ -0,0 +1,96 @@ +"""Keep independently reachable code entries out of Windows TLS jump patches.""" + +from pathlib import Path +import struct +import subprocess +import sys +import tempfile + + +TLS_AND_FOLLOWER = bytes.fromhex("64 48 8b 08 b8 2a 00 00 00 c3") +SEPARATE_FUNCTION = bytes.fromhex("b8 2a 00 00 00 c3") + + +def make_image(root=None): + image = bytearray(0x1000) + image[:16] = b"\x7fELF\x02\x01\x01" + bytes(9) + struct.pack_into(" Date: Wed, 14 Oct 2026 06:04:34 -0700 Subject: [PATCH 0056/1000] fix(tools): reject mixed NID modes with empty filters --- tools/nid_names.py | 7 ++++--- tools/tests/test_nid_names.py | 22 ++++++++++++++++++++++ 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/tools/nid_names.py b/tools/nid_names.py index 0df8c3f147..2c4cce5a23 100644 --- a/tools/nid_names.py +++ b/tools/nid_names.py @@ -163,9 +163,11 @@ def main(argv=None): options = [option for action in parser._actions for option in action.option_strings] arguments = sys.argv[1:] if argv is None else argv args = parser.parse_args(nid_arguments(arguments, options)) + requested_options = {option_name(argument, options) for argument in arguments} + requested_nids = "--nid" in requested_options + if requested_nids and requested_options & {"--name", "--symbol"}: + parser.error("--name/--symbol cannot be combined with --nid") if args.name: - if args.nid: - parser.error("--name/--symbol cannot be combined with --nid") rows = [{"name": name, "nid": compute_nid(name)} for name in args.name] if args.json: print(json.dumps({"symbols": rows}, indent=1)) @@ -174,7 +176,6 @@ def main(argv=None): for row in rows: print("%-40s %s" % (row["name"], row["nid"])) return 0 - requested_nids = any(option_name(argument, options) == "--nid" for argument in arguments) for nid in args.nid or []: if re.fullmatch(r"[A-Za-z0-9+\-]{11}", nid) is None: parser.error(f"invalid NID {nid!r}: expected 11 characters from A-Z, a-z, 0-9, + or -") diff --git a/tools/tests/test_nid_names.py b/tools/tests/test_nid_names.py index 6dd4b20804..b4f6516f96 100644 --- a/tools/tests/test_nid_names.py +++ b/tools/tests/test_nid_names.py @@ -94,6 +94,28 @@ class DatabaseCacheTests(unittest.TestCase): class NidArgumentTests(unittest.TestCase): options = ["-h", "--help", "--db", "--nid", "--json"] + def test_name_and_nid_modes_conflict_even_with_empty_filters(self): + for name_option in ("--name", "--symbol", "--na", "--sym"): + for nid_option in ("--nid", "--ni"): + for name_values, nid_values in (([], []), (["ceilf"], []), ([], ["AAAAAAAAAAA"])): + names = [name_option, *name_values] + nids = [nid_option, *nid_values] + for arguments in (names + nids, nids + names): + with self.subTest(arguments=arguments), \ + contextlib.redirect_stdout(io.StringIO()) as output, \ + contextlib.redirect_stderr(io.StringIO()) as error, \ + patch.object(nid_names, "load_db", return_value={}) as database, \ + patch.object(nid_names, "collect_unknowns", return_value={}) as sources, \ + patch.object(nid_names, "collect_real_names", return_value=set()) as definitions: + with self.assertRaises(SystemExit) as exited: + nid_names.main([*arguments, "--json"]) + self.assertEqual(exited.exception.code, 2) + self.assertIn("--name/--symbol cannot be combined with --nid", error.getvalue()) + self.assertEqual(output.getvalue(), "") + database.assert_not_called() + sources.assert_not_called() + definitions.assert_not_called() + def test_invalid_nids_fail_before_database_or_source_access(self): for nid in ("short", "ABCDEFGHIJKL", "ABCDEFGHIJ/", "ABCDEFGHIJ_", "ABCDEFGHIJ\u00e9", "", "--jsooon"): with self.subTest(nid=nid): From 541a4e705296fc5699bfedd334c4c0dfed8751ea Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Wed, 14 Oct 2026 06:04:39 +0000 Subject: [PATCH 0057/1000] test(tools): shard conventions comment checks under CTest timeout --- CMakeLists.txt | 12 ++++- .../tests/test_check_conventions_comments.py | 45 +++++++++++++++++++ 2 files changed, 55 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 0c47828842..261c4e031f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -109,8 +109,16 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_import_audit.py) add_test(NAME unwrap_self COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_unwrap_self.py) - add_test(NAME conventions_function_bodies COMMAND ${Python3_EXECUTABLE} - ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_comments.py) + foreach(shard RANGE 0 5) + if(shard EQUAL 0) + set(conventions_test_name conventions_function_bodies) + else() + set(conventions_test_name conventions_function_bodies_shard_${shard}) + endif() + add_test(NAME ${conventions_test_name} COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_comments.py --shard ${shard}/6) + set_tests_properties(${conventions_test_name} PROPERTIES TIMEOUT 30) + endforeach() add_test(NAME conventions_wrapped_declarations COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_wrapped_declarations.py) add_test(NAME conventions_ci_gate COMMAND ${Python3_EXECUTABLE} diff --git a/tools/tests/test_check_conventions_comments.py b/tools/tests/test_check_conventions_comments.py index 25e0b1f871..e06c3c4386 100644 --- a/tools/tests/test_check_conventions_comments.py +++ b/tools/tests/test_check_conventions_comments.py @@ -11,6 +11,39 @@ from unittest.mock import patch sys.path.insert(0, str(Path(__file__).resolve().parents[1])) from check_conventions import STUB_BODY, SYSTEM_DEPENDENCY, Check, function_body, links, output +CONVENTIONS_TEST_SHARDS = 6 + + +def iter_test_cases(suite): + for test in suite: + if isinstance(test, unittest.TestSuite): + yield from iter_test_cases(test) + else: + yield test + + +def discovered_test_cases(): + suite = unittest.defaultTestLoader.loadTestsFromModule(sys.modules[__name__]) + return sorted(iter_test_cases(suite), key=lambda test: test.id()) + + +def conventions_test_shard(shard, shard_count=CONVENTIONS_TEST_SHARDS): + if shard_count <= 0 or shard < 0 or shard >= shard_count: + raise ValueError("shard index must be within the shard count") + tests = discovered_test_cases() + return unittest.TestSuite(test for index, test in enumerate(tests) if index % shard_count == shard) + + +class TestShardTests(unittest.TestCase): + def test_shards_discover_each_case_once(self): + discovered = [test.id() for test in discovered_test_cases()] + sharded = [test.id() for shard in range(CONVENTIONS_TEST_SHARDS) + for test in iter_test_cases(conventions_test_shard(shard))] + self.assertEqual(sorted(sharded), sorted(discovered)) + self.assertEqual(len(sharded), len(set(sharded))) + self.assertTrue(all(len(list(iter_test_cases(conventions_test_shard(shard)))) > 0 + for shard in range(CONVENTIONS_TEST_SHARDS))) + class FunctionBodyTests(unittest.TestCase): def test_line_comments_preserve_following_statements(self): @@ -596,4 +629,16 @@ class RepositoryChecksTests(unittest.TestCase): if __name__ == "__main__": + if len(sys.argv) > 1 and sys.argv[1] == "--shard": + if len(sys.argv) != 3 or "/" not in sys.argv[2]: + print("usage: test_check_conventions_comments.py --shard INDEX/COUNT", file=sys.stderr) + raise SystemExit(2) + try: + shard_index, shard_count = (int(part) for part in sys.argv[2].split("/", 1)) + suite = conventions_test_shard(shard_index, shard_count) + except ValueError as error: + print(f"invalid shard: {error}", file=sys.stderr) + raise SystemExit(2) + result = unittest.TextTestRunner().run(suite) + raise SystemExit(0 if result.wasSuccessful() else 1) unittest.main() From d422c760cdf10401264b669324c813bde4bfa114 Mon Sep 17 00:00:00 2001 From: Gjon Gojcevic Date: Wed, 14 Oct 2026 09:38:31 -0400 Subject: [PATCH 0058/1000] perf(shader): look up the BDA range of a user-data pointer once at program entry Address-based reads whose 64-bit base is a pair of user-data registers search the BDA table at every read, although the base is one value for the whole draw. Where a program has two or more such reads of one base (at most four bases, those with the most reads), the range holding the base is now searched once at the program's entry, by the subgroup's elected lane when the program already uses subgroup ballots. Each read then takes one branch: when that range holds all of its bytes, and for dword reads the device address is 4-aligned, it loads them directly; otherwise it runs the lookup path as before, so data, faults, invocation stops and wraparound are unchanged. Ranges ending above 0xffffffff00000000, whose base plus a 32-bit offset could wrap, and ranges whose device addresses wrap are not used. The direct path relies on the table being ascending and disjoint, which the driver builds and now requires (BdaAbi::RangesOrdered). AI-assisted: yes --- .../Graphics/src/BdaResources.cpp | 1 + .../libSceAgcDriver/tests/BdaContracts.cpp | 6 + .../libSceAgcDriver/tests/BdaExecution.cpp | 125 +++++++++++ .../prx/libSceAgcDriver/tests/BdaShader.cpp | 87 ++++++++ .../prx/libSceAgcDriver/tests/BdaShader.hpp | 15 ++ .../libSceAgcDriver/tests/ShaderMemory.cpp | 74 ++++++ core/shader/recompiler/BdaAbi.hpp | 8 + core/shader/recompiler/CMakeLists.txt | 1 + .../include/SpirvBackend/SpirvBda.hpp | 6 + .../SpirvBackend/SpirvEmitterState.hpp | 9 + .../SpirvBackend/src/SpirvBdaLookup.cpp | 28 ++- .../SpirvBackend/src/SpirvBdaRead.cpp | 211 ++++++++++++++++++ .../SpirvMemory/SpirvMemoryInstructions.cpp | 28 ++- .../SpirvBackend/src/SpirvModuleEmitter.cpp | 1 + 14 files changed, 591 insertions(+), 9 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp index 0b94d5e0c9..4bb8b097ed 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp @@ -192,6 +192,7 @@ BdaResources::BdaResources(const Context& context, const GuestBufferMemory& memo ++it; } } + Require(ShaderRecompiler::BdaAbi::RangesOrdered(ranges.data(), ranges.size()), "BDA table ranges are not ascending and disjoint"); for (const auto& range : ranges) Require((range.permissions & ShaderRecompiler::BdaAbi::Write) == 0 || range.end <= ShaderRecompiler::BdaAbi::WrittenAddressLimit, "a writable BDA range ends past the addresses the written-page record can hold"); table = std::make_shared(context, tableBytes, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); const ShaderRecompiler::BdaAbi::Header header{ShaderRecompiler::BdaAbi::Version, static_cast(ranges.size()), sizeof(ShaderRecompiler::BdaAbi::Range), 0}; diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaContracts.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaContracts.cpp index cf54e315dd..dd65417557 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaContracts.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaContracts.cpp @@ -2,6 +2,7 @@ #include "ControlFlow/RequestSerializer.hpp" #include "SpirvBackend/SpirvBda.hpp" #include +#include namespace { @@ -56,4 +57,9 @@ void RunBdaContractTests() { invalid[0] = invalid[0] == 'A' ? 'B' : 'A'; reject([&] { static_cast(serializer.Deserialize(invalid)); }, "request signature"); reject([&] { static_cast(serializer.Deserialize("NVNQQWMAAAA=")); }, "serialization version"); + const auto ordered = [](std::initializer_list ranges) { return BdaAbi::RangesOrdered(ranges.begin(), ranges.size()); }; + AgcDriver::Graphics::Require(ordered({}) && ordered({{0x1000, 0x2000, 1, 1, 0}, {0x2000, 0x2000, 1, 1, 0}, {0x2000, 0x3000, 1, 1, 0}, {0x4000, 0x5000, 1, 1, 0}}), "ascending disjoint BDA ranges were refused"); + AgcDriver::Graphics::Require(!ordered({{0x1000, 0x3000, 1, 1, 0}, {0x2000, 0x2100, 1, 1, 0}}), "overlapping BDA ranges were accepted"); + AgcDriver::Graphics::Require(!ordered({{0x4000, 0x5000, 1, 1, 0}, {0x1000, 0x2000, 1, 1, 0}}), "unsorted BDA ranges were accepted"); + AgcDriver::Graphics::Require(!ordered({{0x2000, 0x1000, 1, 1, 0}}), "a BDA range ending before its begin was accepted"); } diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaExecution.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaExecution.cpp index 2804e7e81b..e3a7a3f99d 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaExecution.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaExecution.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -213,6 +214,129 @@ static void RunBdaDwordWriteTests(const Context& context) { write(guest + 0x7c, 2, 8, static_cast(0), 64); } +static void RunBdaAnchorTests(const Context& context) { + namespace Abi = ShaderRecompiler::BdaAbi; + constexpr std::uint64_t guest = 0x7fff12340001ULL; + constexpr std::uint64_t limit = 0xffffffff00000000ULL; + constexpr std::uint64_t highest = 0xffffffffffffff00ULL; + constexpr std::uint32_t lanes = 64; + const auto usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT; + Buffer first(context, 3, usage); + Buffer second(context, 2, usage); + Buffer words(context, 16, usage); + Buffer shifted(context, 20, usage); + Buffer large(context, 1024, usage); + Buffer top(context, 16, usage); + Buffer above(context, 16, usage); + std::uint32_t seed = 0x11; + for (auto* buffer : {&first, &second, &words, &shifted, &large, &top, &above}) { + for (std::size_t index = 0; index < buffer->Bytes().size(); ++index) buffer->Bytes()[index] = std::byte(static_cast(seed + index * 7u)); + seed += 0x33; + } + for (std::uint32_t index = 0; index < 16u; ++index) words.Bytes()[index] = std::byte(static_cast(0xa0u + index)); + for (std::uint32_t index = 0; index < 20u; ++index) shifted.Bytes()[index] = std::byte(static_cast(0xc0u + index)); + const std::array ranges{{{guest, guest + 3, first.DeviceAddress(), Abi::Read, 0}, + {guest + 3, guest + 5, second.DeviceAddress(), Abi::Read, 0}, + {guest + 0x100, guest + 0x110, words.DeviceAddress(), Abi::Read, 0}, + {guest + 0x200, guest + 0x210, shifted.DeviceAddress() + 1u, Abi::Read, 0}, + {guest + 0x400, guest + 0x800, large.DeviceAddress(), Abi::Read, 0}, + {limit - 0x10, limit, top.DeviceAddress(), Abi::Read, 0}, + {highest, highest + 0x10, above.DeviceAddress(), Abi::Read, 0}}}; + Require(Abi::RangesOrdered(ranges.data(), ranges.size()), "the BDA anchor test table is not ascending and disjoint"); + Buffer table(context, sizeof(Abi::Header) + sizeof(ranges), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + Buffer fault(context, sizeof(Abi::Fault), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + Buffer output(context, (BdaAnchoredReadInputWord + 3u + lanes) * sizeof(std::uint32_t), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + struct Outcome { + Abi::Fault fault{}; + std::array values{}; + }; + std::uint32_t count = static_cast(ranges.size()); + std::uint32_t permissions = Abi::Read; + const auto run = [&](Pipeline& pipeline, std::uint32_t read, std::uint64_t anchor, const std::vector& lows) { + auto copy = ranges; + copy[2].permissions = permissions; + const Abi::Header header{Abi::Version, count, sizeof(Abi::Range), 0}; + std::memcpy(table.Bytes().data(), &header, sizeof(header)); + std::memcpy(table.Bytes().data() + sizeof(header), copy.data(), sizeof(copy)); + std::memset(fault.Bytes().data(), 0, fault.Bytes().size()); + std::memset(output.Bytes().data(), 0xa5, BdaAnchoredReadInputWord * sizeof(std::uint32_t)); + std::vector inputs{static_cast(anchor), static_cast(anchor >> 32u), read}; + inputs.insert(inputs.end(), lows.begin(), lows.end()); + std::memcpy(output.Bytes().data() + BdaAnchoredReadInputWord * sizeof(std::uint32_t), inputs.data(), inputs.size() * sizeof(std::uint32_t)); + pipeline.Run(1); + Outcome outcome; + std::memcpy(&outcome.fault, fault.Bytes().data(), sizeof(outcome.fault)); + std::memcpy(outcome.values.data(), output.Bytes().data(), sizeof(outcome.values)); + return outcome; + }; + struct Programs { + Programs(const Context& context, const BdaAnchoredReadProgram& reads, const std::array& buffers) : reads(reads), plain(context, MakeBdaAnchoredReadTestShader(reads, false), buffers), anchored(context, MakeBdaAnchoredReadTestShader(reads, true), buffers) {} + BdaAnchoredReadProgram reads; + Pipeline plain; + Pipeline anchored; + }; + const auto build = [&](const BdaAnchoredReadProgram& reads) { return std::make_unique(context, reads, std::array{&table, &fault, &output}); }; + const auto compare = [&](Programs& programs, std::initializer_list anchors, const std::vector& lows) { + std::vector> batches; + if (programs.reads.lanes == 1u) { + for (const auto low : lows) batches.push_back({low}); + } else { + batches.push_back(lows); + batches.back().resize(programs.reads.lanes, lows.back()); + } + for (std::uint32_t index = 0; index < programs.reads.reads.size(); ++index) { + const auto& read = programs.reads.reads[index]; + for (const auto anchor : anchors) { + for (const auto& batch : batches) { + const auto expected = run(programs.plain, index, anchor, batch); + const auto actual = run(programs.anchored, index, anchor, batch); + const bool faultsMatch = programs.reads.lanes == 1u ? std::memcmp(&expected.fault, &actual.fault, sizeof(expected.fault)) == 0 : expected.fault.state == actual.fault.state; + Require(faultsMatch && expected.values == actual.values, "an anchored BDA read differs from the same read through the lookups at anchor 0x" + std::to_string(anchor) + " + " + std::to_string(batch[0]) + " + (" + std::to_string(read.immediate) + "), " + std::to_string(read.bits) + " bits, " + std::to_string(read.dwords) + " dwords"); + } + } + } + }; + std::vector reads; + for (const std::int32_t immediate : {0, 4, -1, -8, 0x7ffffff0, std::numeric_limits::min()}) { + for (const auto& [bits, dwords, used] : std::initializer_list>{{8, 1, 1}, {16, 1, 1}, {32, 1, 1}, {32, 4, 15}, {32, 4, 10}, {32, 3, 4}}) reads.push_back({immediate, bits, dwords, used}); + } + const auto anchors = {guest + 0x100, guest + 0x108, guest, guest + 3, guest + 0x200, guest - 1, guest + 0x110, guest + 0x400, limit - 0x10, limit - 4, highest, highest + 8, std::numeric_limits::max() - 2}; + std::vector lows{0u, 1u, 2u, 3u, 4u, 8u, 0xcu, 0xdu, 0xeu, 0xfu, 0x10u, 0x14u, 0x100u, 0x108u, 0x10cu, 0x1fcu, 0x200u, 0x20eu, 0x3f0u, 0x3fcu, 0x400u, 0x7fffu, 0x7fffffffu, 0x80000000u, 0xfffffff0u, 0xfffffffcu, 0xffffffffu}; + for (std::uint32_t lane = 0; lane < 32u; ++lane) lows.push_back(lane * 0x24u); + std::vector strided; + for (std::uint32_t lane = 0; lane < lanes; ++lane) strided.push_back(lane * 16u); + auto continuing = build({reads, false, false, false, lanes}); + compare(*continuing, anchors, lows); + compare(*continuing, {guest + 0x400}, strided); + auto elected = build({reads, false, true, false, lanes}); + compare(*elected, anchors, lows); + compare(*elected, {guest + 0x400}, strided); + auto coherent = build({{reads[0], reads[2], reads[9], reads[16], reads[22]}, false, false, true, lanes}); + compare(*coherent, anchors, lows); + auto stopping = build({reads, true, true, false, 1}); + compare(*stopping, anchors, {0u, 0x80000000u, 0xfffffffcu}); + const auto check = [&](std::uint32_t read, std::uint64_t anchor, std::uint32_t low) { return run(stopping->anchored, read, anchor, {low}); }; + const auto wide = check(3, guest + 0x100, 0); + Require(wide.fault.state == Abi::FaultState::Empty && wide.values[0] == 0xa3a2a1a0u && wide.values[1] == 0xa7a6a5a4u && wide.values[2] == 0xabaaa9a8u && wide.values[3] == 0xafaeadacu, "an anchored span read returned wrong data"); + const auto some = check(4, guest + 0x100, 0); + Require(some.fault.state == Abi::FaultState::Empty && some.values[0] == 0u && some.values[1] == 0xa7a6a5a4u && some.values[2] == 0u && some.values[3] == 0xafaeadacu, "an anchored read of some dwords returned wrong data"); + const auto odd = check(2, guest + 0x200, 0); + Require(odd.fault.state == Abi::FaultState::Empty && odd.values[0] == 0xc4c3c2c1u, "an anchored read of an odd device address returned wrong data"); + const auto byte = check(0, guest + 0x200, 0xd); + Require(byte.fault.state == Abi::FaultState::Empty && byte.values[0] == 0xceu, "an anchored byte read returned wrong data"); + const auto below = check(20, guest + 0x108, 4); + Require(below.fault.state == Abi::FaultState::Empty && below.values[0] == 0xa7a6a5a4u, "an anchored read below its base returned wrong data"); + const auto unmapped = check(2, guest + 0x100, 0x10); + Require(unmapped.fault.state == Abi::FaultState::Ready && unmapped.fault.reason == Abi::FaultReason::Unmapped && unmapped.fault.address == guest + 0x110 && unmapped.fault.instruction == 0x1234 && unmapped.values[0] == 0xa5a5a5a5u, "an anchored read past its range did not fault"); + permissions = 0; + compare(*stopping, {guest + 0x100}, {0u, 4u}); + compare(*elected, {guest + 0x100}, lows); + permissions = Abi::Read; + count = 3; + compare(*stopping, {guest + 0x100, guest}, {0u, 4u}); + compare(*elected, {guest + 0x100, guest}, lows); +} + void RunBdaExecutionTests(const Context& context) { namespace Abi = ShaderRecompiler::BdaAbi; Buffer first(context, 3, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT); @@ -333,6 +457,7 @@ void RunBdaExecutionTests(const Context& context) { ranges[0].permissions = 0; run(guest, 8, 0, Abi::FaultReason::Permission); RunBdaDwordWriteTests(context); + RunBdaAnchorTests(context); } void RunBdaStoreExecutionTests(const Context& context) { diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaShader.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaShader.cpp index 60a2e87853..a24dd6a955 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaShader.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaShader.cpp @@ -133,3 +133,90 @@ std::vector MakeBdaWriteTestShader(std::uint64_t address, std::ui #endif return shader; } + +std::vector MakeBdaAnchoredReadTestShader(const BdaAnchoredReadProgram& reads, bool anchored) { + using namespace ShaderRecompiler; + IrProgram program; + program.Resources().stage = IrShaderStage::Compute; + program.Info().usesDma = true; + if (reads.coherent) { + MemoryInfo memory; + memory.coherent = true; + program.Resources().memoryInfo = {memory}; + } + SpirvEmitterState state(program, {}); + EmitBaseHeader(state.module, program); + if (reads.elect) { + state.module.EmitCapability(spv::CapabilityGroupNonUniform); + state.module.EmitCapability(spv::CapabilityGroupNonUniformBallot); + state.requirements.subgroupBallot = true; + } + const auto define = [&](std::uint32_t binding) { + const auto variable = state.module.DefineGlobalVariable(TypeStorageBufferPointer(state), spv::StorageClassStorageBuffer); + state.module.AddAnnotation(spv::OpDecorate, variable, spv::DecorationDescriptorSet, 0u); + state.module.AddAnnotation(spv::OpDecorate, variable, spv::DecorationBinding, binding); + return variable; + }; + state.bdaPagetableVariable = define(0); + state.faultBufferVariable = define(1); + const auto output = define(2); + std::vector interfaceVariables{state.module.DefineGlobalVariable(TypePointer(state, spv::StorageClassInput, TypeU32(state)), spv::StorageClassInput)}; + state.module.AddAnnotation(spv::OpDecorate, interfaceVariables[0], spv::DecorationBuiltIn, spv::BuiltInLocalInvocationIndex); + state.bdaStopsInvocations = reads.stops; + DefineGetBdaPointer(state); + if (anchored) DefineBdaAnchorFunction(state); + const auto main = state.module.AllocateId(); + state.module.AddFunction(spv::OpFunction, TypeVoid(state), main, spv::FunctionControlMaskNone, TypeFunction(state)); + EmitLabel(state, state.module.AllocateId()); + SpirvValueEmitContext ctx(state); + const auto u32 = TypeU32(state); + const auto u64 = TypeScalarU64(state); + const auto input = [&](std::uint32_t word) { + const auto value = state.module.AllocateId(); + state.module.AddFunction(spv::OpLoad, u32, value, BdaWord(state, output, word)); + return value; + }; + const auto base = Binary(state, spv::OpBitwiseOr, u64, Unary(state, spv::OpUConvert, u64, input(ConstantU32(state, BdaAnchoredReadInputWord))), Binary(state, spv::OpShiftLeftLogical, u64, Unary(state, spv::OpUConvert, u64, input(ConstantU32(state, BdaAnchoredReadInputWord + 1u))), BdaConstant(state, 32u))); + SpirvEmitterState::BdaAnchor anchor{}; + if (anchored) EmitBdaAnchorRange(state, anchor, base); + const auto selector = input(ConstantU32(state, BdaAnchoredReadInputWord + 2u)); + const auto laneIndex = state.module.AllocateId(); + state.module.AddFunction(spv::OpLoad, u32, laneIndex, interfaceVariables[0]); + const auto low = input(Binary(state, spv::OpIAdd, u32, laneIndex, ConstantU32(state, BdaAnchoredReadInputWord + 3u))); + const auto slot = Binary(state, spv::OpIMul, u32, laneIndex, ConstantU32(state, 4u)); + IrBuilder ir(program); + for (std::uint32_t index = 0; index < reads.reads.size(); ++index) { + const auto& read = reads.reads[index]; + const auto opcode = read.bits == 8u ? IrOpcode::LoadAddressU8 : read.bits == 16u ? IrOpcode::LoadAddressU16 : read.dwords == 2u ? IrOpcode::LoadAddressU32x2 : read.dwords == 3u ? IrOpcode::LoadAddressU32x3 : read.dwords == 4u ? IrOpcode::LoadAddressU32x4 : IrOpcode::LoadAddressU32; + const auto type = read.bits != 32u || read.dwords == 1u ? IrType::U32 : read.dwords == 2u ? IrType::U32x2 : read.dwords == 3u ? IrType::U32x3 : IrType::U32x4; + auto& instruction = program.CreateValue(opcode, type); + MemoryFlags flags{}; + flags.pc = 0x1234; + instruction.SetFlags(flags); + if (read.bits == 32u && read.dwords > 1u) { + const auto extract = read.dwords == 2u ? IrOpcode::CompositeExtractU32x2 : read.dwords == 3u ? IrOpcode::CompositeExtractU32x3 : IrOpcode::CompositeExtractU32x4; + for (std::uint32_t dword = 0; dword < read.dwords; ++dword) { + if ((read.used & (1u << dword)) == 0u) continue; + auto& user = program.CreateValue(extract, IrType::U32); + user.AddArgument(&instruction); + user.AddArgument(&ir.Constant(dword)); + } + } + EmitIfCondition(state, Binary(state, spv::OpIEqual, TypeBool(state), selector, ConstantU32(state, index)), [&] { + const auto lookup = [&] { + const auto address = AddBdaAddress(ctx, instruction, base, Unary(state, spv::OpUConvert, u64, low), false); + if (read.bits == 32u) return EmitBdaDwordReads(ctx, instruction, address, static_cast(read.immediate), read.dwords); + return std::array{EmitBdaRead(ctx, instruction, AddBdaImmediate(ctx, instruction, address, read.immediate), read.bits)}; + }; + const auto values = anchored ? EmitAnchoredBdaRead(state, instruction, anchor, low, read.immediate, read.bits, read.dwords, 0u, lookup) : lookup(); + for (std::uint32_t dword = 0; dword < (read.bits == 32u ? read.dwords : 1u); ++dword) { + state.module.AddFunction(spv::OpStore, BdaWord(state, output, Binary(state, spv::OpIAdd, u32, slot, ConstantU32(state, dword))), values[dword]); + } + }); + } + state.module.AddFunction(spv::OpReturn); + state.module.AddFunction(spv::OpFunctionEnd); + state.module.AddExecutionMode(main, spv::ExecutionModeLocalSize, reads.lanes, 1u, 1u); + state.module.EmitEntryPoint(spv::ExecutionModelGLCompute, main, "main", interfaceVariables); + return state.module.Finalize(); +} diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaShader.hpp b/core/libs/prx/libSceAgcDriver/tests/BdaShader.hpp index 62a3adcb19..ec04f67aba 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaShader.hpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaShader.hpp @@ -11,6 +11,21 @@ std::vector MakeBdaSpanReadTestShader(std::uint64_t address, std: std::vector MakeBdaDwordWriteTestShader(std::uint64_t address, std::uint32_t dwords, const std::uint32_t* values); std::vector MakeBdaStoreTestShader(std::uint64_t address, std::uint32_t value, std::uint32_t bits, bool coherent); std::vector MakeBdaWriteTestShader(std::uint64_t address, std::uint32_t value, std::uint32_t bits, bool coherent); +inline constexpr std::uint32_t BdaAnchoredReadInputWord = 256; +struct BdaAnchoredRead { + std::int32_t immediate = 0; + std::uint32_t bits = 32; + std::uint32_t dwords = 1; + std::uint32_t used = 1; +}; +struct BdaAnchoredReadProgram { + std::vector reads; + bool stops = true; + bool elect = false; + bool coherent = false; + std::uint32_t lanes = 1; +}; +std::vector MakeBdaAnchoredReadTestShader(const BdaAnchoredReadProgram& reads, bool anchored); void RunBdaStoreExecutionTests(const AgcDriver::Graphics::Context& context); void RunBdaExecutionTests(const AgcDriver::Graphics::Context& context); void RunBdaContractTests(); diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 9021647520..9cded9c715 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -3666,6 +3666,79 @@ void verifyBdaReadFallbackFunctions() { } } +void verifyBdaAnchors() { + using namespace ShaderRecompiler; + const std::array capabilities{spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess}; + const std::array extensions{"SPV_KHR_physical_storage_buffer", "SPV_KHR_8bit_storage"}; + alignas(256) static std::array output{}; + const auto outputAddress = static_cast(reinterpret_cast(output.data())); + const std::array userData{0x10000000u, 0u, 0u, 0u, static_cast(outputAddress), static_cast((outputAddress >> 32u) & 0xffffu), 128u, 0x01016facu}; + const auto compile = [&](std::uint32_t scalarBase, std::uint32_t stride) { + std::vector code{0x7e020200u, 0x7e040201u}; + for (std::uint32_t load = 0; load < scalarBase; ++load) { + code.push_back(0xdc308000u | (stride * load + 4u)); + code.push_back(((3u + load) << 24u) | 0x00040000u); + } + code.push_back(0xdc308000u | 4u); + code.push_back(((3u + scalarBase) << 24u) | 0x007d0001u); + code.push_back(0xbf8c3f70u); + for (std::uint32_t load = 1; load <= scalarBase; ++load) code.push_back(0x4a060103u | ((3u + load) << 9u)); + code.insert(code.end(), {0xe0700000u, 0x80010300u, 0xbf810000u}); + RecompileRequest request{}; + request.shader = {ShaderStage::Compute, 0x60000u, code, 0, {}}; + request.context.waveSize = 32; + request.context.userDataBaseRegister = 0; + request.context.userData = userData; + request.context.compute = ShaderComputeStageInfo{{32u, 1u, 1u}, 0u, {false, false, false}, false, 1u}; + request.target.vulkanVersion = 0x00401000u; + request.target.spirvVersion = 0x00010300u; + request.target.subgroupSize = 32; + request.target.bdaAbiVersion = BdaAbi::Version; + request.target.supportedCapabilities = capabilities; + request.target.supportedExtensions = extensions; + request.target.fragmentShaderBarycentricEnabled = false; + request.layout.pushConstantSizeBytes = 128; + request.useCache = false; + return Recompile(request).spirv; + }; + for (const std::uint32_t stride : {4u, 32u}) { + for (const std::uint32_t loads : {0u, 1u, 2u, 4u}) { + const bool anchored = loads >= 2u; + const auto words = compile(loads, stride); + std::map names; + std::string function; + std::map mainCalls; + std::size_t anchorFunctions = 0; + std::size_t branchesBeforeAnchor = 0; + std::size_t directLoads = 0; + for (std::size_t cursor = 5; cursor < words.size();) { + const auto length = words[cursor] >> 16u; + require(length != 0 && length <= words.size() - cursor, "BDA anchors: truncated SPIR-V instruction"); + const auto op = words[cursor] & 0xffffu; + if (op == spv::OpName) names[words[cursor + 1]] = reinterpret_cast(&words[cursor + 2]); + if (op == spv::OpFunction) { + function = names[words[cursor + 2]]; + if (function == "bda_anchor_range") ++anchorFunctions; + } + if (op == spv::OpFunctionCall && function == "main") { + const auto& callee = names[words[cursor + 3]]; + if (callee == "bda_anchor_range") require(mainCalls.empty(), "BDA anchors: the anchor is searched after a read"); + ++mainCalls[callee.starts_with("read_bda_span") ? std::string("read_bda_span") : callee]; + } + if (op == spv::OpBranchConditional && function == "main" && mainCalls["bda_anchor_range"] == 0u) ++branchesBeforeAnchor; + if (op == spv::OpLoad && function == "main" && length > 4u && words[cursor + 4] == spv::MemoryAccessAlignedMask && words[cursor + 5] == 4u) ++directLoads; + cursor += length; + } + const auto spans = loads == 0u ? 1u : stride == 4u ? 2u : loads + 1u; + require(anchorFunctions == (anchored ? 1u : 0u), "BDA anchors: the anchor search is defined without anchored reads, or twice"); + require(mainCalls["bda_anchor_range"] == (anchored ? 1u : 0u), "BDA anchors: the reads of one user data base do not share one anchor"); + require(!anchored || branchesBeforeAnchor == 0u, "BDA anchors: the anchor is not searched at the program's entry"); + require(mainCalls["read_bda_span"] == spans, "BDA anchors: a read lost its lookup path"); + require(directLoads == (anchored ? loads : 0u), "BDA anchors: an anchored read does not load its dwords directly"); + } + } +} + void verifyFunctionLdsBound() { using namespace ShaderRecompiler; const auto build = [](const auto& body) { @@ -4278,6 +4351,7 @@ int main(int argc, char** argv) { verifyWaveUniformValues(); verifyTwoLaneUniformValues(); verifyBdaReadFallbackFunctions(); + verifyBdaAnchors(); verifyFunctionLdsBound(); verifyFunctionLdsUpdates(); verifySrtReadPatching(); diff --git a/core/shader/recompiler/BdaAbi.hpp b/core/shader/recompiler/BdaAbi.hpp index f964d7fe99..bdb3049862 100644 --- a/core/shader/recompiler/BdaAbi.hpp +++ b/core/shader/recompiler/BdaAbi.hpp @@ -27,6 +27,14 @@ struct Range { std::uint32_t reserved; }; +inline bool RangesOrdered(const Range* ranges, std::size_t count) { + for (std::size_t index = 0; index < count; ++index) { + if (ranges[index].end < ranges[index].begin) return false; + if (index != 0 && ranges[index].begin < ranges[index - 1].end) return false; + } + return true; +} + enum class FaultState : std::uint32_t { Empty, Writing, Ready }; enum class FaultReason : std::uint32_t { Unmapped = 1, Permission, Overflow, InvalidTable, InvalidRectangle, LoopLimit, Unaligned, InvalidDescriptor }; diff --git a/core/shader/recompiler/CMakeLists.txt b/core/shader/recompiler/CMakeLists.txt index 7598e4aad9..9b1dcd0233 100644 --- a/core/shader/recompiler/CMakeLists.txt +++ b/core/shader/recompiler/CMakeLists.txt @@ -217,6 +217,7 @@ function(add_shader_recompiler target runtimeTarget) string(APPEND shaderCacheExtra " system-deps=${ANYPS5_SYSTEM_DEPS_STAMP}") endif() set(shaderCacheReviewedNames + BdaAbi::RangesOrdered BuildBarycentricGeometryShader BuildPreparedShaderKey BuildRectListShaders diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvBda.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvBda.hpp index 47ffeb796c..67b486dd52 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvBda.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvBda.hpp @@ -38,6 +38,12 @@ std::uint32_t EmitBdaAtomic(SpirvValueEmitContext& ctx, const IrValue& inst, std std::array EmitBdaDwordReads(SpirvValueEmitContext& ctx, const IrValue& inst, std::uint32_t address, std::uint32_t offset, std::uint32_t dwords, bool everyDword = false, std::uint32_t usedDwords = 0u); bool BdaByteReadsForced(); std::uint32_t AddBdaAddress(SpirvValueEmitContext& ctx, const IrValue& inst, std::uint32_t address, std::uint32_t offset, bool subtract); +void CollectBdaAnchors(SpirvEmitterState& state); +void DefineBdaAnchorFunction(SpirvEmitterState& state); +void EmitBdaAnchors(SpirvEmitterState& state); +void EmitBdaAnchorRange(SpirvEmitterState& state, SpirvEmitterState::BdaAnchor& anchor, std::uint32_t base); +const SpirvEmitterState::BdaAnchor* FindBdaAnchor(const SpirvEmitterState& state, const IrValue& inst); +std::array EmitAnchoredBdaRead(SpirvEmitterState& state, const IrValue& inst, const SpirvEmitterState::BdaAnchor& anchor, std::uint32_t low, std::int32_t immediate, std::uint32_t bits, std::uint32_t dwords, std::uint32_t usedDwords, const std::function()>& lookup); std::uint32_t AddBdaImmediate(SpirvValueEmitContext& ctx, const IrValue& inst, std::uint32_t address, std::int32_t immediate); } diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp index afd26653ac..b9d8ba6436 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp @@ -129,6 +129,15 @@ struct SpirvEmitterState { std::array, 2> bdaDwordReadFunctions {}; std::array, 2> bdaSpanReadFunctions {}; std::unordered_map> bdaGroupedLoads; + struct BdaAnchor { + std::uint32_t lowRegister = 0; + std::uint32_t highRegister = 0; + std::uint32_t size = 0; + std::uint32_t device = 0; + std::uint32_t offset = 0; + }; + std::vector bdaAnchors; + std::uint32_t bdaAnchorFunction = 0; std::uint32_t bdaStopValue = 0; std::uint32_t bdaPcOverride = 0; // False for programs with workgroup barriers: faulting BDA accesses then continue (see BdaInvocationsMayStop). diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvBdaLookup.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvBdaLookup.cpp index b48e8775b5..55172995b1 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvBdaLookup.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvBdaLookup.cpp @@ -10,18 +10,18 @@ namespace ShaderRecompiler { namespace { -void ReturnBdaZeroIf(SpirvEmitterState& state, std::uint32_t condition) { +void ReturnBdaZeroIf(SpirvEmitterState& state, std::uint32_t condition, std::uint32_t zero) { const auto failed = state.module.AllocateId(); const auto next = state.module.AllocateId(); state.module.AddFunction(spv::OpSelectionMerge, next, spv::SelectionControlMaskNone); state.module.AddFunction(spv::OpBranchConditional, condition, failed, next); EmitLabel(state, failed); - state.module.AddFunction(spv::OpReturnValue, BdaConstant(state, 0u)); + state.module.AddFunction(spv::OpReturnValue, zero); EmitLabel(state, next); } // (u64 address, u32 bytes, u32 instruction) -> u64 device address of the range holding all the bytes, -std::uint32_t DefineBdaLookup(SpirvEmitterState& state, const char* name, bool recordFaults, std::uint32_t permission = BdaAbi::Read) { +std::uint32_t DefineBdaLookup(SpirvEmitterState& state, const char* name, bool recordFaults, std::uint32_t permission = BdaAbi::Read, bool rangeResult = false) { const auto u32 = TypeU32(state); const auto u64 = TypeScalarU64(state); const auto boolean = TypeBool(state); @@ -29,7 +29,10 @@ std::uint32_t DefineBdaLookup(SpirvEmitterState& state, const char* name, bool r const auto binary = [&](std::uint32_t op, std::uint32_t type, std::uint32_t left, std::uint32_t right) { return Binary(state, op, type, left, right); }; const auto function = state.module.AllocateId(); state.module.AddName(function, name); - state.module.AddFunction(spv::OpFunction, u64, function, spv::FunctionControlDontInlineMask, state.module.Type(spv::OpTypeFunction, u64, u64, u32, u32)); + const auto rangeType = rangeResult ? state.module.Type(spv::OpTypeVector, u64, 3u) : 0u; + const auto resultType = rangeResult ? rangeType : u64; + const auto zero = rangeResult ? state.module.Constant(spv::OpConstantComposite, rangeType, BdaConstant(state, 0u), BdaConstant(state, 0u), BdaConstant(state, 0u)) : BdaConstant(state, 0u); + state.module.AddFunction(spv::OpFunction, resultType, function, rangeResult ? spv::FunctionControlMaskNone : spv::FunctionControlDontInlineMask, state.module.Type(spv::OpTypeFunction, resultType, u64, u32, u32)); const auto address = state.module.AllocateId(); const auto bytes = state.module.AllocateId(); const auto instruction = state.module.AllocateId(); @@ -41,7 +44,7 @@ std::uint32_t DefineBdaLookup(SpirvEmitterState& state, const char* name, bool r if (recordFaults) { ReturnBdaFailureIf(state, condition, address, bytes, instruction, reason); } else { - ReturnBdaZeroIf(state, condition); + ReturnBdaZeroIf(state, condition, zero); } }; const auto failFirst = [&](std::initializer_list> checks) { @@ -51,7 +54,7 @@ std::uint32_t DefineBdaLookup(SpirvEmitterState& state, const char* name, bool r } const auto failed = binary(spv::OpINotEqual, boolean, reason, constant(0u)); if (!recordFaults) { - ReturnBdaZeroIf(state, failed); + ReturnBdaZeroIf(state, failed, zero); return; } const auto failedLabel = state.module.AllocateId(); @@ -124,6 +127,13 @@ std::uint32_t DefineBdaLookup(SpirvEmitterState& state, const char* name, bool r {binary(spv::OpULessThan, boolean, result, base), BdaAbi::FaultReason::Overflow}, {binary(spv::OpULessThanEqual, boolean, deviceEnd, result), BdaAbi::FaultReason::Overflow}, }); + if (rangeResult) { + const auto range = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeConstruct, rangeType, range, begin, finish, base); + state.module.AddFunction(spv::OpReturnValue, range); + state.module.AddFunction(spv::OpFunctionEnd); + return function; + } state.module.AddFunction(spv::OpReturnValue, result); state.module.AddFunction(spv::OpFunctionEnd); return function; @@ -212,6 +222,10 @@ static std::uint32_t DefineBdaNoteWrite(SpirvEmitterState& state) { return function; } +void DefineBdaAnchorFunction(SpirvEmitterState& state) { + if (state.bdaAnchorFunction == 0) state.bdaAnchorFunction = DefineBdaLookup(state, "bda_anchor_range", false, BdaAbi::Read, true); +} + void DefineGetBdaPointer(SpirvEmitterState& state) { if (state.faultBufferVariable != 0) DefineBdaFaultFunction(state); if (!state.program.Info().usesDma) return; @@ -219,6 +233,8 @@ void DefineGetBdaPointer(SpirvEmitterState& state) { if (!BdaByteReadsForced()) state.bdaProbeFunction = DefineBdaLookup(state, "probe_bda_pointer", false); DefineBdaDwordReadFunctions(state); DefineBdaSpanReadFunctions(state); + CollectBdaAnchors(state); + if (!state.bdaAnchors.empty()) DefineBdaAnchorFunction(state); if (state.program.Info().bdaWrites) { state.bdaWritePointerFunction = DefineBdaLookup(state, "get_bda_write_pointer", true, BdaAbi::Write); if (!BdaByteReadsForced()) state.bdaWriteProbeFunction = DefineBdaLookup(state, "probe_bda_write_pointer", false, BdaAbi::Write); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp index b43f89f997..73a196bfc6 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp @@ -1,12 +1,16 @@ #include "SpirvBackend/SpirvBda.hpp" #include "SpirvBackend/SpirvMemory/SpirvTypes.hpp" #include "SpirvBackend/SpirvMemory/SpirvConstants.hpp" +#include "SpirvBackend/SpirvEmitterInstructions.hpp" #include #include +#include #include #include +#include #include #include +#include namespace ShaderRecompiler { @@ -417,6 +421,213 @@ bool BdaInvocationsMayStop(const IrProgram& program) { return true; } +namespace { + +constexpr std::size_t MaxBdaAnchors = 4; +constexpr std::uint64_t BdaAnchorAddressLimit = ~std::uint64_t{0} << 32u; + +std::optional> UserDataBase(const IrProgram& program, const IrValue& inst) { + if (AddressOpcodeInfoOf(inst.Opcode()).access != AddressAccess::Read || inst.ArgumentCount() < 2u) return std::nullopt; + const auto index = inst.Flags().index; + const auto& memory = program.Resources().memoryInfo; + if (index >= memory.size()) return std::nullopt; + const auto& info = memory[index]; + if (info.kind != ResourceKind::ScalarAddress && info.kind != ResourceKind::Flat && info.kind != ResourceKind::Global) return std::nullopt; + if (info.planningOnly || info.addressIsFull || (info.kind == ResourceKind::Flat && program.Resources().stage == IrShaderStage::Compute)) return std::nullopt; + const IrValue* handle = inst.Argument(0) != nullptr ? inst.Argument(0)->Resolve() : nullptr; + if (handle == nullptr || handle->Opcode() != IrOpcode::GetAddressResource || handle->ArgumentCount() != 2u) return std::nullopt; + const auto userData = [](const IrValue* value) -> std::optional { + value = value != nullptr ? value->Resolve() : nullptr; + if (value == nullptr || value->Opcode() != IrOpcode::GetUserData || value->ArgumentCount() == 0u || value->Argument(0) == nullptr) return std::nullopt; + return value->Argument(0)->Register().index; + }; + const auto low = userData(handle->Argument(0)); + const auto high = userData(handle->Argument(1)); + if (!low || !high) return std::nullopt; + return std::pair{*low, *high}; +} + +std::uint32_t EmitElectedBdaRange(SpirvEmitterState& state, std::uint32_t type, std::uint32_t zero, const std::function& search) { + const auto elected = state.module.AllocateId(); + state.module.AddFunction(spv::OpGroupNonUniformElect, TypeBool(state), elected, ConstantU32(state, spv::ScopeSubgroup)); + const auto before = state.currentLabel; + const auto electedLabel = state.module.AllocateId(); + const auto merge = state.module.AllocateId(); + state.module.AddFunction(spv::OpSelectionMerge, merge, spv::SelectionControlMaskNone); + state.module.AddFunction(spv::OpBranchConditional, elected, electedLabel, merge); + EmitLabel(state, electedLabel); + const auto found = search(); + const auto electedExit = state.currentLabel; + state.module.AddFunction(spv::OpBranch, merge); + EmitLabel(state, merge); + const auto value = state.module.AllocateId(); + state.module.AddFunction(spv::OpPhi, type, value, found, electedExit, zero, before); + const auto u64 = TypeScalarU64(state); + const auto pair = TypeU32Vector(state, 2); + std::array parts{}; + for (std::uint32_t component = 0; component < parts.size(); ++component) { + const auto part = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeExtract, u64, part, value, component); + const auto shared = state.module.AllocateId(); + state.module.AddFunction(spv::OpGroupNonUniformBroadcastFirst, pair, shared, ConstantU32(state, spv::ScopeSubgroup), Unary(state, spv::OpBitcast, pair, part)); + parts[component] = Unary(state, spv::OpBitcast, u64, shared); + } + const auto result = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeConstruct, type, result, parts[0], parts[1], parts[2]); + return result; +} + +std::uint32_t BdaAnchorHolds(SpirvEmitterState& state, const SpirvEmitterState::BdaAnchor& anchor, std::uint32_t low, std::int32_t immediate, std::uint32_t bytes, std::uint32_t& translated) { + const auto u64 = TypeScalarU64(state); + const auto boolean = TypeBool(state); + auto offset = Binary(state, spv::OpIAdd, u64, anchor.offset, Unary(state, spv::OpUConvert, u64, low)); + if (immediate != 0) offset = Binary(state, spv::OpIAdd, u64, offset, BdaConstant(state, static_cast(static_cast(immediate)))); + const auto width = BdaConstant(state, bytes); + const auto fits = Binary(state, spv::OpUGreaterThanEqual, boolean, anchor.size, width); + const auto inside = Binary(state, spv::OpULessThanEqual, boolean, offset, Binary(state, spv::OpISub, u64, anchor.size, width)); + translated = Binary(state, spv::OpIAdd, u64, anchor.device, offset); + return Binary(state, spv::OpLogicalAnd, boolean, fits, inside); +} + +} + +void CollectBdaAnchors(SpirvEmitterState& state) { + state.bdaAnchors.clear(); + if (!state.program.Info().usesDma || state.bdaProbeFunction == 0) return; + std::vector, std::size_t>> bases; + for (const IrBlock* block : state.program.BlockOrder()) { + for (const IrValue* inst : block->Instructions()) { + const auto base = UserDataBase(state.program, *inst); + if (!base) continue; + const auto found = std::find_if(bases.begin(), bases.end(), [&](const auto& entry) { return entry.first == *base; }); + if (found != bases.end()) ++found->second; + else bases.push_back({*base, 1u}); + } + } + std::stable_sort(bases.begin(), bases.end(), [](const auto& left, const auto& right) { return left.second > right.second; }); + for (const auto& [base, reads] : bases) { + if (reads < 2u || state.bdaAnchors.size() == MaxBdaAnchors) break; + state.bdaAnchors.push_back({base.first, base.second}); + } +} + +void EmitBdaAnchorRange(SpirvEmitterState& state, SpirvEmitterState::BdaAnchor& anchor, std::uint32_t base) { + if (state.bdaAnchorFunction == 0) throw std::runtime_error("the BDA anchor search function is missing"); + const auto u64 = TypeScalarU64(state); + const auto boolean = TypeBool(state); + const auto rangeType = state.module.Type(spv::OpTypeVector, u64, 3u); + const auto search = [&] { + const auto range = state.module.AllocateId(); + state.module.AddFunction(spv::OpFunctionCall, rangeType, range, state.bdaAnchorFunction, base, ConstantU32(state, 1u), ConstantU32(state, 0u)); + return range; + }; + const auto zero = state.module.Constant(spv::OpConstantComposite, rangeType, BdaConstant(state, 0u), BdaConstant(state, 0u), BdaConstant(state, 0u)); + const auto range = state.requirements.subgroupBallot ? EmitElectedBdaRange(state, rangeType, zero, search) : search(); + const auto extract = [&](std::uint32_t component) { + const auto value = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeExtract, u64, value, range, component); + return value; + }; + const auto begin = extract(0u); + const auto end = extract(1u); + anchor.device = extract(2u); + anchor.offset = Binary(state, spv::OpISub, u64, base, begin); + const auto span = Binary(state, spv::OpISub, u64, end, begin); + const auto deviceEnd = Binary(state, spv::OpIAdd, u64, anchor.device, span); + auto usable = Binary(state, spv::OpLogicalAnd, boolean, Binary(state, spv::OpINotEqual, boolean, anchor.device, BdaConstant(state, 0u)), Binary(state, spv::OpUGreaterThan, boolean, deviceEnd, anchor.device)); + usable = Binary(state, spv::OpLogicalAnd, boolean, usable, Binary(state, spv::OpULessThanEqual, boolean, end, BdaConstant(state, BdaAnchorAddressLimit))); + anchor.size = Select(state, u64, usable, span, BdaConstant(state, 0u)); +} + +void EmitBdaAnchors(SpirvEmitterState& state) { + const auto u64 = TypeScalarU64(state); + for (auto& anchor : state.bdaAnchors) { + const auto low = Unary(state, spv::OpUConvert, u64, EmitGetUserData(state, static_cast(anchor.lowRegister))); + const auto high = Unary(state, spv::OpUConvert, u64, EmitGetUserData(state, static_cast(anchor.highRegister))); + EmitBdaAnchorRange(state, anchor, Binary(state, spv::OpBitwiseOr, u64, low, Binary(state, spv::OpShiftLeftLogical, u64, high, BdaConstant(state, 32u)))); + } +} + +const SpirvEmitterState::BdaAnchor* FindBdaAnchor(const SpirvEmitterState& state, const IrValue& inst) { + if (state.bdaAnchors.empty()) return nullptr; + const auto base = UserDataBase(state.program, inst); + if (!base) return nullptr; + for (const auto& anchor : state.bdaAnchors) { + if (anchor.lowRegister == base->first && anchor.highRegister == base->second && anchor.size != 0u) return &anchor; + } + return nullptr; +} + +std::array EmitAnchoredBdaRead(SpirvEmitterState& state, const IrValue& inst, const SpirvEmitterState::BdaAnchor& anchor, std::uint32_t low, std::int32_t immediate, std::uint32_t bits, std::uint32_t dwords, std::uint32_t usedDwords, const std::function()>& lookup) { + if (bits != 8u && bits != 16u && bits != 32u) throw std::runtime_error("unsupported anchored BDA read width"); + if (bits == 32u && (dwords == 0u || dwords > 4u)) throw std::runtime_error("unsupported anchored BDA read dword count"); + const auto used = bits != 32u ? 1u : usedDwords != 0u ? usedDwords : UsedBdaDwords(inst, dwords); + if (used == 0u) return lookup(); + const auto u32 = TypeU32(state); + const auto u64 = TypeScalarU64(state); + const auto boolean = TypeBool(state); + std::uint32_t first = 0; + std::uint32_t last = 0; + std::uint32_t bytes = bits / 8u; + if (bits == 32u) { + first = static_cast(std::countr_zero(used)); + last = 31u - static_cast(std::countl_zero(used)); + bytes = (last - first + 1u) * 4u; + } + std::uint32_t translated = 0; + auto direct = BdaAnchorHolds(state, anchor, low, static_cast(static_cast(immediate) + first * 4u), bytes, translated); + if (bits == 32u) direct = Binary(state, spv::OpLogicalAnd, boolean, direct, Unary(state, spv::OpLogicalNot, boolean, BdaAddressUnaligned(state, translated))); + const auto directLabel = state.module.AllocateId(); + const auto lookupLabel = state.module.AllocateId(); + const auto merge = state.module.AllocateId(); + state.module.AddFunction(spv::OpSelectionMerge, merge, spv::SelectionControlMaskNone); + state.module.AddFunction(spv::OpBranchConditional, direct, directLabel, lookupLabel); + EmitLabel(state, directLabel); + std::array directValues{}; + const bool coherent = BdaCoherent(state, inst); + if (bits == 32u) { + for (std::uint32_t dword = first; dword <= last; ++dword) { + if ((used & (1u << dword)) == 0u) continue; + const auto element = dword == first ? translated : Binary(state, spv::OpIAdd, u64, translated, BdaConstant(state, (dword - first) * 4u)); + const auto pointer = state.module.AllocateId(); + state.module.AddFunction(spv::OpConvertUToPtr, TypePhysicalU32Pointer(state), pointer, element); + directValues[dword] = state.module.AllocateId(); + if (coherent) state.module.AddFunction(spv::OpAtomicLoad, u32, directValues[dword], pointer, ConstantU32(state, spv::ScopeDevice), ConstantU32(state, 0u)); + else state.module.AddFunction(spv::OpLoad, u32, directValues[dword], pointer, BdaAccessMask(false), 4u); + } + } else { + const auto byteType = state.module.Type(spv::OpTypeInt, 8u, 0u); + const auto bytePointer = TypePointer(state, spv::StorageClassPhysicalStorageBuffer, byteType); + auto value = ConstantU32(state, 0u); + for (std::uint32_t byte = 0; byte < bytes; ++byte) { + const auto element = byte == 0u ? translated : Binary(state, spv::OpIAdd, u64, translated, BdaConstant(state, byte)); + const auto pointer = state.module.AllocateId(); + state.module.AddFunction(spv::OpConvertUToPtr, bytePointer, pointer, element); + const auto loaded = state.module.AllocateId(); + state.module.AddFunction(spv::OpLoad, byteType, loaded, pointer, BdaAccessMask(coherent), 1u); + value = Binary(state, spv::OpBitwiseOr, u32, value, Binary(state, spv::OpShiftLeftLogical, u32, Unary(state, spv::OpUConvert, u32, loaded), ConstantU32(state, byte * 8u))); + } + directValues[0] = value; + } + const auto directExit = state.currentLabel; + state.module.AddFunction(spv::OpBranch, merge); + EmitLabel(state, lookupLabel); + const auto lookupValues = lookup(); + const auto lookupExit = state.currentLabel; + state.module.AddFunction(spv::OpBranch, merge); + EmitLabel(state, merge); + std::array values{}; + for (std::uint32_t dword = 0; dword < (bits == 32u ? dwords : 1u); ++dword) { + if ((used & (1u << dword)) == 0u) { + values[dword] = ConstantU32(state, 0u); + continue; + } + values[dword] = state.module.AllocateId(); + state.module.AddFunction(spv::OpPhi, u32, values[dword], directValues[dword], directExit, lookupValues[dword], lookupExit); + } + return values; +} + std::uint32_t EmitBdaRead(SpirvValueEmitContext& ctx, const IrValue& inst, std::uint32_t address, std::uint32_t bits) { auto& state = ctx.state; if (bits != 8u && bits != 16u && bits != 32u) ctx.Fail(inst, "unsupported BDA read width"); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp index c454eee2c5..e8c470ddaa 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp @@ -203,6 +203,28 @@ std::uint32_t GuestAddress(SpirvValueEmitContext& ctx, const IrValue& inst, cons return AddBdaImmediate(ctx, inst, address, immediate); } +std::uint32_t GuestAddressLow(SpirvValueEmitContext& ctx, const IrValue& inst, const MemoryInfo& mem) { + const auto low = ctx.Arg(inst, 1); + if (mem.kind != ResourceKind::ScalarAddress) return low; + return Binary(ctx.state, spv::OpBitwiseAnd, TypeU32(ctx.state), low, ConstantU32(ctx.state, ~3u)); +} + +std::array ReadBdaDwords(SpirvValueEmitContext& ctx, const IrValue& inst, const MemoryInfo& mem, std::uint32_t dwords, std::uint32_t used) { + const auto lookup = [&] { return EmitBdaDwordReads(ctx, inst, GuestAddressBase(ctx, inst, mem), mem.offset, dwords, false, used); }; + const auto* anchor = FindBdaAnchor(ctx.state, inst); + if (anchor == nullptr) return lookup(); + return EmitAnchoredBdaRead(ctx.state, inst, *anchor, GuestAddressLow(ctx, inst, mem), static_cast(mem.offset), 32u, dwords, used, lookup); +} + +std::uint32_t ReadBda(SpirvValueEmitContext& ctx, const IrValue& inst, const MemoryInfo& mem, std::uint32_t bits) { + const auto lookup = [&] { return std::array{EmitBdaRead(ctx, inst, GuestAddress(ctx, inst, mem), bits)}; }; + const auto* anchor = FindBdaAnchor(ctx.state, inst); + if (anchor == nullptr) return lookup()[0]; + auto immediate = mem.offset; + if (mem.kind == ResourceKind::ScalarAddress) immediate &= ~3u; + return EmitAnchoredBdaRead(ctx.state, inst, *anchor, GuestAddressLow(ctx, inst, mem), static_cast(immediate), bits, 1u, 1u, lookup)[0]; +} + bool SameAddressValue(const IrValue* left, const IrValue* right, std::uint32_t depth = 0u) { if (left == right) return true; if (left == nullptr || right == nullptr || depth > 3u || left->Opcode() != right->Opcode() || left->ArgumentCount() != right->ArgumentCount()) return false; @@ -272,7 +294,7 @@ std::uint32_t LoadBda(SpirvValueEmitContext& ctx, const IrValue& inst, const Mem const auto none = state.module.AllocateId(); state.module.AddFunction(spv::OpCompositeConstruct, vector, none, zero, zero, zero, zero); const auto all = EmitValueOrDefaultIfCondition(state, ActiveArgument(ctx, inst), vector, none, [&]() { - const auto values = EmitBdaDwordReads(ctx, inst, GuestAddressBase(ctx, inst, mem), mem.offset, 4u, false, used); + const auto values = ReadBdaDwords(ctx, inst, mem, 4u, used); const auto read = state.module.AllocateId(); state.module.AddFunction(spv::OpCompositeConstruct, vector, read, values[0], values[1], values[2], values[3]); return read; @@ -284,7 +306,7 @@ std::uint32_t LoadBda(SpirvValueEmitContext& ctx, const IrValue& inst, const Mem } } return EmitValueOrZeroIfCondition(ctx.state, ActiveArgument(ctx, inst), [&]() { - return EmitBdaRead(ctx, inst, GuestAddress(ctx, inst, mem), bits); + return ReadBda(ctx, inst, mem, bits); }); } @@ -578,7 +600,7 @@ void LoadAddress(SpirvValueEmitContext& ctx, const IrValue& inst, std::uint32_t std::uint32_t LoadBdaWide(SpirvValueEmitContext& ctx, const IrValue& inst, const MemoryInfo& mem, std::uint32_t components) { auto& state = ctx.state; return EmitValueOrDefaultIfCondition(state, ActiveArgument(ctx, inst), TypeU32Composite(state, components), ConstantU32CompositeZero(state, components), [&]() { - return ConstructU32Composite(state, components, EmitBdaDwordReads(ctx, inst, GuestAddressBase(ctx, inst, mem), mem.offset, components)); + return ConstructU32Composite(state, components, ReadBdaDwords(ctx, inst, mem, components, 0u)); }); } diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp index 395e365f6e..d2a17cc285 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp @@ -1164,6 +1164,7 @@ void EmitProgram(SpirvEmitterState& state) { state.module.AddFunction(spv::OpStore, state.pixelValidMaskVariable, ConstantU32(state, 1u)); } if (AlphaToCoverage(state)) StoreCoverage(state, ConstantI32(state, -1)); + EmitBdaAnchors(state); if (OrderedPixelShader(state)) { state.module.AddFunction(spv::OpBeginInvocationInterlockEXT); } From 86de982f25ccb56841fe7c33e05a3489e79dcdfe Mon Sep 17 00:00:00 2001 From: Seif Allah Karoui <21332330+SeifKaroui@users.noreply.github.com> Date: Wed, 14 Oct 2026 14:29:59 +0100 Subject: [PATCH 0059/1000] fix(avplayer): defer loop warnings until frame delivery Queue demux loop epochs and report each once when audio or video first delivers that pass. Discard pending loop boundaries on seek and restart. Add playback regressions and record unverified console callback timing. AI-assisted: yes (Codex). --- core/libs/prx/libSceAvPlayer/src/Source.cpp | 11 +- core/libs/tests/GuestAvPlayer.cpp | 118 +++++++++++++++++++- docs/dev/debt/libSceAvPlayer.md | 2 + 3 files changed, 129 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAvPlayer/src/Source.cpp b/core/libs/prx/libSceAvPlayer/src/Source.cpp index 7778568ddc..1d92faa018 100644 --- a/core/libs/prx/libSceAvPlayer/src/Source.cpp +++ b/core/libs/prx/libSceAvPlayer/src/Source.cpp @@ -343,6 +343,7 @@ public: demuxEpoch = 1; minEpoch = 1; clockEpoch = 0; + loopEpochs.clear(); audioDriving = false; presented = false; lastPresented = 0; @@ -716,6 +717,12 @@ private: decoder.current = std::move(decoder.frames.front()); decoder.frames.pop_front(); info = decoder.current->info; + bool looped = false; + while (!loopEpochs.empty() && loopEpochs.front() <= decoder.current->epoch) { + looped = looped || loopEpochs.front() == decoder.current->epoch; + loopEpochs.pop_front(); + } + if (looped) events.OnWarning(SCE_AVPLAYER_ERROR_WAR_LOOPING_BACK); } void dropLateAudio() { @@ -756,6 +763,7 @@ private: presentationClock.Rebase(static_cast(requested->target)); ++demuxEpoch; minEpoch = demuxEpoch; + loopEpochs.clear(); for (auto* decoder : {&video, &audio}) { if (decoder->stream < 0) continue; for (auto& item : decoder->packets) av_packet_free(&item.packet); @@ -788,10 +796,10 @@ private: { std::lock_guard lock(mutex); ++demuxEpoch; + loopEpochs.push_back(demuxEpoch); pushAll(PacketKind::Drain, demuxEpoch, 0); } condition.notify_all(); - events.OnWarning(SCE_AVPLAYER_ERROR_WAR_LOOPING_BACK); continue; } { @@ -1071,6 +1079,7 @@ private: std::uint64_t demuxEpoch = 0; std::uint64_t minEpoch = 0; std::uint64_t clockEpoch = 0; + std::deque loopEpochs; bool audioDriving = false; bool presented = false; std::uint64_t lastPresented = 0; diff --git a/core/libs/tests/GuestAvPlayer.cpp b/core/libs/tests/GuestAvPlayer.cpp index 0410bbab96..153dcad01d 100644 --- a/core/libs/tests/GuestAvPlayer.cpp +++ b/core/libs/tests/GuestAvPlayer.cpp @@ -543,6 +543,11 @@ struct Events { return std::any_of(received.begin(), received.end(), [&](const auto& event) { return event.first == id && event.second == warning; }); } + int Count(std::int32_t id, std::int32_t warning = 0) { + std::lock_guard lock(mutex); + return static_cast(std::count_if(received.begin(), received.end(), [&](const auto& event) { return event.first == id && event.second == warning; })); + } + void Clear() { std::lock_guard lock(mutex); received.clear(); @@ -829,7 +834,7 @@ void TestPlayback() { } return wrapped && frames > FrameCount + 5; }), "looping never wrapped"); - Check(events.Seen(EventWarning, LoopingBack), "looping warning missing"); + Check(WaitFor([&] { return events.Seen(EventWarning, LoopingBack); }), "looping warning missing"); Check(sceAvPlayerIsActive(player), "looping player went inactive"); Check(sceAvPlayerStop(player) == 0, "stop failed"); Check(sceAvPlayerStop(player) == OperationFailed, "second stop accepted"); @@ -1123,6 +1128,114 @@ void TestUnsyncedVideoKeepsUpWithAudio() { Check(sceAvPlayerClose(player) == 0, "close failed"); } +void TestLoopWarningAtPlayback() { + Events events; + AvPlayerInitData init = InitData(&events); + auto* player = sceAvPlayerInit(&init); + Check(player != nullptr, "loop timing init failed"); + Check(sceAvPlayerAddSource(player, "/app0/avplayer.mp4") == 0, "loop timing source failed"); + Check(WaitFor([&] { return events.Seen(EventReady); }), "loop timing ready missing"); + Check(sceAvPlayerEnableStream(player, VideoStream) == 0, "loop timing video failed"); + Check(sceAvPlayerSetAvSyncMode(player, 1) == 0, "loop timing sync mode failed"); + Check(sceAvPlayerSetLooping(player, 1) == 0, "loop timing looping failed"); + Check(sceAvPlayerStart(player) == 0, "loop timing start failed"); + Check(WaitFor([&] { return events.Seen(EventPlay); }), "loop timing play missing"); + AvPlayerFrameInfoEx frame{}; + for (int index = 0; index <= FrameCount * 3; ++index) { + Check(WaitFor([&] { return sceAvPlayerGetVideoDataEx(player, &frame) != 0; }), "loop timing frame missing"); + Check(FrameIndex(frame.timestamp) == index % FrameCount, "loop timing frame out of order"); + const int loops = index / FrameCount; + if (loops > 0) Check(WaitFor([&] { return events.Count(EventWarning, LoopingBack) >= loops; }), "loop timing warning missing"); + std::this_thread::sleep_for(std::chrono::milliseconds(5)); + Check(events.Count(EventWarning, LoopingBack) == loops, "loop warning did not match the playback boundary"); + } + events.Clear(); + Check(sceAvPlayerSetLooping(player, 0) == 0, "loop timing disable looping failed"); + Check(sceAvPlayerJumpToTime(player, 0) == 0, "loop timing jump failed"); + Check(WaitFor([&] { return events.Seen(EventWarning, JumpComplete); }), "loop timing jump warning missing"); + for (int index = 0; index < FrameCount; ++index) { + Check(WaitFor([&] { return sceAvPlayerGetVideoDataEx(player, &frame) != 0; }), "loop timing seek frame missing"); + Check(FrameIndex(frame.timestamp) == index, "loop timing seek frame out of order"); + } + Check(sceAvPlayerStop(player) == 0, "loop timing stop failed"); + Check(WaitFor([&] { return events.Seen(EventStop); }), "loop timing stop event missing"); + Check(events.Count(EventWarning, LoopingBack) == 0, "seek delivered a pending loop warning"); + events.Clear(); + Check(sceAvPlayerStart(player) == 0, "loop timing restart failed"); + Check(WaitFor([&] { return events.Seen(EventPlay); }), "loop timing restart event missing"); + for (int index = 0; index < FrameCount; ++index) { + Check(WaitFor([&] { return sceAvPlayerGetVideoDataEx(player, &frame) != 0; }), "loop timing restart frame missing"); + Check(FrameIndex(frame.timestamp) == index, "loop timing restart frame out of order"); + } + Check(sceAvPlayerStop(player) == 0, "loop timing restart stop failed"); + Check(WaitFor([&] { return events.Seen(EventStop); }), "loop timing restart stop event missing"); + Check(events.Count(EventWarning, LoopingBack) == 0, "restart delivered a pending loop warning"); + Check(sceAvPlayerClose(player) == 0, "loop timing close failed"); +} + +void TestAudioLoopWarningAtPlayback() { + Events events; + AvPlayerInitData init = InitData(&events); + auto* player = sceAvPlayerInit(&init); + Check(player != nullptr, "audio loop timing init failed"); + Check(sceAvPlayerAddSource(player, "/app0/avplayer.mp4") == 0, "audio loop timing source failed"); + Check(WaitFor([&] { return events.Seen(EventReady); }), "audio loop timing ready missing"); + Check(sceAvPlayerEnableStream(player, EnglishAudioStream) == 0, "audio loop timing stream failed"); + Check(sceAvPlayerSetLooping(player, 1) == 0, "audio loop timing looping failed"); + Check(sceAvPlayerStart(player) == 0, "audio loop timing start failed"); + Check(WaitFor([&] { return events.Seen(EventPlay); }), "audio loop timing play missing"); + AvPlayerFrameInfo frame{}; + Check(WaitFor([&] { return sceAvPlayerGetAudioData(player, &frame) != 0; }), "audio loop timing first frame missing"); + int loops = 0; + for (int index = 0; index < 256 && loops < 3; ++index) { + const auto previous = frame.timestamp; + Check(WaitFor([&] { return sceAvPlayerGetAudioData(player, &frame) != 0; }), "audio loop timing frame missing"); + if (frame.timestamp < previous) ++loops; + if (loops > 0) Check(WaitFor([&] { return events.Count(EventWarning, LoopingBack) >= loops; }), "audio loop timing warning missing"); + std::this_thread::sleep_for(std::chrono::milliseconds(2)); + Check(events.Count(EventWarning, LoopingBack) == loops, "audio loop warning did not match the playback boundary"); + } + Check(loops == 3, "audio loop timing did not wrap three times"); + Check(sceAvPlayerClose(player) == 0, "audio loop timing close failed"); +} + +void TestLoopWarningSharedByStreams() { + Events events; + AvPlayerInitData init = InitData(&events); + auto* player = sceAvPlayerInit(&init); + Check(player != nullptr, "shared loop timing init failed"); + Check(sceAvPlayerAddSource(player, "/app0/avplayer.mp4") == 0, "shared loop timing source failed"); + Check(WaitFor([&] { return events.Seen(EventReady); }), "shared loop timing ready missing"); + Check(sceAvPlayerEnableStream(player, VideoStream) == 0, "shared loop timing video failed"); + Check(sceAvPlayerEnableStream(player, EnglishAudioStream) == 0, "shared loop timing audio failed"); + Check(sceAvPlayerSetLooping(player, 1) == 0, "shared loop timing looping failed"); + Check(sceAvPlayerStart(player) == 0, "shared loop timing start failed"); + Check(WaitFor([&] { return events.Seen(EventPlay); }), "shared loop timing play missing"); + AvPlayerFrameInfoEx frame{}; + AvPlayerFrameInfo sound{}; + std::uint64_t lastVideo = 0; + std::uint64_t lastAudio = 0; + bool videoWrapped = false; + bool audioWrapped = false; + Check(WaitFor([&] { + if (!audioWrapped && sceAvPlayerGetAudioData(player, &sound)) { + CheckEnglishAudio(sound); + audioWrapped = sound.timestamp < lastAudio; + lastAudio = sound.timestamp; + } + if (!videoWrapped && sceAvPlayerGetVideoDataEx(player, &frame)) { + CheckVideoFrame(frame); + videoWrapped = frame.timestamp < lastVideo; + lastVideo = frame.timestamp; + } + return audioWrapped && videoWrapped; + }), "shared loop timing streams did not both wrap"); + Check(sceAvPlayerPause(player) == 0, "shared loop timing pause failed"); + Check(WaitFor([&] { return events.Seen(EventPause); }), "shared loop timing pause event missing"); + Check(events.Count(EventWarning, LoopingBack) == 1, "audio and video reported the same loop more than once"); + Check(sceAvPlayerClose(player) == 0, "shared loop timing close failed"); +} + void TestEofStopLatency() { constexpr int Rounds = 9; std::array elapsed{}; @@ -1183,6 +1296,9 @@ void TestEofStopLatency() { int main() { try { WriteMovie(); + TestLoopWarningAtPlayback(); + TestAudioLoopWarningAtPlayback(); + TestLoopWarningSharedByStreams(); TestPlayback(); TestChangeStream(); TestExtendedExports(); diff --git a/docs/dev/debt/libSceAvPlayer.md b/docs/dev/debt/libSceAvPlayer.md index 5b8396edbf..148f5db253 100644 --- a/docs/dev/debt/libSceAvPlayer.md +++ b/docs/dev/debt/libSceAvPlayer.md @@ -7,6 +7,8 @@ ### Unknown function info +- [AvPlayer loop warnings](../../../core/libs/prx/libSceAvPlayer/src/Source.cpp) are queued when the first audio or video frame of a loop is delivered, following [KytyPS5's playback boundary tracking](https://github.com/KytyPS5/KytyPS5/blob/a6f4e8e7f456b8697d3102823d3f16400c3e121e/src/libs/avPlayer.cpp#L1038-L1043). The console's exact callback timing is unverified. Decode-ahead does not announce a loop while the caller is consuming the previous pass. + - [sceAvPlayerAddSource](../../../core/libs/prx/libSceAvPlayer/src/Source.cpp) (libSceAvPlayer) - requests up to 4 additional decode-ahead video framebuffers from the memory replacement beyond `num_output_video_framebuffers` when memory is available; the console's internal buffering behaviour is unknown - [sceAvPlayerStartEx](../../../core/libs/prx/libSceAvPlayer/Export.cpp) (libSceAvPlayer) - start info layout unknown; it is ignored and playback starts as with `sceAvPlayerStart` - [sceAvPlayerInit / sceAvPlayerInitEx](../../../core/libs/prx/libSceAvPlayer/src/Player.cpp) (libSceAvPlayer) - behaviour without a memory replacement unknown; frame and sample buffers then come from the guest heap From a0fc988a5359154c61446dbbfb24ae3ffffc345c Mon Sep 17 00:00:00 2001 From: Muhammad Wasif Ali Wasif <76695420+wachi35676@users.noreply.github.com> Date: Wed, 14 Oct 2026 00:58:42 +0500 Subject: [PATCH 0060/1000] refactor(agc): name dbOlWdppb4o and vieBRwlh1Lw the _10000 interpolant mappings The names hash to the NIDs dbOlWdppb4o and vieBRwlh1Lw that the exports carried as sceAgcUnknownCreateInterpolantMapping and sceAgcUnknown_vieBRwlh1Lw. Co-Authored-By: Claude Opus 5.5 --- .../Shader/src/InterpolantMapping.cpp | 6 ++-- .../libSceAgc/tests/InterpolantMapping.cpp | 34 +++++++++---------- docs/dev/debt/libSceAgc.md | 6 ++-- 3 files changed, 22 insertions(+), 24 deletions(-) diff --git a/core/libs/prx/libSceAgc/Shader/src/InterpolantMapping.cpp b/core/libs/prx/libSceAgc/Shader/src/InterpolantMapping.cpp index 608c859ad8..a77ed0e295 100644 --- a/core/libs/prx/libSceAgc/Shader/src/InterpolantMapping.cpp +++ b/core/libs/prx/libSceAgc/Shader/src/InterpolantMapping.cpp @@ -127,13 +127,11 @@ int APS5_VABI sceAgcCreateInterpolantMapping_0100(ShaderRegister* regs, const Sh return CreateInterpolantMapping(__func__, regs, gs, ps, CreateInterpolantValue, true); } -APS5_EXPORT("dbOlWdppb4o", sceAgcUnknownCreateInterpolantMapping); -int APS5_VABI sceAgcUnknownCreateInterpolantMapping(ShaderRegister* regs, const Shader* gs, const Shader* ps) { +int APS5_VABI sceAgcCreateInterpolantMapping_10000(ShaderRegister* regs, const Shader* gs, const Shader* ps) { return CreateInterpolantMapping(__func__, regs, gs, ps, CreateInterpolantValueSplitF16, true); } -APS5_EXPORT("vieBRwlh1Lw", sceAgcUnknown_vieBRwlh1Lw); -int APS5_VABI sceAgcUnknown_vieBRwlh1Lw(ShaderRegister* regs, const Shader* gs, const Shader* ps) { +int APS5_VABI sceAgcUpdateInterpolantMapping_10000(ShaderRegister* regs, const Shader* gs, const Shader* ps) { return CreateInterpolantMapping(__func__, regs, gs, ps, UpdateInterpolantValue, false); } diff --git a/core/libs/prx/libSceAgc/tests/InterpolantMapping.cpp b/core/libs/prx/libSceAgc/tests/InterpolantMapping.cpp index b176809a48..652eacfea7 100644 --- a/core/libs/prx/libSceAgc/tests/InterpolantMapping.cpp +++ b/core/libs/prx/libSceAgc/tests/InterpolantMapping.cpp @@ -12,8 +12,8 @@ #include extern "C" int APS5_VABI sceAgcCreateInterpolantMapping_0100(ShaderRegister* regs, const Shader* gs, const Shader* ps); -extern "C" int APS5_VABI sceAgcUnknownCreateInterpolantMapping(ShaderRegister* regs, const Shader* gs, const Shader* ps); -extern "C" int APS5_VABI sceAgcUnknown_vieBRwlh1Lw(ShaderRegister* regs, const Shader* gs, const Shader* ps); +extern "C" int APS5_VABI sceAgcCreateInterpolantMapping_10000(ShaderRegister* regs, const Shader* gs, const Shader* ps); +extern "C" int APS5_VABI sceAgcUpdateInterpolantMapping_10000(ShaderRegister* regs, const Shader* gs, const Shader* ps); namespace { @@ -61,14 +61,14 @@ void checkIdentity(const Registers& regs, std::uint32_t first, const char* messa void testIdentity() { auto regs = filled(); - check(sceAgcUnknownCreateInterpolantMapping(regs.data(), nullptr, nullptr) == 0, "mapping without shaders failed"); + check(sceAgcCreateInterpolantMapping_10000(regs.data(), nullptr, nullptr) == 0, "mapping without shaders failed"); checkIdentity(regs, 0, "mapping without a pixel shader is not the identity"); Shader ps{}; regs = filled(); - check(sceAgcUnknownCreateInterpolantMapping(regs.data(), nullptr, &ps) == 0, "mapping without inputs failed"); + check(sceAgcCreateInterpolantMapping_10000(regs.data(), nullptr, &ps) == 0, "mapping without inputs failed"); checkIdentity(regs, 0, "mapping without inputs is not the identity"); regs = filled(); - check(sceAgcUnknown_vieBRwlh1Lw(regs.data(), nullptr, &ps) == 0, "update without inputs failed"); + check(sceAgcUpdateInterpolantMapping_10000(regs.data(), nullptr, &ps) == 0, "update without inputs failed"); check(preparedCount == 0u, "update without inputs prepared registers"); const auto untouched = filled(); check(std::memcmp(regs.data(), untouched.data(), sizeof(regs)) == 0, "update without inputs wrote registers"); @@ -94,7 +94,7 @@ void testMapping() { ps.num_input_semantics = static_cast(inputs.size()); auto split = filled(); - check(sceAgcUnknownCreateInterpolantMapping(split.data(), &gs, &ps) == 0, "split f16 mapping failed"); + check(sceAgcCreateInterpolantMapping_10000(split.data(), &gs, &ps) == 0, "split f16 mapping failed"); check(std::memcmp(split.data(), preparedMapping.data(), sizeof(split)) == 0, "prepared split mapping differs from published registers"); for (std::uint32_t i = 0; i < inputs.size(); ++i) { check(split[i].offset == ShaderRegs::SPI_PS_INPUT_CNTL_0 + i, "interpolant register offset changed"); @@ -109,7 +109,7 @@ void testMapping() { auto update = filled(); const auto untouched = filled(); const std::size_t written = inputs.size() * sizeof(ShaderRegister); - check(sceAgcUnknown_vieBRwlh1Lw(update.data(), &gs, &ps) == 0, "update mapping failed"); + check(sceAgcUpdateInterpolantMapping_10000(update.data(), &gs, &ps) == 0, "update mapping failed"); check(preparedCount == inputs.size(), "update prepared registers past its inputs"); check(std::memcmp(update.data(), preparedMapping.data(), written) == 0, "prepared update differs from published registers"); check(std::memcmp(update.data(), split.data(), written) == 0, "update mapping differs from the split f16 mapping"); @@ -142,7 +142,7 @@ void testUpdateMapping() { auto update = filled(); const auto untouched = filled(); - check(sceAgcUnknown_vieBRwlh1Lw(update.data(), &gs, &ps) == 0, "update mapping failed"); + check(sceAgcUpdateInterpolantMapping_10000(update.data(), &gs, &ps) == 0, "update mapping failed"); check(update[0].value == 0x00000625u, "custom interpolant update changed"); check(update[1].value == 0x01380107u, "low f16 half update changed"); check(update[2].value == 0x02480220u, "unmatched high f16 half update changed"); @@ -154,14 +154,14 @@ void testUpdateMapping() { check(std::memcmp(update.data() + inputs.size(), untouched.data(), sizeof(update) - written) == 0, "update wrote past its inputs"); auto create = filled(); - check(sceAgcUnknownCreateInterpolantMapping(create.data(), &gs, &ps) == 0, "split f16 mapping failed"); + check(sceAgcCreateInterpolantMapping_10000(create.data(), &gs, &ps) == 0, "split f16 mapping failed"); check(std::memcmp(create.data(), update.data(), written) == 0, "update differs from the split f16 mapping"); checkIdentity(create, static_cast(inputs.size()), "unused interpolants are not the identity"); for (const std::uint32_t index : {1u, 3u}) { inputs[index].default_value = 0; update = filled(); - expectFailure([&] { sceAgcUnknown_vieBRwlh1Lw(update.data(), &gs, &ps); }); + expectFailure([&] { sceAgcUpdateInterpolantMapping_10000(update.data(), &gs, &ps); }); check(std::memcmp(update.data(), untouched.data(), sizeof(update)) == 0, "rejected update wrote registers"); inputs[index].default_value = inputs[index].default_value_hi; } @@ -177,15 +177,15 @@ void testRejections() { std::memset(regs.data(), 0xcc, sizeof(regs)); const auto saved = regs; expectFailure([&] { sceAgcCreateInterpolantMapping_0100(regs.data(), &gs, &ps); }); - expectFailure([&] { sceAgcUnknownCreateInterpolantMapping(regs.data(), &gs, &ps); }); - expectFailure([&] { sceAgcUnknown_vieBRwlh1Lw(regs.data(), &gs, &ps); }); + expectFailure([&] { sceAgcCreateInterpolantMapping_10000(regs.data(), &gs, &ps); }); + expectFailure([&] { sceAgcUpdateInterpolantMapping_10000(regs.data(), &gs, &ps); }); check(std::memcmp(regs.data(), saved.data(), sizeof(regs)) == 0, "rejected mapping wrote registers"); ps.num_input_semantics = 1; - expectFailure([&] { sceAgcUnknownCreateInterpolantMapping(regs.data(), nullptr, &ps); }); - expectFailure([&] { sceAgcUnknownCreateInterpolantMapping(nullptr, &gs, &ps); }); - expectFailure([&] { sceAgcUnknown_vieBRwlh1Lw(regs.data(), nullptr, &ps); }); - expectFailure([&] { sceAgcUnknown_vieBRwlh1Lw(nullptr, &gs, &ps); }); - expectFailure([&] { sceAgcUnknown_vieBRwlh1Lw(regs.data(), &gs, nullptr); }); + expectFailure([&] { sceAgcCreateInterpolantMapping_10000(regs.data(), nullptr, &ps); }); + expectFailure([&] { sceAgcCreateInterpolantMapping_10000(nullptr, &gs, &ps); }); + expectFailure([&] { sceAgcUpdateInterpolantMapping_10000(regs.data(), nullptr, &ps); }); + expectFailure([&] { sceAgcUpdateInterpolantMapping_10000(nullptr, &gs, &ps); }); + expectFailure([&] { sceAgcUpdateInterpolantMapping_10000(regs.data(), &gs, nullptr); }); } } diff --git a/docs/dev/debt/libSceAgc.md b/docs/dev/debt/libSceAgc.md index 2a41360807..ab608ee85a 100644 --- a/docs/dev/debt/libSceAgc.md +++ b/docs/dev/debt/libSceAgc.md @@ -20,15 +20,15 @@ - [sceAgcDcbSet{Cx,Sh,Uc}RegistersIndirectGetSize](../../../core/libs/prx/libSceAgc/DcbState/src/Registers.cpp) (libSceAgc) - the count argument is ignored: PPSA28416 calls them with leftover register contents (0xf5f5039c), so the real functions likely take none; the size is 20 bytes either way - [sceAgcUpdateInterpolantMapping_0100](../../../core/libs/prx/libSceAgc/Misc/src/ShaderFusion.cpp) (libSceAgc) - name from its NID (`k0E7vkgqAuE`); the `(regs, vs, ps)` arguments are the declared ones, behaviour unknown - [sceAgcWriteDataPatchSetDst, sceAgcWriteDataPatchSetCachePolicy and their Async forms](../../../core/libs/prx/libSceAgc/Patch/src/WaitRegMem.cpp) (libSceAgc) - the signatures `(packet, uint8 value)` are inferred from the other patch functions, as no public declaration was found; the value is taken in the encoding of the `dst` and `cachePolicy` arguments of `sceAgcDcbWriteData` (graphics) and `sceAgcAcbWriteData` (Async, compute), and the patched packet is the one those calls would write. `sceAgcAsyncWriteDataPatchSetAddressOrOffset` is assumed to patch the address like `sceAgcWriteDataPatchSetAddressOrOffset`, as the compute WRITE_DATA packet keeps it in the same dwords -- [dbOlWdppb4o](../../../core/libs/prx/libSceAgc/Shader/src/InterpolantMapping.cpp) (libSceAgc) - unknown name; same arguments as `sceAgcCreateInterpolantMapping_0100`; the `SPI_PS_INPUT_CNTL` bits for `is_f16` 2 inputs follow KytyPS5, not confirmed on hardware +- [sceAgcCreateInterpolantMapping_10000](../../../core/libs/prx/libSceAgc/Shader/src/InterpolantMapping.cpp) (libSceAgc) - name from its NID (`dbOlWdppb4o`), not in the aerolib NID list; same arguments as `sceAgcCreateInterpolantMapping_0100`; the `SPI_PS_INPUT_CNTL` bits for `is_f16` 2 inputs follow KytyPS5, not confirmed on hardware - [sceAgcGetDataPacketPayloadRange](../../../core/libs/prx/libSceAgc/Misc/src/PacketInfo.cpp) (libSceAgc) - signature, the `{base, size in bytes}` output and the type 0 range (header + 1, one dword longer than the type 1 payload) from KytyPS5 only, matching `sceAgcGetDataPacketPayloadAddress_0090`; prosper returns header + 2 for both types from that address function - [sceAgcAsyncCondExecPatchSetCommandAddress, sceAgcAsyncCondExecPatchSetEnd](../../../core/libs/prx/libSceAgc/Patch/src/CondExec.cpp) (libSceAgc) - no reference has a body for them; they are assumed to patch the COND_EXEC packet like `sceAgcCondExecPatchSetCommandAddress` and `sceAgcCondExecPatchSetEnd`, since `sceAgcAcbCondExec` writes the same packet as `sceAgcDcbCondExec`, as `sceAgcAsyncRewindPatchSetRewindState` does for REWIND - [sceAgcAcbAtomicGds_0900](../../../core/libs/prx/libSceAgc/Acb/src/Control.cpp) (libSceAgc) - name from its NID (`gQkqkLttcpw`), signature unknown - [sceAgcDcbPrimeUtcl2](../../../core/libs/prx/libSceAgc/DcbState/src/Display.cpp) (libSceAgc) - assumed to write the packet `sceAgcAcbPrimeUtcl2` writes: PRIME_UTCL2 has the same 5 dwords on the graphics and compute rings. Not confirmed on a title. Both write a NOP of that size, as there is no TLB to prime - [sceAgcDcbQueueEndOfShaderActionGetSize](../../../core/libs/prx/libSceAgc/DcbState/src/ContextState.cpp) (libSceAgc) - the builder is inline in the SDK and not exported; the size is assumed to be that of the RELEASE_MEM packet `sceAgcCbReleaseMem` writes (32 bytes), the same as `sceAgcAcbQueueEndOfShaderActionGetSize`, as in [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/main/src/game_compat.c) and [REmu](https://github.com/xnetcat/REmu/blob/main/crates/remu-runtime/src/agc_dcb.rs) -- [vieBRwlh1Lw](../../../core/libs/prx/libSceAgc/Shader/src/InterpolantMapping.cpp) (libSceAgc) - unknown name; the `(regs, gs, ps)` arguments of `sceAgcCreateInterpolantMapping_0100` come from OpenAGC only +- [sceAgcUpdateInterpolantMapping_10000](../../../core/libs/prx/libSceAgc/Shader/src/InterpolantMapping.cpp) (libSceAgc) - name from its NID (`vieBRwlh1Lw`), not in the aerolib NID list; the `(regs, gs, ps)` arguments of `sceAgcCreateInterpolantMapping_0100` come from OpenAGC only - [sceAgcDcbSetZPassPredicationEnableGetSize, sceAgcDcbSetPredicationDisableGetSize, sceAgcDcbSetBoolPredicationEnableGetSize](../../../core/libs/prx/libSceAgc/DcbState/src/Predication.cpp) (libSceAgc) - the builders are inline in the SDK and not exported; the size is assumed to be that of the SET_PREDICATION packet `sceAgcDcbSetPredication` writes (16 bytes) -- [vieBRwlh1Lw](../../../core/libs/prx/libSceAgc/Shader/src/InterpolantMapping.cpp) (libSceAgc) follows [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/game_compat.c) only, not confirmed on hardware: the `dbOlWdppb4o` transform without the identity tail, so it writes one register per pixel shader input and none for a pixel shader without inputs, and a null pixel shader throws. For an `is_f16` 1 or 3 input whose `default_value` and `default_value_hi` differ it throws: OpenAGC takes `DEFAULT_VAL` from `default_value_hi`, while KytyPS5, sharpemu and `dbOlWdppb4o` here take it from `default_value` +- [sceAgcUpdateInterpolantMapping_10000](../../../core/libs/prx/libSceAgc/Shader/src/InterpolantMapping.cpp) (libSceAgc) follows [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/game_compat.c) only, not confirmed on hardware: the `sceAgcCreateInterpolantMapping_10000` transform without the identity tail, so it writes one register per pixel shader input and none for a pixel shader without inputs, and a null pixel shader throws. For an `is_f16` 1 or 3 input whose `default_value` and `default_value_hi` differ it throws: OpenAGC takes `DEFAULT_VAL` from `default_value_hi`, while KytyPS5, sharpemu and `sceAgcCreateInterpolantMapping_10000` here take it from `default_value` - [sceAgcAcbWaitUntilSafeForRendering](../../../core/libs/prx/libSceAgc/Acb/src/Control.cpp) (libSceAgc) - `(buffer, videoOutHandle, displayBufferIndex)`, the declared arguments, which PPSA12544 (01.000.010) passes inside `waitUntilSafeForRendering` and `waitUntilSafeForRendering VR` markers on an asynchronous compute command buffer, ignoring the result. It writes the same rendering-wait packet as `sceAgcDcbWaitUntilSafeForRendering`, which the driver now accepts on compute queues too; the wait blocks only that queue's worker. Whether the console encodes the compute variant differently is not known ### Functional From 654634b9765adf270214e5e08725f5e550c2d572 Mon Sep 17 00:00:00 2001 From: Muhammad Wasif Ali Wasif <76695420+wachi35676@users.noreply.github.com> Date: Tue, 13 Oct 2026 23:21:02 +0500 Subject: [PATCH 0061/1000] docs(contributing): manual builds run the relinker matrix only when its files change Co-Authored-By: Claude Opus 5.5 --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b83c070c09..41c100b3d4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -37,7 +37,7 @@ For pull requests, full Linux and Windows builds and tests run only when a colla gh workflow run build.yml --repo boykopovar/AnyPS5 --ref main --field pr=1234 ``` -This manual run also builds and tests the relinker on Linux, Windows and macOS, regardless of the author's previous contributions or the changed files. The relinker matrix also runs automatically when a pull request targeting `main` is opened, updated or reopened, if its author already has a commit in `main` and it changes a file the relinker-only build uses; full builds never run automatically for pull requests. Every job that runs automatically for pull requests runs only when the pull request changes its files: a `paths` filter on the `pull_request` trigger, or `needs: eligibility` with an `if` on the files output it sets in `build.yml`. The Conventions check fails a workflow change that adds a job without one. All build jobs build the selected pull request merged into the current `main` (GitHub's merge commit), so a pull request that no longer merges cleanly must be rebased first. New commits require another manual run for full builds. +This manual run also builds and tests the relinker on Linux, Windows and macOS when the pull request changes a file the relinker-only build uses, regardless of the author's previous contributions. The relinker matrix also runs automatically when a pull request targeting `main` is opened, updated or reopened, if its author already has a commit in `main` and it changes a file the relinker-only build uses; full builds never run automatically for pull requests. Every job that runs automatically for pull requests runs only when the pull request changes its files: a `paths` filter on the `pull_request` trigger, or `needs: eligibility` with an `if` on the files output it sets in `build.yml`. The Conventions check fails a workflow change that adds a job without one. All build jobs build the selected pull request merged into the current `main` (GitHub's merge commit), so a pull request that no longer merges cleanly must be rebased first. New commits require another manual run for full builds. The Conventions check runs automatically for every contributor, including first-time contributors. It uses the base branch checker and reads pull request Git objects without executing pull request code. The Conventions check runs on every pull request and fails when a rule on this page is broken. It accepts code comments only when the pull request also changes [technical debt](docs/dev/TechnicalDebt.md), the index or a file in `docs/dev/debt/`, and only UTF-8 text files. Run it locally before pushing: From 937f235dccb6ec79d02d60e6dc7ed371de35288a Mon Sep 17 00:00:00 2001 From: tototomate123 Date: Sat, 10 Oct 2026 17:27:14 +0200 Subject: [PATCH 0062/1000] fix(agc-driver): allow stale clear flags on color-only draws --- .../prx/libSceAgcDriver/Graphics/src/State.cpp | 6 +++--- .../prx/libSceAgcDriver/tests/Graphics.cpp | 18 +++++++++++++++++- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index 955504eccb..74368ceaef 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -767,7 +767,7 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) { const auto renderControl = find(cx, 0x000); const bool clear = renderControl != cx.end() && (renderControl->second & 3u) != 0; const auto tests = planeTests(cx, depthControl); - if (clear || (tests != 0 && depthSurfaceBound(cx))) { + if ((clear || tests != 0) && depthSurfaceBound(cx)) { decodeDepth(cx, depthControl, (read(cx, 0x203) & 2u) != 0 && !PixelProgramSkipped(queue), result); } else if (depthPassThrough(depthControl) || ((depthControl & 0xbu) != 0 && (tests == 0 || depthPlanesAbsent(cx)))) { static bool reported = false; @@ -1331,11 +1331,11 @@ std::string DrawRejection(const QueueState& queue, bool indexed) { const auto renderControl = find(cx, 0x000); const bool depthClear = renderControl != cx.end() && (renderControl->second & 1u) != 0; const bool stencilClear = renderControl != cx.end() && (renderControl->second & 2u) != 0; - if (stencilClear) { + if (stencilClear && !depthPlanesAbsent(cx)) { std::uint32_t stencil = 0, view = 0; if ((value(cx, 0x011, stencil) && (stencil & 1u) == 0) || (value(cx, 0x002, view) && (view & 0x02000000u) != 0)) return require(false, "stencil clear requires a writable stencil plane"); } - if (depthClear) { + if (depthClear && !depthPlanesAbsent(cx)) { std::uint32_t z = 0, view = 0; if ((value(cx, 0x010, z) && (z & 3u) == 0) || (value(cx, 0x002, view) && (view & 0x01000000u) != 0)) return require(false, "depth clear requires a writable depth plane"); } diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 8af6ed8ccf..331a19f9ab 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -1572,7 +1572,23 @@ void DepthStencilTests() { expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "writable stencil plane"); Require(AgcDriver::Graphics::DrawRejection(queue, false).find("writable stencil plane") != std::string::npos, "precheck accepted clear without a stencil plane"); queue.context[0x010] = 0; - expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "writable stencil plane"); + state = AgcDriver::Graphics::DecodeState(queue); + Require(!state.depth && !state.depthTest && !state.depthWrite && !state.stencilTest && state.colors.size() == 2, "clear flags without depth/stencil planes changed color-only rendering"); + Require(AgcDriver::Graphics::DrawRejection(queue, false).empty(), "precheck rejected clear flags without depth/stencil planes"); + queue = makeState(); + queue.context[0x010] = 0x80000180; + queue.context[0x011] = 0x20000180; + queue.context[0x200] = 0x776; + queue.context[0x1b3] = 2; + queue.context[0x1b4] = 2; + const auto colorOnly = AgcDriver::Graphics::DecodeState(queue); + for (const auto control : {1u, 2u, 3u}) { + queue.context[0x000] = control; + state = AgcDriver::Graphics::DecodeState(queue); + Require(!state.depth && !state.depthTest && !state.depthWrite && !state.stencilTest, "stale clear flags enabled absent depth/stencil planes"); + Require(state.color.address == colorOnly.color.address && state.blends[0].colorWriteMask == colorOnly.blends[0].colorWriteMask && state.viewport.minDepth == colorOnly.viewport.minDepth && state.viewport.maxDepth == colorOnly.viewport.maxDepth, "stale clear flags changed color writes or viewport depth"); + Require(AgcDriver::Graphics::DrawRejection(queue, false).empty(), "precheck rejected a color-only draw with stale clear flags: " + AgcDriver::Graphics::DrawRejection(queue, false)); + } queue = makeState(); queue.context[0x31b] = 1u << 26u; expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "mip exceeds"); From 54f391be07580ad1ccd7670f2b9352e2dfe613e2 Mon Sep 17 00:00:00 2001 From: Nithishvelan M Date: Wed, 14 Oct 2026 19:37:39 +0530 Subject: [PATCH 0063/1000] test(libSceLoginDialog): cover uninitialized close and initial status in guest test --- core/libs/tests/GuestLoginDialog.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/core/libs/tests/GuestLoginDialog.cpp b/core/libs/tests/GuestLoginDialog.cpp index 7eaeb5619f..cd631caa4e 100644 --- a/core/libs/tests/GuestLoginDialog.cpp +++ b/core/libs/tests/GuestLoginDialog.cpp @@ -16,6 +16,8 @@ namespace { void Require(bool value) { if (!value) std::abort(); } +constexpr int kStatusNone = 0; +constexpr int kStatusInitialized = 1; constexpr int kStatusFinished = 3; constexpr int kErrNotInitialized = static_cast(0x80B80003); constexpr int kErrAlreadyInitialized = static_cast(0x80B80004); @@ -31,10 +33,15 @@ int main() { Require(sceLoginDialogTerminate() == kErrNotInitialized); Require(sceLoginDialogOpen(¶m) == kErrNotInitialized); + Require(sceLoginDialogClose() == kErrNotInitialized); Require(sceLoginDialogGetResult(result) == kErrNotInitialized); + Require(sceLoginDialogGetStatus() == kStatusNone); + Require(sceLoginDialogUpdateStatus() == kStatusNone); Require(sceLoginDialogInitialize() == 0); Require(sceLoginDialogInitialize() == kErrAlreadyInitialized); + Require(sceLoginDialogGetStatus() == kStatusInitialized); + Require(sceLoginDialogUpdateStatus() == kStatusInitialized); Require(sceLoginDialogOpen(nullptr) == kErrArgNull); Require(sceLoginDialogGetResult(result) == kErrNotFinished); @@ -51,4 +58,5 @@ int main() { Require(sceLoginDialogClose() == 0); Require(sceLoginDialogTerminate() == 0); Require(sceLoginDialogTerminate() == kErrNotInitialized); + Require(sceLoginDialogGetStatus() == kStatusNone); } From 3265cb6d79969021002c1808efee4d220668a74d Mon Sep 17 00:00:00 2001 From: Gjon Gojcevic Date: Wed, 14 Oct 2026 10:09:08 -0400 Subject: [PATCH 0064/1000] fix(shader): keep BDA anchors exact on tables whose ranges overlap The driver builds tables whose ranges ascend by begin but can overlap (a per-build region read over the address space's ranges). A lookup selects the last range beginning at or below an address, so the anchor's range now ends at the next range's begin when that comes first, and the driver requires only ascending begins (BdaAbi::RangesOrdered). An anchor is also not used for a range longer than 2^62 bytes, whose offset arithmetic could wrap with a positive immediate. Anchors are counted per read site after adjacent loads are grouped and reads with no used dwords are dropped, so a program does not search for one effective read; read eligibility shares RoutesApertures. Tests: the anchored test programs can replace the lookup path by a sentinel, which shows which reads load directly; the anchor table has an overlapping range; a recompiled compute program reads through an anchor on the GPU. AI-assisted: yes --- .../Graphics/src/BdaResources.cpp | 2 +- .../libSceAgcDriver/tests/BdaContracts.cpp | 5 +- .../libSceAgcDriver/tests/BdaExecution.cpp | 57 +++++- .../prx/libSceAgcDriver/tests/BdaShader.cpp | 4 + .../prx/libSceAgcDriver/tests/BdaShader.hpp | 2 + .../libSceAgcDriver/tests/ShaderMemory.cpp | 2 +- .../tests/execution/BdaAnchorReads.cpp | 164 ++++++++++++++++++ core/shader/recompiler/BdaAbi.hpp | 5 +- .../include/SpirvBackend/SpirvBda.hpp | 3 + .../SpirvBackend/src/SpirvBdaLookup.cpp | 5 +- .../SpirvBackend/src/SpirvBdaRead.cpp | 62 +++---- .../SpirvMemory/SpirvMemoryInstructions.cpp | 37 +++- 12 files changed, 295 insertions(+), 53 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/BdaAnchorReads.cpp diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp index 4bb8b097ed..eb3c163130 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp @@ -192,7 +192,7 @@ BdaResources::BdaResources(const Context& context, const GuestBufferMemory& memo ++it; } } - Require(ShaderRecompiler::BdaAbi::RangesOrdered(ranges.data(), ranges.size()), "BDA table ranges are not ascending and disjoint"); + Require(ShaderRecompiler::BdaAbi::RangesOrdered(ranges.data(), ranges.size()), "BDA table ranges do not ascend by begin"); for (const auto& range : ranges) Require((range.permissions & ShaderRecompiler::BdaAbi::Write) == 0 || range.end <= ShaderRecompiler::BdaAbi::WrittenAddressLimit, "a writable BDA range ends past the addresses the written-page record can hold"); table = std::make_shared(context, tableBytes, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); const ShaderRecompiler::BdaAbi::Header header{ShaderRecompiler::BdaAbi::Version, static_cast(ranges.size()), sizeof(ShaderRecompiler::BdaAbi::Range), 0}; diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaContracts.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaContracts.cpp index dd65417557..e113105055 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaContracts.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaContracts.cpp @@ -58,8 +58,7 @@ void RunBdaContractTests() { reject([&] { static_cast(serializer.Deserialize(invalid)); }, "request signature"); reject([&] { static_cast(serializer.Deserialize("NVNQQWMAAAA=")); }, "serialization version"); const auto ordered = [](std::initializer_list ranges) { return BdaAbi::RangesOrdered(ranges.begin(), ranges.size()); }; - AgcDriver::Graphics::Require(ordered({}) && ordered({{0x1000, 0x2000, 1, 1, 0}, {0x2000, 0x2000, 1, 1, 0}, {0x2000, 0x3000, 1, 1, 0}, {0x4000, 0x5000, 1, 1, 0}}), "ascending disjoint BDA ranges were refused"); - AgcDriver::Graphics::Require(!ordered({{0x1000, 0x3000, 1, 1, 0}, {0x2000, 0x2100, 1, 1, 0}}), "overlapping BDA ranges were accepted"); + AgcDriver::Graphics::Require(ordered({}) && ordered({{0x1000, 0x2000, 1, 1, 0}, {0x2000, 0x2000, 1, 1, 0}, {0x2000, 0x3000, 1, 1, 0}, {0x4000, 0x5000, 1, 1, 0}}), "ascending BDA ranges were refused"); + AgcDriver::Graphics::Require(ordered({{0x1000, 0x3000, 1, 1, 0}, {0x2f00, 0x3100, 1, 1, 0}}), "BDA ranges ascending by begin and overlapping were refused"); AgcDriver::Graphics::Require(!ordered({{0x4000, 0x5000, 1, 1, 0}, {0x1000, 0x2000, 1, 1, 0}}), "unsorted BDA ranges were accepted"); - AgcDriver::Graphics::Require(!ordered({{0x2000, 0x1000, 1, 1, 0}}), "a BDA range ending before its begin was accepted"); } diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaExecution.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaExecution.cpp index e3a7a3f99d..113991ac43 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaExecution.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaExecution.cpp @@ -228,22 +228,24 @@ static void RunBdaAnchorTests(const Context& context) { Buffer large(context, 1024, usage); Buffer top(context, 16, usage); Buffer above(context, 16, usage); + Buffer overlap(context, 0x200, usage); std::uint32_t seed = 0x11; - for (auto* buffer : {&first, &second, &words, &shifted, &large, &top, &above}) { + for (auto* buffer : {&first, &second, &words, &shifted, &large, &top, &above, &overlap}) { for (std::size_t index = 0; index < buffer->Bytes().size(); ++index) buffer->Bytes()[index] = std::byte(static_cast(seed + index * 7u)); seed += 0x33; } for (std::uint32_t index = 0; index < 16u; ++index) words.Bytes()[index] = std::byte(static_cast(0xa0u + index)); for (std::uint32_t index = 0; index < 20u; ++index) shifted.Bytes()[index] = std::byte(static_cast(0xc0u + index)); - const std::array ranges{{{guest, guest + 3, first.DeviceAddress(), Abi::Read, 0}, + std::vector ranges{{guest, guest + 3, first.DeviceAddress(), Abi::Read, 0}, {guest + 3, guest + 5, second.DeviceAddress(), Abi::Read, 0}, {guest + 0x100, guest + 0x110, words.DeviceAddress(), Abi::Read, 0}, {guest + 0x200, guest + 0x210, shifted.DeviceAddress() + 1u, Abi::Read, 0}, {guest + 0x400, guest + 0x800, large.DeviceAddress(), Abi::Read, 0}, + {guest + 0x700, guest + 0x900, overlap.DeviceAddress(), Abi::Read, 0}, {limit - 0x10, limit, top.DeviceAddress(), Abi::Read, 0}, - {highest, highest + 0x10, above.DeviceAddress(), Abi::Read, 0}}}; - Require(Abi::RangesOrdered(ranges.data(), ranges.size()), "the BDA anchor test table is not ascending and disjoint"); - Buffer table(context, sizeof(Abi::Header) + sizeof(ranges), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + {highest, highest + 0x10, above.DeviceAddress(), Abi::Read, 0}}; + Require(Abi::RangesOrdered(ranges.data(), ranges.size()), "the BDA anchor test table does not ascend by begin"); + Buffer table(context, sizeof(Abi::Header) + ranges.size() * sizeof(Abi::Range), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); Buffer fault(context, sizeof(Abi::Fault), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); Buffer output(context, (BdaAnchoredReadInputWord + 3u + lanes) * sizeof(std::uint32_t), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); struct Outcome { @@ -254,10 +256,10 @@ static void RunBdaAnchorTests(const Context& context) { std::uint32_t permissions = Abi::Read; const auto run = [&](Pipeline& pipeline, std::uint32_t read, std::uint64_t anchor, const std::vector& lows) { auto copy = ranges; - copy[2].permissions = permissions; + if (copy.size() > 2u) copy[2].permissions = permissions; const Abi::Header header{Abi::Version, count, sizeof(Abi::Range), 0}; std::memcpy(table.Bytes().data(), &header, sizeof(header)); - std::memcpy(table.Bytes().data() + sizeof(header), copy.data(), sizeof(copy)); + std::memcpy(table.Bytes().data() + sizeof(header), copy.data(), copy.size() * sizeof(Abi::Range)); std::memset(fault.Bytes().data(), 0, fault.Bytes().size()); std::memset(output.Bytes().data(), 0xa5, BdaAnchoredReadInputWord * sizeof(std::uint32_t)); std::vector inputs{static_cast(anchor), static_cast(anchor >> 32u), read}; @@ -300,7 +302,7 @@ static void RunBdaAnchorTests(const Context& context) { for (const std::int32_t immediate : {0, 4, -1, -8, 0x7ffffff0, std::numeric_limits::min()}) { for (const auto& [bits, dwords, used] : std::initializer_list>{{8, 1, 1}, {16, 1, 1}, {32, 1, 1}, {32, 4, 15}, {32, 4, 10}, {32, 3, 4}}) reads.push_back({immediate, bits, dwords, used}); } - const auto anchors = {guest + 0x100, guest + 0x108, guest, guest + 3, guest + 0x200, guest - 1, guest + 0x110, guest + 0x400, limit - 0x10, limit - 4, highest, highest + 8, std::numeric_limits::max() - 2}; + const auto anchors = {guest + 0x100, guest + 0x108, guest, guest + 3, guest + 0x200, guest - 1, guest + 0x110, guest + 0x400, guest + 0x6f0, guest + 0x700, guest + 0x800, limit - 0x10, limit - 4, highest, highest + 8, std::numeric_limits::max() - 2}; std::vector lows{0u, 1u, 2u, 3u, 4u, 8u, 0xcu, 0xdu, 0xeu, 0xfu, 0x10u, 0x14u, 0x100u, 0x108u, 0x10cu, 0x1fcu, 0x200u, 0x20eu, 0x3f0u, 0x3fcu, 0x400u, 0x7fffu, 0x7fffffffu, 0x80000000u, 0xfffffff0u, 0xfffffffcu, 0xffffffffu}; for (std::uint32_t lane = 0; lane < 32u; ++lane) lows.push_back(lane * 0x24u); std::vector strided; @@ -316,6 +318,39 @@ static void RunBdaAnchorTests(const Context& context) { auto stopping = build({reads, true, true, false, 1}); compare(*stopping, anchors, {0u, 0x80000000u, 0xfffffffcu}); const auto check = [&](std::uint32_t read, std::uint64_t anchor, std::uint32_t low) { return run(stopping->anchored, read, anchor, {low}); }; + auto direct = std::make_unique(context, MakeBdaAnchoredReadTestShader({reads, true, false, false, 1, true}, true), std::array{&table, &fault, &output}); + const auto dword = [](Buffer& buffer, std::size_t offset) { + std::uint32_t value = 0; + std::memcpy(&value, buffer.Bytes().data() + offset, sizeof(value)); + return value; + }; + const auto loads = [&](std::uint32_t read, std::uint64_t anchor, std::uint32_t low, std::initializer_list values) { + const auto outcome = run(*direct, read, anchor, {low}); + Require(outcome.fault.state == Abi::FaultState::Empty && std::equal(values.begin(), values.end(), outcome.values.begin()), "a read the anchor holds did not load directly at anchor 0x" + std::to_string(anchor) + " + " + std::to_string(low) + ", read " + std::to_string(read)); + }; + const auto misses = [&](std::uint32_t read, std::uint64_t anchor, std::uint32_t low) { + const auto outcome = run(*direct, read, anchor, {low}); + Require(outcome.fault.state == Abi::FaultState::Empty && outcome.values[0] == BdaAnchoredReadSentinel, "a read the anchor does not hold did not take the lookup path at anchor 0x" + std::to_string(anchor) + " + " + std::to_string(low) + ", read " + std::to_string(read)); + }; + loads(2, guest + 0x100, 0, {dword(words, 0)}); + loads(2, guest + 0x100, 0xc, {dword(words, 0xc)}); + loads(3, guest + 0x100, 0, {dword(words, 0), dword(words, 4), dword(words, 8), dword(words, 0xc)}); + loads(0, guest + 0x200, 0xd, {0xceu}); + loads(0, guest + 3, 1, {static_cast(second.Bytes()[1])}); + loads(1, guest, 1, {static_cast(first.Bytes()[1]) | static_cast(first.Bytes()[2]) << 8u}); + loads(2, guest + 0x400, 0x2fc, {dword(large, 0x2fc)}); + loads(2, guest + 0x700, 0x1fc, {dword(overlap, 0x1fc)}); + loads(2, limit - 0x10, 0xc, {dword(top, 0xc)}); + misses(2, guest + 0x100, 0xd); + misses(3, guest + 0x100, 4); + misses(2, guest + 0x200, 0); + misses(1, guest + 2, 0); + misses(0, guest + 3, 2); + misses(2, guest + 0x400, 0x300); + misses(2, guest + 0x400, 0x2fd); + misses(2, limit - 0x10, 0xd); + misses(2, highest, 0); + misses(2, guest - 1, 1); const auto wide = check(3, guest + 0x100, 0); Require(wide.fault.state == Abi::FaultState::Empty && wide.values[0] == 0xa3a2a1a0u && wide.values[1] == 0xa7a6a5a4u && wide.values[2] == 0xabaaa9a8u && wide.values[3] == 0xafaeadacu, "an anchored span read returned wrong data"); const auto some = check(4, guest + 0x100, 0); @@ -329,12 +364,18 @@ static void RunBdaAnchorTests(const Context& context) { const auto unmapped = check(2, guest + 0x100, 0x10); Require(unmapped.fault.state == Abi::FaultState::Ready && unmapped.fault.reason == Abi::FaultReason::Unmapped && unmapped.fault.address == guest + 0x110 && unmapped.fault.instruction == 0x1234 && unmapped.values[0] == 0xa5a5a5a5u, "an anchored read past its range did not fault"); permissions = 0; + misses(2, guest + 0x100, 0); compare(*stopping, {guest + 0x100}, {0u, 4u}); compare(*elected, {guest + 0x100}, lows); permissions = Abi::Read; count = 3; + misses(2, guest + 0x100, 0); + misses(0, guest, 0); compare(*stopping, {guest + 0x100, guest}, {0u, 4u}); compare(*elected, {guest + 0x100, guest}, lows); + ranges = {{0, limit, large.DeviceAddress(), Abi::Read, 0}}; + count = 1; + compare(*stopping, {limit - 1}, {0xffffffffu}); } void RunBdaExecutionTests(const Context& context) { diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaShader.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaShader.cpp index a24dd6a955..77707e646e 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaShader.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaShader.cpp @@ -204,6 +204,10 @@ std::vector MakeBdaAnchoredReadTestShader(const BdaAnchoredReadPr } EmitIfCondition(state, Binary(state, spv::OpIEqual, TypeBool(state), selector, ConstantU32(state, index)), [&] { const auto lookup = [&] { + if (reads.sentinel) { + const auto sentinel = ConstantU32(state, BdaAnchoredReadSentinel); + return std::array{sentinel, sentinel, sentinel, sentinel}; + } const auto address = AddBdaAddress(ctx, instruction, base, Unary(state, spv::OpUConvert, u64, low), false); if (read.bits == 32u) return EmitBdaDwordReads(ctx, instruction, address, static_cast(read.immediate), read.dwords); return std::array{EmitBdaRead(ctx, instruction, AddBdaImmediate(ctx, instruction, address, read.immediate), read.bits)}; diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaShader.hpp b/core/libs/prx/libSceAgcDriver/tests/BdaShader.hpp index ec04f67aba..555397c843 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaShader.hpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaShader.hpp @@ -24,7 +24,9 @@ struct BdaAnchoredReadProgram { bool elect = false; bool coherent = false; std::uint32_t lanes = 1; + bool sentinel = false; }; +inline constexpr std::uint32_t BdaAnchoredReadSentinel = 0xdeadbeefu; std::vector MakeBdaAnchoredReadTestShader(const BdaAnchoredReadProgram& reads, bool anchored); void RunBdaStoreExecutionTests(const AgcDriver::Graphics::Context& context); void RunBdaExecutionTests(const AgcDriver::Graphics::Context& context); diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 9cded9c715..53f3512c90 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -3703,7 +3703,7 @@ void verifyBdaAnchors() { }; for (const std::uint32_t stride : {4u, 32u}) { for (const std::uint32_t loads : {0u, 1u, 2u, 4u}) { - const bool anchored = loads >= 2u; + const bool anchored = stride == 32u && loads >= 2u; const auto words = compile(loads, stride); std::map names; std::string function; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/BdaAnchorReads.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/BdaAnchorReads.cpp new file mode 100644 index 0000000000..9bd219d267 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/BdaAnchorReads.cpp @@ -0,0 +1,164 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 64; +constexpr std::size_t BlockBytes = 65536; +constexpr std::uint32_t InputDwords = 1024; +constexpr std::uint32_t Fill = 0xcdcdcdcdu; +constexpr std::uint32_t OutputBase = 0x8000; +constexpr std::uint32_t OutputDwords = 8; + +alignas(256) constexpr std::array Code{ + 0x34040082, 0x34060085, 0x4a0606ff, OutputBase, + 0xdc308010, 0x04000002, 0xdc308014, 0x05000002, 0xdc308018, 0x06000002, + 0xdc308403, 0x07000002, + 0xf4000100, 0xfa000203, 0xf4000140, 0xfa0002f6, + 0xbf8c3f70, 0xbf8cc07f, 0x7e100204, 0x7e120205, + 0xdc708000, 0x00000403, 0xdc708004, 0x00000503, 0xdc708008, 0x00000603, 0xdc70800c, 0x00000703, + 0xdc708010, 0x00000803, 0xdc708014, 0x00000903, 0xbf810000}; + +class GuestBlock { +public: + GuestBlock() { +#ifdef _WIN32 + block = static_cast(VirtualAlloc(nullptr, BlockBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); +#else + block = static_cast(std::aligned_alloc(BlockBytes, BlockBytes)); +#endif + Require(block != nullptr, "BDA anchor reads: cannot allocate the guest block"); + GuestAllocations::Mutation().Add(block, BlockBytes, true, true, true); + } + + ~GuestBlock() { + GuestAllocations::Mutation().Remove(block); +#ifdef _WIN32 + VirtualFree(block, 0, MEM_RELEASE); +#else + std::free(block); +#endif + } + + GuestBlock(const GuestBlock&) = delete; + GuestBlock& operator=(const GuestBlock&) = delete; + + std::uint8_t* Data() { return block; } + +private: + std::uint8_t* block = nullptr; +}; + +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + +std::vector Initial() { + std::vector memory(BlockBytes / 4u, Fill); + for (std::uint32_t dword = 0; dword < InputDwords; ++dword) memory[dword] = 0x61000000u + dword * 0x00010307u; + return memory; +} + +std::uint32_t Bytes(const std::vector& memory, std::uint32_t address) { + std::uint32_t value = 0; + for (std::uint32_t byte = 0; byte < 4u; ++byte) { + const std::uint32_t at = address + byte; + value |= ((memory[at / 4u] >> (at % 4u * 8u)) & 0xffu) << (byte * 8u); + } + return value; +} + +std::vector Expected() { + const auto initial = Initial(); + auto memory = initial; + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + auto* out = &memory[OutputBase / 4u + tid * OutputDwords]; + out[0] = Bytes(initial, tid * 4u + 0x10u); + out[1] = Bytes(initial, tid * 4u + 0x14u); + out[2] = Bytes(initial, tid * 4u + 0x18u); + out[3] = Bytes(initial, tid * 4u + 0x403u); + out[4] = Bytes(initial, 0x200u); + out[5] = Bytes(initial, 0x2f4u); + } + return memory; +} + +ShaderRecompiler::RecompileResult Recompile(std::uint32_t waveSize, std::span userData, std::span guest, const ShaderRecompiler::SpirvTarget& target) { + const std::span code(Code); + const std::array regions{{{reinterpret_cast(code.data()), std::as_bytes(code)}, {reinterpret_cast(guest.data()), guest}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {waveSize, 0, userData, compute, std::nullopt, std::nullopt, regions}, + target, + {0, 0, 0, 128} + }; + request.useCache = false; + return ShaderRecompiler::Recompile(request); +} + +bool SearchesAnchor(const ShaderRecompiler::SharedSpirv& spirv) { + const std::string_view text(reinterpret_cast(spirv.data()), spirv.size() * sizeof(std::uint32_t)); + return text.find("bda_anchor_range") != std::string_view::npos; +} + +void Run(AgcDriver::VulkanDevice& device, GuestBlock& guest, std::uint32_t waveSize, const ShaderRecompiler::SpirvTarget& target, const std::string& name) { + auto memory = Initial(); + std::memcpy(guest.Data(), memory.data(), BlockBytes); + const auto address = static_cast(reinterpret_cast(guest.Data())); + const std::vector userData{static_cast(address), static_cast(address >> 32u)}; + const auto result = Recompile(waveSize, userData, std::span(reinterpret_cast(guest.Data()), InputDwords * sizeof(std::uint32_t)), target); + Require(SearchesAnchor(result.spirv), "BDA anchor reads: " + name + " reads of one user-data base do not share an anchor"); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(Code.data())); + device.WaitIdle(); + std::memcpy(memory.data(), guest.Data(), BlockBytes); + const auto expected = Expected(); + for (std::size_t dword = 0; dword < memory.size(); ++dword) { + Require(memory[dword] == expected[dword], "BDA anchor reads: " + name + " byte " + std::to_string(dword * 4u) + " is " + Hex(memory[dword]) + ", expected " + Hex(expected[dword])); + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + GuestBlock guest; + const auto native = device->Target(); + const auto split = device->ComputeTarget(32); + Run(*device, guest, 32, native, "wave32"); + Run(*device, guest, 64, native, "wave64"); + if (split.subgroupSize != native.subgroupSize) Run(*device, guest, 64, split, "wave64 split"); + std::puts("BDA anchor read tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/shader/recompiler/BdaAbi.hpp b/core/shader/recompiler/BdaAbi.hpp index bdb3049862..48d8d2bce1 100644 --- a/core/shader/recompiler/BdaAbi.hpp +++ b/core/shader/recompiler/BdaAbi.hpp @@ -28,9 +28,8 @@ struct Range { }; inline bool RangesOrdered(const Range* ranges, std::size_t count) { - for (std::size_t index = 0; index < count; ++index) { - if (ranges[index].end < ranges[index].begin) return false; - if (index != 0 && ranges[index].begin < ranges[index - 1].end) return false; + for (std::size_t index = 1; index < count; ++index) { + if (ranges[index].begin < ranges[index - 1].begin) return false; } return true; } diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvBda.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvBda.hpp index 67b486dd52..1d280f1017 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvBda.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvBda.hpp @@ -38,6 +38,9 @@ std::uint32_t EmitBdaAtomic(SpirvValueEmitContext& ctx, const IrValue& inst, std std::array EmitBdaDwordReads(SpirvValueEmitContext& ctx, const IrValue& inst, std::uint32_t address, std::uint32_t offset, std::uint32_t dwords, bool everyDword = false, std::uint32_t usedDwords = 0u); bool BdaByteReadsForced(); std::uint32_t AddBdaAddress(SpirvValueEmitContext& ctx, const IrValue& inst, std::uint32_t address, std::uint32_t offset, bool subtract); +bool RoutesApertures(const SpirvEmitterState& state, const MemoryInfo& mem); +std::uint32_t UsedBdaDwords(const IrValue& inst, std::uint32_t dwords); +std::vector BdaReadSites(const SpirvEmitterState& state); void CollectBdaAnchors(SpirvEmitterState& state); void DefineBdaAnchorFunction(SpirvEmitterState& state); void EmitBdaAnchors(SpirvEmitterState& state); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvBdaLookup.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvBdaLookup.cpp index 55172995b1..c87aa246a2 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvBdaLookup.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvBdaLookup.cpp @@ -128,8 +128,11 @@ std::uint32_t DefineBdaLookup(SpirvEmitterState& state, const char* name, bool r {binary(spv::OpULessThanEqual, boolean, deviceEnd, result), BdaAbi::FaultReason::Overflow}, }); if (rangeResult) { + const auto last = binary(spv::OpUGreaterThanEqual, boolean, index, count); + const auto next = BdaLoadAddress(state, binary(spv::OpIAdd, u32, constant(4), binary(spv::OpIMul, u32, Select(state, u32, last, binary(spv::OpISub, u32, index, constant(1)), index), constant(8)))); + const auto bound = Select(state, u64, binary(spv::OpLogicalOr, boolean, last, binary(spv::OpULessThan, boolean, finish, next)), finish, next); const auto range = state.module.AllocateId(); - state.module.AddFunction(spv::OpCompositeConstruct, rangeType, range, begin, finish, base); + state.module.AddFunction(spv::OpCompositeConstruct, rangeType, range, begin, bound, base); state.module.AddFunction(spv::OpReturnValue, range); state.module.AddFunction(spv::OpFunctionEnd); return function; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp index 73a196bfc6..2237fa9714 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp @@ -105,23 +105,6 @@ std::uint32_t EmitBdaBytes(SpirvEmitterState& state, std::uint32_t address, std: return EmitBdaByteLoop(state, address, ConstantU32(state, bytes), instruction, accessMask); } -// The dwords of a wide load that the program extracts (all of them when the composite is used -// whole). Dead-code elimination has dropped the unused extracts, as it dropped unused dword loads -// when every dword was its own instruction. -std::uint32_t UsedBdaDwords(const IrValue& inst, std::uint32_t dwords) { - const auto all = (1u << dwords) - 1u; - if (dwords == 1u) return all; - std::uint32_t used = 0; - for (const IrValue* user : inst.Uses()) { - const auto opcode = user->Opcode(); - if (opcode != IrOpcode::CompositeExtractU32x2 && opcode != IrOpcode::CompositeExtractU32x3 && opcode != IrOpcode::CompositeExtractU32x4) return all; - const IrValue* index = user->ArgumentCount() > 1u ? user->Argument(1) : nullptr; - if (index == nullptr || !index->HasImmediate() || index->ImmediateU32() >= dwords) return all; - used |= 1u << index->ImmediateU32(); - } - return used; -} - void EmitBdaStoreAt(SpirvEmitterState& state, std::uint32_t address, std::uint32_t bytes, std::uint32_t pointerType, std::uint32_t value, std::uint32_t instruction, std::uint32_t accessMask) { const auto physical = state.module.AllocateId(); state.module.AddFunction(spv::OpFunctionCall, TypeScalarU64(state), physical, state.bdaWritePointerFunction, address, ConstantU32(state, bytes), instruction); @@ -379,6 +362,24 @@ bool BdaByteReadsForced() { return forced; } +std::uint32_t UsedBdaDwords(const IrValue& inst, std::uint32_t dwords) { + const auto all = (1u << dwords) - 1u; + if (dwords == 1u) return all; + std::uint32_t used = 0; + for (const IrValue* user : inst.Uses()) { + const auto opcode = user->Opcode(); + if (opcode != IrOpcode::CompositeExtractU32x2 && opcode != IrOpcode::CompositeExtractU32x3 && opcode != IrOpcode::CompositeExtractU32x4) return all; + const IrValue* index = user->ArgumentCount() > 1u ? user->Argument(1) : nullptr; + if (index == nullptr || !index->HasImmediate() || index->ImmediateU32() >= dwords) return all; + used |= 1u << index->ImmediateU32(); + } + return used; +} + +bool RoutesApertures(const SpirvEmitterState& state, const MemoryInfo& mem) { + return mem.kind == ResourceKind::Flat && state.program.Resources().stage == IrShaderStage::Compute; +} + std::uint32_t BdaInstructionPc(SpirvEmitterState& state, const IrValue& inst) { return state.bdaPcOverride != 0u ? state.bdaPcOverride : ConstantU32(state, inst.Flags().pc); } @@ -425,15 +426,16 @@ namespace { constexpr std::size_t MaxBdaAnchors = 4; constexpr std::uint64_t BdaAnchorAddressLimit = ~std::uint64_t{0} << 32u; +constexpr std::uint64_t BdaAnchorSpanLimit = std::uint64_t{1} << 62u; -std::optional> UserDataBase(const IrProgram& program, const IrValue& inst) { +std::optional> UserDataBase(const SpirvEmitterState& state, const IrValue& inst) { if (AddressOpcodeInfoOf(inst.Opcode()).access != AddressAccess::Read || inst.ArgumentCount() < 2u) return std::nullopt; const auto index = inst.Flags().index; - const auto& memory = program.Resources().memoryInfo; + const auto& memory = state.program.Resources().memoryInfo; if (index >= memory.size()) return std::nullopt; const auto& info = memory[index]; if (info.kind != ResourceKind::ScalarAddress && info.kind != ResourceKind::Flat && info.kind != ResourceKind::Global) return std::nullopt; - if (info.planningOnly || info.addressIsFull || (info.kind == ResourceKind::Flat && program.Resources().stage == IrShaderStage::Compute)) return std::nullopt; + if (info.planningOnly || info.addressIsFull || RoutesApertures(state, info)) return std::nullopt; const IrValue* handle = inst.Argument(0) != nullptr ? inst.Argument(0)->Resolve() : nullptr; if (handle == nullptr || handle->Opcode() != IrOpcode::GetAddressResource || handle->ArgumentCount() != 2u) return std::nullopt; const auto userData = [](const IrValue* value) -> std::optional { @@ -495,14 +497,12 @@ void CollectBdaAnchors(SpirvEmitterState& state) { state.bdaAnchors.clear(); if (!state.program.Info().usesDma || state.bdaProbeFunction == 0) return; std::vector, std::size_t>> bases; - for (const IrBlock* block : state.program.BlockOrder()) { - for (const IrValue* inst : block->Instructions()) { - const auto base = UserDataBase(state.program, *inst); - if (!base) continue; - const auto found = std::find_if(bases.begin(), bases.end(), [&](const auto& entry) { return entry.first == *base; }); - if (found != bases.end()) ++found->second; - else bases.push_back({*base, 1u}); - } + for (const IrValue* inst : BdaReadSites(state)) { + const auto base = UserDataBase(state, *inst); + if (!base) continue; + const auto found = std::find_if(bases.begin(), bases.end(), [&](const auto& entry) { return entry.first == *base; }); + if (found != bases.end()) ++found->second; + else bases.push_back({*base, 1u}); } std::stable_sort(bases.begin(), bases.end(), [](const auto& left, const auto& right) { return left.second > right.second; }); for (const auto& [base, reads] : bases) { @@ -536,6 +536,7 @@ void EmitBdaAnchorRange(SpirvEmitterState& state, SpirvEmitterState::BdaAnchor& const auto deviceEnd = Binary(state, spv::OpIAdd, u64, anchor.device, span); auto usable = Binary(state, spv::OpLogicalAnd, boolean, Binary(state, spv::OpINotEqual, boolean, anchor.device, BdaConstant(state, 0u)), Binary(state, spv::OpUGreaterThan, boolean, deviceEnd, anchor.device)); usable = Binary(state, spv::OpLogicalAnd, boolean, usable, Binary(state, spv::OpULessThanEqual, boolean, end, BdaConstant(state, BdaAnchorAddressLimit))); + usable = Binary(state, spv::OpLogicalAnd, boolean, usable, Binary(state, spv::OpULessThanEqual, boolean, span, BdaConstant(state, BdaAnchorSpanLimit))); anchor.size = Select(state, u64, usable, span, BdaConstant(state, 0u)); } @@ -550,10 +551,11 @@ void EmitBdaAnchors(SpirvEmitterState& state) { const SpirvEmitterState::BdaAnchor* FindBdaAnchor(const SpirvEmitterState& state, const IrValue& inst) { if (state.bdaAnchors.empty()) return nullptr; - const auto base = UserDataBase(state.program, inst); + const auto base = UserDataBase(state, inst); if (!base) return nullptr; for (const auto& anchor : state.bdaAnchors) { - if (anchor.lowRegister == base->first && anchor.highRegister == base->second && anchor.size != 0u) return &anchor; + const bool emitted = anchor.size != 0u; + if (emitted && anchor.lowRegister == base->first && anchor.highRegister == base->second) return &anchor; } return nullptr; } diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp index e8c470ddaa..6e2593fd1e 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include namespace ShaderRecompiler { @@ -247,10 +248,10 @@ bool WritesMemory(IrOpcode opcode) { image == ImageAccess::Write || image == ImageAccess::Atomic || opcode == IrOpcode::Barrier || opcode == IrOpcode::StoreVectorWindow; } -std::vector> AdjacentBdaLoads(SpirvValueEmitContext& ctx, const IrValue& inst, const MemoryInfo& mem) { +std::vector> AdjacentBdaLoads(const SpirvEmitterState& state, const IrValue& inst, const MemoryInfo& mem) { std::vector> members; const IrBlock* block = inst.Parent(); - if (block == nullptr || ctx.state.laneCount != 1u || (mem.offset & 3u) != 0u) return members; + if (block == nullptr || state.laneCount != 1u || (mem.offset & 3u) != 0u) return members; const auto& instructions = block->Instructions(); auto next = std::find(instructions.begin(), instructions.end(), &inst); if (next == instructions.end()) return members; @@ -261,7 +262,7 @@ std::vector> AdjacentBdaLoads(SpirvValu if (WritesMemory(other->Opcode())) break; continue; } - const auto& otherMemory = ctx.Memory(*other); + const auto& otherMemory = state.program.Resources().memoryInfo.at(other->Flags().index); if (otherMemory.planningOnly || otherMemory.kind != mem.kind || otherMemory.addressIsFull != mem.addressIsFull || otherMemory.coherent != mem.coherent || other->ArgumentCount() != inst.ArgumentCount()) continue; bool sameArguments = true; for (std::size_t index = 0; index < inst.ArgumentCount(); ++index) { @@ -285,7 +286,7 @@ std::uint32_t LoadBda(SpirvValueEmitContext& ctx, const IrValue& inst, const Mem state.module.AddFunction(spv::OpCompositeExtract, TypeU32(state), value, grouped->second.first, grouped->second.second); return value; } - const auto members = AdjacentBdaLoads(ctx, inst, mem); + const auto members = AdjacentBdaLoads(state, inst, mem); if (!members.empty()) { std::uint32_t used = 1u; for (const auto& member : members) used |= 1u << member.second; @@ -438,10 +439,34 @@ const MemoryInfo& SharedMemory(SpirvValueEmitContext& ctx, const IrValue& inst) return mem; } -bool RoutesApertures(const SpirvEmitterState& state, const MemoryInfo& mem) { - return mem.kind == ResourceKind::Flat && state.program.Resources().stage == IrShaderStage::Compute; + } +std::vector BdaReadSites(const SpirvEmitterState& state) { + std::vector sites; + std::unordered_set grouped; + const auto& memory = state.program.Resources().memoryInfo; + for (const IrBlock* block : state.program.BlockOrder()) { + for (const IrValue* inst : block->Instructions()) { + const auto info = AddressOpcodeInfoOf(inst->Opcode()); + const auto index = inst->Flags().index; + if (info.access != AddressAccess::Read || index >= memory.size() || grouped.contains(inst)) continue; + const auto& mem = memory[index]; + if (mem.planningOnly || RoutesApertures(state, mem)) continue; + if (mem.kind != ResourceKind::ScalarAddress && mem.kind != ResourceKind::Flat && mem.kind != ResourceKind::Global) continue; + if (info.dataBits == 32u && info.components == 1u) { + for (const auto& member : AdjacentBdaLoads(state, *inst, mem)) grouped.insert(member.first); + } else if (info.components > 1u && UsedBdaDwords(*inst, info.components) == 0u) { + continue; + } + sites.push_back(inst); + } + } + return sites; +} + +namespace { + struct ApertureAddress { std::uint32_t low = 0; std::uint32_t shared = 0; From 48aaec50f8dfbc9deb68f9dd2fceef7c79755d23 Mon Sep 17 00:00:00 2001 From: lorkydey <57268822+Lorkydey@users.noreply.github.com> Date: Wed, 14 Oct 2026 16:15:21 +0200 Subject: [PATCH 0065/1000] fix(relinker): propagate POSIX autorun failures --- core/relinker/cli/src/Autorun.cpp | 9 +++++--- core/relinker/cli/tests/AutorunExitTests.cpp | 24 ++++++++++++++++++++ 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/core/relinker/cli/src/Autorun.cpp b/core/relinker/cli/src/Autorun.cpp index 7030a03da8..aa54150b47 100644 --- a/core/relinker/cli/src/Autorun.cpp +++ b/core/relinker/cli/src/Autorun.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #ifdef _WIN32 #include @@ -59,10 +60,12 @@ int Autorun(const std::string& absPath, bool toWindows) { cmd += '\''; rawCode = std::system(cmd.c_str()); - if (rawCode != -1) { - const int signal = rawCode & 0x7F; - exitCode = signal ? 128 + signal : (rawCode >> 8) & 0xFF; + if (rawCode == -1) { + const int runError = errno; + throw Domain::RelinkerException("Cannot run or wait for the output executable: " + absPath + " (error " + std::to_string(runError) + ")"); } + const int signal = rawCode & 0x7F; + exitCode = signal ? 128 + signal : (rawCode >> 8) & 0xFF; std::cout << "\nRaw exit code: " << rawCode << "; Unpacked: " << exitCode << '\n'; #endif diff --git a/core/relinker/cli/tests/AutorunExitTests.cpp b/core/relinker/cli/tests/AutorunExitTests.cpp index 4890de71de..c4883221a5 100644 --- a/core/relinker/cli/tests/AutorunExitTests.cpp +++ b/core/relinker/cli/tests/AutorunExitTests.cpp @@ -1,4 +1,6 @@ #include +#include +#include #include #include #include @@ -42,6 +44,28 @@ int main(int, char* argv[]) { } } #ifndef _WIN32 + const auto savedHandler = std::signal(SIGCHLD, SIG_IGN); + if (savedHandler == SIG_ERR) throw std::runtime_error("Cannot ignore SIGCHLD for the autorun failure test"); + try { + for (const bool windows : {false, true}) { + bool rejected = false; + try { + Cli::Autorun(child.string(), windows); + } catch (const Domain::RelinkerException& error) { + const std::string message = error.what(); + if (message.find(child.string()) == std::string::npos || + message.find("(error " + std::to_string(ECHILD) + ")") == std::string::npos) + throw std::runtime_error("Autorun lost the failed wait's path or ECHILD error: " + message); + rejected = true; + } + if (!rejected) throw std::runtime_error("Autorun reported success after losing the child exit status"); + } + } catch (...) { + std::signal(SIGCHLD, savedHandler); + throw; + } + if (std::signal(SIGCHLD, savedHandler) == SIG_ERR) + throw std::runtime_error("Cannot restore SIGCHLD after the autorun failure test"); std::filesystem::rename(child, signaled); const int code = Cli::Autorun(signaled.string(), false); if (code != 128 + SIGILL) throw std::runtime_error("Autorun lost the child signal"); From ed1ee702c962f96727634339f6b9f2360fe4ae04 Mon Sep 17 00:00:00 2001 From: Nithishvelan M Date: Wed, 14 Oct 2026 19:49:23 +0530 Subject: [PATCH 0066/1000] test(libSceCdlgG2P): cover uninitialized close and initial status in guest test --- core/libs/tests/GuestG2PDialog.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/core/libs/tests/GuestG2PDialog.cpp b/core/libs/tests/GuestG2PDialog.cpp index d1666fd28d..e896a87e42 100644 --- a/core/libs/tests/GuestG2PDialog.cpp +++ b/core/libs/tests/GuestG2PDialog.cpp @@ -16,6 +16,8 @@ namespace { void Require(bool value) { if (!value) std::abort(); } +constexpr int kStatusNone = 0; +constexpr int kStatusInitialized = 1; constexpr int kStatusFinished = 3; constexpr int kErrNotInitialized = static_cast(0x80B80003); constexpr int kErrAlreadyInitialized = static_cast(0x80B80004); @@ -31,10 +33,15 @@ int main() { Require(sceG2PDialogTerminate() == kErrNotInitialized); Require(sceG2PDialogOpen(¶m) == kErrNotInitialized); + Require(sceG2PDialogClose() == kErrNotInitialized); Require(sceG2PDialogGetResult(result) == kErrNotInitialized); + Require(sceG2PDialogGetStatus() == kStatusNone); + Require(sceG2PDialogUpdateStatus() == kStatusNone); Require(sceG2PDialogInitialize() == 0); Require(sceG2PDialogInitialize() == kErrAlreadyInitialized); + Require(sceG2PDialogGetStatus() == kStatusInitialized); + Require(sceG2PDialogUpdateStatus() == kStatusInitialized); Require(sceG2PDialogOpen(nullptr) == kErrArgNull); Require(sceG2PDialogGetResult(result) == kErrNotFinished); @@ -51,4 +58,5 @@ int main() { Require(sceG2PDialogClose() == 0); Require(sceG2PDialogTerminate() == 0); Require(sceG2PDialogTerminate() == kErrNotInitialized); + Require(sceG2PDialogGetStatus() == kStatusNone); } From 7b06e7594084ed55b218cc256056fddba793353a Mon Sep 17 00:00:00 2001 From: Sam Date: Wed, 14 Oct 2026 03:13:24 +0100 Subject: [PATCH 0067/1000] fix(agc): promote a held import window to its whole allocation A 64 MiB import window held by the active recorder blocked the containing import of the same registered allocation, so an address-based build copied the whole range instead. Demon's Souls then stopped after two frames: "an address-based build copies 358 MiB of 1 registered ranges, past 64 MiB". Allow the containing import when the recorder is active; the retired window stays alive through the hold (KeepThroughHold). Cover promotion, a sync inside the hold and release after the hold's last batch. --- .../Graphics/src/GuestBufferMemory.cpp | 8 +++-- .../tests/HostImportRetireHold.cpp | 36 +++++++++++++++++++ 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index 578128f2fe..823b632022 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -939,15 +939,17 @@ const HostImport* importRunAt(const Context& context, HostImports& state, Import if (sameRun(failed->second, run)) return nullptr; failures.erase(failed); } - // The imports this one replaces overlap it; none may be held by a build, and the import must fit the budget (with - // what may be evicted) before any of them is retired. auto first = state.imports.lower_bound(base); if (first != state.imports.begin() && std::prev(first)->second.base + std::prev(first)->second.bytes > base) --first; std::uint64_t replaced = 0; // A join may replace imports this build already looked up: it runs before any region confirms an import // (UploadFinish's pre-pass re-resolves every pointer after it), and a retired import stays alive through the // hold (KeepThroughHold). Refusing it there would keep the run copied for good. - const auto held = [&](const HostImport& entry) { return !joined && pointerHeld(state, entry); }; + const auto held = [&](const HostImport& entry) { + if (joined || !pointerHeld(state, entry)) return false; + const bool expandedWindow = run.ranges.size() == 1 && entry.ranges.size() == 1 && entry.range == run.ranges.front() && base <= entry.base && base + bytes >= entry.base + entry.bytes && bytes > entry.bytes; + return !expandedWindow || Recorder::Active() == nullptr; + }; for (auto it = first; it != state.imports.end() && it->first < base + bytes; ++it) { if (held(it->second)) return nullptr; replaced += it->second.bytes; diff --git a/core/libs/prx/libSceAgcDriver/tests/HostImportRetireHold.cpp b/core/libs/prx/libSceAgcDriver/tests/HostImportRetireHold.cpp index d1ce27fe58..92c6fe639a 100644 --- a/core/libs/prx/libSceAgcDriver/tests/HostImportRetireHold.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/HostImportRetireHold.cpp @@ -247,6 +247,41 @@ struct ActiveRecorder { } }; +void WindowPromotionOutlivesTheHold() { + struct RestoreWindow { + ~RestoreWindow() { SetHostImportRangeWindow(std::uint64_t{64} << 20u); } + } restore; + SetHostImportRangeWindow(Alignment); + Heap heap; + ActiveRecorder active; + auto& recorder = *active.recorder; + const auto context = mockContext(); + VkBuffer retired = VK_NULL_HANDLE; + { + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + const auto* taken = HostImportFor(context, heap.Address(), 4096); + Expect(taken != nullptr && taken->bytes == Alignment, "the first access did not import one window"); + if (taken == nullptr) return; + retired = taken->buffer; + const auto* whole = HostImportFor(context, heap.Address(), RangeBytes); + Expect(whole != nullptr && whole->base == heap.Address() && whole->bytes == RangeBytes && whole->buffer != retired, "a held import window could not be promoted to its whole registered range"); + if (whole == nullptr) return; + Expect(Live(retired) && taken->buffer == retired && taken->bytes == Alignment, "promotion invalidated the held window or its entry"); + recorder.Commands(); + recorder.Submit(); + recorder.Sync(); + Expect(Live(retired), "a sync inside the promoting hold destroyed its old window"); + recorder.Commands(); + recorder.Submit(); + } + { + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + Expect(Live(retired), "the old window died while its hold's later batch was in flight"); + recorder.Sync(); + } + Expect(!Live(retired), "the old window survived every batch of the promoting hold"); +} + void IdleRetireOutlivesTheHold() { Heap heap; ActiveRecorder active; @@ -334,6 +369,7 @@ int main() { setenv("APS5_WRITE_WATCH_IMPORTS", "unwatch", 1); #endif try { + WindowPromotionOutlivesTheHold(); IdleRetireOutlivesTheHold(); BusyRetireOutlivesALaterBatch(); } catch (const std::exception& error) { From 048834560392c36034950f0a9db0563cca98f941 Mon Sep 17 00:00:00 2001 From: Nithishvelan M Date: Wed, 14 Oct 2026 19:51:39 +0530 Subject: [PATCH 0068/1000] test(libSceVrSetupDialog): cover uninitialized close and initial status in guest test --- core/libs/tests/GuestVrSetupDialog.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/core/libs/tests/GuestVrSetupDialog.cpp b/core/libs/tests/GuestVrSetupDialog.cpp index a3afc479f3..e747df363a 100644 --- a/core/libs/tests/GuestVrSetupDialog.cpp +++ b/core/libs/tests/GuestVrSetupDialog.cpp @@ -15,6 +15,8 @@ namespace { void Require(bool value) { if (!value) std::abort(); } +constexpr int kStatusNone = 0; +constexpr int kStatusInitialized = 1; constexpr int kStatusFinished = 3; constexpr int kErrNotInitialized = static_cast(0x80B80003); constexpr int kErrAlreadyInitialized = static_cast(0x80B80004); @@ -30,10 +32,13 @@ int main() { Require(sceVrSetupDialogTerminate() == kErrNotInitialized); Require(sceVrSetupDialogOpen(¶m) == kErrNotInitialized); + Require(sceVrSetupDialogClose() == kErrNotInitialized); Require(sceVrSetupDialogGetResult(result) == kErrNotInitialized); + Require(sceVrSetupDialogUpdateStatus() == kStatusNone); Require(sceVrSetupDialogInitialize() == 0); Require(sceVrSetupDialogInitialize() == kErrAlreadyInitialized); + Require(sceVrSetupDialogUpdateStatus() == kStatusInitialized); Require(sceVrSetupDialogOpen(nullptr) == kErrArgNull); Require(sceVrSetupDialogGetResult(result) == kErrNotFinished); @@ -49,4 +54,5 @@ int main() { Require(sceVrSetupDialogClose() == 0); Require(sceVrSetupDialogTerminate() == 0); Require(sceVrSetupDialogTerminate() == kErrNotInitialized); + Require(sceVrSetupDialogUpdateStatus() == kStatusNone); } From 8b5595c7668ed94a393078d0368abcf5274d3e6b Mon Sep 17 00:00:00 2001 From: Nithishvelan M Date: Wed, 14 Oct 2026 19:53:53 +0530 Subject: [PATCH 0069/1000] fix(libScePlayGoDialog): drop unused header and cover uninitialized close in guest test --- core/libs/prx/libScePlayGoDialog/Export.cpp | 1 - core/libs/tests/GuestPlayGoDialog.cpp | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libScePlayGoDialog/Export.cpp b/core/libs/prx/libScePlayGoDialog/Export.cpp index 16615b02ee..09a8df031c 100644 --- a/core/libs/prx/libScePlayGoDialog/Export.cpp +++ b/core/libs/prx/libScePlayGoDialog/Export.cpp @@ -2,7 +2,6 @@ #include #include #include -#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" diff --git a/core/libs/tests/GuestPlayGoDialog.cpp b/core/libs/tests/GuestPlayGoDialog.cpp index 40a6a6710a..f66e459c7c 100644 --- a/core/libs/tests/GuestPlayGoDialog.cpp +++ b/core/libs/tests/GuestPlayGoDialog.cpp @@ -24,6 +24,7 @@ int main() { Require(scePlayGoDialogGetStatus() == 0); Require(scePlayGoDialogTerminate() == notInitialized); + Require(scePlayGoDialogClose() == notInitialized); Require(scePlayGoDialogOpen(nullptr) == paramInvalid); Require(scePlayGoDialogGetResult(nullptr) == notInitialized); From 6509ec274a9c7817ee927735a6da37c13879da7b Mon Sep 17 00:00:00 2001 From: Nawasfadili Date: Wed, 14 Oct 2026 10:50:56 +0000 Subject: [PATCH 0070/1000] feat(libc): implement wide and unlocked stream I/O --- core/libs/prx/libc/src/FormattingWide.cpp | 32 +++++++++ core/libs/prx/libc/src/StandardStreams.cpp | 31 +++++++- core/libs/tests/GuestStandardStreams.cpp | 82 ++++++++++++++++++++++ docs/dev/debt/libc.md | 3 +- 4 files changed, 146 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libc/src/FormattingWide.cpp b/core/libs/prx/libc/src/FormattingWide.cpp index e9a5661781..713a520591 100644 --- a/core/libs/prx/libc/src/FormattingWide.cpp +++ b/core/libs/prx/libc/src/FormattingWide.cpp @@ -215,6 +215,9 @@ std::u16string FormatWide(const char16_t* format, VaList* source, bool secure = } // namespace extern "C" { +extern FileStream* __stdoutp_nid_postfix; +std::int32_t APS5_VABI fgetwc_nid_postfix(FileStream* stream); +int APS5_VABI ferror_nid_postfix(FileStream* stream); int APS5_VABI vswprintf_nid_postfix(char16_t* buffer, std::size_t size, const char16_t* format, VaList* args) { if (buffer == nullptr || size == 0) { errno = 22; return -1; } @@ -359,4 +362,33 @@ int APS5_VABI fputws_nid_postfix(const char16_t* str, FileStream* stream) { } } +int APS5_VABI putwc_nid_postfix(char16_t value, FileStream* stream) { + return fputwc_nid_postfix(value, stream); +} + +int APS5_VABI putwchar_nid_postfix(char16_t value) { + return fputwc_nid_postfix(value, __stdoutp_nid_postfix); +} + +char16_t* APS5_VABI fgetws_nid_postfix(char16_t* buffer, int size, FileStream* stream) { + if (buffer == nullptr || stream == nullptr || size <= 0) { + if (stream != nullptr) stream->SetError(); + errno = 22; + return nullptr; + } + int count = 0; + while (count < size - 1) { + const std::int32_t character = fgetwc_nid_postfix(stream); + if (character == -1) { + if (count == 0) return nullptr; + break; + } + buffer[count++] = static_cast(character); + if (character == static_cast(u'\n')) break; + } + if (ferror_nid_postfix(stream) != 0) return nullptr; + buffer[count] = 0; + return buffer; +} + } diff --git a/core/libs/prx/libc/src/StandardStreams.cpp b/core/libs/prx/libc/src/StandardStreams.cpp index 69e24de20c..dfdd3b5613 100644 --- a/core/libs/prx/libc/src/StandardStreams.cpp +++ b/core/libs/prx/libc/src/StandardStreams.cpp @@ -20,9 +20,34 @@ int APS5_VABI fgetc_nid_postfix(FileStream* stream) { return result; } std::int32_t APS5_VABI fgetwc_nid_postfix(FileStream* stream) { - return fgetc_nid_postfix(stream); + if (stream == nullptr) { + errno = 22; + return -1; + } + const int first = fgetc_nid_postfix(stream); + if (first == -1) return -1; + const unsigned char lead = static_cast(first); + if (lead < 0x80) return lead; + const std::size_t extra = lead >= 0xf0 ? 3 : lead >= 0xe0 ? 2 : lead >= 0xc0 ? 1 : 0; + if (extra == 0) { + errno = 86; + stream->SetError(); + return -1; + } + std::uint32_t code = extra == 3 ? (lead & 0x07) : extra == 2 ? (lead & 0x0f) : (lead & 0x1f); + for (std::size_t index = 0; index < extra; ++index) { + const int next = fgetc_nid_postfix(stream); + if (next == -1 || (static_cast(next) & 0xc0) != 0x80) { + errno = 86; + stream->SetError(); + return -1; + } + code = (code << 6) | (static_cast(next) & 0x3f); + } + return static_cast(code); } + std::int32_t APS5_VABI ungetwc_nid_postfix(std::int32_t value, FileStream* stream) { if (value == -1) return -1; if (value < 0 || value > 255) { @@ -46,6 +71,9 @@ int APS5_VABI getc_unlocked_nid_postfix(FileStream* stream) { } int APS5_VABI __srget_nid_postfix(FileStream* stream) { return fgetc_nid_postfix(stream); } int APS5_VABI getchar_nid_postfix() { return fgetc_nid_postfix(__stdinp_nid_postfix); } +std::int32_t APS5_VABI getwc_nid_postfix(FileStream* stream) { return fgetwc_nid_postfix(stream); } +std::int32_t APS5_VABI getwchar_nid_postfix() { return fgetwc_nid_postfix(__stdinp_nid_postfix); } +int APS5_VABI getchar_unlocked_nid_postfix() { return getc_unlocked_nid_postfix(__stdinp_nid_postfix); } int APS5_VABI fputc_nid_postfix(int value, FileStream* stream) { const int result = std::fputc(value, GetNativeStream(stream)); stream->SyncStatus(); @@ -63,6 +91,7 @@ int APS5_VABI putc_unlocked_nid_postfix(int value, FileStream* stream) { } int APS5_VABI __swbuf_nid_postfix(int value, FileStream* stream) { return fputc_nid_postfix(value, stream); } int APS5_VABI putchar_nid_postfix(int value) { return fputc_nid_postfix(value, __stdoutp_nid_postfix); } +int APS5_VABI putchar_unlocked_nid_postfix(int value) { return putc_unlocked_nid_postfix(value, __stdoutp_nid_postfix); } int APS5_VABI ungetc_nid_postfix(int value, FileStream* stream) { const int result = std::ungetc(value, GetNativeStream(stream)); stream->SyncStatus(); diff --git a/core/libs/tests/GuestStandardStreams.cpp b/core/libs/tests/GuestStandardStreams.cpp index 83b97d5ecd..e32e8d0c30 100644 --- a/core/libs/tests/GuestStandardStreams.cpp +++ b/core/libs/tests/GuestStandardStreams.cpp @@ -42,6 +42,13 @@ int APS5_VABI getc_unlocked_nid_postfix(FileStream*); int APS5_VABI putc_unlocked_nid_postfix(int, FileStream*); int APS5_VABI fputwc_nid_postfix(char16_t, FileStream*); int APS5_VABI fputws_nid_postfix(const char16_t*, FileStream*); +std::int32_t APS5_VABI getwc_nid_postfix(FileStream*); +std::int32_t APS5_VABI getwchar_nid_postfix(); +int APS5_VABI putwc_nid_postfix(char16_t, FileStream*); +int APS5_VABI putwchar_nid_postfix(char16_t); +char16_t* APS5_VABI fgetws_nid_postfix(char16_t*, int, FileStream*); +int APS5_VABI getchar_unlocked_nid_postfix(); +int APS5_VABI putchar_unlocked_nid_postfix(int); int APS5_VABI fscanf_nid_postfix(FileStream*, const char*, ...); int APS5_VABI __srget_nid_postfix(FileStream*); int APS5_VABI __swbuf_nid_postfix(int, FileStream*); @@ -543,6 +550,25 @@ int main() { Require(fileno_nid_postfix(__stdinp_nid_postfix) == 0); Require(fileno_nid_postfix(__stdoutp_nid_postfix) == 1); Require(fileno_nid_postfix(__stderrp_nid_postfix) == 2); + auto* const savedInput = __stdinp_nid_postfix; + auto* const savedOutput = __stdoutp_nid_postfix; + FileStream customIn(std::tmpfile()); + Require(fputc_nid_postfix('T', &customIn) == 'T'); + std::rewind(customIn.GetHandle()); + __stdinp_nid_postfix = &customIn; + Require(getchar_unlocked_nid_postfix() == 'T'); + Require(getchar_unlocked_nid_postfix() == EOF); + Require(ungetc_nid_postfix('W', &customIn) == 'W'); + Require(getwchar_nid_postfix() == 'W'); + FileStream customOut(std::tmpfile()); + __stdoutp_nid_postfix = &customOut; + Require(putchar_unlocked_nid_postfix('U') == 'U'); + Require(putwchar_nid_postfix(u'V') == u'V'); + std::rewind(customOut.GetHandle()); + Require(fgetc_nid_postfix(&customOut) == 'U'); + Require(fgetc_nid_postfix(&customOut) == 'V'); + __stdinp_nid_postfix = savedInput; + __stdoutp_nid_postfix = savedOutput; FileStream stream(std::tmpfile()); auto& guest = *reinterpret_cast(&stream); Require(&guest == &stream.GuestState()); @@ -579,6 +605,62 @@ int main() { Require(std::memcmp(wideBytes, "AB\xC3\xA9", 4) == 0); wide.Close(); + FileStream wideIo(std::tmpfile()); + Require(putwc_nid_postfix(u'H', &wideIo) == u'H'); + Require(putwc_nid_postfix(u'i', &wideIo) == u'i'); + Require(putwc_nid_postfix(u'\n', &wideIo) == u'\n'); + Require(fputws_nid_postfix(u"Next\n", &wideIo) == 5); + Require(fputws_nid_postfix(u"End", &wideIo) == 3); + std::rewind(wideIo.GetHandle()); + Require(getwc_nid_postfix(&wideIo) == 'H'); + std::rewind(wideIo.GetHandle()); + char16_t wideBuf[16]{}; + Require(fgetws_nid_postfix(wideBuf, 16, &wideIo) == wideBuf); + Require(wideBuf[0] == u'H' && wideBuf[1] == u'i' && wideBuf[2] == u'\n' && wideBuf[3] == 0); + Require(fgetws_nid_postfix(wideBuf, 16, &wideIo) == wideBuf); + Require(wideBuf[0] == u'N' && wideBuf[1] == u'e' && wideBuf[2] == u'x' && wideBuf[3] == u't' && wideBuf[4] == u'\n' && wideBuf[5] == 0); + Require(fgetws_nid_postfix(wideBuf, 16, &wideIo) == wideBuf); + Require(wideBuf[0] == u'E' && wideBuf[1] == u'n' && wideBuf[2] == u'd' && wideBuf[3] == 0); + Require(fgetws_nid_postfix(wideBuf, 16, &wideIo) == nullptr); + Require(feof_nid_postfix(&wideIo)); + Require(fgetws_nid_postfix(wideBuf, 0, &wideIo) == nullptr && *__error_nid_postfix() == 22); + Require(ferror_nid_postfix(&wideIo)); + clearerr_nid_postfix(&wideIo); + Require(fgetws_nid_postfix(nullptr, 16, &wideIo) == nullptr && *__error_nid_postfix() == 22); + Require(ferror_nid_postfix(&wideIo)); + clearerr_nid_postfix(&wideIo); + Require(fgetws_nid_postfix(wideBuf, 16, nullptr) == nullptr && *__error_nid_postfix() == 22); + wideBuf[0] = u'X'; + wideBuf[1] = u'Y'; + std::rewind(wideIo.GetHandle()); + Require(fgetws_nid_postfix(wideBuf, 1, &wideIo) == wideBuf && wideBuf[0] == 0 && wideBuf[1] == u'Y'); + Require(fgetc_nid_postfix(&wideIo) == 'H'); + wideIo.Close(); + + FileStream roundTrip(std::tmpfile()); + Require(putwc_nid_postfix(u'é', &roundTrip) == u'é'); + Require(putwc_nid_postfix(u'\u20ac', &roundTrip) == u'\u20ac'); + Require(putwc_nid_postfix(u'\n', &roundTrip) == u'\n'); + std::rewind(roundTrip.GetHandle()); + Require(getwc_nid_postfix(&roundTrip) == static_cast(u'é')); + Require(getwc_nid_postfix(&roundTrip) == static_cast(u'\u20ac')); + Require(getwc_nid_postfix(&roundTrip) == static_cast(u'\n')); + Require(getwc_nid_postfix(&roundTrip) == -1); + std::rewind(roundTrip.GetHandle()); + char16_t utfBuf[16]{}; + Require(fgetws_nid_postfix(utfBuf, 16, &roundTrip) == utfBuf); + Require(utfBuf[0] == u'é' && utfBuf[1] == u'\u20ac' && utfBuf[2] == u'\n' && utfBuf[3] == 0); + roundTrip.Close(); + + FileStream midErr(std::tmpfile()); + Require(fputc_nid_postfix('A', &midErr) == 'A'); + Require(fputc_nid_postfix(0xff, &midErr) == 0xff); + std::rewind(midErr.GetHandle()); + char16_t midBuf[16]{}; + Require(fgetws_nid_postfix(midBuf, 16, &midErr) == nullptr); + Require(ferror_nid_postfix(&midErr)); + midErr.Close(); + FileStream formatted(std::tmpfile()); const char expected[] = "guest 4294967297 1.25 1 2 3 4 5 6 7 8\n"; Require(fprintf_nid_postfix(&formatted, "%s %ld %.2f %d %d %d %d %d %d %d %d\n", diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 04ff5afa9f..7f433802e3 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -33,7 +33,8 @@ ### Functional - Windows direct-memory backing reserves a paging-file section. A [direct memory page](../../../core/libs/prx/libc/include/WindowsMappings.hpp) mapped at one guest address is private write-watched memory at that address and is not committed in the section; a page mapped at a second address moves its whole resident run into the section (committed there) and every address becomes a view of it, tracked by write protection. An unmapped resident page that is not all zeros is kept in a private reservation until it is mapped again. Committed section contents remain across unmap/remap and are released when the backing and its remaining views are destroyed. A host import alias over part of a resident run is refused. Linux retains its existing backing. Peak gameplay memory and Windows versions other than the tested host remain unverified. -- [getc_unlocked, putc_unlocked](../../../core/libs/prx/libc/src/StandardStreams.cpp) (libc) - byte I/O uses the host unlocked operations; guest EOF/error flags and macro counters follow `FileStream`. Linux status queries use `feof_unlocked`/`ferror_unlocked` because [glibc `feof`](https://github.com/bminor/glibc/blob/d2097651cc57834dbfcaa102ddfacae0d86cfb66/libio/feof.c) and [`ferror`](https://github.com/bminor/glibc/blob/d2097651cc57834dbfcaa102ddfacae0d86cfb66/libio/ferror.c) can acquire the stream lock. Windows uses [the CRT no-lock byte operations](https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/fgetc-nolock-fgetwc-nolock) and `feof`/`ferror`, verified under a held stream lock on UCRT. Console behavior and other host CRTs remain unverified. +- [getc_unlocked, putc_unlocked, getchar_unlocked, putchar_unlocked](../../../core/libs/prx/libc/src/StandardStreams.cpp) (libc) - byte I/O uses the host unlocked operations; guest EOF/error flags and macro counters follow `FileStream`. Linux status queries use `feof_unlocked`/`ferror_unlocked` because [glibc `feof`](https://github.com/bminor/glibc/blob/d2097651cc57834dbfcaa102ddfacae0d86cfb66/libio/feof.c) and [`ferror`](https://github.com/bminor/glibc/blob/d2097651cc57834dbfcaa102ddfacae0d86cfb66/libio/ferror.c) can acquire the stream lock. Windows uses [the CRT no-lock byte operations](https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/fgetc-nolock-fgetwc-nolock) and `feof`/`ferror`, verified under a held stream lock on UCRT. Console behavior and other host CRTs remain unverified. +- [fgetws, getwc, getwchar, putwc, putwchar](../../../core/libs/prx/libc/src/FormattingWide.cpp) (libc) - wide stream I/O encodes and decodes wide characters as UTF-8 on the underlying byte stream; `fgetws` reads characters into the destination buffer until newline or EOF and null-terminates, returning null on stream error. Console wide stream buffering and encoding details remain unverified. - [ftrylockfile](../../../core/libs/prx/libc/src/RuntimeSupport.cpp) (libc) - tries the host CRT stream lock that `flockfile`, `funlockfile` and each locking host stdio call on the stream take, so it returns -1 while another thread holds the stream through `flockfile` or is inside such a call, and 0 when it acquires the lock or already owns it (recursively, released by as many `funlockfile` calls), as FreeBSD 11 [`_ftrylockfile`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/stdio/_flock_stub.c#L89-L104) does. A libc export holds the lock only for each host call it makes, not across several host calls or while it updates the guest `FILE` prefix. Linux uses glibc `ftrylockfile`. UCRT has no try variant of `_lock_file`, so Windows calls `TryEnterCriticalSection` on the `CRITICAL_SECTION` that `_lock_file` enters, found through the private UCRT stream layout (`__crt_stdio_stream_data` in `ucrt/inc/corecrt_internal_stdio.h`, unchanged from Windows SDK 10.0.19041 to 10.0.26100); another CRT or a change of that layout breaks it. A null or closed stream throws where FreeBSD dereferences it - [Wide-character conversions](../../../core/libs/prx/libc/src/Multibyte.cpp) (libc) - The wide `wcsto*` functions parse the ASCII prefix of the guest string, as the C locale accepts only ASCII white space and digits. [`wcsrtombs`](../../../core/libs/prx/libc/src/Multibyte.cpp) uses the guest's single-byte C locale through `wcrtomb`; other encodings and shift states are not supported - [realpath](../../../core/libs/prx/libc/src/RealPath.cpp) (libc) follows FreeBSD 11's [`realpath.c`](https://github.com/freebsd/freebsd-src/blob/stable/11/lib/libc/stdlib/realpath.c) except for symlinks. The console's guest filesystem has none, so a host symlink under the guest root is followed for existence and type but not expanded, and `..` drops the previous component lexically, as `Resolve` does for every guest path. Through a host link `linkdir -> sub/deep`, `linkdir/../x` gives `x` beside the link, not beside its target. From 04b673e53d58a85f520e798879bad8dd6a6d0da5 Mon Sep 17 00:00:00 2001 From: Nithishvelan M Date: Wed, 14 Oct 2026 19:58:56 +0530 Subject: [PATCH 0071/1000] test(libSceErrorDialog): cover initial status queries and uninitialized close in guest test --- core/libs/tests/GuestErrorDialog.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/core/libs/tests/GuestErrorDialog.cpp b/core/libs/tests/GuestErrorDialog.cpp index cb91dc4fbf..106819b6a8 100644 --- a/core/libs/tests/GuestErrorDialog.cpp +++ b/core/libs/tests/GuestErrorDialog.cpp @@ -34,11 +34,16 @@ struct ErrorDialogParam { int main() { ErrorDialogParam param{16, static_cast(0x80020001), 0, 0}; + Require(sceErrorDialogGetStatus() == 0); + Require(sceErrorDialogUpdateStatus() == 0); Require(sceErrorDialogTerminate() == kErrNotInitialized); + Require(sceErrorDialogClose() == kErrInvalidState); Require(sceErrorDialogOpen(¶m) == kErrInvalidState); Require(sceErrorDialogInitialize() == 0); Require(sceErrorDialogInitialize() == kErrAlreadyInitialized); + Require(sceErrorDialogGetStatus() == 1); + Require(sceErrorDialogUpdateStatus() == 1); Require(sceErrorDialogClose() == kErrInvalidState); @@ -60,4 +65,7 @@ int main() { Require(sceErrorDialogTerminate() == 0); Require(sceErrorDialogTerminate() == kErrNotInitialized); + Require(sceErrorDialogGetStatus() == 0); + Require(sceErrorDialogUpdateStatus() == 0); + Require(sceErrorDialogClose() == kErrInvalidState); } From 305822ec73aee3ee6a7429b637bacc1c628dd6ef Mon Sep 17 00:00:00 2001 From: Nithishvelan M Date: Wed, 14 Oct 2026 20:03:31 +0530 Subject: [PATCH 0072/1000] test(libkernel): cover sceKernelIsTrinityMode and dual-null in operation mode guest test --- core/libs/tests/GuestOperationMode.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/core/libs/tests/GuestOperationMode.cpp b/core/libs/tests/GuestOperationMode.cpp index 248ec491ac..b3fe29f518 100644 --- a/core/libs/tests/GuestOperationMode.cpp +++ b/core/libs/tests/GuestOperationMode.cpp @@ -3,6 +3,7 @@ #include extern "C" { void APS5_VABI sceKernelGetOperationMode(int* mode, int* submode); +int APS5_VABI sceKernelIsTrinityMode(void); } static void Require(bool value) { if (!value) std::abort(); } static bool Throws(int* mode, int* submode) { @@ -15,6 +16,7 @@ int main() { Require(out[0] == 2 && out[1] == -1); Require(out[2] == 0 && out[3] == -1); int value = -1; - Require(Throws(nullptr, &value) && Throws(&value, nullptr)); + Require(Throws(nullptr, &value) && Throws(&value, nullptr) && Throws(nullptr, nullptr)); Require(value == -1); + Require(sceKernelIsTrinityMode() == 0); } From cece0fafc9cfc9b65ac6ef66e55763edea489ed8 Mon Sep 17 00:00:00 2001 From: christopherflores9312 <84460061+christopherflores9312@users.noreply.github.com> Date: Wed, 14 Oct 2026 07:34:11 -0700 Subject: [PATCH 0073/1000] test(tools): cover inline NID mode conflicts --- tools/tests/test_nid_names.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/tools/tests/test_nid_names.py b/tools/tests/test_nid_names.py index b4f6516f96..213f79c47f 100644 --- a/tools/tests/test_nid_names.py +++ b/tools/tests/test_nid_names.py @@ -97,9 +97,12 @@ class NidArgumentTests(unittest.TestCase): def test_name_and_nid_modes_conflict_even_with_empty_filters(self): for name_option in ("--name", "--symbol", "--na", "--sym"): for nid_option in ("--nid", "--ni"): - for name_values, nid_values in (([], []), (["ceilf"], []), ([], ["AAAAAAAAAAA"])): - names = [name_option, *name_values] - nids = [nid_option, *nid_values] + for names, nids in ( + ([name_option], [nid_option]), + ([name_option, "ceilf"], [nid_option]), + ([name_option + "=ceilf"], [nid_option]), + ([name_option], [nid_option, "AAAAAAAAAAA"]), + ([name_option], [nid_option + "=AAAAAAAAAAA"])): for arguments in (names + nids, nids + names): with self.subTest(arguments=arguments), \ contextlib.redirect_stdout(io.StringIO()) as output, \ From 766f6de920ab1639c64a97df8ae063b1cf61d4e8 Mon Sep 17 00:00:00 2001 From: Yuri Olive Date: Tue, 13 Oct 2026 20:51:48 -0300 Subject: [PATCH 0074/1000] fix(agc): read an unnormalized sampler without a mip filter at its base level A guest sampler with FORCE_UNNORMALIZED and a mip filter of none read a multi-level texture and the driver refused it: an unnormalized sampler is accepted on a view with several levels only when every use is image_sample_lz and the S# has no minimum LOD and no LOD bias. Kena: Bridge of Spirits (PPSA01746) binds such a sampler (implicit LOD, point filters, mip filter none, LOD bias 7) to a 9-level texture and stopped with "guest texture sampled with unnormalized coordinates is not a single-layer 1D or 2D view ...". With the mip filter off the sampler reads only level 0, so the base-level path the driver already has for unnormalized samplers is exact for it whatever its uses, minimum LOD or bias. A sampler with a point or linear mip filter is unchanged. Measured on a PS5 (gfx1013) through a sampler descriptor rewritten to force unnormalized coordinates, a 64x64 texture of four levels with a distinct marker per level, coordinates in texel units: - mip filter point, explicit LOD 0, 1, 2, 3: level 0, 1, 2, 3, 256 of 256 texels each, coordinates read in texel units of the selected level - mip filter point, implicit LOD from a quad derivative of 8 texels: level 3 - mip filter none with LOD bias 7: explicit LOD 2, explicit LOD 3 and the implicit LOD of 3 each read level 0, 256 of 256 texels Tested: Windows 11, MinGW-w64 GCC 15.2.0, Release, RTX 4070 SUPER. agc_shader_memory_tests passes with the change and fails without it on the new assertion; agc_driver_image_sample_unnormalized_tests, agc_shader_disk_cache_tests and agc_driver_graphics_tests pass. --- core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp | 10 ++++++++++ .../Optimization/src/DescriptorBindingBuilder.cpp | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 9021647520..bb68c2e371 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -3447,6 +3447,16 @@ void verifyUnusedUnnormalizedSampler() { snapshot.samplers[0].dwords[2] = 0x05500180u; require(atBaseLevel(levelZero) == std::vector{false}, "unnormalized samplers: an S# with a LOD bias was flagged as exact at the base level"); snapshot.samplers[0].dwords[2] = 0x05500000u; + auto implicitUse = levelZero; + implicitUse.samplers[0].uses = SamplerUseImplicitLod; + snapshot.samplers[0].dwords[1] = 0x00fff100u; + snapshot.samplers[0].dwords[2] = 0x01500180u; + require(atBaseLevel(implicitUse) == std::vector{true}, "unnormalized samplers: an image read through an S# without a mip filter was not flagged as exact at its base level"); + require(atBaseLevel(selectedLod) == std::vector{true}, "unnormalized samplers: an image read at a selected LOD through an S# without a mip filter was not flagged as exact at its base level"); + snapshot.samplers[0].dwords[2] = 0x05500180u; + require(atBaseLevel(implicitUse) == std::vector{false}, "unnormalized samplers: an image read through an S# with a point mip filter was flagged as exact at its base level"); + snapshot.samplers[0].dwords[1] = 0x00fff000u; + snapshot.samplers[0].dwords[2] = 0x05500000u; snapshot.samplers[0].dwords[0] = 0x00000092u; const auto normalized = populate(info); require(normalized[0].imageUnnormalized == std::vector{false} && normalized[1].samplerUnnormalized == std::vector{false}, "unnormalized samplers: a normalized S# was flagged"); diff --git a/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp b/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp index 6081365137..d65fa13fbe 100644 --- a/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp +++ b/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp @@ -201,7 +201,7 @@ UnnormalizedProof ProveUnnormalized(const ShaderInfo& info, const ResourceSnapsh if (!exact) break; if (pair.image != resource) continue; const auto& words = snapshot.samplers.at(pair.sampler).dwords; - exact = proof.samplers[pair.sampler] && info.samplers.at(pair.sampler).uses == SamplerUseExplicitLod && (words[1] & 0xfffu) == 0u && (words[2] & 0x3fffu) == 0u; + exact = proof.samplers[pair.sampler] && (((words[2] >> 26u) & 3u) == 0u || (info.samplers.at(pair.sampler).uses == SamplerUseExplicitLod && (words[1] & 0xfffu) == 0u && (words[2] & 0x3fffu) == 0u)); } proof.imagesAtBaseLevel[resource] = exact; } From a6e6209a73a42b78da88544acb345b7181df4806 Mon Sep 17 00:00:00 2001 From: Nithishvelan M Date: Wed, 14 Oct 2026 20:08:07 +0530 Subject: [PATCH 0075/1000] test(libkernel): cover valid probe in cpu temperature guest test --- core/libs/tests/GuestThermalSensors.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/core/libs/tests/GuestThermalSensors.cpp b/core/libs/tests/GuestThermalSensors.cpp index a62992aba8..3c051911a6 100644 --- a/core/libs/tests/GuestThermalSensors.cpp +++ b/core/libs/tests/GuestThermalSensors.cpp @@ -73,6 +73,9 @@ int main() { int probe = -12345; const int result = sceKernelGetSocSensorTemperature(0, &probe); Require(result == 0 ? (probe >= 0 && probe <= 130) : (result == SCE_KERNEL_ERROR_EINVAL && probe == -12345)); + int cpuProbe = -12345; + const int cpuResult = sceKernelGetCpuTemperature(&cpuProbe); + Require(cpuResult == 0 ? (cpuProbe >= 0 && cpuProbe <= 130) : (cpuResult == SCE_KERNEL_ERROR_EINVAL && cpuProbe == -12345)); Require(sceKernelGetSocPowerConsumption(nullptr) == SCE_KERNEL_ERROR_EFAULT); std::uint32_t consumption[32]; From 102db2e1a4d9a38685e7cae68842120579af8923 Mon Sep 17 00:00:00 2001 From: Yossi Yadgar Date: Wed, 14 Oct 2026 14:40:41 +0300 Subject: [PATCH 0076/1000] fix(libSceVideodec2): hold an AVC unit of parameter sets for the next slice Twispero (PPSA99474) plays a Twitch stream through its own FFmpeg demuxer and submits the H.264 sequence and picture parameter sets as an access unit of their own, before the first slices. libavcodec rejects a packet with parameter sets and no slice (h264 "no frame!", AVERROR_INVALIDDATA, with or without AV_EF_EXPLODE), so sceVideodec2Decode threw "packet submission failed" and the title stopped as soon as a stream was opened. A unit without a slice NAL (types 1 to 5) is now kept on the decoder and put in front of the next unit that has one, and the call returns with no picture. sceVideodec2Reset drops what is kept. --- core/libs/prx/libSceVideodec2/Export.cpp | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceVideodec2/Export.cpp b/core/libs/prx/libSceVideodec2/Export.cpp index 89b25e2b09..3dad0960da 100644 --- a/core/libs/prx/libSceVideodec2/Export.cpp +++ b/core/libs/prx/libSceVideodec2/Export.cpp @@ -295,6 +295,7 @@ struct Decoder { std::int64_t nextKey = 0; std::deque ready; std::vector annexB; + std::vector parameterSets; bool flushing = false; std::array vp9References{}; Vp9PictureInfo vp9Colors{}; @@ -380,6 +381,16 @@ const std::vector* ToAnnexB(Decoder& decoder, const std::uint8_t* return &decoder.annexB; } +bool CarriesAvcSlice(const std::vector& unit) { + for (std::size_t index = 0; index + 3 < unit.size(); ++index) { + if (unit[index] != 0 || unit[index + 1] != 0 || unit[index + 2] != 1) continue; + const auto type = unit[index + 3] & 0x1fu; + if (type >= 1 && type <= 5) return true; + index += 2; + } + return false; +} + void Receive(Decoder& decoder) { for (;;) { FramePointer frame(av_frame_alloc()); @@ -564,12 +575,20 @@ int APS5_VABI sceVideodec2Decode_nid_postfix(std::uint64_t handle, const InputDa return 0; } const auto* unit = ToAnnexB(decoder, input->auData, static_cast(input->auSize)); + if (decoder.codec == CodecAvc && !CarriesAvcSlice(*unit)) { + decoder.parameterSets.insert(decoder.parameterSets.end(), unit->begin(), unit->end()); + EmitDecoded(handle, decoder, frame, output); + return 0; + } AVPacket* packet = av_packet_alloc(); if (!packet) throw std::runtime_error("Videodec2: cannot allocate packet"); const auto freePacket = [](AVPacket* value) { av_packet_free(&value); }; const std::unique_ptr packetOwner(packet, freePacket); - if (av_new_packet(packet, static_cast(unit->size())) < 0) throw std::runtime_error("Videodec2: cannot allocate packet data"); - std::memcpy(packet->data, unit->data(), unit->size()); + if (decoder.parameterSets.size() + unit->size() > static_cast(std::numeric_limits::max())) throw std::runtime_error("Videodec2: invalid access unit"); + if (av_new_packet(packet, static_cast(decoder.parameterSets.size() + unit->size())) < 0) throw std::runtime_error("Videodec2: cannot allocate packet data"); + std::memcpy(packet->data, decoder.parameterSets.data(), decoder.parameterSets.size()); + std::memcpy(packet->data + decoder.parameterSets.size(), unit->data(), unit->size()); + decoder.parameterSets.clear(); packet->pts = decoder.nextKey; decoder.inputs.emplace(decoder.nextKey++, picture); Send(decoder, packet); @@ -606,6 +625,7 @@ int APS5_VABI sceVideodec2Reset_nid_postfix(std::uint64_t handle) { avcodec_flush_buffers(decoder.context); decoder.ready.clear(); decoder.inputs.clear(); + decoder.parameterSets.clear(); decoder.flushing = false; return 0; } From 5bdcb2d98fe77bebf0934d3617c5f855827688a9 Mon Sep 17 00:00:00 2001 From: Yossi Yadgar Date: Wed, 14 Oct 2026 14:40:41 +0300 Subject: [PATCH 0077/1000] test(libSceVideodec2): decode an AVC stream whose parameter sets come alone guest_videodec2 splits the first access unit of the H.264 fixture into its parameter sets and its slices and decodes the parameter sets as a unit of their own: the call must succeed without a picture, and every picture after it must still match the reference hashes. With the previous Export.cpp the first call throws "packet submission failed". --- core/libs/tests/GuestVideodec2.cpp | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/core/libs/tests/GuestVideodec2.cpp b/core/libs/tests/GuestVideodec2.cpp index df41978f8c..fd1873a2aa 100644 --- a/core/libs/tests/GuestVideodec2.cpp +++ b/core/libs/tests/GuestVideodec2.cpp @@ -509,7 +509,7 @@ void testTenBit() { } } -void testDecode(bool lengthPrefixed) { +void testDecode(bool lengthPrefixed, bool separateParameters = false) { const DecoderConfigInfo config{sizeof(DecoderConfigInfo), 0, 1, 100, 0, static_cast(Width), static_cast(Height), 4, 1}; DecoderMemoryInfo memory{sizeof(DecoderMemoryInfo)}; check(sceVideodec2QueryDecoderMemoryInfo_nid_postfix(&config, &memory) == 0, "memory query failed"); @@ -534,8 +534,29 @@ void testDecode(bool lengthPrefixed) { ++pictures; return true; }; - const auto units = accessUnits(lengthPrefixed); + auto units = accessUnits(lengthPrefixed); check(units.size() == PictureHashes.size(), "unexpected access unit count"); + if (separateParameters) { + auto& first = units.front(); + std::size_t slice = 0; + for (std::size_t index = 0; index + 3 < first.size(); ++index) { + if (first[index] != 0 || first[index + 1] != 0 || first[index + 2] != 1) continue; + const auto type = first[index + 3] & 0x1fu; + if (type >= 1 && type <= 5) { + slice = index > 0 && first[index - 1] == 0 ? index - 1 : index; + break; + } + index += 2; + } + check(slice != 0, "the first access unit has no slice after its parameter sets"); + const std::vector parameters(first.begin(), first.begin() + static_cast(slice)); + first.erase(first.begin(), first.begin() + static_cast(slice)); + const InputData input{sizeof(InputData), parameters.data(), parameters.size(), 999, 0, 0x9f}; + FrameBuffer frame{sizeof(FrameBuffer), buffer.data(), buffer.size(), false}; + OutputInfo output{sizeof(OutputInfo)}; + check(sceVideodec2Decode_nid_postfix(handle, &input, &frame, &output) == 0, "a unit of parameter sets alone was not accepted"); + check(!output.isValid && !frame.isAccepted, "a unit of parameter sets alone produced a picture"); + } for (std::size_t unit = 0; unit < units.size(); ++unit) { const InputData input{sizeof(InputData), units[unit].data(), units[unit].size(), 1000 + unit, unit, 0xa0 + unit}; FrameBuffer frame{sizeof(FrameBuffer), buffer.data(), buffer.size(), false}; @@ -727,6 +748,7 @@ int main() { testFailures(); testDecode(false); testDecode(true); + testDecode(false, true); const auto hevc = hevcUnits(HevcStream.data(), HevcStream.size()); testClip(CodecHevc, 1, hevc, HevcHashes); std::vector> hevcLengthPrefixed; From 3969d4f8b381cf11a6aefb1d54019e57e1f143e9 Mon Sep 17 00:00:00 2001 From: Yossi Yadgar Date: Wed, 14 Oct 2026 14:43:12 +0300 Subject: [PATCH 0078/1000] docs(debt): note how AVC units of parameter sets alone are decoded --- docs/dev/debt/libSceVideodec2.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/dev/debt/libSceVideodec2.md b/docs/dev/debt/libSceVideodec2.md index ee47bcc9aa..faf4fae0a2 100644 --- a/docs/dev/debt/libSceVideodec2.md +++ b/docs/dev/debt/libSceVideodec2.md @@ -6,6 +6,8 @@ ### Functional +- [AVC parameter-set units](../../../core/libs/prx/libSceVideodec2/Export.cpp) (libSceVideodec2) - an H.264 access unit with no slice NAL (types 1 to 5), such as the sequence and picture parameter sets Twispero (PPSA99474) submits on their own before a stream's first slices, is kept and put in front of the next unit that has a slice, and `sceVideodec2Decode` returns 0 with no picture for it, since libavcodec refuses a packet without a slice. What the console returns for such a unit is not known. HEVC units of parameter sets alone are still passed to libavcodec as they are + - [sceVideodec2CreateDecoder](../../../core/libs/prx/libSceVideodec2/Export.cpp) (libSceVideodec2) decodes HEVC (codec type 974921) and VP9 (2382845) through FFmpeg, besides H.264. The tags come from SharpProspero's `Videodec2CodecType` and EVO-PLAYER-PS5's `sce_videodec2.h`, which records them from hardware runs. HEVC input is converted from length-prefixed NAL units to Annex B as for H.264, and VP9 frames are passed through. Output is the same 8-bit NV12 for every codec; a decoder for HEVC Main10 or VP9 Profile 2 is created, but a frame deeper than 8 bits throws, since the console's 10-bit output layout is unknown, and `QueryDecoderMemoryInfo` reports the 8-bit frame size for it. `sceVideodec2GetPictureInfo` and `sceVideodec2GetAvcPictureInfo` throw for HEVC and VP9 pictures, which use other layouts (`SceVideodec2HevcPictureInfo`, `SceVideodec2Vp9PictureInfo`), and `sceVideodec2GetHevcPictureInfo` is not exported; `sceVideodec2GetVp9PictureInfo` has its own VP9 layout - [sceVideodec2GetVp9PictureInfo](../../../core/libs/prx/libSceVideodec2/Export.cpp) uses the 88-byte layout and two-pointer signature from [SharpProspero Videodec2.cs](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Video/Videodec2.cs). Frame metadata follows the [VP9 specification](https://storage.googleapis.com/downloads.webmproject.org/docs/vp9/vp9-bitstream-specification-v0.7-20170222-draft.pdf), sections 5.26 and 6.2: profile, key/intra-only flags, bit depth, color space/range, coded/render dimensions and superframe count are retained per emitted buffer. Level is 0 because the elementary stream does not signal a level; the console value is unverified. Null pointers, undersized structures and querying another codec throw; console error codes and support for other structure sizes are unknown. Existing NV12 output rejects pictures deeper than 8 bits. Superframes are submitted as individual coded frames to keep each output associated with its input metadata. From 352a300dd8b39950ba9cb57130cad21df3570d7a Mon Sep 17 00:00:00 2001 From: KernelPanicNT <76663133+KernelPanicNT@users.noreply.github.com> Date: Wed, 14 Oct 2026 14:38:15 +0000 Subject: [PATCH 0079/1000] feat(libkernel): answer sceCoredumpGetThreadContextInfo outside the coredump handler sceCoredumpGetThreadContextInfo takes (void* threadContextInfo, size_t threadContextInfoSize) as in Orbital's IDA type database, and the 144-byte block of 18 64-bit registers follows SharpProspero. It answers as sceCoredumpGetStopInfoCpu: the handler is never invoked here, so an aligned 144-byte request returns NOT_IN_COREDUMP_HANDLER without reading or writing the block, and null, unaligned or wrapping buffers and other sizes throw, as the console validation order is unverified. Co-Authored-By: Claude Opus 5.5 --- .../prx/libkernel/System/src/Coredump.cpp | 8 ++++---- core/libs/tests/GuestCoredumpAttach.cpp | 20 ++++++++++++++++--- docs/dev/debt/libkernel.md | 2 +- 3 files changed, 22 insertions(+), 8 deletions(-) diff --git a/core/libs/prx/libkernel/System/src/Coredump.cpp b/core/libs/prx/libkernel/System/src/Coredump.cpp index 1a42cca261..205d61970b 100644 --- a/core/libs/prx/libkernel/System/src/Coredump.cpp +++ b/core/libs/prx/libkernel/System/src/Coredump.cpp @@ -93,10 +93,10 @@ int APS5_VABI sceCoredumpGetStopInfoCpu(void* info, size_t size) { } int APS5_VABI sceCoredumpGetThreadContextInfo(void* threadContextInfo, size_t threadContextInfoSize) { - (void)threadContextInfo; - (void)threadContextInfoSize; - NotImplemented_nid_no_patch(__func__); - return 0; + const auto address = reinterpret_cast(threadContextInfo); + if (threadContextInfo == nullptr || threadContextInfoSize != 144 || address % alignof(uint64_t) != 0 || threadContextInfoSize > UINTPTR_MAX - address) + throw std::runtime_error(std::string(__func__) + ": null, unaligned, wrapping or unsupported thread context buffer outside the core dump handler"); + return SCE_COREDUMP_ERROR_NOT_IN_COREDUMP_HANDLER; } int APS5_VABI sceCoredumpWriteUserString() { diff --git a/core/libs/tests/GuestCoredumpAttach.cpp b/core/libs/tests/GuestCoredumpAttach.cpp index 5c12d2cddc..1fb6e65f1e 100644 --- a/core/libs/tests/GuestCoredumpAttach.cpp +++ b/core/libs/tests/GuestCoredumpAttach.cpp @@ -13,6 +13,7 @@ int APS5_VABI sceCoredumpUnregisterCoredumpHandler(void); int APS5_VABI sceCoredumpAttachMemoryRegion(uint32_t userValue, const void* mem, size_t size); int APS5_VABI sceCoredumpAttachUserMemoryFile(uint32_t userValue, const void* mem, size_t size); int APS5_VABI sceCoredumpGetStopInfoCpu(void* info, size_t size); +int APS5_VABI sceCoredumpGetThreadContextInfo(void* threadContextInfo, size_t threadContextInfoSize); } namespace { @@ -40,11 +41,15 @@ int main() { alignas(uint64_t) std::array region{}; for (size_t index = 0; index < region.size(); ++index) region[index] = static_cast(index * 7 + 3); const auto original = region; + alignas(uint64_t) std::array context{}; + for (size_t index = 0; index < context.size(); ++index) context[index] = static_cast(index * 5 + 1); + const auto originalContext = context; Require(sceCoredumpAttachMemoryRegion(1, region.data(), region.size()) == NotInCoredumpHandler); Require(sceCoredumpAttachUserMemoryFile(UserFileTruncateIfExceedLimit | 2, region.data(), region.size()) == NotInCoredumpHandler); Require(sceCoredumpGetStopInfoCpu(region.data() + 16, 16) == NotInCoredumpHandler); - Require(region == original); + Require(sceCoredumpGetThreadContextInfo(context.data() + 8, 144) == NotInCoredumpHandler); + Require(region == original && context == originalContext); Require(sceCoredumpRegisterCoredumpHandler(reinterpret_cast(&Handler), 16 * 1024, reinterpret_cast(&handlerContext)) == 0); Require(sceCoredumpAttachMemoryRegion(0, region.data(), 1) == NotInCoredumpHandler); @@ -55,7 +60,10 @@ int main() { Require(fromThread == NotInCoredumpHandler); std::thread([&] { fromThread = sceCoredumpGetStopInfoCpu(region.data() + 16, 16); }).join(); Require(fromThread == NotInCoredumpHandler); - Require(region == original); + Require(sceCoredumpGetThreadContextInfo(context.data(), 144) == NotInCoredumpHandler); + std::thread([&] { fromThread = sceCoredumpGetThreadContextInfo(context.data() + 8, 144); }).join(); + Require(fromThread == NotInCoredumpHandler); + Require(region == original && context == originalContext); Require(!handlerRan); Require(Throws([&] { sceCoredumpAttachMemoryRegion(1, nullptr, region.size()); }, "sceCoredumpAttachMemoryRegion")); @@ -69,12 +77,18 @@ int main() { Require(Throws([&] { sceCoredumpGetStopInfoCpu(reinterpret_cast(UINTPTR_MAX - 7u), 16); }, "sceCoredumpGetStopInfoCpu")); for (const size_t size : {size_t{0}, size_t{1}, size_t{15}, size_t{17}, SIZE_MAX}) Require(Throws([&] { sceCoredumpGetStopInfoCpu(region.data() + 16, size); }, "sceCoredumpGetStopInfoCpu")); + Require(Throws([&] { sceCoredumpGetThreadContextInfo(nullptr, 144); }, "sceCoredumpGetThreadContextInfo")); + Require(Throws([&] { sceCoredumpGetThreadContextInfo(context.data() + 1, 144); }, "sceCoredumpGetThreadContextInfo")); + Require(Throws([&] { sceCoredumpGetThreadContextInfo(reinterpret_cast(UINTPTR_MAX - 7u), 144); }, "sceCoredumpGetThreadContextInfo")); + for (const size_t size : {size_t{0}, size_t{1}, size_t{143}, size_t{145}, SIZE_MAX}) + Require(Throws([&] { sceCoredumpGetThreadContextInfo(context.data(), size); }, "sceCoredumpGetThreadContextInfo")); Require(sceCoredumpUnregisterCoredumpHandler() == 0); Require(sceCoredumpAttachMemoryRegion(1, region.data(), region.size()) == NotInCoredumpHandler); Require(sceCoredumpAttachUserMemoryFile(1, region.data(), region.size()) == NotInCoredumpHandler); Require(sceCoredumpGetStopInfoCpu(region.data() + 16, 16) == NotInCoredumpHandler); - Require(region == original); + Require(sceCoredumpGetThreadContextInfo(context.data() + 8, 144) == NotInCoredumpHandler); + Require(region == original && context == originalContext); Require(!handlerRan); return 0; } diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 6eaefa8c88..8f030cba3e 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -13,7 +13,6 @@ ### Unknown function info - [getlogin_r](../../../core/libs/prx/libkernel/System/src/Login.cpp) (libkernel) returns the host effective account name: `GetUserNameW` as UTF-8 on Windows and `getpwuid_r(geteuid())` on POSIX. A valid account returns a name even without a login session; host `getlogin_r`, `USER` and `LOGNAME` are not consulted. The console's login name and behaviour without a session are unknown. Account lookup or encoding failures, a missing or empty account name, and POSIX account records exceeding the 16 KiB lookup buffer throw; there is no replacement name or retry. A short guest buffer returns `ERANGE` without modifying it, and a null output returns `EFAULT`. -- [sceCoredumpGetThreadContextInfo](../../../core/libs/prx/libkernel/System/src/Coredump.cpp) (libkernel, NID `ShChva57wIM`) - declared as a throwing stub. The pointer and size arguments and integer return type follow [SharpProspero's PS5 bindings](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Kernel/KernelSystem.cs). The context structure and console behavior are unverified; the stub does not read or write caller memory. PPSA01736 imports it - [sceKernelGetSystemSwVersion](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) reports baseline system software version `0x01000000` ("01.000.000") for ProsperoStore (PPSA99000); the console's firmware version is not modeled. Measured on PS5 firmware 13.42: record values 0, 24, 39, 40, 41, 48 and 64 in the leading 64-bit field are accepted and preserved, the 28-byte string at offset 8 and packed version at 0x24 are filled, and bytes from 0x28 onward remain untouched. This API reports "13.590.001" / `0x13590001` in that environment, while the Prospero variant reports "13.420.000" / `0x13420000`; their values must not be assumed identical. A null pointer still returns `SCE_KERNEL_ERROR_EFAULT`, which was not measured; other firmware versions and record values remain unverified. - [kern.rng_pseudo](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - `sysctlbyname` fills the whole output buffer with bytes from the host's `std::random_device` and leaves the length unchanged; a write returns -1 with `EPERM` and a call without an output buffer throws. The only caller seen is mbedTLS's `mbedtls_hardware_poll` in the catalog's ProsperoLight boilerplate, which asks for 64-byte blocks and fails if fewer come back. The console's generator, its per-call maximum and its answer to a size query are unknown - [sysconf](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - `_SC_CLK_TCK` returns 128, FreeBSD's `CLK_TCK` as its [`sysconf`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/gen/sysconf.c#L100-L101) returns it ([`time.h`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/include/time.h#L48-L54)); the console's value is not checked, and no `times` reports ticks at that rate. CPython's `posix` module stops its import with "cannot read ticks_per_second" when the value is below 1, as the Python in PS5MC (PPSA99420) does. Names other than `_SC_CLK_TCK`, `_SC_PAGESIZE`, `_SC_NPROCESSORS_CONF`, `_SC_NPROCESSORS_ONLN` and `_SC_PHYS_PAGES` fail with `EINVAL` @@ -71,6 +70,7 @@ - [pthread_getname_np](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - a thread that has already been joined returns `ESRCH` (3) on a PS5 (firmware 13.60), but its object is freed after the join here, so the pointer cannot be told apart from a live thread and its name is read as for a live one - [sceKernelGetOperationMode](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - the signature `(int* mode, int* submode)` comes from PPSA12544, which logs both values after the call and ignores the result. A homebrew payload on a PS5 (firmware 13.60) reads mode 2 and submode 0, nothing else is written, and the result register holds no status, so the function is taken to return nothing. Whether a title process reads the same values is unverified. Null outputs throw - [sceCoredumpGetStopInfoCpu](../../../core/libs/prx/libkernel/System/src/Coredump.cpp) (libkernel) takes `(void* info, size_t size)` as in the [Orbital type database](https://github.com/AlexAltea/orbital/blob/cc408367da6a6ebbf5ac1ac559fefeadc73b2831/tools/ida/db_types.json); the 16-byte, 8-byte-aligned block follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Kernel/KernelSystem.cs), which places coredump reads inside the handler. The handler is never invoked here, so a well-formed request returns `NOT_IN_COREDUMP_HANDLER` without reading or writing the block. Its value `0x81180003` follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/e0a466753a1aacbc8db8253be685bf7ab4d9daae/src/core/libraries/kernel/coredump/coredump_error.h) and the NHL 27 (PPSA34063) call site reported in [#4275](https://github.com/boykopovar/AnyPS5/discussions/4275), not a PS5 measurement. The console validation order is unverified: null, unaligned or wrapping buffers and sizes other than 16 throw instead of guessing an argument error. Information from an active coredump handler remains unsupported +- [sceCoredumpGetThreadContextInfo](../../../core/libs/prx/libkernel/System/src/Coredump.cpp) (libkernel, NID `ShChva57wIM`) takes `(void* threadContextInfo, size_t threadContextInfoSize)` as in the [Orbital type database](https://github.com/AlexAltea/orbital/blob/cc408367da6a6ebbf5ac1ac559fefeadc73b2831/tools/ida/db_types.json); the 144-byte block of 18 64-bit registers follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Kernel/KernelSystem.cs#L120-L141). It answers as `sceCoredumpGetStopInfoCpu`: the handler is never invoked here, so an aligned 144-byte request returns `NOT_IN_COREDUMP_HANDLER` without reading or writing the block, and null, unaligned or wrapping buffers and other sizes throw, as the console validation order is unverified. Planet Coaster (PPSA01736) imports it - [sceCoredumpAttachMemoryRegion, sceCoredumpAttachUserMemoryFile](../../../core/libs/prx/libkernel/System/src/Coredump.cpp) (libkernel) - both take `(uint32_t userValue, const void* mem, size_t size)`: for the first, the IDA type database of [Orbital](https://github.com/AlexAltea/orbital/blob/master/tools/ida/db_types.json) and the [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Kernel/KernelSystem.cs) bindings agree; the second has only SharpProspero (the PS4 5.05 firmware does not export it, so Orbital has no entry) and is described there as attaching the region as an unnamed user file. Not confirmed on a title; the RED engine's PS4 crash handler ([Vangaurd](https://github.com/vurimad/Vangaurd/blob/main/source/imported/common/redSystem/src/errorHandlerImplErrorHooksOrbis.cpp)) calls the sibling `sceCoredumpAttachMemoryRegionAsUserFile(999, buffer, bytes, name)` from inside its handler. Attaching belongs inside the core dump handler, which the system runs on its own thread after the process stops (SharpProspero's remark, the RED engine's usage and `SCE_COREDUMP_ERROR_NOT_IN_COREDUMP_HANDLER`, "function was not called from registered core dump handler", in the Vita's SceCoredump error table, [RivetDB](https://github.com/CreepNT/RivetDB/blob/master/rivetdb.json)). The handler is never invoked here, so every call is outside it and returns that error; nothing is recorded, as there is no dump to attach to. Its value `0x81180003` comes from shadPS4 (PS4), not confirmed on a PS5: [fpPS4](https://github.com/red-prig/fpPS4/blob/master/kernel/ps4_libkernel.pas) has `NOT_REGISTERED` 0x81180001 and `ALREADY_REGISTERED` 0x81180002, shadPS4's `coredump_error.h` has `NOT_IN_COREDUMP_HANDLER` = 0x81180003, and the Vita table numbers `PARAM` 0, `NOT_REGISTERED` 1, `ALREADY_REGISTERED` 2 and `NOT_IN_COREDUMP_HANDLER` 3. Whether the console checks the region before the calling context is unknown, so a null, empty or wrapping region throws. `userValue` is not interpreted; SharpProspero names its bits 31 and 30 as truncation flags for the user file variants - [sceCoredumpWriteUserData, sceCoredumpDebugTextOut](../../../core/libs/prx/libkernel/System/src/Coredump.cpp) (libkernel) - the signatures `ssize_t (const void* data, size_t size)` and `void (const char* str, int len)` follow the [Orbital](https://github.com/AlexAltea/orbital/blob/cc408367da6a6ebbf5ac1ac559fefeadc73b2831/tools/ida/db_types.json) IDA type database and the [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Kernel/KernelSystem.cs) bindings ("Repeated calls append", "The bytes written, or a negative error code"). User data is written from inside the core dump handler: the Vita's [`sceCoredumpWriteUserData`](https://github.com/vitasdk/vita-headers/blob/e66ebe90b73d1fa4cce005a5b2072cec27322544/include/psp2/coredump_nounlink.h) returns `0x800A0003` ("No user-data capture is active") outside it, and the RED engine's PS4 crash handler ([Vangaurd](https://github.com/vurimad/Vangaurd/blob/4f86d2b0e1a6c0d50526152c517a0292b1a68af4/source/imported/common/redSystem/src/errorHandlerImplErrorHooksOrbis.cpp)) makes all its coredump calls, `sceCoredumpDebugTextOut` included, from its handler. The handler is never invoked here, so `sceCoredumpWriteUserData` returns `SCE_COREDUMP_ERROR_NOT_IN_COREDUMP_HANDLER` (0x81180003 in shadPS4's [`coredump_error.h`](https://github.com/shadps4-emu/shadPS4/blob/e0a466753a1aacbc8db8253be685bf7ab4d9daae/src/core/libraries/kernel/coredump/coredump_error.h), PS4 numbering) sign-extended to 64 bits and keeps nothing; whether the console checks the buffer before the calling context is unknown, so a null, empty or wrapping buffer throws. `sceCoredumpDebugTextOut` returns nothing; its text, which goes into the report on the console, is printed to stderr with the `[coredump]` prefix of `sceKernelDebugWriteCppExceptionInfo`, and a negative length or a null text with a nonzero length throws. No title is seen calling either: 007 First Light (PPSA11386) imports `sceCoredumpWriteUserData` and does not call it before its stop (#2523) - [sceCoredumpAttachUserFile, sceCoredumpAttachMemoryRegionAsUserFile](../../../core/libs/prx/libkernel/System/src/Coredump.cpp) (libkernel) - the signatures `int (uint32_t userValue, const char* path)` and `int (uint32_t userValue, const void* mem, size_t size, const char* name)` follow the [Orbital](https://github.com/AlexAltea/orbital/blob/cc408367da6a6ebbf5ac1ac559fefeadc73b2831/tools/ida/db_types.json) IDA type database and the [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Kernel/KernelSystem.cs) bindings. The crash handler calls both from inside its core dump handler, `sceCoredumpAttachUserFile(priority, absolutePath)` and `sceCoredumpAttachMemoryRegionAsUserFile(999, buffer, bytes, "LoggerQueueCrashDump.txt")`. The handler is never invoked here, so both return `SCE_COREDUMP_ERROR_NOT_IN_COREDUMP_HANDLER` (0x81180003, PS4 numbering from shadPS4) and attach nothing; whether the console checks the arguments before the calling context is unknown, so a null path or name and a null, empty or wrapping region throw. An empty path or name is passed through to that error, and `userValue` is not interpreted. No title is seen calling either From 6976be9c103119e25f1e543bc710dc3f86724a58 Mon Sep 17 00:00:00 2001 From: hamb3r Date: Wed, 14 Oct 2026 15:36:01 +0300 Subject: [PATCH 0080/1000] perf(libc): add sub-tick spin tail to PreciseSleepUs and PreciseSleepUntil - Windows (`_WIN32`): replace full-duration waitable-timer sleep in `PreciseSleepHundredNanos` and the repeated `PreciseSleepUs` call loop in `PreciseSleepUntil` with a hybrid sleep-then-spin helper (`PreciseSleepFromCounterWithTimer`) that sleeps on the thread-local high-resolution waitable timer up to a 250 us spin tail, falls back to `std::this_thread::sleep_for` if timer setup/wait fails, and finishes the remaining window using `QueryPerformanceCounter` + `YieldProcessor()` (thresholded to 200 ns and ceiling-rounded via `PreciseHundredNanosToQpcTicks` for non-10MHz QPC frequencies). - Linux & POSIX (`!_WIN32 && !__EMSCRIPTEN__`): replace repeated `nanosleep` loop in `PreciseSleepUs` and `PreciseSleepUntil` with `PreciseSleepFromNanos` using `clock_gettime(CLOCK_MONOTONIC)`. On Linux (`__linux__`), cache `PR_SET_TIMERSLACK` (`1 ns`) once per thread (`thread_local`) and sleep via `clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME)` up to a 15 us spin tail (falling back to a conservative 100 us tail if `prctl` fails; 100 us `nanosleep` tail on other POSIX platforms) before finishing with `_mm_pause()` on x86 or `std::atomic_signal_fence` on non-x86. - Tests: expand `core/libs/tests/PreciseWait.cpp` to assert that the 31-sample median of `PreciseSleepUs(50)` stays below 90 us (which fails on `main` at ~118 us on Linux and ~518 us on Windows) and verify ceiling QPC tick conversion and timer fallbacks. --- core/libs/prx/libc/include/PreciseWait.hpp | 228 +++++++++++++++++++-- core/libs/tests/PreciseWait.cpp | 98 ++++++++- 2 files changed, 307 insertions(+), 19 deletions(-) diff --git a/core/libs/prx/libc/include/PreciseWait.hpp b/core/libs/prx/libc/include/PreciseWait.hpp index 73249384bb..af79e3fc8e 100644 --- a/core/libs/prx/libc/include/PreciseWait.hpp +++ b/core/libs/prx/libc/include/PreciseWait.hpp @@ -1,21 +1,44 @@ #ifndef CORE_LIBS_PRX_LIBC_INCLUDE_PRECISEWAIT_HPP #define CORE_LIBS_PRX_LIBC_INCLUDE_PRECISEWAIT_HPP +#include #include +#include #include +#include + +inline constexpr std::uint64_t PreciseHundredNanosToQpcTicks(unsigned long long hundredNanos, std::uint64_t freq) { + if (freq == 10000000ULL) [[likely]] { + return hundredNanos; + } + const std::uint64_t wholeSeconds = hundredNanos / 10000000ULL; + const std::uint64_t remHundredNanos = hundredNanos % 10000000ULL; + return wholeSeconds * freq + (remHundredNanos * freq + 9999999ULL) / 10000000ULL; +} + +inline constexpr std::uint64_t PrecisePauseThresholdTicks(std::uint64_t freq) { + return PreciseHundredNanosToQpcTicks(2ULL, freq); +} #ifdef _WIN32 #include #include "prx/libc/include/HostThreadLocal.hpp" -#endif -inline constexpr unsigned long long kSubTickWaitUs = 2000; - -inline void PreciseSleepUs(unsigned long long micros) { - if (micros == 0) { - return; +inline void PreciseSleepWaitableTimerWithHandle(HANDLE handle, unsigned long long sleepHundredNanos) { + bool slept = false; + if (handle != nullptr && handle != INVALID_HANDLE_VALUE) [[likely]] { + LARGE_INTEGER due; + due.QuadPart = -static_cast(sleepHundredNanos); + if (SetWaitableTimer(handle, &due, 0, nullptr, nullptr, FALSE) != FALSE) [[likely]] { + slept = (WaitForSingleObject(handle, INFINITE) == WAIT_OBJECT_0); + } } -#ifdef _WIN32 + if (!slept) [[unlikely]] { + std::this_thread::sleep_for(std::chrono::nanoseconds(sleepHundredNanos * 100ULL)); + } +} + +inline HANDLE PreciseWaitThreadTimer() { struct Timer { HANDLE handle = CreateWaitableTimerExW(nullptr, nullptr, CREATE_WAITABLE_TIMER_HIGH_RESOLUTION, TIMER_ALL_ACCESS); @@ -25,24 +48,201 @@ inline void PreciseSleepUs(unsigned long long micros) { } } }; - auto& timer = HostThreadLocal(); - if (timer.handle != nullptr) { - LARGE_INTEGER due{}; - due.QuadPart = -static_cast(micros * 10ULL); - if (SetWaitableTimerEx(timer.handle, &due, 0, nullptr, nullptr, nullptr, 0) != FALSE) { - WaitForSingleObject(timer.handle, INFINITE); - return; + return HostThreadLocal().handle; +} + +inline void PreciseSleepFromCounterWithTimer( + LARGE_INTEGER counter, + unsigned long long hundredNanos, + HANDLE timerHandle, + bool useCustomTimer) { + if (hundredNanos == 0) { + return; + } + constexpr unsigned long long kSpinTailHundredNanos = 2500; + static std::atomic cachedFreq{0}; + std::uint64_t freq = cachedFreq.load(std::memory_order_relaxed); + std::uint64_t deltaTicks = hundredNanos; + std::uint64_t pauseThresholdTicks = 2ULL; + if (freq != 10000000ULL) [[unlikely]] { + if (freq == 0) { + LARGE_INTEGER f; + QueryPerformanceFrequency(&f); + freq = static_cast(f.QuadPart); + cachedFreq.store(freq, std::memory_order_relaxed); + } + if (freq != 10000000ULL) { + deltaTicks = PreciseHundredNanosToQpcTicks(hundredNanos, freq); + pauseThresholdTicks = PrecisePauseThresholdTicks(freq); + } + } + const std::uint64_t deadlineTicks = static_cast(counter.QuadPart) + deltaTicks; + if (hundredNanos > kSpinTailHundredNanos) [[unlikely]] { + const HANDLE handle = useCustomTimer ? timerHandle : PreciseWaitThreadTimer(); + PreciseSleepWaitableTimerWithHandle(handle, hundredNanos - kSpinTailHundredNanos); + } + for (;;) { + QueryPerformanceCounter(&counter); + const std::uint64_t nowTicks = static_cast(counter.QuadPart); + if (nowTicks >= deadlineTicks) { + break; + } + if (deadlineTicks - nowTicks > pauseThresholdTicks) { + YieldProcessor(); + } + } +} + +inline void PreciseSleepFromCounter(LARGE_INTEGER counter, unsigned long long hundredNanos) { + PreciseSleepFromCounterWithTimer(counter, hundredNanos, nullptr, false); +} + +inline void PreciseSleepHundredNanos(unsigned long long hundredNanos) { + if (hundredNanos == 0) { + return; + } + LARGE_INTEGER counter; + QueryPerformanceCounter(&counter); + PreciseSleepFromCounter(counter, hundredNanos); +} +#elif !defined(__EMSCRIPTEN__) +#include +#include +#ifdef __linux__ +#include +#endif +#if defined(__x86_64__) || defined(_M_X64) || defined(__i386__) || defined(_M_IX86) +#include +#endif + +inline void PreciseSpinPause() { +#if defined(__x86_64__) || defined(_M_X64) || defined(__i386__) || defined(_M_IX86) + _mm_pause(); +#else + std::atomic_signal_fence(std::memory_order_seq_cst); +#endif +} + +inline std::uint64_t PreciseMonotonicNanos() { + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return static_cast(ts.tv_sec) * 1000000000ULL + static_cast(ts.tv_nsec); +} + +#ifdef __linux__ +inline bool PreciseLinuxLowTimerSlack() { + static thread_local const bool lowSlack = [] { + const long current = prctl(PR_GET_TIMERSLACK, 0UL, 0UL, 0UL, 0UL); + return current == 1L || prctl(PR_SET_TIMERSLACK, 1UL, 0UL, 0UL, 0UL) == 0; + }(); + return lowSlack; +} +#endif + +inline void PreciseSleepFromNanosWithSlack(std::uint64_t startNanos, unsigned long long nanos, bool allowLowSlack) { + if (nanos == 0) { + return; + } + const std::uint64_t deadlineNanos = startNanos + nanos; +#ifdef __linux__ + const bool lowSlack = allowLowSlack && PreciseLinuxLowTimerSlack(); + const unsigned long long spinTailNanos = lowSlack ? 15000ULL : 100000ULL; + if (nanos > spinTailNanos) [[unlikely]] { + const std::uint64_t wakeNanos = deadlineNanos - spinTailNanos; + struct timespec req; + req.tv_sec = static_cast(wakeNanos / 1000000000ULL); + req.tv_nsec = static_cast(wakeNanos % 1000000000ULL); + while (clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &req, nullptr) == EINTR) { + } + } +#else + (void)allowLowSlack; + constexpr unsigned long long kSpinTailNanos = 100000ULL; + if (nanos > kSpinTailNanos) [[unlikely]] { + const unsigned long long sleepNanos = nanos - kSpinTailNanos; + struct timespec req; + req.tv_sec = static_cast(sleepNanos / 1000000000ULL); + req.tv_nsec = static_cast(sleepNanos % 1000000000ULL); + while (nanosleep(&req, &req) == -1 && errno == EINTR) { } } #endif + for (;;) { + const std::uint64_t nowNanos = PreciseMonotonicNanos(); + if (nowNanos >= deadlineNanos) { + break; + } + if (deadlineNanos - nowNanos > 200ULL) { + PreciseSpinPause(); + } + } +} + +inline void PreciseSleepFromNanos(std::uint64_t startNanos, unsigned long long nanos) { + PreciseSleepFromNanosWithSlack(startNanos, nanos, true); +} + +inline void PreciseSleepNanos(unsigned long long nanos) { + if (nanos == 0) { + return; + } + PreciseSleepFromNanos(PreciseMonotonicNanos(), nanos); +} +#endif + +inline constexpr unsigned long long kSubTickWaitUs = 2000; + +inline void PreciseSleepUs(unsigned long long micros) { + if (micros == 0) { + return; + } +#ifdef _WIN32 + PreciseSleepHundredNanos(micros * 10ULL); + return; +#elif !defined(__EMSCRIPTEN__) + PreciseSleepNanos(micros * 1000ULL); + return; +#else std::this_thread::sleep_for(std::chrono::microseconds(micros)); +#endif } template inline void PreciseSleepUntil(const std::chrono::time_point& deadline) { +#ifdef _WIN32 + LARGE_INTEGER counter; + QueryPerformanceCounter(&counter); + const auto now = TClock::now(); + if (now < deadline) { + const auto remainingNs = std::chrono::duration_cast(deadline - now).count(); + if (remainingNs > 0) { + PreciseSleepFromCounter(counter, (static_cast(remainingNs) + 99ULL) / 100ULL); + } + } + for (auto current = TClock::now(); current < deadline; current = TClock::now()) { + if (deadline - current > std::chrono::nanoseconds(200)) { + YieldProcessor(); + } + } +#elif !defined(__EMSCRIPTEN__) + const std::uint64_t startNanos = PreciseMonotonicNanos(); + const auto now = TClock::now(); + if (now < deadline) { + const auto remainingNs = std::chrono::duration_cast(deadline - now).count(); + if (remainingNs > 0) { + PreciseSleepFromNanos(startNanos, static_cast(remainingNs)); + } + } + for (auto current = TClock::now(); current < deadline; current = TClock::now()) { + if (deadline - current > std::chrono::nanoseconds(200)) { + PreciseSpinPause(); + } + } +#else for (auto now = TClock::now(); now < deadline; now = TClock::now()) { PreciseSleepUs(static_cast(std::chrono::ceil(deadline - now).count())); } +#endif } #endif diff --git a/core/libs/tests/PreciseWait.cpp b/core/libs/tests/PreciseWait.cpp index 4089cb4abb..7ca7b91673 100644 --- a/core/libs/tests/PreciseWait.cpp +++ b/core/libs/tests/PreciseWait.cpp @@ -1,14 +1,102 @@ #include "prx/libc/include/PreciseWait.hpp" +#include +#include #include +#include #include int main() { - const auto start = std::chrono::steady_clock::now(); - PreciseSleepUs(50); - const auto elapsed = std::chrono::steady_clock::now() - start; - if (elapsed < std::chrono::microseconds(50)) { - std::cerr << "PreciseSleepUs returned before its requested duration\n"; + static_assert(PreciseHundredNanosToQpcTicks(500, 10000000ULL) == 500); + static_assert(PreciseHundredNanosToQpcTicks(500, 3579545ULL) == 179); + static_assert(PreciseHundredNanosToQpcTicks(500, 14318180ULL) == 716); + static_assert(PreciseHundredNanosToQpcTicks(500, 24000000ULL) == 1200); + static_assert(PrecisePauseThresholdTicks(3579545ULL) == 1); + static_assert(PrecisePauseThresholdTicks(10000000ULL) == 2); + static_assert(PrecisePauseThresholdTicks(24000000ULL) == 5); + + for (const std::uint64_t freq : {3579545ULL, 10000000ULL, 14318180ULL, 19200000ULL, 24000000ULL}) { + for (const unsigned long long hundredNanos : {1ULL, 5ULL, 100ULL, 300ULL, 500ULL, 10000ULL, 53330ULL}) { + const std::uint64_t ticks = PreciseHundredNanosToQpcTicks(hundredNanos, freq); + if (ticks * 10000000ULL < hundredNanos * freq) { + std::cerr << "PreciseHundredNanosToQpcTicks rounded below requested duration\n"; + return 1; + } + } + } + + PreciseSleepUs(0); + + for (const unsigned long long micros : {50ULL, 1000ULL}) { + const auto start = std::chrono::steady_clock::now(); + PreciseSleepUs(micros); + const auto elapsed = std::chrono::steady_clock::now() - start; + if (elapsed < std::chrono::microseconds(micros)) { + std::cerr << "PreciseSleepUs returned before its requested duration\n"; + return 1; + } + } + + std::array samples{}; + for (auto& sample : samples) { + const auto start = std::chrono::steady_clock::now(); + PreciseSleepUs(50); + sample = std::chrono::steady_clock::now() - start; + if (sample < std::chrono::microseconds(50)) { + std::cerr << "PreciseSleepUs(50) returned before its requested duration\n"; + return 1; + } + } + std::sort(samples.begin(), samples.end()); + const auto median = samples[samples.size() / 2]; + if (median >= std::chrono::microseconds(90)) { + std::cerr << "PreciseSleepUs(50) median overslept target bound: " + << std::chrono::duration_cast(median).count() << " ns\n"; return 1; } + + const auto subMicroDeadline = std::chrono::steady_clock::now() + std::chrono::nanoseconds(500); + PreciseSleepUntil(subMicroDeadline); + if (std::chrono::steady_clock::now() < subMicroDeadline) { + std::cerr << "PreciseSleepUntil returned before sub-microsecond deadline\n"; + return 1; + } + + const auto millisecondDeadline = std::chrono::steady_clock::now() + std::chrono::microseconds(1000); + PreciseSleepUntil(millisecondDeadline); + if (std::chrono::steady_clock::now() < millisecondDeadline) { + std::cerr << "PreciseSleepUntil returned before millisecond deadline\n"; + return 1; + } + +#ifdef _WIN32 + for (const HANDLE badHandle : {static_cast(nullptr), GetCurrentProcess()}) { + const auto start = std::chrono::steady_clock::now(); + LARGE_INTEGER counter; + QueryPerformanceCounter(&counter); + PreciseSleepFromCounterWithTimer(counter, 10000ULL, badHandle, true); + const auto elapsed = std::chrono::steady_clock::now() - start; + if (elapsed < std::chrono::microseconds(1000)) { + std::cerr << "PreciseSleepFromCounterWithTimer fallback returned early\n"; + return 1; + } + } +#endif + +#ifdef __linux__ + if (PreciseLinuxLowTimerSlack() && prctl(PR_GET_TIMERSLACK, 0UL, 0UL, 0UL, 0UL) != 1L) { + std::cerr << "PreciseLinuxLowTimerSlack did not configure 1ns timer slack\n"; + return 1; + } + for (const unsigned long long nanos : {80000ULL, 1000000ULL}) { + const auto start = std::chrono::steady_clock::now(); + PreciseSleepFromNanosWithSlack(PreciseMonotonicNanos(), nanos, false); + const auto elapsed = std::chrono::steady_clock::now() - start; + if (elapsed < std::chrono::nanoseconds(nanos)) { + std::cerr << "PreciseSleepFromNanosWithSlack conservative fallback returned early\n"; + return 1; + } + } +#endif + return 0; } From 4cb3271fb2ec2c9c4a84bbe569a3d6626df396f6 Mon Sep 17 00:00:00 2001 From: Jota <139649225+jotitadev@users.noreply.github.com> Date: Wed, 14 Oct 2026 12:10:46 -0300 Subject: [PATCH 0081/1000] docs(build): list Mach-O among relinker-only outputs --- docs/dev/BUILD.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/BUILD.md b/docs/dev/BUILD.md index a3bd542283..a789975326 100644 --- a/docs/dev/BUILD.md +++ b/docs/dev/BUILD.md @@ -14,7 +14,7 @@ Python must be found during CMake configuration for Python tests to be registere This mode builds the conversion tool and its tests on Linux, Windows and macOS, including Apple Silicon. macOS uses AppleClang from the Xcode command-line tools; Windows uses the MinGW-w64 toolchain below. The executable is `build-relinker/core/relinker/relinker` (`relinker.exe` on Windows with Ninja). -The output remains x86-64 Linux ELF or Windows PE. Converted games need system libraries built for the target OS and a compatible x86-64 host. This mode does not build those libraries or provide macOS game execution. Tests inspect both output formats; execution checks run only on their compatible hosts. +The output is x86-64: Linux ELF by default, Windows PE with `--windows` or Mach-O with `--macos` ([usage](../user/USAGE.md#input-and-conversion)). Converted games need system libraries built for the target OS and a compatible x86-64 host. This mode does not build those libraries, and releases ship them for Linux and Windows only. Tests inspect all three output formats; execution checks run only on their compatible hosts. Use a separate build directory for the full build. From 019285d46fd404e3d07d49b170f5fbd1d3a066f4 Mon Sep 17 00:00:00 2001 From: kjhq <74146396+kjhq@users.noreply.github.com> Date: Wed, 14 Oct 2026 14:41:00 +0000 Subject: [PATCH 0082/1000] fix(tools): place an empty segment that ends a load segment in unwrap_self unwrap_self refused a container when a non-load segment with no file data sat exactly at the end of a PT_LOAD, as an empty PT_TLS does in Sifu (PPSA03001): "segment 6 vaddr 0x88cb7a0 is in no PT_LOAD". The eboot and 36 of its 37 bundled modules carry such a segment. A segment of size zero now matches the load segment it ends, so it is placed at that segment's end. --- tools/tests/test_unwrap_self.py | 12 ++++++++++++ tools/unwrap_self.py | 7 ++++--- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/tools/tests/test_unwrap_self.py b/tools/tests/test_unwrap_self.py index 44e1897ccb..57fee3b981 100644 --- a/tools/tests/test_unwrap_self.py +++ b/tools/tests/test_unwrap_self.py @@ -13,6 +13,7 @@ ELF_HEADER_OFFSET = 0x1A0 PROGRAM_TABLE_OFFSET = 0x40 PT_LOAD = 1 PT_DYNAMIC = 2 +PT_TLS = 7 FIRST_REGION = 0x400 HEADER = b"\x7fELF\x02\x01\x01\x09" + b"\x00" * 56 @@ -117,6 +118,17 @@ class UnwrapTest(unittest.TestCase): self.assertEqual(dynamic[2], read_only[2] + (0x1040 - 0x1000)) self.assertEqual(dynamic[2] % 8, 0x1040 % 8) + def test_an_empty_segment_at_the_end_of_a_load_segment_is_placed_after_it(self): + segments = default_segments() + [(PT_TLS, 4, 0x1580, 0x1080, 0x0, 8)] + container = build_container(segments, default_records(), default_payloads()) + result, output = self.convert(container) + self.assertEqual(result.returncode, 0, result.stderr) + data, headers = read_program_headers(output) + read_only = [header for header in headers if header[0] == PT_LOAD][1] + tls = [header for header in headers if header[0] == PT_TLS][0] + self.assertEqual(tls[2], read_only[2] + 0x80) + self.assertEqual(tls[5], 0) + def test_declared_offsets_are_used_when_the_container_has_no_records(self): segments = [(PT_LOAD, 1, 0x800, 0x0, 0x40, 0x1000), (PT_LOAD, 4, 0x900, 0x1000, 0x40, 0x1000)] payloads = {0x800: pattern(3, 0x40), 0x900: pattern(5, 0x40)} diff --git a/tools/unwrap_self.py b/tools/unwrap_self.py index 05af4b508a..c0715feba0 100644 --- a/tools/unwrap_self.py +++ b/tools/unwrap_self.py @@ -127,10 +127,11 @@ def place_segments(data, headers, records): header["source"] = segment break - def load_for(vaddr): + def load_for(vaddr, size): found = None for header in loads: - if header["memory"] and header["vaddr"] <= vaddr < header["vaddr"] + header["memory"]: + end = header["vaddr"] + header["memory"] + if header["memory"] and header["vaddr"] <= vaddr and (vaddr < end or (size == 0 and vaddr == end)): found = header return found @@ -139,7 +140,7 @@ def place_segments(data, headers, records): if header["type"] == PT_LOAD: continue if header["vaddr"] != 0: - host = load_for(header["vaddr"]) + host = load_for(header["vaddr"], header["size"]) if host is None: raise Refused("segment %d vaddr %#x is in no PT_LOAD" % (header["index"], header["vaddr"])) header["host"] = host From f708a73969801f9ac0127fcc139bafebbadd5679 Mon Sep 17 00:00:00 2001 From: Elvis Bruhin Date: Wed, 14 Oct 2026 17:28:07 +0200 Subject: [PATCH 0083/1000] feat(libSceSsl): record connection verify options, minimum version and ALPN sceSslEnableVerifyOption, sceSslDisableVerifyOption, sceSslSetMinSslVersion, sceSslSetAlpn and sceSslGetAlpnSelected kept throwing because their signatures were unknown. SharpProspero, the source already used for the other connection functions, declares them; the signatures follow it. The options, minimum version and ALPN list are recorded on the connection like the verification callback; nothing reads them until sceSslConnect has a transport. sceSslGetAlpnSelected returns null because no handshake completes. Unknown connections and terminated contexts throw. The PPSA07411 functional debt entry moves to libSceJson2.md, now its first linked library. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01SaSmTp7ZiSYcnUMTDVa7hZ --- core/libs/prx/libSceSsl/Export.cpp | 41 ++++++++++++++++++++---------- core/libs/tests/GuestSsl.cpp | 29 +++++++++++++++++++++ docs/dev/debt/libSceJson2.md | 1 + docs/dev/debt/libSceSsl.md | 6 +---- 4 files changed, 59 insertions(+), 18 deletions(-) diff --git a/core/libs/prx/libSceSsl/Export.cpp b/core/libs/prx/libSceSsl/Export.cpp index b12d6bb913..f598565ab5 100644 --- a/core/libs/prx/libSceSsl/Export.cpp +++ b/core/libs/prx/libSceSsl/Export.cpp @@ -158,12 +158,18 @@ struct Context { using SslVerifyCallback = int (APS5_VABI *)(int, unsigned int, void**, int, void*); +constexpr int SslVersionNone = 0; +constexpr int SslVersionTls12 = 5; + struct SslConnection { int context; int socket; std::string host; SslVerifyCallback verifyCallback; void* verifyArgument; + std::uint32_t verifyOptions = 0; + int minimumVersion = SslVersionNone; + std::string alpn; }; struct Runtime { @@ -473,23 +479,37 @@ int APS5_VABI sceSslFreeSslCertName(void) { return 0; } -int APS5_VABI sceSslDisableVerifyOption(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceSslEnableVerifyOption(int sslConnectionId, std::uint32_t option) { + std::lock_guard lock(State().mutex); + RequireConnection(__func__, sslConnectionId).verifyOptions |= option; return 0; } -int APS5_VABI sceSslGetAlpnSelected(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceSslDisableVerifyOption(int sslConnectionId, std::uint32_t option) { + std::lock_guard lock(State().mutex); + RequireConnection(__func__, sslConnectionId).verifyOptions &= ~option; return 0; } -int APS5_VABI sceSslSetAlpn(void) { - NotImplemented_nid_no_patch(__func__); +const char* APS5_VABI sceSslGetAlpnSelected(int sslConnectionId) { + std::lock_guard lock(State().mutex); + RequireConnection(__func__, sslConnectionId); + return nullptr; +} + +int APS5_VABI sceSslSetAlpn(int sslConnectionId, const char* proto) { + if (!proto) throw std::invalid_argument("sceSslSetAlpn: null protocol list"); + std::lock_guard lock(State().mutex); + RequireConnection(__func__, sslConnectionId).alpn = proto; return 0; } -int APS5_VABI sceSslSetMinSslVersion(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceSslSetMinSslVersion(int sslConnectionId, int version) { + if (version < SslVersionNone || version > SslVersionTls12) { + throw std::invalid_argument("sceSslSetMinSslVersion: unknown version " + std::to_string(version)); + } + std::lock_guard lock(State().mutex); + RequireConnection(__func__, sslConnectionId).minimumVersion = version; return 0; } @@ -518,9 +538,4 @@ int APS5_VABI sceSslGetSubjectName(void) { return 0; } -int APS5_VABI sceSslEnableVerifyOption(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - } diff --git a/core/libs/tests/GuestSsl.cpp b/core/libs/tests/GuestSsl.cpp index 1046e7ee55..efda5060a0 100644 --- a/core/libs/tests/GuestSsl.cpp +++ b/core/libs/tests/GuestSsl.cpp @@ -18,6 +18,11 @@ int APS5_VABI sceSslSetVerifyCallback(int, int (APS5_VABI *)(int, unsigned int, int APS5_VABI sceSslConnect(int); int APS5_VABI sceSslRead(int, void*, std::size_t, int); int APS5_VABI sceSslWrite(int, void*, std::size_t, int); +int APS5_VABI sceSslEnableVerifyOption(int, std::uint32_t); +int APS5_VABI sceSslDisableVerifyOption(int, std::uint32_t); +int APS5_VABI sceSslSetMinSslVersion(int, int); +int APS5_VABI sceSslSetAlpn(int, const char*); +const char* APS5_VABI sceSslGetAlpnSelected(int); } struct SslMemoryPoolStats { @@ -103,6 +108,20 @@ int main() { Require(connection > 0 && connection != context); Require(sceSslSetVerifyCallback(connection, VerifyCallback, &marker) == 0); Require(sceSslSetVerifyCallback(connection, nullptr, nullptr) == 0); + constexpr std::uint32_t serverVerify = 0x1; + constexpr std::uint32_t cnCheck = 0x4; + Require(sceSslEnableVerifyOption(connection, serverVerify | cnCheck) == 0); + Require(sceSslDisableVerifyOption(connection, cnCheck) == 0); + Require(sceSslDisableVerifyOption(connection, serverVerify | cnCheck) == 0); + Require(sceSslSetMinSslVersion(connection, 0) == 0); + Require(sceSslSetMinSslVersion(connection, 5) == 0); + Require(Throws([&] { sceSslSetMinSslVersion(connection, -1); })); + Require(Throws([&] { sceSslSetMinSslVersion(connection, 6); })); + Require(sceSslGetAlpnSelected(connection) == nullptr); + Require(sceSslSetAlpn(connection, "http/1.1") == 0); + Require(sceSslSetAlpn(connection, "") == 0); + Require(Throws([&] { sceSslSetAlpn(connection, nullptr); })); + Require(sceSslGetAlpnSelected(connection) == nullptr); char buffer[4]{}; Require(Throws([&] { sceSslConnect(connection); })); Require(Throws([&] { sceSslRead(connection, buffer, sizeof(buffer), 0); })); @@ -110,10 +129,20 @@ int main() { const int unknown = connection + 1000; Require(Throws([&] { sceSslSetVerifyCallback(unknown, nullptr, nullptr); })); + Require(Throws([&] { sceSslEnableVerifyOption(unknown, serverVerify); })); + Require(Throws([&] { sceSslDisableVerifyOption(unknown, serverVerify); })); + Require(Throws([&] { sceSslSetMinSslVersion(unknown, 5); })); + Require(Throws([&] { sceSslSetAlpn(unknown, "http/1.1"); })); + Require(Throws([&] { sceSslGetAlpnSelected(unknown); })); Require(Throws([&] { sceSslDeleteConnection(unknown); })); Require(sceSslTerm_nid_postfix(context) == 0); Require(Throws([&] { sceSslSetVerifyCallback(connection, nullptr, nullptr); })); + Require(Throws([&] { sceSslEnableVerifyOption(connection, serverVerify); })); + Require(Throws([&] { sceSslDisableVerifyOption(connection, serverVerify); })); + Require(Throws([&] { sceSslSetMinSslVersion(connection, 5); })); + Require(Throws([&] { sceSslSetAlpn(connection, "http/1.1"); })); + Require(Throws([&] { sceSslGetAlpnSelected(connection); })); Require(sceSslCreateConnection(context, 0, "example.com") == invalidId); Require(sceSslDeleteConnection(connection) == 0); Require(Throws([&] { sceSslDeleteConnection(connection); })); diff --git a/docs/dev/debt/libSceJson2.md b/docs/dev/debt/libSceJson2.md index 8f71262efb..f3061164b1 100644 --- a/docs/dev/debt/libSceJson2.md +++ b/docs/dev/debt/libSceJson2.md @@ -10,5 +10,6 @@ ### Functional +- PPSA07411 imports declared without parameters, signatures unknown, added without an implementation because the title fails to load without them: [four sce::Json::Value members (setElementAccessFailureHandler, setSpecialFloatHandler, setTypeMismatchHandler, operator bool)](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2), [sceConvertKeycodeGetCharacter](../../../core/libs/prx/libSceConvertKeycode/Export.cpp) (libSceConvertKeycode) and [sceNpSessionSignalingGetGroupFromSessionId](../../../core/libs/prx/libSceNpSessionSignaling/Export.cpp) (libSceNpSessionSignaling) - PPSA30601 imports declared without parameters, signatures unknown, added without an implementation because the title fails to load without them: [sce::Json::Object::insert](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2), whose return type, and so whether a result pointer precedes `this`, is unknown, and [sceMouseResetAbsolutePosition](../../../core/libs/prx/libSceMouse/Export.cpp) (libSceMouse) - [sce::Json::InitParameter2 base-object constructor](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - forwards to the complete-object constructor and uses its inferred layout and default field values; constructor equivalence and initialization bytes remain unverified on the console diff --git a/docs/dev/debt/libSceSsl.md b/docs/dev/debt/libSceSsl.md index f5b9ed5354..d6239318ab 100644 --- a/docs/dev/debt/libSceSsl.md +++ b/docs/dev/debt/libSceSsl.md @@ -6,8 +6,4 @@ - [sceSslClose](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - unknown signature - [sceSslGetSerialNumber](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - unknown signature - [sceSslLoadCert, sceSslGetCaList, sceSslFreeCaList, sceSslGetCaCerts, sceSslFreeCaCerts, sceSslUnloadCert](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - signatures and layouts follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Http.cs). PPSA15508/01.002.000 calls `sceSslLoadCert` with one 1,408-byte PEM CA and null client certificate/key arguments; this confirms that call shape, not console error behavior. Mbed TLS 3.6.7 parses DER/PEM chains and checks an optional client certificate/key pair. Imports are retained per context and committed together; a failed import preserves existing credentials. CA queries return independently owned snapshots; unload clears active credentials while outstanding snapshots remain valid. Appending repeated CA imports, snapshot lifetime across unload, memory overhead and error precedence have not been measured on a PS5. Invalid loads, malformed material, exhausted pools and unsupported keys throw rather than inventing console error codes. No root bundle is shipped; an empty context still returns `NOT_FOUND`. The existing null-list error, unknown-context behavior and legacy clearing of foreign raw CA lists remain unchanged. Certificate readers remain unimplemented. Loading credentials neither starts a TLS connection nor authenticates a peer -- PPSA01317 imports declared without parameters, signatures unknown: [sceSslDisableVerifyOption, sceSslGetAlpnSelected, sceSslSetAlpn, sceSslSetMinSslVersion](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl). Connection storage exists, but their signatures and option behavior remain unknown, so they still throw - -### Functional - -- PPSA07411 imports declared without parameters, signatures unknown, added without an implementation because the title fails to load without them: [sceSslEnableVerifyOption](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl), [four sce::Json::Value members (setElementAccessFailureHandler, setSpecialFloatHandler, setTypeMismatchHandler, operator bool)](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2), [sceConvertKeycodeGetCharacter](../../../core/libs/prx/libSceConvertKeycode/Export.cpp) (libSceConvertKeycode) and [sceNpSessionSignalingGetGroupFromSessionId](../../../core/libs/prx/libSceNpSessionSignaling/Export.cpp) (libSceNpSessionSignaling) +- [sceSslEnableVerifyOption, sceSslDisableVerifyOption, sceSslSetMinSslVersion, sceSslSetAlpn, sceSslGetAlpnSelected](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - imported by PPSA01317 and PPSA07411. Signatures follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Net/Http.cs), the source of the other connection signatures. The verify options, the minimum version and the ALPN protocol list are recorded on the connection; nothing reads them until `sceSslConnect` has a transport, and how the console applies them is unknown. The initial verify options are unknown and taken as 0. `sceSslSetMinSslVersion` accepts the `SceSslVersion` values SharpProspero lists, 0 (none) to 5 (TLS 1.2), and throws for others; whether the console accepts a TLS 1.3 value is unknown. `sceSslSetAlpn` stores the string up to its terminator and throws for a null list; whether the console expects a comma-separated list or the TLS wire format is unknown. `sceSslGetAlpnSelected` returns null, as no handshake completes. An unknown connection and a connection whose context was terminated throw, as for `sceSslSetVerifyCallback` From 5fe47808120df7c612760688754264b66344b481 Mon Sep 17 00:00:00 2001 From: JosepBove Date: Wed, 14 Oct 2026 17:29:25 +0200 Subject: [PATCH 0084/1000] fix(ngs2): return the voice-index error from sceNgs2RackGetVoiceHandle An index at or beyond the rack's voice count returned through APS5_INVALID_ARG_EX, which throws and ends the process. Heretic + Hexen (PPSA26525) creates a one-voice reverb rack, takes voice 0 and asks for voice 1 at start-up, so it stopped at 1.5 s on "sceNgs2RackGetVoiceHandle: invalid argument". Return 0x804A8305 and leave the output handle untouched, after the existing null-output and rack-handle checks. The value follows prosper's analysis of libSceNgs2.native.sprx; shadPS4 lists 0x804A0302 for the PS4 library. Neither is a PS5 measurement, recorded in the technical debt. ngs2_voiceindex creates one-voice sampler, submixer, mastering and reverb racks and a default sampler rack, and checks the error, the untouched output, the null-output and invalid-rack codes and that the last valid index still resolves afterwards. It aborts on main's library with the throw above and passes with this change. --- .../libs/prx/libSceNgs2.native/CMakeLists.txt | 2 +- .../libSceNgs2.native/include/Ngs2Types.hpp | 1 + core/libs/prx/libSceNgs2.native/src/Rack.cpp | 2 +- .../tests/Ngs2VoiceIndex.cpp | 91 +++++++++++++++++++ docs/dev/debt/libSceNgs2.native.md | 4 + 5 files changed, 98 insertions(+), 2 deletions(-) create mode 100644 core/libs/prx/libSceNgs2.native/tests/Ngs2VoiceIndex.cpp diff --git a/core/libs/prx/libSceNgs2.native/CMakeLists.txt b/core/libs/prx/libSceNgs2.native/CMakeLists.txt index ca10bd5243..0f7fec5756 100644 --- a/core/libs/prx/libSceNgs2.native/CMakeLists.txt +++ b/core/libs/prx/libSceNgs2.native/CMakeLists.txt @@ -2,7 +2,7 @@ include(${CMAKE_CURRENT_SOURCE_DIR}/Ngs2Library.cmake) add_sce_ngs2_library(libSceNgs2.native) if(BUILD_TESTING) - foreach(ngs2Test IN ITEMS Render Atrac9 Filter Custom Parse Stream UserFx Report Geom) + foreach(ngs2Test IN ITEMS Render Atrac9 Filter Custom Parse Stream UserFx Report Geom VoiceIndex) string(TOLOWER ${ngs2Test} testName) add_test_executable(ngs2_${testName}_tests ${CMAKE_CURRENT_SOURCE_DIR}/tests/Ngs2${ngs2Test}.cpp) target_include_directories(ngs2_${testName}_tests PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/3rdparty/LibAtrac9/C/src) diff --git a/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp b/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp index ca1b035a39..ebd39fe595 100644 --- a/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp +++ b/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp @@ -11,6 +11,7 @@ static constexpr int SCE_NGS2_ERROR_INVALID_OUT_ADDRESS = static_cast(0x804 static constexpr int SCE_NGS2_ERROR_INVALID_OUT_SIZE = static_cast(0x804A8011); static constexpr int SCE_NGS2_ERROR_INVALID_SYSTEM_HANDLE = static_cast(0x804A0230); static constexpr int SCE_NGS2_ERROR_INVALID_RACK_HANDLE = static_cast(0x804A0261); +static constexpr int SCE_NGS2_ERROR_INVALID_VOICE_INDEX = static_cast(0x804A8305); static constexpr int SCE_NGS2_ERROR_INVALID_WAVEFORM_DATA = static_cast(0x804A8430); static constexpr int SCE_NGS2_ERROR_INVALID_WAVEFORM_FORMAT = static_cast(0x804A8431); static constexpr int SCE_NGS2_ERROR_UNKNOWN_WAVEFORM_FORMAT = static_cast(0x804A8432); diff --git a/core/libs/prx/libSceNgs2.native/src/Rack.cpp b/core/libs/prx/libSceNgs2.native/src/Rack.cpp index a9df1495ce..50f9c9ef2a 100644 --- a/core/libs/prx/libSceNgs2.native/src/Rack.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Rack.cpp @@ -204,7 +204,7 @@ int APS5_VABI sceNgs2RackGetVoiceHandle(uintptr_t rack_handle, uint32_t voice_id std::lock_guard lock(Ngs2Mutex()); auto* rack = Ngs2FindRack(rack_handle); if (rack == nullptr) return SCE_NGS2_ERROR_INVALID_RACK_HANDLE; - if (voice_id >= rack->voices.size()) APS5_INVALID_ARG_EX; + if (voice_id >= rack->voices.size()) return SCE_NGS2_ERROR_INVALID_VOICE_INDEX; *handle = reinterpret_cast(&rack->voices[voice_id]); return SCE_NGS2_OK; } diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2VoiceIndex.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2VoiceIndex.cpp new file mode 100644 index 0000000000..2b7453a1a0 --- /dev/null +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2VoiceIndex.cpp @@ -0,0 +1,91 @@ +#include "Ngs2Test.hpp" + +#include +#include +#include + +namespace { + +union RackOption { + Ngs2RackOption common; + Ngs2SamplerRackOption sampler; + Ngs2SubmixerRackOption submixer; + Ngs2MasteringRackOption mastering; + Ngs2ReverbRackOption reverb; +}; + +std::size_t OptionSize(std::uint32_t rackId) { + switch (rackId) { + case SCE_NGS2_RACK_ID_SAMPLER: return sizeof(Ngs2SamplerRackOption); + case SCE_NGS2_RACK_ID_SUBMIXER: return sizeof(Ngs2SubmixerRackOption); + case SCE_NGS2_RACK_ID_MASTERING: return sizeof(Ngs2MasteringRackOption); + default: return sizeof(Ngs2ReverbRackOption); + } +} + +uintptr_t CreateSingleVoiceRack(uintptr_t system, std::uint32_t rackId) { + RackOption option{}; + option.common.size = OptionSize(rackId); + option.common.max_grain_samples = 512; + option.common.max_voices = 1; + option.common.max_input_delay_blocks = 1; + option.common.max_matrices = 1; + option.common.max_ports = 8; + switch (rackId) { + case SCE_NGS2_RACK_ID_SAMPLER: + option.sampler.max_channel_works = 1; + option.sampler.max_waveform_blocks = 1; + break; + case SCE_NGS2_RACK_ID_SUBMIXER: + option.submixer.max_channels = 2; + option.submixer.max_inputs = 1; + break; + case SCE_NGS2_RACK_ID_MASTERING: + option.mastering.max_channels = 2; + break; + default: + option.reverb.max_channels = 2; + option.reverb.reverb_size = 1; + break; + } + Ngs2ContextBufferInfo query{}; + Require(sceNgs2RackQueryBufferSize(rackId, &option.common, &query) == SCE_NGS2_OK); + const auto info = Buffer(query); + uintptr_t rack = 0; + Require(sceNgs2RackCreate(system, rackId, &option.common, &info, &rack) == SCE_NGS2_OK && rack != 0); + return rack; +} + +void CheckOutOfRange(uintptr_t rack, std::uint32_t maxVoices) { + uintptr_t last = 0; + Require(sceNgs2RackGetVoiceHandle(rack, maxVoices - 1, &last) == SCE_NGS2_OK && last != 0); + for (const auto index : std::array{maxVoices, maxVoices + 1, std::numeric_limits::max()}) { + uintptr_t output = 0x5a5a; + Require(sceNgs2RackGetVoiceHandle(rack, index, &output) == SCE_NGS2_ERROR_INVALID_VOICE_INDEX); + Require(output == 0x5a5a); + } + Require(sceNgs2RackGetVoiceHandle(rack, maxVoices, nullptr) == SCE_NGS2_ERROR_INVALID_OUT_ADDRESS); + uintptr_t again = 0; + Require(sceNgs2RackGetVoiceHandle(rack, maxVoices - 1, &again) == SCE_NGS2_OK && again == last); +} + +} + +int main() { + const auto system = CreateSystem(); + for (const auto rackId : std::array{SCE_NGS2_RACK_ID_SAMPLER, SCE_NGS2_RACK_ID_SUBMIXER, + SCE_NGS2_RACK_ID_MASTERING, SCE_NGS2_RACK_ID_REVERB}) { + const auto rack = CreateSingleVoiceRack(system, rackId); + CheckOutOfRange(rack, 1); + Require(sceNgs2RackDestroy(rack, nullptr) == SCE_NGS2_OK); + } + const auto sampler = CreateRack(system, SCE_NGS2_RACK_ID_SAMPLER); + Ngs2RackInfo info{}; + Require(sceNgs2RackGetInfo(sampler, &info, sizeof(info)) == SCE_NGS2_OK && info.max_voices == 256); + CheckOutOfRange(sampler, info.max_voices); + uintptr_t output = 0x5a5a; + Require(sceNgs2RackGetVoiceHandle(0x1234, 0, &output) == SCE_NGS2_ERROR_INVALID_RACK_HANDLE && output == 0x5a5a); + Require(sceNgs2RackDestroy(sampler, nullptr) == SCE_NGS2_OK); + Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); + std::puts("NGS2 voice index: PASS"); +} diff --git a/docs/dev/debt/libSceNgs2.native.md b/docs/dev/debt/libSceNgs2.native.md index 5416c84150..16171e4270 100644 --- a/docs/dev/debt/libSceNgs2.native.md +++ b/docs/dev/debt/libSceNgs2.native.md @@ -26,3 +26,7 @@ - [sceNgs2GeomCalcListener](../../../core/libs/prx/libSceNgs2.native/src/Geom.cpp) supports only a listener at the origin with front +Z, up +Y and flags 0. The identity matrix and preserved finite velocity and positive finite sound speed follow [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/df5b9b92be7dfd463b47756b707d6d356a9f0550/src/libs/ngs2.cpp) and [PS5PCEM](https://github.com/iStark/PS5PCEM/blob/d2534bac6325a2c61c0fa88977e5321074304484/src/hle/ngs2_geom.zig) for that case, not a PS5 measurement. Other orientations (including non-unit vectors), translations and flags throw without changing the output; the matrix convention, handedness flags and console error codes remain unverified. Nonfinite input and nonpositive sound speed also throw. Dead Cells (PPSA15552) reaches this call after the custom sampler rack in #4334, but its arguments have not been captured here. Source attenuation, panning and Doppler in `sceNgs2GeomApply` remain unimplemented - [NGS2 custom sampler rack](../../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (libSceNgs2.native, rack 0x4001) - implements bounded support for the custom sampler rack and its built-in module 0x13 (Custom Voice Filter), unblocking PPSA15552 (Dead Cells) startup. Structure layouts come from [shadPS4's ngs2_custom.h](https://raw.githubusercontent.com/shadps4-emu/shadPS4/main/src/core/libraries/ngs2/ngs2_custom.h) (OrbisNgs2CustomVoiceFilterParam, 40 bytes) and live trace correlation from [prosper's hle_audio.cpp](https://raw.githubusercontent.com/mattias800/prosper/main/prosper/src/hle/audio/hle_audio.cpp) (0x40001300 command and parameters like 10000.0f and 0.707f). Routes parameter updates via the 0x40001300 command family into the Biquad filter engine, supporting only filter type 1; other filter types and unmodeled configurations throw. - [sceNgs2VoiceGetState custom sampler](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native, rack 0x4001) - only the size and one field come from a title. PPSA15552 (Dead Cells) calls it on custom sampler voices with a 48-byte buffer at two sites (eboot `0x1740f90`, `0x1741009`) and reads only the 64-bit value at offset 16: the first site keeps it and the second subtracts the kept value, so it is filled with the voice's decoded sample count. Other sizes return `INVALID_OUT_SIZE` as for the other racks. The state flags fill the common voice state; the remaining fields stay zero. shadPS4's `OrbisNgs2CustomSamplerVoiceState` is about 80 bytes and does not match the size this title passes, so the field names and the console's value are unverified + +### Functional + +- [sceNgs2RackGetVoiceHandle voice index](../../../core/libs/prx/libSceNgs2.native/src/Rack.cpp) (libSceNgs2.native) - an index at or beyond the rack's voice count returns `0x804A8305` and leaves the output handle untouched, after the null-output and rack-handle checks that keep their order and codes. The value follows [prosper's analysis of `libSceNgs2.native.sprx`](https://github.com/mattias800/prosper/blob/d61eb2c0ad2de879ef74df9c4aa050c512804a18/prosper/src/hle/audio/hle_audio.cpp#L4299) (export `MwmHz8pAdAo` at `0xfbd0`, bounds error at `0xfc73`), which also resolves the rack before the output check; [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/e6faa2e3b6794a2fa79a709352ddfccfaa817ed1/src/core/libraries/ngs2/ngs2_error.h) lists `0x804A0302` for the PS4 library. Neither is a PS5 measurement. Heretic + Hexen (PPSA26525) and STAR WARS: Dark Forces Remaster (PPSA19728) create a one-voice reverb rack, take voice 0 and ask for voice 1 at start-up, log the failure and go on From 4e82f15c9f718aa968c81a6ad6cb7f0099e7cf2e Mon Sep 17 00:00:00 2001 From: Ondra97 <47064087+Ondra97@users.noreply.github.com> Date: Wed, 14 Oct 2026 12:58:02 +0200 Subject: [PATCH 0085/1000] fix(agc): validate mesh index descriptors on cached draws --- .../include/Driver/Dispatch/DispatchCache.hpp | 1 + .../Execution/src/Driver/Draw/DrawCache.cpp | 2 +- .../Execution/src/Driver/Draw/DrawCapture.cpp | 3 + .../Execution/src/Driver/Draw/DrawLookup.cpp | 6 + core/libs/prx/libSceAgcDriver/tests/Mesh.cpp | 17 +- .../tests/MeshGeometryFixture.hpp | 27 ++++ .../tests/execution/MeshIndexCache.cpp | 145 ++++++++++++++++++ 7 files changed, 185 insertions(+), 16 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/MeshGeometryFixture.hpp create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/MeshIndexCache.cpp diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Dispatch/DispatchCache.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Dispatch/DispatchCache.hpp index 1591221fdf..8d5385215e 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Dispatch/DispatchCache.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Dispatch/DispatchCache.hpp @@ -39,6 +39,7 @@ struct DispatchVariant { std::uint32_t flatBinding = NoFlatBinding; std::uint32_t pushOffset = 0; + std::array meshIndexDescriptor{}; std::shared_ptr vertexInfo; std::shared_ptr memory; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCache.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCache.cpp index bf7cf368bf..30ec439503 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCache.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCache.cpp @@ -103,7 +103,7 @@ void Driver::insertDrawEntry(std::uint64_t key, std::vectorstages[i]; - const auto present = std::find_if(variants.begin(), variants.end(), [&](const std::shared_ptr& kept) { return kept->pushOffset == fresh[i]->pushOffset && kept->runs == fresh[i]->runs && kept->words == fresh[i]->words; }); + const auto present = std::find_if(variants.begin(), variants.end(), [&](const std::shared_ptr& kept) { return kept->pushOffset == fresh[i]->pushOffset && kept->meshIndexDescriptor == fresh[i]->meshIndexDescriptor && kept->runs == fresh[i]->runs && kept->words == fresh[i]->words; }); if (present != variants.end()) { ++counters.present; fresh[i] = *present; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCapture.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCapture.cpp index 4f3db831ac..622f582d5d 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCapture.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCapture.cpp @@ -93,6 +93,9 @@ void Driver::cacheDrawStages(bool useDrawEntries, bool drawHit, const Pm4::DrawP variant->shader = programs[i].snapshot; variant->forgetSerial = stageCapture.forgetSerial; variant->pushOffset = stageCapture.pushOffset; + if (programs[i].binary.stage == ShaderRecompiler::ShaderStage::Mesh) { + std::copy_n(programs[i].userData.begin() + ShaderRecompiler::MeshIndexBufferUserWord, variant->meshIndexDescriptor.size(), variant->meshIndexDescriptor.begin()); + } if (vertexInfos[i]) variant->vertexInfo = std::make_shared(*vertexInfos[i]); std::vector regions(stageCapture.regions.begin(), stageCapture.regions.end()); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp index 40b6eeef9d..5812215a5b 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp @@ -1,6 +1,7 @@ #include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include namespace AgcDriver::DriverDetail { @@ -101,6 +102,11 @@ void Driver::lookupDraw(const Submission& submission, const std::shared_ptrpushOffset != cursor) continue; + if (programs[i].binary.stage == ShaderRecompiler::ShaderStage::Mesh && !std::equal(variant->meshIndexDescriptor.begin(), variant->meshIndexDescriptor.end(), programs[i].userData.begin() + ShaderRecompiler::MeshIndexBufferUserWord)) { + if (!anyLayout) outcome = EntryOutcome::Differing; + anyLayout = true; + continue; + } auto& regions = matchedRegions[i]; regions.clear(); appendEntryRegions(*variant, regions); diff --git a/core/libs/prx/libSceAgcDriver/tests/Mesh.cpp b/core/libs/prx/libSceAgcDriver/tests/Mesh.cpp index ac2fff1057..9a9b548f49 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Mesh.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Mesh.cpp @@ -1,5 +1,6 @@ #include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" #include "Recompiler.hpp" +#include "MeshGeometryFixture.hpp" #include "execution/VulkanTestDevice.hpp" #include "prx/libc/include/GuestAllocations.hpp" #include @@ -24,21 +25,7 @@ constexpr std::uint32_t Height = 128; constexpr std::array Background{16, 24, 40, 255}; alignas(256) std::array Pixels{}; -alignas(256) constexpr std::array GeometryCode{ - 0x8f6a9003, 0x94fe6ac1, 0xbf88000b, 0xd7650006, 0x000100c1, 0xd7660006, 0x00020cc1, 0x93ebff03, - 0x00040018, 0xd7460006, 0x04190c6b, 0x340c0c82, 0xd8340000, 0x00000506, 0xbf8cc07f, 0xbefe04c1, - 0xbf8a0000, 0x938dff02, 0x00090016, 0x9382ff03, 0x00040018, 0x938cff03, 0x00080008, 0xd7650009, - 0x000100c1, 0xd7660009, 0x000212c1, 0xd746000a, 0x04250c02, 0x7da8120c, 0xbf88002e, 0x361600ff, - 0x0000ffff, 0x2c180090, 0x361a02ff, 0x0000ffff, 0xd8d80000, 0x0b00000b, 0xd8d80000, 0x0c00000c, - 0xd8d80000, 0x0d00000d, 0xbf8cc07f, 0xe0382000, 0x8002100b, 0xe0382010, 0x8002140b, 0xe0382000, - 0x8002180c, 0xe0382010, 0x80021c0c, 0xe0382000, 0x8002200d, 0xe0382010, 0x8002240d, 0x161c1483, - 0x161e14ff, 0x00000060, 0xbf8c3f70, 0xdb7c0400, 0x0000100f, 0xdb7c0410, 0x0000140f, 0xdb7c0420, - 0x0000180f, 0xdb7c0430, 0x00001c0f, 0xdb7c0440, 0x0000200f, 0xdb7c0450, 0x0000240f, 0x4a501c81, - 0x4a521c82, 0x3450508a, 0x34525294, 0xd772002a, 0x04a6510e, 0xbf8cc07f, 0xbefe04c1, 0xbf8a0000, - 0x930e830d, 0xbf078002, 0xbf850003, 0x8f0f8c0d, 0x887c0f0e, 0xbf900009, 0x7da8140d, 0xbf880002, - 0xf8000941, 0x0000002a, 0xbefe04c1, 0x7da8140e, 0xbf88000a, 0x34561485, 0xdbfc0400, 0x2c00002b, - 0xdbfc0410, 0x3000002b, 0xbf8cc07f, 0xf80008cf, 0x2f2e2d2c, 0xf800020f, 0x33323130, 0xbf810000, -}; +constexpr auto& GeometryCode = AgcDriver::Test::MeshGeometryCode; alignas(256) constexpr auto Wave32GeometryCode = [] { auto code = GeometryCode; diff --git a/core/libs/prx/libSceAgcDriver/tests/MeshGeometryFixture.hpp b/core/libs/prx/libSceAgcDriver/tests/MeshGeometryFixture.hpp new file mode 100644 index 0000000000..0c59ec020a --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/MeshGeometryFixture.hpp @@ -0,0 +1,27 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_TESTS_MESHGEOMETRYFIXTURE_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_TESTS_MESHGEOMETRYFIXTURE_HPP + +#include +#include + +namespace AgcDriver::Test { + +alignas(256) inline constexpr std::array MeshGeometryCode{ + 0x8f6a9003, 0x94fe6ac1, 0xbf88000b, 0xd7650006, 0x000100c1, 0xd7660006, 0x00020cc1, 0x93ebff03, + 0x00040018, 0xd7460006, 0x04190c6b, 0x340c0c82, 0xd8340000, 0x00000506, 0xbf8cc07f, 0xbefe04c1, + 0xbf8a0000, 0x938dff02, 0x00090016, 0x9382ff03, 0x00040018, 0x938cff03, 0x00080008, 0xd7650009, + 0x000100c1, 0xd7660009, 0x000212c1, 0xd746000a, 0x04250c02, 0x7da8120c, 0xbf88002e, 0x361600ff, + 0x0000ffff, 0x2c180090, 0x361a02ff, 0x0000ffff, 0xd8d80000, 0x0b00000b, 0xd8d80000, 0x0c00000c, + 0xd8d80000, 0x0d00000d, 0xbf8cc07f, 0xe0382000, 0x8002100b, 0xe0382010, 0x8002140b, 0xe0382000, + 0x8002180c, 0xe0382010, 0x80021c0c, 0xe0382000, 0x8002200d, 0xe0382010, 0x8002240d, 0x161c1483, + 0x161e14ff, 0x00000060, 0xbf8c3f70, 0xdb7c0400, 0x0000100f, 0xdb7c0410, 0x0000140f, 0xdb7c0420, + 0x0000180f, 0xdb7c0430, 0x00001c0f, 0xdb7c0440, 0x0000200f, 0xdb7c0450, 0x0000240f, 0x4a501c81, + 0x4a521c82, 0x3450508a, 0x34525294, 0xd772002a, 0x04a6510e, 0xbf8cc07f, 0xbefe04c1, 0xbf8a0000, + 0x930e830d, 0xbf078002, 0xbf850003, 0x8f0f8c0d, 0x887c0f0e, 0xbf900009, 0x7da8140d, 0xbf880002, + 0xf8000941, 0x0000002a, 0xbefe04c1, 0x7da8140e, 0xbf88000a, 0x34561485, 0xdbfc0400, 0x2c00002b, + 0xdbfc0410, 0x3000002b, 0xbf8cc07f, 0xf80008cf, 0x2f2e2d2c, 0xf800020f, 0x33323130, 0xbf810000, +}; + +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/MeshIndexCache.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/MeshIndexCache.cpp new file mode 100644 index 0000000000..e03f45cd77 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/MeshIndexCache.cpp @@ -0,0 +1,145 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" +#include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" +#include "../MeshGeometryFixture.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using AgcDriver::DriverDetail::DrawMiss; + +constexpr std::uint32_t Width = 64; +constexpr std::uint32_t Height = 64; +alignas(256) std::array Pixels{}; +alignas(256) constexpr std::array Indices{0, 1, 2, 3, 4, 5, 0, 0, 0, 0}; +alignas(256) constexpr std::array RightIndices{3, 4, 5, 0}; +alignas(256) constexpr std::array, 6> Vertices{{ + {-0.9f, -0.8f, 0, 1, 1, 1, 1, 1}, {-0.1f, -0.8f, 0, 1, 1, 1, 1, 1}, {-0.5f, 0.8f, 0, 1, 1, 1, 1, 1}, + {0.1f, -0.8f, 0, 1, 1, 1, 1, 1}, {0.9f, -0.8f, 0, 1, 1, 1, 1, 1}, {0.5f, 0.8f, 0, 1, 1, 1, 1, 1} +}}; +alignas(256) constexpr std::array PixelCode{0x7e0e02f2u, 0xf800180fu, 0x07070707u, 0xbf810000u}; + +struct Header { + Shader shader{}; + ShaderUserData users{}; +}; + +Header GeometryHeader{}; +Header PixelHeader{}; + +void Register(Header& header, const std::uint32_t* code, std::size_t size, std::uint8_t type) { + header.shader.file_header = 0x34333231u; + header.shader.version = 0x18u; + header.shader.header_size = sizeof(header); + header.shader.shader_size = static_cast(size); + header.shader.code = code; + header.shader.type = type; + header.shader.user_data = &header.users; + AgcDriverRegisterShader_nid_postfix(&header.shader); +} + +void Write(std::vector& commands, std::uint32_t opcode, std::uint32_t offset, std::uint32_t value) { + commands.insert(commands.end(), {0xc0010000u | (opcode << 8u), offset, value}); +} + +std::vector Setup() { + const auto color = reinterpret_cast(Pixels.data()); + const auto geometry = reinterpret_cast(AgcDriver::Test::MeshGeometryCode.data()); + const auto pixel = reinterpret_cast(PixelCode.data()); + const auto vertices = reinterpret_cast(Vertices.data()); + const std::vector> context{ + {0x2d5, 0x20}, {0x1ff, 12}, {0x2ce, 3}, {0x29b, 2}, {0x2ab, 4}, {0x260, 0}, + {0x1b6, 0}, {0x207, 0}, {0x200, 0}, {0x203, 0x800}, + {0x2dc, 0xaa00}, {0x2f8, 0}, {0x292, 2}, {0x293, 0}, + {0x80, 0}, {0x8d, 0}, {0x83, 0xffff}, {0x8c, 0xa}, + {0x2f9, 0x2d}, {0x313, 0x6000}, {0x30e, 0xffffffff}, {0x30f, 0xffffffff}, + {0x206, 0x43f}, {0x204, 0x80000}, {0x205, 0x240}, + {0x8e, 0xf}, {0x8f, 0xf}, {0x202, 0xcc0010}, + {0x1c4, 0}, {0x1c5, 9}, {0x1c3, 4}, {0x31c, 0x28028}, + {0x31b, 0}, {0x31d, 0}, {0x3b0, ((Height - 1u) << 14u) | (Width - 1u)}, + {0x3b8, 0x9000000}, {0x1e0, 0}, {0xc, 0}, {0xd, (Height << 16u) | Width}, + {0x81, 0x80000000}, {0x82, (Height << 16u) | Width}, + {0x90, 0x80000000}, {0x91, (Height << 16u) | Width}, + {0x94, 0x80000000}, {0x95, (Height << 16u) | Width}, + {0x318, static_cast(color >> 8u)}, {0x390, static_cast(color >> 40u)}, + {0x10f, std::bit_cast(32.0f)}, {0x110, std::bit_cast(32.0f)}, + {0x111, std::bit_cast(32.0f)}, {0x112, std::bit_cast(32.0f)}, + {0x113, std::bit_cast(1.0f)}, {0x114, 0}, + {0xb4, 0}, {0xb5, std::bit_cast(1.0f)} + }; + const std::vector> shaders{ + {0xc8, static_cast(geometry >> 8u)}, {0xc9, static_cast(geometry >> 40u)}, + {0x8a, 0x600c0000u}, {0x8b, (8u << 19u) | (3u << 16u) | (4u << 1u)}, + {0x8c, static_cast(vertices)}, {0x8d, static_cast(vertices >> 32u) | (32u << 16u)}, + {0x8e, static_cast(Vertices.size())}, {0x8f, 0x01016facu}, + {0x8, static_cast(pixel >> 8u)}, {0x9, static_cast(pixel >> 40u)}, {0xb, 0} + }; + std::vector commands; + Write(commands, 0x79, 0x242, 4); + Write(commands, 0x79, 0x25b, (12u << 9u) | 4u); + for (const auto& [offset, value] : context) Write(commands, 0x69, offset, value); + for (const auto& [offset, value] : shaders) Write(commands, 0x76, offset, value); + commands.insert(commands.end(), {0xc0002a00u, 0u}); + return commands; +} + +void Draw(const std::uint16_t* indices, std::uint32_t count, bool left, bool right) { + Pixels.fill(std::byte{0}); + auto commands = Setup(); + const auto address = reinterpret_cast(indices); + commands.insert(commands.end(), {0xc0042700u, count, static_cast(address), static_cast(address >> 32u), count, 0u}); + Packet packet{commands.data(), static_cast(commands.size()), 0, {}}; + Require(sceAgcDriverSubmitDcb(&packet) == 0, "mesh submission failed"); + AgcDriverWaitIdle_nid_postfix(); + const auto lit = [](std::uint32_t x) { return std::to_integer(Pixels[(24u * Width + x) * 4u]) > 200u; }; + Require(lit(16) == left && lit(48) == right, "cached mesh draw used another draw's indices: count=" + std::to_string(count)); +} + +} + +int main() { + try { + { + const auto device = OpenVulkanTestDevice(); + if (!device || !TargetHasCapability(device->Target(), spv::CapabilityMeshShadingEXT)) return VulkanTestSkipped; + } + Register(GeometryHeader, AgcDriver::Test::MeshGeometryCode.data(), sizeof(AgcDriver::Test::MeshGeometryCode), 2); + Register(PixelHeader, PixelCode.data(), sizeof(PixelCode), 1); + for (unsigned i = 0; i < 3; ++i) Draw(Indices.data(), 3, true, false); + const auto warm = AgcDriver::DriverDetail::Driver::Get().DrawCacheCounters(); + Require(warm.hits != 0, "mesh warm-up did not reach the draw cache"); + const auto front = static_cast(DrawMiss::FrontDiffering); + const auto layout = static_cast(DrawMiss::Layout); + Draw(Indices.data(), 6, true, true); + const auto range = AgcDriver::DriverDetail::Driver::Get().DrawCacheCounters(); + Require(range.misses[front] == warm.misses[front] + 1 && range.misses[layout] == warm.misses[layout], "an index-range change was not counted as a differing front stage"); + Draw(Indices.data(), 6, true, true); + const auto rangeReuse = AgcDriver::DriverDetail::Driver::Get().DrawCacheCounters(); + Require(rangeReuse.hits == range.hits + 1, "the changed index range was not retained for cache reuse"); + Draw(RightIndices.data(), 3, false, true); + const auto address = AgcDriver::DriverDetail::Driver::Get().DrawCacheCounters(); + Require(address.misses[front] == rangeReuse.misses[front] + 1 && address.misses[layout] == rangeReuse.misses[layout], "an index-address change was not counted as a differing front stage"); + Draw(RightIndices.data(), 3, false, true); + const auto addressReuse = AgcDriver::DriverDetail::Driver::Get().DrawCacheCounters(); + Require(addressReuse.hits == address.hits + 1, "the changed index address was not retained for cache reuse"); + Draw(Indices.data(), 3, true, false); + const auto earlier = AgcDriver::DriverDetail::Driver::Get().DrawCacheCounters(); + Require(earlier.hits == addressReuse.hits + 1, "returning to an earlier index descriptor did not hit the cache"); + AgcDriverShutdown_nid_postfix(); + std::cout << "Cached mesh draws retained their index ranges and addresses\n"; + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + AgcDriverShutdown_nid_postfix(); + return 1; + } +} From ed26cb81a88939a00e3613f9ed2cd7ca32f0b51e Mon Sep 17 00:00:00 2001 From: Ondra97 <47064087+Ondra97@users.noreply.github.com> Date: Wed, 14 Oct 2026 15:51:14 +0200 Subject: [PATCH 0086/1000] fix(agc): keep mesh cache metadata separate from relocation fields --- .../Execution/include/Driver/Dispatch/DispatchCache.hpp | 2 +- .../Execution/src/Driver/Draw/DrawCapture.cpp | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Dispatch/DispatchCache.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Dispatch/DispatchCache.hpp index 8d5385215e..a6e52b4e00 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Dispatch/DispatchCache.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Dispatch/DispatchCache.hpp @@ -28,6 +28,7 @@ struct DispatchVariant { std::vector words; std::uint64_t forgetSerial = 0; std::shared_ptr compiled; + std::array meshIndexDescriptor{}; std::shared_ptr shader; @@ -39,7 +40,6 @@ struct DispatchVariant { std::uint32_t flatBinding = NoFlatBinding; std::uint32_t pushOffset = 0; - std::array meshIndexDescriptor{}; std::shared_ptr vertexInfo; std::shared_ptr memory; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCapture.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCapture.cpp index 622f582d5d..2e1cd92b50 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCapture.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCapture.cpp @@ -90,12 +90,12 @@ void Driver::cacheDrawStages(bool useDrawEntries, bool drawHit, const Pm4::DrawP if (stageCapture.compiled == nullptr || !CacheableResult(*stageCapture.compiled)) continue; auto variant = std::make_shared(); variant->compiled = stageCapture.compiled; - variant->shader = programs[i].snapshot; - variant->forgetSerial = stageCapture.forgetSerial; - variant->pushOffset = stageCapture.pushOffset; if (programs[i].binary.stage == ShaderRecompiler::ShaderStage::Mesh) { std::copy_n(programs[i].userData.begin() + ShaderRecompiler::MeshIndexBufferUserWord, variant->meshIndexDescriptor.size(), variant->meshIndexDescriptor.begin()); } + variant->shader = programs[i].snapshot; + variant->forgetSerial = stageCapture.forgetSerial; + variant->pushOffset = stageCapture.pushOffset; if (vertexInfos[i]) variant->vertexInfo = std::make_shared(*vertexInfos[i]); std::vector regions(stageCapture.regions.begin(), stageCapture.regions.end()); From 032dea0aabbf26cf8744934afd9c1b8fa7faff76 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Wed, 14 Oct 2026 08:37:48 -0700 Subject: [PATCH 0087/1000] fix(libSceNet): read SO_RCVTIMEO and SO_SNDTIMEO as a timeval in sceNetSetsockopt sceNetSetsockopt took the BSD timeout options, SO_SNDTIMEO (0x1005) and SO_RCVTIMEO (0x1006), like Sony's 0x1105 and 0x1106: as an int of microseconds read from the first four bytes. A caller passing a struct timeval got tv_sec taken as microseconds, so {0, 100000} meant to wait 100 ms became 0 and disabled the timeout, and the receive blocked until data came. On the console 0x1006 takes a timeval, as FreeBSD 11.4's sosetopt does. ProsperoLight (PPSA99002) gives it {0, 100000} through sceNetSetsockopt, and its log from a console (firmware 12.20) shows Moonlight's receive timing out every 100 ms before the first packet ("Received first video packet after 400 ms"). Sony moved only these two timeval options to 0x1105 and 0x1106, as ints. On AnyPS5 the receive never times out, so ProsperoLight's receive threads never wake when the PC stops sending and LiStopConnection never returns: the stream screen freezes when a session ends from the PC side. Steam Remote Play (PPSA99350) sets {0, 10000} the same way and stays on "THE PC DID NOT ANSWER" when the PC doesn't reply. 0x1005 and 0x1006 now go through the timeval path that setsockopt() on a libSceNet socket already uses (EINVAL for a value shorter than a timeval, EDOM for a negative or out-of-range one). sceNetGetsockopt gives them back as a timeval cut to the caller's length, as FreeBSD's sooptcopyout does. 0x1105 and 0x1106 keep taking an int of microseconds. --- core/libs/prx/libSceNet/Export.cpp | 14 +++++++++-- core/libs/tests/GuestSceNet.cpp | 37 ++++++++++++++++++++++-------- 2 files changed, 39 insertions(+), 12 deletions(-) diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index e03f88e831..0302256900 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -1009,6 +1009,10 @@ int APS5_VABI sceNetSetsockopt(int s, int level, int optname, const void* optval socket = it->second; } if (level != NET_SOL_SOCKET) return fail(NET_EOPNOTSUPP); + if (optname == NET_SO_RCVTIMEO || optname == NET_SO_SNDTIMEO) { + const int error = set_kernel_timeout(s, optname, optval, optlen); + return error == 0 ? 0 : fail(error); + } optname = timeout_option(optname); if (!optval || optlen < sizeof(int)) return fail(NET_EINVAL); int value = 0; @@ -1050,9 +1054,15 @@ int APS5_VABI sceNetGetsockopt(int s, int level, int optname, void* optval, uint if (it == g_socks.end()) return fail(NET_EBADF); socket = it->second; } - if (optval == nullptr || optlen == nullptr || *optlen < sizeof(int)) { - return fail(NET_EINVAL); + if (optval == nullptr || optlen == nullptr) return fail(NET_EINVAL); + if (level == NET_SOL_SOCKET && (optname == NET_SO_RCVTIMEO || optname == NET_SO_SNDTIMEO)) { + const int microseconds = optname == NET_SO_RCVTIMEO ? socket.rcv_timeout_us : socket.snd_timeout_us; + const KernelTimeval timeout{microseconds / 1000000, microseconds % 1000000}; + *optlen = std::min(*optlen, static_cast(sizeof(timeout))); + std::memcpy(optval, &timeout, *optlen); + return 0; } + if (*optlen < sizeof(int)) return fail(NET_EINVAL); if (level != NET_SOL_SOCKET) return fail(NET_EOPNOTSUPP); int value = 0; switch (timeout_option(optname)) { diff --git a/core/libs/tests/GuestSceNet.cpp b/core/libs/tests/GuestSceNet.cpp index c117778e49..fb27f79b8e 100644 --- a/core/libs/tests/GuestSceNet.cpp +++ b/core/libs/tests/GuestSceNet.cpp @@ -306,7 +306,7 @@ static void CheckDontWait() { std::uint32_t size = address.size(); Require(sceNetGetsockname(receiver, address.data(), &size) == 0); const int timeout = 2000000; - Require(sceNetSetsockopt(receiver, 0xffff, 0x1006, &timeout, sizeof(timeout)) == 0); + Require(sceNetSetsockopt(receiver, 0xffff, 0x1106, &timeout, sizeof(timeout)) == 0); char buffer[16]{}; NetIovec in[] = {{buffer, sizeof(buffer)}}; NetMsghdr receive{nullptr, 0, in, 1, nullptr, 0, 0}; @@ -340,8 +340,11 @@ static void CheckDontWait() { } static void CheckReceiveTimeout(int receiver, int sender, const std::array& address) { - const int timeout = 50000; + const KernelTimeval timeout{0, 50000}; Require(sceNetSetsockopt(receiver, 0xffff, 0x1006, &timeout, sizeof(timeout)) == 0); + int microseconds = -1; + std::uint32_t length = sizeof(microseconds); + Require(sceNetGetsockopt(receiver, 0xffff, 0x1106, µseconds, &length) == 0 && microseconds == 50000); char buffer[8]{}; std::array source{}; std::uint32_t size = source.size(); @@ -353,7 +356,7 @@ static void CheckReceiveTimeout(int receiver, int sender, const std::array(timeout.tv_sec), static_cast(timeout.tv_usec), microseconds, expected); std::abort(); } } @@ -491,7 +497,18 @@ static void CheckKernelTimeouts() { Require(setsockopt_nid_postfix(descriptor, 0xffff, 0x1006, µseconds, sizeof(microseconds)) == -1 && *__error_nid_postfix() == 22); RequireTimeout(descriptor, 0x1006, 1250000); RequireTimeout(descriptor, 0x1005, 500000); - Require(sceNetSetsockopt(descriptor, 0xffff, 0x1006, µseconds, sizeof(microseconds)) == 0); + Require(Failed(sceNetSetsockopt(descriptor, 0xffff, 0x1006, µseconds, sizeof(microseconds)), 22)); + Require(Failed(sceNetSetsockopt(descriptor, 0xffff, 0x1006, &invalid, sizeof(invalid)), 33)); + Require(Failed(sceNetSetsockopt(descriptor, 0xffff, 0x1005, &negative, sizeof(negative)), 33)); + RequireTimeout(descriptor, 0x1006, 1250000); + RequireTimeout(descriptor, 0x1005, 500000); + int seconds = -1; + std::uint32_t truncated = sizeof(seconds); + Require(sceNetGetsockopt(descriptor, 0xffff, 0x1006, &seconds, &truncated) == 0 && truncated == sizeof(seconds) && seconds == 1); + const KernelTimeval quarter{0, 250000}; + Require(sceNetSetsockopt(descriptor, 0xffff, 0x1006, &quarter, sizeof(quarter)) == 0); + RequireTimeout(descriptor, 0x1006, 250000); + Require(sceNetSetsockopt(descriptor, 0xffff, 0x1106, µseconds, sizeof(microseconds)) == 0); RequireTimeout(descriptor, 0x1006, microseconds); #ifndef _WIN32 std::array loopback{16, 2, 0, 0, 127, 0, 0, 1}; @@ -796,7 +813,7 @@ static void CheckDatagramDisconnect() { Require(local[4] == 0 && local[5] == 0 && local[6] == 0 && local[7] == 0); #endif const int timeout = 2000000; - Require(sceNetSetsockopt(dissolved, 0xffff, 0x1006, &timeout, sizeof(timeout)) == 0); + Require(sceNetSetsockopt(dissolved, 0xffff, 0x1106, &timeout, sizeof(timeout)) == 0); const char unrelated[] = "unrelated sender"; Require(sceNetSendto(other, unrelated, sizeof(unrelated), 0, dissolvedAddress.data(), dissolvedAddress.size()) == sizeof(unrelated)); char buffer[32]{}; From 7f82da1080f1e95767269119750a79a613e13e05 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Wed, 14 Oct 2026 08:37:49 -0700 Subject: [PATCH 0088/1000] docs(libSceNet): record which timeout options take a timeval --- docs/dev/debt/libSceNet.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/dev/debt/libSceNet.md b/docs/dev/debt/libSceNet.md index fae79da01e..5e2bf5f0cc 100644 --- a/docs/dev/debt/libSceNet.md +++ b/docs/dev/debt/libSceNet.md @@ -10,6 +10,7 @@ - [sceNetConnect to AF_UNSPEC](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) dissolves a datagram association as FreeBSD 11.4's `udp_disconnect()` does, which resets the local address to the wildcard address and keeps the port. On Linux a socket bound to an explicit port keeps its local address as well (`127.0.0.1:` stays `127.0.0.1:`), because a bound socket cannot be bound again; one bound to port 0 is bound again to the wildcard address with the port it had - [sceNetSetsockopt](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) sets `SO_RCVTIMEO` and `SO_SNDTIMEO` as the host socket's timeouts. On Windows a receive or send on a stream socket that times out fails with `ETIMEDOUT`, where FreeBSD returns `EAGAIN` or the partial count: Winsock leaves the connection in an indeterminate state after a timeout and reports no partial count ([SOL_SOCKET options](https://learn.microsoft.com/en-us/windows/win32/winsock/sol-socket-socket-options)), so FreeBSD's behaviour needs the timeout waited for without the host option. A receive on a datagram socket that times out fails with `EAGAIN` +- [sceNetSetsockopt, sceNetGetsockopt](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) take `SO_SNDTIMEO` and `SO_RCVTIMEO` (0x1005, 0x1006) as a 16-byte `struct timeval`, as FreeBSD 11.4's [`sosetopt` and `sogetopt`](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/sys/kern/uipc_socket.c#L2657-L2690) do, and Sony's 0x1105 and 0x1106 as an `int` of microseconds. A [console log of ProsperoLight](https://github.com/blackbearreloaded/ProsperoLight/issues/49) (firmware 12.20) shows receives timing out every 100 ms after it gave 0x1006 a `{0, 100000}` timeval. The errors for a value shorter than a timeval (`EINVAL`) or out of range (`EDOM`), and `sceNetGetsockopt` cutting the timeval to the caller's length, follow FreeBSD's `sooptcopyin` and `sooptcopyout` and are not measured on a console - [sceNetGetsockopt](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) reads `SO_ERROR` from the host socket. Winsock does not clear a socket's pending error when it is read, so on Windows the library reports a pending error once and shows it again only after a new `sceNetConnect`; a second failure with the same code on the same socket without a `sceNetConnect` in between would be hidden. - [sceNetSendmsg and sceNetRecvmsg](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) copy the scatter/gather list through one host buffer and accept only the flags `sceNetSend`/`sceNetRecv` accept. Sending control data throws; a receive never returns control data (`msg_controllen` 0), since no socket option that queues it is supported - [MSG_DONTWAIT](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) in `sceNetSend`, `sceNetSendto`, `sceNetRecv`, `sceNetRecvfrom`, `sceNetRecvmsg` and `sceNetSendmsg` makes that one call non-blocking through the host's `MSG_DONTWAIT`, as FreeBSD 11.4's `soreceive_generic` returns `EWOULDBLOCK` for it (`sys/kern/uipc_socket.c`); the value `0x80` is FreeBSD's (`sys/sys/socket.h`) and [shadPS4's](https://github.com/shadps4-emu/shadPS4/blob/e0a466753a1aacbc8db8253be685bf7ab4d9daae/src/core/libraries/net/net_types.h#L104). Winsock has no per-call non-blocking flag, so on Windows these calls still fail with `EOPNOTSUPP` for it From 17f8bb521e5f4ac8dd679680fcfd26430274d696 Mon Sep 17 00:00:00 2001 From: JosepBove Date: Wed, 14 Oct 2026 17:55:09 +0200 Subject: [PATCH 0089/1000] docs(ngs2): name DOOM + DOOM II among the titles that probe the voice index --- docs/dev/debt/libSceNgs2.native.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceNgs2.native.md b/docs/dev/debt/libSceNgs2.native.md index 16171e4270..7ea9329ad2 100644 --- a/docs/dev/debt/libSceNgs2.native.md +++ b/docs/dev/debt/libSceNgs2.native.md @@ -29,4 +29,4 @@ ### Functional -- [sceNgs2RackGetVoiceHandle voice index](../../../core/libs/prx/libSceNgs2.native/src/Rack.cpp) (libSceNgs2.native) - an index at or beyond the rack's voice count returns `0x804A8305` and leaves the output handle untouched, after the null-output and rack-handle checks that keep their order and codes. The value follows [prosper's analysis of `libSceNgs2.native.sprx`](https://github.com/mattias800/prosper/blob/d61eb2c0ad2de879ef74df9c4aa050c512804a18/prosper/src/hle/audio/hle_audio.cpp#L4299) (export `MwmHz8pAdAo` at `0xfbd0`, bounds error at `0xfc73`), which also resolves the rack before the output check; [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/e6faa2e3b6794a2fa79a709352ddfccfaa817ed1/src/core/libraries/ngs2/ngs2_error.h) lists `0x804A0302` for the PS4 library. Neither is a PS5 measurement. Heretic + Hexen (PPSA26525) and STAR WARS: Dark Forces Remaster (PPSA19728) create a one-voice reverb rack, take voice 0 and ask for voice 1 at start-up, log the failure and go on +- [sceNgs2RackGetVoiceHandle voice index](../../../core/libs/prx/libSceNgs2.native/src/Rack.cpp) (libSceNgs2.native) - an index at or beyond the rack's voice count returns `0x804A8305` and leaves the output handle untouched, after the null-output and rack-handle checks that keep their order and codes. The value follows [prosper's analysis of `libSceNgs2.native.sprx`](https://github.com/mattias800/prosper/blob/d61eb2c0ad2de879ef74df9c4aa050c512804a18/prosper/src/hle/audio/hle_audio.cpp#L4299) (export `MwmHz8pAdAo` at `0xfbd0`, bounds error at `0xfc73`), which also resolves the rack before the output check; [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/e6faa2e3b6794a2fa79a709352ddfccfaa817ed1/src/core/libraries/ngs2/ngs2_error.h) lists `0x804A0302` for the PS4 library. Neither is a PS5 measurement. Heretic + Hexen (PPSA26525), DOOM + DOOM II (PPSA21444) and STAR WARS: Dark Forces Remaster (PPSA19728) create a one-voice reverb rack, take voice 0 and ask for voices 1 to 3 at start-up, log the failures and go on From 9a0aa5f6a7671adc5123bc162aa254a1f52132cc Mon Sep 17 00:00:00 2001 From: KernelPanicNT <76663133+KernelPanicNT@users.noreply.github.com> Date: Wed, 14 Oct 2026 15:57:41 +0000 Subject: [PATCH 0090/1000] fix(exports): export the PS VR2 calls Phasmophobia's psvr2.prx imports PPSA09030's Media/Plugins/psvr2.prx imports sceHmd2ReprojectionGetStatus from libSceHmd2 and sceVrTracker2GetCoordinateSystem and sceVrTracker2GetPlayAreaBoundaryGeometry from libSceVrTracker2, so the title does not start with the plugin loaded (discussion #4680). Declare the three as throwing exports, like the earlier PS VR2 imports of PPSA29866. The names resolve from the NIDs 8GkaY2B7opM, Y-3JCiU9bbU and SCph4ZbkqzU with tools/nid_names.py; no reference gives their signatures. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceHmd2/Export.cpp | 5 +++++ core/libs/prx/libSceVrTracker2/Export.cpp | 10 ++++++++++ docs/dev/debt/libSceHmd2.md | 1 + 3 files changed, 16 insertions(+) diff --git a/core/libs/prx/libSceHmd2/Export.cpp b/core/libs/prx/libSceHmd2/Export.cpp index 9f1c4cb8f9..e7361d9b35 100644 --- a/core/libs/prx/libSceHmd2/Export.cpp +++ b/core/libs/prx/libSceHmd2/Export.cpp @@ -11,6 +11,11 @@ constexpr std::int32_t SCE_HMD2_ERROR_UNSUPPORTED_FEATURE = static_cast Date: Wed, 14 Oct 2026 18:02:08 +0200 Subject: [PATCH 0091/1000] fix(libSceNet): guard recvmsg guest outputs --- core/libs/prx/libSceNet/CMakeLists.txt | 2 +- core/libs/prx/libSceNet/Export.cpp | 9 ++++ core/libs/tests/GuestSocketHostWrite.cpp | 64 +++++++++++++++++++++++- 3 files changed, 72 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceNet/CMakeLists.txt b/core/libs/prx/libSceNet/CMakeLists.txt index 98776d4b5e..4bd77b100e 100644 --- a/core/libs/prx/libSceNet/CMakeLists.txt +++ b/core/libs/prx/libSceNet/CMakeLists.txt @@ -20,7 +20,7 @@ if(BUILD_TESTING) target_include_directories(guest_socket_host_write_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_socket_host_write_tests PRIVATE libSceNet libkernel libc) configure_windows_unwind(guest_socket_host_write_tests) - foreach(api IN ITEMS recv recvfrom read sceKernelRead sceNetRecv sceNetRecvfrom) + foreach(api IN ITEMS recv recvfrom read sceKernelRead sceNetRecv sceNetRecvfrom sceNetRecvmsg) add_test(NAME guest_socket_host_write_${api}_udp COMMAND guest_socket_host_write_tests ${api}) add_test(NAME guest_socket_host_write_${api}_tcp COMMAND guest_socket_host_write_tests ${api} tcp) set_tests_properties(guest_socket_host_write_${api}_udp guest_socket_host_write_${api}_tcp diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index e03f88e831..b675ff29c1 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -23,6 +23,7 @@ #include #include #include +#include #include #include #include @@ -953,6 +954,14 @@ int64_t APS5_VABI sceNetRecvmsg(int s, NetMsghdr* msg, int flags) { if (it == g_socks.end()) return fail(NET_EBADF); socket = it->second; } + const GuestArena::HostWrite messageOutput(msg, sizeof(*msg)); + if (!messageOutput.Open()) return fail(NET_EFAULT); + std::deque destinations; + for (int i = 0; i < msg->iov_length; ++i) { + const auto& entry = msg->iov[i]; + if (entry.length == 0) continue; + if (!destinations.emplace_back(entry.base, static_cast(entry.length)).Open()) return fail(NET_EFAULT); + } const bool datagram = socket.type != NET_SOCK_STREAM; std::vector buffer; try { diff --git a/core/libs/tests/GuestSocketHostWrite.cpp b/core/libs/tests/GuestSocketHostWrite.cpp index 06e23b986e..c6a09d49d6 100644 --- a/core/libs/tests/GuestSocketHostWrite.cpp +++ b/core/libs/tests/GuestSocketHostWrite.cpp @@ -5,12 +5,29 @@ #include #include #include +#include #include #include #include #include #include +struct NetIovec { + void* base; + std::uint64_t length; +}; + +struct NetMsghdr { + void* name; + std::uint32_t nameLength; + NetIovec* iov; + int iovLength; + void* control; + std::uint32_t controlLength; + int flags; +}; +static_assert(sizeof(NetMsghdr) == 48 && offsetof(NetMsghdr, iov) == 16 && offsetof(NetMsghdr, flags) == 44); + extern "C" { int APS5_VABI socket_nid_postfix(int, int, int); int APS5_VABI bind_nid_postfix(int, const void*, std::uint32_t); @@ -36,6 +53,7 @@ int APS5_VABI sceNetSetsockopt(int, int, int, const void*, std::uint32_t); std::int64_t APS5_VABI sceNetSendto(int, const void*, std::size_t, int, const void*, std::uint32_t); std::int64_t APS5_VABI sceNetRecv(int, void*, std::size_t, int); std::int64_t APS5_VABI sceNetRecvfrom(int, void*, std::size_t, int, void*, std::uint32_t*); +std::int64_t APS5_VABI sceNetRecvmsg(int, NetMsghdr*, int); int APS5_VABI sceNetSocketClose(int); int* APS5_VABI sceNetErrnoLoc(); int APS5_VABI sceKernelAllocateDirectMemory(std::int64_t, std::int64_t, std::size_t, std::size_t, int, std::int64_t*); @@ -61,7 +79,7 @@ static DWORD Protection(const void* address) { int main(int argc, char** argv) { constexpr std::size_t page = 0x4000; constexpr std::size_t capacity = 32; - constexpr const char* names[] = {"recv", "recvfrom", "read", "sceKernelRead", "sceNetRecv", "sceNetRecvfrom"}; + constexpr const char* names[] = {"recv", "recvfrom", "read", "sceKernelRead", "sceNetRecv", "sceNetRecvfrom", "sceNetRecvmsg"}; Require(argc == 2 || argc == 3, "receive API argument required"); Require(argc != 3 || std::strcmp(argv[2], "tcp") == 0, "unknown transport"); const bool stream = argc == 3; @@ -121,7 +139,13 @@ int main(int argc, char** argv) { case 2: return read_nid_postfix(receiver, buffer, bytes); case 3: return sceKernelRead(receiver, buffer, bytes); case 4: return sceNetRecv(receiver, buffer, bytes, flags); - default: return sceNetRecvfrom(receiver, buffer, bytes, flags, address, addressLength); + case 5: return sceNetRecvfrom(receiver, buffer, bytes, flags, address, addressLength); + default: { + const auto first = std::min(bytes, 3); + NetIovec vectors[]{{buffer, first}, {nullptr, 0}, {static_cast(buffer) + first, bytes - first}}; + NetMsghdr message{nullptr, 0, vectors, 3, nullptr, 0, 0}; + return sceNetRecvmsg(receiver, &message, flags); + } } }; const auto waitForPacket = [&] { @@ -257,6 +281,42 @@ int main(int argc, char** argv) { collect(); Require(Protection(buffer) == PAGE_READONLY && Protection(buffer + capacity - 1) == PAGE_READONLY, "failed receive releases the host write guard"); + if (api == 6) { + Require(sceKernelMprotect(static_cast(mapping) + page, page, 1) == 0, + "make the later receive vector read-only"); + send(); + waitForPacket(); + Require(receive(buffer, capacity, 0, nullptr, nullptr) == static_cast(0x8041010eu) && *error == 14, + "a later read-only vector fails with EFAULT"); + Require(std::all_of(buffer - 1, buffer + capacity + 1, [](char value) { return value == '!'; }), + "a rejected vector list leaves every destination unchanged"); + collect(); + Require(Protection(buffer) == PAGE_READONLY, "a later vector failure releases earlier guards"); + std::array recovery{}; + Require(receive(recovery.data(), recovery.size(), 0, nullptr, nullptr) == sizeof(payload), + "a rejected vector list preserves queued data"); + Require(sceKernelMprotect(static_cast(mapping) + page, page, 3) == 0, + "restore the later receive vector"); + NetIovec vectors[]{{buffer, 3}, {nullptr, 0}, {buffer + 3, capacity - 3}}; + auto* message = reinterpret_cast(static_cast(twin) + 32); + const NetMsghdr initial{nullptr, 0, vectors, 3, nullptr, 123, -1}; + { + const GuestArena::HostWrite destination(message, sizeof(*message)); + Require(destination.Open(), "initialize the receive message"); + std::memcpy(message, &initial, sizeof(initial)); + } + Require(sceKernelMprotect(twin, page, 1) == 0, "make the receive message read-only"); + send(); + waitForPacket(); + Require(sceNetRecvmsg(receiver, message, 0) == static_cast(0x8041010eu) && *error == 14, + "read-only message outputs fail with EFAULT"); + Require(message->flags == -1 && message->controlLength == 123, "rejected message outputs unchanged"); + Require(receive(recovery.data(), recovery.size(), 0, nullptr, nullptr) == sizeof(payload), + "a rejected message preserves queued data"); + Require(sceKernelMprotect(twin, page, 3) == 0, "restore receive message write permission"); + reset(); + collect(); + } Require(sceKernelMprotect(mapping, page * 2, 1) == 0, "make guest memory read-only"); send(); waitForPacket(); From e9b40a14dddec73affdc9c0839ca4657739cdfa9 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Wed, 14 Oct 2026 09:07:56 -0700 Subject: [PATCH 0092/1000] fix(libSceNet): let a datagram receive on an unbound socket wait on Windows sceNetRecv, sceNetRecvfrom and sceNetRecvmsg called Winsock's receive straight away. Winsock fails a receive on a socket with no local address with WSAEINVAL, so on Windows the call returned EINVAL at once. On the console the receive waits: FreeBSD 11.4's soreceive_dgram sleeps on the receive buffer whether or not the socket has an address, returns EWOULDBLOCK for a non-blocking socket, and a send from another thread binds the socket so the reply can arrive. Steam Remote Play (PPSA99350) relies on that. Its stream library opens its discovery socket, starts receiving on a worker thread, and binds the socket only when another thread sends the discovery broadcast. On Windows the worker got EINVAL, took it as fatal and closed the socket before the broadcast went out, so the title never found a PC running Steam. On Windows a receive on a datagram socket with no local address now waits until a bind, connect or send gives it one, checking every 10 ms. A non-blocking socket fails with EAGAIN at once, and the receive timeout ends the wait with EAGAIN. An abort or close ends the wait, and the host receive then fails as before. Once the socket has an address it is noted as bound, so later receives skip the check, and a later sceNetBind fails with EINVAL as it does on the console. --- core/libs/prx/libSceNet/Export.cpp | 38 +++++++++++++++++++ core/libs/tests/GuestSceNet.cpp | 61 ++++++++++++++++++++++++++++++ 2 files changed, 99 insertions(+) diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index e03f88e831..c6e510a913 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -489,6 +489,35 @@ int native_message_flags(int flags) { return native; } +#ifdef _WIN32 +bool has_local_address(NativeSocket native) { + sockaddr_storage local{}; + NativeLength local_length = sizeof(local); + return ::getsockname(native, reinterpret_cast(&local), &local_length) == 0 || WSAGetLastError() != WSAEINVAL; +} + +int wait_for_local_address(int s, const Sock& socket) { + if (socket.type == NET_SOCK_STREAM || socket.bound) return 0; + const bool named = has_local_address(socket.native->value); + if (!named && socket.nonblock) return NET_EAGAIN; + const auto deadline = socket.rcv_timeout_us > 0 + ? std::chrono::steady_clock::now() + std::chrono::microseconds(socket.rcv_timeout_us) + : std::chrono::steady_clock::time_point::max(); + std::unique_lock lk(g_mutex); + for (;;) { + const auto it = g_socks.find(s); + if (it == g_socks.end() || it->second.native != socket.native || it->second.aborted) return 0; + if (named || has_local_address(socket.native->value)) { + it->second.bound = true; + return 0; + } + const auto now = std::chrono::steady_clock::now(); + if (now >= deadline) return NET_EAGAIN; + g_cv.WaitUntil(lk, std::min(deadline, now + std::chrono::milliseconds(10))); + } +} +#endif + } // namespace extern "C" { @@ -846,6 +875,9 @@ int64_t APS5_VABI sceNetRecv(int s, void* buf, size_t len, int flags) { if (it == g_socks.end()) return fail(NET_EBADF); socket = it->second; } +#ifdef _WIN32 + if (const int error = wait_for_local_address(s, socket)) return fail(error); +#endif const GuestArena::HostWrite destination(len == 0 ? nullptr : buf, len); if (!destination.Open()) return fail(NET_EFAULT); const int native_flags = native_message_flags(flags); @@ -868,6 +900,9 @@ int64_t APS5_VABI sceNetRecvfrom(int s, void* buf, size_t len, int flags, void* if (it == g_socks.end()) return fail(NET_EBADF); socket = it->second; } +#ifdef _WIN32 + if (const int error = wait_for_local_address(s, socket)) return fail(error); +#endif const GuestArena::HostWrite destination(len == 0 ? nullptr : buf, len); if (!destination.Open()) return fail(NET_EFAULT); sockaddr_storage peer{}; @@ -953,6 +988,9 @@ int64_t APS5_VABI sceNetRecvmsg(int s, NetMsghdr* msg, int flags) { if (it == g_socks.end()) return fail(NET_EBADF); socket = it->second; } +#ifdef _WIN32 + if (const int error = wait_for_local_address(s, socket)) return fail(error); +#endif const bool datagram = socket.type != NET_SOCK_STREAM; std::vector buffer; try { diff --git a/core/libs/tests/GuestSceNet.cpp b/core/libs/tests/GuestSceNet.cpp index c117778e49..de2230fc0e 100644 --- a/core/libs/tests/GuestSceNet.cpp +++ b/core/libs/tests/GuestSceNet.cpp @@ -42,6 +42,7 @@ std::int64_t APS5_VABI sceNetRecv(int, void*, std::size_t, int); std::int64_t APS5_VABI sceNetSendto(int, const void*, std::size_t, int, const void*, std::uint32_t); std::int64_t APS5_VABI sceNetRecvfrom(int, void*, std::size_t, int, void*, std::uint32_t*); int APS5_VABI sceNetSocketClose(int); +int APS5_VABI sceNetSocketAbort(int, int); int APS5_VABI sceNetShutdown(int, int); int APS5_VABI sceNetSetsockopt(int, int, int, const void*, std::uint32_t); int APS5_VABI sceNetGetsockopt(int, int, int, void*, std::uint32_t*); @@ -257,6 +258,65 @@ static void CheckUnboundSocketName() { } } +static void CheckUnboundDatagramReceive() { + const int waiting = sceNetSocket("unbound-receive", 2, 2, 17); + const int peer = sceNetSocket("unbound-peer", 2, 2, 17); + Require(waiting >= 0 && peer >= 0); + const int timeout = 50000; + Require(sceNetSetsockopt(waiting, 0xffff, 0x1106, &timeout, sizeof(timeout)) == 0); + char buffer[16]{}; + NetIovec in[] = {{buffer, sizeof(buffer)}}; + NetMsghdr receive{nullptr, 0, in, 1, nullptr, 0, 0}; + for (int call = 0; call < 3; ++call) { + const auto start = std::chrono::steady_clock::now(); + const std::int64_t result = call == 0 ? sceNetRecv(waiting, buffer, sizeof(buffer), 0) + : call == 1 ? sceNetRecvfrom(waiting, buffer, sizeof(buffer), 0, nullptr, nullptr) + : sceNetRecvmsg(waiting, &receive, 0); + Require(Failed(result, 35)); + Require(std::chrono::steady_clock::now() - start >= std::chrono::milliseconds(40)); + } + const int on = 1; + const int off = 0; + Require(sceNetSetsockopt(waiting, 0xffff, 0x1200, &on, sizeof(on)) == 0); + Require(Failed(sceNetRecvfrom(waiting, buffer, sizeof(buffer), 0, nullptr, nullptr), 35)); + Require(sceNetSetsockopt(waiting, 0xffff, 0x1200, &off, sizeof(off)) == 0); + const int longTimeout = 2000000; + Require(sceNetSetsockopt(waiting, 0xffff, 0x1106, &longTimeout, sizeof(longTimeout)) == 0); + std::array address{16, 2, 0, 0, 127, 0, 0, 1}; + Require(sceNetBind_nid_postfix(peer, address.data(), address.size()) == 0); + std::uint32_t size = address.size(); + Require(sceNetGetsockname(peer, address.data(), &size) == 0); + auto reply = std::async(std::launch::async, [waiting] { + char answer[16]{}; + return sceNetRecvfrom(waiting, answer, sizeof(answer), 0, nullptr, nullptr) == 5 && std::strcmp(answer, "pong") == 0; + }); + std::this_thread::sleep_for(std::chrono::milliseconds(20)); + Require(reply.wait_for(std::chrono::seconds(0)) == std::future_status::timeout); + Require(sceNetSendto(waiting, "ping", 5, 0, address.data(), address.size()) == 5); + std::array source{}; + size = source.size(); + Require(sceNetRecvfrom(peer, buffer, sizeof(buffer), 0, source.data(), &size) == 5 && std::strcmp(buffer, "ping") == 0); + Require(sceNetSendto(peer, "pong", 5, 0, source.data(), size) == 5); + Require(reply.wait_for(std::chrono::seconds(1)) == std::future_status::ready && reply.get()); + Require(Failed(sceNetBind_nid_postfix(waiting, address.data(), address.size()), 22)); + for (const int descriptor : {waiting, peer}) Require(sceNetSocketClose(descriptor) == 0); +#ifdef _WIN32 + for (const bool abort : {true, false}) { + const int socket = sceNetSocket("unbound-abort", 2, 2, 17); + Require(socket >= 0); + auto waiter = std::async(std::launch::async, [socket] { + char ignored[4]; + return sceNetRecvfrom(socket, ignored, sizeof(ignored), 0, nullptr, nullptr); + }); + std::this_thread::sleep_for(std::chrono::milliseconds(20)); + Require(waiter.wait_for(std::chrono::seconds(0)) == std::future_status::timeout); + Require((abort ? sceNetSocketAbort(socket, 0) : sceNetSocketClose(socket)) == 0); + Require(waiter.wait_for(std::chrono::seconds(1)) == std::future_status::ready && waiter.get() < 0); + if (abort) Require(sceNetSocketClose(socket) == 0); + } +#endif +} + static void CheckEmptyEpoll() { const int epoll = sceNetEpollCreate("empty-epoll", 0); Require(epoll >= 0); @@ -830,6 +890,7 @@ int main() { CheckEpollIdent(); CheckLinkLocalAllNodesIpv6(); CheckUnboundSocketName(); + CheckUnboundDatagramReceive(); CheckUnspecifiedIpv6(); if (!ipv6Unavailable) CheckIpv6SocketsAreIpv6Only(); CheckDatagramDisconnect(); From 85ff7136d096867f32d8b2399f8d7583ea8b25b7 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Wed, 14 Oct 2026 09:07:56 -0700 Subject: [PATCH 0093/1000] docs(libSceNet): record the wait for a datagram socket's local address on Windows --- docs/dev/debt/libSceNet.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/dev/debt/libSceNet.md b/docs/dev/debt/libSceNet.md index fae79da01e..cb92dc78d2 100644 --- a/docs/dev/debt/libSceNet.md +++ b/docs/dev/debt/libSceNet.md @@ -13,5 +13,6 @@ - [sceNetGetsockopt](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) reads `SO_ERROR` from the host socket. Winsock does not clear a socket's pending error when it is read, so on Windows the library reports a pending error once and shows it again only after a new `sceNetConnect`; a second failure with the same code on the same socket without a `sceNetConnect` in between would be hidden. - [sceNetSendmsg and sceNetRecvmsg](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) copy the scatter/gather list through one host buffer and accept only the flags `sceNetSend`/`sceNetRecv` accept. Sending control data throws; a receive never returns control data (`msg_controllen` 0), since no socket option that queues it is supported - [MSG_DONTWAIT](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) in `sceNetSend`, `sceNetSendto`, `sceNetRecv`, `sceNetRecvfrom`, `sceNetRecvmsg` and `sceNetSendmsg` makes that one call non-blocking through the host's `MSG_DONTWAIT`, as FreeBSD 11.4's `soreceive_generic` returns `EWOULDBLOCK` for it (`sys/kern/uipc_socket.c`); the value `0x80` is FreeBSD's (`sys/sys/socket.h`) and [shadPS4's](https://github.com/shadps4-emu/shadPS4/blob/e0a466753a1aacbc8db8253be685bf7ab4d9daae/src/core/libraries/net/net_types.h#L104). Winsock has no per-call non-blocking flag, so on Windows these calls still fail with `EOPNOTSUPP` for it +- [sceNetRecv, sceNetRecvfrom and sceNetRecvmsg](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) on a datagram socket with no local address wait on Windows until a bind, connect or send gives it one, checking the host socket every 10 ms. Winsock fails such a receive with `WSAEINVAL`, where FreeBSD 11.4's `soreceive_dgram` waits for a datagram (`sys/kern/uipc_socket.c`). A non-blocking socket fails with `EAGAIN` at once, and the receive timeout ends the wait with `EAGAIN`; when the address comes first, the host receive that follows waits the whole timeout again, so the call can take up to twice the timeout. An abort or close ends the wait, and the host receive then fails as it did before - [sceNetResolverAbort and sceNetResolverStartNtoaMultipleRecords](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) follow shadPS4 ([net.cpp](https://github.com/shadps4-emu/shadPS4/blob/811bfcd7d17ed8ae975c477d9ff28abc744e1ca8/src/core/libraries/net/net.cpp), [net_resolver.cpp](https://github.com/shadps4-emu/shadPS4/blob/811bfcd7d17ed8ae975c477d9ff28abc744e1ca8/src/core/libraries/net/net_resolver.cpp)); the `(int rid, int flags)` signature of the abort also matches [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Socket.cs). Unverified on hardware. Lookups run synchronously over the host resolver, which cannot be interrupted: an abort while a lookup runs on another thread returns at once, and that lookup fails with `EINTR` without writing its output only when the host call returns; the abort flags are then ignored, as shadPS4 does for a running lookup. With nothing running, abort flag 1 (NTOA preservation) or 2 (ATON preservation) makes the next `sceNetResolverStartNtoa*` or `sceNetResolverStartAton` call on that resolver fail with `EINTR`, reported by `sceNetResolverGetError`, after its arguments are checked. Unlike shadPS4, which ignores them, other abort flag bits throw, and `sceNetResolverStartNtoaMultipleRecords` throws for any flag, including async (1) and 0x10000 (IP address literals disabled), as `sceNetResolverStartNtoaMultipleRecordsEx` does - [sceNetResolverStartNtoa6](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) resolves a host name to its first IPv6 address through the host resolver (`getaddrinfo` with `AF_INET6`) and writes the 16-byte address; the `(rid, hostname, addr, timeout, retry, flags)` signature follows `sceNetResolverStartNtoa`. A name with no IPv6 address fails with `RESOLVER_ENODNS` (`0x804101E1`) like a failed lookup; whether the console returns IPv4-mapped addresses instead is not verified. Timeout and retry are ignored as in `sceNetResolverStartNtoa`, and any nonzero flags throw because their meaning for this call is not known. Unverified on hardware. From 9399cc93aa8bdec2a755c20a3cd0edf719945f53 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 14 Oct 2026 16:18:32 +0000 Subject: [PATCH 0094/1000] fix(shader): flush v_cvt_pknorm_*_f16 denormal inputs by FLOAT_MODE Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Ha6Tsy62AFUyp9a5Mf7vjA --- .../tests/execution/PknormF16Denormals.cpp | 151 ++++++++++++++++++ .../Translation/src/FloatInstructions.cpp | 4 +- docs/dev/debt/shader.md | 2 +- 3 files changed, 154 insertions(+), 3 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/PknormF16Denormals.cpp diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PknormF16Denormals.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PknormF16Denormals.cpp new file mode 100644 index 0000000000..7e87976c54 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PknormF16Denormals.cpp @@ -0,0 +1,151 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 32; +constexpr std::uint32_t Inputs = 4; +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; + +struct alignas(256) Shader { + std::array words; +}; + +struct Vector { + std::uint32_t low; + std::uint32_t high; + std::uint32_t expected[4]; +}; + +struct Case { + const char* name; + Shader shader; + std::span rows; +}; + +constexpr Vector PknormI16Rows[] = { + {0x0001u, 0x8001u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}}, + {0x0155u, 0x8155u, {0x00000000u, 0xffff0001u, 0x00000000u, 0xffff0001u}}, + {0x0200u, 0x8200u, {0x00000000u, 0xffff0001u, 0x00000000u, 0xffff0001u}}, + {0x03ffu, 0x83ffu, {0x00000000u, 0xfffe0002u, 0x00000000u, 0xfffe0002u}}, + {0x8001u, 0x0001u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}}, + {0x8200u, 0x0200u, {0x00000000u, 0x0001ffffu, 0x00000000u, 0x0001ffffu}}, + {0x83ffu, 0x03ffu, {0x00000000u, 0x0002fffeu, 0x00000000u, 0x0002fffeu}}, + {0x0400u, 0x8400u, {0xfffe0002u, 0xfffe0002u, 0xfffe0002u, 0xfffe0002u}}, + {0x8400u, 0x0400u, {0x0002fffeu, 0x0002fffeu, 0x0002fffeu, 0x0002fffeu}}, + {0x0000u, 0x8000u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}}, + {0x8000u, 0x0000u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}}, + {0x3c00u, 0xbc00u, {0x80017fffu, 0x80017fffu, 0x80017fffu, 0x80017fffu}}, + {0xbc00u, 0x3c00u, {0x7fff8001u, 0x7fff8001u, 0x7fff8001u, 0x7fff8001u}}, + {0x3800u, 0xb800u, {0xc0004000u, 0xc0004000u, 0xc0004000u, 0xc0004000u}}, + {0x7c00u, 0xfc00u, {0x80017fffu, 0x80017fffu, 0x80017fffu, 0x80017fffu}}, + {0x7e00u, 0xfe00u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}}, +}; + +constexpr Vector PknormU16Rows[] = { + {0x0001u, 0x8001u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}}, + {0x0155u, 0x8155u, {0x00000000u, 0x00000001u, 0x00000000u, 0x00000001u}}, + {0x0200u, 0x8200u, {0x00000000u, 0x00000002u, 0x00000000u, 0x00000002u}}, + {0x03ffu, 0x83ffu, {0x00000000u, 0x00000004u, 0x00000000u, 0x00000004u}}, + {0x8001u, 0x0001u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}}, + {0x8200u, 0x0200u, {0x00000000u, 0x00020000u, 0x00000000u, 0x00020000u}}, + {0x83ffu, 0x03ffu, {0x00000000u, 0x00040000u, 0x00000000u, 0x00040000u}}, + {0x0400u, 0x8400u, {0x00000004u, 0x00000004u, 0x00000004u, 0x00000004u}}, + {0x8400u, 0x0400u, {0x00040000u, 0x00040000u, 0x00040000u, 0x00040000u}}, + {0x0000u, 0x8000u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}}, + {0x8000u, 0x0000u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}}, + {0x3c00u, 0xbc00u, {0x0000ffffu, 0x0000ffffu, 0x0000ffffu, 0x0000ffffu}}, + {0xbc00u, 0x3c00u, {0xffff0000u, 0xffff0000u, 0xffff0000u, 0xffff0000u}}, + {0x3800u, 0xb800u, {0x00008000u, 0x00008000u, 0x00008000u, 0x00008000u}}, + {0x7c00u, 0xfc00u, {0x0000ffffu, 0x0000ffffu, 0x0000ffffu, 0x0000ffffu}}, + {0x7e00u, 0xfe00u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}}, +}; + +const Case Cases[] = { + {"v_cvt_pknorm_i16_f16", {{0x34020084u, 0x34040082u, 0xe0381000u, 0x80000401u, 0xbf8c3f70u, 0x7e140280u, 0xd712000au, 0x00020b04u, 0xe0701000u, 0x80010a02u, 0xbf810000u}}, PknormI16Rows}, + {"v_cvt_pknorm_u16_f16", {{0x34020084u, 0x34040082u, 0xe0381000u, 0x80000401u, 0xbf8c3f70u, 0x7e140280u, 0xd713000au, 0x00020b04u, 0xe0701000u, 0x80010a02u, 0xbf810000u}}, PknormU16Rows}, +}; + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + +void Run(AgcDriver::VulkanDevice& device, const Case& item, std::uint32_t denorm16, bool ieee) { + Input.fill(0u); + for (std::uint32_t row = 0; row < item.rows.size(); ++row) { + Input[row * Inputs] = item.rows[row].low; + Input[row * Inputs + 1] = item.rows[row].high; + } + Output.fill(0xdeadbeefu); + std::vector userData(8, 0u); + const auto input = BufferDescriptor(Input.data(), static_cast(Input.size() * 4u)); + const auto output = BufferDescriptor(Output.data(), static_cast(Output.size() * 4u)); + std::copy(input.begin(), input.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4); + const std::span code(item.shader.words); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + request.context.floatMode = ShaderRecompiler::ShaderFloatMode{denorm16 << 6u, true, ieee, false}; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); +} + +void Check(const Case& item, std::uint32_t denorm16, bool ieee) { + for (std::uint32_t row = 0; row < item.rows.size(); ++row) { + const auto actual = Output[row]; + const auto expected = item.rows[row].expected[denorm16]; + Require(actual == expected, std::string(item.name) + " denorm16 " + std::to_string(denorm16) + (ieee ? " ieee" : " no ieee") + " input " + + Hex(item.rows[row].low) + " " + Hex(item.rows[row].high) + " is " + Hex(actual) + ", expected " + Hex(expected)); + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + for (const auto& item : Cases) { + for (std::uint32_t denorm16 = 0u; denorm16 < 4u; ++denorm16) { + for (const bool ieee : {false, true}) { + Run(*device, item, denorm16, ieee); + Check(item, denorm16, ieee); + } + } + } + std::puts("pknorm f16 denormal tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/shader/recompiler/Translation/src/FloatInstructions.cpp b/core/shader/recompiler/Translation/src/FloatInstructions.cpp index 8b0ea3b902..c0951aad1f 100644 --- a/core/shader/recompiler/Translation/src/FloatInstructions.cpp +++ b/core/shader/recompiler/Translation/src/FloatInstructions.cpp @@ -400,8 +400,8 @@ bool TranslationContext::vCvtNormF16(const RdnaInstruction& inst, bool signedVal } bool TranslationContext::vCvtPknormF16(const RdnaInstruction& inst, bool signedValue) { - const IrU32 low = normF16(readF16Bits(sourceAt(inst, 0u)), signedValue); - const IrU32 high = normF16(readF16Bits(sourceAt(inst, 1u)), signedValue); + const IrU32 low = normF16(flushF16Input(readF16Bits(sourceAt(inst, 0u))), signedValue); + const IrU32 high = normF16(flushF16Input(readF16Bits(sourceAt(inst, 1u))), signedValue); const IrU32 result(ir.BitwiseOr(low.Value(), ir.ShiftLeftLogical(high.Value(), ir.Constant(16u)))); writeOperand(inst.destination, &result.Value()); return true; diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index db9fedf073..2f91fbcb8c 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -74,7 +74,7 @@ - [v_fma_mixlo_f16 and v_fma_mixhi_f16](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) round the exact product-sum once to f16 with the f64 arithmetic of v_interp_p2_f16, so they need the Float64 capability. On RDNA2 (gfx1036) they follow the `FLOAT_MODE` f16 round and denormal fields, not the f32 round field: -1.5 * 2^-24 + 2^-126 from f32 sources (0xbfc00000, 0x33800000, 0x00800000) gives 0x8001 with f16 denormals kept and round to nearest even, 0x8000 with f16 denormals flushed and 0x8002 with f16 round toward -infinity. With f16 denormals kept, every f16 rounding mode is translated: the exact product-sum is rounded once in the mode's direction. All four modes match RDNA2 (gfx1036) on the 2,233 rows of the #2631 sweep, including `FLOAT_MODE` 0xcc (toward zero, used by ps5-opengl) and the exact cancellations toward -infinity; the PS5's gfx1013 is not measured. A result beyond 65504 becomes infinity or 65504 as IEEE 754 gives for the mode. Flushed f16 denormals still throw, and packed f16 still throws in every mode but round to nearest even with f16 denormals kept. `FP16_OVFL` set is not measured. - [Signaling NaN inputs](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) of the f32 and f16 unary instructions (`v_sqrt/rcp/rsq/log/exp/sin/cos/fract/floor/ceil/trunc/rndne/rcp_iflag/frexp_mant`), `v_ldexp_f32/f16`, `v_frexp_mant_f16`, `v_div_fixup_f32/f16` and `v_mullit_f32` are returned quieted only with IEEE=1 and unchanged with IEEE=0, measured on RDNA2 (gfx1035) in both modes, and so are those of the f64 forms (`v_sqrt/rcp/rsq/fract/floor/ceil/trunc/rndne/frexp_mant/ldexp/div_fixup_f64`) and `v_cvt_f64_f32`, measured on RDNA2 (gfx1036) in both modes; shaders without a known float mode follow IEEE=0. `v_pack_b32_f16` quiets each signaling NaN half by the same rule, measured on RDNA2 (gfx1036) in both modes. A signaling NaN result of `v_cubesc/cubetc/cubema_f32` is quieted by the same rule and keeps the sign the instruction gives it, measured on RDNA2 (gfx1036) in both modes. `v_add/sub/subrev/mul/fma/fmac/fmamk/fmaak/mul_legacy_f32` and `v_fma_mix_f32` with f32 sources choose their NaN result on the bits, measured over 1000 operand triples in both modes: the first NaN source in operand order after its modifiers (the subtrahend negated, as the hardware adds its negation), quieted only with IEEE=1, or the default NaN 0xffc00000 for an invalid operation; in the fma forms an invalid product (inf * 0) gives the default NaN even when src2 is a NaN, and in the legacy forms a zero factor makes the product 0 even when the other factor is a NaN (a NaN src2 still wins). `v_mad_legacy_f32` and `v_mac_legacy_f32` follow the same rule with their separately rounded product, as measured on gfx1010 (#1281, #1290): a NaN source quieted only with IEEE=1, inf - inf giving 0xffc00000, the zero rule hiding a NaN factor and a NaN src2 winning. The f16 and f64 arithmetic, `v_fma_mix_f32` with f16 sources and the separately rounded mad/mac forms (not measurable on RDNA2) follow the same rule on hardware but their NaN bits are left to the driver. `v_cvt_f16_f32`, `v_cvt_f32_f64`, `v_dot2_f32_f16`, `v_fma_mixlo_f16` and `v_fma_mixhi_f16` always quiet and `v_div_scale_f32` never does: the f32 signaling NaN 0x7f812345 as src0 or src2 gives 0x7e09 from mixlo and mixhi at IEEE=0 and IEEE=1 on RDNA2 (gfx1036), where `v_fma_mix_f32` gives 0x7f812345 at IEEE=0 and 0x7fc12345 at IEEE=1. The payload of an f16 NaN source of mixlo and mixhi is left to the driver's f16 to f32 conversion, which NVIDIA replaces. - [Scalar f16 add, sub, subrev and mul denormals](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) follow the `FLOAT_MODE` f16 denormal field at nearest-even: bit 0 preserves inputs and bit 1 preserves the result; otherwise a denormal is flushed to its signed zero. Measured on RDNA2 (gfx1036) with IEEE=0/1 and FP16_OVFL=0/1, over 32 operand pairs in all four denormal modes, including signed zeros, denormals, minimum normals, infinities and NaNs. Multiply detects tininess after rounding to 11 significant bits with an unbounded exponent, before gradual half packing: `0x3bff * 0x0400` flushes, while `0x3c01 * 0x03ff` rounds to `0x0400` and is kept. Directed rounding and ternary arithmetic keep their previous denormal lowering. Shaders without a known float mode keep the previous lowering. Scalar `v_sub_f16`/`v_subrev_f16` share `float16Binary` with add/mul and were measured on gfx1036 over the same operand set as add (1156 non-NaN-input rows per mode); NaN result bits still depend on the driver as recorded above. -- [f16 unary, frexp and conversion denormals](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) follow the `FLOAT_MODE` f16 denormal field: `v_ceil_f16`, `v_floor_f16`, `v_fract_f16`, `v_rcp_f16`, `v_rsq_f16`, `v_sqrt_f16`, `v_log_f16`, `v_exp_f16`, `v_sin_f16`, `v_frexp_mant_f16`, `v_frexp_exp_i16_f16`, `v_cvt_norm_i16_f16`, `v_cvt_norm_u16_f16` and `v_cvt_f32_f16` flush a denormal half input to its signed zero when bit 0 is clear (0 and 2), and the f16-result ones flush a denormal half result to its signed zero when bit 1 is clear (0 and 1), after the host rounds it to half. Measured on RDNA2 (gfx1036) at IEEE=0 and IEEE=1 with denorm16 0..3 over every non-NaN f16 input (63,490 rows per mode and instruction): the only mismatches are the 1-ULP driver differences of `v_exp_f16` (2 inputs) and `v_sin_f16` (14), which are the same with denormals kept. NaN inputs are not compared. `v_ldexp_f16` shares the input rule. `v_cvt_pknorm_i16_f16` and `v_cvt_pknorm_u16_f16` are not measured and keep their previous lowering. Shaders without a known float mode keep the previous lowering. +- [f16 unary, frexp and conversion denormals](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) follow the `FLOAT_MODE` f16 denormal field: `v_ceil_f16`, `v_floor_f16`, `v_fract_f16`, `v_rcp_f16`, `v_rsq_f16`, `v_sqrt_f16`, `v_log_f16`, `v_exp_f16`, `v_sin_f16`, `v_frexp_mant_f16`, `v_frexp_exp_i16_f16`, `v_cvt_norm_i16_f16`, `v_cvt_norm_u16_f16` and `v_cvt_f32_f16` flush a denormal half input to its signed zero when bit 0 is clear (0 and 2), and the f16-result ones flush a denormal half result to its signed zero when bit 1 is clear (0 and 1), after the host rounds it to half. Measured on RDNA2 (gfx1036) at IEEE=0 and IEEE=1 with denorm16 0..3 over every non-NaN f16 input (63,490 rows per mode and instruction): the only mismatches are the 1-ULP driver differences of `v_exp_f16` (2 inputs) and `v_sin_f16` (14), which are the same with denormals kept. NaN inputs are not compared. `v_ldexp_f16` shares the input rule. `v_cvt_pknorm_i16_f16` and `v_cvt_pknorm_u16_f16` flush both half inputs by the same bit, measured on RDNA2 (gfx1030) over every f16 input (each half also negated in the other source) at IEEE=0 and IEEE=1 with denorm16 0..3 and at f16 rounding 1..3: every row matches. Shaders without a known float mode keep the previous lowering. - [Packed f16 NaN results](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) (`v_pk_add/mul/fma/min/max_f16`) follow the hardware measured on RDNA2 (gfx1035) at IEEE=0 and IEEE=1 with f16 round-to-nearest-even and f16 denormals kept: with IEEE=1 a signaling NaN input is quieted and min/max return the other operand only for a quiet NaN; with IEEE=0 NaN inputs pass through unquieted and min/max ignore signaling. In `v_pk_fma_f16` an invalid product (inf * 0) gives the default NaN even when src2 is a NaN. A shader without a known float mode is translated as IEEE=0, the mode titles use; a mode with another f16 rounding or that flushes f16 denormals throws. - [f32 denormals](../../../core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp) follow the shader's `FLOAT_MODE` f32 denormal field (bits 5:4) when it flushes them (0, and shaders without a known float mode, as in the 0xc0 most titles use), measured on RDNA2 (gfx1035) over 1444 rows per instruction at IEEE=0 and IEEE=1. `v_add/sub/subrev/mul/mul_legacy/mullit/fma/fmac/fmamk/fmaak/min/max/min3/max3/med3/fract/floor/ceil/frexp_mant/sin/cos_f32` flush their f32 inputs and their result to a zero of the same sign (`v_mullit_f32` measured on RDNA2 (gfx1036) over 6907 rows per mode at IEEE=0 and IEEE=1, so a positive denormal src2 counts as zero); the f32 compares, `v_cvt_i32/u32/rpi_i32/flr_i32_f32`, `v_frexp_exp_i32_f32`, `v_cvt_f64_f32`, `v_cvt_f16_f32`, `v_cvt_pkrtz_f16_f32`, `v_cvt_pknorm_i16/u16_f32` and `v_cvt_pk_u8_f32` flush their f32 inputs. `v_div_scale_f32` flushes all three inputs to signed zeros, measured on RDNA2 (gfx1036) over 2744 operand triples per mode at IEEE=0 and IEEE=1 with f32 denormals flushed and kept; its scale-down behaviour and host denormal preservation remain incomplete. `v_div_fixup_f32` flushes its src1 and src2 and keeps a denormal src0 and result, measured on RDNA2 (gfx1036) over 6859 rows per mode at IEEE=0 and IEEE=1. Tininess is detected after rounding, measured on RDNA2 (gfx1036) for `v_mul_f32` and `v_mullit_f32` at IEEE=0 and IEEE=1: a product is flushed when its value rounded to 24 bits with an unbounded exponent is below 2^-126, so `0x3f7ffffe * 0x00800001` (2^-126 * (1 - 2^-46)) gives `0x00800000` and `0x3f7fffff * 0x00800000` (2^-126 * (1 - 2^-24)) gives zero. The check rounds in the f32 round mode of `FLOAT_MODE` (bits 1:0), measured in all four modes, while the products themselves are still rounded to nearest even. This applies to `v_mul_f32`, `v_mul_legacy_f32`, `v_mullit_f32`, and to `v_fma_f32`, `v_fmac_f32`, `v_fmamk_f32`, `v_fmaak_f32` and `v_fma_mix_f32` when the addend is zero; with a nonzero addend the fused result is kept, so an exact result just below 2^-126 that rounds to it is not flushed. In the three directed modes the check is computed exactly from the significands. To nearest even (0, the mode of shaders without a known float mode) one factor is multiplied by 2^64, which is exact below 2^64, and the product with the other factor is compared with 2^-62. That multiply is a plain `OpFMul` like the product, and no f32 rounding mode is requested in the SPIR-V, so the check relies on the driver rounding f32 to nearest even by default, as the products do, and a product that rounds up to 2^-126 is the emitted product. `v_fma_mix_f32` follows this rule with mixed f16/f32 sources too, measured on RDNA2 (gfx1036) with f16 in either multiplicand slot, low and high half selection, and an f32 or f16 zero addend: f32 `0x00801002` times f16 `0x3bff` gives `0x00800000` to nearest at IEEE=0 and IEEE=1, while the neighbours `0x00801001` and `0x00801003` give zero and `0x00800001`. The tiny-product boundary was also measured for both signs in all four f32 round modes at IEEE=0; f16 inputs retain their previous conversion and denormal handling. `v_fma_mixlo_f16` and `v_fma_mixhi_f16` flush their f32 sources too and keep a product below 2^-126, measured on RDNA2 (gfx1036). `v_mad_legacy_f32` and `v_mac_legacy_f32` flush their inputs, their product and their result in every mode, see its entry; their product is still checked for tininess on its exact value, so one below 2^-126 gives zero even when it rounds to 2^-126, as the console's separately rounded encoding is not measured at this boundary. Denormal modes 1 and 2 throw for the other instructions. Not covered, and not measurable on RDNA2 (no such opcodes) or not measured: `v_mac_f32`, `v_mad_f32`, `v_madak_f32` and `v_madmk_f32` (rounded separately), `v_dot2_f32_f16` and `v_div_fmas_f32` (on hardware `v_div_fmas_f32` flushes only its result), f32 interpolation and `ds_add_f32` keep the previous lowering; `ds_min/max/cmpst_f32` flush both operands by the field, see the LDS float atomics entry. With f32 denormals kept (3) the results depend on the driver's default denormal behaviour, as no `DenormPreserve` execution mode is requested; `v_ldexp_f32` and the f32 compares are the exceptions. `v_cmp_X_f32` and `v_cmpx_X_f32` compare the operands as sign-magnitude integers with the NaN cases handled apart, so a denormal is not read as zero, measured on RDNA2 (gfx1036) over 256 operand pairs per opcode at IEEE=0 and IEEE=1 with f32 denormals kept and flushed. `v_ldexp_f32` preserves subnormal inputs and rounds a subnormal result in the f32 round mode of `FLOAT_MODE`, measured on RDNA2 (gfx1036) over 2163 rows in each of the four round modes at IEEE=0 and IEEE=1. An overflow rounds like any other result (infinity, or the largest finite value when the mode rounds that sign toward zero). With f32 denormals flushed it flushes a subnormal input and result, measured over the same rows in each round mode at IEEE=0 only. A device whose driver splits `Fma` into a multiply and an add flushes a product below 2^-126 before adding a non-zero addend in `v_fma_f32` and `v_fmac_f32`: `0x1fffffff * 0x1fffffff + 0x00800000` gives `0x00800000` instead of the fused `0x00ffffff` on RADV RENOIR (Mesa 26.2.4), whose compiled shader has `v_mad_f32` and no `v_fma_f32`. [F32DenormalFlush](../../../core/libs/prx/libSceAgcDriver/tests/execution/F32DenormalFlush.cpp) accepts that sum on such a device. - [v_rcp, v_rsq, v_sqrt, v_log, v_exp, v_sin and v_cos](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) (f32 and f16) use the driver's approximation for finite results, not the console's: the low bits can differ. Only NaN, invalid, zero and infinite results and the exact sin/cos zeros are chosen on the bits. Measured on RDNA2 (gfx1036) over every f16 input and 32,832 f32 inputs: RADV returns the hardware's bits for every input of rcp, rsq, sqrt and log and all but one of exp (f16 0x11c5, 1 ULP); NVIDIA (615.71.09) stays within 2 ULP except `v_log_f32` near 1.0 (next entry). [v_sin and v_cos](../../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) take the driver's sine or cosine of the radians of the exact cycle, except within 2^-12 of a turn from a zero of the function, where the result is the linear term of the exact distance to the zero: the hardware's constant 6.2831826 (0x40c90fd5) at the integers, which the hardware applies exactly to inputs below 2^-12 of a turn, and the f32 2π at the half and quarter turns, where the hardware is within 7 ULP of it. Outside those bands, a driver result more than 4 ULP from a Taylor series in turns of the exact cycle is replaced by the series, which is within 2 ULP of the true sine and cosine; the hardware is up to 9 ULP from them (gfx1035, 243,092 f32 inputs), so results stay within 13 ULP of the hardware on any driver. RADV returns the hardware's bits for every f16 input and for 32,422 sine and 32,486 cosine results of the 32,832 f32 inputs; the rest are 1 ULP off, except up to 14 ULP on either side of non-zero integers and up to 15 ULP beyond ±256 cycles, where the hardware's own reduction differs from the exact fraction, and up to 6 ULP inside the half- and quarter-turn bands. NVIDIA's own sine is within 1 ULP on 93% of the f32 inputs and up to 1,166 ULP off just outside the bands, its cosine within 163 ULP; the series replaces those results. A denormal f32 input with f32 denormals kept gives the hardware the input times 6.2831826; both hosts flush it. From c1783f9d6444100e2f7a5dc3f1ba62a18e084452 Mon Sep 17 00:00:00 2001 From: fma-rc <188743521+fma-rc@users.noreply.github.com> Date: Wed, 14 Oct 2026 17:25:10 +0200 Subject: [PATCH 0095/1000] fix(libkernel): register a relinked guest module's image when Linux protects its pages --- .../prx/libc/include/GuestAllocations.hpp | 4 - core/libs/prx/libc/src/GuestAllocations.cpp | 94 +++++++++++++------ .../libkernel/DirectMemory/DirectMemory.cpp | 36 +++++++ .../libkernel/Module/src/DynamicLoader.cpp | 10 +- core/libs/tests/GuestMemory.cpp | 54 ++++++++++- 5 files changed, 157 insertions(+), 41 deletions(-) diff --git a/core/libs/prx/libc/include/GuestAllocations.hpp b/core/libs/prx/libc/include/GuestAllocations.hpp index ce42a0be61..e8b614729b 100644 --- a/core/libs/prx/libc/include/GuestAllocations.hpp +++ b/core/libs/prx/libc/include/GuestAllocations.hpp @@ -53,10 +53,8 @@ using Mapped = std::vector>; extern "C" { void* GuestAllocationsBegin_nid_postfix(); void GuestAllocationsRegisterMainImage_nid_postfix(void* mutation); -#ifdef _WIN32 void GuestAllocationsRegisterImage_nid_postfix(void* mutation, const void* image); void GuestAllocationsUnregisterImage_nid_postfix(void* mutation, const void* image); -#endif void GuestAllocationsEnd_nid_postfix(void* mutation) noexcept; void GuestAllocationsAdd_nid_postfix(void* mutation, void* pointer, std::size_t bytes, bool readable, bool writable, bool gpu); void GuestAllocationsRequireUnpinned_nid_postfix(void* mutation, const void* pointer, std::size_t bytes); @@ -94,10 +92,8 @@ public: Mutation(const Mutation&) = delete; Mutation& operator=(const Mutation&) = delete; void RegisterMainImage() { GuestAllocationsRegisterMainImage_nid_postfix(handle); } -#ifdef _WIN32 void RegisterImage(const void* image) { GuestAllocationsRegisterImage_nid_postfix(handle, image); } void UnregisterImage(const void* image) { GuestAllocationsUnregisterImage_nid_postfix(handle, image); } -#endif void Add(void* pointer, std::size_t bytes, bool readable, bool writable, bool gpu) { GuestAllocationsAdd_nid_postfix(handle, pointer, bytes, readable, writable, gpu); } void RequireUnpinned(const void* pointer, std::size_t bytes) const { GuestAllocationsRequireUnpinned_nid_postfix(handle, pointer, bytes); } void RequireAvailable(const void* pointer, std::size_t bytes) const { GuestAllocationsRequireAvailable_nid_postfix(handle, pointer, bytes); } diff --git a/core/libs/prx/libc/src/GuestAllocations.cpp b/core/libs/prx/libc/src/GuestAllocations.cpp index c7b286af87..a005511b40 100644 --- a/core/libs/prx/libc/src/GuestAllocations.cpp +++ b/core/libs/prx/libc/src/GuestAllocations.cpp @@ -227,43 +227,42 @@ void GuestAllocationsRegisterImage_nid_postfix(void*, const void* image) { registerImage(image); } -void GuestAllocationsUnregisterImage_nid_postfix(void* mutation, const void* image) { - auto& state = registry(); - const auto found = state.registeredImages.find(reinterpret_cast(image)); - if (found == state.registeredImages.end()) return; - const auto [begin, end] = *found; - GuestAllocationsRequireUnpinned_nid_postfix(mutation, image, end - begin); - recordChange(mutation, image, end - begin); - std::erase_if(state.ranges, [&](const auto& entry) { return !entry.second->releasable && entry.second->allocationAddress >= begin && entry.second->allocationAddress < end; }); - state.registeredImages.erase(found); -} #else -void GuestAllocationsRegisterMainImage_nid_postfix(void*) { - auto& state = registry(); - if (state.mainImageRegistered) return; +namespace { +struct ImagePages { struct Page { bool readable = false; bool writable = false; }; + std::uintptr_t base = 0; + bool main = false; + bool found = false; + std::uint64_t pageSize = 0; std::map pages; - const auto pageSize = static_cast(::sysconf(_SC_PAGESIZE)); - std::pair*, std::uint64_t> collection{&pages, pageSize}; - dl_iterate_phdr([](dl_phdr_info* image, std::size_t, void* data) { - auto& collected = *static_cast*, std::uint64_t>*>(data); - for (int index = 0; index < image->dlpi_phnum; ++index) { - const auto& header = image->dlpi_phdr[index]; - if (header.p_type != PT_LOAD || header.p_memsz == 0) continue; - const auto start = (image->dlpi_addr + header.p_vaddr) & ~(collected.second - 1); - const auto end = (image->dlpi_addr + header.p_vaddr + header.p_memsz + collected.second - 1) & ~(collected.second - 1); - for (auto page = start; page < end; page += collected.second) { - auto& entry = (*collected.first)[page]; - entry.readable = entry.readable || (header.p_flags & (PF_R | PF_W)) != 0; - entry.writable = entry.writable || (header.p_flags & PF_W) != 0; - } +}; + +int collectImagePages(dl_phdr_info* image, std::size_t, void* data) { + auto& collected = *static_cast(data); + if (!collected.main && image->dlpi_addr != collected.base) return 0; + collected.found = true; + for (int index = 0; index < image->dlpi_phnum; ++index) { + const auto& header = image->dlpi_phdr[index]; + if (header.p_type != PT_LOAD || header.p_memsz == 0) continue; + const auto start = (image->dlpi_addr + header.p_vaddr) & ~(collected.pageSize - 1); + const auto end = (image->dlpi_addr + header.p_vaddr + header.p_memsz + collected.pageSize - 1) & ~(collected.pageSize - 1); + for (auto page = start; page < end; page += collected.pageSize) { + auto& entry = collected.pages[page]; + entry.readable = entry.readable || (header.p_flags & (PF_R | PF_W)) != 0; + entry.writable = entry.writable || (header.p_flags & PF_W) != 0; } - return 1; - }, &collection); - require(!pages.empty(), "main guest image has no loadable segments"); + } + return 1; +} + +std::uintptr_t registerImagePages(const ImagePages& collected) { + auto& state = registry(); + const auto pageSize = collected.pageSize; + const auto& pages = collected.pages; auto replacement = state.ranges; for (auto page = pages.begin(); page != pages.end();) { auto last = page; @@ -280,10 +279,45 @@ void GuestAllocationsRegisterMainImage_nid_postfix(void*) { page = std::next(last); } state.ranges.swap(replacement); + return static_cast(std::prev(pages.end())->first + pageSize); +} +} + +void GuestAllocationsRegisterMainImage_nid_postfix(void*) { + auto& state = registry(); + if (state.mainImageRegistered) return; + ImagePages collected{.main = true, .pageSize = static_cast(::sysconf(_SC_PAGESIZE))}; + dl_iterate_phdr(collectImagePages, &collected); + require(!collected.pages.empty(), "main guest image has no loadable segments"); + registerImagePages(collected); state.mainImageRegistered = true; } + +void GuestAllocationsRegisterImage_nid_postfix(void*, const void* image) { + require(image != nullptr, "cannot register a guest image without a base"); + auto& state = registry(); + const auto base = reinterpret_cast(image); + if (state.registeredImages.contains(base)) return; + ImagePages collected{.base = base, .pageSize = static_cast(::sysconf(_SC_PAGESIZE))}; + dl_iterate_phdr(collectImagePages, &collected); + require(collected.found, "cannot locate a guest image"); + require(!collected.pages.empty(), "guest image has no loadable segments"); + require(collected.pages.begin()->first == base, "guest image does not start at its base"); + state.registeredImages.emplace(base, registerImagePages(collected)); +} #endif +void GuestAllocationsUnregisterImage_nid_postfix(void* mutation, const void* image) { + auto& state = registry(); + const auto found = state.registeredImages.find(reinterpret_cast(image)); + if (found == state.registeredImages.end()) return; + const auto [begin, end] = *found; + GuestAllocationsRequireUnpinned_nid_postfix(mutation, image, end - begin); + recordChange(mutation, image, end - begin); + std::erase_if(state.ranges, [&](const auto& entry) { return !entry.second->releasable && entry.second->allocationAddress >= begin && entry.second->allocationAddress < end; }); + state.registeredImages.erase(found); +} + void GuestAllocationsAdd_nid_postfix(void* mutation, void* pointer, std::size_t bytes, bool readable, bool writable, bool gpu) { recordChange(mutation, pointer, bytes); const auto address = reinterpret_cast(pointer); diff --git a/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp b/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp index c68e05455b..ae35567484 100644 --- a/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp +++ b/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp @@ -23,6 +23,7 @@ #if defined(__linux__) #include +#include #include #include #include @@ -1006,6 +1007,35 @@ static bool IsGuestModuleImage(const void* base) { } return false; } +#else +struct GuestImageSearch { + std::uintptr_t address = 0; + std::uintptr_t base = 0; + std::uintptr_t pageSize = 0; + bool guestModule = false; + bool first = true; + bool main = false; + bool found = false; +}; + +static int FindImageContaining(dl_phdr_info* image, std::size_t, void* data) { + auto& search = *static_cast(data); + const bool main = search.first; + search.first = false; + for (int index = 0; index < image->dlpi_phnum; ++index) { + const auto& header = image->dlpi_phdr[index]; + if (header.p_type != PT_LOAD) continue; + const auto start = (image->dlpi_addr + header.p_vaddr) & ~(search.pageSize - 1); + const auto end = (image->dlpi_addr + header.p_vaddr + header.p_memsz + search.pageSize - 1) & ~(search.pageSize - 1); + if (search.address < start || search.address >= end) continue; + search.base = image->dlpi_addr; + search.guestModule = image->dlpi_name != nullptr && std::string_view(image->dlpi_name).ends_with(".guest.prx"); + search.main = main; + search.found = true; + return 1; + } + return 0; +} #endif int DoMprotect(const void* addr, size_t len, int prot) { @@ -1046,6 +1076,12 @@ int DoMprotect(const void* addr, size_t len, int prot) { } #else mutation.RegisterMainImage(); + GuestImageSearch image{.address = address, .pageSize = static_cast(::sysconf(_SC_PAGESIZE))}; + dl_iterate_phdr(FindImageContaining, &image); + if (image.found && !image.main) { + if (!image.guestModule) throw std::invalid_argument("Memory protection of a foreign image is not supported"); + mutation.RegisterImage(reinterpret_cast(image.base)); + } #endif mutation.Protect(pointer, bytes, (prot & 3) != 0, (prot & 2) != 0, (prot & GuestProtGpuReadWrite) != 0, [&] { if (mprotect(const_cast(pointer), bytes, nativeProtection) != 0) throw std::system_error(errno, std::generic_category(), "mprotect failed"); diff --git a/core/libs/prx/libkernel/Module/src/DynamicLoader.cpp b/core/libs/prx/libkernel/Module/src/DynamicLoader.cpp index 4dac33c1dd..410f9973d8 100644 --- a/core/libs/prx/libkernel/Module/src/DynamicLoader.cpp +++ b/core/libs/prx/libkernel/Module/src/DynamicLoader.cpp @@ -226,26 +226,24 @@ void* APS5_VABI dlsym_nid_postfix(void* handle, const char* name) { int APS5_VABI dlclose_nid_postfix(void* handle) { const auto key = reinterpret_cast(handle); std::shared_ptr module; -#ifdef _WIN32 bool lastReference = true; -#endif { std::lock_guard lock(modulesMutex); auto found = modules.find(key); if (found == modules.end()) { Error("dlclose: invalid module handle"); return -1; } module = found->second; -#ifdef _WIN32 for (const auto& [other, entry] : modules) { if (other != key && entry->native == module->native) lastReference = false; } -#endif } -#ifdef _WIN32 if (module->owned && module->native && lastReference) { GuestAllocations::Mutation mutation; +#ifdef _WIN32 mutation.UnregisterImage(module->native); - } +#else + mutation.UnregisterImage(reinterpret_cast(static_cast(module->native)->l_addr)); #endif + } { std::lock_guard lock(modulesMutex); auto found = modules.find(key); diff --git a/core/libs/tests/GuestMemory.cpp b/core/libs/tests/GuestMemory.cpp index f9730f94c9..7ea75d2f95 100644 --- a/core/libs/tests/GuestMemory.cpp +++ b/core/libs/tests/GuestMemory.cpp @@ -895,6 +895,7 @@ static DWORD ImageProtection(const void* pointer) { Require(VirtualQuery(pointer, &memory, sizeof(memory)) == sizeof(memory)); return memory.Protect & 0xffu; } +#endif static unsigned char* GuestModulePage(void* module, std::size_t page) { auto* data = static_cast(dlsym_nid_postfix(module, "guestMemoryModuleData")); @@ -909,6 +910,7 @@ static bool RegisteredGuestRange(const void* pointer, std::size_t bytes) { return mutation.Overlaps(pointer, bytes); } +#ifdef _WIN32 static void CheckGuestModuleImageProtection() { constexpr std::size_t page = 0x4000; const auto path = std::filesystem::relative(GUEST_MEMORY_ELF0_MODULE).generic_string(); @@ -1015,6 +1017,54 @@ static void CheckGuestModuleImageProtection() { Require(!RegisteredGuestRange(plainTarget, page)); Require(dlclose_nid_postfix(plainModule) == 0); } +#elif defined(__linux__) +static std::string MappingPermissions(const void* pointer) { + const auto address = reinterpret_cast(pointer); + std::ifstream maps("/proc/self/maps"); + std::string line; + while (std::getline(maps, line)) { + unsigned long begin = 0; + unsigned long end = 0; + char permissions[5]{}; + if (std::sscanf(line.c_str(), "%lx-%lx %4s", &begin, &end, permissions) == 3 && address >= begin && address < end) return permissions; + } + return {}; +} + +static void CheckGuestModuleImageProtection() { + constexpr std::size_t page = 0x4000; + const auto path = std::filesystem::relative(GUEST_MEMORY_MODULE).generic_string(); + void* module = dlopen_nid_postfix(path.c_str(), 2); + Require(module != nullptr); + auto* target = GuestModulePage(module, page); + Require(!RegisteredGuestRange(target, page)); + Require(sceKernelMprotect(target, page, 1) == 0); + Require(RegisteredGuestRange(target, page)); + Require(MappingPermissions(target).starts_with("r--")); + Require(sceKernelMprotect(target, page, 3) == 0); + Require(MappingPermissions(target).starts_with("rw-")); + target[1] = 7; + Require(target[1] == 7); + Require(dlclose_nid_postfix(module) == 0); + Require(!RegisteredGuestRange(target, page)); + + module = dlopen_nid_postfix(path.c_str(), 2); + Require(module != nullptr); + target = GuestModulePage(module, page); + Require(!RegisteredGuestRange(target, page)); + Require(sceKernelMprotect(target, page, 1) == 0); + Require(RegisteredGuestRange(target, page)); + Require(dlclose_nid_postfix(module) == 0); + Require(!RegisteredGuestRange(target, page)); + + bool rejected = false; + try { + sceKernelMprotect(reinterpret_cast(&::getpid), page, 1); + } catch (const std::invalid_argument&) { + rejected = true; + } + Require(rejected); +} #endif static void CheckUnhintedKernelMappingsLandInTheWindow() { @@ -2570,8 +2620,10 @@ int main(int argc, char** argv) { CheckHeapAfterMappingReuse(); CheckNoOverwriteRejectsHostOccupiedMapping(); CheckFixedMappingsReachTheApplicationAreaEnd(); -#ifdef _WIN32 +#if defined(_WIN32) || defined(__linux__) CheckGuestModuleImageProtection(); +#endif +#ifdef _WIN32 CheckAliasAcrossDirectAllocations(); CheckAliasOfReorderedSharedPages(); CheckAliasRefusesPagesThatAreNotSharedViews(); From 793224067fbc920fbf7e3f38f15201a77bcb4862 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 14 Oct 2026 16:25:23 +0000 Subject: [PATCH 0096/1000] fix(shader): round and flush f16 conversion results by FLOAT_MODE in every denormal mode Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Ha6Tsy62AFUyp9a5Mf7vjA --- .../execution/F16ConversionFloatMode.cpp | 193 ++++++++++++++++++ .../Translation/TranslationContext.hpp | 1 + .../src/TranslationContext/SubwordPacking.cpp | 22 +- docs/dev/debt/shader.md | 2 +- 4 files changed, 214 insertions(+), 4 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/F16ConversionFloatMode.cpp diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/F16ConversionFloatMode.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/F16ConversionFloatMode.cpp new file mode 100644 index 0000000000..86aa123c60 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/F16ConversionFloatMode.cpp @@ -0,0 +1,193 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 64; +constexpr std::uint32_t Inputs = 4; +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; + +struct alignas(256) Shader { + std::array words; +}; + +struct Vector { + std::uint32_t a; + std::uint32_t b; + std::uint32_t expected[16]; +}; + +struct Case { + const char* name; + Shader shader; + std::span rows; +}; + +constexpr Vector VCvtF16F32Rows[] = { + {0x7f800001u, 0x00000000u, {0x00007e00u, 0x00007e00u, 0x00007e00u, 0x00007e00u, 0x00007e00u, 0x00007e00u, 0x00007e00u, 0x00007e00u, 0x00007e00u, 0x00007e00u, 0x00007e00u, 0x00007e00u, 0x00007e00u, 0x00007e00u, 0x00007e00u, 0x00007e00u}}, + {0x00000001u, 0x00000000u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}}, + {0x00000001u, 0x00000000u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}}, + {0x00800000u, 0x00000000u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000001u, 0x00000001u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}}, + {0x33400000u, 0x00000000u, {0x00000000u, 0x00000000u, 0x00000001u, 0x00000001u, 0x00000000u, 0x00000000u, 0x00000001u, 0x00000001u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}}, + {0x33800000u, 0x00000000u, {0x00000000u, 0x00000000u, 0x00000001u, 0x00000001u, 0x00000000u, 0x00000000u, 0x00000001u, 0x00000001u, 0x00000000u, 0x00000000u, 0x00000001u, 0x00000001u, 0x00000000u, 0x00000000u, 0x00000001u, 0x00000001u}}, + {0x33a00000u, 0x00000000u, {0x00000000u, 0x00000000u, 0x00000001u, 0x00000001u, 0x00000000u, 0x00000000u, 0x00000002u, 0x00000002u, 0x00000000u, 0x00000000u, 0x00000001u, 0x00000001u, 0x00000000u, 0x00000000u, 0x00000001u, 0x00000001u}}, + {0x33c00000u, 0x00000000u, {0x00000000u, 0x00000000u, 0x00000002u, 0x00000002u, 0x00000000u, 0x00000000u, 0x00000002u, 0x00000002u, 0x00000000u, 0x00000000u, 0x00000001u, 0x00000001u, 0x00000000u, 0x00000000u, 0x00000001u, 0x00000001u}}, + {0x387ff000u, 0x00000000u, {0x00000400u, 0x00000400u, 0x00000400u, 0x00000400u, 0x00000400u, 0x00000400u, 0x00000400u, 0x00000400u, 0x00000000u, 0x00000000u, 0x000003ffu, 0x000003ffu, 0x00000000u, 0x00000000u, 0x000003ffu, 0x000003ffu}}, + {0x38800800u, 0x00000000u, {0x00000400u, 0x00000400u, 0x00000400u, 0x00000400u, 0x00000401u, 0x00000401u, 0x00000401u, 0x00000401u, 0x00000400u, 0x00000400u, 0x00000400u, 0x00000400u, 0x00000400u, 0x00000400u, 0x00000400u, 0x00000400u}}, + {0x38801800u, 0x00000000u, {0x00000401u, 0x00000401u, 0x00000401u, 0x00000401u, 0x00000401u, 0x00000401u, 0x00000401u, 0x00000401u, 0x00000400u, 0x00000400u, 0x00000400u, 0x00000400u, 0x00000400u, 0x00000400u, 0x00000400u, 0x00000400u}}, + {0x7f800001u, 0x00000000u, {0x00007e00u, 0x00007e00u, 0x00007e00u, 0x00007e00u, 0x00007e00u, 0x00007e00u, 0x00007e00u, 0x00007e00u, 0x00007e00u, 0x00007e00u, 0x00007e00u, 0x00007e00u, 0x00007e00u, 0x00007e00u, 0x00007e00u, 0x00007e00u}}, + {0x80800000u, 0x00000000u, {0x00008000u, 0x00008000u, 0x00008000u, 0x00008000u, 0x00008000u, 0x00008000u, 0x00008000u, 0x00008000u, 0x00008000u, 0x00008000u, 0x00008001u, 0x00008001u, 0x00008000u, 0x00008000u, 0x00008000u, 0x00008000u}}, + {0xb3400000u, 0x00000000u, {0x00008000u, 0x00008000u, 0x00008001u, 0x00008001u, 0x00008000u, 0x00008000u, 0x00008000u, 0x00008000u, 0x00008000u, 0x00008000u, 0x00008001u, 0x00008001u, 0x00008000u, 0x00008000u, 0x00008000u, 0x00008000u}}, + {0xb3800000u, 0x00000000u, {0x00008000u, 0x00008000u, 0x00008001u, 0x00008001u, 0x00008000u, 0x00008000u, 0x00008001u, 0x00008001u, 0x00008000u, 0x00008000u, 0x00008001u, 0x00008001u, 0x00008000u, 0x00008000u, 0x00008001u, 0x00008001u}}, + {0xb3a00000u, 0x00000000u, {0x00008000u, 0x00008000u, 0x00008001u, 0x00008001u, 0x00008000u, 0x00008000u, 0x00008001u, 0x00008001u, 0x00008000u, 0x00008000u, 0x00008002u, 0x00008002u, 0x00008000u, 0x00008000u, 0x00008001u, 0x00008001u}}, + {0xb3c00000u, 0x00000000u, {0x00008000u, 0x00008000u, 0x00008002u, 0x00008002u, 0x00008000u, 0x00008000u, 0x00008001u, 0x00008001u, 0x00008000u, 0x00008000u, 0x00008002u, 0x00008002u, 0x00008000u, 0x00008000u, 0x00008001u, 0x00008001u}}, + {0xb87ff000u, 0x00000000u, {0x00008400u, 0x00008400u, 0x00008400u, 0x00008400u, 0x00008000u, 0x00008000u, 0x000083ffu, 0x000083ffu, 0x00008400u, 0x00008400u, 0x00008400u, 0x00008400u, 0x00008000u, 0x00008000u, 0x000083ffu, 0x000083ffu}}, + {0xb8800800u, 0x00000000u, {0x00008400u, 0x00008400u, 0x00008400u, 0x00008400u, 0x00008400u, 0x00008400u, 0x00008400u, 0x00008400u, 0x00008401u, 0x00008401u, 0x00008401u, 0x00008401u, 0x00008400u, 0x00008400u, 0x00008400u, 0x00008400u}}, + {0xb8801800u, 0x00000000u, {0x00008401u, 0x00008401u, 0x00008401u, 0x00008401u, 0x00008400u, 0x00008400u, 0x00008400u, 0x00008400u, 0x00008401u, 0x00008401u, 0x00008401u, 0x00008401u, 0x00008400u, 0x00008400u, 0x00008400u, 0x00008400u}}, +}; + +constexpr Vector VCvtF16F32ClampRows[] = { + {0x00800000u, 0x00000000u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000001u, 0x00000001u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}}, + {0x33400000u, 0x00000000u, {0x00000000u, 0x00000000u, 0x00000001u, 0x00000001u, 0x00000000u, 0x00000000u, 0x00000001u, 0x00000001u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}}, + {0x33800000u, 0x00000000u, {0x00000000u, 0x00000000u, 0x00000001u, 0x00000001u, 0x00000000u, 0x00000000u, 0x00000001u, 0x00000001u, 0x00000000u, 0x00000000u, 0x00000001u, 0x00000001u, 0x00000000u, 0x00000000u, 0x00000001u, 0x00000001u}}, + {0x33a00000u, 0x00000000u, {0x00000000u, 0x00000000u, 0x00000001u, 0x00000001u, 0x00000000u, 0x00000000u, 0x00000002u, 0x00000002u, 0x00000000u, 0x00000000u, 0x00000001u, 0x00000001u, 0x00000000u, 0x00000000u, 0x00000001u, 0x00000001u}}, + {0x33c00000u, 0x00000000u, {0x00000000u, 0x00000000u, 0x00000002u, 0x00000002u, 0x00000000u, 0x00000000u, 0x00000002u, 0x00000002u, 0x00000000u, 0x00000000u, 0x00000001u, 0x00000001u, 0x00000000u, 0x00000000u, 0x00000001u, 0x00000001u}}, + {0x387ff000u, 0x00000000u, {0x00000400u, 0x00000400u, 0x00000400u, 0x00000400u, 0x00000400u, 0x00000400u, 0x00000400u, 0x00000400u, 0x00000000u, 0x00000000u, 0x000003ffu, 0x000003ffu, 0x00000000u, 0x00000000u, 0x000003ffu, 0x000003ffu}}, + {0x38800800u, 0x00000000u, {0x00000400u, 0x00000400u, 0x00000400u, 0x00000400u, 0x00000401u, 0x00000401u, 0x00000401u, 0x00000401u, 0x00000400u, 0x00000400u, 0x00000400u, 0x00000400u, 0x00000400u, 0x00000400u, 0x00000400u, 0x00000400u}}, + {0x38801800u, 0x00000000u, {0x00000401u, 0x00000401u, 0x00000401u, 0x00000401u, 0x00000401u, 0x00000401u, 0x00000401u, 0x00000401u, 0x00000400u, 0x00000400u, 0x00000400u, 0x00000400u, 0x00000400u, 0x00000400u, 0x00000400u, 0x00000400u}}, +}; + +constexpr Vector VCvtF16U16Rows[] = { + {0x00000801u, 0x00000000u, {0x00006800u, 0x00006800u, 0x00006800u, 0x00006800u, 0x00006801u, 0x00006801u, 0x00006801u, 0x00006801u, 0x00006800u, 0x00006800u, 0x00006800u, 0x00006800u, 0x00006800u, 0x00006800u, 0x00006800u, 0x00006800u}}, + {0x00000803u, 0x00000000u, {0x00006802u, 0x00006802u, 0x00006802u, 0x00006802u, 0x00006802u, 0x00006802u, 0x00006802u, 0x00006802u, 0x00006801u, 0x00006801u, 0x00006801u, 0x00006801u, 0x00006801u, 0x00006801u, 0x00006801u, 0x00006801u}}, + {0x00008001u, 0x00000000u, {0x00007800u, 0x00007800u, 0x00007800u, 0x00007800u, 0x00007801u, 0x00007801u, 0x00007801u, 0x00007801u, 0x00007800u, 0x00007800u, 0x00007800u, 0x00007800u, 0x00007800u, 0x00007800u, 0x00007800u, 0x00007800u}}, + {0x00008011u, 0x00000000u, {0x00007801u, 0x00007801u, 0x00007801u, 0x00007801u, 0x00007801u, 0x00007801u, 0x00007801u, 0x00007801u, 0x00007800u, 0x00007800u, 0x00007800u, 0x00007800u, 0x00007800u, 0x00007800u, 0x00007800u, 0x00007800u}}, +}; + +constexpr Vector VCvtF16I16Rows[] = { + {0x00000801u, 0x00000000u, {0x00006800u, 0x00006800u, 0x00006800u, 0x00006800u, 0x00006801u, 0x00006801u, 0x00006801u, 0x00006801u, 0x00006800u, 0x00006800u, 0x00006800u, 0x00006800u, 0x00006800u, 0x00006800u, 0x00006800u, 0x00006800u}}, + {0x00000803u, 0x00000000u, {0x00006802u, 0x00006802u, 0x00006802u, 0x00006802u, 0x00006802u, 0x00006802u, 0x00006802u, 0x00006802u, 0x00006801u, 0x00006801u, 0x00006801u, 0x00006801u, 0x00006801u, 0x00006801u, 0x00006801u, 0x00006801u}}, + {0x00008001u, 0x00000000u, {0x0000f800u, 0x0000f800u, 0x0000f800u, 0x0000f800u, 0x0000f7ffu, 0x0000f7ffu, 0x0000f7ffu, 0x0000f7ffu, 0x0000f800u, 0x0000f800u, 0x0000f800u, 0x0000f800u, 0x0000f7ffu, 0x0000f7ffu, 0x0000f7ffu, 0x0000f7ffu}}, + {0x00008009u, 0x00000000u, {0x0000f7ffu, 0x0000f7ffu, 0x0000f7ffu, 0x0000f7ffu, 0x0000f7ffu, 0x0000f7ffu, 0x0000f7ffu, 0x0000f7ffu, 0x0000f800u, 0x0000f800u, 0x0000f800u, 0x0000f800u, 0x0000f7ffu, 0x0000f7ffu, 0x0000f7ffu, 0x0000f7ffu}}, +}; + +constexpr Vector VLdexpF16Rows[] = { + {0x00000001u, 0x0000ffe2u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000001u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}}, + {0x00000001u, 0x00000000u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000001u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000001u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000001u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000001u}}, + {0x00000001u, 0x0000000au, {0x00000000u, 0x00000400u, 0x00000000u, 0x00000400u, 0x00000000u, 0x00000400u, 0x00000000u, 0x00000400u, 0x00000000u, 0x00000400u, 0x00000000u, 0x00000400u, 0x00000000u, 0x00000400u, 0x00000000u, 0x00000400u}}, + {0x00000001u, 0x000003e8u, {0x00000000u, 0x00007c00u, 0x00000000u, 0x00007c00u, 0x00000000u, 0x00007c00u, 0x00000000u, 0x00007c00u, 0x00000000u, 0x00007bffu, 0x00000000u, 0x00007bffu, 0x00000000u, 0x00007bffu, 0x00000000u, 0x00007bffu}}, + {0x00000003u, 0x0000fffeu, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000001u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000001u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}}, + {0x00000003u, 0x0000ffffu, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000002u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000002u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000001u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000001u}}, + {0x00000155u, 0x0000fff8u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000001u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000002u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000001u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000001u}}, + {0x00000400u, 0x0000ffe2u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000001u, 0x00000001u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}}, + {0x00000400u, 0x0000fff6u, {0x00000000u, 0x00000000u, 0x00000001u, 0x00000001u, 0x00000000u, 0x00000000u, 0x00000001u, 0x00000001u, 0x00000000u, 0x00000000u, 0x00000001u, 0x00000001u, 0x00000000u, 0x00000000u, 0x00000001u, 0x00000001u}}, + {0x00000400u, 0x0000001eu, {0x00007c00u, 0x00007c00u, 0x00007c00u, 0x00007c00u, 0x00007c00u, 0x00007c00u, 0x00007c00u, 0x00007c00u, 0x00007bffu, 0x00007bffu, 0x00007bffu, 0x00007bffu, 0x00007bffu, 0x00007bffu, 0x00007bffu, 0x00007bffu}}, + {0x00000401u, 0x0000fff5u, {0x00000000u, 0x00000000u, 0x00000001u, 0x00000001u, 0x00000000u, 0x00000000u, 0x00000001u, 0x00000001u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}}, + {0x00000401u, 0x0000fff6u, {0x00000000u, 0x00000000u, 0x00000001u, 0x00000001u, 0x00000000u, 0x00000000u, 0x00000002u, 0x00000002u, 0x00000000u, 0x00000000u, 0x00000001u, 0x00000001u, 0x00000000u, 0x00000000u, 0x00000001u, 0x00000001u}}, + {0x00000555u, 0x0000fff7u, {0x00000000u, 0x00000000u, 0x00000003u, 0x00000003u, 0x00000000u, 0x00000000u, 0x00000003u, 0x00000003u, 0x00000000u, 0x00000000u, 0x00000002u, 0x00000002u, 0x00000000u, 0x00000000u, 0x00000002u, 0x00000002u}}, + {0x00008001u, 0x0000ffe2u, {0x00008000u, 0x00008000u, 0x00008000u, 0x00008000u, 0x00008000u, 0x00008000u, 0x00008000u, 0x00008000u, 0x00008000u, 0x00008000u, 0x00008000u, 0x00008001u, 0x00008000u, 0x00008000u, 0x00008000u, 0x00008000u}}, + {0x00008001u, 0x00000000u, {0x00008000u, 0x00008000u, 0x00008000u, 0x00008001u, 0x00008000u, 0x00008000u, 0x00008000u, 0x00008001u, 0x00008000u, 0x00008000u, 0x00008000u, 0x00008001u, 0x00008000u, 0x00008000u, 0x00008000u, 0x00008001u}}, + {0x00008001u, 0x0000000au, {0x00008000u, 0x00008400u, 0x00008000u, 0x00008400u, 0x00008000u, 0x00008400u, 0x00008000u, 0x00008400u, 0x00008000u, 0x00008400u, 0x00008000u, 0x00008400u, 0x00008000u, 0x00008400u, 0x00008000u, 0x00008400u}}, + {0x00008001u, 0x000003e8u, {0x00008000u, 0x0000fc00u, 0x00008000u, 0x0000fc00u, 0x00008000u, 0x0000fbffu, 0x00008000u, 0x0000fbffu, 0x00008000u, 0x0000fc00u, 0x00008000u, 0x0000fc00u, 0x00008000u, 0x0000fbffu, 0x00008000u, 0x0000fbffu}}, + {0x00008003u, 0x0000fffeu, {0x00008000u, 0x00008000u, 0x00008000u, 0x00008001u, 0x00008000u, 0x00008000u, 0x00008000u, 0x00008000u, 0x00008000u, 0x00008000u, 0x00008000u, 0x00008001u, 0x00008000u, 0x00008000u, 0x00008000u, 0x00008000u}}, + {0x00008003u, 0x0000ffffu, {0x00008000u, 0x00008000u, 0x00008000u, 0x00008002u, 0x00008000u, 0x00008000u, 0x00008000u, 0x00008001u, 0x00008000u, 0x00008000u, 0x00008000u, 0x00008002u, 0x00008000u, 0x00008000u, 0x00008000u, 0x00008001u}}, + {0x00008155u, 0x0000fff8u, {0x00008000u, 0x00008000u, 0x00008000u, 0x00008001u, 0x00008000u, 0x00008000u, 0x00008000u, 0x00008001u, 0x00008000u, 0x00008000u, 0x00008000u, 0x00008002u, 0x00008000u, 0x00008000u, 0x00008000u, 0x00008001u}}, + {0x00008400u, 0x0000ffe2u, {0x00008000u, 0x00008000u, 0x00008000u, 0x00008000u, 0x00008000u, 0x00008000u, 0x00008000u, 0x00008000u, 0x00008000u, 0x00008000u, 0x00008001u, 0x00008001u, 0x00008000u, 0x00008000u, 0x00008000u, 0x00008000u}}, + {0x00008400u, 0x0000fff6u, {0x00008000u, 0x00008000u, 0x00008001u, 0x00008001u, 0x00008000u, 0x00008000u, 0x00008001u, 0x00008001u, 0x00008000u, 0x00008000u, 0x00008001u, 0x00008001u, 0x00008000u, 0x00008000u, 0x00008001u, 0x00008001u}}, + {0x00008400u, 0x0000001eu, {0x0000fc00u, 0x0000fc00u, 0x0000fc00u, 0x0000fc00u, 0x0000fbffu, 0x0000fbffu, 0x0000fbffu, 0x0000fbffu, 0x0000fc00u, 0x0000fc00u, 0x0000fc00u, 0x0000fc00u, 0x0000fbffu, 0x0000fbffu, 0x0000fbffu, 0x0000fbffu}}, + {0x00008401u, 0x0000fff5u, {0x00008000u, 0x00008000u, 0x00008001u, 0x00008001u, 0x00008000u, 0x00008000u, 0x00008000u, 0x00008000u, 0x00008000u, 0x00008000u, 0x00008001u, 0x00008001u, 0x00008000u, 0x00008000u, 0x00008000u, 0x00008000u}}, + {0x00008401u, 0x0000fff6u, {0x00008000u, 0x00008000u, 0x00008001u, 0x00008001u, 0x00008000u, 0x00008000u, 0x00008001u, 0x00008001u, 0x00008000u, 0x00008000u, 0x00008002u, 0x00008002u, 0x00008000u, 0x00008000u, 0x00008001u, 0x00008001u}}, + {0x00008555u, 0x0000fff7u, {0x00008000u, 0x00008000u, 0x00008003u, 0x00008003u, 0x00008000u, 0x00008000u, 0x00008002u, 0x00008002u, 0x00008000u, 0x00008000u, 0x00008003u, 0x00008003u, 0x00008000u, 0x00008000u, 0x00008002u, 0x00008002u}}, +}; + +const Case Cases[] = { + {"v_cvt_f16_f32", {{0x34020084u, 0x34040082u, 0xe0381000u, 0x80000401u, 0xbf8c3f70u, 0x7e140280u, 0x7e141504u, 0xe0701000u, 0x80010a02u, 0xbf810000u, 0xbf810000u, 0xbf810000u}}, VCvtF16F32Rows}, + {"v_cvt_f16_f32 clamp", {{0x34020084u, 0x34040082u, 0xe0381000u, 0x80000401u, 0xbf8c3f70u, 0x7e140280u, 0xd58a800au, 0x00000104u, 0xe0701000u, 0x80010a02u, 0xbf810000u, 0xbf810000u}}, VCvtF16F32ClampRows}, + {"v_cvt_f16_u16", {{0x34020084u, 0x34040082u, 0xe0381000u, 0x80000401u, 0xbf8c3f70u, 0x7e140280u, 0x7e14a104u, 0xe0701000u, 0x80010a02u, 0xbf810000u, 0xbf810000u, 0xbf810000u}}, VCvtF16U16Rows}, + {"v_cvt_f16_i16", {{0x34020084u, 0x34040082u, 0xe0381000u, 0x80000401u, 0xbf8c3f70u, 0x7e140280u, 0x7e14a304u, 0xe0701000u, 0x80010a02u, 0xbf810000u, 0xbf810000u, 0xbf810000u}}, VCvtF16I16Rows}, + {"v_ldexp_f16", {{0x34020084u, 0x34040082u, 0xe0381000u, 0x80000401u, 0xbf8c3f70u, 0x7e140280u, 0x76140b04u, 0xe0701000u, 0x80010a02u, 0xbf810000u, 0xbf810000u, 0xbf810000u}}, VLdexpF16Rows}, +}; + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + +void Run(AgcDriver::VulkanDevice& device, const Case& item, std::uint32_t round16, std::uint32_t denorm16) { + Input.fill(0u); + for (std::uint32_t row = 0; row < item.rows.size(); ++row) { + Input[row * Inputs] = item.rows[row].a; + Input[row * Inputs + 1] = item.rows[row].b; + } + Output.fill(0xdeadbeefu); + std::vector userData(8, 0u); + const auto input = BufferDescriptor(Input.data(), static_cast(Input.size() * 4u)); + const auto output = BufferDescriptor(Output.data(), static_cast(Output.size() * 4u)); + std::copy(input.begin(), input.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4); + const std::span code(item.shader.words); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + request.context.floatMode = ShaderRecompiler::ShaderFloatMode{(round16 << 2u) | (denorm16 << 6u), true, false, false}; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); +} + +void Check(const Case& item, std::uint32_t round16, std::uint32_t denorm16) { + for (std::uint32_t row = 0; row < item.rows.size(); ++row) { + const auto& vector = item.rows[row]; + const auto expected = vector.expected[round16 * 4u + denorm16]; + Require(Output[row] == expected, std::string(item.name) + " round16 " + std::to_string(round16) + " denorm16 " + std::to_string(denorm16) + " inputs " + + Hex(vector.a) + " " + Hex(vector.b) + " is " + Hex(Output[row]) + ", expected " + Hex(expected)); + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + for (const auto& item : Cases) { + for (std::uint32_t round16 = 0u; round16 < 4u; ++round16) { + for (std::uint32_t denorm16 = 0u; denorm16 < 4u; ++denorm16) { + Run(*device, item, round16, denorm16); + Check(item, round16, denorm16); + } + } + } + std::puts("f16 conversion float mode tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp b/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp index 711f219057..e7d442382a 100644 --- a/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp +++ b/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp @@ -73,6 +73,7 @@ private: IrU32 flushF16Result(IrU32 bits); IrF32 flushF16InputValue(IrF32 value); IrU32 convertF32ToF16Bits(IrF32 value, std::initializer_list sources); + IrU32 flushTinyF16Conversion(IrF32 value, IrU32 bits); void writeF16Conversion(const RdnaOperand& operand, IrF32 value, std::initializer_list sources); IrU32 readU32(const RdnaOperand& operand); IrU32 flushF32Denormal(IrU32 bits); diff --git a/core/shader/recompiler/Translation/src/TranslationContext/SubwordPacking.cpp b/core/shader/recompiler/Translation/src/TranslationContext/SubwordPacking.cpp index cf6c3a789f..f64a0c72ee 100644 --- a/core/shader/recompiler/Translation/src/TranslationContext/SubwordPacking.cpp +++ b/core/shader/recompiler/Translation/src/TranslationContext/SubwordPacking.cpp @@ -165,7 +165,7 @@ void TranslationContext::write16Bits(const RdnaOperand& operand, IrU32 value) { } bool TranslationContext::directedF16Rounding() const { - return floatMode.has_value() && ((floatMode->floatMode >> 2u) & 3u) != 0u && ((floatMode->floatMode >> 6u) & 3u) == 3u; + return floatMode.has_value() && ((floatMode->floatMode >> 2u) & 3u) != 0u; } IrU32 TranslationContext::flushF16Bits(IrU32 bits, std::uint32_t keepBit) { @@ -220,12 +220,28 @@ IrU32 TranslationContext::convertF32ToF16Bits(IrF32 value, std::initializer_list return IrU32(ir.Select(ir.ULessThan(magnitude.Value(), ir.Constant(0x7f800000u)), result.Value(), native.Value())); } +IrU32 TranslationContext::flushTinyF16Conversion(IrF32 value, IrU32 bits) { + if (!floatMode.has_value() || (floatMode->floatMode & 0x80u) != 0u) return bits; + const std::uint32_t rounding = (floatMode->floatMode >> 2u) & 3u; + const IrU32 word(ir.BitCastU32(value.Value())); + const IrU32 magnitude(ir.BitwiseAnd(word.Value(), ir.Constant(0x7fffffffu))); + const IrU32 sign(ir.BitwiseAnd(ir.ShiftRightLogical(word.Value(), ir.Constant(16u)), ir.Constant(0x8000u))); + IrValue* limit = &ir.Constant(rounding == 0u ? 0x387ff000u : 0x38800000u); + if (rounding == 1u || rounding == 2u) { + const IrU1 away(ir.IEqual(sign.Value(), ir.Constant(rounding == 1u ? 0u : 0x8000u))); + limit = &ir.Select(away.Value(), ir.Constant(0x387fe001u), ir.Constant(0x38800000u)); + } + const IrU1 tiny(ir.ULessThan(magnitude.Value(), *limit)); + return IrU32(ir.Select(tiny.Value(), sign.Value(), bits.Value())); +} + void TranslationContext::writeF16Conversion(const RdnaOperand& operand, IrF32 value, std::initializer_list sources) { - if (!directedF16Rounding()) { + if (!directedF16Rounding() && (!floatMode.has_value() || (floatMode->floatMode & 0x80u) != 0u)) { writeF16(operand, value, sources); return; } - write16Bits(operand, convertF32ToF16Bits(applyF16ResultModifiers(operand, value), sources)); + const IrF32 modified = applyF16ResultModifiers(operand, value); + write16Bits(operand, flushTinyF16Conversion(modified, convertF32ToF16Bits(modified, sources))); } void TranslationContext::writeF16(const RdnaOperand& operand, IrF32 value, std::initializer_list sources) { diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index db9fedf073..619eab02f2 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -95,7 +95,7 @@ - The [output modifier](../../../core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp) (`mul:2`, `mul:4`, `div:2`) follows the shader's float mode as measured on RDNA2 (gfx1035): the hardware applies it only with IEEE mode off and output denormals of the result's precision flushed (bit 1 of the `FLOAT_MODE` denormal field clear), and ignores it otherwise. On f32 results it is applied as the hardware does with f32 denormals flushed; a shader without a known float mode is translated with IEEE mode off and f32 denormals flushed, the mode titles use. On f16 and f64 results it is ignored, and it throws in the modes where the hardware applies it. - The [clamp modifier](../../../core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp) follows `DX10_CLAMP`, measured on RDNA2 (gfx1035) for f32, f16, packed f16 and f64 results at IEEE=0 and IEEE=1: with `DX10_CLAMP`=1 (and in shaders without a known float mode) a NaN result clamps to +0, including the quieted NaN that `v_pk_min_f16` and `v_pk_max_f16` return for a signaling NaN operand with IEEE=1 (measured on RDNA2 gfx1036 at both `IEEE_MODE` and both `DX10_CLAMP` settings); with `DX10_CLAMP`=0 a NaN result is written unchanged and every other value clamps to [0, 1] as before. f64 results follow the same rule on hardware, but a clamped f64 result with `DX10_CLAMP`=0 throws until it has an execution test. With `DX10_CLAMP`=0 the NaN's bits are those the translation already produces for the instruction without clamp, so where those are left to the driver (f32 and scalar f16 arithmetic) the payload is the driver's, not the hardware's input propagation. `v_dot2_f32_f16` and the integer clamps are unchanged. - [FP16_OVFL](../../../core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp) follows the shader's float mode as measured on RDNA2 (gfx1036) at IEEE=0 and IEEE=1 with f16 denormals kept and flushed: with `FP16_OVFL`=1 an f16 result that rounds to ±infinity is written as ±0x7bff unless a source operand is infinite, so `v_rcp_f16`, `v_rsq_f16` and `v_log_f16` of ±0 and `v_exp_f16` of 16 or more are clamped while an infinite input passes through, and `v_div_fixup_f16` keeps the infinity it writes for a zero denominator or an infinite numerator and clamps any other infinite quotient, a NaN one included. `v_cvt_pkrtz_f16_f32` rounds toward zero and never overflows. The sources are checked as the f32 values the translation computes with; the rule is applied to `v_fma_mix*_f16` with f32 sources and to `v_interp_p2_f16` without a measurement of infinite f32 sources or parameters. Round modes other than nearest even are not modelled, so the overflow threshold is the nearest-even one (65520). -- [Directed f16 rounding](../../../core/shader/recompiler/Translation/src/TranslationContext/SubwordPacking.cpp) for `v_cvt_f16_f32`, `v_cvt_f16_i16`, `v_cvt_f16_u16` and `v_ldexp_f16` follows `FLOAT_MODE` bits 3:2 only when f16 denormals are preserved (bits 7:6 = 3), measured on RDNA2 (gfx1036). With f16 denormals flushed (bits 7:6 = 0), or flushed on one side (modes 1 and 2, not measured), the hardware also flushes the f16 result and the directed path does not model that, so those shaders keep round-to-nearest-even. The `clamp` modifier with directed rounding is not measured. +- [Directed f16 rounding](../../../core/shader/recompiler/Translation/src/TranslationContext/SubwordPacking.cpp) for `v_cvt_f16_f32`, `v_cvt_f16_i16`, `v_cvt_f16_u16` and `v_ldexp_f16` follows `FLOAT_MODE` bits 3:2 in every f16 denormal mode, measured on RDNA2 (gfx1036 with f16 denormals preserved, gfx1030 in all sixteen combinations of f16 rounding and denormal mode). When bit 7 flushes f16 results, `v_cvt_f16_f32`, `v_cvt_f16_i16` and `v_cvt_f16_u16` write a zero of the result's sign for a value that, rounded in that mode with an unbounded exponent, is below 2^-14, so a value just below 2^-14 that rounds up to it stays 0x0400; `v_ldexp_f16` keeps its rule above. The `clamp` modifier is applied before the rounding. On gfx1030 every row matches: 8,996 f32 inputs around the half denormal, normal and overflow boundaries with and without `clamp`, every 16-bit integer, and 2,470 `v_ldexp_f16` operands. - [Comparison sampling of a color texture](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) (`image_sample_c`/`image_sample_c_lz` on a texture that is not R32 float or R16 unorm) is emulated in the shader, since Vulkan compares only depth formats: the red channel is fetched, compared and, for bilinear filtering, blended (compare, then filter). Only 2D, 2D array and cube views, float/unorm/snorm formats, wrap, clamp-to-edge and clamp-to-border addressing (border red 0 for the black borders, 1 for opaque white) and point or bilinear filtering at one level are implemented, with texel offsets added to the texel indices; LOD biases and clamps are accepted, as they cannot select another level of a single-level sample. Half-border addressing, the border color table, gradients, gathers, other dimensions, anisotropy (except on the 8-bit formats below) and mip filtering throw. Under bilinear filtering, taps past a clamp-to-border edge compare the border value and blend with the texel taps, so results near the edge depend on the filter weights. AMD documentation does not say how a color format's reference is handled: it is clamped to [0, 1] for unorm and [-1, 1] for snorm, as Vulkan does for unorm depth, and not clamped for float. 8-bit sRGB and UNORM (`Format8Srgb`, `Format8_8Srgb`, `Format8_8_8_8Srgb`, `Format8UNorm`, `Format8_8UNorm`, `Format8_8_8_8UNorm`) return 0 for every compare function, measured with `image_sample_c_lz` on gfx1035 over references -0.5..1.5 and texel codes 0..255. A bilinear cube of those 8-bit formats without `DISABLE_CUBE_WRAP`, and an anisotropic S# on them, return 0 for the same reason; float, snorm and the other unorm bilinear cubes without that bit still throw. Bilinear weights are full float, not the hardware's fixed-point subtexel weights - [Negative color-texture comparison wrap indices](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) (libSceAgcDriver) use explicit `OpSRem` normalization to implement modulo; the negative-offset point-wrap regression fails with direct `OpSMod` and passes with normalization on Mesa 22.3.6 llvmpipe (LLVM 15.0.6), but negative-index wrap has not been measured on PS5 hardware. - [Converted unorm images](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) (10_11_11_UNORM, read and written through an R32_UINT view): samples, gathers, LOD queries, 16-bit data and a DST_SEL of the missing W channel throw. From ce17977ed91e537996f2bca8731dfa8005fca34f Mon Sep 17 00:00:00 2001 From: RINNECODER <93304526+RINNECODER@users.noreply.github.com> Date: Wed, 14 Oct 2026 11:30:16 -0500 Subject: [PATCH 0097/1000] fix(tools): key compile commands relative to the checkout in check_compile The key was the source path relative to the common parent of the build directory and the source. With a build directory beside the checkout that parent is above the checkout, so the key carried the checkout directory name and no changed file matched. The key is now the source path relative to the repository root. --- tools/check_compile.py | 14 ++++++++------ tools/tests/test_check_compile.py | 18 ++++++++++++++++++ 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/tools/check_compile.py b/tools/check_compile.py index 1ec01a433c..c15a8dc1d0 100644 --- a/tools/check_compile.py +++ b/tools/check_compile.py @@ -40,20 +40,22 @@ def entries(build): return json.loads(result.stdout) -def entry_key(entry): +def entry_key(entry, root): directory = Path(entry["directory"]).resolve() source = Path(entry["file"]) if not source.is_absolute(): source = directory / source source = source.resolve() - common = Path(os.path.commonpath([str(directory), str(source)])) - return os.path.relpath(source, common).replace(os.sep, "/") + try: + return source.relative_to(Path(root).resolve()).as_posix() + except ValueError: + return source.as_posix() -def index(build): +def index(build, root): commands = {} for entry in entries(build): - commands.setdefault(entry_key(entry), entry) + commands.setdefault(entry_key(entry, root), entry) return commands @@ -96,7 +98,7 @@ def syntax_only(entry, source): def check(root, build, files): - commands = index(build) + commands = index(build, root) checked = 0 failed = 0 for file in files: diff --git a/tools/tests/test_check_compile.py b/tools/tests/test_check_compile.py index b691d60d45..dd27d12654 100644 --- a/tools/tests/test_check_compile.py +++ b/tools/tests/test_check_compile.py @@ -124,6 +124,24 @@ class CheckCompileTests(unittest.TestCase): self.assertEqual(result.returncode, 0) self.assertIn("1 translation units compile", result.stdout) + def test_a_build_directory_beside_the_checkout_is_matched(self): + repo = self.root / "AnyPS5" + repo.mkdir() + subprocess.run(["git", "init", "-q", str(repo)], check=True) + sibling = self.root / "sibling-build" + sibling.mkdir() + source = repo / "src" / "unit.cpp" + source.parent.mkdir() + source.write_text("int x;\n", encoding="utf-8") + script = self.compiler("import sys\nsys.exit(0)\n") + entry = {"directory": str(sibling), "command": f'"{sys.executable}" "{script}" "{source}"', + "file": str(source)} + (sibling / "compile_commands.json").write_text(json.dumps([entry]), encoding="utf-8") + result = subprocess.run([sys.executable, "-I", str(TOOL), "--build", str(sibling), "src/unit.cpp"], + cwd=repo, capture_output=True, text=True) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertIn("1 translation units compile", result.stdout) + def test_a_changed_file_with_a_non_ascii_name_is_checked(self): self.failing("caf\u00e9.cpp") subprocess.run(["git", "-C", str(self.root), "add", "caf\u00e9.cpp"], check=True) From 5474b7756dc935df60ecad42e57d4ec31b60f12d Mon Sep 17 00:00:00 2001 From: Ondra97 <47064087+Ondra97@users.noreply.github.com> Date: Wed, 14 Oct 2026 18:33:02 +0200 Subject: [PATCH 0098/1000] fix(avplayer): close safely from event callbacks --- core/libs/prx/libSceAvPlayer/CMakeLists.txt | 6 ++ .../prx/libSceAvPlayer/include/AvPlayer.hpp | 3 +- core/libs/prx/libSceAvPlayer/src/Player.cpp | 12 +++- core/libs/tests/GuestAvPlayer.cpp | 61 ++++++++++++++++++- 4 files changed, 77 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libSceAvPlayer/CMakeLists.txt b/core/libs/prx/libSceAvPlayer/CMakeLists.txt index bdc296e7eb..0acb2eeeb0 100644 --- a/core/libs/prx/libSceAvPlayer/CMakeLists.txt +++ b/core/libs/prx/libSceAvPlayer/CMakeLists.txt @@ -11,11 +11,17 @@ if(BUILD_TESTING) configure_windows_unwind(guest_avplayer_tests) file(MAKE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/guest_avplayer) add_test(NAME guest_avplayer COMMAND guest_avplayer_tests) + add_test(NAME guest_avplayer_callback_close COMMAND guest_avplayer_tests --callback-close) set_tests_properties(guest_avplayer PROPERTIES WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/guest_avplayer TIMEOUT 60) + set_tests_properties(guest_avplayer_callback_close PROPERTIES + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/guest_avplayer + TIMEOUT 20 RUN_SERIAL TRUE) if(WIN32) set_tests_properties(guest_avplayer PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + set_tests_properties(guest_avplayer_callback_close PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") endif() endif() diff --git a/core/libs/prx/libSceAvPlayer/include/AvPlayer.hpp b/core/libs/prx/libSceAvPlayer/include/AvPlayer.hpp index 740f552d2e..40ea457572 100644 --- a/core/libs/prx/libSceAvPlayer/include/AvPlayer.hpp +++ b/core/libs/prx/libSceAvPlayer/include/AvPlayer.hpp @@ -126,7 +126,7 @@ private: void OnWarning(std::int32_t code) override; void OnError() override; void queue(const Event& event); - void controllerLoop(); + void controllerLoop(const bool& closed); void deliver(const Event& event); void autoStart(); void checkEndOfFile(); @@ -154,6 +154,7 @@ private: TimedWait::Condition eventCondition; std::deque events; bool quit = false; + std::shared_ptr controllerClosed = std::make_shared(false); std::thread controller; }; diff --git a/core/libs/prx/libSceAvPlayer/src/Player.cpp b/core/libs/prx/libSceAvPlayer/src/Player.cpp index 6f418b8421..f5c51ce58e 100644 --- a/core/libs/prx/libSceAvPlayer/src/Player.cpp +++ b/core/libs/prx/libSceAvPlayer/src/Player.cpp @@ -70,7 +70,7 @@ Player::Player(const AvPlayerInitData& init, std::uint32_t bufferCount) if (init.default_language) { for (std::size_t index = 0; index < 3 && init.default_language[index] != '\0'; ++index) language[index] = init.default_language[index]; } - controller = std::thread([this] { controllerLoop(); }); + controller = std::thread([this, closed = controllerClosed] { controllerLoop(*closed); }); } Player::~Player() { @@ -79,7 +79,12 @@ Player::~Player() { quit = true; } eventCondition.NotifyAll(); - controller.join(); + if (controller.get_id() == std::this_thread::get_id()) { + *controllerClosed = true; + controller.detach(); + } else { + controller.join(); + } std::lock_guard lock(mutex); source.reset(); } @@ -100,7 +105,7 @@ void Player::OnError() { queue({0, 0, false, true}); } -void Player::controllerLoop() { +void Player::controllerLoop(const bool& closed) { for (;;) { Event event{}; bool pending = false; @@ -119,6 +124,7 @@ void Player::controllerLoop() { } else { checkEndOfFile(); } + if (closed) return; } } diff --git a/core/libs/tests/GuestAvPlayer.cpp b/core/libs/tests/GuestAvPlayer.cpp index 0410bbab96..b673fd6381 100644 --- a/core/libs/tests/GuestAvPlayer.cpp +++ b/core/libs/tests/GuestAvPlayer.cpp @@ -1178,10 +1178,69 @@ void TestEofStopLatency() { Check(median < std::chrono::milliseconds(10), "eof stop median " + std::to_string(medianMs) + " ms exceeds 10 ms"); } +struct ClosingEvents { + std::mutex mutex; + AvPlayerInternal* player = nullptr; + AvPlayerInternal* replacement = nullptr; + std::int32_t closeEvent; + bool closed = false; +}; + +void APS5_VABI CloseOnEvent(void* object, std::int32_t id, std::int32_t, void*) { + auto& events = *static_cast(object); + if (id != events.closeEvent) return; + std::lock_guard lock(events.mutex); + Check(sceAvPlayerClose(events.player) == 0, "close from event callback failed"); + events.player = nullptr; + AvPlayerInitData init = InitData(nullptr); + events.replacement = sceAvPlayerInit(&init); + Check(events.replacement != nullptr, "replacement player init from callback failed"); + events.closed = true; } -int main() { +void TestCloseFromEventCallback(std::int32_t event) { + ClosingEvents events; + events.closeEvent = event; + AvPlayerInitData init = InitData(nullptr); + init.event_replacement = {&events, CloseOnEvent}; + init.auto_start = event == EventStop; + { + std::lock_guard lock(events.mutex); + events.player = sceAvPlayerInit(&init); + Check(events.player != nullptr, "callback-close init failed"); + Check(sceAvPlayerAddSource(events.player, "/app0/avplayer.mp4") == 0, "callback-close source failed"); + } + Check(WaitFor([&] { + std::lock_guard lock(events.mutex); + if (events.closed) return true; + if (event == EventStop) { + AvPlayerFrameInfoEx video{}; + AvPlayerFrameInfo audio{}; + if (sceAvPlayerGetVideoDataEx(events.player, &video)) CheckVideoFrame(video); + if (sceAvPlayerGetAudioData(events.player, &audio)) CheckEnglishAudio(audio); + } + return false; + }), "event callback did not finish closing the player"); + { + std::lock_guard lock(events.mutex); + Check(sceAvPlayerClose(events.replacement) == 0, "replacement player close failed"); + } + std::lock_guard lock(allocations.mutex); + Check(allocations.blocks.empty(), "callback close leaked media allocations"); +} + +} + +int main(int argc, char** argv) { try { + if (argc == 2 && std::strcmp(argv[1], "--callback-close") == 0) { + WriteMovie(); + TestCloseFromEventCallback(EventReady); + TestCloseFromEventCallback(EventStop); + std::puts("AvPlayer callback close tests passed"); + return 0; + } + Check(argc == 1, "unknown AvPlayer test arguments"); WriteMovie(); TestPlayback(); TestChangeStream(); From 9926c6b76b16e9493cc41d6771fc8358112177e4 Mon Sep 17 00:00:00 2001 From: JosepBove Date: Wed, 14 Oct 2026 18:36:33 +0200 Subject: [PATCH 0099/1000] docs(ngs2): cite the PS5 module for the voice-index error --- docs/dev/debt/libSceNgs2.native.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceNgs2.native.md b/docs/dev/debt/libSceNgs2.native.md index 7ea9329ad2..15628d8a8d 100644 --- a/docs/dev/debt/libSceNgs2.native.md +++ b/docs/dev/debt/libSceNgs2.native.md @@ -29,4 +29,4 @@ ### Functional -- [sceNgs2RackGetVoiceHandle voice index](../../../core/libs/prx/libSceNgs2.native/src/Rack.cpp) (libSceNgs2.native) - an index at or beyond the rack's voice count returns `0x804A8305` and leaves the output handle untouched, after the null-output and rack-handle checks that keep their order and codes. The value follows [prosper's analysis of `libSceNgs2.native.sprx`](https://github.com/mattias800/prosper/blob/d61eb2c0ad2de879ef74df9c4aa050c512804a18/prosper/src/hle/audio/hle_audio.cpp#L4299) (export `MwmHz8pAdAo` at `0xfbd0`, bounds error at `0xfc73`), which also resolves the rack before the output check; [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/e6faa2e3b6794a2fa79a709352ddfccfaa817ed1/src/core/libraries/ngs2/ngs2_error.h) lists `0x804A0302` for the PS4 library. Neither is a PS5 measurement. Heretic + Hexen (PPSA26525), DOOM + DOOM II (PPSA21444) and STAR WARS: Dark Forces Remaster (PPSA19728) create a one-voice reverb rack, take voice 0 and ask for voices 1 to 3 at start-up, log the failures and go on +- [sceNgs2RackGetVoiceHandle voice index](../../../core/libs/prx/libSceNgs2.native/src/Rack.cpp) (libSceNgs2.native) - an index at or beyond the rack's voice count returns `0x804A8305` and leaves the output handle untouched, after the null-output and rack-handle checks that keep their order and codes. The value is read from the PS5 `libSceNgs2.native.sprx` export `MwmHz8pAdAo` (firmware 10.01 at `0x11f10`, 12.70 at `0x12130`, and every module from 1.00 to 12.70 checked in [#5916](https://github.com/boykopovar/AnyPS5/pull/5916#issuecomment-6172832590)): an index at or past the rack's voice count returns it through the same error helper that answers `0x804A8010` for a null output, and the output handle is written only on success. That export also checks the rack handle before the output pointer and returns `0x804A8202` for a bad one, while this library checks the output first and returns `0x804A0261`, which does not appear in the module; that order and code are unchanged here. Heretic + Hexen (PPSA26525), DOOM + DOOM II (PPSA21444) and STAR WARS: Dark Forces Remaster (PPSA19728) create a one-voice reverb rack, take voice 0 and ask for voices 1 to 3 at start-up, log the failures and go on From df91160af8dbd978320641a8ce3c58bf58db8dc9 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Wed, 14 Oct 2026 16:36:48 +0000 Subject: [PATCH 0100/1000] feat(agc): draw a performance overlay over presented frames ANYPS5_OVERLAY=1 shows frames per second over the last second, the last frame time, a graph of the last 120 frame times with their minimum, average and maximum, and with APS5_PROFILE_GPU and APS5_PROFILE_DRAW the GPU and driver time per frame, in the corner ANYPS5_OVERLAY_POS names. A 5x7 bitmap font and solid rectangles are drawn as instanced quads by one render pass after the frame reaches the swapchain image, on the presentation pass and the blit paths alike, in SDR and at 203 nits on an HDR10 swapchain. The text scales with the swapchain height. While hidden, the presentation records nothing for it. --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 60 ++ .../Execution/include/PerformanceOverlay.hpp | 166 +++++ .../Execution/include/Presentation.hpp | 1 + .../src/Driver/Packets/PacketExecution.cpp | 2 + .../Execution/src/PerformanceOverlay.cpp | 584 ++++++++++++++++++ .../Execution/src/VulkanDevice.cpp | 12 +- .../Graphics/shaders/PerformanceOverlay.frag | 12 + .../Graphics/shaders/PerformanceOverlay.vert | 16 + .../libSceAgcDriver/Graphics/src/Recorder.cpp | 2 + .../tests/PerformanceOverlay.cpp | 518 ++++++++++++++++ .../tests/PerformanceOverlayStatistics.cpp | 238 +++++++ 11 files changed, 1609 insertions(+), 2 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/Execution/include/PerformanceOverlay.hpp create mode 100644 core/libs/prx/libSceAgcDriver/Execution/src/PerformanceOverlay.cpp create mode 100644 core/libs/prx/libSceAgcDriver/Graphics/shaders/PerformanceOverlay.frag create mode 100644 core/libs/prx/libSceAgcDriver/Graphics/shaders/PerformanceOverlay.vert create mode 100644 core/libs/prx/libSceAgcDriver/tests/PerformanceOverlay.cpp create mode 100644 core/libs/prx/libSceAgcDriver/tests/PerformanceOverlayStatistics.cpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index dca6910043..b798358b13 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -125,6 +125,23 @@ foreach(agcPresentationStage IN ITEMS Vert Frag) ) list(APPEND agcPresentationShaderHeaders ${agcPresentationShaderHeader}) endforeach() +foreach(agcOverlayStage IN ITEMS Vert Frag) + string(TOLOWER "${agcOverlayStage}" agcOverlayExtension) + string(TOUPPER "${agcOverlayStage}" agcOverlaySymbol) + set(agcOverlayShaderSource ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/shaders/PerformanceOverlay.${agcOverlayExtension}) + set(agcOverlayShaderSpv ${agcTextureDetileGeneratedDir}/PerformanceOverlay${agcOverlayStage}.spv) + set(agcOverlayShaderHeader ${agcTextureDetileGeneratedDir}/PerformanceOverlay${agcOverlayStage}_spv.h) + add_custom_command( + OUTPUT "${agcOverlayShaderHeader}" + COMMAND ${CMAKE_COMMAND} -E make_directory "${agcTextureDetileGeneratedDir}" + COMMAND "$" -V --target-env vulkan1.1 -o "${agcOverlayShaderSpv}" "${agcOverlayShaderSource}" + COMMAND ${CMAKE_COMMAND} -DINPUT=${agcOverlayShaderSpv} -DOUTPUT=${agcOverlayShaderHeader} + -DSYMBOL=PERFORMANCE_OVERLAY_${agcOverlaySymbol}_SPV -P "${CMAKE_CURRENT_SOURCE_DIR}/embed_spirv.cmake" + DEPENDS "${agcOverlayShaderSource}" glslang-standalone + VERBATIM + ) + list(APPEND agcPresentationShaderHeaders ${agcOverlayShaderHeader}) +endforeach() set(agcDriverSources ${CMAKE_CURRENT_SOURCE_DIR}/Unimplemented.cpp @@ -194,6 +211,7 @@ set(agcDriverSources ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/AspectFit.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/PresentationScaler.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/PresentationPass.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/PerformanceOverlay.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/Draw.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/DepthSurface.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/MultisampleTarget.cpp @@ -1183,6 +1201,48 @@ if(BUILD_TESTING) endif() endif() +add_test_executable(agc_performance_overlay_statistics_tests EXCLUDE_FROM_ALL + tests/PerformanceOverlayStatistics.cpp +) +target_include_directories(agc_performance_overlay_statistics_tests PRIVATE + ${LIBS_INCLUDE_DIR} + ${CMAKE_SOURCE_DIR}/core/shader/recompiler + ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR} +) +target_link_libraries(agc_performance_overlay_statistics_tests PRIVATE + libSceAgcDriver + libc +) +set_target_properties(agc_performance_overlay_statistics_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_performance_overlay_statistics_tests) + +add_test_executable(agc_performance_overlay_tests EXCLUDE_FROM_ALL + tests/PerformanceOverlay.cpp +) +target_include_directories(agc_performance_overlay_tests PRIVATE + ${LIBS_INCLUDE_DIR} + ${CMAKE_SOURCE_DIR}/core/shader/recompiler + ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR} +) +target_link_libraries(agc_performance_overlay_tests PRIVATE + libSceAgcDriver + SDL2-static + libc +) +set_target_properties(agc_performance_overlay_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_performance_overlay_tests) + +if(BUILD_TESTING) + set_target_properties(agc_performance_overlay_statistics_tests agc_performance_overlay_tests PROPERTIES EXCLUDE_FROM_ALL OFF) + add_test(NAME agc_performance_overlay_statistics COMMAND agc_performance_overlay_statistics_tests) + add_test(NAME agc_performance_overlay COMMAND agc_performance_overlay_tests) + set_tests_properties(agc_performance_overlay PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30) + if(WIN32) + set_tests_properties(agc_performance_overlay_statistics agc_performance_overlay PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() + add_test_executable(agc_driver_pm4_tests EXCLUDE_FROM_ALL tests/Pm4.cpp ) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/PerformanceOverlay.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/PerformanceOverlay.hpp new file mode 100644 index 0000000000..f99d27af9f --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Execution/include/PerformanceOverlay.hpp @@ -0,0 +1,166 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_PERFORMANCEOVERLAY_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_PERFORMANCEOVERLAY_HPP + +#define VK_NO_PROTOTYPES +#include +#include "prx/libSceAgcDriver/Graphics/include/Context.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace AgcDriver { + +namespace Graphics { +class Buffer; +} + +enum class OverlayCorner : std::uint8_t { TopLeft, TopRight, BottomLeft, BottomRight }; + +struct OverlaySettings { + bool visible = false; + OverlayCorner corner = OverlayCorner::TopLeft; +}; + +OverlaySettings ParseOverlaySettings(const char* enabled, const char* position); + +class PerformanceCounters { +public: + static void AddGpuNanoseconds(std::uint64_t nanoseconds); + static void AddDriverNanoseconds(std::uint64_t nanoseconds); + static std::uint64_t GpuNanoseconds(); + static std::uint64_t DriverNanoseconds(); + static bool GpuEnabled(); + static bool DriverEnabled(); +}; + +class OverlayStatistics { +public: + using Clock = std::chrono::steady_clock; + static constexpr std::size_t FrameHistory = 120; + static constexpr Clock::duration Window = std::chrono::seconds(1); + + struct Range { + double minimum = 0; + double average = 0; + double maximum = 0; + }; + + void AddPresent(Clock::time_point at, std::uint64_t gpuNanoseconds, std::uint64_t driverNanoseconds); + void Reset(); + double Fps() const; + double LastFrameMs() const; + std::vector FrameTimes() const; + Range FrameTimeRange() const; + double GpuMsPerFrame() const; + double DriverMsPerFrame() const; + +private: + struct Sample { + Clock::time_point at; + std::uint64_t gpu = 0; + std::uint64_t driver = 0; + }; + + double perFrameMs(std::uint64_t Sample::*counter) const; + + std::deque window; + std::array frameTimes{}; + std::size_t frameCount = 0; + std::size_t frameNext = 0; +}; + +struct OverlayRect { + std::int32_t x = 0; + std::int32_t y = 0; + std::int32_t width = 0; + std::int32_t height = 0; + std::uint32_t color = 0; +}; + +enum class OverlayColor : std::uint32_t { Background = 0, Text = 1, Graph = 2 }; +inline constexpr std::size_t OverlayColorCount = 3; + +struct OverlayContent { + std::vector above; + std::vector frameTimes; + std::vector below; +}; + +struct OverlayLayout { + OverlayRect bounds; + std::vector rects; + std::int32_t scale = 1; +}; + +struct OverlayMetrics { + static constexpr std::int32_t GlyphWidth = 5; + static constexpr std::int32_t GlyphHeight = 7; + static constexpr std::int32_t Advance = 6; + static constexpr std::int32_t LinePitch = 9; + static constexpr std::int32_t Columns = 27; + static constexpr std::int32_t Padding = 3; + static constexpr std::int32_t Margin = 4; + static constexpr std::int32_t GraphHeight = 20; + static constexpr std::int32_t GraphGap = 2; + static constexpr std::int32_t InnerWidth = Columns * Advance - 1; +}; + +const std::array& OverlayGlyph(char character); +std::string FormatOverlayValue(double value); +OverlayContent ComposeOverlay(const OverlayStatistics& statistics, bool gpu, bool driver); +std::int32_t OverlayScale(VkExtent2D extent, const OverlayContent& content); +OverlayLayout LayOutOverlay(const OverlayContent& content, VkExtent2D extent, OverlayCorner corner); +std::array, OverlayColorCount> OverlayPalette(VkFormat format, VkColorSpaceKHR colorSpace); + +class PerformanceOverlay { +public: + PerformanceOverlay(const Graphics::Context& context, std::size_t slots); + ~PerformanceOverlay(); + PerformanceOverlay(const PerformanceOverlay&) = delete; + PerformanceOverlay& operator=(const PerformanceOverlay&) = delete; + + static bool Visible(); + static void Toggle(); + static OverlayCorner Corner(); + + void NotePresent(OverlayStatistics::Clock::time_point at); + void NotePresent(OverlayStatistics::Clock::time_point at, std::uint64_t gpuNanoseconds, std::uint64_t driverNanoseconds); + const OverlayStatistics& Statistics() const { return statistics; } + OverlayRect Record(VkCommandBuffer commands, std::size_t slot, VkImage destination, VkFormat format, VkColorSpaceKHR colorSpace, VkExtent2D extent, VkImageLayout initialLayout, VkImageLayout finalLayout); + OverlayRect Record(VkCommandBuffer commands, std::size_t slot, VkImage destination, VkFormat format, VkColorSpaceKHR colorSpace, VkExtent2D extent, VkImageLayout initialLayout, VkImageLayout finalLayout, const OverlayContent& content, OverlayCorner corner); + void Release(std::size_t slot) noexcept; + +private: + struct Slot { + std::unique_ptr rects; + std::size_t capacity = 0; + std::vector> transient; + }; + + VkRenderPass renderPass(VkFormat format, VkImageLayout initialLayout, VkImageLayout finalLayout); + VkPipeline pipeline(VkFormat format, VkRenderPass compatible); + void releaseTransient(Slot& slot) noexcept; + void release() noexcept; + + Graphics::Context context; + VkPipelineLayout pipelineLayout = VK_NULL_HANDLE; + VkShaderModule vertex = VK_NULL_HANDLE; + VkShaderModule fragment = VK_NULL_HANDLE; + std::map, VkRenderPass> renderPasses; + std::map pipelines; + std::vector slots; + OverlayStatistics statistics; + std::uint64_t toggles = 0; +}; + +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Presentation.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Presentation.hpp index 3f771fbb1a..3cdc977947 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Presentation.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Presentation.hpp @@ -29,6 +29,7 @@ extern "C" void AgcDriverAttachWindow_nid_postfix(const AgcDriver::PresentationW extern "C" void AgcDriverPresentClear_nid_postfix(const AgcDriver::PresentationWindow& window, bool opaque, void (*gpuReady)(void*), void* context); extern "C" void AgcDriverPresentBuffer_nid_postfix(const AgcDriver::PresentationWindow& window, const AgcDriver::DisplayBuffer& buffer, void (*gpuReady)(void*), void* context); extern "C" void AgcDriverReleaseWindow_nid_postfix(void* window); +extern "C" void AgcDriverTogglePerformanceOverlay_nid_postfix(); extern "C" void AgcDriverReportFailure_nid_postfix(std::exception_ptr error); #endif diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp index 492a2e4095..66f9d6d4b6 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp @@ -6,6 +6,7 @@ #include "prx/libSceAgcDriver/Execution/include/Driver/Synchronization/DeferredLabels.hpp" #include "prx/libSceAgcDriver/Execution/include/CaptureTrace.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/PerformanceOverlay.hpp" #include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp" #include "prx/libSceAgcDriver/Execution/include/Pm4.hpp" #include "prx/libSceAgcDriver/Eq/include/Event.hpp" @@ -23,6 +24,7 @@ void Driver::timed(double WorkerProfile::*bucket, TWork&& work) { const auto end = std::chrono::steady_clock::now(); profile.*bucket += std::chrono::duration(end - begin).count(); static const bool report = std::getenv("APS5_PROFILE_DRAW") != nullptr; + if (report && bucket != &WorkerProfile::waitMs) PerformanceCounters::AddDriverNanoseconds(static_cast(std::chrono::duration_cast(end - begin).count())); if (report && end - profile.reported > std::chrono::seconds(10)) { profile.reported = end; AgcDriver::ProfilePrint_nid_no_patch( "[gpu] worker at %.0f s: dispatch %.1f s, draw %.1f s, wait %.1f s\n", std::chrono::duration(end - profile.start).count(), profile.dispatchMs / 1000, profile.drawMs / 1000, profile.waitMs / 1000); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceOverlay.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceOverlay.cpp new file mode 100644 index 0000000000..f9218396a3 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceOverlay.cpp @@ -0,0 +1,584 @@ +#include "prx/libSceAgcDriver/Execution/include/PerformanceOverlay.hpp" +#include "prx/libSceAgcDriver/Execution/include/Presentation.hpp" +#include "prx/libSceAgcDriver/Execution/include/PqToSdr.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" +#include "prx/libSceAgcDriver/Graphics/shaders/PerformanceOverlayFrag_spv.h" +#include "prx/libSceAgcDriver/Graphics/shaders/PerformanceOverlayVert_spv.h" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace AgcDriver { +namespace { + +static_assert(sizeof(OverlayRect) == 20, "overlay rectangles are read as one ivec4 and one uint per instance"); + +struct OverlayParameters { + std::array, OverlayColorCount> palette; + std::array extent; +}; + +std::atomic toggleCount{0}; +std::atomic gpuTotal{0}; +std::atomic driverTotal{0}; + +const OverlaySettings& settings() { + static const OverlaySettings parsed = ParseOverlaySettings(std::getenv("ANYPS5_OVERLAY"), std::getenv("ANYPS5_OVERLAY_POS")); + return parsed; +} + +std::int32_t panelWidth(std::int32_t scale) { + return (OverlayMetrics::InnerWidth + 2 * OverlayMetrics::Padding) * scale; +} + +std::int32_t panelHeight(const OverlayContent& content, std::int32_t scale) { + const auto lines = static_cast(content.above.size() + content.below.size()); + const auto glyphGap = OverlayMetrics::LinePitch - OverlayMetrics::GlyphHeight; + return (2 * OverlayMetrics::Padding + lines * OverlayMetrics::LinePitch + OverlayMetrics::GraphHeight + OverlayMetrics::GraphGap - glyphGap) * scale; +} + +bool fits(VkExtent2D extent, const OverlayContent& content, std::int32_t scale) { + const auto margins = 2 * OverlayMetrics::Margin * scale; + return panelWidth(scale) + margins <= static_cast(extent.width) && panelHeight(content, scale) + margins <= static_cast(extent.height); +} + +float srgbDecode(float value) { + return value <= 0.04045f ? value / 12.92f : std::pow((value + 0.055f) / 1.055f, 2.4f); +} + +float pqEncode(float nits) { + constexpr double m1 = 2610.0 / 16384.0; + constexpr double m2 = 2523.0 / 4096.0 * 128.0; + constexpr double c1 = 3424.0 / 4096.0; + constexpr double c2 = 2413.0 / 4096.0 * 32.0; + constexpr double c3 = 2392.0 / 4096.0 * 32.0; + const double power = std::pow(std::clamp(static_cast(nits) / 10000.0, 0.0, 1.0), m1); + return static_cast(std::pow((c1 + c2 * power) / (1.0 + c3 * power), m2)); +} + +bool srgbFormat(VkFormat format) { + return format == VK_FORMAT_B8G8R8A8_SRGB || format == VK_FORMAT_R8G8B8A8_SRGB || format == VK_FORMAT_A8B8G8R8_SRGB_PACK32; +} + +} + +OverlaySettings ParseOverlaySettings(const char* enabled, const char* position) { + OverlaySettings parsed; + const std::string_view visible = enabled != nullptr ? enabled : ""; + if (visible == "1") parsed.visible = true; + else if (!visible.empty() && visible != "0") throw std::runtime_error("ANYPS5_OVERLAY must be 1 or 0, not '" + std::string(visible) + "'"); + const std::string_view corner = position != nullptr ? position : ""; + if (corner.empty() || corner == "tl") parsed.corner = OverlayCorner::TopLeft; + else if (corner == "tr") parsed.corner = OverlayCorner::TopRight; + else if (corner == "bl") parsed.corner = OverlayCorner::BottomLeft; + else if (corner == "br") parsed.corner = OverlayCorner::BottomRight; + else throw std::runtime_error("ANYPS5_OVERLAY_POS must be tl, tr, bl or br, not '" + std::string(corner) + "'"); + return parsed; +} + +void PerformanceCounters::AddGpuNanoseconds(std::uint64_t nanoseconds) { + gpuTotal.fetch_add(nanoseconds, std::memory_order_relaxed); +} + +void PerformanceCounters::AddDriverNanoseconds(std::uint64_t nanoseconds) { + driverTotal.fetch_add(nanoseconds, std::memory_order_relaxed); +} + +std::uint64_t PerformanceCounters::GpuNanoseconds() { + return gpuTotal.load(std::memory_order_relaxed); +} + +std::uint64_t PerformanceCounters::DriverNanoseconds() { + return driverTotal.load(std::memory_order_relaxed); +} + +bool PerformanceCounters::GpuEnabled() { + return Graphics::Recorder::GpuTimingEnabled(); +} + +bool PerformanceCounters::DriverEnabled() { + static const bool enabled = std::getenv("APS5_PROFILE_DRAW") != nullptr; + return enabled; +} + +void OverlayStatistics::AddPresent(Clock::time_point at, std::uint64_t gpuNanoseconds, std::uint64_t driverNanoseconds) { + if (!window.empty()) { + Graphics::Require(at >= window.back().at, "presents reached the performance overlay out of order"); + frameTimes[frameNext] = std::chrono::duration(at - window.back().at).count(); + frameNext = (frameNext + 1) % FrameHistory; + frameCount = std::min(frameCount + 1, FrameHistory); + } + window.push_back({at, gpuNanoseconds, driverNanoseconds}); + while (at - window.front().at > Window) window.pop_front(); +} + +void OverlayStatistics::Reset() { + window.clear(); + frameTimes = {}; + frameCount = 0; + frameNext = 0; +} + +double OverlayStatistics::Fps() const { + if (window.size() < 2) return 0; + const auto seconds = std::chrono::duration(window.back().at - window.front().at).count(); + return seconds > 0 ? static_cast(window.size() - 1) / seconds : 0; +} + +double OverlayStatistics::LastFrameMs() const { + return frameCount == 0 ? 0 : frameTimes[(frameNext + FrameHistory - 1) % FrameHistory]; +} + +std::vector OverlayStatistics::FrameTimes() const { + std::vector ordered; + ordered.reserve(frameCount); + for (std::size_t i = 0; i < frameCount; ++i) ordered.push_back(frameTimes[(frameNext + FrameHistory - frameCount + i) % FrameHistory]); + return ordered; +} + +OverlayStatistics::Range OverlayStatistics::FrameTimeRange() const { + const auto times = FrameTimes(); + if (times.empty()) return {}; + Range range{times.front(), 0, times.front()}; + double sum = 0; + for (const auto time : times) { + range.minimum = std::min(range.minimum, time); + range.maximum = std::max(range.maximum, time); + sum += time; + } + range.average = sum / static_cast(times.size()); + return range; +} + +double OverlayStatistics::perFrameMs(std::uint64_t Sample::*counter) const { + if (window.size() < 2) return 0; + const auto first = window.front().*counter; + const auto last = window.back().*counter; + return last >= first ? static_cast(last - first) / 1e6 / static_cast(window.size() - 1) : 0; +} + +double OverlayStatistics::GpuMsPerFrame() const { + return perFrameMs(&Sample::gpu); +} + +double OverlayStatistics::DriverMsPerFrame() const { + return perFrameMs(&Sample::driver); +} + +const std::array& OverlayGlyph(char character) { + using Glyph = std::array; + static constexpr Glyph space{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + static constexpr std::array digits{{ + {0x0e, 0x11, 0x13, 0x15, 0x19, 0x11, 0x0e}, + {0x04, 0x0c, 0x04, 0x04, 0x04, 0x04, 0x0e}, + {0x0e, 0x11, 0x01, 0x02, 0x04, 0x08, 0x1f}, + {0x1f, 0x02, 0x04, 0x02, 0x01, 0x11, 0x0e}, + {0x02, 0x06, 0x0a, 0x12, 0x1f, 0x02, 0x02}, + {0x1f, 0x10, 0x1e, 0x01, 0x01, 0x11, 0x0e}, + {0x06, 0x08, 0x10, 0x1e, 0x11, 0x11, 0x0e}, + {0x1f, 0x01, 0x02, 0x04, 0x08, 0x08, 0x08}, + {0x0e, 0x11, 0x11, 0x0e, 0x11, 0x11, 0x0e}, + {0x0e, 0x11, 0x11, 0x0f, 0x01, 0x02, 0x0c}, + }}; + static constexpr Glyph period{0x00, 0x00, 0x00, 0x00, 0x00, 0x0c, 0x0c}; + static constexpr Glyph colon{0x00, 0x0c, 0x0c, 0x00, 0x0c, 0x0c, 0x00}; + static constexpr Glyph slash{0x00, 0x01, 0x02, 0x04, 0x08, 0x10, 0x00}; + static constexpr Glyph a{0x0e, 0x11, 0x11, 0x11, 0x1f, 0x11, 0x11}; + static constexpr Glyph d{0x1c, 0x12, 0x11, 0x11, 0x11, 0x12, 0x1c}; + static constexpr Glyph e{0x1f, 0x10, 0x10, 0x1e, 0x10, 0x10, 0x1f}; + static constexpr Glyph f{0x1f, 0x10, 0x10, 0x1e, 0x10, 0x10, 0x10}; + static constexpr Glyph g{0x0e, 0x11, 0x10, 0x17, 0x11, 0x11, 0x0f}; + static constexpr Glyph i{0x0e, 0x04, 0x04, 0x04, 0x04, 0x04, 0x0e}; + static constexpr Glyph m{0x11, 0x1b, 0x15, 0x15, 0x11, 0x11, 0x11}; + static constexpr Glyph n{0x11, 0x11, 0x19, 0x15, 0x13, 0x11, 0x11}; + static constexpr Glyph p{0x1e, 0x11, 0x11, 0x1e, 0x10, 0x10, 0x10}; + static constexpr Glyph r{0x1e, 0x11, 0x11, 0x1e, 0x14, 0x12, 0x11}; + static constexpr Glyph s{0x0f, 0x10, 0x10, 0x0e, 0x01, 0x01, 0x1e}; + static constexpr Glyph u{0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x0e}; + static constexpr Glyph v{0x11, 0x11, 0x11, 0x11, 0x11, 0x0a, 0x04}; + static constexpr Glyph x{0x11, 0x11, 0x0a, 0x04, 0x0a, 0x11, 0x11}; + if (character >= '0' && character <= '9') return digits[static_cast(character - '0')]; + switch (character) { + case ' ': return space; + case '.': return period; + case ':': return colon; + case '/': return slash; + case 'A': return a; + case 'D': return d; + case 'E': return e; + case 'F': return f; + case 'G': return g; + case 'I': return i; + case 'M': return m; + case 'N': return n; + case 'P': return p; + case 'R': return r; + case 'S': return s; + case 'U': return u; + case 'V': return v; + case 'X': return x; + default: throw std::runtime_error(std::string("the performance overlay font has no glyph for '") + character + "'"); + } +} + +std::string FormatOverlayValue(double value) { + const double clamped = std::isfinite(value) ? std::clamp(value, 0.0, 9999.0) : 0.0; + char text[16]; + std::snprintf(text, sizeof(text), clamped < 99.95 ? "%.1f" : "%.0f", clamped); + return text; +} + +OverlayContent ComposeOverlay(const OverlayStatistics& statistics, bool gpu, bool driver) { + OverlayContent content; + content.above.push_back("FPS: " + FormatOverlayValue(statistics.Fps())); + content.above.push_back("FRAME: " + FormatOverlayValue(statistics.LastFrameMs()) + " MS"); + content.frameTimes = statistics.FrameTimes(); + const auto range = statistics.FrameTimeRange(); + content.below.push_back("MIN/AVG/MAX: " + FormatOverlayValue(range.minimum) + "/" + FormatOverlayValue(range.average) + "/" + FormatOverlayValue(range.maximum)); + if (gpu) content.below.push_back("GPU: " + FormatOverlayValue(statistics.GpuMsPerFrame()) + " MS"); + if (driver) content.below.push_back("DRIVER: " + FormatOverlayValue(statistics.DriverMsPerFrame()) + " MS"); + return content; +} + +std::int32_t OverlayScale(VkExtent2D extent, const OverlayContent& content) { + auto scale = std::max(1, static_cast((extent.height + 240) / 480)); + while (scale > 1 && !fits(extent, content, scale)) --scale; + return scale; +} + +OverlayLayout LayOutOverlay(const OverlayContent& content, VkExtent2D extent, OverlayCorner corner) { + Graphics::Require(extent.width != 0 && extent.height != 0, "the performance overlay needs a destination"); + Graphics::Require(content.frameTimes.size() <= OverlayStatistics::FrameHistory, "the performance overlay graph holds at most 120 frames"); + OverlayLayout layout; + const auto scale = OverlayScale(extent, content); + layout.scale = scale; + const auto width = panelWidth(scale); + const auto height = panelHeight(content, scale); + const auto margin = OverlayMetrics::Margin * scale; + const bool right = corner == OverlayCorner::TopRight || corner == OverlayCorner::BottomRight; + const bool bottom = corner == OverlayCorner::BottomLeft || corner == OverlayCorner::BottomRight; + const auto left = std::max(0, right ? static_cast(extent.width) - margin - width : margin); + const auto top = std::max(0, bottom ? static_cast(extent.height) - margin - height : margin); + layout.bounds = {left, top, width, height, static_cast(OverlayColor::Background)}; + layout.rects.push_back(layout.bounds); + const auto text = [&](const std::string& line, std::int32_t row) { + Graphics::Require(line.size() <= static_cast(OverlayMetrics::Columns), "a performance overlay line is wider than the panel"); + for (std::size_t column = 0; column < line.size(); ++column) { + const auto& glyph = OverlayGlyph(line[column]); + const auto glyphLeft = left + (OverlayMetrics::Padding + static_cast(column) * OverlayMetrics::Advance) * scale; + for (std::int32_t y = 0; y < OverlayMetrics::GlyphHeight; ++y) { + const auto bits = glyph[static_cast(y)]; + std::int32_t x = 0; + while (x < OverlayMetrics::GlyphWidth) { + if ((bits & (0x10u >> x)) == 0) { + ++x; + continue; + } + auto end = x; + while (end < OverlayMetrics::GlyphWidth && (bits & (0x10u >> end)) != 0) ++end; + layout.rects.push_back({glyphLeft + x * scale, top + (row + y) * scale, (end - x) * scale, scale, static_cast(OverlayColor::Text)}); + x = end; + } + } + } + }; + auto row = OverlayMetrics::Padding; + for (const auto& line : content.above) { + text(line, row); + row += OverlayMetrics::LinePitch; + } + const auto graphBottom = top + (row + OverlayMetrics::GraphHeight) * scale; + if (!content.frameTimes.empty()) { + double maximum = 0; + double sum = 0; + for (const auto time : content.frameTimes) { + maximum = std::max(maximum, time); + sum += time; + } + const double fullScale = std::max(maximum, 2.0 * sum / static_cast(content.frameTimes.size())); + const auto first = static_cast(OverlayStatistics::FrameHistory - content.frameTimes.size()); + for (std::size_t i = 0; i < content.frameTimes.size() && fullScale > 0; ++i) { + const auto pixels = std::clamp(static_cast(std::lround(content.frameTimes[i] / fullScale * OverlayMetrics::GraphHeight * scale)), 1, OverlayMetrics::GraphHeight * scale); + layout.rects.push_back({left + (OverlayMetrics::Padding + first + static_cast(i)) * scale, graphBottom - pixels, scale, pixels, static_cast(OverlayColor::Graph)}); + } + } + row += OverlayMetrics::GraphHeight + OverlayMetrics::GraphGap; + for (const auto& line : content.below) { + text(line, row); + row += OverlayMetrics::LinePitch; + } + return layout; +} + +std::array, OverlayColorCount> OverlayPalette(VkFormat format, VkColorSpaceKHR colorSpace) { + std::array, OverlayColorCount> palette{{{0.0f, 0.0f, 0.0f, 0.6f}, {1.0f, 1.0f, 1.0f, 1.0f}, {0.35f, 0.8f, 0.35f, 1.0f}}}; + if (colorSpace == VK_COLOR_SPACE_HDR10_ST2084_EXT) { + for (auto& color : palette) { + const float red = srgbDecode(color[0]); + const float green = srgbDecode(color[1]); + const float blue = srgbDecode(color[2]); + const std::array bt2020{ + 0.6274f * red + 0.3293f * green + 0.0433f * blue, + 0.0691f * red + 0.9195f * green + 0.0114f * blue, + 0.0164f * red + 0.0880f * green + 0.8956f * blue}; + for (std::size_t channel = 0; channel < 3; ++channel) color[channel] = pqEncode(bt2020[channel] * PqReferenceWhiteNits); + } + } else if (srgbFormat(format)) { + for (auto& color : palette) { + for (std::size_t channel = 0; channel < 3; ++channel) color[channel] = srgbDecode(color[channel]); + } + } + return palette; +} + +PerformanceOverlay::PerformanceOverlay(const Graphics::Context& context, std::size_t slotCount) : context(context), slots(slotCount) { + Graphics::Require(slotCount != 0, "the performance overlay needs at least one slot"); + try { + const VkPushConstantRange push{VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof(OverlayParameters)}; + VkPipelineLayoutCreateInfo layoutInfo{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO}; + layoutInfo.pushConstantRangeCount = 1; + layoutInfo.pPushConstantRanges = &push; + Graphics::Check(context.Function("vkCreatePipelineLayout")(context.device, &layoutInfo, nullptr, &pipelineLayout), "vkCreatePipelineLayout performance overlay"); + const auto createModule = context.Function("vkCreateShaderModule"); + VkShaderModuleCreateInfo moduleInfo{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; + moduleInfo.codeSize = sizeof(PERFORMANCE_OVERLAY_VERT_SPV); + moduleInfo.pCode = PERFORMANCE_OVERLAY_VERT_SPV; + Graphics::Check(createModule(context.device, &moduleInfo, nullptr, &vertex), "vkCreateShaderModule performance overlay vertex"); + moduleInfo.codeSize = sizeof(PERFORMANCE_OVERLAY_FRAG_SPV); + moduleInfo.pCode = PERFORMANCE_OVERLAY_FRAG_SPV; + Graphics::Check(createModule(context.device, &moduleInfo, nullptr, &fragment), "vkCreateShaderModule performance overlay fragment"); + } catch (...) { + release(); + throw; + } +} + +PerformanceOverlay::~PerformanceOverlay() { + release(); +} + +void PerformanceOverlay::releaseTransient(Slot& slot) noexcept { + for (const auto& [view, framebuffer] : slot.transient) { + if (framebuffer) context.Function("vkDestroyFramebuffer")(context.device, framebuffer, nullptr); + if (view) context.Function("vkDestroyImageView")(context.device, view, nullptr); + } + slot.transient.clear(); +} + +void PerformanceOverlay::Release(std::size_t index) noexcept { + if (index < slots.size()) releaseTransient(slots[index]); +} + +void PerformanceOverlay::release() noexcept { + for (auto& slot : slots) { + releaseTransient(slot); + slot.rects.reset(); + slot.capacity = 0; + } + for (const auto& [format, entry] : pipelines) context.Function("vkDestroyPipeline")(context.device, entry, nullptr); + pipelines.clear(); + for (const auto& [key, entry] : renderPasses) context.Function("vkDestroyRenderPass")(context.device, entry, nullptr); + renderPasses.clear(); + if (fragment) context.Function("vkDestroyShaderModule")(context.device, fragment, nullptr); + if (vertex) context.Function("vkDestroyShaderModule")(context.device, vertex, nullptr); + if (pipelineLayout) context.Function("vkDestroyPipelineLayout")(context.device, pipelineLayout, nullptr); + fragment = vertex = VK_NULL_HANDLE; + pipelineLayout = VK_NULL_HANDLE; +} + +bool PerformanceOverlay::Visible() { + return settings().visible != ((toggleCount.load(std::memory_order_relaxed) & 1u) != 0); +} + +void PerformanceOverlay::Toggle() { + toggleCount.fetch_add(1, std::memory_order_relaxed); +} + +OverlayCorner PerformanceOverlay::Corner() { + return settings().corner; +} + +void PerformanceOverlay::NotePresent(OverlayStatistics::Clock::time_point at) { + NotePresent(at, PerformanceCounters::GpuEnabled() ? PerformanceCounters::GpuNanoseconds() : 0, PerformanceCounters::DriverEnabled() ? PerformanceCounters::DriverNanoseconds() : 0); +} + +void PerformanceOverlay::NotePresent(OverlayStatistics::Clock::time_point at, std::uint64_t gpuNanoseconds, std::uint64_t driverNanoseconds) { + const auto current = toggleCount.load(std::memory_order_relaxed); + if (current != toggles) { + statistics.Reset(); + toggles = current; + } + statistics.AddPresent(at, gpuNanoseconds, driverNanoseconds); +} + +VkRenderPass PerformanceOverlay::renderPass(VkFormat format, VkImageLayout initialLayout, VkImageLayout finalLayout) { + const auto key = std::tuple{format, initialLayout, finalLayout}; + if (const auto found = renderPasses.find(key); found != renderPasses.end()) return found->second; + VkFormatProperties properties{}; + context.formatProperties(context.physical, format, &properties); + Graphics::Require((properties.optimalTilingFeatures & VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BLEND_BIT) != 0, "the performance overlay cannot blend into this format"); + VkAttachmentDescription attachment{}; + attachment.format = format; + attachment.samples = VK_SAMPLE_COUNT_1_BIT; + attachment.loadOp = VK_ATTACHMENT_LOAD_OP_LOAD; + attachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE; + attachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; + attachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; + attachment.initialLayout = initialLayout; + attachment.finalLayout = finalLayout; + const VkAttachmentReference reference{0, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL}; + VkSubpassDescription subpass{}; + subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; + subpass.colorAttachmentCount = 1; + subpass.pColorAttachments = &reference; + const bool transfer = finalLayout == VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; + const VkSubpassDependency dependencies[2] = { + {VK_SUBPASS_EXTERNAL, 0, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, 0}, + {0, VK_SUBPASS_EXTERNAL, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, transfer ? VK_PIPELINE_STAGE_TRANSFER_BIT : VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, transfer ? VK_ACCESS_TRANSFER_READ_BIT : VkAccessFlags{0}, 0}, + }; + VkRenderPassCreateInfo info{VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO}; + info.attachmentCount = 1; + info.pAttachments = &attachment; + info.subpassCount = 1; + info.pSubpasses = &subpass; + info.dependencyCount = 2; + info.pDependencies = dependencies; + VkRenderPass created = VK_NULL_HANDLE; + Graphics::Check(context.Function("vkCreateRenderPass")(context.device, &info, nullptr, &created), "vkCreateRenderPass performance overlay"); + return renderPasses.emplace(key, created).first->second; +} + +VkPipeline PerformanceOverlay::pipeline(VkFormat format, VkRenderPass compatible) { + if (const auto found = pipelines.find(format); found != pipelines.end()) return found->second; + VkPipelineShaderStageCreateInfo stages[2]{{VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO}, {VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO}}; + stages[0].stage = VK_SHADER_STAGE_VERTEX_BIT; + stages[0].module = vertex; + stages[0].pName = "main"; + stages[1].stage = VK_SHADER_STAGE_FRAGMENT_BIT; + stages[1].module = fragment; + stages[1].pName = "main"; + const VkVertexInputBindingDescription binding{0, sizeof(OverlayRect), VK_VERTEX_INPUT_RATE_INSTANCE}; + const VkVertexInputAttributeDescription attributes[2] = {{0, 0, VK_FORMAT_R32G32B32A32_SINT, 0}, {1, 0, VK_FORMAT_R32_UINT, 16}}; + VkPipelineVertexInputStateCreateInfo vertexInput{VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO}; + vertexInput.vertexBindingDescriptionCount = 1; + vertexInput.pVertexBindingDescriptions = &binding; + vertexInput.vertexAttributeDescriptionCount = 2; + vertexInput.pVertexAttributeDescriptions = attributes; + VkPipelineInputAssemblyStateCreateInfo assembly{VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO}; + assembly.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP; + VkPipelineViewportStateCreateInfo viewport{VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO}; + viewport.viewportCount = 1; + viewport.scissorCount = 1; + VkPipelineRasterizationStateCreateInfo rasterization{VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO}; + rasterization.polygonMode = VK_POLYGON_MODE_FILL; + rasterization.cullMode = VK_CULL_MODE_NONE; + rasterization.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + rasterization.lineWidth = 1.0f; + VkPipelineMultisampleStateCreateInfo multisample{VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO}; + multisample.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT; + VkPipelineColorBlendAttachmentState blendAttachment{}; + blendAttachment.blendEnable = VK_TRUE; + blendAttachment.srcColorBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA; + blendAttachment.dstColorBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA; + blendAttachment.colorBlendOp = VK_BLEND_OP_ADD; + blendAttachment.srcAlphaBlendFactor = VK_BLEND_FACTOR_ZERO; + blendAttachment.dstAlphaBlendFactor = VK_BLEND_FACTOR_ONE; + blendAttachment.alphaBlendOp = VK_BLEND_OP_ADD; + blendAttachment.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT; + VkPipelineColorBlendStateCreateInfo blend{VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO}; + blend.attachmentCount = 1; + blend.pAttachments = &blendAttachment; + const VkDynamicState dynamicStates[2] = {VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR}; + VkPipelineDynamicStateCreateInfo dynamic{VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO}; + dynamic.dynamicStateCount = 2; + dynamic.pDynamicStates = dynamicStates; + VkGraphicsPipelineCreateInfo info{VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO}; + info.stageCount = 2; + info.pStages = stages; + info.pVertexInputState = &vertexInput; + info.pInputAssemblyState = &assembly; + info.pViewportState = &viewport; + info.pRasterizationState = &rasterization; + info.pMultisampleState = &multisample; + info.pColorBlendState = &blend; + info.pDynamicState = &dynamic; + info.layout = pipelineLayout; + info.renderPass = compatible; + VkPipeline created = VK_NULL_HANDLE; + Graphics::Check(context.Function("vkCreateGraphicsPipelines")(context.device, VK_NULL_HANDLE, 1, &info, nullptr, &created), "vkCreateGraphicsPipelines performance overlay"); + return pipelines.emplace(format, created).first->second; +} + +OverlayRect PerformanceOverlay::Record(VkCommandBuffer commands, std::size_t slot, VkImage destination, VkFormat format, VkColorSpaceKHR colorSpace, VkExtent2D extent, VkImageLayout initialLayout, VkImageLayout finalLayout) { + return Record(commands, slot, destination, format, colorSpace, extent, initialLayout, finalLayout, ComposeOverlay(statistics, PerformanceCounters::GpuEnabled(), PerformanceCounters::DriverEnabled()), Corner()); +} + +OverlayRect PerformanceOverlay::Record(VkCommandBuffer commands, std::size_t index, VkImage destination, VkFormat format, VkColorSpaceKHR colorSpace, VkExtent2D extent, VkImageLayout initialLayout, VkImageLayout finalLayout, const OverlayContent& content, OverlayCorner corner) { + Graphics::Require(index < slots.size(), "performance overlay slot is out of range"); + Graphics::Require(destination != VK_NULL_HANDLE && extent.width != 0 && extent.height != 0, "the performance overlay destination is unavailable"); + auto& slot = slots[index]; + releaseTransient(slot); + const auto layout = LayOutOverlay(content, extent, corner); + if (slot.capacity < layout.rects.size()) { + const auto capacity = std::max(layout.rects.size(), 1024); + slot.rects = std::make_unique(context, capacity * sizeof(OverlayRect), VK_BUFFER_USAGE_VERTEX_BUFFER_BIT); + slot.capacity = capacity; + } + std::memcpy(slot.rects->Bytes().data(), layout.rects.data(), layout.rects.size() * sizeof(OverlayRect)); + const auto pass = renderPass(format, initialLayout, finalLayout); + const auto drawing = pipeline(format, pass); + VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; + viewInfo.image = destination; + viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D; + viewInfo.format = format; + viewInfo.components = {VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY}; + viewInfo.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + VkImageViewUsageCreateInfo usageInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_USAGE_CREATE_INFO}; + usageInfo.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT; + viewInfo.pNext = &usageInfo; + VkImageView view = VK_NULL_HANDLE; + Graphics::Check(context.Function("vkCreateImageView")(context.device, &viewInfo, nullptr, &view), "vkCreateImageView performance overlay"); + VkFramebufferCreateInfo framebufferInfo{VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO}; + framebufferInfo.renderPass = pass; + framebufferInfo.attachmentCount = 1; + framebufferInfo.pAttachments = &view; + framebufferInfo.width = extent.width; + framebufferInfo.height = extent.height; + framebufferInfo.layers = 1; + VkFramebuffer framebuffer = VK_NULL_HANDLE; + const auto created = context.Function("vkCreateFramebuffer")(context.device, &framebufferInfo, nullptr, &framebuffer); + slot.transient.emplace_back(view, framebuffer); + Graphics::Check(created, "vkCreateFramebuffer performance overlay"); + const auto right = std::min(layout.bounds.x + layout.bounds.width, static_cast(extent.width)); + const auto bottom = std::min(layout.bounds.y + layout.bounds.height, static_cast(extent.height)); + const VkRect2D area{{layout.bounds.x, layout.bounds.y}, {static_cast(std::max(1, right - layout.bounds.x)), static_cast(std::max(1, bottom - layout.bounds.y))}}; + VkRenderPassBeginInfo begin{VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO}; + begin.renderPass = pass; + begin.framebuffer = framebuffer; + begin.renderArea = area; + context.Function("vkCmdBeginRenderPass")(commands, &begin, VK_SUBPASS_CONTENTS_INLINE); + context.Function("vkCmdBindPipeline")(commands, VK_PIPELINE_BIND_POINT_GRAPHICS, drawing); + const VkViewport viewport{0.0f, 0.0f, static_cast(extent.width), static_cast(extent.height), 0.0f, 1.0f}; + context.Function("vkCmdSetViewport")(commands, 0, 1, &viewport); + context.Function("vkCmdSetScissor")(commands, 0, 1, &area); + const OverlayParameters parameters{OverlayPalette(format, colorSpace), {static_cast(extent.width), static_cast(extent.height)}}; + context.Function("vkCmdPushConstants")(commands, pipelineLayout, VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof(parameters), ¶meters); + const VkBuffer buffer = slot.rects->Handle(); + const VkDeviceSize offset = 0; + context.Function("vkCmdBindVertexBuffers")(commands, 0, 1, &buffer, &offset); + context.Function("vkCmdDraw")(commands, 4, static_cast(layout.rects.size()), 0, 0); + context.Function("vkCmdEndRenderPass")(commands); + return layout.bounds; +} + +} + +extern "C" void AgcDriverTogglePerformanceOverlay_nid_postfix() { + AgcDriver::PerformanceOverlay::Toggle(); +} diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index c8830d353d..bc614c2e0a 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -15,6 +15,7 @@ #include "prx/libSceAgcDriver/Execution/include/SubgroupClock.hpp" #include "prx/libSceAgcDriver/Execution/include/ShaderDeviceProfile.hpp" #include "prx/libSceAgcDriver/Execution/include/PresentMode.hpp" +#include "prx/libSceAgcDriver/Execution/include/PerformanceOverlay.hpp" #include "prx/libSceAgcDriver/Execution/include/PresentationPass.hpp" #include "prx/libSceAgcDriver/Execution/include/PresentationScaler.hpp" #include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" @@ -372,6 +373,7 @@ struct VulkanDevice::State { VkPhysicalDeviceMeshShaderPropertiesEXT meshLimits{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MESH_SHADER_PROPERTIES_EXT}; std::unique_ptr scaler; std::unique_ptr presentation; + std::unique_ptr overlay; VkFormat surfaceFormat = VK_FORMAT_B8G8R8A8_UNORM; VkColorSpaceKHR surfaceColorSpace = VK_COLOR_SPACE_SRGB_NONLINEAR_KHR; bool swapchainColorSpaces = false; @@ -723,6 +725,7 @@ struct VulkanDevice::State { textureCache.reset(); detiler.reset(); colorTransfer.reset(); + overlay.reset(); presentation.reset(); scaler.reset(); hdrScaler.reset(); @@ -2944,6 +2947,7 @@ bool VulkanDevice::present(std::uint32_t width, std::uint32_t height, bool opaqu if (state->presentSlots[state->presentCursor].inFlight) RetirePresents(state->presentSlots.size() - 1); auto& slot = state->presentSlots[state->presentCursor]; require(!slot.inFlight, "presentation slot is still in flight"); + const bool overlay = PerformanceOverlay::Visible(); if (display != nullptr && resident == nullptr) { static_cast(DisplayBufferSize(*display)); state->colorTransfer->Upload(display->address, width, height, Graphics::ColorTileMode::RenderTarget, DisplayTexelBytes(display->pixelFormat)); @@ -3035,7 +3039,7 @@ bool VulkanDevice::present(std::uint32_t width, std::uint32_t height, bool opaqu } if (drawn) { state->presentation->Bind(slotIndex, resident->Image(), width, height, display->pixelFormat, whiteNits, residentLayer); - state->presentation->RecordPresent(commands, slotIndex, barrier.image, state->surfaceFormat, state->surfaceColorSpace, state->extent, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR); + state->presentation->RecordPresent(commands, slotIndex, barrier.image, state->surfaceFormat, state->surfaceColorSpace, state->extent, overlay ? VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL : VK_IMAGE_LAYOUT_PRESENT_SRC_KHR); } else { scaler->RecordLetterbox(commands, width, height, barrier.image, state->extent.width, state->extent.height); if (direct) PresentationScaler::RecordBlitFrom(graphicsContext(), commands, resident->Image(), VK_IMAGE_LAYOUT_GENERAL, width, height, residentFilter, barrier.image, state->extent.width, state->extent.height, residentLayer); @@ -3073,7 +3077,11 @@ bool VulkanDevice::present(std::uint32_t width, std::uint32_t height, bool opaqu Graphics::Recorder::CountBarriers(CommandClass::PresentBlit); } } - if (!drawn) { + if (overlay) { + if (!state->overlay) state->overlay = std::make_unique(graphicsContext(), state->presentSlots.size()); + state->overlay->NotePresent(std::chrono::steady_clock::now()); + state->overlay->Record(commands, static_cast(&slot - state->presentSlots.data()), barrier.image, state->surfaceFormat, state->surfaceColorSpace, state->extent, drawn ? VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL : VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR); + } else if (!drawn) { barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; barrier.dstAccessMask = 0; barrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/shaders/PerformanceOverlay.frag b/core/libs/prx/libSceAgcDriver/Graphics/shaders/PerformanceOverlay.frag new file mode 100644 index 0000000000..33f1f9740d --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Graphics/shaders/PerformanceOverlay.frag @@ -0,0 +1,12 @@ +#version 450 + +layout(push_constant) uniform Parameters { + vec4 palette[3]; + vec2 extent; +} parameters; +layout(location = 0) flat in uint paletteIndex; +layout(location = 0) out vec4 color; + +void main() { + color = parameters.palette[paletteIndex]; +} diff --git a/core/libs/prx/libSceAgcDriver/Graphics/shaders/PerformanceOverlay.vert b/core/libs/prx/libSceAgcDriver/Graphics/shaders/PerformanceOverlay.vert new file mode 100644 index 0000000000..b59069433c --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Graphics/shaders/PerformanceOverlay.vert @@ -0,0 +1,16 @@ +#version 450 + +layout(location = 0) in ivec4 area; +layout(location = 1) in uint color; +layout(push_constant) uniform Parameters { + vec4 palette[3]; + vec2 extent; +} parameters; +layout(location = 0) flat out uint paletteIndex; + +void main() { + vec2 corner = vec2(float(gl_VertexIndex & 1), float((gl_VertexIndex >> 1) & 1)); + vec2 pixel = vec2(area.xy) + corner * vec2(area.zw); + gl_Position = vec4(pixel / parameters.extent * 2.0 - 1.0, 0.0, 1.0); + paletteIndex = color; +} diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp index e329a0d449..6c4f039b89 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp @@ -9,6 +9,7 @@ #include "prx/libSceAgcDriver/Graphics/shaders/SampleCounter_spv.h" #include "prx/libSceAgcDriver/Graphics/shaders/SampleDumps_spv.h" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/PerformanceOverlay.hpp" #include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp" #include "prx/libSceAgcDriver/Execution/include/Pm4Opcodes.hpp" #include "prx/libc/include/CpuTopology.hpp" @@ -2390,6 +2391,7 @@ void Recorder::readGpuTiming(Batch& batch) { batch.gpuEndNs = static_cast(stamps[i * 2 + 1]) * period; PerformanceTrace::GpuRange("batch", batch.gpuStartNs, batch.gpuEndNs, batch.traceId); timingBatchMs += ns / 1e6; + AgcDriver::PerformanceCounters::AddGpuNanoseconds(static_cast(ns)); continue; } const auto key = batch.timedKeys[i]; diff --git a/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlay.cpp b/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlay.cpp new file mode 100644 index 0000000000..b3e573052b --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlay.cpp @@ -0,0 +1,518 @@ +#include "prx/libSceAgcDriver/Execution/include/PerformanceOverlay.hpp" +#include "prx/libSceAgcDriver/Execution/include/PresentationPass.hpp" +#include "prx/libSceAgcDriver/Execution/include/PresentationScaler.hpp" +#include "prx/libSceAgcDriver/Execution/include/ToneMapLuminance.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using namespace AgcDriver::Graphics; +using AgcDriver::OverlayContent; +using AgcDriver::OverlayCorner; +using AgcDriver::OverlayMetrics; +using AgcDriver::OverlayRect; +using AgcDriver::PerformanceOverlay; +using AgcDriver::PresentationPass; + +constexpr int VulkanTestSkipped = 77; +constexpr std::uint64_t Bgra8 = 0x8000000000000000ull; + +class Device { +public: + Device() { +#ifdef _WIN32 + library = SDL_LoadObject("vulkan-1.dll"); +#else + library = SDL_LoadObject("libvulkan.so.1"); +#endif + Require(library != nullptr, "cannot load Vulkan"); + try { + instanceProc = reinterpret_cast(SDL_LoadFunction(library, "vkGetInstanceProcAddr")); + Require(instanceProc != nullptr, "missing Vulkan instance resolver"); + VkApplicationInfo application{VK_STRUCTURE_TYPE_APPLICATION_INFO}; + application.apiVersion = VK_API_VERSION_1_1; + VkInstanceCreateInfo info{VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO}; + info.pApplicationInfo = &application; + Check(function("vkCreateInstance")(&info, nullptr, &instance), "vkCreateInstance"); + std::uint32_t count = 0; + const auto enumerate = function("vkEnumeratePhysicalDevices"); + Check(enumerate(instance, &count, nullptr), "vkEnumeratePhysicalDevices"); + Require(count != 0, "no Vulkan device"); + std::vector devices(count); + Check(enumerate(instance, &count, devices.data()), "vkEnumeratePhysicalDevices"); + context.physical = devices.front(); + const auto queues = function("vkGetPhysicalDeviceQueueFamilyProperties"); + queues(context.physical, &count, nullptr); + std::vector families(count); + queues(context.physical, &count, families.data()); + std::uint32_t family = 0; + while (family < count && (families[family].queueFlags & VK_QUEUE_GRAPHICS_BIT) == 0) ++family; + Require(family < count, "no Vulkan graphics queue"); + timestampBits = families[family].timestampValidBits; + const float priority = 1; + VkDeviceQueueCreateInfo queue{VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO}; + queue.queueFamilyIndex = family; + queue.queueCount = 1; + queue.pQueuePriorities = &priority; + VkDeviceCreateInfo device{VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO}; + device.queueCreateInfoCount = 1; + device.pQueueCreateInfos = &queue; + Check(function("vkCreateDevice")(context.physical, &device, nullptr, &context.device), "vkCreateDevice"); + context.deviceProc = function("vkGetDeviceProcAddr"); + function("vkGetPhysicalDeviceMemoryProperties")(context.physical, &context.memory); + VkPhysicalDeviceProperties properties{}; + function("vkGetPhysicalDeviceProperties")(context.physical, &properties); + context.limits = properties.limits; + name = properties.deviceName; + context.formatProperties = function("vkGetPhysicalDeviceFormatProperties"); + context.imageFormatProperties = function("vkGetPhysicalDeviceImageFormatProperties"); + context.Function("vkGetDeviceQueue")(context.device, family, 0, &context.queue); + VkCommandPoolCreateInfo pool{VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO}; + pool.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT; + pool.queueFamilyIndex = family; + Check(context.Function("vkCreateCommandPool")(context.device, &pool, nullptr, &context.pool), "vkCreateCommandPool"); + } catch (...) { + release(); + throw; + } + } + + ~Device() { release(); } + const Context& GetContext() const { return context; } + const std::string& Name() const { return name; } + std::uint32_t TimestampBits() const { return timestampBits; } + +private: + template + TFunction function(const char* functionName) const { + const auto result = reinterpret_cast(instanceProc(instance, functionName)); + Require(result != nullptr, functionName); + return result; + } + + void release() noexcept { + if (context.pool != VK_NULL_HANDLE) context.Function("vkDestroyCommandPool")(context.device, context.pool, nullptr); + context.bufferPool.reset(); + if (context.device != VK_NULL_HANDLE) function("vkDestroyDevice")(context.device, nullptr); + if (instance != VK_NULL_HANDLE) function("vkDestroyInstance")(instance, nullptr); + if (library != nullptr) SDL_UnloadObject(library); + } + + void* library = nullptr; + PFN_vkGetInstanceProcAddr instanceProc = nullptr; + VkInstance instance = VK_NULL_HANDLE; + Context context{}; + std::string name; + std::uint32_t timestampBits = 0; +}; + +class Image { +public: + Image(const Context& context, VkFormat format, VkExtent2D extent, VkImageUsageFlags usage, VkImageCreateFlags flags = 0) : context(context), extent(extent) { + VkImageCreateInfo info{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO}; + info.flags = flags; + info.imageType = VK_IMAGE_TYPE_2D; + info.format = format; + info.extent = {extent.width, extent.height, 1}; + info.mipLevels = 1; + info.arrayLayers = 1; + info.samples = VK_SAMPLE_COUNT_1_BIT; + info.tiling = VK_IMAGE_TILING_OPTIMAL; + info.usage = usage; + info.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + Check(context.Function("vkCreateImage")(context.device, &info, nullptr, &image), "vkCreateImage performance overlay test"); + VkMemoryRequirements requirements{}; + context.Function("vkGetImageMemoryRequirements")(context.device, image, &requirements); + VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; + allocation.allocationSize = requirements.size; + allocation.memoryTypeIndex = context.MemoryType(requirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); + const auto allocated = context.Function("vkAllocateMemory")(context.device, &allocation, nullptr, &memory); + if (allocated != VK_SUCCESS) context.Function("vkDestroyImage")(context.device, image, nullptr); + Check(allocated, "vkAllocateMemory performance overlay test"); + Check(context.Function("vkBindImageMemory")(context.device, image, memory, 0), "vkBindImageMemory performance overlay test"); + } + ~Image() { + context.Function("vkDestroyImage")(context.device, image, nullptr); + context.Function("vkFreeMemory")(context.device, memory, nullptr); + } + Image(const Image&) = delete; + Image& operator=(const Image&) = delete; + VkImage Handle() const { return image; } + VkExtent2D Extent() const { return extent; } + +private: + const Context& context; + VkExtent2D extent; + VkImage image = VK_NULL_HANDLE; + VkDeviceMemory memory = VK_NULL_HANDLE; +}; + +void transition(const Context& context, VkCommandBuffer commands, VkImage image, VkImageLayout from, VkImageLayout to, VkPipelineStageFlags sourceStage, VkPipelineStageFlags destinationStage, VkAccessFlags sourceAccess, VkAccessFlags destinationAccess) { + VkImageMemoryBarrier barrier{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; + barrier.srcAccessMask = sourceAccess; + barrier.dstAccessMask = destinationAccess; + barrier.oldLayout = from; + barrier.newLayout = to; + barrier.srcQueueFamilyIndex = barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.image = image; + barrier.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + context.Function("vkCmdPipelineBarrier")(commands, sourceStage, destinationStage, 0, 0, nullptr, 0, nullptr, 1, &barrier); +} + +void readImage(const Context& context, VkCommandBuffer commands, const Image& image, Buffer& destination) { + VkBufferImageCopy copy{}; + copy.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; + copy.imageExtent = {image.Extent().width, image.Extent().height, 1}; + context.Function("vkCmdCopyImageToBuffer")(commands, image.Handle(), VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, destination.Handle(), 1, ©); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_HOST_READ_BIT); +} + +std::vector wordsOf(Buffer& buffer, VkExtent2D extent) { + std::vector words(static_cast(extent.width) * extent.height); + std::memcpy(words.data(), buffer.Bytes().data(), words.size() * sizeof(std::uint32_t)); + return words; +} + +void setEnvironment(const char* name, const char* value) { +#ifdef _WIN32 + _putenv_s(name, value); +#else + setenv(name, value, 1); +#endif +} + +bool inside(const OverlayRect& rect, std::int32_t x, std::int32_t y) { + return x >= rect.x && x < rect.x + rect.width && y >= rect.y && y < rect.y + rect.height; +} + +OverlayContent sixtyFps() { + AgcDriver::OverlayStatistics statistics; + const auto start = AgcDriver::OverlayStatistics::Clock::time_point{} + std::chrono::hours(1); + for (int i = 0; i <= 60; ++i) statistics.AddPresent(start + std::chrono::nanoseconds(16'666'667) * i, 0, 0); + auto content = AgcDriver::ComposeOverlay(statistics, false, false); + Require(content.above.front() == "FPS: 60.0", "60 presents a second compose as '" + content.above.front() + "'"); + return content; +} + +std::vector clearAndOverlay(const Context& context, PerformanceOverlay* overlay, VkFormat format, VkColorSpaceKHR colorSpace, VkExtent2D extent, const VkClearColorValue& clear, const OverlayContent& content, OverlayCorner corner, OverlayRect* bounds) { + Image destination(context, format, extent, VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT); + Buffer readback(context, static_cast(extent.width) * extent.height * 4, VK_BUFFER_USAGE_TRANSFER_DST_BIT); + { + CommandBatch batch(context); + const auto commands = batch.Handle(); + transition(context, commands, destination.Handle(), VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, VK_ACCESS_TRANSFER_WRITE_BIT); + const VkImageSubresourceRange range{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + context.Function("vkCmdClearColorImage")(commands, destination.Handle(), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &clear, 1, &range); + if (overlay != nullptr) { + *bounds = overlay->Record(commands, 0, destination.Handle(), format, colorSpace, extent, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, content, corner); + } else { + transition(context, commands, destination.Handle(), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT); + } + readImage(context, commands, destination, readback); + batch.SubmitAndWait(); + } + if (overlay != nullptr) overlay->Release(0); + return wordsOf(readback, extent); +} + +void onlyInside(const std::vector& reference, const std::vector& drawn, VkExtent2D extent, const OverlayRect& bounds, const std::string& what, bool everyPixel = true) { + std::size_t changed = 0; + for (std::int32_t y = 0; y < static_cast(extent.height); ++y) { + for (std::int32_t x = 0; x < static_cast(extent.width); ++x) { + const auto index = static_cast(y) * extent.width + static_cast(x); + if (!inside(bounds, x, y)) { + Require(drawn[index] == reference[index], what + ": the overlay changed pixel " + std::to_string(x) + "," + std::to_string(y) + " outside its panel"); + continue; + } + if (drawn[index] != reference[index]) ++changed; + else Require(!everyPixel, what + ": the overlay left pixel " + std::to_string(x) + "," + std::to_string(y) + " of its panel unchanged"); + } + } + Require(changed != 0, what + ": the overlay changed nothing"); +} + +int channel(std::uint32_t word, unsigned shift) { + return static_cast((word >> shift) & 0xffu); +} + +void sdrOverlayTest(const Context& context, PerformanceOverlay& overlay) { + const VkExtent2D extent{1280, 720}; + VkClearColorValue clear{}; + clear.float32[0] = 0.75f; + clear.float32[1] = 0.5f; + clear.float32[2] = 0.25f; + clear.float32[3] = 1.0f; + const auto content = sixtyFps(); + OverlayRect bounds{}; + const auto reference = clearAndOverlay(context, nullptr, VK_FORMAT_B8G8R8A8_UNORM, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR, extent, clear, content, OverlayCorner::TopLeft, nullptr); + const auto drawn = clearAndOverlay(context, &overlay, VK_FORMAT_B8G8R8A8_UNORM, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR, extent, clear, content, OverlayCorner::TopLeft, &bounds); + Require(bounds.x == AgcDriver::LayOutOverlay(content, extent, OverlayCorner::TopLeft).bounds.x && bounds.width > 0 && bounds.height > 0, "the overlay drew another panel than its layout"); + onlyInside(reference, drawn, extent, bounds, "SDR 1280x720"); + const auto scale = AgcDriver::OverlayScale(extent, content); + const auto clearWord = reference.front(); + const std::string digits = "60.0"; + const auto firstColumn = content.above.front().find(digits); + Require(firstColumn != std::string::npos, "the FPS line has no '60.0'"); + for (std::size_t i = 0; i < digits.size(); ++i) { + const auto& glyph = AgcDriver::OverlayGlyph(digits[i]); + const auto column = static_cast(firstColumn + i); + for (std::int32_t gy = 0; gy < OverlayMetrics::GlyphHeight; ++gy) { + for (std::int32_t gx = 0; gx < OverlayMetrics::Advance; ++gx) { + const bool set = gx < OverlayMetrics::GlyphWidth && (glyph[static_cast(gy)] & (0x10u >> gx)) != 0; + for (std::int32_t py = 0; py < scale; ++py) { + for (std::int32_t px = 0; px < scale; ++px) { + const auto x = bounds.x + (OverlayMetrics::Padding + column * OverlayMetrics::Advance + gx) * scale + px; + const auto y = bounds.y + (OverlayMetrics::Padding + gy) * scale + py; + const auto word = drawn[static_cast(y) * extent.width + static_cast(x)]; + const auto where = std::string("'") + digits[i] + "' glyph pixel " + std::to_string(gx) + "," + std::to_string(gy); + Require(channel(word, 24) == channel(clearWord, 24), where + " changed the alpha"); + if (set) { + Require((word & 0x00ffffffu) == 0x00ffffffu, where + " is not white text: 0x" + std::to_string(word)); + continue; + } + for (const unsigned shift : {0u, 8u, 16u}) { + const int expected = static_cast(std::lround(channel(clearWord, shift) * 0.4)); + Require(std::abs(channel(word, shift) - expected) <= 1, where + " is not the panel over the frame: " + std::to_string(channel(word, shift)) + " for " + std::to_string(expected)); + } + } + } + } + } + } + std::printf("SDR overlay: panel %dx%d at %d,%d (scale %d), FPS digits drawn bit for bit\n", bounds.width, bounds.height, bounds.x, bounds.y, scale); +} + +void hdrOverlayTest(const Context& context, PerformanceOverlay& overlay) { + const auto format = VK_FORMAT_A2B10G10R10_UNORM_PACK32; + VkFormatProperties properties{}; + context.formatProperties(context.physical, format, &properties); + if ((properties.optimalTilingFeatures & VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BLEND_BIT) == 0) { + std::printf("A2B10G10R10 cannot be blended into here; HDR10 overlay not exercised\n"); + return; + } + const VkExtent2D extent{1920, 1080}; + VkClearColorValue clear{}; + clear.float32[0] = 0.4f; + clear.float32[1] = 0.5f; + clear.float32[2] = 0.6f; + clear.float32[3] = 1.0f; + const auto content = sixtyFps(); + OverlayRect bounds{}; + const auto reference = clearAndOverlay(context, nullptr, format, VK_COLOR_SPACE_HDR10_ST2084_EXT, extent, clear, content, OverlayCorner::BottomRight, nullptr); + const auto drawn = clearAndOverlay(context, &overlay, format, VK_COLOR_SPACE_HDR10_ST2084_EXT, extent, clear, content, OverlayCorner::BottomRight, &bounds); + onlyInside(reference, drawn, extent, bounds, "HDR10 1920x1080"); + const auto scale = AgcDriver::OverlayScale(extent, content); + const auto& six = AgcDriver::OverlayGlyph('6'); + const auto column = static_cast(content.above.front().find('6')); + std::size_t checked = 0; + for (std::int32_t gy = 0; gy < OverlayMetrics::GlyphHeight; ++gy) { + for (std::int32_t gx = 0; gx < OverlayMetrics::GlyphWidth; ++gx) { + if ((six[static_cast(gy)] & (0x10u >> gx)) == 0) continue; + const auto x = bounds.x + (OverlayMetrics::Padding + column * OverlayMetrics::Advance + gx) * scale + scale / 2; + const auto y = bounds.y + (OverlayMetrics::Padding + gy) * scale + scale / 2; + const auto word = drawn[static_cast(y) * extent.width + static_cast(x)]; + for (const unsigned shift : {0u, 10u, 20u}) { + const int code = static_cast((word >> shift) & 0x3ffu); + Require(std::abs(code - 594) <= 1, "HDR10 text is not at 203 nits: PQ code " + std::to_string(code) + " for 594"); + } + Require((word >> 30u) == (reference.front() >> 30u), "HDR10 text changed the alpha"); + ++checked; + } + } + Require(checked != 0, "no HDR10 text pixel was checked"); + std::printf("HDR10 overlay: text at PQ code 594 (203 nits), nothing changed outside the %dx%d panel\n", bounds.width, bounds.height); +} + +void cornerTest(const Context& context, PerformanceOverlay& overlay) { + const VkExtent2D extent{640, 360}; + VkClearColorValue clear{}; + clear.float32[1] = 1.0f; + clear.float32[3] = 1.0f; + const auto content = sixtyFps(); + const auto reference = clearAndOverlay(context, nullptr, VK_FORMAT_B8G8R8A8_UNORM, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR, extent, clear, content, OverlayCorner::TopLeft, nullptr); + for (const auto corner : {OverlayCorner::TopLeft, OverlayCorner::TopRight, OverlayCorner::BottomLeft, OverlayCorner::BottomRight}) { + OverlayRect bounds{}; + const auto drawn = clearAndOverlay(context, &overlay, VK_FORMAT_B8G8R8A8_UNORM, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR, extent, clear, content, corner, &bounds); + const auto expected = AgcDriver::LayOutOverlay(content, extent, corner).bounds; + Require(bounds.x == expected.x && bounds.y == expected.y && bounds.width == expected.width && bounds.height == expected.height, "the drawn panel is not where the layout puts it"); + onlyInside(reference, drawn, extent, bounds, "corner " + std::to_string(static_cast(corner))); + } + std::printf("overlay corners: tl, tr, bl and br drawn where the layout puts them\n"); +} + +class Presenter { +public: + explicit Presenter(const Context& context) : context(context), pass(context, 1), overlay(context, 1), source(context, VK_FORMAT_B8G8R8A8_UNORM, {256, 144}, VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT, VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT | VK_IMAGE_CREATE_EXTENDED_USAGE_BIT) { + Buffer upload(context, static_cast(256) * 144 * 4, VK_BUFFER_USAGE_TRANSFER_SRC_BIT); + std::uint32_t seed = 0x2545f491u; + for (auto& byte : upload.Bytes()) { + seed = seed * 1664525u + 1013904223u; + byte = static_cast(seed >> 24u); + } + CommandBatch batch(context); + const auto commands = batch.Handle(); + transition(context, commands, source.Handle(), VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, VK_ACCESS_TRANSFER_WRITE_BIT); + VkBufferImageCopy copy{}; + copy.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; + copy.imageExtent = {256, 144, 1}; + context.Function("vkCmdCopyBufferToImage")(commands, upload.Handle(), source.Handle(), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ©); + transition(context, commands, source.Handle(), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_GENERAL, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT); + batch.SubmitAndWait(); + } + + std::vector Drawn(OverlayRect* bounds) { + Image destination(context, VK_FORMAT_B8G8R8A8_UNORM, Extent, VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT); + Buffer readback(context, static_cast(Extent.width) * Extent.height * 4, VK_BUFFER_USAGE_TRANSFER_DST_BIT); + const bool visible = PerformanceOverlay::Visible(); + pass.Bind(0, source.Handle(), 256, 144, Bgra8, AgcDriver::SdrToneMapLuminanceNits); + { + CommandBatch batch(context); + const auto commands = batch.Handle(); + pass.RecordPresent(commands, 0, destination.Handle(), VK_FORMAT_B8G8R8A8_UNORM, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR, Extent, visible ? VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL : VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL); + if (visible) *bounds = overlay.Record(commands, 0, destination.Handle(), VK_FORMAT_B8G8R8A8_UNORM, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR, Extent, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, sixtyFps(), OverlayCorner::TopLeft); + readImage(context, commands, destination, readback); + batch.SubmitAndWait(); + } + pass.Release(0); + overlay.Release(0); + return wordsOf(readback, Extent); + } + + std::vector Blitted(OverlayRect* bounds) { + Image destination(context, VK_FORMAT_B8G8R8A8_UNORM, Extent, VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT); + Buffer readback(context, static_cast(Extent.width) * Extent.height * 4, VK_BUFFER_USAGE_TRANSFER_DST_BIT); + const bool visible = PerformanceOverlay::Visible(); + { + CommandBatch batch(context); + const auto commands = batch.Handle(); + transition(context, commands, destination.Handle(), VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, VK_ACCESS_TRANSFER_WRITE_BIT); + transition(context, commands, source.Handle(), VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_GENERAL, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_SHADER_READ_BIT, VK_ACCESS_TRANSFER_READ_BIT); + VkClearColorValue black{}; + black.float32[3] = 1.0f; + const VkImageSubresourceRange range{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + context.Function("vkCmdClearColorImage")(commands, destination.Handle(), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &black, 1, &range); + transition(context, commands, destination.Handle(), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_WRITE_BIT); + AgcDriver::PresentationScaler::RecordBlitFrom(context, commands, source.Handle(), VK_IMAGE_LAYOUT_GENERAL, 256, 144, VK_FILTER_LINEAR, destination.Handle(), Extent.width, Extent.height); + if (visible) { + *bounds = overlay.Record(commands, 0, destination.Handle(), VK_FORMAT_B8G8R8A8_UNORM, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR, Extent, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, sixtyFps(), OverlayCorner::TopLeft); + } else { + transition(context, commands, destination.Handle(), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT); + } + transition(context, commands, source.Handle(), VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_GENERAL, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, VK_ACCESS_TRANSFER_READ_BIT, VK_ACCESS_SHADER_READ_BIT); + readImage(context, commands, destination, readback); + batch.SubmitAndWait(); + } + overlay.Release(0); + return wordsOf(readback, Extent); + } + + static constexpr VkExtent2D Extent{400, 300}; + +private: + const Context& context; + PresentationPass pass; + PerformanceOverlay overlay; + Image source; +}; + +void disabledIsUnchangedTest(const Context& context) { + Require(!PerformanceOverlay::Visible(), "the performance overlay is shown without ANYPS5_OVERLAY=1"); + Presenter presenter(context); + OverlayRect unused{}; + const auto drawnBefore = presenter.Drawn(&unused); + const auto blittedBefore = presenter.Blitted(&unused); + PerformanceOverlay::Toggle(); + Require(PerformanceOverlay::Visible(), "F10 does not show the performance overlay"); + OverlayRect bounds{}; + onlyInside(drawnBefore, presenter.Drawn(&bounds), Presenter::Extent, bounds, "presentation pass with the overlay", false); + onlyInside(blittedBefore, presenter.Blitted(&bounds), Presenter::Extent, bounds, "blit with the overlay", false); + PerformanceOverlay::Toggle(); + Require(!PerformanceOverlay::Visible(), "F10 does not hide the performance overlay again"); + Require(presenter.Drawn(&unused) == drawnBefore, "the presentation pass output differs after the overlay was hidden"); + Require(presenter.Blitted(&unused) == blittedBefore, "the blit output differs after the overlay was hidden"); + std::printf("overlay hidden: presentation pass and blit output bit-identical before and after showing it\n"); +} + +void costReport(const Device& device, PerformanceOverlay& overlay) { + const auto& context = device.GetContext(); + if (device.TimestampBits() == 0) { + std::printf("the graphics queue has no timestamps; overlay cost not measured\n"); + return; + } + VkQueryPoolCreateInfo info{VK_STRUCTURE_TYPE_QUERY_POOL_CREATE_INFO}; + info.queryType = VK_QUERY_TYPE_TIMESTAMP; + info.queryCount = 2; + VkQueryPool queries = VK_NULL_HANDLE; + Check(context.Function("vkCreateQueryPool")(context.device, &info, nullptr, &queries), "vkCreateQueryPool overlay cost"); + for (const VkExtent2D extent : {VkExtent2D{1920, 1080}, VkExtent2D{3840, 2160}}) { + Image destination(context, VK_FORMAT_B8G8R8A8_UNORM, extent, VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT); + auto content = sixtyFps(); + content.below.push_back("GPU: 4.2 MS"); + content.below.push_back("DRIVER: 1.3 MS"); + std::vector gpuMs; + std::vector cpuMs; + for (int run = 0; run < 12; ++run) { + CommandBatch batch(context); + const auto commands = batch.Handle(); + context.Function("vkCmdResetQueryPool")(commands, queries, 0, 2); + transition(context, commands, destination.Handle(), VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, VK_ACCESS_TRANSFER_WRITE_BIT); + context.Function("vkCmdWriteTimestamp")(commands, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, queries, 0); + const auto recordStart = std::chrono::steady_clock::now(); + static_cast(overlay.Record(commands, 0, destination.Handle(), VK_FORMAT_B8G8R8A8_UNORM, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR, extent, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, content, OverlayCorner::TopLeft)); + cpuMs.push_back(std::chrono::duration(std::chrono::steady_clock::now() - recordStart).count()); + context.Function("vkCmdWriteTimestamp")(commands, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, queries, 1); + batch.SubmitAndWait(); + overlay.Release(0); + std::array stamps{}; + Check(context.Function("vkGetQueryPoolResults")(context.device, queries, 0, 2, sizeof(stamps), stamps.data(), sizeof(std::uint64_t), VK_QUERY_RESULT_64_BIT | VK_QUERY_RESULT_WAIT_BIT), "vkGetQueryPoolResults overlay cost"); + if (run >= 2) gpuMs.push_back(static_cast(stamps[1] - stamps[0]) * context.limits.timestampPeriod / 1e6); + } + std::sort(gpuMs.begin(), gpuMs.end()); + std::sort(cpuMs.begin(), cpuMs.end()); + std::printf("overlay cost on %s at %ux%u: GPU median %.3f ms (min %.3f, max %.3f), recording median %.3f ms\n", device.Name().c_str(), extent.width, extent.height, gpuMs[gpuMs.size() / 2], gpuMs.front(), gpuMs.back(), cpuMs[cpuMs.size() / 2]); + } + context.Function("vkDestroyQueryPool")(context.device, queries, nullptr); +} + +} + +int main() { + try { + setEnvironment("ANYPS5_OVERLAY", ""); + setEnvironment("ANYPS5_OVERLAY_POS", ""); + std::unique_ptr device; + try { + device = std::make_unique(); + } catch (const std::exception& error) { + if (std::getenv("ANYPS5_REQUIRE_VULKAN") != nullptr) throw; + std::printf("skipped, no usable Vulkan device: %s\n", error.what()); + return VulkanTestSkipped; + } + const auto& context = device->GetContext(); + { + PerformanceOverlay overlay(context, 1); + sdrOverlayTest(context, overlay); + hdrOverlayTest(context, overlay); + cornerTest(context, overlay); + costReport(*device, overlay); + } + disabledIsUnchangedTest(context); + std::cout << "performance overlay tests passed\n"; + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlayStatistics.cpp b/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlayStatistics.cpp new file mode 100644 index 0000000000..86ecedbb5b --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlayStatistics.cpp @@ -0,0 +1,238 @@ +#include "prx/libSceAgcDriver/Execution/include/PerformanceOverlay.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::OverlayContent; +using AgcDriver::OverlayCorner; +using AgcDriver::OverlayMetrics; +using AgcDriver::OverlayRect; +using AgcDriver::OverlayStatistics; +using Clock = OverlayStatistics::Clock; +using namespace std::chrono_literals; + +constexpr Clock::time_point Start = Clock::time_point{} + std::chrono::hours(1); + +void require(bool condition, const std::string& reason) { + if (!condition) throw std::runtime_error(reason); +} + +bool near(double value, double expected, double tolerance = 1e-9) { + return std::abs(value - expected) <= tolerance; +} + +void fpsWindowTests() { + OverlayStatistics statistics; + require(statistics.Fps() == 0 && statistics.LastFrameMs() == 0 && statistics.FrameTimes().empty(), "a new overlay shows statistics"); + statistics.AddPresent(Start, 0, 0); + require(statistics.Fps() == 0, "one present reads as a frame rate"); + for (int i = 1; i <= 120; ++i) statistics.AddPresent(Start + i * 10ms, 0, 0); + require(near(statistics.Fps(), 100.0), "a present every 10 ms reads as " + std::to_string(statistics.Fps()) + " FPS, not 100"); + statistics.AddPresent(Start + 1700ms, 0, 0); + require(near(statistics.Fps(), 51.0), "after a 500 ms stall the last second holds 52 presents, read as " + std::to_string(statistics.Fps()) + " FPS, not 51"); + statistics.AddPresent(Start + 3700ms, 0, 0); + require(statistics.Fps() == 0, "presents older than 1 s still count after a 2 s stall"); + statistics.AddPresent(Start + 3750ms, 0, 0); + require(near(statistics.Fps(), 20.0), "two presents 50 ms apart read as " + std::to_string(statistics.Fps()) + " FPS, not 20"); + bool refused = false; + try { + statistics.AddPresent(Start + 3740ms, 0, 0); + } catch (const std::exception&) { + refused = true; + } + require(refused, "a present earlier than the previous one was accepted"); + statistics.Reset(); + require(statistics.Fps() == 0 && statistics.LastFrameMs() == 0 && statistics.FrameTimes().empty(), "a reset kept statistics"); +} + +void frameRingTests() { + OverlayStatistics statistics; + auto at = Start; + statistics.AddPresent(at, 0, 0); + for (int i = 1; i <= 4; ++i) { + at += std::chrono::milliseconds(i); + statistics.AddPresent(at, 0, 0); + } + const auto partial = statistics.FrameTimes(); + require(partial.size() == 4 && near(partial[0], 1) && near(partial[3], 4), "the frame times of a short run are not in present order"); + for (int i = 5; i <= 150; ++i) { + at += std::chrono::milliseconds(i); + statistics.AddPresent(at, 0, 0); + } + const auto times = statistics.FrameTimes(); + require(times.size() == OverlayStatistics::FrameHistory, "the ring holds " + std::to_string(times.size()) + " frames, not 120"); + for (std::size_t i = 0; i < times.size(); ++i) require(near(times[i], 31.0 + static_cast(i)), "frame " + std::to_string(i) + " of the ring is " + std::to_string(times[i]) + " ms"); + require(near(statistics.LastFrameMs(), 150), "the last frame is not the newest"); + const auto range = statistics.FrameTimeRange(); + require(near(range.minimum, 31) && near(range.maximum, 150) && near(range.average, 90.5), "the graph range of 31..150 ms reads as " + std::to_string(range.minimum) + "/" + std::to_string(range.average) + "/" + std::to_string(range.maximum)); +} + +void counterTests() { + OverlayStatistics statistics; + constexpr std::uint64_t Earlier = 1'000'000'000u; + for (std::uint64_t i = 0; i <= 60; ++i) statistics.AddPresent(Start + i * 16ms, Earlier + i * 4'000'000u, Earlier + i * 1'500'000u); + require(near(statistics.GpuMsPerFrame(), 4.0) && near(statistics.DriverMsPerFrame(), 1.5), "4 ms of GPU and 1.5 ms of driver time per frame read as " + std::to_string(statistics.GpuMsPerFrame()) + " and " + std::to_string(statistics.DriverMsPerFrame())); + statistics.AddPresent(Start + 61 * 16ms, 5, 5); + require(statistics.GpuMsPerFrame() == 0 && statistics.DriverMsPerFrame() == 0, "a counter that went back is read as time"); +} + +void formatTests() { + const std::pair cases[] = {{59.94, "59.9"}, {0, "0.0"}, {99.94, "99.9"}, {99.96, "100"}, {144.4, "144"}, {12345, "9999"}, {-3, "0.0"}, {std::nan(""), "0.0"}}; + for (const auto& [value, expected] : cases) require(AgcDriver::FormatOverlayValue(value) == expected, std::to_string(value) + " is shown as '" + AgcDriver::FormatOverlayValue(value) + "', not '" + expected + "'"); +} + +void settingsTests() { + const auto hidden = AgcDriver::ParseOverlaySettings(nullptr, nullptr); + require(!hidden.visible && hidden.corner == OverlayCorner::TopLeft, "the overlay is not off and top-left by default"); + require(!AgcDriver::ParseOverlaySettings("", "").visible && !AgcDriver::ParseOverlaySettings("0", nullptr).visible, "an empty or 0 ANYPS5_OVERLAY shows the overlay"); + require(AgcDriver::ParseOverlaySettings("1", nullptr).visible, "ANYPS5_OVERLAY=1 does not show the overlay"); + const std::pair corners[] = {{"tl", OverlayCorner::TopLeft}, {"tr", OverlayCorner::TopRight}, {"bl", OverlayCorner::BottomLeft}, {"br", OverlayCorner::BottomRight}}; + for (const auto& [name, corner] : corners) require(AgcDriver::ParseOverlaySettings("1", name).corner == corner, std::string("ANYPS5_OVERLAY_POS=") + name + " picks another corner"); + const auto refused = [](const char* enabled, const char* position, const char* variable) { + try { + static_cast(AgcDriver::ParseOverlaySettings(enabled, position)); + } catch (const std::exception& error) { + return std::string(error.what()).find(variable) != std::string::npos; + } + return false; + }; + require(refused("yes", nullptr, "ANYPS5_OVERLAY") && refused("2", nullptr, "ANYPS5_OVERLAY"), "an ANYPS5_OVERLAY value other than 1 or 0 was accepted or not named"); + require(refused("1", "center", "ANYPS5_OVERLAY_POS") && refused(nullptr, "TL", "ANYPS5_OVERLAY_POS"), "an unknown ANYPS5_OVERLAY_POS was accepted or not named"); +} + +void composeTests() { + OverlayStatistics statistics; + for (std::uint64_t i = 0; i <= 100; ++i) statistics.AddPresent(Start + i * 10ms, i * 2'000'000u, i * 500'000u); + const auto plain = AgcDriver::ComposeOverlay(statistics, false, false); + require(plain.above.size() == 2 && plain.above[0] == "FPS: 100" && plain.above[1] == "FRAME: 10.0 MS", "the frame lines read '" + plain.above[0] + "' and '" + plain.above[1] + "'"); + require(plain.below.size() == 1 && plain.below[0] == "MIN/AVG/MAX: 10.0/10.0/10.0", "the graph line reads '" + plain.below[0] + "'"); + require(plain.frameTimes.size() == 100, "the graph does not hold every frame time"); + const auto profiled = AgcDriver::ComposeOverlay(statistics, true, true); + require(profiled.below.size() == 3 && profiled.below[1] == "GPU: 2.0 MS" && profiled.below[2] == "DRIVER: 0.5 MS", "the profiled lines read '" + profiled.below[1] + "' and '" + profiled.below[2] + "'"); + require(AgcDriver::ComposeOverlay(statistics, true, false).below.size() == 2 && AgcDriver::ComposeOverlay(statistics, false, true).below[1].rfind("DRIVER", 0) == 0, "a line is shown without its source"); +} + +std::vector coverage(const AgcDriver::OverlayLayout& layout, std::uint32_t color) { + const auto& bounds = layout.bounds; + std::vector covered(static_cast(bounds.width) * static_cast(bounds.height), 0); + for (const auto& rect : layout.rects) { + if (rect.color != color) continue; + for (std::int32_t y = rect.y; y < rect.y + rect.height; ++y) { + for (std::int32_t x = rect.x; x < rect.x + rect.width; ++x) ++covered[static_cast(y - bounds.y) * static_cast(bounds.width) + static_cast(x - bounds.x)]; + } + } + return covered; +} + +void layoutTests() { + OverlayContent content{{"FPS: 60.0", "FRAME: 16.7 MS"}, {}, {"MIN/AVG/MAX: 16.7/16.7/16.7"}}; + for (int i = 0; i < 40; ++i) content.frameTimes.push_back(i == 39 ? 50.0 : 10.0); + const std::pair scales[] = {{{1280, 720}, 2}, {{1920, 1080}, 2}, {{2560, 1440}, 3}, {{3840, 2160}, 5}, {{320, 180}, 1}, {{2560, 720}, 2}}; + for (const auto& [extent, scale] : scales) require(AgcDriver::OverlayScale(extent, content) == scale, std::to_string(extent.width) + "x" + std::to_string(extent.height) + " draws the overlay at scale " + std::to_string(AgcDriver::OverlayScale(extent, content)) + ", not " + std::to_string(scale)); + + const VkExtent2D extent{1920, 1080}; + const auto layout = AgcDriver::LayOutOverlay(content, extent, OverlayCorner::TopLeft); + const auto s = layout.scale; + const auto margin = OverlayMetrics::Margin * s; + require(layout.bounds.x == margin && layout.bounds.y == margin && layout.bounds.width == (OverlayMetrics::InnerWidth + 2 * OverlayMetrics::Padding) * s, "the top-left panel is not at the margin"); + require(layout.rects.front().x == layout.bounds.x && layout.rects.front().width == layout.bounds.width && layout.rects.front().color == static_cast(AgcDriver::OverlayColor::Background), "the background is not drawn first over the whole panel"); + for (const auto& rect : layout.rects) { + require(rect.width > 0 && rect.height > 0 && rect.x >= layout.bounds.x && rect.y >= layout.bounds.y && rect.x + rect.width <= layout.bounds.x + layout.bounds.width && rect.y + rect.height <= layout.bounds.y + layout.bounds.height, "a rectangle leaves the panel"); + } + + const auto text = coverage(layout, static_cast(AgcDriver::OverlayColor::Text)); + const auto lineTop = [&](std::size_t line) { return OverlayMetrics::Padding + static_cast(line) * OverlayMetrics::LinePitch; }; + const auto checkLine = [&](const std::string& line, std::int32_t top) { + for (std::size_t column = 0; column < line.size(); ++column) { + const auto& glyph = AgcDriver::OverlayGlyph(line[column]); + for (std::int32_t gy = 0; gy < OverlayMetrics::GlyphHeight; ++gy) { + for (std::int32_t gx = 0; gx < OverlayMetrics::Advance; ++gx) { + const bool set = gx < OverlayMetrics::GlyphWidth && (glyph[static_cast(gy)] & (0x10u >> gx)) != 0; + for (std::int32_t py = 0; py < s; ++py) { + for (std::int32_t px = 0; px < s; ++px) { + const auto x = (OverlayMetrics::Padding + static_cast(column) * OverlayMetrics::Advance + gx) * s + px; + const auto y = (top + gy) * s + py; + require(text[static_cast(y) * static_cast(layout.bounds.width) + static_cast(x)] == (set ? 1 : 0), "'" + std::string(1, line[column]) + "' of '" + line + "' is drawn wrong at glyph pixel " + std::to_string(gx) + "," + std::to_string(gy)); + } + } + } + } + } + }; + checkLine(content.above[0], lineTop(0)); + checkLine(content.above[1], lineTop(1)); + checkLine(content.below[0], lineTop(2) + OverlayMetrics::GraphHeight + OverlayMetrics::GraphGap); + + std::vector bars; + for (const auto& rect : layout.rects) { + if (rect.color == static_cast(AgcDriver::OverlayColor::Graph)) bars.push_back(rect); + } + require(bars.size() == content.frameTimes.size(), "the graph has " + std::to_string(bars.size()) + " bars for 40 frames"); + const auto graphBottom = layout.bounds.y + (lineTop(2) + OverlayMetrics::GraphHeight) * s; + const auto full = OverlayMetrics::GraphHeight * s; + require(bars.back().x == layout.bounds.x + (OverlayMetrics::Padding + static_cast(OverlayStatistics::FrameHistory) - 1) * s, "the newest frame is not at the right end of the graph"); + for (std::size_t i = 0; i + 1 < bars.size(); ++i) require(bars[i + 1].x == bars[i].x + s && bars[i].width == s, "the graph bars are not side by side"); + require(bars.back().height == full && bars.back().y + bars.back().height == graphBottom, "the slowest frame does not fill the graph"); + require(bars.front().height == static_cast(std::lround(10.0 / 50.0 * full)) && bars.front().y + bars.front().height == graphBottom, "a 10 ms frame is not a fifth of a 50 ms one"); + OverlayContent steady = content; + std::fill(steady.frameTimes.begin(), steady.frameTimes.end(), 16.0); + for (const auto& rect : AgcDriver::LayOutOverlay(steady, extent, OverlayCorner::TopLeft).rects) { + if (rect.color == static_cast(AgcDriver::OverlayColor::Graph)) require(rect.height == full / 2, "steady frames do not reach half the graph"); + } + + const std::pair> corners[] = {{OverlayCorner::TopRight, {true, false}}, {OverlayCorner::BottomLeft, {false, true}}, {OverlayCorner::BottomRight, {true, true}}}; + for (const auto& [corner, sides] : corners) { + const auto placed = AgcDriver::LayOutOverlay(content, extent, corner).bounds; + const auto x = sides.first ? static_cast(extent.width) - margin - placed.width : margin; + const auto y = sides.second ? static_cast(extent.height) - margin - placed.height : margin; + require(placed.x == x && placed.y == y && placed.width == layout.bounds.width && placed.height == layout.bounds.height, "a corner places the panel elsewhere"); + } + OverlayContent profiled = content; + profiled.below.push_back("GPU: 2.0 MS"); + require(AgcDriver::LayOutOverlay(profiled, extent, OverlayCorner::TopLeft).bounds.height == layout.bounds.height + OverlayMetrics::LinePitch * s, "a profiled line does not add one line to the panel"); + + bool refused = false; + try { + static_cast(AgcDriver::OverlayGlyph('q')); + } catch (const std::exception&) { + refused = true; + } + require(refused, "a character without a glyph was drawn"); +} + +void paletteTests() { + const auto sdr = AgcDriver::OverlayPalette(VK_FORMAT_B8G8R8A8_UNORM, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR); + require(sdr[1] == std::array{1.0f, 1.0f, 1.0f, 1.0f} && sdr[0][3] > 0.0f && sdr[0][3] < 1.0f, "the SDR palette is not opaque white text over a translucent panel"); + const auto srgb = AgcDriver::OverlayPalette(VK_FORMAT_B8G8R8A8_SRGB, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR); + require(near(srgb[2][1], std::pow((0.8 + 0.055) / 1.055, 2.4), 1e-5) && srgb[1][0] == 1.0f, "an sRGB swapchain gets encoded colors"); + const auto hdr = AgcDriver::OverlayPalette(VK_FORMAT_A2B10G10R10_UNORM_PACK32, VK_COLOR_SPACE_HDR10_ST2084_EXT); + for (std::size_t channel = 0; channel < 3; ++channel) require(near(hdr[1][channel], 0.5807, 0.0015), "HDR10 white text is not at 203 nits (PQ " + std::to_string(hdr[1][channel]) + ")"); + require(std::lround(hdr[0][0] * 1023.0f) == 0 && hdr[0][3] == sdr[0][3], "the HDR10 panel is not black at the same opacity"); +} + +} + +int main() { + try { + fpsWindowTests(); + frameRingTests(); + counterTests(); + formatTests(); + settingsTests(); + composeTests(); + layoutTests(); + paletteTests(); + std::cout << "performance overlay statistics, layout and palette tests passed\n"; + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} From a41be04ed9829738b68217f3da0f56d9b30077fd Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Wed, 14 Oct 2026 16:36:48 +0000 Subject: [PATCH 0101/1000] feat(videoout): toggle the performance overlay with F10 TogglePerformanceOverlay is bound to F10 by default and, like ToggleFullscreen, accepts only a keyboard key in anyps5-input.ini. --- .../prx/libScePad/include/InputMapping.hpp | 1 + .../prx/libScePad/include/KeyboardMouse.hpp | 1 + .../prx/libScePad/include/PadInputTypes.hpp | 3 ++- .../prx/libSceVideoOut/src/InputMapping.cpp | 6 +++++- .../prx/libSceVideoOut/src/KeyboardMouse.cpp | 2 ++ core/libs/prx/libSceVideoOut/src/PadInput.cpp | 2 ++ .../prx/libSceVideoOut/tests/InputMapping.cpp | 18 ++++++++++++++++++ .../prx/libSceVideoOut/tests/KeyboardMouse.cpp | 3 +++ 8 files changed, 34 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libScePad/include/InputMapping.hpp b/core/libs/prx/libScePad/include/InputMapping.hpp index a8914303d5..70e875fed8 100644 --- a/core/libs/prx/libScePad/include/InputMapping.hpp +++ b/core/libs/prx/libScePad/include/InputMapping.hpp @@ -35,6 +35,7 @@ struct InputConfiguration { inline constexpr std::array InputMapping{ InputBinding{SDL_SCANCODE_F11, MouseButton::None, InputControl::ToggleFullscreen}, + InputBinding{SDL_SCANCODE_F10, MouseButton::None, InputControl::TogglePerformanceOverlay}, InputBinding{SDL_SCANCODE_RETURN, MouseButton::None, InputControl::Button, PadButton::Cross}, InputBinding{SDL_SCANCODE_SPACE, MouseButton::None, InputControl::Button, PadButton::Cross}, InputBinding{SDL_SCANCODE_ESCAPE, MouseButton::None, InputControl::Button, PadButton::Options}, diff --git a/core/libs/prx/libScePad/include/KeyboardMouse.hpp b/core/libs/prx/libScePad/include/KeyboardMouse.hpp index 68e501a384..6864ba9440 100644 --- a/core/libs/prx/libScePad/include/KeyboardMouse.hpp +++ b/core/libs/prx/libScePad/include/KeyboardMouse.hpp @@ -36,6 +36,7 @@ private: struct InputRequests { bool toggleFullscreen = false; + bool togglePerformanceOverlay = false; bool toggleMouse = false; bool releaseMouse = false; }; diff --git a/core/libs/prx/libScePad/include/PadInputTypes.hpp b/core/libs/prx/libScePad/include/PadInputTypes.hpp index 81842bae72..8f9db0ffb4 100644 --- a/core/libs/prx/libScePad/include/PadInputTypes.hpp +++ b/core/libs/prx/libScePad/include/PadInputTypes.hpp @@ -20,7 +20,8 @@ enum class InputControl { TouchLeft, TouchRight, ToggleMouse, - ToggleFullscreen + ToggleFullscreen, + TogglePerformanceOverlay }; enum class PadButton : std::uint32_t { diff --git a/core/libs/prx/libSceVideoOut/src/InputMapping.cpp b/core/libs/prx/libSceVideoOut/src/InputMapping.cpp index d974db9e75..8662d18e0b 100644 --- a/core/libs/prx/libSceVideoOut/src/InputMapping.cpp +++ b/core/libs/prx/libSceVideoOut/src/InputMapping.cpp @@ -52,7 +52,8 @@ constexpr auto actions = std::array{ InputAction{"TouchLeft", Pad::InputControl::TouchLeft, Pad::PadButton::None}, InputAction{"TouchRight", Pad::InputControl::TouchRight, Pad::PadButton::None}, InputAction{"ToggleMouse", Pad::InputControl::ToggleMouse, Pad::PadButton::None}, - InputAction{"ToggleFullscreen", Pad::InputControl::ToggleFullscreen, Pad::PadButton::None} + InputAction{"ToggleFullscreen", Pad::InputControl::ToggleFullscreen, Pad::PadButton::None}, + InputAction{"TogglePerformanceOverlay", Pad::InputControl::TogglePerformanceOverlay, Pad::PadButton::None} }; std::string upper(std::string_view value) { @@ -111,6 +112,9 @@ Pad::InputBinding parseBinding(const InputAction& action, std::string_view sourc if (action.control == Pad::InputControl::ToggleFullscreen) { throw std::runtime_error("ToggleFullscreen can only use a keyboard key"); } + if (action.control == Pad::InputControl::TogglePerformanceOverlay) { + throw std::runtime_error("TogglePerformanceOverlay can only use a keyboard key"); + } return {SDL_SCANCODE_UNKNOWN, parseMouseButton(value), action.control, action.button, 0}; } if (type == "WHEEL") { diff --git a/core/libs/prx/libSceVideoOut/src/KeyboardMouse.cpp b/core/libs/prx/libSceVideoOut/src/KeyboardMouse.cpp index d8cf0e3d7c..07e0b26f6d 100644 --- a/core/libs/prx/libSceVideoOut/src/KeyboardMouse.cpp +++ b/core/libs/prx/libSceVideoOut/src/KeyboardMouse.cpp @@ -146,6 +146,7 @@ Pad::InputRequests Pad::KeyboardMouse::HandleEvent(const SDL_Event& event, Clock : binding.mouseButton != MouseButton::None && binding.mouseButton == static_cast(event.button.button); if (!matches) continue; if (binding.control == InputControl::ToggleFullscreen && keyboard && down && !pressed[index]) requests.toggleFullscreen = true; + if (binding.control == InputControl::TogglePerformanceOverlay && keyboard && down && !pressed[index]) requests.togglePerformanceOverlay = true; if (binding.control == InputControl::ToggleMouse && down && !pressed[index]) requests.toggleMouse = true; pressed[index] = down; } @@ -177,6 +178,7 @@ PadInputState Pad::KeyboardMouse::Compose(const PadInputState& controller) const case InputControl::TouchRight: state.touchRight = true; break; case InputControl::ToggleMouse: break; case InputControl::ToggleFullscreen: break; + case InputControl::TogglePerformanceOverlay: break; } } for (std::size_t axis = 0; axis < state.sticks.size(); ++axis) { diff --git a/core/libs/prx/libSceVideoOut/src/PadInput.cpp b/core/libs/prx/libSceVideoOut/src/PadInput.cpp index 06352b8775..b84286d6c7 100644 --- a/core/libs/prx/libSceVideoOut/src/PadInput.cpp +++ b/core/libs/prx/libSceVideoOut/src/PadInput.cpp @@ -10,6 +10,7 @@ #include "SDL.h" #include "prx/libSceVideoOut/include/PadInput.hpp" #include "prx/libSceVideoOut/include/DisplayWindow.hpp" +#include "prx/libSceAgcDriver/Execution/include/Presentation.hpp" #include "prx/libScePad/include/PadState.hpp" #include "prx/libScePad/include/PadInputTypes.hpp" #include "prx/libc/include/General.hpp" @@ -226,6 +227,7 @@ void PadInput::HandleEvent(const SDL_Event& event, DisplayWindow& window) { if (event.type == SDL_KEYDOWN && ImeCapturesKeyboard_nid_postfix()) return; const auto requests = keyboardMouse.HandleEvent(event, std::chrono::steady_clock::now()); if (requests.toggleFullscreen && window.Handle() != nullptr && event.key.windowID == SDL_GetWindowID(window.Handle())) window.ToggleFullscreen(); + if (requests.togglePerformanceOverlay && window.Handle() != nullptr && event.key.windowID == SDL_GetWindowID(window.Handle())) AgcDriverTogglePerformanceOverlay_nid_postfix(); if (requests.toggleMouse) setMouseMode(!keyboardMouse.MouseMode()); if (event.type != SDL_MOUSEMOTION) publish(); } diff --git a/core/libs/prx/libSceVideoOut/tests/InputMapping.cpp b/core/libs/prx/libSceVideoOut/tests/InputMapping.cpp index daa2dad4d6..5789bb5b96 100644 --- a/core/libs/prx/libSceVideoOut/tests/InputMapping.cpp +++ b/core/libs/prx/libSceVideoOut/tests/InputMapping.cpp @@ -75,6 +75,23 @@ void settingsReject() { require(rejects("MouseSensitivity = 1\nMouseSensitivity = 2\n"), "a setting given twice is rejected"); } +void performanceOverlayBinding() { + useConfig("Cross = KEY:F\n"); + bool onF10 = false; + for (const auto& binding : Pad::LoadInputMapping().bindings) onF10 = onF10 || (binding.control == Pad::InputControl::TogglePerformanceOverlay && binding.key == SDL_SCANCODE_F10); + require(onF10, "F10 toggles the performance overlay by default"); + useConfig("togglePerformanceOverlay = KEY:F9\n"); + bool onF9 = false; + bool stillOnF10 = false; + for (const auto& binding : Pad::LoadInputMapping().bindings) { + if (binding.control != Pad::InputControl::TogglePerformanceOverlay) continue; + onF9 = onF9 || binding.key == SDL_SCANCODE_F9; + stillOnF10 = stillOnF10 || binding.key == SDL_SCANCODE_F10; + } + require(onF9 && !stillOnF10, "the performance overlay toggle moves to another key"); + require(rejects("TogglePerformanceOverlay = MOUSE:Left\n"), "the performance overlay toggle refuses a mouse button"); +} + void mouseLookDefault() { useConfig("Cross = KEY:F\n"); const auto settings = Pad::LoadInputMapping().settings; @@ -138,6 +155,7 @@ int main() try { settingsDefault(); settingsParse(); settingsReject(); + performanceOverlayBinding(); mouseLookDefault(); mouseLookParse(); mouseLookReject(); diff --git a/core/libs/prx/libSceVideoOut/tests/KeyboardMouse.cpp b/core/libs/prx/libSceVideoOut/tests/KeyboardMouse.cpp index 93715508cb..008c36e2e0 100644 --- a/core/libs/prx/libSceVideoOut/tests/KeyboardMouse.cpp +++ b/core/libs/prx/libSceVideoOut/tests/KeyboardMouse.cpp @@ -284,6 +284,9 @@ void testToggleRequests() { check(keyboardMouse.HandleEvent(PadSimulation::ButtonEvent(SDL_BUTTON_MIDDLE, true), now).toggleMouse, "middle click asks to toggle the mouse"); check(!keyboardMouse.HandleEvent(PadSimulation::ButtonEvent(SDL_BUTTON_MIDDLE, false), now).toggleMouse, "releasing it asks for nothing"); check(keyboardMouse.HandleEvent(KeyEvent(SDL_SCANCODE_F11, true), now).toggleFullscreen, "F11 asks to toggle fullscreen"); + const auto overlay = keyboardMouse.HandleEvent(KeyEvent(SDL_SCANCODE_F10, true), now); + check(overlay.togglePerformanceOverlay && !overlay.toggleFullscreen, "F10 asks to toggle the performance overlay"); + check(!keyboardMouse.HandleEvent(KeyEvent(SDL_SCANCODE_F10, false), now).togglePerformanceOverlay, "releasing F10 asks for nothing"); } } From 98b9b57ef3dc4e6ede3f18a6cd3220f5c40b2052 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Wed, 14 Oct 2026 16:36:48 +0000 Subject: [PATCH 0102/1000] docs: describe the performance overlay --- docs/user/INPUT_MAPPING.md | 2 +- docs/user/USAGE.md | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/docs/user/INPUT_MAPPING.md b/docs/user/INPUT_MAPPING.md index d61e36d70a..ad5b146e65 100644 --- a/docs/user/INPUT_MAPPING.md +++ b/docs/user/INPUT_MAPPING.md @@ -16,7 +16,7 @@ While a title has a mouse open (`sceMouseOpen`) and the window is focused, AnyPS Game-controller bindings are enabled automatically and are not affected by keyboard or mouse overrides in this file. -Supported actions are `Cross`, `Circle`, `Triangle`, `Square`, `L1`, `R1`, `L2`, `R2`, `L3`, `R3`, `Options`, `Up`, `Right`, `Down`, `Left`, `LeftStickLeft`, `LeftStickRight`, `LeftStickUp`, `LeftStickDown`, `RightStickLeft`, `RightStickRight`, `RightStickUp`, `RightStickDown`, `TouchLeft`, `TouchRight`, `ToggleMouse`, and `ToggleFullscreen`. +Supported actions are `Cross`, `Circle`, `Triangle`, `Square`, `L1`, `R1`, `L2`, `R2`, `L3`, `R3`, `Options`, `Up`, `Right`, `Down`, `Left`, `LeftStickLeft`, `LeftStickRight`, `LeftStickUp`, `LeftStickDown`, `RightStickLeft`, `RightStickRight`, `RightStickUp`, `RightStickDown`, `TouchLeft`, `TouchRight`, `ToggleMouse`, `ToggleFullscreen` (F11), and `TogglePerformanceOverlay` (F10, the [performance overlay](USAGE.md#performance-overlay)). `ToggleFullscreen` and `TogglePerformanceOverlay` accept only keyboard keys. For example, this changes Cross to F or Space, moves the left stick to IJKL, uses the mouse buttons for Square and R2, and keeps all other built-in bindings: diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index 1e736e2820..65ad40ed1d 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -165,6 +165,21 @@ Do not disable Dozen if it is the only Vulkan driver available; the application HDR output is available on Windows only. Titles see it when the monitor showing the window runs in HDR mode (Settings > Display > Use HDR), the window's Vulkan surface offers an HDR10 (`VK_COLOR_SPACE_HDR10_ST2084_EXT`) swapchain in a 10-bit format, the instance has `VK_EXT_swapchain_colorspace` and the device `VK_EXT_hdr_metadata`. The monitor's luminance comes from DXGI; a monitor that reports no peak luminance is taken as 1000 nits peak and full-frame, 0 nits black. On Linux and other hosts the output is always SDR, because the surface's formats do not show whether the display is in HDR. A title's BT.2100 PQ display buffers then reach the display unchanged; without HDR output they are converted to SDR. +### Performance overlay + +F10 shows or hides a performance overlay over the presented image; `ANYPS5_OVERLAY=1` shows it from the first frame. `ANYPS5_OVERLAY_POS` puts it in a corner of the window: `tl` (top left, the default), `tr`, `bl` or `br`. For either variable an empty value is the same as an unset one, and any other value stops the game at its first frame with an error that names the variable. The key can be moved with `TogglePerformanceOverlay` in the [input mapping](INPUT_MAPPING.md). The text scales with the window height, at a whole multiple of its 5x7 pixel font; on an HDR10 swapchain the overlay is drawn at 203 nits. + +| Line | Meaning | +|------|---------| +| `FPS` | Frames presented per second, over the last second. | +| `FRAME` | Time between the last two presented frames, in ms. | +| graph | The last 120 frame times, the newest on the right. The full height is the longest of them or twice their average, whichever is larger. | +| `MIN/AVG/MAX` | The shortest, average and longest of the 120 frame times, in ms. | +| `GPU` | GPU time per frame over the last second, in ms: the span of each recorded batch from its first to its last command, from the timestamp queries of `APS5_PROFILE_GPU=1`. Shown only with that variable. Batches are counted when they complete, so the line trails a sudden change by up to a second. | +| `DRIVER` | CPU time per frame over the last second, in ms, that the packet workers spent in dispatch and draw packets (the `dispatch` and `draw` time of the `[gpu] worker` line of `APS5_PROFILE_DRAW=1`). Shown only with that variable. | + +Times are shown with one decimal below 100 and as whole numbers above, up to 9999. While the overlay is hidden the presentation records nothing for it and the presented image is unchanged; while it is shown, one small render pass draws it over each presented frame. + ### Storing GPU results at each flip GPU results in storage images and render targets stay on the GPU until something reads their memory. A title that frees such an image after waiting for its GPU work and reuses the memory can have its new data overwritten when the results are stored later ([TechnicalDebt](../dev/TechnicalDebt.md)). `APS5_STORE_AT_FLIP=1` stores every pending result when a display buffer is flipped, at the cost of one write-back per pending image and frame. Any other value stops the title at its first flip. From b4ee57e6ffff42aa8b1bec5c6a2b643e230aa932 Mon Sep 17 00:00:00 2001 From: Kurenaiiiii Date: Wed, 14 Oct 2026 09:47:41 +0530 Subject: [PATCH 0103/1000] fix(libSceGameUpdate): return INVALID_ARGUMENT for null entitlement label --- core/libs/prx/libSceGameUpdate/Export.cpp | 2 +- core/libs/tests/GuestGameUpdate.cpp | 5 +---- docs/dev/debt/libSceGameUpdate.md | 2 +- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libSceGameUpdate/Export.cpp b/core/libs/prx/libSceGameUpdate/Export.cpp index 4c0d0bb575..7acb4eecc2 100644 --- a/core/libs/prx/libSceGameUpdate/Export.cpp +++ b/core/libs/prx/libSceGameUpdate/Export.cpp @@ -44,7 +44,7 @@ int APS5_VABI sceGameUpdateGetAddcontLatestVersion(uint32_t service_label, const return SCE_GAME_UPDATE_ERROR_INVALID_ARGUMENT; } if (entitlement_label == nullptr) { - NotImplemented_nid_no_patch(__func__); + return SCE_GAME_UPDATE_ERROR_INVALID_ARGUMENT; } if (info->size < sizeof(GameUpdateAddcontVersionInfo)) { return SCE_GAME_UPDATE_ERROR_INVALID_SIZE; diff --git a/core/libs/tests/GuestGameUpdate.cpp b/core/libs/tests/GuestGameUpdate.cpp index 12aa20e597..0394f1283d 100644 --- a/core/libs/tests/GuestGameUpdate.cpp +++ b/core/libs/tests/GuestGameUpdate.cpp @@ -27,10 +27,7 @@ int main() { Require(sceGameUpdateInitialize() == 0); Require(sceGameUpdateGetAddcontLatestVersion(0, label, nullptr) == invalidArgument); - bool threw = false; - try { sceGameUpdateGetAddcontLatestVersion(0, nullptr, &info); } - catch (const std::runtime_error&) { threw = true; } - Require(threw); + Require(sceGameUpdateGetAddcontLatestVersion(0, nullptr, &info) == invalidArgument); info.size = sizeof(info) - 1; Require(sceGameUpdateGetAddcontLatestVersion(0, label, &info) == invalidSize); diff --git a/docs/dev/debt/libSceGameUpdate.md b/docs/dev/debt/libSceGameUpdate.md index 7d7e234519..d82804dae6 100644 --- a/docs/dev/debt/libSceGameUpdate.md +++ b/docs/dev/debt/libSceGameUpdate.md @@ -2,4 +2,4 @@ ### Unknown function info -- [sceGameUpdateGetAddcontLatestVersion](../../../core/libs/prx/libSceGameUpdate/Export.cpp) (libSceGameUpdate) - a null `entitlement_label` throws: KytyPS5 accepts it, prosper returns `0x80412803`. Bytes after `found` are zeroed as in KytyPS5; prosper leaves them untouched +- [sceGameUpdateGetAddcontLatestVersion](../../../core/libs/prx/libSceGameUpdate/Export.cpp) (libSceGameUpdate) - a null `entitlement_label` returns `0x80412803` as in prosper; KytyPS5 accepts it instead, and neither is confirmed on a console. Bytes after `found` are zeroed as in KytyPS5; prosper leaves them untouched From 36ffe08690fca31b9a7e27673827fe66a218ca4e Mon Sep 17 00:00:00 2001 From: Kurenaiiiii Date: Wed, 14 Oct 2026 21:42:49 +0530 Subject: [PATCH 0104/1000] feat(libSceNpWebApi2): accept PushEventAbortHandle for known contexts --- core/libs/prx/libSceNpWebApi2/Export.cpp | 4 ++-- core/libs/tests/GuestNpWebApi2.cpp | 4 ++++ docs/dev/debt/libSceNpWebApi2.md | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceNpWebApi2/Export.cpp b/core/libs/prx/libSceNpWebApi2/Export.cpp index f36474b481..1157934220 100644 --- a/core/libs/prx/libSceNpWebApi2/Export.cpp +++ b/core/libs/prx/libSceNpWebApi2/Export.cpp @@ -119,9 +119,9 @@ int APS5_VABI sceNpWebApi2PushEventCreateHandle(int lib_ctx_id) { } int APS5_VABI sceNpWebApi2PushEventAbortHandle(int libCtxId, int handleId) { - (void)libCtxId; (void)handleId; - NotImplemented_nid_no_patch(__func__); + std::lock_guard lock(g_poolsMutex); + if (!g_pools.contains(libCtxId)) throw std::invalid_argument("sceNpWebApi2PushEventAbortHandle: unknown context"); return 0; } diff --git a/core/libs/tests/GuestNpWebApi2.cpp b/core/libs/tests/GuestNpWebApi2.cpp index b01b7e1902..f6504104c1 100644 --- a/core/libs/tests/GuestNpWebApi2.cpp +++ b/core/libs/tests/GuestNpWebApi2.cpp @@ -16,6 +16,7 @@ int APS5_VABI sceNpWebApi2Initialize(int, std::size_t); int APS5_VABI sceNpWebApi2Terminate(int); int APS5_VABI sceNpWebApi2GetMemoryPoolStats(int, NpWebApi2MemoryPoolStats*); int APS5_VABI sceNpWebApi2PushEventCreateHandle(int); +int APS5_VABI sceNpWebApi2PushEventAbortHandle(int, int); int APS5_VABI sceNpWebApi2PushEventSetHandleTimeout(int, int, std::uint32_t); int APS5_VABI sceNpWebApi2CreateUserContext(int, int); int APS5_VABI sceNpWebApi2CreateRequest(int, const char*, const char*, const char*, const void*, std::int64_t*); @@ -46,6 +47,8 @@ int main() { Require(Throws([&] { sceNpWebApi2GetMemoryPoolStats(context, nullptr); })); const int handle = sceNpWebApi2PushEventCreateHandle(context); Require(handle > 0); + Require(sceNpWebApi2PushEventAbortHandle(context, handle) == 0); + Require(sceNpWebApi2PushEventAbortHandle(context, handle + 1000) == 0); Require(sceNpWebApi2PushEventSetHandleTimeout(context, handle, 30000) == 0); const int user = sceNpWebApi2CreateUserContext(context, 1); std::int64_t request = 0; @@ -54,6 +57,7 @@ int main() { Require(sceNpWebApi2DeleteRequest(request) == 0); Require(sceNpWebApi2Terminate(context) == 0); Require(Throws([&] { sceNpWebApi2GetMemoryPoolStats(context, &stats); })); + Require(Throws([&] { sceNpWebApi2PushEventAbortHandle(context, handle); })); Require(Throws([&] { sceNpWebApi2PushEventSetHandleTimeout(context, handle, 30000); })); Require(sceNpWebApi2Terminate(other) == 0); return 0; diff --git a/docs/dev/debt/libSceNpWebApi2.md b/docs/dev/debt/libSceNpWebApi2.md index 9e8e3461c6..c805a3decc 100644 --- a/docs/dev/debt/libSceNpWebApi2.md +++ b/docs/dev/debt/libSceNpWebApi2.md @@ -6,6 +6,6 @@ ### Unknown function info -- [sceNpWebApi2PushEventAbortHandle](../../../core/libs/prx/libSceNpWebApi2/Export.cpp) (libSceNpWebApi2, NID `1OLgvahaSco`) is a throwing stub. The two signed 32-bit handle arguments and return type follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/443f022450f6857256e780daa3d5a14029a748f7/src/core/libraries/np/np_web_api2/np_web_api2.cpp) (PS4); the PS5 signature and abort behavior are unverified. PPSA01736 imports it. +- [sceNpWebApi2PushEventAbortHandle](../../../core/libs/prx/libSceNpWebApi2/Export.cpp) (libSceNpWebApi2, NID `1OLgvahaSco`) - the two signed 32-bit handle arguments and return type follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/443f022450f6857256e780daa3d5a14029a748f7/src/core/libraries/np/np_web_api2/np_web_api2.cpp) (PS4); the PS5 signature and abort behavior are unverified. PPSA01736 imports it. An unknown library context throws, as in `sceNpWebApi2PushEventSetHandleTimeout`. The handle is not checked and nothing is aborted, as push event handles are not tracked and nothing waits on a handle offline - [sceNpWebApi2GetMemoryPoolStats](../../../core/libs/prx/libSceNpWebApi2/Export.cpp) (libSceNpWebApi2) - imported by PPSA21703's Unity PSN plugin. The signature `(int libCtxId, stats*)` and the layout of the Http2 statistics above (measured on a PS5 for Http2, Ssl and Net) are assumed for this library, not measured. The pool size is the one given to `sceNpWebApi2Initialize`, and the peak and current use are 0. The error codes are unknown, so a null output or an unknown context throws - [sceNpWebApi2PushEventSetHandleTimeout](../../../core/libs/prx/libSceNpWebApi2/Export.cpp) (libSceNpWebApi2) - imported by PPSA01317, which fails to load without it. The signature `(int libCtxId, int handleId, uint32_t timeout)` follows `sceNpWebApiSetHandleTimeout` of the PS4 libSceNpWebApi in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/np/np_web_api.cpp) and is unverified on the PS5. An unknown library context throws, as in `sceNpWebApi2GetMemoryPoolStats`, since its error code is unknown. The handle is not checked, as push event handles are not tracked (`sceNpWebApi2PushEventDeleteHandle` accepts any handle), and the timeout is not kept, as nothing waits on a handle offline From d902afe2d4684c8c820a1d0e2c244692a865f874 Mon Sep 17 00:00:00 2001 From: Ondra97 <47064087+Ondra97@users.noreply.github.com> Date: Wed, 14 Oct 2026 18:47:19 +0200 Subject: [PATCH 0105/1000] fix(libc): create noreplace streams exclusively --- core/libs/prx/libc/src/FileAndHeap.cpp | 13 ++---- core/libs/tests/FileStreams.cpp | 63 +++++++++++++++++++++++++- 2 files changed, 66 insertions(+), 10 deletions(-) diff --git a/core/libs/prx/libc/src/FileAndHeap.cpp b/core/libs/prx/libc/src/FileAndHeap.cpp index 69e7f8d0fe..b0be4d5d32 100644 --- a/core/libs/prx/libc/src/FileAndHeap.cpp +++ b/core/libs/prx/libc/src/FileAndHeap.cpp @@ -339,17 +339,14 @@ FileStream* APS5_VABI _ZSt7_FiopenPKcNSt5_IosbIiE9_OpenmodeEi_nid_postfix(const {In, "r"}, {Out, "w"}, {Out | Trunc, "w"}, {Out | App, "a"}, {In | Binary, "rb"}, {Out | Binary, "wb"}, {Out | Trunc | Binary, "wb"}, {Out | App | Binary, "ab"}, {In | Out, "r+"}, {In | Out | Trunc, "w+"}, {In | Out | App, "a+"}, - {In | Out | Binary, "r+b"}, {In | Out | Trunc | Binary, "w+b"}, {In | Out | App | Binary, "a+b"}}; - const int open = (mode & (In | Out | App | Trunc | Binary)) | ((mode & Nocreate) ? In : 0) | ((mode & App) ? Out : 0); + {In | Out | Binary, "r+b"}, {In | Out | Trunc | Binary, "w+b"}, {In | Out | App | Binary, "a+b"}, + {Out | Noreplace, "wx"}, {Out | Trunc | Noreplace, "wx"}, {In | Out | Trunc | Noreplace, "w+x"}, + {Out | Binary | Noreplace, "wbx"}, {Out | Trunc | Binary | Noreplace, "wbx"}, + {In | Out | Trunc | Binary | Noreplace, "w+bx"}}; + const int open = (mode & (In | Out | App | Trunc | Binary | Noreplace)) | ((mode & Nocreate) ? In : 0) | ((mode & App) ? Out : 0); const char* openMode = nullptr; for (const auto& [flags, text] : modes) if (flags == open) openMode = text; if (!openMode) return nullptr; - if ((mode & Noreplace) && (open & Out)) { - if (auto* existing = fopen_nid_postfix(filename, "r")) { - fclose_nid_postfix(existing); - return nullptr; - } - } auto* stream = fopen_nid_postfix(filename, openMode); if (!stream || !(mode & Ate) || fseek_nid_postfix(stream, 0, SEEK_END) == 0) return stream; fclose_nid_postfix(stream); diff --git a/core/libs/tests/FileStreams.cpp b/core/libs/tests/FileStreams.cpp index ff80543263..92c8b584ad 100644 --- a/core/libs/tests/FileStreams.cpp +++ b/core/libs/tests/FileStreams.cpp @@ -1,6 +1,7 @@ #include "prx/libc/include/FileStream.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include +#include #include #include #include @@ -8,8 +9,11 @@ #include #include #include +#include #include #ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN +#include #include #else #include @@ -17,6 +21,7 @@ extern "C" { FileStream* APS5_VABI fopen_nid_postfix(const char* filename, const char* mode); +FileStream* APS5_VABI _ZSt7_FiopenPKcNSt5_IosbIiE9_OpenmodeEi_nid_postfix(const char*, int, int); FileStream* APS5_VABI freopen_nid_postfix(const char* filename, const char* mode, FileStream* stream); int APS5_VABI fclose_nid_postfix(FileStream* stream); std::size_t APS5_VABI fread_nid_postfix(void* buffer, std::size_t size, std::size_t count, FileStream* stream); @@ -31,8 +36,8 @@ int APS5_VABI ferror_nid_postfix(FileStream* stream); void APS5_VABI clearerr_nid_postfix(FileStream* stream); } -static void Require(bool condition) { - if (!condition) throw std::runtime_error("File stream check failed"); +static void Require(bool condition, const char* message = "File stream check failed") { + if (!condition) throw std::runtime_error(message); } template @@ -57,8 +62,62 @@ static void ReplaceWithReadOnlyDescriptor(FileStream* stream, const char* path) #endif } +static void CheckExclusiveCppOpen(const std::string& base) { + constexpr int Out = 0x02, Binary = 0x80, Noreplace = 0x40; + const std::string path = base + ".exclusive"; + std::remove(path.c_str()); + auto* stream = _ZSt7_FiopenPKcNSt5_IosbIiE9_OpenmodeEi_nid_postfix(path.c_str(), Out | Binary | Noreplace, 0); + Require(stream != nullptr); + constexpr char original[] = "keep existing contents"; + Require(fwrite_nid_postfix(original, 1, sizeof(original), stream) == sizeof(original)); + Require(fclose_nid_postfix(stream) == 0); +#ifdef _WIN32 + const auto nativePath = std::filesystem::path(path).wstring(); + const auto writer = CreateFileW(nativePath.c_str(), GENERIC_WRITE, FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, + OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); + Require(writer != INVALID_HANDLE_VALUE); + auto* refused = _ZSt7_FiopenPKcNSt5_IosbIiE9_OpenmodeEi_nid_postfix(path.c_str(), Out | Binary | Noreplace, 0); + if (refused) fclose_nid_postfix(refused); + Require(CloseHandle(writer) != 0); + Require(refused == nullptr, "C++ noreplace truncated an existing write-shared file"); +#endif + stream = fopen_nid_postfix(path.c_str(), "rb"); + Require(stream != nullptr); + std::array saved{}; + Require(fread_nid_postfix(saved.data(), 1, saved.size(), stream) == saved.size()); + Require(std::memcmp(saved.data(), original, saved.size()) == 0); + Require(fclose_nid_postfix(stream) == 0 && std::remove(path.c_str()) == 0); + for (const int mode : {0x42, 0x52, 0x53, 0xc2, 0xd2, 0xd3}) { + stream = _ZSt7_FiopenPKcNSt5_IosbIiE9_OpenmodeEi_nid_postfix(path.c_str(), mode | 0x04, 0); + Require(stream != nullptr && ftell_nid_postfix(stream) == 0); + Require(fwrite_nid_postfix(original, 1, sizeof(original), stream) == sizeof(original)); + Require(fclose_nid_postfix(stream) == 0); + errno = 0; + Require(_ZSt7_FiopenPKcNSt5_IosbIiE9_OpenmodeEi_nid_postfix(path.c_str(), mode, 0) == nullptr && errno == EEXIST); + stream = fopen_nid_postfix(path.c_str(), "rb"); + Require(stream != nullptr && fread_nid_postfix(saved.data(), 1, saved.size(), stream) == saved.size()); + Require(std::memcmp(saved.data(), original, saved.size()) == 0); + Require(fclose_nid_postfix(stream) == 0 && std::remove(path.c_str()) == 0); + } + for (int round = 0; round < 12; ++round) { + std::array created{}; + std::barrier start(2); + const auto create = [&](int index) { + start.arrive_and_wait(); + created[index] = _ZSt7_FiopenPKcNSt5_IosbIiE9_OpenmodeEi_nid_postfix(path.c_str(), Out | Binary | Noreplace, 0); + }; + std::thread first(create, 0), second(create, 1); + first.join(); + second.join(); + const auto winners = static_cast(created[0] != nullptr) + static_cast(created[1] != nullptr); + for (auto* opened : created) if (opened) Require(fclose_nid_postfix(opened) == 0); + Require(winners == 1 && std::remove(path.c_str()) == 0); + } +} + int main(int argc, char** argv) { Require(argc == 2); + CheckExclusiveCppOpen(argv[1]); Require(_Stdout_nid_postfix.GetHandle() == stdout); Require(_Stderr_nid_postfix.GetHandle() == stderr); Require(fputs_nid_postfix("stdout object works\n", &_Stdout_nid_postfix) >= 0); From e91b8ba7d32277d96366640fac5cbbd54d7e0923 Mon Sep 17 00:00:00 2001 From: mrfelek <17274927+mrfelek@users.noreply.github.com> Date: Wed, 14 Oct 2026 18:47:25 +0200 Subject: [PATCH 0106/1000] test(agc): skip the sparse dma-buf import tests when udmabuf size_limit_mb is above 1024 Both tests build a range one chunk above udmabuf's size_limit_mb. When the limit is set very high (for example to its maximum, as a workaround for a dma-buf import larger than the default 64 MB), that range is petabytes and mmap fails: agc_host_import_sparse stopped with 'cannot reserve the guest range' and agc_driver_sparse_dma_buf_import with 'cannot map the backing file'. The driver does not take the sparse path for such a limit either, so there is nothing to test; both now skip, which ctest reports as skipped. --- .../prx/libSceAgcDriver/tests/HostImportSparseDmaBuf.cpp | 5 +++++ .../libSceAgcDriver/tests/execution/SparseDmaBufImport.cpp | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/HostImportSparseDmaBuf.cpp b/core/libs/prx/libSceAgcDriver/tests/HostImportSparseDmaBuf.cpp index a6d05d4dc7..9ac2de377c 100644 --- a/core/libs/prx/libSceAgcDriver/tests/HostImportSparseDmaBuf.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/HostImportSparseDmaBuf.cpp @@ -40,6 +40,7 @@ void Expect(bool condition, const std::string& what) { constexpr std::uint64_t Alignment = 65536; constexpr std::uint64_t ChunkBytes = std::uint64_t{64} << 20u; +constexpr std::uint64_t LargestTestedLimit = std::uint64_t{1} << 30u; struct Bind { VkDeviceSize resourceOffset; @@ -286,6 +287,10 @@ int main() { return 77; } close(udmabuf); + if (UdmabufLimitBytes() > LargestTestedLimit) { + std::printf("skipped, udmabuf size_limit_mb is above %llu: a range above it is too large to map\n", static_cast(LargestTestedLimit >> 20u)); + return 77; + } setenv("APS5_WRITE_WATCH_IMPORTS", "unwatch", 1); GuestArena::GuestArenaSetSharedBacking_nid_postfix(Resolve); try { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SparseDmaBufImport.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SparseDmaBufImport.cpp index 6c271a9f48..c3f1312821 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/SparseDmaBufImport.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SparseDmaBufImport.cpp @@ -29,6 +29,7 @@ namespace { using AgcDriver::Graphics::Require; constexpr std::uint64_t ChunkBytes = std::uint64_t{64} << 20u; +constexpr std::uint64_t LargestTestedLimit = std::uint64_t{1} << 30u; std::uint8_t* mapping = nullptr; std::uint64_t mappingBytes = 0; @@ -66,6 +67,10 @@ int main() { return VulkanTestSkipped; } close(udmabuf); + if (UdmabufLimitBytes() > LargestTestedLimit) { + std::printf("skipped, udmabuf size_limit_mb is above %llu: a range above it is too large to map\n", static_cast(LargestTestedLimit >> 20u)); + return VulkanTestSkipped; + } auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; From 661b1671ff0f5f3abeb0b715c49f6676323b596e Mon Sep 17 00:00:00 2001 From: Rujul Talekar <113801876+Roojool@users.noreply.github.com> Date: Wed, 14 Oct 2026 03:17:01 +0530 Subject: [PATCH 0107/1000] feat(libc): implement collate::_Getcat --- core/libs/prx/libc/src/LocaleSupport.cpp | 10 +++- core/libs/tests/GuestCollate.cpp | 59 ++++++++++++++++++++++++ docs/dev/debt/libc.md | 2 +- 3 files changed, 68 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libc/src/LocaleSupport.cpp b/core/libs/prx/libc/src/LocaleSupport.cpp index 9d70e10e48..83a08a3a21 100644 --- a/core/libs/prx/libc/src/LocaleSupport.cpp +++ b/core/libs/prx/libc/src/LocaleSupport.cpp @@ -271,8 +271,14 @@ GuestLocale::Implementation* APS5_VABI _ZNSt6locale16_GetgloballocaleEv_nid_post return &g_classicLocale; } -void APS5_VABI _ZNSt7collateIwE7_GetcatEPPKNSt6locale5facetEPKS1__nid_postfix(GuestLocale::Facet**, const GuestLocale::Implementation*) { - NotImplemented_nid_no_patch(__func__); +std::size_t APS5_VABI _ZNSt7collateIwE7_GetcatEPPKNSt6locale5facetEPKS1__nid_postfix(GuestLocale::Facet** facet, const GuestLocale::Implementation* const* locale) { + if (facet != nullptr && *facet == nullptr) { + if (locale == nullptr || *locale == nullptr || (*locale)->name == nullptr || std::strcmp((*locale)->name, "C") != 0) throw std::invalid_argument("collate::_Getcat: only the C locale is supported"); + auto* collate = static_cast(_Znwm_nid_postfix(sizeof(GuestLocale::CollateFacet))); + *collate = {{&g_collateVtable.facet, 0, 0}, nullptr, nullptr}; + *facet = &collate->base; + } + return 1; } std::size_t APS5_VABI _ZNSt7collateIcE7_GetcatEPPKNSt6locale5facetEPKS1__nid_postfix(GuestLocale::Facet** facet, const GuestLocale::Implementation* const* locale) { diff --git a/core/libs/tests/GuestCollate.cpp b/core/libs/tests/GuestCollate.cpp index ddaf978819..8e1061053e 100644 --- a/core/libs/tests/GuestCollate.cpp +++ b/core/libs/tests/GuestCollate.cpp @@ -11,6 +11,7 @@ extern "C" { extern GuestLocale::Implementation* _ZSt21_sceLibcClassicLocale_nid_postfix; std::size_t APS5_VABI _ZNSt7collateIcE7_GetcatEPPKNSt6locale5facetEPKS1__nid_postfix(GuestLocale::Facet** facet, const GuestLocale::Implementation* const* locale); +std::size_t APS5_VABI _ZNSt7collateIwE7_GetcatEPPKNSt6locale5facetEPKS1__nid_postfix(GuestLocale::Facet** facet, const GuestLocale::Implementation* const* locale); } namespace { @@ -63,6 +64,60 @@ std::uint64_t Hash(const GuestLocale::CollateFacet* facet, const std::string& te return static_cast(Vtable(&facet->base).hash(facet, text.data(), text.data() + text.size())); } +// The wide facet mirrors the narrow one: the same facet allocation, the same C-locale +// validation and the same vtable, because GuestLocale models one CollateVtable for both. +void CheckWideGetcat(const GuestLocale::Implementation* const* classic, const void* narrowVtable) { + require(_ZNSt7collateIwE7_GetcatEPPKNSt6locale5facetEPKS1__nid_postfix(nullptr, classic) == 1); + + const std::size_t before = allocations; + GuestLocale::Facet existing{}; + GuestLocale::Facet* facet = &existing; + require(_ZNSt7collateIwE7_GetcatEPPKNSt6locale5facetEPKS1__nid_postfix(&facet, classic) == 1); + require(facet == &existing && allocations == before); + + GuestLocale::Implementation french = *_ZSt21_sceLibcClassicLocale_nid_postfix; + french.name = "fr_FR"; + const GuestLocale::Implementation* frenchPointer = &french; + GuestLocale::Implementation unnamed = *_ZSt21_sceLibcClassicLocale_nid_postfix; + unnamed.name = nullptr; + const GuestLocale::Implementation* unnamedPointer = &unnamed; + const GuestLocale::Implementation* missingPointer = nullptr; + for (const GuestLocale::Implementation* const* rejected : {&frenchPointer, &unnamedPointer, &missingPointer}) { + GuestLocale::Facet* absent = nullptr; + reject([&] { _ZNSt7collateIwE7_GetcatEPPKNSt6locale5facetEPKS1__nid_postfix(&absent, rejected); }); + require(absent == nullptr && allocations == before); + } + GuestLocale::Facet* absent = nullptr; + reject([&] { _ZNSt7collateIwE7_GetcatEPPKNSt6locale5facetEPKS1__nid_postfix(&absent, nullptr); }); + require(absent == nullptr && allocations == before); + + facet = nullptr; + require(_ZNSt7collateIwE7_GetcatEPPKNSt6locale5facetEPKS1__nid_postfix(&facet, classic) == 1); + require(facet != nullptr && allocations == before + 1 && lastSize == sizeof(GuestLocale::CollateFacet) && lastAllocation == facet); + const auto* wide = reinterpret_cast(facet); + require(facet->references == 0 && facet->vtable == narrowVtable); + require(wide->collation == nullptr && wide->wideCollation == nullptr); + + require(Compare(wide, "abc", "abd") == -1); + require(Compare(wide, "abd", "abc") == 1); + require(Compare(wide, "abc", "abc") == 0); + require(Compare(wide, "ab", "abc") == -1); + require(Compare(wide, "", "a") == -1); + require(Hash(wide, "foobar") == 0x85944171f73967e8ull); + const char narrow[] = "abc"; + reject([&] { Vtable(facet).compare(wide, narrow + 2, narrow, narrow, narrow + 1); }); + + Vtable(facet).facet.retain(facet); + require(facet->references == 1); + require(Vtable(facet).facet.release(facet) == facet && facet->references == 0); + const std::size_t freed = frees; + Vtable(facet).facet.destroy(facet); + require(frees == freed); + Vtable(facet).facet.deleteObject(facet); + require(frees == freed + 1 && lastFree == wide); + require(allocations == before + 1); +} + } int main() { @@ -87,6 +142,7 @@ int main() { require(facet != nullptr && allocations == 1 && lastSize == sizeof(GuestLocale::CollateFacet) && lastAllocation == facet); const auto* collate = reinterpret_cast(facet); require(facet->references == 0 && collate->collation == nullptr && collate->wideCollation == nullptr); + const void* narrowVtable = facet->vtable; require(Compare(collate, "abc", "abd") == -1); require(Compare(collate, "abd", "abc") == 1); @@ -159,5 +215,8 @@ int main() { require(frees == 0); Vtable(facet).facet.deleteObject(facet); require(frees == 1 && lastFree == collate); + + CheckWideGetcat(classic, narrowVtable); + require(frees == 2 && allocations == 5); return 0; } diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index e92d357f7f..e814a52bde 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -58,7 +58,7 @@ - [gethostname](../../../core/libs/prx/libc/src/Hostname.cpp) (libc) reports the fixed guest name `AnyPS5` without querying the host machine name. Buffer copying, null queries and `ENAMETOOLONG` follow [FreeBSD gethostname](https://github.com/freebsd/freebsd-src/blob/main/lib/libc/gen/gethostname.c); the console's configurable hostname and its integration with `sysctl` and name resolution are unverified and not implemented. - [wcsrtombs_s](../../../core/libs/prx/libc/src/Multibyte.cpp) (libc) follows the console's `libc.prx`, but a runtime-constraint violation only returns its error: the default handler there writes the message and a newline to `stderr` before returning, which is not done here. Conversion uses the C locale of `wcrtomb` (`MB_CUR_MAX` 1), so the path where a multibyte character does not fit the remaining space is not reached - [The narrow printf family](../../../core/libs/prx/libc/src/Formatting.cpp) (libc) converts 16-bit guest wide arguments to UTF-8 on both platforms, independently of the host locale. It does not use the guest's single-byte `wcrtomb`; the console's treatment of invalid UTF-16 and locale-specific encoding remains unverified. Linux retains host formatting for formats without wide arguments or `%m`, so a narrow `%p` and a NaN there still follow the host (glibc prints a null pointer as `(nil)`, where FreeBSD prints `0x0`, and a NaN with its sign, `-nan`, and with the `+` and space flags, where FreeBSD prints `nan`); argument positions above 99 (FreeBSD's `NL_ARGMAX`) throw when a format requires the guest formatter -- [collate::_Getcat](../../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc) builds the facet for the C locale only and throws for any other locale name. The console's `libc.prx` stores pointers to its collation state tables at offsets 0x10 and 0x18 of the facet; ours leaves them null, as only the facet's own functions read them. `do_transform` throws for input with embedded null characters, which the console drops depending on the output buffer size. Its result capacity (`length | 15`) and the over-aligned storage of results of 4080 characters or more follow the console's `libc.prx` code and are not checked on hardware. +- [collate and collate _Getcat](../../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc) build the facet for the C locale only and throw for any other locale name, the wide entry point following the narrow one it sits next to. The wide facet carries the narrow vtable because [GuestLocale](../../../core/libs/prx/libc/include/GuestLocale.hpp) models one facet layout and one vtable for both element types, so a `collate` comparison, transform or hash runs the narrow `do_compare`, `do_transform` and `do_hash` over the guest's 16-bit characters read as bytes; the console's `libc.prx` exports both `_Getcat` (NIDs BSVJqITGCyI and 2wz4rthdiy8) with identical 0x19d-byte bodies, believed to build the same 0x20-byte facet; the console bodies themselves are unreadable (obfuscated dynsym), the wide vtable is not reproduced here and no console run checks the two. The console's `libc.prx` stores pointers to its collation state tables at offsets 0x10 and 0x18 of the facet; ours leaves them null, as only the facet's own functions read them. `do_transform` throws for input with embedded null characters, which the console drops depending on the output buffer size. Its result capacity (`length | 15`) and the over-aligned storage of results of 4080 characters or more follow the console's `libc.prx` code and are not checked on hardware. No module of the PPSA59322, PPSA76677 or PPSA97358 dumps in this tree imports either `_Getcat`, so neither has been exercised by a title. - [The scanf family](../../../core/libs/prx/libc/src/Formatting.cpp) (libc), the `_s` forms included, on Linux throws for the 16-bit wide conversions (`%ls`, `%lc`, `%l[`, `%S`, `%C`): glibc would store a 32-bit `wchar_t` per character into the guest's 16-bit buffer. Windows hands `%ls`, `%lc` and `%l[` to the host, whose `wchar_t` is 16 bits - [The scanf family](../../../core/libs/prx/libc/include/WindowsScanning.hpp) (libc) on Windows returns 0, as C17 and FreeBSD do, when input fails after only suppressed conversions have completed (`sscanf("12", "%*d%d", &value)`). Linux hands the format to glibc, which returns `EOF` there. On Windows the host stops a `%x`, `%X`, `%i` or `%p` after the `x` of a `0x` that no hex digit follows (or that the width cuts off), and a float conversion (`%e`, `%f`, `%g`, `%a`) after the `e` of an exponent with no digits (`1e`), where C17 and FreeBSD stop before the `x` or the `e` - [Guest path resolution](../../../core/libs/prx/libc/src/General.cpp) falls back to ASCII case-insensitive component lookup, following [shadPS4's host filesystem](https://github.com/shadps4-emu/shadPS4/blob/945dbc3cc3eee80ac3e053b438502ed936fa6bb2/src/core/file_sys/backends/host_fs.cpp). PPSA03747 requests `Skyrim.INI`, `Data/Video` and lowercase archive/plugin names that differ from the dump's filenames. Exact host names take precedence; multiple fallback matches throw instead of selecting an arbitrary file. Existing parent names are resolved when creating a new file, and guest mount aliases use the same ASCII comparison. Results are not cached, so renames and newly created files remain visible. Unicode case folding and PS5 behaviour for case-colliding host entries are unverified; host roots are used as supplied. Explicit native Windows drive and UNC paths bypass guest parent-component validation.. A relinked module's `.guest.prx` is matched the same way when the original module is not beside it: Borderlands 4 (PPSA01494) loads `/app0/prx/libRenoirCore.PS5.prx`, which it ships as `librenoircore.ps5.prx` From 7126e05ca3b8f7fc7372b46aca5dcadfbac1f8bf Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Wed, 14 Oct 2026 10:01:08 -0700 Subject: [PATCH 0108/1000] docs: say what the overlay's FPS and GPU lines count --- docs/user/USAGE.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index 65ad40ed1d..56f50fa8d8 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -171,11 +171,11 @@ F10 shows or hides a performance overlay over the presented image; `ANYPS5_OVERL | Line | Meaning | |------|---------| -| `FPS` | Frames presented per second, over the last second. | +| `FPS` | Frames presented per second, over the last second. Every present counts, including frames a title presents cleared, for example while it loads. | | `FRAME` | Time between the last two presented frames, in ms. | | graph | The last 120 frame times, the newest on the right. The full height is the longest of them or twice their average, whichever is larger. | | `MIN/AVG/MAX` | The shortest, average and longest of the 120 frame times, in ms. | -| `GPU` | GPU time per frame over the last second, in ms: the span of each recorded batch from its first to its last command, from the timestamp queries of `APS5_PROFILE_GPU=1`. Shown only with that variable. Batches are counted when they complete, so the line trails a sudden change by up to a second. | +| `GPU` | GPU time per frame over the last second, in ms: the span of each recorded batch from its first to its last command, from the timestamp queries of `APS5_PROFILE_GPU=1`. Shown only with that variable. Batches are counted when they complete, so the line trails a sudden change by up to a second. Batches that overlap on different queues each count in full, so the line can exceed `FRAME`. | | `DRIVER` | CPU time per frame over the last second, in ms, that the packet workers spent in dispatch and draw packets (the `dispatch` and `draw` time of the `[gpu] worker` line of `APS5_PROFILE_DRAW=1`). Shown only with that variable. | Times are shown with one decimal below 100 and as whole numbers above, up to 9999. While the overlay is hidden the presentation records nothing for it and the presented image is unchanged; while it is shown, one small render pass draws it over each presented frame. From 6bddb1aa2cedb1d571b23751948b82e3de65f976 Mon Sep 17 00:00:00 2001 From: Ondra97 <47064087+Ondra97@users.noreply.github.com> Date: Wed, 14 Oct 2026 19:25:50 +0200 Subject: [PATCH 0109/1000] fix(pad): read controller commands from Unicode paths --- .../prx/libScePad/include/InputCommandFile.hpp | 5 +++-- core/libs/prx/libScePad/src/PadState.cpp | 9 ++++++++- core/libs/tests/GuestPad.cpp | 15 +++++++++------ 3 files changed, 20 insertions(+), 9 deletions(-) diff --git a/core/libs/prx/libScePad/include/InputCommandFile.hpp b/core/libs/prx/libScePad/include/InputCommandFile.hpp index 7433fd9f49..a4a1c4d405 100644 --- a/core/libs/prx/libScePad/include/InputCommandFile.hpp +++ b/core/libs/prx/libScePad/include/InputCommandFile.hpp @@ -6,6 +6,7 @@ #include #include #include +#include #include namespace Pad { @@ -19,7 +20,7 @@ public: bool operator==(const Input&) const = default; }; - explicit InputCommandFile(const char* path) : path(path != nullptr ? path : "") { + explicit InputCommandFile(const std::filesystem::path& path) : path(path) { if (!this->path.empty()) lastSequence = read().sequence; } @@ -77,7 +78,7 @@ private: return command; } - std::string path; + std::filesystem::path path; std::uint64_t lastSequence = 0; std::uint64_t nextPoll = 0; std::uint64_t until = 0; diff --git a/core/libs/prx/libScePad/src/PadState.cpp b/core/libs/prx/libScePad/src/PadState.cpp index 1288a0fa8b..4f509f8354 100644 --- a/core/libs/prx/libScePad/src/PadState.cpp +++ b/core/libs/prx/libScePad/src/PadState.cpp @@ -186,7 +186,14 @@ PadData BuildData(const PadInputState& state, std::uint64_t changeTime, std::uin } Pad::InputCommandFile::Input InjectedInput(std::uint64_t now) { - static Pad::InputCommandFile commands(std::getenv("APS5_PAD_INPUT_FILE")); + static Pad::InputCommandFile commands([] { +#ifdef _WIN32 + const auto* path = _wgetenv(L"APS5_PAD_INPUT_FILE"); +#else + const auto* path = std::getenv("APS5_PAD_INPUT_FILE"); +#endif + return path ? std::filesystem::path(path) : std::filesystem::path{}; + }()); static Pad::InputCommandFile::Input lastInjected; const auto injected = commands.Poll(now); if (injected != lastInjected) { diff --git a/core/libs/tests/GuestPad.cpp b/core/libs/tests/GuestPad.cpp index 4cb68bd29e..0a6fc05f9a 100644 --- a/core/libs/tests/GuestPad.cpp +++ b/core/libs/tests/GuestPad.cpp @@ -36,7 +36,7 @@ static void CheckInputCommands() { } cleanup{path}; const auto write = [&](const char* command) { std::ofstream(path) << command << '\n'; }; write("1 16384 200"); - Pad::InputCommandFile commands(path.string().c_str()); + Pad::InputCommandFile commands(path); Require(commands.Buttons(1000) == 0); write("2 16384 200"); Require(commands.Buttons(101000) == 0x4000); @@ -85,7 +85,7 @@ static void CheckInputCommands() { Require(accepted.buttons == 0x2000 && accepted.hasSticks && accepted.sticks[3] == 4); std::filesystem::remove(path); Require(commands.Buttons(3001000) == 0); - Pad::InputCommandFile disabled(nullptr); + Pad::InputCommandFile disabled(std::filesystem::path{}); Require(disabled.Buttons(0) == 0); } @@ -244,11 +244,14 @@ int main() { struct Cleanup { std::filesystem::path path; ~Cleanup() { std::error_code error; std::filesystem::remove(path, error); } - } cleanup{path}; + }; + auto commandPath = path; + commandPath += std::filesystem::path(u8"-žluťoučký-漢字"); + Cleanup cleanup{commandPath}; #ifdef _WIN32 - Require(_putenv_s("APS5_PAD_INPUT_FILE", path.string().c_str()) == 0); + Require(_wputenv_s(L"APS5_PAD_INPUT_FILE", commandPath.c_str()) == 0); #else - Require(setenv("APS5_PAD_INPUT_FILE", path.string().c_str(), 1) == 0); + Require(setenv("APS5_PAD_INPUT_FILE", commandPath.string().c_str(), 1) == 0); #endif { std::uint32_t sequence = 0; @@ -292,7 +295,7 @@ int main() { CheckReadQueue(handle); CheckReadStateHandle(handle); CheckRemoteController(handle); - CheckInjectedState(path); + CheckInjectedState(commandPath); Require(scePadSetVibrationMode(handle, 1) == 0); Require(scePadSetVibrationMode(handle, 2) == 0); Require(scePadSetVibrationMode(handle, 3) == 0); From 4211a411771f88fad0c7775a51198d18314b09de Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Wed, 14 Oct 2026 10:30:23 -0700 Subject: [PATCH 0110/1000] refactor(libSceNet): wait for a local address after sceNetRecvmsg's socket type check --- core/libs/prx/libSceNet/Export.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index c6e510a913..152a49eb49 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -988,10 +988,10 @@ int64_t APS5_VABI sceNetRecvmsg(int s, NetMsghdr* msg, int flags) { if (it == g_socks.end()) return fail(NET_EBADF); socket = it->second; } + const bool datagram = socket.type != NET_SOCK_STREAM; #ifdef _WIN32 if (const int error = wait_for_local_address(s, socket)) return fail(error); #endif - const bool datagram = socket.type != NET_SOCK_STREAM; std::vector buffer; try { buffer.resize(static_cast(total) + (datagram ? 1u : 0u)); From 0948f77208c9cbe887f769782e6a0e0ef5ea2bc8 Mon Sep 17 00:00:00 2001 From: Ondra97 <47064087+Ondra97@users.noreply.github.com> Date: Wed, 14 Oct 2026 19:45:46 +0200 Subject: [PATCH 0111/1000] fix(libkernel): reject invalid ftruncate descriptors safely --- core/libs/prx/libkernel/File/src/Stdio.cpp | 5 +-- core/libs/tests/GuestTruncate.cpp | 37 ++++++++++++++++++++++ 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index 58f00fafaa..bec0cec85c 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -111,8 +111,9 @@ static int NativeFtruncate(int descriptor, std::int64_t length) { const int result = File::TruncateSharedMemory_nid_no_patch(descriptor, length); if (result != -2) return result; const auto handle = reinterpret_cast(WithoutParameterHandler([descriptor] { return ::_get_osfhandle(descriptor); })); - if (handle != INVALID_HANDLE_VALUE && ::GetFileType(handle) != FILE_TYPE_DISK) return EINVAL; - return static_cast(::_chsize_s(descriptor, length)); + if (handle == INVALID_HANDLE_VALUE) return EBADF; + if (::GetFileType(handle) != FILE_TYPE_DISK) return EINVAL; + return static_cast(WithoutParameterHandler([descriptor, length] { return ::_chsize_s(descriptor, length); })); } static int NativeTruncate(const std::filesystem::path& path, std::int64_t length) { const int descriptor = ::_wopen(path.c_str(), _O_WRONLY | _O_BINARY); diff --git a/core/libs/tests/GuestTruncate.cpp b/core/libs/tests/GuestTruncate.cpp index 0f8d4b1e50..1e037fd9d8 100644 --- a/core/libs/tests/GuestTruncate.cpp +++ b/core/libs/tests/GuestTruncate.cpp @@ -5,12 +5,15 @@ #include #include #include +#include #include #ifdef _WIN32 #ifndef NOMINMAX #define NOMINMAX #endif #include +#include +extern "C" _invalid_parameter_handler _set_thread_local_invalid_parameter_handler(_invalid_parameter_handler); #else #include #endif @@ -18,12 +21,17 @@ extern "C" { int APS5_VABI truncate_nid_postfix(const char*, std::int64_t); int APS5_VABI sceKernelTruncate_nid_postfix(const char*, std::int64_t); +int APS5_VABI ftruncate_nid_postfix(int, std::int64_t); +int APS5_VABI sceKernelFtruncate(int, std::int64_t); +int APS5_VABI sceKernelOpen(const char*, int, std::uint16_t); +int APS5_VABI sceKernelClose(int); int* APS5_VABI __error_nid_postfix(); } static void Require(bool value) { if (!value) std::abort(); } constexpr int GuestEnoent = 2; +constexpr int GuestEbadf = 9; constexpr int GuestEacces = 13; constexpr int GuestEfault = 14; constexpr int GuestEisdir = 21; @@ -40,6 +48,31 @@ static std::string Contents(const std::filesystem::path& file) { return std::string(std::istreambuf_iterator(stream), std::istreambuf_iterator()); } +#ifdef _WIN32 +static unsigned parameterReports = 0; + +static void CountParameterReport(const wchar_t*, const wchar_t*, const wchar_t*, unsigned int, std::uintptr_t) { + ++parameterReports; +} + +static void CheckDescriptorFailures(const char* path) { + const int closed = sceKernelOpen(path, 2, 0); + Require(closed >= 0 && sceKernelClose(closed) == 0); + const auto previous = _set_thread_local_invalid_parameter_handler(CountParameterReport); + for (const int descriptor : {-2, -1, closed, std::numeric_limits::max()}) { + for (const std::int64_t length : {std::int64_t{0}, std::int64_t{8}}) { + RequireFailure(ftruncate_nid_postfix(descriptor, length), GuestEbadf); + Require(sceKernelFtruncate(descriptor, length) == SceKernelErrorBase + GuestEbadf); + } + } + Require(_set_thread_local_invalid_parameter_handler(previous) == CountParameterReport); + if (parameterReports != 0) std::fprintf(stderr, "ftruncate invoked the CRT parameter handler %u times\n", parameterReports); + Require(parameterReports == 0); + RequireFailure(ftruncate_nid_postfix(closed, 0), GuestEbadf); + Require(sceKernelFtruncate(-1, 0) == SceKernelErrorBase + GuestEbadf); +} +#endif + int main() { const auto name = "anyps5-truncate-test-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()); const auto directory = std::filesystem::path(name); @@ -47,6 +80,10 @@ int main() { const auto file = directory / "sized.txt"; { std::ofstream output(file, std::ios::binary); output << "0123456789abcdef"; } const auto path = file.string(); +#ifdef _WIN32 + CheckDescriptorFailures(path.c_str()); + Require(Contents(file) == "0123456789abcdef"); +#endif Require(truncate_nid_postfix(path.c_str(), 6) == 0); Require(std::filesystem::file_size(file) == 6); From 901b92c79474db31f7e04b4fe480781ead72b14b Mon Sep 17 00:00:00 2001 From: Get-fix <100056769+Get-fix@users.noreply.github.com> Date: Sun, 11 Oct 2026 17:46:31 +0300 Subject: [PATCH 0112/1000] feat(libSceVideoOut): register PS4 display buffers Add the PS4 functions sceVideoOutSetBufferAttribute and sceVideoOutRegisterBuffers on top of the existing registration. The PS4 call uses the first free attribute set and returns its index. It accepts linear A8R8G8B8_SRGB and A8B8G8R8_SRGB buffers, which map to the PS5 B8G8R8A8 and R8G8B8A8 formats. It throws for tiled buffers, whose PS4 layout differs from the PS5 one. Linear PS4 buffers may be 4 KiB aligned: the OpenOrbis graphics sample places its second 1920x1080 buffer 8,294,400 bytes after the first. PS5 buffers keep the 64 KiB rule. A handle with PS4 buffers writes the PS4 flip status (64 bytes) and vblank status (32 bytes). Writing the 128-byte PS5 flip status into the sample's stack structure overwrote its saved frame pointer. --- core/libs/SceTypes.hpp | 36 +++++ .../Execution/include/DisplayBuffer.hpp | 1 + .../Execution/src/DisplayBuffer.cpp | 7 +- core/libs/prx/libSceVideoOut/CMakeLists.txt | 2 +- .../prx/libSceVideoOut/include/Buffer.hpp | 2 + .../libSceVideoOut/include/VideoOutDriver.hpp | 2 + core/libs/prx/libSceVideoOut/src/Buffer.cpp | 132 ++++++++++++++---- core/libs/prx/libSceVideoOut/src/Output.cpp | 16 ++- core/libs/prx/libSceVideoOut/tests/Flip.cpp | 70 ++++++++++ docs/dev/debt/libSceVideoOut.md | 1 + 10 files changed, 234 insertions(+), 35 deletions(-) diff --git a/core/libs/SceTypes.hpp b/core/libs/SceTypes.hpp index 9ddb8391f4..f8883fc12a 100644 --- a/core/libs/SceTypes.hpp +++ b/core/libs/SceTypes.hpp @@ -1943,6 +1943,42 @@ struct VideoOutVblankStatus { std::uint8_t pad1[6] = {}; }; +struct VideoOutBufferAttribute { + std::uint32_t pixel_format; + std::uint32_t tiling_mode; + std::uint32_t aspect_ratio; + std::uint32_t width; + std::uint32_t height; + std::uint32_t pitch_in_pixel; + std::uint32_t option; + std::uint32_t reserved0; + std::uint64_t reserved1; +}; +static_assert(sizeof(VideoOutBufferAttribute) == 40); + +struct VideoOutFlipStatusPs4 { + std::uint64_t count = 0; + std::uint64_t processTime = 0; + std::uint64_t tsc = 0; + std::int64_t flipArg = 0; + std::uint64_t submitTsc = 0; + std::uint64_t reserved0 = 0; + std::int32_t gcQueueNum = 0; + std::int32_t flipPendingNum = 0; + std::int32_t currentBuffer = 0; + std::uint32_t reserved1 = 0; +}; +static_assert(sizeof(VideoOutFlipStatusPs4) == 64); + +struct VideoOutVblankStatusPs4 { + std::uint64_t count = 0; + std::uint64_t processTime = 0; + std::uint64_t tsc = 0; + std::uint8_t flags = 0; + std::uint8_t pad[7] = {}; +}; +static_assert(sizeof(VideoOutVblankStatusPs4) == 32); + struct VideoOutOutputStatus { std::uint32_t resolution = 0; std::uint32_t dynamicRange = 0; diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/DisplayBuffer.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/DisplayBuffer.hpp index d43ee2e18a..685976d255 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/DisplayBuffer.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/DisplayBuffer.hpp @@ -22,6 +22,7 @@ struct DisplayBuffer { std::uint32_t pitchInPixel = 0; std::uint64_t dccAddress = 0; std::uint64_t dccClearColor = 0; + std::uint32_t alignment = 65536; }; std::size_t DisplayBufferSize(const DisplayBuffer& buffer); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/DisplayBuffer.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/DisplayBuffer.cpp index e540fe0ad9..6f7dd42ade 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/DisplayBuffer.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/DisplayBuffer.cpp @@ -130,7 +130,8 @@ std::size_t DisplayBufferSize(const DisplayBuffer& buffer) { throw std::runtime_error(message); } require(!halfFloat || buffer.dccAddress == 0, "VideoOut: DCC metadata on a 16-bit float display buffer is not implemented"); - require(buffer.address != 0 && (buffer.address & 65535u) == 0, "VideoOut: display buffer requires 64 KiB alignment"); + require(buffer.alignment == 65536u || (buffer.alignment == 4096u && buffer.tilingMode == 1), "VideoOut: 4 KiB display buffer alignment is only supported for linear buffers"); + require(buffer.address != 0 && (buffer.address & (buffer.alignment - 1u)) == 0, buffer.alignment == 65536u ? "VideoOut: display buffer requires 64 KiB alignment" : "VideoOut: display buffer requires 4 KiB alignment"); require(buffer.tilingMode <= 1, "VideoOut: unsupported display tiling mode"); require(buffer.tilingMode != 0 || buffer.pitchInPixel == 0, "VideoOut: tiled display pitch is unsupported"); require(buffer.dccAddress == 0 || buffer.tilingMode == 0, "VideoOut: a linear display buffer cannot carry DCC metadata"); @@ -249,7 +250,7 @@ std::vector ReadDisplayBuffer(const DisplayBuffer& buffer) { std::fprintf(stderr, "[gpu] scanout display buffer 0x%llx %ux%u format 0x%016llx at %.1f s\n", static_cast(buffer.address), buffer.width, buffer.height, static_cast(buffer.pixelFormat), std::chrono::duration(std::chrono::steady_clock::now() - start).count()); } GuestMemory::FlushGpuWrites(buffer.address, size); - GuestMemory::CheckRange(reinterpret_cast(buffer.address), size, 65536); + GuestMemory::CheckRange(reinterpret_cast(buffer.address), size, buffer.alignment); const auto* source = reinterpret_cast(buffer.address); return DecodeDisplayBuffer(buffer, {source, size}); } @@ -267,7 +268,7 @@ std::vector DisplayBufferTexels(const DisplayBuffer& buffer, std::spa std::vector ReadDisplayBufferTexels(const DisplayBuffer& buffer) { const auto size = DisplayBufferSize(buffer); GuestMemory::FlushGpuWrites(buffer.address, size); - GuestMemory::CheckRange(reinterpret_cast(buffer.address), size, 65536); + GuestMemory::CheckRange(reinterpret_cast(buffer.address), size, buffer.alignment); return DisplayBufferTexels(buffer, {reinterpret_cast(buffer.address), size}); } diff --git a/core/libs/prx/libSceVideoOut/CMakeLists.txt b/core/libs/prx/libSceVideoOut/CMakeLists.txt index e216b43fb4..49cce22aee 100644 --- a/core/libs/prx/libSceVideoOut/CMakeLists.txt +++ b/core/libs/prx/libSceVideoOut/CMakeLists.txt @@ -84,7 +84,7 @@ if(BUILD_TESTING) endforeach() set_target_properties(video_out_flip_tests PROPERTIES EXCLUDE_FROM_ALL OFF) - foreach(flipMode IN ITEMS lifetime reopen decode controls overlay unopened) + foreach(flipMode IN ITEMS lifetime reopen decode controls overlay unopened ps4) if(flipMode STREQUAL "lifetime") add_test(NAME video_out_flip_${flipMode} COMMAND video_out_flip_tests) else() diff --git a/core/libs/prx/libSceVideoOut/include/Buffer.hpp b/core/libs/prx/libSceVideoOut/include/Buffer.hpp index 633ecc5c8e..8fb708400b 100644 --- a/core/libs/prx/libSceVideoOut/include/Buffer.hpp +++ b/core/libs/prx/libSceVideoOut/include/Buffer.hpp @@ -8,6 +8,8 @@ extern "C" { +int APS5_VABI sceVideoOutRegisterBuffers(int handle, int buffer_index_start, void* const* addresses, int buffer_num, const VideoOutBufferAttribute* attribute); +void APS5_VABI sceVideoOutSetBufferAttribute(VideoOutBufferAttribute* attribute, uint32_t pixel_format, uint32_t tiling_mode, uint32_t aspect_ratio, uint32_t width, uint32_t height, uint32_t pitch_in_pixel); int APS5_VABI sceVideoOutRegisterBuffers2(int handle, int set_index, int buffer_index_start, const VideoOutBuffers* buffers, int buffer_num, const VideoOutBufferAttribute2* attribute, int category, void* option); void APS5_VABI sceVideoOutSetBufferAttribute2(VideoOutBufferAttribute2* attribute, uint64_t pixel_format, uint32_t tiling_mode, uint32_t width, uint32_t height, uint64_t option, uint32_t dcc_control, uint64_t dcc_cb_register_clear_color); void APS5_VABI sceVideoOutAddBufferAttributeOption(VideoOutBufferAttribute2* attribute, uint64_t option); diff --git a/core/libs/prx/libSceVideoOut/include/VideoOutDriver.hpp b/core/libs/prx/libSceVideoOut/include/VideoOutDriver.hpp index ecf9c559b2..069a00c174 100644 --- a/core/libs/prx/libSceVideoOut/include/VideoOutDriver.hpp +++ b/core/libs/prx/libSceVideoOut/include/VideoOutDriver.hpp @@ -107,6 +107,7 @@ struct BufferAttributeGroup { VideoOutBufferAttribute2 attribute{}; int category = VIDEO_OUT_BUFFER_ATTRIBUTE_CATEGORY_UNCOMPRESSED; bool occupied = false; + bool ps4 = false; }; AgcDriver::DisplayBuffer DescribeVideoOutBuffer(const VideoOutBuffer& buffer, const BufferAttributeGroup& group); @@ -132,6 +133,7 @@ struct VideoOutConfig { int busType = VIDEO_OUT_BUS_TYPE_MAIN; bool opened = false; bool closing = false; + bool ps4 = false; std::exception_ptr failure; std::stop_token shutdownToken = LibcShutdownToken_nid_postfix(); int flipRate = 0; diff --git a/core/libs/prx/libSceVideoOut/src/Buffer.cpp b/core/libs/prx/libSceVideoOut/src/Buffer.cpp index 4a80aa47d4..81722d188e 100644 --- a/core/libs/prx/libSceVideoOut/src/Buffer.cpp +++ b/core/libs/prx/libSceVideoOut/src/Buffer.cpp @@ -24,11 +24,62 @@ AgcDriver::DisplayBuffer DescribeVideoOutBuffer(const VideoOutBuffer& buffer, co if (compressed && buffer.metadataAddress == 0) throw std::runtime_error("VideoOut: DCC-compressed buffer " + hex(buffer.dataAddress) + " without DCC metadata"); if (compressed && attribute.tiling_mode != 0) throw std::runtime_error("VideoOut: DCC-compressed buffer " + hex(buffer.dataAddress) + " with linear tiling"); if (compressed && (attribute.dcc_control & ~VIDEO_OUT_DCC_CONTROL_BLOCK_LAYOUT) != 0) throw std::runtime_error("VideoOut: unsupported DCC control " + hex(attribute.dcc_control) + " (only the block size and independence fields " + hex(VIDEO_OUT_DCC_CONTROL_BLOCK_LAYOUT) + " are known)"); - const AgcDriver::DisplayBuffer result{buffer.dataAddress, attribute.pixel_format, attribute.width, attribute.height, attribute.tiling_mode, attribute.pitch_in_pixel, compressed ? buffer.metadataAddress : 0, compressed ? attribute.dcc_cb_register_clear_color : 0}; + AgcDriver::DisplayBuffer result{buffer.dataAddress, attribute.pixel_format, attribute.width, attribute.height, attribute.tiling_mode, attribute.pitch_in_pixel, compressed ? buffer.metadataAddress : 0, compressed ? attribute.dcc_cb_register_clear_color : 0}; + if (group.ps4) result.alignment = 4096; static_cast(AgcDriverDisplayBufferSize_nid_postfix(result)); return result; } +namespace { + +int CommitBufferGroup(const std::string& function, VideoOutConfig& cfg, int setIndex, int bufferIndexStart, const VideoOutBuffers* buffers, int bufferNum, const BufferAttributeGroup& group) { + std::unique_lock lock(cfg.mutex); + cfg.Check(); + if (cfg.closing) { + throw std::runtime_error(function + ": VIDEO_OUT_ERROR_INVALID_HANDLE"); + } + if (setIndex < 0) { + for (int index = 0; index < VIDEO_OUT_BUFFER_ATTRIBUTE_NUM_MAX && setIndex < 0; ++index) + if (!cfg.groups[index].occupied) setIndex = index; + if (setIndex < 0) { + throw std::runtime_error(function + ": VIDEO_OUT_ERROR_NO_EMPTY_SLOT"); + } + } else if (cfg.groups[setIndex].occupied) { + throw std::runtime_error(function + ": VIDEO_OUT_ERROR_INVALID_INDEX"); + } + for (int i = 0; i < bufferNum; i++) { + if (cfg.buffers[bufferIndexStart + i].Occupied()) { + throw std::runtime_error(function + ": VIDEO_OUT_ERROR_SLOT_OCCUPIED"); + } + } + std::array registered{}; + for (int i = 0; i < bufferNum; ++i) { + registered[i] = {setIndex, reinterpret_cast(buffers[i].data), reinterpret_cast(buffers[i].metadata)}; + const auto display = DescribeVideoOutBuffer(registered[i], group); + AgcDriverCheckGuestMemory_nid_postfix(buffers[i].data, AgcDriverDisplayBufferSize_nid_postfix(display), display.alignment); + } + cfg.groups[setIndex] = group; + cfg.width = group.attribute.width; + cfg.height = group.attribute.height; + for (int i = 0; i < bufferNum; i++) { + cfg.buffers[bufferIndexStart + i] = registered[i]; + auto& label = cfg.bufferLabels[bufferIndexStart + i]; + AgcDriverStoreGuestLabel_nid_postfix(&label, 0); + } + if (group.ps4) cfg.ps4 = true; + return setIndex; +} + +uint64_t Ps5PixelFormat(uint32_t pixelFormat) { + if (pixelFormat == 0x80000000u) return 0x8000000000000000ull; + if (pixelFormat == 0x80002200u) return 0x8000000022000000ull; + char text[96]; + std::snprintf(text, sizeof(text), "sceVideoOutRegisterBuffers: unsupported PS4 pixel format 0x%08x", pixelFormat); + throw std::runtime_error(text); +} + +} + extern "C" { int APS5_VABI sceVideoOutGetBufferLabelAddress(int handle, uintptr_t* address) try { @@ -91,42 +142,65 @@ int APS5_VABI sceVideoOutRegisterBuffers2(int handle, int setIndex, int bufferIn } AgcDriverCheckGuestMemory_nid_postfix(attribute, sizeof(*attribute), alignof(VideoOutBufferAttribute2)); AgcDriverCheckGuestMemory_nid_postfix(buffers, static_cast(bufferNum) * sizeof(*buffers), alignof(VideoOutBuffers)); - std::unique_lock lock(cfg->mutex); - cfg->Check(); - if (cfg->closing) { - throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_HANDLE"); - } - if (cfg->groups[setIndex].occupied) { - throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_INDEX"); - } - for (int i = 0; i < bufferNum; i++) { - if (cfg->buffers[bufferIndexStart + i].Occupied()) { - throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_SLOT_OCCUPIED"); - } - } BufferAttributeGroup group{}; group.attribute = *attribute; group.category = category; group.occupied = true; - std::array registered{}; - for (int i = 0; i < bufferNum; ++i) { - registered[i] = {setIndex, reinterpret_cast(buffers[i].data), reinterpret_cast(buffers[i].metadata)}; - const auto display = DescribeVideoOutBuffer(registered[i], group); - AgcDriverCheckGuestMemory_nid_postfix(buffers[i].data, AgcDriverDisplayBufferSize_nid_postfix(display), 65536); - } - cfg->groups[setIndex] = group; - cfg->width = attribute->width; - cfg->height = attribute->height; - for (int i = 0; i < bufferNum; i++) { - cfg->buffers[bufferIndexStart + i] = registered[i]; - auto& label = cfg->bufferLabels[bufferIndexStart + i]; - AgcDriverStoreGuestLabel_nid_postfix(&label, 0); - } + CommitBufferGroup(__func__, *cfg, setIndex, bufferIndexStart, buffers, bufferNum, group); return 0; } catch (const ProcessShutdown&) { LibcAwaitExit_nid_postfix(); } +void APS5_VABI sceVideoOutSetBufferAttribute(VideoOutBufferAttribute* attribute, uint32_t pixelFormat, uint32_t tilingMode, uint32_t aspectRatio, uint32_t width, uint32_t height, uint32_t pitchInPixel) { + if (attribute == nullptr) { + throw std::runtime_error("sceVideoOutSetBufferAttribute: null attribute"); + } + std::memset(attribute, 0, sizeof(VideoOutBufferAttribute)); + attribute->pixel_format = pixelFormat; + attribute->tiling_mode = tilingMode; + attribute->aspect_ratio = aspectRatio; + attribute->width = width; + attribute->height = height; + attribute->pitch_in_pixel = pitchInPixel; +} + +int APS5_VABI sceVideoOutRegisterBuffers(int handle, int bufferIndexStart, void* const* addresses, int bufferNum, const VideoOutBufferAttribute* attribute) try { + auto cfg = VideoOutDriver::Get().GetConfig(handle); + if (cfg == nullptr) { + throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_HANDLE"); + } + if (addresses == nullptr || attribute == nullptr) { + throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_ADDRESS"); + } + if (bufferIndexStart < 0 || bufferIndexStart >= VIDEO_OUT_BUFFER_NUM_MAX || bufferNum < 1 || bufferNum > VIDEO_OUT_BUFFER_NUM_MAX || bufferIndexStart + bufferNum > VIDEO_OUT_BUFFER_NUM_MAX) { + throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_VALUE"); + } + AgcDriverCheckGuestMemory_nid_postfix(attribute, sizeof(*attribute), alignof(VideoOutBufferAttribute)); + AgcDriverCheckGuestMemory_nid_postfix(addresses, static_cast(bufferNum) * sizeof(*addresses), alignof(void*)); + if (attribute->option != 0 || attribute->reserved0 != 0 || attribute->reserved1 != 0) { + throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_OPTION"); + } + if (attribute->tiling_mode != 1) { + throw std::runtime_error(std::string(__func__) + ": PS4 tiled display buffers are not implemented"); + } + BufferAttributeGroup group{}; + group.attribute.pixel_format = Ps5PixelFormat(attribute->pixel_format); + group.attribute.tiling_mode = attribute->tiling_mode; + group.attribute.aspect_ratio = attribute->aspect_ratio; + group.attribute.width = attribute->width; + group.attribute.height = attribute->height; + group.attribute.pitch_in_pixel = attribute->pitch_in_pixel; + group.category = VIDEO_OUT_BUFFER_ATTRIBUTE_CATEGORY_UNCOMPRESSED; + group.occupied = true; + group.ps4 = true; + std::array buffers{}; + for (int i = 0; i < bufferNum; ++i) buffers[i].data = addresses[i]; + return CommitBufferGroup(__func__, *cfg, -1, bufferIndexStart, buffers.data(), bufferNum, group); +} catch (const ProcessShutdown&) { + LibcAwaitExit_nid_postfix(); +} + int APS5_VABI sceVideoOutSubmitChangeBufferAttribute2(int handle, int setIndex, const VideoOutBufferAttribute2* attribute, void* option) try { auto cfg = VideoOutDriver::Get().GetConfig(handle); if (cfg == nullptr) { @@ -152,7 +226,7 @@ int APS5_VABI sceVideoOutSubmitChangeBufferAttribute2(int handle, int setIndex, for (int i = 0; i < VIDEO_OUT_BUFFER_NUM_MAX; ++i) { if (cfg->buffers[i].groupIndex != setIndex) continue; const auto display = DescribeVideoOutBuffer(cfg->buffers[i], group); - AgcDriverCheckGuestMemory_nid_postfix(reinterpret_cast(display.address), AgcDriverDisplayBufferSize_nid_postfix(display), 65536); + AgcDriverCheckGuestMemory_nid_postfix(reinterpret_cast(display.address), AgcDriverDisplayBufferSize_nid_postfix(display), display.alignment); } cfg->groups[setIndex] = group; return 0; diff --git a/core/libs/prx/libSceVideoOut/src/Output.cpp b/core/libs/prx/libSceVideoOut/src/Output.cpp index 688a12f47d..0779ca1bc7 100644 --- a/core/libs/prx/libSceVideoOut/src/Output.cpp +++ b/core/libs/prx/libSceVideoOut/src/Output.cpp @@ -138,7 +138,13 @@ int APS5_VABI sceVideoOutGetFlipStatus(int handle, VideoOutFlipStatus* status) t } std::unique_lock lock(cfg->mutex); cfg->Check(); - *status = cfg->flipStatus; + if (cfg->ps4) { + const auto& current = cfg->flipStatus; + VideoOutFlipStatusPs4 legacy{current.count, current.processTime, 0, current.flipArg, 0, 0, current.gcQueueNum, current.flipPendingNum, current.currentBuffer, 0}; + *reinterpret_cast(status) = legacy; + } else { + *status = cfg->flipStatus; + } return 0; } catch (const ProcessShutdown&) { LibcAwaitExit_nid_postfix(); @@ -154,7 +160,13 @@ int APS5_VABI sceVideoOutGetVblankStatus(int handle, VideoOutVblankStatus* statu } std::unique_lock lock(cfg->mutex); cfg->Check(); - *status = cfg->vblankStatus; + if (cfg->ps4) { + const auto& current = cfg->vblankStatus; + VideoOutVblankStatusPs4 legacy{current.count, current.processTime, 0, current.flags}; + *reinterpret_cast(status) = legacy; + } else { + *status = cfg->vblankStatus; + } return 0; } catch (const ProcessShutdown&) { LibcAwaitExit_nid_postfix(); diff --git a/core/libs/prx/libSceVideoOut/tests/Flip.cpp b/core/libs/prx/libSceVideoOut/tests/Flip.cpp index b85d4c9876..6e61a71594 100644 --- a/core/libs/prx/libSceVideoOut/tests/Flip.cpp +++ b/core/libs/prx/libSceVideoOut/tests/Flip.cpp @@ -124,6 +124,75 @@ void testLifetime(bool reopen) { LibcRunShutdown_nid_postfix(); } +void testPs4Buffers() { + const int handle = sceVideoOutOpen(255, 0, 0, nullptr); + std::vector allocation(2 * 65536 + 65535); + const auto storage = alignedBuffer(allocation); + VideoOutBufferAttribute attribute{}; + sceVideoOutSetBufferAttribute(&attribute, 0x80000000u, 1, 0, 64, 64, 64); + check(attribute.pixel_format == 0x80000000u && attribute.tiling_mode == 1 && attribute.aspect_ratio == 0 && attribute.width == 64 && attribute.height == 64 && attribute.pitch_in_pixel == 64 && attribute.option == 0 && attribute.reserved0 == 0 && attribute.reserved1 == 0, "PS4 buffer attribute fields are wrong"); + void* const pair[2] = {storage.data(), storage.data() + 20480}; + VideoOutBuffers second{pair[1], nullptr, {nullptr, nullptr}}; + VideoOutBufferAttribute2 ps5{}; + ps5.width = 64; + ps5.height = 64; + ps5.tiling_mode = 1; + ps5.pitch_in_pixel = 64; + ps5.pixel_format = 0x8000000000000000ull; + check(expectFailure([&] { sceVideoOutRegisterBuffers2(handle, 3, 5, &second, 1, &ps5, 0, nullptr); }).find("64 KiB alignment") != std::string::npos, "a PS5 buffer was accepted at 4 KiB alignment"); + check(sceVideoOutRegisterBuffers(handle, 0, pair, 2, &attribute) == 0, "the first PS4 set index is not 0"); + void* const third[1] = {storage.data() + 49152}; + check(sceVideoOutRegisterBuffers(handle, 2, third, 1, &attribute) == 1, "the second PS4 set index is not 1"); + { + const auto cfg = VideoOutDriver::Get().GetConfig(handle); + std::lock_guard lock(cfg->mutex); + check(cfg->groups[0].attribute.pixel_format == 0x8000000000000000ull && cfg->buffers[1].dataAddress == reinterpret_cast(pair[1]) && cfg->buffers[2].groupIndex == 1, "PS4 buffers were registered wrongly"); + } + VideoOutBufferAttribute2 tiledChange{}; + tiledChange.width = 64; + tiledChange.height = 64; + tiledChange.pixel_format = 0x8000000000000000ull; + check(expectFailure([&] { sceVideoOutSubmitChangeBufferAttribute2(handle, 0, &tiledChange, nullptr); }).find("only supported for linear buffers") != std::string::npos, "a PS4 buffer set was changed to tiled"); + void* const odd[1] = {storage.data() + 65536 + 256}; + check(expectFailure([&] { sceVideoOutRegisterBuffers(handle, 3, odd, 1, &attribute); }).find("4 KiB alignment") != std::string::npos, "a PS4 buffer was accepted below 4 KiB alignment"); + void* const fourth[1] = {storage.data() + 65536}; + auto tiled = attribute; + tiled.tiling_mode = 0; + tiled.pitch_in_pixel = 0; + check(expectFailure([&] { sceVideoOutRegisterBuffers(handle, 3, fourth, 1, &tiled); }).find("tiled display buffers are not implemented") != std::string::npos, "a PS4 tiled buffer was accepted"); + auto tenBit = attribute; + tenBit.pixel_format = 0x88060000u; + check(expectFailure([&] { sceVideoOutRegisterBuffers(handle, 3, fourth, 1, &tenBit); }).find("unsupported PS4 pixel format 0x88060000") != std::string::npos, "an unknown PS4 pixel format was accepted"); + auto withOption = attribute; + withOption.option = 1; + check(expectFailure([&] { sceVideoOutRegisterBuffers(handle, 3, fourth, 1, &withOption); }).find("VIDEO_OUT_ERROR_INVALID_OPTION") != std::string::npos, "a PS4 buffer option was accepted"); + std::array raw{}; + raw.fill(0xaa); + check(sceVideoOutGetFlipStatus(handle, reinterpret_cast(raw.data())) == 0, "the PS4 flip status failed"); + check(std::all_of(raw.begin() + sizeof(VideoOutFlipStatusPs4), raw.end(), [](std::uint8_t value) { return value == 0xaa; }), "the PS4 flip status overflowed its 64 bytes"); + VideoOutFlipStatusPs4 flip; + std::memcpy(&flip, raw.data(), sizeof(flip)); + check(flip.count == 0 && flip.flipArg == -1 && flip.currentBuffer == -1 && flip.flipPendingNum == 0, "the PS4 flip status fields are wrong"); + raw.fill(0xaa); + check(sceVideoOutGetVblankStatus(handle, reinterpret_cast(raw.data())) == 0, "the PS4 vblank status failed"); + check(std::all_of(raw.begin() + sizeof(VideoOutVblankStatusPs4), raw.end(), [](std::uint8_t value) { return value == 0xaa; }), "the PS4 vblank status overflowed its 32 bytes"); + { + const auto cfg = VideoOutDriver::Get().GetConfig(handle); + check(sceVideoOutSubmitFlip(handle, 1, 1, 7) == 0, "the PS4 flip was not accepted"); + std::unique_lock lock(cfg->mutex); + check(cfg->vblankCond.wait_for(lock, std::chrono::seconds(15), [&] { return (cfg->failure && cfg->flipStatus.flipPendingNum == 0) || cfg->flipStatus.count == 1; }), "the PS4 flip did not complete"); + if (cfg->failure) std::rethrow_exception(cfg->failure); + check(cfg->flipStatus.flipArg == 7 && cfg->flipStatus.currentBuffer == 1, "the PS4 flip status is wrong"); + } + sceVideoOutClose(handle); + const int ps5Handle = sceVideoOutOpen(255, 0, 0, nullptr); + raw.fill(0xaa); + check(sceVideoOutGetFlipStatus(ps5Handle, reinterpret_cast(raw.data())) == 0, "the PS5 flip status failed"); + check(std::all_of(raw.begin() + sizeof(VideoOutFlipStatusPs4), raw.end(), [](std::uint8_t value) { return value == 0; }), "the PS5 flip status did not fill its 128 bytes"); + sceVideoOutClose(ps5Handle); + LibcRunShutdown_nid_postfix(); +} + void testDeferredBuffer() { const int handle = sceVideoOutOpen(255, 0, 0, nullptr); auto cfg = VideoOutDriver::Get().GetConfig(handle); @@ -902,6 +971,7 @@ int main(int argc, char** argv) { else if (argc == 2 && std::string(argv[1]) == "overlay") testOverlay(); else if (argc == 2 && std::string(argv[1]) == "deferred") testDeferredBuffer(); else if (argc == 2 && std::string(argv[1]) == "unregistered") testUnregisteredBuffer(); + else if (argc == 2 && std::string(argv[1]) == "ps4") testPs4Buffers(); else testLifetime(argc == 2 && std::string(argv[1]) == "reopen"); std::puts("VideoOut flip tests passed"); return 0; diff --git a/docs/dev/debt/libSceVideoOut.md b/docs/dev/debt/libSceVideoOut.md index 248855971a..b77edf30eb 100644 --- a/docs/dev/debt/libSceVideoOut.md +++ b/docs/dev/debt/libSceVideoOut.md @@ -19,6 +19,7 @@ - [sceVideoOutGetBufferLabelAddress](../../../core/libs/prx/libSceVideoOut/src/Buffer.cpp) exposes a port-owned contiguous array of 16 aligned 64-bit labels and returns its count. Astro Bot PPSA21564's bundled AGC driver passes a pointer output and addresses each label at `base + index * 8`. The count, zero initialization on registration, and release of the previous displayed buffer when its replacement completes follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/23c41333822fe8eca4c609b43119180af63ae4ac/src/core/libraries/videoout/video_out.cpp) and its driver; PS5 console equivalence has not been measured. Labels live until the port closes, and reopening creates a new array. Error precedence follows the reference for null output and the existing open-port policy for handles. Exact scanout release timing, repeated same-buffer flips and concurrent guest label stores remain unverified. EOP-flip registration and interrupt tokens remain unimplemented; exposing these labels does not implement that separate API. - [GPU flips](../../../core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp) (libSceVideoOut) - a command-buffer flip of a buffer index with no registered buffer when the GPU reaches it presents black and completes like any flip (flip event, count, `flipPendingNum`). The console's behaviour is unknown; PPSA05684 requires that it completes: its renderer submits a flip of buffer 0 while another thread waits for `sceVideoOutIsFlipPending` to reach 0 before registering the buffers, and `flipPendingNum` counts that flip from its submission as in shadPS4 - [sceVideoOutAddPreVblankStartEvent](../../../core/libs/prx/libSceVideoOut/src/Event.cpp) (libSceVideoOut) - how long before the vblank the console signals pre-vblank start is unknown. The event is triggered on each emulated vblank tick immediately before the vblank event, with its own count as the event data +- [sceVideoOutRegisterBuffers, sceVideoOutGetFlipStatus, sceVideoOutGetVblankStatus](../../../core/libs/prx/libSceVideoOut/src/Buffer.cpp) (libSceVideoOut) - PS4 callers. A handle is treated as PS4 once buffers are registered through the PS4 `sceVideoOutRegisterBuffers`; from then on its flip and vblank status are written in the PS4 64- and 32-byte layouts, with `tsc` and `submitTsc` 0. A PS4 title that reads a status before it registers buffers gets the larger PS5 layout. The PS4 function accepts only linear A8R8G8B8_SRGB (0x80000000) and A8B8G8R8_SRGB (0x80002200) buffers without options; tiled PS4 buffers throw, as their layout differs from the PS5 tiling. Linear PS4 buffers may be 4 KiB aligned: the OpenOrbis graphics sample registers its second 1920x1080 buffer 8,294,400 bytes after the first. The PS4 alignment rule is not known ### Functional From f12d2de271a58b84c4c3aa13bf742f972a395e46 Mon Sep 17 00:00:00 2001 From: Get-fix <100056769+Get-fix@users.noreply.github.com> Date: Sun, 11 Oct 2026 17:46:31 +0300 Subject: [PATCH 0113/1000] fix(libkernel): export _readv, _writev, _fcntl, _fstat, __sys_socketex OpenOrbis programs and modules import these PS4 libkernel names (the graphics, library and networking samples). The underscore names forward to readv, writev, fcntl and fstat, as _ioctl already does; _fcntl passes its argument on as a full 64-bit value, so the record lock commands keep their whole pointer. __sys_socketex creates a socket like socket(); its name argument is ignored, as sceNetSocket does. --- core/libs/prx/libkernel/File/src/Stdio.cpp | 12 ++++++++++++ core/libs/prx/libkernel/Socket/src/Udp.cpp | 18 ++++++++++++++++++ core/libs/tests/GuestFileTimestamps.cpp | 6 ++++-- core/libs/tests/GuestFileVector.cpp | 7 +++++++ core/libs/tests/GuestKernelFcntl.cpp | 8 ++++++++ 5 files changed, 49 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index 58f00fafaa..3e4b1da3b2 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -729,6 +729,10 @@ int64_t APS5_VABI fstat_nid_disambig1_nid_postfix(int d, FileStat* sb) { return 0; } +int64_t APS5_VABI _fstat_nid_postfix(int d, FileStat* sb) { + return fstat_nid_disambig1_nid_postfix(d, sb); +} + int APS5_VABI ftruncate_nid_postfix(int d, int64_t length) { if (length < 0 || GuestSockets::IsOpen(d)) { return PosixFailure(GUEST_EINVAL); @@ -1342,4 +1346,12 @@ int64_t APS5_VABI writev_nid_postfix(int d, const KernelIovec* iov, int iovcnt) return result < 0 ? PosixFailure(static_cast(result) & 0xffff) : result; } +int64_t APS5_VABI _readv_nid_postfix(int d, const KernelIovec* iov, int iovcnt) { + return readv_nid_postfix(d, iov, iovcnt); +} + +int64_t APS5_VABI _writev_nid_postfix(int d, const KernelIovec* iov, int iovcnt) { + return writev_nid_postfix(d, iov, iovcnt); +} + } diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index 435d75c4d7..28771eb8c2 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -789,6 +789,20 @@ int APS5_VABI fcntl_nid_postfix(int descriptor, int command, ...) { socket->nonblocking = enabled != 0; return 0; } +int APS5_VABI _fcntl_nid_postfix(int descriptor, int command, ...) { +#ifdef _WIN32 + __builtin_sysv_va_list arguments; + __builtin_sysv_va_start(arguments, command); + const auto argument = __builtin_va_arg(arguments, std::uint64_t); + __builtin_sysv_va_end(arguments); +#else + std::va_list arguments; + va_start(arguments, command); + const auto argument = va_arg(arguments, std::uint64_t); + va_end(arguments); +#endif + return fcntl_nid_postfix(descriptor, command, argument); +} int APS5_VABI setsockopt_nid_postfix(int descriptor, int level, int option, const void* value, std::uint32_t length) { int netError = 0; @@ -838,6 +852,10 @@ int APS5_VABI socket_nid_postfix(int family, int type, int protocol) { return Fail(12); } } +int APS5_VABI __sys_socketex_nid_postfix(const char* name, int family, int type, int protocol) { + static_cast(name); + return socket_nid_postfix(family, type, protocol); +} int APS5_VABI socketpair_nid_postfix(int family, int type, int protocol, int* descriptors) { const bool nonblocking = (type & GuestNonBlocking) != 0; const int plainType = type & ~(GuestCloseOnExec | GuestNonBlocking); diff --git a/core/libs/tests/GuestFileTimestamps.cpp b/core/libs/tests/GuestFileTimestamps.cpp index 13c71a958d..b1e91115f8 100644 --- a/core/libs/tests/GuestFileTimestamps.cpp +++ b/core/libs/tests/GuestFileTimestamps.cpp @@ -19,6 +19,7 @@ int APS5_VABI stat_nid_postfix(const char*, FileStat*); int APS5_VABI lstat_nid_postfix(const char*, FileStat*); int APS5_VABI sceKernelFstat(int, FileStat*); std::int64_t APS5_VABI fstat_nid_disambig1_nid_postfix(int, FileStat*); +std::int64_t APS5_VABI _fstat_nid_postfix(int, FileStat*); int APS5_VABI open_nid_postfix(const char*, int, int); int APS5_VABI close_nid_postfix(int); int APS5_VABI pipe_nid_postfix(int*); @@ -37,11 +38,12 @@ static bool Same(const KernelTimespec& actual, const KernelTimeval& expected) { } static void CheckTimes(const std::string& path, int fd, const KernelTimeval* expected) { - FileStat byPath{}, byFd{}, byKernel{}; + FileStat byPath{}, byFd{}, byKernel{}, byAlias{}; Require(stat_nid_postfix(path.c_str(), &byPath) == 0, "stat"); Require(fstat_nid_disambig1_nid_postfix(fd, &byFd) == 0, "fstat"); Require(sceKernelFstat(fd, &byKernel) == 0, "sceKernelFstat"); - for (const auto* status : {&byPath, &byFd, &byKernel}) { + Require(_fstat_nid_postfix(fd, &byAlias) == 0, "_fstat"); + for (const auto* status : {&byPath, &byFd, &byKernel, &byAlias}) { Require(Same(status->st_atim, expected[0]), "access time preserves microseconds"); Require(Same(status->st_mtim, expected[1]), "modification time preserves microseconds"); } diff --git a/core/libs/tests/GuestFileVector.cpp b/core/libs/tests/GuestFileVector.cpp index 76f392261f..21d6249e87 100644 --- a/core/libs/tests/GuestFileVector.cpp +++ b/core/libs/tests/GuestFileVector.cpp @@ -36,6 +36,8 @@ int APS5_VABI sceKernelLseek(int, std::int64_t, int); std::int64_t APS5_VABI sceKernelReadv(int, const GuestIovec*, int); std::int64_t APS5_VABI readv_nid_postfix(int, const GuestIovec*, int); std::int64_t APS5_VABI writev_nid_postfix(int, const GuestIovec*, int); +std::int64_t APS5_VABI _readv_nid_postfix(int, const GuestIovec*, int); +std::int64_t APS5_VABI _writev_nid_postfix(int, const GuestIovec*, int); int* APS5_VABI __error_nid_postfix(); std::int64_t APS5_VABI sceKernelWritev(int, const GuestIovec*, int); std::int64_t APS5_VABI sceKernelPreadv(int, const GuestIovec*, int, std::int64_t); @@ -94,6 +96,8 @@ int main() { Require(sceKernelLseek(file, 0, 1) == 7); Require(readv_nid_postfix(file, reads, 2) == 3); Require(readv_nid_postfix(file, reads, 2) == 0); + Require(sceKernelLseek(file, 3, 0) == 3 && _readv_nid_postfix(file, reads, 2) == 7); + Require(std::memcmp(first, "345", 3) == 0 && std::memcmp(second, "6789", 4) == 0); Require(readv_nid_postfix(file, reads, -1) == -1 && *__error_nid_postfix() == 22); Require(writev_nid_postfix(file, nullptr, 1) == -1 && *__error_nid_postfix() == 14); @@ -118,6 +122,9 @@ int main() { Require(sceKernelLseek(file, 0, 1) == 13 && Contents(path) == "01ABCDE7CDEAB"); Require(sceKernelLseek(file, 8, 0) == 8 && writev_nid_postfix(file, writes, 2) == 5); Require(Contents(path) == "01ABCDE7ABCDE"); + Require(sceKernelLseek(file, 8, 0) == 8 && _writev_nid_postfix(file, swapped, 2) == 5); + Require(sceKernelLseek(file, 0, 1) == 13 && Contents(path) == "01ABCDE7CDEAB"); + Require(sceKernelLseek(file, 8, 0) == 8 && writev_nid_postfix(file, writes, 2) == 5); GuestIovec gapped[3] = {{first, 2}, {nullptr, 0}, {second, 3}}; Require(sceKernelPreadv(file, gapped, 3, 0) == 5); diff --git a/core/libs/tests/GuestKernelFcntl.cpp b/core/libs/tests/GuestKernelFcntl.cpp index 679a739b64..d53e717eaf 100644 --- a/core/libs/tests/GuestKernelFcntl.cpp +++ b/core/libs/tests/GuestKernelFcntl.cpp @@ -11,9 +11,11 @@ #include extern "C" { int APS5_VABI socket_nid_postfix(int, int, int); +int APS5_VABI __sys_socketex_nid_postfix(const char*, int, int, int); int APS5_VABI close_nid_postfix(int); int APS5_VABI pipe_nid_postfix(int*); int APS5_VABI fcntl_nid_postfix(int, int, ...); +int APS5_VABI _fcntl_nid_postfix(int, int, ...); int APS5_VABI ioctl_nid_postfix(int, std::uint64_t, void*); std::int64_t APS5_VABI read_nid_postfix(int, void*, std::uint64_t); std::int64_t APS5_VABI write_nid_postfix(int, const char*, std::int64_t); @@ -110,6 +112,8 @@ static void SocketsKeepTheirPath() { Require(sceKernelFcntl(socket, GetFlags) == SceEbadf); Require(sceKernelFcntl(-1, GetFlags) == SceEbadf); Require(sceKernelFcntl(12345, GetFlags) == SceEbadf); + const int named = __sys_socketex_nid_postfix("probe", 2, 1, 0); + Require(named >= 0 && sceKernelFcntl(named, GetFlags) == ReadWrite && close_nid_postfix(named) == 0); } static void FileFlagsRoundTrip() { @@ -166,6 +170,10 @@ static void FileFlagsRoundTrip() { Require(fcntl_nid_postfix(file, SetLock, &lock) == 0); GuestLock probe{0, 0, 0, 1, 0, 0}; Require(fcntl_nid_postfix(file, GetLock, &probe) == 0 && probe.type == 2); + static GuestLock imageProbe{0, 0, 0, 1, 0, 0}; + Require(_fcntl_nid_postfix(file, GetLock, &imageProbe) == 0 && imageProbe.type == 2); + Require(_fcntl_nid_postfix(file, SetDescriptorFlags, 1) == 0 && _fcntl_nid_postfix(file, GetDescriptorFlags) == 1); + Require(_fcntl_nid_postfix(file, SetDescriptorFlags, 0) == 0); lock.type = 2; Require(fcntl_nid_postfix(file, SetLock, &lock) == 0); lock.type = 7; From 8997cac1b840998182bfc6afc32f70c91f5f3d56 Mon Sep 17 00:00:00 2001 From: Get-fix <100056769+Get-fix@users.noreply.github.com> Date: Wed, 14 Oct 2026 18:33:34 +0300 Subject: [PATCH 0114/1000] fix(relinker): stop module syscall scans at the PLT and skip the PS4 loader path OpenOrbis modules keep read-only data after their code, as executables do. The syscall scan of sce_module modules decoded it, and the module of the OpenOrbis library sample (libExample.prx) failed with "Forbidden syscall instruction". For modules with a PT_SCE_DYNLIBDATA segment, the scan now stops at the end of the PLT after the last unwind function, found by FindSceCodeEnd as for executables. The same modules begin their executable segment with the /libexec/ld-elf.so.1 string but have no PT_INTERP header, so code analysis decoded the string and failed with "Unsupported Windows guest TLS instruction (bytes: 64 2d 65 6c 66 2e)", which is the text "d-elf.". When no PT_INTERP header covers the start of a PS4 code segment, code analysis now starts after that exact string. --- core/relinker/CMakeLists.txt | 2 + .../src/analysis/CodeInstructionCollector.cpp | 3 + .../relinker/src/guest/GuestModuleBuilder.cpp | 11 +- .../tests/test_sce_module_code_extent.py | 160 ++++++++++++++++++ docs/dev/debt/relinker.md | 3 +- 5 files changed, 176 insertions(+), 3 deletions(-) create mode 100644 core/relinker/relinker/tests/test_sce_module_code_extent.py diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index f8fd8039fe..477d4f9109 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -568,6 +568,8 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_sce_code_extent.py $) add_test(NAME sce_relro COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_sce_relro.py $) + add_test(NAME sce_module_code_extent COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_sce_module_code_extent.py $) add_test(NAME call_registry COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_call_registry.py $) add_test(NAME jmprel_table_bounds COMMAND ${Python3_EXECUTABLE} diff --git a/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp b/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp index c5ffda7b73..0d4a06db1c 100644 --- a/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp +++ b/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include namespace Relinker { @@ -103,6 +104,8 @@ std::set CodeInstructionCollector::Collect(const std::ve auto begin = header.MappedAddress; for (const auto& interpreter : headers) if (interpreter.Type == 3 && interpreter.MappedAddress >= begin && interpreter.MappedAddress < end) begin = interpreter.MappedAddress + std::min(interpreter.FileSize, end - interpreter.MappedAddress); + const std::string_view loaderPath("/libexec/ld-elf.so.1", 21); + if (begin == header.MappedAddress && end - begin > loaderPath.size() && std::equal(loaderPath.begin(), loaderPath.end(), segment.begin())) begin += loaderPath.size(); header.Offset += begin - header.MappedAddress; header.MappedAddress = begin; header.FileSize = end - begin; diff --git a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp index d7598f0a26..6c9837a018 100644 --- a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp +++ b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp @@ -1,4 +1,6 @@ #include +#include +#include #include #include #include @@ -304,9 +306,14 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path image.Trampolines = std::move(converted.Trampolines); image.Bytes = std::move(converted.Bytes); } + const bool sceImage = std::any_of(image.Headers.begin(), image.Headers.end(), [](const Domain::ProgramHeader& header) { return header.Type == 0x61000000; }); + const auto unwindFunctions = sceImage ? UnusedNidFilter::ReadExceptionFunctions(image.Bytes, image.Headers, {}, {}) : std::vector{}; for (const auto& header : codeHeaders) { - const std::vector code(image.Bytes.begin() + header.Offset, image.Bytes.begin() + header.Offset + header.FileSize); - for (const auto offset : syscallScanner.ScanCodeSectionForSyscalls(code, header.MappedAddress, header.FileSize, Io::Utf8Path(image.SourcePath))) { + std::vector code(image.Bytes.begin() + header.Offset, image.Bytes.begin() + header.Offset + header.FileSize); + if (sceImage) + if (const auto end = FindSceCodeEnd(code, header.MappedAddress, unwindFunctions, image.Got, image.Dynamic.RelaPltData.size())) + code.resize(*end - header.MappedAddress); + for (const auto offset : syscallScanner.ScanCodeSectionForSyscalls(code, header.MappedAddress, code.size(), Io::Utf8Path(image.SourcePath))) { if (offset > code.size() || code.size() - offset < 2u) throw Domain::RelinkerException("Syscall trap patch exceeds guest executable segment", header.MappedAddress + offset); image.Bytes[header.Offset + offset] = 0x0F; diff --git a/core/relinker/relinker/tests/test_sce_module_code_extent.py b/core/relinker/relinker/tests/test_sce_module_code_extent.py new file mode 100644 index 0000000000..28a19c721e --- /dev/null +++ b/core/relinker/relinker/tests/test_sce_module_code_extent.py @@ -0,0 +1,160 @@ +from pathlib import Path +import struct +import subprocess +import sys +import tempfile + +from test_guest_symbol_names import importing_executable + +PT_LOAD = 1 +PT_DYNAMIC = 2 +PT_INTERP = 3 +PT_GNU_EH_FRAME = 0x6474E550 +PT_SCE_DYNLIBDATA = 0x61000000 +TEXT_OFFSET = 0x1000 +TEXT_SIZE = 0x1000 +DATA_OFFSET = 0x3000 +DATA_ADDRESS = 0x2000 +BLOCK_OFFSET = 0x3800 +IMAGE_SIZE = 0x4000 +INTERP = b"/libexec/ld-elf.so.1\x00" +FS_LOOKALIKE = b"\x64\x2d\x65\x6c\x66\x2e" +FUNCTION = 0x100 +FUNCTION_SIZE = 0x10 +PLT = 0x110 +RODATA = 0x140 +GOT = DATA_ADDRESS + 0x100 +EH_FRAME_HEADER = 0x800 +EH_FRAME = 0x840 + + +def fixture(interp_address=True, rodata_address=True, interp_header=True): + image = bytearray(IMAGE_SIZE) + base = BLOCK_OFFSET + strtab, symtab, jmprel = BLOCK_OFFSET, BLOCK_OFFSET + 0x20, BLOCK_OFFSET + 0x100 + tags = [ + (1, 1), + (0x61000035, strtab - base), (0x61000037, 16), + (0x61000039, symtab - base), (0x6100003B, 24), (0x6100003F, 48), + (0x6100002F, jmprel - base), (0x61000031, 0), (0x61000033, 24), + (0x61000027, GOT), (0x61000029, jmprel - base), (0x6100002B, 7), (0x6100002D, 24), + (0, 0), + ] + headers = [ + (PT_LOAD, 5, TEXT_OFFSET, 0, TEXT_SIZE, TEXT_SIZE, 0x1000), + (PT_LOAD, 6, DATA_OFFSET, DATA_ADDRESS, 0x400, 0x400, 0x1000), + (PT_DYNAMIC, 6, DATA_OFFSET, DATA_ADDRESS, len(tags) * 16, len(tags) * 16, 8), + (PT_SCE_DYNLIBDATA, 4, BLOCK_OFFSET, 0, 0x800, 0, 0x10), + (PT_INTERP if interp_header else 0, 4, TEXT_OFFSET, 0, len(INTERP), len(INTERP), 1), + (PT_GNU_EH_FRAME, 4, TEXT_OFFSET + EH_FRAME_HEADER, EH_FRAME_HEADER, 20, 20, 4), + ] + image[:16] = b"\x7fELF\x02\x01\x01" + bytes(9) + struct.pack_into(" Date: Wed, 14 Oct 2026 11:14:37 -0700 Subject: [PATCH 0115/1000] docs: list the performance overlay with the diagnostics in USAGE Its section moves from after HDR output to before Windows crash diagnostics, next to the other switches for looking into a run. Other open changes add their own sections right after HDR output. --- docs/user/USAGE.md | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index 56f50fa8d8..c169a8b495 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -165,21 +165,6 @@ Do not disable Dozen if it is the only Vulkan driver available; the application HDR output is available on Windows only. Titles see it when the monitor showing the window runs in HDR mode (Settings > Display > Use HDR), the window's Vulkan surface offers an HDR10 (`VK_COLOR_SPACE_HDR10_ST2084_EXT`) swapchain in a 10-bit format, the instance has `VK_EXT_swapchain_colorspace` and the device `VK_EXT_hdr_metadata`. The monitor's luminance comes from DXGI; a monitor that reports no peak luminance is taken as 1000 nits peak and full-frame, 0 nits black. On Linux and other hosts the output is always SDR, because the surface's formats do not show whether the display is in HDR. A title's BT.2100 PQ display buffers then reach the display unchanged; without HDR output they are converted to SDR. -### Performance overlay - -F10 shows or hides a performance overlay over the presented image; `ANYPS5_OVERLAY=1` shows it from the first frame. `ANYPS5_OVERLAY_POS` puts it in a corner of the window: `tl` (top left, the default), `tr`, `bl` or `br`. For either variable an empty value is the same as an unset one, and any other value stops the game at its first frame with an error that names the variable. The key can be moved with `TogglePerformanceOverlay` in the [input mapping](INPUT_MAPPING.md). The text scales with the window height, at a whole multiple of its 5x7 pixel font; on an HDR10 swapchain the overlay is drawn at 203 nits. - -| Line | Meaning | -|------|---------| -| `FPS` | Frames presented per second, over the last second. Every present counts, including frames a title presents cleared, for example while it loads. | -| `FRAME` | Time between the last two presented frames, in ms. | -| graph | The last 120 frame times, the newest on the right. The full height is the longest of them or twice their average, whichever is larger. | -| `MIN/AVG/MAX` | The shortest, average and longest of the 120 frame times, in ms. | -| `GPU` | GPU time per frame over the last second, in ms: the span of each recorded batch from its first to its last command, from the timestamp queries of `APS5_PROFILE_GPU=1`. Shown only with that variable. Batches are counted when they complete, so the line trails a sudden change by up to a second. Batches that overlap on different queues each count in full, so the line can exceed `FRAME`. | -| `DRIVER` | CPU time per frame over the last second, in ms, that the packet workers spent in dispatch and draw packets (the `dispatch` and `draw` time of the `[gpu] worker` line of `APS5_PROFILE_DRAW=1`). Shown only with that variable. | - -Times are shown with one decimal below 100 and as whole numbers above, up to 9999. While the overlay is hidden the presentation records nothing for it and the presented image is unchanged; while it is shown, one small render pass draws it over each presented frame. - ### Storing GPU results at each flip GPU results in storage images and render targets stay on the GPU until something reads their memory. A title that frees such an image after waiting for its GPU work and reuses the memory can have its new data overwritten when the results are stored later ([TechnicalDebt](../dev/TechnicalDebt.md)). `APS5_STORE_AT_FLIP=1` stores every pending result when a display buffer is flipped, at the cost of one write-back per pending image and frame. Any other value stops the title at its first flip. @@ -199,6 +184,21 @@ most called traced imports (1 calls in total): Limits are listed in the [libc](../dev/debt/libc.md) and [relinker](../dev/debt/relinker.md) technical debt. +### Performance overlay + +F10 shows or hides a performance overlay over the presented image; `ANYPS5_OVERLAY=1` shows it from the first frame. `ANYPS5_OVERLAY_POS` puts it in a corner of the window: `tl` (top left, the default), `tr`, `bl` or `br`. For either variable an empty value is the same as an unset one, and any other value stops the game at its first frame with an error that names the variable. The key can be moved with `TogglePerformanceOverlay` in the [input mapping](INPUT_MAPPING.md). The text scales with the window height, at a whole multiple of its 5x7 pixel font; on an HDR10 swapchain the overlay is drawn at 203 nits. + +| Line | Meaning | +|------|---------| +| `FPS` | Frames presented per second, over the last second. Every present counts, including frames a title presents cleared, for example while it loads. | +| `FRAME` | Time between the last two presented frames, in ms. | +| graph | The last 120 frame times, the newest on the right. The full height is the longest of them or twice their average, whichever is larger. | +| `MIN/AVG/MAX` | The shortest, average and longest of the 120 frame times, in ms. | +| `GPU` | GPU time per frame over the last second, in ms: the span of each recorded batch from its first to its last command, from the timestamp queries of `APS5_PROFILE_GPU=1`. Shown only with that variable. Batches are counted when they complete, so the line trails a sudden change by up to a second. Batches that overlap on different queues each count in full, so the line can exceed `FRAME`. | +| `DRIVER` | CPU time per frame over the last second, in ms, that the packet workers spent in dispatch and draw packets (the `dispatch` and `draw` time of the `[gpu] worker` line of `APS5_PROFILE_DRAW=1`). Shown only with that variable. | + +Times are shown with one decimal below 100 and as whole numbers above, up to 9999. While the overlay is hidden the presentation records nothing for it and the presented image is unchanged; while it is shown, one small render pass draws it over each presented frame. + ### Windows crash diagnostics Set `APS5_CRASH_INDIRECT_MEMORY=1` before launching the executable to include up to 64 bytes from each pointer found in the register memory dump. The report labels each block with its register, pointer offset and target address. Inaccessible, incomplete and executable targets are skipped. This additional dump is disabled when the variable is unset or empty. From 2898e583570ad3bbc38560dfe4ac07969564f8e0 Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Wed, 14 Oct 2026 18:24:55 +0000 Subject: [PATCH 0116/1000] fix(tools): recognize assembly NID exports --- tools/nid_names.py | 21 +++++++++++++++++++++ tools/tests/test_nid_names.py | 14 ++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/tools/nid_names.py b/tools/nid_names.py index 0df8c3f147..573ec4a1fe 100644 --- a/tools/nid_names.py +++ b/tools/nid_names.py @@ -26,6 +26,7 @@ CPP_TOKEN = re.compile( r"|(?P(?:[0-9]|\.[0-9])[A-Za-z0-9_.']*|[A-Za-z_][A-Za-z0-9_]*|\S)") DEFINITION = re.compile(r"\bAPS5_VABI\s+(\w+)\s*\([^;{]*\)\s*(?:noexcept\s*)?(?:try\s*)?\{") NID_POSTFIX = "_nid_postfix" +ASM_FUNCTION_MACROS = {"APS5_ASM_FUNCTION", "APS5_SIGNAL_ASM_FUNCTION"} def compute_nid(symbol): @@ -92,6 +93,22 @@ def source_definitions(text): return DEFINITION.finditer(CPP_TOKEN.sub(mask, text.replace("\\\n", "").replace("\\\r\n", ""))) +def source_assembly_definitions(text): + tokens = [match for match in CPP_TOKEN.finditer(text.replace("\\\n", "").replace("\\\r\n", "")) + if match.group("comment") is None] + for index, token in enumerate(tokens): + if token.group("code") is None or token.group() not in ASM_FUNCTION_MACROS: + continue + if index + 3 >= len(tokens): + continue + opening, literal, closing = tokens[index + 1:index + 4] + if opening.group() != "(" or closing.group() != ")" or literal.group("string") is None: + continue + match = re.fullmatch(r'"([A-Za-z_][A-Za-z0-9_]*)"', literal.group("string")) + if match is not None: + yield match.group(1) + + def collect_unknowns(): found = {} for path in source_files(PRX): @@ -111,6 +128,10 @@ def collect_real_names(): names.add(name) if name.endswith(NID_POSTFIX): names.add(name[:-len(NID_POSTFIX)]) + for name in source_assembly_definitions(text): + names.add(name) + if name.endswith(NID_POSTFIX): + names.add(name[:-len(NID_POSTFIX)]) return names diff --git a/tools/tests/test_nid_names.py b/tools/tests/test_nid_names.py index 6dd4b20804..221219f1ce 100644 --- a/tools/tests/test_nid_names.py +++ b/tools/tests/test_nid_names.py @@ -189,6 +189,20 @@ int APS5_VABI sceReal() { return 0; } self.assertEqual(nid_names.collect_unknowns(), {"CCCCCCCCCCC": "core/libs/prx/libSceExample/Export.cpp:exampleUnknown02"}) self.assertEqual(nid_names.collect_real_names(), {"sceReal"}) + def test_assembly_export_macros_are_real_names(self): + self.write_source('''asm(".text\n" + APS5_ASM_FUNCTION("sceAsmCall_nid_postfix") + "ret\n" + APS5_SIGNAL_ASM_FUNCTION("sigAsmCall_nid_postfix") + "ret\n"); +// APS5_ASM_FUNCTION("sceCommented_nid_postfix") +auto example = "APS5_ASM_FUNCTION(\"sceQuoted_nid_postfix\")"; +''') + self.assertEqual(nid_names.collect_real_names(), { + "sceAsmCall", "sceAsmCall_nid_postfix", + "sigAsmCall", "sigAsmCall_nid_postfix", + }) + def test_formatted_alias_macro_is_detected(self): self.write_source('APS5_EXPORT (\n "AAAAAAAAAAA" ,\n exampleUnknown00\n );\n') self.assertEqual(nid_names.collect_unknowns(), {"AAAAAAAAAAA": "core/libs/prx/libSceExample/Export.cpp:exampleUnknown00"}) From fd9525a2eaa27250d7c10f8bc29394ed59953afa Mon Sep 17 00:00:00 2001 From: Ondra97 <47064087+Ondra97@users.noreply.github.com> Date: Wed, 14 Oct 2026 20:34:57 +0200 Subject: [PATCH 0117/1000] fix(agc): release sampled views after storage eviction --- .../Graphics/src/ShaderResources.cpp | 5 ++ .../tests/execution/SampledCacheBudget.cpp | 54 +++++++++++++++++-- 2 files changed, 54 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index 2c120f5cd8..61d48b57c9 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -932,6 +932,11 @@ std::shared_ptr cachedStorageTexture(const Context& context, std for (auto& resource : resources) recorder->Keep(std::move(resource)); } } + if (!evicted.empty()) { + auto& sampled = Textures(); + std::lock_guard lock(sampled.mutex); + dropUncachedViews(sampled); + } return texture; } diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp index 9d1978dbb2..67b8af060c 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp @@ -2,6 +2,7 @@ #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" #include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" #include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp" #include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureDetiler.hpp" @@ -236,6 +237,44 @@ std::shared_ptr ImageResources(const Context& context, const Bl return std::make_shared(context, shader); } +void recorderEvictionTests(const Context& context, const Block& block) { + auto& cache = SharedResourceCache(); + cache.Clear(); + ClearCachedTextures(context.device); + auto sampled = ImageResources(context, block, 0, 1, true); + const auto images = sampled->StorageImages(); + Require(images.size() == 1, "the eviction fixture has no sampled storage image"); + const std::weak_ptr resources = sampled; + const std::weak_ptr view = sampled->SampledTextures().front(); + const std::weak_ptr image = sampled->SampledTextures().front()->SharedStorageSource(); + cache.Insert({1}, sampled); + Require(cache.Find({1}) == sampled, "the recorder fixture's descriptor set was not cached"); + sampled.reset(); + Buffer readback(context, sizeof(std::uint32_t), VK_BUFFER_USAGE_TRANSFER_DST_BIT); + { + Recorder recorder(context); + recorder.Activate(); + recorder.Commands(); + for (std::size_t surface = 1; surface < Surfaces; ++surface) CachedStorageSurface(context, Resource(block.Surface(surface))); + Require(cache.Find({1}) == nullptr, "storage eviction left the recorded descriptor set cached"); + Require(!resources.expired() && !view.expired() && !image.expired(), "eviction destroyed the recorder's descriptor set, view or image"); + Require(!image.lock()->Cached(), "the recorder fixture did not evict the source image"); + const auto commands = recorder.Commands(); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_MEMORY_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT); + VkBufferImageCopy copy{}; + copy.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; + copy.imageExtent = {1, 1, 1}; + context.Function("vkCmdCopyImageToBuffer")(commands, images.front().first, VK_IMAGE_LAYOUT_GENERAL, readback.Handle(), 1, ©); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_HOST_READ_BIT); + recorder.Sync(); + std::uint32_t pixel = 0; + std::memcpy(&pixel, readback.Bytes().data(), sizeof(pixel)); + Require(pixel == 0x5a5a5a5a, "eviction changed the recorder's storage image pixel"); + } + Require(resources.expired() && view.expired() && image.expired(), "an evicted image outlived its completed recorder"); + std::puts("storage eviction recorder lifetime and pixel readback passed"); +} + bool resourceCacheTests(Context context, const std::string& mode) { PlaceholderImages placeholders(context); context.placeholderImages = &placeholders; @@ -281,7 +320,6 @@ bool resourceCacheTests(Context context, const std::string& mode) { Require(cache.Size() == 0 && displaced.empty(), sampledImages ? "an oversized sampled descriptor set entered the resource cache" : "an oversized storage descriptor set entered the resource cache"); image.reset(); oversized.reset(); - TextureCacheUsage(); Require(rejected.expired() && early.expired(), "the rejected oversized descriptor set retained an evicted storage image"); std::puts(sampledImages ? "resource cache oversized sampled lifetime passed" : "resource cache oversized storage lifetime passed"); ClearCachedTextures(context.device); @@ -313,10 +351,10 @@ bool resourceCacheTests(Context context, const std::string& mode) { Require(storageSet.expired() && !sampledSet.expired() && !staleImage.expired(), "stale storage release did not preserve the sampled caller's image"); std::puts("resource cache stale storage caller release passed"); sampled.reset(); - TextureCacheUsage(); Require(sampledSet.expired() && staleImage.expired(), "the rejected descriptor sets retained an evicted storage image"); std::puts("resource cache stale sampled caller release passed"); } + if (mode == "resource-admission") recorderEvictionTests(context, block); } catch (...) { cache.Clear(); throw; @@ -339,7 +377,7 @@ void viewTests(const Context& context) { auto view = Sampled(context, block.Surface(0)); Require(view->ViewsStorageImage(), "a sampled texture in host-imported memory is not a view of its storage image"); const std::weak_ptr viewed = view; - const auto* image = view->StorageSource(); + const std::weak_ptr storage = view->SharedStorageSource(); const auto after = TextureCacheUsage(); Require(after.sampledEntries == before.sampledEntries + 1u, "the view did not enter the sampled texture cache"); Require(after.sampledBytes == before.sampledBytes, "a view of a cached storage image counted " + std::to_string(after.sampledBytes - before.sampledBytes) + " bytes against the sampled texture cache"); @@ -347,12 +385,18 @@ void viewTests(const Context& context) { Require(Sampled(context, block.Surface(0)) == view, "the second lookup of the surface did not hit its view"); view.reset(); for (std::size_t surface = 1; surface < Surfaces; ++surface) CachedStorageSurface(context, Resource(block.Surface(surface))); - Require(!StorageImageCached(context, image), "the storage image cache never evicted the viewed image under a 1 MiB budget"); + Require(viewed.expired() && storage.expired(), "the sampled texture cache kept the view of an evicted storage image"); const auto evicted = TextureCacheUsage(); - Require(viewed.expired(), "the sampled texture cache kept the view of an evicted storage image"); Require(evicted.sampledEntries == after.sampledEntries - 1u && evicted.sampledBytes == before.sampledBytes, "dropping the view of the evicted storage image changed other sampled entries"); auto again = Sampled(context, block.Surface(0)); Require(again->ViewsStorageImage() && again->StorageSource() != nullptr && StorageImageCached(context, again->StorageSource()), "the surface's next lookup does not view its new cached storage image"); + const std::weak_ptr heldView = again; + const std::weak_ptr heldStorage = again->SharedStorageSource(); + for (std::size_t surface = 1; surface < Surfaces; ++surface) CachedStorageSurface(context, Resource(block.Surface(surface))); + Require(!StorageImageCached(context, again->StorageSource()), "the held view's source was not evicted"); + Require(!heldView.expired() && !heldStorage.expired(), "eviction destroyed an active caller's view or image"); + again.reset(); + Require(heldView.expired() && heldStorage.expired(), "the evicted image outlived its last caller without a sampled lookup"); } void reportedBudgetTests(const Context& context) { From 659eddb10725cc839015dd59ab82bc027f72b5f4 Mon Sep 17 00:00:00 2001 From: zmeizy Date: Wed, 14 Oct 2026 20:08:12 +0300 Subject: [PATCH 0118/1000] fix(shader): translate f32 compares in f32 denormal modes 1 and 2 v_cmp_X_f32 and v_cmpx_X_f32 flush their denormal inputs by bit 0 of the FLOAT_MODE f32 denormal field, as measured on RDNA2 (gfx1030): mode 1 keeps them and mode 2 flushes them. Both modes threw before. Co-Authored-By: Claude Opus 5.5 --- .../tests/execution/VCmpF32DenormalModes.cpp | 180 ++++++++++++++++++ .../Translation/src/VectorInstructions.cpp | 1 + docs/dev/debt/shader.md | 1 + 3 files changed, 182 insertions(+) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/VCmpF32DenormalModes.cpp diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/VCmpF32DenormalModes.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/VCmpF32DenormalModes.cpp new file mode 100644 index 0000000000..4e23992b13 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/VCmpF32DenormalModes.cpp @@ -0,0 +1,180 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t ValueCount = 16; +constexpr std::uint32_t Threads = ValueCount * ValueCount; +constexpr std::uint32_t Groups = Threads / 32; +constexpr std::uint32_t Inputs = 4; +constexpr std::uint32_t Results = 16; +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; + +constexpr std::uint32_t Values[ValueCount] = { + 0x00000000u, 0x80000000u, 0x00000001u, 0x80000001u, 0x007fffffu, 0x807fffffu, 0x00800000u, 0x80800000u, + 0x3f800000u, 0xbf800000u, 0x7f800000u, 0xff800000u, 0x7fc00000u, 0x7f800001u, 0xffc00000u, 0x7f7fffffu, +}; + +alignas(256) constexpr std::array CompareCode{ + 0x34020084u, 0x34040086u, 0xe0381000u, 0x80000401u, 0xbf8c3f70u, 0x7c040b04u, 0xd501000au, 0x01a90280u, + 0x7c0c0b04u, 0xd501000bu, 0x01a90280u, 0x7c080b04u, 0xd501000cu, 0x01a90280u, 0x7c060b04u, 0xd501000du, + 0x01a90280u, 0x7c0a0b04u, 0xd501000eu, 0x01a90280u, 0x7c020b04u, 0xd501000fu, 0x01a90280u, 0x7c1a0b04u, + 0xd5010010u, 0x01a90280u, 0x7c120b04u, 0xd5010011u, 0x01a90280u, 0x7c160b04u, 0xd5010012u, 0x01a90280u, + 0x7c180b04u, 0xd5010013u, 0x01a90280u, 0x7c140b04u, 0xd5010014u, 0x01a90280u, 0x7c1c0b04u, 0xd5010015u, + 0x01a90280u, 0xe0781000u, 0x80010a02u, 0xe0781010u, 0x80010e02u, 0xe0781020u, 0x80011202u, 0xbf810000u, +}; + +alignas(256) constexpr std::array CompareExecCode{ + 0x34020084u, 0x34040086u, 0xe0381000u, 0x80000401u, 0xbf8c3f70u, 0xbe94037eu, 0x7c240b04u, 0xbe95037eu, + 0xbefe0314u, 0xd501000au, 0x00550280u, 0xbe94037eu, 0x7c2c0b04u, 0xbe95037eu, 0xbefe0314u, 0xd501000bu, + 0x00550280u, 0xbe94037eu, 0x7c280b04u, 0xbe95037eu, 0xbefe0314u, 0xd501000cu, 0x00550280u, 0xbe94037eu, + 0x7c260b04u, 0xbe95037eu, 0xbefe0314u, 0xd501000du, 0x00550280u, 0xbe94037eu, 0x7c2a0b04u, 0xbe95037eu, + 0xbefe0314u, 0xd501000eu, 0x00550280u, 0xbe94037eu, 0x7c220b04u, 0xbe95037eu, 0xbefe0314u, 0xd501000fu, + 0x00550280u, 0xbe94037eu, 0x7c3a0b04u, 0xbe95037eu, 0xbefe0314u, 0xd5010010u, 0x00550280u, 0xbe94037eu, + 0x7c320b04u, 0xbe95037eu, 0xbefe0314u, 0xd5010011u, 0x00550280u, 0xbe94037eu, 0x7c360b04u, 0xbe95037eu, + 0xbefe0314u, 0xd5010012u, 0x00550280u, 0xbe94037eu, 0x7c380b04u, 0xbe95037eu, 0xbefe0314u, 0xd5010013u, + 0x00550280u, 0xbe94037eu, 0x7c340b04u, 0xbe95037eu, 0xbefe0314u, 0xd5010014u, 0x00550280u, 0xbe94037eu, + 0x7c3c0b04u, 0xbe95037eu, 0xbefe0314u, 0xd5010015u, 0x00550280u, 0xe0781000u, 0x80010a02u, 0xe0781010u, + 0x80010e02u, 0xe0781020u, 0x80011202u, 0xbf810000u, +}; + +struct Row { + const char* condition; + std::uint32_t inputsPreserved[Groups]; + std::uint32_t inputsFlushed[Groups]; +}; + +constexpr Row Rows[] = { + {"eq", + {0x00030003u, 0x00080004u, 0x00200010u, 0x00800040u, 0x02000100u, 0x08000400u, 0x00000000u, 0x80000000u}, + {0x003f003fu, 0x003f003fu, 0x003f003fu, 0x00800040u, 0x02000100u, 0x08000400u, 0x00000000u, 0x80000000u}}, + {"ge", + {0x0aab0aabu, 0x0aa80aafu, 0x0aa00abfu, 0x0a800affu, 0x0a000bffu, 0x08008fffu, 0x00000000u, 0x8bff0000u}, + {0x0abf0abfu, 0x0abf0abfu, 0x0abf0abfu, 0x0a800affu, 0x0a000bffu, 0x08008fffu, 0x00000000u, 0x8bff0000u}}, + {"gt", + {0x0aa80aa8u, 0x0aa00aabu, 0x0a800aafu, 0x0a000abfu, 0x08000affu, 0x00008bffu, 0x00000000u, 0x0bff0000u}, + {0x0a800a80u, 0x0a800a80u, 0x0a800a80u, 0x0a000abfu, 0x08000affu, 0x00008bffu, 0x00000000u, 0x0bff0000u}}, + {"le", + {0x85578557u, 0x855f8554u, 0x857f8550u, 0x85ff8540u, 0x87ff8500u, 0x8fff0400u, 0x00000000u, 0x84000000u}, + {0x857f857fu, 0x857f857fu, 0x857f857fu, 0x85ff8540u, 0x87ff8500u, 0x8fff0400u, 0x00000000u, 0x84000000u}}, + {"lg", + {0x8ffc8ffcu, 0x8ff78ffbu, 0x8fdf8fefu, 0x8f7f8fbfu, 0x8dff8effu, 0x87ff8bffu, 0x00000000u, 0x0fff0000u}, + {0x8fc08fc0u, 0x8fc08fc0u, 0x8fc08fc0u, 0x8f7f8fbfu, 0x8dff8effu, 0x87ff8bffu, 0x00000000u, 0x0fff0000u}}, + {"lt", + {0x85548554u, 0x85578550u, 0x855f8540u, 0x857f8500u, 0x85ff8400u, 0x87ff0000u, 0x00000000u, 0x04000000u}, + {0x85408540u, 0x85408540u, 0x85408540u, 0x857f8500u, 0x85ff8400u, 0x87ff0000u, 0x00000000u, 0x04000000u}}, + {"neq", + {0xfffcfffcu, 0xfff7fffbu, 0xffdfffefu, 0xff7fffbfu, 0xfdfffeffu, 0xf7fffbffu, 0xffffffffu, 0x7fffffffu}, + {0xffc0ffc0u, 0xffc0ffc0u, 0xffc0ffc0u, 0xff7fffbfu, 0xfdfffeffu, 0xf7fffbffu, 0xffffffffu, 0x7fffffffu}}, + {"nge", + {0xf554f554u, 0xf557f550u, 0xf55ff540u, 0xf57ff500u, 0xf5fff400u, 0xf7ff7000u, 0xffffffffu, 0x7400ffffu}, + {0xf540f540u, 0xf540f540u, 0xf540f540u, 0xf57ff500u, 0xf5fff400u, 0xf7ff7000u, 0xffffffffu, 0x7400ffffu}}, + {"ngt", + {0xf557f557u, 0xf55ff554u, 0xf57ff550u, 0xf5fff540u, 0xf7fff500u, 0xffff7400u, 0xffffffffu, 0xf400ffffu}, + {0xf57ff57fu, 0xf57ff57fu, 0xf57ff57fu, 0xf5fff540u, 0xf7fff500u, 0xffff7400u, 0xffffffffu, 0xf400ffffu}}, + {"nle", + {0x7aa87aa8u, 0x7aa07aabu, 0x7a807aafu, 0x7a007abfu, 0x78007affu, 0x7000fbffu, 0xffffffffu, 0x7bffffffu}, + {0x7a807a80u, 0x7a807a80u, 0x7a807a80u, 0x7a007abfu, 0x78007affu, 0x7000fbffu, 0xffffffffu, 0x7bffffffu}}, + {"nlg", + {0x70037003u, 0x70087004u, 0x70207010u, 0x70807040u, 0x72007100u, 0x78007400u, 0xffffffffu, 0xf000ffffu}, + {0x703f703fu, 0x703f703fu, 0x703f703fu, 0x70807040u, 0x72007100u, 0x78007400u, 0xffffffffu, 0xf000ffffu}}, + {"nlt", + {0x7aab7aabu, 0x7aa87aafu, 0x7aa07abfu, 0x7a807affu, 0x7a007bffu, 0x7800ffffu, 0xffffffffu, 0xfbffffffu}, + {0x7abf7abfu, 0x7abf7abfu, 0x7abf7abfu, 0x7a807affu, 0x7a007bffu, 0x7800ffffu, 0xffffffffu, 0xfbffffffu}}, +}; + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + +void Run(AgcDriver::VulkanDevice& device, std::span code, ShaderRecompiler::ShaderFloatMode mode) { + for (std::uint32_t pair = 0; pair < Threads; ++pair) { + Input[pair * Inputs] = Values[pair / ValueCount]; + Input[pair * Inputs + 1u] = Values[pair % ValueCount]; + Input[pair * Inputs + 2u] = 0u; + Input[pair * Inputs + 3u] = 0u; + } + Output.fill(0xdeadbeefu); + std::vector userData(8, 0u); + const auto input = BufferDescriptor(Input.data(), static_cast(Input.size() * 4u)); + const auto output = BufferDescriptor(Output.data(), static_cast(Output.size() * 4u)); + std::copy(input.begin(), input.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + request.context.floatMode = mode; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); +} + +void Check(const char* form, const char* modeName, bool flushed) { + for (std::uint32_t index = 0; index < std::size(Rows); ++index) { + const Row& row = Rows[index]; + for (std::uint32_t pair = 0; pair < Threads; ++pair) { + const std::uint32_t actual = Output[pair * Results + index]; + const std::uint32_t expected = ((flushed ? row.inputsFlushed : row.inputsPreserved)[pair / 32u] >> (pair % 32u)) & 1u; + Require(actual == expected, std::string(form) + row.condition + "_f32 " + modeName + " of " + Hex(Values[pair / ValueCount]) + " and " + + Hex(Values[pair % ValueCount]) + " is " + Hex(actual) + ", expected " + std::to_string(expected)); + } + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + struct Mode { + const char* name; + ShaderRecompiler::ShaderFloatMode mode; + bool flushed; + }; + const Mode modes[] = { + {"denormal mode 1, IEEE mode on", ShaderRecompiler::ShaderFloatMode{0xd0u, true, true, false}, false}, + {"denormal mode 1, IEEE mode off", ShaderRecompiler::ShaderFloatMode{0xd0u, true, false, false}, false}, + {"denormal mode 2, IEEE mode on", ShaderRecompiler::ShaderFloatMode{0xe0u, true, true, false}, true}, + {"denormal mode 2, IEEE mode off", ShaderRecompiler::ShaderFloatMode{0xe0u, true, false, false}, true}, + }; + for (const Mode& mode : modes) { + Run(*device, CompareCode, mode.mode); + Check("v_cmp_", mode.name, mode.flushed); + Run(*device, CompareExecCode, mode.mode); + Check("v_cmpx_", mode.name, mode.flushed); + } + std::puts("v_cmp and v_cmpx f32 denormal mode 1 and 2 tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/shader/recompiler/Translation/src/VectorInstructions.cpp b/core/shader/recompiler/Translation/src/VectorInstructions.cpp index 645b98534e..bba0a9020a 100644 --- a/core/shader/recompiler/Translation/src/VectorInstructions.cpp +++ b/core/shader/recompiler/Translation/src/VectorInstructions.cpp @@ -75,6 +75,7 @@ std::uint32_t TranslationContext::f32DenormalFlushFor(const RdnaInstruction& ins case RdnaOpcode::VCmpxNleF32: case RdnaOpcode::VCmpxNlgF32: case RdnaOpcode::VCmpxNltF32: + return floatMode.has_value() && ((floatMode->floatMode >> 4u) & 1u) != 0u ? 0u : 1u; case RdnaOpcode::VCvtI32F32: case RdnaOpcode::VCvtU32F32: case RdnaOpcode::VCvtRpiI32F32: diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index 7392cc8399..cfe2e26afd 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -93,6 +93,7 @@ - [v_rcp, v_rsq, v_sqrt, v_log, v_exp, v_sin and v_cos](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) (f32 and f16) use the driver's approximation for finite results, not the console's: the low bits can differ. Only NaN, invalid, zero and infinite results and the exact sin/cos zeros are chosen on the bits. Measured on RDNA2 (gfx1036) over every f16 input and 32,832 f32 inputs: RADV returns the hardware's bits for every input of rcp, rsq, sqrt and log and all but one of exp (f16 0x11c5, 1 ULP); NVIDIA (615.71.09) stays within 2 ULP except `v_log_f32` near 1.0 (next entry). [v_sin and v_cos](../../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) take the driver's sine or cosine of the radians of the exact cycle, except within 2^-12 of a turn from a zero of the function, where the result is the linear term of the exact distance to the zero: the hardware's constant 6.2831826 (0x40c90fd5) at the integers, which the hardware applies exactly to inputs below 2^-12 of a turn, and the f32 2π at the half and quarter turns, where the hardware is within 7 ULP of it. Outside those bands, a driver result more than 4 ULP from a Taylor series in turns of the exact cycle is replaced by the series, which is within 2 ULP of the true sine and cosine; the hardware is up to 9 ULP from them (gfx1035, 243,092 f32 inputs), so results stay within 13 ULP of the hardware on any driver. RADV returns the hardware's bits for every f16 input and for 32,422 sine and 32,486 cosine results of the 32,832 f32 inputs; the rest are 1 ULP off, except up to 14 ULP on either side of non-zero integers and up to 15 ULP beyond ±256 cycles, where the hardware's own reduction differs from the exact fraction, and up to 6 ULP inside the half- and quarter-turn bands. NVIDIA's own sine is within 1 ULP on 93% of the f32 inputs and up to 1,166 ULP off just outside the bands, its cosine within 163 ULP; the series replaces those results. A denormal f32 input with f32 denormals kept gives the hardware the input times 6.2831826; both hosts flush it. - [v_log_f32 and v_log_f16](../../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) keep the driver's log2 unless, for inputs within 1/8 of 1.0, it is more than 4 ULP from an eight-term series in x - 1, which then replaces it. `v_log_f16` goes through the same f32 lowering and is rounded to half. The series is evaluated for every `v_log` and chosen with `OpSelect` rather than a branch, so its seven fused multiply-adds and two multiplies are an unconditional cost. Measured on RDNA2 over all 3,145,728 f32 inputs within 1/8 of 1.0 (gfx1035, IEEE 0, f32 denormals flushed), the hardware is within 1 ULP of the correctly rounded log2 and the series within 2 ULP of it and within 3 ULP of the hardware (2 except at 0x3f752e47, 0x3f7baab0 and 0x3f7bc4fb), so a kept driver value is within 7 ULP of the hardware and a driver that returns the hardware's bits never triggers the gate; RADV (Mesa 26.2.4) returns the hardware's bits on the 9,280 such inputs measured on gfx1036; NVIDIA (615.71.09) was up to 23,947,836 ULP off at 1 - 2^-24 and is within 6 ULP inside the band after the change; outside it its own log2 stays up to 9 ULP off (measured up to 1.375). Over every f16 input, the change moves one `v_log_f16` result on NVIDIA (0x3bed, 1 ULP, onto the hardware's bits) and none on RADV. - [Ordered not-equal, less-than and greater-than compares](../../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) (shader recompiler) - `FPOrdNotEqual32` is emitted as `OpFUnordNotEqual`, and `FPOrdLessThan32` and `FPOrdGreaterThan32` as their ordered forms, each with both operands checked for NaN on their bits; the results are the same for every input. The AMD Windows driver 32.0.13058.8001 (Radeon 860M, RDNA 3.5) returns 1 for a NaN operand when any float form of not-equal feeds a ballot on a 32-wide subgroup (#825), and so does a less-than or greater-than against an infinity the driver sees as a constant (#3536), presumably rewritten into a not-equal by its compiler +- [f32 compares in denormal modes 1 and 2](../../../core/shader/recompiler/Translation/src/VectorInstructions.cpp) (`v_cmp_X_f32` and `v_cmpx_X_f32` with X = eq, ge, gt, le, lg, lt, neq, nge, ngt, nle, nlg, nlt) flush their f32 denormal inputs by bit 0 of the `FLOAT_MODE` f32 denormal field: mode 1 keeps them as mode 3 does, and mode 2 flushes them as mode 0 does. Measured on RDNA2 (gfx1030, Radeon RX 6800, AMD Windows driver 32.0.21045.5002 through HIP) over 484 operand pairs of zeros, denormals, minimum normals, ones, the largest finite value, infinities and quiet and signaling NaNs of both signs, in VOPC form, at IEEE=0 and IEEE=1 with each f32 denormal mode 0 to 3, f32 round to nearest even, DX10_CLAMP=1, f16 denormals kept, f16 round to nearest even and FP16_OVFL=0. The PS5's gfx1013 is not measured. - [DB_SHADER_CONTROL.PRIMITIVE_ORDERED_PIXEL_SHADER](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) (shader recompiler) - the host pixel interlock spans the whole shader, not only up to `s_sendmsg` ORDERED_PS_DONE. - [POPS collision SGPRs](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) (shader recompiler) - the collision wave ID and intrawave collision mask read 0, so a title skips its POPS wait and runs its per-collision loop once. - [Image loads](../../../core/shader/recompiler/Optimization/src/ResourceTracker.cpp) (shader recompiler) - an image_load reads a sampled binding, also of an image the shader stores to, so in a primitive-ordered pixel shader it misses an earlier overlapping primitive's image_store in the same draw. From f11382faf475383fbe8d7022b0a18c8783dc254e Mon Sep 17 00:00:00 2001 From: zmeizy Date: Wed, 14 Oct 2026 20:21:49 +0300 Subject: [PATCH 0119/1000] fix(shader): translate v_ldexp_f32 in f32 denormal modes 1 and 2 Mode 1 keeps subnormal inputs and flushes a result below 2^-126 to a zero of its sign; mode 2 flushes subnormal inputs and rounds a subnormal result in the f32 round mode, as measured on RDNA2 (gfx1030) in every f32 round mode at IEEE=0 and IEEE=1. Both modes threw before. Co-Authored-By: Claude Opus 5.5 --- .../tests/execution/LdexpF32DenormalModes.cpp | 148 ++++++++++++++++++ .../Translation/src/FloatInstructions.cpp | 11 +- docs/dev/debt/shader.md | 1 + 3 files changed, 153 insertions(+), 7 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/LdexpF32DenormalModes.cpp diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/LdexpF32DenormalModes.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/LdexpF32DenormalModes.cpp new file mode 100644 index 0000000000..33aa721ef7 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/LdexpF32DenormalModes.cpp @@ -0,0 +1,148 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 64; +constexpr std::uint32_t Inputs = 4; +constexpr std::uint32_t Results = 16; +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; + +alignas(256) constexpr std::array Code{ + 0x34020084u, 0x34060086u, 0xe0381000u, 0x80000401u, 0xbf8c3f70u, 0xd762000au, 0x02020b04u, 0xe0701000u, + 0x80010a03u, 0xbf810000u, +}; + +struct LdexpVector { + std::uint32_t value; + std::uint32_t exponent; + std::uint32_t outputsFlushed[4]; + std::uint32_t inputsFlushed[4]; +}; + +constexpr std::array Vectors{{ + {0x00000001u, 0x00000000u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}}, + {0x807fffffu, 0x00000000u, {0x80000000u, 0x80000000u, 0x80000000u, 0x80000000u}, {0x80000000u, 0x80000000u, 0x80000000u, 0x80000000u}}, + {0x00000001u, 0x00000003u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}}, + {0x00000001u, 0x00000017u, {0x00800000u, 0x00800000u, 0x00800000u, 0x00800000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}}, + {0x80000001u, 0x00000017u, {0x80800000u, 0x80800000u, 0x80800000u, 0x80800000u}, {0x80000000u, 0x80000000u, 0x80000000u, 0x80000000u}}, + {0x00000001u, 0x00000028u, {0x09000000u, 0x09000000u, 0x09000000u, 0x09000000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}}, + {0x007fffffu, 0x00000001u, {0x00fffffeu, 0x00fffffeu, 0x00fffffeu, 0x00fffffeu}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}}, + {0x007fffffu, 0xffffffffu, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}}, + {0x00400001u, 0xffffffffu, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}}, + {0x00000003u, 0xffffffffu, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}}, + {0x00800000u, 0xffffffffu, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x00400000u, 0x00400000u, 0x00400000u, 0x00400000u}}, + {0x00800001u, 0xffffffffu, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x00400000u, 0x00400001u, 0x00400000u, 0x00400000u}}, + {0x80800001u, 0xffffffffu, {0x80000000u, 0x80000000u, 0x80000000u, 0x80000000u}, {0x80400000u, 0x80400000u, 0x80400001u, 0x80400000u}}, + {0x00c00000u, 0xffffffe8u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x00000001u, 0x00000001u, 0x00000000u, 0x00000000u}}, + {0x80c00000u, 0xffffffe8u, {0x80000000u, 0x80000000u, 0x80000000u, 0x80000000u}, {0x80000001u, 0x80000000u, 0x80000001u, 0x80000000u}}, + {0x00800000u, 0xffffffe8u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x00000000u, 0x00000001u, 0x00000000u, 0x00000000u}}, + {0x00ffffffu, 0xffffffe9u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x00000002u, 0x00000002u, 0x00000001u, 0x00000001u}}, + {0x3f800000u, 0xffffff82u, {0x00800000u, 0x00800000u, 0x00800000u, 0x00800000u}, {0x00800000u, 0x00800000u, 0x00800000u, 0x00800000u}}, + {0x3f800000u, 0xffffff81u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x00400000u, 0x00400000u, 0x00400000u, 0x00400000u}}, + {0x3fffffffu, 0xffffff81u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x00800000u, 0x00800000u, 0x007fffffu, 0x007fffffu}}, + {0xbfffffffu, 0xffffff81u, {0x80000000u, 0x80000000u, 0x80000000u, 0x80000000u}, {0x80800000u, 0x807fffffu, 0x80800000u, 0x807fffffu}}, + {0x3f800000u, 0xffffff6bu, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x00000001u, 0x00000001u, 0x00000001u, 0x00000001u}}, + {0x3f800000u, 0xffffff6au, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x00000000u, 0x00000001u, 0x00000000u, 0x00000000u}}, + {0xbf800000u, 0xffffff6au, {0x80000000u, 0x80000000u, 0x80000000u, 0x80000000u}, {0x80000000u, 0x80000000u, 0x80000001u, 0x80000000u}}, + {0x3fc00000u, 0xffffff6au, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x00000001u, 0x00000001u, 0x00000000u, 0x00000000u}}, + {0x3faaaaabu, 0xffffff7eu, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x000aaaabu, 0x000aaaabu, 0x000aaaaau, 0x000aaaaau}}, + {0x7f7fffffu, 0xffffff03u, {0x00ffffffu, 0x00ffffffu, 0x00ffffffu, 0x00ffffffu}, {0x00ffffffu, 0x00ffffffu, 0x00ffffffu, 0x00ffffffu}}, + {0x7f7fffffu, 0xffffff02u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x00800000u, 0x00800000u, 0x007fffffu, 0x007fffffu}}, + {0x007fffffu, 0x000003e8u, {0x7f800000u, 0x7f800000u, 0x7f7fffffu, 0x7f7fffffu}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}}, + {0x3f800000u, 0x000000c8u, {0x7f800000u, 0x7f800000u, 0x7f7fffffu, 0x7f7fffffu}, {0x7f800000u, 0x7f800000u, 0x7f7fffffu, 0x7f7fffffu}}, + {0xbf800000u, 0x000000c8u, {0xff800000u, 0xff7fffffu, 0xff800000u, 0xff7fffffu}, {0xff800000u, 0xff7fffffu, 0xff800000u, 0xff7fffffu}}, + {0x00000001u, 0x7fffffffu, {0x7f800000u, 0x7f800000u, 0x7f7fffffu, 0x7f7fffffu}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}}, + {0x3f800000u, 0x80000000u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x00000000u, 0x00000001u, 0x00000000u, 0x00000000u}}, + {0xbf800000u, 0x80000000u, {0x80000000u, 0x80000000u, 0x80000000u, 0x80000000u}, {0x80000000u, 0x80000000u, 0x80000001u, 0x80000000u}}, + {0x00000000u, 0x00000005u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}}, + {0x80000000u, 0xfffffffbu, {0x80000000u, 0x80000000u, 0x80000000u, 0x80000000u}, {0x80000000u, 0x80000000u, 0x80000000u, 0x80000000u}}, + {0x7f800000u, 0xfffffffbu, {0x7f800000u, 0x7f800000u, 0x7f800000u, 0x7f800000u}, {0x7f800000u, 0x7f800000u, 0x7f800000u, 0x7f800000u}}, + {0xff800000u, 0x00000005u, {0xff800000u, 0xff800000u, 0xff800000u, 0xff800000u}, {0xff800000u, 0xff800000u, 0xff800000u, 0xff800000u}}, + {0x7fc00000u, 0x00000003u, {0x7fc00000u, 0x7fc00000u, 0x7fc00000u, 0x7fc00000u}, {0x7fc00000u, 0x7fc00000u, 0x7fc00000u, 0x7fc00000u}}, +}}; + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + +void Run(AgcDriver::VulkanDevice& device, ShaderRecompiler::ShaderFloatMode floatMode) { + Input.fill(0u); + for (std::uint32_t lane = 0; lane < Vectors.size(); ++lane) { + Input[lane * Inputs] = Vectors[lane].value; + Input[lane * Inputs + 1] = Vectors[lane].exponent; + } + Output.fill(0xdeadbeefu); + std::vector userData(8, 0u); + const auto input = BufferDescriptor(Input.data(), static_cast(Input.size() * 4u)); + const auto output = BufferDescriptor(Output.data(), static_cast(Output.size() * 4u)); + std::copy(input.begin(), input.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4); + const std::span code(Code); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + request.context.floatMode = floatMode; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); +} + +void Check(std::uint32_t denormals, std::uint32_t rounding, bool ieee) { + for (std::uint32_t lane = 0; lane < Vectors.size(); ++lane) { + const auto& vector = Vectors[lane]; + const auto actual = Output[lane * Results]; + const auto expected = denormals == 1u ? vector.outputsFlushed[rounding] : vector.inputsFlushed[rounding]; + Require(actual == expected, "ldexp f32 denormal mode " + std::to_string(denormals) + ", round mode " + std::to_string(rounding) + (ieee ? ", IEEE=1" : ", IEEE=0") + + ": v_ldexp_f32 (" + Hex(vector.value) + ", " + Hex(vector.exponent) + ") is " + Hex(actual) + ", expected " + Hex(expected)); + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + for (const std::uint32_t denormals : {1u, 2u}) { + for (std::uint32_t rounding = 0; rounding < 4u; ++rounding) { + for (const bool ieee : {false, true}) { + Run(*device, ShaderRecompiler::ShaderFloatMode{0xc0u | denormals << 4u | rounding, true, ieee, false}); + Check(denormals, rounding, ieee); + } + } + } + std::puts("ldexp f32 denormal mode 1 and 2 tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/shader/recompiler/Translation/src/FloatInstructions.cpp b/core/shader/recompiler/Translation/src/FloatInstructions.cpp index 8b0ea3b902..262eb78e0a 100644 --- a/core/shader/recompiler/Translation/src/FloatInstructions.cpp +++ b/core/shader/recompiler/Translation/src/FloatInstructions.cpp @@ -745,10 +745,7 @@ bool TranslationContext::vLdexpF32(const RdnaInstruction& inst) { if (roundMode == 1u) overflowBits = &ir.Select(*positive, ir.Constant(0x7f800000u), ir.Constant(0xff7fffffu)); else if (roundMode == 2u) overflowBits = &ir.Select(*positive, ir.Constant(0x7f7fffffu), ir.Constant(0xff800000u)); else if (roundMode == 3u) overflowBits = &ir.BitwiseOr(sign.Value(), ir.Constant(0x7f7fffffu)); - if (denorm32 == 1u || denorm32 == 2u) { - throw std::runtime_error("f32 denormal mode " + std::to_string(denorm32) + " at pc " + std::to_string(inst.programCounter) + " is not implemented"); - } - if (denorm32 == 3u) { + if (denorm32 != 0u) { const IrU32 mantissa(ir.BitwiseAnd(bits.Value(), ir.Constant(0x007fffffu))); const IrU1 isSubnormal(ir.IEqual(exponent.Value(), ir.Constant(0u))); const IrU32 sig(ir.Select(isSubnormal.Value(), mantissa.Value(), ir.BitwiseOr(mantissa.Value(), ir.Constant(0x00800000u)))); @@ -773,9 +770,9 @@ bool TranslationContext::vLdexpF32(const RdnaInstruction& inst) { const IrU32 magnitude(roundMode == 3u ? unrounded : IrU32(ir.Select(*roundUp, ir.IAdd(unrounded.Value(), ir.Constant(1u)), unrounded.Value()))); const IrU32 subResult(ir.BitwiseOr(sign.Value(), magnitude.Value())); IrU32 result(ir.Select(ir.Emit(IrOpcode::SGreaterThanEqual32, IrType::U1, {&expNorm.Value(), &ir.Constant(255u)}), *overflowBits, normResult.Value())); - result = IrU32(ir.Select(ir.Emit(IrOpcode::SLessThanEqual32, IrType::U1, {&expNorm.Value(), &ir.Constant(0u)}), subResult.Value(), result.Value())); - const IrU1 zero(ir.IEqual(ir.BitwiseAnd(bits.Value(), ir.Constant(0x7fffffffu)), ir.Constant(0u))); - result = IrU32(ir.Select(zero.Value(), bits.Value(), result.Value())); + result = IrU32(ir.Select(ir.Emit(IrOpcode::SLessThanEqual32, IrType::U1, {&expNorm.Value(), &ir.Constant(0u)}), (denorm32 & 2u) != 0u ? subResult.Value() : sign.Value(), result.Value())); + const IrU1 zero((denorm32 & 1u) != 0u ? ir.IEqual(ir.BitwiseAnd(bits.Value(), ir.Constant(0x7fffffffu)), ir.Constant(0u)) : ir.IEqual(exponent.Value(), ir.Constant(0u))); + result = IrU32(ir.Select(zero.Value(), sign.Value(), result.Value())); const IrU1 nan(ir.UGreaterThan(ir.BitwiseAnd(bits.Value(), ir.Constant(0x7fffffffu)), ir.Constant(0x7f800000u))); result = IrU32(ir.Select(ir.IEqual(exponent.Value(), ir.Constant(0xffu)), ir.Select(nan.Value(), quietNan32(bits).Value(), bits.Value()), result.Value())); writeOperand(inst.destination, &ir.BitCastF32(result.Value())); diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index 7392cc8399..0b47dc621e 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -91,6 +91,7 @@ - [Packed f16 NaN results](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) (`v_pk_add/mul/fma/min/max_f16`) follow the hardware measured on RDNA2 (gfx1035) at IEEE=0 and IEEE=1 with f16 round-to-nearest-even and f16 denormals kept: with IEEE=1 a signaling NaN input is quieted and min/max return the other operand only for a quiet NaN; with IEEE=0 NaN inputs pass through unquieted and min/max ignore signaling. In `v_pk_fma_f16` an invalid product (inf * 0) gives the default NaN even when src2 is a NaN. A shader without a known float mode is translated as IEEE=0, the mode titles use; a mode with another f16 rounding or that flushes f16 denormals throws. - [f32 denormals](../../../core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp) follow the shader's `FLOAT_MODE` f32 denormal field (bits 5:4) when it flushes them (0, and shaders without a known float mode, as in the 0xc0 most titles use), measured on RDNA2 (gfx1035) over 1444 rows per instruction at IEEE=0 and IEEE=1. `v_add/sub/subrev/mul/mul_legacy/mullit/fma/fmac/fmamk/fmaak/min/max/min3/max3/med3/fract/floor/ceil/frexp_mant/sin/cos_f32` flush their f32 inputs and their result to a zero of the same sign (`v_mullit_f32` measured on RDNA2 (gfx1036) over 6907 rows per mode at IEEE=0 and IEEE=1, so a positive denormal src2 counts as zero); the f32 compares, `v_cvt_i32/u32/rpi_i32/flr_i32_f32`, `v_frexp_exp_i32_f32`, `v_cvt_f64_f32`, `v_cvt_f16_f32`, `v_cvt_pkrtz_f16_f32`, `v_cvt_pknorm_i16/u16_f32` and `v_cvt_pk_u8_f32` flush their f32 inputs. `v_div_scale_f32` flushes all three inputs to signed zeros, measured on RDNA2 (gfx1036) over 2744 operand triples per mode at IEEE=0 and IEEE=1 with f32 denormals flushed and kept; its scale-down behaviour and host denormal preservation remain incomplete. `v_div_fixup_f32` flushes its src1 and src2 and keeps a denormal src0 and result, measured on RDNA2 (gfx1036) over 6859 rows per mode at IEEE=0 and IEEE=1. Tininess is detected after rounding, measured on RDNA2 (gfx1036) for `v_mul_f32` and `v_mullit_f32` at IEEE=0 and IEEE=1: a product is flushed when its value rounded to 24 bits with an unbounded exponent is below 2^-126, so `0x3f7ffffe * 0x00800001` (2^-126 * (1 - 2^-46)) gives `0x00800000` and `0x3f7fffff * 0x00800000` (2^-126 * (1 - 2^-24)) gives zero. The check rounds in the f32 round mode of `FLOAT_MODE` (bits 1:0), measured in all four modes, while the products themselves are still rounded to nearest even. This applies to `v_mul_f32`, `v_mul_legacy_f32`, `v_mullit_f32`, and to `v_fma_f32`, `v_fmac_f32`, `v_fmamk_f32`, `v_fmaak_f32` and `v_fma_mix_f32` when the addend is zero; with a nonzero addend the fused result is kept, so an exact result just below 2^-126 that rounds to it is not flushed. In the three directed modes the check is computed exactly from the significands. To nearest even (0, the mode of shaders without a known float mode) one factor is multiplied by 2^64, which is exact below 2^64, and the product with the other factor is compared with 2^-62. That multiply is a plain `OpFMul` like the product, and no f32 rounding mode is requested in the SPIR-V, so the check relies on the driver rounding f32 to nearest even by default, as the products do, and a product that rounds up to 2^-126 is the emitted product. `v_fma_mix_f32` follows this rule with mixed f16/f32 sources too, measured on RDNA2 (gfx1036) with f16 in either multiplicand slot, low and high half selection, and an f32 or f16 zero addend: f32 `0x00801002` times f16 `0x3bff` gives `0x00800000` to nearest at IEEE=0 and IEEE=1, while the neighbours `0x00801001` and `0x00801003` give zero and `0x00800001`. The tiny-product boundary was also measured for both signs in all four f32 round modes at IEEE=0; f16 inputs retain their previous conversion and denormal handling. `v_fma_mixlo_f16` and `v_fma_mixhi_f16` flush their f32 sources too and keep a product below 2^-126, measured on RDNA2 (gfx1036). `v_mad_legacy_f32` and `v_mac_legacy_f32` flush their inputs, their product and their result in every mode, see its entry; their product is still checked for tininess on its exact value, so one below 2^-126 gives zero even when it rounds to 2^-126, as the console's separately rounded encoding is not measured at this boundary. Denormal modes 1 and 2 throw for the other instructions. Not covered, and not measurable on RDNA2 (no such opcodes) or not measured: `v_mac_f32`, `v_mad_f32`, `v_madak_f32` and `v_madmk_f32` (rounded separately), `v_dot2_f32_f16` and `v_div_fmas_f32` (on hardware `v_div_fmas_f32` flushes only its result), f32 interpolation and `ds_add_f32` keep the previous lowering; `ds_min/max/cmpst_f32` flush both operands by the field, see the LDS float atomics entry. With f32 denormals kept (3) the results depend on the driver's default denormal behaviour, as no `DenormPreserve` execution mode is requested; `v_ldexp_f32` and the f32 compares are the exceptions. `v_cmp_X_f32` and `v_cmpx_X_f32` compare the operands as sign-magnitude integers with the NaN cases handled apart, so a denormal is not read as zero, measured on RDNA2 (gfx1036) over 256 operand pairs per opcode at IEEE=0 and IEEE=1 with f32 denormals kept and flushed. `v_ldexp_f32` preserves subnormal inputs and rounds a subnormal result in the f32 round mode of `FLOAT_MODE`, measured on RDNA2 (gfx1036) over 2163 rows in each of the four round modes at IEEE=0 and IEEE=1. An overflow rounds like any other result (infinity, or the largest finite value when the mode rounds that sign toward zero). With f32 denormals flushed it flushes a subnormal input and result, measured over the same rows in each round mode at IEEE=0 only. A device whose driver splits `Fma` into a multiply and an add flushes a product below 2^-126 before adding a non-zero addend in `v_fma_f32` and `v_fmac_f32`: `0x1fffffff * 0x1fffffff + 0x00800000` gives `0x00800000` instead of the fused `0x00ffffff` on RADV RENOIR (Mesa 26.2.4), whose compiled shader has `v_mad_f32` and no `v_fma_f32`. [F32DenormalFlush](../../../core/libs/prx/libSceAgcDriver/tests/execution/F32DenormalFlush.cpp) accepts that sum on such a device. - [v_rcp, v_rsq, v_sqrt, v_log, v_exp, v_sin and v_cos](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) (f32 and f16) use the driver's approximation for finite results, not the console's: the low bits can differ. Only NaN, invalid, zero and infinite results and the exact sin/cos zeros are chosen on the bits. Measured on RDNA2 (gfx1036) over every f16 input and 32,832 f32 inputs: RADV returns the hardware's bits for every input of rcp, rsq, sqrt and log and all but one of exp (f16 0x11c5, 1 ULP); NVIDIA (615.71.09) stays within 2 ULP except `v_log_f32` near 1.0 (next entry). [v_sin and v_cos](../../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) take the driver's sine or cosine of the radians of the exact cycle, except within 2^-12 of a turn from a zero of the function, where the result is the linear term of the exact distance to the zero: the hardware's constant 6.2831826 (0x40c90fd5) at the integers, which the hardware applies exactly to inputs below 2^-12 of a turn, and the f32 2π at the half and quarter turns, where the hardware is within 7 ULP of it. Outside those bands, a driver result more than 4 ULP from a Taylor series in turns of the exact cycle is replaced by the series, which is within 2 ULP of the true sine and cosine; the hardware is up to 9 ULP from them (gfx1035, 243,092 f32 inputs), so results stay within 13 ULP of the hardware on any driver. RADV returns the hardware's bits for every f16 input and for 32,422 sine and 32,486 cosine results of the 32,832 f32 inputs; the rest are 1 ULP off, except up to 14 ULP on either side of non-zero integers and up to 15 ULP beyond ±256 cycles, where the hardware's own reduction differs from the exact fraction, and up to 6 ULP inside the half- and quarter-turn bands. NVIDIA's own sine is within 1 ULP on 93% of the f32 inputs and up to 1,166 ULP off just outside the bands, its cosine within 163 ULP; the series replaces those results. A denormal f32 input with f32 denormals kept gives the hardware the input times 6.2831826; both hosts flush it. +- [v_ldexp_f32 in denormal modes 1 and 2](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) follows the `FLOAT_MODE` f32 denormal field: mode 1 keeps a subnormal input and flushes a result below 2^-126 to a zero of its sign in every f32 round mode, also one that rounds to 2^-126 at subnormal precision (`0x3fffffff` scaled by 2^-127); mode 2 flushes a subnormal input to a zero of its sign and rounds a subnormal result in the f32 round mode of `FLOAT_MODE`, as mode 3 does. Measured on RDNA2 (gfx1030, Radeon RX 6800, AMD Windows driver 32.0.21045.5002 through HIP) over 10,260 rows (54 inputs of both signs: zeros, subnormals, normals near 2^-126 and 1.0, the largest finite value, infinities and NaNs, each scaled by 190 exponents from -2^31 to 2^31-1) at IEEE=0 and IEEE=1 with each f32 denormal mode 0 to 3 and f32 round mode 0 to 3, DX10_CLAMP=1, f16 denormals kept, f16 round to nearest even and FP16_OVFL=0; with the `clamp` modifier the result is clamped as in the other modes. The PS5's gfx1013 is not measured. - [v_log_f32 and v_log_f16](../../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) keep the driver's log2 unless, for inputs within 1/8 of 1.0, it is more than 4 ULP from an eight-term series in x - 1, which then replaces it. `v_log_f16` goes through the same f32 lowering and is rounded to half. The series is evaluated for every `v_log` and chosen with `OpSelect` rather than a branch, so its seven fused multiply-adds and two multiplies are an unconditional cost. Measured on RDNA2 over all 3,145,728 f32 inputs within 1/8 of 1.0 (gfx1035, IEEE 0, f32 denormals flushed), the hardware is within 1 ULP of the correctly rounded log2 and the series within 2 ULP of it and within 3 ULP of the hardware (2 except at 0x3f752e47, 0x3f7baab0 and 0x3f7bc4fb), so a kept driver value is within 7 ULP of the hardware and a driver that returns the hardware's bits never triggers the gate; RADV (Mesa 26.2.4) returns the hardware's bits on the 9,280 such inputs measured on gfx1036; NVIDIA (615.71.09) was up to 23,947,836 ULP off at 1 - 2^-24 and is within 6 ULP inside the band after the change; outside it its own log2 stays up to 9 ULP off (measured up to 1.375). Over every f16 input, the change moves one `v_log_f16` result on NVIDIA (0x3bed, 1 ULP, onto the hardware's bits) and none on RADV. - [Ordered not-equal, less-than and greater-than compares](../../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) (shader recompiler) - `FPOrdNotEqual32` is emitted as `OpFUnordNotEqual`, and `FPOrdLessThan32` and `FPOrdGreaterThan32` as their ordered forms, each with both operands checked for NaN on their bits; the results are the same for every input. The AMD Windows driver 32.0.13058.8001 (Radeon 860M, RDNA 3.5) returns 1 for a NaN operand when any float form of not-equal feeds a ballot on a 32-wide subgroup (#825), and so does a less-than or greater-than against an infinity the driver sees as a constant (#3536), presumably rewritten into a not-equal by its compiler - [DB_SHADER_CONTROL.PRIMITIVE_ORDERED_PIXEL_SHADER](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) (shader recompiler) - the host pixel interlock spans the whole shader, not only up to `s_sendmsg` ORDERED_PS_DONE. From cf762829d1ba0cbe6278653a96300c53447a5dda Mon Sep 17 00:00:00 2001 From: KernelPanicNT <76663133+KernelPanicNT@users.noreply.github.com> Date: Wed, 14 Oct 2026 13:03:29 +0000 Subject: [PATCH 0120/1000] feat(libSceFont): implement sceFontCharactersRefersTextCodes and text code stepping sceFontCharactersRefersTextCodes fills a 64-byte FontTextCodes with the order and code of a text character and keeps the current, start and term characters at +0x28, +0x30 and +0x38. The traversal follows the PS4 11.00 libSceFont.sprx disassembly posted in the review of #4050, which implemented the same rules: Refers never compares the character with the term and returns null for a clustered or synthetic character, sceFontTextCodesStepNext returns null at the term after visiting it, sceFontTextCodesStepBack returns null at the start character, and both steps skip synthetic characters. Null from StepNext at the end of the chain and from StepBack at the head of the chain is assumed and not covered by the report. The signatures follow Orbital's IDA type database and SharpProspero. A null cursor or output returns null as in KytyPS5; a null text character throws. guest_font_tests gets configure_windows_unwind so the expected exception reaches the test on Windows. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceFont/CMakeLists.txt | 1 + .../libs/prx/libSceFont/include/FontTypes.hpp | 10 ++++ core/libs/prx/libSceFont/src/Text.cpp | 47 +++++++++++++++ .../libs/prx/libSceFont/src/Unimplemented.cpp | 15 ----- core/libs/tests/GuestFont.cpp | 60 +++++++++++++++++++ docs/dev/debt/libSceFont.md | 1 + 6 files changed, 119 insertions(+), 15 deletions(-) diff --git a/core/libs/prx/libSceFont/CMakeLists.txt b/core/libs/prx/libSceFont/CMakeLists.txt index 458e3e5fd7..8fa51ea749 100644 --- a/core/libs/prx/libSceFont/CMakeLists.txt +++ b/core/libs/prx/libSceFont/CMakeLists.txt @@ -5,6 +5,7 @@ if(BUILD_TESTING) add_test_executable(guest_font_tests ${LIBS_INCLUDE_DIR}/tests/GuestFont.cpp) target_include_directories(guest_font_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_font_tests PRIVATE libSceFont libSceFontFt libc) + configure_windows_unwind(guest_font_tests) add_test(NAME guest_font COMMAND guest_font_tests) add_test_executable(guest_system_font_tests ${LIBS_INCLUDE_DIR}/tests/GuestSystemFont.cpp) target_include_directories(guest_system_font_tests PRIVATE ${LIBS_INCLUDE_DIR}) diff --git a/core/libs/prx/libSceFont/include/FontTypes.hpp b/core/libs/prx/libSceFont/include/FontTypes.hpp index f5b1e9b6a9..eb1a548bb4 100644 --- a/core/libs/prx/libSceFont/include/FontTypes.hpp +++ b/core/libs/prx/libSceFont/include/FontTypes.hpp @@ -375,6 +375,15 @@ struct FontWritingLetterStep { } Components; }; +union FontTextCodes { + struct { + void* order; + std::uint32_t code; + std::uint32_t reserved; + } text; + void* systemUse[8]; +}; + struct FontWritingExtent { float top; float bottom; @@ -441,6 +450,7 @@ static_assert(sizeof(FontWriting) == 0x100); static_assert(sizeof(FontWritingProfile) == sizeof(std::uint32_t)); static_assert(sizeof(FontWritingStep) == 0x48); static_assert(sizeof(FontWritingLetterStep) == 0x40); +static_assert(sizeof(FontTextCodes) == 0x40); static_assert(sizeof(FontWritingMetrics) == 0x18); static_assert(sizeof(FontWritingLineStep) == 0x40); static_assert(offsetof(FontWritingLineStep, writingOrderer) == 0x18); diff --git a/core/libs/prx/libSceFont/src/Text.cpp b/core/libs/prx/libSceFont/src/Text.cpp index c34ed9a2dd..afc8a452f7 100644 --- a/core/libs/prx/libSceFont/src/Text.cpp +++ b/core/libs/prx/libSceFont/src/Text.cpp @@ -208,6 +208,20 @@ void AccumulateWritingMetrics(FontWritingData& writing) { writing.metrics.Extent.bottom = std::max(previous.Extent.bottom, bottom); } +constexpr std::size_t TEXT_CODES_CURRENT = 5; +constexpr std::size_t TEXT_CODES_START = 6; +constexpr std::size_t TEXT_CODES_TERM = 7; + +void StepTextCodes(FontTextCodes* textCodes, const FontTextCharacter* character) { + textCodes->text.order = character->textOrder; + textCodes->text.code = character->characterCode; + textCodes->systemUse[TEXT_CODES_CURRENT] = const_cast(character); +} + +const FontTextCharacter* TextCodesCharacter(const FontTextCodes* textCodes, std::size_t slot) { + return static_cast(textCodes->systemUse[slot]); +} + } #pragma GCC visibility push(default) @@ -551,6 +565,39 @@ FontTextCharacter* APS5_VABI sceFontCharacterRefersTextNext(const FontTextCharac return nullptr; } +FontTextCodes* APS5_VABI sceFontCharactersRefersTextCodes(const FontTextCharacter* textCharacter, const FontTextCharacter* termCharacter, FontTextCodes* textCodes) { + if (!textCodes) return nullptr; + if (!textCharacter) throw std::runtime_error("libSceFont: sceFontCharactersRefersTextCodes without a text character"); + if (textCharacter->clusterIndex != 0 || textCharacter->synthetic != 0) return nullptr; + std::memset(textCodes, 0, sizeof(FontTextCodes)); + StepTextCodes(textCodes, textCharacter); + textCodes->systemUse[TEXT_CODES_START] = const_cast(textCharacter); + textCodes->systemUse[TEXT_CODES_TERM] = const_cast(termCharacter); + return textCodes; +} + +FontTextCodes* APS5_VABI sceFontTextCodesStepNext(FontTextCodes* textCodesStep) { + if (!textCodesStep) return nullptr; + const FontTextCharacter* current = TextCodesCharacter(textCodesStep, TEXT_CODES_CURRENT); + if (!current || current == TextCodesCharacter(textCodesStep, TEXT_CODES_TERM)) return nullptr; + const FontTextCharacter* next = current->next; + while (next && next->synthetic != 0) next = next->next; + if (!next) return nullptr; + StepTextCodes(textCodesStep, next); + return textCodesStep; +} + +FontTextCodes* APS5_VABI sceFontTextCodesStepBack(FontTextCodes* textCodesStep) { + if (!textCodesStep) return nullptr; + const FontTextCharacter* current = TextCodesCharacter(textCodesStep, TEXT_CODES_CURRENT); + if (!current || current == TextCodesCharacter(textCodesStep, TEXT_CODES_START)) return nullptr; + const FontTextCharacter* previous = current->prev; + while (previous && previous->synthetic != 0) previous = previous->prev; + if (!previous) return nullptr; + StepTextCodes(textCodesStep, previous); + return textCodesStep; +} + int APS5_VABI sceFontWritingInit(FontWriting* fontWriting, FontString fontString, const FontTextCharacter* fontCharacter) { if (!fontWriting || !fontString || !fontCharacter) return SCE_FONT_ERROR_INVALID_PARAMETER; auto* data = GetStringData(fontString); diff --git a/core/libs/prx/libSceFont/src/Unimplemented.cpp b/core/libs/prx/libSceFont/src/Unimplemented.cpp index b3c2e79b7c..aa341a63b5 100644 --- a/core/libs/prx/libSceFont/src/Unimplemented.cpp +++ b/core/libs/prx/libSceFont/src/Unimplemented.cpp @@ -7,11 +7,6 @@ extern "C" { -int APS5_VABI sceFontCharactersRefersTextCodes() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceFontControl() { NotImplemented_nid_no_patch(__func__); return 0; @@ -358,16 +353,6 @@ int APS5_VABI sceFontSupportGlyphs() { return 0; } -int APS5_VABI sceFontTextCodesStepBack() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFontTextCodesStepNext() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceFontWordsFindWordCharacters() { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/tests/GuestFont.cpp b/core/libs/tests/GuestFont.cpp index fd90eb3349..53f9158dd4 100644 --- a/core/libs/tests/GuestFont.cpp +++ b/core/libs/tests/GuestFont.cpp @@ -3,10 +3,12 @@ #include #include #include +#include #include #include #include #include +#include #include #include @@ -56,6 +58,9 @@ FontTextCharacter* APS5_VABI sceFontWritingRefersRenderStepCharacter(FontWriting const void* APS5_VABI sceFontSelectLibraryFt(int); const void* APS5_VABI sceFontSelectRendererFt(int); int APS5_VABI sceFontCharacterGetSyllableStringState(const FontTextCharacter*, int*); +FontTextCodes* APS5_VABI sceFontCharactersRefersTextCodes(const FontTextCharacter*, const FontTextCharacter*, FontTextCodes*); +FontTextCodes* APS5_VABI sceFontTextCodesStepNext(FontTextCodes*); +FontTextCodes* APS5_VABI sceFontTextCodesStepBack(FontTextCodes*); } static void Check(bool value, int line) { @@ -153,6 +158,60 @@ static std::int32_t APS5_VABI ParseText(FontTextSource* source, void** order, Fo return 1; } +static void CheckTextCodes(const FontMemory* memory, FontHandle font) { + ParsedText parsed{"AVA", 0, font}; + FontTextSource source{}; + Require(sceFontTextSourceInit(&source, parsed.text, 3, ParseText, &parsed) == SCE_FONT_OK); + FontString string = nullptr; + Require(sceFontCreateString(memory, &source, nullptr, &string) == SCE_FONT_OK && string != nullptr); + std::uint32_t count = 0; + FontTextCharacter* characters = sceFontStringRefersTextCharacters(string, &count); + Require(characters != nullptr && count == 3); + + FontTextCodes codes; + std::memset(&codes, 0xa5, sizeof(codes)); + Require(sceFontCharactersRefersTextCodes(characters, nullptr, &codes) == &codes); + Require(codes.text.order == parsed.text && codes.text.code == 'A' && codes.text.reserved == 0); + Require(codes.systemUse[5] == characters && codes.systemUse[6] == characters && codes.systemUse[7] == nullptr); + Require(sceFontTextCodesStepNext(&codes) == &codes && codes.text.code == 'V' && codes.text.order == parsed.text + 1); + Require(sceFontTextCodesStepNext(&codes) == &codes && codes.text.code == 'A' && codes.text.order == parsed.text + 2); + Require(sceFontTextCodesStepNext(&codes) == nullptr && codes.text.order == parsed.text + 2); + Require(sceFontTextCodesStepBack(&codes) == &codes && codes.text.code == 'V' && codes.text.order == parsed.text + 1); + Require(sceFontTextCodesStepBack(&codes) == &codes && codes.text.code == 'A' && codes.text.order == parsed.text); + Require(sceFontTextCodesStepBack(&codes) == nullptr && codes.text.order == parsed.text); + + Require(sceFontCharactersRefersTextCodes(characters + 1, characters + 2, &codes) == &codes && codes.text.code == 'V'); + Require(sceFontTextCodesStepNext(&codes) == &codes && codes.text.order == parsed.text + 2); + Require(sceFontTextCodesStepNext(&codes) == nullptr && codes.text.order == parsed.text + 2); + Require(sceFontTextCodesStepBack(&codes) == &codes && codes.text.order == parsed.text + 1); + Require(sceFontTextCodesStepBack(&codes) == nullptr && codes.text.order == parsed.text + 1); + + Require(sceFontCharactersRefersTextCodes(characters + 2, characters + 2, &codes) == &codes && codes.text.order == parsed.text + 2); + Require(sceFontTextCodesStepNext(&codes) == nullptr && sceFontTextCodesStepBack(&codes) == nullptr); + + characters[1].synthetic = 1; + Require(sceFontCharactersRefersTextCodes(characters, nullptr, &codes) == &codes); + Require(sceFontTextCodesStepNext(&codes) == &codes && codes.text.order == parsed.text + 2); + Require(sceFontTextCodesStepBack(&codes) == &codes && codes.text.order == parsed.text); + Require(sceFontCharactersRefersTextCodes(characters + 1, nullptr, &codes) == nullptr && codes.text.order == parsed.text); + characters[1].synthetic = 0; + characters[1].clusterIndex = 1; + Require(sceFontCharactersRefersTextCodes(characters + 1, nullptr, &codes) == nullptr && codes.text.order == parsed.text); + Require(sceFontTextCodesStepNext(&codes) == &codes && codes.text.order == parsed.text + 1); + characters[1].clusterIndex = 0; + + Require(sceFontCharactersRefersTextCodes(characters, nullptr, nullptr) == nullptr); + Require(sceFontTextCodesStepNext(nullptr) == nullptr && sceFontTextCodesStepBack(nullptr) == nullptr); + bool threw = false; + try { + sceFontCharactersRefersTextCodes(nullptr, nullptr, &codes); + } catch (const std::runtime_error&) { + threw = true; + } + Require(threw && codes.text.order == parsed.text + 1); + Require(sceFontDestroyString(&string) == SCE_FONT_OK); +} + static bool KerningIs(const FontKerning& kerning, float offsetX) { return kerning.offsetX == offsetX && kerning.offsetY == 0.0f && kerning.positionX == 0.0f && kerning.positionY == 0.0f; } @@ -363,6 +422,7 @@ int main() { Require(out.letter->x == step->x && out.letter->Components.textsIndex == 1 && out.letter->Components.glyphsIndex == 1); Require(sceFontWritingRefersRenderStepCharacter(&writing, nullptr) == characters + 1); Require(sceFontDestroyString(&string) == SCE_FONT_OK); + CheckTextCodes(&memory, font); Require(sceFontCloseFont(font) == SCE_FONT_OK); Require(sceFontDestroyRenderer(&renderer) == SCE_FONT_OK && renderer == nullptr); diff --git a/docs/dev/debt/libSceFont.md b/docs/dev/debt/libSceFont.md index c2b60afaf4..15c10ccaf8 100644 --- a/docs/dev/debt/libSceFont.md +++ b/docs/dev/debt/libSceFont.md @@ -16,6 +16,7 @@ - [sceFontWritingRefersRenderStepCharacter](../../../core/libs/prx/libSceFont/src/Text.cpp) (libSceFont) - the second argument is an out pointer to the writing's letter step, as in [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/d2413fc2ebd91d1b7234197c2dd5bd48e1cb6a51/src/libs/libFont.cpp#L1882-L1896) and the [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Font/SceFont.Engine.cs) bindings. Measured on PS4 firmware 11.00 `libSceFont.sprx` (not on a PS5): exactly 8 bytes are written, the address of a step stored inside the writing, and a null writing writes null; shadPS4 and prosper treat the argument as a struct, which the measurement contradicts. The letter step lives in the writing, so the pointer stays valid as long as the writing does, and its contents are rebuilt by each call of this function. On the PS4 library repeated calls on one step walk the characters it covers and then return null; here every call returns the step's one character, so the second call differs. Assumed: each step covers one character and one glyph. PPSA06699 (Hot Shots Golf 2) dereferences the returned pointer for its character counts. - [sceFontWritingLineGetOrderingSpace](../../../core/libs/prx/libSceFont/src/WritingLine.cpp) (libSceFont) - the four outputs are written in the order the PS4 11.00 `libSceFont.sprx` writes them (line advance, leading space, space between spaced runs, trailing space); the parameter names in the [Orbital](https://github.com/AlexAltea/orbital/blob/master/tools/ida/db_types.json) IDA type database and the [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Font/SceFont.Engine.cs) bindings order them differently (head, inline, tail, advance), and the PS5 library was not measured. PPSA01325's only call uses them in the PS4 order: it computes first - second - fourth - 0.25 * third and compares that, divided by a scale, with its width limit. The PS4 library writes the trailing space through its pointer without a null check whenever the leading-space pointer is set; that call throws here - [sceFontCharacterGetSyllableStringState](../../../core/libs/prx/libSceFont/src/Text.cpp) (libSceFont) - the signature `(textCharacter, int* syllableStringState)` follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Font/SceFont.Engine.cs). Measured on PS4 firmware 11.00 `libSceFont.sprx` (not on a PS5): the state is 0 for Latin, CJK, Hangul, Arabic, Hebrew, Devanagari and the other scripts tried, and the library sets 1, 3 or 4 (never 2 on real text) for Thai while it creates the string; a null character or a null result returns `SCE_FONT_ERROR_INVALID_PARAMETER`, and a null character writes 0 to a non-null result. This returns 0 for every character outside the Thai block (U+0E00 to U+0E7F), as [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/d2413fc2ebd91d1b7234197c2dd5bd48e1cb6a51/src/libs/libFont.cpp#L1805-L1815) does, and throws for Thai characters, whose cluster states are not modelled. SharpProspero's `SceFontSyllableString` names 0 NonDetection, 1 Starting, 2 Following, 3 Depending and 4 Connecting. PPSA06699 (Hot Shots Golf 2) calls it. +- [sceFontCharactersRefersTextCodes, sceFontTextCodesStepNext and sceFontTextCodesStepBack](../../../core/libs/prx/libSceFont/src/Text.cpp) (libSceFont) - the signatures follow the [Orbital](https://github.com/AlexAltea/orbital/blob/cc408367da6a6ebbf5ac1ac559fefeadc73b2831/tools/ida/db_types.json#L679) IDA type database and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Font/SceFont.Engine.cs#L395-L406), and the 64-byte `FontTextCodes` with the order pointer at 0 and the code at 8 follows SharpProspero's [SceFontTextCodes](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Font/SceFontStructs.cs#L216-L228). The traversal follows the PS4 11.00 `libSceFont.sprx` disassembly posted in [#4050](https://github.com/boykopovar/AnyPS5/pull/4050#issuecomment-6114504202), not a PS5 measurement: the cursor keeps the current, start and term characters at +0x28, +0x30 and +0x38; `Refers` never compares the character with the term and returns null for a clustered or synthetic character; `StepNext` returns null at the term after visiting it; `StepBack` returns null when the current character is the start; both steps skip synthetic characters. Assumed and not covered by the report, which says `StepNext` returns null only at the term: `StepNext` also returns null at the end of the chain (for example with a null term), and `StepBack` at the head of the chain. A null cursor or output returns null as in [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/d2413fc2ebd91d1b7234197c2dd5bd48e1cb6a51/src/libs/libFont.cpp#L1696-L1740); a null text character throws, as its console result is unknown ### Functional From 7d42db40a3ff8cadf8a6ade7594ae7928a8eee9f Mon Sep 17 00:00:00 2001 From: KernelPanicNT <76663133+KernelPanicNT@users.noreply.github.com> Date: Wed, 14 Oct 2026 13:06:37 +0000 Subject: [PATCH 0121/1000] feat(libSceAppContent): implement sceAppContentGetAddcontDownloadProgress The signature (uint32_t service label, const NpUnifiedEntitlementLabel*, progress*) follows SharpProspero Interop/AppContent/AppContent.cs. dlc_emu dlc_content.cpp (fcf69b16) answers PARAMETER for a null label or progress and NOT_FOUND for a label that is not installed additional content. No additional content is ever installed here, as sceAppContentAddcontMount, sceAppContentAddcontEnqueueDownload and sceAppContentAddcontDelete already model, so every other call answers NOT_FOUND and the progress, whose 16-byte layout SharpProspero gives, is never written. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAppContent/Export.cpp | 7 ++++--- core/libs/tests/GuestAppContent.cpp | 10 ++++++++++ docs/dev/debt/libSceAppContent.md | 3 +-- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libSceAppContent/Export.cpp b/core/libs/prx/libSceAppContent/Export.cpp index c3b44b3d75..be7cad24d9 100644 --- a/core/libs/prx/libSceAppContent/Export.cpp +++ b/core/libs/prx/libSceAppContent/Export.cpp @@ -184,8 +184,9 @@ int APS5_VABI sceAppContentAddcontDelete(uint32_t service_label, const NpUnified return SCE_APP_CONTENT_ERROR_NOT_FOUND; } -int APS5_VABI sceAppContentGetAddcontDownloadProgress(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceAppContentGetAddcontDownloadProgress(uint32_t service_label, const NpUnifiedEntitlementLabel* entitlement_label, void* progress) { + (void)service_label; + if (!entitlement_label || !progress) return SCE_APP_CONTENT_ERROR_PARAMETER; + return SCE_APP_CONTENT_ERROR_NOT_FOUND; } } diff --git a/core/libs/tests/GuestAppContent.cpp b/core/libs/tests/GuestAppContent.cpp index 8fb8a16677..fef723221e 100644 --- a/core/libs/tests/GuestAppContent.cpp +++ b/core/libs/tests/GuestAppContent.cpp @@ -12,6 +12,7 @@ int APS5_VABI sceAppContentAddcontMount(uint32_t, const NpUnifiedEntitlementLabe int APS5_VABI sceAppContentAddcontUnmount(const AppContentMountPoint*); int APS5_VABI sceAppContentAddcontEnqueueDownload(uint32_t, const NpUnifiedEntitlementLabel*); int APS5_VABI sceAppContentAddcontDelete(uint32_t, const NpUnifiedEntitlementLabel*); +int APS5_VABI sceAppContentGetAddcontDownloadProgress(uint32_t, const NpUnifiedEntitlementLabel*, void*); int APS5_VABI sceAppContentTemporaryDataMount2(uint32_t, AppContentMountPoint*); int APS5_VABI sceAppContentTemporaryDataUnmount(const AppContentMountPoint*); int APS5_VABI sceAppContentTemporaryDataFormat(const AppContentMountPoint*); @@ -88,6 +89,15 @@ int main() { Require(sceAppContentAddcontEnqueueDownload(7, &label) == ErrorNotFound); Require(sceAppContentAddcontDelete(0, &label) == ErrorNotFound); Require(sceAppContentAddcontDelete(7, nullptr) == ErrorParameter); + unsigned char progress[64]; + std::memset(progress, 0x5a, sizeof(progress)); + unsigned char progressBefore[sizeof(progress)]; + std::memcpy(progressBefore, progress, sizeof(progress)); + Require(sceAppContentGetAddcontDownloadProgress(0, &label, progress) == ErrorNotFound); + Require(sceAppContentGetAddcontDownloadProgress(7, &label, progress) == ErrorNotFound); + Require(sceAppContentGetAddcontDownloadProgress(0, nullptr, progress) == ErrorParameter); + Require(sceAppContentGetAddcontDownloadProgress(0, &label, nullptr) == ErrorParameter); + Require(std::memcmp(progress, progressBefore, sizeof(progress)) == 0); Require(sceAppContentAddcontEnqueueDownload(0, nullptr) == ErrorParameter); Require(sceAppContentAddcontMount(0, &label, &mountPoint) == ErrorNotFound); diff --git a/docs/dev/debt/libSceAppContent.md b/docs/dev/debt/libSceAppContent.md index 957dc51912..5639476d66 100644 --- a/docs/dev/debt/libSceAppContent.md +++ b/docs/dev/debt/libSceAppContent.md @@ -3,10 +3,9 @@ ### Unknown function info - [sceAppContentDownloadDataGetAvailableSpaceKb](../../../core/libs/prx/libSceAppContent/Export.cpp) (libSceAppContent) - a title whose `param.json` declares `downloadDataSize` 0 gets 0 KB and OK; the console's behaviour for a title without a download data area is unverified (it may return an error instead). KytyPS5 does not special-case it, and shadPS4 stubs 1048576 KB -- [sceAppContentGetAddcontDownloadProgress](../../../core/libs/prx/libSceAppContent/Export.cpp) (libSceAppContent) - imported by PPSA08755, which fails to load without it; added without an implementation because its behavior is unverified. [SharpProspero](https://github.com/SvenGDK/SharpProspero) lists `(uint32_t serviceLabel, entitlementLabel, SceAppContentAddcontDownloadProgress*)`, but no PS5 reference gives the progress layout or the error codes - [sceAppContentDownloadDataFormat](../../../core/libs/prx/libSceAppContent/Export.cpp) (libSceAppContent) - empties the `/download0` directory, creating it if needed. Imported by PPSA01317. A null mount point returns `PARAMETER` as `sceAppContentTemporaryDataFormat` does, and another mount point throws as in `sceAppContentDownloadDataGetAvailableSpaceKb`; the console's error codes for both and its behaviour with files still open are unverified ### Functional -- [Additional content](../../../core/libs/prx/libSceAppContent/Export.cpp) (libSceAppContent) is not installed or mounted: `sceAppContentAddcontMount` answers `NOT_FOUND` for every entitlement label and `sceAppContentAddcontUnmount` for every mount point, as no add-content mount is ever handed out; `sceAppContentGetAddcontInfo` answers `DRM_NO_ENTITLEMENT` for every label and `sceAppContentGetAddcontInfoList` reports 0 entries; `sceAppContentAddcontEnqueueDownload` answers `PARAMETER` for a null label and `NOT_FOUND` otherwise, since no download is ever queued; `sceAppContentAddcontDelete` answers `PARAMETER` for a null label and `NOT_FOUND` otherwise, since no additional content is ever installed. The signature follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/AppContent/AppContent.cs); no reference gives the console's error codes, so both are unverified +- [Additional content](../../../core/libs/prx/libSceAppContent/Export.cpp) (libSceAppContent) is not installed or mounted: `sceAppContentAddcontMount` answers `NOT_FOUND` for every entitlement label and `sceAppContentAddcontUnmount` for every mount point, as no add-content mount is ever handed out; `sceAppContentGetAddcontInfo` answers `DRM_NO_ENTITLEMENT` for every label and `sceAppContentGetAddcontInfoList` reports 0 entries; `sceAppContentAddcontEnqueueDownload` answers `PARAMETER` for a null label and `NOT_FOUND` otherwise, since no download is ever queued; `sceAppContentAddcontDelete` answers `PARAMETER` for a null label and `NOT_FOUND` otherwise, since no additional content is ever installed. `sceAppContentGetAddcontDownloadProgress` answers `PARAMETER` for a null label or progress and `NOT_FOUND` otherwise, as [dlc_emu](https://github.com/drakmor/dlc_emu/blob/fcf69b16d46a9850578bce19dc2c1b4dc867cbb5/src/dlc_modules/dlc_content.cpp#L2328-L2341) does for a label that is not installed additional content, and never writes the progress. Its 16-byte layout (`DataSize`, then `DownloadedSize`, both 64-bit) follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/AppContent/AppContent.cs#L45-L54) and is unused here. The signatures of `sceAppContentAddcontEnqueueDownload` and `sceAppContentGetAddcontDownloadProgress` follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/AppContent/AppContent.cs#L140-L148), and the signature of `sceAppContentAddcontDelete` follows [dlc_emu](https://github.com/drakmor/dlc_emu/blob/fcf69b16d46a9850578bce19dc2c1b4dc867cbb5/src/dlc_modules/dlc_content.h#L91-L92); dlc_emu is not a console measurement and no console source gives the error codes, so the signatures and the error codes are unverified - [Temporary app-content storage](../../../core/libs/prx/libSceAppContent/Export.cpp) tracks one `/temp0` mount, preserves its files on unmount, and rejects new path resolutions until remount. Mount, format, space-query and unmount errors follow [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd43e6fddd1ab033e7980cd356d14c964905/sys/sys_path.pas); the pointer ABI is also declared by [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/AppContent/AppContent.cs). Existing open host descriptors remain usable after unmount; console behaviour with open files is unverified. Host filesystem failures still throw. From 73e843bfde9a6e0fee68c1741d632a4be629c4bc Mon Sep 17 00:00:00 2001 From: RedNixer <162635417+RedNixer@users.noreply.github.com> Date: Wed, 14 Oct 2026 20:46:29 +0200 Subject: [PATCH 0122/1000] fix(tools): accept BOM in performance summaries --- tools/performance_compare.py | 2 +- tools/tests/test_performance_compare.py | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/tools/performance_compare.py b/tools/performance_compare.py index 31131859f9..bbbf7c949d 100644 --- a/tools/performance_compare.py +++ b/tools/performance_compare.py @@ -57,7 +57,7 @@ def validate(summary): def load_summary(path, trace=None): records = [] - with Path(path).open(encoding="utf-8") as source: + with Path(path).open(encoding="utf-8-sig") as source: for line_number, line in enumerate(source, 1): if not line.strip(): continue diff --git a/tools/tests/test_performance_compare.py b/tools/tests/test_performance_compare.py index 7cba7c6deb..4b27dd7159 100644 --- a/tools/tests/test_performance_compare.py +++ b/tools/tests/test_performance_compare.py @@ -193,6 +193,14 @@ class PerformanceCompareTests(unittest.TestCase): self.assertEqual(result.returncode, 2) self.assertIn("avant-\u00e9.jsonl:1:", result.stderr) + def test_bom_summary_loads(self): + self.before.write_text(json.dumps(self.baseline) + "\n", encoding="utf-8-sig") + self.write(self.after, self.candidate) + self.assertEqual(performance_compare.load_summary(self.before), self.baseline) + result = self.run_cli("--json") + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(json.loads(result.stdout)["baseline"], self.baseline) + if __name__ == "__main__": unittest.main() From 9206b5ae4b9c72a8178217c1b7abe3ea6e3c3dc2 Mon Sep 17 00:00:00 2001 From: XairRrR Date: Wed, 14 Oct 2026 02:44:36 +0200 Subject: [PATCH 0123/1000] fix(libSceFiber): run a fiber on its own context when the context is large Since 794714983 every fiber ran on an mmap'd host stack so that host calls cannot overrun the small contexts PPSA01341 packs next to each other. A title that derives state from the stack pointer then reads the wrong memory: PPSA05684's job fibers have 512 KiB contexts at 1 MiB-aligned addresses and find their job record by masking rsp with ~0xfffff, so the job system lost track of a GameInit job and the game waited forever right after loading big4r/actor76.txt. Its 64 KiB fibers fault later on a host stack too. A fiber now gets a host stack only when its context is smaller than 64 KiB; a larger context is the stack again, as on the console. The limit sits between the 16 KiB contexts that need protection and the 64 KiB ones that need the guest stack (technical debt entry). The fiber test runs a 512 KiB fiber and checks that its stack is in its context; it fails without this change. AI-assisted: yes --- core/libs/prx/libSceFiber/Export.cpp | 4 +++- core/libs/tests/Fiber.cpp | 21 +++++++++++++++++++++ docs/dev/debt/libSceFiber.md | 1 + 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceFiber/Export.cpp b/core/libs/prx/libSceFiber/Export.cpp index a727830404..ad7af98ad8 100644 --- a/core/libs/prx/libSceFiber/Export.cpp +++ b/core/libs/prx/libSceFiber/Export.cpp @@ -35,6 +35,7 @@ static constexpr std::size_t FIBER_OPT_PARAM_SIZE = 0x80; static constexpr std::size_t FIBER_MIN_CONTEXT_SIZE = 512; static constexpr std::uint64_t FIBER_CONTEXT_FILL = 0xdeadbeefdeadbeefull; static constexpr std::size_t FIBER_HOST_STACK_MARGIN = 1u << 20; +static constexpr std::size_t FIBER_GUEST_STACK_MIN = 64u << 10; using GuestFiberEntry = void (APS5_VABI*)(std::uint64_t argOnInitialize, std::uint64_t argOnRun); @@ -327,6 +328,7 @@ static std::uint64_t* SizedWindow(const Fiber* fiber) { #ifdef _WIN32 static StackBounds FiberBounds(const Fiber* fiber) { + if (!fiber->hostStack) return {fiber->context + fiber->contextSize, fiber->context, fiber->context}; return {fiber->hostStack + fiber->hostStackBytes, fiber->hostStack + HostStackGuardBytes(), fiber->hostStack}; } @@ -417,7 +419,7 @@ int32_t APS5_VABI _sceFiberInitializeImpl_nid_postfix(FiberObject* object, const fiber->context = static_cast(addr_context); fiber->contextSize = size_context; std::strncpy(fiber->name, name, FIBER_MAX_NAME_LENGTH); - AllocateHostStack(fiber); + if (size_context < FIBER_GUEST_STACK_MIN) AllocateHostStack(fiber); fiber->contextSizeCheck = g_contextSizeCheck.load(std::memory_order_relaxed); if (fiber->contextSizeCheck) { auto* words = SizedWindow(fiber); diff --git a/core/libs/tests/Fiber.cpp b/core/libs/tests/Fiber.cpp index 248325f569..9e305c3080 100644 --- a/core/libs/tests/Fiber.cpp +++ b/core/libs/tests/Fiber.cpp @@ -211,6 +211,26 @@ void RunFrameChain() { Require(sceFiberFinalize(fiber) == 0, "finalize chain"); } +constexpr std::size_t LargeContextSize = 512 * 1024; +alignas(16) std::array g_largeContext; +FiberStorage g_largeStorage; +bool g_largeOnContext = false; + +void APS5_VABI LargeContextEntry(std::uint64_t, std::uint64_t) { + volatile unsigned char local = 0; + const auto* address = const_cast(&local); + g_largeOnContext = address >= g_largeContext.data() && address < g_largeContext.data() + g_largeContext.size(); + sceFiberReturnToThread(0, nullptr); +} + +void RunOnLargeContext() { + auto* fiber = reinterpret_cast(&g_largeStorage); + Require(_sceFiberInitializeImpl_nid_postfix(fiber, "large", LargeContextEntry, 0, g_largeContext.data(), g_largeContext.size(), nullptr, 0) == 0, "initialize large fiber"); + Require(sceFiberRun_nid_postfix(fiber, 0, nullptr) == 0, "run large fiber"); + Require(g_largeOnContext, "a fiber with a 512 KiB context runs on its context"); + Require(sceFiberFinalize(fiber) == 0, "finalize large fiber"); +} + [[gnu::noinline]] std::int32_t RunFirst(std::uint64_t* returned) { g_threadFramePointer = reinterpret_cast(__builtin_frame_address(0)); const std::int32_t result = sceFiberRun_nid_postfix(g_first, 100, returned); @@ -259,4 +279,5 @@ int main() { RunHostCallOnSmallContext(); RunFrameChain(); + RunOnLargeContext(); } diff --git a/docs/dev/debt/libSceFiber.md b/docs/dev/debt/libSceFiber.md index 0a64c52ca5..2e52af3853 100644 --- a/docs/dev/debt/libSceFiber.md +++ b/docs/dev/debt/libSceFiber.md @@ -7,3 +7,4 @@ ### Unknown function info - [_sceFiberGetThreadFramePointerAddress](../../../core/libs/prx/libSceFiber/Export.cpp) (libSceFiber, NID `0dy4JtMUcMQ`) forwards to `sceFiberGetThreadFramePointerAddress`; that the argument list matches is inferred from the export without the underscore, as for the underscore Ult exports, not measured +- [Fiber stacks](../../../core/libs/prx/libSceFiber/Export.cpp) (libSceFiber) - a fiber whose context is smaller than 64 KiB runs on an mmap'd host stack of its context size plus 1 MiB, so host calls made from it (21 KiB deep when PPSA01341 reaches SPIRV-Tools from a 16 KiB context) do not overwrite the contexts packed next to it; a larger context is the stack, as on the console. PPSA05684 needs the second: its job fibers have 64 KiB and 512 KiB contexts, the 512 KiB ones at 1 MiB-aligned addresses, and find their job record by masking the stack pointer (`~0xfffff` for the 512 KiB ones), which a host stack breaks (the game waits forever in GameInit, or faults later in a 64 KiB fiber). The 64 KiB limit is chosen between these two titles, not measured: a title with small contexts that also derives state from the stack pointer, or one whose host calls exceed what is left of a large context, is not covered From cf914b51025d1bb9339829fa03f14c314b602e5a Mon Sep 17 00:00:00 2001 From: XairRrR Date: Wed, 14 Oct 2026 05:16:59 +0200 Subject: [PATCH 0124/1000] fix(agc): write nothing for an empty register list sceAgcCbSetShRegistersDirect and the other register list writers threw on a count of 0 although the matching size functions report 0 words for it. PPSA05684 calls the writer with an empty list at its main menu and terminated. An empty list now writes no packet and returns the cursor, as its size says; a null buffer still throws. AI-assisted: yes --- core/libs/prx/libSceAgc/Command/src/Packet.cpp | 5 ++++- core/libs/prx/libSceAgc/tests/RegisterCommands.cpp | 3 +++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgc/Command/src/Packet.cpp b/core/libs/prx/libSceAgc/Command/src/Packet.cpp index 4bf5a844f4..743fed53f2 100644 --- a/core/libs/prx/libSceAgc/Command/src/Packet.cpp +++ b/core/libs/prx/libSceAgc/Command/src/Packet.cpp @@ -107,7 +107,10 @@ std::uint32_t* WriteRegisterRange(CommandBuffer* buffer, std::uint32_t opcode, s } std::uint32_t* WriteRegisters(CommandBuffer* buffer, std::uint32_t opcode, const volatile ShaderRegister* registers, std::uint32_t count, bool snapshotAll, const char* function) { - Require(count != 0, function, "empty register list"); + if (count == 0) { + Require(buffer != nullptr, function, "null command buffer"); + return buffer->cursor_up; + } CheckAddress(reinterpret_cast(registers), 4, function); std::vector snapshot; if (snapshotAll) { diff --git a/core/libs/prx/libSceAgc/tests/RegisterCommands.cpp b/core/libs/prx/libSceAgc/tests/RegisterCommands.cpp index a4ac992501..251c1d767b 100644 --- a/core/libs/prx/libSceAgc/tests/RegisterCommands.cpp +++ b/core/libs/prx/libSceAgc/tests/RegisterCommands.cpp @@ -193,6 +193,9 @@ void testDirectList() { packet = writers[i](&split.buffer, paired.data(), 4); check(std::equal(expectedSplit.begin(), expectedSplit.end(), packet) && split.buffer.cursor_up == packet + expectedSplit.size(), "register pairs were not written as two ranges"); check(sizes[i](0) == 0, "an empty register list has a size"); + Storage empty; + empty.words.fill(sentinel); + check(writers[i](&empty.buffer, nullptr, 0) == empty.words.data() && empty.buffer.cursor_up == empty.words.data() && empty.words.front() == sentinel, "an empty register list wrote a packet"); check(sizes[i](0x15555555u) == 0xfffffffcu, "largest register list size mismatch"); for (const auto count : {0x15555556u, 0x40000000u, 0xffffffffu}) { expectFailure([&] { sizes[i](count); }); From 5b49ab30b28d0ea66fea24924a69fab1e8dec8e4 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Wed, 14 Oct 2026 19:00:29 +0000 Subject: [PATCH 0125/1000] fix(agc): show the last frame's numbers in the performance overlay below 1 fps Below 1 fps no one-second window holds two presents, so the overlay showed FPS: 0.0, GPU: 0.0 MS and DRIVER: 0.0 MS, which is the common case for titles that run slowly today. When the last second holds fewer than two presents, FPS is now the last frame's rate, 1000 divided by its time in ms, and GPU and DRIVER are that frame's growth of each total. With two or more presents in the last second the numbers are the last second's as before, so the two modes meet at 1 fps. FPS below 1 is shown with two decimals: a frame every 2 s reads 0.50 and one every 12 s reads 0.08. Times keep their format and their 9999 cap, which USAGE.md now states for frames of 10 s or more. Nothing changes while the overlay is hidden. --- .../Execution/include/PerformanceOverlay.hpp | 3 + .../Execution/src/PerformanceOverlay.cpp | 24 +++++-- .../tests/PerformanceOverlayStatistics.cpp | 69 ++++++++++++++++++- docs/user/USAGE.md | 8 +-- 4 files changed, 94 insertions(+), 10 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/PerformanceOverlay.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/PerformanceOverlay.hpp index f99d27af9f..63f8cc5aad 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/PerformanceOverlay.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/PerformanceOverlay.hpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -72,6 +73,7 @@ private: double perFrameMs(std::uint64_t Sample::*counter) const; std::deque window; + std::optional previous; std::array frameTimes{}; std::size_t frameCount = 0; std::size_t frameNext = 0; @@ -115,6 +117,7 @@ struct OverlayMetrics { const std::array& OverlayGlyph(char character); std::string FormatOverlayValue(double value); +std::string FormatOverlayRate(double value); OverlayContent ComposeOverlay(const OverlayStatistics& statistics, bool gpu, bool driver); std::int32_t OverlayScale(VkExtent2D extent, const OverlayContent& content); OverlayLayout LayOutOverlay(const OverlayContent& content, VkExtent2D extent, OverlayCorner corner); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceOverlay.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceOverlay.cpp index f9218396a3..ff5e043363 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceOverlay.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceOverlay.cpp @@ -113,6 +113,7 @@ void OverlayStatistics::AddPresent(Clock::time_point at, std::uint64_t gpuNanose frameTimes[frameNext] = std::chrono::duration(at - window.back().at).count(); frameNext = (frameNext + 1) % FrameHistory; frameCount = std::min(frameCount + 1, FrameHistory); + previous = window.back(); } window.push_back({at, gpuNanoseconds, driverNanoseconds}); while (at - window.front().at > Window) window.pop_front(); @@ -120,13 +121,17 @@ void OverlayStatistics::AddPresent(Clock::time_point at, std::uint64_t gpuNanose void OverlayStatistics::Reset() { window.clear(); + previous.reset(); frameTimes = {}; frameCount = 0; frameNext = 0; } double OverlayStatistics::Fps() const { - if (window.size() < 2) return 0; + if (window.size() < 2) { + const auto frameMs = LastFrameMs(); + return frameMs > 0 ? 1000.0 / frameMs : 0; + } const auto seconds = std::chrono::duration(window.back().at - window.front().at).count(); return seconds > 0 ? static_cast(window.size() - 1) / seconds : 0; } @@ -157,10 +162,12 @@ OverlayStatistics::Range OverlayStatistics::FrameTimeRange() const { } double OverlayStatistics::perFrameMs(std::uint64_t Sample::*counter) const { - if (window.size() < 2) return 0; - const auto first = window.front().*counter; + const bool windowed = window.size() >= 2; + if (!windowed && !previous) return 0; + const auto first = windowed ? window.front().*counter : (*previous).*counter; const auto last = window.back().*counter; - return last >= first ? static_cast(last - first) / 1e6 / static_cast(window.size() - 1) : 0; + const auto frames = windowed ? window.size() - 1 : std::size_t{1}; + return last >= first ? static_cast(last - first) / 1e6 / static_cast(frames) : 0; } double OverlayStatistics::GpuMsPerFrame() const { @@ -234,9 +241,16 @@ std::string FormatOverlayValue(double value) { return text; } +std::string FormatOverlayRate(double value) { + if (!std::isfinite(value) || value <= 0 || value >= 0.995) return FormatOverlayValue(value); + char text[16]; + std::snprintf(text, sizeof(text), "%.2f", value); + return text; +} + OverlayContent ComposeOverlay(const OverlayStatistics& statistics, bool gpu, bool driver) { OverlayContent content; - content.above.push_back("FPS: " + FormatOverlayValue(statistics.Fps())); + content.above.push_back("FPS: " + FormatOverlayRate(statistics.Fps())); content.above.push_back("FRAME: " + FormatOverlayValue(statistics.LastFrameMs()) + " MS"); content.frameTimes = statistics.FrameTimes(); const auto range = statistics.FrameTimeRange(); diff --git a/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlayStatistics.cpp b/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlayStatistics.cpp index 86ecedbb5b..504da82594 100644 --- a/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlayStatistics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlayStatistics.cpp @@ -38,7 +38,7 @@ void fpsWindowTests() { statistics.AddPresent(Start + 1700ms, 0, 0); require(near(statistics.Fps(), 51.0), "after a 500 ms stall the last second holds 52 presents, read as " + std::to_string(statistics.Fps()) + " FPS, not 51"); statistics.AddPresent(Start + 3700ms, 0, 0); - require(statistics.Fps() == 0, "presents older than 1 s still count after a 2 s stall"); + require(near(statistics.Fps(), 0.5), "after a 2 s stall the last second holds one present, read as " + std::to_string(statistics.Fps()) + " FPS, not the last frame's 0.5"); statistics.AddPresent(Start + 3750ms, 0, 0); require(near(statistics.Fps(), 20.0), "two presents 50 ms apart read as " + std::to_string(statistics.Fps()) + " FPS, not 20"); bool refused = false; @@ -86,6 +86,72 @@ void counterTests() { void formatTests() { const std::pair cases[] = {{59.94, "59.9"}, {0, "0.0"}, {99.94, "99.9"}, {99.96, "100"}, {144.4, "144"}, {12345, "9999"}, {-3, "0.0"}, {std::nan(""), "0.0"}}; for (const auto& [value, expected] : cases) require(AgcDriver::FormatOverlayValue(value) == expected, std::to_string(value) + " is shown as '" + AgcDriver::FormatOverlayValue(value) + "', not '" + expected + "'"); + const std::pair rates[] = {{0.5, "0.50"}, {1000.0 / 12000.0, "0.08"}, {0.05, "0.05"}, {0.004, "0.00"}, {0.994, "0.99"}, {0.996, "1.0"}, {1.5, "1.5"}, {59.94, "59.9"}, {144.4, "144"}, {12345, "9999"}, {0, "0.0"}, {-1, "0.0"}, {std::nan(""), "0.0"}}; + for (const auto& [value, expected] : rates) require(AgcDriver::FormatOverlayRate(value) == expected, "a rate of " + std::to_string(value) + " is shown as '" + AgcDriver::FormatOverlayRate(value) + "', not '" + expected + "'"); +} + +void lowRateTests() { + OverlayStatistics slow; + constexpr std::uint64_t Gpu[] = {30'000'000u, 45'000'000u, 20'000'000u}; + constexpr std::uint64_t Driver[] = {12'000'000u, 20'000'000u, 8'000'000u}; + std::uint64_t gpuTotal = 0; + std::uint64_t driverTotal = 0; + slow.AddPresent(Start, 0, 0); + require(slow.Fps() == 0 && slow.GpuMsPerFrame() == 0 && slow.DriverMsPerFrame() == 0, "a single present reads as a frame"); + for (int i = 0; i < 3; ++i) { + gpuTotal += Gpu[i]; + driverTotal += Driver[i]; + slow.AddPresent(Start + (i + 1) * 2s, gpuTotal, driverTotal); + require(near(slow.Fps(), 0.5) && near(slow.LastFrameMs(), 2000), "a present every 2 s reads as " + std::to_string(slow.Fps()) + " FPS and " + std::to_string(slow.LastFrameMs()) + " ms, not 0.5 and 2000"); + require(near(slow.GpuMsPerFrame(), static_cast(Gpu[i]) / 1e6) && near(slow.DriverMsPerFrame(), static_cast(Driver[i]) / 1e6), "frame " + std::to_string(i + 1) + " every 2 s reads " + std::to_string(slow.GpuMsPerFrame()) + " ms of GPU and " + std::to_string(slow.DriverMsPerFrame()) + " ms of driver time, not its own"); + } + const auto everyTwoSeconds = AgcDriver::ComposeOverlay(slow, true, true); + require(everyTwoSeconds.above[0] == "FPS: 0.50" && everyTwoSeconds.above[1] == "FRAME: 2000 MS", "a present every 2 s shows '" + everyTwoSeconds.above[0] + "' and '" + everyTwoSeconds.above[1] + "'"); + require(everyTwoSeconds.below[0] == "MIN/AVG/MAX: 2000/2000/2000" && everyTwoSeconds.below[1] == "GPU: 20.0 MS" && everyTwoSeconds.below[2] == "DRIVER: 8.0 MS", "a present every 2 s shows '" + everyTwoSeconds.below[0] + "', '" + everyTwoSeconds.below[1] + "' and '" + everyTwoSeconds.below[2] + "'"); + + OverlayStatistics glacial; + glacial.AddPresent(Start, 0, 0); + for (int i = 1; i <= 5; ++i) glacial.AddPresent(Start + i * 12s, static_cast(i) * 250'000'000u, static_cast(i) * 12'500'000'000u); + require(near(glacial.Fps(), 1000.0 / 12000.0) && near(glacial.LastFrameMs(), 12000) && near(glacial.GpuMsPerFrame(), 250) && near(glacial.DriverMsPerFrame(), 12500), "a present every 12 s reads as " + std::to_string(glacial.Fps()) + " FPS, " + std::to_string(glacial.LastFrameMs()) + " ms, " + std::to_string(glacial.GpuMsPerFrame()) + " and " + std::to_string(glacial.DriverMsPerFrame()) + " ms"); + const auto everyTwelveSeconds = AgcDriver::ComposeOverlay(glacial, true, true); + require(everyTwelveSeconds.above[0] == "FPS: 0.08" && everyTwelveSeconds.above[1] == "FRAME: 9999 MS", "a present every 12 s shows '" + everyTwelveSeconds.above[0] + "' and '" + everyTwelveSeconds.above[1] + "'"); + require(everyTwelveSeconds.below[0] == "MIN/AVG/MAX: 9999/9999/9999" && everyTwelveSeconds.below[1] == "GPU: 250 MS" && everyTwelveSeconds.below[2] == "DRIVER: 9999 MS", "a present every 12 s shows '" + everyTwelveSeconds.below[0] + "', '" + everyTwelveSeconds.below[1] + "' and '" + everyTwelveSeconds.below[2] + "'"); + require(everyTwelveSeconds.frameTimes == std::vector(5, 12000.0), "the graph does not hold the five 12 s frames"); + const auto layout = AgcDriver::LayOutOverlay(everyTwelveSeconds, {1920, 1080}, OverlayCorner::TopLeft); + std::size_t bars = 0; + for (const auto& rect : layout.rects) { + if (rect.color != static_cast(AgcDriver::OverlayColor::Graph)) continue; + ++bars; + require(rect.height == OverlayMetrics::GraphHeight * layout.scale / 2, "steady 12 s frames do not reach half the graph"); + } + require(bars == 5, "the graph has " + std::to_string(bars) + " bars for five 12 s frames"); + + OverlayStatistics rising; + auto at = Start; + std::uint64_t total = 0; + rising.AddPresent(at, 0, 0); + const auto step = [&](Clock::duration interval, std::uint64_t gpuNanoseconds) { + at += interval; + total += gpuNanoseconds; + rising.AddPresent(at, total, 0); + }; + const auto check = [&](double fps, double gpuMs, const char* shown, const std::string& when) { + require(near(rising.Fps(), fps) && near(rising.GpuMsPerFrame(), gpuMs) && AgcDriver::FormatOverlayRate(rising.Fps()) == shown, when + " reads as " + std::to_string(rising.Fps()) + " FPS ('" + AgcDriver::FormatOverlayRate(rising.Fps()) + "') and " + std::to_string(rising.GpuMsPerFrame()) + " ms of GPU time, not " + std::to_string(fps) + " ('" + shown + "') and " + std::to_string(gpuMs)); + }; + step(2s, 40'000'000u); + check(0.5, 40, "0.50", "a 2 s frame"); + step(1250ms, 30'000'000u); + check(0.8, 30, "0.80", "a 1.25 s frame"); + step(1001ms, 20'000'000u); + check(1000.0 / 1001.0, 20, "1.0", "a 1.001 s frame, the last second holding one present"); + step(1000ms, 10'000'000u); + check(1.0, 10, "1.0", "a 1 s frame, the last second holding two presents"); + step(750ms, 6'000'000u); + check(1.0 / 0.75, 6, "1.3", "a 750 ms frame"); + for (int i = 0; i < 8; ++i) step(250ms, i % 2 == 0 ? 2'000'000u : 6'000'000u); + check(4.0, 4.0, "4.0", "four presents a second, alternating 2 and 6 ms of GPU time"); + step(3s, 9'000'000u); + check(1.0 / 3.0, 9, "0.33", "a 3 s frame after the fast ones"); } void settingsTests() { @@ -225,6 +291,7 @@ int main() { frameRingTests(); counterTests(); formatTests(); + lowRateTests(); settingsTests(); composeTests(); layoutTests(); diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index c169a8b495..8c561c4459 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -190,14 +190,14 @@ F10 shows or hides a performance overlay over the presented image; `ANYPS5_OVERL | Line | Meaning | |------|---------| -| `FPS` | Frames presented per second, over the last second. Every present counts, including frames a title presents cleared, for example while it loads. | +| `FPS` | Frames presented per second, over the last second. When the last second holds fewer than two presents (below 1 fps), the rate of the last frame instead, 1000 divided by its time in ms: a frame every 2 s shows `0.50`. Every present counts, including frames a title presents cleared, for example while it loads. | | `FRAME` | Time between the last two presented frames, in ms. | | graph | The last 120 frame times, the newest on the right. The full height is the longest of them or twice their average, whichever is larger. | | `MIN/AVG/MAX` | The shortest, average and longest of the 120 frame times, in ms. | -| `GPU` | GPU time per frame over the last second, in ms: the span of each recorded batch from its first to its last command, from the timestamp queries of `APS5_PROFILE_GPU=1`. Shown only with that variable. Batches are counted when they complete, so the line trails a sudden change by up to a second. Batches that overlap on different queues each count in full, so the line can exceed `FRAME`. | -| `DRIVER` | CPU time per frame over the last second, in ms, that the packet workers spent in dispatch and draw packets (the `dispatch` and `draw` time of the `[gpu] worker` line of `APS5_PROFILE_DRAW=1`). Shown only with that variable. | +| `GPU` | GPU time per frame over the last second, or of the last frame below 1 fps, in ms: the span of each recorded batch from its first to its last command, from the timestamp queries of `APS5_PROFILE_GPU=1`. Shown only with that variable. Batches are counted when they complete, so the line trails a sudden change by up to a second, or by a frame below 1 fps. Batches that overlap on different queues each count in full, so the line can exceed `FRAME`. | +| `DRIVER` | CPU time per frame over the last second, or of the last frame below 1 fps, in ms, that the packet workers spent in dispatch and draw packets (the `dispatch` and `draw` time of the `[gpu] worker` line of `APS5_PROFILE_DRAW=1`). Shown only with that variable. | -Times are shown with one decimal below 100 and as whole numbers above, up to 9999. While the overlay is hidden the presentation records nothing for it and the presented image is unchanged; while it is shown, one small render pass draws it over each presented frame. +Times are shown with one decimal below 100 and as whole numbers above; anything above 9999 shows as 9999, so `FRAME` and `MIN/AVG/MAX` read 9999 for frames of 10 s or more. `FPS` has the same format, with two decimals below 1. While the overlay is hidden the presentation records nothing for it and the presented image is unchanged; while it is shown, one small render pass draws it over each presented frame. ### Windows crash diagnostics From 17ded56f12aa22668f97c28148113475780f4659 Mon Sep 17 00:00:00 2001 From: Evgeniy Kazarin Date: Wed, 14 Oct 2026 18:23:57 +0300 Subject: [PATCH 0126/1000] feat(libkernel): apply SO_RCVTIMEO and SO_SNDTIMEO on a guest socket Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libkernel/CMakeLists.txt | 10 ++ .../Socket/include/SocketOptions.hpp | 2 + .../libkernel/Socket/src/SocketOptions.cpp | 30 ++++ core/libs/prx/libkernel/Socket/src/Udp.cpp | 35 ++++ core/libs/tests/GuestSocketTimeouts.cpp | 167 ++++++++++++++++++ docs/dev/debt/libkernel.md | 2 +- 6 files changed, 245 insertions(+), 1 deletion(-) create mode 100644 core/libs/tests/GuestSocketTimeouts.cpp diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 88c4a75c76..ab597ba71d 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -1078,6 +1078,16 @@ if(BUILD_TESTING) set_tests_properties(guest_socket_stream_options PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_socket_timeouts_tests ${LIBS_INCLUDE_DIR}/tests/GuestSocketTimeouts.cpp) + target_include_directories(guest_socket_timeouts_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_socket_timeouts_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_socket_timeouts_tests) + add_test(NAME guest_socket_timeouts COMMAND guest_socket_timeouts_tests) + set_tests_properties(guest_socket_timeouts PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_socket_timeouts PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_udp_tests ${LIBS_INCLUDE_DIR}/tests/GuestUdp.cpp) target_include_directories(guest_udp_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_udp_tests PRIVATE libkernel libc) diff --git a/core/libs/prx/libkernel/Socket/include/SocketOptions.hpp b/core/libs/prx/libkernel/Socket/include/SocketOptions.hpp index 4597fbff2d..c81d93d46b 100644 --- a/core/libs/prx/libkernel/Socket/include/SocketOptions.hpp +++ b/core/libs/prx/libkernel/Socket/include/SocketOptions.hpp @@ -7,6 +7,8 @@ struct SocketOptionState { int reportedError = 0; bool nonblocking = false; bool noSignal = false; + std::int64_t sendTimeout = 0; + std::int64_t receiveTimeout = 0; }; int SetSocketLevelOption(std::uintptr_t native, SocketOptionState& state, int option, const void* value, std::uint32_t length); diff --git a/core/libs/prx/libkernel/Socket/src/SocketOptions.cpp b/core/libs/prx/libkernel/Socket/src/SocketOptions.cpp index 78f7720167..184219224d 100644 --- a/core/libs/prx/libkernel/Socket/src/SocketOptions.cpp +++ b/core/libs/prx/libkernel/Socket/src/SocketOptions.cpp @@ -26,6 +26,8 @@ constexpr int GuestOobInline = 0x100; constexpr int GuestNoSigpipe = 0x800; constexpr int GuestSendBuffer = 0x1001; constexpr int GuestReceiveBuffer = 0x1002; +constexpr int GuestSendTimeout = 0x1005; +constexpr int GuestReceiveTimeout = 0x1006; constexpr int GuestError = 0x1007; constexpr int GuestType = 0x1008; constexpr int GuestNonBlocking = 0x1200; @@ -36,12 +38,27 @@ constexpr int ErrorInvalid = 22; constexpr int ErrorDomain = 33; constexpr int ErrorNoProtocolOption = 42; constexpr int MaximumLinger = 65535; +constexpr std::int64_t LongestTimeout = 0x7fffffffffffffffLL; struct GuestLingerValue { std::int32_t enabled; std::int32_t seconds; }; +struct GuestTimeval { + std::int64_t seconds; + std::int64_t microseconds; +}; + +std::int64_t TimeoutOf(const GuestTimeval& time) { + if (time.seconds > INT32_MAX) return LongestTimeout; + return (time.seconds << 32) + static_cast((static_cast(time.microseconds) * ((std::uint64_t{1} << 63) / 500000)) >> 32); +} + +GuestTimeval TimevalOf(std::int64_t timeout) { + return {timeout >> 32, (1000000 * static_cast(static_cast(timeout))) >> 32}; +} + #ifdef _WIN32 using NativeHandle = SOCKET; using NativeOptionValue = const char*; @@ -95,6 +112,15 @@ int SetSocketLevelOption(std::uintptr_t native, SocketOptionState& state, int op if (::setsockopt(handle, SOL_SOCKET, SO_LINGER, reinterpret_cast(&request), sizeof(request))) return HostError(); return 0; } + if (option == GuestSendTimeout || option == GuestReceiveTimeout) { + if (length < sizeof(GuestTimeval)) return ErrorInvalid; + if (!value) return ErrorInvalid; + GuestTimeval time; + std::memcpy(&time, value, sizeof(time)); + if (time.seconds < 0 || time.microseconds < 0 || time.microseconds >= 1000000) return ErrorDomain; + (option == GuestSendTimeout ? state.sendTimeout : state.receiveTimeout) = TimeoutOf(time); + return 0; + } if (option == GuestNoSigpipe || option == GuestNonBlocking) { if (length < sizeof(int)) return ErrorInvalid; if (!value) return ErrorInvalid; @@ -142,6 +168,10 @@ int GetSocketLevelOption(std::uintptr_t native, SocketOptionState& state, int ty const GuestLingerValue guest{current.l_onoff != 0 ? GuestLinger : 0, static_cast(current.l_linger)}; return Copy(value, length, &guest, sizeof(guest)); } + if (option == GuestSendTimeout || option == GuestReceiveTimeout) { + const GuestTimeval time = TimevalOf(option == GuestSendTimeout ? state.sendTimeout : state.receiveTimeout); + return Copy(value, length, &time, sizeof(time)); + } int number = 0; if (const int nativeOption = NativeBoolean(option); nativeOption != -1) { int enabled = 0; diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index f91667d212..7588023bb7 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -563,6 +563,34 @@ int SocketSendFlags(Socket& socket, int flags) { std::lock_guard lock(socket.modeMutex); return NativeSendFlags(socket.options.noSignal ? flags | GuestNoSignal : flags); } +int WaitForTransfer(Socket& socket, bool send, int flags) { + if ((flags & GuestDontWait) != 0) return 0; + std::int64_t timeout; + { + std::lock_guard lock(socket.modeMutex); + if (socket.options.nonblocking) return 0; + timeout = send ? socket.options.sendTimeout : socket.options.receiveTimeout; + } + if (timeout == 0) return 0; + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(timeout >> 32) + + std::chrono::nanoseconds((static_cast(static_cast(timeout)) * 1000000000) >> 32); + for (;;) { + const auto left = deadline - std::chrono::steady_clock::now(); + if (left <= std::chrono::steady_clock::duration::zero()) return 35; + const int milliseconds = static_cast(std::min(std::chrono::ceil(left).count(), INT_MAX)); + NativePollDescriptor entry{}; + entry.fd = socket.value; +#ifdef _WIN32 + entry.events = static_cast(send ? POLLWRNORM : POLLRDNORM); + const int ready = WSAPoll(&entry, 1, milliseconds); + if (ready != 0) return 0; +#else + entry.events = static_cast(send ? POLLOUT : POLLIN); + const int ready = ::poll(&entry, 1, milliseconds); + if (ready > 0 || (ready < 0 && errno != EINTR)) return 0; +#endif + } +} #ifdef _WIN32 bool LoopbackName(NativeSocket socket, sockaddr_in& name) { int size = sizeof(name); @@ -959,6 +987,8 @@ int APS5_VABI accept_nid_postfix(int descriptor, void* address, std::uint32_t* l std::lock_guard modeLock(listener->modeMutex); inheritNonblocking = listener->options.nonblocking; accepted->options.noSignal = listener->options.noSignal; + accepted->options.sendTimeout = listener->options.sendTimeout; + accepted->options.receiveTimeout = listener->options.receiveTimeout; } if (inheritNonblocking && SetNonBlocking(*accepted)) return -1; const int acceptedDescriptor = Install(std::move(accepted)); @@ -975,6 +1005,7 @@ std::int64_t APS5_VABI send_nid_postfix(int descriptor, const void* buffer, std: if ((flags & ~GuestSendFlags) != 0) return Fail(45); if (length > INT_MAX) return Fail(40); if (!buffer && length) return Fail(14); + if (const int waited = WaitForTransfer(*socket, true, flags)) return Fail(waited); const auto result = ::send(socket->value, static_cast(buffer), static_cast(length), SocketSendFlags(*socket, flags)); return result < 0 ? Fail(SendError(socket->type)) : result; } @@ -986,6 +1017,7 @@ std::int64_t APS5_VABI recv_nid_postfix(int descriptor, void* buffer, std::uint6 if (!buffer && length) return Fail(14); const GuestArena::HostWrite destination(length == 0 ? nullptr : buffer, static_cast(length)); if (!destination.Open()) return Fail(14); + if (const int waited = WaitForTransfer(*socket, false, flags)) return Fail(waited); const auto result = ::recv(socket->value, static_cast(buffer), static_cast(length), NativeReceiveFlags(flags)); #ifdef _WIN32 @@ -1072,6 +1104,7 @@ std::int64_t APS5_VABI sendto_nid_postfix(int descriptor, const void* buffer, st if ((flags & ~GuestSendFlags) != 0) return Fail(45); if (length > INT_MAX) return Fail(40); if (!buffer && length) return Fail(14); + if (const int waited = WaitForTransfer(*socket, true, flags)) return Fail(waited); int result; if (!address) { if (addressLength != 0) return Fail(22); @@ -1097,6 +1130,7 @@ std::int64_t APS5_VABI recvfrom_nid_postfix(int descriptor, void* buffer, std::u if (!destination.Open()) return Fail(14); sockaddr_storage native{}; socklen_t size = sizeof(native); + if (const int waited = WaitForTransfer(*socket, false, flags)) return Fail(waited); auto result = ::recvfrom(socket->value, static_cast(buffer), static_cast(length), NativeReceiveFlags(flags), reinterpret_cast(&native), &size); #ifdef _WIN32 @@ -1172,6 +1206,7 @@ std::int64_t APS5_VABI recvmsg_nid_postfix(int descriptor, GuestMsghdr* message, } sockaddr_storage native{}; socklen_t size = sizeof(native); + if (const int waited = WaitForTransfer(*socket, false, flags)) return Fail(waited); std::int64_t received = ::recvfrom(socket->value, buffer.data(), static_cast(buffer.size()), NativeReceiveFlags(flags), reinterpret_cast(&native), &size); if (received < 0) { diff --git a/core/libs/tests/GuestSocketTimeouts.cpp b/core/libs/tests/GuestSocketTimeouts.cpp new file mode 100644 index 0000000000..b475463949 --- /dev/null +++ b/core/libs/tests/GuestSocketTimeouts.cpp @@ -0,0 +1,167 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include + +struct Timeval { + std::int64_t seconds; + std::int64_t microseconds; +}; + +struct Iovec { + void* base; + std::uint64_t length; +}; + +struct Msghdr { + void* name; + std::uint32_t nameLength; + Iovec* iov; + int iovLength; + void* control; + std::uint32_t controlLength; + int flags; +}; + +extern "C" { +int APS5_VABI socket_nid_postfix(int, int, int); +int APS5_VABI bind_nid_postfix(int, const void*, std::uint32_t); +int APS5_VABI listen_nid_postfix(int, int); +int APS5_VABI getsockname_nid_postfix(int, void*, std::uint32_t*); +int APS5_VABI connect_nid_postfix(int, const void*, std::uint32_t); +int APS5_VABI accept_nid_postfix(int, void*, std::uint32_t*); +int APS5_VABI setsockopt_nid_postfix(int, int, int, const void*, std::uint32_t); +int APS5_VABI getsockopt_nid_postfix(int, int, int, void*, std::uint32_t*); +int APS5_VABI close_nid_postfix(int); +std::int64_t APS5_VABI send_nid_postfix(int, const void*, std::uint64_t, int); +std::int64_t APS5_VABI sendto_nid_postfix(int, const void*, std::uint64_t, int, const void*, std::uint32_t); +std::int64_t APS5_VABI recv_nid_postfix(int, void*, std::uint64_t, int); +std::int64_t APS5_VABI recvfrom_nid_postfix(int, void*, std::uint64_t, int, void*, std::uint32_t*); +std::int64_t APS5_VABI recvmsg_nid_postfix(int, Msghdr*, int); +std::int64_t APS5_VABI read_nid_postfix(int, void*, std::uint64_t); +int* APS5_VABI __error_nid_postfix(); +} + +static void Check(bool value, int line) { + if (!value) { + std::fprintf(stderr, "socket timeout check failed at line %d, errno %d\n", line, *__error_nid_postfix()); + std::abort(); + } +} +#define Require(value) Check((value), __LINE__) + +constexpr int SocketLevel = 0xffff; +constexpr int TcpLevel = 6; +constexpr int SendBuffer = 0x1001; +constexpr int ReceiveBuffer = 0x1002; +constexpr int SendTimeout = 0x1005; +constexpr int ReceiveTimeout = 0x1006; +constexpr int DontWait = 0x80; +constexpr int Invalid = 22; +constexpr int Domain = 33; +constexpr int WouldBlock = 35; +constexpr int NoProtocolOption = 42; + +using Clock = std::chrono::steady_clock; + +static bool Fails(std::int64_t result, int error) { + return result == -1 && *__error_nid_postfix() == error; +} + +static int Set(int descriptor, int option, std::int64_t seconds, std::int64_t microseconds) { + const Timeval time{seconds, microseconds}; + return setsockopt_nid_postfix(descriptor, SocketLevel, option, &time, sizeof(time)); +} + +static bool Reads(int descriptor, int option, std::int64_t seconds, std::int64_t microseconds) { + Timeval time{-1, -1}; + std::uint32_t length = sizeof(time); + return getsockopt_nid_postfix(descriptor, SocketLevel, option, &time, &length) == 0 && length == sizeof(time) && + time.seconds == seconds && time.microseconds == microseconds; +} + +int main() { + const int datagram = socket_nid_postfix(2, 2, 0); + std::array own{16, 2, 0, 0, 127, 0, 0, 1}; + Require(datagram >= 0 && bind_nid_postfix(datagram, own.data(), own.size()) == 0); + std::uint32_t ownSize = own.size(); + Require(getsockname_nid_postfix(datagram, own.data(), &ownSize) == 0); + + Require(Reads(datagram, ReceiveTimeout, 0, 0) && Reads(datagram, SendTimeout, 0, 0)); + Require(Set(datagram, ReceiveTimeout, 2, 0) == 0 && Reads(datagram, ReceiveTimeout, 2, 0)); + Require(Reads(datagram, SendTimeout, 0, 0)); + Require(Set(datagram, SendTimeout, 1, 500000) == 0 && Reads(datagram, SendTimeout, 1, 499999)); + Require(Set(datagram, SendTimeout, 0, 1) == 0 && Reads(datagram, SendTimeout, 0, 0)); + Require(Set(datagram, SendTimeout, std::int64_t{1} << 31, 7) == 0 && Reads(datagram, SendTimeout, 0x7fffffff, 999999)); + Require(Set(datagram, SendTimeout, 0, 0) == 0 && Reads(datagram, SendTimeout, 0, 0)); + Require(Fails(Set(datagram, ReceiveTimeout, -1, 0), Domain)); + Require(Fails(Set(datagram, ReceiveTimeout, 0, -1), Domain)); + Require(Fails(Set(datagram, ReceiveTimeout, 0, 1000000), Domain)); + Timeval time{0, 0}; + Require(Fails(setsockopt_nid_postfix(datagram, SocketLevel, ReceiveTimeout, &time, sizeof(time) - 1), Invalid)); + Require(Reads(datagram, ReceiveTimeout, 2, 0)); + std::int64_t seconds = -1; + std::uint32_t length = sizeof(seconds); + Require(getsockopt_nid_postfix(datagram, SocketLevel, ReceiveTimeout, &seconds, &length) == 0 && length == sizeof(seconds) && seconds == 2); + length = sizeof(time); + Require(Fails(getsockopt_nid_postfix(datagram, TcpLevel, ReceiveTimeout, &time, &length), Invalid)); + + char byte = 0; + Require(Set(datagram, ReceiveTimeout, 0, 60000) == 0); + auto started = Clock::now(); + Require(Fails(recv_nid_postfix(datagram, &byte, sizeof(byte), 0), WouldBlock)); + Require(Clock::now() - started >= std::chrono::milliseconds(59)); + Require(Fails(recvfrom_nid_postfix(datagram, &byte, sizeof(byte), 0, nullptr, nullptr), WouldBlock)); + Iovec part{&byte, sizeof(byte)}; + Msghdr message{nullptr, 0, &part, 1, nullptr, 0, 0}; + Require(Fails(recvmsg_nid_postfix(datagram, &message, 0), WouldBlock)); + Require(Fails(read_nid_postfix(datagram, &byte, sizeof(byte)), WouldBlock)); +#ifndef _WIN32 + Require(Set(datagram, ReceiveTimeout, 60, 0) == 0); + Require(Fails(recv_nid_postfix(datagram, &byte, sizeof(byte), DontWait), WouldBlock)); + Require(Fails(recvfrom_nid_postfix(datagram, &byte, sizeof(byte), DontWait, nullptr, nullptr), WouldBlock)); + Require(Fails(recvmsg_nid_postfix(datagram, &message, DontWait), WouldBlock)); + Require(Set(datagram, ReceiveTimeout, 0, 60000) == 0); +#endif + Require(sendto_nid_postfix(datagram, "d", 1, 0, own.data(), own.size()) == 1); + Require(recv_nid_postfix(datagram, &byte, sizeof(byte), 0) == 1 && byte == 'd'); + + const int listener = socket_nid_postfix(2, 1, 0); + std::array address{16, 2, 0, 0, 127, 0, 0, 1}; + Require(listener >= 0 && bind_nid_postfix(listener, address.data(), address.size()) == 0); + int small = 4096; + Require(setsockopt_nid_postfix(listener, SocketLevel, ReceiveBuffer, &small, sizeof(small)) == 0); + Require(Set(listener, ReceiveTimeout, 3, 0) == 0); + Require(listen_nid_postfix(listener, 4) == 0); + std::uint32_t addressSize = address.size(); + Require(getsockname_nid_postfix(listener, address.data(), &addressSize) == 0); + const int client = socket_nid_postfix(2, 1, 0); + Require(client >= 0 && setsockopt_nid_postfix(client, SocketLevel, SendBuffer, &small, sizeof(small)) == 0); + Require(connect_nid_postfix(client, address.data(), address.size()) == 0); + const int server = accept_nid_postfix(listener, nullptr, nullptr); + Require(server >= 0 && Reads(server, ReceiveTimeout, 3, 0) && Reads(server, SendTimeout, 0, 0)); + + Require(Set(client, ReceiveTimeout, 0, 60000) == 0); + started = Clock::now(); + Require(Fails(recv_nid_postfix(client, &byte, sizeof(byte), 0), WouldBlock)); + Require(Clock::now() - started >= std::chrono::milliseconds(59)); + + Require(Set(client, SendTimeout, 0, 60000) == 0); + const std::vector chunk(1024, 'x'); + std::int64_t sent = 0; + bool stopped = false; + for (int round = 0; round < 200000 && !stopped; ++round) { + const auto result = send_nid_postfix(client, chunk.data(), chunk.size(), 0); + if (result > 0) sent += result; + else stopped = Fails(result, WouldBlock); + Require(result > 0 || stopped); + } + Require(stopped && sent > 0); + + Require(close_nid_postfix(server) == 0 && close_nid_postfix(client) == 0); + Require(close_nid_postfix(listener) == 0 && close_nid_postfix(datagram) == 0); + return 0; +} diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index b84e491b5f..5ffa2659c2 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -166,7 +166,7 @@ - [fstat on a guest socket](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) fills the fields FreeBSD 11's soo_stat and pru_sense_null fill and zeroes the rest: S_IFSOCK, the read bits while the receive side is open or data is queued, the write bits while the send side is open, st_size and st_blksize. Only this process's shutdown closes a side here; a peer's shutdown, close or reset does not clear the bits as FreeBSD's socantrcvmore and socantsendmore do. st_size is the host's FIONREAD, which for a Linux datagram socket is the size of the next datagram where FreeBSD counts every queued byte, and st_blksize is the host's SO_SNDBUF, which Linux reports as twice the size that was set. A socketpair end reports st_dev and st_ino 0, where FreeBSD's uipc_sense reports NODEV and a per-socket number - [poll](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) waits on guest sockets and pipes, through `WSAPoll` on Windows and `poll` on Linux; the `pollfd` layout and flag values are FreeBSD's. A libSceNet socket joins the same host wait through the native socket libSceNet lends to libkernel. `WSAPoll` rejects `POLLPRI`, so on Windows a socket's `POLLPRI` is asked as `POLLRDBAND` and `POLLIN` as `POLLRDNORM`; Winsock reports urgent data until it is read with `MSG_OOB`, where FreeBSD 11.4's `sopoll_generic` stops once the normal stream passes the mark. Guest `send` and `recv` refuse `MSG_OOB` with `EOPNOTSUPP`, so urgent data can only come from an outside peer and Windows keeps reporting `POLLPRI` after the stream passes the mark. A pipe follows FreeBSD's `pipe_poll`: the read end is readable when data is buffered, either end reports `POLLHUP` once the other end is closed, with the requested `POLLIN` and `POLLRDNORM` bits and without `POLLOUT`, which `pollscan` clears, and the write end is writable when `PIPE_BUF` (512) bytes are free on Windows, or when the host kernel says so on Linux. `POLLOUT` on a read end throws: FreeBSD opens both ends for reading and writing and reports it writable, and host pipes are one-way. Windows pipes cannot be waited on, so while a pipe is polled `poll` checks it with `NtQueryInformationFile` and waits on the sockets in slices of 1 ms. A closed descriptor reports `POLLNVAL`, and an open descriptor that is neither a guest socket, a libSceNet socket nor a pipe (files, stdio) throws, as do event bits other than `POLLIN`, `POLLPRI`, `POLLOUT`, `POLLRDNORM`, `POLLRDBAND` and `POLLWRBAND` (the `POLLERR`, `POLLHUP` and `POLLNVAL` bits are ignored in `events`), an infinite timeout with no socket or pipe to wait on. A closed socket descriptor reports `POLLNVAL`; the console's limit on the descriptor count is unknown and not checked - [Socket error numbers](../../../core/libs/prx/libkernel/Socket/include/SocketError.hpp) (libkernel) - a host socket error becomes the FreeBSD 11 `errno.h` number of the same name; the numbers are not checked on the console. Winsock `WSAESHUTDOWN` becomes `EPIPE`, as a send after `shutdown` does on FreeBSD, and a host error with no FreeBSD socket counterpart becomes `EIO`. A nonblocking `connect` in progress reports `EINPROGRESS` on Linux but `EAGAIN` on Windows (`WSAEWOULDBLOCK`), where FreeBSD reports `EINPROGRESS`. On Windows a refused nonblocking `connect` is never reported (`SO_ERROR` is unsupported), so a repeated one keeps reporting `EALREADY` where FreeBSD reports `ECONNREFUSED`. A receive after `shutdown(SHUT_RD)` reports `EPIPE` on Windows where FreeBSD returns 0 -- [getsockopt, setsockopt](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) on a guest socket handle the SOL_SOCKET level and `TCP_NODELAY`, after FreeBSD 11.4's [`sosetopt` and `sogetopt`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c): `SO_REUSEADDR`, `SO_KEEPALIVE`, `SO_DONTROUTE`, `SO_BROADCAST`, `SO_OOBINLINE` and `SO_LINGER` act on the host socket, and `SO_ERROR`, `SO_TYPE`, `SO_ACCEPTCONN`, `SO_SNDBUF` and `SO_RCVBUF` are read. A boolean option reads back as its own bit, as FreeBSD returns `so_options & option`. Winsock does not clear a socket's pending error when it is read, so on Windows the library reports a pending error once and shows it again only after a new `connect`; a second failure with the same code on the same socket without a `connect` in between would be hidden. `SO_SNDBUF` and `SO_RCVBUF` read the host's value, which is not the size FreeBSD reports (Linux doubles it). `SO_NOSIGPIPE` is kept per socket and reads back as its bit. On Linux a `send`, `sendto`, `sendmsg` or `write` on that socket passes `MSG_NOSIGNAL` to the host, as [`kern_sendit`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_syscalls.c#L890-L895) and [`soo_write`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/sys_socket.c#L147-L151) leave out the signal. An accepted socket takes it from its listener, as [`sonewconn`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L603) copies the options. `SO_NBIO` (`0x1200`) is the console's own option, not FreeBSD's. It switches the socket between blocking and non-blocking as `FIONBIO` does and reads back as 0 or 1, as `sceNetSetsockopt` and `sceNetGetsockopt` treat it here; what the console reads back for it is not measured. `TCP_NODELAY` (`IPPROTO_TCP`, 1) is set on the host socket and reads back as `TF_NODELAY` (4), as [`tcp_default_ctloutput`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/netinet/tcp_usrreq.c#L1860-L1861) returns it. The `IPPROTO_TCP` level on a datagram or `AF_UNIX` socket fails with `EINVAL`, as [`ip_ctloutput`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/netinet/ip_output.c#L943-L944) and [`uipc_ctloutput`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_usrreq.c#L1228-L1229) answer it, and an accepted socket takes `TCP_NODELAY` through the host. A null value fails with `EINVAL`, where FreeBSD's [`kern_setsockopt`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_syscalls.c#L1414-L1415) returns `EFAULT`. Every other SOL_SOCKET option (`SO_DEBUG`, `SO_USELOOPBACK`, `SO_REUSEPORT` and `SO_TIMESTAMP`, which FreeBSD stores and no title has been seen to set, the send and receive timeouts and low-water marks among them), every other `IPPROTO_TCP` option (`TCP_MAXSEG`, `TCP_KEEPIDLE`, `TCP_KEEPINTVL` and `TCP_KEEPCNT` among them) and every other level (`IPPROTO_IP`, `IPPROTO_IPV6`) fail with `ENOPROTOOPT`. None of this was measured on a console. +- [getsockopt, setsockopt](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) on a guest socket handle the SOL_SOCKET level and `TCP_NODELAY`, after FreeBSD 11.4's [`sosetopt` and `sogetopt`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c): `SO_REUSEADDR`, `SO_KEEPALIVE`, `SO_DONTROUTE`, `SO_BROADCAST`, `SO_OOBINLINE` and `SO_LINGER` act on the host socket, and `SO_ERROR`, `SO_TYPE`, `SO_ACCEPTCONN`, `SO_SNDBUF` and `SO_RCVBUF` are read. A boolean option reads back as its own bit, as FreeBSD returns `so_options & option`. Winsock does not clear a socket's pending error when it is read, so on Windows the library reports a pending error once and shows it again only after a new `connect`; a second failure with the same code on the same socket without a `connect` in between would be hidden. `SO_SNDBUF` and `SO_RCVBUF` read the host's value, which is not the size FreeBSD reports (Linux doubles it). `SO_NOSIGPIPE` is kept per socket and reads back as its bit. On Linux a `send`, `sendto`, `sendmsg` or `write` on that socket passes `MSG_NOSIGNAL` to the host, as [`kern_sendit`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_syscalls.c#L890-L895) and [`soo_write`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/sys_socket.c#L147-L151) leave out the signal. An accepted socket takes it from its listener, as [`sonewconn`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L603) copies the options. `SO_NBIO` (`0x1200`) is the console's own option, not FreeBSD's. It switches the socket between blocking and non-blocking as `FIONBIO` does and reads back as 0 or 1, as `sceNetSetsockopt` and `sceNetGetsockopt` treat it here; what the console reads back for it is not measured. `TCP_NODELAY` (`IPPROTO_TCP`, 1) is set on the host socket and reads back as `TF_NODELAY` (4), as [`tcp_default_ctloutput`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/netinet/tcp_usrreq.c#L1860-L1861) returns it. The `IPPROTO_TCP` level on a datagram or `AF_UNIX` socket fails with `EINVAL`, as [`ip_ctloutput`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/netinet/ip_output.c#L943-L944) and [`uipc_ctloutput`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_usrreq.c#L1228-L1229) answer it, and an accepted socket takes `TCP_NODELAY` through the host. `SO_SNDTIMEO` and `SO_RCVTIMEO` follow [`sosetopt`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L2657-L2689) and [`sogetopt`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L2846-L2850). A 16-byte `timeval` with a negative field or 1000000 microseconds or more fails with `EDOM`, and more than `INT32_MAX` seconds becomes the longest wait. The value is kept and read back through FreeBSD's 32.32 fixed-point conversion ([`tvtosbt`, `sbttotv`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/sys/time.h#L178-L286)), so half a second reads back as 499999 microseconds and one microsecond as 0. A `recv`, `recvfrom`, `recvmsg`, `read` or `readv`, and a `send`, `sendto`, `sendmsg`, `write` or `writev`, on a blocking socket then waits that long for the host to report the socket readable or writable, and fails with `EWOULDBLOCK`; a call with `MSG_DONTWAIT` does not wait. An accepted socket takes both from its listener ([`sonewconn`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L629-L630)). The wait covers only the first transfer: once the socket is writable the host send runs without a limit, where FreeBSD stops a send that cannot finish and returns the bytes sent so far, and a receive whose data another thread takes between the wait and the host call blocks again. `sendfile` ignores the send timeout. The host wait has millisecond steps, so a shorter timeout lasts up to a millisecond longer. A null value fails with `EINVAL`, where FreeBSD's [`kern_setsockopt`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_syscalls.c#L1414-L1415) returns `EFAULT`. Every other SOL_SOCKET option (`SO_DEBUG`, `SO_USELOOPBACK`, `SO_REUSEPORT` and `SO_TIMESTAMP`, which FreeBSD stores and no title has been seen to set, the low-water marks among them), every other `IPPROTO_TCP` option (`TCP_MAXSEG`, `TCP_KEEPIDLE`, `TCP_KEEPINTVL` and `TCP_KEEPCNT` among them) and every other level (`IPPROTO_IP`, `IPPROTO_IPV6`) fail with `ENOPROTOOPT`. None of this was measured on a console. - [connect to AF_UNSPEC](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) dissolves a datagram association as FreeBSD 11.4's `udp_disconnect()` does, which resets the local address to the wildcard address and keeps the port. On Linux a socket bound to an explicit port keeps its local address as well (`127.0.0.1:` stays `127.0.0.1:`), because a bound socket cannot be bound again; one bound to port 0 is bound again to the wildcard address with the port it had - [msync](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) (libkernel) follows FreeBSD 11's `kern_msync` and `vm_map_sync`, not measured on the console: the address is rounded down to the page, a range that wraps is `EINVAL`, as is `MS_ASYNC` with `MS_INVALIDATE`, and a range whose start or any page is not mapped (as `sceKernelVirtualQuery` reports it, reservations included) is `ENOMEM`; a page unmapped from inside a larger mapping still counts as mapped there, since `sceKernelVirtualQuery` keeps reporting the whole mapping. The guest has no file mappings, so a successful call writes nothing back. `MS_INVALIDATE` on a mapped range throws: FreeBSD returns `EBUSY` when the range holds `mlock`ed pages, and locked pages are not tracked - [madvise](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) (libkernel) accepts `MADV_PROTECT` (10) as a no-op hint without modifying process protection. FreeBSD 11's `kern_madvise` (`sys/vm/vm_mmap.c`) handles it before the address check, ignores the address and length, and calls `kern_procctl(..., PROC_SPROTECT, PPROT_SET)`, which requires `PRIV_VM_MADV_PROTECT` and fails with `EPERM` without it. Returning 0 unconditionally assumes the calling process is privileged. Console behaviour is unverified From b44ee9c45b5dd02d8f60cd9f8164de6d68ad7fa6 Mon Sep 17 00:00:00 2001 From: wh1trope Date: Wed, 14 Oct 2026 21:20:45 +0200 Subject: [PATCH 0127/1000] perf(shader): intersect dominator sets as bitsets Structurizing recomputes dominators after every CFG rewrite, and each block started from a vector of every block id. The same fixed point now runs on bitsets and still writes the sorted id lists. Co-authored-by: Cursor --- .../ControlFlow/src/Structurizer.cpp | 333 +++++++++++++----- 1 file changed, 243 insertions(+), 90 deletions(-) diff --git a/core/shader/recompiler/ControlFlow/src/Structurizer.cpp b/core/shader/recompiler/ControlFlow/src/Structurizer.cpp index c78d429445..3d80b21d8e 100644 --- a/core/shader/recompiler/ControlFlow/src/Structurizer.cpp +++ b/core/shader/recompiler/ControlFlow/src/Structurizer.cpp @@ -1,6 +1,8 @@ #include "ControlFlow/Structurizer.hpp" #include "ControlFlow/ControlFlowHelpers.hpp" #include +#include +#include #include #include #include @@ -50,6 +52,79 @@ bool inPlaceIntersections() { return enabled; } +struct BlockSet { + std::vector words; + + static std::size_t wordCount(std::uint32_t count) { + return (static_cast(count) + 63u) / 64u; + } + + static BlockSet filled(std::uint32_t count, std::uint64_t fill) { + BlockSet set; + set.words.assign(wordCount(count), fill); + return set; + } + + static BlockSet none(std::uint32_t count) { + return filled(count, 0); + } + + static BlockSet all(std::uint32_t count) { + auto set = filled(count, ~std::uint64_t{0}); + if (const auto tail = count % 64u) set.words.back() &= (std::uint64_t{1} << tail) - 1u; + return set; + } + + static BlockSet only(std::uint32_t count, std::uint32_t id) { + auto set = none(count); + set.add(id); + return set; + } + + static BlockSet fromIds(std::uint32_t count, const std::vector& ids) { + auto set = none(count); + for (const auto id : ids) set.add(id); + return set; + } + + void add(std::uint32_t id) { + words[static_cast(id) / 64u] |= std::uint64_t{1} << (id % 64u); + } + + void intersect(const BlockSet& other) { + for (std::size_t i = 0; i < words.size(); ++i) words[i] &= other.words[i]; + } + + bool operator==(const BlockSet& other) const { + return words == other.words; + } + + std::vector ids() const { + std::vector out; + for (std::size_t word = 0; word < words.size(); ++word) { + auto bits = words[word]; + while (bits != 0) { + const auto bit = static_cast(std::countr_zero(bits)); + out.push_back(static_cast(word * 64u) + bit); + bits &= bits - 1u; + } + } + return out; + } +}; + +bool replaceSet(BlockSet& destination, BlockSet&& next) { + if (next == destination) return false; + destination = std::move(next); + return true; +} + +std::vector> analysisVectors(const std::vector& sets) { + std::vector> vectors(sets.size()); + for (std::size_t i = 0; i < sets.size(); ++i) vectors[i] = sets[i].ids(); + return vectors; +} + void intersectAnalysisSet(std::vector& values, const std::vector& other) { if (!inPlaceIntersections()) { values = intersectSorted(values, other); @@ -366,27 +441,41 @@ std::uint32_t joinWithinIteration(const ControlFlowGraph& graph, const NaturalLo if (!joins[trueTarget] || !joins[falseTarget]) { return globalMerge; } - std::vector> postDominators(graph.blocks.size()); - for (const auto blockId : joining) { - postDominators[blockId] = blockId == globalMerge ? std::vector{globalMerge} : joining; - } + const auto count = static_cast(graph.blocks.size()); + std::vector> postDominators(count); const auto order = analysisOrder(graph, true); - for (bool changed = true; changed;) { - changed = false; - for (const auto blockId : order) { - if (!joins[blockId] || blockId == globalMerge) { - continue; - } - auto next = joining; - for (const auto successor : graph.FindBlock(blockId).successors) { - if (joins[successor]) { - next = intersectSorted(next, postDominators[successor]); + if (inPlaceIntersections()) { + const auto universe = BlockSet::fromIds(count, joining); + std::vector sets(count, BlockSet::none(count)); + for (const auto blockId : joining) sets[blockId] = blockId == globalMerge ? BlockSet::only(count, globalMerge) : universe; + for (bool changed = true; changed;) { + changed = false; + for (const auto blockId : order) { + if (!joins[blockId] || blockId == globalMerge) continue; + auto next = universe; + for (const auto successor : graph.FindBlock(blockId).successors) { + if (joins[successor]) next.intersect(sets[successor]); } + next.add(blockId); + changed = replaceSet(sets[blockId], std::move(next)) || changed; } - addSortedUnique(next, blockId); - if (next != postDominators[blockId]) { - postDominators[blockId] = std::move(next); - changed = true; + } + postDominators = analysisVectors(sets); + } else { + for (const auto blockId : joining) postDominators[blockId] = blockId == globalMerge ? std::vector{globalMerge} : joining; + for (bool changed = true; changed;) { + changed = false; + for (const auto blockId : order) { + if (!joins[blockId] || blockId == globalMerge) continue; + auto next = joining; + for (const auto successor : graph.FindBlock(blockId).successors) { + if (joins[successor]) next = intersectSorted(next, postDominators[successor]); + } + addSortedUnique(next, blockId); + if (next != postDominators[blockId]) { + postDominators[blockId] = std::move(next); + changed = true; + } } } } @@ -449,33 +538,47 @@ std::uint32_t mergeBesideReturns(const ControlFlowGraph& graph, const BasicBlock return std::all_of(tail.begin(), tail.end(), [&](std::uint32_t member) { return graph.FindBlock(member).predecessors.size() == 1u; }); }; std::vector> kept(count); - std::vector> postDominators(count, allBlockIds(count)); for (const auto& block : graph.blocks) { for (const auto successor : block.successors) { - if (!returns(successor)) { - kept[block.id].push_back(successor); - } - } - if (kept[block.id].empty()) { - postDominators[block.id] = {block.id}; + if (!returns(successor)) kept[block.id].push_back(successor); } } const auto order = analysisOrder(graph, true); - for (bool changed = true; changed;) { - changed = false; - for (const auto blockId : order) { - const auto& block = graph.blocks[blockId]; - if (kept[block.id].empty()) { - continue; + std::vector> postDominators; + if (inPlaceIntersections()) { + std::vector sets(count, BlockSet::all(count)); + for (const auto& block : graph.blocks) { + if (kept[block.id].empty()) sets[block.id] = BlockSet::only(count, block.id); + } + for (bool changed = true; changed;) { + changed = false; + for (const auto blockId : order) { + const auto& block = graph.blocks[blockId]; + if (kept[block.id].empty()) continue; + auto next = sets[kept[block.id].front()]; + for (std::size_t i = 1; i < kept[block.id].size(); ++i) next.intersect(sets[kept[block.id][i]]); + next.add(block.id); + changed = replaceSet(sets[block.id], std::move(next)) || changed; } - auto next = postDominators[kept[block.id].front()]; - for (std::size_t i = 1; i < kept[block.id].size(); ++i) { - intersectAnalysisSet(next, postDominators[kept[block.id][i]]); - } - addSortedUnique(next, block.id); - if (next != postDominators[block.id]) { - replaceAnalysisSet(postDominators[block.id], std::move(next)); - changed = true; + } + postDominators = analysisVectors(sets); + } else { + postDominators.assign(count, allBlockIds(count)); + for (const auto& block : graph.blocks) { + if (kept[block.id].empty()) postDominators[block.id] = {block.id}; + } + for (bool changed = true; changed;) { + changed = false; + for (const auto blockId : order) { + const auto& block = graph.blocks[blockId]; + if (kept[block.id].empty()) continue; + auto next = postDominators[kept[block.id].front()]; + for (std::size_t i = 1; i < kept[block.id].size(); ++i) intersectAnalysisSet(next, postDominators[kept[block.id][i]]); + addSortedUnique(next, block.id); + if (next != postDominators[block.id]) { + replaceAnalysisSet(postDominators[block.id], std::move(next)); + changed = true; + } } } } @@ -532,41 +635,66 @@ std::uint32_t iterationSelectionMerge(const ControlFlowGraph& graph, const Natur if (!joins[trueTarget] || !joins[falseTarget]) { return InvalidControlFlowId; } - std::vector> postDominators(count, allBlockIds(count)); - postDominators[join] = {join}; const auto order = analysisOrder(graph, true); - for (bool changed = true; changed;) { - changed = false; - for (const auto blockId : order) { - const auto& block = graph.blocks[blockId]; - if (!joins[block.id] || block.id == join) { - continue; - } - bool seeded = !reuse; - auto next = seeded ? allBlockIds(count) : std::vector{}; - for (const auto successor : block.successors) { - if (joins[successor]) { + std::vector> postDominators; + if (reuse && inPlaceIntersections()) { + std::vector sets(count, BlockSet::all(count)); + sets[join] = BlockSet::only(count, join); + for (bool changed = true; changed;) { + changed = false; + for (const auto blockId : order) { + const auto& block = graph.blocks[blockId]; + if (!joins[block.id] || block.id == join) continue; + bool seeded = false; + auto next = BlockSet::none(count); + for (const auto successor : block.successors) { + if (!joins[successor]) continue; if (!seeded) { - next = postDominators[successor]; + next = sets[successor]; seeded = true; - } else if (reuse) { - intersectAnalysisSet(next, postDominators[successor]); } else { - next = intersectSorted(next, postDominators[successor]); + next.intersect(sets[successor]); } } + if (!seeded) next = BlockSet::all(count); + next.add(block.id); + changed = replaceSet(sets[block.id], std::move(next)) || changed; } - if (!seeded) next = allBlockIds(count); - if (reuse) { - addSortedUnique(next, block.id); - } else { - addUnique(next, block.id); - sortUnique(next); - } - if (next != postDominators[block.id]) { - if (reuse) replaceAnalysisSet(postDominators[block.id], std::move(next)); - else postDominators[block.id] = std::move(next); - changed = true; + } + postDominators = analysisVectors(sets); + } else { + postDominators.assign(count, allBlockIds(count)); + postDominators[join] = {join}; + for (bool changed = true; changed;) { + changed = false; + for (const auto blockId : order) { + const auto& block = graph.blocks[blockId]; + if (!joins[block.id] || block.id == join) continue; + bool seeded = !reuse; + auto next = seeded ? allBlockIds(count) : std::vector{}; + for (const auto successor : block.successors) { + if (joins[successor]) { + if (!seeded) { + next = postDominators[successor]; + seeded = true; + } else if (reuse) { + intersectAnalysisSet(next, postDominators[successor]); + } else { + next = intersectSorted(next, postDominators[successor]); + } + } + } + if (!seeded) next = allBlockIds(count); + if (reuse) addSortedUnique(next, block.id); + else { + addUnique(next, block.id); + sortUnique(next); + } + if (next != postDominators[block.id]) { + if (reuse) replaceAnalysisSet(postDominators[block.id], std::move(next)); + else postDominators[block.id] = std::move(next); + changed = true; + } } } } @@ -2000,33 +2128,45 @@ void Structurizer::structurize(ControlFlowGraph& graph, bool privatizeReturns, b void Structurizer::computeDominatorTree(ControlFlowGraph& graph) const { const auto count = static_cast(graph.blocks.size()); - const auto all = allBlockIds(count); - - for (auto& block : graph.blocks) { - block.dominators = block.id == graph.entryBlock ? std::vector{block.id} : all; + const auto order = analysisOrder(graph, false); + if (inPlaceIntersections()) { + std::vector sets; + sets.reserve(count); + for (const auto& block : graph.blocks) sets.push_back(block.id == graph.entryBlock ? BlockSet::only(count, block.id) : BlockSet::all(count)); + bool changed = true; + while (changed) { + changed = false; + for (const auto id : order) { + auto& block = graph.blocks[id]; + if (block.id == graph.entryBlock) continue; + BlockSet next = block.predecessors.empty() ? BlockSet::only(count, block.id) : sets[block.predecessors.front()]; + if (!block.predecessors.empty()) { + for (std::size_t i = 1; i < block.predecessors.size(); ++i) next.intersect(sets[block.predecessors[i]]); + next.add(block.id); + } + changed = replaceSet(sets[id], std::move(next)) || changed; + } + } + for (std::uint32_t id = 0; id < count; ++id) graph.blocks[id].dominators = sets[id].ids(); + return; } - const auto order = analysisOrder(graph, false); + const auto all = allBlockIds(count); + for (auto& block : graph.blocks) block.dominators = block.id == graph.entryBlock ? std::vector{block.id} : all; bool changed = true; while (changed) { changed = false; for (const auto id : order) { auto& block = graph.blocks[id]; - if (block.id == graph.entryBlock) { - continue; - } - + if (block.id == graph.entryBlock) continue; std::vector next; if (block.predecessors.empty()) { next = {block.id}; } else { next = graph.blocks[block.predecessors.front()].dominators; - for (std::size_t i = 1; i < block.predecessors.size(); ++i) { - intersectAnalysisSet(next, graph.blocks[block.predecessors[i]].dominators); - } + for (std::size_t i = 1; i < block.predecessors.size(); ++i) intersectAnalysisSet(next, graph.blocks[block.predecessors[i]].dominators); addSortedUnique(next, block.id); } - if (next != block.dominators) { replaceAnalysisSet(block.dominators, std::move(next)); changed = true; @@ -2072,14 +2212,30 @@ void Structurizer::detectNaturalLoops(ControlFlowGraph& graph) const { void Structurizer::computePostDominators(ControlFlowGraph& graph) const { const auto count = static_cast(graph.blocks.size()); - const auto all = allBlockIds(count); - - for (auto& block : graph.blocks) { - block.postDominators = block.successors.empty() ? std::vector{block.id} : all; + const auto order = analysisOrder(graph, true); + if (inPlaceIntersections()) { + std::vector sets; + sets.reserve(count); + for (const auto& block : graph.blocks) sets.push_back(block.successors.empty() ? BlockSet::only(count, block.id) : BlockSet::all(count)); + bool changed = true; + while (changed) { + changed = false; + for (const auto id : order) { + auto& block = graph.blocks[id]; + BlockSet next = block.successors.empty() ? BlockSet::only(count, block.id) : sets[block.successors.front()]; + if (!block.successors.empty()) { + for (std::size_t i = 1; i < block.successors.size(); ++i) next.intersect(sets[block.successors[i]]); + next.add(block.id); + } + changed = replaceSet(sets[id], std::move(next)) || changed; + } + } + for (std::uint32_t id = 0; id < count; ++id) graph.blocks[id].postDominators = sets[id].ids(); + return; } - const auto order = analysisOrder(graph, true); - + const auto all = allBlockIds(count); + for (auto& block : graph.blocks) block.postDominators = block.successors.empty() ? std::vector{block.id} : all; bool changed = true; while (changed) { changed = false; @@ -2090,12 +2246,9 @@ void Structurizer::computePostDominators(ControlFlowGraph& graph) const { next = {block.id}; } else { next = graph.blocks[block.successors.front()].postDominators; - for (std::size_t i = 1; i < block.successors.size(); ++i) { - intersectAnalysisSet(next, graph.blocks[block.successors[i]].postDominators); - } + for (std::size_t i = 1; i < block.successors.size(); ++i) intersectAnalysisSet(next, graph.blocks[block.successors[i]].postDominators); addSortedUnique(next, block.id); } - if (next != block.postDominators) { replaceAnalysisSet(block.postDominators, std::move(next)); changed = true; From bf4945eabd8271f48666b66579ec4d951e837fde Mon Sep 17 00:00:00 2001 From: akkadbakkad89 Date: Wed, 14 Oct 2026 19:20:07 +0000 Subject: [PATCH 0128/1000] feat(libc): export the fmin and fmax family Adds fmin, fminf, fminl, fmax, fmaxf and fmaxl, the remaining missing members of the C99 minimum/maximum family, with the NaN propagation and signed-zero handling guest_fminf pins down (host std::fmin does not guarantee the sign of a zero result). The previous libSceLibcInternal copy of fminf is removed so the export stays single-owner. PPSA28319 imports fmaxf from libc.prx; the other five names were exported by nothing. --- core/libs/prx/libSceLibcInternal/Export.cpp | 11 -------- core/libs/prx/libc/src/Math.cpp | 28 +++++++++++++++++++++ 2 files changed, 28 insertions(+), 11 deletions(-) diff --git a/core/libs/prx/libSceLibcInternal/Export.cpp b/core/libs/prx/libSceLibcInternal/Export.cpp index 8356a4cb52..7ca113639e 100644 --- a/core/libs/prx/libSceLibcInternal/Export.cpp +++ b/core/libs/prx/libSceLibcInternal/Export.cpp @@ -7,7 +7,6 @@ #include #include #include -#include #ifdef _WIN32 #include #else @@ -141,14 +140,4 @@ unsigned int APS5_VABI GuestAlarm_nid_no_patch(unsigned int seconds); unsigned int APS5_VABI alarm_nid_postfix(unsigned int seconds) { return GuestAlarm_nid_no_patch(seconds); } - -float APS5_VABI fminf_nid_postfix(float x, float y) { - if (std::isnan(x)) - return y; - if (std::isnan(y)) - return x; - if (std::signbit(x) != std::signbit(y)) - return std::signbit(x) ? x : y; - return x < y ? x : y; -} } diff --git a/core/libs/prx/libc/src/Math.cpp b/core/libs/prx/libc/src/Math.cpp index ac4d5612d3..13d1e3e46e 100644 --- a/core/libs/prx/libc/src/Math.cpp +++ b/core/libs/prx/libc/src/Math.cpp @@ -165,6 +165,28 @@ long double AsinDenominator(long double t) { return 1.0L + t * (q[0] + t * (q[1] + t * (q[2] + t * (q[3] + t * q[4])))); } +template +T GuestFmin(T x, T y) { + if (std::isnan(x)) + return y; + if (std::isnan(y)) + return x; + if (std::signbit(x) != std::signbit(y)) + return std::signbit(x) ? x : y; + return x < y ? x : y; +} + +template +T GuestFmax(T x, T y) { + if (std::isnan(x)) + return y; + if (std::isnan(y)) + return x; + if (std::signbit(x) != std::signbit(y)) + return std::signbit(x) ? y : x; + return x < y ? y : x; +} + } extern "C" { @@ -256,6 +278,12 @@ float APS5_VABI truncf_nid_postfix(float x) { return std::trunc(x); } float APS5_VABI ceilf_nid_postfix(float x) { return std::ceil(x); } float APS5_VABI floorf_nid_postfix(float x) { return std::floor(x); } double APS5_VABI fma_nid_postfix(double x, double y, double z) { return std::fma(x, y, z); } +double APS5_VABI fmin_nid_postfix(double x, double y) { return GuestFmin(x, y); } +float APS5_VABI fminf_nid_postfix(float x, float y) { return GuestFmin(x, y); } +long double APS5_VABI fminl_nid_postfix(long double x, long double y) { return GuestFmin(x, y); } +double APS5_VABI fmax_nid_postfix(double x, double y) { return GuestFmax(x, y); } +float APS5_VABI fmaxf_nid_postfix(float x, float y) { return GuestFmax(x, y); } +long double APS5_VABI fmaxl_nid_postfix(long double x, long double y) { return GuestFmax(x, y); } long double APS5_VABI acosl_nid_postfix(long double x) { return std::acos(x); } long double APS5_VABI asinl_nid_postfix(long double x) { From 923d73c30599843a31664478e9ee5c1782c52d67 Mon Sep 17 00:00:00 2001 From: Leonardo Date: Wed, 14 Oct 2026 20:10:50 +0200 Subject: [PATCH 0129/1000] feat(libSceVoiceQoS): implement endpoints and connections --- core/libs/prx/libSceVoiceQoS/Export.cpp | 132 ++++++++++++++++++------ core/libs/tests/GuestVoiceQoS.cpp | 44 +++++++- docs/dev/TechnicalDebt.md | 2 +- docs/dev/debt/libSceVoiceQoS.md | 4 +- 4 files changed, 143 insertions(+), 39 deletions(-) diff --git a/core/libs/prx/libSceVoiceQoS/Export.cpp b/core/libs/prx/libSceVoiceQoS/Export.cpp index e8213885a5..3c856d1a4e 100644 --- a/core/libs/prx/libSceVoiceQoS/Export.cpp +++ b/core/libs/prx/libSceVoiceQoS/Export.cpp @@ -1,6 +1,11 @@ #include #include +#include +#include +#include #include +#include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" @@ -13,56 +18,120 @@ constexpr int SCE_VOICE_ERROR_LIBVOICEQOS_NOT_INIT = static_cast(0x804E0901 constexpr int SCE_VOICE_ERROR_LIBVOICEQOS_ARGUMENT_INVALID = static_cast(0x804E0902); constexpr int SCE_VOICE_ERROR_LIBVOICEQOS_INITIALIZED = static_cast(0x804E0905); -std::mutex g_mutex; -bool g_initialized = false; +using Endpoint = std::map>; + +struct Connection { + std::int32_t local; + std::int32_t remote; +}; + +struct QoS { + std::mutex mutex; + bool initialized = false; + std::int32_t nextId = 1; + std::map locals; + std::map remotes; + std::map connections; +}; + +QoS& State() { + static QoS qos; + return qos; +} + +std::int32_t NewId(QoS& qos) { + const std::int32_t id = qos.nextId; + qos.nextId = qos.nextId == std::numeric_limits::max() ? 1 : qos.nextId + 1; + return id; +} + +int CreateEndpoint(std::int32_t* id, std::map& endpoints, QoS& qos) { + if (!id) return SCE_VOICE_ERROR_LIBVOICEQOS_ARGUMENT_INVALID; + *id = NewId(qos); + endpoints.emplace(*id, Endpoint{}); + return 0; +} + +int DeleteEndpoint(std::int32_t id, std::map& endpoints, std::int32_t Connection::*side, const QoS& qos) { + if (!endpoints.contains(id)) return SCE_VOICE_ERROR_LIBVOICEQOS_ARGUMENT_INVALID; + for (const auto& entry : qos.connections) { + if (entry.second.*side == id) return SCE_VOICE_ERROR_LIBVOICEQOS_ARGUMENT_INVALID; + } + endpoints.erase(id); + return 0; +} + +int SetAttribute(std::map& endpoints, std::int32_t id, std::int32_t attribute, const void* value, std::int32_t size) { + const auto endpoint = endpoints.find(id); + if (endpoint == endpoints.end() || !value || size <= 0) return SCE_VOICE_ERROR_LIBVOICEQOS_ARGUMENT_INVALID; + const auto* bytes = static_cast(value); + endpoint->second[attribute].assign(bytes, bytes + size); + return 0; +} + +template +int Locked(Body&& body) { + auto& qos = State(); + std::lock_guard lock(qos.mutex); + if (!qos.initialized) return SCE_VOICE_ERROR_LIBVOICEQOS_NOT_INIT; + return body(qos); +} } extern "C" { -int APS5_VABI sceVoiceQoSInit(void* mem_block, uint32_t mem_size, int32_t app_type) { - std::lock_guard lock(g_mutex); - if (g_initialized) return SCE_VOICE_ERROR_LIBVOICEQOS_INITIALIZED; - if (!mem_block || mem_size != VOICE_QOS_MEMORY_SIZE || (app_type != VOICE_QOS_APP_TYPE_GAME && app_type != VOICE_QOS_APP_TYPE_10000000)) return SCE_VOICE_ERROR_LIBVOICEQOS_ARGUMENT_INVALID; - g_initialized = true; +int APS5_VABI sceVoiceQoSInit(void* memBlock, uint32_t memSize, int32_t appType) { + auto& qos = State(); + std::lock_guard lock(qos.mutex); + if (qos.initialized) return SCE_VOICE_ERROR_LIBVOICEQOS_INITIALIZED; + if (!memBlock || memSize != VOICE_QOS_MEMORY_SIZE || (appType != VOICE_QOS_APP_TYPE_GAME && appType != VOICE_QOS_APP_TYPE_10000000)) return SCE_VOICE_ERROR_LIBVOICEQOS_ARGUMENT_INVALID; + qos.initialized = true; return 0; } int APS5_VABI sceVoiceQoSEnd() { - std::lock_guard lock(g_mutex); - if (!g_initialized) return SCE_VOICE_ERROR_LIBVOICEQOS_NOT_INIT; - g_initialized = false; - return 0; + return Locked([](QoS& qos) { + qos.connections.clear(); + qos.locals.clear(); + qos.remotes.clear(); + qos.initialized = false; + return 0; + }); } -int APS5_VABI sceVoiceQoSCreateLocalEndpoint() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceVoiceQoSCreateLocalEndpoint(int32_t* localId, int32_t userId, int32_t device) { + (void)userId; + (void)device; + return Locked([&](QoS& qos) { return CreateEndpoint(localId, qos.locals, qos); }); } -int APS5_VABI sceVoiceQoSDeleteLocalEndpoint() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceVoiceQoSDeleteLocalEndpoint(int32_t localId) { + return Locked([&](QoS& qos) { return DeleteEndpoint(localId, qos.locals, &Connection::local, qos); }); } -int APS5_VABI sceVoiceQoSCreateRemoteEndpoint() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceVoiceQoSCreateRemoteEndpoint(int32_t* remoteId) { + return Locked([&](QoS& qos) { return CreateEndpoint(remoteId, qos.remotes, qos); }); } -int APS5_VABI sceVoiceQoSDeleteRemoteEndpoint() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceVoiceQoSDeleteRemoteEndpoint(int32_t remoteId) { + return Locked([&](QoS& qos) { return DeleteEndpoint(remoteId, qos.remotes, &Connection::remote, qos); }); } -int APS5_VABI sceVoiceQoSConnect() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceVoiceQoSConnect(int32_t* connectionId, int32_t localId, int32_t remoteId) { + return Locked([&](QoS& qos) { + if (!connectionId || !qos.locals.contains(localId) || !qos.remotes.contains(remoteId)) return SCE_VOICE_ERROR_LIBVOICEQOS_ARGUMENT_INVALID; + for (const auto& entry : qos.connections) { + if (entry.second.local == localId && entry.second.remote == remoteId) return SCE_VOICE_ERROR_LIBVOICEQOS_ARGUMENT_INVALID; + } + *connectionId = NewId(qos); + qos.connections.emplace(*connectionId, Connection{localId, remoteId}); + return 0; + }); } -int APS5_VABI sceVoiceQoSDisconnect() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceVoiceQoSDisconnect(int32_t connectionId) { + return Locked([&](QoS& qos) { return qos.connections.erase(connectionId) != 0 ? 0 : SCE_VOICE_ERROR_LIBVOICEQOS_ARGUMENT_INVALID; }); } int APS5_VABI sceVoiceQoSSetConnectionAttribute() { @@ -70,9 +139,8 @@ int APS5_VABI sceVoiceQoSSetConnectionAttribute() { return 0; } -int APS5_VABI sceVoiceQoSSetLocalEndpointAttribute() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceVoiceQoSSetLocalEndpointAttribute(int32_t localId, int32_t attributeId, const void* value, int32_t size) { + return Locked([&](QoS& qos) { return SetAttribute(qos.locals, localId, attributeId, value, size); }); } int APS5_VABI sceVoiceQoSSetRemoteEndpointAttribute() { diff --git a/core/libs/tests/GuestVoiceQoS.cpp b/core/libs/tests/GuestVoiceQoS.cpp index 44aa996344..a1aa646fc0 100644 --- a/core/libs/tests/GuestVoiceQoS.cpp +++ b/core/libs/tests/GuestVoiceQoS.cpp @@ -6,6 +6,13 @@ extern "C" { int APS5_VABI sceVoiceQoSInit(void*, std::uint32_t, std::int32_t); int APS5_VABI sceVoiceQoSEnd(); +int APS5_VABI sceVoiceQoSCreateLocalEndpoint(std::int32_t*, std::int32_t, std::int32_t); +int APS5_VABI sceVoiceQoSDeleteLocalEndpoint(std::int32_t); +int APS5_VABI sceVoiceQoSCreateRemoteEndpoint(std::int32_t*); +int APS5_VABI sceVoiceQoSDeleteRemoteEndpoint(std::int32_t); +int APS5_VABI sceVoiceQoSConnect(std::int32_t*, std::int32_t, std::int32_t); +int APS5_VABI sceVoiceQoSDisconnect(std::int32_t); +int APS5_VABI sceVoiceQoSSetLocalEndpointAttribute(std::int32_t, std::int32_t, const void*, std::int32_t); } static void Require(bool value) { if (!value) std::abort(); } @@ -28,11 +35,40 @@ int main(int argc, char** argv) { Require(sceVoiceQoSInit(memory.data(), 0x40000, appType) == 0); Require(sceVoiceQoSInit(memory.data(), 0x40000, appType) == initialized); Require(sceVoiceQoSInit(nullptr, 0, 0) == initialized); + + std::int32_t local = -1; + std::int32_t remote = -1; + std::int32_t connection = -1; + Require(sceVoiceQoSCreateLocalEndpoint(nullptr, 1, 0) == argumentInvalid); + Require(sceVoiceQoSCreateLocalEndpoint(&local, 1, 0) == 0 && local > 0); + Require(sceVoiceQoSCreateRemoteEndpoint(&remote) == 0 && remote > 0 && remote != local); + Require(sceVoiceQoSConnect(&connection, remote, local) == argumentInvalid); + Require(sceVoiceQoSConnect(&connection, local, remote) == 0 && connection > 0); + std::int32_t duplicate = -1; + Require(sceVoiceQoSConnect(&duplicate, local, remote) == argumentInvalid); + Require(sceVoiceQoSDeleteLocalEndpoint(local) == argumentInvalid); + + const std::uint32_t level = 0x1234; + Require(sceVoiceQoSSetLocalEndpointAttribute(local, 3, &level, sizeof(level)) == 0); + Require(sceVoiceQoSSetLocalEndpointAttribute(local, 3, nullptr, sizeof(level)) == argumentInvalid); + Require(sceVoiceQoSSetLocalEndpointAttribute(local, 3, &level, 0) == argumentInvalid); + Require(sceVoiceQoSSetLocalEndpointAttribute(local, 3, &level, -1) == argumentInvalid); + Require(sceVoiceQoSSetLocalEndpointAttribute(local + 100, 3, &level, sizeof(level)) == argumentInvalid); + + Require(sceVoiceQoSDisconnect(connection) == 0); + Require(sceVoiceQoSDisconnect(connection) == argumentInvalid); + Require(sceVoiceQoSDeleteLocalEndpoint(local) == 0); + Require(sceVoiceQoSDeleteLocalEndpoint(local) == argumentInvalid); + Require(sceVoiceQoSDeleteRemoteEndpoint(remote) == 0); + + Require(sceVoiceQoSCreateRemoteEndpoint(&remote) == 0); + Require(sceVoiceQoSEnd() == 0); + Require(sceVoiceQoSEnd() == notInitialized); + Require(sceVoiceQoSDeleteRemoteEndpoint(remote) == notInitialized); + Require(sceVoiceQoSCreateLocalEndpoint(&local, 1, 0) == notInitialized); + Require(sceVoiceQoSInit(memory.data(), 0x40000, appType) == 0); + Require(sceVoiceQoSDeleteRemoteEndpoint(remote) == argumentInvalid); Require(sceVoiceQoSEnd() == 0); Require(sceVoiceQoSEnd() == notInitialized); Require(sceVoiceQoSInit(nullptr, 0x40000, appType) == argumentInvalid); - Require(sceVoiceQoSInit(memory.data(), 0x40000, appType) == 0); - Require(sceVoiceQoSInit(memory.data(), 0x40000, appType) == initialized); - Require(sceVoiceQoSEnd() == 0); - Require(sceVoiceQoSEnd() == notInitialized); } diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 2f721015f3..b9bdc7aa01 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -20,7 +20,7 @@ Throughout the project, every function at every stage either **does exactly what - `sceNetCtlGetIfStat` signature and 56-byte layout come from PPSA09030's call site and SharpProspero's `SceNetCtlIfStat`; the device kind is always 0, the byte counters are the host's totals over its up non-loopback hardware interfaces (not "since cleared"), and the not-connected result is assumed to match `sceNetCtlGetState` - Font, Http2, Net, Ssl, SystemService and libkernel exports imported by PPSA12544 and added without an implementation have unknown signatures - The Share, NpTrophy2, NpEntitlementAccess and WebBrowserDialog exports imported by PPSA12544's Unity plugins and added without an implementation have unknown signatures -- Acm, GameLiveStreaming, Net, NpEntitlementAccess, NpManager, VoiceQoS, WebBrowserDialog and libkernel exports imported by PPSA14632 and added without an implementation are declared without parameters +- Acm, GameLiveStreaming, Net, NpEntitlementAccess, NpManager, VoiceQoS, WebBrowserDialog and libkernel exports imported by PPSA14632 and added without an implementation are declared without parameters; of the VoiceQoS ones only those still throwing remain so ### Functional diff --git a/docs/dev/debt/libSceVoiceQoS.md b/docs/dev/debt/libSceVoiceQoS.md index 7b3937b6a9..c37089e935 100644 --- a/docs/dev/debt/libSceVoiceQoS.md +++ b/docs/dev/debt/libSceVoiceQoS.md @@ -3,8 +3,8 @@ ### Unknown function info - [sceVoiceQoSSetConnectionAttribute](../../../core/libs/prx/libSceVoiceQoS/Export.cpp) (libSceVoiceQoS, NID `KCGOxg8iX7s`) is a throwing stub declared without parameters, like the other endpoint calls of the library: PPSA04159 fails to load without it and its signature is unknown. The name hashes to the imported NID (`tools/nid_names.py`) -- [sceVoiceQoSInit](../../../core/libs/prx/libSceVoiceQoS/Export.cpp) (libSceVoiceQoS) - `0x804E0905` for a second initialization comes from [prosper](https://github.com/mattias800/prosper)'s reading of the module and was not measured: on a PS5 a valid call blocks inside a homebrew payload, so it never returns there. The app type and the block are not used, as no endpoint is implemented +- [sceVoiceQoSInit](../../../core/libs/prx/libSceVoiceQoS/Export.cpp) (libSceVoiceQoS) - `0x804E0905` for a second initialization comes from [prosper](https://github.com/mattias800/prosper)'s reading of the module and was not measured: on a PS5 a valid call blocks inside a homebrew payload, so it never returns there. The app type and the block are not used. The endpoint calls assume the PS4 `libSceVoiceQoS` signatures (`CreateLocalEndpoint(int* id, user, device)`, `DeleteLocalEndpoint(id)`, `CreateRemoteEndpoint(int* id)`, `DeleteRemoteEndpoint(id)`, `Connect(int* id, local, remote)`, `Disconnect(connection)`, `SetLocalEndpointAttribute(id, attribute, value, size)`), not confirmed on a PS5 title. Endpoints and connections are tracked (an endpoint in a connection cannot be deleted, a pair connects once); local endpoint attributes are stored as given, as their ids and defaults are unknown; other endpoint and packet calls remain throwing stubs. The not-initialized error 0x804E0901 is assumed from the 0x804E09xx numbering, and unknown ids return `ARGUMENT_INVALID`. Aliens: Fireteam Elite (PPSA03393) uses all seven implemented calls on its first load path, starting with `sceVoiceQoSCreateLocalEndpoint` when it registers the local talker; it stops with `not implemented` when any of them throws ### Functional -- [sceVoiceQoSEnd](../../../core/libs/prx/libSceVoiceQoS/Export.cpp) (libSceVoiceQoS) - `0x804E0901` (not initialized) before any initialization was measured on a PS5 (firmware 13.60, #1407); ending an initialized library was not, because a valid `sceVoiceQoSInit` never returns inside a homebrew payload. PPSA14632 calls it at shutdown and PPSA26344 after a nonzero `sceVoiceQoSInit`. After an initialization it returns 0 and clears the init flag, so the library can be initialized again. Whether the console also ends the libSceVoice instance that its `sceVoiceQoSInit` starts ([prosper](https://github.com/mattias800/prosper)'s reading of the module), and what it answers while endpoints exist, is unknown; no endpoint is implemented +- [sceVoiceQoSEnd](../../../core/libs/prx/libSceVoiceQoS/Export.cpp) (libSceVoiceQoS) - `0x804E0901` (not initialized) before any initialization was measured on a PS5 (firmware 13.60, #1407); ending an initialized library was not, because a valid `sceVoiceQoSInit` never returns inside a homebrew payload. PPSA14632 calls it at shutdown and PPSA26344 after a nonzero `sceVoiceQoSInit`. After an initialization it returns 0 and clears the init flag, so the library can be initialized again. Whether the console also ends the libSceVoice instance that its `sceVoiceQoSInit` starts ([prosper](https://github.com/mattias800/prosper)'s reading of the module), and what it answers while endpoints exist, is unknown; here it drops every endpoint and connection From 18114a241d94098567aa1b7ab98c135f034de7df Mon Sep 17 00:00:00 2001 From: Ondra97 <47064087+Ondra97@users.noreply.github.com> Date: Wed, 14 Oct 2026 21:31:14 +0200 Subject: [PATCH 0130/1000] fix(ngs2): stop stale rendering after waveform callbacks --- .../libs/prx/libSceNgs2.native/src/Render.cpp | 13 +- .../libSceNgs2.native/tests/Ngs2Render.cpp | 131 ++++++++++++++++++ 2 files changed, 142 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceNgs2.native/src/Render.cpp b/core/libs/prx/libSceNgs2.native/src/Render.cpp index bf14563e82..0fe18cb32e 100644 --- a/core/libs/prx/libSceNgs2.native/src/Render.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Render.cpp @@ -95,16 +95,19 @@ static void Advance(Ngs2Voice& voice, std::uint64_t frames) { if (block.streaming) { while (block.pieces.size() > 1 && block.cursor >= block.pieces.front().firstFrame + block.pieces.front().frames) block.pieces.erase(block.pieces.begin()); } - if (block.cursor == block.info.num_samples || (block.streaming && !voice.acceptsBlocks && block.cursor == block.availableFrames)) Ngs2FinishBlock(voice); + if (block.cursor == block.info.num_samples || (block.streaming && !voice.acceptsBlocks && block.cursor == block.availableFrames)) { + if (!Ngs2FinishBlock(voice)) return; + } } } static void ConsumeSamples(Ngs2Voice& voice, std::uint32_t grain, std::uint32_t systemRate) { + const auto revision = voice.waveformRevision; const auto step = static_cast(std::llround(voice.sampleRate * static_cast(voice.pitch) / systemRate * PHASE_ONE)); std::uint32_t written = 0; float current[NGS2_MAX_CHANNELS]; float next[NGS2_MAX_CHANNELS]; - for (; written < grain && voice.state == Ngs2PlayState::Playing && !voice.blocks.empty(); written++) { + for (; written < grain && voice.state == Ngs2PlayState::Playing && !voice.blocks.empty() && voice.waveformRevision == revision; written++) { ReadFrames(voice, current, next); const auto fraction = static_cast(static_cast(voice.phase) / PHASE_ONE); for (std::uint32_t channel = 0; channel < voice.channels; channel++) { @@ -146,8 +149,14 @@ static void ApplyFilter(Ngs2Voice& voice, Ngs2Filter& filter, std::uint32_t grai } static void RenderSampler(Ngs2Voice& voice, std::uint32_t grain, std::uint32_t systemRate) { + const auto channels = voice.channels; if (voice.waveformType == SCE_NGS2_WAVEFORM_TYPE_ATRAC9) Ngs2ConsumeAtrac9(voice, grain, systemRate); else ConsumeSamples(voice, grain, systemRate); + if (voice.channels != channels) { + voice.samples.assign(static_cast(grain) * voice.channels, 0.0f); + voice.hasSamples = false; + return; + } for (auto& filter : voice.filters) { if (!filter.enabled || (!voice.hasSamples && !HasHistory(filter))) continue; ApplyFilter(voice, filter, grain); diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp index e9ae7428fa..b46dc70dbe 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp @@ -297,6 +297,134 @@ static void TestPitchAndRepeat() { Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); } +struct PcmResetCallback { + std::vector pcm; + std::uint32_t flag; + int calls = 0; + std::uint32_t waveformFlags = SCE_NGS2_WAVEFORM_BLOCKS_FLAG_RESET; + bool pause = false; +}; + +static void APS5_VABI ResetPcm(const Ngs2VoiceCallbackInfo* info) { + auto& callback = *reinterpret_cast(info->callback_data); + if (++callback.calls != 1) return; + Require(info->flag == callback.flag); + const Ngs2WaveformBlock block{0, callback.pcm.size() * sizeof(std::int16_t), 0, 0, static_cast(callback.pcm.size()), 0, 0}; + Control(info->voice_handle, SCE_NGS2_SAMPLER_VOICE_PARAM_ADD_WAVEFORM_BLOCKS, + Ngs2SamplerVoiceWaveformBlocksParam{{}, callback.pcm.data(), callback.waveformFlags, 1, &block}); + if (callback.pause) Event(info->voice_handle, SCE_NGS2_VOICE_EVENT_PAUSE); +} + +static void TestPcmCallbackReset() { + for (const auto flag : {SCE_NGS2_VOICE_CALLBACK_FLAG_BLOCK_END, SCE_NGS2_VOICE_CALLBACK_FLAG_BLOCK_REPEAT}) { + const auto system = CreateSystem(); + const std::vector pcm{16384}; + const auto sampler = Sampler(system, pcm, flag == SCE_NGS2_VOICE_CALLBACK_FLAG_BLOCK_REPEAT ? 1 : 0); + Patch(sampler, Mastering(system, 1)); + Control(sampler, SCE_NGS2_SAMPLER_VOICE_PARAM_PITCH, Ngs2SamplerVoicePitchParam{{}, 2.0f}); + PcmResetCallback callback{{}, flag}; + for (std::uint32_t frame = 0; frame < Grain * 2 + 2; ++frame) callback.pcm.push_back(static_cast(1000 + frame * 100)); + Control(sampler, SCE_NGS2_VOICE_PARAM_CALLBACK, + Ngs2VoiceCallbackParam{{}, ResetPcm, reinterpret_cast(&callback), flag, 0}); + Event(sampler, SCE_NGS2_VOICE_EVENT_PLAY); + const auto first = RenderI16(system); + Require(callback.calls == 1); + Ngs2SamplerVoiceState state{}; + Require(sceNgs2VoiceGetState(sampler, &state.voice_state, sizeof(state)) == SCE_NGS2_OK); + if (state.num_decoded_samples != 1) std::fprintf(stderr, "PCM callback reset consumed %llu samples before the next grain, expected 1\n", static_cast(state.num_decoded_samples)); + Require(state.num_decoded_samples == 1 && state.decoded_data_size == sizeof(std::int16_t) && state.waveform_data == callback.pcm.data()); + for (std::uint32_t frame = 0; frame < Grain; ++frame) Require(first[frame] == (frame == 0 ? 16384 : 0)); + const auto second = RenderI16(system); + for (std::uint32_t frame = 0; frame < Grain; ++frame) Require(second[frame] == callback.pcm[frame * 2]); + Require(callback.calls == 1); + Require(sceNgs2VoiceGetState(sampler, &state.voice_state, sizeof(state)) == SCE_NGS2_OK); + Require(state.num_decoded_samples == 1 + Grain * 2 && state.waveform_data == callback.pcm.data() + Grain * 2); + Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); + } +} + +static void TestPcmCallbackRefill() { + for (const bool pause : {false, true}) { + const auto system = CreateSystem(); + const auto sampler = Voice(CreateRack(system, SCE_NGS2_RACK_ID_SAMPLER)); + Control(sampler, SCE_NGS2_SAMPLER_VOICE_PARAM_SETUP, Ngs2SamplerVoiceSetupParam{{}, {SCE_NGS2_WAVEFORM_TYPE_PCM_I16L, 1, 48000, 0, 0, 0}}); + Patch(sampler, Mastering(system, 1)); + const std::int16_t pcm = 16384; + const Ngs2WaveformBlock block{0, sizeof(pcm), 0, 0, 1, 0, 0}; + Control(sampler, SCE_NGS2_SAMPLER_VOICE_PARAM_ADD_WAVEFORM_BLOCKS, + Ngs2SamplerVoiceWaveformBlocksParam{{}, &pcm, SCE_NGS2_WAVEFORM_BLOCKS_FLAG_CONTINUE, 1, &block}); + PcmResetCallback callback{{}, SCE_NGS2_VOICE_CALLBACK_FLAG_BLOCK_END, 0, 0, pause}; + for (std::uint32_t frame = 0; frame < Grain * 2; ++frame) callback.pcm.push_back(static_cast(1000 + frame * 100)); + Control(sampler, SCE_NGS2_VOICE_PARAM_CALLBACK, + Ngs2VoiceCallbackParam{{}, ResetPcm, reinterpret_cast(&callback), callback.flag, 0}); + Event(sampler, SCE_NGS2_VOICE_EVENT_PLAY); + const auto first = RenderI16(system); + Require(callback.calls == 1 && first.front() == pcm); + for (std::uint32_t frame = 1; frame < Grain; ++frame) Require(first[frame] == (pause ? 0 : callback.pcm[frame - 1])); + Ngs2SamplerVoiceState state{}; + Require(sceNgs2VoiceGetState(sampler, &state.voice_state, sizeof(state)) == SCE_NGS2_OK); + const std::uint32_t consumed = pause ? 0 : Grain - 1; + Require(state.num_decoded_samples == consumed + 1 && state.waveform_data == callback.pcm.data() + consumed); + if (pause) Event(sampler, SCE_NGS2_VOICE_EVENT_RESUME); + const auto second = RenderI16(system); + for (std::uint32_t frame = 0; frame < Grain; ++frame) Require(second[frame] == callback.pcm[consumed + frame]); + Require(callback.calls == 1); + Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); + } +} + +struct PcmSetupCallback { + uintptr_t master; + std::uint32_t channels; + std::uint32_t flag; + std::vector pcm; + int calls = 0; +}; + +static void APS5_VABI SetupPcm(const Ngs2VoiceCallbackInfo* info) { + auto& callback = *reinterpret_cast(info->callback_data); + if (++callback.calls != 1) return; + Require(info->flag == callback.flag); + Control(info->voice_handle, SCE_NGS2_SAMPLER_VOICE_PARAM_SETUP, + Ngs2SamplerVoiceSetupParam{{}, {SCE_NGS2_WAVEFORM_TYPE_PCM_I16L, callback.channels, 48000, 0, 0, 0}}); + const Ngs2WaveformBlock block{0, callback.pcm.size() * sizeof(std::int16_t), 0, 0, Grain * 2, 0, 0}; + Control(info->voice_handle, SCE_NGS2_SAMPLER_VOICE_PARAM_ADD_WAVEFORM_BLOCKS, + Ngs2SamplerVoiceWaveformBlocksParam{{}, callback.pcm.data(), 0, 1, &block}); + Patch(info->voice_handle, callback.master); + Event(info->voice_handle, SCE_NGS2_VOICE_EVENT_PLAY); +} + +static void TestPcmCallbackSetup() { + for (const auto channels : {2u, 8u}) { + for (const auto flag : {SCE_NGS2_VOICE_CALLBACK_FLAG_BLOCK_END, SCE_NGS2_VOICE_CALLBACK_FLAG_BLOCK_REPEAT}) { + const auto system = CreateSystem(); + const std::vector pcm{16384}; + const auto sampler = Sampler(system, pcm, flag == SCE_NGS2_VOICE_CALLBACK_FLAG_BLOCK_REPEAT ? 1 : 0); + PcmSetupCallback callback{Mastering(system, channels), channels, flag, {}}; + for (std::uint32_t frame = 0; frame < Grain * 2; ++frame) { + for (std::uint32_t channel = 0; channel < channels; ++channel) callback.pcm.push_back(static_cast(1000 + frame * 25 + channel * 100)); + } + Patch(sampler, callback.master); + Control(sampler, SCE_NGS2_VOICE_PARAM_CALLBACK, + Ngs2VoiceCallbackParam{{}, SetupPcm, reinterpret_cast(&callback), flag, 0}); + Event(sampler, SCE_NGS2_VOICE_EVENT_PLAY); + std::vector output(Grain * channels, -1); + const Ngs2RenderBufferInfo info{output.data(), output.size() * sizeof(std::int16_t), SCE_NGS2_WAVEFORM_TYPE_PCM_I16L, channels}; + Require(sceNgs2SystemRender(system, &info, 1) == SCE_NGS2_OK); + Require(callback.calls == 1 && std::all_of(output.begin(), output.end(), [](std::int16_t sample) { return sample == 0; })); + Ngs2SamplerVoiceState state{}; + Require(sceNgs2VoiceGetState(sampler, &state.voice_state, sizeof(state)) == SCE_NGS2_OK); + Require(state.num_decoded_samples == 0 && state.decoded_data_size == 0 && state.waveform_data == callback.pcm.data()); + Require(sceNgs2SystemRender(system, &info, 1) == SCE_NGS2_OK); + Require(callback.calls == 1 && std::equal(output.begin(), output.end(), callback.pcm.begin())); + Require(sceNgs2VoiceGetState(sampler, &state.voice_state, sizeof(state)) == SCE_NGS2_OK); + Require(state.num_decoded_samples == Grain && state.decoded_data_size == Grain * channels * sizeof(std::int16_t)); + Require(state.waveform_data == callback.pcm.data() + Grain * channels); + Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); + } + } +} + static void TestSubmixerMatrix() { const auto system = CreateSystem(); const auto master = Mastering(system, 2); @@ -916,6 +1044,9 @@ int main() { TestReverbSetup(); TestSampleRate(); TestUserData(); + TestPcmCallbackReset(); + TestPcmCallbackRefill(); + TestPcmCallbackSetup(); TestMasteringGain(); TestMatrixLevelClamp(); TestStereoIntoSurround(); From 3abfe906bdf19897be847668c09a7c9ae63368b7 Mon Sep 17 00:00:00 2001 From: Niko Date: Wed, 14 Oct 2026 21:20:44 +0200 Subject: [PATCH 0131/1000] test(relinker): run the file writer's replacement test on a copy of itself The running-executable replacement test copied /bin/sleep under the names "running" and "leftover" and started the copy. On Ubuntu 26.04 /bin/sleep is the uutils multi-call coreutils binary, which picks the utility from the file name of the path it was started through, not from argv[0], so the copy printed "coreutils: unknown program 'running'" and exited, and file_writer_binary and file_writer_text failed without ever replacing a running executable. The test now copies its own executable and starts it with a "wait" argument that sleeps, so it no longer depends on how a system binary reads its name. Co-Authored-By: Claude Opus 5.5 --- core/relinker/io/tests/FileWriterTests.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/core/relinker/io/tests/FileWriterTests.cpp b/core/relinker/io/tests/FileWriterTests.cpp index 900518b16f..8ec3b79abd 100644 --- a/core/relinker/io/tests/FileWriterTests.cpp +++ b/core/relinker/io/tests/FileWriterTests.cpp @@ -51,7 +51,7 @@ void requireRunningReplacement(const std::filesystem::path& directory, const TVa const auto output = directory / (leftover ? "leftover" : "running"); if (leftover) std::ofstream(directory / "leftover.tmp", std::ios::binary) << "killed"; - std::filesystem::copy_file("/bin/sleep", output); + std::filesystem::copy_file(std::filesystem::read_symlink("/proc/self/exe"), output); const auto permissions = std::filesystem::perms::owner_all | std::filesystem::perms::group_all; std::filesystem::permissions(output, permissions); const auto previous = readFile(output); @@ -59,7 +59,7 @@ void requireRunningReplacement(const std::filesystem::path& directory, const TVa if (child < 0) throw std::runtime_error("Cannot start the previous executable"); if (child == 0) { - execl(output.c_str(), "sleep", "30", nullptr); + execl(output.c_str(), output.c_str(), "wait", nullptr); _exit(127); } std::error_code error; @@ -172,6 +172,10 @@ int main(int argc, char* argv[]) { try { if (argc != 2) throw std::runtime_error("Expected binary or text"); const std::string mode = argv[1]; + if (mode == "wait") { + sleep(30); + return 0; + } for (const std::size_t size : {std::size_t{3}, std::size_t{65536}}) { if (mode == "binary") { const std::vector data(size, 0x41); From 060fe72e7da9bf9b8b317f8cda095431cdd5f6da Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Wed, 14 Oct 2026 17:50:38 +0000 Subject: [PATCH 0132/1000] refactor(agc): make the largest device-local heap lookup public LargestDeviceLocalHeap picks the heap that the texture cache budgets read their VK_EXT_memory_budget numbers from. It moves out of the anonymous namespace of ShaderResources.cpp so the performance overlay can report the same heap. --- .../Graphics/include/ShaderResources.hpp | 1 + .../Graphics/src/ShaderResources.cpp | 22 +++++++++---------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp index 493436dcbc..ba7825f464 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp @@ -28,6 +28,7 @@ class Recorder; void FlushCachedTextures(VkDevice device); void ClearCachedTextures(VkDevice device); +std::optional LargestDeviceLocalHeap(const VkPhysicalDeviceMemoryProperties& memory); std::uint64_t TextureCacheBudget(const VkPhysicalDeviceMemoryProperties& memory); std::uint64_t SampledTextureBudget(const VkPhysicalDeviceMemoryProperties& memory, const VkPhysicalDeviceMemoryBudgetPropertiesEXT* reported, std::uint64_t textureBytes); bool SampledBudgetReportDue(std::uint64_t reported, std::uint64_t budget, std::chrono::steady_clock::duration sinceReport); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index e89fa2b31d..ea64c982c2 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -474,14 +474,6 @@ std::shared_ptr sampledStorageSource(const Context& context, con std::uint64_t storageCacheBytes(); -std::optional largestDeviceLocalHeap(const VkPhysicalDeviceMemoryProperties& memory) { - std::optional heap; - for (std::uint32_t i = 0; i < std::min(memory.memoryHeapCount, VK_MAX_MEMORY_HEAPS); ++i) { - if ((memory.memoryHeaps[i].flags & VK_MEMORY_HEAP_DEVICE_LOCAL_BIT) != 0 && (!heap || memory.memoryHeaps[i].size > memory.memoryHeaps[*heap].size)) heap = i; - } - return heap; -} - std::uint64_t forcedCacheBudget() { static const std::uint64_t forced = [] { const char* text = std::getenv("APS5_TEXTURE_CACHE_MIB"); @@ -508,7 +500,7 @@ std::uint64_t sampledBudget(const Context& context, TextureCache& cache) { const auto storage = storageCacheBytes(); const auto budget = SampledTextureBudget(context.memory, &reported, sampled + storage); const bool fresh = cache.budgetDevice != context.physical; - if (const auto heap = largestDeviceLocalHeap(context.memory); heap && (fresh || SampledBudgetReportDue(cache.reportedBudget, budget, now - cache.reportedAt))) { + if (const auto heap = LargestDeviceLocalHeap(context.memory); heap && (fresh || SampledBudgetReportDue(cache.reportedBudget, budget, now - cache.reportedAt))) { char was[48] = ""; if (!fresh) std::snprintf(was, sizeof(was), " (was %llu MiB)", static_cast(cache.reportedBudget >> 20u)); std::fprintf(stderr, "[gpu] sampled texture cache budget %llu MiB%s: device-local heap %u has a VK_EXT_memory_budget budget of %llu MiB and uses %llu MiB, %llu MiB of it sampled textures (%llu MiB in the cache) and %llu MiB cached storage images; storage image cache budget %llu MiB\n", static_cast(budget >> 20u), was, *heap, static_cast(reported.heapBudget[*heap] >> 20u), static_cast(reported.heapUsage[*heap] >> 20u), static_cast(sampled >> 20u), static_cast(cache.bytes >> 20u), static_cast(storage >> 20u), static_cast(TextureCacheBudget(context.memory) >> 20u)); @@ -931,13 +923,21 @@ std::shared_ptr cachedStorageTexture(const Context& context, std } +std::optional LargestDeviceLocalHeap(const VkPhysicalDeviceMemoryProperties& memory) { + std::optional heap; + for (std::uint32_t i = 0; i < std::min(memory.memoryHeapCount, VK_MAX_MEMORY_HEAPS); ++i) { + if ((memory.memoryHeaps[i].flags & VK_MEMORY_HEAP_DEVICE_LOCAL_BIT) != 0 && (!heap || memory.memoryHeaps[i].size > memory.memoryHeaps[*heap].size)) heap = i; + } + return heap; +} + std::uint64_t TextureCacheBudget(const VkPhysicalDeviceMemoryProperties& memory) { - const auto heap = largestDeviceLocalHeap(memory); + const auto heap = LargestDeviceLocalHeap(memory); return std::max(2048ull << 20u, heap ? memory.memoryHeaps[*heap].size / 4u : 0u); } std::uint64_t SampledTextureBudget(const VkPhysicalDeviceMemoryProperties& memory, const VkPhysicalDeviceMemoryBudgetPropertiesEXT* reported, std::uint64_t textureBytes) { - const auto heap = largestDeviceLocalHeap(memory); + const auto heap = LargestDeviceLocalHeap(memory); if (reported == nullptr || !heap || reported->heapBudget[*heap] == 0) return TextureCacheBudget(memory); const auto budget = reported->heapBudget[*heap]; const auto usage = reported->heapUsage[*heap]; From 3da081aaad9317dea39ab566f21acd32fc83c34b Mon Sep 17 00:00:00 2001 From: simonbronk <275178677+simonbronk@users.noreply.github.com> Date: Wed, 14 Oct 2026 11:45:34 +0200 Subject: [PATCH 0133/1000] fix(libkernel): drop undefined protection bits in sceKernelMprotect Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libkernel/DirectMemory/Export.cpp | 3 ++- core/libs/tests/GuestMemory.cpp | 11 +++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libkernel/DirectMemory/Export.cpp b/core/libs/prx/libkernel/DirectMemory/Export.cpp index dca35ab051..a65f7749c5 100644 --- a/core/libs/prx/libkernel/DirectMemory/Export.cpp +++ b/core/libs/prx/libkernel/DirectMemory/Export.cpp @@ -225,7 +225,8 @@ int32_t APS5_VABI sceKernelMapNamedFlexibleMemoryInternal(void** addr_in_out, si } int APS5_VABI sceKernelMprotect(const void* addr, size_t len, int prot) { - return DoMprotect(addr, len, prot); + constexpr int DefinedProtection = 0x3F7; + return DoMprotect(addr, len, prot & DefinedProtection); } int APS5_VABI sceKernelMunmap(uint64_t vaddr, size_t len) { diff --git a/core/libs/tests/GuestMemory.cpp b/core/libs/tests/GuestMemory.cpp index a7460faf47..bfd89500b3 100644 --- a/core/libs/tests/GuestMemory.cpp +++ b/core/libs/tests/GuestMemory.cpp @@ -203,6 +203,16 @@ static void CheckAudioCoprocessorProtection() { Require(rejected && undefined == nullptr); } +static void CheckMprotectIgnoresUndefinedBits() { + constexpr std::size_t length = 0x4000; + void* mapping = nullptr; + Require(sceKernelMapFlexibleMemory(&mapping, length, 3, 0) == 0); + Require(sceKernelMprotect(mapping, length, static_cast(0xfffffff2u)) == 0); + static_cast(mapping)[length - 1] = 5; + Require(static_cast(mapping)[length - 1] == 5); + Require(sceKernelMunmap(mapping, length) == 0); +} + static void CheckInternalNamedFlexibleMapping() { constexpr std::size_t length = 0x10000; std::size_t before = 0; @@ -2563,6 +2573,7 @@ int main(int argc, char** argv) { CheckBatchMapStopsAtInvalidEntry(); CheckCheckedReleaseDirectMemory(); CheckAudioCoprocessorProtection(); + CheckMprotectIgnoresUndefinedBits(); CheckDirectMemoryFollowsPhysicalPages(); CheckSparseDirectMemoryBacking(); CheckReleaseDirectMemoryClearsMappings(); From ce074f9302e19ef7881a6c9d402c7ddc5620579e Mon Sep 17 00:00:00 2001 From: simonbronk <275178677+simonbronk@users.noreply.github.com> Date: Wed, 14 Oct 2026 11:49:12 +0200 Subject: [PATCH 0134/1000] docs(debt): note the protection bits sceKernelMprotect drops Co-Authored-By: Claude Opus 5.5 --- docs/dev/debt/libkernel.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index efa9156809..8444dd58bb 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -106,6 +106,7 @@ ### Functional - [sceKernelMprotect, sceKernelMtypeprotect](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (libkernel) over a range with a gap that is not registered in the allocation tracker protect the registered parts and skip the gap, and return 0, as FreeBSD 11.4's [`vm_map_protect`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/vm/vm_map.c) does (it has no check for holes); a range whose start is unmapped (also when later pages are mapped) returns `SCE_KERNEL_ERROR_EINVAL` (`mprotect`: `EINVAL`) because an existing test pins that for an unmapped start; FreeBSD answers 0 there too (`vm_map_protect` takes the next entry), so this errno is not taken from the source and was not changed. `sceKernelMprotect` threw for a gap before, which ended the title. Every part is checked for a GPU or image pin before the first one is protected; the parts are then protected one after the other, so a host protection failure in a later part leaves the earlier parts changed (FreeBSD's `vm_map_protect` cannot fail in its second pass). The behaviour on the console is not known. None of this was measured on a console. +- [sceKernelMprotect protection bits](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - bits outside `0x3F7` are dropped instead of refused, as FreeBSD 11's [`sys_mprotect`](https://github.com/freebsd/freebsd-src/blob/34bd34750187bae02a0086402c78c031c59dd8b3/sys/vm/vm_mmap.c#L606) does with `VM_PROT_ALL`. Onimusha: Way of the Sword (PPSA27836 01.000.001) maps direct memory with `0xf2` and then protects it with `0xfffffff2`. Not measured on a console; the map calls and `sceKernelMtypeprotect` still refuse undefined bits - [JIT shared memory](../../../core/libs/prx/libkernel/DirectMemory/Jit.cpp) (libkernel) - `sceKernelJitCreateSharedMemory`, `sceKernelJitCreateAliasOfSharedMemory` and `sceKernelJitMapSharedMemory` take the signatures of [Orbital](https://github.com/AlexAltea/orbital)'s `tools/ida/db_types.json` (name, length, maximum protection, descriptor out; descriptor, maximum protection, descriptor out; descriptor, protection, address in and out), which ps4sdk, ps5-jar-loader and the call sites in RetroArch PPSA99169, XPSemu PPSA97358 and VLC PPSA85300 match; SharpProspero declares the alias as returning the descriptor. The object is a host anonymous file shared by its descriptors (a `memfd` on Linux, a delete-on-close temporary file mapped as an executable section on Windows), the alias is a duplicate descriptor, and every map maps the whole object, so writes through one view show through the others; a non-null address is a hint, as in `mmap` without `MAP_FIXED`. Not measured on the console: a length that is zero or not a multiple of 16 KiB and protection bits other than CPU read, write and execute are `EINVAL`; an alias or a map asking for more than the descriptor's maximum protection (write and execute counting as read) is `EACCES`, as FreeBSD 11's `vm_map_protect` treats a protection past `max_protection`; a descriptor that is not a JIT object is `EBADF`. The name is ignored, the objects do not count against the flexible memory budget, `mprotect` of a view is not limited to the maximum protection, and `mmap` of the descriptors is unsupported, as for `shm_open`. On Linux the `memfd` is created without `MFD_EXEC`, which Linux 6.3 added and older kernels reject with `EINVAL`, and relies on the protection of the mapping: the flag only sets the file's execute permission bits, and the direct memory `memfd` is mapped executable the same way; a host with `vm.memfd_noexec` set to 2 refuses the creation and the call throws - [kqueue and kevent](../../../core/libs/prx/libkernel/Equeue/src/Kqueue.cpp) (libkernel) follow FreeBSD 11's [`kern_event.c`](https://github.com/freebsd/freebsd-src/blob/34bd34750187bae02a0086402c78c031c59dd8b3/sys/kern/kern_event.c) (`kqueue_kevent`, `kqueue_register`, `kqueue_scan`, the timer and user filters) over the event queue; nothing is measured on a console. The descriptor is a host null device registered as the kqueue: `close` and `sceKernelClose` close the queue and `dup`/`dup2` of it throw, but `read`, `write` and `fstat` act on the null device instead of failing with `EOPNOTSUPP` or reporting `S_IFIFO`. Read and write events are supported on guest sockets only, plus the timer, user and [vnode](../../../core/libs/prx/libkernel/Equeue/src/FileEvent.cpp) filters; other filters, `EV_DISABLE`, `EV_DISPATCH`, `EV_FORCEONESHOT`, `NOTE_LOWAT`, `EV_CLEAR` on a socket, `EVFILT_READ` on another kqueue, a `NOTE_NSECONDS` period above one second with a fraction (11.0 converts the fraction with the microsecond macro), any change to an existing timer (11.0 keeps the armed expiry and applies the new period after it, 11.4's `filt_timertouch` restarts it) and a negative `nevents` throw. A new user event keeps its `fflags` unmasked and is not triggered by `NOTE_TRIGGER`, because `filt_userattach` reads the already cleared `kn_fflags`. The timer limit `kq_calloutmax` (`ENOMEM`) is not enforced, and closing a kqueue wakes a thread waiting on it in `kevent` with `EBADF`, where `kqueue_drain` makes the close wait for that thread. A level-triggered event is reported once per call and moved behind the other events, as in `kqueue_scan`; the `sceKernel*Equeue` calls keep their own collection loop - [EVFILT_VNODE, sceKernelAddFileEvent and sceKernelDeleteFileEvent](../../../core/libs/prx/libkernel/Equeue/src/FileEvent.cpp) (libkernel) watch the host file or directory behind the descriptor and turn host changes into FreeBSD 11 vnode hints, which [`filt_vfsvnode`](https://github.com/freebsd/freebsd-src/blob/34bd34750187bae02a0086402c78c031c59dd8b3/sys/kern/vfs_subr.c#L5032) adds whole when they share a bit with the requested `fflags`: a write that grows a file is `NOTE_WRITE | NOTE_EXTEND`, another write `NOTE_WRITE`, a shrinking truncate or an attribute change `NOTE_ATTRIB`, a higher link count `NOTE_LINK`, a lower one or a pending delete `NOTE_DELETE`, a new path `NOTE_RENAME`; in a directory, an entry created, removed or renamed is `NOTE_WRITE`, with `NOTE_LINK` for a subdirectory, and on Linux an entry moved from or to another directory adds `NOTE_EXTEND`; inotify queues the two halves of a rename one after the other, so a watcher woken by the first can read it alone, and a half read without the other is classified only after a `getdents64` on the watched directory, which takes the directory lock that `rename` holds until both halves are queued ([`do_renameat2`](https://github.com/torvalds/linux/blob/v6.8/fs/namei.c#L4975) around `vfs_rename` and its `fsnotify_move`, [`iterate_dir`](https://github.com/torvalds/linux/blob/v6.8/fs/readdir.c#L103)), an ordering taken from the kernel source, not from inotify(7), which promises none; a directory registered with `NOTE_EXTEND` on Windows is accepted, as `vfs_kqfilter` does not check it, but never receives it, because the directory handle cannot tell a move between directories from a removal plus an addition. FreeBSD's hints depend on the filesystem and the console's filesystems are not known: a growing `ftruncate` reports `NOTE_WRITE | NOTE_EXTEND` where `vop_setattr_post` reports `NOTE_ATTRIB`, and on Windows `utimes` on a file reports `NOTE_WRITE`. Windows uses `FindFirstChangeNotification` on the parent directory (and on a watched directory itself) and compares `GetFileInformationByHandleEx` snapshots at least every 50 ms, because a write through an open handle is reported only when the handle is flushed or closed, so file hints can arrive 50 ms late, and a file deleted while watched, which the POSIX delete of Windows 10 1903 moves into `$Extend\$Deleted` until its last handle closes, reports `NOTE_DELETE` without `NOTE_RENAME` and keeps its handle without a parent directory watch, as that directory cannot be watched; Linux uses inotify on the file and is not run. `NOTE_REVOKE`, `NOTE_OPEN`, `NOTE_CLOSE`, `NOTE_CLOSE_WRITE`, `NOTE_READ`, unknown bits and descriptors other than files, directories, pipes and sockets throw. `sceKernelAddFileEvent(eq, fd, watch, udata)` and `sceKernelDeleteFileEvent(eq, fd)` take the [OpenOrbis](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain/blob/0a1aaf9d/include/orbis/libkernel.h#L35) and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0/src/SharpProspero/Interop/Kernel/KernelEqueue.cs#L164) signatures; `watch` is taken as the `fflags` and the event is added with `EV_CLEAR`, neither confirmed on a title From c3cf2aff08b2045194018c3c9a62206f84663f1f Mon Sep 17 00:00:00 2001 From: Muhammad Wasif Ali Wasif <76695420+wachi35676@users.noreply.github.com> Date: Tue, 13 Oct 2026 23:35:32 +0500 Subject: [PATCH 0135/1000] fix(tools): write technical debt files with LF line endings On Windows debt_split rewrote the index and the debt files with CRLF, and resolve staged them that way when core.autocrlf is off. The files are now written through open(..., newline="\n"), which also works on Python 3.9, where Path.write_text has no newline argument. Co-Authored-By: Claude Opus 5.5 --- tools/debt_split.py | 11 ++++++++--- tools/tests/test_debt_split.py | 4 ++++ 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/tools/debt_split.py b/tools/debt_split.py index 5c27562a4e..2967f6c46a 100644 --- a/tools/debt_split.py +++ b/tools/debt_split.py @@ -77,6 +77,11 @@ def read_debt(root, name): return sections +def write_lf(path, text): + with open(path, "w", encoding="utf-8", newline="\n") as file: + file.write(text) + + def write_debt(root, name, sections, order): path = root / debt_file(name) sections = {section: entries for section, entries in sections.items() if entries} @@ -85,7 +90,7 @@ def write_debt(root, name, sections, order): path.unlink() return path.parent.mkdir(parents=True, exist_ok=True) - path.write_text(render(f"# {name} technical debt", sections, order), encoding="utf-8") + write_lf(path, render(f"# {name} technical debt", sections, order)) def sections_of(text): @@ -132,7 +137,7 @@ def command_split(root): write_debt(root, name, existing, order) if index_note() not in index: index.insert(1, index_note()) - (root / INDEX).write_text(render_index(index), encoding="utf-8") + write_lf(root / INDEX, render_index(index)) print("\n".join(sorted(debt_file(name) for name in files))) @@ -215,7 +220,7 @@ def command_resolve(root): touched = apply_change(root, base, theirs, ours) except LookupError as error: sys.exit(str(error)) - (root / INDEX).write_text(ours, encoding="utf-8") + write_lf(root / INDEX, ours) git(root, "add", "--", INDEX, *touched) print("\n".join(touched)) diff --git a/tools/tests/test_debt_split.py b/tools/tests/test_debt_split.py index 8ffc27c4ca..d8fd887317 100644 --- a/tools/tests/test_debt_split.py +++ b/tools/tests/test_debt_split.py @@ -84,6 +84,8 @@ class DebtSplitTests(unittest.TestCase): self.assertNotIn(entry.split(" - ")[0], text) for kept in ("- A toolchain limitation", "Everything throws, except:", SHARED, "### Silent stubs", "[debt](debt/)"): self.assertIn(kept, text) + for path in (INDEX, "docs/dev/debt/libSceA.md"): + self.assertNotIn(b"\r", (self.root / path).read_bytes()) def test_split_again_appends_to_existing_files(self): self.split() @@ -116,6 +118,8 @@ class DebtSplitTests(unittest.TestCase): self.assertEqual(sorted(result.stdout.split()), ["docs/dev/debt/libSceA.md", "docs/dev/debt/libSceB.md", "docs/dev/debt/libSceC.md"]) self.assertEqual(self.git("diff", "--name-only", "--diff-filter=U").stdout, "") + for path in (INDEX, "docs/dev/debt/libSceA.md"): + self.assertNotIn(b"\r", (self.root / path).read_bytes()) self.git("rebase", "--continue") a = self.read("docs/dev/debt/libSceA.md") self.assertLess(a.index("`sceAOne`"), a.index("`sceAFour`")) From fe490d29de5861930d2c125db36a3c1a6f08628a Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Wed, 14 Oct 2026 17:50:38 +0000 Subject: [PATCH 0136/1000] feat(agc): show draws, pipelines, memory and resolution in the performance overlay The overlay gains five lines below the first ones, each shown only when its data exists: - DRAWS: draw and dispatch packets per frame over the last second. Each packet worker counts them in its own counter, one relaxed load, add and store per packet without a lock prefix, which the overlay sums once per shown frame. - SHADERS: pipelines created in the last second and in total, counted where a graphics pipeline is built for a draw state the cache had not seen and where a compute pipeline is created for a dispatch shader. - VRAM: usage and budget of the largest device-local heap from VK_EXT_memory_budget. - RAM: the process's private memory, PrivateUsage on Windows and RssAnon plus VmSwap from /proc/self/status on Linux. - RES: the display buffer's and the swapchain's size. VRAM and RAM are read at most once a second, and only while the overlay is shown; hidden, the only added work is the packet and pipeline increments. The font gains B, C, H, K, L, O, T and W. With every line the panel is 232 pixels tall at 1080p, a fifth of the window, so it keeps one mode. --- .../Execution/include/PerformanceOverlay.hpp | 64 ++++- .../src/Driver/Packets/PacketExecution.cpp | 3 + .../Execution/src/PerformanceOverlay.cpp | 219 ++++++++++++++++-- .../Execution/src/VulkanDevice.cpp | 3 +- .../libSceAgcDriver/Graphics/src/Pipeline.cpp | 2 + 5 files changed, 269 insertions(+), 22 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/PerformanceOverlay.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/PerformanceOverlay.hpp index 63f8cc5aad..653ff19e76 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/PerformanceOverlay.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/PerformanceOverlay.hpp @@ -5,6 +5,7 @@ #include #include "prx/libSceAgcDriver/Graphics/include/Context.hpp" #include +#include #include #include #include @@ -36,12 +37,40 @@ class PerformanceCounters { public: static void AddGpuNanoseconds(std::uint64_t nanoseconds); static void AddDriverNanoseconds(std::uint64_t nanoseconds); + static void CountPipeline(); static std::uint64_t GpuNanoseconds(); static std::uint64_t DriverNanoseconds(); + static std::uint64_t Pipelines(); static bool GpuEnabled(); static bool DriverEnabled(); }; +class PacketCounters { +public: + struct Totals { + std::uint64_t draws = 0; + std::uint64_t dispatches = 0; + }; + + static PacketCounters& ThisThread(); + static Totals Sum(); + + void CountDraw() { draws.store(draws.load(std::memory_order_relaxed) + 1, std::memory_order_relaxed); } + void CountDispatch() { dispatches.store(dispatches.load(std::memory_order_relaxed) + 1, std::memory_order_relaxed); } + +private: + alignas(64) std::atomic draws{0}; + std::atomic dispatches{0}; +}; + +struct OverlayCounters { + std::uint64_t gpuNanoseconds = 0; + std::uint64_t driverNanoseconds = 0; + std::uint64_t draws = 0; + std::uint64_t dispatches = 0; + std::uint64_t pipelines = 0; +}; + class OverlayStatistics { public: using Clock = std::chrono::steady_clock; @@ -55,6 +84,7 @@ public: }; void AddPresent(Clock::time_point at, std::uint64_t gpuNanoseconds, std::uint64_t driverNanoseconds); + void AddPresent(Clock::time_point at, const OverlayCounters& counters); void Reset(); double Fps() const; double LastFrameMs() const; @@ -62,15 +92,18 @@ public: Range FrameTimeRange() const; double GpuMsPerFrame() const; double DriverMsPerFrame() const; + double DrawsPerFrame() const; + double DispatchesPerFrame() const; + std::uint64_t PipelinesInWindow() const; + OverlayCounters Latest() const; private: struct Sample { Clock::time_point at; - std::uint64_t gpu = 0; - std::uint64_t driver = 0; + OverlayCounters counters; }; - double perFrameMs(std::uint64_t Sample::*counter) const; + double perFrame(std::uint64_t OverlayCounters::*counter) const; std::deque window; std::optional previous; @@ -96,6 +129,18 @@ struct OverlayContent { std::vector below; }; +struct OverlayMemory { + std::uint64_t used = 0; + std::uint64_t budget = 0; +}; + +struct OverlayDetails { + std::optional vram; + std::optional ram; + VkExtent2D display{}; + VkExtent2D window{}; +}; + struct OverlayLayout { OverlayRect bounds; std::vector rects; @@ -118,7 +163,13 @@ struct OverlayMetrics { const std::array& OverlayGlyph(char character); std::string FormatOverlayValue(double value); std::string FormatOverlayRate(double value); +std::string FormatOverlayCount(double value); +std::string FormatOverlayGigabytes(std::uint64_t bytes); OverlayContent ComposeOverlay(const OverlayStatistics& statistics, bool gpu, bool driver); +OverlayContent ComposeOverlay(const OverlayStatistics& statistics, bool gpu, bool driver, const OverlayDetails& details); +bool OverlaySampleDue(std::optional sampledAt, OverlayStatistics::Clock::time_point now); +std::optional ReadVramBudget(const Graphics::Context& context); +std::optional ReadProcessPrivateBytes(); std::int32_t OverlayScale(VkExtent2D extent, const OverlayContent& content); OverlayLayout LayOutOverlay(const OverlayContent& content, VkExtent2D extent, OverlayCorner corner); std::array, OverlayColorCount> OverlayPalette(VkFormat format, VkColorSpaceKHR colorSpace); @@ -134,9 +185,9 @@ public: static void Toggle(); static OverlayCorner Corner(); - void NotePresent(OverlayStatistics::Clock::time_point at); - void NotePresent(OverlayStatistics::Clock::time_point at, std::uint64_t gpuNanoseconds, std::uint64_t driverNanoseconds); + void NotePresent(OverlayStatistics::Clock::time_point at, VkExtent2D display); const OverlayStatistics& Statistics() const { return statistics; } + const OverlayDetails& Details() const { return details; } OverlayRect Record(VkCommandBuffer commands, std::size_t slot, VkImage destination, VkFormat format, VkColorSpaceKHR colorSpace, VkExtent2D extent, VkImageLayout initialLayout, VkImageLayout finalLayout); OverlayRect Record(VkCommandBuffer commands, std::size_t slot, VkImage destination, VkFormat format, VkColorSpaceKHR colorSpace, VkExtent2D extent, VkImageLayout initialLayout, VkImageLayout finalLayout, const OverlayContent& content, OverlayCorner corner); void Release(std::size_t slot) noexcept; @@ -152,6 +203,7 @@ private: VkPipeline pipeline(VkFormat format, VkRenderPass compatible); void releaseTransient(Slot& slot) noexcept; void release() noexcept; + void restartIfToggled(); Graphics::Context context; VkPipelineLayout pipelineLayout = VK_NULL_HANDLE; @@ -161,6 +213,8 @@ private: std::map pipelines; std::vector slots; OverlayStatistics statistics; + OverlayDetails details; + std::optional sampledAt; std::uint64_t toggles = 0; }; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp index 66f9d6d4b6..903d447aaf 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp @@ -109,6 +109,7 @@ void Driver::execute(const Submission& submission) { thread_local PacketProfile* packetProfileSlot = nullptr; auto& packetProfile = ShaderRecompiler::ThreadOwned(packetProfileSlot); ++packetProfile.submissions; + auto& packetCounters = PacketCounters::ThisThread(); bumpEpoch(&EpochBumps::submissions); clearResolvedAhead(); @@ -247,6 +248,7 @@ void Driver::execute(const Submission& submission) { flipHold.Release(); submission.flips.at(cursor)->GpuReady(frame); } else if (opcode == 0x15 || opcode == 0x16) { + packetCounters.CountDispatch(); landPendingWaits(submission.queue); currentPacketOffset() = cursor; if (gateOpened()) { @@ -275,6 +277,7 @@ void Driver::execute(const Submission& submission) { } } } else if (drawPacket) { + packetCounters.CountDraw(); landPendingWaits(submission.queue); bool drawn = false; timed(&WorkerProfile::drawMs, [&] { diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceOverlay.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceOverlay.cpp index ff5e043363..d1c24864a0 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceOverlay.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceOverlay.cpp @@ -5,14 +5,25 @@ #include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" #include "prx/libSceAgcDriver/Graphics/shaders/PerformanceOverlayFrag_spv.h" #include "prx/libSceAgcDriver/Graphics/shaders/PerformanceOverlayVert_spv.h" +#include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp" +#include "prx/libc/include/HostThreadLocal.hpp" #include #include #include #include #include #include +#include #include #include +#ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#include +#endif namespace AgcDriver { namespace { @@ -27,6 +38,30 @@ struct OverlayParameters { std::atomic toggleCount{0}; std::atomic gpuTotal{0}; std::atomic driverTotal{0}; +std::atomic pipelineTotal{0}; + +struct PacketCounterRegistry { + std::mutex mutex; + std::deque counters; + std::vector unused; +}; + +PacketCounterRegistry& packetCounterRegistry() { + static auto* registry = new PacketCounterRegistry; + return *registry; +} + +struct PacketCounterLease { + PacketCounters* counters = nullptr; + ~PacketCounterLease() { + if (counters == nullptr) return; + auto& registry = packetCounterRegistry(); + std::lock_guard lock(registry.mutex); + registry.unused.push_back(counters); + } +}; + +constexpr double GigabyteBytes = 1024.0 * 1024.0 * 1024.0; const OverlaySettings& settings() { static const OverlaySettings parsed = ParseOverlaySettings(std::getenv("ANYPS5_OVERLAY"), std::getenv("ANYPS5_OVERLAY_POS")); @@ -90,6 +125,10 @@ void PerformanceCounters::AddDriverNanoseconds(std::uint64_t nanoseconds) { driverTotal.fetch_add(nanoseconds, std::memory_order_relaxed); } +void PerformanceCounters::CountPipeline() { + pipelineTotal.fetch_add(1, std::memory_order_relaxed); +} + std::uint64_t PerformanceCounters::GpuNanoseconds() { return gpuTotal.load(std::memory_order_relaxed); } @@ -98,6 +137,36 @@ std::uint64_t PerformanceCounters::DriverNanoseconds() { return driverTotal.load(std::memory_order_relaxed); } +std::uint64_t PerformanceCounters::Pipelines() { + return pipelineTotal.load(std::memory_order_relaxed); +} + +PacketCounters& PacketCounters::ThisThread() { + auto& lease = HostThreadLocal(); + if (lease.counters == nullptr) { + auto& registry = packetCounterRegistry(); + std::lock_guard lock(registry.mutex); + if (registry.unused.empty()) { + lease.counters = ®istry.counters.emplace_back(); + } else { + lease.counters = registry.unused.back(); + registry.unused.pop_back(); + } + } + return *lease.counters; +} + +PacketCounters::Totals PacketCounters::Sum() { + auto& registry = packetCounterRegistry(); + std::lock_guard lock(registry.mutex); + Totals totals; + for (const auto& counters : registry.counters) { + totals.draws += counters.draws.load(std::memory_order_relaxed); + totals.dispatches += counters.dispatches.load(std::memory_order_relaxed); + } + return totals; +} + bool PerformanceCounters::GpuEnabled() { return Graphics::Recorder::GpuTimingEnabled(); } @@ -108,6 +177,10 @@ bool PerformanceCounters::DriverEnabled() { } void OverlayStatistics::AddPresent(Clock::time_point at, std::uint64_t gpuNanoseconds, std::uint64_t driverNanoseconds) { + AddPresent(at, OverlayCounters{gpuNanoseconds, driverNanoseconds}); +} + +void OverlayStatistics::AddPresent(Clock::time_point at, const OverlayCounters& counters) { if (!window.empty()) { Graphics::Require(at >= window.back().at, "presents reached the performance overlay out of order"); frameTimes[frameNext] = std::chrono::duration(at - window.back().at).count(); @@ -115,7 +188,7 @@ void OverlayStatistics::AddPresent(Clock::time_point at, std::uint64_t gpuNanose frameCount = std::min(frameCount + 1, FrameHistory); previous = window.back(); } - window.push_back({at, gpuNanoseconds, driverNanoseconds}); + window.push_back({at, counters}); while (at - window.front().at > Window) window.pop_front(); } @@ -161,21 +234,40 @@ OverlayStatistics::Range OverlayStatistics::FrameTimeRange() const { return range; } -double OverlayStatistics::perFrameMs(std::uint64_t Sample::*counter) const { +double OverlayStatistics::perFrame(std::uint64_t OverlayCounters::*counter) const { const bool windowed = window.size() >= 2; if (!windowed && !previous) return 0; - const auto first = windowed ? window.front().*counter : (*previous).*counter; - const auto last = window.back().*counter; + const auto first = windowed ? window.front().counters.*counter : previous->counters.*counter; + const auto last = window.back().counters.*counter; const auto frames = windowed ? window.size() - 1 : std::size_t{1}; - return last >= first ? static_cast(last - first) / 1e6 / static_cast(frames) : 0; + return last >= first ? static_cast(last - first) / static_cast(frames) : 0; } double OverlayStatistics::GpuMsPerFrame() const { - return perFrameMs(&Sample::gpu); + return perFrame(&OverlayCounters::gpuNanoseconds) / 1e6; } double OverlayStatistics::DriverMsPerFrame() const { - return perFrameMs(&Sample::driver); + return perFrame(&OverlayCounters::driverNanoseconds) / 1e6; +} + +double OverlayStatistics::DrawsPerFrame() const { + return perFrame(&OverlayCounters::draws); +} + +double OverlayStatistics::DispatchesPerFrame() const { + return perFrame(&OverlayCounters::dispatches); +} + +std::uint64_t OverlayStatistics::PipelinesInWindow() const { + if (window.empty()) return 0; + const auto first = window.front().counters.pipelines; + const auto last = window.back().counters.pipelines; + return last >= first ? last - first : 0; +} + +OverlayCounters OverlayStatistics::Latest() const { + return window.empty() ? OverlayCounters{} : window.back().counters; } const std::array& OverlayGlyph(char character) { @@ -197,18 +289,26 @@ const std::array& OverlayGlyph(char c static constexpr Glyph colon{0x00, 0x0c, 0x0c, 0x00, 0x0c, 0x0c, 0x00}; static constexpr Glyph slash{0x00, 0x01, 0x02, 0x04, 0x08, 0x10, 0x00}; static constexpr Glyph a{0x0e, 0x11, 0x11, 0x11, 0x1f, 0x11, 0x11}; + static constexpr Glyph b{0x1e, 0x11, 0x11, 0x1e, 0x11, 0x11, 0x1e}; + static constexpr Glyph c{0x0e, 0x11, 0x10, 0x10, 0x10, 0x11, 0x0e}; static constexpr Glyph d{0x1c, 0x12, 0x11, 0x11, 0x11, 0x12, 0x1c}; static constexpr Glyph e{0x1f, 0x10, 0x10, 0x1e, 0x10, 0x10, 0x1f}; static constexpr Glyph f{0x1f, 0x10, 0x10, 0x1e, 0x10, 0x10, 0x10}; static constexpr Glyph g{0x0e, 0x11, 0x10, 0x17, 0x11, 0x11, 0x0f}; + static constexpr Glyph h{0x11, 0x11, 0x11, 0x1f, 0x11, 0x11, 0x11}; static constexpr Glyph i{0x0e, 0x04, 0x04, 0x04, 0x04, 0x04, 0x0e}; + static constexpr Glyph k{0x11, 0x12, 0x14, 0x18, 0x14, 0x12, 0x11}; + static constexpr Glyph l{0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x1f}; static constexpr Glyph m{0x11, 0x1b, 0x15, 0x15, 0x11, 0x11, 0x11}; static constexpr Glyph n{0x11, 0x11, 0x19, 0x15, 0x13, 0x11, 0x11}; + static constexpr Glyph o{0x0e, 0x11, 0x11, 0x11, 0x11, 0x11, 0x0e}; static constexpr Glyph p{0x1e, 0x11, 0x11, 0x1e, 0x10, 0x10, 0x10}; static constexpr Glyph r{0x1e, 0x11, 0x11, 0x1e, 0x14, 0x12, 0x11}; static constexpr Glyph s{0x0f, 0x10, 0x10, 0x0e, 0x01, 0x01, 0x1e}; + static constexpr Glyph t{0x1f, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04}; static constexpr Glyph u{0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x0e}; static constexpr Glyph v{0x11, 0x11, 0x11, 0x11, 0x11, 0x0a, 0x04}; + static constexpr Glyph w{0x11, 0x11, 0x11, 0x15, 0x15, 0x15, 0x0a}; static constexpr Glyph x{0x11, 0x11, 0x0a, 0x04, 0x0a, 0x11, 0x11}; if (character >= '0' && character <= '9') return digits[static_cast(character - '0')]; switch (character) { @@ -217,18 +317,26 @@ const std::array& OverlayGlyph(char c case ':': return colon; case '/': return slash; case 'A': return a; + case 'B': return b; + case 'C': return c; case 'D': return d; case 'E': return e; case 'F': return f; case 'G': return g; + case 'H': return h; case 'I': return i; + case 'K': return k; + case 'L': return l; case 'M': return m; case 'N': return n; + case 'O': return o; case 'P': return p; case 'R': return r; case 'S': return s; + case 'T': return t; case 'U': return u; case 'V': return v; + case 'W': return w; case 'X': return x; default: throw std::runtime_error(std::string("the performance overlay font has no glyph for '") + character + "'"); } @@ -248,7 +356,24 @@ std::string FormatOverlayRate(double value) { return text; } +std::string FormatOverlayCount(double value) { + const long long rounded = std::isfinite(value) && value > 0 ? std::llround(std::min(value, 1e12)) : 0; + char text[24]; + if (rounded < 10000) std::snprintf(text, sizeof(text), "%lld", rounded); + else if (rounded < 1000000) std::snprintf(text, sizeof(text), "%lldK", rounded / 1000); + else std::snprintf(text, sizeof(text), "%lldM", std::min(rounded / 1000000, 999)); + return text; +} + +std::string FormatOverlayGigabytes(std::uint64_t bytes) { + return FormatOverlayValue(static_cast(bytes) / GigabyteBytes); +} + OverlayContent ComposeOverlay(const OverlayStatistics& statistics, bool gpu, bool driver) { + return ComposeOverlay(statistics, gpu, driver, OverlayDetails{}); +} + +OverlayContent ComposeOverlay(const OverlayStatistics& statistics, bool gpu, bool driver, const OverlayDetails& details) { OverlayContent content; content.above.push_back("FPS: " + FormatOverlayRate(statistics.Fps())); content.above.push_back("FRAME: " + FormatOverlayValue(statistics.LastFrameMs()) + " MS"); @@ -257,9 +382,61 @@ OverlayContent ComposeOverlay(const OverlayStatistics& statistics, bool gpu, boo content.below.push_back("MIN/AVG/MAX: " + FormatOverlayValue(range.minimum) + "/" + FormatOverlayValue(range.average) + "/" + FormatOverlayValue(range.maximum)); if (gpu) content.below.push_back("GPU: " + FormatOverlayValue(statistics.GpuMsPerFrame()) + " MS"); if (driver) content.below.push_back("DRIVER: " + FormatOverlayValue(statistics.DriverMsPerFrame()) + " MS"); + const auto latest = statistics.Latest(); + if (latest.draws != 0 || latest.dispatches != 0) content.below.push_back("DRAWS: " + FormatOverlayCount(statistics.DrawsPerFrame()) + " DISPATCH: " + FormatOverlayCount(statistics.DispatchesPerFrame())); + if (latest.pipelines != 0) content.below.push_back("SHADERS: " + FormatOverlayCount(static_cast(statistics.PipelinesInWindow())) + " TOTAL: " + FormatOverlayCount(static_cast(latest.pipelines))); + if (details.vram) content.below.push_back("VRAM: " + FormatOverlayGigabytes(details.vram->used) + "/" + FormatOverlayGigabytes(details.vram->budget) + " GB"); + if (details.ram) content.below.push_back("RAM: " + FormatOverlayGigabytes(*details.ram) + " GB"); + if (details.display.width != 0 && details.display.height != 0 && details.window.width != 0 && details.window.height != 0) { + const auto size = [](VkExtent2D extent) { return std::to_string(extent.width) + "X" + std::to_string(extent.height); }; + for (const std::string_view separator : {" / ", "/"}) { + auto line = "RES: " + size(details.display) + std::string(separator) + size(details.window); + if (line.size() > static_cast(OverlayMetrics::Columns)) continue; + content.below.push_back(std::move(line)); + break; + } + } return content; } +bool OverlaySampleDue(std::optional sampledAt, OverlayStatistics::Clock::time_point now) { + return !sampledAt || now - *sampledAt >= std::chrono::seconds(1); +} + +std::optional ReadVramBudget(const Graphics::Context& context) { + if (context.memoryProperties2 == nullptr || context.physical == VK_NULL_HANDLE) return std::nullopt; + const auto heap = Graphics::LargestDeviceLocalHeap(context.memory); + if (!heap) return std::nullopt; + VkPhysicalDeviceMemoryBudgetPropertiesEXT reported{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_BUDGET_PROPERTIES_EXT}; + VkPhysicalDeviceMemoryProperties2 properties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_PROPERTIES_2, &reported}; + context.memoryProperties2(context.physical, &properties); + if (reported.heapBudget[*heap] == 0) return std::nullopt; + return OverlayMemory{reported.heapUsage[*heap], reported.heapBudget[*heap]}; +} + +std::optional ReadProcessPrivateBytes() { +#ifdef _WIN32 + PROCESS_MEMORY_COUNTERS_EX counters{}; + counters.cb = sizeof(counters); + if (K32GetProcessMemoryInfo(GetCurrentProcess(), reinterpret_cast(&counters), sizeof(counters)) == 0) return std::nullopt; + return static_cast(counters.PrivateUsage); +#else + std::FILE* status = std::fopen("/proc/self/status", "r"); + if (status == nullptr) return std::nullopt; + std::optional anonymousKb; + std::uint64_t swapKb = 0; + char line[256]; + while (std::fgets(line, sizeof(line), status) != nullptr) { + unsigned long long value = 0; + if (std::sscanf(line, "RssAnon: %llu kB", &value) == 1) anonymousKb = value; + else if (std::sscanf(line, "VmSwap: %llu kB", &value) == 1) swapKb = value; + } + std::fclose(status); + if (!anonymousKb) return std::nullopt; + return (*anonymousKb + swapKb) * 1024u; +#endif +} + std::int32_t OverlayScale(VkExtent2D extent, const OverlayContent& content) { auto scale = std::max(1, static_cast((extent.height + 240) / 480)); while (scale > 1 && !fits(extent, content, scale)) --scale; @@ -418,17 +595,25 @@ OverlayCorner PerformanceOverlay::Corner() { return settings().corner; } -void PerformanceOverlay::NotePresent(OverlayStatistics::Clock::time_point at) { - NotePresent(at, PerformanceCounters::GpuEnabled() ? PerformanceCounters::GpuNanoseconds() : 0, PerformanceCounters::DriverEnabled() ? PerformanceCounters::DriverNanoseconds() : 0); +void PerformanceOverlay::NotePresent(OverlayStatistics::Clock::time_point at, VkExtent2D display) { + restartIfToggled(); + if (OverlaySampleDue(sampledAt, at)) { + details.vram = ReadVramBudget(context); + details.ram = ReadProcessPrivateBytes(); + sampledAt = at; + } + details.display = display; + const auto packets = PacketCounters::Sum(); + statistics.AddPresent(at, OverlayCounters{PerformanceCounters::GpuEnabled() ? PerformanceCounters::GpuNanoseconds() : 0, PerformanceCounters::DriverEnabled() ? PerformanceCounters::DriverNanoseconds() : 0, packets.draws, packets.dispatches, PerformanceCounters::Pipelines()}); } -void PerformanceOverlay::NotePresent(OverlayStatistics::Clock::time_point at, std::uint64_t gpuNanoseconds, std::uint64_t driverNanoseconds) { +void PerformanceOverlay::restartIfToggled() { const auto current = toggleCount.load(std::memory_order_relaxed); - if (current != toggles) { - statistics.Reset(); - toggles = current; - } - statistics.AddPresent(at, gpuNanoseconds, driverNanoseconds); + if (current == toggles) return; + statistics.Reset(); + details = {}; + sampledAt.reset(); + toggles = current; } VkRenderPass PerformanceOverlay::renderPass(VkFormat format, VkImageLayout initialLayout, VkImageLayout finalLayout) { @@ -530,7 +715,9 @@ VkPipeline PerformanceOverlay::pipeline(VkFormat format, VkRenderPass compatible } OverlayRect PerformanceOverlay::Record(VkCommandBuffer commands, std::size_t slot, VkImage destination, VkFormat format, VkColorSpaceKHR colorSpace, VkExtent2D extent, VkImageLayout initialLayout, VkImageLayout finalLayout) { - return Record(commands, slot, destination, format, colorSpace, extent, initialLayout, finalLayout, ComposeOverlay(statistics, PerformanceCounters::GpuEnabled(), PerformanceCounters::DriverEnabled()), Corner()); + auto shown = details; + shown.window = extent; + return Record(commands, slot, destination, format, colorSpace, extent, initialLayout, finalLayout, ComposeOverlay(statistics, PerformanceCounters::GpuEnabled(), PerformanceCounters::DriverEnabled(), shown), Corner()); } OverlayRect PerformanceOverlay::Record(VkCommandBuffer commands, std::size_t index, VkImage destination, VkFormat format, VkColorSpaceKHR colorSpace, VkExtent2D extent, VkImageLayout initialLayout, VkImageLayout finalLayout, const OverlayContent& content, OverlayCorner corner) { diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index bc614c2e0a..59d5ce689e 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -3079,7 +3079,7 @@ bool VulkanDevice::present(std::uint32_t width, std::uint32_t height, bool opaqu } if (overlay) { if (!state->overlay) state->overlay = std::make_unique(graphicsContext(), state->presentSlots.size()); - state->overlay->NotePresent(std::chrono::steady_clock::now()); + state->overlay->NotePresent(std::chrono::steady_clock::now(), clearOnly ? VkExtent2D{} : VkExtent2D{width, height}); state->overlay->Record(commands, static_cast(&slot - state->presentSlots.data()), barrier.image, state->surfaceFormat, state->surfaceColorSpace, state->extent, drawn ? VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL : VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR); } else if (!drawn) { barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; @@ -4540,6 +4540,7 @@ VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::Rec CreateComputePipeline(state->device, state->DeviceFunction("vkCreateComputePipelines"), context.pipelineCache, objects->module, objects->layout, pipelineFlags, 0, specialization.Info(), subgroupSize, &objects->pipeline, context.pipelineCacheSynchronization); objects->destroyModule(objects->device, objects->module, nullptr); objects->module = VK_NULL_HANDLE; + PerformanceCounters::CountPipeline(); Graphics::LogPipelineStatistics_nid_no_patch(context, objects->pipeline); timing.Mark("pipeline_create"); if (pipelineKey != 0) { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp index 87ae687aad..6096ed0aeb 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp @@ -1,4 +1,5 @@ #include "prx/libSceAgcDriver/Execution/include/ProfileOutput.hpp" +#include "prx/libSceAgcDriver/Execution/include/PerformanceOverlay.hpp" #include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp" #include "prx/libSceAgcDriver/Graphics/include/Pipeline.hpp" #include "prx/libSceAgcDriver/Graphics/include/PipelineLibrary.hpp" @@ -528,6 +529,7 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput for (const auto module : _modules) context.Function("vkDestroyShaderModule")(context.device, module, nullptr); _modules.clear(); timing.Mark("create"); + PerformanceCounters::CountPipeline(); LogPipelineStatistics_nid_no_patch(context, pipeline); } catch (...) { release(); From 6151c4f588073676280feee63c72d87b837599ac Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Wed, 14 Oct 2026 17:50:38 +0000 Subject: [PATCH 0137/1000] test(agc): cover the performance overlay's new lines agc_performance_overlay_statistics checks the per-frame draw and dispatch averages, the pipelines of the last second, the per-worker packet counters across threads, the count and size formats, the order and omission of the new lines, the RES fallback for long sizes, the glyph bits of every line of the full panel and of the new letters, the once-a-second sampling and that touching 64 MiB raises the private memory. It also prints what a counted packet costs. agc_performance_overlay draws the full panel at 1920x1080 on lavapipe and compares every glyph pixel of every line, with nothing changed outside the panel; reads VRAM through VK_EXT_memory_budget; checks that a shown frame samples the memory and draws the VRAM, RAM and RES lines; and keeps the hidden presentation bit-identical with the full panel. --- .../tests/PerformanceOverlay.cpp | 184 +++++++++++--- .../tests/PerformanceOverlayStatistics.cpp | 232 ++++++++++++++++-- 2 files changed, 362 insertions(+), 54 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlay.cpp b/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlay.cpp index b3e573052b..0992ffadeb 100644 --- a/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlay.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlay.cpp @@ -61,6 +61,12 @@ public: while (family < count && (families[family].queueFlags & VK_QUEUE_GRAPHICS_BIT) == 0) ++family; Require(family < count, "no Vulkan graphics queue"); timestampBits = families[family].timestampValidBits; + const auto enumerateExtensions = function("vkEnumerateDeviceExtensionProperties"); + Check(enumerateExtensions(context.physical, nullptr, &count, nullptr), "vkEnumerateDeviceExtensionProperties"); + std::vector extensions(count); + Check(enumerateExtensions(context.physical, nullptr, &count, extensions.data()), "vkEnumerateDeviceExtensionProperties"); + memoryBudget = std::any_of(extensions.begin(), extensions.end(), [](const VkExtensionProperties& extension) { return std::string(extension.extensionName) == VK_EXT_MEMORY_BUDGET_EXTENSION_NAME; }); + const char* const budgetExtension = VK_EXT_MEMORY_BUDGET_EXTENSION_NAME; const float priority = 1; VkDeviceQueueCreateInfo queue{VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO}; queue.queueFamilyIndex = family; @@ -69,7 +75,10 @@ public: VkDeviceCreateInfo device{VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO}; device.queueCreateInfoCount = 1; device.pQueueCreateInfos = &queue; + device.enabledExtensionCount = memoryBudget ? 1 : 0; + device.ppEnabledExtensionNames = memoryBudget ? &budgetExtension : nullptr; Check(function("vkCreateDevice")(context.physical, &device, nullptr, &context.device), "vkCreateDevice"); + if (memoryBudget) context.memoryProperties2 = function("vkGetPhysicalDeviceMemoryProperties2"); context.deviceProc = function("vkGetDeviceProcAddr"); function("vkGetPhysicalDeviceMemoryProperties")(context.physical, &context.memory); VkPhysicalDeviceProperties properties{}; @@ -93,6 +102,7 @@ public: const Context& GetContext() const { return context; } const std::string& Name() const { return name; } std::uint32_t TimestampBits() const { return timestampBits; } + bool MemoryBudget() const { return memoryBudget; } private: template @@ -116,6 +126,7 @@ private: Context context{}; std::string name; std::uint32_t timestampBits = 0; + bool memoryBudget = false; }; class Image { @@ -247,6 +258,72 @@ int channel(std::uint32_t word, unsigned shift) { return static_cast((word >> shift) & 0xffu); } +void requireTextDrawn(const std::vector& drawn, VkExtent2D extent, const OverlayRect& bounds, std::int32_t scale, std::uint32_t frameWord, const std::string& line, std::int32_t row, std::size_t first = 0, std::size_t count = std::string::npos) { + const auto end = count == std::string::npos ? line.size() : std::min(line.size(), first + count); + for (std::size_t index = first; index < end; ++index) { + const auto& glyph = AgcDriver::OverlayGlyph(line[index]); + const auto column = static_cast(index); + for (std::int32_t gy = 0; gy < OverlayMetrics::GlyphHeight; ++gy) { + for (std::int32_t gx = 0; gx < OverlayMetrics::Advance; ++gx) { + const bool set = gx < OverlayMetrics::GlyphWidth && (glyph[static_cast(gy)] & (0x10u >> gx)) != 0; + for (std::int32_t py = 0; py < scale; ++py) { + for (std::int32_t px = 0; px < scale; ++px) { + const auto x = bounds.x + (OverlayMetrics::Padding + column * OverlayMetrics::Advance + gx) * scale + px; + const auto y = bounds.y + (row + gy) * scale + py; + const auto word = drawn[static_cast(y) * extent.width + static_cast(x)]; + const auto where = std::string("'") + line[index] + "' of '" + line + "' glyph pixel " + std::to_string(gx) + "," + std::to_string(gy); + Require(channel(word, 24) == channel(frameWord, 24), where + " changed the alpha"); + if (set) { + Require((word & 0x00ffffffu) == 0x00ffffffu, where + " is not white text: 0x" + std::to_string(word)); + continue; + } + for (const unsigned shift : {0u, 8u, 16u}) { + const int expected = static_cast(std::lround(channel(frameWord, shift) * 0.4)); + Require(std::abs(channel(word, shift) - expected) <= 1, where + " is not the panel over the frame: " + std::to_string(channel(word, shift)) + " for " + std::to_string(expected)); + } + } + } + } + } + } +} + +AgcDriver::OverlayDetails measuredDetails(const Context& context, VkExtent2D display, VkExtent2D window) { + AgcDriver::OverlayDetails details; + details.vram = AgcDriver::ReadVramBudget(context); + details.ram = AgcDriver::ReadProcessPrivateBytes(); + details.display = display; + details.window = window; + return details; +} + +AgcDriver::OverlayDetails fixedDetails(VkExtent2D display, VkExtent2D window) { + constexpr double GiB = 1024.0 * 1024.0 * 1024.0; + AgcDriver::OverlayDetails details; + details.vram = AgcDriver::OverlayMemory{static_cast(6.1 * GiB), static_cast(7.8 * GiB)}; + details.ram = static_cast(3.2 * GiB); + details.display = display; + details.window = window; + return details; +} + +OverlayContent fullPanel(const AgcDriver::OverlayDetails& details) { + AgcDriver::OverlayStatistics statistics; + const auto start = AgcDriver::OverlayStatistics::Clock::time_point{} + std::chrono::hours(1); + for (std::uint64_t i = 0; i <= 60; ++i) { + AgcDriver::OverlayCounters counters; + counters.gpuNanoseconds = i * 4'200'000u; + counters.driverNanoseconds = i * 1'300'000u; + counters.draws = i * 1240; + counters.dispatches = i * 85; + counters.pipelines = i < 50 ? 1500 : 1503; + statistics.AddPresent(start + std::chrono::nanoseconds(16'666'667) * i, counters); + } + auto content = AgcDriver::ComposeOverlay(statistics, true, true, details); + Require(content.above.front() == "FPS: 60.0" && content.below.size() == 5u + (details.vram ? 1u : 0u) + (details.ram ? 1u : 0u) + (details.display.width != 0 ? 1u : 0u), "the full panel composes as '" + content.above.front() + "' with " + std::to_string(content.below.size()) + " lines below its graph"); + return content; +} + void sdrOverlayTest(const Context& context, PerformanceOverlay& overlay) { const VkExtent2D extent{1280, 720}; VkClearColorValue clear{}; @@ -261,36 +338,10 @@ void sdrOverlayTest(const Context& context, PerformanceOverlay& overlay) { Require(bounds.x == AgcDriver::LayOutOverlay(content, extent, OverlayCorner::TopLeft).bounds.x && bounds.width > 0 && bounds.height > 0, "the overlay drew another panel than its layout"); onlyInside(reference, drawn, extent, bounds, "SDR 1280x720"); const auto scale = AgcDriver::OverlayScale(extent, content); - const auto clearWord = reference.front(); const std::string digits = "60.0"; const auto firstColumn = content.above.front().find(digits); Require(firstColumn != std::string::npos, "the FPS line has no '60.0'"); - for (std::size_t i = 0; i < digits.size(); ++i) { - const auto& glyph = AgcDriver::OverlayGlyph(digits[i]); - const auto column = static_cast(firstColumn + i); - for (std::int32_t gy = 0; gy < OverlayMetrics::GlyphHeight; ++gy) { - for (std::int32_t gx = 0; gx < OverlayMetrics::Advance; ++gx) { - const bool set = gx < OverlayMetrics::GlyphWidth && (glyph[static_cast(gy)] & (0x10u >> gx)) != 0; - for (std::int32_t py = 0; py < scale; ++py) { - for (std::int32_t px = 0; px < scale; ++px) { - const auto x = bounds.x + (OverlayMetrics::Padding + column * OverlayMetrics::Advance + gx) * scale + px; - const auto y = bounds.y + (OverlayMetrics::Padding + gy) * scale + py; - const auto word = drawn[static_cast(y) * extent.width + static_cast(x)]; - const auto where = std::string("'") + digits[i] + "' glyph pixel " + std::to_string(gx) + "," + std::to_string(gy); - Require(channel(word, 24) == channel(clearWord, 24), where + " changed the alpha"); - if (set) { - Require((word & 0x00ffffffu) == 0x00ffffffu, where + " is not white text: 0x" + std::to_string(word)); - continue; - } - for (const unsigned shift : {0u, 8u, 16u}) { - const int expected = static_cast(std::lround(channel(clearWord, shift) * 0.4)); - Require(std::abs(channel(word, shift) - expected) <= 1, where + " is not the panel over the frame: " + std::to_string(channel(word, shift)) + " for " + std::to_string(expected)); - } - } - } - } - } - } + requireTextDrawn(drawn, extent, bounds, scale, reference.front(), content.above.front(), OverlayMetrics::Padding, firstColumn, digits.size()); std::printf("SDR overlay: panel %dx%d at %d,%d (scale %d), FPS digits drawn bit for bit\n", bounds.width, bounds.height, bounds.x, bounds.y, scale); } @@ -352,6 +403,69 @@ void cornerTest(const Context& context, PerformanceOverlay& overlay) { std::printf("overlay corners: tl, tr, bl and br drawn where the layout puts them\n"); } +void fullPanelTest(const Device& device, PerformanceOverlay& overlay) { + const auto& context = device.GetContext(); + const VkExtent2D extent{1920, 1080}; + auto details = measuredDetails(context, extent, extent); + Require(details.ram.has_value(), "the process's private memory cannot be read"); + Require(details.vram.has_value() == device.MemoryBudget(), device.MemoryBudget() ? "VK_EXT_memory_budget gives no device-local budget" : "VRAM is read without VK_EXT_memory_budget"); + auto withoutBudget = context; + withoutBudget.memoryProperties2 = nullptr; + Require(!AgcDriver::ReadVramBudget(withoutBudget).has_value(), "VRAM is read without vkGetPhysicalDeviceMemoryProperties2"); + VkClearColorValue clear{}; + clear.float32[0] = 0.75f; + clear.float32[1] = 0.5f; + clear.float32[2] = 0.25f; + clear.float32[3] = 1.0f; + const auto content = fullPanel(details); + OverlayRect bounds{}; + const auto reference = clearAndOverlay(context, nullptr, VK_FORMAT_B8G8R8A8_UNORM, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR, extent, clear, content, OverlayCorner::TopLeft, nullptr); + const auto drawn = clearAndOverlay(context, &overlay, VK_FORMAT_B8G8R8A8_UNORM, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR, extent, clear, content, OverlayCorner::TopLeft, &bounds); + const auto layout = AgcDriver::LayOutOverlay(content, extent, OverlayCorner::TopLeft); + Require(bounds.x == layout.bounds.x && bounds.y == layout.bounds.y && bounds.width == layout.bounds.width && bounds.height == layout.bounds.height, "the full panel is not drawn where its layout puts it"); + onlyInside(reference, drawn, extent, bounds, "full panel 1920x1080"); + std::int32_t row = OverlayMetrics::Padding; + for (const auto& line : content.above) { + requireTextDrawn(drawn, extent, bounds, layout.scale, reference.front(), line, row); + row += OverlayMetrics::LinePitch; + } + row += OverlayMetrics::GraphHeight + OverlayMetrics::GraphGap; + for (const auto& line : content.below) { + requireTextDrawn(drawn, extent, bounds, layout.scale, reference.front(), line, row); + row += OverlayMetrics::LinePitch; + } + std::printf("full panel at 1920x1080: %dx%d at %d,%d (scale %d, %.1f%% of the height), every line drawn bit for bit, nothing changed outside it:\n", bounds.width, bounds.height, bounds.x, bounds.y, layout.scale, 100.0 * bounds.height / extent.height); + for (const auto& line : content.above) std::printf(" %s\n", line.c_str()); + std::printf(" (graph of %zu frame times)\n", content.frameTimes.size()); + for (const auto& line : content.below) std::printf(" %s\n", line.c_str()); +} + +void notePresentTest(const Device& device) { + const auto& context = device.GetContext(); + const VkExtent2D extent{1280, 720}; + PerformanceOverlay overlay(context, 1); + overlay.NotePresent(AgcDriver::OverlayStatistics::Clock::now(), VkExtent2D{640, 360}); + const auto& details = overlay.Details(); + Require(details.ram.has_value() && details.vram.has_value() == device.MemoryBudget() && details.display.width == 640 && details.display.height == 360, "a shown frame did not sample the memory or keep the display buffer size"); + Image destination(context, VK_FORMAT_B8G8R8A8_UNORM, extent, VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT); + OverlayRect bounds{}; + { + CommandBatch batch(context); + const auto commands = batch.Handle(); + transition(context, commands, destination.Handle(), VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, VK_ACCESS_TRANSFER_WRITE_BIT); + bounds = overlay.Record(commands, 0, destination.Handle(), VK_FORMAT_B8G8R8A8_UNORM, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR, extent, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL); + batch.SubmitAndWait(); + } + overlay.Release(0); + auto shown = details; + shown.window = extent; + const auto expected = AgcDriver::ComposeOverlay(overlay.Statistics(), AgcDriver::PerformanceCounters::GpuEnabled(), AgcDriver::PerformanceCounters::DriverEnabled(), shown); + Require(expected.below.back() == "RES: 640X360 / 1280X720", "the RES line reads '" + expected.below.back() + "'"); + const auto layout = AgcDriver::LayOutOverlay(expected, extent, PerformanceOverlay::Corner()); + Require(bounds.height == layout.bounds.height && bounds.width == layout.bounds.width, "a shown frame's panel does not hold the sampled VRAM, RAM and RES lines"); + std::printf("shown frame: memory sampled, %zu lines below the graph, the last '%s'\n", expected.below.size(), expected.below.back().c_str()); +} + class Presenter { public: explicit Presenter(const Context& context) : context(context), pass(context, 1), overlay(context, 1), source(context, VK_FORMAT_B8G8R8A8_UNORM, {256, 144}, VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT, VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT | VK_IMAGE_CREATE_EXTENDED_USAGE_BIT) { @@ -381,7 +495,7 @@ public: CommandBatch batch(context); const auto commands = batch.Handle(); pass.RecordPresent(commands, 0, destination.Handle(), VK_FORMAT_B8G8R8A8_UNORM, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR, Extent, visible ? VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL : VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL); - if (visible) *bounds = overlay.Record(commands, 0, destination.Handle(), VK_FORMAT_B8G8R8A8_UNORM, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR, Extent, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, sixtyFps(), OverlayCorner::TopLeft); + if (visible) *bounds = overlay.Record(commands, 0, destination.Handle(), VK_FORMAT_B8G8R8A8_UNORM, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR, Extent, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, fullPanel(fixedDetails({256, 144}, Extent)), OverlayCorner::TopLeft); readImage(context, commands, destination, readback); batch.SubmitAndWait(); } @@ -406,7 +520,7 @@ public: transition(context, commands, destination.Handle(), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_WRITE_BIT); AgcDriver::PresentationScaler::RecordBlitFrom(context, commands, source.Handle(), VK_IMAGE_LAYOUT_GENERAL, 256, 144, VK_FILTER_LINEAR, destination.Handle(), Extent.width, Extent.height); if (visible) { - *bounds = overlay.Record(commands, 0, destination.Handle(), VK_FORMAT_B8G8R8A8_UNORM, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR, Extent, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, sixtyFps(), OverlayCorner::TopLeft); + *bounds = overlay.Record(commands, 0, destination.Handle(), VK_FORMAT_B8G8R8A8_UNORM, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR, Extent, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, fullPanel(fixedDetails({256, 144}, Extent)), OverlayCorner::TopLeft); } else { transition(context, commands, destination.Handle(), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT); } @@ -456,11 +570,13 @@ void costReport(const Device& device, PerformanceOverlay& overlay) { info.queryCount = 2; VkQueryPool queries = VK_NULL_HANDLE; Check(context.Function("vkCreateQueryPool")(context.device, &info, nullptr, &queries), "vkCreateQueryPool overlay cost"); + const auto sampled = std::chrono::steady_clock::now(); + for (int run = 0; run < 100; ++run) static_cast(AgcDriver::ReadVramBudget(context)); + const auto vramUs = std::chrono::duration(std::chrono::steady_clock::now() - sampled).count() / 100; + std::printf("VRAM budget read in %.1f us (%s), once a second while the overlay is shown\n", vramUs, device.MemoryBudget() ? "VK_EXT_memory_budget" : "no VK_EXT_memory_budget"); for (const VkExtent2D extent : {VkExtent2D{1920, 1080}, VkExtent2D{3840, 2160}}) { Image destination(context, VK_FORMAT_B8G8R8A8_UNORM, extent, VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT); - auto content = sixtyFps(); - content.below.push_back("GPU: 4.2 MS"); - content.below.push_back("DRIVER: 1.3 MS"); + const auto content = fullPanel(fixedDetails(extent, extent)); std::vector gpuMs; std::vector cpuMs; for (int run = 0; run < 12; ++run) { @@ -481,7 +597,7 @@ void costReport(const Device& device, PerformanceOverlay& overlay) { } std::sort(gpuMs.begin(), gpuMs.end()); std::sort(cpuMs.begin(), cpuMs.end()); - std::printf("overlay cost on %s at %ux%u: GPU median %.3f ms (min %.3f, max %.3f), recording median %.3f ms\n", device.Name().c_str(), extent.width, extent.height, gpuMs[gpuMs.size() / 2], gpuMs.front(), gpuMs.back(), cpuMs[cpuMs.size() / 2]); + std::printf("full panel cost on %s at %ux%u: GPU median %.3f ms (min %.3f, max %.3f), recording median %.3f ms\n", device.Name().c_str(), extent.width, extent.height, gpuMs[gpuMs.size() / 2], gpuMs.front(), gpuMs.back(), cpuMs[cpuMs.size() / 2]); } context.Function("vkDestroyQueryPool")(context.device, queries, nullptr); } @@ -506,8 +622,10 @@ int main() { sdrOverlayTest(context, overlay); hdrOverlayTest(context, overlay); cornerTest(context, overlay); + fullPanelTest(*device, overlay); costReport(*device, overlay); } + notePresentTest(*device); disabledIsUnchangedTest(context); std::cout << "performance overlay tests passed\n"; return 0; diff --git a/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlayStatistics.cpp b/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlayStatistics.cpp index 504da82594..583565c9f2 100644 --- a/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlayStatistics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlayStatistics.cpp @@ -3,9 +3,14 @@ #include #include #include +#include +#include #include +#include +#include #include #include +#include #include namespace { @@ -197,6 +202,25 @@ std::vector coverage(const AgcDriver::OverlayLayout& layout, std:: return covered; } +void requireLineDrawn(const AgcDriver::OverlayLayout& layout, const std::vector& text, const std::string& line, std::int32_t top) { + const auto s = layout.scale; + for (std::size_t column = 0; column < line.size(); ++column) { + const auto& glyph = AgcDriver::OverlayGlyph(line[column]); + for (std::int32_t gy = 0; gy < OverlayMetrics::GlyphHeight; ++gy) { + for (std::int32_t gx = 0; gx < OverlayMetrics::Advance; ++gx) { + const bool set = gx < OverlayMetrics::GlyphWidth && (glyph[static_cast(gy)] & (0x10u >> gx)) != 0; + for (std::int32_t py = 0; py < s; ++py) { + for (std::int32_t px = 0; px < s; ++px) { + const auto x = (OverlayMetrics::Padding + static_cast(column) * OverlayMetrics::Advance + gx) * s + px; + const auto y = (top + gy) * s + py; + require(text[static_cast(y) * static_cast(layout.bounds.width) + static_cast(x)] == (set ? 1 : 0), "'" + std::string(1, line[column]) + "' of '" + line + "' is drawn wrong at glyph pixel " + std::to_string(gx) + "," + std::to_string(gy)); + } + } + } + } + } +} + void layoutTests() { OverlayContent content{{"FPS: 60.0", "FRAME: 16.7 MS"}, {}, {"MIN/AVG/MAX: 16.7/16.7/16.7"}}; for (int i = 0; i < 40; ++i) content.frameTimes.push_back(i == 39 ? 50.0 : 10.0); @@ -215,26 +239,9 @@ void layoutTests() { const auto text = coverage(layout, static_cast(AgcDriver::OverlayColor::Text)); const auto lineTop = [&](std::size_t line) { return OverlayMetrics::Padding + static_cast(line) * OverlayMetrics::LinePitch; }; - const auto checkLine = [&](const std::string& line, std::int32_t top) { - for (std::size_t column = 0; column < line.size(); ++column) { - const auto& glyph = AgcDriver::OverlayGlyph(line[column]); - for (std::int32_t gy = 0; gy < OverlayMetrics::GlyphHeight; ++gy) { - for (std::int32_t gx = 0; gx < OverlayMetrics::Advance; ++gx) { - const bool set = gx < OverlayMetrics::GlyphWidth && (glyph[static_cast(gy)] & (0x10u >> gx)) != 0; - for (std::int32_t py = 0; py < s; ++py) { - for (std::int32_t px = 0; px < s; ++px) { - const auto x = (OverlayMetrics::Padding + static_cast(column) * OverlayMetrics::Advance + gx) * s + px; - const auto y = (top + gy) * s + py; - require(text[static_cast(y) * static_cast(layout.bounds.width) + static_cast(x)] == (set ? 1 : 0), "'" + std::string(1, line[column]) + "' of '" + line + "' is drawn wrong at glyph pixel " + std::to_string(gx) + "," + std::to_string(gy)); - } - } - } - } - } - }; - checkLine(content.above[0], lineTop(0)); - checkLine(content.above[1], lineTop(1)); - checkLine(content.below[0], lineTop(2) + OverlayMetrics::GraphHeight + OverlayMetrics::GraphGap); + requireLineDrawn(layout, text, content.above[0], lineTop(0)); + requireLineDrawn(layout, text, content.above[1], lineTop(1)); + requireLineDrawn(layout, text, content.below[0], lineTop(2) + OverlayMetrics::GraphHeight + OverlayMetrics::GraphGap); std::vector bars; for (const auto& rect : layout.rects) { @@ -273,6 +280,182 @@ void layoutTests() { require(refused, "a character without a glyph was drawn"); } +constexpr std::uint64_t GiB = 1ull << 30u; + +OverlayStatistics countedStatistics() { + OverlayStatistics statistics; + for (std::uint64_t i = 0; i <= 60; ++i) { + AgcDriver::OverlayCounters counters; + counters.gpuNanoseconds = i * 4'000'000u; + counters.driverNanoseconds = i * 1'500'000u; + counters.draws = 5000 + i * 1240; + counters.dispatches = 70 + i * 85; + counters.pipelines = i < 50 ? 1500 : 1503; + statistics.AddPresent(Start + i * 16ms, counters); + } + return statistics; +} + +AgcDriver::OverlayDetails fullDetails() { + AgcDriver::OverlayDetails details; + details.vram = AgcDriver::OverlayMemory{static_cast(6.1 * GiB), static_cast(7.8 * GiB)}; + details.ram = static_cast(3.2 * GiB); + details.display = {1920, 1080}; + details.window = {2560, 1440}; + return details; +} + +void packetStatisticsTests() { + const auto statistics = countedStatistics(); + require(near(statistics.DrawsPerFrame(), 1240) && near(statistics.DispatchesPerFrame(), 85), "1240 draws and 85 dispatches per frame read as " + std::to_string(statistics.DrawsPerFrame()) + " and " + std::to_string(statistics.DispatchesPerFrame())); + require(statistics.PipelinesInWindow() == 3 && statistics.Latest().pipelines == 1503, "3 pipelines created within the last second of 1503 read as " + std::to_string(statistics.PipelinesInWindow()) + " of " + std::to_string(statistics.Latest().pipelines)); + require(near(statistics.GpuMsPerFrame(), 4.0) && near(statistics.DriverMsPerFrame(), 1.5), "the time counters changed with the packet counters"); + auto stalled = statistics; + auto counters = stalled.Latest(); + counters.pipelines += 7; + stalled.AddPresent(Start + 3000ms, counters); + require(stalled.DrawsPerFrame() == 0 && stalled.PipelinesInWindow() == 0 && stalled.Latest().pipelines == 1510, "a present alone in its second reads as packets or pipelines of that second"); + auto restarted = statistics; + counters = restarted.Latest(); + counters.draws = 3; + restarted.AddPresent(Start + 61 * 16ms, counters); + require(restarted.DrawsPerFrame() == 0, "a draw counter that went back is read as draws"); + OverlayStatistics empty; + require(empty.PipelinesInWindow() == 0 && empty.Latest().draws == 0, "an overlay without presents shows packet counts"); +} + +void packetCounterTests() { + const auto before = AgcDriver::PacketCounters::Sum(); + auto& counters = AgcDriver::PacketCounters::ThisThread(); + require(&counters == &AgcDriver::PacketCounters::ThisThread(), "one thread got two packet counters"); + for (int i = 0; i < 1240; ++i) counters.CountDraw(); + for (int i = 0; i < 85; ++i) counters.CountDispatch(); + bool separate = false; + std::thread worker([&] { + auto& own = AgcDriver::PacketCounters::ThisThread(); + separate = &own != &counters; + for (int i = 0; i < 1000; ++i) own.CountDraw(); + own.CountDispatch(); + }); + worker.join(); + require(separate, "two workers share packet counters"); + const auto after = AgcDriver::PacketCounters::Sum(); + require(after.draws - before.draws == 2240 && after.dispatches - before.dispatches == 86, "two workers' 2240 draws and 86 dispatches sum to " + std::to_string(after.draws - before.draws) + " and " + std::to_string(after.dispatches - before.dispatches)); + std::thread next([] { AgcDriver::PacketCounters::ThisThread().CountDraw(); }); + next.join(); + require(AgcDriver::PacketCounters::Sum().draws == after.draws + 1, "the counts of a worker that ended were lost, or a new worker restarted them"); +} + +void countFormatTests() { + const std::pair counts[] = {{0, "0"}, {0.4, "0"}, {0.5, "1"}, {85, "85"}, {1239.6, "1240"}, {9999, "9999"}, {9999.5, "10K"}, {12345, "12K"}, {999999, "999K"}, {1e6, "1M"}, {2.5e9, "999M"}, {-3, "0"}, {std::nan(""), "0"}, {INFINITY, "0"}}; + for (const auto& [value, expected] : counts) require(AgcDriver::FormatOverlayCount(value) == expected, std::to_string(value) + " is counted as '" + AgcDriver::FormatOverlayCount(value) + "', not '" + expected + "'"); + const std::pair sizes[] = {{0, "0.0"}, {static_cast(6.1 * GiB), "6.1"}, {static_cast(7.8 * GiB), "7.8"}, {GiB / 2, "0.5"}, {1536 * GiB, "1536"}, {20000 * GiB, "9999"}}; + for (const auto& [bytes, expected] : sizes) require(AgcDriver::FormatOverlayGigabytes(bytes) == expected, std::to_string(bytes) + " bytes are shown as '" + AgcDriver::FormatOverlayGigabytes(bytes) + "' GB, not '" + expected + "'"); +} + +void detailComposeTests() { + const auto statistics = countedStatistics(); + const auto full = AgcDriver::ComposeOverlay(statistics, true, true, fullDetails()); + const std::vector expected{"MIN/AVG/MAX: 16.0/16.0/16.0", "GPU: 4.0 MS", "DRIVER: 1.5 MS", "DRAWS: 1240 DISPATCH: 85", "SHADERS: 3 TOTAL: 1503", "VRAM: 6.1/7.8 GB", "RAM: 3.2 GB", "RES: 1920X1080 / 2560X1440"}; + require(full.below == expected, "the full panel's lines below the graph read '" + (full.below.empty() ? std::string() : full.below.back()) + "' last, of " + std::to_string(full.below.size())); + for (const auto& line : full.below) require(line.size() <= static_cast(OverlayMetrics::Columns), "'" + line + "' is wider than the panel"); + + OverlayStatistics uncounted; + for (int i = 0; i <= 60; ++i) uncounted.AddPresent(Start + i * 16ms, 0, 0); + const auto plain = AgcDriver::ComposeOverlay(uncounted, false, false); + const auto none = AgcDriver::ComposeOverlay(uncounted, false, false, AgcDriver::OverlayDetails{}); + require(none.above == plain.above && none.below == plain.below && none.below.size() == 1, "a panel without packets, pipelines, memory or a display buffer shows more than the first lines"); + + auto details = fullDetails(); + details.vram.reset(); + details.ram.reset(); + details.display = {}; + const auto partial = AgcDriver::ComposeOverlay(statistics, false, false, details); + require(partial.below.size() == 3 && partial.below.back().rfind("SHADERS", 0) == 0, "a line is shown without its data: the last line is '" + partial.below.back() + "'"); + + details = fullDetails(); + details.display = {3840, 2160}; + details.window = {16384, 16384}; + require(AgcDriver::ComposeOverlay(uncounted, false, false, details).below.back() == "RES: 3840X2160/16384X16384", "a long RES line is not written without spaces"); + details.display = {12345, 12345}; + details.window = {12345, 12345}; + require(AgcDriver::ComposeOverlay(uncounted, false, false, details).below.back() == "RAM: 3.2 GB", "a RES line wider than the panel is shown"); + details = fullDetails(); + details.window = {}; + require(AgcDriver::ComposeOverlay(uncounted, false, false, details).below.back() == "RAM: 3.2 GB", "RES is shown without a window size"); + auto large = countedStatistics(); + auto counters = large.Latest(); + counters.draws += 12345 * 2; + counters.pipelines = 123456; + large.AddPresent(Start + 62 * 16ms, counters); + const auto busy = AgcDriver::ComposeOverlay(large, false, false); + require(busy.below.size() == 3 && busy.below[2] == "SHADERS: 121K TOTAL: 123K", "large counts are not shortened: '" + busy.below.back() + "'"); +} + +void fullPanelLayoutTests() { + auto content = AgcDriver::ComposeOverlay(countedStatistics(), true, true, fullDetails()); + content.below[3] = "DRAWS: 9999 DISPATCH: 12K"; + content.below[4] = "SHADERS: 999M TOTAL: 9999"; + const VkExtent2D hd{1920, 1080}; + const auto layout = AgcDriver::LayOutOverlay(content, hd, OverlayCorner::TopLeft); + require(layout.scale == 2 && layout.bounds.height * 3 <= static_cast(hd.height), "the full panel at 1080p is " + std::to_string(layout.bounds.height) + " pixels tall at scale " + std::to_string(layout.scale) + ", more than a third of the window"); + const auto text = coverage(layout, static_cast(AgcDriver::OverlayColor::Text)); + std::int32_t top = OverlayMetrics::Padding; + for (const auto& line : content.above) { + requireLineDrawn(layout, text, line, top); + top += OverlayMetrics::LinePitch; + } + top += OverlayMetrics::GraphHeight + OverlayMetrics::GraphGap; + for (const auto& line : content.below) { + requireLineDrawn(layout, text, line, top); + top += OverlayMetrics::LinePitch; + } + require(layout.bounds.height == (top - OverlayMetrics::LinePitch + OverlayMetrics::GlyphHeight + OverlayMetrics::Padding) * layout.scale, "the panel does not end one padding below its last line"); + const std::pair scales[] = {{{1280, 720}, 2}, {{2560, 1440}, 3}, {{3840, 2160}, 5}, {{320, 180}, 1}}; + for (const auto& [extent, scale] : scales) { + const auto placed = AgcDriver::LayOutOverlay(content, extent, OverlayCorner::BottomRight); + require(placed.scale == scale && placed.bounds.x >= 0 && placed.bounds.y >= 0 && placed.bounds.y + placed.bounds.height <= static_cast(extent.height), "the full panel does not fit " + std::to_string(extent.width) + "x" + std::to_string(extent.height) + " at scale " + std::to_string(scale)); + } + const std::string added = "BCHKLOTW"; + for (const char letter : added) { + const auto& glyph = AgcDriver::OverlayGlyph(letter); + require(std::any_of(glyph.begin(), glyph.end(), [](std::uint8_t row) { return row != 0; }) && std::all_of(glyph.begin(), glyph.end(), [](std::uint8_t row) { return row <= 0x1f; }), std::string("the glyph of '") + letter + "' is empty or wider than 5 pixels"); + for (const char other : std::string("0123456789ADEFGIMNPRSUVX") + added) { + if (other != letter) require(glyph != AgcDriver::OverlayGlyph(other), std::string("'") + letter + "' and '" + other + "' have the same glyph"); + } + } +} + +void sampleTests() { + require(AgcDriver::OverlaySampleDue(std::nullopt, Start), "the first frame is not sampled"); + require(!AgcDriver::OverlaySampleDue(Start, Start + 999ms) && AgcDriver::OverlaySampleDue(Start, Start + 1000ms), "memory is sampled more than once a second, or not after a second"); + static std::uint8_t* volatile touched = nullptr; + const auto before = AgcDriver::ReadProcessPrivateBytes(); + require(before.has_value() && *before != 0, "the process's private memory cannot be read"); + constexpr std::size_t Bytes = std::size_t{64} << 20u; + const auto block = std::make_unique(Bytes); + touched = block.get(); + std::memset(touched, 0x5a, Bytes); + const auto after = AgcDriver::ReadProcessPrivateBytes(); + require(after.has_value() && *after >= *before + Bytes * 3 / 4, "touching 64 MiB raised the private memory from " + std::to_string(*before) + " to " + std::to_string(after.value_or(0)) + " bytes"); + require(touched[Bytes - 1] == 0x5a, "the touched block was not written"); +} + +void costReport() { + auto& counters = AgcDriver::PacketCounters::ThisThread(); + constexpr int Increments = 20'000'000; + const auto counted = Clock::now(); + for (int i = 0; i < Increments; ++i) counters.CountDraw(); + const auto countNs = std::chrono::duration(Clock::now() - counted).count() / Increments; + const auto summed = Clock::now(); + for (int i = 0; i < 1000; ++i) static_cast(AgcDriver::PacketCounters::Sum()); + const auto sumUs = std::chrono::duration(Clock::now() - summed).count() / 1000; + const auto sampled = Clock::now(); + for (int i = 0; i < 100; ++i) static_cast(AgcDriver::ReadProcessPrivateBytes()); + const auto ramUs = std::chrono::duration(Clock::now() - sampled).count() / 100; + std::printf("hidden cost: %.2f ns per counted draw or dispatch packet; shown: %.2f us to sum the packet counters per frame, %.1f us to read the private memory once a second\n", countNs, sumUs, ramUs); +} + void paletteTests() { const auto sdr = AgcDriver::OverlayPalette(VK_FORMAT_B8G8R8A8_UNORM, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR); require(sdr[1] == std::array{1.0f, 1.0f, 1.0f, 1.0f} && sdr[0][3] > 0.0f && sdr[0][3] < 1.0f, "the SDR palette is not opaque white text over a translucent panel"); @@ -296,7 +479,14 @@ int main() { composeTests(); layoutTests(); paletteTests(); - std::cout << "performance overlay statistics, layout and palette tests passed\n"; + packetStatisticsTests(); + packetCounterTests(); + countFormatTests(); + detailComposeTests(); + fullPanelLayoutTests(); + sampleTests(); + costReport(); + std::cout << "performance overlay statistics, layout, palette, packet, pipeline, memory and resolution tests passed\n"; return 0; } catch (const std::exception& error) { std::cerr << error.what() << '\n'; From 5594cda62f49cba863179d60faccc5bb739b0f6f Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Wed, 14 Oct 2026 17:50:38 +0000 Subject: [PATCH 0138/1000] docs: describe the performance overlay's new lines USAGE.md explains DRAWS, SHADERS, VRAM, RAM and RES, where each number comes from, the count and memory formats, and what counting costs while the overlay is hidden. --- docs/user/USAGE.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index 8c561c4459..a7dcafec49 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -196,8 +196,13 @@ F10 shows or hides a performance overlay over the presented image; `ANYPS5_OVERL | `MIN/AVG/MAX` | The shortest, average and longest of the 120 frame times, in ms. | | `GPU` | GPU time per frame over the last second, or of the last frame below 1 fps, in ms: the span of each recorded batch from its first to its last command, from the timestamp queries of `APS5_PROFILE_GPU=1`. Shown only with that variable. Batches are counted when they complete, so the line trails a sudden change by up to a second, or by a frame below 1 fps. Batches that overlap on different queues each count in full, so the line can exceed `FRAME`. | | `DRIVER` | CPU time per frame over the last second, or of the last frame below 1 fps, in ms, that the packet workers spent in dispatch and draw packets (the `dispatch` and `draw` time of the `[gpu] worker` line of `APS5_PROFILE_DRAW=1`). Shown only with that variable. | +| `DRAWS` | Draw packets per frame over the last second, or of the last frame below 1 fps, then dispatch packets per frame (`DISPATCH`), as the packet workers execute them; packets that predication skips are not counted. Shown once the title has submitted either. | +| `SHADERS` | Pipelines created in the last second, then since the start (`TOTAL`): a graphics pipeline for each draw state the pipeline cache had not seen and a compute pipeline for each new dispatch shader. A rise while the game stutters points at pipeline compilation. Shown once a pipeline was created. | +| `VRAM` | Usage and budget of the GPU's largest device-local memory heap from `VK_EXT_memory_budget`, read once a second. Not shown when the driver lacks the extension. | +| `RAM` | The process's private memory, read once a second: `PrivateUsage` of `GetProcessMemoryInfo` on Windows, `RssAnon` plus `VmSwap` of `/proc/self/status` on Linux. The title's direct memory is shared memory on both systems and is not included. | +| `RES` | The display buffer's size, then the window's swapchain size, for example `RES: 1920X1080 / 2560X1440`. Not shown for frames without a display buffer. | -Times are shown with one decimal below 100 and as whole numbers above; anything above 9999 shows as 9999, so `FRAME` and `MIN/AVG/MAX` read 9999 for frames of 10 s or more. `FPS` has the same format, with two decimals below 1. While the overlay is hidden the presentation records nothing for it and the presented image is unchanged; while it is shown, one small render pass draws it over each presented frame. +Times are shown with one decimal below 100 and as whole numbers above; anything above 9999 shows as 9999, so `FRAME` and `MIN/AVG/MAX` read 9999 for frames of 10 s or more. `FPS` has the same format, with two decimals below 1. Counts are whole numbers, in thousands (`12K`) from 10000 and in millions (`3M`) from a million. Memory is in GB of 2^30 bytes. While the overlay is hidden the presentation records nothing for it and the presented image is unchanged; while it is shown, one small render pass draws it over each presented frame. Each draw and dispatch packet and each pipeline creation is counted also while the overlay is hidden, with one increment of a counter that belongs to the packet worker or, for pipelines, a shared one; the memory and the counters are read only while the overlay is shown. ### Windows crash diagnostics From 8a1f298e6c46c9cd1ca5ab70e27e1bb6651e917f Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Wed, 14 Oct 2026 12:38:55 -0700 Subject: [PATCH 0139/1000] fix(agc): count the last frame's pipelines in the performance overlay below 1 fps SHADERS counted the pipelines created between the first and the last present of the last second, so below 1 fps, with one present in that second, it read 0, as FPS did before. It now counts those created during the last frame there, like the other lines. The statistics test checks DRAWS, DISPATCH and SHADERS for a present alone in its second. --- .../prx/libSceAgcDriver/Execution/src/PerformanceOverlay.cpp | 2 +- .../libSceAgcDriver/tests/PerformanceOverlayStatistics.cpp | 4 +++- docs/user/USAGE.md | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceOverlay.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceOverlay.cpp index d1c24864a0..42abaffea0 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceOverlay.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceOverlay.cpp @@ -261,7 +261,7 @@ double OverlayStatistics::DispatchesPerFrame() const { std::uint64_t OverlayStatistics::PipelinesInWindow() const { if (window.empty()) return 0; - const auto first = window.front().counters.pipelines; + const auto first = window.size() < 2 && previous ? previous->counters.pipelines : window.front().counters.pipelines; const auto last = window.back().counters.pipelines; return last >= first ? last - first : 0; } diff --git a/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlayStatistics.cpp b/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlayStatistics.cpp index 583565c9f2..54d25e7412 100644 --- a/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlayStatistics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlayStatistics.cpp @@ -312,9 +312,11 @@ void packetStatisticsTests() { require(near(statistics.GpuMsPerFrame(), 4.0) && near(statistics.DriverMsPerFrame(), 1.5), "the time counters changed with the packet counters"); auto stalled = statistics; auto counters = stalled.Latest(); + counters.draws += 900; + counters.dispatches += 40; counters.pipelines += 7; stalled.AddPresent(Start + 3000ms, counters); - require(stalled.DrawsPerFrame() == 0 && stalled.PipelinesInWindow() == 0 && stalled.Latest().pipelines == 1510, "a present alone in its second reads as packets or pipelines of that second"); + require(near(stalled.DrawsPerFrame(), 900) && near(stalled.DispatchesPerFrame(), 40) && stalled.PipelinesInWindow() == 7 && stalled.Latest().pipelines == 1510, "a present alone in its second reads as " + std::to_string(stalled.DrawsPerFrame()) + " draws, " + std::to_string(stalled.DispatchesPerFrame()) + " dispatches and " + std::to_string(stalled.PipelinesInWindow()) + " pipelines, not the last frame's 900, 40 and 7"); auto restarted = statistics; counters = restarted.Latest(); counters.draws = 3; diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index a7dcafec49..90b408bc5c 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -197,7 +197,7 @@ F10 shows or hides a performance overlay over the presented image; `ANYPS5_OVERL | `GPU` | GPU time per frame over the last second, or of the last frame below 1 fps, in ms: the span of each recorded batch from its first to its last command, from the timestamp queries of `APS5_PROFILE_GPU=1`. Shown only with that variable. Batches are counted when they complete, so the line trails a sudden change by up to a second, or by a frame below 1 fps. Batches that overlap on different queues each count in full, so the line can exceed `FRAME`. | | `DRIVER` | CPU time per frame over the last second, or of the last frame below 1 fps, in ms, that the packet workers spent in dispatch and draw packets (the `dispatch` and `draw` time of the `[gpu] worker` line of `APS5_PROFILE_DRAW=1`). Shown only with that variable. | | `DRAWS` | Draw packets per frame over the last second, or of the last frame below 1 fps, then dispatch packets per frame (`DISPATCH`), as the packet workers execute them; packets that predication skips are not counted. Shown once the title has submitted either. | -| `SHADERS` | Pipelines created in the last second, then since the start (`TOTAL`): a graphics pipeline for each draw state the pipeline cache had not seen and a compute pipeline for each new dispatch shader. A rise while the game stutters points at pipeline compilation. Shown once a pipeline was created. | +| `SHADERS` | Pipelines created in the last second, or during the last frame below 1 fps, then since the start (`TOTAL`): a graphics pipeline for each draw state the pipeline cache had not seen and a compute pipeline for each new dispatch shader. A rise while the game stutters points at pipeline compilation. Shown once a pipeline was created. | | `VRAM` | Usage and budget of the GPU's largest device-local memory heap from `VK_EXT_memory_budget`, read once a second. Not shown when the driver lacks the extension. | | `RAM` | The process's private memory, read once a second: `PrivateUsage` of `GetProcessMemoryInfo` on Windows, `RssAnon` plus `VmSwap` of `/proc/self/status` on Linux. The title's direct memory is shared memory on both systems and is not included. | | `RES` | The display buffer's size, then the window's swapchain size, for example `RES: 1920X1080 / 2560X1440`. Not shown for frames without a display buffer. | From 907fc73d1405a2872bc3164700d87500ff54d04c Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Wed, 14 Oct 2026 12:44:30 -0700 Subject: [PATCH 0140/1000] feat(agc): mark the performance overlay PROFILED while a profile switch is on GPU and DRIVER need APS5_PROFILE_GPU or APS5_PROFILE_DRAW, and those switches add timestamp queries to every batch and timers to every packet, which changes the timings the panel shows. A screenshot of such a run could pass for an unprofiled one, so while either switch is on the panel now starts with a PROFILED line. --- .../libSceAgcDriver/Execution/src/PerformanceOverlay.cpp | 1 + core/libs/prx/libSceAgcDriver/tests/PerformanceOverlay.cpp | 2 +- .../libSceAgcDriver/tests/PerformanceOverlayStatistics.cpp | 7 +++++-- docs/user/USAGE.md | 1 + 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceOverlay.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceOverlay.cpp index 42abaffea0..d7b2b9aebf 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceOverlay.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceOverlay.cpp @@ -375,6 +375,7 @@ OverlayContent ComposeOverlay(const OverlayStatistics& statistics, bool gpu, boo OverlayContent ComposeOverlay(const OverlayStatistics& statistics, bool gpu, bool driver, const OverlayDetails& details) { OverlayContent content; + if (gpu || driver) content.above.push_back("PROFILED"); content.above.push_back("FPS: " + FormatOverlayRate(statistics.Fps())); content.above.push_back("FRAME: " + FormatOverlayValue(statistics.LastFrameMs()) + " MS"); content.frameTimes = statistics.FrameTimes(); diff --git a/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlay.cpp b/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlay.cpp index 0992ffadeb..ceb94348e8 100644 --- a/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlay.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlay.cpp @@ -320,7 +320,7 @@ OverlayContent fullPanel(const AgcDriver::OverlayDetails& details) { statistics.AddPresent(start + std::chrono::nanoseconds(16'666'667) * i, counters); } auto content = AgcDriver::ComposeOverlay(statistics, true, true, details); - Require(content.above.front() == "FPS: 60.0" && content.below.size() == 5u + (details.vram ? 1u : 0u) + (details.ram ? 1u : 0u) + (details.display.width != 0 ? 1u : 0u), "the full panel composes as '" + content.above.front() + "' with " + std::to_string(content.below.size()) + " lines below its graph"); + Require(content.above.size() == 3 && content.above[0] == "PROFILED" && content.above[1] == "FPS: 60.0" && content.below.size() == 5u + (details.vram ? 1u : 0u) + (details.ram ? 1u : 0u) + (details.display.width != 0 ? 1u : 0u), "the full panel composes as '" + content.above.front() + "' with " + std::to_string(content.below.size()) + " lines below its graph"); return content; } diff --git a/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlayStatistics.cpp b/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlayStatistics.cpp index 54d25e7412..aaf594a7db 100644 --- a/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlayStatistics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlayStatistics.cpp @@ -111,7 +111,7 @@ void lowRateTests() { require(near(slow.GpuMsPerFrame(), static_cast(Gpu[i]) / 1e6) && near(slow.DriverMsPerFrame(), static_cast(Driver[i]) / 1e6), "frame " + std::to_string(i + 1) + " every 2 s reads " + std::to_string(slow.GpuMsPerFrame()) + " ms of GPU and " + std::to_string(slow.DriverMsPerFrame()) + " ms of driver time, not its own"); } const auto everyTwoSeconds = AgcDriver::ComposeOverlay(slow, true, true); - require(everyTwoSeconds.above[0] == "FPS: 0.50" && everyTwoSeconds.above[1] == "FRAME: 2000 MS", "a present every 2 s shows '" + everyTwoSeconds.above[0] + "' and '" + everyTwoSeconds.above[1] + "'"); + require(everyTwoSeconds.above == std::vector{"PROFILED", "FPS: 0.50", "FRAME: 2000 MS"}, "a present every 2 s shows " + std::to_string(everyTwoSeconds.above.size()) + " lines above the graph, the last '" + everyTwoSeconds.above.back() + "'"); require(everyTwoSeconds.below[0] == "MIN/AVG/MAX: 2000/2000/2000" && everyTwoSeconds.below[1] == "GPU: 20.0 MS" && everyTwoSeconds.below[2] == "DRIVER: 8.0 MS", "a present every 2 s shows '" + everyTwoSeconds.below[0] + "', '" + everyTwoSeconds.below[1] + "' and '" + everyTwoSeconds.below[2] + "'"); OverlayStatistics glacial; @@ -119,7 +119,7 @@ void lowRateTests() { for (int i = 1; i <= 5; ++i) glacial.AddPresent(Start + i * 12s, static_cast(i) * 250'000'000u, static_cast(i) * 12'500'000'000u); require(near(glacial.Fps(), 1000.0 / 12000.0) && near(glacial.LastFrameMs(), 12000) && near(glacial.GpuMsPerFrame(), 250) && near(glacial.DriverMsPerFrame(), 12500), "a present every 12 s reads as " + std::to_string(glacial.Fps()) + " FPS, " + std::to_string(glacial.LastFrameMs()) + " ms, " + std::to_string(glacial.GpuMsPerFrame()) + " and " + std::to_string(glacial.DriverMsPerFrame()) + " ms"); const auto everyTwelveSeconds = AgcDriver::ComposeOverlay(glacial, true, true); - require(everyTwelveSeconds.above[0] == "FPS: 0.08" && everyTwelveSeconds.above[1] == "FRAME: 9999 MS", "a present every 12 s shows '" + everyTwelveSeconds.above[0] + "' and '" + everyTwelveSeconds.above[1] + "'"); + require(everyTwelveSeconds.above == std::vector{"PROFILED", "FPS: 0.08", "FRAME: 9999 MS"}, "a present every 12 s shows " + std::to_string(everyTwelveSeconds.above.size()) + " lines above the graph, the last '" + everyTwelveSeconds.above.back() + "'"); require(everyTwelveSeconds.below[0] == "MIN/AVG/MAX: 9999/9999/9999" && everyTwelveSeconds.below[1] == "GPU: 250 MS" && everyTwelveSeconds.below[2] == "DRIVER: 9999 MS", "a present every 12 s shows '" + everyTwelveSeconds.below[0] + "', '" + everyTwelveSeconds.below[1] + "' and '" + everyTwelveSeconds.below[2] + "'"); require(everyTwelveSeconds.frameTimes == std::vector(5, 12000.0), "the graph does not hold the five 12 s frames"); const auto layout = AgcDriver::LayOutOverlay(everyTwelveSeconds, {1920, 1080}, OverlayCorner::TopLeft); @@ -187,6 +187,9 @@ void composeTests() { require(plain.frameTimes.size() == 100, "the graph does not hold every frame time"); const auto profiled = AgcDriver::ComposeOverlay(statistics, true, true); require(profiled.below.size() == 3 && profiled.below[1] == "GPU: 2.0 MS" && profiled.below[2] == "DRIVER: 0.5 MS", "the profiled lines read '" + profiled.below[1] + "' and '" + profiled.below[2] + "'"); + std::vector marked{"PROFILED"}; + marked.insert(marked.end(), plain.above.begin(), plain.above.end()); + require(profiled.above == marked && AgcDriver::ComposeOverlay(statistics, true, false).above == marked && AgcDriver::ComposeOverlay(statistics, false, true).above == marked, "a profiled panel does not start with PROFILED above the unprofiled lines: it starts with '" + profiled.above.front() + "'"); require(AgcDriver::ComposeOverlay(statistics, true, false).below.size() == 2 && AgcDriver::ComposeOverlay(statistics, false, true).below[1].rfind("DRIVER", 0) == 0, "a line is shown without its source"); } diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index 90b408bc5c..29f34b4fd0 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -190,6 +190,7 @@ F10 shows or hides a performance overlay over the presented image; `ANYPS5_OVERL | Line | Meaning | |------|---------| +| `PROFILED` | The first line while `APS5_PROFILE_GPU=1` or `APS5_PROFILE_DRAW=1` is set. Those switches add timestamp queries to every batch and timers to every packet, so the numbers of such a run aren't comparable with an unprofiled one. | | `FPS` | Frames presented per second, over the last second. When the last second holds fewer than two presents (below 1 fps), the rate of the last frame instead, 1000 divided by its time in ms: a frame every 2 s shows `0.50`. Every present counts, including frames a title presents cleared, for example while it loads. | | `FRAME` | Time between the last two presented frames, in ms. | | graph | The last 120 frame times, the newest on the right. The full height is the longest of them or twice their average, whichever is larger. | From c63281824f7f7dcd6c38e65a6e8c624a276e381a Mon Sep 17 00:00:00 2001 From: billythemenace2 Date: Wed, 14 Oct 2026 15:28:17 +0100 Subject: [PATCH 0141/1000] fix(agc): rasterize a multisampled target without MSAA_ENABLE at the pixel center A draw with PA_SC_AA_CONFIG.MSAA_NUM_SAMPLES > 0 and PA_SC_MODE_CNTL_0.MSAA_ENABLE clear threw. On GFX10 and later the bit is what makes the rasterizer use the programmed sample locations; with it clear the draw now keeps the target's sample count with every sample at the pixel center. Points, lines, a depth target and alpha-to-coverage still throw. Co-Authored-By: Claude Opus 5.5 (1M context) --- core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp | 7 ++++++- core/libs/prx/libSceAgcDriver/tests/Graphics.cpp | 7 ++++++- docs/dev/debt/libSceAgcDriver.md | 1 + 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index 44cf86c915..8b710c745f 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -804,7 +804,12 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) { } Require(!result.depth || result.depth->samples == result.samples, "the depth target's sample count differs from the rasterizer's"); zero(cx, 0x292, ScanModeMask & ~1u, "scan conversion mode"); - Require(result.samples == 1 || (read(cx, 0x292) & 1u) != 0, "multisampled targets rasterized without MSAA_ENABLE are unsupported"); + if (result.samples > 1 && (read(cx, 0x292) & 1u) == 0) { + const bool filled = result.topology != VK_PRIMITIVE_TOPOLOGY_POINT_LIST && result.topology != VK_PRIMITIVE_TOPOLOGY_LINE_LIST && result.topology != VK_PRIMITIVE_TOPOLOGY_LINE_STRIP; + Require(filled && !result.depth && !result.alphaToCoverage, "multisampled targets rasterized without MSAA_ENABLE are unsupported for points, lines, a depth target or alpha-to-coverage"); + result.customSampleLocations = true; + result.sampleLocations.fill(0); + } if (result.samples == 1 && (read(cx, 0x292) & 1u) != 0) { for (std::uint32_t pixel = 0; pixel < 4; ++pixel) static_cast(read(cx, 0x2feu + pixel * 4u)); if (auto reason = singleSampleMsaaRejection(queue); !reason.empty()) throw std::runtime_error(reason); diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 27661c9e92..2a07793bdb 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -2464,7 +2464,12 @@ void multisampleTests() { expectFailure([&] { DecodeState(queue); }, "sample count differs"); queue = multisampled(); queue.context[0x292] = 2; - expectFailure([&] { DecodeState(queue); }, "without MSAA_ENABLE"); + state = DecodeState(queue); + Require(state.samples == 2 && state.colors.size() == 1 && state.colors[0].samples == 2 && state.customSampleLocations, "a 2x target rasterized without MSAA_ENABLE did not rasterize the target's samples"); + const auto aliased = AgcDriver::Graphics::SampleLocations(state); + for (std::uint32_t sample = 0; sample < state.samples; ++sample) Require(aliased[sample].x == 0.5f && aliased[sample].y == 0.5f, "a 2x target rasterized without MSAA_ENABLE did not place every sample at the pixel center"); + queue.context[0x2dc] = 0x0000aa01u; + expectFailure([&] { DecodeState(queue); }, "without MSAA_ENABLE are unsupported for points, lines, a depth target or alpha-to-coverage"); queue = multisampled(); queue.context[0x2f8] |= 0x10; expectFailure([&] { DecodeState(queue); }, "coverage conversion"); diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index cc7bb35e1d..ef52ccdb68 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -9,6 +9,7 @@ - [GS_FAST_LAUNCH geometry](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - `VGT_SHADER_STAGES_EN.GS_FAST_LAUNCH = 1` with a point list and `GE_CNTL` groups of one vertex and one primitive decodes as one point per subgroup, with the primitive limit taken from `GE_NGG_SUBGRP_CNTL.PRIM_AMP_FACTOR` and the GS/ES VGPR counts not required. Follows how RADV drives mesh shaders on gfx10.3 (`ac_nir_lower_ngg.c`, the workgroup index comes from the vertex id of the first thread), not measured on the console. Fast launch 2, other group sizes or input primitives and indexed fast-launch draws still throw. PPSA03671's fast-launch draws decode, but the title stops earlier in a wave64 vertex program, so they have not reached the GPU yet. - [Alpha-to-coverage](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - only `DB_ALPHA_TO_MASK = 0xaa01` is accepted: the enable bit with the four quad offsets at 2 and no `OFFSET_ROUND`, the undithered setting [radeonsi](https://gitlab.freedesktop.org/mesa/mesa/-/blob/bffd91947efc1932da99aca69fc540d6d4201b0c/src/gallium/drivers/radeonsi/si_state.c#L371-L381) programs. Draws are single-sampled, so the pixel shader [writes](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) `SampleMask` 0 when its MRT0 alpha is below one half or NaN and all ones otherwise, and runs with late fragment tests. That rule was measured on a Radeon 660M (gfx1035, RADV with `RADV_DEBUG=noatocdithering`, which programs the same register) for 0, -0, 0.25, the two floats below one half, one half, the two above, 0.75, 1, 2, -1, both infinities and both NaNs, and is pinned in [AlphaToCoverage.cpp](../../../core/libs/prx/libSceAgcDriver/tests/execution/AlphaToCoverage.cpp); it is not measured on gfx1013. Vulkan's `alphaToCoverageEnable` is not used: the NVIDIA driver 615.78.08 dithers single-sample coverage (an alpha of 0.25 writes half the pixels of an RTX 3050 Laptop target). Other offsets, `OFFSET_ROUND`, a sample-mask export, and an MRT0 that is missing, swizzled, packed or exported without a floating-point alpha throw. A draw without a color target on MRT0 keeps alpha-to-coverage from its MRT0 export, as [radeonsi exports MRT0 as `32_AR` for alpha-to-coverage without a color buffer](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-24.0.0/src/gallium/drivers/radeonsi/si_state_shaders.cpp#L2425-L2431). A draw that enables alpha-to-coverage with no MRT0 export still throws: what the console covers then is not measured. Default dithered `DB_ALPHA_TO_MASK = 0x18701` additionally uses a per-quad sample-mask table [measured on gfx1036](https://github.com/boykopovar/AnyPS5/pull/4451#issuecomment-6124848921) for eight standard samples with FP16_ABGR or 32_ABGR exports; other sample counts, custom locations, EQAA combinations and dithered UNORM16/SNORM16 exports throw. The dithered rule is not measured on gfx1013. - [MSAA_ENABLE on a single-sample target](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - `PA_SC_MODE_CNTL_0.MSAA_ENABLE` with `PA_SC_AA_CONFIG.MSAA_NUM_SAMPLES = 0` is drawn like the same state without the bit when sample 0 of the four `PA_SC_AA_SAMPLE_LOCS_PIXEL_*_0` registers is at the pixel center and the draw rasterizes filled triangles with alpha-to-coverage and conservative rasterization off. [RADV](https://gitlab.freedesktop.org/mesa/mesa/-/blob/745709a000482a4a24c91faef5c5a40bd653a863/src/amd/vulkan/radv_cmd_buffer.c#L13606-13609) programs this state for one-sample custom sample locations on GFX10 and later, where the bit makes the rasterizer use the programmed location of sample 0 ([061bc615](https://gitlab.freedesktop.org/mesa/mesa/-/commit/061bc6151a11597f720535f00e79a5752e24aff6)); that the bit changes nothing else for such a draw is inferred from that and is not measured on hardware. Other locations of sample 0, absent location registers, points, lines, rectangles, alpha-to-coverage and conservative rasterization throw +- [A multisampled target rasterized without MSAA_ENABLE](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - `PA_SC_AA_CONFIG.MSAA_NUM_SAMPLES` > 0 with `PA_SC_MODE_CNTL_0.MSAA_ENABLE` clear is drawn with the target's sample count and every sample at the pixel center, as a single-sample scan into a multisampled target already is. On GFX10 and later the bit is what makes the rasterizer use the programmed sample locations ([RADV 061bc615](https://gitlab.freedesktop.org/mesa/mesa/-/commit/061bc6151a11597f720535f00e79a5752e24aff6)), and with multisampling disabled GL rasterizes at the pixel center and gives the fragment full coverage, which radeonsi programs by clearing the bit; that the hardware does the same with `MSAA_NUM_SAMPLES` > 0 is inferred from these and is not measured on hardware. Points, lines, a depth target and alpha-to-coverage throw. SONIC SUPERSTARS (PPSA06888) draws such a 2x target. - [sceAgcDriverGetEqContextId](../../../core/libs/prx/libSceAgcDriver/Eq/src/Query.cpp) (libSceAgcDriver) - returns `ident`: an end-of-pipe interrupt is delivered only to the registration whose id equals the raising queue, so `ident` is that context's id (titles register 0 or 1 for graphics and `0x20 + 8 * pipe + queue` for compute). The console's value is not known; KytyPS5 returns the event's `data`, which it fills with the interrupt's context id, whereas `data` here is the EV_CLEAR trigger count. Other event filters throw - [User data registers](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Diagnostics.cpp) (AGC driver) - a `SPI_SHADER_USER_DATA_*` or `COMPUTE_USER_DATA_*` register the title never wrote reads as 0 when a stage's RSRC2 count covers it, as Kyty (zeroed `UserSgprInfo`) and SharpEmu (missing registers read 0) do, and as AMD PAL and Mesa initialize them; the console's value is not confirmed - [SET_PREDICATION](../../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (AGC driver) - boolean predication follows the AMD PM4 encoding [Mesa](https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/src/amd/common/sid.h) uses: operation 3 reads 64 bits and 4 reads 32 bits, and bit 8 runs predicated packets when the value is non-zero (clear: when it is zero); the bit 12 hint is ignored. The address must have its low 4 bits clear, as `sceAgcDcbSetPredication` writes it. Query predication (Z-pass, primitive count), a predicated COND_EXEC, predicated command buffer chains and flips in predicated command buffers throw From e10ec39046b199da3bc4b69f6af05f556f3b2aec Mon Sep 17 00:00:00 2001 From: billythemenace2 Date: Wed, 14 Oct 2026 21:03:28 +0100 Subject: [PATCH 0142/1000] fix(agc): clear the sample location grid of a target centred without MSAA_ENABLE Co-Authored-By: Claude Opus 5.5 (1M context) --- core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp | 1 + core/libs/prx/libSceAgcDriver/tests/Graphics.cpp | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index 8b710c745f..ff6252d61b 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -809,6 +809,7 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) { Require(filled && !result.depth && !result.alphaToCoverage, "multisampled targets rasterized without MSAA_ENABLE are unsupported for points, lines, a depth target or alpha-to-coverage"); result.customSampleLocations = true; result.sampleLocations.fill(0); + result.sampleLocationGrid = false; } if (result.samples == 1 && (read(cx, 0x292) & 1u) != 0) { for (std::uint32_t pixel = 0; pixel < 4; ++pixel) static_cast(read(cx, 0x2feu + pixel * 4u)); diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 2a07793bdb..860cc8fb7a 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -2471,6 +2471,11 @@ void multisampleTests() { queue.context[0x2dc] = 0x0000aa01u; expectFailure([&] { DecodeState(queue); }, "without MSAA_ENABLE are unsupported for points, lines, a depth target or alpha-to-coverage"); queue = multisampled(); + queue.context[0x292] = 2; + queue.context[0x302] = 0x44cc; + state = DecodeState(queue); + Require(state.customSampleLocations && !state.sampleLocationGrid, "a 2x target rasterized without MSAA_ENABLE kept the per-pixel sample location grid of its centred samples"); + queue = multisampled(); queue.context[0x2f8] |= 0x10; expectFailure([&] { DecodeState(queue); }, "coverage conversion"); queue = multisampled(); From b160c0a395cb5dffc5353513f68ff956f52148f4 Mon Sep 17 00:00:00 2001 From: Ondra97 <47064087+Ondra97@users.noreply.github.com> Date: Wed, 14 Oct 2026 22:10:51 +0200 Subject: [PATCH 0143/1000] fix(ajm): honor Opus gapless resets --- core/libs/prx/libSceAjm.native/src/Ajm.cpp | 3 +- core/libs/tests/GuestAjm.cpp | 75 ++++++++++++++++++++++ 2 files changed, 77 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAjm.native/src/Ajm.cpp b/core/libs/prx/libSceAjm.native/src/Ajm.cpp index 4f17031b65..6dc4fc0c0e 100644 --- a/core/libs/prx/libSceAjm.native/src/Ajm.cpp +++ b/core/libs/prx/libSceAjm.native/src/Ajm.cpp @@ -1111,9 +1111,10 @@ void RunOpus(Instance& instance, const JobHeader& job, const AjmBuffer* inputs, count -= skip; } if (instance.gapless.totalSamples != 0) { - const std::uint64_t remaining = instance.gapless.totalSamples > instance.totalDecodedSamples ? instance.gapless.totalSamples - instance.totalDecodedSamples : 0; + const std::uint64_t remaining = instance.gapless.totalSamples > instance.segmentSamples ? instance.gapless.totalSamples - instance.segmentSamples : 0; count = static_cast(std::min(count, remaining)); } + instance.segmentSamples += count; instance.totalDecodedSamples += count; if (Resampling(instance)) { std::vector values(count * channels); diff --git a/core/libs/tests/GuestAjm.cpp b/core/libs/tests/GuestAjm.cpp index 54344eb553..3001db3edb 100644 --- a/core/libs/tests/GuestAjm.cpp +++ b/core/libs/tests/GuestAjm.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #ifdef _WIN32 @@ -1573,6 +1574,77 @@ void TestAt9RunDecodesWholeInput(std::uint32_t context) { Require(sceAjmInstanceDestroy(context, instance) == 0); } +void TestOpusGaplessReset(std::uint32_t context, std::size_t outputFrames, float ratio) { + const auto instance = CreateOpus(context); + const auto reference = CreateOpus(context); + std::vector batch(4096); + std::vector pcm(outputFrames * 2 + 4, -12345); + std::vector expected(960 * 2); + const std::uint32_t totalSamples = ratio > 0 ? 860 : 500; + const GaplessDecode gapless{totalSamples, 100, 0}; + const GaplessDecode unlimited{0, 100, 0}; + AjmBatchInfo info{}; + if (ratio > 0) { + std::int32_t result[2][2] = {{-1, -1}, {-1, -1}}; + Require(sceAjmBatchInitialize(batch.data(), batch.size(), &info) == 0); + Require(sceAjmBatchJobSetResampleParameters(&info, instance, ratio, 1, result[0]) == 0); + Require(sceAjmBatchJobSetResampleParameters(&info, reference, ratio, 1, result[1]) == 0); + Submit(context, info); + Require(result[0][0] == 0 && result[1][0] == 0); + } + std::size_t offset = 0; + for (int segment = 0; segment < 3; ++segment) { + std::int32_t result[2][2] = {{-1, -1}, {-1, -1}}; + GaplessSideband state{-1, -1, 0, 0, 0xffff}; + Require(sceAjmBatchInitialize(batch.data(), batch.size(), &info) == 0); + Require(sceAjmBatchJobSetGaplessDecode(&info, instance, &gapless, 1, result[0]) == 0); + Require(sceAjmBatchJobSetGaplessDecode(&info, reference, &unlimited, 1, result[1]) == 0); + Require(sceAjmBatchJobGetGaplessDecode(&info, instance, &state) == 0); + Submit(context, info); + Require(result[0][0] == 0 && result[1][0] == 0); + Require(state.result == 0 && state.totalSamples == totalSamples && state.skipSamples == 100 && state.skippedSamples == 0); + const std::size_t packetBytes = 2 + (OPUS_STEREO[offset] | (std::size_t{OPUS_STEREO[offset + 1]} << 8u)); + DecodeSideband baseline{}; + RunDecode(context, reference, OPUS_STEREO + offset, packetBytes, expected.data(), expected.size() * sizeof(std::int16_t), baseline); + Require(baseline.result == 0 && static_cast(baseline.inputConsumed) == packetBytes); + Require(baseline.totalDecodedSamples == static_cast(segment + 1) * 860 + (ratio == 0 ? segment * 960 : 0)); + const std::size_t expectedBytes = ratio > 0 ? static_cast(baseline.outputWritten) : totalSamples * 2 * sizeof(std::int16_t); + Require(expectedBytes > 0); + std::size_t produced = 0; + for (int drain = 0; produced < expectedBytes && drain < 16; ++drain) { + std::fill(pcm.begin(), pcm.end(), -12345); + DecodeSideband decoded{}; + RunDecode(context, instance, OPUS_STEREO + offset, drain == 0 ? packetBytes : 0, pcm.data(), outputFrames * 2 * sizeof(std::int16_t), decoded); + if (decoded.outputWritten <= 0) throw std::runtime_error("Opus gapless segment " + std::to_string(segment) + " wrote " + std::to_string(decoded.outputWritten) + " bytes with " + std::to_string(expectedBytes - produced) + " still expected"); + Require(decoded.result == 0 && static_cast(decoded.inputConsumed) == (drain == 0 ? packetBytes : 0)); + Require(decoded.totalDecodedSamples == static_cast(segment + 1) * totalSamples); + const auto samples = static_cast(decoded.outputWritten) / sizeof(std::int16_t); + Require(samples <= outputFrames * 2 && produced + static_cast(decoded.outputWritten) <= expectedBytes); + Require(std::equal(pcm.begin(), pcm.begin() + samples, expected.begin() + produced / sizeof(std::int16_t))); + Require(std::all_of(pcm.begin() + samples, pcm.end(), [](auto sample) { return sample == -12345; })); + produced += static_cast(decoded.outputWritten); + } + Require(produced == expectedBytes); + if (ratio == 0) { + Require(sceAjmBatchInitialize(batch.data(), batch.size(), &info) == 0); + Require(sceAjmBatchJobSetGaplessDecode(&info, instance, &gapless, 0, result[0]) == 0); + Require(sceAjmBatchJobGetGaplessDecode(&info, instance, &state) == 0); + Submit(context, info); + Require(result[0][0] == 0 && state.result == 0 && state.skippedSamples == 100); + std::fill(pcm.begin(), pcm.end(), -12345); + DecodeSideband decoded{}; + RunDecode(context, instance, OPUS_STEREO + offset, packetBytes, pcm.data(), outputFrames * 2 * sizeof(std::int16_t), decoded); + RunDecode(context, reference, OPUS_STEREO + offset, packetBytes, expected.data(), expected.size() * sizeof(std::int16_t), baseline); + Require(decoded.result == 0 && static_cast(decoded.inputConsumed) == packetBytes && decoded.outputWritten == 0); + Require(decoded.totalDecodedSamples == static_cast(segment + 1) * totalSamples); + Require(std::all_of(pcm.begin(), pcm.end(), [](auto sample) { return sample == -12345; })); + } + offset = (offset + packetBytes) % sizeof(OPUS_STEREO); + } + Require(sceAjmInstanceDestroy(context, instance) == 0); + Require(sceAjmInstanceDestroy(context, reference) == 0); +} + } int main() { @@ -1629,6 +1701,9 @@ int main() { TestMultichannelAt9(context); TestControlMp3(context); TestControlOpus(context); + TestOpusGaplessReset(context, 960, 0); + TestOpusGaplessReset(context, 128, 0); + TestOpusGaplessReset(context, 960, 2.0f); std::vector delayedAac; TestM4aacAdts(context, delayedAac); TestM4aacNondelay(context, delayedAac); From 46abaab054693ed6714ef3bc39ab4925f4ecb9ea Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Wed, 14 Oct 2026 13:16:26 -0700 Subject: [PATCH 0144/1000] refactor(agc): count compute pipelines in the helper that creates them The overlay's SHADERS count for compute pipelines sat in VulkanDevice::dispatch, inside a block that another open change moves. CreateComputePipeline now counts each pipeline it creates, so the count follows the creation wherever it runs. The capture replay path, which creates the same title pipelines, is counted too. --- core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 59d5ce689e..a77efedf17 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -114,6 +114,7 @@ void CreateComputePipeline(VkDevice device, PFN_vkCreateComputePipelines create, info.layout = layout; auto cacheLock = synchronization != nullptr ? synchronization->LockForPipelineUse() : Graphics::PipelineCacheSynchronization::PipelineUseLock{}; check(create(device, cache, 1, &info, nullptr, pipeline), "vkCreateComputePipelines"); + PerformanceCounters::CountPipeline(); } bool CaptureComputePipeline(std::size_t words) { @@ -4540,7 +4541,6 @@ VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::Rec CreateComputePipeline(state->device, state->DeviceFunction("vkCreateComputePipelines"), context.pipelineCache, objects->module, objects->layout, pipelineFlags, 0, specialization.Info(), subgroupSize, &objects->pipeline, context.pipelineCacheSynchronization); objects->destroyModule(objects->device, objects->module, nullptr); objects->module = VK_NULL_HANDLE; - PerformanceCounters::CountPipeline(); Graphics::LogPipelineStatistics_nid_no_patch(context, objects->pipeline); timing.Mark("pipeline_create"); if (pipelineKey != 0) { From 895061ca621f22714e72210b5b4904daef90afe2 Mon Sep 17 00:00:00 2001 From: Muhammad Wasif Ali Wasif <76695420+wachi35676@users.noreply.github.com> Date: Tue, 13 Oct 2026 23:23:11 +0500 Subject: [PATCH 0145/1000] docs(build): list the ANYPS5_BUILD_WASM flag Co-Authored-By: Claude Opus 5.5 --- docs/dev/BUILD.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/dev/BUILD.md b/docs/dev/BUILD.md index 12b552df7c..271a7ec2a1 100644 --- a/docs/dev/BUILD.md +++ b/docs/dev/BUILD.md @@ -59,6 +59,7 @@ Project switches accept `ON` or `OFF`: |----------------------------------|---------|--------------------------------------------------| | `-DBUILD_TESTING=ON` | `OFF` | Build and register tests. | | `-DANYPS5_RELINKER_ONLY=ON` | `OFF` | Build only the relinker and its tests, without third-party dependencies. | +| `-DANYPS5_BUILD_WASM=ON` | `OFF` | Build only the browser relinker (`relinker_web` target). Requires `emcmake` from Emscripten and `BUILD_TESTING=OFF`. | | `-DANYPS5_ENABLE_SPIRV_TOOLS=ON` | `OFF` | Enable SPIR-V validation and optimization. | | `-DANYPS5_ENABLE_ASAN=ON` | `OFF` | Build with AddressSanitizer on Linux and move the guest arena out of ASan's shadow gap. | | `-DANYPS5_USE_SYSTEM_DEPS=ON` | `OFF` | Resolve SDL2, freetype, Vulkan-Headers, SPIRV-Headers, glslang and SPIRV-Tools from the system instead of the submodules. | From cf780ffe7e00ce78a94b3935d28386649a55e86c Mon Sep 17 00:00:00 2001 From: Muhammad Wasif Ali Wasif <76695420+wachi35676@users.noreply.github.com> Date: Thu, 15 Oct 2026 01:22:40 +0500 Subject: [PATCH 0146/1000] docs(progress): the README links the history page, not its chart The README no longer embeds history.svg; it links history.html, which the progress page also links. Co-Authored-By: Claude Opus 5.5 --- docs/dev/PROGRESS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/PROGRESS.md b/docs/dev/PROGRESS.md index fe31e07c2f..e66bf9fcad 100644 --- a/docs/dev/PROGRESS.md +++ b/docs/dev/PROGRESS.md @@ -14,7 +14,7 @@ A library with no scanned definitions and no shared sources, such as one whose ` ## History -[`tools/progress_history.py`](../../tools/progress_history.py) measures the first-parent history of `main` with the same rules as `progress.py` and writes `history.json`, `history.svg` (shown in the README) and `history.html` (the chart with a tooltip per day plus a table, linked from the end of the progress page). The chart plots counts, not percentages: implemented and declared library functions and decoded shader instructions, so it shows how much is added over time; the percentages are on the progress map and the badges. It takes one point per day, at the last commit of the day, and skips commits whose tree lacks the scanned paths. The chart compacts as days accumulate: up to 31 days it labels every day, up to 150 days one label per Monday, beyond that one per month; points (and their tooltips) are drawn up to 45 days, after which the table and `history.json` keep every day. Old trees are measured with today's counting rules and instruction list, so the series is comparable but not what the page showed on that day. The workflow checks out the full history for it. +[`tools/progress_history.py`](../../tools/progress_history.py) measures the first-parent history of `main` with the same rules as `progress.py` and writes `history.json`, `history.svg` and `history.html` (the chart with a tooltip per day plus a table, linked from the README and the end of the progress page). The chart plots counts, not percentages: implemented and declared library functions and decoded shader instructions, so it shows how much is added over time; the percentages are on the progress map and the badges. It takes one point per day, at the last commit of the day, and skips commits whose tree lacks the scanned paths. The chart compacts as days accumulate: up to 31 days it labels every day, up to 150 days one label per Monday, beyond that one per month; points (and their tooltips) are drawn up to 45 days, after which the table and `history.json` keep every day. Old trees are measured with today's counting rules and instruction list, so the series is comparable but not what the page showed on that day. The workflow checks out the full history for it. ## Comparisons From ea690728c32a4c6014932bca09cf971ae3d5b60d Mon Sep 17 00:00:00 2001 From: hamb3r Date: Wed, 14 Oct 2026 23:26:08 +0300 Subject: [PATCH 0147/1000] perf(libc): limit Windows PreciseSleepUs spin tail to pacing sleeps --- core/libs/prx/libc/include/PreciseWait.hpp | 14 ++- core/libs/tests/PreciseWait.cpp | 104 ++++++++++++++++++--- 2 files changed, 99 insertions(+), 19 deletions(-) diff --git a/core/libs/prx/libc/include/PreciseWait.hpp b/core/libs/prx/libc/include/PreciseWait.hpp index af79e3fc8e..e1829564db 100644 --- a/core/libs/prx/libc/include/PreciseWait.hpp +++ b/core/libs/prx/libc/include/PreciseWait.hpp @@ -55,11 +55,13 @@ inline void PreciseSleepFromCounterWithTimer( LARGE_INTEGER counter, unsigned long long hundredNanos, HANDLE timerHandle, - bool useCustomTimer) { + bool useCustomTimer, + bool allowSpinTail = true) { if (hundredNanos == 0) { return; } constexpr unsigned long long kSpinTailHundredNanos = 2500; + constexpr unsigned long long kMinTimerSpinSleepHundredNanos = 10000; static std::atomic cachedFreq{0}; std::uint64_t freq = cachedFreq.load(std::memory_order_relaxed); std::uint64_t deltaTicks = hundredNanos; @@ -77,9 +79,11 @@ inline void PreciseSleepFromCounterWithTimer( } } const std::uint64_t deadlineTicks = static_cast(counter.QuadPart) + deltaTicks; - if (hundredNanos > kSpinTailHundredNanos) [[unlikely]] { + const bool useSpinTail = allowSpinTail || hundredNanos >= kMinTimerSpinSleepHundredNanos; + if (!useSpinTail || hundredNanos > kSpinTailHundredNanos) [[unlikely]] { const HANDLE handle = useCustomTimer ? timerHandle : PreciseWaitThreadTimer(); - PreciseSleepWaitableTimerWithHandle(handle, hundredNanos - kSpinTailHundredNanos); + const unsigned long long timerHundredNanos = useSpinTail ? (hundredNanos - kSpinTailHundredNanos) : hundredNanos; + PreciseSleepWaitableTimerWithHandle(handle, timerHundredNanos); } for (;;) { QueryPerformanceCounter(&counter); @@ -94,7 +98,7 @@ inline void PreciseSleepFromCounterWithTimer( } inline void PreciseSleepFromCounter(LARGE_INTEGER counter, unsigned long long hundredNanos) { - PreciseSleepFromCounterWithTimer(counter, hundredNanos, nullptr, false); + PreciseSleepFromCounterWithTimer(counter, hundredNanos, nullptr, false, true); } inline void PreciseSleepHundredNanos(unsigned long long hundredNanos) { @@ -103,7 +107,7 @@ inline void PreciseSleepHundredNanos(unsigned long long hundredNanos) { } LARGE_INTEGER counter; QueryPerformanceCounter(&counter); - PreciseSleepFromCounter(counter, hundredNanos); + PreciseSleepFromCounterWithTimer(counter, hundredNanos, nullptr, false, false); } #elif !defined(__EMSCRIPTEN__) #include diff --git a/core/libs/tests/PreciseWait.cpp b/core/libs/tests/PreciseWait.cpp index 7ca7b91673..cf7d7d6979 100644 --- a/core/libs/tests/PreciseWait.cpp +++ b/core/libs/tests/PreciseWait.cpp @@ -3,8 +3,76 @@ #include #include #include +#include #include +namespace { + +bool IsRunnerPreempted() { +#ifdef _WIN32 + char skip[8]{}; + const DWORD len = GetEnvironmentVariableA("APS5_SKIP_TIMING_TESTS", skip, sizeof(skip)); + if (len > 0 && skip[0] != '0') { + return true; + } +#else + if (const char* skip = std::getenv("APS5_SKIP_TIMING_TESTS"); skip != nullptr && skip[0] != '\0' && skip[0] != '0') { + return true; + } +#endif + const auto probeStart = std::chrono::steady_clock::now(); + auto prev = probeStart; + std::chrono::nanoseconds maxGap{0}; + while (prev - probeStart < std::chrono::milliseconds(2)) { + const auto cur = std::chrono::steady_clock::now(); + const auto gap = cur - prev; + if (gap > maxGap) { + maxGap = gap; + } + prev = cur; + } + return maxGap > std::chrono::microseconds(25); +} + +template +bool CheckMedianSleepBound( + const char* label, + std::chrono::nanoseconds minDuration, + std::chrono::nanoseconds maxMedian, + Fn&& sleepFn) { + std::array samples{}; + std::chrono::nanoseconds bestMedian{}; + for (int attempt = 0; attempt < 3; ++attempt) { + for (auto& sample : samples) { + const auto start = std::chrono::steady_clock::now(); + sleepFn(); + sample = std::chrono::steady_clock::now() - start; + if (sample < minDuration) { + std::cerr << label << " returned before its requested duration\n"; + return false; + } + } + std::sort(samples.begin(), samples.end()); + const auto median = samples[samples.size() / 2]; + if (attempt == 0 || median < bestMedian) { + bestMedian = median; + } + if (bestMedian < maxMedian) { + return true; + } + } + if (IsRunnerPreempted() || samples.back() - samples.front() > std::chrono::milliseconds(1)) { + std::clog << "Skipping strict median bound for " << label << " on busy runner (" + << std::chrono::duration_cast(bestMedian).count() << " ns)\n"; + return true; + } + std::cerr << label << " median overslept target bound: " + << std::chrono::duration_cast(bestMedian).count() << " ns\n"; + return false; +} + +} // namespace + int main() { static_assert(PreciseHundredNanosToQpcTicks(500, 10000000ULL) == 500); static_assert(PreciseHundredNanosToQpcTicks(500, 3579545ULL) == 179); @@ -36,21 +104,29 @@ int main() { } } - std::array samples{}; - for (auto& sample : samples) { - const auto start = std::chrono::steady_clock::now(); - PreciseSleepUs(50); - sample = std::chrono::steady_clock::now() - start; - if (sample < std::chrono::microseconds(50)) { - std::cerr << "PreciseSleepUs(50) returned before its requested duration\n"; - return 1; - } +#if !defined(_WIN32) + if (!CheckMedianSleepBound( + "PreciseSleepUs(50)", + std::chrono::microseconds(50), + std::chrono::microseconds(90), + [] { PreciseSleepUs(50); })) { + return 1; } - std::sort(samples.begin(), samples.end()); - const auto median = samples[samples.size() / 2]; - if (median >= std::chrono::microseconds(90)) { - std::cerr << "PreciseSleepUs(50) median overslept target bound: " - << std::chrono::duration_cast(median).count() << " ns\n"; +#endif + + if (!CheckMedianSleepBound( + "PreciseSleepUntil(+50us)", + std::chrono::microseconds(50), + std::chrono::microseconds(90), + [] { PreciseSleepUntil(std::chrono::steady_clock::now() + std::chrono::microseconds(50)); })) { + return 1; + } + + if (!CheckMedianSleepBound( + "PreciseSleepUs(1500)", + std::chrono::microseconds(1500), + std::chrono::microseconds(1650), + [] { PreciseSleepUs(1500); })) { return 1; } From 9f1e3052a3c19d1ea880c3f48c16bc7abc7022c8 Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Mon, 12 Oct 2026 19:40:31 +0000 Subject: [PATCH 0148/1000] docs: record GPU test coverage in pull request checklist --- CONTRIBUTING.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a9a443afc7..32c7be8882 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -58,6 +58,7 @@ The [pull request template](.github/pull_request_template.md) is the checklist f - A pull request that conflicts with `main` and has no activity for 7 days is labeled `stale` with a reminder. It is not closed. - If a pull request needs another one first, say so on the checklist line (`Depends on: #N`; `none` otherwise). - If a pull request fixes a stop or a wrong result seen on a title, name it on the checklist line with the title ID and its [Game bring-up](https://github.com/boykopovar/AnyPS5/discussions/categories/game-bring-up) discussion (`Unblocks: PPSA01325 #3232`; `none` otherwise). Reviewers review these first. +- For GPU changes, list each tested GPU's vendor, model, driver and OS on the checklist line. CI runs GPU tests only on lavapipe, so reviewers may ask for a run on another vendor before merging. - Fixes a title needs go to `main` as pull requests. A Game bring-up discussion lists the open pull requests and local changes the title still needs; update the list when one is merged, closed or replaced. - The PR overlap comment lists open pull requests that change the same lines or exports; agree with their authors which one goes first and declare it with `Depends on`. - Run the tests before opening the pull request and describe what was tested (OS, title or homebrew). From e60dd9d5a79306ac9679d7b4225f9e9cd0215022 Mon Sep 17 00:00:00 2001 From: Ondra97 <47064087+Ondra97@users.noreply.github.com> Date: Wed, 14 Oct 2026 22:37:53 +0200 Subject: [PATCH 0149/1000] fix(ajm): preserve live decoder handles across ID wrap --- core/libs/prx/libSceAjm.native/src/Ajm.cpp | 14 +++-- core/libs/tests/GuestAjm.cpp | 61 ++++++++++++++++++++++ docs/dev/debt/libSceAjm.native.md | 1 + 3 files changed, 71 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libSceAjm.native/src/Ajm.cpp b/core/libs/prx/libSceAjm.native/src/Ajm.cpp index 4f17031b65..eb8e5f51df 100644 --- a/core/libs/prx/libSceAjm.native/src/Ajm.cpp +++ b/core/libs/prx/libSceAjm.native/src/Ajm.cpp @@ -1526,12 +1526,16 @@ int APS5_VABI sceAjmInstanceCreate(uint32_t context, uint32_t codec, uint64_t fl created->initialized = true; } if (codec == CODEC_M4AAC) CheckAacFlags(flags); - const std::uint32_t id = (codec << 14) | (g_nextInstance.fetch_add(1, std::memory_order_relaxed) & 0x3FFF); std::lock_guard lock(g_lock); - g_instances[id] = std::move(created); - *instance = id; - AJM_TRACE("[ajm] instance create: context %u, codec %u, flags 0x%llx -> instance %u\n", context, codec, static_cast(flags), id); - return 0; + for (std::uint32_t attempt = 0; attempt < 0x4000; ++attempt) { + const std::uint32_t id = (codec << 14) | (g_nextInstance.fetch_add(1, std::memory_order_relaxed) & 0x3FFF); + if (g_instances.contains(id)) continue; + g_instances.emplace(id, std::move(created)); + *instance = id; + AJM_TRACE("[ajm] instance create: context %u, codec %u, flags 0x%llx -> instance %u\n", context, codec, static_cast(flags), id); + return 0; + } + return SCE_AJM_ERROR_OUT_OF_RESOURCES; } int APS5_VABI sceAjmInstanceDestroy(uint32_t context, uint32_t instance) { diff --git a/core/libs/tests/GuestAjm.cpp b/core/libs/tests/GuestAjm.cpp index 54344eb553..13170f3ac7 100644 --- a/core/libs/tests/GuestAjm.cpp +++ b/core/libs/tests/GuestAjm.cpp @@ -1282,6 +1282,66 @@ std::uint32_t CreateOpus(std::uint32_t context) { return instance; } +void TestInstanceHandleLifetime(std::uint32_t context) { + const auto live = CreateOpus(context); + const auto reference = CreateOpus(context); + const std::size_t packetBytes = 2 + (OPUS_STEREO[0] | (std::size_t{OPUS_STEREO[1]} << 8u)); + std::vector pcm(128 * 2); + std::vector expected(pcm.size()); + DecodeSideband decoded{}; + DecodeSideband baseline{}; + RunDecode(context, live, OPUS_STEREO, packetBytes, pcm.data(), pcm.size() * sizeof(std::int16_t), decoded); + RunDecode(context, reference, OPUS_STEREO, packetBytes, expected.data(), expected.size() * sizeof(std::int16_t), baseline); + Require(decoded.result == 0 && decoded.outputWritten == 128 * 2 * 2 && decoded.totalDecodedSamples == 960); + Require(baseline.result == 0 && pcm == expected); + for (std::size_t index = 0; index < 0x8000; ++index) { + std::uint32_t temporary = 0; + Require(sceAjmInstanceCreate(context, 24, 0, &temporary) == 0); + if (temporary == live || temporary == reference) throw std::runtime_error("AJM creation replaced an existing live decoder handle"); + Require(temporary >> 14 == 24); + Require(sceAjmInstanceDestroy(context, temporary) == 0); + } + std::vector occupied(0x4000); + occupied[live & 0x3FFF] = true; + occupied[reference & 0x3FFF] = true; + std::vector instances; + instances.reserve(0x4000 - 2); + for (std::size_t index = 0; index < 0x4000 - 2; ++index) { + std::uint32_t instance = 0; + Require(sceAjmInstanceCreate(context, 24, 0, &instance) == 0 && instance >> 14 == 24); + Require(!occupied[instance & 0x3FFF]); + occupied[instance & 0x3FFF] = true; + instances.push_back(instance); + } + Require(std::all_of(occupied.begin(), occupied.end(), [](bool used) { return used; })); + constexpr int outOfResources = static_cast(0x80930007); + std::uint32_t refused = 0xDEADBEEF; + Require(sceAjmInstanceCreate(context, 24, 0, &refused) == outOfResources && refused == 0xDEADBEEF); + std::uint32_t otherCodec = 0; + Require(sceAjmInstanceCreate(context, 1, 0, &otherCodec) == 0 && otherCodec >> 14 == 1); + const auto released = instances.back(); + Require(sceAjmInstanceDestroy(context, released) == 0); + Require(sceAjmInstanceCreate(context, 24, 0, &instances.back()) == 0 && instances.back() == released); + Require(sceAjmInstanceCreate(context, 24, 0, &refused) == outOfResources && refused == 0xDEADBEEF); + pcm.assign(832 * 2, -12345); + expected.assign(pcm.size(), -12345); + RunDecode(context, live, OPUS_STEREO, 0, pcm.data(), pcm.size() * sizeof(std::int16_t), decoded); + RunDecode(context, reference, OPUS_STEREO, 0, expected.data(), expected.size() * sizeof(std::int16_t), baseline); + Require(decoded.result == 0 && decoded.inputConsumed == 0 && decoded.outputWritten == 832 * 2 * 2 && decoded.totalDecodedSamples == 960); + Require(baseline.result == 0 && baseline.outputWritten == decoded.outputWritten && pcm == expected); + const std::size_t nextPacketBytes = 2 + (OPUS_STEREO[packetBytes] | (std::size_t{OPUS_STEREO[packetBytes + 1]} << 8u)); + pcm.assign(960 * 2, -12345); + expected.assign(pcm.size(), -12345); + RunDecode(context, live, OPUS_STEREO + packetBytes, nextPacketBytes, pcm.data(), pcm.size() * sizeof(std::int16_t), decoded); + RunDecode(context, reference, OPUS_STEREO + packetBytes, nextPacketBytes, expected.data(), expected.size() * sizeof(std::int16_t), baseline); + Require(decoded.result == 0 && static_cast(decoded.inputConsumed) == nextPacketBytes && decoded.outputWritten == 960 * 2 * 2 && decoded.totalDecodedSamples == 1920); + Require(baseline.result == 0 && baseline.outputWritten == decoded.outputWritten && pcm == expected); + for (const auto instance : instances) Require(sceAjmInstanceDestroy(context, instance) == 0); + Require(sceAjmInstanceDestroy(context, otherCodec) == 0); + Require(sceAjmInstanceDestroy(context, live) == 0); + Require(sceAjmInstanceDestroy(context, reference) == 0); +} + ResampleInfo GetResampleInfo(std::uint32_t context, std::uint32_t instance) { ResampleInfo resample; std::memset(&resample, 0xAA, sizeof(resample)); @@ -1624,6 +1684,7 @@ int main() { TestCodecInfo(context); TestGetInfo(context); TestBatchWaitRelease(context); + TestInstanceHandleLifetime(context); TestBatchCancel(context); TestControlAt9(context); TestMultichannelAt9(context); diff --git a/docs/dev/debt/libSceAjm.native.md b/docs/dev/debt/libSceAjm.native.md index 21b00e1b01..b1056a8b89 100644 --- a/docs/dev/debt/libSceAjm.native.md +++ b/docs/dev/debt/libSceAjm.native.md @@ -2,6 +2,7 @@ ### Unknown function info +- [sceAjmInstanceCreate](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - a full 14-bit instance slot namespace returns `0x80930007` (`OUT_OF_RESOURCES`), as [shadPS4's InstanceCreate](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/ajm/ajm_context.cpp) does for its own instance pool (PS4). The 16384 slots per codec prefix are this runtime's existing ID layout, not a measured console capacity; the PS5 capacity and exhaustion result are unverified. - [sceAjmDecWVorbisCreateHeaderPacket, sceAjmDecWVorbisCreateSetupPacket](../../../core/libs/prx/libSceAjm.native/Export.cpp) (libSceAjmWVorbis, implemented in libSceAjm.native) - unknown signatures - [sceAjmBatchJobGetInfo](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - writes the result and format sideband of a run job without buffers with the `SIDEBAND_FORMAT` flag (0x400000000000), as [KytyPS5](https://github.com/KytyPS5/KytyPS5) does, not confirmed on a PS5 title; the format reports bitrate 0 for ATRAC9 as in KytyPS5, while [shadPS4](https://github.com/shadps4-emu/shadPS4) derives it from the superframe size; before the instance is initialized it returns the not-initialized result (1) and no format, like the other run jobs here, while KytyPS5 returns result 0 with the format of the uninitialized decoder - [sceAjmDecMp3ParseFrame](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - signature, result layout and rate tables taken from the [shadPS4](https://github.com/shadps4-emu/shadPS4) reimplementation of the PS4 library, not confirmed on a PS5 title: the layer field is not checked and MPEG-2.5 stops at 64 kbps; the original file length (`parse_ofl`) layout (Xing/Info + LAME tag, VBRI, Fraunhofer `0xB4` block with its CRC, encoder delay = samples per frame + LAME delay + 529) also comes from shadPS4 only, and the Fraunhofer block has no other public description From 27b375326006bf20615abc4da97c73c775f2c203 Mon Sep 17 00:00:00 2001 From: notaxeltv Date: Wed, 14 Oct 2026 22:48:13 +0200 Subject: [PATCH 0150/1000] feat(libSceNpPartner001): answer sceNpHasEAAccessSubscription without an EA Access server Co-authored-by: Cursor --- core/libs/prx/libSceNpPartner001/Export.cpp | 8 ++++++++ core/libs/tests/GuestNpPartner.cpp | 10 ++++++++++ docs/dev/debt/libSceNpPartner001.md | 2 +- 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceNpPartner001/Export.cpp b/core/libs/prx/libSceNpPartner001/Export.cpp index 9d2e089775..d4dbeeefd9 100644 --- a/core/libs/prx/libSceNpPartner001/Export.cpp +++ b/core/libs/prx/libSceNpPartner001/Export.cpp @@ -21,4 +21,12 @@ int APS5_VABI sceNpEAAccessTerminate(void) { return 0; } +int APS5_VABI sceNpHasEAAccessSubscription(int userId, bool* result) { + (void)userId; + if (!g_initialized.load()) throw std::logic_error(std::string(__func__) + ": not initialized"); + if (result == nullptr) throw std::logic_error(std::string(__func__) + ": null result"); + *result = false; + return 0; +} + } diff --git a/core/libs/tests/GuestNpPartner.cpp b/core/libs/tests/GuestNpPartner.cpp index 0121f30c88..700570470a 100644 --- a/core/libs/tests/GuestNpPartner.cpp +++ b/core/libs/tests/GuestNpPartner.cpp @@ -5,6 +5,7 @@ extern "C" { int APS5_VABI sceNpEAAccessInitialize(void); int APS5_VABI sceNpEAAccessTerminate(void); +int APS5_VABI sceNpHasEAAccessSubscription(int, bool*); } namespace { @@ -24,11 +25,20 @@ bool Throws(TAction action) { } int main() { + bool subscribed = true; Require(Throws([] { sceNpEAAccessTerminate(); })); + Require(Throws([&] { sceNpHasEAAccessSubscription(1, &subscribed); })); Require(sceNpEAAccessInitialize() == 0); Require(Throws([] { sceNpEAAccessInitialize(); })); + Require(Throws([] { sceNpHasEAAccessSubscription(1, nullptr); })); + Require(sceNpHasEAAccessSubscription(1, &subscribed) == 0); + Require(!subscribed); + subscribed = true; + Require(sceNpHasEAAccessSubscription(0, &subscribed) == 0); + Require(!subscribed); Require(sceNpEAAccessTerminate() == 0); Require(Throws([] { sceNpEAAccessTerminate(); })); + Require(Throws([&] { sceNpHasEAAccessSubscription(1, &subscribed); })); Require(sceNpEAAccessInitialize() == 0); Require(sceNpEAAccessTerminate() == 0); } diff --git a/docs/dev/debt/libSceNpPartner001.md b/docs/dev/debt/libSceNpPartner001.md index a8a1d8b242..27edb961fb 100644 --- a/docs/dev/debt/libSceNpPartner001.md +++ b/docs/dev/debt/libSceNpPartner001.md @@ -3,4 +3,4 @@ ### Functional - [sceNpEAAccessInitialize, sceNpEAAccessTerminate](../../../core/libs/prx/libSceNpPartner001/Export.cpp) (libSceNpPartner001) - initialization marks the library initialized without contacting the server and termination clears it, as in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/np/np_partner.cpp); whether the console fails initialization while signed out is unknown. Termination without initialization throws where shadPS4 returns the PS4 error `0x819D0001`, whose PS5 value is unverified, and a second initialization throws where shadPS4 returns 0 without checking. PPSA23566 imports both, not confirmed on its start-up path -- [sceNpEAAccessInitialize, sceNpEAAccessTerminate](../../../core/libs/prx/libSceNpPartner001/Export.cpp) (libSceNpPartner001) - initialization marks the library initialized without contacting the server and termination clears it, as in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/np/np_partner.cpp); whether the console fails initialization while signed out is unknown. Termination without initialization throws where shadPS4 returns the PS4 error `0x819D0001`, whose PS5 value is unverified, and a second initialization throws where shadPS4 returns 0 without checking. PPSA23566 imports both, not confirmed on its start-up path +- [sceNpHasEAAccessSubscription](../../../core/libs/prx/libSceNpPartner001/Export.cpp) (libSceNpPartner001) - `(int userId, bool* result)` from [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/np/np_partner.cpp) and PPSA34015's only call (`mov edi, r14d`; `lea rsi, [rbp-0x67]`). Writes `false` and returns 0: AnyPS5 has no EA Access server. Not initialized or a null result throw where shadPS4 returns the PS4 errors `0x819D0001` / invalid argument, whose PS5 values are unverified. Whether the console errors while signed out, and what it writes when the user has a subscription, are unknown From 4b34a6d5a705affde7fef6b47c8539f6316c2e48 Mon Sep 17 00:00:00 2001 From: Leonardo Date: Wed, 14 Oct 2026 16:38:18 +0200 Subject: [PATCH 0151/1000] feat(libkernel): support microsecond timeout in size-0 _umtx_op --- .../SyncOnAddress/src/SyncOnAddress.cpp | 8 +++-- core/libs/tests/GuestUmtxOp.cpp | 33 ++++++++++--------- docs/dev/debt/libkernel.md | 2 +- 3 files changed, 23 insertions(+), 20 deletions(-) diff --git a/core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp b/core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp index 178c6459e0..50b6eb5d26 100644 --- a/core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp +++ b/core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp @@ -158,6 +158,11 @@ int UmtxDeadline(const void* sizeArgument, const void* timeArgument, std::option if (timeArgument == nullptr) { return 0; } + if (sizeArgument == nullptr) { + const auto usecs = *static_cast(timeArgument); + deadlineNanos = TimedWait::DeadlineNanos(usecs); + return 0; + } UmtxTime time{{0, 0}, 0, GUEST_CLOCK_REALTIME}; if (reinterpret_cast(sizeArgument) <= sizeof(KernelTimespec)) { time.timeout = *static_cast(timeArgument); @@ -165,9 +170,6 @@ int UmtxDeadline(const void* sizeArgument, const void* timeArgument, std::option time = *static_cast(timeArgument); } if (time.timeout.tv_sec < 0 || time.timeout.tv_nsec < 0 || time.timeout.tv_nsec >= NANOS_PER_SECOND) { - if (sizeArgument == nullptr) { - NotImplemented_nid_no_patch("_umtx_op timeout with size argument 0 that is not a valid timespec"); - } return GUEST_EINVAL; } diff --git a/core/libs/tests/GuestUmtxOp.cpp b/core/libs/tests/GuestUmtxOp.cpp index 48ec61d508..56abb976c2 100644 --- a/core/libs/tests/GuestUmtxOp.cpp +++ b/core/libs/tests/GuestUmtxOp.cpp @@ -123,19 +123,20 @@ static void TimesOutWhileTheValueMatches() { static void RejectsAnInvalidTimeout() { std::uint32_t word = 1; KernelTimespec negative{-1, 0}; - Require(FailsWith(Umtx(&word, UMTX_OP_WAIT_UINT, 1, SizeOf(sizeof(negative)), &negative), GUEST_EINVAL)); KernelTimespec tooManyNanos{0, 1000000000}; - Require(FailsWith(Umtx(&word, UMTX_OP_WAIT_UINT, 1, SizeOf(sizeof(tooManyNanos)), &tooManyNanos), GUEST_EINVAL)); for (const int operation : {UMTX_OP_WAIT, UMTX_OP_WAIT_UINT}) { - bool threw = false; - try { Umtx(&word, operation, 1, nullptr, &tooManyNanos); } - catch (const std::runtime_error&) { threw = true; } - Require(threw); - threw = false; - try { Umtx(&word, operation, 1, nullptr, &negative); } - catch (const std::runtime_error&) { threw = true; } - Require(threw); + Require(FailsWith(Umtx(&word, operation, 1, SizeOf(sizeof(negative)), &negative), GUEST_EINVAL)); + Require(FailsWith(Umtx(&word, operation, 1, SizeOf(sizeof(tooManyNanos)), &tooManyNanos), GUEST_EINVAL)); } + struct { + std::uint32_t usecs = 20000; + std::uint32_t zero32 = 0; + std::int64_t zero64 = 0; + } zeroNeighbours; + auto start = std::chrono::steady_clock::now(); + Require(FailsWith(Umtx(&word, UMTX_OP_WAIT_UINT, 1, nullptr, &zeroNeighbours), GUEST_ETIMEDOUT)); + Require(std::chrono::steady_clock::now() - start >= std::chrono::milliseconds(15)); + Require(std::chrono::steady_clock::now() - start < std::chrono::seconds(1)); UmtxTime negativeNanos{{0, -1}, 0, CLOCK_MONOTONIC_ID}; Require(FailsWith(Umtx(&word, UMTX_OP_WAIT_UINT, 1, SizeOf(sizeof(negativeNanos)), &negativeNanos), GUEST_EINVAL)); } @@ -146,7 +147,7 @@ static void WakeReleasesAWaiterWithoutAValueChange() { int result = -2; std::thread waiter([&] { KernelTimespec failsafe = FailsafeTimeout(); - result = Umtx(&value, UMTX_OP_WAIT, 1, nullptr, &failsafe); + result = Umtx(&value, UMTX_OP_WAIT, 1, SizeOf(sizeof(failsafe)), &failsafe); ++finished; }); while (finished.load() == 0) { @@ -171,14 +172,14 @@ static void WakeHonoursTheCountAndTheAddress() { waiters.emplace_back([&, i] { KernelTimespec failsafe = FailsafeTimeout(); ++started; - results[i] = Umtx(&word, UMTX_OP_WAIT_UINT_PRIVATE, 1, nullptr, &failsafe); + results[i] = Umtx(&word, UMTX_OP_WAIT_UINT_PRIVATE, 1, SizeOf(sizeof(failsafe)), &failsafe); ++finished; }); } std::thread otherWaiter([&] { KernelTimespec failsafe = FailsafeTimeout(); ++started; - otherResult = Umtx(&other, UMTX_OP_WAIT_UINT, 1, nullptr, &failsafe); + otherResult = Umtx(&other, UMTX_OP_WAIT_UINT, 1, SizeOf(sizeof(failsafe)), &failsafe); ++otherFinished; }); AwaitCount(started, 5); @@ -227,20 +228,20 @@ static void NwakeWakesEveryWaiterOnEachListedAddress() { waiters.emplace_back([&, i] { KernelTimespec failsafe = FailsafeTimeout(); ++started; - results[i] = Umtx(&first, UMTX_OP_WAIT_UINT_PRIVATE, 1, nullptr, &failsafe); + results[i] = Umtx(&first, UMTX_OP_WAIT_UINT_PRIVATE, 1, SizeOf(sizeof(failsafe)), &failsafe); ++finished; }); } waiters.emplace_back([&] { KernelTimespec failsafe = FailsafeTimeout(); ++started; - results[3] = Umtx(&second, UMTX_OP_WAIT, 1, nullptr, &failsafe); + results[3] = Umtx(&second, UMTX_OP_WAIT, 1, SizeOf(sizeof(failsafe)), &failsafe); ++finished; }); std::thread unlistedWaiter([&] { KernelTimespec failsafe = FailsafeTimeout(); ++started; - unlistedResult = Umtx(&unlisted, UMTX_OP_WAIT_UINT, 1, nullptr, &failsafe); + unlistedResult = Umtx(&unlisted, UMTX_OP_WAIT_UINT, 1, SizeOf(sizeof(failsafe)), &failsafe); ++unlistedFinished; }); AwaitCount(started, 5); diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index efa9156809..031a98bd06 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -47,7 +47,7 @@ - [sceKernelAvailableFlexibleMemorySize](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - the segments of the title's executable and of its `.guest.prx` modules count against the configured flexible-memory budget (448 MiB by default) while loaded, for this size and for flexible maps. A title may declare a larger budget through `kernel.flexibleMemorySize` in `param.json`; this does not reduce the 13.5 GiB direct-memory size. Rejection of zero or a value not aligned to 16 KiB has not been measured on a console. This segment-accounting model follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/3a9f57265d28975658f1ca941daa5c879c7a74bd/src/core/module.cpp#L165), which maps every segment of a module outside its system module directory as flexible memory on the PS4. [Counted](../../../core/libs/prx/libkernel/Module/src/ModuleInfo.cpp) are the segments the title's ELF gives permissions, each rounded to 16 KiB pages; not counted are segments without permissions, the relinker's own blocks (on Linux both are the read/write/execute `PT_LOAD`s, plus the header block at file offset 0; on Windows everything outside the `.elf` sections) and the host system libraries. The Linux image carries no marker for the relinker's blocks, so they are told apart by those flags and that offset only: a title segment that is writable and executable, or starts at file offset 0, is not counted. A module whose segments exceed what is left still loads, and the available size then reads 0; shadPS4 fails the segment map instead ([memory.cpp](https://github.com/shadps4-emu/shadPS4/blob/3a9f57265d28975658f1ca941daa5c879c7a74bd/src/core/memory.cpp#L519-L524) returns `EINVAL`, [module.cpp](https://github.com/shadps4-emu/shadPS4/blob/3a9f57265d28975658f1ca941daa5c879c7a74bd/src/core/module.cpp#L168-L169) asserts). Which segments the PS5 charges, its rounding, whether system modules use the title's budget and when an unload releases the charge are not measured. PPSA15508 subtracts the reported size and a 10 MiB module allowance from its own 448 MiB budget at start-up and stops when the result exceeds its 146 MiB code and data limit ([#2883](https://github.com/boykopovar/AnyPS5/issues/2883)) - [Memory protection bits 0x100 and 0x200](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (libkernel) - taken as audio coprocessor (ACP) read and write, as in the [KytyPS5](https://github.com/KytyPS5/KytyPS5) and [etaHEN](https://github.com/etaHEN/etaHEN) headers; the coprocessor work runs on the host CPU, so they grant host read and read/write access like the GPU bits - [Foreign guest module image protection on Windows](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (libkernel) - `sceKernelMprotect` on Windows allows protection mutations on mapped PE images containing a `.elf0` section (as relinked guest modules produce, such as NVIDIA Real-time Denoisers `libNRD.prx` bundled in Dying Light 2: Stay Human PPSA02262), registering their sections into `GuestAllocations`. Host libraries and system DLLs (such as `libkernel.prx` and `kernel32.dll`) without a `.elf0` section continue to be rejected -- [_umtx_op](../../../core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp) (libkernel) - `UMTX_OP_WAIT`, `UMTX_OP_WAKE`, `UMTX_OP_WAIT_UINT`, `UMTX_OP_WAIT_UINT_PRIVATE`, `UMTX_OP_WAKE_PRIVATE` and `UMTX_OP_NWAKE_PRIVATE` follow FreeBSD 11's [kern_umtx.c](https://github.com/freebsd/freebsd-src/blob/stable/11/sys/kern/kern_umtx.c): a timeout whose size argument is at most 16 is a relative `timespec`, a larger one an `_umtx_time`, and a wake of 0 or fewer wakes one waiter. `UMTX_OP_NWAKE_PRIVATE` reads the value as an `int` count of pointers at the object and wakes every waiter on each one; a count of 0 or fewer reads nothing, and a null list fails with `EFAULT`, where FreeBSD's `copyin` would also fail for any other unreadable list. Waiters share the queue of `sceKernelSyncOnAddressWait`. Operation 0 fails with `EOPNOTSUPP` and operations above 26 with `EINVAL`, as in FreeBSD 11; `UMTX_OP_WAIT_UINT` with a value that does not fit in 32 bits returns 0 at once, as FreeBSD's zero-extended compare does. Operation 1 throws, since PPSA12544's call_once uses it in a way FreeBSD rejects; the other operations throw, and so does a misaligned object. PPSA12544's `libc.prx` passes `UMTX_OP_WAIT` and `UMTX_OP_WAIT_UINT` a pointer to a 4-byte microsecond timeout with size argument 0, which FreeBSD reads as a `timespec`, and waits for a running call_once with operation 1; its `PS5Util.prx` passes a `timespec`. Whether the console reads a microsecond timeout there, and what operation 1 does on it, is unverified. A libc call cannot be told apart from a `timespec` call by the size argument alone, so a timeout with size argument 0 is read as a `timespec` and only throws when it is not a valid one (negative seconds, nanoseconds outside [0, 1e9)), which is where FreeBSD would return `EINVAL`; a microsecond value that happens to read as a valid `timespec` is waited on as one +- [_umtx_op](../../../core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp) (libkernel) - `UMTX_OP_WAIT`, `UMTX_OP_WAKE`, `UMTX_OP_WAIT_UINT`, `UMTX_OP_WAIT_UINT_PRIVATE`, `UMTX_OP_WAKE_PRIVATE` and `UMTX_OP_NWAKE_PRIVATE` follow FreeBSD 11's [kern_umtx.c](https://github.com/freebsd/freebsd-src/blob/stable/11/sys/kern/kern_umtx.c): a timeout whose size argument is at most 16 is a relative `timespec`, a larger one an `_umtx_time`, and a wake of 0 or fewer wakes one waiter. `UMTX_OP_NWAKE_PRIVATE` reads the value as an `int` count of pointers at the object and wakes every waiter on each one; a count of 0 or fewer reads nothing, and a null list fails with `EFAULT`, where FreeBSD's `copyin` would also fail for any other unreadable list. Waiters share the queue of `sceKernelSyncOnAddressWait`. Operation 0 fails with `EOPNOTSUPP` and operations above 26 with `EINVAL`, as in FreeBSD 11; `UMTX_OP_WAIT_UINT` with a value that does not fit in 32 bits returns 0 at once, as FreeBSD's zero-extended compare does. Operation 1 throws, since PPSA12544's call_once uses it in a way FreeBSD rejects; the other operations throw, and so does a misaligned object. PPSA12544's `libc.prx` and Unity's `PS5Util.prx` (PPSA18194) pass `UMTX_OP_WAIT` and `UMTX_OP_WAIT_UINT` a pointer to a 4-byte microsecond timeout with size argument 0, which FreeBSD reads as a `timespec`, and wait for a running call_once with operation 1. Whether the console reads a microsecond timeout there in all cases, and what operation 1 does on it, is unverified. A call with size argument 0 is read unconditionally as a 4-byte microsecond timeout, matching callers like `libc.prx` and `PS5Util.prx`'s `zlqfTyrQSPk` wrapper which pass size 0 with a 32-bit microsecond count - [sceKernelSyncOnAddressWait](../../../core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp) (libkernel) - the only known caller passes a null timeout and a name string as the fourth argument; the timeout is assumed to point to microseconds like the other kernel waits, and the name is ignored - [sceKernelSyncOnAddressWait8 / sceKernelSyncOnAddressWait16 / sceKernelSyncOnAddressWait32 / sceKernelSyncOnAddressWait64](../../../core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp) (libkernel) - libc's `__std_atomic_wait_direct_8/16/32/64` pass a microsecond timeout or null and ignore the result; the by-value comparand, return values and the 2- and 8-byte alignment of Wait16 and Wait64 are assumed - [scePthreadRwlockDestroy, pthread_rwlock_destroy](../../../core/libs/prx/libkernel/Pthread/src/Rwlock.cpp) (libkernel) - measured on a PS5 (firmware 13.60): destroying a rwlock returns 0 whether it is free or held for reading or writing, and leaves 1 in the handle; every call on that handle then returns `EINVAL` until it is initialized again, and a zero handle is initialized on first use by the `scePthreadRwlock` calls as by the POSIX ones. A rwlock destroyed while held is not freed, so its memory leaks. Destroying a rwlock while another thread is acquiring it is not guarded From 9936b8b9ee2395ab279e4d338d73dd354dfe4ce5 Mon Sep 17 00:00:00 2001 From: MiniRML <134226498+MiniRML@users.noreply.github.com> Date: Wed, 14 Oct 2026 13:50:14 -0700 Subject: [PATCH 0152/1000] fix(tools): reserve program header space when unwrapping SELF --- tools/tests/test_unwrap_self.py | 29 ++++++++++++++++++++++++----- tools/unwrap_self.py | 4 ++-- 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/tools/tests/test_unwrap_self.py b/tools/tests/test_unwrap_self.py index 44e1897ccb..fd024985de 100644 --- a/tools/tests/test_unwrap_self.py +++ b/tools/tests/test_unwrap_self.py @@ -23,18 +23,19 @@ def pattern(seed, size): return bytes((seed + index * 7) % 251 for index in range(size)) -def build_elf(segments): +def build_elf(segments, program_offset=PROGRAM_TABLE_OFFSET): header = bytearray(HEADER) struct.pack_into(" Date: Wed, 14 Oct 2026 13:56:21 -0700 Subject: [PATCH 0153/1000] fix(tools): parse Windows compile commands without losing backslashes --- tools/check_compile.py | 23 ++++++++++++++++++++++- tools/tests/test_check_compile.py | 16 ++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/tools/check_compile.py b/tools/check_compile.py index 1ec01a433c..632dc8531f 100644 --- a/tools/check_compile.py +++ b/tools/check_compile.py @@ -68,8 +68,29 @@ def changed(root, base): return [name for name in names if name.endswith(CPP)] +def command_arguments(command): + if os.name != "nt": + return shlex.split(command) + import ctypes + from ctypes import wintypes + count = ctypes.c_int() + split = ctypes.WinDLL("shell32", use_last_error=True).CommandLineToArgvW + split.argtypes = (wintypes.LPCWSTR, ctypes.POINTER(ctypes.c_int)) + split.restype = ctypes.POINTER(wintypes.LPWSTR) + free = ctypes.WinDLL("kernel32", use_last_error=True).LocalFree + free.argtypes = (wintypes.HLOCAL,) + free.restype = wintypes.HLOCAL + arguments = split(command, ctypes.byref(count)) + if not arguments: + raise ctypes.WinError(ctypes.get_last_error()) + try: + return [arguments[index] for index in range(count.value)] + finally: + free(arguments) + + def syntax_only(entry, source): - arguments = shlex.split(entry["command"]) + arguments = command_arguments(entry["command"]) directory = Path(entry["directory"]) wanted = source.resolve() trimmed = [] diff --git a/tools/tests/test_check_compile.py b/tools/tests/test_check_compile.py index b691d60d45..4516777066 100644 --- a/tools/tests/test_check_compile.py +++ b/tools/tests/test_check_compile.py @@ -80,6 +80,22 @@ class CheckCompileTests(unittest.TestCase): self.assertEqual(result.returncode, 0) self.assertEqual(json.loads(seen.read_text(encoding="utf-8")), ["-fsyntax-only", str(source)]) + @unittest.skipUnless(os.name == "nt", "Windows command-line quoting") + def test_windows_paths_and_quoted_defines_survive_the_syntax_check(self): + source = self.source("unit.cpp") + script = self.compiler( + "import json\nimport sys\nfrom pathlib import Path\n" + "Path(sys.argv[1]).write_text(json.dumps(sys.argv[2:]))\n") + seen = self.root / "arguments.json" + flags = ['-DNAME=quoted "value"', '-IC:\\sdk files\\include\\'] + arguments = [sys.executable, str(script), str(seen), *flags, + "-c", "-o", "unit.o", "-MD", "-MF", "unit.o.d", str(source)] + self.manifest([self.entry("unit.cpp", subprocess.list2cmdline(arguments))]) + result = self.tool("unit.cpp") + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(json.loads(seen.read_text(encoding="utf-8")), + [*flags, "-fsyntax-only", str(source)]) + def test_nothing_checked_is_a_failure(self): self.manifest([]) result = self.tool("orphan.cpp") From cd95f1a42675323231e0b7d3b05f2107c400807b Mon Sep 17 00:00:00 2001 From: JAB-dev <68183649+JAB-dev@users.noreply.github.com> Date: Wed, 14 Oct 2026 22:57:08 +0200 Subject: [PATCH 0154/1000] perf(libSceSysmodule): cache the unwind region of loaded images on Windows sceSysmoduleGetModuleInfoForUnwind called VirtualQuery and GetMappedFileNameA on every call. VirtualQuery on a large image section costs time proportional to the section (2 us for 16 KiB, 51 us for 8 MiB, 167 us for 32 MiB), and a title that unwinds exceptions while loading calls it for every frame of every throw. The region and file name of an address inside an image of the Windows loader's module list are kept per region and used while the loader still reports the same image (base, time stamp and size). Other addresses are queried every time. Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libSceSysmodule/Export.cpp | 80 +++++++++++++++++++++--- core/libs/tests/GuestSysmodule.cpp | 51 ++++++++++++++- docs/dev/debt/libSceSysmodule.md | 5 ++ 3 files changed, 126 insertions(+), 10 deletions(-) create mode 100644 docs/dev/debt/libSceSysmodule.md diff --git a/core/libs/prx/libSceSysmodule/Export.cpp b/core/libs/prx/libSceSysmodule/Export.cpp index 8ed9239477..889ee2cfad 100644 --- a/core/libs/prx/libSceSysmodule/Export.cpp +++ b/core/libs/prx/libSceSysmodule/Export.cpp @@ -1,5 +1,6 @@ #include #include +#include #include #include #include @@ -38,23 +39,84 @@ void* openHostModule(const char* name) { return dlopen_nid_postfix(relative.c_str(), kRtldNow); } -bool fillModuleInfoForUnwind(std::uint64_t addr, ModuleInfoForUnwind* info) { #ifdef _WIN32 - MEMORY_BASIC_INFORMATION mbi{}; - if (!VirtualQuery(reinterpret_cast(addr), &mbi, sizeof(mbi))) { - return false; - } +struct ImageRegion { + std::uint64_t end = 0; + std::uintptr_t image = 0; + DWORD stamp = 0; + DWORD size = 0; + std::string name; +}; + +std::mutex gRegionsMutex; +std::map gRegions; + +bool imageIdentity(std::uintptr_t image, DWORD& stamp, DWORD& size) { + const auto* dos = reinterpret_cast(image); + if (dos->e_magic != IMAGE_DOS_SIGNATURE) return false; + const auto* nt = reinterpret_cast(image + dos->e_lfanew); + if (nt->Signature != IMAGE_NT_SIGNATURE) return false; + stamp = nt->FileHeader.TimeDateStamp; + size = nt->OptionalHeader.SizeOfImage; + return true; +} + +std::uintptr_t loaderImage(std::uint64_t addr) { + PVOID image = nullptr; + if (RtlPcToFileHeader(reinterpret_cast(addr), &image) == nullptr) return 0; + return reinterpret_cast(image); +} + +void writeUnwindRegion(std::uint64_t begin, std::uint64_t size, const std::string& name, ModuleInfoForUnwind* info) { info->st_size = sizeof(ModuleInfoForUnwind); info->eh_frame_hdr_addr = 0; info->eh_frame_addr = 0; info->eh_frame_size = 0; - info->seg0_addr = reinterpret_cast(mbi.BaseAddress); - info->seg0_size = mbi.RegionSize; + info->seg0_addr = begin; + info->seg0_size = size; + std::strncpy(info->name, name.c_str(), sizeof(info->name) - 1); + info->name[sizeof(info->name) - 1] = '\0'; +} + +bool fillCachedImageRegion(std::uint64_t addr, std::uintptr_t image, ModuleInfoForUnwind* info) { + std::lock_guard lock(gRegionsMutex); + auto found = gRegions.upper_bound(addr); + if (found == gRegions.begin()) return false; + --found; + const auto& region = found->second; + if (addr >= region.end) return false; + DWORD stamp = 0; + DWORD size = 0; + if (region.image != image || !imageIdentity(image, stamp, size) || stamp != region.stamp || size != region.size) { + gRegions.erase(found); + return false; + } + writeUnwindRegion(found->first, region.end - found->first, region.name, info); + return true; +} +#endif + +bool fillModuleInfoForUnwind(std::uint64_t addr, ModuleInfoForUnwind* info) { +#ifdef _WIN32 + const auto image = loaderImage(addr); + if (image != 0 && fillCachedImageRegion(addr, image, info)) return true; + MEMORY_BASIC_INFORMATION mbi{}; + if (!VirtualQuery(reinterpret_cast(addr), &mbi, sizeof(mbi))) { + return false; + } char path[4096] = {}; DWORD len = GetMappedFileNameA(GetCurrentProcess(), mbi.BaseAddress, path, sizeof(path) - 1); path[len] = '\0'; - std::strncpy(info->name, path, sizeof(info->name) - 1); - info->name[sizeof(info->name) - 1] = '\0'; + const auto begin = reinterpret_cast(mbi.BaseAddress); + writeUnwindRegion(begin, mbi.RegionSize, path, info); + ImageRegion region; + if (mbi.Type == MEM_IMAGE && image != 0 && imageIdentity(image, region.stamp, region.size) && begin >= image && begin + mbi.RegionSize <= image + region.size) { + region.end = begin + mbi.RegionSize; + region.image = image; + region.name = path; + std::lock_guard lock(gRegionsMutex); + gRegions.insert_or_assign(begin, std::move(region)); + } return true; #else const auto mapping = HostMapping::Find(addr, "sceSysmoduleGetModuleInfoForUnwind"); diff --git a/core/libs/tests/GuestSysmodule.cpp b/core/libs/tests/GuestSysmodule.cpp index 4c7f86455c..5f7cac0296 100644 --- a/core/libs/tests/GuestSysmodule.cpp +++ b/core/libs/tests/GuestSysmodule.cpp @@ -2,9 +2,14 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include #include +#include #include #include #include +#ifdef _WIN32 +#include +#include +#endif #ifdef __linux__ #include #include @@ -103,6 +108,50 @@ void CheckUnwindInfo() { } #endif +#ifdef _WIN32 +void RequireUnwindMatchesRegion(const void* pointer) { + const auto address = reinterpret_cast(pointer); + MEMORY_BASIC_INFORMATION expected{}; + Require(VirtualQuery(pointer, &expected, sizeof(expected)) != 0); + char mapped[4096] = {}; + const DWORD length = GetMappedFileNameA(GetCurrentProcess(), expected.BaseAddress, mapped, sizeof(mapped) - 1); + mapped[length] = '\0'; + for (int repeat = 0; repeat < 3; ++repeat) { + ModuleInfoForUnwind info{}; + Require(sceSysmoduleGetModuleInfoForUnwind(address, 0, &info) == 0); + Require(info.st_size == sizeof(ModuleInfoForUnwind)); + Require(info.seg0_addr == reinterpret_cast(expected.BaseAddress) && info.seg0_size == expected.RegionSize); + Require(std::strncmp(info.name, mapped, sizeof(info.name) - 1) == 0); + Require(info.eh_frame_hdr_addr == 0 && info.eh_frame_addr == 0 && info.eh_frame_size == 0); + } +} + +void CheckUnwindInfo() { + RequireUnwindMatchesRegion(reinterpret_cast(&RequireUnwindMatchesRegion)); + RequireUnwindMatchesRegion(reinterpret_cast(&sceSysmoduleGetModuleInfoForUnwind)); + RequireUnwindMatchesRegion(reinterpret_cast(&GetModuleHandleW)); + static int data = 1; + static const char text[] = "unwind region"; + RequireUnwindMatchesRegion(&data); + RequireUnwindMatchesRegion(text); + RequireUnwindMatchesRegion(reinterpret_cast(&RequireUnwindMatchesRegion)); + int local = 0; + RequireUnwindMatchesRegion(&local); + SYSTEM_INFO system{}; + GetSystemInfo(&system); + const std::size_t page = system.dwPageSize; + auto* memory = static_cast(VirtualAlloc(nullptr, 3 * page, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE)); + Require(memory != nullptr); + RequireUnwindMatchesRegion(memory); + DWORD previous = 0; + Require(VirtualProtect(memory + 2 * page, page, PAGE_READONLY, &previous) != 0); + RequireUnwindMatchesRegion(memory); + RequireUnwindMatchesRegion(memory + 2 * page); + Require(VirtualFree(memory, 0, MEM_RELEASE) != 0); + RequireUnwindMatchesRegion(memory); +} +#endif + } int main() { @@ -178,7 +227,7 @@ int main() { Require(sceSysmoduleUnloadModuleInternal(kAgcModuleId) == 0); Require(sceSysmoduleUnloadModuleInternal(kAgcModuleId) == 0); Require(sceSysmoduleUnloadModuleInternal(kAgcModuleId) == kModuleUnloaded); -#ifdef __linux__ +#if defined(__linux__) || defined(_WIN32) CheckUnwindInfo(); #endif diff --git a/docs/dev/debt/libSceSysmodule.md b/docs/dev/debt/libSceSysmodule.md new file mode 100644 index 0000000000..739a0ac8db --- /dev/null +++ b/docs/dev/debt/libSceSysmodule.md @@ -0,0 +1,5 @@ +# libSceSysmodule technical debt + +### Functional + +- [sceSysmoduleGetModuleInfoForUnwind](../../../core/libs/prx/libSceSysmodule/Export.cpp) (libSceSysmodule) - on Windows the region and file name of an address inside a loaded image are cached after the first lookup, because `VirtualQuery` on a large image section is slow. An entry is used only while the loader still reports the same image for the address (same base, time stamp and size). A page protection change inside the image after the first lookup, which would split or merge the region `VirtualQuery` reports, is not noticed. Addresses outside loaded images are queried every time From 784cb4d3dbe7f8248186e8f5948fa2fc98aef731 Mon Sep 17 00:00:00 2001 From: notaxeltv Date: Wed, 14 Oct 2026 23:00:30 +0200 Subject: [PATCH 0155/1000] feat(libScePlayerSelectionDialog): run common-dialog state machine without UI Co-authored-by: Cursor --- .../CMakeLists.txt | 14 ++++++ .../libScePlayerSelectionDialog/Export.cpp | 49 ++++++++++++++++++ .../libs/tests/GuestPlayerSelectionDialog.cpp | 50 +++++++++++++++++++ docs/dev/debt/libScePlayerSelectionDialog.md | 5 ++ 4 files changed, 118 insertions(+) create mode 100644 core/libs/prx/libScePlayerSelectionDialog/CMakeLists.txt create mode 100644 core/libs/tests/GuestPlayerSelectionDialog.cpp create mode 100644 docs/dev/debt/libScePlayerSelectionDialog.md diff --git a/core/libs/prx/libScePlayerSelectionDialog/CMakeLists.txt b/core/libs/prx/libScePlayerSelectionDialog/CMakeLists.txt new file mode 100644 index 0000000000..02e53fdc16 --- /dev/null +++ b/core/libs/prx/libScePlayerSelectionDialog/CMakeLists.txt @@ -0,0 +1,14 @@ +add_library(libScePlayerSelectionDialog SHARED EXCLUDE_FROM_ALL Export.cpp) +target_include_directories(libScePlayerSelectionDialog PRIVATE ${LIBS_INCLUDE_DIR}) +configure_windows_unwind(libScePlayerSelectionDialog) + +if(BUILD_TESTING) + add_test_executable(guest_player_selection_dialog_tests ${LIBS_INCLUDE_DIR}/tests/GuestPlayerSelectionDialog.cpp) + target_include_directories(guest_player_selection_dialog_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_player_selection_dialog_tests PRIVATE libScePlayerSelectionDialog) + add_test(NAME guest_player_selection_dialog COMMAND guest_player_selection_dialog_tests) + if(WIN32) + set_tests_properties(guest_player_selection_dialog PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libScePlayerSelectionDialog/Export.cpp b/core/libs/prx/libScePlayerSelectionDialog/Export.cpp index e32913a7ad..6093f69d67 100644 --- a/core/libs/prx/libScePlayerSelectionDialog/Export.cpp +++ b/core/libs/prx/libScePlayerSelectionDialog/Export.cpp @@ -1,11 +1,60 @@ +#include #include #include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +namespace { + +constexpr int COMMON_DIALOG_STATUS_NONE = 0; +constexpr int COMMON_DIALOG_STATUS_INITIALIZED = 1; +constexpr int COMMON_DIALOG_STATUS_RUNNING = 2; +constexpr int COMMON_DIALOG_STATUS_FINISHED = 3; +constexpr int COMMON_DIALOG_ERROR_NOT_INITIALIZED = static_cast(0x80B80003u); +constexpr int COMMON_DIALOG_ERROR_ALREADY_INITIALIZED = static_cast(0x80B80004u); +constexpr int COMMON_DIALOG_ERROR_NOT_FINISHED = static_cast(0x80B80005u); +constexpr int COMMON_DIALOG_ERROR_BUSY = static_cast(0x80B80007u); +constexpr int COMMON_DIALOG_ERROR_ARG_NULL = static_cast(0x80B8000Du); +constexpr int COMMON_DIALOG_RESULT_USER_CANCELED = 1; + +std::atomic g_status{COMMON_DIALOG_STATUS_NONE}; + +} + extern "C" { +int APS5_VABI scePlayerSelectionDialogInitialize(void) { + int expected = COMMON_DIALOG_STATUS_NONE; + if (!g_status.compare_exchange_strong(expected, COMMON_DIALOG_STATUS_INITIALIZED)) return COMMON_DIALOG_ERROR_ALREADY_INITIALIZED; + return 0; +} + +int APS5_VABI scePlayerSelectionDialogOpen(const void* param) { + const int status = g_status.load(); + if (status == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + if (status == COMMON_DIALOG_STATUS_RUNNING) return COMMON_DIALOG_ERROR_BUSY; + if (param == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; + g_status = COMMON_DIALOG_STATUS_FINISHED; + return 0; +} + +int APS5_VABI scePlayerSelectionDialogUpdateStatus(void) { + return g_status.load(); +} + +int APS5_VABI scePlayerSelectionDialogGetResult(void* result) { + const int status = g_status.load(); + if (status == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + if (result == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; + if (status != COMMON_DIALOG_STATUS_FINISHED) return COMMON_DIALOG_ERROR_NOT_FINISHED; + const std::int32_t canceled = COMMON_DIALOG_RESULT_USER_CANCELED; + std::memcpy(result, &canceled, sizeof(canceled)); + return 0; +} + int APS5_VABI scePlayerSelectionDialogTerminate(void) { + if (g_status.exchange(COMMON_DIALOG_STATUS_NONE) == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; return 0; } diff --git a/core/libs/tests/GuestPlayerSelectionDialog.cpp b/core/libs/tests/GuestPlayerSelectionDialog.cpp new file mode 100644 index 0000000000..cd5d62693e --- /dev/null +++ b/core/libs/tests/GuestPlayerSelectionDialog.cpp @@ -0,0 +1,50 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include + +extern "C" { +int APS5_VABI scePlayerSelectionDialogInitialize(void); +int APS5_VABI scePlayerSelectionDialogOpen(const void* param); +int APS5_VABI scePlayerSelectionDialogUpdateStatus(void); +int APS5_VABI scePlayerSelectionDialogGetResult(void* result); +int APS5_VABI scePlayerSelectionDialogTerminate(void); +} + +namespace { + +void Require(bool value) { if (!value) std::abort(); } + +constexpr int kStatusFinished = 3; +constexpr int kErrNotInitialized = static_cast(0x80B80003); +constexpr int kErrAlreadyInitialized = static_cast(0x80B80004); +constexpr int kErrNotFinished = static_cast(0x80B80005); +constexpr int kErrArgNull = static_cast(0x80B8000D); +constexpr int kResultUserCanceled = 1; + +} + +int main() { + std::int64_t param = 0; + std::int32_t result[2] = {-1, -1}; + + Require(scePlayerSelectionDialogTerminate() == kErrNotInitialized); + Require(scePlayerSelectionDialogOpen(¶m) == kErrNotInitialized); + Require(scePlayerSelectionDialogGetResult(result) == kErrNotInitialized); + + Require(scePlayerSelectionDialogInitialize() == 0); + Require(scePlayerSelectionDialogInitialize() == kErrAlreadyInitialized); + + Require(scePlayerSelectionDialogOpen(nullptr) == kErrArgNull); + Require(scePlayerSelectionDialogGetResult(result) == kErrNotFinished); + + Require(scePlayerSelectionDialogOpen(¶m) == 0); + Require(scePlayerSelectionDialogUpdateStatus() == kStatusFinished); + + Require(scePlayerSelectionDialogGetResult(nullptr) == kErrArgNull); + Require(scePlayerSelectionDialogGetResult(result) == 0); + Require(result[0] == kResultUserCanceled); + Require(result[1] == -1); + + Require(scePlayerSelectionDialogTerminate() == 0); + Require(scePlayerSelectionDialogTerminate() == kErrNotInitialized); +} diff --git a/docs/dev/debt/libScePlayerSelectionDialog.md b/docs/dev/debt/libScePlayerSelectionDialog.md new file mode 100644 index 0000000000..602e740063 --- /dev/null +++ b/docs/dev/debt/libScePlayerSelectionDialog.md @@ -0,0 +1,5 @@ +# libScePlayerSelectionDialog technical debt + +### Silent stubs + +- [libScePlayerSelectionDialog](../../../core/libs/prx/libScePlayerSelectionDialog/Export.cpp) - the player-selection dialog runs the common-dialog state machine but shows nothing and reports the user as having cancelled. `GetResult` writes only the first `int32` (`USER_CANCELED`); the rest of `ScePlayerSelectionDialogResult` (player list pointer and length) is not filled. EA SPORTS FC 27 (PPSA34015) fails to load `libScePlayerSelectionDialog.prx` without `Initialize`, `Open`, `UpdateStatus` and `GetResult` (`unresolved ELF import` / `GetLastError: 127`) From 4ceb020b551947064c9237059bc37cc279f8e753 Mon Sep 17 00:00:00 2001 From: David Date: Thu, 15 Oct 2026 00:20:43 +0400 Subject: [PATCH 0156/1000] fix(relinker): trap guest GS accesses in Windows output instead of refusing the module On Windows the host owns GS (the thread environment block), so a guest instruction that reads or writes through GS cannot be translated the way FS thread-pointer accesses are. Such an instruction refused the whole module. Overwrite it in place with ud2 and NOP padding instead, so the module converts and the instruction reports itself through the crash reporter if it runs. --- .../src/windows/WindowsTlsBuilder.cpp | 22 +++++++++++++++++++ .../tests/test_tls_function_coverage.py | 1 - 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp b/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp index 2a017013c4..faac015edb 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp @@ -1,4 +1,5 @@ #include +#include #include #include #include @@ -90,6 +91,20 @@ void patchAccess(std::vector& sections, const TlsAccess& access, cons throw Domain::RelinkerException("TLS instruction is outside the PE image", access.Rva); } +void trapInstruction(std::vector& sections, const std::uint32_t rva, const std::size_t length) { + for (auto& section : sections) { + if (rva < section.Rva || rva - section.Rva >= section.Data.size()) + continue; + const auto offset = rva - section.Rva; + if (length > section.Data.size() - offset) + throw Domain::RelinkerException("Guest GS instruction crosses a PE section boundary", rva); + std::fill_n(section.Data.begin() + offset, length, 0x90); + std::copy(std::begin(kStubOpUd2), std::end(kStubOpUd2), section.Data.begin() + offset); + return; + } + throw Domain::RelinkerException("Guest GS instruction is outside the PE image", rva); +} + } PeDirectory WindowsTlsBuilder::Build(const std::vector& source, const std::vector& headers, const WindowsLoadImage& image, std::vector& sections, std::vector& relocations, std::uint32_t& nextRva, std::uint32_t* tlsIndexRva, const std::function& progress) const { @@ -98,6 +113,7 @@ PeDirectory WindowsTlsBuilder::Build(const std::vector& source, co const Codegen::X64InstructionDecoder decoder; std::vector accesses; + std::vector> gsTraps; std::set branchTargets; const auto instructions = Relinker::CodeInstructionCollector().Collect(source, headers, [&](auto completed, auto total) { report(7000 * completed / total); }); std::uint64_t scanned = 0; @@ -127,6 +143,10 @@ PeDirectory WindowsTlsBuilder::Build(const std::vector& source, co branchTargets.insert(static_cast(target)); } + if (info.SegmentPrefix == 0x65 && !insidePrevious) { + gsTraps.emplace_back(rva, info.Length); + continue; + } if (info.SegmentPrefix != 0 && !(insidePrevious && info.SegmentPrefix != 0x64)) { const auto position = info.OpcodeOffset; bool hasOperandSizePrefix = false; @@ -192,6 +212,8 @@ PeDirectory WindowsTlsBuilder::Build(const std::vector& source, co } report(9000); + for (const auto& [rva, length] : gsTraps) + trapInstruction(sections, rva, length); if (tls == nullptr) { if (!accesses.empty()) throw Domain::RelinkerException("Guest TLS access without PT_TLS"); diff --git a/core/relinker/relinker/tests/test_tls_function_coverage.py b/core/relinker/relinker/tests/test_tls_function_coverage.py index 8061ee1638..371a3204e7 100644 --- a/core/relinker/relinker/tests/test_tls_function_coverage.py +++ b/core/relinker/relinker/tests/test_tls_function_coverage.py @@ -735,7 +735,6 @@ def main(): "rsp-displacement": fs_load(4, 40), "rsp-alu": fs_alu(0x33, 4, 40), "dword-load": bytes.fromhex("64 8b 04 25 28 00 00 00"), - "gs-load": bytes.fromhex("65 48 8b 04 25 28 00 00 00"), "rex-b-load": bytes.fromhex("64 49 8b 04 25 28 00 00 00"), "rsp-base-load": bytes.fromhex("64 48 8b 04 24"), "rsp-register-address": bytes.fromhex("64 48 8b 20"), From 7b9b91765d0a051c9c9ecc45df0262eed2e99d5b Mon Sep 17 00:00:00 2001 From: David Date: Thu, 15 Oct 2026 00:21:48 +0400 Subject: [PATCH 0157/1000] test(relinker): cover GS loads, stores and ALU reads trapped in Windows output Each form converts and is replaced by ud2 and NOP padding of its own length, leaving the next instruction in place. The forms are the ones Wine's ntdll in Prospero Win (PPSA99995) uses, plus a register-address form shorter than the jump an FS access is rewritten with. --- core/relinker/CMakeLists.txt | 1 + .../tests/test_tls_function_coverage.py | 18 +++++++++++++++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index 4660de2490..9839446231 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -414,6 +414,7 @@ if(BUILD_TESTING) foreach(opcode IN ITEMS add or adc sbb and sub xor cmp) list(APPEND tlsCoverageGroups alu_${opcode}) endforeach() + list(APPEND tlsCoverageGroups gs_traps) foreach(group IN LISTS tlsCoverageGroups) add_test(NAME tls_function_coverage_${group} COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_tls_function_coverage.py $ ${group}) diff --git a/core/relinker/relinker/tests/test_tls_function_coverage.py b/core/relinker/relinker/tests/test_tls_function_coverage.py index 371a3204e7..b36211185c 100644 --- a/core/relinker/relinker/tests/test_tls_function_coverage.py +++ b/core/relinker/relinker/tests/test_tls_function_coverage.py @@ -511,7 +511,7 @@ def add_alias(image, size, begin=0x1200): def main(): groups = ("metadata", "padding", "register_loads", "displacement_bounds", "alu_execution", - "register_address_loads", "moved_branch", "rejected") + "register_address_loads", "moved_branch", "rejected", "gs_traps") groups += tuple(f"displacement_r{register}" for register in range(16) if register != 4) groups += tuple(f"alu_{name}" for name in ALU_READ) parser = argparse.ArgumentParser(description="Run Windows guest TLS function coverage fixtures") @@ -751,6 +751,22 @@ def main(): conflicting[232:344] = unwind[232:344] struct.pack_into(" Date: Thu, 15 Oct 2026 00:22:32 +0400 Subject: [PATCH 0158/1000] test(relinker): trap a GS access in a module without a TLS segment The Windows TLS builder returns early for a module without PT_TLS. A GS access there must still be trapped; left in place it would read the host's thread environment block. --- .../relinker/tests/test_tls_function_coverage.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/core/relinker/relinker/tests/test_tls_function_coverage.py b/core/relinker/relinker/tests/test_tls_function_coverage.py index b36211185c..e0897cb6dc 100644 --- a/core/relinker/relinker/tests/test_tls_function_coverage.py +++ b/core/relinker/relinker/tests/test_tls_function_coverage.py @@ -767,6 +767,17 @@ def main(): assert result.returncode == 0, (name, result.stdout, result.stderr) patched = pe_bytes_at(output.read_bytes(), 0x11240, len(instruction) + 1) assert patched == b"\x0f\x0b" + b"\x90" * (len(instruction) - 2) + b"\xc3", (name, patched.hex()) + instruction = bytes.fromhex("65 48 8b 0c 25 78 03 00 00") + image = make_image("register", "unwind", body=instruction + b"\xc3") + struct.pack_into(" Date: Wed, 14 Oct 2026 23:55:36 +0300 Subject: [PATCH 0159/1000] feat(opus): decode at 8, 12, 16 and 24 kHz Accept every creation rate listed by the public decoder research in libSceOpusDec and libSceOpusCeltDec, resampling FFmpeg's 48 kHz output. Co-Authored-By: Claude Opus 5.5 --- .../prx/libSceOpusDec/include/OpusDecoder.hpp | 38 +++++++++++++-- core/libs/tests/GuestOpusCeltDec.cpp | 48 +++++++++++++++++++ core/libs/tests/GuestOpusDec.cpp | 21 +++++++- docs/dev/debt/libSceOpusDec.md | 3 +- 4 files changed, 104 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libSceOpusDec/include/OpusDecoder.hpp b/core/libs/prx/libSceOpusDec/include/OpusDecoder.hpp index 0e31fb9e0d..eef8d86d52 100644 --- a/core/libs/prx/libSceOpusDec/include/OpusDecoder.hpp +++ b/core/libs/prx/libSceOpusDec/include/OpusDecoder.hpp @@ -12,10 +12,12 @@ #include #include #include +#include #include extern "C" { #include +#include } namespace { @@ -54,13 +56,15 @@ public: int CreateEx(std::uint32_t* context, void* state, int sampleRate, int channels) { if (!context || !state) fail("null context or state"); GetSize(channels); - if (sampleRate != 48000) fail("sample rates other than 48000 Hz are not implemented"); + if (sampleRate != 8000 && sampleRate != 12000 && sampleRate != 16000 && sampleRate != 24000 && sampleRate != 48000) + fail("sample rates other than 8000, 12000, 16000, 24000 and 48000 Hz are not supported"); std::lock_guard lock(mutex); if (!contexts.contains(*context)) fail("unknown context"); if (decoders.contains(state)) fail("state already owns a decoder"); auto decoder = std::make_unique(); decoder->context = *context; decoder->channels = channels; + decoder->sampleRate = sampleRate; const auto* codec = avcodec_find_decoder(AV_CODEC_ID_OPUS); if (!codec) fail("FFmpeg Opus decoder unavailable"); decoder->codec = avcodec_alloc_context3(codec); @@ -68,6 +72,13 @@ public: av_channel_layout_default(&decoder->codec->ch_layout, channels); decoder->codec->sample_rate = 48000; if (avcodec_open2(decoder->codec, codec, nullptr) < 0) fail("cannot open FFmpeg decoder"); + if (sampleRate != 48000) { + AVChannelLayout layout{}; + av_channel_layout_default(&layout, channels); + if (swr_alloc_set_opts2(&decoder->resampler, &layout, AV_SAMPLE_FMT_FLT, sampleRate, &layout, AV_SAMPLE_FMT_FLT, 48000, 0, nullptr) < 0 || + swr_init(decoder->resampler) < 0) + fail("cannot open FFmpeg resampler"); + } decoders.emplace(state, std::move(decoder)); return 0; } @@ -80,7 +91,7 @@ public: if (found == decoders.end()) fail("unknown decoder state"); auto& decoder = *found->second; if (celtOnly && (data[0] >> 3) < 16) fail("SILK and hybrid packets are not implemented"); - const int samples = packetSamples(data, bytes); + const int samples = packetSamples(data, bytes) / (48000 / decoder.sampleRate); const int outputBytes = samples * decoder.channels * 2; if (capacity / 2 * 2 < outputBytes) fail("PCM buffer is too small"); std::unique_ptr packet(av_packet_alloc(), [](AVPacket* p) { av_packet_free(&p); }); @@ -93,12 +104,26 @@ public: if (frame->ch_layout.nb_channels != decoder.channels || frame->sample_rate != 48000 || (format != AV_SAMPLE_FMT_FLTP && format != AV_SAMPLE_FMT_FLT)) fail("unexpected FFmpeg PCM format"); + std::vector decoded; + decoded.reserve(static_cast(frame->nb_samples) * decoder.channels); for (int sample = 0; sample < frame->nb_samples; ++sample) { for (int channel = 0; channel < decoder.channels; ++channel) { const auto* source = reinterpret_cast(frame->extended_data[format == AV_SAMPLE_FMT_FLTP ? channel : 0]); - decoder.pending.push_back(source[format == AV_SAMPLE_FMT_FLTP ? sample : sample * decoder.channels + channel]); + decoded.push_back(source[format == AV_SAMPLE_FMT_FLTP ? sample : sample * decoder.channels + channel]); } } + if (decoder.resampler) { + const int capacitySamples = swr_get_out_samples(decoder.resampler, frame->nb_samples); + if (capacitySamples < 0) fail("FFmpeg resampler rejected the frame"); + std::vector resampled(static_cast(capacitySamples) * decoder.channels); + auto* output = reinterpret_cast(resampled.data()); + const auto* input = reinterpret_cast(decoded.data()); + const int converted = swr_convert(decoder.resampler, &output, capacitySamples, &input, frame->nb_samples); + if (converted < 0) fail("FFmpeg resampler rejected the frame"); + resampled.resize(static_cast(converted) * decoder.channels); + decoded = std::move(resampled); + } + decoder.pending.insert(decoder.pending.end(), decoded.begin(), decoded.end()); const auto count = static_cast(samples) * decoder.channels; if (decoder.pending.size() < count) decoder.pending.insert(decoder.pending.begin(), count - decoder.pending.size(), 0.0f); for (std::size_t index = 0; index < count; ++index) { @@ -119,9 +144,14 @@ private: struct Decoder { std::uint32_t context; int channels; + int sampleRate; AVCodecContext* codec = nullptr; + SwrContext* resampler = nullptr; std::vector pending; - ~Decoder() { avcodec_free_context(&codec); } + ~Decoder() { + swr_free(&resampler); + avcodec_free_context(&codec); + } }; const char* name; diff --git a/core/libs/tests/GuestOpusCeltDec.cpp b/core/libs/tests/GuestOpusCeltDec.cpp index 29c139a019..aa3572b7e1 100644 --- a/core/libs/tests/GuestOpusCeltDec.cpp +++ b/core/libs/tests/GuestOpusCeltDec.cpp @@ -43,6 +43,35 @@ const std::uint8_t CELT_STEREO[] = { const std::uint8_t HYBRID_PACKET[] = {0x7C, 0x87, 0xFC, 0xB1}; +const std::uint8_t CELT_TONE[4][60] = { + {0xFC, 0x9F, 0xF9, 0x33, 0x84, 0xFB, 0x41, 0x76, 0xA8, 0x21, 0xCE, 0x57, 0xBB, 0x16, 0x49, 0x05, 0x21, 0x3F, 0x2A, 0xC4, + 0x79, 0xD0, 0x73, 0x62, 0x89, 0x7E, 0xF9, 0x29, 0x75, 0x1E, 0xAC, 0x80, 0xAE, 0x30, 0xF1, 0x46, 0x18, 0x1F, 0xF8, 0x8B, + 0xD0, 0xC4, 0x37, 0xF1, 0x12, 0x47, 0x01, 0xF9, 0x43, 0xA4, 0x1A, 0x8C, 0x80, 0x40, 0xF0, 0x0A, 0xC3, 0xFD, 0x4C, 0xB1}, + {0xFC, 0x9E, 0xA4, 0xC8, 0xDF, 0x47, 0x28, 0xAE, 0x34, 0xFD, 0x32, 0x11, 0xB9, 0x69, 0x99, 0x7B, 0x61, 0xC6, 0x21, 0x94, + 0x0E, 0x20, 0xED, 0xC2, 0x95, 0x07, 0x78, 0x0D, 0x00, 0x92, 0x42, 0x12, 0xD4, 0xE9, 0xEC, 0x5E, 0xDC, 0xF4, 0xB9, 0x7A, + 0x2E, 0x23, 0xA1, 0xC6, 0x4C, 0xD7, 0x69, 0xEC, 0x34, 0x8F, 0x78, 0x21, 0x35, 0xF1, 0x1B, 0xB0, 0xF0, 0xF5, 0x5C, 0xD1}, + {0xFC, 0x9E, 0xA6, 0x12, 0xDC, 0x36, 0xC3, 0x4B, 0x2C, 0x3D, 0x85, 0x17, 0x32, 0xE1, 0x56, 0x62, 0xFE, 0x39, 0xDB, 0x85, + 0xFD, 0x83, 0x0D, 0x90, 0xF9, 0x32, 0xEB, 0xEB, 0x38, 0xFD, 0x3D, 0x96, 0x26, 0x21, 0xE6, 0x38, 0x6A, 0x8F, 0xDD, 0x1F, + 0x68, 0xBD, 0xAF, 0x4F, 0x9D, 0x08, 0x16, 0xED, 0x7B, 0x2A, 0x62, 0x4C, 0x7A, 0x85, 0xAB, 0x7B, 0x0F, 0x3D, 0x43, 0xD1}, + {0xFC, 0x7F, 0xDF, 0x09, 0x18, 0xD2, 0x71, 0xA1, 0xE6, 0x6C, 0x70, 0xED, 0xCA, 0x2F, 0x47, 0x79, 0xA7, 0xEA, 0xE3, 0xD1, + 0x9B, 0xCD, 0x9E, 0x87, 0xE5, 0xE0, 0x0A, 0x86, 0x0A, 0xEE, 0x8E, 0x63, 0xE4, 0x77, 0x45, 0xE9, 0xF0, 0x82, 0xF8, 0x2A, + 0x13, 0xD2, 0xDB, 0x17, 0x11, 0x2E, 0x36, 0x10, 0x03, 0x30, 0x20, 0x00, 0xF7, 0x42, 0xD8, 0x71, 0x7E, 0xB0, 0xF3, 0x3C}, +}; + +void RequireTone(const std::int16_t* pcm, int samples, int channels, int rate) { + for (int channel = 0; channel < channels; ++channel) { + int peak = 0, crossings = 0; + for (int sample = 0; sample < samples; ++sample) { + const int value = pcm[sample * channels + channel]; + peak = std::max(peak, std::abs(value)); + if (sample > 0 && (value < 0) != (pcm[(sample - 1) * channels + channel] < 0)) ++crossings; + } + Require(peak > 100 && peak < 200); + const int expected = samples * 2000 / rate; + Require(crossings >= expected - 4 && crossings <= expected + 4); + } +} + } // namespace int main() { @@ -77,6 +106,25 @@ int main() { offset += bytes + 2; } Require(audible); + + std::array unsupported{}; + for (int rate : {0, 22050, 44100, 96000}) + RequireThrows([&] { sceOpusCeltDecCreateEx(&context, unsupported.data(), rate, 2); }); + for (int rate : {8000, 12000, 16000, 24000, 48000}) { + std::array toneState{}; + Require(sceOpusCeltDecCreateEx(&context, toneState.data(), rate, 2) == 0); + const int samples = 960 * rate / 48000; + std::array tone{}; + for (int frame = 0; frame < 4; ++frame) { + tone.fill(12345); + RequireThrows([&] { sceOpusCeltDecDecode(toneState.data(), CELT_TONE[frame], 60, tone.data() + 1, samples * 4 - 1); }); + Require(sceOpusCeltDecDecode(toneState.data(), CELT_TONE[frame], 60, tone.data() + 1, samples * 4) == samples * 4); + Require(tone[0] == 12345 && tone[samples * 2 + 1] == 12345); + if (frame == 1 || frame == 2) RequireTone(tone.data() + 1, samples, 2, rate); + } + Require(sceOpusCeltDecDestroy(toneState.data()) == 0); + } + Require(sceOpusCeltDecDestroy(state.data()) == 0); RequireThrows([&] { sceOpusCeltDecDestroy(state.data()); }); RequireThrows([&] { sceOpusCeltDecDecode(state.data(), CELT_STEREO + 2, CELT_STEREO[0], pcm.data(), 3840); }); diff --git a/core/libs/tests/GuestOpusDec.cpp b/core/libs/tests/GuestOpusDec.cpp index 0b462ff31c..a5824ee855 100644 --- a/core/libs/tests/GuestOpusDec.cpp +++ b/core/libs/tests/GuestOpusDec.cpp @@ -62,7 +62,7 @@ int main() { RequireThrows([&] { sceOpusDecCreateEx(&context, state.data(), 48000, 2); }); RequireThrows([&] { sceOpusDecGetSize(3); }); std::array unsupported{}; - for (int rate : {8000, 12000, 16000, 24000}) + for (int rate : {0, 22050, 44100, 96000}) RequireThrows([&] { sceOpusDecCreateEx(&context, unsupported.data(), rate, 2); }); std::array pcm{}, reference{}; pcm.fill(12345); @@ -99,6 +99,25 @@ int main() { Require(std::any_of(mono.begin() + 1, mono.end() - 1, [](auto value) { return value != 0; })); offset += bytes + 2; } + for (int rate : {8000, 12000, 16000, 24000}) { + std::array rateState{}; + Require(sceOpusDecCreateEx(&context, rateState.data(), rate, 2) == 0); + const int samples = 960 * rate / 48000; + std::array resampled{}; + int peak = 0; + for (std::size_t at = 0, frame = 0; frame < 3; ++frame) { + const int bytes = OPUS_STEREO[at] | (OPUS_STEREO[at + 1] << 8); + resampled.fill(12345); + RequireThrows([&] { sceOpusDecDecode(rateState.data(), OPUS_STEREO + at + 2, bytes, resampled.data() + 1, samples * 4 - 1); }); + Require(sceOpusDecDecode(rateState.data(), OPUS_STEREO + at + 2, bytes, resampled.data() + 1, samples * 4) == samples * 4); + Require(resampled[0] == 12345 && resampled[samples * 2 + 1] == 12345); + for (int index = 1; index <= samples * 2; ++index) peak = std::max(peak, std::abs(static_cast(resampled[index]))); + at += bytes + 2; + } + Require(peak > 900 && peak < 1200); + Require(sceOpusDecDestroy(rateState.data()) == 0); + } + Require(sceOpusDecDestroy(state.data()) == 0); RequireThrows([&] { sceOpusDecDestroy(state.data()); }); RequireThrows([&] { sceOpusDecDecode(state.data(), OPUS_STEREO + 2, 70, pcm.data(), 3840); }); diff --git a/docs/dev/debt/libSceOpusDec.md b/docs/dev/debt/libSceOpusDec.md index 74c417e67e..9273851ee1 100644 --- a/docs/dev/debt/libSceOpusDec.md +++ b/docs/dev/debt/libSceOpusDec.md @@ -2,5 +2,6 @@ ### Functional -- [libSceOpusDec](../../../core/libs/prx/libSceOpusDec/Export.cpp) uses FFmpeg for 48000 Hz mono/stereo signed-16 decoding. The six signatures, 640-byte opaque state allocation and byte-count return follow [public PS5 decoder research](https://github.com/blackbearreloaded/ps5-hardware-audio-decoding/blob/26be5a12f92b2cdc67f8a9e082fd69ae0e9a35a2/docs/DECODING.md#direct-opus-abi) and [ProsperoRadio](https://github.com/blackbearreloaded/ProsperoRadio/blob/2c4029c3a8f43e9e1dfef98374d94d99b14d745d/src/opus_decoder.cpp). Console PCM equivalence and error codes are unverified. Other sample rates, packet loss concealment, invalid inputs and termination with live decoders throw. Context identifiers and host decoder state are private registries; the firmware's opaque state layout and error recovery are unknown. +- [libSceOpusDec](../../../core/libs/prx/libSceOpusDec/Export.cpp) uses FFmpeg for mono/stereo signed-16 decoding at the creation rates the research lists: 8000, 12000, 16000, 24000 and 48000 Hz. The six signatures, 640-byte opaque state allocation and byte-count return follow [public PS5 decoder research](https://github.com/blackbearreloaded/ps5-hardware-audio-decoding/blob/26be5a12f92b2cdc67f8a9e082fd69ae0e9a35a2/docs/DECODING.md#direct-opus-abi) and [ProsperoRadio](https://github.com/blackbearreloaded/ProsperoRadio/blob/2c4029c3a8f43e9e1dfef98374d94d99b14d745d/src/opus_decoder.cpp). Console PCM equivalence and error codes are unverified. Other sample rates, packet loss concealment, invalid inputs and termination with live decoders throw. Context identifiers and host decoder state are private registries; the firmware's opaque state layout and error recovery are unknown. - [sceOpusDecDecode](../../../core/libs/prx/libSceOpusDec/Export.cpp) (libSceOpusDec) returns exactly the packet's duration, as libopus does. FFmpeg resamples narrowband SILK from 8 kHz and holds back 24 samples of the first narrowband packet after the decoder starts or after a packet of another bandwidth, releasing them with the next packet that is not narrowband. The decoder keeps FFmpeg's output in order and pads the start with silence, so such a packet begins with 24 samples (0.5 ms) of silence and the output after it is 24 samples late; libopus has no such gap +- [sceOpusDecDecode](../../../core/libs/prx/libSceOpusDec/include/OpusDecoder.hpp) (libSceOpusDec, libSceOpusCeltDec) decodes at 48000 Hz and converts to a lower creation rate with FFmpeg's resampler, where libopus decodes at that rate directly. The resampler's filter delay is padded with silence at the start of the stream like the narrowband delay above, so the output is late by that delay and is not sample-identical to libopus; which of the two the console matches is not measured From 2df4c76f5b0e8cb6dc1926181202040cc0e291a9 Mon Sep 17 00:00:00 2001 From: MiniRML <134226498+MiniRML@users.noreply.github.com> Date: Wed, 14 Oct 2026 14:07:38 -0700 Subject: [PATCH 0160/1000] fix(tools): validate inner ELF metadata before unwrapping SELF --- tools/tests/test_unwrap_self.py | 22 ++++++++++++++++++++++ tools/unwrap_self.py | 4 ++++ 2 files changed, 26 insertions(+) diff --git a/tools/tests/test_unwrap_self.py b/tools/tests/test_unwrap_self.py index 44e1897ccb..7a966ab130 100644 --- a/tools/tests/test_unwrap_self.py +++ b/tools/tests/test_unwrap_self.py @@ -155,6 +155,28 @@ class UnwrapTest(unittest.TestCase): self.assertIn("no inner ELF header", result.stderr) self.assertFalse(output.exists()) + def test_incomplete_elf_metadata_is_refused_without_replacing_output(self): + truncated_header = bytearray(FIRST_REGION) + struct.pack_into(" len(data): + raise Refused("inner ELF header is truncated") header = bytearray(data[inner:inner + 0x40]) program_offset = struct.unpack_from(" len(data): + raise Refused("ELF program header table does not fit the file") headers = [] for index in range(count): offset = inner + program_offset + index * 56 From 4a9f7ac565013d8b5dc2833afa8145d10cac440b Mon Sep 17 00:00:00 2001 From: MiniRML <134226498+MiniRML@users.noreply.github.com> Date: Wed, 14 Oct 2026 14:11:35 -0700 Subject: [PATCH 0161/1000] fix(tools): preserve Unicode paths in NID CLI output --- tools/nid_names.py | 3 +++ tools/tests/test_nid_names.py | 27 +++++++++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/tools/nid_names.py b/tools/nid_names.py index 0df8c3f147..22e3e62304 100644 --- a/tools/nid_names.py +++ b/tools/nid_names.py @@ -220,4 +220,7 @@ def main(argv=None): if __name__ == "__main__": + for stream in (sys.stdout, sys.stderr): + if hasattr(stream, "reconfigure"): + stream.reconfigure(encoding="utf-8") sys.exit(main()) diff --git a/tools/tests/test_nid_names.py b/tools/tests/test_nid_names.py index 6dd4b20804..d1e8309e11 100644 --- a/tools/tests/test_nid_names.py +++ b/tools/tests/test_nid_names.py @@ -1,6 +1,8 @@ import contextlib import io import json +import os +import subprocess import tempfile import sys import unittest @@ -178,6 +180,31 @@ class SourceCollectionTests(unittest.TestCase): path.write_text(text, encoding="utf-8") return path + def cli(self, *arguments): + tools = self.root / "tools" + tools.mkdir(exist_ok=True) + tool = tools / "nid_names.py" + tool.write_bytes(Path(nid_names.__file__).read_bytes()) + return subprocess.run([sys.executable, str(tool), *map(str, arguments)], + env={**os.environ, "PYTHONIOENCODING": "cp1252"}, + capture_output=True, timeout=10) + + def test_report_preserves_unicode_paths_with_legacy_stdout(self): + self.write_source('APS5_EXPORT("AAAAAAAAAAA", exampleUnknown00);\n', "\u8cc7\u6599.cpp") + database = self.root / "aerolib.csv" + database.write_bytes(b"") + result = self.cli("--db", database) + self.assertEqual(result.returncode, 0, result.stderr.decode("utf-8", errors="replace")) + self.assertIn("core/libs/prx/libSceExample/\u8cc7\u6599.cpp", result.stdout.decode("utf-8")) + + def test_error_preserves_unicode_paths_with_legacy_stderr(self): + database = self.root / "\u8cc7\u6599" + database.mkdir() + result = self.cli("--db", database) + self.assertEqual(result.returncode, 2, result.stderr.decode("utf-8", errors="replace")) + self.assertIn("\u8cc7\u6599", result.stderr.decode("utf-8")) + self.assertNotIn(b"Traceback", result.stderr) + def test_comments_and_literals_do_not_create_export_records(self): self.write_source('''// APS5_EXPORT("AAAAAAAAAAA", exampleUnknown00); /* int APS5_VABI sceCommented() { return 0; } */ From ce0679c03b93e638ea9a000b9826fbc039afa392 Mon Sep 17 00:00:00 2001 From: fdfawef324 Date: Wed, 14 Oct 2026 16:49:34 -0400 Subject: [PATCH 0162/1000] fix(libkernel): use precise waits for select and poll on pipes on Windows On Windows, select over pipes polls readiness in HostPoll and sleeps std::this_thread::sleep_for(1 ms) between rounds, and poll with pipes but no native sockets sleeps sleep_for(slice). winpthreads ends these sleeps on 15.6 ms timer ticks (#1471), so a guest select or poll with a short timeout on an idle pipe waits about 15 ms per call instead of its timeout, and a longer wait checks the pipes only every 15.6 ms. Both paths now sleep with PreciseSleepUntil. Each sleep requests at most 1 ms and ends no later than the deadline; the old HostPoll sleep could pass its deadline by a whole tick. poll on pipes also stops spinning through the last partial millisecond before its deadline, where the remaining time truncated to 0 ms and no sleep was made. Infinite timeouts still sleep 1 ms per round. The pipes are now checked about every 1.5 ms instead of every 15.6 ms: a pipe that becomes ready is noticed sooner, and a thread blocked on an idle pipe wakes about ten times as often. A 1 ms poll on an idle pipe returns after about 1.5 ms instead of spinning for 1 ms. guest_poll times 20 polls with a 2 ms timeout on an empty pipe read end and guest_select times 20 selects with a 1 ms timeout on it; each set must take less than 200 ms. A 1 ms poll does not reach the old sleep, because the remaining time truncates to 0 ms and the old loop spins until the deadline, so the poll check uses 2 ms. On Windows Server 2025 both sets took 312 ms before this change and about 50 ms and 30 ms after it. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libkernel/Socket/src/Socket.cpp | 2 +- core/libs/prx/libkernel/Socket/src/Udp.cpp | 6 +++--- core/libs/tests/GuestPoll.cpp | 4 ++++ core/libs/tests/GuestSelect.cpp | 13 +++++++++++++ 4 files changed, 21 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libkernel/Socket/src/Socket.cpp b/core/libs/prx/libkernel/Socket/src/Socket.cpp index 8500256e25..c4adb38dfd 100644 --- a/core/libs/prx/libkernel/Socket/src/Socket.cpp +++ b/core/libs/prx/libkernel/Socket/src/Socket.cpp @@ -110,7 +110,7 @@ int HostPoll(KernelSocketPoll::Entry* descriptors, int count, int timeoutMillise if (entry.revents != 0) ++ready; } if (ready > 0 || std::chrono::steady_clock::now() >= deadline) return ready; - std::this_thread::sleep_for(std::chrono::milliseconds(1)); + PreciseSleepUntil(std::min(deadline, std::chrono::steady_clock::now() + std::chrono::milliseconds(1))); } } diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index 2c9d15da2a..1612614e75 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -42,7 +42,6 @@ #include #include #include -#include #include extern "C" int* APS5_VABI __error_nid_postfix(); @@ -1370,8 +1369,9 @@ int APS5_VABI poll_nid_postfix(GuestPollDescriptor* descriptors, std::uint32_t c if (!native.empty()) { for (auto& request : native) request.revents = 0; if (WSAPoll(native.data(), static_cast(native.size()), slice) < 0) return Fail(NativeError()); - } else if (slice > 0) { - std::this_thread::sleep_for(std::chrono::milliseconds(slice)); + } else if (!ready && !pipeReady) { + const auto wake = std::chrono::steady_clock::now() + std::chrono::milliseconds(1); + PreciseSleepUntil(timeout < 0 ? wake : std::min(deadline, wake)); } int socketReady = 0; for (const auto& request : native) diff --git a/core/libs/tests/GuestPoll.cpp b/core/libs/tests/GuestPoll.cpp index 1738f11dd8..f96ae8f616 100644 --- a/core/libs/tests/GuestPoll.cpp +++ b/core/libs/tests/GuestPoll.cpp @@ -64,6 +64,10 @@ static void TestPipes(int socket) { const auto start = std::chrono::steady_clock::now(); Require(poll_nid_postfix(&reader, 1, 50) == 0 && reader.revents == 0); Require(std::chrono::steady_clock::now() - start >= std::chrono::milliseconds(40)); + const auto shortWaitStart = std::chrono::steady_clock::now(); + for (int i = 0; i < 20; ++i) Require(poll_nid_postfix(&reader, 1, 2) == 0 && reader.revents == 0); + const auto shortWait = std::chrono::steady_clock::now() - shortWaitStart; + Require(shortWait >= std::chrono::milliseconds(40) && shortWait < std::chrono::milliseconds(200)); Require(write_nid_postfix(ends[1], "wake", 4) == 4); Require(poll_nid_postfix(&reader, 1, 1000) == 1 && reader.revents == (In | ReadNormal)); PollDescriptor writer{ends[1], In, -1}; diff --git a/core/libs/tests/GuestSelect.cpp b/core/libs/tests/GuestSelect.cpp index f5eddd3d84..466d204c8c 100644 --- a/core/libs/tests/GuestSelect.cpp +++ b/core/libs/tests/GuestSelect.cpp @@ -59,6 +59,19 @@ int main() { Require(!read.Has(reader)); } + { + const GuestTimeval limit{0, 1000}; + const auto start = std::chrono::steady_clock::now(); + for (int i = 0; i < 20; ++i) { + GuestDescriptorSet read{}; + read.Add(reader); + Require(select_nid_postfix(reader + 1, read.words.data(), nullptr, nullptr, &limit) == 0); + Require(!read.Has(reader)); + } + const auto elapsed = std::chrono::steady_clock::now() - start; + Require(elapsed >= std::chrono::milliseconds(20) && elapsed < std::chrono::milliseconds(200)); + } + { const char byte = 's'; Require(write_nid_postfix(writer, &byte, 1) == 1); From eb8146b7342a05b14dc43bcce5173c18f209ab3e Mon Sep 17 00:00:00 2001 From: Ondra97 <47064087+Ondra97@users.noreply.github.com> Date: Wed, 14 Oct 2026 15:24:45 +0200 Subject: [PATCH 0163/1000] fix(libc): skip guest exit handlers when closing the host window --- core/libs/prx/libSceSystemService/CMakeLists.txt | 6 ++++-- core/libs/prx/libc/src/Process.cpp | 2 +- core/libs/tests/GuestSystemExit.cpp | 9 +++++++++ 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceSystemService/CMakeLists.txt b/core/libs/prx/libSceSystemService/CMakeLists.txt index d7ff11174c..ca57051164 100644 --- a/core/libs/prx/libSceSystemService/CMakeLists.txt +++ b/core/libs/prx/libSceSystemService/CMakeLists.txt @@ -34,14 +34,16 @@ if(BUILD_TESTING) configure_windows_unwind(guest_system_exit_tests) add_test(NAME guest_system_exit_parameters COMMAND guest_system_exit_tests) add_test(NAME guest_system_exit COMMAND guest_system_exit_tests --exit) + add_test(NAME guest_requested_exit COMMAND guest_system_exit_tests --requested-exit-driver) add_test(NAME guest_quick_exit COMMAND guest_system_exit_tests --quick-exit) add_test(NAME guest_immediate_exit COMMAND guest_system_exit_tests --immediate) add_test(NAME guest_return_from_main COMMAND guest_system_exit_tests --return-from-main) add_test(NAME guest_system_relaunch COMMAND guest_system_exit_tests --relaunch-driver) add_test(NAME guest_nested_exit COMMAND guest_system_exit_tests --nested-exit-driver) - set_tests_properties(guest_system_exit PROPERTIES + set_tests_properties(guest_system_exit guest_requested_exit PROPERTIES PASS_REGULAR_EXPRESSION "Guest shutdown completed" FAIL_REGULAR_EXPRESSION "Guest exit handler ran|Guest thread resumed during exit") + set_tests_properties(guest_requested_exit PROPERTIES TIMEOUT 10) set_tests_properties(guest_quick_exit PROPERTIES PASS_REGULAR_EXPRESSION "Guest quick_exit handlers completed") set_tests_properties(guest_system_relaunch PROPERTIES @@ -49,7 +51,7 @@ if(BUILD_TESTING) set_tests_properties(guest_nested_exit PROPERTIES PASS_REGULAR_EXPRESSION "Guest nested exit completed" TIMEOUT 30) if(WIN32) - set_tests_properties(guest_system_exit guest_system_exit_parameters guest_immediate_exit guest_return_from_main guest_quick_exit guest_system_relaunch guest_nested_exit PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(guest_system_exit guest_requested_exit guest_system_exit_parameters guest_immediate_exit guest_return_from_main guest_quick_exit guest_system_relaunch guest_nested_exit PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() add_test_executable(guest_system_service_tests ${LIBS_INCLUDE_DIR}/tests/GuestSystemService.cpp) diff --git a/core/libs/prx/libc/src/Process.cpp b/core/libs/prx/libc/src/Process.cpp index d4bca7d3f2..78b4e6195e 100644 --- a/core/libs/prx/libc/src/Process.cpp +++ b/core/libs/prx/libc/src/Process.cpp @@ -52,7 +52,7 @@ extern "C" void LibcRequestShutdown_nid_postfix() { extern "C" void LibcRequestExit_nid_postfix(int code) { if (exitRequested.exchange(true)) return; LibcRequestShutdown_nid_postfix(); - std::thread([code] { LibcExit_nid_no_patch(code); }).detach(); + std::thread([code] { LibcExitWithoutHandlers_nid_no_patch(code); }).detach(); } extern "C" [[noreturn]] void LibcAwaitExit_nid_postfix() { diff --git a/core/libs/tests/GuestSystemExit.cpp b/core/libs/tests/GuestSystemExit.cpp index 5242a51ef2..73c20d67c0 100644 --- a/core/libs/tests/GuestSystemExit.cpp +++ b/core/libs/tests/GuestSystemExit.cpp @@ -159,6 +159,10 @@ int DriveRelaunch() { } int main(int argc, char** argv) { const int firstArgument = argc > 0 && std::strncmp(argv[0], "--", 2) == 0 ? 0 : 1; + if (argc > firstArgument && std::strcmp(argv[firstArgument], "--requested-exit-driver") == 0) { + Require(RunChild(OwnExecutable(), "--requested-exit") == 0); + return 0; + } if (argc > firstArgument && std::strcmp(argv[firstArgument], "--immediate") == 0) { LibcRegisterShutdown_nid_postfix(UnexpectedCleanup); Require(std::atexit(UnexpectedCleanup) == 0); @@ -217,6 +221,11 @@ int main(int argc, char** argv) { LibcRegisterShutdown_nid_postfix(Cleanup); Require(__cxa_atexit_nid_postfix(UnexpectedFinalizer, nullptr, nullptr) == 0); Require(std::atexit(ExitHandlerRan) == 0); + if (std::strcmp(argv[firstArgument], "--requested-exit") == 0) { + LibcRequestExit_nid_postfix(0); + LibcRequestExit_nid_postfix(7); + LibcAwaitExit_nid_postfix(); + } sceSystemServiceLoadExec("exit", nullptr); return 2; } From 7e624831a952398ecbee921a63cda28c35c64a44 Mon Sep 17 00:00:00 2001 From: Yossi Yadgar Date: Wed, 14 Oct 2026 11:17:21 +0300 Subject: [PATCH 0164/1000] fix(agc): refuse last-vertex strips only for reads of a vertex slot With fragmentShaderBarycentric (RADV, NVIDIA), v_interp_p1_f32 and v_interp_p2_f32 are translated as P0 + I * P10 + J * P20 over the per-vertex values, so every smooth input became per-vertex. A triangle strip drawn with PROVOKING_VTX_LAST then threw "last provoking vertex with explicit per-vertex triangle-strip interpolation is unsupported" for any shader that interpolates, which stops OliSe Player (PPSA01153) and RetroArch's ES-DE frontend (PPSA99169), both ps5-opengl titles whose shaders use only v_interp_p1/p2. I and J come from gl_BaryCoordKHR[1] and [2], in the same vertex order as the pervertexEXT values, so the result is b0 * v0 + b1 * v1 + b2 * v2 whatever order the device gives an odd strip triangle. Only a shader that reads a slot itself depends on that order. The translator now records the inputs read through v_interp_mov_f32 or 16-bit interpolation, FragmentParameter carries that as vertexOrder, and the strip check refuses those alone. 16-bit interpolation would cancel out the same way but has not been checked, so it stays refused. --- core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp | 2 +- .../IntermediateRepresentation/IrMetadata/ProgramMetadata.hpp | 1 + core/shader/recompiler/Recompiler.hpp | 1 + core/shader/recompiler/ShaderDiskCache.cpp | 4 +++- .../SpirvBackend/src/SpirvMemory/SpirvInputOutput.cpp | 4 +++- .../recompiler/Translation/src/AttributeInstructions.cpp | 2 ++ 6 files changed, 11 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp index 7ef9781948..5c3d6d636c 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp @@ -110,7 +110,7 @@ void ValidateProvokingVertex(const Context& context, const State& state, std::sp if (shader.stage != ShaderRecompiler::ShaderStage::Fragment) continue; Require(shader.program != nullptr, "missing compiled fragment shader"); Require(std::none_of(shader.program->fragmentParameters.begin(), shader.program->fragmentParameters.end(), [](const ShaderRecompiler::FragmentParameter& parameter) { return parameter.flat && parameter.perVertex; }), "last provoking vertex with explicit per-vertex flat interpolation is unsupported"); - Require(state.topology != VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP || std::none_of(shader.program->fragmentParameters.begin(), shader.program->fragmentParameters.end(), [](const ShaderRecompiler::FragmentParameter& parameter) { return parameter.perVertex; }), "last provoking vertex with explicit per-vertex triangle-strip interpolation is unsupported"); + Require(state.topology != VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP || std::none_of(shader.program->fragmentParameters.begin(), shader.program->fragmentParameters.end(), [](const ShaderRecompiler::FragmentParameter& parameter) { return parameter.vertexOrder; }), "last provoking vertex with explicit per-vertex triangle-strip interpolation is unsupported"); } } diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ProgramMetadata.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ProgramMetadata.hpp index 3a5e28dd46..95c47deccf 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ProgramMetadata.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ProgramMetadata.hpp @@ -19,6 +19,7 @@ struct IrProgramMetadata { std::vector exportInfo; std::uint32_t pixelLinearInputs = 0; std::uint32_t pixelPerspectiveInputs = 0; + std::uint32_t pixelVertexOrderInputs = 0; bool barycentricEmulation = false; bool invalidatesCaches = false; std::vector dynamicReads; diff --git a/core/shader/recompiler/Recompiler.hpp b/core/shader/recompiler/Recompiler.hpp index e3c14236fd..3c5fd0fb45 100644 --- a/core/shader/recompiler/Recompiler.hpp +++ b/core/shader/recompiler/Recompiler.hpp @@ -432,6 +432,7 @@ struct FragmentParameter { bool perVertex; bool custom = false; bool flatP10 = false; + bool vertexOrder = false; }; struct BarycentricEmulation { diff --git a/core/shader/recompiler/ShaderDiskCache.cpp b/core/shader/recompiler/ShaderDiskCache.cpp index 91309260d9..409b6c376c 100644 --- a/core/shader/recompiler/ShaderDiskCache.cpp +++ b/core/shader/recompiler/ShaderDiskCache.cpp @@ -90,7 +90,7 @@ static_assert(sizeof(RecompileResult) == 472, "RecompileResult changed: update E static_assert(sizeof(DescriptorBinding) == 592, "DescriptorBinding changed: update the binding encoder"); static_assert(sizeof(VertexAttribute) == 32, "VertexAttribute changed: update the attribute encoder"); static_assert(sizeof(VertexInput) == 16, "VertexInput changed: update the vertex input encoder"); -static_assert(sizeof(FragmentParameter) == 12, "FragmentParameter changed: update the parameter encoder"); +static_assert(sizeof(FragmentParameter) == 16, "FragmentParameter changed: update the parameter encoder"); static_assert(sizeof(CompiledShaderInfo) == 352, "CompiledShaderInfo changed: update the info encoder"); static_assert(sizeof(ShaderInfo) == 232, "ShaderInfo changed: update the info encoder"); static_assert(sizeof(BufferResource) == 32, "BufferResource changed: update the info encoder"); @@ -494,6 +494,7 @@ void encodeArtifact(Writer& writer, const CompiledShaderArtifact& result) { out.Value(parameter.perVertex); out.Value(parameter.custom); out.Value(parameter.flatP10); + out.Value(parameter.vertexOrder); }); writer.Value(result.barycentricEmulation.active); writer.Value(result.barycentricEmulation.smooth); @@ -539,6 +540,7 @@ void decodeArtifact(Reader& reader, CompiledShaderArtifact& result) { in.Value(parameter.perVertex); in.Value(parameter.custom); in.Value(parameter.flatP10); + in.Value(parameter.vertexOrder); }); reader.Value(result.barycentricEmulation.active); reader.Value(result.barycentricEmulation.smooth); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvInputOutput.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvInputOutput.cpp index a7a465d86a..849e954bbe 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvInputOutput.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvInputOutput.cpp @@ -61,12 +61,14 @@ namespace ShaderRecompiler const auto location = PixelInputLocation(pixel, input.location); const bool flat = PixelInputIsFlat(pixel, input.location); const bool flatP10 = flat && !input.perVertex && input.location < readsP10.size() && readsP10[input.location]; + const bool vertexOrder = input.perVertex && (input.location >= 32u || (program.Metadata().pixelVertexOrderInputs >> input.location & 1u) != 0u); const auto existing = std::find_if(result.begin(), result.end(), [&](const auto& parameter) { return parameter.location == location; }); if (existing != result.end()) { existing->flatP10 = existing->flatP10 || flatP10; + existing->vertexOrder = existing->vertexOrder || vertexOrder; continue; } - result.push_back({location, location, flat, input.perVertex, PixelInputIsCustom(pixel, input.location), flatP10}); + result.push_back({location, location, flat, input.perVertex, PixelInputIsCustom(pixel, input.location), flatP10, vertexOrder}); } return result; } diff --git a/core/shader/recompiler/Translation/src/AttributeInstructions.cpp b/core/shader/recompiler/Translation/src/AttributeInstructions.cpp index f2878ed016..44588aa9f7 100644 --- a/core/shader/recompiler/Translation/src/AttributeInstructions.cpp +++ b/core/shader/recompiler/Translation/src/AttributeInstructions.cpp @@ -93,6 +93,7 @@ void TranslationContext::vInterpMovF32(const RdnaInstruction& inst) { if (inst.source0.value >= 3u) { throw std::runtime_error("v_interp_mov_f32 mode is reserved"); } + if (inst.source1.value < 32u) program.Metadata().pixelVertexOrderInputs |= 1u << inst.source1.value; IrValue& value = ir.Emit(IrOpcode::GetInterpolationParameter, IrType::U32, {&ir.Constant(inst.source1.value), &ir.Constant(inst.source2.value), &ir.Constant(inst.source0.value)}); writeOperand(inst.destination, &value); } @@ -104,6 +105,7 @@ IrF32 TranslationContext::interpolationParameterF16(const RdnaInstruction& inst, if (pixelInput == nullptr || !pixelInput->InputIsFp16(inst.source1.value)) { throw std::runtime_error("pixel input " + std::to_string(inst.source1.value) + " is read with 16-bit interpolation without FP16_INTERP_MODE"); } + if (inst.source1.value < 32u) program.Metadata().pixelVertexOrderInputs |= 1u << inst.source1.value; return IrF32(ir.Emit(IrOpcode::GetInterpolationParameterF16, IrType::F32, {&ir.Constant(inst.source1.value), &ir.Constant(inst.source2.value), &ir.Constant(mode), &ir.Constant(inst.source1.opSel ? 1u : 0u)})); } From e8ab547b2cfcf7506697e486a7dece916ed3cafd Mon Sep 17 00:00:00 2001 From: Yossi Yadgar Date: Wed, 14 Oct 2026 11:17:21 +0300 Subject: [PATCH 0165/1000] test(agc): check the strip guard on interpolated and slot-read inputs A pixel shader that interpolates its input with v_interp_p1/p2 compiles to a per-vertex input without vertexOrder and passes a last-vertex strip; the same input read through v_interp_mov P10 is vertexOrder and still throws on a strip, not on a list. The synthetic matrix gets vertexOrder as a third axis. The first check fails with the guard on perVertex, the second without the v_interp_mov record. --- .../prx/libSceAgcDriver/tests/Graphics.cpp | 37 ++++++++++++++----- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 27661c9e92..4f3f0c6264 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -5416,6 +5416,20 @@ void ProvokingVertexTests() { const std::array flatP10Shaders{{{ShaderRecompiler::ShaderStage::Fragment, &flatP10Pixel, 0}}}; ValidateProvokingVertex(supported, last, flatP10Shaders); expectFailure([&] { ValidateProvokingVertex(supported, first, flatP10Shaders); }, "v_interp_mov p10 of a flat input with the first provoking vertex is not measured"); + auto lastStrip = last; + lastStrip.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP; + auto lastList = last; + lastList.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + const std::array smoothColor{0xc8100000u, 0xc8110001u, 0xc8140100u, 0xc8150101u, 0xc8180200u, 0xc8190201u, 0xc81c0300u, 0xc81d0301u, 0xf800180fu, 0x07060504u, 0xbf810000u}; + const auto smoothPixel = recompilePixel({0x0u}, smoothColor); + Require(smoothPixel.fragmentParameters.size() == 1 && smoothPixel.fragmentParameters[0].perVertex && !smoothPixel.fragmentParameters[0].vertexOrder, "v_interp_p1/p2 of a smooth input was recorded as reading a vertex slot"); + const std::array smoothShaders{{{ShaderRecompiler::ShaderStage::Fragment, &smoothPixel, 0}}}; + ValidateProvokingVertex(supported, lastStrip, smoothShaders); + const auto slotPixel = recompilePixel({0x0u}, flatP10Color); + Require(slotPixel.fragmentParameters.size() == 1 && slotPixel.fragmentParameters[0].perVertex && slotPixel.fragmentParameters[0].vertexOrder, "v_interp_mov P10 of a smooth input was not recorded as reading a vertex slot"); + const std::array slotShaders{{{ShaderRecompiler::ShaderStage::Fragment, &slotPixel, 0}}}; + expectFailure([&] { ValidateProvokingVertex(supported, lastStrip, slotShaders); }, "last provoking vertex with explicit per-vertex triangle-strip interpolation is unsupported"); + ValidateProvokingVertex(supported, lastList, slotShaders); ShaderRecompiler::RecompileResult fragment{}; const std::array shaders{{{ShaderRecompiler::ShaderStage::Fragment, &fragment, 0}}}; for (const auto topology : {VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST, VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP, VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN}) { @@ -5423,16 +5437,19 @@ void ProvokingVertexTests() { state.topology = topology; for (const auto flat : {false, true}) { for (const auto perVertex : {false, true}) { - fragment.fragmentParameters = {{0, 0, flat, perVertex}}; - auto firstState = state; - firstState.provokingVertexMode = VK_PROVOKING_VERTEX_MODE_FIRST_VERTEX_EXT; - ValidateProvokingVertex(unsupported, firstState, shaders); - if (flat && perVertex) { - expectFailure([&] { ValidateProvokingVertex(supported, state, shaders); }, "last provoking vertex with explicit per-vertex flat interpolation is unsupported"); - } else if (perVertex && topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP) { - expectFailure([&] { ValidateProvokingVertex(supported, state, shaders); }, "last provoking vertex with explicit per-vertex triangle-strip interpolation is unsupported"); - } else { - ValidateProvokingVertex(supported, state, shaders); + for (const auto vertexOrder : {false, true}) { + if (vertexOrder && !perVertex) continue; + fragment.fragmentParameters = {{0, 0, flat, perVertex, false, false, vertexOrder}}; + auto firstState = state; + firstState.provokingVertexMode = VK_PROVOKING_VERTEX_MODE_FIRST_VERTEX_EXT; + ValidateProvokingVertex(unsupported, firstState, shaders); + if (flat && perVertex) { + expectFailure([&] { ValidateProvokingVertex(supported, state, shaders); }, "last provoking vertex with explicit per-vertex flat interpolation is unsupported"); + } else if (vertexOrder && topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP) { + expectFailure([&] { ValidateProvokingVertex(supported, state, shaders); }, "last provoking vertex with explicit per-vertex triangle-strip interpolation is unsupported"); + } else { + ValidateProvokingVertex(supported, state, shaders); + } } } } From e4d0e2dc77a84deabe9b180ccff5c5abeb038165 Mon Sep 17 00:00:00 2001 From: Yossi Yadgar Date: Wed, 14 Oct 2026 11:17:21 +0300 Subject: [PATCH 0166/1000] docs(debt): narrow the last-vertex strip entry to vertex slot reads --- docs/dev/debt/libSceAgcDriver.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 534ce7d828..de8fbdfc64 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -76,7 +76,7 @@ - The [dispatches that follow a compute queue's wait](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchAhead.cpp) (libSceAgcDriver), up to the next wait that memory does not satisfy, a packet that writes guest memory other than a label, or a conditional packet, are resolved (memory captured, shader recompiled) when the wait passes, as the CP reads them right after it; the graphics queue is exempt (`APS5_GROUP_CAPTURE_ALL_QUEUES=1` includes it). The device still reads guest memory when the dispatch is issued; when that memory no longer matches the capture, the dispatch is resolved again from current memory. - [PA_CL_CLIP_CNTL](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - `DX_LINEAR_ATTR_CLIP_ENA` (bit 24) clear is drawn like bit 24 set, with Vulkan's clipping of `NoPerspective` outputs; what a clear bit changes on the console is undocumented. - [10_11_11 unorm color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) render into R32_UINT attachments: the pixel shader packs each export, rounded to nearest even like format-30 image stores, which is assumed to match the CB's conversion. -- [PA_SU_SC_MODE_CNTL.PROVOKING_VTX_LAST](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp) (libSceAgcDriver) uses Vulkan's last-vertex mode for native primitives. Bit 19 follows [opengnm's `flatshade_first` mapping](https://github.com/blackbearreloaded/ps5-opengl/blob/db752da4c89380af228497b5b6852163a4e61069/src/gallium/ps5/ps5_screen.c#L2244); vertex selection follows [Vulkan](https://docs.vulkan.org/spec/latest/chapters/drawing.html#drawing-primitive-topologies), without console measurements. Last mode throws without `VK_EXT_provoking_vertex`, for generated rectangle, mesh or tessellation primitives, explicit per-vertex flat inputs, and triangle strips with per-vertex inputs, whose [ordering depends on the device](https://docs.vulkan.org/refpages/latest/refpages/source/VkPhysicalDeviceFragmentShaderBarycentricPropertiesKHR.html). A FLAT_SHADE input without pass-through that is read through `v_interp_mov_f32` P10 and not P20 [returns the provoking vertex's value](../../../core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp), like P0. ps5-opengl reads every flat input that way, and its driver passes the GL 4.6 CTS on a PS5 (#1413). The lane itself was not measured on the console, so with the first provoking vertex such a shader throws. +- [PA_SU_SC_MODE_CNTL.PROVOKING_VTX_LAST](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp) (libSceAgcDriver) uses Vulkan's last-vertex mode for native primitives. Bit 19 follows [opengnm's `flatshade_first` mapping](https://github.com/blackbearreloaded/ps5-opengl/blob/db752da4c89380af228497b5b6852163a4e61069/src/gallium/ps5/ps5_screen.c#L2244); vertex selection follows [Vulkan](https://docs.vulkan.org/spec/latest/chapters/drawing.html#drawing-primitive-topologies), without console measurements. Last mode throws without `VK_EXT_provoking_vertex`, for generated rectangle, mesh or tessellation primitives, explicit per-vertex flat inputs, and triangle strips whose pixel shader reads a vertex slot itself (`v_interp_mov_f32`, or 16-bit interpolation, which is not checked yet), since the vertex order of odd strip triangles [depends on the device](https://docs.vulkan.org/refpages/latest/refpages/source/VkPhysicalDeviceFragmentShaderBarycentricPropertiesKHR.html). `v_interp_p1_f32` and `v_interp_p2_f32` read P10 and P20 against the guest's I and J, which come from `gl_BaryCoordKHR` in the same vertex order as the per-vertex values, so the order cancels out and strips with only those reads draw. A FLAT_SHADE input without pass-through that is read through `v_interp_mov_f32` P10 and not P20 [returns the provoking vertex's value](../../../core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp), like P0. ps5-opengl reads every flat input that way, and its driver passes the GL 4.6 CTS on a PS5 (#1413). The lane itself was not measured on the console, so with the first provoking vertex such a shader throws. - [Dual-source blending](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) takes the MRT1 export as the second source of the MRT0 blend, as RADV and radeonsi program it; what the CB does with other export or target layouts is unknown, so they throw. - [DB_SHADER_CONTROL.CONSERVATIVE_Z_EXPORT](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp) (libSceAgcDriver) becomes `DepthLess`/`DepthGreater`. For a pixel shader that breaks the promise, the host driver decides whether its interpolated Z is depth-tested before shading; what the console does then was not measured. - [DB_SHADER_CONTROL.STENCIL_TEST_VAL_EXPORT_ENABLE](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) becomes `FragStencilRefEXT` when available, taken from the second MRTZ channel of the 32_GR and 32_ABGR formats as Mesa's AMD drivers write it ([`emit_ps_mrtz_export`](https://gitlab.freedesktop.org/mesa/mesa/-/blob/3e32b0e1f132231b7a972c1b0b5b06c7e16cf8b9/src/amd/common/nir/ac_nir_lower_ps_late.c#L194)); the console was not measured. The host value is the test value of both faces and what `REPLACE_TEST` writes, so a stencil operation that writes all ones or the operation value throws with it, as do a stencil clear draw, the UINT16_ABGR layout and `STENCIL_OP_VAL_EXPORT_ENABLE`. A device without the extension uses the bounded replay described above. From 70c080458a3b5ffeb7b6b49c2af29f31842855c8 Mon Sep 17 00:00:00 2001 From: notaxeltv Date: Wed, 14 Oct 2026 23:21:34 +0200 Subject: [PATCH 0167/1000] feat(libc): raise logic_error::_Doraise and _Xruntime_error Co-authored-by: Cursor --- core/libs/prx/libc/CMakeLists.txt | 1 + .../libc/src/exception/StandardExceptions.cpp | 5 ++++ core/libs/tests/GuestMessageDestructorAbi.cpp | 25 +++++++++++++++++-- 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libc/CMakeLists.txt b/core/libs/prx/libc/CMakeLists.txt index 02162709a5..5f0aed79d5 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -714,6 +714,7 @@ if(MINGW) target_link_libraries(guest_message_destructor_abi_tests PRIVATE libc) configure_windows_unwind(guest_message_destructor_abi_tests) foreach(mode IN ITEMS control decoy zero release regex-decoy regex-zero + runtime-decoy runtime-zero doraise-decoy doraise-zero unshared-decoy unshared-zero regex-unshared-decoy regex-unshared-zero) add_test(NAME guest_message_destructor_${mode} COMMAND guest_message_destructor_abi_tests ${mode}) set_tests_properties(guest_message_destructor_${mode} PROPERTIES TIMEOUT 10 diff --git a/core/libs/prx/libc/src/exception/StandardExceptions.cpp b/core/libs/prx/libc/src/exception/StandardExceptions.cpp index fdaca18500..61d5ee879a 100644 --- a/core/libs/prx/libc/src/exception/StandardExceptions.cpp +++ b/core/libs/prx/libc/src/exception/StandardExceptions.cpp @@ -344,6 +344,10 @@ void APS5_VABI _ZNSt11logic_errorC2EPKc_nid_postfix(LibcException::ExceptionObje void APS5_VABI _ZNSt11logic_errorC1ERKS__nid_postfix(LibcException::ExceptionObject* self, const LibcException::ExceptionObject* source) { LibcException::Copy(self, source, _ZTVSt11logic_error_nid_postfix); } void APS5_VABI _ZNSt11logic_errorC2ERKS__nid_postfix(LibcException::ExceptionObject* self, const LibcException::ExceptionObject* source) { LibcException::Copy(self, source, _ZTVSt11logic_error_nid_postfix); } LibcException::ExceptionObject* APS5_VABI _ZNSt11logic_erroraSERKS__nid_postfix(LibcException::ExceptionObject* self, const LibcException::ExceptionObject* source) { return LibcException::Assign(self, source); } +[[noreturn]] void APS5_VABI _ZNKSt11logic_error8_DoraiseEv_nid_postfix(const LibcException::ExceptionObject* self) { + if (!self) throw std::invalid_argument("exception object is null"); + LibcException::ThrowMessage(_ZTVSt11logic_error_nid_postfix, LibcException::MessageWhat(self)); +} LibcException::TypeRecord _ZTISt13runtime_error_nid_postfix {LibcException::SingleTypeVtable + 2, "St13runtime_error", &_ZTISt9exception_nid_postfix}; LibcException::ExceptionVtable _ZTVSt13runtime_error_nid_postfix {0, &_ZTISt13runtime_error_nid_postfix, LibcException::GuestDestroyMessage_nid_no_patch, LibcException::GuestDeleteMessage_nid_no_patch, LibcException::GuestMessageWhat_nid_no_patch}; @@ -459,6 +463,7 @@ const char* APS5_VABI _ZNKSt8ios_base7failure4whatEv_nid_postfix(const LibcExcep [[noreturn]] void APS5_VABI _ZSt14_Xout_of_rangePKc_nid_postfix(const char* message) { LibcException::ThrowMessage(_ZTVSt12out_of_range_nid_postfix, message); } [[noreturn]] void APS5_VABI _ZSt14_Xlength_errorPKc_nid_postfix(const char* message) { LibcException::ThrowMessage(_ZTVSt12length_error_nid_postfix, message); } [[noreturn]] void APS5_VABI _ZSt18_Xinvalid_argumentPKc_nid_postfix(const char* message) { LibcException::ThrowMessage(_ZTVSt16invalid_argument_nid_postfix, message); } +[[noreturn]] void APS5_VABI _ZSt15_Xruntime_errorPKc_nid_postfix(const char* message) { LibcException::ThrowMessage(_ZTVSt13runtime_error_nid_postfix, message); } [[noreturn]] void APS5_VABI _ZSt13_Xrange_errorPKc_nid_postfix(const char* message) { LibcException::ThrowMessage(_ZTVSt11range_error_nid_postfix, message); } [[noreturn]] void APS5_VABI _ZSt19_Xbad_function_callv_nid_postfix() { LibcException::ThrowPlain(_ZTVSt17bad_function_call_nid_postfix); } [[noreturn]] void APS5_VABI _ZNSt8__sce_v219_Xbad_function_callEv_nid_postfix() { LibcException::ThrowPlain(_ZTVSt17bad_function_call_nid_postfix); } diff --git a/core/libs/tests/GuestMessageDestructorAbi.cpp b/core/libs/tests/GuestMessageDestructorAbi.cpp index edf1e8cd41..68e398ef4d 100644 --- a/core/libs/tests/GuestMessageDestructorAbi.cpp +++ b/core/libs/tests/GuestMessageDestructorAbi.cpp @@ -20,10 +20,16 @@ extern const unsigned char _ZTVN10__cxxabiv117__class_type_infoE_nid_postfix[]; extern const unsigned char _ZTVN10__cxxabiv120__si_class_type_infoE_nid_postfix[]; extern const unsigned char _ZTISt12out_of_range_nid_postfix[]; extern const unsigned char _ZTISt11regex_error_nid_postfix[]; +extern const unsigned char _ZTISt13runtime_error_nid_postfix[]; +extern const unsigned char _ZTISt11logic_error_nid_postfix[]; void APS5_VABI _ZNSt12out_of_rangeC1EPKc_nid_postfix(Object*, const char*); void APS5_VABI _ZNSt12out_of_rangeC1ERKS__nid_postfix(Object*, const Object*); void APS5_VABI _ZNSt12out_of_rangeD1Ev_nid_postfix(Object*); +void APS5_VABI _ZNSt11logic_errorC1EPKc_nid_postfix(Object*, const char*); +void APS5_VABI _ZNSt11logic_errorD1Ev_nid_postfix(Object*); [[noreturn]] void APS5_VABI _ZSt14_Xout_of_rangePKc_nid_postfix(const char*); +[[noreturn]] void APS5_VABI _ZSt15_Xruntime_errorPKc_nid_postfix(const char*); +[[noreturn]] void APS5_VABI _ZNKSt11logic_error8_DoraiseEv_nid_postfix(const Object*); [[noreturn]] void APS5_VABI _ZSt13_Xregex_errorNSt15regex_constants10error_typeE_nid_postfix(std::regex_constants::error_type); int APS5_VABI GuestOuter(); void APS5_VABI GuestStdCapture(); @@ -206,25 +212,40 @@ int main(int argc, char** argv) { return 0; } const bool regex = std::strstr(argv[1], "regex") != nullptr; + const bool runtime = std::strstr(argv[1], "runtime") != nullptr; + const bool doraise = std::strstr(argv[1], "doraise") != nullptr; regularRelease = std::strcmp(argv[1], "release") == 0; keepMessage = std::strstr(argv[1], "unshared") == nullptr; FixtureArgument = reinterpret_cast(FixtureMessage); + Object raised{}; if (regex) { expectedMessage = "regular expression error"; FixtureStdOutType = _ZTISt11regex_error_nid_postfix; FixtureThrow = reinterpret_cast(_ZSt13_Xregex_errorNSt15regex_constants10error_typeE_nid_postfix); FixtureArgument = static_cast(std::regex_constants::error_collate); + } else if (runtime) { + expectedMessage = FixtureMessage; + FixtureStdOutType = _ZTISt13runtime_error_nid_postfix; + FixtureThrow = reinterpret_cast(_ZSt15_Xruntime_errorPKc_nid_postfix); + } else if (doraise) { + _ZNSt11logic_errorC1EPKc_nid_postfix(&raised, FixtureMessage); + expectedMessage = FixtureMessage; + FixtureStdOutType = _ZTISt11logic_error_nid_postfix; + FixtureThrow = reinterpret_cast(_ZNKSt11logic_error8_DoraiseEv_nid_postfix); + FixtureArgument = reinterpret_cast(&raised); } _ZNSt12out_of_rangeC1EPKc_nid_postfix(&decoy, "unrelated message"); poison = std::strstr(argv[1], "zero") ? nullptr : &decoy; GuestStdCapture(); + const char* expected = regex ? "regular expression error" : FixtureMessage; if (callbacks != (regularRelease ? 0u : 1u) || (keepMessage && (MessageHeader(retained)->references.load() != 0 || - std::strcmp(retained.message, regex ? "regular expression error" : FixtureMessage))) || + std::strcmp(retained.message, expected))) || !decoy.message || std::strcmp(decoy.message, "unrelated message")) return 44; _ZNSt12out_of_rangeD1Ev_nid_postfix(&retained); _ZNSt12out_of_rangeD1Ev_nid_postfix(&decoy); - if (retained.message || decoy.message) return 45; + if (doraise) _ZNSt11logic_errorD1Ev_nid_postfix(&raised); + if (retained.message || decoy.message || (doraise && raised.message)) return 45; std::puts("guest message callback, retained ownership and release: PASS"); return 0; } From 99f7f6fe06ce6661df824fd462f5a8e797f21198 Mon Sep 17 00:00:00 2001 From: MiniRML <134226498+MiniRML@users.noreply.github.com> Date: Wed, 14 Oct 2026 14:22:43 -0700 Subject: [PATCH 0168/1000] fix(tools): reject empty hardware oracle input files --- tools/hw-oracle/hw_oracle.py | 2 ++ tools/hw-oracle/test_hw_oracle.py | 22 ++++++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/tools/hw-oracle/hw_oracle.py b/tools/hw-oracle/hw_oracle.py index 8424ab09bf..a564666f20 100644 --- a/tools/hw-oracle/hw_oracle.py +++ b/tools/hw-oracle/hw_oracle.py @@ -152,6 +152,8 @@ def main(): parser.add_argument("--json", action="store_true", help="emit inputs, results and run settings as JSON; target is the assembly target, not a GPU model or driver") args = parser.parse_args() rows = [tuple(parse_word(v) for v in line.split()) for line in args.rows.read_text().splitlines() if line.strip()] + if not rows: + parser.error("rows file must contain at least one input row") if any(len(r) != 4 for r in rows): sys.exit("every row needs 4 values") extra = args.extra.read_bytes() if args.extra else b"" diff --git a/tools/hw-oracle/test_hw_oracle.py b/tools/hw-oracle/test_hw_oracle.py index 31fa5240e0..90f2d0b49c 100644 --- a/tools/hw-oracle/test_hw_oracle.py +++ b/tools/hw-oracle/test_hw_oracle.py @@ -87,6 +87,28 @@ class InputRowsTests(unittest.TestCase): hw_oracle.run("s_nop 0", [row], **MODES) assemble.assert_not_called() + def test_cli_rejects_empty_rows_before_running_tools(self): + with tempfile.TemporaryDirectory() as tmp: + body, rows = Path(tmp) / "body.s", Path(tmp) / "rows.txt" + body.write_text("s_nop 0", encoding="utf-8") + argv = ["hw_oracle.py", str(body), str(rows), "--json"] + argv += [part for name, value in MODES.items() + for part in ["--" + name.replace("_", "-"), str(value)]] + for contents in ("", "\n \t\n"): + with self.subTest(contents=contents): + rows.write_text(contents, encoding="utf-8") + with patch("sys.argv", argv), patch.object(hw_oracle, "run", return_value=[]) as run, \ + patch.object(hw_oracle, "target", return_value="gfx1030") as target, \ + contextlib.redirect_stdout(io.StringIO()) as output, \ + contextlib.redirect_stderr(io.StringIO()) as errors, \ + self.assertRaises(SystemExit) as exited: + hw_oracle.main() + self.assertEqual(exited.exception.code, 2) + self.assertIn("at least one input row", errors.getvalue()) + self.assertEqual(output.getvalue(), "") + run.assert_not_called() + target.assert_not_called() + def test_unsigned_bits_and_indexable_values_are_preserved(self): class Indexable: def __index__(self): From 875a159f6467618385e0ec2ac97646863c332856 Mon Sep 17 00:00:00 2001 From: Zinebyte <197974009+Zinebyte@users.noreply.github.com> Date: Wed, 14 Oct 2026 17:24:28 -0400 Subject: [PATCH 0169/1000] fix(relinker): preserve weak imports in Linux guest modules --- .../src/linux/LinuxGuestModuleWriter.cpp | 1 - .../relinker/tests/test_weak_imports.py | 134 ++++++++++++++++++ 2 files changed, 134 insertions(+), 1 deletion(-) diff --git a/core/relinker/elfpatcher/src/linux/LinuxGuestModuleWriter.cpp b/core/relinker/elfpatcher/src/linux/LinuxGuestModuleWriter.cpp index 76598e35ae..084d6fc564 100644 --- a/core/relinker/elfpatcher/src/linux/LinuxGuestModuleWriter.cpp +++ b/core/relinker/elfpatcher/src/linux/LinuxGuestModuleWriter.cpp @@ -55,7 +55,6 @@ std::vector GuestModuleWriter::WriteLinux(const Relinker::GuestIma Io::WriteU32(symbols, index * 24, addString("__tls_get_addr")); needsTlsResolver = true; } - if (symbol.Section == 0 && (symbol.Info >> 4) == 2) symbols[index * 24 + 4] = static_cast(0x10 | (symbol.Info & 15)); } std::vector needed; for (const auto& dependency : dependencies) needed.push_back(addString(dependency)); diff --git a/core/relinker/relinker/tests/test_weak_imports.py b/core/relinker/relinker/tests/test_weak_imports.py index 5dffdc29f2..c02ad7b54d 100644 --- a/core/relinker/relinker/tests/test_weak_imports.py +++ b/core/relinker/relinker/tests/test_weak_imports.py @@ -9,12 +9,134 @@ from pathlib import Path from test_macos_output import MachO from test_windows_import_modules import executable, provider +from test_guest_intel_trampolines import main_fixture +from test_guest_module_directories import needed_libraries +from test_guest_symbol_names import dynamic_tags, file_offset from import_fixture import fixture as macos_fixture from nid import nid SHT_DYNSYM = 11 STB_GLOBAL = 1 STB_WEAK = 2 +OPTIONAL = 'AAAAAAAAAAA' +REQUIRED = 'BBBBBBBBBBB' +ENTRY = 'ProbeOptional' +HOST = 'libWeakHost.prx' + + +def module_fixture(symbols, code, dependency=None, relocations=()): + image = bytearray(0x3000) + image[:16] = b'\x7fELF\x02\x01\x01' + bytes(9) + struct.pack_into('> 4, info & 15) + return attributes + + +def load_weak_module(path, symbol, libraries): + return subprocess.run([sys.executable, __file__, '--load-weak-module', str(path), symbol], + env={**os.environ, 'LD_LIBRARY_PATH': str(libraries)}, + capture_output=True, text=True, timeout=10) + + +def check_guest_weak_imports(relinker, work): + linux = sys.platform == 'linux' and os.uname().machine == 'x86_64' + for name, binding, optional, required, expected, missing in ( + ('weak-missing', STB_WEAK, False, True, 42, None), + ('weak-present', STB_WEAK, True, True, 77, None), + ('required-missing', STB_WEAK, True, False, None, REQUIRED), + ('global-missing', STB_GLOBAL, False, True, None, OPTIONAL)): + case = work / ('guest-' + name) + modules, libraries = case / 'sce_module', case / 'libs' + modules.mkdir(parents=True) + libraries.mkdir() + raw = modules / 'optional.prx' + raw.write_bytes(weak_guest_module(binding)) + (libraries / HOST).write_bytes(weak_host_provider(optional, required)) + source, output = case / 'input.elf', case / 'output.elf' + source.write_bytes(main_fixture()) + result = subprocess.run([str(relinker), str(source), str(output)], + capture_output=True, text=True, timeout=30) + assert result.returncode == 0, (name, result.stdout, result.stderr) + converted = case / 'app0/sce_module/optional.prx.guest.prx' + image = converted.read_bytes() + attributes = module_import_attributes(image) + assert attributes.get(OPTIONAL) == (binding, 2), (name, attributes) + assert attributes.get(REQUIRED) == (STB_GLOBAL, 2), (name, attributes) + assert needed_libraries(image) == [HOST], (name, needed_libraries(image)) + if linux: + for module, symbol in ((raw, ENTRY), (converted, ENTRY + '#guest')): + run = load_weak_module(module, symbol, libraries) + if expected is None: + assert run.returncode != 0 and 'undefined symbol: ' + missing in run.stderr, \ + (name, module, run.returncode, run.stdout, run.stderr) + else: + assert run.returncode == 0 and run.stdout.strip() == str(expected), \ + (name, module, run.returncode, run.stdout, run.stderr) + if name == 'weak-missing': + (libraries / HOST).unlink() + run = load_weak_module(converted, ENTRY + '#guest', libraries) + assert run.returncode != 0 and HOST in run.stderr and 'cannot open shared object file' in run.stderr, \ + (name, run.returncode, run.stdout, run.stderr) def weak_executable(owner, symbol): @@ -54,6 +176,17 @@ def macho_bind_opcodes(data): def main(): + if sys.argv[1] == '--load-weak-module': + import ctypes + try: + module = ctypes.CDLL(sys.argv[2], mode=os.RTLD_NOW | os.RTLD_LOCAL) + entry = getattr(module, sys.argv[3]) + entry.restype = ctypes.c_int + print(entry()) + except OSError as error: + print(error, file=sys.stderr) + raise SystemExit(1) + return relinker = Path(sys.argv[1]).resolve() with tempfile.TemporaryDirectory(prefix='anyps5-weak-imports-') as directory: work = Path(directory) @@ -96,6 +229,7 @@ def main(): assert result.returncode == 0, (name, result.stdout, result.stderr) opcodes = macho_bind_opcodes(output.read_bytes()) assert opcodes == expected, (name, opcodes) + check_guest_weak_imports(relinker, work) print('Weak import tests passed') From f7b2ebd8b434ee3eff691b0db777a31d44cdf937 Mon Sep 17 00:00:00 2001 From: Nahid Delic Date: Wed, 14 Oct 2026 23:29:15 +0200 Subject: [PATCH 0170/1000] fix(shader): allow 32 sampler pairs per shader A pixel shader of MXGP 26 (PPSA30839) uses 17 samplers, over the limit of 16 sampler pairs. The sampler heap now holds 64 descriptors, ShaderData grows by 32 sampler metadata records and the runtime ABI version moves to 13. Co-Authored-By: Claude Opus 5.5 --- .../prx/libSceAgcDriver/tests/ShaderDeviceProfile.cpp | 1 + core/shader/recompiler/RuntimeAbi.hpp | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderDeviceProfile.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderDeviceProfile.cpp index 1143e50b0d..b6377648fb 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderDeviceProfile.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderDeviceProfile.cpp @@ -142,6 +142,7 @@ void CheckHeaps() { const auto single = allocate(image, 1u); const auto sixteenSamplers = allocate(image, 1u, 16u); Require(BindingAllocator{}.FindBinding(sixteenSamplers.layout, DescriptorBindingKind::Samplers).resources.size() == 32u, "sixteen logical samplers must fit with both descriptor variants"); + Require(BindingAllocator{}.FindBinding(allocate(image, 1u, 17u).layout, DescriptorBindingKind::Samplers).resources.size() == 34u, "seventeen logical samplers must fit with both descriptor variants"); const auto full = allocate(image, RuntimeAbi::SampledHeapCapacity, RuntimeAbi::SamplerHeapCapacity / 2u); Require(single.layout.ShaderDataDwords() == full.layout.ShaderDataDwords() && single.layout.memoryOffsetDword == full.layout.memoryOffsetDword && !full.layout.UsesPushData(), "runtime layout depends on resource count"); Require(full.layout.memoryOffsetDword == 0u && full.layout.DispatchThreadLimitDword() == 0u && full.layout.ShaderDataDwords() == 0u, "direct image resources allocated runtime metadata"); diff --git a/core/shader/recompiler/RuntimeAbi.hpp b/core/shader/recompiler/RuntimeAbi.hpp index 99358baf0d..ce3489c14f 100644 --- a/core/shader/recompiler/RuntimeAbi.hpp +++ b/core/shader/recompiler/RuntimeAbi.hpp @@ -9,7 +9,7 @@ namespace ShaderRecompiler::RuntimeAbi { -inline constexpr std::uint32_t Version = 12u; +inline constexpr std::uint32_t Version = 13u; inline constexpr std::uint32_t DescriptorSet = 0u; inline constexpr std::uint32_t FragmentDescriptorSet = 1u; inline constexpr std::uint32_t StageCount = 4u; @@ -42,7 +42,7 @@ inline constexpr std::uint32_t SampledHeapCapacity = 64u; inline constexpr std::uint32_t BindlessTableSlots = 16u; inline constexpr std::uint32_t StorageMipSlots = 4u; inline constexpr std::uint32_t StorageHeapCapacity = 16u; -inline constexpr std::uint32_t SamplerHeapCapacity = 32u; +inline constexpr std::uint32_t SamplerHeapCapacity = 64u; inline constexpr std::uint32_t StateExportMappings = 0u; inline constexpr std::uint32_t StateDwords = 2u; inline constexpr std::uint32_t StateVertexSelectors = 0u; @@ -87,9 +87,9 @@ inline std::uint32_t HeapCapacity(Binding binding) { } static_assert(std::is_standard_layout_v && std::is_trivially_copyable_v && sizeof(ResourceMetadata) == 48u); -static_assert(std::is_standard_layout_v && std::is_trivially_copyable_v && sizeof(ShaderData) == 14528u); +static_assert(std::is_standard_layout_v && std::is_trivially_copyable_v && sizeof(ShaderData) == 16064u); static_assert(UserDataDword == 4u && BufferOffsetsDword == 132u && DispatchThreadLimitDword == 164u); -static_assert(ExportMappingsDword == 3624u); +static_assert(ExportMappingsDword == 4008u); static_assert(offsetof(ResourceMetadata, descriptor) == 16u && offsetof(ShaderData, images) == 672u && offsetof(ShaderData, samplers) == 12960u); inline void RequireVersion(std::uint32_t version) { From 0103e485d232b4072b2d8e9549cb17c06a2a1e28 Mon Sep 17 00:00:00 2001 From: MiniRML <134226498+MiniRML@users.noreply.github.com> Date: Wed, 14 Oct 2026 14:00:09 -0700 Subject: [PATCH 0171/1000] fix(tools): encode filenames in PR overlap links --- tools/pr_overlap.py | 4 ++-- tools/tests/test_pr_overlap.py | 10 +++++++++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/tools/pr_overlap.py b/tools/pr_overlap.py index 2e4c4950fd..7a7dfb2663 100644 --- a/tools/pr_overlap.py +++ b/tools/pr_overlap.py @@ -5,7 +5,7 @@ import re import subprocess from itertools import combinations from pathlib import Path -from urllib.parse import urlencode +from urllib.parse import quote, urlencode PARENT = 'if .parent then "\\(.parent.owner.login)/\\(.parent.name)" else .nameWithOwner end' EXPORT = re.compile(r"^\+.*\bAPS5_VABI\s+(\w+)\s*\(") @@ -176,7 +176,7 @@ def cell(items): def spans(conflicts, blob): - return "
".join(f"`{path}` " + ", ".join(f"[L{s}-{e}]({blob}/{path}#L{s}-L{e})" if s != e else f"[L{s}]({blob}/{path}#L{s})" + return "
".join(f"`{path}` " + ", ".join(f"[L{s}-{e}]({blob}/{quote(path)}#L{s}-L{e})" if s != e else f"[L{s}]({blob}/{quote(path)}#L{s})" for s, e in ranges) for path, ranges in sorted((conflicts or {}).items())) diff --git a/tools/tests/test_pr_overlap.py b/tools/tests/test_pr_overlap.py index 575b7ac4b8..6177acea1a 100644 --- a/tools/tests/test_pr_overlap.py +++ b/tools/tests/test_pr_overlap.py @@ -6,6 +6,7 @@ import sys import unittest from pathlib import Path from unittest.mock import patch +from urllib.parse import unquote, urlsplit sys.path.insert(0, str(Path(__file__).resolve().parents[1])) import pr_overlap @@ -132,7 +133,7 @@ class UnicodePathTests(unittest.TestCase): "-c", "commit.gpgsign=false", *args], cwd=root, capture_output=True, text=True, encoding="utf-8", check=True).stdout.strip() - filename = "core/libs/prx/libSceExample/caf\u00e9.cpp" + filename = "core/libs/prx/libSceExample/caf\u00e9 #1 (%20).cpp" source = root / filename source.parent.mkdir(parents=True) source.write_text("int value = 0;\n", encoding="utf-8") @@ -155,6 +156,13 @@ class UnicodePathTests(unittest.TestCase): self.assertEqual(left["files"], {filename}) self.assertEqual(right["files"], {filename}) self.assertEqual(pr_overlap.conflicts(left, right), {filename: [(1, 1)]}) + blob = f"https://github.com/owner/project/blob/{base}" + message = pr_overlap.report({7: {"conflicts": pr_overlap.conflicts(left, right), "added": set()}}, blob) + link = message.split("[L1](", 1)[1].split(")", 1)[0] + parsed = urlsplit(link) + self.assertEqual(unquote(parsed.path), f"/owner/project/blob/{base}/{filename}") + self.assertEqual(parsed.fragment, "L1") + self.assertEqual(parsed.query, "") finally: os.chdir(previous) From 0f0aecea85d599b1967d0f928413ba4a7f2ba1d2 Mon Sep 17 00:00:00 2001 From: MiniRML <134226498+MiniRML@users.noreply.github.com> Date: Wed, 14 Oct 2026 14:28:11 -0700 Subject: [PATCH 0172/1000] fix(tools): cache oracle targets per visible device selection --- tools/hw-oracle/hw_oracle.py | 12 ++++++++--- tools/hw-oracle/test_hw_oracle.py | 33 +++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/tools/hw-oracle/hw_oracle.py b/tools/hw-oracle/hw_oracle.py index 8424ab09bf..81b6180277 100644 --- a/tools/hw-oracle/hw_oracle.py +++ b/tools/hw-oracle/hw_oracle.py @@ -65,12 +65,18 @@ def oracle(): @cache -def detected_target(binary): - return subprocess.run([binary, "--target"], capture_output=True, text=True, check=True).stdout.strip() +def detected_target(binary, visible_devices): + environment = dict(os.environ) + if visible_devices is None: + environment.pop("ROCR_VISIBLE_DEVICES", None) + else: + environment["ROCR_VISIBLE_DEVICES"] = visible_devices + return subprocess.run([binary, "--target"], capture_output=True, text=True, check=True, + env=environment).stdout.strip() def target(): - return os.environ.get("HW_ORACLE_TARGET") or detected_target(oracle()) + return os.environ.get("HW_ORACLE_TARGET") or detected_target(oracle(), os.environ.get("ROCR_VISIBLE_DEVICES")) def assemble(body, work, wave64, *, ieee, denorm32, denorm16, dx10_clamp, round32, round16, fp16_overflow, lds=DEFAULT_LDS): diff --git a/tools/hw-oracle/test_hw_oracle.py b/tools/hw-oracle/test_hw_oracle.py index 31fa5240e0..b59c6b62a9 100644 --- a/tools/hw-oracle/test_hw_oracle.py +++ b/tools/hw-oracle/test_hw_oracle.py @@ -279,6 +279,39 @@ class TargetOverrideTests(unittest.TestCase): with patch.dict(os.environ, {"HW_ORACLE_TARGET": "gfx1100"}): self.assertEqual(hw_oracle.target(), "gfx1100") + def test_device_selection_uses_its_own_cached_target(self): + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + runner = root / "probe.py" + runner.write_text( + "import os\nfrom pathlib import Path\n" + "visible = os.environ.get('ROCR_VISIBLE_DEVICES', 'unset')\n" + "with Path(__file__).with_name('probes.txt').open('a', encoding='utf-8') as log:\n" + " log.write(visible + '\\n')\n" + "print({'0': 'gfx1030', '1': 'gfx1100', 'unset': 'gfx900'}[visible])\n", + encoding="utf-8", newline="\n") + binary = root / ("oracle.cmd" if os.name == "nt" else "oracle") + if os.name == "nt": + binary.write_text(f'@"{sys.executable}" "{runner}" %*\n', encoding="utf-8") + else: + binary.write_text(f'#!/bin/sh\nexec {shlex.quote(sys.executable)} {shlex.quote(str(runner))} "$@"\n', + encoding="utf-8") + binary.chmod(0o755) + with patch.object(hw_oracle, "oracle", return_value=binary), \ + patch.dict(os.environ, {"HW_ORACLE_TARGET": ""}): + os.environ["ROCR_VISIBLE_DEVICES"] = "0" + self.assertEqual(hw_oracle.detected_target(binary, "1"), "gfx1100") + for visible, expected in (("0", "gfx1030"), ("0", "gfx1030"), + ("1", "gfx1100"), ("1", "gfx1100"), + ("0", "gfx1030"), (None, "gfx900"), (None, "gfx900")): + if visible is None: + os.environ.pop("ROCR_VISIBLE_DEVICES", None) + else: + os.environ["ROCR_VISIBLE_DEVICES"] = visible + self.assertEqual(hw_oracle.target(), expected) + self.assertEqual((root / "probes.txt").read_text(encoding="utf-8").splitlines(), + ["1", "0", "unset"]) + def test_removing_override_restores_cached_detection_for_that_binary(self): reply = subprocess.CompletedProcess([], 0, "gfx1030\n", "") with patch.object(hw_oracle, "oracle", return_value=Path("oracle-a")), \ From 241053ad9f470b7e536ad942da8b907c6f9ba334 Mon Sep 17 00:00:00 2001 From: Ondra97 <47064087+Ondra97@users.noreply.github.com> Date: Wed, 14 Oct 2026 23:34:42 +0200 Subject: [PATCH 0173/1000] fix(libkernel): preserve condition signals across cancellation --- .../prx/libkernel/Pthread/include/Pthread.hpp | 3 +- core/libs/prx/libkernel/Pthread/src/Cond.cpp | 59 +++--- .../libs/tests/GuestPthreadCancelRecovery.cpp | 186 ++++++++++++++++++ docs/dev/debt/libkernel.md | 2 +- 4 files changed, 223 insertions(+), 27 deletions(-) diff --git a/core/libs/prx/libkernel/Pthread/include/Pthread.hpp b/core/libs/prx/libkernel/Pthread/include/Pthread.hpp index e22426757f..6b1c2c0ff1 100644 --- a/core/libs/prx/libkernel/Pthread/include/Pthread.hpp +++ b/core/libs/prx/libkernel/Pthread/include/Pthread.hpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -61,7 +62,7 @@ struct PthreadCondattrPrivate { }; struct PthreadCondPrivate { - TimedWait::Condition _cv; + std::shared_ptr _cv = std::make_shared(); int _clockid = 0; }; diff --git a/core/libs/prx/libkernel/Pthread/src/Cond.cpp b/core/libs/prx/libkernel/Pthread/src/Cond.cpp index d576c64ff0..c177d2a5ad 100644 --- a/core/libs/prx/libkernel/Pthread/src/Cond.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Cond.cpp @@ -2,6 +2,7 @@ #include "prx/libkernel/Pthread/include/Mutex.hpp" #include "prx/libkernel/Pthread/include/Cond.hpp" #include "prx/libkernel/Pthread/include/Cancel.hpp" +#include "prx/libkernel/Pthread/include/ThreadLifecycle.hpp" #include "prx/libkernel/Pthread/Posix/Common.hpp" #include "prx/libkernel/KernelErrors.hpp" #include "prx/libkernel/Time/include/Time.hpp" @@ -13,6 +14,7 @@ #include #include #include +#include namespace { @@ -31,21 +33,20 @@ PthreadMutex adaptiveInitializer() { return reinterpret_cast(std::uintptr_t{1}); } -PthreadCond resolveCond(PthreadCond* cond) { +std::shared_ptr resolveCond(PthreadCond* cond, int* clockId = nullptr) { if (!cond) throw std::invalid_argument("Condition variable pointer is null"); - std::atomic_ref slot(*cond); - if (const auto current = slot.load(std::memory_order_acquire); current && current != destroyedCond()) - return current; std::lock_guard lock(condInitializationMutex); - const auto current = slot.load(std::memory_order_acquire); + std::atomic_ref slot(*cond); + auto current = slot.load(std::memory_order_acquire); if (current == destroyedCond()) throw std::runtime_error("Condition variable has been destroyed"); - if (current) - return current; - auto* created = new PthreadCondPrivate(); - slot.store(created, std::memory_order_release); - return created; + if (!current) { + current = new PthreadCondPrivate(); + slot.store(current, std::memory_order_release); + } + if (clockId) *clockId = current->_clockid; + return current->_cv; } int mutexOwnership(PthreadMutex* mutex, PthreadMutex* owned) { @@ -73,8 +74,16 @@ struct WakingLock { } }; -int waitUntil(PthreadCond* cond, PthreadMutex* mutex, std::optional deadlineNanos, const void* caller) { - auto* c = resolveCond(cond); +int finishWait(std::shared_ptr& condition, bool timedOut) { + if (ThreadCancel::Requested()) { + condition->NotifyOne(); + condition.reset(); + scePthreadExit(GUEST_CANCELED); + } + return timedOut ? sceTimedOut : 0; +} + +int waitUntil(std::shared_ptr condition, PthreadMutex* mutex, std::optional deadlineNanos, const void* caller) { PthreadMutex m = nullptr; if (const int error = mutexOwnership(mutex, &m)) return error; @@ -92,32 +101,32 @@ int waitUntil(PthreadCond* cond, PthreadMutex* mutex, std::optional_owner.store(std::thread::id{}, std::memory_order_release); for (int level = 1; level < previousCount; ++level) m->_rmtx.unlock(); - timedOut = !ThreadCancel::Wait(c->_cv, lock, deadlineNanos); + timedOut = !ThreadCancel::Wait(*condition, lock, deadlineNanos); for (int level = 1; level < previousCount; ++level) m->_rmtx.lock(); m->_owner.store(std::this_thread::get_id(), std::memory_order_release); m->_count = previousCount; lock.release(); - ThreadCancel::Check(); - return timedOut ? sceTimedOut : 0; + return finishWait(condition, timedOut); } WakingLock waking{m, m->_mtx}; std::unique_lock lock(waking, std::adopt_lock); m->_owner.store(std::thread::id{}, std::memory_order_release); - timedOut = !ThreadCancel::Wait(c->_cv, lock, deadlineNanos); + timedOut = !ThreadCancel::Wait(*condition, lock, deadlineNanos); m->_owner.store(std::this_thread::get_id(), std::memory_order_release); lock.release(); - ThreadCancel::Check(); - return timedOut ? sceTimedOut : 0; + return finishWait(condition, timedOut); } } int CondOperations::AbsoluteTimedwait(PthreadCond* cond, PthreadMutex* mutex, const KernelTimespec* abstime) { KernelUseconds usec = 0; - if (!PosixThread::RelativeMicroseconds(resolveCond(cond)->_clockid, abstime, &usec)) + int clockId = 0; + auto condition = resolveCond(cond, &clockId); + if (!PosixThread::RelativeMicroseconds(clockId, abstime, &usec)) throw std::invalid_argument("Invalid absolute condition variable timeout"); - return waitUntil(cond, mutex, TimedWait::DeadlineNanos(usec), __builtin_return_address(0)); + return waitUntil(std::move(condition), mutex, TimedWait::DeadlineNanos(usec), __builtin_return_address(0)); } extern "C" { @@ -169,27 +178,27 @@ int APS5_VABI scePthreadCondDestroy(PthreadCond* cond) { } int APS5_VABI scePthreadCondSignal(PthreadCond* cond) { - resolveCond(cond)->_cv.NotifyOne(); + resolveCond(cond)->NotifyOne(); return 0; } int APS5_VABI scePthreadCondBroadcast(PthreadCond* cond) { - resolveCond(cond)->_cv.NotifyAll(); + resolveCond(cond)->NotifyAll(); return 0; } int APS5_VABI scePthreadCondSignalto(PthreadCond* cond, Pthread thread) { (void)thread; - resolveCond(cond)->_cv.NotifyAll(); + resolveCond(cond)->NotifyAll(); return 0; } int APS5_VABI scePthreadCondWait(PthreadCond* cond, PthreadMutex* mutex) { - return waitUntil(cond, mutex, std::nullopt, __builtin_return_address(0)); + return waitUntil(resolveCond(cond), mutex, std::nullopt, __builtin_return_address(0)); } int APS5_VABI scePthreadCondTimedwait(PthreadCond* cond, PthreadMutex* mutex, KernelUseconds usec) { - return waitUntil(cond, mutex, TimedWait::DeadlineNanos(usec), __builtin_return_address(0)); + return waitUntil(resolveCond(cond), mutex, TimedWait::DeadlineNanos(usec), __builtin_return_address(0)); } } diff --git a/core/libs/tests/GuestPthreadCancelRecovery.cpp b/core/libs/tests/GuestPthreadCancelRecovery.cpp index 59e6056c85..297f761a92 100644 --- a/core/libs/tests/GuestPthreadCancelRecovery.cpp +++ b/core/libs/tests/GuestPthreadCancelRecovery.cpp @@ -1,10 +1,21 @@ #include "SceTypes.hpp" +#include "prx/libkernel/Pthread/include/Pthread.hpp" #include #include +#include #include #include #include #include +#include +#include + +#ifdef _WIN32 +#include +#else +#include +#include +#endif extern "C" { int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, PthreadEntry entry, void* arg, const char* name); @@ -17,6 +28,9 @@ int APS5_VABI scePthreadKeyCreate(PthreadKey* key, pthread_key_destructor_func_t int APS5_VABI scePthreadKeyDelete(PthreadKey key); int APS5_VABI scePthreadSetspecific(PthreadKey key, void* value); int APS5_VABI scePthreadMutexInit(PthreadMutex* mutex, const PthreadMutexattr* attr, const char* name); +int APS5_VABI scePthreadMutexattrInit(PthreadMutexattr* attr); +int APS5_VABI scePthreadMutexattrDestroy(PthreadMutexattr* attr); +int APS5_VABI scePthreadMutexattrSettype(PthreadMutexattr* attr, int type); int APS5_VABI scePthreadMutexDestroy(PthreadMutex* mutex); int APS5_VABI scePthreadMutexLock(PthreadMutex* mutex); int APS5_VABI scePthreadMutexUnlock(PthreadMutex* mutex); @@ -24,6 +38,10 @@ int APS5_VABI scePthreadCondInit(PthreadCond* cond, const PthreadCondattr* attr, int APS5_VABI scePthreadCondDestroy(PthreadCond* cond); int APS5_VABI scePthreadCondSignal(PthreadCond* cond); int APS5_VABI scePthreadCondTimedwait(PthreadCond* cond, PthreadMutex* mutex, KernelUseconds usec); +int APS5_VABI scePthreadCondWait(PthreadCond* cond, PthreadMutex* mutex); +int APS5_VABI pthread_cond_wait_nid_postfix(PthreadCond* cond, PthreadMutex* mutex); +int APS5_VABI pthread_cond_timedwait_nid_postfix(PthreadCond* cond, PthreadMutex* mutex, const KernelTimespec* abstime); +int APS5_VABI clock_gettime_nid_postfix(int clockId, KernelTimespec* time); int APS5_VABI scePthreadSemInit(PthreadSem* sem, int flag, unsigned int value, const char* name); int APS5_VABI scePthreadSemDestroy(PthreadSem* sem); int APS5_VABI scePthreadSemWait(PthreadSem* sem); @@ -226,6 +244,166 @@ static void ConditionRecovery() { Require(scePthreadMutexDestroy(&mutex) == 0); } +enum class ConditionWaitMode { SceTimed, Sce, PosixTimed, Posix }; + +struct SignaledCondition { + PthreadMutex mutex = nullptr; + PthreadCond condition = nullptr; + PthreadCond* slot = &condition; + ConditionWaitMode mode = ConditionWaitMode::SceTimed; + int lockCount = 1; + bool cancellationDisabled = false; + bool released = false; + std::atomic ready{0}; + std::array results{-1, -1}; + std::array cleaned{}; +}; + +struct SignaledConditionWorker { + SignaledCondition* context; + int index; +}; + +static void APS5_VABI UnlockSignaledCondition(void* value) { + auto& worker = *static_cast(value); + auto& context = *worker.context; + for (int level = 0; level < context.lockCount; ++level) Require(scePthreadMutexUnlock(&context.mutex) == 0); + Require(scePthreadMutexUnlock(&context.mutex) == static_cast(0x80020001)); + context.cleaned[worker.index] = true; +} + +static void* APS5_VABI SignaledConditionWaiter(void* value) { + auto& worker = *static_cast(value); + auto& context = *worker.context; + if (context.cancellationDisabled) Require(scePthreadSetcancelstate(CANCEL_DISABLE, nullptr) == 0); + for (int level = 0; level < context.lockCount; ++level) Require(scePthreadMutexLock(&context.mutex) == 0); + std::uintptr_t cleanup[8]; + __pthread_cleanup_push_imp_nid_postfix(UnlockSignaledCondition, &worker, cleanup); + context.ready.fetch_add(1); + int result = 0; + do { + switch (context.mode) { + case ConditionWaitMode::SceTimed: + result = scePthreadCondTimedwait(context.slot, &context.mutex, 2000000); + break; + case ConditionWaitMode::Sce: + result = scePthreadCondWait(context.slot, &context.mutex); + break; + case ConditionWaitMode::PosixTimed: { + KernelTimespec deadline{}; + Require(clock_gettime_nid_postfix(0, &deadline) == 0); + deadline.tv_sec += 2; + result = pthread_cond_timedwait_nid_postfix(context.slot, &context.mutex, &deadline); + break; + } + case ConditionWaitMode::Posix: + result = pthread_cond_wait_nid_postfix(context.slot, &context.mutex); + break; + } + } while (result == 0 && !context.released); + context.results[worker.index] = result; + __pthread_cleanup_pop_imp_nid_postfix(1); + return nullptr; +} + +static bool ConditionRegistered(Pthread thread) { + const std::lock_guard lock(thread->cancelLock); + return thread->cancelWait != nullptr; +} + +static void InitializeSignaledCondition(SignaledCondition& context, bool recursive) { + PthreadMutexattr attr = nullptr; + Require(scePthreadMutexattrInit(&attr) == 0); + Require(scePthreadMutexattrSettype(&attr, recursive ? 2 : 1) == 0); + Require(scePthreadMutexInit(&context.mutex, &attr, nullptr) == 0); + Require(scePthreadMutexattrDestroy(&attr) == 0); + context.lockCount = recursive ? 3 : 1; + Require(scePthreadCondInit(context.slot, nullptr, nullptr) == 0); +} + +static void CanceledConditionSignal(ConditionWaitMode mode, bool recursive) { + SignaledCondition context; + context.mode = mode; + InitializeSignaledCondition(context, recursive); + std::array workers{{{&context, 0}, {&context, 1}}}; + std::array threads{}; + for (int index = 0; index < 2; ++index) { + Require(scePthreadCreate(&threads[index], nullptr, SignaledConditionWaiter, &workers[index], nullptr) == 0); + while (context.ready.load() <= index) std::this_thread::yield(); + Require(scePthreadMutexLock(&context.mutex) == 0); + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(1); + while (!ConditionRegistered(threads[index]) && std::chrono::steady_clock::now() < deadline) { + Require(scePthreadMutexUnlock(&context.mutex) == 0); + std::this_thread::yield(); + Require(scePthreadMutexLock(&context.mutex) == 0); + } + Require(ConditionRegistered(threads[index])); + Require(scePthreadMutexUnlock(&context.mutex) == 0); + } + Require(scePthreadMutexLock(&context.mutex) == 0); + context.released = true; + Require(scePthreadCondSignal(&context.condition) == 0); + int selected = -1; + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(1); + while (selected < 0 && std::chrono::steady_clock::now() < deadline) { + for (int index = 0; index < 2; ++index) if (!ConditionRegistered(threads[index])) selected = index; + std::this_thread::yield(); + } + Require(selected >= 0); + Require(scePthreadCancel(threads[selected]) == 0); + Require(scePthreadMutexUnlock(&context.mutex) == 0); + Join(threads[selected], CANCELED); + Join(threads[1 - selected], nullptr); + Require(context.results[selected] == -1 && context.results[1 - selected] == 0); + Require(context.cleaned[0] && context.cleaned[1]); + Require(scePthreadCondDestroy(&context.condition) == 0); + Require(scePthreadMutexDestroy(&context.mutex) == 0); +} + +static void DestroyedSignaledCondition(ConditionWaitMode mode, bool recursive, bool cancel, bool cancellationDisabled) { + SignaledCondition context; + context.mode = mode; + context.cancellationDisabled = cancellationDisabled; +#ifdef _WIN32 + SYSTEM_INFO system{}; + GetSystemInfo(&system); + const auto bytes = static_cast(system.dwPageSize); + context.slot = static_cast(VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); + Require(context.slot != nullptr); +#else + const auto bytes = static_cast(sysconf(_SC_PAGESIZE)); + context.slot = static_cast(mmap(nullptr, bytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0)); + Require(context.slot != MAP_FAILED); +#endif + *context.slot = nullptr; + InitializeSignaledCondition(context, recursive); + const std::weak_ptr retained = (*context.slot)->_cv; + SignaledConditionWorker worker{&context, 0}; + Pthread thread = nullptr; + Require(scePthreadCreate(&thread, nullptr, SignaledConditionWaiter, &worker, nullptr) == 0); + while (context.ready.load() == 0) std::this_thread::yield(); + Require(scePthreadMutexLock(&context.mutex) == 0); + context.released = true; + Require(scePthreadCondSignal(context.slot) == 0); + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(1); + while (ConditionRegistered(thread) && std::chrono::steady_clock::now() < deadline) std::this_thread::yield(); + Require(!ConditionRegistered(thread)); + Require(scePthreadCondDestroy(context.slot) == 0); + Require(!retained.expired()); +#ifdef _WIN32 + Require(VirtualFree(context.slot, 0, MEM_RELEASE) != 0); +#else + Require(munmap(context.slot, bytes) == 0); +#endif + if (cancel) Require(scePthreadCancel(thread) == 0); + Require(scePthreadMutexUnlock(&context.mutex) == 0); + Join(thread, cancel && !cancellationDisabled ? CANCELED : nullptr); + Require(context.cleaned[0]); + Require(context.results[0] == (cancel && !cancellationDisabled ? -1 : 0)); + Require(retained.expired()); + Require(scePthreadMutexDestroy(&context.mutex) == 0); +} + static void* APS5_VABI HeldTarget(void*) { CheckDefaults(); targetRelease.wait(false); @@ -274,6 +452,14 @@ int main() { } Require(scePthreadKeyDelete(key) == 0); ConditionRecovery(); + for (const auto mode : {ConditionWaitMode::SceTimed, ConditionWaitMode::Sce, ConditionWaitMode::PosixTimed, ConditionWaitMode::Posix}) { + for (bool recursive : {false, true}) { + CanceledConditionSignal(mode, recursive); + DestroyedSignaledCondition(mode, recursive, true, false); + DestroyedSignaledCondition(mode, recursive, false, false); + DestroyedSignaledCondition(mode, recursive, true, true); + } + } disabled = true; ConditionRecovery(); JoinRecovery(); diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index efa9156809..a2afebc98f 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -51,7 +51,7 @@ - [sceKernelSyncOnAddressWait](../../../core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp) (libkernel) - the only known caller passes a null timeout and a name string as the fourth argument; the timeout is assumed to point to microseconds like the other kernel waits, and the name is ignored - [sceKernelSyncOnAddressWait8 / sceKernelSyncOnAddressWait16 / sceKernelSyncOnAddressWait32 / sceKernelSyncOnAddressWait64](../../../core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp) (libkernel) - libc's `__std_atomic_wait_direct_8/16/32/64` pass a microsecond timeout or null and ignore the result; the by-value comparand, return values and the 2- and 8-byte alignment of Wait16 and Wait64 are assumed - [scePthreadRwlockDestroy, pthread_rwlock_destroy](../../../core/libs/prx/libkernel/Pthread/src/Rwlock.cpp) (libkernel) - measured on a PS5 (firmware 13.60): destroying a rwlock returns 0 whether it is free or held for reading or writing, and leaves 1 in the handle; every call on that handle then returns `EINVAL` until it is initialized again, and a zero handle is initialized on first use by the `scePthreadRwlock` calls as by the POSIX ones. A rwlock destroyed while held is not freed, so its memory leaks. Destroying a rwlock while another thread is acquiring it is not guarded -- [scePthreadCondWait / scePthreadCondTimedwait](../../../core/libs/prx/libkernel/Pthread/src/Cond.cpp) (libkernel) - before waiting, FreeBSD libthr's [`cond_wait_common`](https://github.com/freebsd/freebsd-src/blob/main/lib/libthr/thread/thr_cond.c#L1992-L2029) checks ownership through [`_mutex_owned`](https://github.com/freebsd/freebsd-src/blob/main/lib/libthr/thread/thr_mutex.c#L1166-L1178): an unowned mutex returns `EPERM`, while its destroyed marker returns `EINVAL`. An untouched adaptive static initializer (slot value 1) therefore returns `EPERM`; the test covers this, an uninitialized slot, a mutex owned by another thread, and the destroyed marker for both timed and untimed sce and POSIX waits. A recursive mutex locked several times is released completely for the wait and relocked to the same depth afterwards, as FreeBSD libthr does (`_mutex_cv_unlock`, `_mutex_cv_lock`). These FreeBSD-derived behaviors remain unverified on console hardware +- [scePthreadCondWait / scePthreadCondTimedwait](../../../core/libs/prx/libkernel/Pthread/src/Cond.cpp) (libkernel) - before waiting, FreeBSD libthr's [`cond_wait_common`](https://github.com/freebsd/freebsd-src/blob/main/lib/libthr/thread/thr_cond.c#L1992-L2029) checks ownership through [`_mutex_owned`](https://github.com/freebsd/freebsd-src/blob/main/lib/libthr/thread/thr_mutex.c#L1166-L1178): an unowned mutex returns `EPERM`, while its destroyed marker returns `EINVAL`. An untouched adaptive static initializer (slot value 1) therefore returns `EPERM`; the test covers this, an uninitialized slot, a mutex owned by another thread, and the destroyed marker for both timed and untimed sce and POSIX waits. A recursive mutex locked several times is released completely for the wait and relocked to the same depth afterwards, as FreeBSD libthr does (`_mutex_cv_unlock`, `_mutex_cv_lock`). These FreeBSD-derived behaviors remain unverified on console hardware. Cancellation follows the [POSIX condition wait contract](https://pubs.opengroup.org/onlinepubs/9799919799/functions/pthread_cond_clockwait.html): the original mutex and recursive depth are restored before cleanup, and a canceled waiter forwards a wake to the remaining waiters. The host condition state is retained across mutex reacquisition so the guest can destroy and free its condition after notification; cancellation ordering and signal replacement are not measured on a console - [sceKernelAioSubmitReadCommandsMultiple / sceKernelAioSubmitWriteCommandsMultiple / sceKernelAioWaitRequests / sceKernelAioPollRequests / sceKernelAioCancelRequest / sceKernelAioCancelRequests / sceKernelAioDeleteRequests](../../../core/libs/prx/libkernel/Aio/src/Aio.cpp) (libkernel) - behaviour from shadPS4 (one id per request for the Multiple submits, cancel turns any request aborted and id 0 reports processing, wait mode 2 returns once one request completed, poll copies every state without waiting); the 128 requests or ids per batch limit is from SharpProspero, with an unknown error, so larger batches throw; wait modes other than 1 (and) and 2 (or) throw; null pointers, negative counts and invalid ids return the codes the single-request siblings use, checked before any state is written - [scePthreadAttr setters and getters](../../../core/libs/prx/libkernel/Pthread/src/Attr.cpp) (libkernel) - measured on a PS5 (firmware 13.60): a null or uninitialized attribute returns `EINVAL`, as does a null output of the stack size, detach state and scheduling parameter getters; a policy other than 1 and 3, an inheritance other than 0 and 4, or a priority outside 256 to 767 (or a null parameter) returns `ENOTSUP` (`0x8002002D`); a detach state other than 0 and 1 and a stack size below 16384 return `EINVAL`; any affinity mask is accepted. A null attribute pointer to `scePthreadAttrInit` and a null output of `scePthreadAttrGetaffinity` crashed the payload, so they still throw. A new attribute holds a 64 KiB stack size on the console and 1 MiB here, which the host code that runs on guest threads needs - [sceKernelGetAvailableCpumask](../../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) - returns the default thread affinity, 0x1FFF (CPUs 0 to 12), as KytyPS5 does. Titles pin threads within it (PPSA02664 passes 0x1FFB, Hades II PPSA36082 0x3 and 0x3F); whether CPU 13 is available to a title is not confirmed From e90616b16c592f25d417b67106a8f485278999cf Mon Sep 17 00:00:00 2001 From: notaxeltv Date: Wed, 14 Oct 2026 23:35:41 +0200 Subject: [PATCH 0174/1000] feat(libc): ignore name in _Cnd_init_with_default_name_override Absent PPSA34015 import; forward to _Cnd_init like Ampr's named init. Co-authored-by: Cursor --- core/libs/prx/libc/CMakeLists.txt | 12 ++++++ core/libs/prx/libc/src/Threads.cpp | 4 ++ .../tests/GuestLibcCndInitDefaultName.cpp | 42 +++++++++++++++++++ 3 files changed, 58 insertions(+) create mode 100644 core/libs/tests/GuestLibcCndInitDefaultName.cpp diff --git a/core/libs/prx/libc/CMakeLists.txt b/core/libs/prx/libc/CMakeLists.txt index 02162709a5..7817307f16 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -692,6 +692,18 @@ if(BUILD_TESTING) endif() endif() +add_test_executable(guest_libc_cnd_init_default_name_tests EXCLUDE_FROM_ALL ${LIBS_INCLUDE_DIR}/tests/GuestLibcCndInitDefaultName.cpp) +target_include_directories(guest_libc_cnd_init_default_name_tests PRIVATE ${LIBS_INCLUDE_DIR}) +target_link_libraries(guest_libc_cnd_init_default_name_tests PRIVATE libc) +configure_windows_unwind(guest_libc_cnd_init_default_name_tests) +if(BUILD_TESTING) + set_target_properties(guest_libc_cnd_init_default_name_tests PROPERTIES EXCLUDE_FROM_ALL FALSE) + add_test(NAME guest_libc_cnd_init_default_name COMMAND guest_libc_cnd_init_default_name_tests) + if(WIN32) + set_tests_properties(guest_libc_cnd_init_default_name PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() +endif() + add_test_executable(guest_libc_cnd_timed_wait_tests EXCLUDE_FROM_ALL ${LIBS_INCLUDE_DIR}/tests/GuestLibcCndTimedWait.cpp) target_include_directories(guest_libc_cnd_timed_wait_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_libc_cnd_timed_wait_tests PRIVATE libc) diff --git a/core/libs/prx/libc/src/Threads.cpp b/core/libs/prx/libc/src/Threads.cpp index b1d85f9faf..28abb1454b 100644 --- a/core/libs/prx/libc/src/Threads.cpp +++ b/core/libs/prx/libc/src/Threads.cpp @@ -187,6 +187,10 @@ int APS5_VABI _Cnd_init_nid_postfix(void** handle) { return threadSuccess; } +int APS5_VABI _Cnd_init_with_default_name_override_nid_postfix(void** handle, const char*) { + return _Cnd_init_nid_postfix(handle); +} + void APS5_VABI _Cnd_destroy_nid_postfix(void** handle) { if (!handle) throw std::invalid_argument("_Cnd_destroy: null condition handle pointer"); diff --git a/core/libs/tests/GuestLibcCndInitDefaultName.cpp b/core/libs/tests/GuestLibcCndInitDefaultName.cpp new file mode 100644 index 0000000000..6d5d046398 --- /dev/null +++ b/core/libs/tests/GuestLibcCndInitDefaultName.cpp @@ -0,0 +1,42 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include + +extern "C" { +int APS5_VABI _Cnd_init_with_default_name_override_nid_postfix(void** handle, const char* name); +void APS5_VABI _Cnd_destroy_nid_postfix(void** handle); +int APS5_VABI _Cnd_signal_nid_postfix(void** handle); +} + +static void Require(bool value) { if (!value) std::abort(); } + +template +static void RequireThrows(TCall call) { + try { + call(); + } catch (const TException&) { + return; + } + std::abort(); +} + +constexpr int success = 0; + +int main() { + void* condition = nullptr; + Require(_Cnd_init_with_default_name_override_nid_postfix(&condition, "GameCond") == success); + Require(condition != nullptr); + Require(_Cnd_signal_nid_postfix(&condition) == success); + _Cnd_destroy_nid_postfix(&condition); + Require(condition == nullptr); + + void* unnamed = nullptr; + Require(_Cnd_init_with_default_name_override_nid_postfix(&unnamed, nullptr) == success); + Require(unnamed != nullptr); + _Cnd_destroy_nid_postfix(&unnamed); + + RequireThrows([] { + _Cnd_init_with_default_name_override_nid_postfix(nullptr, "NullHandle"); + }); + return 0; +} From a7730cb9e600b67a303910cc29bf8ae8e3e23cce Mon Sep 17 00:00:00 2001 From: AnyPS5 Contributors Date: Wed, 14 Oct 2026 17:48:14 -0400 Subject: [PATCH 0175/1000] fix(graphics): keep discarded import probes from pinning windows --- .../Graphics/src/GuestBufferMemory.cpp | 23 ++- .../libSceAgcDriver/tests/BdaResources.cpp | 147 ++++++++++++++++++ 2 files changed, 164 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index c04d2bdf93..1674531dd1 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -892,6 +892,7 @@ ImportRun windowRun(const HostImports& state, ImportRun run, std::uint64_t begin return run; } +const HostImport* peekImport(const HostImports& state, std::uint64_t begin, std::uint64_t end); const HostImport* findImport(HostImports& state, std::uint64_t begin, std::uint64_t end); bool importsStale(const Context& context, const HostImports& state); const HostImport* importRunAt(const Context& context, HostImports& state, ImportRun run, bool joined, std::uint64_t address, const GuestAllocations::Lease& lease, std::uint64_t end, bool evict); @@ -910,7 +911,10 @@ const HostImport* importAllocation(const Context& context, HostImports& state, s if (fresh && end != 0) { if (const auto* current = findImport(state, address, end)) return current; } else if (!fresh) { - if (const auto* current = findImport(state, address, address + 1); current != nullptr && current->joined && sameRange(*current, lease)) return current; + if (const auto* current = peekImport(state, address, address + 1); current != nullptr && current->joined && sameRange(*current, lease)) { + current->lastUse = ++state.useCounter; + return current; + } } auto run = importRun(lease, address); // Without an end the caller views the whole run through the import (AcquireRegistered): a window that holds the @@ -941,7 +945,10 @@ const HostImport* importRunAt(const Context& context, HostImports& state, Import failures.insert_or_assign(base, ImportFailure{base, bytes, GuestAllocations::Mapped(run.ranges.begin(), run.ranges.end())}); return nullptr; } - if (const auto found = state.imports.find(base); found != state.imports.end() && sameRun(found->second, run)) return &found->second; + if (const auto found = state.imports.find(base); found != state.imports.end() && sameRun(found->second, run)) { + found->second.lastUse = ++state.useCounter; + return &found->second; + } if (alignment == 0 || base % alignment != 0 || bytes % alignment != 0) return nullptr; if (const auto failed = failures.find(base); failed != failures.end()) { if (sameRun(failed->second, run)) return nullptr; @@ -1153,14 +1160,18 @@ void refreshImports(const Context& context, HostImports& state, const GuestAlloc for (auto it = state.failedJoins.begin(); it != state.failedJoins.end();) it = sameRun(it->second, joinedRun(lease, it->second.base, it->second.base + it->second.bytes)) ? std::next(it) : state.failedJoins.erase(it); } -const HostImport* findImport(HostImports& state, std::uint64_t begin, std::uint64_t end) { +const HostImport* peekImport(const HostImports& state, std::uint64_t begin, std::uint64_t end) { auto found = state.imports.upper_bound(begin); if (found == state.imports.begin()) return nullptr; --found; const auto& entry = found->second; - if (begin < entry.base || end > entry.base + entry.bytes) return nullptr; - entry.lastUse = ++state.useCounter; - return &entry; + return begin < entry.base || end > entry.base + entry.bytes ? nullptr : &entry; +} + +const HostImport* findImport(HostImports& state, std::uint64_t begin, std::uint64_t end) { + const auto* entry = peekImport(state, begin, end); + if (entry != nullptr) entry->lastUse = ++state.useCounter; + return entry; } const HostImport* importAccess(const Context& context, HostImports& state, std::uint64_t begin, std::uint64_t end, const GuestAllocations::Lease& lease) { diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp index 2fe20f100c..b79cab3909 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include #include #include @@ -608,6 +609,151 @@ void importWholeRangeOverWindowTests(const Context& context) { leased.WriteBack(); } +struct ObservedHostImport { + const void* pointer; + VkDeviceSize bytes; +}; + +class ImportAllocationProbe { +public: + explicit ImportAllocationProbe(Context& context) : context(context), original(context.deviceProc) { + Require(active == nullptr && original != nullptr, "invalid import allocation probe scope"); + active = this; + context.deviceProc = Resolve; + } + ~ImportAllocationProbe() { + context.deviceProc = original; + active = nullptr; + } + ImportAllocationProbe(const ImportAllocationProbe&) = delete; + ImportAllocationProbe& operator=(const ImportAllocationProbe&) = delete; + + std::vector calls; + +private: + static PFN_vkVoidFunction VKAPI_CALL Resolve(VkDevice device, const char* name) { + Require(active != nullptr, "import allocation probe escaped its scope"); + if (std::strcmp(name, "vkAllocateMemory") == 0) return reinterpret_cast(Allocate); + return active->original(device, name); + } + static VkResult VKAPI_CALL Allocate(VkDevice device, const VkMemoryAllocateInfo* info, const VkAllocationCallbacks* allocator, VkDeviceMemory* memory) { + Require(active != nullptr, "import allocation callback escaped its scope"); + for (const auto* next = static_cast(info->pNext); next != nullptr; next = next->pNext) { + if (next->sType != VK_STRUCTURE_TYPE_IMPORT_MEMORY_HOST_POINTER_INFO_EXT) continue; + const auto* imported = reinterpret_cast(next); + active->calls.push_back({imported->pHostPointer, info->allocationSize}); + } + const auto allocate = reinterpret_cast(active->original(device, "vkAllocateMemory")); + Require(allocate != nullptr, "import allocation probe has no original allocator"); + return allocate(device, info, allocator, memory); + } + + Context& context; + PFN_vkGetDeviceProcAddr original; + inline static thread_local ImportAllocationProbe* active = nullptr; +}; + +class UnrelatedImportProbeRange { +public: + UnrelatedImportProbeRange() { + GuestAllocations::Mutation().Add(storage.data(), storage.size(), true, false, false); + } + ~UnrelatedImportProbeRange() { + GuestAllocations::Mutation().Remove(storage.data()); + } + UnrelatedImportProbeRange(const UnrelatedImportProbeRange&) = delete; + UnrelatedImportProbeRange& operator=(const UnrelatedImportProbeRange&) = delete; + +private: + std::array storage{}; +}; + +void importStaleProbeTests(const Context& context, const BdaTestAccess& access) { + for (const auto scenario : {0u, 1u, 2u}) { + const bool held = scenario == 1u; + const bool exactWhole = scenario == 2u; + WindowedRange range(context, 2 * WindowedRange::piece, 2); + ImportAllocationProbe probe(range.importing); + const auto first = range.First(); + auto* guest = reinterpret_cast(first); + for (std::size_t offset = 17u; offset < range.bytes; offset += 4096u) guest[offset] = static_cast((offset / 4096u) & 0xffu); + const std::vector originalGuest(guest, guest + range.bytes); + const auto* window = HostImportFor(range.importing, first, exactWhole ? range.bytes : 32u); + Require(window != nullptr && !window->joined && window->base == first && window->bytes == (exactWhole ? range.bytes : range.window), "the stale-probe fixture did not create its ordinary import extent"); + const auto originalBuffer = window->buffer; + const auto originalUse = window->lastUse; + const auto originalSerial = HostImportSerial(range.importing, first, 32u, false); + auto mockContents = access.bytes(window->buffer); + Require(mockContents.size() == window->bytes, "the mock import buffer has an incorrect extent"); + std::copy_n(originalGuest.begin(), window->bytes, mockContents.begin()); + std::uint32_t scratch = 0x12345678u; + { + GuestBufferMemory copied(range.importing); + copied.AddReadable(reinterpret_cast(&scratch), sizeof(scratch)); + copied.Upload(false); + copied.WriteBack(); + } + Require(window->lastUse == originalUse && HostImportSerial(range.importing, first, 32u, false) == originalSerial, "an unrelated copied build touched or replaced the prefix window"); + GuestBufferMemory reader(range.importing); + VkDescriptorBufferInfo heldView{}; + std::uint32_t heldAdjustment = 0u; + if (held) { + reader.AddReadable(first + 16u, 32u); + reader.Upload(false); + heldView = reader.Descriptor(first + 16u, 32u, heldAdjustment); + Require(heldView.buffer == originalBuffer, "the held control did not bind the prefix window"); + } + GuestAllocations::Mapped mapped; + { + const auto lease = GuestAllocations::GuestAllocationsAcquire_nid_postfix(); + for (const auto& owner : lease) if (owner->address == first && owner->bytes == range.bytes) mapped.push_back(owner); + } + Require(mapped.size() == 1u, "the stale-probe fixture lost its registered range"); + const auto originalOwner = mapped.front().lock(); + const auto generation = GuestAllocations::GuestAllocationsGeneration_nid_postfix(); + UnrelatedImportProbeRange unrelated; + Require(GuestAllocations::GuestAllocationsGeneration_nid_postfix() != generation && HostImportsIdentity(range.importing).device == VK_NULL_HANDLE, + "the unrelated registry change did not make imports stale"); + const auto attempts = access.allocationAttempts(); + probe.calls.clear(); + { + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + Require(ImportMappedRanges(range.importing, mapped, GuestAllocations::GuestAllocationsGeneration_nid_postfix(), false), "the current device did not admit the map-time import pass"); + } + Require(mapped.front().lock() == originalOwner, "an unrelated registry change replaced the import's memory owner"); + if (held) { + Require(access.allocationAttempts() == attempts && probe.calls.empty(), "a stale probe retired an import held by an actual descriptor"); + const auto* kept = HostImportFor(range.importing, first + 16u, 32u); + Require(kept != nullptr && kept->buffer == originalBuffer && kept->bytes == range.window && HostImportSerial(range.importing, first, 32u, false) == originalSerial, + "the actual held import lost its buffer, extent or serial"); + std::uint32_t adjustment = 0u; + const auto view = reader.Descriptor(first + 16u, 32u, adjustment); + Require(view.buffer == heldView.buffer && view.offset == heldView.offset && view.range == heldView.range && adjustment == heldAdjustment, + "a map-time stale probe invalidated a descriptor that still uses its import"); + const auto contents = access.bytes(view.buffer); + Require(view.offset + adjustment + 32u <= contents.size() && std::memcmp(contents.data() + view.offset + adjustment, reinterpret_cast(first + 16u), 32u) == 0, + "the protected descriptor no longer reads the registered bytes"); + reader.WriteBack(); + } else if (exactWhole) { + Require(access.allocationAttempts() == attempts && probe.calls.empty() && window->lastUse > originalUse, + "accepting the stale exact whole import reallocated it or did not protect its returned pointer"); + const auto* kept = HostImportFor(range.importing, first, range.bytes); + Require(kept != nullptr && kept->buffer == originalBuffer && kept->ranges.size() == 1u && kept->ranges.front() == originalOwner && HostImportSerial(range.importing, first, range.bytes, false) == originalSerial, + "accepting the exact whole import changed its native or registered identity"); + } else { + Require(access.allocationAttempts() == attempts + 1u, "a discarded stale probe made an unheld prefix window block the whole import"); + const auto* whole = HostImportFor(range.importing, first, range.bytes); + Require(whole != nullptr && whole->base == first && whole->bytes == range.bytes && whole->ranges.size() == 1u && whole->ranges.front() == originalOwner, + "the stale-probe repair did not retain the exact registered owner over the whole import"); + Require(HostImportSerial(range.importing, first, range.bytes, false) != originalSerial, "replacing the unheld window reused its old import identity"); + Require(probe.calls.size() == 1u && probe.calls.front().pointer == reinterpret_cast(first) && probe.calls.front().bytes == range.bytes, + "the replacement native import did not reference the exact whole registered memory"); + Require(access.bytes(whole->buffer).size() == range.bytes, "the replacement import buffer does not cover its registered extent"); + } + Require(scratch == 0x12345678u && std::equal(originalGuest.begin(), originalGuest.end(), guest), "the import probe changed guest bytes or unrelated copied data"); + } +} + // An address-based build whose whole-range import does not fit the budget leaves the range's window alone: before, // every such build retired the window and the next access imported it again. void importRefusedWholeRangeTests(const Context& context) { @@ -1787,6 +1933,7 @@ void RunBdaResourceTests(const Context& context, const BdaTestAccess& access) { spaceReadAcrossImportsWrittenTests(context); importJoinTests(context); importWholeRangeOverWindowTests(context); + importStaleProbeTests(context, access); importRefusedWholeRangeTests(context); importNeighbouringEdgesTests(context); importAtMapOverBudgetTests(context); From 84f72fc05731d1688ea65bd1e913194d15c2e673 Mon Sep 17 00:00:00 2001 From: David Date: Thu, 15 Oct 2026 01:57:53 +0400 Subject: [PATCH 0176/1000] docs(debt): record that guest GS accesses trap in Windows output --- docs/dev/debt/relinker.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/dev/debt/relinker.md b/docs/dev/debt/relinker.md index e5841ca308..27b53899d9 100644 --- a/docs/dev/debt/relinker.md +++ b/docs/dev/debt/relinker.md @@ -9,6 +9,7 @@ - `--to-intel` does not lower RDPRU/MCOMMIT; the [matcher](../../../core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp) fails the relink instead. - The length-changing path of the [instruction rewriter](../../../core/relinker/codegen/src/x86/X64InstructionRewriter.cpp) is not used by the [converter](../../../core/relinker/codegen/src/Amd64OnlyConverter.cpp): it does not adjust data-to-code references (relocations, FDEs, jump tables) or segment sizes, so every substitution keeps the instruction length. - A `PT_SCE_RELRO` segment that no `PT_LOAD` covers, the PS4 layout, is [loaded](../../../core/relinker/relinker/src/parsing/ElfReader.cpp) as a read-write `PT_LOAD` and stays writable after relocation; FreeBSD rtld makes `PT_GNU_RELRO` read-only once relocation is done, and what the console does with `PT_SCE_RELRO` is not checked. +- In Windows output, a guest instruction that addresses memory through GS becomes `UD2` and NOP padding of its own length ([TLS builder](../../../core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp)): the host owns GS, and the guest GS base that FreeBSD's `sysarch(AMD64_SET_GSBASE)` would set is not emulated. A converted module runs until such an instruction is reached, which then faults. Prospero Win (PPSA99995) has six in its Wine `ntdll`. - In executables with a `PT_SCE_DYNLIBDATA` segment, the PS4 layout, the executable segment also holds read-only data after the code (CUSA00900: 37 MiB after its last function). The [relinker pipeline](../../../core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp) therefore scans for syscalls and import call sites only up to the end of the PLT that follows the last function of the unwind table; code after the PLT without an unwind entry is not scanned. `--to-intel` and the `unused-filter=1` and `unused-filter=2` analyses are not limited this way. - In executables and guest modules with a `PT_SCE_DYNLIBDATA` segment, the [code analysis](../../../core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp) of the Windows and macOS outputs follows code only from the end of `PT_INTERP` to the end of the PLT after the last function of the unwind table, the end the pipeline's scans above use; a guest module with PLT relocations and no PLT after its last unwind function fails as the executable does. Code after that PLT is not analyzed: a TLS access in it is not rewritten, an entry point or pointer into it is ignored, and a function symbol or init/fini array entry pointing into it fails the conversion. - A relocation against a dynamic symbol entry that is all zero, like the null symbol, takes the value 0 plus its addend, as the gABI defines for `STN_UNDEF` ([pipeline](../../../core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp)). The Linux output imports such an entry as an unnamed weak symbol, which the host loader resolves to 0; the Windows and macOS outputs write the value into the slot ([relocation builder](../../../core/relinker/elfpatcher/src/windows/WindowsRelocationBuilder.cpp)). What the console's loader does with such an entry is not measured. OliSe Player (PPSA01153) has seven of them, and its code calls through those slots only when they are not 0 ([#4437](https://github.com/boykopovar/AnyPS5/issues/4437)). From 58a1e712cb932da41e5945b7d4e6554646dac59e Mon Sep 17 00:00:00 2001 From: Zinebyte <197974009+Zinebyte@users.noreply.github.com> Date: Wed, 14 Oct 2026 18:09:15 -0400 Subject: [PATCH 0177/1000] fix(libkernel): cancel before acquiring a ready timed semaphore --- core/libs/prx/libkernel/Pthread/Posix/Sem.cpp | 1 + core/libs/tests/GuestPthreadCancel.cpp | 46 +++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/core/libs/prx/libkernel/Pthread/Posix/Sem.cpp b/core/libs/prx/libkernel/Pthread/Posix/Sem.cpp index d11bda07ad..e780d7f59f 100644 --- a/core/libs/prx/libkernel/Pthread/Posix/Sem.cpp +++ b/core/libs/prx/libkernel/Pthread/Posix/Sem.cpp @@ -84,6 +84,7 @@ int APS5_VABI sem_trywait_nid_postfix(void* sem) { int APS5_VABI sem_timedwait_nid_postfix(void* sem, const KernelTimespec* abstime) { auto* semaphore = Get(sem); if (!semaphore) return Fail(PosixThread::GUEST_EINVAL); + ThreadCancel::Check(); { std::lock_guard lock(semaphore->lock); if (semaphore->count > 0) { diff --git a/core/libs/tests/GuestPthreadCancel.cpp b/core/libs/tests/GuestPthreadCancel.cpp index 311fb8a5a4..576414c6c3 100644 --- a/core/libs/tests/GuestPthreadCancel.cpp +++ b/core/libs/tests/GuestPthreadCancel.cpp @@ -8,6 +8,7 @@ extern "C" { int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, PthreadEntry entry, void* arg, const char* name); int APS5_VABI scePthreadJoin(Pthread thread, void** retval); +Pthread APS5_VABI scePthreadSelf(); void APS5_VABI scePthreadExit(void* retval); int APS5_VABI scePthreadCancel(Pthread thread); int APS5_VABI scePthreadSetcancelstate(int state, int* old_state); @@ -29,6 +30,8 @@ int APS5_VABI pthread_setcanceltype_nid_postfix(int type, int* old_type); int APS5_VABI sem_init_nid_postfix(void* sem, int pshared, unsigned int value); int APS5_VABI sem_destroy_nid_postfix(void* sem); int APS5_VABI sem_wait_nid_postfix(void* sem); +int APS5_VABI sem_timedwait_nid_postfix(void* sem, const KernelTimespec* abstime); +int APS5_VABI sem_getvalue_nid_postfix(void* sem, int* value); void APS5_VABI __pthread_cleanup_push_imp_nid_postfix(void (APS5_VABI* routine)(void*), void* argument, void* info); void APS5_VABI __pthread_cleanup_pop_imp_nid_postfix(int execute); } @@ -93,6 +96,44 @@ static void* APS5_VABI PosixSemWaiter(void*) { return nullptr; } +struct AvailableTimedSemaphore { + void* semaphore = nullptr; + bool pending; + bool disabled; + bool expired; + bool returned = false; + int result = -2; +}; + +static void* APS5_VABI AvailableTimedSemaphoreWaiter(void* argument) { + auto& value = *static_cast(argument); + if (value.disabled) Require(scePthreadSetcancelstate(CANCEL_DISABLE, nullptr) == SCE_OK); + if (value.pending) Require(pthread_cancel_nid_postfix(scePthreadSelf()) == 0); + KernelTimespec deadline{}; + if (!value.expired) + deadline.tv_sec = std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count() + 60; + value.result = sem_timedwait_nid_postfix(&value.semaphore, &deadline); + value.returned = true; + return reinterpret_cast(std::intptr_t{7}); +} + +static void CheckAvailableTimedSemaphore(bool pending, bool disabled, bool expired) { + AvailableTimedSemaphore value{nullptr, pending, disabled, expired}; + Require(sem_init_nid_postfix(&value.semaphore, 0, 1) == 0); + Pthread thread = nullptr; + Require(scePthreadCreate(&thread, nullptr, AvailableTimedSemaphoreWaiter, &value, nullptr) == SCE_OK); + void* result = nullptr; + Require(scePthreadJoin(thread, &result) == SCE_OK); + const bool canceled = pending && !disabled; + Require(result == (canceled ? CANCELED : reinterpret_cast(std::intptr_t{7}))); + Require(value.returned != canceled); + if (!canceled) Require(value.result == 0); + int permits = -1; + Require(sem_getvalue_nid_postfix(&value.semaphore, &permits) == 0); + Require(permits == (canceled ? 1 : 0)); + Require(sem_destroy_nid_postfix(&value.semaphore) == 0); +} + static void* APS5_VABI Idler(void*) { while (!shared.release.load()) std::this_thread::sleep_for(std::chrono::milliseconds(1)); return nullptr; @@ -197,6 +238,11 @@ int main() { Require(pthread_cancel_nid_postfix(thread) == 0); JoinCanceled(thread); + CheckAvailableTimedSemaphore(true, false, false); + CheckAvailableTimedSemaphore(true, false, true); + CheckAvailableTimedSemaphore(true, true, false); + CheckAvailableTimedSemaphore(false, false, true); + Require(scePthreadCreate(&shared.target, nullptr, Idler, nullptr, nullptr) == SCE_OK); thread = Start(Joiner); Require(scePthreadCancel(thread) == SCE_OK); From 0ee7af6f5f75c3dcc5322f7e1b26ecc1b64d1b75 Mon Sep 17 00:00:00 2001 From: fdfawef324 Date: Wed, 14 Oct 2026 17:14:29 -0400 Subject: [PATCH 0178/1000] feat(agc): accept SAMPLE_STREAMOUTSTATS events without a destination in EVENT_WRITE EVENT_WRITE (PM4 opcode 0x46) with event type SAMPLE_STREAMOUTSTATS1-3 (0x01-0x03) or SAMPLE_STREAMOUTSTATS (0x20) failed submission validation with "EVENT_WRITE event type 1 is not implemented". Minecraft Preview (PPSA19639, discussion #1277, stop 9) submits a 2-dword type-1 packet while entering a world, and the GPU thread aborted intermittently. The four values are VGT_EVENT_TYPE entries in Linux drivers/gpu/drm/amd/include/navi10_enum.h L14155-14188 at 0fcb05b72344f40806cbe810de8872f1698bf053. drivers/gpu/drm/amd/amdgpu/nvd.h L326-347 at the same commit documents these events only with the sampling index (3, or EVENT_INDEX__SAMPLE_STREAMOUTSTATS* 8-11 on gfx10), followed by ADDRESS_LO/HI. The 2-dword index-0 packet has no destination dwords, and what the CP does with it is neither documented nor measured. AnyPS5 does not emulate the VGT stream-out counters, so there is nothing to sample. Packet execution already skips EVENT_WRITE. These event types are now accepted on the graphics queue as 2-dword packets with event index 0. The sampling indices 3 and 8-11 throw that stream-out statistics sampling to memory is not implemented, before the size and reserved-bit checks. Other indices, other sizes and the compute queue still throw. sceAgcDcbEventWrite ignores the address for every event type except 0x38 and 0x39, so a call with one of these event types and an address now builds a packet that validates and writes nothing. This is recorded in docs/dev/debt/libSceAgcDriver.md with the unmeasured hardware behaviour. Co-Authored-By: Claude Opus 5.5 --- .../prx/libSceAgcDriver/Execution/src/Pm4.cpp | 11 +++++++++-- core/libs/prx/libSceAgcDriver/tests/Pm4.cpp | 18 ++++++++++++++++++ docs/dev/debt/libSceAgcDriver.md | 1 + 3 files changed, 28 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp index bdee46ee16..5eb31653b5 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp @@ -373,9 +373,11 @@ void Validate(std::span packet, std::uint32_t queue) { require((packet[1] & 0x7fffffffu) == 0 && (packet[2] & 0x7fffffffu) == 0, "CONTEXT_CONTROL register loading and shadowing are not implemented"); break; case 0x46: { - require((packet[1] & ~0x73fu) == 0, "unsupported EVENT_WRITE flags or reserved bits"); const auto eventType = packet[1] & 0x3fu; - const auto eventIndex = (packet[1] >> 8u) & 7u; + const auto eventIndex = (packet[1] >> 8u) & 0xfu; + const auto streamOutStatistics = (eventType >= 0x01u && eventType <= 0x03u) || eventType == 0x20u; + require(!streamOutStatistics || (eventIndex != 3 && (eventIndex < 8 || eventIndex > 11)), "stream-out statistics sampling to memory is not implemented"); + require((packet[1] & ~0x73fu) == 0, "unsupported EVENT_WRITE flags or reserved bits"); switch (eventType) { case 0x07: case 0x0f: case 0x10: size(2); @@ -396,6 +398,11 @@ void Validate(std::span packet, std::uint32_t queue) { size(2); require(eventIndex == 0, "invalid VGT_FLUSH event index"); break; + case 0x01: case 0x02: case 0x03: case 0x20: + graphics(); + size(2); + require(eventIndex == 0, "invalid stream-out statistics event index"); + break; case 0x39: graphics(); size(4); diff --git a/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp b/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp index 39131eb9ca..3414e86dbd 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp @@ -787,6 +787,22 @@ void testEventWrite() { expectFailure([] { AgcDriver::Pm4::Validate(makePacket(0x46, {0x24}), 0x20); }, "compute queue"); expectFailure([] { AgcDriver::Pm4::Validate(makePacket(0x46, {0x424}), 0); }, "VGT_FLUSH event index"); expectFailure([] { AgcDriver::Pm4::Validate(makePacket(0x46, {0x1e, 0x1000, 0}), 0); }, "event type 30"); + for (const auto eventType : {0x01u, 0x02u, 0x03u, 0x20u}) { + AgcDriver::Pm4::Validate(makePacket(0x46, {eventType}), 0); + expectFailure([&] { AgcDriver::Pm4::Validate(makePacket(0x46, {eventType}), 0x20); }, "compute queue"); + for (std::uint32_t index = 1; index < 8; ++index) { + if (index == 3) continue; + expectFailure([&] { AgcDriver::Pm4::Validate(makePacket(0x46, {(index << 8u) | eventType}), 0); }, "stream-out statistics event index"); + } + for (const auto index : {3u, 8u, 9u, 10u, 11u}) { + expectFailure([&] { AgcDriver::Pm4::Validate(makePacket(0x46, {(index << 8u) | eventType, 0x1000, 0x2}), 0); }, "stream-out statistics sampling to memory is not implemented"); + expectFailure([&] { AgcDriver::Pm4::Validate(makePacket(0x46, {(index << 8u) | eventType}), 0); }, "stream-out statistics sampling to memory is not implemented"); + } + for (const auto index : {12u, 13u, 14u, 15u}) { + expectFailure([&] { AgcDriver::Pm4::Validate(makePacket(0x46, {(index << 8u) | eventType}), 0); }, "reserved bits"); + } + expectFailure([&] { AgcDriver::Pm4::Validate(makePacket(0x46, {eventType, 0x1000, 0x2}), 0); }, "packet size"); + } } void testAcquireMem() { @@ -891,6 +907,8 @@ void testDriverSubmission() { makePacket(0x46, {0x2a}), makePacket(0x46, {0x72c}), makePacket(0x46, {0x2e}), + makePacket(0x46, {0x01}), + makePacket(0x46, {0x20}), makePacket(0x58, {0x02007fc0, 0, 0, 0, 0, 10, 0x200}), makePacket(0x58, {0x00800000, 0xffffffff, 0, 0, 0, 10}), makePacket(0x83, {0, 1, low(destination.data()), high(destination.data())}) diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 7f3aa4d73c..d5b156766d 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -108,6 +108,7 @@ - [1_5_5_5 color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) render into A1R5G5B5. AMD formats keep the first component in the least significant bits (X in bits 0-4, Y in 5-9, Z in 10-14, W in bit 15, as in the [fpPS4](https://github.com/red-prig/fpPS4) `kSurfaceFormat1_5_5_5` doc comment and as measured for textures in the packed 16-bit textures entry), while A1R5G5B5 keeps R in bits 10-14 and B in bits 0-4, so the pixel exports are written with X and Z exchanged, which makes the stored bits follow the guest layout. The decoded format, component mapping and write mask are tested, and an execution test draws 32_ABGR exports into tiled and linear targets and compares every stored texel with the guest layout on the host device; the export direction is not measured on a PS5. Only UNORM with the standard component swap is accepted. Constant blend colors go through the same component-swap handling as other targets with that export mapping (`0xc6`), which is not tested for this format. PPSA06699 renders into this format. - [COPY_DATA from the GPU clock](../../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (libSceAgcDriver, `SRC_SEL` 9) writes the host steady clock in 10 ns ticks when the packet runs, the same counter as the `RELEASE_MEM` timestamp, not the console's GPU clock. The `COPY_DATA` cache policy fields are accepted and have no effect. - [COPY_DATA from a register](../../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (libSceAgcDriver, `SRC_SEL` 0) copies the value the queue holds for a shader, context or user config register (`COUNT_SEL` two consecutive ones) and throws for a register with no value. A dispatch records `COMPUTE_DISPATCH_INITIATOR` and, for `DISPATCH_DIRECT`, `COMPUTE_DIM_X/Y/Z` in the shader registers; `DISPATCH_INDIRECT` clears the three counts. Neither the register values the CP writes nor the copy's ordering against in-flight dispatches have been measured on hardware. +- [EVENT_WRITE stream-out statistics events](../../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (libSceAgcDriver, opcode 0x46) - `SAMPLE_STREAMOUTSTATS1`-`3` and `SAMPLE_STREAMOUTSTATS` (event types 0x01-0x03 and 0x20 in [navi10_enum.h](https://github.com/torvalds/linux/blob/0fcb05b72344f40806cbe810de8872f1698bf053/drivers/gpu/drm/amd/include/navi10_enum.h#L14155-L14188)) are accepted on the graphics queue as a 2-dword packet with event index 0 and do nothing. [nvd.h](https://github.com/torvalds/linux/blob/0fcb05b72344f40806cbe810de8872f1698bf053/drivers/gpu/drm/amd/amdgpu/nvd.h#L326-L347) documents these events only with the sampling index (3, or `EVENT_INDEX__SAMPLE_STREAMOUTSTATS*` 8-11 on gfx10); the index-0 packet has no destination dwords, and what the CP does with it is neither documented nor measured on hardware. AnyPS5 does not emulate the VGT stream-out counters, so there is nothing to sample. The sampling indices throw that sampling to memory is not implemented, and other indices, other sizes and the compute queue throw. [sceAgcDcbEventWrite](../../../core/libs/prx/libSceAgc/DcbFlow/src/Sync.cpp) ignores the address for every event type except 0x38 and 0x39, so a call with one of these event types and an address builds the index-0 packet: nothing is written and nothing throws. Whether the console's builder emits a sampling packet for an address is not known. - [INDEX_ATTRIBUTES_INDIRECT](../../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (libSceAgcDriver, opcode 0x91) loads INDEX_BASE, INDEX_BUFFER_SIZE (in indices) and INDEX_TYPE from the 16-byte record at `attribute_base + attribute_index * 16`, read when the packet runs. The record layout (address low, address high, size, type) is PAL's [`IndexAttribIndirect`](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/hw/gfxip/gfx9/gfx9Chip.h#L60-L67) and the packet is the one [`BuildIndexAttributesIndirect`](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/hw/gfxip/gfx9/gfx9CmdUtil.cpp#L2586-L2613) writes; neither is measured on a PS5 or seen in a title. `indirect_mode` 1 (`addr_offset`, which only PAL's ExecuteIndirect path emits) throws, and so does a record whose index type is above 3, as INDEX_TYPE does. - [16-bit RGBA color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) support the standard and the alternate (`COMP_SWAP` 1) component order. Alternate swaps red and blue in shader exports, write masks and constant-color blend factors, following `ac_translate_colorswap` in [Mesa 26.2.2 ac_formats.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-26.2.2/src/amd/common/ac_formats.c). The reverse orders (`COMP_SWAP` 2 and 3) keep throwing when blending, and simultaneous targets requiring conflicting blend constants throw. Console equivalence has not been measured on hardware. - [UINT16_ABGR color exports](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - export format 7 is accepted only into an unsigned integer target, and each 16-bit value reaches the target unchanged; a value beyond a narrower target's range (`R8_UINT`) is undefined in Vulkan, and whether the console clamps or truncates it is not confirmed. SINT16_ABGR (8) throws From 9f96444c4a511d64fb7efd184dafe12de3fa66b9 Mon Sep 17 00:00:00 2001 From: fdfawef324 Date: Wed, 14 Oct 2026 17:12:13 -0400 Subject: [PATCH 0179/1000] feat(libSceNpManager): register NP state callbacks and report signed-out from sceNpCheckCallback sceNpRegisterStateCallback and sceNpRegisterStateCallbackA stored nothing, the unregister calls always succeeded and sceNpCheckCallback did nothing, so a title that waits for its NP state callback never learned the user's state. #1277 reports that Minecraft Preview (PPSA19639) needs the callback (stop 2). sceNpRegisterStateCallbackA now keeps up to 8 callbacks and returns the slot plus 1. A null callback returns INVALID_ARGUMENT, a callback that is already registered CALLBACK_ALREADY_REGISTERED and a ninth one CALLBACK_MAX (0x8055001D). sceNpUnregisterStateCallbackA returns INVALID_ARGUMENT for an id outside 1..8 and CALLBACK_NOT_REGISTERED for an empty slot. The legacy sceNpRegisterStateCallback keeps one callback with the same errors. The rules follow shadPS4 src/core/libraries/np/np_manager.cpp @15abf68c9b52 (RegisterStateCallbackA and UnregisterStateCallbackAById L857-L901, the legacy pair L981-L1005, ORBIS_NP_STATE_CALLBACK_MAX L36) and np_error.h L38. Each registration is marked pending. sceNpCheckCallback takes the pending registrations under the lock and calls them after releasing it, on the calling thread, with SIGNED_OUT for the initial user 0x10000000. For the A form this follows KytyPS5 src/libs/network.cpp @9e41d8320a27 (NpRegisterStateCallbackA L4021-L4034 marks the registration pending, NpCheckCallback L3959-L3974 delivers it once). KytyPS5 never calls the legacy callback (NpRegisterStateCallback and NpUnregisterStateCallback, L4009-L4019, do nothing). The legacy callback is called as (user, SIGNED_OUT, nullptr, userdata), as in shadPS4, which passes an NpId only for SIGNED_IN (L823, L938-L940). shadPS4 queues SIGNED_OUT from the user service login event instead (DispatchPendingNpStateCallbacks L903-L962), and the same event calls the reachability callback once with UNAVAILABLE (L922-L931, L959-L961). The reachability callback is still not called here. These differences and the unmeasured console behaviour are recorded in docs/dev/debt/libSceNpManager.md. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceNpManager/Export.cpp | 68 +++++++++-- core/libs/tests/GuestNpManager.cpp | 141 +++++++++++++++++++++++ docs/dev/debt/libSceNpManager.md | 1 + 3 files changed, 203 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libSceNpManager/Export.cpp b/core/libs/prx/libSceNpManager/Export.cpp index a384c22023..0662f51026 100644 --- a/core/libs/prx/libSceNpManager/Export.cpp +++ b/core/libs/prx/libSceNpManager/Export.cpp @@ -2,6 +2,8 @@ #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libSceUserService/UserService.hpp" +#include #include #include #include @@ -13,6 +15,7 @@ static constexpr int SCE_NP_ERROR_USER_NOT_FOUND = static_cast(0x80550007); static constexpr int SCE_NP_ERROR_CALLBACK_ALREADY_REGISTERED = static_cast(0x80550008); static constexpr int SCE_NP_ERROR_CALLBACK_NOT_REGISTERED = static_cast(0x80550009); static constexpr int SCE_NP_ERROR_REQUEST_NOT_FOUND = static_cast(0x80550014); +static constexpr int SCE_NP_ERROR_CALLBACK_MAX = static_cast(0x8055001D); static constexpr uint32_t NP_STATE_SIGNED_OUT = 1; static constexpr int NP_POLL_ASYNC_FINISHED = 0; static constexpr uint32_t NP_REACHABILITY_STATE_UNAVAILABLE = 0; @@ -32,6 +35,27 @@ int AddRequest() { liveRequests.insert(reqId); return reqId; } + +using NpStateCallbackA = void (APS5_VABI *)(int userId, uint32_t state, void* userdata); +using NpStateCallback = void (APS5_VABI *)(int userId, uint32_t state, NpId* npId, void* userdata); + +constexpr int NP_STATE_CALLBACK_MAX = 8; + +struct StateCallbackA { + NpStateCallbackA callback = nullptr; + void* userdata = nullptr; + bool pending = false; +}; + +struct StateCallback { + NpStateCallback callback = nullptr; + void* userdata = nullptr; + bool pending = false; +}; + +std::mutex stateMutex; +std::array stateCallbacksA; +StateCallback stateCallback; } extern "C" { @@ -42,6 +66,19 @@ int APS5_VABI sceNpAbortRequest(int req_id) { } int APS5_VABI sceNpCheckCallback(void) { + std::array callbacksA; + StateCallback legacy; + { + std::lock_guard lock(stateMutex); + callbacksA = stateCallbacksA; + legacy = stateCallback; + for (auto& entry : stateCallbacksA) entry.pending = false; + stateCallback.pending = false; + } + if (legacy.pending) legacy.callback(USER_SERVICE_INITIAL_USER_ID, NP_STATE_SIGNED_OUT, nullptr, legacy.userdata); + for (const auto& entry : callbacksA) { + if (entry.pending) entry.callback(USER_SERVICE_INITIAL_USER_ID, NP_STATE_SIGNED_OUT, entry.userdata); + } return 0; } @@ -172,9 +209,11 @@ int APS5_VABI sceNpRegisterPremiumEventCallback(void* callback, void* userdata) return 0; } -int APS5_VABI sceNpRegisterStateCallback(void* callback, void* userdata) { - (void)userdata; +int APS5_VABI sceNpRegisterStateCallback(NpStateCallback callback, void* userdata) { if (!callback) return SCE_NP_ERROR_INVALID_ARGUMENT; + std::lock_guard lock(stateMutex); + if (stateCallback.callback) return SCE_NP_ERROR_CALLBACK_ALREADY_REGISTERED; + stateCallback = {callback, userdata, true}; return 0; } @@ -208,6 +247,9 @@ int APS5_VABI sceNpSetTimeout(int reqId, int32_t resolveRetry, uint32_t resolveT } int APS5_VABI sceNpUnregisterStateCallback(void) { + std::lock_guard lock(stateMutex); + if (!stateCallback.callback) return SCE_NP_ERROR_CALLBACK_NOT_REGISTERED; + stateCallback = {}; return 0; } @@ -223,14 +265,26 @@ int APS5_VABI sceNpNotifyPremiumFeature(const void* param) { return 0; } -int APS5_VABI sceNpRegisterStateCallbackA(void* callback, void* userdata) { - (void)userdata; +int APS5_VABI sceNpRegisterStateCallbackA(NpStateCallbackA callback, void* userdata) { if (!callback) return SCE_NP_ERROR_INVALID_ARGUMENT; - return 1; + std::lock_guard lock(stateMutex); + for (const auto& entry : stateCallbacksA) { + if (entry.callback == callback) return SCE_NP_ERROR_CALLBACK_ALREADY_REGISTERED; + } + for (int index = 0; index < NP_STATE_CALLBACK_MAX; ++index) { + if (stateCallbacksA[index].callback) continue; + stateCallbacksA[index] = {callback, userdata, true}; + return index + 1; + } + return SCE_NP_ERROR_CALLBACK_MAX; } -int APS5_VABI sceNpUnregisterStateCallbackA(int callback_id) { - (void)callback_id; +int APS5_VABI sceNpUnregisterStateCallbackA(int callbackId) { + if (callbackId < 1 || callbackId > NP_STATE_CALLBACK_MAX) return SCE_NP_ERROR_INVALID_ARGUMENT; + std::lock_guard lock(stateMutex); + auto& entry = stateCallbacksA[callbackId - 1]; + if (!entry.callback) return SCE_NP_ERROR_CALLBACK_NOT_REGISTERED; + entry = {}; return 0; } diff --git a/core/libs/tests/GuestNpManager.cpp b/core/libs/tests/GuestNpManager.cpp index eaa8a77d45..246c48367e 100644 --- a/core/libs/tests/GuestNpManager.cpp +++ b/core/libs/tests/GuestNpManager.cpp @@ -17,10 +17,22 @@ int APS5_VABI sceNpSetTimeout(int reqId, std::int32_t resolveRetry, std::uint32_ std::uint32_t connTimeout, std::uint32_t sendTimeout, std::uint32_t recvTimeout); } +extern "C" { +int APS5_VABI sceNpCheckCallback(void); +int APS5_VABI sceNpRegisterStateCallback(void* callback, void* userdata); +int APS5_VABI sceNpUnregisterStateCallback(void); +int APS5_VABI sceNpRegisterStateCallbackA(void* callback, void* userdata); +int APS5_VABI sceNpUnregisterStateCallbackA(int callbackId); +int APS5_VABI sceNpRegisterNpReachabilityStateCallback(void* callback, void* userdata); +int APS5_VABI sceNpUnregisterNpReachabilityStateCallback(void); +} + namespace { constexpr int InvalidArgument = static_cast(0x80550003u); constexpr int SignedOut = static_cast(0x80550006u); +constexpr int AlreadyRegistered = static_cast(0x80550008u); +constexpr int NotRegistered = static_cast(0x80550009u); constexpr int RequestNotFound = static_cast(0x80550014u); void Require(bool condition, const char* message) { @@ -30,6 +42,133 @@ void Require(bool condition, const char* message) { } } +struct StateCall { + int calls; + int userId; + std::uint32_t state; + NpId* npId; + void* userdata; +}; + +constexpr int InitialUserId = 0x10000000; +constexpr std::uint32_t StateSignedOut = 1; +StateCall stateCallsA[9]; +StateCall legacyStateCall; + +template +void APS5_VABI StateCallbackA(int userId, std::uint32_t state, void* userdata) { + stateCallsA[Index] = {stateCallsA[Index].calls + 1, userId, state, nullptr, userdata}; +} + +void APS5_VABI LegacyStateCallback(int userId, std::uint32_t state, NpId* npId, void* userdata) { + legacyStateCall = {legacyStateCall.calls + 1, userId, state, npId, userdata}; +} + +bool SignedOutOnce(const StateCall& call, void* userdata) { + return call.calls == 1 && call.userId == InitialUserId && call.state == StateSignedOut && + call.npId == nullptr && call.userdata == userdata; +} + +void APS5_VABI AbortingReachabilityCallback(int, std::uint32_t, void*) { + std::fprintf(stderr, "NpManager: the reachability callback was called\n"); + std::abort(); +} + +struct SelfUnregister { + int id; + int calls; + int result; +}; + +void APS5_VABI SelfUnregisteringCallback(int, std::uint32_t, void* userdata) { + auto* context = static_cast(userdata); + ++context->calls; + context->result = sceNpUnregisterStateCallbackA(context->id); +} + +void CheckStateCallbacksA() { + constexpr int CallbackMax = static_cast(0x8055001Du); + void* const callbacks[] = { + reinterpret_cast(&StateCallbackA<0>), reinterpret_cast(&StateCallbackA<1>), + reinterpret_cast(&StateCallbackA<2>), reinterpret_cast(&StateCallbackA<3>), + reinterpret_cast(&StateCallbackA<4>), reinterpret_cast(&StateCallbackA<5>), + reinterpret_cast(&StateCallbackA<6>), reinterpret_cast(&StateCallbackA<7>), + reinterpret_cast(&StateCallbackA<8>), + }; + int userdata[9]{}; + Require(sceNpRegisterStateCallbackA(nullptr, &userdata[0]) == InvalidArgument, "null state callback"); + int ids[8]{}; + ids[0] = sceNpRegisterStateCallbackA(callbacks[0], &userdata[0]); + Require(ids[0] >= 1 && ids[0] <= 8, "state callback id outside 1..8"); + Require(sceNpRegisterStateCallbackA(callbacks[0], &userdata[1]) == AlreadyRegistered, "duplicate state callback"); + Require(stateCallsA[0].calls == 0, "registering calls the state callback"); + Require(sceNpRegisterNpReachabilityStateCallback(reinterpret_cast(&AbortingReachabilityCallback), nullptr) == 0, + "registering the reachability callback"); + Require(sceNpCheckCallback() == 0, "sceNpCheckCallback failed"); + Require(SignedOutOnce(stateCallsA[0], &userdata[0]), "sceNpCheckCallback must deliver SIGNED_OUT once"); + Require(sceNpUnregisterNpReachabilityStateCallback() == 0, "unregistering the reachability callback"); + Require(sceNpCheckCallback() == 0, "second sceNpCheckCallback failed"); + Require(stateCallsA[0].calls == 1, "the second sceNpCheckCallback delivered the state again"); + for (int index = 1; index < 8; ++index) { + ids[index] = sceNpRegisterStateCallbackA(callbacks[index], &userdata[index]); + Require(ids[index] >= 1 && ids[index] <= 8, "state callback id outside 1..8"); + for (int other = 0; other < index; ++other) { + Require(ids[index] != ids[other], "two state callbacks got the same id"); + } + } + Require(sceNpRegisterStateCallbackA(callbacks[8], &userdata[8]) == CallbackMax, "ninth state callback"); + Require(sceNpCheckCallback() == 0, "sceNpCheckCallback failed with eight callbacks"); + for (int index = 0; index < 8; ++index) { + Require(SignedOutOnce(stateCallsA[index], &userdata[index]), "each callback gets SIGNED_OUT once"); + } + Require(stateCallsA[8].calls == 0, "a rejected callback was called"); + Require(sceNpUnregisterStateCallbackA(0) == InvalidArgument, "callback id 0"); + Require(sceNpUnregisterStateCallbackA(9) == InvalidArgument, "callback id 9"); + Require(sceNpUnregisterStateCallbackA(ids[3]) == 0, "unregistering one state callback"); + for (int index = 0; index < 8; ++index) { + if (index == 3) continue; + Require(sceNpRegisterStateCallbackA(callbacks[index], &userdata[index]) == AlreadyRegistered, + "unregistering one id removed another callback"); + } + Require(sceNpRegisterStateCallbackA(callbacks[3], &userdata[3]) == ids[3], "registering again takes the freed slot"); + Require(sceNpCheckCallback() == 0, "sceNpCheckCallback failed after registering again"); + for (int index = 0; index < 8; ++index) { + Require(stateCallsA[index].calls == (index == 3 ? 2 : 1), "only the registration made again gets SIGNED_OUT"); + } + for (int index = 0; index < 8; ++index) { + Require(sceNpUnregisterStateCallbackA(ids[index]) == 0, "unregistering a state callback"); + } + Require(sceNpUnregisterStateCallbackA(ids[0]) == NotRegistered, "unregistering a state callback twice"); + const int removed = sceNpRegisterStateCallbackA(callbacks[8], &userdata[8]); + Require(removed >= 1 && removed <= 8, "a freed slot is reused"); + Require(sceNpUnregisterStateCallbackA(removed) == 0, "unregistering before sceNpCheckCallback"); + Require(sceNpCheckCallback() == 0, "sceNpCheckCallback failed with no callbacks"); + Require(stateCallsA[8].calls == 0, "an unregistered callback was called"); + SelfUnregister self{}; + self.id = sceNpRegisterStateCallbackA(reinterpret_cast(&SelfUnregisteringCallback), &self); + Require(self.id >= 1 && self.id <= 8, "state callback id outside 1..8"); + Require(sceNpCheckCallback() == 0, "sceNpCheckCallback failed with a callback that unregisters itself"); + Require(self.calls == 1 && self.result == 0, "a state callback must be able to unregister itself"); + Require(sceNpUnregisterStateCallbackA(self.id) == NotRegistered, "the callback did not unregister itself"); +} + +void CheckLegacyStateCallback() { + int userdata = 0; + NpId sentinel{}; + auto* callback = reinterpret_cast(&LegacyStateCallback); + Require(sceNpUnregisterStateCallback() == NotRegistered, "unregistering with no legacy callback"); + Require(sceNpRegisterStateCallback(nullptr, &userdata) == InvalidArgument, "null legacy callback"); + Require(sceNpRegisterStateCallback(callback, &userdata) == 0, "registering the legacy callback"); + Require(sceNpRegisterStateCallback(callback, nullptr) == AlreadyRegistered, "duplicate legacy callback"); + legacyStateCall.npId = &sentinel; + Require(sceNpCheckCallback() == 0, "sceNpCheckCallback failed with the legacy callback"); + Require(SignedOutOnce(legacyStateCall, &userdata), "the legacy callback must get SIGNED_OUT with no NpId"); + Require(sceNpCheckCallback() == 0, "second sceNpCheckCallback failed with the legacy callback"); + Require(legacyStateCall.calls == 1, "the legacy callback got the state again"); + Require(sceNpUnregisterStateCallback() == 0, "unregistering the legacy callback"); + Require(sceNpUnregisterStateCallback() == NotRegistered, "unregistering the legacy callback twice"); +} + void CheckRequestTimeouts() { Require(sceNpSetTimeout(0x7fffffff, 2, 0, 0, 0, 0) == RequestNotFound, "never created request"); Require(sceNpSetTimeout(0x7fffffff, 0, 0, 0, 0, 0) == InvalidArgument, "arguments checked before the request"); @@ -59,6 +198,8 @@ void CheckRequestTimeouts() { } int main() { + CheckStateCallbacksA(); + CheckLegacyStateCallback(); CheckRequestTimeouts(); NpId npId{}; std::memset(&npId, 0x5a, sizeof(npId)); diff --git a/docs/dev/debt/libSceNpManager.md b/docs/dev/debt/libSceNpManager.md index cb0212dd00..18d9bc1995 100644 --- a/docs/dev/debt/libSceNpManager.md +++ b/docs/dev/debt/libSceNpManager.md @@ -9,3 +9,4 @@ - [NP reachability callbacks](../../../core/libs/prx/libSceNpManager/Export.cpp) retain one callback and its user data; duplicate registration and unregistering an absent callback return the errors used by [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/945dbc3cc3eee80ac3e053b438502ed936fa6bb2/src/core/libraries/np/np_manager.cpp). No callback is delivered because the existing NP model remains signed out with reachability unavailable. - [sceNpCheckNpAvailability](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager) - the `(req_id, online_id)` signature and the errors follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0fe263a4760dfbfa973366890061749b4af0de97/src/core/libraries/np/np_manager.cpp) with no user signed in to PSN: a null online id is `SCE_NP_ERROR_INVALID_ARGUMENT`, and any other online id matches no user, `SCE_NP_ERROR_USER_NOT_FOUND`; the request id is not checked, as in the other NpManager requests +- [NP state callbacks](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager) - the registration rules are [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/np/np_manager.cpp#L857-L901)'s: `sceNpRegisterStateCallbackA` keeps up to 8 callbacks and returns the slot plus 1, a null callback returns `INVALID_ARGUMENT`, a callback already registered `CALLBACK_ALREADY_REGISTERED` and a ninth one `CALLBACK_MAX` (0x8055001D, [np_error.h](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/np/np_error.h#L38)); `sceNpUnregisterStateCallbackA` returns `INVALID_ARGUMENT` for an id outside 1..8 and `CALLBACK_NOT_REGISTERED` for an empty slot; the legacy [sceNpRegisterStateCallback](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/np/np_manager.cpp#L981-L1005) keeps one callback with the same errors. Each registration gets `SIGNED_OUT` for the initial user (0x10000000) once, from the next `sceNpCheckCallback` on the calling thread. For the A form this follows [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/9e41d8320a2774b303fbad344c2347e8d0292e83/src/libs/network.cpp#L3959-L3974), whose [sceNpRegisterStateCallbackA](https://github.com/KytyPS5/KytyPS5/blob/9e41d8320a2774b303fbad344c2347e8d0292e83/src/libs/network.cpp#L4021-L4034) marks the registration pending. KytyPS5 never calls the legacy callback ([sceNpRegisterStateCallback](https://github.com/KytyPS5/KytyPS5/blob/9e41d8320a2774b303fbad344c2347e8d0292e83/src/libs/network.cpp#L4009-L4019) stores nothing); it is called as `(user, SIGNED_OUT, nullptr, userdata)`, as in shadPS4, which passes an NpId only for `SIGNED_IN` ([L823](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/np/np_manager.cpp#L823), [L938-L940](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/np/np_manager.cpp#L938-L940)). shadPS4 instead queues `SIGNED_OUT` from the user service login event and delivers it from `sceNpCheckCallback` ([DispatchPendingNpStateCallbacks](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/np/np_manager.cpp#L903-L962)); the reachability callback is not given an initial `UNAVAILABLE`, unlike that function ([L922-L961](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/np/np_manager.cpp#L922-L961)). A callback being delivered when another thread unregisters it is still called once. When and how often the console calls the state callbacks, and the errors, are not measured From 7a86e5ae32655bf0a70d8eff35167e28f8a860bc Mon Sep 17 00:00:00 2001 From: David Date: Thu, 15 Oct 2026 02:15:26 +0400 Subject: [PATCH 0180/1000] feat(libSceVideoOut): export the hardware cursor functions as not implemented sceVideoOutCursorEnable, sceVideoOutCursorDisable, sceVideoOutCursorSetImageAddress and sceVideoOutCursorSetPosition are imported by Prospero Win's (PPSA99995) Vulkan module, so on Windows, where imports are bound at load, the title cannot load without them. They report that they are not implemented when called. --- .../libs/prx/libSceVideoOut/Unimplemented.cpp | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/core/libs/prx/libSceVideoOut/Unimplemented.cpp b/core/libs/prx/libSceVideoOut/Unimplemented.cpp index ef58c55add..2205809941 100644 --- a/core/libs/prx/libSceVideoOut/Unimplemented.cpp +++ b/core/libs/prx/libSceVideoOut/Unimplemented.cpp @@ -17,6 +17,26 @@ int APS5_VABI sceVideoOutClearFlipMaster() { return 0; } +int APS5_VABI sceVideoOutCursorEnable(int32_t, int32_t, const void*) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVideoOutCursorDisable(int32_t, int32_t) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVideoOutCursorSetImageAddress(int32_t, int32_t, const void*) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVideoOutCursorSetPosition(int32_t, int32_t, uint32_t, uint32_t) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + int APS5_VABI sceVideoOutAdjustColor_(int handle, const VideoOutColorSettings* settings, uint32_t settings_size) { if (settings_size < sizeof(VideoOutColorSettings)) throw std::runtime_error("sceVideoOutAdjustColor_: VIDEO_OUT_ERROR_INVALID_VALUE"); return sceVideoOutAdjustColor(handle, settings); From e8d32bbc2db839aaf4de703073fde971c35f3b4a Mon Sep 17 00:00:00 2001 From: David Date: Thu, 15 Oct 2026 02:16:48 +0400 Subject: [PATCH 0181/1000] test(libSceVideoOut): check that the cursor functions report they are not implemented guest_video_out_cursor calls each of the four cursor functions with a handle, a cursor index, an image and a position, and requires the "not implemented" error. Without the exports it does not link. --- core/libs/prx/libSceVideoOut/CMakeLists.txt | 10 ++++++ core/libs/tests/GuestVideoOutCursor.cpp | 35 +++++++++++++++++++++ 2 files changed, 45 insertions(+) create mode 100644 core/libs/tests/GuestVideoOutCursor.cpp diff --git a/core/libs/prx/libSceVideoOut/CMakeLists.txt b/core/libs/prx/libSceVideoOut/CMakeLists.txt index e216b43fb4..08c199ab5c 100644 --- a/core/libs/prx/libSceVideoOut/CMakeLists.txt +++ b/core/libs/prx/libSceVideoOut/CMakeLists.txt @@ -71,6 +71,16 @@ if(BUILD_TESTING) set_tests_properties(guest_video_out_high_frame_rate PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_video_out_cursor_tests ${LIBS_INCLUDE_DIR}/tests/GuestVideoOutCursor.cpp) + target_include_directories(guest_video_out_cursor_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_video_out_cursor_tests PRIVATE libSceVideoOut libkernel libc) + configure_windows_unwind(guest_video_out_cursor_tests) + add_test(NAME guest_video_out_cursor COMMAND guest_video_out_cursor_tests) + set_tests_properties(guest_video_out_cursor PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_video_out_cursor PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() set(ANYPS5_VIDEO_OUT_TEST_DRIVER "offscreen" CACHE STRING "SDL video driver for VideoOut flip tests") diff --git a/core/libs/tests/GuestVideoOutCursor.cpp b/core/libs/tests/GuestVideoOutCursor.cpp new file mode 100644 index 0000000000..214293bdd2 --- /dev/null +++ b/core/libs/tests/GuestVideoOutCursor.cpp @@ -0,0 +1,35 @@ +#include "prx/libc/include/general/VabiMacros.hpp" + +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceVideoOutCursorEnable(std::int32_t handle, std::int32_t index, const void* address); +int APS5_VABI sceVideoOutCursorDisable(std::int32_t handle, std::int32_t index); +int APS5_VABI sceVideoOutCursorSetImageAddress(std::int32_t handle, std::int32_t index, const void* address); +int APS5_VABI sceVideoOutCursorSetPosition(std::int32_t handle, std::int32_t index, std::uint32_t x, std::uint32_t y); +} + +static bool ReportsNotImplemented(const std::string& name, const std::function& call) { + try { + call(); + } catch (const std::runtime_error& error) { + if (error.what() == name + " not implemented") return true; + std::fprintf(stderr, "%s threw: %s\n", name.c_str(), error.what()); + return false; + } + std::fprintf(stderr, "%s returned instead of reporting that it is not implemented\n", name.c_str()); + return false; +} + +int main() { + static const std::uint32_t image[64 * 64] = {}; + bool passed = ReportsNotImplemented("sceVideoOutCursorEnable", [] { return sceVideoOutCursorEnable(1, 0, image); }); + passed = ReportsNotImplemented("sceVideoOutCursorSetImageAddress", [] { return sceVideoOutCursorSetImageAddress(1, 0, image); }) && passed; + passed = ReportsNotImplemented("sceVideoOutCursorSetPosition", [] { return sceVideoOutCursorSetPosition(1, 0, 960, 540); }) && passed; + passed = ReportsNotImplemented("sceVideoOutCursorDisable", [] { return sceVideoOutCursorDisable(1, 0); }) && passed; + return passed ? 0 : 1; +} From b36f6f32602059c2975de146f62296900e902d7f Mon Sep 17 00:00:00 2001 From: YatoVoid Date: Wed, 14 Oct 2026 17:22:06 -0500 Subject: [PATCH 0182/1000] test(agc): disable the shader disk cache in agc_shader_memory_tests --- core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 261c21e612..1239ee3ab3 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -4932,6 +4932,11 @@ void verifySrgbDepthCompareModes() { int main(int argc, char** argv) { try { +#ifdef _WIN32 + _putenv_s("ANYPS5_NO_SHADER_CACHE", "1"); +#else + setenv("ANYPS5_NO_SHADER_CACHE", "1", 1); +#endif using namespace ShaderRecompiler; if (argc == 2 && std::string_view(argv[1]) == "--frontend-pair") { verifyFrontendPair(); From fcd477faf8fd0b4959f927db1f36590b317fa2b7 Mon Sep 17 00:00:00 2001 From: ArjenS-LA Date: Wed, 14 Oct 2026 15:35:59 -0700 Subject: [PATCH 0183/1000] fix(tools): normalize qualified ELF module exports --- tools/import_audit.py | 2 +- tools/tests/test_import_audit.py | 27 +++++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/tools/import_audit.py b/tools/import_audit.py index bc819fc9bf..993ba42611 100644 --- a/tools/import_audit.py +++ b/tools/import_audit.py @@ -182,7 +182,7 @@ def elf_exports(path, data): binding, visibility = info >> 4, other & 3 if name == 0 or index == 0 or binding not in (1, 2, 10) or visibility not in (0, 3): continue - names.add(cstring(data, strings_offset + name, strings_offset + strings_size, path, "export name")) + names.add(cstring(data, strings_offset + name, strings_offset + strings_size, path, "export name").split(MODULE_SUFFIX, 1)[0]) return names diff --git a/tools/tests/test_import_audit.py b/tools/tests/test_import_audit.py index 2b6bf042f6..ec05e0e371 100644 --- a/tools/tests/test_import_audit.py +++ b/tools/tests/test_import_audit.py @@ -635,6 +635,33 @@ class ImportAuditTests(unittest.TestCase): self.assertEqual(data["unique_by_class"], {"implemented": 1, "stub": 0, "absent": 0, "module": 0}) self.assertEqual(data["references"], 1) + def test_module_export_suffix_is_stripped_with_and_without_sections(self): + work = self.work + nid = nid_names.compute_nid("sceModuleCall") + (work.libs / "libSceA.prx").write_bytes(exporting()) + (work.modules / "consumer.prx").write_bytes(build_elf([ + (nid + "#import#module", 0, GLOBAL_FUNCTION)])) + provider = work.modules / "provider.prx" + registry, result = work.root / "game.json", work.root / "audit.json" + write_registry(registry, []) + symbols = [(nid + "#export#module", 1, GLOBAL_FUNCTION)] + for layout, image in (("sections", build_elf(symbols)), + ("sysv", build_sectionless_elf(symbols)), + ("gnu", build_sectionless_elf(symbols, gnu_hash=True))): + with self.subTest(layout=layout): + provider.write_bytes(image) + code, out, err = work.run(registry, "--json", str(result)) + self.assertEqual((code, err), (0, "")) + record, = json.loads(result.read_text())["imports"] + self.assertEqual((record["nid"], record["class"], record["providers"]), + (nid, "implemented", ["provider.prx"])) + self.assertEqual(record["module"], "consumer.prx") + provider.write_bytes(exporting(nid + "other#export#module")) + code, out, err = work.run(registry, "--json", str(result)) + self.assertEqual((code, err), (1, "")) + record, = json.loads(result.read_text())["imports"] + self.assertEqual((record["nid"], record["class"], record["providers"]), (nid, "absent", [])) + def test_clean_audit_exits_zero_and_empty_registry_is_empty(self): work = self.work (work.libs / "libSceA.prx").write_bytes(exporting("AAAAAAAAAAA")) From 93b10d96476be306be390cf07a54531a94731fc2 Mon Sep 17 00:00:00 2001 From: ArjenS-LA Date: Wed, 14 Oct 2026 15:36:09 -0700 Subject: [PATCH 0184/1000] fix(release): reject dangling directory junction destinations --- tools/package_release.py | 8 ++++++-- tools/tests/test_package_release.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/tools/package_release.py b/tools/package_release.py index 72c23905fe..53e1fdcc29 100644 --- a/tools/package_release.py +++ b/tools/package_release.py @@ -43,8 +43,12 @@ def package(platform, build, output, version): destination = output / name if destination.exists() and not destination.is_file(): raise RuntimeError(f"Release destination is not a file: {destination}") - if os.name == "nt" and os.path.lexists(destination) and destination.lstat().st_file_attributes & stat.FILE_ATTRIBUTE_READONLY: - raise RuntimeError(f"Release destination is read-only: {destination}") + if os.name == "nt" and os.path.lexists(destination): + attributes = destination.lstat().st_file_attributes + if attributes & stat.FILE_ATTRIBUTE_DIRECTORY: + raise RuntimeError(f"Release destination is not a file: {destination}") + if attributes & stat.FILE_ATTRIBUTE_READONLY: + raise RuntimeError(f"Release destination is read-only: {destination}") output.mkdir(parents=True, exist_ok=True) with tempfile.TemporaryDirectory(prefix=".release-", dir=output) as temporary: staging = Path(temporary) diff --git a/tools/tests/test_package_release.py b/tools/tests/test_package_release.py index 6c7b5f76cc..58d1c4bb39 100644 --- a/tools/tests/test_package_release.py +++ b/tools/tests/test_package_release.py @@ -2,6 +2,7 @@ import os import json import ctypes import stat +import subprocess import sys import tarfile import tempfile @@ -182,6 +183,33 @@ class ReleasePackageTests(unittest.TestCase): finally: destination.chmod(stat.S_IREAD | stat.S_IWRITE) + @unittest.skipUnless(os.name == "nt", "Windows directory junction replacement semantics") + def test_dangling_directory_junction_preserves_all_existing_assets(self): + names = ["prx-linux-v1.zip", "prx-linux-v1.tar.gz", "relinker-v1"] + for index, blocked in enumerate(names): + with self.subTest(blocked=blocked): + output = self.output / str(index) + output.mkdir(parents=True) + previous = {name: b"old " + name.encode() for name in names if name != blocked} + previous["unrelated.txt"] = b"unrelated" + for name, content in previous.items(): + (output / name).write_bytes(content) + target = self.root / f"target-{index}" + target.mkdir() + destination = output / blocked + subprocess.run(["cmd", "/c", "mklink", "/J", str(destination), str(target)], + check=True, capture_output=True) + self.addCleanup(destination.rmdir) + target.rmdir() + self.assertFalse(destination.exists()) + self.assertTrue(destination.lstat().st_file_attributes & stat.FILE_ATTRIBUTE_DIRECTORY) + with self.assertRaises((OSError, RuntimeError)) as error: + package_release.package("linux", self.build, output, "v1") + self.assertEqual({path.name: path.read_bytes() for path in output.iterdir() if path != destination}, previous) + self.assertTrue(destination.lstat().st_file_attributes & stat.FILE_ATTRIBUTE_DIRECTORY) + self.assertFalse(target.exists()) + self.assertIn("not a file", str(error.exception)) + def file_link(self, link, target): try: link.symlink_to(target) From 62b09018d4676bf385d4744e982f13e330416359 Mon Sep 17 00:00:00 2001 From: ArjenS-LA Date: Wed, 14 Oct 2026 15:36:25 -0700 Subject: [PATCH 0185/1000] fix(relinker): reject needed companion output collisions --- core/relinker/main.cpp | 1 + .../relinker/tests/test_output_collisions.py | 32 ++++++++++++++++++- .../tests/test_secondary_output_collisions.py | 3 +- 3 files changed, 34 insertions(+), 2 deletions(-) diff --git a/core/relinker/main.cpp b/core/relinker/main.cpp index 6b967356a5..1f143afb7f 100644 --- a/core/relinker/main.cpp +++ b/core/relinker/main.cpp @@ -290,6 +290,7 @@ int Run(const int argc, char* argv[]) { for (const auto& executable : executables) outputPaths.push_back(Io::NativePath(executable.OutputPath)); for (const auto& artifact : guestArtifacts) outputPaths.push_back(artifact.Path); outputPaths.insert(outputPaths.end(), registryPaths.begin(), registryPaths.end()); + outputPaths.insert(outputPaths.end(), neededPaths.begin(), neededPaths.end()); if (desktopEntry) { outputPaths.push_back(desktopEntry->IconPath); outputPaths.push_back(desktopEntry->EntryPath); diff --git a/core/relinker/relinker/tests/test_output_collisions.py b/core/relinker/relinker/tests/test_output_collisions.py index 2026412310..16466abcbe 100644 --- a/core/relinker/relinker/tests/test_output_collisions.py +++ b/core/relinker/relinker/tests/test_output_collisions.py @@ -25,6 +25,8 @@ def prepare(case, windows): "module": case / "app0/sce_module/sample.prx.guest.prx", "registry": case / "output.registry.json", "module-registry": case / "output.sample.prx.guest.prx.registry.json", + "needed": case / "output.needed.json", + "module-needed": case / "output.sample.prx.guest.prx.needed.json", } if windows: paths["diagnostics"] = case / "windows-diagnostics-imports.txt" @@ -53,6 +55,34 @@ def main(): checked = 0 with tempfile.TemporaryDirectory(prefix="anyps5-output-collisions-") as directory: work = Path(directory) + for windows, existing in itertools.product((False, True), repeat=2): + case = work / f"needed-secondary-{windows}-{existing}" + case.mkdir() + source = case / "input.elf" + secondary = case / "output.needed" + source.write_bytes(main_fixture()) + secondary.write_bytes(main_fixture()) + module = case / "sce_module/sample.prx" + module.parent.mkdir() + module.write_bytes(guest_fixture(PLAIN_SITE)) + outputs = [case / name for name in ("output.json", "output.needed.json", "output.registry.json", + "output.needed.registry.json", "output.needed.needed.json", + "app0/sce_module/sample.prx.guest.prx", + "output.sample.prx.guest.prx.registry.json", + "output.sample.prx.guest.prx.needed.json")] + if existing: + for path in outputs: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_bytes(b"previous output") + result = subprocess.run([str(relinker), *(["--windows"] if windows else []), "--registry", + "--executable", secondary.name, str(source), str(outputs[0])], cwd=case, + capture_output=True, text=True, timeout=20) + assert result.returncode == 2 and "Output paths collide" in result.stderr, ( + windows, existing, result.returncode, result.stdout, result.stderr) + for path in outputs: + assert path.read_bytes() == b"previous output" if existing else not path.exists(), path + assert source.read_bytes() == secondary.read_bytes() == main_fixture() + assert module.read_bytes() == guest_fixture(PLAIN_SITE) for windows in (False, True): command, paths, inputs = prepare(work / f"{windows}-control", windows) links = paths["executable"].parent / "separate-links" @@ -65,7 +95,7 @@ def main(): assert result.returncode == 0, (result.stdout, result.stderr) for name in ("executable", "module"): assert paths[name].read_bytes().startswith(b"MZ" if windows else b"\x7fELF") - for name in ("registry", "module-registry"): + for name in ("registry", "module-registry", "needed", "module-needed"): json.loads(paths[name].read_text()) for path, original in inputs.items(): assert path.read_bytes() == original diff --git a/core/relinker/relinker/tests/test_secondary_output_collisions.py b/core/relinker/relinker/tests/test_secondary_output_collisions.py index a0db558788..070536466c 100644 --- a/core/relinker/relinker/tests/test_secondary_output_collisions.py +++ b/core/relinker/relinker/tests/test_secondary_output_collisions.py @@ -20,6 +20,7 @@ def prepare_secondary(case, windows): options.extend(["--executable", source.name]) paths[name] = case / f"{source.name}{paths['executable'].suffix}" paths[f"{name}-registry"] = case / f"{source.name}.registry.json" + paths[f"{name}-needed"] = case / f"{source.name}.needed.json" if not windows: icon = case / "sce_sys/icon0.png" inputs[icon] = icon.read_bytes() @@ -46,7 +47,7 @@ def main(): for name in ("executable", "module", *sorted(secondary)): assert paths[name].read_bytes().startswith(b"MZ" if windows else b"\x7fELF"), paths[name] for name, path in paths.items(): - if "registry" in name: + if "registry" in name or "needed" in name: json.loads(path.read_text()) for path, original in inputs.items(): assert path.read_bytes() == original, path From e715f9855bdf4c051afd9aa6e3cdae7ed9311fb7 Mon Sep 17 00:00:00 2001 From: ArjenS-LA Date: Wed, 14 Oct 2026 15:37:10 -0700 Subject: [PATCH 0186/1000] fix(tools): accept compilation database argument arrays --- tools/check_compile.py | 10 +++++++++- tools/tests/test_check_compile.py | 32 +++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/tools/check_compile.py b/tools/check_compile.py index 1ec01a433c..a733048ba4 100644 --- a/tools/check_compile.py +++ b/tools/check_compile.py @@ -71,6 +71,12 @@ def changed(root, base): def syntax_only(entry, source): arguments = shlex.split(entry["command"]) directory = Path(entry["directory"]) + return syntax_arguments(arguments, directory, source) + + +def syntax_arguments(arguments, directory, source): + if not isinstance(arguments, list) or not arguments or any(not isinstance(argument, str) for argument in arguments) or not arguments[0]: + raise ValueError("arguments must be a nonempty list of strings with an executable") wanted = source.resolve() trimmed = [] index = 0 @@ -107,7 +113,9 @@ def check(root, build, files): print(f"{name}: no compile command in {build}", file=sys.stderr) continue checked += 1 - result = subprocess.run(syntax_only(entry, source), cwd=entry["directory"], + command = (syntax_arguments(entry["arguments"], Path(entry["directory"]), source) + if "arguments" in entry else syntax_only(entry, source)) + result = subprocess.run(command, cwd=entry["directory"], capture_output=True, text=True, encoding="utf-8", errors="replace") if result.returncode == 0: continue diff --git a/tools/tests/test_check_compile.py b/tools/tests/test_check_compile.py index b691d60d45..26e9c718dd 100644 --- a/tools/tests/test_check_compile.py +++ b/tools/tests/test_check_compile.py @@ -198,5 +198,37 @@ class CheckCompileTests(unittest.TestCase): self.assertIn("1 translation units compile", result.stdout) + def test_argument_arrays_preserve_tokens_and_take_precedence_over_command(self): + source = self.source("unit with spaces.cpp") + script = self.compiler( + "import json\nimport sys\nfrom pathlib import Path\n" + "Path(sys.argv[1]).write_text(json.dumps(sys.argv[2:]))\n") + seen = self.root / "arguments.json" + flags = ['-DNAME=quoted "value"', '-IC:\\sdk files\\include\\'] + arguments = [sys.executable, str(script), str(seen), *flags, + "-c", "-o", "unit.o", "-MD", "-MT", "unit.o", "-MF", "unit.o.d", + os.path.relpath(source, self.build)] + entry = {"directory": str(self.build), "file": str(source), "arguments": arguments} + for command in (None, "nonexistent-compiler -c unit.cpp"): + with self.subTest(command=command): + if command is not None: + entry["command"] = command + self.manifest([entry]) + result = self.tool(source.name) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(json.loads(seen.read_text(encoding="utf-8")), + [*flags, "-fsyntax-only", str(source)]) + + def test_invalid_argument_arrays_are_rejected_without_a_traceback(self): + source = self.source("unit.cpp") + for arguments in (None, [], "compiler -c unit.cpp", ["compiler", 3], [""]): + with self.subTest(arguments=arguments): + self.manifest([{"directory": str(self.build), "file": str(source), "arguments": arguments}]) + result = self.tool(source.name) + self.assertEqual(result.returncode, 2) + self.assertIn("arguments must be a nonempty list of strings with an executable", result.stderr) + self.assertNotIn("Traceback", result.stderr) + + if __name__ == "__main__": unittest.main() From 10bc2f05aff0a57e64a4e71a3195d59416e0da43 Mon Sep 17 00:00:00 2001 From: David Date: Thu, 15 Oct 2026 02:46:21 +0400 Subject: [PATCH 0187/1000] docs(debt): record where the cursor functions' signatures come from --- docs/dev/debt/libSceVideoOut.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/dev/debt/libSceVideoOut.md b/docs/dev/debt/libSceVideoOut.md index 248855971a..f6f21f6918 100644 --- a/docs/dev/debt/libSceVideoOut.md +++ b/docs/dev/debt/libSceVideoOut.md @@ -9,6 +9,7 @@ ### Unknown function info - [sceVideoOutDeleteVrrActiveStatusEvent](../../../core/libs/prx/libSceVideoOut/src/Event.cpp) (libSceVideoOut, NID `-q-vbO859Tw`) - the name hashes to the NID PPSA02530 imports and is listed in [sce_symbols](https://github.com/zecoxao/sce_symbols) `aerolib.csv` at `fe2cc2948e`. The signature `(eq, handle)` and the behaviour, removing the handle's VRR status registration from the queue as the other `Delete*Event` calls do, follow sceVideoOutAddVrrActiveStatusEvent; no title has been observed calling it +- [sceVideoOutCursorEnable, sceVideoOutCursorDisable, sceVideoOutCursorSetImageAddress, sceVideoOutCursorSetPosition](../../../core/libs/prx/libSceVideoOut/Unimplemented.cpp) (libSceVideoOut, NIDs `UPZWCH8qTM4`, `fEPEwROdCwM`, `u-9bhWQApq8`, `N17AK88NdD0`) - the names hash to the NIDs Prospero Win (PPSA99995) imports, and the parameter lists `(handle, index, address)`, `(handle, index)`, `(handle, index, address)` and `(handle, index, x, y)` follow its [declarations](https://github.com/mpereiraesaa/prospero-win/blob/644e32e400c555e340d507383398c5d21b39b0b0/wine/ps5/pw_vulkan_radv.c#L65-L68). The cursor image's format and size and the console's return values are not known; the functions report that they are not implemented. - [sceVideoOutGetOutputStatusWithVrr](../../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut, NID `gWT7X8H0bYs`) - `(int handle, void* status)`, the 80-byte layout and the return codes follow the clean-room description in #2746. Bytes 0-47 are `sceVideoOutGetOutputStatus`'s; bytes 48-53, which the console copies from an internal query whose fields are not established, are written as zero, as are the rest, so the 16-bit value at byte 52 never adjusts `refreshRate`. A null `status` returns `0x80290002` and an unknown handle `0x8029000B`. The console's earlier global-flag check (`0x8029001C`) and its object-field check (`0x8029001B`) test states that are not modelled and are never reported. - [sceVideoOutRegisterBuffers2](../../../core/libs/prx/libSceVideoOut/src/Buffer.cpp) (libSceVideoOut) - buffer option 8 (STRICT_COLORIMETRY) is presented as option 0, with the stored values unchanged; what the console changes for it is unknown. Only options 0 and 8 are accepted - [sceVideoOutAddBufferAttributeOption](../../../core/libs/prx/libSceVideoOut/src/Buffer.cpp) (libSceVideoOut, NID `MBlv8YWoyOk`) - the signature `(VideoOutBufferAttribute2*, uint64_t option)` comes from [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/VideoOut/VideoOut.cs#L174-L176) and the call site in PPSA25380, which passes 0x20 and ignores the result. The option is ORed into `attribute->option`; whether the console validates the bits, keeps them elsewhere or fails on an unknown option is unknown. A null attribute throws. Option 0x20 makes `sceVideoOutRegisterBuffers2` throw, as every option other than 0 and 8; what it selects for an overlay buffer is unknown From 3e98703fc8ca943e5c7f5dd494245aa3f8c694b5 Mon Sep 17 00:00:00 2001 From: yripper Date: Tue, 13 Oct 2026 02:20:31 -0300 Subject: [PATCH 0188/1000] feat(libc): reserve the guest arena on macOS macOS has no /proc/self/maps or MAP_FIXED_NOREPLACE, so the arena never came up and every guest allocation threw. Enumerate host regions with mach_vm_region and reserve free chunks with a fixed mach_vm_map, which fails instead of replacing. This skips dyld, the Rosetta runtime, and ranges the kernel never lets a process map (0x10_0000_0000-0x70_0000_0000). The POSIX commit/protection paths now build on macOS unchanged. Adds standalone guest_arena tests (x86_64 under Rosetta and arm64). --- core/libs/CMakeLists.txt | 1 + core/libs/prx/libc/src/GuestArena.cpp | 68 +++++++++-- core/libs/tests/GuestArenaHost.cpp | 125 +++++++++++++++++++++ core/libs/tests/guest_arena/CMakeLists.txt | 21 ++++ 4 files changed, 204 insertions(+), 11 deletions(-) create mode 100644 core/libs/tests/GuestArenaHost.cpp create mode 100644 core/libs/tests/guest_arena/CMakeLists.txt diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index 8385138261..6a4c4aa131 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -257,6 +257,7 @@ if(BUILD_TESTING) endif() if(APPLE) add_subdirectory(tests/write_watch) + add_subdirectory(tests/guest_arena) endif() if(WIN32) add_test_executable(windows_write_watch_tests tests/WindowsWriteWatch.cpp) diff --git a/core/libs/prx/libc/src/GuestArena.cpp b/core/libs/prx/libc/src/GuestArena.cpp index ca580fc5bd..008ad0da76 100644 --- a/core/libs/prx/libc/src/GuestArena.cpp +++ b/core/libs/prx/libc/src/GuestArena.cpp @@ -11,6 +11,10 @@ #include #include #endif +#ifdef __APPLE__ +#include +#include +#endif #include #include #include @@ -148,8 +152,8 @@ public: private: Arena() { -#if defined(__linux__) - SeedLinuxUsedRanges(); +#if defined(__linux__) || defined(__APPLE__) + SeedHostUsedRanges(); #elif defined(_WIN32) _writeWatched = std::getenv("APS5_NO_WRITE_WATCH") == nullptr; SYSTEM_INFO system{}; @@ -182,7 +186,8 @@ private: } #if defined(__linux__) - void SeedLinuxUsedRanges() { + static std::vector> HostMappings() { + std::vector> mappings; std::ifstream maps("/proc/self/maps"); if (!maps.is_open()) throw std::runtime_error("guest arena: cannot open /proc/self/maps"); std::string line; @@ -191,6 +196,51 @@ private: std::uintptr_t begin = 0, end = 0; char dash = 0; if (!(fields >> std::hex >> begin >> dash >> end) || dash != '-' || end <= begin) throw std::runtime_error("guest arena: malformed /proc/self/maps line: " + line); + mappings.emplace_back(begin, end); + } + return mappings; + } + + static int ReserveNoReplace(std::uintptr_t address, std::size_t bytes) { + void* const wanted = reinterpret_cast(address); + void* const placed = mmap(wanted, bytes, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE | MAP_FIXED_NOREPLACE, -1, 0); + if (placed == wanted) return 0; + if (placed == MAP_FAILED) return errno; + munmap(placed, bytes); + return EINVAL; + } +#elif defined(__APPLE__) + // macOS has no /proc/self/maps; dyld, the Rosetta runtime and ranges the kernel never lets a process + // map (max protection none, e.g. 0x10_0000_0000-0x70_0000_0000) all sit inside the PS5 application map. + static std::vector> HostMappings() { + std::vector> mappings; + mach_vm_address_t address = ArenaStart; + while (address < ApplicationAreaEnd) { + mach_vm_size_t size = 0; + vm_region_basic_info_data_64_t info{}; + mach_msg_type_number_t count = VM_REGION_BASIC_INFO_COUNT_64; + mach_port_t object = MACH_PORT_NULL; + const kern_return_t result = mach_vm_region(mach_task_self(), &address, &size, VM_REGION_BASIC_INFO_64, reinterpret_cast(&info), &count, &object); + if (result == KERN_INVALID_ADDRESS) break; + if (result != KERN_SUCCESS) throw std::runtime_error("guest arena: mach_vm_region failed"); + mappings.emplace_back(static_cast(address), static_cast(address + size)); + address += size; + } + return mappings; + } + + // MAP_FIXED_NOREPLACE does not exist on macOS; a fixed mach_vm_map fails instead of replacing. + static int ReserveNoReplace(std::uintptr_t address, std::size_t bytes) { + mach_vm_address_t wanted = address; + const kern_return_t result = mach_vm_map(mach_task_self(), &wanted, bytes, 0, VM_FLAGS_FIXED, MEMORY_OBJECT_NULL, 0, FALSE, VM_PROT_NONE, VM_PROT_ALL, VM_INHERIT_DEFAULT); + if (result == KERN_SUCCESS) return 0; + return result == KERN_NO_SPACE ? EEXIST : ENOMEM; + } +#endif + +#if defined(__linux__) || defined(__APPLE__) + void SeedHostUsedRanges() { + for (const auto& [begin, end] : HostMappings()) { if (end <= ArenaStart || begin >= ApplicationAreaEnd) continue; const auto first = std::max(begin, ArenaStart); const auto last = std::min(end, ApplicationAreaEnd); @@ -208,12 +258,8 @@ private: constexpr std::uintptr_t kChunk = 1ULL << 30; for (std::uintptr_t cursor = from; cursor < to; cursor += kChunk) { const auto end = std::min(cursor + kChunk, to); - void* const wanted = reinterpret_cast(cursor); - void* const placed = mmap(wanted, end - cursor, PROT_NONE, - MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE | MAP_FIXED_NOREPLACE, -1, 0); - if (placed == wanted) continue; - const int failed = placed == MAP_FAILED ? errno : EINVAL; - if (placed != MAP_FAILED) munmap(placed, end - cursor); + const int failed = ReserveNoReplace(cursor, end - cursor); + if (failed == 0) continue; if (failed != EEXIST) { char message[128]; std::snprintf(message, sizeof(message), "guest arena: cannot reserve 0x%llx+0x%llx", @@ -428,7 +474,7 @@ void GuestArenaForgetSection_nid_postfix(void* section) { } #endif -#if defined(__linux__) +#if defined(__linux__) || defined(__APPLE__) namespace { int PosixProtection(std::uint32_t windowsProtection) { switch (windowsProtection) { @@ -449,7 +495,7 @@ void* PlaceAt(void* pointer, std::size_t bytes, std::uint32_t protection) { } #endif -#if defined(__linux__) +#if defined(__linux__) || defined(__APPLE__) void GuestArenaSetProtection_nid_postfix(std::uintptr_t address, std::size_t bytes, std::uint32_t protection) { if (!Arena::Get().Contains(reinterpret_cast(address), bytes)) throw OutsideArena("set protection", reinterpret_cast(address), bytes); if (mprotect(reinterpret_cast(address), bytes, PosixProtection(protection)) != 0) diff --git a/core/libs/tests/GuestArenaHost.cpp b/core/libs/tests/GuestArenaHost.cpp new file mode 100644 index 0000000000..6378a14180 --- /dev/null +++ b/core/libs/tests/GuestArenaHost.cpp @@ -0,0 +1,125 @@ +#include "prx/libc/include/GuestArena.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#if !defined(__APPLE__) +#error These tests cover the macOS guest arena reservation +#endif + +using namespace GuestArena; + +namespace { + +constexpr std::uintptr_t ArenaStart = 0x0000000200000000ull; +constexpr std::uintptr_t ApplicationAreaEnd = 0x000000FC00000000ull; +constexpr std::uint32_t ProtectionNoAccess = 0x01; +constexpr std::uint32_t ProtectionReadOnly = 0x02; +constexpr std::uint32_t ProtectionReadWrite = 0x04; + +struct HostRegion { + std::uintptr_t start; + std::uintptr_t end; + vm_prot_t maxProtection; +}; + +void Require(bool condition, const char* message) { + if (!condition) throw std::runtime_error(message); +} + +std::vector RegionsInArenaRange() { + std::vector regions; + mach_vm_address_t address = ArenaStart; + while (address < ApplicationAreaEnd) { + mach_vm_size_t size = 0; + vm_region_basic_info_data_64_t info{}; + mach_msg_type_number_t count = VM_REGION_BASIC_INFO_COUNT_64; + mach_port_t object = MACH_PORT_NULL; + if (mach_vm_region(mach_task_self(), &address, &size, VM_REGION_BASIC_INFO_64, reinterpret_cast(&info), &count, &object) != KERN_SUCCESS) break; + if (address >= ApplicationAreaEnd) break; + regions.push_back({static_cast(address), static_cast(address + size), info.max_protection}); + address += size; + } + return regions; +} + +// The kernel keeps ranges inside the PS5 application map that no process may map (max protection none); +// the arena has to come up around them instead of giving up. +void CheckAvailable() { + Require(GuestArenaAvailable_nid_postfix(), "the guest arena is unavailable on macOS"); + std::uintptr_t base = 0; + std::size_t bytes = 0; + GuestArenaRange_nid_postfix(&base, &bytes); + Require(base == ArenaStart, "the guest arena does not start at the PS5 application map base"); + Require(base + bytes == ApplicationAreaEnd, "the guest arena does not end at the PS5 application map limit"); +} + +void CheckSkipsUnmappable() { + for (const auto& region : RegionsInArenaRange()) { + if (region.maxProtection != VM_PROT_NONE || region.end - region.start < 0x10000) continue; + void* placed = GuestArenaAllocateAtOrAbove_nid_postfix(region.start, 0x10000, 0x10000); + const auto address = reinterpret_cast(placed); + Require(address >= region.end, "an allocation landed inside a range the kernel never lets the process map"); + GuestArenaRelease_nid_postfix(placed, 0x10000); + return; + } + std::puts("no unmappable range inside the arena on this host; nothing to skip"); +} + +void CheckHostRegionsKept() { + std::size_t hostRegions = 0; + for (const auto& region : RegionsInArenaRange()) { + // Reservations made by the arena itself are no-access with full max protection. + if (region.maxProtection == VM_PROT_ALL) continue; + ++hostRegions; + bool rejected = false; + try { + GuestArenaMarkUsed_nid_postfix(reinterpret_cast(region.start), region.end - region.start); + } catch (const std::runtime_error&) { + rejected = true; + } + Require(rejected, "a fixed guest mapping over a host region was accepted"); + } + std::printf("%zu host regions inside the arena range are protected\n", hostRegions); +} + +void CheckCommitAndProtect() { + constexpr std::size_t bytes = 0x10000; + auto* pointer = static_cast(GuestArenaAllocate_nid_postfix(bytes, bytes)); + GuestArenaCommit_nid_postfix(pointer, bytes, ProtectionReadWrite, bytes); + pointer[0] = 0x5a; + pointer[bytes - 1] = 0xa5; + Require(pointer[0] == 0x5a && pointer[bytes - 1] == 0xa5, "committed guest memory does not keep writes"); + GuestArenaSetProtection_nid_postfix(reinterpret_cast(pointer), bytes, ProtectionReadOnly); + Require(pointer[0] == 0x5a, "read-only guest memory lost its contents"); + GuestArenaReset_nid_postfix(pointer, bytes); + GuestArenaSetProtection_nid_postfix(reinterpret_cast(pointer), bytes, ProtectionNoAccess); + GuestArenaRelease_nid_postfix(pointer, bytes); +} + +} + +int main(int argc, char** argv) { + try { + Require(argc == 2, "expected available, unmappable, host or commit"); + const std::string_view mode(argv[1]); + if (mode == "available") CheckAvailable(); + else if (mode == "unmappable") CheckSkipsUnmappable(); + else if (mode == "host") CheckHostRegionsKept(); + else if (mode == "commit") CheckCommitAndProtect(); + else throw std::runtime_error("unknown guest arena test"); + std::printf("guest arena %s passed\n", argv[1]); + return 0; + } catch (const std::exception& error) { + std::fprintf(stderr, "%s\n", error.what()); + return 1; + } +} diff --git a/core/libs/tests/guest_arena/CMakeLists.txt b/core/libs/tests/guest_arena/CMakeLists.txt new file mode 100644 index 0000000000..f84f4e6842 --- /dev/null +++ b/core/libs/tests/guest_arena/CMakeLists.txt @@ -0,0 +1,21 @@ +cmake_minimum_required(VERSION 3.20) + +if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR) + project(GuestArenaTests LANGUAGES CXX) + enable_testing() + set(ANYPS5_TEST_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/tests") +endif() + +find_package(Threads REQUIRED) +add_executable(guest_arena_tests ../GuestArenaHost.cpp ../../prx/libc/src/GuestArena.cpp ../../prx/libc/src/GuestWriteWatch.cpp) +target_include_directories(guest_arena_tests PRIVATE ../..) +target_compile_features(guest_arena_tests PRIVATE cxx_std_20) +target_link_libraries(guest_arena_tests PRIVATE Threads::Threads) +set_target_properties(guest_arena_tests PROPERTIES + CXX_EXTENSIONS OFF + RUNTIME_OUTPUT_DIRECTORY "$<1:${ANYPS5_TEST_OUTPUT_DIRECTORY}>") + +foreach(mode available unmappable host commit) + add_test(NAME guest_arena_${mode} COMMAND guest_arena_tests ${mode}) + set_tests_properties(guest_arena_${mode} PROPERTIES TIMEOUT 30) +endforeach() From 913adca3aabedfe6bbaabe6cb528225911e636eb Mon Sep 17 00:00:00 2001 From: yripper Date: Tue, 13 Oct 2026 02:20:58 -0300 Subject: [PATCH 0189/1000] docs(libc): record the macOS guest arena holes as debt Move the explanatory comments into docs/dev/debt/libc.md, as the conventions require. --- core/libs/prx/libc/src/GuestArena.cpp | 3 --- core/libs/tests/GuestArenaHost.cpp | 3 --- docs/dev/debt/libc.md | 1 + 3 files changed, 1 insertion(+), 6 deletions(-) diff --git a/core/libs/prx/libc/src/GuestArena.cpp b/core/libs/prx/libc/src/GuestArena.cpp index 008ad0da76..876cf75826 100644 --- a/core/libs/prx/libc/src/GuestArena.cpp +++ b/core/libs/prx/libc/src/GuestArena.cpp @@ -210,8 +210,6 @@ private: return EINVAL; } #elif defined(__APPLE__) - // macOS has no /proc/self/maps; dyld, the Rosetta runtime and ranges the kernel never lets a process - // map (max protection none, e.g. 0x10_0000_0000-0x70_0000_0000) all sit inside the PS5 application map. static std::vector> HostMappings() { std::vector> mappings; mach_vm_address_t address = ArenaStart; @@ -229,7 +227,6 @@ private: return mappings; } - // MAP_FIXED_NOREPLACE does not exist on macOS; a fixed mach_vm_map fails instead of replacing. static int ReserveNoReplace(std::uintptr_t address, std::size_t bytes) { mach_vm_address_t wanted = address; const kern_return_t result = mach_vm_map(mach_task_self(), &wanted, bytes, 0, VM_FLAGS_FIXED, MEMORY_OBJECT_NULL, 0, FALSE, VM_PROT_NONE, VM_PROT_ALL, VM_INHERIT_DEFAULT); diff --git a/core/libs/tests/GuestArenaHost.cpp b/core/libs/tests/GuestArenaHost.cpp index 6378a14180..6a9a907466 100644 --- a/core/libs/tests/GuestArenaHost.cpp +++ b/core/libs/tests/GuestArenaHost.cpp @@ -51,8 +51,6 @@ std::vector RegionsInArenaRange() { return regions; } -// The kernel keeps ranges inside the PS5 application map that no process may map (max protection none); -// the arena has to come up around them instead of giving up. void CheckAvailable() { Require(GuestArenaAvailable_nid_postfix(), "the guest arena is unavailable on macOS"); std::uintptr_t base = 0; @@ -77,7 +75,6 @@ void CheckSkipsUnmappable() { void CheckHostRegionsKept() { std::size_t hostRegions = 0; for (const auto& region : RegionsInArenaRange()) { - // Reservations made by the arena itself are no-access with full max protection. if (region.maxProtection == VM_PROT_ALL) continue; ++hostRegions; bool rejected = false; diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index e92d357f7f..dca57c44c6 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -48,6 +48,7 @@ - a system call that writes into a protected page fails with `EFAULT` instead of faulting. `GuestArena::HostWrite` lifts the protection for the calls it brackets (file reads and socket receives); a host call that writes guest memory without it (`clock_gettime` and the like into a watched page) fails; - code that changes the protection of watched pages, or maps over them, has to call `GuestWriteWatchProtectionChanged`, and code that asks the kernel for a page's protection has to ask `GuestWriteWatchProtection` first, since a watched page is read-only between a collection and the next write. Nothing calls either yet: the macOS memory paths are not in the tree; - the handler is installed when libc loads and passes other faults to the handler installed before it. A handler installed later has to do the same. +- On macOS the [guest arena](../../../core/libs/prx/libc/src/GuestArena.cpp) finds host regions with `mach_vm_region` and reserves the rest with a fixed `mach_vm_map`, which fails instead of replacing, as macOS has no `/proc/self/maps` or `MAP_FIXED_NOREPLACE`. The kernel keeps 0x10_0000_0000-0x70_0000_0000 unmappable for every process (max protection none; macOS 26.5, M1 Pro, arm64 and x86_64 under Rosetta), and dyld and the Rosetta runtime sit near 0x2_0200_0000 and 0x3_0400_0000, so a guest mapping fixed inside those ranges fails where the console places it; mappings without a fixed address are placed around them. - [sceKernelSync](../../../core/libs/prx/libc/src/General.cpp) (libkernel) flushes only the host files and directories recorded through `RecordWrittenPath_nid_no_patch` (the libc, libkernel, save data and app content file functions) and their changed parent directories. A host write in a library that does not record its path is not flushed by `sceKernelSync` - [sceKernelSync](../../../core/libs/prx/libc/src/General.cpp) (libkernel) on Windows skips a recorded file it cannot open for writing (read-only, or held open by a handle that denies write sharing), since `FlushFileBuffers` needs a writable handle. On Linux, it skips a recorded file it cannot open for reading. Such a file's data may not reach the disk before a host crash; the guest reads the same data either way - [`fopen` streams](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) on Windows share delete access, like the descriptors [`sceKernelOpen`](../../../core/libs/prx/libkernel/File/src/Open.cpp) opens, so a file the guest holds open can be removed or renamed. A stream reopened with `freopen` still goes through `_wfreopen`, which does not share delete access. On Windows, opening a directory in a read mode fails with `EACCES`, where FreeBSD opens it; directory streams are not emulated From d42f4963e30eb53f5efb56f9fc743d3da86f79ca Mon Sep 17 00:00:00 2001 From: fdfawef324 Date: Wed, 14 Oct 2026 17:55:39 -0400 Subject: [PATCH 0190/1000] fix(agc): print the VGT_TF_PARAM and VGT_LS_HS_CONFIG fields of a refused tessellation draw A tessellation draw whose domain, partitioning or output topology the reference path does not support was refused with only VGT_SHADER_STAGES_EN, so a log did not show which configuration was refused. The refusal now prints VGT_TF_PARAM with TYPE, PARTITIONING and TOPOLOGY, and VGT_LS_HS_CONFIG with NUM_PATCHES, HS_NUM_INPUT_CP and HS_NUM_OUTPUT_CP. The fields come before the explanation, so the reason that the APS5_CAPTURE_TRACE log cuts at 256 characters still holds all of them. Field and enumerator names are those of Mesa's src/amd/registers/gfx103.json (enums VGT_TESS_TYPE, VGT_TESS_PARTITION and VGT_TESS_TOPOLOGY). A value with no name there is printed as a number only. The accepted configurations are unchanged. The new UnsupportedTessellationFieldTests drives DecodeState into the refusal with power-of-two triangle and quad patches, an isoline domain, line and point output and unnamed TYPE, PARTITIONING and TOPOLOGY values, and checks the decoded fields of both registers, including the order of the input and output control-point counts. Co-Authored-By: Claude Opus 5.5 --- .../libSceAgcDriver/Graphics/src/State.cpp | 22 ++++++++++++++++- .../prx/libSceAgcDriver/tests/Graphics.cpp | 24 +++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index 44cf86c915..92b1fe1805 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -16,6 +16,7 @@ #include #include #include +#include namespace AgcDriver::Graphics { namespace { @@ -49,6 +50,25 @@ std::string zeroMessage(std::uint32_t offset, std::uint32_t value, const char* n return std::string(name) + " is unsupported" + detail; } +std::string tessellationMessage(std::uint32_t config, std::uint32_t parameters, const ShaderRecompiler::TessellationConfiguration& tess) { + static constexpr std::array Types{"TESS_ISOLINE", "TESS_TRIANGLE", "TESS_QUAD"}; + static constexpr std::array Partitionings{"PART_INTEGER", "PART_POW2", "PART_FRAC_ODD", "PART_FRAC_EVEN"}; + static constexpr std::array Topologies{"OUTPUT_POINT", "OUTPUT_LINE", "OUTPUT_TRIANGLE_CW", "OUTPUT_TRIANGLE_CCW"}; + std::ostringstream message; + const auto field = [&](std::string_view name, std::uint32_t value, std::span names) { + message << name << '=' << value; + if (value < names.size()) message << ' ' << names[value]; + }; + message << "VGT_TF_PARAM=0x" << std::hex << parameters << std::dec << " ("; + field("TYPE", tess.domain, Types); + message << ", "; + field("PARTITIONING", tess.partitioning, Partitionings); + message << ", "; + field("TOPOLOGY", tess.outputTopology, Topologies); + message << "), VGT_LS_HS_CONFIG=0x" << std::hex << config << std::dec << " (NUM_PATCHES=" << tess.patchesPerGroup << ", HS_NUM_INPUT_CP=" << tess.inputControlPoints << ", HS_NUM_OUTPUT_CP=" << tess.outputControlPoints << "): only triangle and quad domains with integer, fractional-even or fractional-odd partitioning and triangle output are supported by the reference path"; + return message.str(); +} + void zero(const Registers& registers, std::uint32_t offset, std::uint32_t mask, const char* name, RegisterBank bank = RegisterBank::Context) { const auto value = read(registers, offset, bank); if ((value & mask) != 0) throw std::runtime_error(zeroMessage(offset, value, name)); @@ -677,7 +697,7 @@ ShaderStages DecodeShaderStages(const QueueState& queue, bool vertexWorkgroup) { validate(tess.inputControlPoints != 0 && tess.inputControlPoints <= 32 && tess.outputControlPoints != 0 && tess.outputControlPoints <= 32, "invalid tessellation control-point counts"); validate(tess.patchesPerGroup != 0, "VGT_LS_HS_CONFIG.NUM_PATCHES is zero"); if ((parameters & ~0x000600ffu) != 0) Require(false, zeroMessage(0x2db, parameters, "VGT_TF_PARAM donut, factor detection, memory type or reserved state")); - validate((tess.domain == 1 || tess.domain == 2) && (tess.partitioning == 0 || tess.partitioning == 2 || tess.partitioning == 3) && (tess.outputTopology == 2 || tess.outputTopology == 3), "only triangle and quad domains with integer, fractional-even or fractional-odd partitioning and triangle output are supported by the reference path"); + if (!((tess.domain == 1 || tess.domain == 2) && (tess.partitioning == 0 || tess.partitioning == 2 || tess.partitioning == 3) && (tess.outputTopology == 2 || tess.outputTopology == 3))) validate(false, tessellationMessage(config, parameters, tess).c_str()); result.minTessellationLevel = readFloat(queue.context, 0x287); result.maxTessellationLevel = readFloat(queue.context, 0x286); Require(result.minTessellationLevel >= 0.0f && result.minTessellationLevel <= 63.0f && result.minTessellationLevel <= result.maxTessellationLevel && result.maxTessellationLevel >= 1.0f, "tessellation level clamps need 0 <= VGT_HOS_MIN_TESS_LEVEL <= 63, VGT_HOS_MAX_TESS_LEVEL >= 1 and the minimum at most the maximum"); diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 3f4bace986..fd76a87698 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -834,6 +834,29 @@ void ShaderStageTests() { expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "missing register"); } +void UnsupportedTessellationFieldTests() { + auto queue = makeState(); + queue.context[0x2d5] = 0x0200210d; + queue.userConfig[0x242] = 9; + queue.userConfig[0x25b] = 0x1fe55; + queue.context[0x286] = 0x42800000; + queue.context[0x287] = 0x3f800000; + for (const auto [config, parameters, fields] : { + std::tuple{0xc355u, 0x40045u, "VGT_TF_PARAM=0x40045 (TYPE=1 TESS_TRIANGLE, PARTITIONING=1 PART_POW2, TOPOLOGY=2 OUTPUT_TRIANGLE_CW), VGT_LS_HS_CONFIG=0xc355 (NUM_PATCHES=85, HS_NUM_INPUT_CP=3, HS_NUM_OUTPUT_CP=3)"}, + std::tuple{0x1033fu, 0x40066u, "VGT_TF_PARAM=0x40066 (TYPE=2 TESS_QUAD, PARTITIONING=1 PART_POW2, TOPOLOGY=3 OUTPUT_TRIANGLE_CCW), VGT_LS_HS_CONFIG=0x1033f (NUM_PATCHES=63, HS_NUM_INPUT_CP=3, HS_NUM_OUTPUT_CP=4)"}, + std::tuple{0xc355u, 0x40048u, "VGT_TF_PARAM=0x40048 (TYPE=0 TESS_ISOLINE, PARTITIONING=2 PART_FRAC_ODD, TOPOLOGY=2 OUTPUT_TRIANGLE_CW)"}, + std::tuple{0xc355u, 0x40029u, "(TYPE=1 TESS_TRIANGLE, PARTITIONING=2 PART_FRAC_ODD, TOPOLOGY=1 OUTPUT_LINE)"}, + std::tuple{0xc355u, 0x4000du, "(TYPE=1 TESS_TRIANGLE, PARTITIONING=3 PART_FRAC_EVEN, TOPOLOGY=0 OUTPUT_POINT)"}, + std::tuple{0xc355u, 0x4004bu, "(TYPE=3, PARTITIONING=2 PART_FRAC_ODD, TOPOLOGY=2 OUTPUT_TRIANGLE_CW)"}, + std::tuple{0xc355u, 0x400f1u, "(TYPE=1 TESS_TRIANGLE, PARTITIONING=4, TOPOLOGY=7)"}}) { + queue.context[0x2d6] = config; + queue.context[0x2db] = parameters; + expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "VGT_SHADER_STAGES_EN=0x200210d: VGT_TF_PARAM=0x"); + expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, fields); + expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "): only triangle and quad domains with integer, fractional-even or fractional-odd partitioning and triangle output are supported by the reference path"); + } +} + void NggVertexWorkgroupTests() { auto queue = makeState(); queue.context[0x2d5] = 0x12010; @@ -6635,6 +6658,7 @@ int main(int argc, char** argv) { framebufferLayerTests(); UnwrittenSampleLocationTests(); ShaderStageTests(); + UnsupportedTessellationFieldTests(); NggVertexWorkgroupTests(); inactiveVertexPointSizeTests(); TuningFieldTests(); From ec2acea5bd2c4517ccb1aed5ad4817b6b91b37e9 Mon Sep 17 00:00:00 2001 From: Akul Tyagi <69496404+AkulTyagi11@users.noreply.github.com> Date: Thu, 15 Oct 2026 04:25:52 +0530 Subject: [PATCH 0191/1000] feat(libc): implement __modti3 Implement __modti3, the signed 128-bit integer modulo runtime helper complementing __divti3, __udivti3 and __umodti3. Add unit test coverage in guest_crt_tests and document zero-divisor exception behavior in technical debt. --- core/libs/prx/libc/include/SonyCrt.hpp | 14 ++++++++++++ core/libs/prx/libc/src/RuntimeSupport.cpp | 4 ++++ core/libs/tests/GuestCrt.cpp | 26 +++++++++++++++++++++++ docs/dev/debt/libc.md | 1 + 4 files changed, 45 insertions(+) diff --git a/core/libs/prx/libc/include/SonyCrt.hpp b/core/libs/prx/libc/include/SonyCrt.hpp index 39a655f51e..497f1c40aa 100644 --- a/core/libs/prx/libc/include/SonyCrt.hpp +++ b/core/libs/prx/libc/include/SonyCrt.hpp @@ -226,6 +226,20 @@ inline __int128 SonyDivti3(__int128 dividend, __int128 divisor) { return (__int128)(negative ? (unsigned __int128)0 - magnitude : magnitude); } +inline __int128 SonyModti3(__int128 dividend, __int128 divisor) { + if (divisor == 0) throw std::runtime_error("__modti3: integer divide by zero"); + const bool negative = dividend < 0; + const unsigned __int128 magnitudeDividend = negative + ? (unsigned __int128)0 - (unsigned __int128)dividend + : (unsigned __int128)dividend; + const unsigned __int128 magnitudeDivisor = divisor < 0 + ? (unsigned __int128)0 - (unsigned __int128)divisor + : (unsigned __int128)divisor; + unsigned __int128 remainder = 0; + SonyUdivmod128(magnitudeDividend, magnitudeDivisor, &remainder); + return (__int128)(negative ? (unsigned __int128)0 - remainder : remainder); +} + } #endif diff --git a/core/libs/prx/libc/src/RuntimeSupport.cpp b/core/libs/prx/libc/src/RuntimeSupport.cpp index 4b7ae591d7..013183fc95 100644 --- a/core/libs/prx/libc/src/RuntimeSupport.cpp +++ b/core/libs/prx/libc/src/RuntimeSupport.cpp @@ -192,6 +192,10 @@ unsigned __int128 APS5_VABI __umodti3_nid_postfix(unsigned __int128 dividend, un return SonyUmodti3(dividend, divisor); } +__int128 APS5_VABI __modti3_nid_postfix(__int128 dividend, __int128 divisor) { + return SonyModti3(dividend, divisor); +} + void APS5_VABI _ZNSt8ios_base7_AddstdEPS__nid_postfix(void*) { NotImplemented_nid_no_patch(__func__); } diff --git a/core/libs/tests/GuestCrt.cpp b/core/libs/tests/GuestCrt.cpp index f6600788de..df087488e3 100644 --- a/core/libs/tests/GuestCrt.cpp +++ b/core/libs/tests/GuestCrt.cpp @@ -33,6 +33,7 @@ int APS5_VABI __fpclassifyd_nid_postfix(double value); unsigned __int128 APS5_VABI __udivti3_nid_postfix(unsigned __int128 dividend, unsigned __int128 divisor); __int128 APS5_VABI __divti3_nid_postfix(__int128 dividend, __int128 divisor); unsigned __int128 APS5_VABI __umodti3_nid_postfix(unsigned __int128 dividend, unsigned __int128 divisor); +__int128 APS5_VABI __modti3_nid_postfix(__int128 dividend, __int128 divisor); SonyLconv* APS5_VABI localeconv_nid_postfix(void); int APS5_VABI sceAmprFpclassifyd(double value); unsigned int APS5_VABI _ZSt14_Random_devicev_nid_postfix(); @@ -110,6 +111,31 @@ int main() { } Require(trapped); + Require(__modti3_nid_postfix(100, 3) == 1); + Require(__modti3_nid_postfix(-100, 3) == -1); + Require(__modti3_nid_postfix(100, -3) == 1); + Require(__modti3_nid_postfix(-100, -3) == -1); + Require(SonyModti3(0, 5) == 0); + Require(SonyModti3(3, 100) == 3); + Require(SonyModti3(-3, 100) == -3); + Require(SonyModti3(3, -100) == 3); + Require(SonyModti3(-3, -100) == -3); + Require(SonyModti3(large, 3) == large % 3); + Require(SonyModti3(-large, 3) == -(large % 3)); + Require(SonyModti3(large, -3) == large % 3); + Require(SonyModti3(-large, -3) == -(large % 3)); + Require(SonyModti3(minimum, 1) == 0); + Require(SonyModti3(minimum, -1) == 0); + Require(SonyModti3(maximum, 1) == 0); + Require(SonyModti3(maximum, -1) == 0); + trapped = false; + try { + SonyModti3(1, 0); + } catch (const std::exception&) { + trapped = true; + } + Require(trapped); + const unsigned __int128 unsignedMaximum = ~(unsigned __int128)0; const unsigned __int128 largeDivisor = ((unsigned __int128)1 << 64) + 3; Require(__umodti3_nid_postfix(100, 3) == 1); diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index e92d357f7f..b8b6364041 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -30,6 +30,7 @@ - [set_constraint_handler_s](../../../core/libs/prx/libc/src/WideConversion.cpp) (libc, re-exported by libSceAmpr) - the default handler is assumed to abort; each module keeps its own handler state, so a handler set through one library is not seen by the other. `ctime_s` and `asctime_s` follow the Dinkumware 26-byte format with `EINVAL` for a buffer under 26 bytes, a null pointer or an out-of-range weekday/month; `ctime_s` uses the guest time zone of `localtime_s`, and the console's exact checks are not verified - [__udivti3](../../../core/libs/prx/libc/src/RuntimeSupport.cpp) (libc, re-exported by libSceAmpr) - the hardware traps on a zero divisor; this throws instead - [__umodti3](../../../core/libs/prx/libc/src/RuntimeSupport.cpp) (libc) - the hardware traps on a zero divisor; this throws instead +- [__modti3](../../../core/libs/prx/libc/src/RuntimeSupport.cpp) (libc) - the hardware traps on a zero divisor; this throws instead ### Functional From e03e6f7a4e0d0c642742b9739c90e4f1c6e12d2d Mon Sep 17 00:00:00 2001 From: al3w0f205 Date: Wed, 14 Oct 2026 00:15:02 -0500 Subject: [PATCH 0192/1000] feat(libkernel): add munlockall export Follows mlockall (#4386) for XPSemu (PPSA97358, QEMU os-posix.c), which imports munlockall (NpLBpgVV7PU) from libkernel.prx. Predicted stop: FAIL: unresolved ELF import NpLBpgVV7PU from libkernel.prx The NID is computed with tools/nid_names.py and matches aerolib. Follows FreeBSD 11.4 sys/vm/vm_mmap.c sys_munlockall (8c639945a44588f6cfdddfb057bb5aad88735da0): checks PRIV_VM_MUNLOCK, clears wire state and returns 0. To match, munlockall unlocks ranges locked by mlock (through VirtualUnlock on Windows, tolerating ERROR_NOT_LOCKED, and munlockall on Linux). --- .../prx/libkernel/DirectMemory/Export.cpp | 94 +++++++++++++++++-- .../libkernel/DirectMemory/PosixMemory.cpp | 8 ++ core/libs/tests/GuestMlock.cpp | 17 ++++ docs/dev/debt/libkernel.md | 2 +- 4 files changed, 114 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libkernel/DirectMemory/Export.cpp b/core/libs/prx/libkernel/DirectMemory/Export.cpp index dca35ab051..c8dc5dc1a6 100644 --- a/core/libs/prx/libkernel/DirectMemory/Export.cpp +++ b/core/libs/prx/libkernel/DirectMemory/Export.cpp @@ -134,6 +134,38 @@ void _releaseFlexible(uintptr_t start, size_t len) { } } +std::mutex g_lockedRangesLock; +std::map g_lockedRanges; + +void RecordLockedRange(uintptr_t start, uintptr_t end) { + std::lock_guard lock(g_lockedRangesLock); + auto it = g_lockedRanges.begin(); + while (it != g_lockedRanges.end()) { + if (it->first <= end && it->first + it->second >= start) { + start = std::min(start, it->first); + end = std::max(end, it->first + it->second); + it = g_lockedRanges.erase(it); + } else { + ++it; + } + } + g_lockedRanges[start] = end - start; +} + +void ForgetLockedRange(uintptr_t start, uintptr_t end) { + std::lock_guard lock(g_lockedRangesLock); + auto it = g_lockedRanges.upper_bound(start); + if (it != g_lockedRanges.begin()) --it; + while (it != g_lockedRanges.end() && it->first < end) { + const uintptr_t rangeStart = it->first; + const uintptr_t rangeEnd = rangeStart + it->second; + if (rangeEnd <= start) { ++it; continue; } + it = g_lockedRanges.erase(it); + if (rangeStart < start) g_lockedRanges[rangeStart] = start - rangeStart; + if (rangeEnd > end) g_lockedRanges[end] = rangeEnd - end; + } +} + } extern "C" { @@ -230,7 +262,10 @@ int APS5_VABI sceKernelMprotect(const void* addr, size_t len, int prot) { int APS5_VABI sceKernelMunmap(uint64_t vaddr, size_t len) { const int result = DoMunmap(reinterpret_cast(vaddr), len); - if (result == 0) _releaseFlexible(static_cast(vaddr), len); + if (result == 0) { + _releaseFlexible(static_cast(vaddr), len); + ForgetLockedRange(static_cast(vaddr), static_cast(vaddr) + len); + } return result; } @@ -524,7 +559,10 @@ int LockHostPages(std::uintptr_t start, std::uintptr_t end) { auto* const address = reinterpret_cast(start); const SIZE_T bytes = end - start; std::lock_guard lock(g_workingSetLock); - if (VirtualLock(address, bytes)) return 0; + if (VirtualLock(address, bytes)) { + RecordLockedRange(start, end); + return 0; + } if (GetLastError() != ERROR_WORKING_SET_QUOTA) throw std::system_error(static_cast(GetLastError()), std::system_category(), "VirtualLock failed"); SIZE_T minimum = 0; SIZE_T maximum = 0; @@ -532,7 +570,10 @@ int LockHostPages(std::uintptr_t start, std::uintptr_t end) { if (!GetProcessWorkingSetSizeEx(GetCurrentProcess(), &minimum, &maximum, &flags)) throw std::system_error(static_cast(GetLastError()), std::system_category(), "GetProcessWorkingSetSizeEx failed"); if (bytes > std::numeric_limits::max() - maximum) return SCE_KERNEL_ERROR_EAGAIN; if (!SetProcessWorkingSetSizeEx(GetCurrentProcess(), minimum + bytes, maximum + bytes, flags)) return SCE_KERNEL_ERROR_EAGAIN; - if (VirtualLock(address, bytes)) return 0; + if (VirtualLock(address, bytes)) { + RecordLockedRange(start, end); + return 0; + } if (GetLastError() != ERROR_WORKING_SET_QUOTA) throw std::system_error(static_cast(GetLastError()), std::system_category(), "VirtualLock failed"); return SCE_KERNEL_ERROR_EAGAIN; } @@ -581,19 +622,26 @@ int UnlockHostPages(std::uintptr_t start, std::uintptr_t end) { } cursor = next; } + ForgetLockedRange(start, end); return 0; } #else int LockHostPages(std::uintptr_t start, std::uintptr_t end) { auto* const address = reinterpret_cast(start); const std::size_t bytes = end - start; - if (::mlock(address, bytes) == 0) return 0; + if (::mlock(address, bytes) == 0) { + RecordLockedRange(start, end); + return 0; + } int error = errno; rlimit limit{}; if ((error == ENOMEM || error == EPERM || error == EAGAIN) && getrlimit(RLIMIT_MEMLOCK, &limit) == 0 && limit.rlim_cur < limit.rlim_max) { limit.rlim_cur = limit.rlim_max; if (setrlimit(RLIMIT_MEMLOCK, &limit) == 0) { - if (::mlock(address, bytes) == 0) return 0; + if (::mlock(address, bytes) == 0) { + RecordLockedRange(start, end); + return 0; + } error = errno; } } @@ -607,7 +655,10 @@ int LockHostPages(std::uintptr_t start, std::uintptr_t end) { int UnlockHostPages(std::uintptr_t start, std::uintptr_t end) { if (!GuestRangeMapped(start, end)) return SCE_KERNEL_ERROR_ENOMEM; - if (::munlock(reinterpret_cast(start), end - start) == 0) return 0; + if (::munlock(reinterpret_cast(start), end - start) == 0) { + ForgetLockedRange(start, end); + return 0; + } const int error = errno; if (error == ENOMEM) return SCE_KERNEL_ERROR_ENOMEM; throw std::system_error(error, std::generic_category(), "munlock failed"); @@ -634,4 +685,35 @@ int APS5_VABI sceKernelMunlock_nid_postfix(void* address, std::uint64_t length) return UnlockHostPages(start, end); } +int APS5_VABI sceKernelMunlockall_nid_postfix() { +#ifndef _WIN32 + ::munlockall(); +#endif + std::map ranges; + { + std::lock_guard lock(g_lockedRangesLock); + ranges.swap(g_lockedRanges); + } +#ifdef _WIN32 + for (const auto& [start, len] : ranges) { + const auto end = start + len; + for (auto cursor = start; cursor < end;) { + MEMORY_BASIC_INFORMATION region{}; + if (VirtualQuery(reinterpret_cast(cursor), ®ion, sizeof(region)) != sizeof(region)) break; + const auto next = std::min(end, reinterpret_cast(region.BaseAddress) + region.RegionSize); + if (region.State == MEM_COMMIT && (region.Protect & (PAGE_NOACCESS | PAGE_GUARD)) == 0 && !VirtualUnlock(reinterpret_cast(cursor), next - cursor)) { + if (GetLastError() != ERROR_NOT_LOCKED) throw std::system_error(static_cast(GetLastError()), std::system_category(), "VirtualUnlock failed"); + for (auto page = cursor; page < next; page += HostPageSize()) { + if (!VirtualUnlock(reinterpret_cast(page), std::min(HostPageSize(), next - page)) && GetLastError() != ERROR_NOT_LOCKED) { + throw std::system_error(static_cast(GetLastError()), std::system_category(), "VirtualUnlock failed"); + } + } + } + cursor = next; + } + } +#endif + return 0; +} + } diff --git a/core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp b/core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp index c8049b48d9..6f2c99760c 100644 --- a/core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp +++ b/core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp @@ -19,6 +19,7 @@ extern "C" int* APS5_VABI __error_nid_postfix(); extern "C" int APS5_VABI sceKernelMlock_nid_postfix(void* address, std::uint64_t length); extern "C" int APS5_VABI sceKernelMunlock_nid_postfix(void* address, std::uint64_t length); +extern "C" int APS5_VABI sceKernelMunlockall_nid_postfix(); extern "C" int APS5_VABI sceKernelVirtualQuery(const void* addr, int flags, VirtualQueryInfo* info, uint64_t info_size); namespace { @@ -240,6 +241,13 @@ int APS5_VABI mlockall_nid_postfix(int flags) { throw std::runtime_error("mlockall: MCL_CURRENT is not implemented: the process also holds host stacks, heaps, modules and driver mappings"); } +int APS5_VABI munlockall_nid_postfix() { + const int result = sceKernelMunlockall_nid_postfix(); + if (result == 0) return 0; + SetError(result & 0xffff); + return -1; +} + int APS5_VABI msync_nid_postfix(void* address, std::size_t length, int flags) { const auto failed = [](int error) { SetError(error); diff --git a/core/libs/tests/GuestMlock.cpp b/core/libs/tests/GuestMlock.cpp index 8bad49dd13..79b571e9b2 100644 --- a/core/libs/tests/GuestMlock.cpp +++ b/core/libs/tests/GuestMlock.cpp @@ -22,6 +22,7 @@ int APS5_VABI sceKernelMunmap(void*, std::size_t); int APS5_VABI mlock_nid_postfix(const void*, std::size_t); int APS5_VABI munlock_nid_postfix(const void*, std::size_t); int APS5_VABI mlockall_nid_postfix(int); +int APS5_VABI munlockall_nid_postfix(); int* APS5_VABI __error_nid_postfix(); } @@ -110,6 +111,18 @@ int main() { #elif defined(_WIN32) Require(!VirtualUnlock(mapped, page) && GetLastError() == ERROR_NOT_LOCKED); #endif + Require(mlock_nid_postfix(mapped, page) == 0); +#ifdef HAS_LOCKED_KILOBYTES + Require(LockedKilobytes() - lockedBefore == page / 1024); +#endif + *__error_nid_postfix() = untouched; + Require(munlockall_nid_postfix() == 0); + Require(*__error_nid_postfix() == untouched); +#ifdef HAS_LOCKED_KILOBYTES + Require(LockedKilobytes() == lockedBefore); +#elif defined(_WIN32) + Require(!VirtualUnlock(mapped, page) && GetLastError() == ERROR_NOT_LOCKED); +#endif *__error_nid_postfix() = 0; Require(mlock_nid_postfix(reinterpret_cast(std::numeric_limits::max() - page + 1), page * 2) == -1); @@ -144,5 +157,9 @@ int main() { RequireLockAllUnsupported(1); RequireLockAllUnsupported(2); RequireLockAllUnsupported(1 | 2); + + *__error_nid_postfix() = untouched; + Require(munlockall_nid_postfix() == 0); + Require(*__error_nid_postfix() == untouched); return 0; } diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index f16a03c5ab..9300538bb6 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -172,7 +172,7 @@ - [msync](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) (libkernel) follows FreeBSD 11's `kern_msync` and `vm_map_sync`, not measured on the console: the address is rounded down to the page, a range that wraps is `EINVAL`, as is `MS_ASYNC` with `MS_INVALIDATE`, and a range whose start or any page is not mapped (as `sceKernelVirtualQuery` reports it, reservations included) is `ENOMEM`; a page unmapped from inside a larger mapping still counts as mapped there, since `sceKernelVirtualQuery` keeps reporting the whole mapping. The guest has no file mappings, so a successful call writes nothing back. `MS_INVALIDATE` on a mapped range throws: FreeBSD returns `EBUSY` when the range holds `mlock`ed pages, and locked pages are not tracked - [madvise](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) (libkernel) accepts `MADV_PROTECT` (10) as a no-op hint without modifying process protection. FreeBSD 11's `kern_madvise` (`sys/vm/vm_mmap.c`) handles it before the address check, ignores the address and length, and calls `kern_procctl(..., PROC_SPROTECT, PPROT_SET)`, which requires `PRIV_VM_MADV_PROTECT` and fails with `EPERM` without it. Returning 0 unconditionally assumes the calling process is privileged. Console behaviour is unverified - [mincore](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) (libkernel) follows FreeBSD 11's `sys_mincore` (`sys/vm/vm_mmap.c`), not measured on the console: the address is rounded down to the page, length 0 returns 0, a null vector over a mapped range returns `EFAULT`, an overflowing range or any unmapped page in the range (as reported by `sceKernelVirtualQuery`) returns `ENOMEM`. For valid ranges, a page reports `MINCORE_INCORE` (0x1) when `sceKernelVirtualQuery` reports it committed and 0 when it is only reserved; actual residency, referenced and modified bits are not modelled. Length 0 returns 0 when the start page is mapped and `ENOMEM` otherwise, and the range and mapping checks come before the null vector `EFAULT`, as in `kern_mincore` Java 25 LAN / DNS (PPSA99869) imports it -- [mlockall](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) (libkernel) returns `EINVAL` for zero or unknown flags. Valid `MCL_CURRENT` throws because it would lock host stacks, heaps, modules and driver mappings along with guest memory; `MCL_FUTURE` throws because later mappings are not tracked for locking. On main, XPSemu (PPSA97358) fails to load at this import; its `MCL_CURRENT | MCL_FUTURE` calls execute only when `-overcommit mem-lock=on` is enabled. The call has not been observed in a run with this implementation. +- [mlockall, munlockall](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) (libkernel) - `mlockall` returns `EINVAL` for zero or unknown flags. Valid `MCL_CURRENT` throws because it would lock host stacks, heaps, modules and driver mappings along with guest memory; `MCL_FUTURE` throws because later mappings are not tracked for locking. FreeBSD 11.4's `sys_munlockall` (`sys/vm/vm_mmap.c`:1133-1165) checks `priv_check(td, PRIV_VM_MUNLOCK)` first (returning `EPERM` without it) and unwires the user map (`vm_map_unwire` over `vm_map_min`...`vm_map_max` with `VM_MAP_WIRE_HOLESOK`). Returning 0 unconditionally assumes the calling process is privileged. `munlockall` unlocks memory locked by `mlock` (`VirtualUnlock` on Windows, tolerating `ERROR_NOT_LOCKED` for pages never locked, and `munlockall` on Linux). On main, XPSemu (PPSA97358) fails to load at these imports (mlockall #4386, munlockall); their calls in QEMU `os-posix.c` execute only when `-overcommit mem-lock=on` is enabled. The call has not been observed in a run with this implementation. - [sigtimedwait](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) waits on a process-wide pending set instead of throwing: a signal raised while blocked stays pending and is taken by `sigwait`, `sigwaitinfo` or `sigtimedwait`, delivered to the handler when `_sigprocmask` unblocks it, or delivered by `pause`. The set covers all 128 signals and the lowest number that is pending and in the set is selected (FreeBSD specifies the lowest number only among real-time signals). Selection, `EAGAIN` on timeout, `EINVAL` for a `tv_nsec` outside 0 to 1000000000 while nothing is pending and `NULL` timeout blocking indefinitely follow FreeBSD's [sigtimedwait(2)](https://man.freebsd.org/cgi/man.cgi?query=sigtimedwait). `sigwait` returns 0 with the number in its output, as POSIX specifies, and `sigwaitinfo` and `sigtimedwait` return the number, as FreeBSD does. The guest `siginfo_t` follows FreeBSD 11's `sys/sys/siginfo.h`: 80 bytes, `si_signo` at 0, `si_code` at 8, `si_pid` at 12, `si_uid` at 16, `si_addr` at 24, `si_value` at 32 and `si_band` at 40; only `si_signo`, `si_code` (`SI_USER`) and `si_pid` (the guest pid) are written, `si_uid` stays 0 because no uid is modelled, and the console's layout is not verified. PPSA85300 imports `sigtimedwait` and fails to load without it; its only call site is VLC's `vlc_writev` ([filesystem.c](https://github.com/videolan/vlc/blob/6de05adcbaf2e8b85fe86aad4169393098628119/src/posix/filesystem.c#L243-L278)), which polls with a zero timeout to drain a `SIGPIPE` only after `writev` fails with `EPIPE`. The waits are not cancellation points and an unblocked caught signal does not interrupt them with `EINTR`, where FreeBSD's `sigwaitinfo` and `sigtimedwait` may return it. `SIGPIPE` has no host equivalent and is never raised by a host write here (see `read, write and vector I/O`), so VLC's drain finds nothing - [pause, sigsuspend, sigwait, sigwaitinfo](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) take the pending set of `sigtimedwait` instead of throwing. `pause` waits for a pending signal that is not blocked and whose disposition can be delivered, runs its handler on the calling thread and returns -1 with `EINTR`; an ignored signal is discarded and the wait continues, and it never returns for a signal whose handler cannot run. A signal whose registered handler has `SA_SIGINFO` still throws, and one whose disposition is the default action and that has no host equivalent (every signal except 2, 4, 6, 8, 11 and 15) is neither delivered nor discarded: it stays pending until a wait takes it, where FreeBSD runs the default action, which terminates the process for most of them. PPSA68091, PPSA99169, PPSA99420 and PPSA97358 import them and fail to load without them. PPSA68091 calls `pause` at one site (0x8F7266), in a loop that waits while a global holds another thread's `errno` address. PPSA99169 only stores the address of `pause` in a table of symbol addresses (0x30DB1). PPSA99420 calls `pause` at 0x3BD80F6 and 0x3C9E220, `sigwait` at 0x3C9E4C7 and `sigwaitinfo` at 0x3C9E588. PPSA97358 calls `sigwait` from QEMU's `sigwait_compat` thread ([compatfd.c](https://github.com/ZiZc3/XPSemu/blob/8eec07b4e31f66dce8e0b05c1ca1281af303e933/util/compatfd.c#L28-L53)), which `qemu_signal_init` ([main-loop.c](https://github.com/ZiZc3/XPSemu/blob/8eec07b4e31f66dce8e0b05c1ca1281af303e933/util/main-loop.c#L110-L147)) starts for `SIGIO`, `SIGALRM` and `SIGBUS` when the main loop is created, so it reaches the wait at start-up, and from the vCPU thread of the dummy accelerator ([dummy-cpus.c](https://github.com/ZiZc3/XPSemu/blob/8eec07b4e31f66dce8e0b05c1ca1281af303e933/accel/dummy-cpus.c#L48-L56)). `sigsuspend` swaps the process-wide mask of `_sigprocmask` for its argument, waits as `pause` does through the same helper, then restores the mask through `_sigprocmask`, and returns -1 with `EINTR`; a null set throws, as the console faults in libthr instead of returning `EFAULT`. Like `_sigprocmask`, only the first 32 signals are blocked: words 1 to 3 of the argument are not applied, so signals 33 to 128 stay deliverable during the wait; widening the shared mask is a follow-up. Java 25 LAN / DNS (PPSA99869, #5501) imports it and fails to load without it, after `sysctlnametomib`, `fegetenv` and `mincore`. None of the five waits is a cancellation point - [getsid](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) returns the calling process ID for pid 0 and the calling process, and `ESRCH` for other process IDs, assuming the single guest process is its own session leader. PPSA74126 imports it and fails to load without it From a76fcc66488a55aa0193d8798130df617865c7be Mon Sep 17 00:00:00 2001 From: MiniRML <134226498+MiniRML@users.noreply.github.com> Date: Wed, 14 Oct 2026 16:25:47 -0700 Subject: [PATCH 0193/1000] test(tools): support Python 3.9 in oracle cache regression --- tools/hw-oracle/test_hw_oracle.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/hw-oracle/test_hw_oracle.py b/tools/hw-oracle/test_hw_oracle.py index b59c6b62a9..9c456db0ec 100644 --- a/tools/hw-oracle/test_hw_oracle.py +++ b/tools/hw-oracle/test_hw_oracle.py @@ -289,7 +289,7 @@ class TargetOverrideTests(unittest.TestCase): "with Path(__file__).with_name('probes.txt').open('a', encoding='utf-8') as log:\n" " log.write(visible + '\\n')\n" "print({'0': 'gfx1030', '1': 'gfx1100', 'unset': 'gfx900'}[visible])\n", - encoding="utf-8", newline="\n") + encoding="utf-8") binary = root / ("oracle.cmd" if os.name == "nt" else "oracle") if os.name == "nt": binary.write_text(f'@"{sys.executable}" "{runner}" %*\n', encoding="utf-8") From 4aeb8ef499ee163d48dc4fd894b4a3caadf059c6 Mon Sep 17 00:00:00 2001 From: Dextroid17 <217507631+Dextroid17@users.noreply.github.com> Date: Wed, 14 Oct 2026 19:38:46 -0400 Subject: [PATCH 0194/1000] test(agc): stamp the guest writes the recorder tests make behind the driver's back --- core/libs/prx/libSceAgcDriver/tests/Recorder.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 251be75a8e..07afeca25f 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -1942,10 +1942,12 @@ void resourceReadTests(const Device& device, Recorder& recorder) { auto first = resources.PrepareDrawBindings(snapshotRecorder); Require(first != nullptr && first->snapshots.size() == 1, "read-only draw input was not snapshotted"); std::memset(reinterpret_cast(element), 0x22, elementBytes); + AgcDriver::GuestMemory::MarkWritten(element, elementBytes); auto second = resources.PrepareDrawBindings(snapshotRecorder); Require(second != nullptr && second->snapshots.size() == 1, "cached draw input was not snapshotted again"); Require(first->allocation.set != second->allocation.set, "in-flight draws share mutable descriptor bindings"); std::memset(reinterpret_cast(element), 0x33, elementBytes); + AgcDriver::GuestMemory::MarkWritten(element, elementBytes); auto downloaded = std::make_shared(snapshotContext, elementBytes * 2, VK_BUFFER_USAGE_TRANSFER_DST_BIT); const auto commands = snapshotRecorder.Commands(); RecordMemoryBarrier(snapshotContext, commands, VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_HOST_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT); @@ -2303,6 +2305,7 @@ void misalignedRegionTests(const Device& device, Recorder& recorder) { }; check(); guest[offset + 8] ^= 0xffu; + AgcDriver::GuestMemory::MarkWritten(view, elementBytes); check(); regionRecorder.Sync(); } From df4350f3aee7821fda2ec6ee7e88615a4cd84874 Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Wed, 14 Oct 2026 23:40:49 +0000 Subject: [PATCH 0195/1000] fix(libSceVideoOut): preserve relative mouse mode for mouse-to-stick --- core/libs/prx/libSceVideoOut/src/MouseInput.cpp | 4 ++++ core/libs/prx/libSceVideoOut/tests/MouseInput.cpp | 9 ++++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceVideoOut/src/MouseInput.cpp b/core/libs/prx/libSceVideoOut/src/MouseInput.cpp index 4286633290..f1e79af366 100644 --- a/core/libs/prx/libSceVideoOut/src/MouseInput.cpp +++ b/core/libs/prx/libSceVideoOut/src/MouseInput.cpp @@ -82,6 +82,10 @@ bool MouseInput::CaptureWanted(bool allowed) const { } void MouseInput::UpdateCapture(bool allowed) { + if (!allowed) { + captured = false; + return; + } const bool wanted = CaptureWanted(allowed); if (wanted == captured && (SDL_GetRelativeMouseMode() == SDL_TRUE) == wanted) return; if (SDL_SetRelativeMouseMode(wanted ? SDL_TRUE : SDL_FALSE) != 0) { diff --git a/core/libs/prx/libSceVideoOut/tests/MouseInput.cpp b/core/libs/prx/libSceVideoOut/tests/MouseInput.cpp index 5d740a05e4..759c9708e7 100644 --- a/core/libs/prx/libSceVideoOut/tests/MouseInput.cpp +++ b/core/libs/prx/libSceVideoOut/tests/MouseInput.cpp @@ -67,7 +67,14 @@ void CaptureApplied(MouseInput& input) { input.UpdateCapture(true); const bool applied = SDL_GetRelativeMouseMode() == SDL_TRUE; input.UpdateCapture(false); - Require(SDL_GetRelativeMouseMode() == SDL_FALSE, "the capture was not released"); + if (applied) { + Require(SDL_GetRelativeMouseMode() == SDL_TRUE, "mouse-to-stick mode lost relative mouse mode"); + Require(SDL_SetRelativeMouseMode(SDL_FALSE) == 0, "mouse-to-stick mode could not release relative mode"); + input.UpdateCapture(true); + Require(SDL_GetRelativeMouseMode() == SDL_TRUE, "guest mouse capture was not restored after mouse-to-stick mode"); + input.UpdateCapture(false); + Require(SDL_SetRelativeMouseMode(SDL_FALSE) == 0, "relative mouse mode could not be reset"); + } std::printf("mouse input: SDL %s relative mode under the %s driver\n", applied ? "applied" : "refused", SDL_GetCurrentVideoDriver()); SDL_Quit(); } From 006e1768128d23f491e78d51c709ecf58d97ed2c Mon Sep 17 00:00:00 2001 From: Jota <139649225+jotitadev@users.noreply.github.com> Date: Wed, 14 Oct 2026 17:20:14 -0300 Subject: [PATCH 0196/1000] fix(relinker): compare guest module directories by identity (cherry picked from commit 6e3b80d65bd47344b896af9cbc763033fe6d5be0) --- core/relinker/cli/include/NativeModules.hpp | 2 ++ core/relinker/cli/src/NativeModules.cpp | 7 ++++++- core/relinker/main.cpp | 2 +- .../relinker/relinker/tests/test_executables.py | 17 +++++++++++++++++ .../relinker/tests/test_guest_module_dir.py | 13 +++++++++++++ 5 files changed, 39 insertions(+), 2 deletions(-) diff --git a/core/relinker/cli/include/NativeModules.hpp b/core/relinker/cli/include/NativeModules.hpp index bd49e3773b..66139266ca 100644 --- a/core/relinker/cli/include/NativeModules.hpp +++ b/core/relinker/cli/include/NativeModules.hpp @@ -10,6 +10,8 @@ struct NativeModules { std::vector Modules; }; +bool SameDirectory(const std::filesystem::path& first, const std::filesystem::path& second); + NativeModules ReadModules(const std::filesystem::path& inputPath, const std::filesystem::path& sceModulePath, const std::set& excludedModules, const std::vector& moduleDirectories); } diff --git a/core/relinker/cli/src/NativeModules.cpp b/core/relinker/cli/src/NativeModules.cpp index 4943091c64..f3839033c8 100644 --- a/core/relinker/cli/src/NativeModules.cpp +++ b/core/relinker/cli/src/NativeModules.cpp @@ -7,6 +7,11 @@ namespace Cli { +bool SameDirectory(const std::filesystem::path& first, const std::filesystem::path& second) { + if (std::filesystem::exists(first) && std::filesystem::exists(second)) return std::filesystem::equivalent(first, second); + return std::filesystem::weakly_canonical(first) == std::filesystem::weakly_canonical(second); +} + NativeModules ReadModules(const std::filesystem::path& inputPath, const std::filesystem::path& sceModulePath, const std::set& excludedModules, const std::vector& moduleDirectories) { using namespace Relinker; const auto root = std::filesystem::absolute(sceModulePath).lexically_normal(); @@ -32,7 +37,7 @@ NativeModules ReadModules(const std::filesystem::path& inputPath, const std::fil auto directory = (root / relative).lexically_normal(); if (!directory.has_filename()) directory = directory.parent_path(); if (!std::filesystem::is_directory(directory)) throw Domain::RelinkerException("Guest module path is not a directory: " + Io::Utf8Path(directory)); - if (std::find(directories.begin(), directories.end(), directory) != directories.end()) throw Domain::RelinkerException("Duplicate guest module directory: " + Io::Utf8Path(directory)); + if (std::any_of(directories.begin(), directories.end(), [&](const auto& existing) { return SameDirectory(existing, directory); })) throw Domain::RelinkerException("Duplicate guest module directory: " + Io::Utf8Path(directory)); directories.push_back(directory); runtimeDirectories.push_back(directory); } diff --git a/core/relinker/main.cpp b/core/relinker/main.cpp index 6b967356a5..243e8d18b4 100644 --- a/core/relinker/main.cpp +++ b/core/relinker/main.cpp @@ -210,7 +210,7 @@ int Run(const int argc, char* argv[]) { throw Domain::RelinkerException("--executable must be a file inside the guest module parent directory: " + executable); const auto input = moduleRoot / relative; if (!std::filesystem::is_regular_file(input)) throw Domain::RelinkerException("--executable is not a file: " + Io::Utf8Path(input)); - if (std::find(moduleDirectories.begin(), moduleDirectories.end(), input.parent_path()) != moduleDirectories.end()) + if (std::any_of(moduleDirectories.begin(), moduleDirectories.end(), [&](const auto& directory) { return Cli::SameDirectory(directory, input.parent_path()); })) throw Domain::RelinkerException("--executable is in a guest module directory: " + executable); std::string name; for (const auto& part : relative) name += (name.empty() ? "" : ".") + Io::Utf8Path(part); diff --git a/core/relinker/relinker/tests/test_executables.py b/core/relinker/relinker/tests/test_executables.py index baff6b86c1..21569834d8 100644 --- a/core/relinker/relinker/tests/test_executables.py +++ b/core/relinker/relinker/tests/test_executables.py @@ -59,6 +59,23 @@ def main(): assert result.returncode == 2 and message in result.stderr, (options, result.stderr) assert not output.exists(), options + for index, (directory, executable) in enumerate((("Picker", "picker/picker.bin"), ("picker", "PICKER/picker.bin"), ("linked", "picker/picker.bin"))): + case = work / f"{windows}-identity-{index}" + case.mkdir() + insensitive = (work / f"{windows}-IDENTITY-{index}").exists() + if (directory == "linked" and os.name == "nt") or (executable != "picker/picker.bin" and not insensitive): + continue + if directory == "linked": + (case / "linked").symlink_to("picker", target_is_directory=True) + elif not insensitive: + (case / directory).mkdir() + result, output = convert(case, windows, ["--module-dir", directory, "--executable", executable]) + if insensitive or directory == "linked": + assert result.returncode == 2 and "in a guest module directory" in result.stderr, (directory, executable, result.stderr) + assert not output.exists(), (directory, executable) + else: + assert result.returncode == 0, (directory, executable, result.stdout, result.stderr) + case = work / f"{windows}-needed" (case / "Media" / "Modules").mkdir(parents=True) (case / "Media" / "Modules" / "needed.prx").write_bytes(module_with_symbol(True)) diff --git a/core/relinker/relinker/tests/test_guest_module_dir.py b/core/relinker/relinker/tests/test_guest_module_dir.py index 9fd27ed131..ae9aee4bf2 100644 --- a/core/relinker/relinker/tests/test_guest_module_dir.py +++ b/core/relinker/relinker/tests/test_guest_module_dir.py @@ -138,6 +138,19 @@ def main(): result, output = convert(case, windows, ["--module-dir", rejected]) assert result.returncode == 2 and message in result.stderr, (rejected, result.stderr) + aliases = [["SCE_MODULE"], ["Media/Plugins", "media/plugins"]] if (case / "MEDIA").exists() else [] + if os.name != "nt": + (case / "linked").symlink_to("sce_module", target_is_directory=True) + aliases.append(["linked"]) + for names in aliases: + result, output = convert(case, windows, [option for name in names for option in ("--module-dir", name)]) + assert result.returncode == 2 and "Duplicate guest module directory" in result.stderr, (names, result.stderr) + if not (case / "MEDIA").exists(): + (case / "SCE_MODULE").mkdir() + result, output = convert(case, windows, ["--module-dir", "SCE_MODULE"]) + assert result.returncode == 0, (result.stdout, result.stderr) + (case / "SCE_MODULE").rmdir() + result, output = convert(case, windows, ["--skip-sce-module", "--module-dir", "Media/Plugins"]) assert result.returncode != 0 and "conflicts with --skip-sce-module" in result.stderr, result.stderr From c494f33526a7865741555a2c1df034a66213e2e2 Mon Sep 17 00:00:00 2001 From: PYTHONRETARDE <152398455+PYTHONRETARDE@users.noreply.github.com> Date: Thu, 15 Oct 2026 00:18:40 +0000 Subject: [PATCH 0197/1000] test(libkernel): deny the kqueue parent listing to the token user's SID DeniedParentWatch denied FILE_LIST_DIRECTORY to the name GetUserNameW returns. When the user name equals the computer name, LookupAccountNameW resolves that bare name to the computer's domain SID (SidTypeDomain), so the deny entry matched no one, the parent watch was allowed and the test aborted on an empty error. The entry now names the SID of the process token's user. Co-Authored-By: Claude Opus 5.5 --- core/libs/tests/GuestKqueue.cpp | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/core/libs/tests/GuestKqueue.cpp b/core/libs/tests/GuestKqueue.cpp index 8ac99378e5..e2721ab46d 100644 --- a/core/libs/tests/GuestKqueue.cpp +++ b/core/libs/tests/GuestKqueue.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #ifdef _WIN32 #include #include @@ -447,9 +448,14 @@ static std::string Utf8(const std::wstring& text) { } static PACL DenyListing(const std::wstring& directory, PSECURITY_DESCRIPTOR& owner) { - wchar_t account[256]{}; - DWORD count = static_cast(sizeof(account) / sizeof(account[0])); - Require(::GetUserNameW(account, &count) != FALSE); + HANDLE token = nullptr; + Require(::OpenProcessToken(::GetCurrentProcess(), TOKEN_QUERY, &token) != FALSE); + DWORD size = 0; + ::GetTokenInformation(token, TokenUser, nullptr, 0, &size); + std::vector user(size); + const BOOL queried = ::GetTokenInformation(token, TokenUser, user.data(), size, &size); + ::CloseHandle(token); + Require(queried != FALSE); PACL original = nullptr; Require(::GetNamedSecurityInfoW(directory.c_str(), SE_FILE_OBJECT, DACL_SECURITY_INFORMATION, nullptr, nullptr, &original, nullptr, &owner) == ERROR_SUCCESS); @@ -457,9 +463,9 @@ static PACL DenyListing(const std::wstring& directory, PSECURITY_DESCRIPTOR& own denied.grfAccessPermissions = FILE_LIST_DIRECTORY; denied.grfAccessMode = DENY_ACCESS; denied.grfInheritance = NO_INHERITANCE; - denied.Trustee.TrusteeForm = TRUSTEE_IS_NAME; + denied.Trustee.TrusteeForm = TRUSTEE_IS_SID; denied.Trustee.TrusteeType = TRUSTEE_IS_USER; - denied.Trustee.ptstrName = account; + denied.Trustee.ptstrName = static_cast(reinterpret_cast(user.data())->User.Sid); PACL blocked = nullptr; Require(::SetEntriesInAclW(1, &denied, original, &blocked) == ERROR_SUCCESS); const DWORD applied = ::SetNamedSecurityInfoW(const_cast(directory.c_str()), SE_FILE_OBJECT, From af3bd503cbe7ee6d035dca7d111f7dd6d91d634d Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Wed, 14 Oct 2026 17:42:42 -0700 Subject: [PATCH 0198/1000] refactor(agc): declare the largest device-local heap lookup after the budget helpers Its declaration sat right after ClearCachedTextures, where another open change adds a line too. It now follows SampledBudgetReportDue, with the other memory-budget helpers. --- .../prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp index ba7825f464..a8d2c19dbe 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp @@ -28,10 +28,10 @@ class Recorder; void FlushCachedTextures(VkDevice device); void ClearCachedTextures(VkDevice device); -std::optional LargestDeviceLocalHeap(const VkPhysicalDeviceMemoryProperties& memory); std::uint64_t TextureCacheBudget(const VkPhysicalDeviceMemoryProperties& memory); std::uint64_t SampledTextureBudget(const VkPhysicalDeviceMemoryProperties& memory, const VkPhysicalDeviceMemoryBudgetPropertiesEXT* reported, std::uint64_t textureBytes); bool SampledBudgetReportDue(std::uint64_t reported, std::uint64_t budget, std::chrono::steady_clock::duration sinceReport); +std::optional LargestDeviceLocalHeap(const VkPhysicalDeviceMemoryProperties& memory); struct TextureCacheUse { std::size_t sampledEntries = 0; std::uint64_t sampledBytes = 0; From 498c00deb31c3f9a08920ba52e95ae06fb954139 Mon Sep 17 00:00:00 2001 From: NaitikJoshiPro <190247830+NaitikJoshiPro@users.noreply.github.com> Date: Wed, 14 Oct 2026 14:43:43 +0530 Subject: [PATCH 0199/1000] fix(agc): read unregistered graphics programs beside registered shaders Since 39fdc48b5 a graphics program outside every registered shader was read raw only while the registry was empty, and any RegisterShader after the first raw draw threw. A title that registers some shaders through sceAgcCreateShader and also draws programs it never registers stops at its first such draw with "graphics program does not belong to a compatible registered shader": oops-sdk homebrew registers its display shaders while radeonsi (oops-mesa) submits raw programs. Graphics programs now resolve as compute dispatches already do: an address inside a registered shader resolves to that registration (a registration of another type still throws), any other address is read raw. RegisterShader throws only when the new shader's code overlaps a graphics program that was drawn raw, where the program would get a second identity (the draw cache revalidates raw programs by reading them raw), instead of after any raw draw. The drawn range is recorded once per raw snapshot: a flag on the snapshot keeps later decodes of the same program off the lock. Recorded ranges are merged, so the overlap check looks only at the predecessor of the queried end. Tests: agc_driver_prepared_graphics (a draw whose programs are registered as compute shaders is refused, where it used an address outside every registration before), agc_driver (ProgramSnapshot reads raw beside registered shaders and refuses a registered program of another type; nested, overlapping and adjacent raw ranges), agc_driver_prepared_unregistered_graphics (raw-drawn ranges recorded exactly, unrelated code not reported). Co-Authored-By: Claude Opus 5.5 --- .../include/Driver/Shaders/ShaderRegistry.hpp | 4 ++- .../Execution/src/Driver/Draw/DrawDecode.cpp | 2 +- .../src/Driver/Shaders/ShaderRegistry.cpp | 36 +++++++++++++++++-- .../libs/prx/libSceAgcDriver/tests/Submit.cpp | 29 +++++++++++++-- .../tests/execution/PreparedGraphics.cpp | 8 ++++- .../PreparedUnregisteredGraphics.cpp | 12 +++---- 6 files changed, 76 insertions(+), 15 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp index 9aa51c4f40..60a9fee237 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp @@ -87,6 +87,7 @@ struct ShaderSnapshot { std::shared_ptr registeredState; std::shared_ptr> workgroupUse = std::make_shared>(0u); std::shared_ptr> positionVectorUse = std::make_shared>(0u); + std::shared_ptr> rawGraphicsNoted = std::make_shared>(false); std::optional userDataTailRegister; }; @@ -94,8 +95,9 @@ struct ShaderSnapshot { std::shared_ptr ReadRawShader(std::uint64_t address); std::shared_ptr ReadUserDataTailContinuation(std::span code, std::uint32_t userDataBase, std::span userData); std::shared_ptr ProgramSnapshot(const ShaderRegistry& shaders, std::uint64_t address, std::initializer_list types); -void NoteRawGraphicsDraw(); +void NoteRawGraphicsDraw(const ShaderSnapshot& snapshot); bool RawGraphicsDrawn(); +bool RawGraphicsDrawnIn(std::uint64_t begin, std::uint64_t end); bool ProgramUsesWorkgroup(const ShaderSnapshot& snapshot, std::size_t codeOffset); // The PA_CL_VS_OUT_CNTL position export bits a vertex program is compiled with: none for a program // without pos1..pos3 exports, so its artifact does not depend on them. diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawDecode.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawDecode.cpp index f4a26ead03..34177240c4 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawDecode.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawDecode.cpp @@ -65,7 +65,7 @@ void DecodeGraphicsPrograms(DrawDecode& decoded, const QueueState& queue, const const auto& snapshot = *program; require((address - snapshot.codeAddress) % sizeof(std::uint32_t) == 0, "graphics entry point is not dword aligned"); require(snapshot.header.empty() || snapshot.type == type, "graphics program refers to an incompatible shader binary type"); - if (!nullPixel && snapshot.header.empty()) NoteRawGraphicsDraw(); + if (!nullPixel && snapshot.header.empty()) NoteRawGraphicsDraw(snapshot); const auto codeOffset = static_cast((address - snapshot.codeAddress) / sizeof(std::uint32_t)); DrawProgram result{ {stage, address, std::span(snapshot.code).subspan(codeOffset), snapshot.headerAddress, snapshot.header}, diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp index 30a4685d05..2ba97ad85a 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp @@ -24,7 +24,10 @@ #include #include #include +#include #include +#include +#include #include #include #ifdef __GLIBC__ @@ -142,15 +145,35 @@ std::shared_ptr ReadUserDataTailContinuation(std::span ProgramSnapshot(const ShaderRegistry& shaders, std::uint64_t address, std::initializer_list types) { if (auto registered = RegisteredProgram(shaders, address, types)) return registered; - require(shaders.empty(), "graphics program does not belong to a compatible registered shader"); return ReadRawShader(address); } namespace { std::atomic rawGraphicsDrawn{false}; +std::mutex rawGraphicsMutex; +std::map rawGraphicsRanges; } -void NoteRawGraphicsDraw() { +void NoteRawGraphicsDraw(const ShaderSnapshot& snapshot) { + if (snapshot.rawGraphicsNoted->load(std::memory_order_acquire)) return; + auto begin = snapshot.codeAddress; + auto end = snapshot.codeAddress + snapshot.code.size() * sizeof(std::uint32_t); + { + std::lock_guard lock(rawGraphicsMutex); + auto next = rawGraphicsRanges.upper_bound(begin); + if (next != rawGraphicsRanges.begin() && std::prev(next)->second >= begin) { + const auto previous = std::prev(next); + begin = previous->first; + end = std::max(end, previous->second); + rawGraphicsRanges.erase(previous); + } + while (next != rawGraphicsRanges.end() && next->first <= end) { + end = std::max(end, next->second); + next = rawGraphicsRanges.erase(next); + } + rawGraphicsRanges.emplace(begin, end); + } + snapshot.rawGraphicsNoted->store(true, std::memory_order_release); rawGraphicsDrawn.store(true, std::memory_order_relaxed); } @@ -158,6 +181,13 @@ bool RawGraphicsDrawn() { return rawGraphicsDrawn.load(std::memory_order_relaxed); } +bool RawGraphicsDrawnIn(std::uint64_t begin, std::uint64_t end) { + if (begin >= end) return false; + std::lock_guard lock(rawGraphicsMutex); + const auto next = rawGraphicsRanges.lower_bound(end); + return next != rawGraphicsRanges.begin() && std::prev(next)->second > begin; +} + namespace { void AwaitRegisteredPreparation(PreparedShaders& prepared, std::unique_lock& lock) { @@ -1434,7 +1464,6 @@ void Driver::RegisterShader(const Shader* shader) { PerformanceTimer timing("Shader.Register"); ShaderPreparationTransaction transaction; CheckFailure(); - require(!RawGraphicsDrawn(), "a shader was registered after graphics programs were drawn without registration"); GuestMemory::CheckRange(shader, sizeof(Shader), 1); Shader fields; std::memcpy(&fields, static_cast(shader), sizeof(Shader)); @@ -1444,6 +1473,7 @@ void Driver::RegisterShader(const Shader* shader) { GuestMemory::CheckRange(shader, fields.header_size, 1); const auto* code = const_cast(fields.code); GuestMemory::CheckRange(code, fields.shader_size, 256); + require(!RawGraphicsDrawnIn(reinterpret_cast(code), reinterpret_cast(code) + fields.shader_size), "a shader was registered over a graphics program that was drawn without registration"); ShaderSnapshot snapshot{reinterpret_cast(code), reinterpret_cast(shader), fields.type, {}, {}}; snapshot.code.resize(fields.shader_size / sizeof(std::uint32_t)); std::memcpy(snapshot.code.data(), code, fields.shader_size); diff --git a/core/libs/prx/libSceAgcDriver/tests/Submit.cpp b/core/libs/prx/libSceAgcDriver/tests/Submit.cpp index d08d98999a..f4fe928120 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Submit.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Submit.cpp @@ -671,9 +671,10 @@ void testProgramSnapshots() { shaders[registeredAddress] = {entry}; check(AgcDriver::DriverDetail::ProgramSnapshot(shaders, registeredAddress, {2}) == entry, "a registered program was not resolved to its registration"); check(AgcDriver::DriverDetail::ProgramSnapshot(shaders, registeredAddress + 4 * 8, {2}) == entry, "an entry inside registered code was not resolved to its registration"); - check(!expectFailure([&] { static_cast(AgcDriver::DriverDetail::ProgramSnapshot(shaders, rawAddress, {2})); }).empty(), "an unregistered program was read raw while shaders are registered"); - const auto unregistered = AgcDriver::DriverDetail::ProgramSnapshot({}, rawAddress, {2}); - check(unregistered != entry && unregistered->codeAddress == rawAddress && unregistered->header.empty() && unregistered->code.size() == 2, "an empty registry did not read the program as raw code"); + const auto unregistered = AgcDriver::DriverDetail::ProgramSnapshot(shaders, rawAddress, {2}); + check(unregistered != entry && unregistered->codeAddress == rawAddress && unregistered->header.empty() && unregistered->code.size() == 2, "an unregistered program beside registered shaders was not read as raw code"); + check(AgcDriver::DriverDetail::ProgramSnapshot({}, rawAddress, {2}) == unregistered, "an empty registry did not read the program as raw code"); + check(!expectFailure([&] { static_cast(AgcDriver::DriverDetail::ProgramSnapshot(shaders, registeredAddress, {1})); }).empty(), "a registered program was accepted as another shader type"); check(AgcDriver::DriverDetail::ProgramSnapshot({}, rawAddress, {2}) == unregistered, "unchanged raw code lost its snapshot identity"); check(!expectFailure([] { static_cast(AgcDriver::DriverDetail::ProgramSnapshot({}, 0, {2})); }).empty(), "an unmapped unregistered program was accepted"); AgcDriver::DriverDetail::DrawDecode decode{}; @@ -690,6 +691,27 @@ void testProgramSnapshots() { check(AgcDriver::DriverDetail::DecodeProgramsCurrent(decode, shaders), "a decode with the rewritten program was not current"); } +void testRawGraphicsRanges() { + using AgcDriver::DriverDetail::RawGraphicsDrawnIn; + constexpr std::uint64_t base = 0x7e0000000000ull; + const auto note = [](std::uint64_t address, std::size_t dwords) { + AgcDriver::DriverDetail::ShaderSnapshot snapshot{address, 0, 0, std::vector(dwords, 0xbf810000u), {}}; + AgcDriver::DriverDetail::NoteRawGraphicsDraw(snapshot); + check(snapshot.rawGraphicsNoted->load(), "a raw graphics program was not marked as noted"); + AgcDriver::DriverDetail::NoteRawGraphicsDraw(snapshot); + }; + note(base + 0x100, 0x10); + note(base, 0x100); + note(base + 0x3f0, 0x8); + note(base + 0x800, 0x1); + check(RawGraphicsDrawnIn(base + 0x200, base + 0x210), "code inside a raw program that contains a smaller one was not reported"); + check(RawGraphicsDrawnIn(base + 0x40c, base + 0x500), "the end of a raw program that overlaps another was not reported"); + check(!RawGraphicsDrawnIn(base + 0x410, base + 0x800), "code between raw programs was reported"); + check(!RawGraphicsDrawnIn(base - 4, base), "code before a raw program was reported"); + check(RawGraphicsDrawnIn(base + 0x700, base + 0x900), "a range covering a raw program was not reported"); + check(!RawGraphicsDrawnIn(base + 0x804, base + 0x808) && !RawGraphicsDrawnIn(base + 0x200, base + 0x200), "code after a raw program or an empty range was reported"); +} + void testProgramWorkgroupUse() { const auto snapshot = [](std::vector code) { AgcDriver::DriverDetail::ShaderSnapshot result{0x1000, 0, 2, std::move(code), {}}; @@ -860,6 +882,7 @@ int main(int argc, char** argv) { check(!unterminated.empty() && bounded->code.size() == 64, "raw compute crossed inaccessible memory or missed its last instruction"); #endif testProgramSnapshots(); + testRawGraphicsRanges(); testProgramWorkgroupUse(); testVertexTailSnapshots(); testRegisteredVertexWorkgroupRouting(); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedGraphics.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedGraphics.cpp index b296b49beb..1242c30e33 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedGraphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedGraphics.cpp @@ -304,7 +304,13 @@ void Check(AgcDriver::VulkanDevice& device, AgcDriver::Graphics::ShaderPath path registry.erase(late.snapshot->codeAddress); } ShaderRegistry invalidRegistry; - invalidRegistry[1] = {std::make_shared(ShaderSnapshot{1, 0, 2, {}, {}})}; + for (const auto& [address, snapshots] : registry) { + for (const auto& snapshot : snapshots) { + auto compute = *snapshot; + compute.type = 0; + invalidRegistry[address].push_back(std::make_shared(std::move(compute))); + } + } DrawDecode invalid{}; invalid.state = prepared.state; Reject([&] { DecodeGraphicsPrograms(invalid, queue, invalidRegistry, true, true); }, "registered"); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedUnregisteredGraphics.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedUnregisteredGraphics.cpp index 1915467395..df68d87509 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedUnregisteredGraphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedUnregisteredGraphics.cpp @@ -51,13 +51,13 @@ void Check(AgcDriver::VulkanDevice& device) { Require(draw.programs.size() == 2 && draw.roles.back() == ProgramRole::Fragment, "unregistered graphics programs did not decode as a vertex and a fragment stage"); for (const auto& program : draw.programs) Require(program.snapshot != nullptr && program.snapshot->header.empty() && program.snapshot->prepared->entries.empty(), "an unregistered graphics program was not read as raw code"); Require(RawGraphicsDrawn(), "a draw of unregistered graphics programs was not noted"); - bool refused = false; - try { - Driver::Get().RegisterShader(nullptr); - } catch (const std::exception& error) { - refused = std::string(error.what()).find("registered after graphics programs were drawn") != std::string::npos; + for (const auto& program : draw.programs) { + const auto begin = program.snapshot->codeAddress; + Require(RawGraphicsDrawnIn(begin, begin + 4u) && RawGraphicsDrawnIn(begin - 4u, begin + 1u), "a raw-drawn program's code is not recorded"); + Require(!RawGraphicsDrawnIn(begin - 4u, begin), "the raw-drawn range starts before the program"); } - Require(refused, "a shader registration after unregistered graphics draws was accepted"); + alignas(256) static std::array Elsewhere{0xbf810000u, 0xbf810000u, 0xbf810000u, 0xbf810000u}; + Require(!RawGraphicsDrawnIn(reinterpret_cast(Elsewhere.data()), reinterpret_cast(Elsewhere.data()) + sizeof(Elsewhere)), "unrelated code reads as drawn raw"); const auto target = device.Target(); std::vector linked; std::vector memory; From f991052cfd2de5f6e5c0030d3fc47b9aa4b84c31 Mon Sep 17 00:00:00 2001 From: XairRrR Date: Thu, 15 Oct 2026 02:54:50 +0200 Subject: [PATCH 0200/1000] test(agc): an empty register list into a null buffer still throws AI-assisted: yes --- core/libs/prx/libSceAgc/tests/RegisterCommands.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/core/libs/prx/libSceAgc/tests/RegisterCommands.cpp b/core/libs/prx/libSceAgc/tests/RegisterCommands.cpp index 251c1d767b..a1e0295daf 100644 --- a/core/libs/prx/libSceAgc/tests/RegisterCommands.cpp +++ b/core/libs/prx/libSceAgc/tests/RegisterCommands.cpp @@ -196,6 +196,7 @@ void testDirectList() { Storage empty; empty.words.fill(sentinel); check(writers[i](&empty.buffer, nullptr, 0) == empty.words.data() && empty.buffer.cursor_up == empty.words.data() && empty.words.front() == sentinel, "an empty register list wrote a packet"); + expectFailure([&] { writers[i](nullptr, nullptr, 0); }); check(sizes[i](0x15555555u) == 0xfffffffcu, "largest register list size mismatch"); for (const auto count : {0x15555556u, 0x40000000u, 0xffffffffu}) { expectFailure([&] { sizes[i](count); }); From 62bbbb5dcc0c117252a56277177864c8776f7228 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Wed, 14 Oct 2026 18:36:48 -0700 Subject: [PATCH 0201/1000] refactor(agc): count an overlay draw packet after its pending waits land Another open change adds a line at the start of the draw-packet branch in Driver::execute. The overlay's draw count now follows landPendingWaits instead, which counts the same packets. --- .../Execution/src/Driver/Packets/PacketExecution.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp index 903d447aaf..9340e0401c 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp @@ -277,8 +277,8 @@ void Driver::execute(const Submission& submission) { } } } else if (drawPacket) { - packetCounters.CountDraw(); landPendingWaits(submission.queue); + packetCounters.CountDraw(); bool drawn = false; timed(&WorkerProfile::drawMs, [&] { static const bool traceDraws = std::getenv("APS5_TRACE_DRAWS") != nullptr; From 4ef2af62ea4d7d77ac9e2b09dfe0e54c26119ebe Mon Sep 17 00:00:00 2001 From: SandstromPL Date: Thu, 15 Oct 2026 00:23:52 +0530 Subject: [PATCH 0202/1000] fix(agc): rasterize a multisampled depth target without MSAA_ENABLE at the pixel center A draw with PA_SC_AA_CONFIG.MSAA_NUM_SAMPLES 2 (4x) and MSAA_ENABLE clear into a 4x depth/stencil and color pair threw "multisampled targets rasterized without MSAA_ENABLE are unsupported for points, lines, a depth target or alpha-to-coverage", which stops METAL GEAR SOLID: Peace Walker (PPSA18216) at its first presentation (draw B on #5268). A depth target with the same sample count now gets the same pixel center samples as the color targets, so each depth and stencil sample is tested and written with the depth at the pixel center, as GL allows when multisampling is disabled. DB_EQAA Z interpolation or overrasterization and a sample-mask export with a depth target throw on this path. Co-Authored-By: Claude Opus 5.5 --- .../libSceAgcDriver/Graphics/src/State.cpp | 4 +- .../prx/libSceAgcDriver/tests/Graphics.cpp | 75 ++++++++++++++++++- docs/dev/debt/libSceAgcDriver.md | 2 +- 3 files changed, 78 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index ff6252d61b..3d31a0b45d 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -806,7 +806,9 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) { zero(cx, 0x292, ScanModeMask & ~1u, "scan conversion mode"); if (result.samples > 1 && (read(cx, 0x292) & 1u) == 0) { const bool filled = result.topology != VK_PRIMITIVE_TOPOLOGY_POINT_LIST && result.topology != VK_PRIMITIVE_TOPOLOGY_LINE_LIST && result.topology != VK_PRIMITIVE_TOPOLOGY_LINE_STRIP; - Require(filled && !result.depth && !result.alphaToCoverage, "multisampled targets rasterized without MSAA_ENABLE are unsupported for points, lines, a depth target or alpha-to-coverage"); + Require(filled && !result.alphaToCoverage, "multisampled targets rasterized without MSAA_ENABLE are unsupported for points, lines or alpha-to-coverage"); + if (const auto eqaa = find(cx, 0x201); result.depth && eqaa != cx.end() && (eqaa->second & 0x0f0c0000u) != 0) throw std::runtime_error(zeroMessage(0x201, eqaa->second, "a multisampled depth target rasterized without MSAA_ENABLE with Z interpolation or overrasterization (DB_EQAA)")); + if (const auto control = find(cx, 0x203); result.depth && control != cx.end() && (control->second & 0x100u) != 0) throw std::runtime_error(zeroMessage(0x203, control->second, "a multisampled depth target rasterized without MSAA_ENABLE with a sample-mask export")); result.customSampleLocations = true; result.sampleLocations.fill(0); result.sampleLocationGrid = false; diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 860cc8fb7a..8c1cc05c7f 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -2271,6 +2271,78 @@ VKAPI_ATTR VkResult VKAPI_CALL sampleLocationDepthProperties(VkPhysicalDevice, V return depthSampleResult; } +void msaaEnableClearDepthTests() { + using AgcDriver::Graphics::DecodeState; + const auto drawB = [] { + auto queue = makeState(); + queue.context[0x2f8] = 0x0020c002; + queue.context[0x292] = 2; + for (std::uint32_t offset = 0x2fe; offset <= 0x30d; ++offset) queue.context[offset] = (offset - 0x2feu) % 4u == 0 ? 0x622ae6aeu : 0u; + queue.context[0x31c] = 0x8028; + queue.context[0x31d] = 0x12000; + queue.context[0x3b8] = 0x0dc00000u; + queue.context[0x1b3] = 2; + queue.context[0x1b4] = 2; + queue.context[0x000] = 0; + queue.context[0x002] = 0; + queue.context[0x007] = (3u << 16u) | 63u; + queue.context[0x00a] = 0; + queue.context[0x00b] = 0; + queue.context[0x010] = 0xa290098b; + queue.context[0x011] = 0x20000180; + for (const auto offset : {0x012u, 0x014u}) queue.context[offset] = 0x100; + queue.context[0x200] = 0x16; + queue.context[0x201] = 0x00010002; + return queue; + }; + const auto requireCentered = [](const AgcDriver::Graphics::State& state, const std::string& what) { + Require(state.samples == 4 && state.depth && state.depth->samples == 4 && state.customSampleLocations, what + " did not keep the 4x depth target"); + const auto locations = AgcDriver::Graphics::SampleLocations(state); + for (std::uint32_t sample = 0; sample < state.samples; ++sample) Require(locations[sample].x == 0.5f && locations[sample].y == 0.5f, what + " did not place every sample at the pixel center"); + }; + auto queue = drawB(); + queue.context[0x200] = 0x711; + queue.context[0x011] = 0x20000181; + for (const auto offset : {0x013u, 0x015u}) queue.context[offset] = 0x200; + queue.context[0x10b] = 0x333; + queue.context[0x10c] = 0x00ff0000; + queue.context[0x10d] = 0; + auto state = DecodeState(queue); + Require(state.colors.size() == 1 && state.colors[0].samples == 4, "PPSA18216's draw B lost its 4x color target"); + Require(state.stencilTest && !state.depthTest && state.depth && state.depth->format == VK_FORMAT_D32_SFLOAT_S8_UINT && state.stencilFront.compareOp == VK_COMPARE_OP_ALWAYS, "PPSA18216's draw B (DB_DEPTH_CONTROL 0x711) lost its 4x stencil plane"); + Require(state.stencilFront.passOp == VK_STENCIL_OP_REPLACE && state.stencilFront.failOp == VK_STENCIL_OP_REPLACE && state.stencilFront.depthFailOp == VK_STENCIL_OP_REPLACE && state.stencilFront.writeMask == 0xffu && state.stencilFront.reference == 0u, "PPSA18216's draw B did not decode as a stencil clear to 0"); + requireCentered(state, "a 4x depth and stencil target rasterized without MSAA_ENABLE (PPSA18216's draw B)"); + Require(AgcDriver::Graphics::DrawRejection(queue, false).empty(), "the precheck refused PPSA18216's draw B"); + queue.context[0x200] = 0x710; + state = DecodeState(queue); + Require(!state.depth && state.samples == 4 && state.colors.size() == 1, "draw B with DB_DEPTH_CONTROL 0x710 bound a depth target"); + queue.context[0x201] = 0x00050002u; + Require(!DecodeState(queue).depth, "DB_EQAA Z interpolation refused a 4x draw without a depth target"); + queue = drawB(); + state = DecodeState(queue); + Require(state.depthTest && state.colors.size() == 1, "a depth-tested 4x depth and color pair lost a target"); + requireCentered(state, "a depth-tested 4x depth and color pair rasterized without MSAA_ENABLE"); + queue.context[0x8e] = 0; + state = DecodeState(queue); + Require(state.colors.empty(), "a depth-only 4x draw without MSAA_ENABLE decoded a color target"); + requireCentered(state, "a 4x depth target rasterized without MSAA_ENABLE"); + for (const auto eqaa : {0x00050002u, 0x00090002u, 0x01010002u, 0x08010002u}) { + queue = drawB(); + queue.context[0x201] = eqaa; + expectFailure([&] { DecodeState(queue); }, "a multisampled depth target rasterized without MSAA_ENABLE with Z interpolation or overrasterization (DB_EQAA)"); + } + queue = drawB(); + queue.context[0x1c4] = 9; + queue.context[0x203] |= 0x100u; + expectFailure([&] { DecodeState(queue); }, "a multisampled depth target rasterized without MSAA_ENABLE with a sample-mask export"); + queue = drawB(); + queue.context[0x2dc] = 0x0000aa01u; + expectFailure([&] { DecodeState(queue); }, "multisampled targets rasterized without MSAA_ENABLE are unsupported for points, lines or alpha-to-coverage"); + queue = drawB(); + queue.context[0x010] = 0xa2900987; + expectFailure([&] { DecodeState(queue); }, "the depth target's sample count differs from the rasterizer's"); +} + void multisampleTests() { using AgcDriver::Graphics::ColorMetadataPass; using AgcDriver::Graphics::DecodeColorBuffer; @@ -2469,7 +2541,7 @@ void multisampleTests() { const auto aliased = AgcDriver::Graphics::SampleLocations(state); for (std::uint32_t sample = 0; sample < state.samples; ++sample) Require(aliased[sample].x == 0.5f && aliased[sample].y == 0.5f, "a 2x target rasterized without MSAA_ENABLE did not place every sample at the pixel center"); queue.context[0x2dc] = 0x0000aa01u; - expectFailure([&] { DecodeState(queue); }, "without MSAA_ENABLE are unsupported for points, lines, a depth target or alpha-to-coverage"); + expectFailure([&] { DecodeState(queue); }, "without MSAA_ENABLE are unsupported for points, lines or alpha-to-coverage"); queue = multisampled(); queue.context[0x292] = 2; queue.context[0x302] = 0x44cc; @@ -6612,6 +6684,7 @@ int main(int argc, char** argv) { cmaskTests(); uint16ExportTests(); uint8x4TargetTests(); + msaaEnableClearDepthTests(); multisampleTests(); MsaaEnableSingleSampleTests(); clipDistanceTests(); diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index ef52ccdb68..d18dff3dab 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -9,7 +9,7 @@ - [GS_FAST_LAUNCH geometry](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - `VGT_SHADER_STAGES_EN.GS_FAST_LAUNCH = 1` with a point list and `GE_CNTL` groups of one vertex and one primitive decodes as one point per subgroup, with the primitive limit taken from `GE_NGG_SUBGRP_CNTL.PRIM_AMP_FACTOR` and the GS/ES VGPR counts not required. Follows how RADV drives mesh shaders on gfx10.3 (`ac_nir_lower_ngg.c`, the workgroup index comes from the vertex id of the first thread), not measured on the console. Fast launch 2, other group sizes or input primitives and indexed fast-launch draws still throw. PPSA03671's fast-launch draws decode, but the title stops earlier in a wave64 vertex program, so they have not reached the GPU yet. - [Alpha-to-coverage](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - only `DB_ALPHA_TO_MASK = 0xaa01` is accepted: the enable bit with the four quad offsets at 2 and no `OFFSET_ROUND`, the undithered setting [radeonsi](https://gitlab.freedesktop.org/mesa/mesa/-/blob/bffd91947efc1932da99aca69fc540d6d4201b0c/src/gallium/drivers/radeonsi/si_state.c#L371-L381) programs. Draws are single-sampled, so the pixel shader [writes](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) `SampleMask` 0 when its MRT0 alpha is below one half or NaN and all ones otherwise, and runs with late fragment tests. That rule was measured on a Radeon 660M (gfx1035, RADV with `RADV_DEBUG=noatocdithering`, which programs the same register) for 0, -0, 0.25, the two floats below one half, one half, the two above, 0.75, 1, 2, -1, both infinities and both NaNs, and is pinned in [AlphaToCoverage.cpp](../../../core/libs/prx/libSceAgcDriver/tests/execution/AlphaToCoverage.cpp); it is not measured on gfx1013. Vulkan's `alphaToCoverageEnable` is not used: the NVIDIA driver 615.78.08 dithers single-sample coverage (an alpha of 0.25 writes half the pixels of an RTX 3050 Laptop target). Other offsets, `OFFSET_ROUND`, a sample-mask export, and an MRT0 that is missing, swizzled, packed or exported without a floating-point alpha throw. A draw without a color target on MRT0 keeps alpha-to-coverage from its MRT0 export, as [radeonsi exports MRT0 as `32_AR` for alpha-to-coverage without a color buffer](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-24.0.0/src/gallium/drivers/radeonsi/si_state_shaders.cpp#L2425-L2431). A draw that enables alpha-to-coverage with no MRT0 export still throws: what the console covers then is not measured. Default dithered `DB_ALPHA_TO_MASK = 0x18701` additionally uses a per-quad sample-mask table [measured on gfx1036](https://github.com/boykopovar/AnyPS5/pull/4451#issuecomment-6124848921) for eight standard samples with FP16_ABGR or 32_ABGR exports; other sample counts, custom locations, EQAA combinations and dithered UNORM16/SNORM16 exports throw. The dithered rule is not measured on gfx1013. - [MSAA_ENABLE on a single-sample target](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - `PA_SC_MODE_CNTL_0.MSAA_ENABLE` with `PA_SC_AA_CONFIG.MSAA_NUM_SAMPLES = 0` is drawn like the same state without the bit when sample 0 of the four `PA_SC_AA_SAMPLE_LOCS_PIXEL_*_0` registers is at the pixel center and the draw rasterizes filled triangles with alpha-to-coverage and conservative rasterization off. [RADV](https://gitlab.freedesktop.org/mesa/mesa/-/blob/745709a000482a4a24c91faef5c5a40bd653a863/src/amd/vulkan/radv_cmd_buffer.c#L13606-13609) programs this state for one-sample custom sample locations on GFX10 and later, where the bit makes the rasterizer use the programmed location of sample 0 ([061bc615](https://gitlab.freedesktop.org/mesa/mesa/-/commit/061bc6151a11597f720535f00e79a5752e24aff6)); that the bit changes nothing else for such a draw is inferred from that and is not measured on hardware. Other locations of sample 0, absent location registers, points, lines, rectangles, alpha-to-coverage and conservative rasterization throw -- [A multisampled target rasterized without MSAA_ENABLE](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - `PA_SC_AA_CONFIG.MSAA_NUM_SAMPLES` > 0 with `PA_SC_MODE_CNTL_0.MSAA_ENABLE` clear is drawn with the target's sample count and every sample at the pixel center, as a single-sample scan into a multisampled target already is. On GFX10 and later the bit is what makes the rasterizer use the programmed sample locations ([RADV 061bc615](https://gitlab.freedesktop.org/mesa/mesa/-/commit/061bc6151a11597f720535f00e79a5752e24aff6)), and with multisampling disabled GL rasterizes at the pixel center and gives the fragment full coverage, which radeonsi programs by clearing the bit; that the hardware does the same with `MSAA_NUM_SAMPLES` > 0 is inferred from these and is not measured on hardware. Points, lines, a depth target and alpha-to-coverage throw. SONIC SUPERSTARS (PPSA06888) draws such a 2x target. +- [A multisampled target rasterized without MSAA_ENABLE](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - `PA_SC_AA_CONFIG.MSAA_NUM_SAMPLES` > 0 with `PA_SC_MODE_CNTL_0.MSAA_ENABLE` clear is drawn with the target's sample count and every sample at the pixel center, as a single-sample scan into a multisampled target already is. On GFX10 and later the bit is what makes the rasterizer use the programmed sample locations ([RADV 061bc615](https://gitlab.freedesktop.org/mesa/mesa/-/commit/061bc6151a11597f720535f00e79a5752e24aff6)), and with multisampling disabled GL rasterizes at the pixel center and gives the fragment full coverage, which radeonsi programs by clearing the bit; that the hardware does the same with `MSAA_NUM_SAMPLES` > 0 is inferred from these and is not measured on hardware. A depth target with the same sample count also has every sample at the pixel center, so each depth and stencil sample is tested and written with the depth at the pixel center, as GL allows when multisampling is disabled ([L185-L190](https://github.com/KhronosGroup/OpenGL-Registry/blob/6af574a14089ccfee87efe230ebcdd8742859813/extensions/ARB/ARB_multisample.txt#L185-L190), [L422-L425](https://github.com/KhronosGroup/OpenGL-Registry/blob/6af574a14089ccfee87efe230ebcdd8742859813/extensions/ARB/ARB_multisample.txt#L422-L425)). GL also lets an implementation test only a centermost sample and set every sample from it ([L425-L434](https://github.com/KhronosGroup/OpenGL-Registry/blob/6af574a14089ccfee87efe230ebcdd8742859813/extensions/ARB/ARB_multisample.txt#L425-L434)), which differs when a pixel's stored samples differ; which one the console does, and how its occlusion queries count such a draw, is not measured on hardware. `DB_EQAA` Z interpolation or overrasterization and a sample-mask export with a depth target throw. Points, lines and alpha-to-coverage throw. SONIC SUPERSTARS (PPSA06888) draws such a 2x target, and METAL GEAR SOLID: Peace Walker (PPSA18216) a 4x depth and color pair. - [sceAgcDriverGetEqContextId](../../../core/libs/prx/libSceAgcDriver/Eq/src/Query.cpp) (libSceAgcDriver) - returns `ident`: an end-of-pipe interrupt is delivered only to the registration whose id equals the raising queue, so `ident` is that context's id (titles register 0 or 1 for graphics and `0x20 + 8 * pipe + queue` for compute). The console's value is not known; KytyPS5 returns the event's `data`, which it fills with the interrupt's context id, whereas `data` here is the EV_CLEAR trigger count. Other event filters throw - [User data registers](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Diagnostics.cpp) (AGC driver) - a `SPI_SHADER_USER_DATA_*` or `COMPUTE_USER_DATA_*` register the title never wrote reads as 0 when a stage's RSRC2 count covers it, as Kyty (zeroed `UserSgprInfo`) and SharpEmu (missing registers read 0) do, and as AMD PAL and Mesa initialize them; the console's value is not confirmed - [SET_PREDICATION](../../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (AGC driver) - boolean predication follows the AMD PM4 encoding [Mesa](https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/src/amd/common/sid.h) uses: operation 3 reads 64 bits and 4 reads 32 bits, and bit 8 runs predicated packets when the value is non-zero (clear: when it is zero); the bit 12 hint is ignored. The address must have its low 4 bits clear, as `sceAgcDcbSetPredication` writes it. Query predication (Z-pass, primitive count), a predicated COND_EXEC, predicated command buffer chains and flips in predicated command buffers throw From 0aa3b87448926dbfa853fc7bd4cc0c0ced5bc52b Mon Sep 17 00:00:00 2001 From: valenciana-nc Date: Thu, 15 Oct 2026 02:36:05 +0000 Subject: [PATCH 0203/1000] fix(libSceAudioOut): check the audio buffer size for 32-bit overflow prepareBuffer and queueAudio computed frames * channels * bytesPerSample in 32-bit arithmetic. A large samplesNum from sceAudioOutOpen (which only rejects len == 0) wraps the product, so buf.resize allocates a truncated buffer while the mixing loops write frames * channels samples past it. Compute the size in 64 bits and throw past INT32_MAX, which also keeps the downstream int casts well-defined. --- core/libs/prx/libSceAudioOut/src/AudioOut.cpp | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut.cpp b/core/libs/prx/libSceAudioOut/src/AudioOut.cpp index 2f63a8fb03..ac7919ef81 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut.cpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut.cpp @@ -94,6 +94,14 @@ static std::uint32_t bytesPerSample(Format f) { return formatIsFloat(f) ? sizeof(float) : sizeof(std::int16_t); } +static std::uint32_t checkedBufferSize(std::uint32_t frames, std::uint32_t channels, std::uint32_t bps) { + const std::uint64_t size = static_cast(frames) * channels * bps; + if (size > static_cast(std::numeric_limits::max())) { + throw std::runtime_error("checkedBufferSize: audio buffer size overflows"); + } + return static_cast(size); +} + struct Port { bool used = false; int type = 0; @@ -160,7 +168,7 @@ static const void* prepareBuffer(const Port& port, const void* data, std::vector const auto frames = port.samplesNum; const auto ch = static_cast(port.channels); const auto bps = bytesPerSample(port.format); - const auto size = frames * ch * bps; + const auto size = checkedBufferSize(frames, ch, bps); bool volumeChanged = port.mixLevel != DEFAULT_VOLUME; for (std::uint32_t i = 0; i < ch; i++) { @@ -227,8 +235,8 @@ static void queueAudio(Port& port, const void* data, std::unique_lock prepareBuf; const void* prepared = prepareBuffer(port, data, prepareBuf); - const std::uint32_t preparedSize = port.samplesNum * - static_cast(port.channels) * bytesPerSample(port.format); + const std::uint32_t preparedSize = checkedBufferSize(port.samplesNum, + static_cast(port.channels), bytesPerSample(port.format)); SDL_AudioCVT cvt{}; const int cvtResult = SDL_BuildAudioCVT( From d4b33b0ae859433a5d15e0911054767663a0ac7e Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Thu, 15 Oct 2026 02:48:55 +0000 Subject: [PATCH 0204/1000] perf(shader): select the record of a T# whose offset the CPU evaluates Since #757, a T# loaded from a buffer heap at a runtime offset is an image table column, and every capture snapshots the whole heap, up to 65,536 records, even when user data or an SRT read fixes the offset. When the T#'s eight dwords and its offset are runtime values the SRT walker evaluates, the column is now selected: the offset is kept as a descriptor source, the shader indexes entry 0, and the snapshot reads only the record at the evaluated offset, with the heap base moved to that record. The column keeps a table's semantics. An invalid T#, an unmapped record or an offset that carries past 32 bits is poison that faults only when a lane selects it; an offset the walker cannot read faults the table instead of throwing; the record's mode is picked per entry, so no pipeline variant is added; and a dispatch that writes the record faults it as written by its dispatch. Dwords past the heap read zeros, as the scalar loads would, and a record past the key cap is read rather than poisoned. A column stays a whole table when its offset is chosen per lane, or when the shader may write the memory the offset is read from at run time: the buffer it is read from (by V#), or any memory when the read or a write goes through a pointer or a GPU-only descriptor. Samplers are unchanged: an S# the walker evaluates is already a direct sampler. The plan stores the selection, so the disk cache format is 36. Five agc_shader_memory and agc_shader_bindless checks asserted the old planning and now assert the selected record: - "image table SRT key": a key read through the SRT narrowed a 4-key table to records 0 and 2. It now selects record 2 alone, captures no other record, and a changed key moves the capture without changing the module or its disk key. - "image table key union" and "image table late key": two keys of one heap shared one column with two value key domains. Each key now selects its own record, as two one-record tables. - "image table unkeyed access": a read keyed through the SRT shared a whole column with a read keyed per lane. The first now selects its record; the second keeps the whole, unnarrowed table. - "uniform table image": the T# at a user data offset was one key of a full table. It is now the one selected record, and the record the user data does not select is not read. A value key domain only arises for a key the walker evaluates, so it no longer narrows an image column; key buffer domains (keys loaded per lane) still do. --- .../libSceAgcDriver/tests/ShaderMemory.cpp | 85 +++++++++++-------- .../IrMetadata/ControlFlowInfo.hpp | 4 +- .../Optimization/src/ResourceMaterializer.cpp | 52 +++++++++++- .../Optimization/src/ResourceTracker.cpp | 72 +++++++++++++++- core/shader/recompiler/ShaderDiskCache.cpp | 12 ++- core/shader/recompiler/ShaderDiskCache.hpp | 2 +- docs/dev/ARCHITECTURE.md | 6 +- docs/dev/debt/libSceAgcDriver.md | 2 +- docs/dev/debt/shader.md | 4 +- 9 files changed, 188 insertions(+), 51 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 462bbcd8ff..8ba52c6cc2 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -1054,6 +1054,13 @@ TableCompiled CompileTable(ShaderRecompiler::RecompileRequest request) { return out; } +std::array TableSelected(const ShaderRecompiler::ImageTableSnapshot& tables, std::uint32_t table, const char* message) { + namespace Abi = ShaderRecompiler::ImageTableAbi; + const auto& column = tables.tables.at(table); + require(column.records == 1u && column.keys == 1u && column.codes.size() == 1u && (column.codes[0] & Abi::PoisonFlag) == 0u, message); + return tables.words.at(column.codes[0]).dwords; +} + void verifyImageTableKeys() { using namespace ShaderRecompiler; namespace Abi = ImageTableAbi; @@ -1359,7 +1366,6 @@ void verifyImageTables() { void verifyImageTableKeyUnion() { using namespace ShaderRecompiler; - namespace Abi = ImageTableAbi; static TableTexture textures[3]; alignas(4096) static TableGuest guest; auto& heap = guest.heap; @@ -1374,24 +1380,25 @@ void verifyImageTableKeyUnion() { const auto userData = TableUserData(guest); const auto request = TableRequest(TableTwoKeyCode, userData); const auto plan = GetResourcePlan(request); - require(plan->info.images.size() == 1u && plan->info.images[0].table == 0u, "image table key union: two keyed reads of one table were not planned as one image of one column"); - const auto& column = plan->descriptorSources.at(plan->info.images[0].source).tableColumn; - require(column.has_value() && column->keyDomains.size() == 2u && column->keyDomains[0].value && column->keyDomains[1].value && column->keyDomains[0].source != column->keyDomains[1].source, "image table key union: the column does not carry both value key domains"); + require(plan->info.images.size() == 2u && plan->info.images[0].table == 0u && plan->info.images[1].table == 1u, "image table key union: two keyed reads of one heap were not planned as two tables"); + const auto& first = plan->descriptorSources.at(plan->info.images[0].source).tableColumn; + const auto& second = plan->descriptorSources.at(plan->info.images[1].source).tableColumn; + require(first.has_value() && second.has_value() && first->selected && second->selected && first->heapSource == second->heapSource && first->selection != second->selection, "image table key union: the two keys do not each select a record of the one heap"); + const auto held = [](const ImageTableSnapshot& tables, std::uint32_t table) { return TableSelected(tables, table, "image table key union: a table does not hold one readable selected record"); }; const auto compiled = CompileTable(request); - const auto& snapshot = compiled.capture->snapshot.tables.tables.at(0); - const auto outside = Abi::PoisonCode(static_cast(Abi::PoisonReason::OutsideDomain)); - require(compiled.capture->snapshot.tables.tables.size() == 1u && snapshot.keys == 4u && (snapshot.codes[0] & Abi::PoisonFlag) == 0u && (snapshot.codes[1] & Abi::PoisonFlag) == 0u && snapshot.codes[2] == outside && (snapshot.codes[3] & Abi::PoisonFlag) == 0u, "image table key union: the snapshot does not hold the union of the two keys' records"); + const auto& tables = compiled.capture->snapshot.tables; + require(tables.tables.size() == 2u && held(tables, 0u) == heap[1] && held(tables, 1u) == heap[3], "image table key union: the tables do not hold the two keys' records"); + require(TableCovered(compiled.regions, &heap[1], sizeof(heap[1])) && TableCovered(compiled.regions, &heap[3], sizeof(heap[3])) && !TableCovered(compiled.regions, &heap[0], sizeof(std::uint32_t)) && !TableCovered(compiled.regions, &heap[2], sizeof(std::uint32_t)), "image table key union: records no key selects were captured"); const auto* images = TableBinding(*compiled.result, DescriptorRole::ImageTable); - require(images != nullptr && images->count == 3u, "image table key union: the binding does not hold the three distinct images of the selected records"); + require(images != nullptr && images->count == 2u, "image table key union: the binding does not hold the two distinct images of the selected records"); guest.srt[17] = 2u; const auto moved = CompileTable(request); - const auto& movedSnapshot = moved.capture->snapshot.tables.tables.at(0); - require((movedSnapshot.codes[2] & Abi::PoisonFlag) == 0u && movedSnapshot.codes[3] == outside && (movedSnapshot.codes[1] & Abi::PoisonFlag) == 0u && moved.result->spirv == compiled.result->spirv, "image table key union: a changed second key did not move the union or changed the module"); + const auto& movedTables = moved.capture->snapshot.tables; + require(held(movedTables, 0u) == heap[1] && held(movedTables, 1u) == heap[2] && TableCovered(moved.regions, &heap[2], sizeof(heap[2])) && !TableCovered(moved.regions, &heap[3], sizeof(std::uint32_t)) && moved.result->spirv == compiled.result->spirv, "image table key union: a changed second key did not move its record alone or changed the module"); } void verifyImageTableLateKey() { using namespace ShaderRecompiler; - namespace Abi = ImageTableAbi; static TableTexture textures[3]; alignas(4096) static TableGuest guest; auto& heap = guest.heap; @@ -1406,15 +1413,16 @@ void verifyImageTableLateKey() { const auto userData = TableUserData(guest); const auto request = TableRequest(TableLateKeyCode, userData); const auto plan = GetResourcePlan(request); - require(plan->info.images.size() == 1u && plan->info.images[0].table == 0u, "image table late key: two keyed reads of one table were not planned as one image of one column"); - const auto& column = plan->descriptorSources.at(plan->info.images[0].source).tableColumn; - require(column.has_value() && column->keyDomains.size() == 2u && column->keyDomains[0].value && column->keyDomains[1].value && column->keyDomains[0].source != column->keyDomains[1].source, "image table late key: the column does not carry both value key domains"); + require(plan->info.images.size() == 2u && plan->info.images[0].table == 0u && plan->info.images[1].table == 1u, "image table late key: two keyed reads of one heap were not planned as two tables"); + const auto& first = plan->descriptorSources.at(plan->info.images[0].source).tableColumn; + const auto& second = plan->descriptorSources.at(plan->info.images[1].source).tableColumn; + require(first.has_value() && second.has_value() && first->selected && second->selected && first->selection != second->selection, "image table late key: a key read after the first image operation does not select its own record"); + const auto held = [](const ImageTableSnapshot& tables, std::uint32_t table) { return TableSelected(tables, table, "image table late key: a table does not hold one readable selected record"); }; const auto compiled = CompileTable(request); - const auto& snapshot = compiled.capture->snapshot.tables.tables.at(0); - const auto outside = Abi::PoisonCode(static_cast(Abi::PoisonReason::OutsideDomain)); - require(compiled.capture->snapshot.tables.tables.size() == 1u && snapshot.keys == 4u && (snapshot.codes[0] & Abi::PoisonFlag) == 0u && (snapshot.codes[1] & Abi::PoisonFlag) == 0u && snapshot.codes[2] == outside && (snapshot.codes[3] & Abi::PoisonFlag) == 0u, "image table late key: the snapshot does not hold the union of the two keys' records"); + const auto& tables = compiled.capture->snapshot.tables; + require(tables.tables.size() == 2u && held(tables, 0u) == heap[1] && held(tables, 1u) == heap[3], "image table late key: the tables do not hold the two keys' records"); const auto* images = TableBinding(*compiled.result, DescriptorRole::ImageTable); - require(images != nullptr && images->count == 3u, "image table late key: the binding does not hold the three distinct images of the selected records"); + require(images != nullptr && images->count == 2u, "image table late key: the binding does not hold the two distinct images of the selected records"); } void verifyImageTableUnkeyedAccess() { @@ -1437,13 +1445,19 @@ void verifyImageTableUnkeyedAccess() { const auto userData = TableUserData(guest); const auto request = TableRequest(*code, userData); const auto plan = GetResourcePlan(request); - require(!plan->info.images.empty() && std::all_of(plan->info.images.begin(), plan->info.images.end(), [](const ImageResource& image) { return image.table == 0u; }), "image table unkeyed access: the two reads were not planned as one table"); - const auto& column = plan->descriptorSources.at(plan->info.images[0].source).tableColumn; - require(column.has_value() && column->unkeyed && column->keyDomains.empty(), "image table unkeyed access: a column shared with a read that has no key domain kept a key domain"); + const auto& planned = plan->info.images; + require(planned.size() == 2u && std::all_of(planned.begin(), planned.end(), [](const ImageResource& image) { return image.table != NoTable; }), "image table unkeyed access: the two reads were not planned as two tables"); + const auto columnOf = [&](const ImageResource& image) { return *plan->descriptorSources.at(image.source).tableColumn; }; + const auto selected = std::find_if(planned.begin(), planned.end(), [&](const ImageResource& image) { return columnOf(image).selected; }); + const auto whole = std::find_if(planned.begin(), planned.end(), [&](const ImageResource& image) { return !columnOf(image).selected; }); + require(selected != planned.end() && whole != planned.end(), "image table unkeyed access: the read keyed through the SRT does not select its record beside the read keyed per lane"); + require(columnOf(*whole).unkeyed && columnOf(*whole).keyDomains.empty(), "image table unkeyed access: the read keyed per lane kept a key domain"); const auto compiled = CompileTable(request); - const auto& snapshot = compiled.capture->snapshot.tables.tables.at(0); + const auto& tables = compiled.capture->snapshot.tables; + require(TableSelected(tables, selected->table, "image table unkeyed access: a table does not hold one readable selected record") == heap[2], "image table unkeyed access: the read keyed through the SRT does not hold its record"); + const auto& snapshot = tables.tables.at(whole->table); const auto outside = Abi::PoisonCode(static_cast(Abi::PoisonReason::OutsideDomain)); - require(snapshot.keys == 4u && std::none_of(snapshot.codes.begin(), snapshot.codes.end(), [&](std::uint32_t code) { return code == outside; }), "image table unkeyed access: the shared column was narrowed by the keyed read"); + require(snapshot.keys == 4u && std::none_of(snapshot.codes.begin(), snapshot.codes.end(), [&](std::uint32_t entry) { return entry == outside; }), "image table unkeyed access: the table keyed per lane was narrowed"); const auto* images = TableBinding(*compiled.result, DescriptorRole::ImageTable); require(images != nullptr && images->count == 4u, "image table unkeyed access: the binding does not hold the whole domain"); } @@ -1467,18 +1481,19 @@ void verifyImageTableSrtKey() { const auto plan = GetResourcePlan(request); require(plan->info.images.size() == 1u && plan->info.images[0].table == 0u, "image table SRT key: the T# was not planned as a table"); const auto& column = plan->descriptorSources.at(plan->info.images[0].source).tableColumn; - require(column.has_value() && !column->keyDomains.empty(), "image table SRT key: a key read through the SRT has no key domain"); + require(column.has_value() && column->selected && column->selection < plan->descriptorSources.size(), "image table SRT key: a T# whose key is read through the SRT does not select its record"); const auto compiled = CompileTable(request); - const auto& snapshot = compiled.capture->snapshot.tables.tables.at(0); - const auto outside = Abi::PoisonCode(static_cast(Abi::PoisonReason::OutsideDomain)); - require(snapshot.records == 4u && snapshot.keys == 4u && snapshot.codes.size() == 4u && (snapshot.codes[0] & Abi::PoisonFlag) == 0u && snapshot.codes[1] == outside && (snapshot.codes[2] & Abi::PoisonFlag) == 0u && snapshot.codes[3] == outside, "image table SRT key: the snapshot was not narrowed to the key read through the SRT"); + const auto& tables = compiled.capture->snapshot.tables; + const auto& snapshot = tables.tables.at(0); + require(snapshot.records == 1u && snapshot.keys == 1u && snapshot.codes.size() == 1u && (snapshot.codes[0] & Abi::PoisonFlag) == 0u && tables.words.at(snapshot.codes[0]).dwords == heap[2], "image table SRT key: the snapshot does not hold the selected record alone"); const auto* images = TableBinding(*compiled.result, DescriptorRole::ImageTable); - require(images != nullptr && images->count == 2u, "image table SRT key: the binding holds other entries than the selected one and the first"); - require(TableCovered(compiled.regions, &guest.srt[16], sizeof(std::uint32_t)), "image table SRT key: the key was not captured"); + require(images != nullptr && images->count == 1u, "image table SRT key: the binding holds other entries than the selected one"); + require(TableCovered(compiled.regions, &guest.srt[16], sizeof(std::uint32_t)) && TableCovered(compiled.regions, &heap[2], sizeof(heap[2])), "image table SRT key: the key or the selected record was not captured"); + require(!TableCovered(compiled.regions, &heap[0], sizeof(std::uint32_t)) && !TableCovered(compiled.regions, &heap[1], sizeof(std::uint32_t)) && !TableCovered(compiled.regions, &heap[3], sizeof(std::uint32_t)), "image table SRT key: records the key does not select were captured"); guest.srt[16] = 3u; const auto moved = CompileTable(request); - const auto& movedSnapshot = moved.capture->snapshot.tables.tables.at(0); - require((movedSnapshot.codes[3] & Abi::PoisonFlag) == 0u && movedSnapshot.codes[2] == outside && movedSnapshot.codes[1] == outside && moved.result->spirv == compiled.result->spirv && moved.key == compiled.key, "image table SRT key: a changed key did not move the snapshot alone"); + const auto& movedTables = moved.capture->snapshot.tables; + require(movedTables.words.at(movedTables.tables.at(0).codes.at(0)).dwords == heap[3] && TableCovered(moved.regions, &heap[3], sizeof(heap[3])) && !TableCovered(moved.regions, &heap[2], sizeof(std::uint32_t)) && moved.result->spirv == compiled.result->spirv && moved.key == compiled.key, "image table SRT key: a changed key did not move the selected record alone"); } void verifyImageTableMaskedOffset() { @@ -1987,15 +2002,15 @@ void verifyUniformTableImage() { request.layout.pushConstantSizeBytes = 128; const auto plan = GetResourcePlan(request); const auto& column = plan->descriptorSources.at(plan->info.images.at(0).source).tableColumn; - require(plan->info.images.size() == 1u && plan->info.images[0].table == 0u && column.has_value() && column->stride == 48u && column->dwordCount == 8u, "uniform table image: the T# read from a constant buffer at a user data offset was not planned as one table column"); + require(plan->info.images.size() == 1u && plan->info.images[0].table == 0u && column.has_value() && column->stride == 48u && column->dwordCount == 8u && column->selected, "uniform table image: the T# read from a constant buffer at a user data offset does not select its record"); for (const auto& [key, expected] : {std::pair{2u, second}, std::pair{1u, first}}) { userData[2] = key; AgcDriver::ShaderMemory memory({}); const auto capture = memory.Capture(request); const auto& tables = capture->snapshot.tables; - require(tables.tables.size() == 1u && key < tables.tables[0].codes.size(), "uniform table image: the capture has no table for the user data key"); - const auto code = tables.tables[0].codes[key]; - require((code & ImageTableAbi::PoisonFlag) == 0u && std::equal(expected.begin(), expected.end(), tables.words.at(code).dwords.begin()), "uniform table image: the T# of the record the user data selects was not read"); + require(tables.tables.size() == 1u && TableSelected(tables, 0u, "uniform table image: a table does not hold one readable selected record") == expected, "uniform table image: the T# of the record the user data selects was not read"); + const auto regions = memory.Regions(); + require(TableCovered(regions, guest.table[key].data() + 4, sizeof(expected)) && !TableCovered(regions, guest.table[3u - key].data() + 4, sizeof(std::uint32_t)), "uniform table image: the record the user data does not select was read"); } } diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ControlFlowInfo.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ControlFlowInfo.hpp index c402bbee7e..9be02f1133 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ControlFlowInfo.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ControlFlowInfo.hpp @@ -44,9 +44,11 @@ struct TableColumn { std::uint32_t indirectImmediate = 0; std::uint32_t foldedAddend = 0; std::uint32_t foldedCarry = 0; + bool selected = false; + std::uint32_t selection = 0; bool operator==(const TableColumn& other) const { - return heapSource == other.heapSource && stride == other.stride && addend == other.addend && offset == other.offset && dwordCount == other.dwordCount && sampler == other.sampler && address == other.address && indirect == other.indirect && indirectOffset == other.indirectOffset && indirectImmediate == other.indirectImmediate && foldedAddend == other.foldedAddend && foldedCarry == other.foldedCarry; + return heapSource == other.heapSource && stride == other.stride && addend == other.addend && offset == other.offset && dwordCount == other.dwordCount && sampler == other.sampler && address == other.address && indirect == other.indirect && indirectOffset == other.indirectOffset && indirectImmediate == other.indirectImmediate && foldedAddend == other.foldedAddend && foldedCarry == other.foldedCarry && selected == other.selected && selection == other.selection; } }; diff --git a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp index 3ed3870d7b..2f041a1804 100644 --- a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp @@ -314,6 +314,7 @@ struct TableCounters { std::array, PoisonReasonCount> poison{}; std::atomic narrowed{0}; std::atomic narrowSkipped{0}; + std::atomic selected{0}; std::atomic nanoseconds{0}; std::atomic lastReport{0}; }; @@ -341,8 +342,8 @@ void reportTables() { if (count == 0) continue; poison += " " + std::string(ImageTableAbi::PoisonReasonName(static_cast(reason))) + "=" + std::to_string(count); } - std::fprintf(stderr, "[image-table] (10 s): %llu snapshots, %llu tables, %llu keys, structural poison:%s, narrowing %llu applied / %llu skipped, %.1f ms\n", - static_cast(snapshots), static_cast(counters.tables.exchange(0, std::memory_order_relaxed)), static_cast(counters.keys.exchange(0, std::memory_order_relaxed)), poison.empty() ? " none" : poison.c_str(), + std::fprintf(stderr, "[image-table] (10 s): %llu snapshots, %llu tables, %llu keys, %llu selected records, structural poison:%s, narrowing %llu applied / %llu skipped, %.1f ms\n", + static_cast(snapshots), static_cast(counters.tables.exchange(0, std::memory_order_relaxed)), static_cast(counters.keys.exchange(0, std::memory_order_relaxed)), static_cast(counters.selected.exchange(0, std::memory_order_relaxed)), poison.empty() ? " none" : poison.c_str(), static_cast(counters.narrowed.exchange(0, std::memory_order_relaxed)), static_cast(counters.narrowSkipped.exchange(0, std::memory_order_relaxed)), static_cast(counters.nanoseconds.exchange(0, std::memory_order_relaxed)) / 1e6); } @@ -526,6 +527,49 @@ private: tableCounters().narrowed.fetch_add(1, std::memory_order_relaxed); } + void Select(const TableColumn& column, ColumnRecords& records) { + const auto base = records.base; + const auto size = records.size; + DescriptorValue selection; + try { + walker.EvaluateDescriptorSource(plan, column.selection, runtime, selection); + } catch (const std::exception&) { + records.fault = ImageTableAbi::PoisonCode(static_cast(PoisonReason::Unmapped)); + return; + } + tableCounters().selected.fetch_add(1, std::memory_order_relaxed); + const auto offset = selection.dwords[0]; + const auto entry = (column.addend + column.offset) & ~3u; + records.base = base + ((offset + column.offset) & ~3u) - entry; + records.size = entry + static_cast(column.dwordCount) * sizeof(std::uint32_t); + records.records = 1u; + records.keys = 1u; + records.states.assign(1u, RecordState::Read); + records.words.assign(1u, 0u); + if (offset >= column.foldedAddend && offset > 0xffffffffu - column.foldedCarry) { + records.states.front() = RecordState::Carried; + return; + } + DescriptorValue value; + value.dwordCount = 8u; + std::uint64_t low = std::numeric_limits::max(); + std::uint64_t high = 0; + for (std::uint32_t dword = 0; dword < column.dwordCount; dword++) { + const auto position = ImageTableAbi::ScalarBufferDword(offset, column.offset + dword * 4u, size); + if (!position.has_value()) continue; + const auto address = base + *position; + if (!PageAccessible(address, base, base + size)) { + records.states.front() = RecordState::Unmapped; + return; + } + value.dwords[dword] = Read(address); + low = std::min(low, address); + high = std::max(high, address + sizeof(std::uint32_t)); + } + records.words.front() = WordsIndex(value); + if (low < high) tables.ranges.emplace_back(low, high - low); + } + std::uint32_t WordsIndex(const DescriptorValue& value) { const auto [found, inserted] = wordsIndex.try_emplace({value.dwords, value.dwordCount}, static_cast(tables.words.size())); if (inserted) tables.words.push_back(value); @@ -556,6 +600,10 @@ private: } records.base = heap.Base48(); records.size = heap.GetSize(); + if (column.selected) { + Select(column, records); + return records; + } records.records = ImageTableAbi::ScalarBufferRecords(column.addend, column.stride, column.offset + (column.indirect ? 4u : 0u), records.size); if (records.size != 0u) tables.ranges.emplace_back(records.base, records.size); } diff --git a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp index d8634e78ca..580eac4d16 100644 --- a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp @@ -146,11 +146,15 @@ public: } std::vector referenced; for (const auto& plan : m_tableColumns) { - for (std::uint32_t argument = 0; argument < plan.handle->ArgumentCount(); argument++) { - plan.handle->ReplaceArgument(argument, plan.offset); - } const auto& column = *m_sources[plan.source].tableColumn; + IrValue* offset = column.selected ? &m_builder.Constant(column.addend) : plan.offset; + for (std::uint32_t argument = 0; argument < plan.handle->ArgumentCount(); argument++) { + plan.handle->ReplaceArgument(argument, offset); + } KeepSourceAlive(*plan.handle, m_sources[column.heapSource], referenced); + if (column.selected) { + KeepSourceAlive(*plan.handle, m_sources[column.selection], referenced); + } if (plan.keyDomain.has_value()) { KeepSourceAlive(*plan.handle, m_sources[plan.keyDomain->source], referenced); } @@ -650,6 +654,18 @@ private: column.address = address; column.foldedAddend = folded ? baseAddend : 0u; column.foldedCarry = folded ? carry : 0u; + if (!address && !sampler) { + DescriptorSource direct; + MakeSource(handle, width, false, false, direct); + DescriptorSource selection; + selection.dwordCount = 1u; + selection.dwords[0] = offset; + std::uint32_t badDword = 0; + if (ValidateSource(direct, badDword) && ValidateSource(selection, badDword)) { + column.selected = true; + column.selection = InternSource(selection); + } + } const auto domain = masked ? std::optional{} : MatchKeyDomain(key); DescriptorSource columnSource = heapSource; columnSource.tableColumn = column; @@ -1336,8 +1352,58 @@ private: return source < m_sources.size() && m_sources[source].tableColumn.has_value(); } + bool SameBufferSource(const IrValue& handle, std::uint32_t source) const { + const auto& current = m_sources[source]; + if (current.tableColumn.has_value() || current.dwordCount != 4u || handle.ArgumentCount() != 4u) { + return false; + } + for (std::uint32_t dword = 0; dword < 4u; dword++) { + if (!EquivalentValue(m_program.Resources(), current.dwords[dword], handle.Argument(dword))) { + return false; + } + } + return true; + } + + bool SelectionWritten(const TableColumn& column) const { + const auto written = [&](const auto& matches) { + return std::any_of(m_info.buffers.begin(), m_info.buffers.end(), [&](const auto& buffer) { return (buffer.written || buffer.atomic) && matches(buffer.source); }); + }; + std::vector pending{m_sources[column.selection].dwords[0]}; + std::vector seen; + while (!pending.empty()) { + const IrValue* current = pending.back()->Resolve(); + pending.pop_back(); + if (std::ranges::find(seen, current) != seen.end()) { + continue; + } + seen.push_back(current); + const bool pointer = current->Opcode() == IrOpcode::LoadAddressU32; + const bool buffer = current->Opcode() == IrOpcode::ReadConstBuffer && current->ArgumentCount() != 0u; + if ((pointer || buffer) && m_info.bdaWrites) { + return true; + } + if (pointer && written([](std::uint32_t) { return true; })) { + return true; + } + if (buffer) { + const IrValue* handle = current->Argument(0)->Resolve(); + if (written([&](std::uint32_t source) { return SameBufferSource(*handle, source); })) { + return true; + } + } + for (std::size_t index = 0; index < current->ArgumentCount(); index++) { + pending.push_back(current->Argument(index)); + } + } + return false; + } + void CheckTableStores() { for (auto& source : m_sources) { + if (source.tableColumn.has_value() && source.tableColumn->selected && SelectionWritten(*source.tableColumn)) { + source.tableColumn->selected = false; + } if (!source.tableColumn.has_value() || source.tableColumn->keyDomains.empty()) { continue; } diff --git a/core/shader/recompiler/ShaderDiskCache.cpp b/core/shader/recompiler/ShaderDiskCache.cpp index c0a118aeb8..05c6eea6e1 100644 --- a/core/shader/recompiler/ShaderDiskCache.cpp +++ b/core/shader/recompiler/ShaderDiskCache.cpp @@ -108,8 +108,8 @@ static_assert(sizeof(IrResourcePlan) == 584, "IrResourcePlan changed: update Enc static_assert(sizeof(IrValue) == 152, "IrValue changed: update the plan value encoder"); static_assert(sizeof(GuestRegister) == 8, "GuestRegister changed: update the plan value encoder"); static_assert(sizeof(MemoryInfo) == 80, "MemoryInfo changed: update the plan encoder"); -static_assert(sizeof(DescriptorSource) == 160, "DescriptorSource changed: update the plan encoder"); -static_assert(sizeof(TableColumn) == 80, "TableColumn changed: update the plan encoder"); +static_assert(sizeof(DescriptorSource) == 168, "DescriptorSource changed: update the plan encoder"); +static_assert(sizeof(TableColumn) == 88, "TableColumn changed: update the plan encoder"); static_assert(sizeof(KeyDomain) == 16, "KeyDomain changed: update the plan encoder"); static_assert(sizeof(ResourceBlock) == 80, "ResourceBlock changed: update the plan encoder"); static_assert(sizeof(SrtRead) == 16, "SrtRead changed: update the plan encoder"); @@ -149,7 +149,7 @@ static_assert(fieldCount() == 21, "IrResourcePlan changed: updat static_assert(fieldCount() == 2, "GuestRegister changed: update the plan value encoder"); static_assert(fieldCount() == 31, "MemoryInfo changed: update the plan encoder"); static_assert(fieldCount() == 3, "DescriptorSource changed: update the plan encoder"); -static_assert(fieldCount() == 15, "TableColumn changed: update the plan encoder"); +static_assert(fieldCount() == 17, "TableColumn changed: update the plan encoder"); static_assert(fieldCount() == 4, "KeyDomain changed: update the plan encoder"); static_assert(fieldCount() == 4, "ResourceBlock changed: update the plan encoder"); static_assert(fieldCount() == 2, "SrtRead changed: update the plan encoder"); @@ -1041,6 +1041,8 @@ bool encodePlan(Writer& writer, const IrResourcePlan& plan) { out.Value(column.indirectImmediate); out.Value(column.foldedAddend); out.Value(column.foldedCarry); + out.Value(column.selected); + out.Value(column.selection); }); writer.List(plan.controlFlow, [&](Writer& out, const ResourceBlock& block) { out.Value(valueIndex(block.condition)); @@ -1195,6 +1197,8 @@ bool decodePlan(Reader& reader, IrResourcePlan& plan) { in.Value(column.indirectImmediate); in.Value(column.foldedAddend); in.Value(column.foldedCarry); + in.Value(column.selected); + in.Value(column.selection); source.tableColumn = table ? std::optional(std::move(column)) : std::nullopt; }); reader.List(plan.controlFlow, 28, [&](Reader& in, ResourceBlock& block) { @@ -1226,7 +1230,7 @@ bool decodePlan(Reader& reader, IrResourcePlan& plan) { if (!valid || !reader.Done() || fill.words > plan.uniformFill.values.size()) return false; const auto knownSource = [&](std::uint32_t source) { return source < plan.descriptorSources.size(); }; if (!std::ranges::all_of(plan.materializationSources, knownSource)) return false; - if (!std::ranges::all_of(plan.descriptorSources, [&](const DescriptorSource& source) { return source.dwordCount <= source.dwords.size() && (!source.tableColumn.has_value() || (knownSource(source.tableColumn->heapSource) && std::ranges::all_of(source.tableColumn->keyDomains, [&](const KeyDomain& domain) { return knownSource(domain.source); }))); })) return false; + if (!std::ranges::all_of(plan.descriptorSources, [&](const DescriptorSource& source) { return source.dwordCount <= source.dwords.size() && (!source.tableColumn.has_value() || (knownSource(source.tableColumn->heapSource) && (!source.tableColumn->selected || knownSource(source.tableColumn->selection)) && std::ranges::all_of(source.tableColumn->keyDomains, [&](const KeyDomain& domain) { return knownSource(domain.source); }))); })) return false; if (!std::ranges::all_of(plan.info.buffers, [&](const BufferResource& buffer) { return knownSource(buffer.source); })) return false; if (!std::ranges::all_of(plan.info.images, [&](const ImageResource& image) { return knownSource(image.source); })) return false; if (!std::ranges::all_of(plan.info.samplers, [&](const SamplerResource& sampler) { return knownSource(sampler.source); })) return false; diff --git a/core/shader/recompiler/ShaderDiskCache.hpp b/core/shader/recompiler/ShaderDiskCache.hpp index 9e59550c01..b03d85a809 100644 --- a/core/shader/recompiler/ShaderDiskCache.hpp +++ b/core/shader/recompiler/ShaderDiskCache.hpp @@ -13,7 +13,7 @@ namespace ShaderRecompiler::ShaderDiskCache { -inline constexpr std::uint32_t FormatVersion = 35; +inline constexpr std::uint32_t FormatVersion = 36; enum class LoadStatus { Loaded, diff --git a/docs/dev/ARCHITECTURE.md b/docs/dev/ARCHITECTURE.md index ffbc3207e3..573bf0ac92 100644 --- a/docs/dev/ARCHITECTURE.md +++ b/docs/dev/ARCHITECTURE.md @@ -111,11 +111,13 @@ flowchart LR A T# or S# that a shader loads from a buffer at a runtime offset (`key * stride + addend`, or `(key << n) & mask`) is not a direct descriptor. [`ResourceTracker.cpp`](../../core/shader/recompiler/Optimization/src/ResourceTracker.cpp) plans it as a table column (`TableColumn`): the heap V#'s descriptor source, the record stride and addend, the descriptor's offset in the record, and the key domains that may narrow it. Accesses with the same heap and record shape share one column. The shader keeps computing the offset and indexes a table binding: images use one variable-count `SAMPLED_IMAGE` array (`RuntimeAbi::ImageTableBinding`, in the fragment set when pipeline libraries split the sets), samplers a `SAMPLER` array (`Binding::SamplerTable`). Devices without `runtimeDescriptorArray`, `descriptorBindingPartiallyBound` and `descriptorBindingVariableDescriptorCount` refuse such shaders. -At every invocation, `TableSnapshotter` in [`ResourceMaterializer.cpp`](../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) reads each column's records from guest memory into `ResourceSnapshot::tables`: heap base and size, the record count (at most `ImageTableAbi::MaxKeys`), one code per record (an index into the distinct descriptor words, or a poison code) and the ranges it read. A column narrows to the records its keys select, plus record 0, when every access has a key domain: a value the SRT walker evaluates from user data, SRT reads and constants, or a key buffer of at most 256 positions (`APS5_IMAGE_TABLE_KEY_SCAN`). A loop-carried, lane-reduced or masked key keeps the whole column, so the cost of an invocation grows with the column's records and its distinct T#s. The snapshot is part of the invocation, not of the compiled variant: a changed table changes neither the SPIR-V nor the variant's disk key. +A T# whose eight dwords and record offset the SRT walker can evaluate is planned as a selected column (`TableColumn::selected`): the offset is kept as a descriptor source (`TableColumn::selection`) and the shader indexes entry 0 of a one-record table. The column stays a whole table when the shader may write the memory its offset is read from at run time: the buffer it is read from, or any memory when the read or a write goes through a pointer or a GPU-only descriptor. Samplers are not selected: an S# the walker can evaluate is a direct sampler. + +At every invocation, `TableSnapshotter` in [`ResourceMaterializer.cpp`](../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) reads each column's records from guest memory into `ResourceSnapshot::tables`: heap base and size, the record count (at most `ImageTableAbi::MaxKeys`), one code per record (an index into the distinct descriptor words, or a poison code) and the ranges it read. A selected column evaluates its offset and reads only that record, as the scalar loads would: dwords past the heap read zeros, an unmapped record or an offset that carries past 32 bits is poison, and an offset the walker cannot read faults the table instead of throwing. Its heap base moves to the record, so the shader's fault address and the written-heap check cover that record alone. Any other column narrows to the records its keys select, plus record 0, when every access has a key domain: a key buffer of at most 256 positions (`APS5_IMAGE_TABLE_KEY_SCAN`), or a value the SRT walker evaluates from user data, SRT reads and constants (which, for an image, selects its record before it could narrow). A loop-carried, lane-reduced or masked key keeps the whole column, so the cost of an invocation grows with the column's records and its distinct T#s. The snapshot is part of the invocation, not of the compiled variant: a changed table or key changes neither the SPIR-V nor the variant's disk key. `ResolveImageTables` turns the snapshot into the map buffer (`DescriptorRole::ImageTableMap`, layout in [`ImageTableAbi.hpp`](../../core/shader/recompiler/ImageTableAbi.hpp)): a header, eight words per table (map start, key count, heap base and size, the code for keys past the snapshot, a table fault) and one record per key (flags with the decode mode, the swizzle dword and the element for images; flags and element for samplers). The shader reads `map[key]` and switches over the image's prepared runtime modes. Each distinct T# becomes one element, up to `ImageElementBudget` and the device's capacity. An entry that does not decode, is multisampled or over budget, pairs a reducing sampler with a point-only mode, is an unnormalized or reducing S#, or whose surface has no mapped byte is poisoned: it reads zeros and reports a fault through the BDA fault buffer only when a lane selects it (`APS5_IMAGE_TABLE_STRICT=1` throws at the snapshot instead). The driver's `resolveImageTables` in [`ShaderResources.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) looks up or creates a texture per element and keeps the last four resolved tables per thread for reuse within one collect epoch while their memory is unchanged. Cached objects grow with their tables, so the resource cache keeps at most `ResourceCache::TableElementBudget()` table elements (65,536, `APS5_RESOURCE_CACHE_TABLE_ELEMENTS`) and evicts its least recently used table objects past that, and the dispatch cache evicts its least recently used entries while their captured words exceed `Driver::DispatchCacheBytes()` (256 MiB, `APS5_DISPATCH_CACHE_BYTES`). -The disk cache (`FormatVersion` 33) stores the table and table operation of each image and sampler, the image shapes of the table binding, the invocation's table poison list, read ranges and fault count, and the table column of each descriptor source in the front-end plan, with its key domains. +The disk cache (`FormatVersion` 36) stores the table and table operation of each image and sampler, the image shapes of the table binding, the invocation's table poison list, read ranges and fault count, and the table column of each descriptor source in the front-end plan, with its key domains and its selection. ### Untracked storage image comparisons diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 720fc6ccbd..8c381cdf4f 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -129,7 +129,7 @@ - [Table S#s](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) that clamp to border fault only when selected: four variants cannot encode dynamic integer/float pairing. This also rejects otherwise-valid float pairs. - [Table T#s](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) paired with direct S#s fault when a paired border class differs from the decoded image class. Depth-bit views use float borders. Matching float pairs and inactive mismatches do not fault. - [Table S#](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) DISABLE_CUBE_WRAP faults on selection without `VK_EXT_non_seamless_cube_map`: no runtime dimension check, including valid non-cube direct/table T#s. Unselected entries do not fault; with the feature this bit alone is accepted. Table T#/direct S# faults only for cubes needing the missing feature. -- An [image table](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) whose record range overlaps a buffer or storage image written by its own dispatch faults (`table written by its dispatch`) when any entry is selected, even if the write misses the selected record. +- An [image table](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) whose record range overlaps a buffer or storage image written by its own dispatch faults (`table written by its dispatch`) when any entry is selected, even if the write misses the selected record. A selected column's range is its record and the bytes of the record before the descriptor. - An [image table](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) T# whose surface has no mapped byte faults when selected instead of becoming a zero texture as a direct T# does; what the console returns for it is not measured. - [User-data callees](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp) (libSceAgcDriver) - a compute shader's `s_swappc_b64` whose source pair is a user-data pair, or a copy of one made with `s_mov_b32` or `s_mov_b64` in the same block, calls the code at the address that user data holds. At dispatch the callee is read from guest memory up to its `s_setpc_b64` on the link pair, one copy of it is appended per call after the caller's last instruction, and the call is rewritten to `s_call_b64` to that copy. The words after the caller's last instruction are dropped from the recompiled code; in the shaders seen they are metadata or constants the caller reads relative to its program counter, and those reads go to the captured guest code, which linking does not change. A caller that jumps through a table among those words, which the control flow analysis reads from the recompiled code, throws. A copy runs at another program counter than the callee, so a callee that reads its program counter, makes a call, ends the program, jumps through another pair, returns before code it branches to, spans more than 64 KiB or is not readable throws. Callees are read again whenever a dispatch misses the dispatch cache; a dispatch that hits it reuses the linked program, so a title that rewrote a callee in place at the same address would keep running the old code. - [Table callees](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp) (libSceAgcDriver) - a compute shader's `s_swappc_b64` whose source pair is two consecutive dwords of an `s_buffer_load_dword*`, or a copy of them made with `s_mov_b32` or `s_mov_b64` in the same block, calls one of the targets that buffer holds when the load's buffer descriptor comes from an `s_load_dwordx4` without SOFFSET from a user-data pointer and the load's SOFFSET from an `s_lshl_b32` by 2 to 24 bits. At dispatch the descriptor and the buffer are read from guest memory, and every non-zero 64-bit value at the loaded offset plus a multiple of the shift's stride, below the descriptor's size (stride times num_records, or num_records bytes with stride 0), is read as a user-data callee and appended once per call. The call becomes a branch to a chain that compares the source pair with each target (`s_cmp_lg_u32` with a literal and `s_cbranch_scc1` per dword) and `s_call_b64`s the copy that matches; no match runs the original `s_swappc_b64` as an unlinked call. There is one chain for each value SCC has at the call, and the chain sets SCC back to that value (`s_cmp_eq_u32 0, 0` or `s_cmp_lg_u32 0, 0`) before the callee or the unlinked call runs. The searched range is the descriptor's whole size, which covers the range the load can reach whether it is checked in bytes or in strides; which one the PS5 uses for `s_buffer_load` was not measured. More than 64 distinct targets, a buffer above 64 MiB or a copy out of `s_call_b64` range throws. As with user-data callees, the buffer is read again only when a dispatch misses the dispatch cache, so a target a title adds to the buffer later is an unlinked call when a cached program runs. diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index bbae61a42b..61ceaa79aa 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -30,9 +30,9 @@ - [Image table](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) entries of 8_SRGB and 8_8_SRGB textures are decoded by `image_load` like a direct T#; a sample, gather or LOD query of one faults (`sRGB decode`) on a device that cannot sample R8_SRGB or R8G8_SRGB. - [Image tables](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) (T#s and S#s that `s_buffer_load` reads at `key * stride + addend` from a V#) are snapshotted when the dispatch or draw is recorded; a later CPU write to a record is not seen. - An [image table](../../../core/shader/recompiler/ImageTableAbi.hpp) entry that cannot be bound exactly reads zeros, and an active lane that selects it raises a BDA fault (reason 9) naming the record's address, words and reason; the dispatch's write-back reports the first one. -- An [image table](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) snapshot holds at most 65536 keys per table, 65536 distinct images and 256 S# entries per shader; a key past a longer table's snapshot, or an entry over the budget, faults when selected. +- An [image table](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) snapshot holds at most 65536 keys per table, 65536 distinct images and 256 S# entries per shader; a key past a longer table's snapshot, or an entry over the budget, faults when selected. A selected column (a T# the SRT walker evaluates) holds its one record wherever it lies. - [Image tables](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) read by `s_load` through a 64-bit pointer hold the keys the key's known bits allow, at most 65536; a key past them, or an entry at a wrapping, unaligned or unmapped address, faults when selected. -- An [image table](../../../core/shader/recompiler/Optimization/src/ResourceTracker.cpp) whose key is neither a V#-relative scalar read nor a value the SRT walker evaluates from user data, SRT reads and constants alone (for example a shifted or masked runtime-offset V# read, a per-lane value made uniform by `v_readfirstlane`, or a loop-carried phi) binds every record of the snapshot domain instead of the selected ones. So does a table read at `(key << n) & mask`: a key domain would select records by the unmasked key, so that key is not narrowed. +- An [image table](../../../core/shader/recompiler/Optimization/src/ResourceTracker.cpp) whose key is neither a V#-relative scalar read nor a value the SRT walker evaluates (for example a shifted or masked runtime-offset V# read, a per-lane value made uniform by `v_readfirstlane`, or a loop-carried phi) binds every record of the snapshot domain instead of the selected ones. So does a table read at `(key << n) & mask` with such a key: a key domain would select records by the unmasked key, so that key is not narrowed. A key the walker evaluates selects its one record instead, unless the shader may write the memory the key is read from at run time (the same buffer, or any memory when the read or a write goes through a pointer or a GPU-only descriptor); then the column binds the whole domain too. A write to the key's memory through another V# over the same bytes is not detected. - An [image table](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) T# of an MSAA or FMASK image, or of a color format under a depth compare, faults when selected: tables have no multisampled descriptors and no emulated color compare. - `image_store`, image atomics, PCK2/PCK4 and MIMG BY2/BY4 through an [image table](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) T# fault (`unsupported operation`) when an active lane executes them; the store is dropped and the load reads zeros. - An [image table](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) S# with a min or max reduction or FORCE_UNNORMALIZED faults when selected, as does an integer, converted or depth-bits table T# paired with a direct min or max S# that filters between texels. From e1472723680f6b16c856f01542350ac8c5044360 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Thu, 15 Oct 2026 02:49:16 +0000 Subject: [PATCH 0205/1000] feat(shader): name why image table narrowing is skipped The [image-table] line under APS5_PROFILE_DRAW now splits the skipped narrowings by reason: a value key that cannot be evaluated, a key descriptor that is not a buffer, a key buffer with more positions than APS5_IMAGE_TABLE_KEY_SCAN (with the most positions seen), pending GPU writes over the key buffer, and an unmapped key page. Selected records never narrow, so the line counts them apart. --- .../Optimization/src/ResourceMaterializer.cpp | 35 ++++++++++++++----- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp index 2f041a1804..95b984bda9 100644 --- a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp @@ -307,6 +307,9 @@ bool TableTraced() { constexpr std::size_t PoisonReasonCount = static_cast(PoisonReason::Count); +enum class NarrowSkip : std::size_t { Value, NotBuffer, KeyScan, Pending, Unmapped, Count }; +constexpr std::array(NarrowSkip::Count)> NarrowSkipNames{"value", "not-buffer", "key-scan", "pending-write", "unmapped"}; + struct TableCounters { std::atomic snapshots{0}; std::atomic tables{0}; @@ -314,6 +317,8 @@ struct TableCounters { std::array, PoisonReasonCount> poison{}; std::atomic narrowed{0}; std::atomic narrowSkipped{0}; + std::array, static_cast(NarrowSkip::Count)> narrowSkips{}; + std::atomic narrowPositions{0}; std::atomic selected{0}; std::atomic nanoseconds{0}; std::atomic lastReport{0}; @@ -342,9 +347,16 @@ void reportTables() { if (count == 0) continue; poison += " " + std::string(ImageTableAbi::PoisonReasonName(static_cast(reason))) + "=" + std::to_string(count); } - std::fprintf(stderr, "[image-table] (10 s): %llu snapshots, %llu tables, %llu keys, %llu selected records, structural poison:%s, narrowing %llu applied / %llu skipped, %.1f ms\n", + std::string skips; + for (std::size_t reason = 0; reason < counters.narrowSkips.size(); reason++) { + const auto count = counters.narrowSkips[reason].exchange(0, std::memory_order_relaxed); + if (count == 0) continue; + skips += " " + std::string(NarrowSkipNames[reason]) + "=" + std::to_string(count); + if (reason == static_cast(NarrowSkip::KeyScan)) skips += " (up to " + std::to_string(counters.narrowPositions.exchange(0, std::memory_order_relaxed)) + " positions, APS5_IMAGE_TABLE_KEY_SCAN " + std::to_string(TableKeyScanLimit()) + ")"; + } + std::fprintf(stderr, "[image-table] (10 s): %llu snapshots, %llu tables, %llu keys, %llu selected records, structural poison:%s, narrowing %llu applied / %llu skipped%s%s, %.1f ms\n", static_cast(snapshots), static_cast(counters.tables.exchange(0, std::memory_order_relaxed)), static_cast(counters.keys.exchange(0, std::memory_order_relaxed)), static_cast(counters.selected.exchange(0, std::memory_order_relaxed)), poison.empty() ? " none" : poison.c_str(), - static_cast(counters.narrowed.exchange(0, std::memory_order_relaxed)), static_cast(counters.narrowSkipped.exchange(0, std::memory_order_relaxed)), static_cast(counters.nanoseconds.exchange(0, std::memory_order_relaxed)) / 1e6); + static_cast(counters.narrowed.exchange(0, std::memory_order_relaxed)), static_cast(counters.narrowSkipped.exchange(0, std::memory_order_relaxed)), skips.empty() ? "" : ":", skips.c_str(), static_cast(counters.nanoseconds.exchange(0, std::memory_order_relaxed)) / 1e6); } enum class RecordState : std::uint8_t { OutsideDomain, Read, Unmapped, Carried }; @@ -466,8 +478,10 @@ private: } void Narrow(const TableColumn& column, ColumnRecords& records) { - const auto skip = [&] { - tableCounters().narrowSkipped.fetch_add(1, std::memory_order_relaxed); + const auto skip = [&](NarrowSkip reason) { + auto& counters = tableCounters(); + counters.narrowSkipped.fetch_add(1, std::memory_order_relaxed); + counters.narrowSkips[static_cast(reason)].fetch_add(1, std::memory_order_relaxed); }; std::vector values{0u}; std::vector> scanned; @@ -477,7 +491,7 @@ private: try { walker.EvaluateDescriptorSource(plan, domain.source, runtime, key); } catch (const std::exception&) { - skip(); + skip(NarrowSkip::Value); return; } values.push_back(key.dwords[0]); @@ -485,7 +499,7 @@ private: } const auto keyBuffer = EvaluateBuffer(domain.source); if (keyBuffer.Type() != 0u) { - skip(); + skip(NarrowSkip::NotBuffer); return; } const auto base = keyBuffer.Base48(); @@ -493,17 +507,20 @@ private: const std::uint64_t first = domain.offset & ~3u; const std::uint64_t positions = size >= first + 4u ? (size - first - 4u) / domain.stride + 1u : 0u; if (positions > TableKeyScanLimit() || first > 0xffffffffull) { - skip(); + auto& widest = tableCounters().narrowPositions; + auto seen = widest.load(std::memory_order_relaxed); + while (seen < positions && !widest.compare_exchange_weak(seen, positions, std::memory_order_relaxed)) {} + skip(NarrowSkip::KeyScan); return; } if (positions != 0u && runtime.pendingWrite != nullptr && runtime.pendingWrite(runtime.userContext, base + first, size - first)) { - skip(); + skip(NarrowSkip::Pending); return; } for (std::uint64_t index = 0; index < positions; index++) { const auto address = base + first + index * domain.stride; if (!PageAccessible(address, base, base + size)) { - skip(); + skip(NarrowSkip::Unmapped); return; } values.push_back(Read(address)); From 1d983101fe1fd6bd726230c26a167a31ed539d69 Mon Sep 17 00:00:00 2001 From: milescoviello Date: Wed, 14 Oct 2026 17:32:40 +0000 Subject: [PATCH 0206/1000] fix(agc): copy an indirect draw's vertex buffer only up to its last element For an indirect draw, whose counts only the GPU knows, the driver copies the whole range a vertex buffer descriptor covers, records * stride from the descriptor's address. The last record's element ends sooner when the attribute sits at an offset in its record, and the fetch can read at most (records - 1) * stride plus the element. Forspoken (PPSA03984) draws instances from a 48 MiB buffer of 96-byte records with six attributes at offsets 0 to 0x50 in each record. The copy for the attribute at 0x50 ran 0x50 bytes past the buffer into the unmapped page after it, and the draw stopped with "guest memory is not readable". VertexBufferExtent now ends at the last record's element, as VertexBufferReadSize does for the last record of a direct draw. Co-Authored-By: Claude Opus 5.5 --- .../libSceAgcDriver/Graphics/include/VertexInput.hpp | 4 +--- core/libs/prx/libSceAgcDriver/tests/VertexInput.cpp | 11 +++++++++-- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/VertexInput.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/VertexInput.hpp index 1387258786..5c71753057 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/VertexInput.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/VertexInput.hpp @@ -224,8 +224,6 @@ private: std::unordered_map entries; }; -// The whole byte range a vertex buffer descriptor covers (records * stride, or records when the -// stride is 0): what an indirect draw, whose counts only the GPU knows, copies for the fetch. inline std::uint32_t VertexBufferOutOfBoundsSelect(const ShaderRecompiler::VertexAttribute& attribute) { return (attribute.resource.fields[3] >> 28u) & 3u; } @@ -247,7 +245,7 @@ inline std::size_t VertexBufferExtent(const ShaderRecompiler::VertexAttribute& a const auto stride = (attribute.resource.fields[1] >> 16u) & 0x3fffu; const auto records = attribute.resource.fields[2]; Require(attribute.fetchIndex <= 1, "unsupported vertex fetch index"); - const auto bytes = stride == 0 ? ZeroStrideAvailableBytes(attribute) : static_cast(records) * stride; + const auto bytes = stride == 0 ? ZeroStrideAvailableBytes(attribute) : records == 0 ? 0 : static_cast(records - 1u) * stride + DecodeVertexFormat(attribute).bytes; Require(bytes != 0 && bytes <= std::numeric_limits::max(), "empty or oversized vertex buffer descriptor"); const auto address = attribute.resource.fields[0] | (static_cast(attribute.resource.fields[1] & 0xffffu) << 32u); Require(address != 0 && bytes <= std::numeric_limits::max() - address, "invalid vertex buffer address range"); diff --git a/core/libs/prx/libSceAgcDriver/tests/VertexInput.cpp b/core/libs/prx/libSceAgcDriver/tests/VertexInput.cpp index 38ad2f43ea..f0f999a04c 100644 --- a/core/libs/prx/libSceAgcDriver/tests/VertexInput.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/VertexInput.cpp @@ -135,9 +135,16 @@ void run() { input[0].resource.fields[2] = records; Require(live.TryGet(100, input) == stored, "record count rebuilt a vertex layout"); Require(VertexBufferReadSize(input[0], 5, 1) == 176u, "record count changed the direct fetch extent"); - if (records != 0) Require(VertexBufferExtent(input[0]) == 32u * records, "cached layout froze the indirect descriptor extent"); - else fails([&] { VertexBufferExtent(input[0]); }); + if (records != 0) { + Require(VertexBufferExtent(input[0]) == 32u * (records - 1u) + 16u, "cached layout froze the indirect descriptor extent"); + Require(VertexBufferExtent(input[0]) == VertexBufferReadSize(input[0], records - 1u, 1), "the indirect extent is not the fetch of the last record"); + } else { + fails([&] { VertexBufferExtent(input[0]); }); + } } + auto last = attribute(); + last.resource.fields[0] += 16u; + Require(16u + VertexBufferExtent(last) == 32u * 128u, "the last element of each record was copied past the descriptor's records"); input[0].resource.fields[1] = 0; input[0].resource.fields[3] |= 2u << 28u; input[0].resource.fields[2] = 0; From 16914b8fa567d8a0e840a08dffd8740742d89eb2 Mon Sep 17 00:00:00 2001 From: hypermist Date: Thu, 15 Oct 2026 16:00:31 +1300 Subject: [PATCH 0207/1000] docs(debt): describe the narrowed vector window and its measurement The relative vector addressing entry still said every window register lives in one function-scope array. It now describes the array of the bounded slots, the per-slot variables, the OpSwitch fallback and APS5_NO_WINDOW_NARROWING, with the RTX 3060 measurement. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Uzy15wqYTt6iWyxYXEzUWo --- docs/dev/debt/shader.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index d5e026aba1..4b30588815 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -111,7 +111,7 @@ - [EmitSetAttribute](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) (shader recompiler) - a primitive with a vertex whose clip position is (0,0,0,0) is clipped away through an extra `gl_ClipDistance` element on vertex and tessellation evaluation shaders, matching what a Radeon 610M (RDNA2, RADV) and llvmpipe rasterise; NVIDIA 615.78.08 without it draws the vertex at the viewport centre. Measured for triangles (strip and list) and for a point at (0,0,0,0), which the Radeon 610M does not draw, NVIDIA draws at the viewport centre and llvmpipe at the top-left corner; lines with such a vertex are clipped by the same element and were not measured. Mesh, geometry and the generated rect-list and barycentric stages do not write it, a device without `shaderClipDistance` cannot compile vertex shaders (the compile throws), and guest clip distances are still refused by the pipeline validator. The PS5 rasteriser was not measured. - [LINE_STIPPLE_TEX](../../../core/shader/recompiler/Translation/src/InstructionTranslator.cpp) (recompiler) reads as 0: line stipple is not emulated. RADV enables the input only as the required pixel input of a shader that reads no interpolants, because it initializes the fewest VGPRs ([Mesa](https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/src/amd/vulkan/radv_shader.c) `radv_compute_spi_ps_input`), and never reads it - [Static scalar calls](../../../core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp) (`s_call_b64` and `s_swappc_b64`) require an ordinary even-aligned SGPR link pair (s0:s1 through s104:s105) and a later `s_setpc_b64` return reading that exact pair. A pair serves one call, or several calls whose regions are disjoint, each closed by the first return reading the pair after its target. Outside every region of its calls the pair may only be overwritten whole, by `s_mov_b32`, `s_mov_b64`, an SOP2 result or a scalar load, as the caller of a user-data callee does before each call. Regions must nest or be disjoint; recursion, other shared links, escaping link values (including overlapping tuples and resource descriptors), invalid targets, interleaved regions, cross-region branches or fall-through, relative SGPR reads and dynamic `s_setpc_b64` in a program with calls throw. The analysis checks all decoded instructions, including unreachable code. Arbitrary indirect calls and general call frames are not implemented. -- [Relative vector addressing](../../../core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp) (`v_movrels_b32`, `v_movreld_b32`, `v_movrelsd_b32`, `v_movrelsd_2_b32`, `v_swaprel_b32`) keeps every vector register from the lowest base register the program addresses relatively up to the last register the program names in a zero-initialised function-scope array per invocation (`LoadVectorWindow`/`StoreVectorWindow`, one element per register), so an indexed access is one load or store instead of a compare-and-select chain over the window; the host compiler decides whether that array stays in registers or spills to scratch, which is not measured. A register in the window the program never wrote reads 0, as an unwritten SSA register does; the console's value is undefined. M0 is unsigned: an index at or past the window reads `v0` and drops its write, and `v_swaprel_b32` writes neither side when either is out of range, as measured on RDNA2 (gfx1035, `agc_driver_relative_indexing`). The window ends at the last register the program names rather than at the shader's VGPR allocation, which the recompiler does not receive, so a relative write past the named registers but inside the allocation is dropped where the hardware keeps it for a later relative read. Killzone: Liberation's PSP vertex program (PPSA11142, 3176 ISA words, 104 relative accesses over 154 registers) went from 649,515 to 392,333 SPIR-V words with this. Pixel programs whose relative span contains the packed ancillary input instead keep that span in SSA and implement indexed operations with compare/select chains. This keeps ancillary definitions visible to field lowering and masked-write proofs; storing the unmodelled whole word in a private array would require composing unknown bits. A fold after masked-select elimination lowers fields whose consumers become visible only after redundant copies are removed. A whole-word use that the masked-select pass keeps gets the composed word described under the packed pixel ancillary word. Both representations include implicit entry inputs in the inferred register limit and retain unsigned M0, inactive-write and DPP update rules; wave32/wave64 native Vulkan readbacks cover relative reads, writes, split-offset swaps, inactive writes, DPP masks and out-of-range accesses, but console comparison and the performance cost of selecting over this span are unverified. +- [Relative vector addressing](../../../core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp) (`v_movrels_b32`, `v_movreld_b32`, `v_movrelsd_b32`, `v_movrelsd_2_b32`, `v_swaprel_b32`) keeps every vector register from the lowest base register the program addresses relatively up to the last register the program names in zero-initialised function-scope storage per invocation (`LoadVectorWindow`/`StoreVectorWindow`, one element per register), so an indexed access is one load or store instead of a compare-and-select chain over the window. [`IndexedWindowSlots`](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp) bounds each relative index through the IR (bit-field extracts, masks, shifts, adds, selects, phis, `ReadFirstLane` and the window stores that reach a load); the storage is an array of the slots below the largest bound that is under half the window, and one function variable per slot after it. A constant index uses the array element or the slot's variable; a relative access whose index has no such bound, or lands past the array at run time, indexes the array below its size and otherwise branches through an `OpSwitch` over the slot variables, so the bound only decides speed. When no index has a bound under half the window, the whole window is the array, as with `APS5_NO_WINDOW_NARROWING=1` (a recompiler switch in the shader cache key). Measured on an NVIDIA GeForce RTX 3060 (driver 615.78.08, Linux) with a compute program whose 167-register window an ALU loop reads and writes in full and whose relative accesses take `m0 = s8 & 3`: a 2048-group dispatch took 70.2 ms in wave32 and 123.3 ms in wave64 with the whole window in the array (as before narrowing) and 1.9-2.1 ms and 7.8 ms narrowed; with a second relative access whose index has no bound and lands past the array at run time (index 50 or 166), 113 ms and 5.8-6.0 ms in wave32. In the PPSA08427 (Killing Floor 3) hub it made no measurable difference to presents or to the GPU time of the eight most expensive programs. Whether the host compiler keeps the array and the slot variables in registers or spills them to scratch is not measured. A register in the window the program never wrote reads 0, as an unwritten SSA register does; the console's value is undefined. M0 is unsigned: an index at or past the window reads `v0` and drops its write, and `v_swaprel_b32` writes neither side when either is out of range, as measured on RDNA2 (gfx1035, `agc_driver_relative_indexing`). The window ends at the last register the program names rather than at the shader's VGPR allocation, which the recompiler does not receive, so a relative write past the named registers but inside the allocation is dropped where the hardware keeps it for a later relative read. Killzone: Liberation's PSP vertex program (PPSA11142, 3176 ISA words, 104 relative accesses over 154 registers) went from 649,515 to 392,333 SPIR-V words with this. Pixel programs whose relative span contains the packed ancillary input instead keep that span in SSA and implement indexed operations with compare/select chains. This keeps ancillary definitions visible to field lowering and masked-write proofs; storing the unmodelled whole word in a private array would require composing unknown bits. A fold after masked-select elimination lowers fields whose consumers become visible only after redundant copies are removed. A whole-word use that the masked-select pass keeps gets the composed word described under the packed pixel ancillary word. Both representations include implicit entry inputs in the inferred register limit and retain unsigned M0, inactive-write and DPP update rules; wave32/wave64 native Vulkan readbacks cover relative reads, writes, split-offset swaps, inactive writes, DPP masks and out-of-range accesses, but console comparison and the performance cost of selecting over this span are unverified. - [s_call_b64](../../../core/shader/recompiler/Translation/src/ScalarInstructions.cpp) saves `PC + 4` over `GetShaderBase` and branches to `PC + 4 + signext(SIMM16) * 4`, following [AMD RDNA2 ISA](https://gpuopen.com/download/rdna2-shader-instruction-set-architecture.pdf) sections 3.6.3, 12.2 and 12.3. Forward callees after `s_endpgm` are decoded. Measured on RDNA2 (gfx1036) in wave32 and wave64 with SIMM16 = 3: the link pair receives `PC + 4`, the branch goes to `PC + 4 + signext(SIMM16) * 4`, and `s_setpc_b64` of the link returns to the instruction after the call. Execution on the PS5 (gfx1013) and title compatibility for these bounded scalar calls remain unverified; `s_rfe_b64` still throws in translation. - [BDA span reads](../../../core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp) (shader recompiler) - `read_bda_span` is one selection: one arm calls `load_bda_span`, a `DontInline` function that holds the in-place dword loads and calls nothing, and the other arm is the per-dword byte fallback. Two other shapes of the same reads each hang a group of NVIDIA drivers, where a dispatch never finishes and the device is lost. With the loads emitted in the arm itself, a dispatch whose lanes took both arms hung on an RTX 3060 and an RTX 4060 Laptop GPU (Windows, driver 591.86) and an RTX 5060 (Linux, driver 590.48.01) ([#3291](https://github.com/boykopovar/AnyPS5/issues/3291)). With the loads and the fallback as two selections, the first under the span's condition and the second under its negation, it hung on an RTX 4070 Laptop GPU (Windows, drivers 595.97 and 596.36), an RTX 5080 (Windows, driver 610.62) and an RTX 4090 (Linux, driver 610.57.04) ([#4975](https://github.com/boykopovar/AnyPS5/issues/4975)). The current shape is measured on the RTX 4090 with driver 610.57.04 only, where it does not hang with or without SPIRV-Tools; a shape with the loads in their own `DontInline` function was reported passing on driver 591.86 in #3291. The cause inside the driver is not known. - [SDWA source selects on `v_cvt_f32_ubyte1`, `v_cvt_f32_ubyte2` and `v_cvt_f32_ubyte3`](../../../core/shader/recompiler/RdnaDecoder/src/RdnaVectorOpDecoder.cpp) (shader recompiler) - byte N is taken from source 0 after `SRC0_SEL` and `SRC0_SEXT` select and extend it, since SDWA supplies source 0 (RDNA 2 ISA guide 6.8 and 13.3.7) and the opcode reads `S0.u[8N+7:8N]` (12.8). Not measured on hardware. From 43855cb5e8c5dd8cf3f8051d89ffc39f3916bd75 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Thu, 15 Oct 2026 02:49:29 +0000 Subject: [PATCH 0208/1000] test(shader): cover selected image table records and their fallbacks agc_shader_memory and agc_shader_bindless: an invalid selected T# is poisoned with its words without changing the artifact; a key past the heap selects a null image without reading the heap; an unmapped selected record is Unmapped poison and is not read; a key loaded from a constant buffer at an SRT offset selects its record, and one that cannot be read faults the table instead of throwing; a key loaded from a buffer the shader writes keeps the whole table; a key chosen per lane keeps the table; a record past the 65,536-key cap is selected; and a selected record whose second load carries past 32 bits is OffsetCarry poison while the record past the wrap is read. agc_driver_image_table: a dispatch keyed from a constant buffer at an SRT offset samples three records exactly as their direct bindings do, with one module and one bound image; samples zeros for a key past the table; does not fault for an invalid record under a clear EXEC; reports the invalid record's address, words and reason when it selects it; and faults when it stores over its selected record. agc_shader_disk_cache: a selected column round-trips in the hand-built plan, a fixture plans one and materializes it after a round trip, and a selection past the sources is rejected. --- .../libSceAgcDriver/tests/ShaderDiskCache.cpp | 8 ++ .../libSceAgcDriver/tests/ShaderMemory.cpp | 127 ++++++++++++++++++ .../tests/execution/ImageTable.cpp | 52 +++++++ 3 files changed, 187 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp index 6d79855198..0b8ce3301d 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp @@ -1414,6 +1414,10 @@ std::vector planFixtures() { add("bindless material table", 64, {0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080300u, 0xfa000020u, 0xf4080700u, 0xfa000030u, 0x7e200500u, 0x93109010u, 0xf4200406u, 0x20000004u, 0x8f108510u, 0xf42c0502u, 0x20000000u, 0xf09c0f08u, 0x00450000u, 0xe0700000u, 0x80070000u, 0xbf810000u}, srtPointer, {{PlanSrtAddress, tables}, {PlanTableAddress, heap}, {PlanMaterialAddress, materials}}); add("bindless whole table", 64, {0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080300u, 0xfa000020u, 0xf4080700u, 0xfa000030u, 0x7e200500u, 0x8f108510u, 0xf42c0502u, 0x20000000u, 0xf09c0f08u, 0x00450000u, 0xe0700000u, 0x80070000u, 0xbf810000u}, srtPointer, {{PlanSrtAddress, tables}, {PlanTableAddress, heap}, {PlanMaterialAddress, materials}}); add("bindless sample in a loop latch", 64, {0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080300u, 0xfa000020u, 0xf4080700u, 0xfa000030u, 0x7e200500u, 0x8f108510u, 0xf42c0502u, 0x20000000u, 0xbea80380u, 0xf09c0f08u, 0x00450000u, 0x80288128u, 0xbf0a8428u, 0xbf85fffbu, 0xe0700000u, 0x80070000u, 0xbf810000u}, srtPointer, {{PlanSrtAddress, tables}, {PlanTableAddress, heap}, {PlanMaterialAddress, materials}}); + auto keyed = tables; + keyed.resize(20u, 0u); + keyed[16] = 3u; + add("bindless selected record", 64, {0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080300u, 0xfa000020u, 0xf4080700u, 0xfa000030u, 0xf4000400u, 0xfa000040u, 0x8f108510u, 0xf42c0502u, 0x20000000u, 0xf09c0f08u, 0x00450000u, 0xe0700000u, 0x80070000u, 0xbf810000u}, srtPointer, {{PlanSrtAddress, keyed}, {PlanTableAddress, heap}, {PlanMaterialAddress, materials}}); std::vector phis(32, 0u); place(phis, 0, imageWords(PlanTextureAddress)); @@ -1559,6 +1563,8 @@ IrResourcePlan handBuiltPlan() { table.dwordCount = 8u; table.dwords[0] = &readConst(3); table.tableColumn = TableColumn{2u, 32u, 16u, 4u, 8u, 0xffu, false, true, {KeyDomain{3u, 4u, 16u, false}, KeyDomain{2u, 8u, 4u, true}}, false}; + table.tableColumn->selected = true; + table.tableColumn->selection = 2u; DescriptorSource heap; heap.dwordCount = 4u; heap.dwords[1] = &readConst(6); @@ -1807,6 +1813,7 @@ void verifyMalformedPlans() { rejects("a uniform fill of five words", [](IrResourcePlan& plan) { plan.uniformFill.fill.words = 5u; }); rejects("an image table heap past the sources", [](IrResourcePlan& plan) { plan.descriptorSources[1].tableColumn->heapSource = 4u; }); rejects("an image table key past the sources", [](IrResourcePlan& plan) { plan.descriptorSources[1].tableColumn->keyDomains[1].source = 4u; }); + rejects("an image table selection past the sources", [](IrResourcePlan& plan) { plan.descriptorSources[1].tableColumn->selection = 4u; }); auto foreign = handBuiltPlan(); IrValue stray(IrOpcode::Void, IrType::U32, 99u); foreign.srtReads[0].value = &stray; @@ -1866,6 +1873,7 @@ void verifyPlanRoundTrip() { require(covers([](const IrResourcePlan& plan) { return std::any_of(plan.descriptorSources.begin(), plan.descriptorSources.end(), [](const DescriptorSource& source) { return source.tableColumn.has_value() && source.tableColumn->keyDomains.empty(); }); }), "no fixture plans a whole image table"); require(covers([](const IrResourcePlan& plan) { return std::any_of(plan.descriptorSources.begin(), plan.descriptorSources.end(), [](const DescriptorSource& source) { return source.tableColumn.has_value() && !source.tableColumn->keyDomains.empty(); }); }), "no fixture plans an image table with a key domain"); require(covers([](const IrResourcePlan& plan) { return std::any_of(plan.descriptorSources.begin(), plan.descriptorSources.end(), [](const DescriptorSource& source) { return source.tableColumn.has_value() && source.tableColumn->address; }); }), "no fixture plans an image table through a pointer"); + require(covers([](const IrResourcePlan& plan) { return std::any_of(plan.descriptorSources.begin(), plan.descriptorSources.end(), [](const DescriptorSource& source) { return source.tableColumn.has_value() && source.tableColumn->selected; }); }), "no fixture plans a selected image table record"); require(covers([](const IrResourcePlan& plan) { return std::any_of(plan.controlFlow.begin(), plan.controlFlow.end(), [](const ResourceBlock& block) { return block.condition != nullptr; }); }), "no fixture plans a control-flow condition"); require(covers([](const IrResourcePlan& plan) { return std::any_of(plan.valueStorage.begin(), plan.valueStorage.end(), [](const auto& value) { return value.IsPhi(); }); }), "no fixture plans a phi"); require(covers([](const IrResourcePlan& plan) { return !plan.guardedSrtSlots.empty(); }), "no fixture plans a guarded SRT read"); diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 8ba52c6cc2..3980d639f4 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -1781,6 +1781,131 @@ void verifyPointerTableOffsetCarry() { #endif } +void verifySelectedRecordFallbacks() { + using namespace ShaderRecompiler; + namespace Abi = ImageTableAbi; + static TableTexture textures[2]; + alignas(4096) static TableGuest guest; + auto& heap = guest.heap; + std::fill(heap.begin(), heap.end(), std::array{}); + heap[0] = TableT(textures[0].bytes.data(), TableFormat8888UNorm, TableType2D); + heap[1] = TableT(textures[1].bytes.data(), TableFormat32UInt, TableType2D); + heap[2] = heap[1]; + heap[3] = heap[0]; + FillTableSrt(guest, heap.data(), 32u, 4u); + guest.materials = {}; + guest.srt[16] = 2u; + const auto userData = TableUserData(guest); + const auto request = TableRequest(TableSrtKeyCode, userData); + const auto valid = CompileTable(request); + + heap[2][3] &= 0x0fffffffu; + const auto invalid = CompileTable(request); + const auto& invalidMap = TableMap(*invalid.result); + const auto invalidCodes = TableCodes(invalidMap, 0); + require(invalidCodes.size() == 1u && invalidCodes[0] != Abi::NullCode && (invalidCodes[0] & Abi::PoisonFlag) == 0u, "selected record fallbacks: an invalid selected T# has no entry"); + const auto invalidFlags = invalidMap.at(invalidCodes[0] + Abi::ImageRecordFlags); + require((invalidFlags & Abi::PoisonFlag) != 0u, "selected record fallbacks: an invalid selected T# was bound"); + const auto& poison = invalid.result->imageTablePoison.at(invalidFlags & ~Abi::PoisonFlag); + require(poison.reason == static_cast(Abi::PoisonReason::NotImage) && poison.dwordCount == 8u && poison.words[3] == heap[2][3] && invalid.result->imageTableFaults != 0u, "selected record fallbacks: an invalid selected T# was not poisoned with its words"); + require(invalid.result->variantId == valid.result->variantId, "selected record fallbacks: an invalid selected T# changed the artifact"); + heap[2] = heap[1]; + + guest.srt[16] = 200u; + const auto beyond = CompileTable(request); + const auto beyondCodes = TableCodes(TableMap(*beyond.result), 0); + require(beyondCodes.size() == 1u && beyondCodes[0] == Abi::NullCode && beyond.result->imageTableFaults == 0u && !TableTouched(beyond.regions, heap.data(), sizeof(heap)), "selected record fallbacks: a key past the heap did not select a null image without reading the heap"); + guest.srt[16] = 2u; + + void* reserved = nullptr; +#ifdef _WIN32 + reserved = VirtualAlloc(nullptr, 4096, MEM_RESERVE, PAGE_NOACCESS); +#else + reserved = mmap(nullptr, 4096, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (reserved == MAP_FAILED) reserved = nullptr; +#endif + require(reserved != nullptr, "selected record fallbacks: cannot reserve an inaccessible page"); + FillTableSrt(guest, reserved, 32u, 4u); + const auto unmapped = CompileTable(request); + const auto& unmappedTable = unmapped.capture->snapshot.tables.tables.at(0); + require(unmappedTable.keys == 1u && unmappedTable.codes.at(0) == Abi::PoisonCode(static_cast(Abi::PoisonReason::Unmapped)) && !TableTouched(unmapped.regions, reserved, 4096u), "selected record fallbacks: an unmapped selected record is not Unmapped poison or was read"); + FillTableSrt(guest, heap.data(), 32u, 4u); + + std::vector loadedKey(TableSrtKeyCode); + const auto shift = std::find(loadedKey.begin(), loadedKey.end(), 0x8f108510u); + require(shift != loadedKey.end(), "selected record fallbacks: the program has no s_lshl_b32"); + loadedKey.insert(shift, {0xf4200406u, 0x20000000u}); + guest.materials[0][1] = 3u; + guest.srt[16] = 4u; + const auto loadedRequest = TableRequest(loadedKey, userData); + const auto loadedPlan = GetResourcePlan(loadedRequest); + require(loadedPlan->info.images.size() == 1u && loadedPlan->descriptorSources.at(loadedPlan->info.images[0].source).tableColumn->selected, "selected record fallbacks: a key loaded from a constant buffer at an SRT offset does not select its record"); + const auto loaded = CompileTable(loadedRequest); + require(TableSelected(loaded.capture->snapshot.tables, 0u, "selected record fallbacks: a loaded key does not hold one readable selected record") == heap[3] && TableCovered(loaded.regions, &guest.materials[0][1], sizeof(std::uint32_t)), "selected record fallbacks: the record of a loaded key was not selected or its key was not captured"); + const auto materials = TableV(guest.materials.data(), 16u, 3u); + const auto unreadable = TableV(reserved, 16u, 3u); + std::copy(unreadable.begin(), unreadable.end(), guest.srt.begin() + 8); + const auto lost = CompileTable(loadedRequest); + const auto& lostTable = lost.capture->snapshot.tables.tables.at(0); + require(lostTable.keys == 0u && lostTable.fault == Abi::PoisonCode(static_cast(Abi::PoisonReason::Unmapped)) && lost.result->imageTableFaults != 0u && lost.result->variantId == loaded.result->variantId, "selected record fallbacks: a key that cannot be read did not fault its table"); + std::copy(materials.begin(), materials.end(), guest.srt.begin() + 8); +#ifdef _WIN32 + VirtualFree(reserved, 0, MEM_RELEASE); +#else + munmap(reserved, 4096); +#endif + + auto writtenKey = loadedKey; + const auto store = std::find(writtenKey.begin(), writtenKey.end(), 0x80070000u); + require(store != writtenKey.end(), "selected record fallbacks: the program has no buffer_store_dword"); + *store = 0x80030000u; + const auto writtenRequest = TableRequest(writtenKey, userData); + const auto writtenPlan = GetResourcePlan(writtenRequest); + const auto& writtenColumn = writtenPlan->descriptorSources.at(writtenPlan->info.images.at(0).source).tableColumn; + require(writtenColumn.has_value() && !writtenColumn->selected && writtenColumn->keyDomains.empty(), "selected record fallbacks: a key loaded from a buffer the shader writes selected its record"); + const auto written = CompileTable(writtenRequest); + require(written.capture->snapshot.tables.tables.at(0).keys == 4u, "selected record fallbacks: a key loaded from a buffer the shader writes did not keep the whole table"); + + const auto lanePlan = GetResourcePlan(TableRequest(TableWholeCode, userData)); + require(!lanePlan->info.images.empty() && !lanePlan->descriptorSources.at(lanePlan->info.images[0].source).tableColumn->selected, "selected record fallbacks: a T# selected per lane left the table"); + + alignas(4096) static std::array, 70000> large{}; + large.back() = heap[1]; + FillTableSrt(guest, large.data(), 32u, static_cast(large.size())); + guest.srt[16] = static_cast(large.size() - 1u); + const auto pastCap = CompileTable(request); + require(TableSelected(pastCap.capture->snapshot.tables, 0u, "selected record fallbacks: a record past the key cap does not hold one readable selected record") == heap[1] && !pastCap.capture->snapshot.tables.tables[0].outside && pastCap.result->imageTableFaults == 0u, "selected record fallbacks: a record past the key cap was not selected"); + FillTableSrt(guest, heap.data(), 32u, 4u); +} + +void verifySelectedRecordOffsetCarry() { + using namespace ShaderRecompiler; + namespace Abi = ImageTableAbi; + static TableTexture texture; + alignas(4096) static TableGuest guest; + std::fill(guest.heap.begin(), guest.heap.end(), std::array{}); + const auto image = TableT(texture.bytes.data(), TableFormat8888UNorm, TableType2D); + auto* words = guest.heap.front().data(); + std::copy(image.begin(), image.end(), words + 0x50u / 4u); + FillTableSrt(guest, guest.heap.data(), 0u, static_cast(sizeof(guest.heap))); + const auto userData = TableUserData(guest); + const std::vector code{0xf4080100u, 0xfa000000u, 0xf4080700u, 0xfa000030u, 0xf4080800u, 0xfa000010u, 0xf4000440u, 0xfa000040u, 0x8711ff11u, + 0x000000ffu, 0x8f128511u, 0x8113ff12u, 0xffffff10u, 0x8114ff12u, 0xffffff20u, 0xf4280902u, 0x26000040u, 0xf4280a02u, 0x28000040u, 0xf09c0f08u, + 0x01090000u, 0xe0700000u, 0x80070000u, 0xbf810000u}; + const auto request = TableRequest(code, userData); + const auto plan = GetResourcePlan(request); + require(plan->info.images.size() == 1u && plan->info.images[0].table == 0u, "selected record offset carry: a T# read by two loads of its record was not planned as a table"); + const auto& column = *plan->descriptorSources.at(plan->info.images[0].source).tableColumn; + require(column.selected && !column.address && column.stride == 32u && column.addend == 0xffffff10u && column.offset == 0x40u && column.foldedAddend == 0xffffff10u && column.foldedCarry == 16u, "selected record offset carry: the column is wrong"); + guest.srt[16] = 7u; + const auto carried = CompileTable(request); + const auto& carriedTable = carried.capture->snapshot.tables.tables.at(0); + require(carriedTable.keys == 1u && carriedTable.codes.at(0) == Abi::PoisonCode(static_cast(Abi::PoisonReason::OffsetCarry)) && !TableTouched(carried.regions, guest.heap.data(), sizeof(guest.heap)), "selected record offset carry: the record whose second load wraps past 32 bits is not OffsetCarry poison or was read"); + guest.srt[16] = 8u; + const auto wrapped = CompileTable(request); + require(TableSelected(wrapped.capture->snapshot.tables, 0u, "selected record offset carry: the record past the wrap does not hold one readable selected record") == image && TableCovered(wrapped.regions, words + 0x50u / 4u, sizeof(image)), "selected record offset carry: the record past the wrap was not selected"); +} + void verifyDescriptorPhis() { using namespace ShaderRecompiler; constexpr std::uint32_t Format8888UNorm = 56; @@ -5071,6 +5196,8 @@ int main(int argc, char** argv) { verifyScalarAddressRule(); verifyPointerImageTable(); verifyPointerTableOffsetCarry(); + verifySelectedRecordFallbacks(); + verifySelectedRecordOffsetCarry(); std::cout << "Image table keys, maps, records and registration passed\n"; return 0; } diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp index 43f1799617..4c0e9d8d54 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp @@ -110,6 +110,20 @@ alignas(256) constexpr std::array TableWave32Code{ 0x01090401u, 0x7e120202u, 0x34101287u, 0x4a101100u, 0x34101084u, 0xe0781000u, 0x80070408u, 0xbf810000u, }; +alignas(256) constexpr std::array SelectedTableCode{ + 0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080700u, 0xfa000020u, 0xf4000400u, 0xfa000060u, + 0xf4200444u, 0x20000000u, 0x9312b011u, 0xf42c0802u, 0x24000000u, 0x360200bfu, 0x7e020d01u, 0x060202f0u, + 0x100202ffu, 0x3c800000u, 0x7e0402ffu, 0x3e000000u, 0xf09c8f08u, 0x01090401u, 0x7e120202u, 0x34101286u, + 0x4a101100u, 0x34101084u, 0xe0781000u, 0x80070408u, 0xbf810000u, +}; + +alignas(256) constexpr std::array SelectedTableCutCode{ + 0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080700u, 0xfa000020u, 0xf4000400u, 0xfa000060u, + 0xf4200444u, 0x20000000u, 0x9312b011u, 0xf42c0802u, 0x24000000u, 0x360200bfu, 0x7e020d01u, 0x060202f0u, + 0x100202ffu, 0x3c800000u, 0x7e0402ffu, 0x3e000000u, 0x7da400ffu, 0x00001000u, 0xf09c8f08u, 0x01090401u, + 0x7e120202u, 0x34101286u, 0x4a101100u, 0x34101084u, 0xe0781000u, 0x80070408u, 0xbf810000u, +}; + alignas(256) constexpr std::array DirectCode{ 0xf4080700u, 0xfa000020u, 0xf4080800u, 0xfa000030u, 0xf4080900u, 0xfa000040u, 0x360200bfu, 0x7e020d01u, 0x060202f0u, 0x100202ffu, 0x3c800000u, 0x7e0402ffu, 0x3e000000u, 0xf09c8f08u, 0x01090401u, 0x7e120202u, @@ -934,6 +948,43 @@ void RunGuardedTableTests(AgcDriver::VulkanDevice& device) { SetClassRecords(); } +void RunSelectedRecordTests(AgcDriver::VulkanDevice& device) { + Fill(); + SetClassRecords(); + Keys = {}; + Srt[24] = 12u; + Outcome first; + for (const auto record : {2u, 0u, 1u}) { + Keys[3] = record; + const auto selected = Run(device, SelectedTableCode, 1, Lanes, 64); + Require(!Faulted(selected) && selected.imageTableCount == 1u, "image table selected record: record " + std::to_string(record) + " faulted or bound other entries:\n" + selected.log); + const auto* words = Table.data() + record * (Stride / 4u); + const auto control = RunDirect(device, {words[0], words[1], words[2], words[3]}, {words[4], words[5], words[6], words[7]}); + Require(Group(selected, 0) == Group(control, 0), "image table selected record: record " + std::to_string(record) + " differs from the direct binding of the same words"); + if (record == 2u) first = selected; + Require(selected.artifactId == first.artifactId && selected.variantId == first.variantId, "image table selected record: another key changed the module"); + } + + Keys[3] = 1000u; + const auto beyond = Run(device, SelectedTableCode, 1, Lanes, 64); + Require(!Faulted(beyond) && Zeros(beyond), "image table selected record: a key past the table faulted or did not sample zeros:\n" + beyond.log); + + Keys[3] = 7u; + const auto cut = Run(device, SelectedTableCutCode, 1, Lanes, 64); + Require(!Faulted(cut), "image table selected record: an invalid record selected with EXEC clear faulted:\n" + cut.log); + const auto invalid = Run(device, SelectedTableCode, 1, Lanes, 64); + const auto tableAddress = static_cast(reinterpret_cast(Table.data())) + 7u * Stride + 16u; + char address[32]; + std::snprintf(address, sizeof(address), "0x%llx", static_cast(tableAddress)); + Require(Faulted(invalid) && invalid.log.find("not an image") != std::string::npos && invalid.log.find(address) != std::string::npos && invalid.log.find("00001234") != std::string::npos && Zeros(invalid), "image table selected record: an invalid record selected with EXEC set did not report its address, words and reason:\n" + invalid.log); + + Keys[3] = 0u; + const auto aliased = Run(device, SelectedTableCode, 1, Lanes, 64, true); + Require(Faulted(aliased) && aliased.log.find("is written by its own dispatch") != std::string::npos, "image table selected record: a dispatch storing over its selected record did not fault on use:\n" + aliased.log); + SetClassRecords(); + Srt[24] = 0u; +} + template void RequireModeEntry(AgcDriver::VulkanDevice& device, const std::array& tableCode, const std::array& directCode, std::uint32_t record, std::span image, const std::string& what) { Keys = {}; @@ -1325,6 +1376,7 @@ int main() { RunReadTests(*device); RunLoopKeyTests(*device); RunGuardedTableTests(*device); + RunSelectedRecordTests(*device); RunModeTests(*device); RunStoreTests(*device); RunPointerTests(*device); From 625525de79d3f5667ec521f51220ea8ea2132edc Mon Sep 17 00:00:00 2001 From: Celegans12 <53881228+Celegans12@users.noreply.github.com> Date: Thu, 15 Oct 2026 05:55:30 +0200 Subject: [PATCH 0209/1000] fix(libSceNetCtl): answer the DHCP host name, PPPoE name and proxy server as empty strings Since #5056, sceNetCtlGetInfo throws for every info code it does not model. Ghost of Yotei (PPSA26344) asks for code 12, the DHCP host name, during boot and now aborts one second in. The configuration #5056 reports (wired, DHCP, HTTP proxy off) sets no DHCP host name, no PPPoE user name and no proxy server, so codes 12, 13 and 20 now return 0 with an empty string. The Wi-Fi codes (5-10) and the proxy port (21) still throw. GuestNetCtl checks the three empty answers and keeps the throw check for the rest; the libSceNetCtl debt line says the same. --- core/libs/prx/libSceNetCtl/Export.cpp | 6 ++++++ core/libs/tests/GuestNetCtl.cpp | 7 ++++++- docs/dev/debt/libSceNetCtl.md | 2 +- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceNetCtl/Export.cpp b/core/libs/prx/libSceNetCtl/Export.cpp index 27bdd35a7c..d6cac2011c 100644 --- a/core/libs/prx/libSceNetCtl/Export.cpp +++ b/core/libs/prx/libSceNetCtl/Export.cpp @@ -43,12 +43,15 @@ static constexpr int NET_CTL_INFO_ETHER_ADDR = 2; static constexpr int NET_CTL_INFO_MTU = 3; static constexpr int NET_CTL_INFO_LINK = 4; static constexpr int NET_CTL_INFO_IP_CONFIG = 11; +static constexpr int NET_CTL_INFO_DHCP_HOSTNAME = 12; +static constexpr int NET_CTL_INFO_PPPOE_AUTH_NAME = 13; static constexpr int NET_CTL_INFO_IP_ADDRESS = 14; static constexpr int NET_CTL_INFO_NETMASK = 15; static constexpr int NET_CTL_INFO_DEFAULT_ROUTE = 16; static constexpr int NET_CTL_INFO_PRIMARY_DNS = 17; static constexpr int NET_CTL_INFO_SECONDARY_DNS = 18; static constexpr int NET_CTL_INFO_HTTP_PROXY_CONFIG = 19; +static constexpr int NET_CTL_INFO_HTTP_PROXY_SERVER = 20; static constexpr int NET_CTL_INFO_HTTP_PROXY_PORT = 21; static constexpr std::uint32_t NET_CTL_DEVICE_WIRED = 0; static constexpr std::uint32_t NET_CTL_LINK_CONNECTED = 1; @@ -349,6 +352,9 @@ int APS5_VABI sceNetCtlGetInfo(int code, NetCtlInfo* info) { case NET_CTL_INFO_PRIMARY_DNS: WriteInfoAddress(info, host.dns.size() > 0 ? host.dns[0] : in_addr{}); break; case NET_CTL_INFO_SECONDARY_DNS: WriteInfoAddress(info, host.dns.size() > 1 ? host.dns[1] : in_addr{}); break; case NET_CTL_INFO_HTTP_PROXY_CONFIG: WriteInfoWord(info, NET_CTL_HTTP_PROXY_OFF); break; + case NET_CTL_INFO_DHCP_HOSTNAME: + case NET_CTL_INFO_PPPOE_AUTH_NAME: + case NET_CTL_INFO_HTTP_PROXY_SERVER: std::memset(info->opaque, 0, sizeof(info->opaque)); break; default: throw std::runtime_error("sceNetCtlGetInfo: info code " + std::to_string(code) + " is not implemented"); } return 0; diff --git a/core/libs/tests/GuestNetCtl.cpp b/core/libs/tests/GuestNetCtl.cpp index f0196de5a7..92c51b9a39 100644 --- a/core/libs/tests/GuestNetCtl.cpp +++ b/core/libs/tests/GuestNetCtl.cpp @@ -124,7 +124,12 @@ int main() { QueryAddress(17); QueryAddress(18); - for (const int code : {5, 6, 7, 8, 9, 10, 12, 13, 20, 21}) { + for (const int code : {12, 13, 20}) { + std::memset(&info, 0x5a, sizeof(info)); + Require(sceNetCtlGetInfo(code, &info) == 0 && info.opaque[0] == 0); + } + + for (const int code : {5, 6, 7, 8, 9, 10, 21}) { bool threw = false; try { sceNetCtlGetInfo(code, &info); diff --git a/docs/dev/debt/libSceNetCtl.md b/docs/dev/debt/libSceNetCtl.md index 4da9e097aa..317150823e 100644 --- a/docs/dev/debt/libSceNetCtl.md +++ b/docs/dev/debt/libSceNetCtl.md @@ -2,4 +2,4 @@ ### Unknown function info -- [sceNetCtlGetInfo](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - answers from the host interface that holds an IPv4 address, preferring the one that carries the default route: its address, netmask, gateway, MTU and first two IPv4 DNS servers. The device is reported as wired, the link as connected, the IP configuration as DHCP and the HTTP proxy as off, whatever the host uses, and the Ethernet address is the one `sceNetGetMacAddress` reports. A gateway or DNS server the interface does not have is reported as `0.0.0.0`; what a console reports then is unknown. The Wi-Fi codes (5-10), the DHCP host name (12), the PPPoE user name (13) and the HTTP proxy server and port (20, 21) throw +- [sceNetCtlGetInfo](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - answers from the host interface that holds an IPv4 address, preferring the one that carries the default route: its address, netmask, gateway, MTU and first two IPv4 DNS servers. The device is reported as wired, the link as connected, the IP configuration as DHCP and the HTTP proxy as off, whatever the host uses, and the Ethernet address is the one `sceNetGetMacAddress` reports. A gateway or DNS server the interface does not have is reported as `0.0.0.0`; what a console reports then is unknown. The DHCP host name (12), the PPPoE user name (13) and the HTTP proxy server (20) are empty strings, as that configuration sets none of them; the Wi-Fi codes (5-10) and the HTTP proxy port (21) throw From 82b4a665fa5b6449af6a396259dfbe4ef96bd46f Mon Sep 17 00:00:00 2001 From: Romans Pokrovskis Date: Thu, 15 Oct 2026 07:43:41 +0300 Subject: [PATCH 0210/1000] test(agc): honor the GPU selector in recorder fixtures --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 5 + .../prx/libSceAgcDriver/tests/Recorder.cpp | 92 ++++++++++++++----- 2 files changed, 76 insertions(+), 21 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 6f3e73062e..ef03a2441c 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1007,6 +1007,11 @@ set_target_properties(agc_driver_recorder_tests PROPERTIES CXX_EXTENSIONS OFF) configure_windows_unwind(agc_driver_recorder_tests) if(BUILD_TESTING) set_target_properties(agc_driver_recorder_tests PROPERTIES EXCLUDE_FROM_ALL OFF) + add_test(NAME agc_recorder_device_selection COMMAND agc_driver_recorder_tests --device-selection-only) + if(WIN32) + set_tests_properties(agc_recorder_device_selection PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test(NAME agc_pipeline_cache COMMAND agc_driver_recorder_tests --pipeline-cache-only) add_test(NAME agc_pipeline_cache_file COMMAND agc_driver_recorder_tests --pipeline-cache-file-only) add_test(NAME agc_pipeline_library COMMAND agc_driver_recorder_tests --pipeline-library-only) diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 19559de8cb..900f748d56 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -49,6 +49,7 @@ #include #include #include +#include #include #include #include @@ -57,6 +58,7 @@ #include #include #include +#include #include #include #include @@ -105,6 +107,63 @@ void ReleaseWatched(void* block, std::size_t bytes) { #endif } +std::optional selectDevice(std::span devices, std::string_view request) { + const auto rankDeviceType = [](VkPhysicalDeviceType type) { + switch (type) { + case VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU: return 3; + case VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU: return 2; + case VK_PHYSICAL_DEVICE_TYPE_VIRTUAL_GPU: return 1; + default: return 0; + } + }; + std::optional selected; + int selectedRank = -1; + for (std::size_t index = 0; index < devices.size(); ++index) { + const auto& candidate = devices[index]; + if (candidate.apiVersion < VK_API_VERSION_1_1) continue; + const std::string_view name(candidate.deviceName); + if (!request.empty() && std::search(name.begin(), name.end(), request.begin(), request.end(), [](char left, char right) { + return std::tolower(static_cast(left)) == std::tolower(static_cast(right)); + }) == name.end()) continue; + const int rank = request.empty() ? rankDeviceType(candidate.deviceType) : 4; + if (rank <= selectedRank) continue; + selected = index; + selectedRank = rank; + } + return selected; +} + +void deviceSelectionTests() { + const auto properties = [](const char* name, VkPhysicalDeviceType type) { + VkPhysicalDeviceProperties result{}; + result.apiVersion = VK_API_VERSION_1_1; + result.deviceType = type; + std::memcpy(result.deviceName, name, std::strlen(name) + 1u); + return result; + }; + std::array devices{ + properties("AMD Radeon Graphics", VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU), + properties("NVIDIA GeForce RTX 5080", VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU), + properties("Virtual GPU", VK_PHYSICAL_DEVICE_TYPE_VIRTUAL_GPU), + properties("CPU", VK_PHYSICAL_DEVICE_TYPE_CPU), + }; + Require(!selectDevice({}, {}).has_value(), "an empty device list selected a device"); + Require(selectDevice(devices, {}) == 1u, "default selection did not prefer the discrete GPU"); + Require(selectDevice(devices, "aMd rAdEoN") == 0u, "a requested integrated GPU lost to an unrelated discrete GPU"); + Require(selectDevice(devices, "rtx 5080") == 1u, "a partial GPU name did not match without regard to case"); + Require(selectDevice(devices, "Virtual") == 2u, "a requested virtual GPU was not selected"); + Require(selectDevice(devices, "cpu") == 3u, "a requested CPU device was not selected"); + Require(!selectDevice(devices, "missing").has_value(), "an unmatched request selected another GPU"); + Require(!selectDevice(devices, "NVIDIA GeForce RTX 5080 with a longer name").has_value(), "a request longer than the device name matched"); + devices[0].apiVersion = VK_API_VERSION_1_0; + Require(!selectDevice(devices, "AMD").has_value(), "a requested device without Vulkan 1.1 was selected"); + Require(selectDevice(devices, {}) == 1u, "an unsupported device changed default selection"); + devices[0] = devices[1]; + Require(selectDevice(devices, {}) == 0u && selectDevice(devices, "NVIDIA") == 0u, "equally ranked matches did not retain enumeration order"); + for (auto& device : devices) device.apiVersion = VK_API_VERSION_1_0; + Require(!selectDevice(devices, {}).has_value(), "a list without Vulkan 1.1 selected a device"); +} + // A compute-capable device with host imports (VK_EXT_external_memory_host) when the host offers // them, as the driver creates its own; the recorder's batches need a real queue and real fences. class Device { @@ -130,28 +189,14 @@ public: Require(count != 0, "no Vulkan device"); std::vector devices(count); Check(enumerate(instance, &count, devices.data()), "vkEnumeratePhysicalDevices"); - const auto rankDeviceType = [](VkPhysicalDeviceType type) { - switch (type) { - case VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU: return 3; - case VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU: return 2; - case VK_PHYSICAL_DEVICE_TYPE_VIRTUAL_GPU: return 1; - default: return 0; - } - }; const auto physicalProperties = function("vkGetPhysicalDeviceProperties"); - int selectedRank = -1; - for (const auto physical : devices) { - VkPhysicalDeviceProperties candidate{}; - physicalProperties(physical, &candidate); - if (candidate.apiVersion < VK_API_VERSION_1_1) continue; - const int rank = rankDeviceType(candidate.deviceType); - if (rank <= selectedRank) continue; - context.physical = physical; - selectedRank = rank; - } - Require(context.physical != VK_NULL_HANDLE, "no Vulkan 1.1 device"); - VkPhysicalDeviceProperties selected{}; - physicalProperties(context.physical, &selected); + std::vector deviceProperties(devices.size()); + for (std::size_t index = 0; index < devices.size(); ++index) physicalProperties(devices[index], &deviceProperties[index]); + const char* requestedName = std::getenv("ANYPS5_GPU"); + const auto selectedIndex = selectDevice(deviceProperties, requestedName != nullptr ? requestedName : ""); + Require(selectedIndex.has_value(), requestedName != nullptr && *requestedName != '\0' ? std::string("no Vulkan 1.1 device matching ANYPS5_GPU=\"") + requestedName + "\"" : "no Vulkan 1.1 device"); + context.physical = devices[*selectedIndex]; + const auto& selected = deviceProperties[*selectedIndex]; std::cout << "Vulkan device: " << selected.deviceName << '\n'; const auto extensions = function("vkEnumerateDeviceExtensionProperties"); Check(extensions(context.physical, nullptr, &count, nullptr), "vkEnumerateDeviceExtensionProperties"); @@ -7890,6 +7935,11 @@ void Run(Context context) { int main(int argc, char** argv) { try { + if (argc == 2 && std::string_view(argv[1]) == "--device-selection-only") { + deviceSelectionTests(); + std::cout << "recorder device selection tests passed\n"; + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--queued-write-groups-only") { queuedWriteGroupTests(); std::cout << "queued write group tests passed\n"; From 8e5073ebe8de5eb1c90b0165b63da23cbb62774d Mon Sep 17 00:00:00 2001 From: Agnibha Mukherjee Date: Thu, 15 Oct 2026 10:32:57 +0530 Subject: [PATCH 0211/1000] fix(relinker): rebase the relocated program header table at a fixed Linux base --- .../elfpatcher/src/linux/LinuxElfPatcher.cpp | 2 +- .../relinker/tests/test_linux_fixed_base.py | 30 +++++++++++++++++-- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp b/core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp index 76f460b608..ca63d7f56a 100644 --- a/core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp +++ b/core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp @@ -336,7 +336,7 @@ std::vector LinuxElfPatcher::Patch( const std::uint16_t writtenPh = _programHeaderLayoutBuilder->WriteLayout(buf, layoutRequest); _byteWriter->WriteU16(buf, kEhdrPhNumOffset, writtenPh); if (_fixedBase) - RebaseProgramHeaders(buf, phOff, phEntSize, writtenPh, base); + RebaseProgramHeaders(buf, tableOff, phEntSize, writtenPh, base); SectionHeaderTableRequest sectionRequest{}; sectionRequest.DynStrOffset = dynStrOff; diff --git a/core/relinker/relinker/tests/test_linux_fixed_base.py b/core/relinker/relinker/tests/test_linux_fixed_base.py index 61dd57d077..e01dfc92f3 100644 --- a/core/relinker/relinker/tests/test_linux_fixed_base.py +++ b/core/relinker/relinker/tests/test_linux_fixed_base.py @@ -31,14 +31,14 @@ def check_code(base): + b"\xcc" + b"\x0f\x0b") -def fixture(relocation_type=8, addend=ENTRY, alignment=0x4000, memory_size=0x1000): +def fixture(relocation_type=8, addend=ENTRY, alignment=0x4000, memory_size=0x1000, slots=6): image = bytearray(0x8000) image[:16] = b"\x7fELF\x02\x01\x01" + bytes(9) - struct.pack_into(" Date: Wed, 14 Oct 2026 15:33:10 +0530 Subject: [PATCH 0212/1000] fix(relinker): guard against address underflow in encoded unwind pointers and reject zero FDE CIE offset --- .../analysis/UnusedNidFilter/EhFrameReader.cpp | 15 +++++++++++---- .../relinker/tests/UnwindRecordBoundsTests.cpp | 7 +++++-- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/core/relinker/relinker/src/analysis/UnusedNidFilter/EhFrameReader.cpp b/core/relinker/relinker/src/analysis/UnusedNidFilter/EhFrameReader.cpp index fd61157b6a..9a5407569b 100644 --- a/core/relinker/relinker/src/analysis/UnusedNidFilter/EhFrameReader.cpp +++ b/core/relinker/relinker/src/analysis/UnusedNidFilter/EhFrameReader.cpp @@ -126,13 +126,20 @@ private: default: throw RelinkerException("Strict filter: unsupported unwind pointer format", field); } if (value == 0) return 0; + VirtualAddress base = 0; switch (encoding & 0x70) { case 0: break; - case 0x10: value += field; break; - case 0x30: value += dataBase; break; - case 0x40: value += functionBase; break; + case 0x10: base = field; break; + case 0x30: base = dataBase; break; + case 0x40: base = functionBase; break; default: throw RelinkerException("Strict filter: unsupported unwind pointer base", field); } + if ((encoding & 0x70) != 0) { + const bool negative = (encoding & 0x08) != 0 && (value >> 63) != 0; + if (negative ? base < std::uint64_t{0} - value : value > std::numeric_limits::max() - base) + throw RelinkerException("Strict filter: unwind pointer address overflowed", field); + value = base + value; + } if ((encoding & 0x80) != 0 && !importSlots.contains(value)) { const auto pointer = pointers.find(value); if (pointer != pointers.end()) value = pointer->second; @@ -198,7 +205,7 @@ private: const auto end = checkedRangeEnd(position, length); const auto cieField = position; const auto cieOffset = read(position); - if (cieOffset > cieField) throw RelinkerException("Strict filter: invalid FDE CIE offset", address); + if (cieOffset == 0 || cieOffset > cieField) throw RelinkerException("Strict filter: invalid FDE CIE offset", address); const auto cie = readCie(cieField - cieOffset); const auto begin = encoded(position, cie.PointerEncoding); const auto size = encoded(position, cie.PointerEncoding & 0x0F); diff --git a/core/relinker/relinker/tests/UnwindRecordBoundsTests.cpp b/core/relinker/relinker/tests/UnwindRecordBoundsTests.cpp index 0210073af7..cefb4e66ac 100644 --- a/core/relinker/relinker/tests/UnwindRecordBoundsTests.cpp +++ b/core/relinker/relinker/tests/UnwindRecordBoundsTests.cpp @@ -26,7 +26,7 @@ int main() { auto second = load; second.MappedAddress = 0x1048; second.Offset = 72; second.FileSize = 56; auto first = load; first.FileSize = 72; Require(Relinker::UnusedNidFilter::ReadExceptionFunctions(Fixture(), {first, second, eh}, {}, {}).size() == 1); - for (int kind = 0; kind < 7; ++kind) { + for (int kind = 0; kind < 10; ++kind) { auto bytes = Fixture(); auto segment = load; switch (kind) { case 0: Io::WriteU32(bytes, 64, 0x1000); break; @@ -36,12 +36,15 @@ int main() { case 4: segment.Offset = std::numeric_limits::max() - 4095; segment.MappedAddress = 0; segment.FileSize = 0x1100; break; case 5: segment.FileSize = 119; break; case 6: Io::WriteU32(bytes, 64, 8); break; + case 7: Io::WriteU32(bytes, 100, 0); break; + case 8: bytes[3] = 0x1B; Io::WriteU32(bytes, 16, 0xFFFFE000); break; + case 9: bytes[3] = 0x10; Io::WriteU64(bytes, 16, std::numeric_limits::max() - 0x10); break; } bool rejected = false; try { Relinker::UnusedNidFilter::ReadExceptionFunctions(bytes, {segment, eh}, {}, {}); } catch (const std::exception&) { rejected = true; } Require(rejected); } - std::cout << "9 unwind record cases passed\n"; + std::cout << "12 unwind record cases passed\n"; } catch (const std::exception& error) { std::cerr << error.what() << '\n'; return 1; } } From e692264e16522da1ea0a3cb6f06b0bac85d214e7 Mon Sep 17 00:00:00 2001 From: fogydes Date: Wed, 14 Oct 2026 20:07:14 +0530 Subject: [PATCH 0213/1000] fix(relinker): limit relative unwind pointer overflow check to signed encodings --- .../src/analysis/UnusedNidFilter/EhFrameReader.cpp | 8 +++++--- .../relinker/tests/UnwindRecordBoundsTests.cpp | 12 ++++++++++-- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/core/relinker/relinker/src/analysis/UnusedNidFilter/EhFrameReader.cpp b/core/relinker/relinker/src/analysis/UnusedNidFilter/EhFrameReader.cpp index 9a5407569b..35e6193473 100644 --- a/core/relinker/relinker/src/analysis/UnusedNidFilter/EhFrameReader.cpp +++ b/core/relinker/relinker/src/analysis/UnusedNidFilter/EhFrameReader.cpp @@ -135,9 +135,11 @@ private: default: throw RelinkerException("Strict filter: unsupported unwind pointer base", field); } if ((encoding & 0x70) != 0) { - const bool negative = (encoding & 0x08) != 0 && (value >> 63) != 0; - if (negative ? base < std::uint64_t{0} - value : value > std::numeric_limits::max() - base) - throw RelinkerException("Strict filter: unwind pointer address overflowed", field); + if ((encoding & 0x08) != 0) { + const bool negative = (value >> 63) != 0; + if (negative ? base < std::uint64_t{0} - value : value > std::numeric_limits::max() - base) + throw RelinkerException("Strict filter: unwind pointer address overflowed", field); + } value = base + value; } if ((encoding & 0x80) != 0 && !importSlots.contains(value)) { diff --git a/core/relinker/relinker/tests/UnwindRecordBoundsTests.cpp b/core/relinker/relinker/tests/UnwindRecordBoundsTests.cpp index cefb4e66ac..76cda4ce40 100644 --- a/core/relinker/relinker/tests/UnwindRecordBoundsTests.cpp +++ b/core/relinker/relinker/tests/UnwindRecordBoundsTests.cpp @@ -26,6 +26,14 @@ int main() { auto second = load; second.MappedAddress = 0x1048; second.Offset = 72; second.FileSize = 56; auto first = load; first.FileSize = 72; Require(Relinker::UnusedNidFilter::ReadExceptionFunctions(Fixture(), {first, second, eh}, {}, {}).size() == 1); + auto belowBytes = Fixture(); + belowBytes[3] = 0x30; + Io::WriteU64(belowBytes, 16, 0x800 - 0x1000); + Io::WriteU64(belowBytes, 24, 0x1060 - 0x1000); + Io::WriteU64(belowBytes, 104, 0x800); + Domain::ProgramHeader funcSeg{}; funcSeg.Type = 1; funcSeg.MappedAddress = 0x800; funcSeg.FileSize = 0x20; + const auto below = Relinker::UnusedNidFilter::ReadExceptionFunctions(belowBytes, {funcSeg, load, eh}, {}, {}); + Require(below.size() == 1 && below[0].Begin == 0x800 && below[0].End == 0x810); for (int kind = 0; kind < 10; ++kind) { auto bytes = Fixture(); auto segment = load; switch (kind) { @@ -38,13 +46,13 @@ int main() { case 6: Io::WriteU32(bytes, 64, 8); break; case 7: Io::WriteU32(bytes, 100, 0); break; case 8: bytes[3] = 0x1B; Io::WriteU32(bytes, 16, 0xFFFFE000); break; - case 9: bytes[3] = 0x10; Io::WriteU64(bytes, 16, std::numeric_limits::max() - 0x10); break; + case 9: bytes[3] = 0x1C; Io::WriteU64(bytes, 16, 0x8000000000000000ULL); break; } bool rejected = false; try { Relinker::UnusedNidFilter::ReadExceptionFunctions(bytes, {segment, eh}, {}, {}); } catch (const std::exception&) { rejected = true; } Require(rejected); } - std::cout << "12 unwind record cases passed\n"; + std::cout << "13 unwind record cases passed\n"; } catch (const std::exception& error) { std::cerr << error.what() << '\n'; return 1; } } From 65ba674e1b324036c66cb11128d488ccced29898 Mon Sep 17 00:00:00 2001 From: fogydes Date: Thu, 15 Oct 2026 10:53:35 +0530 Subject: [PATCH 0214/1000] test(relinker): cover negative wrapped pointer rejection with matching FDE segment --- .../relinker/tests/UnwindRecordBoundsTests.cpp | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/core/relinker/relinker/tests/UnwindRecordBoundsTests.cpp b/core/relinker/relinker/tests/UnwindRecordBoundsTests.cpp index 76cda4ce40..a0b8529573 100644 --- a/core/relinker/relinker/tests/UnwindRecordBoundsTests.cpp +++ b/core/relinker/relinker/tests/UnwindRecordBoundsTests.cpp @@ -45,11 +45,22 @@ int main() { case 5: segment.FileSize = 119; break; case 6: Io::WriteU32(bytes, 64, 8); break; case 7: Io::WriteU32(bytes, 100, 0); break; - case 8: bytes[3] = 0x1B; Io::WriteU32(bytes, 16, 0xFFFFE000); break; + case 8: { + bytes[3] = 0x1B; + Io::WriteU32(bytes, 16, 0xFFFFE000); + Io::WriteU32(bytes, 20, 0x1060 - 0x1014); + Io::WriteU64(bytes, 104, 0xFFFFFFFFFFFFF010ULL); + break; + } case 9: bytes[3] = 0x1C; Io::WriteU64(bytes, 16, 0x8000000000000000ULL); break; } bool rejected = false; - try { Relinker::UnusedNidFilter::ReadExceptionFunctions(bytes, {segment, eh}, {}, {}); } + std::vector headers = {segment, eh}; + if (kind == 8) { + Domain::ProgramHeader high{}; high.Type = 1; high.MappedAddress = 0xFFFFFFFFFFFFF000ULL; high.FileSize = 0x100; + headers.push_back(high); + } + try { Relinker::UnusedNidFilter::ReadExceptionFunctions(bytes, headers, {}, {}); } catch (const std::exception&) { rejected = true; } Require(rejected); } From 3dffc8ca9ecfc55a8e6afe6b4db20ee6b8dc4366 Mon Sep 17 00:00:00 2001 From: Rujul Talekar <113801876+Roojool@users.noreply.github.com> Date: Thu, 15 Oct 2026 11:07:58 +0530 Subject: [PATCH 0215/1000] fix(libc): give collate its own 16-bit vtable --- core/libs/prx/libc/src/LocaleSupport.cpp | 64 +++++++++++++- core/libs/tests/GuestCollate.cpp | 107 ++++++++++++++++++----- docs/dev/debt/libc.md | 2 +- 3 files changed, 151 insertions(+), 22 deletions(-) diff --git a/core/libs/prx/libc/src/LocaleSupport.cpp b/core/libs/prx/libc/src/LocaleSupport.cpp index 83a08a3a21..a464f6881f 100644 --- a/core/libs/prx/libc/src/LocaleSupport.cpp +++ b/core/libs/prx/libc/src/LocaleSupport.cpp @@ -157,6 +157,68 @@ std::int64_t APS5_VABI CollateHash(const GuestLocale::CollateFacet*, const char* const GuestLocale::CollateVtable g_collateVtable{{DestroyCollate, DeleteCollate, RetainFacet, ReleaseFacet}, CollateCompare, CollateTransform, CollateHash}; +// The guest's wchar_t is 16 bits, so the wide facet needs its own entries: collate reaches the +// same slots with ranges of 16-bit characters, whose length is a count of characters and not of bytes. +// The C locale orders them by value, as Dinkumware's char_traits::compare does, mixes one +// whole character per hash round, and copies them into the result, whose size and capacity are bytes. +constexpr std::size_t GuestWideCharacterSize = 2; +constexpr std::size_t GuestWideInlineCapacity = sizeof(GuestLocale::String::buffer) - GuestWideCharacterSize; + +void ValidateWideCharacterRange(const std::uint16_t* first, const std::uint16_t* last, const char* function) { + if ((first == nullptr) != (last == nullptr) || last < first) throw std::invalid_argument(std::string(function) + ": invalid character range"); +} + +int APS5_VABI WideCollateCompare(const GuestLocale::CollateFacet*, const char* first1, const char* last1, const char* first2, const char* last2) { + const auto* characters1 = reinterpret_cast(first1); + const auto* end1 = reinterpret_cast(last1); + const auto* characters2 = reinterpret_cast(first2); + const auto* end2 = reinterpret_cast(last2); + ValidateWideCharacterRange(characters1, end1, "collate::do_compare"); + ValidateWideCharacterRange(characters2, end2, "collate::do_compare"); + const auto length1 = static_cast(end1 - characters1); + const auto length2 = static_cast(end2 - characters2); + const auto common = std::min(length1, length2); + for (std::size_t index = 0; index < common; ++index) { + if (characters1[index] != characters2[index]) return characters1[index] < characters2[index] ? -1 : 1; + } + if (length1 == length2) return 0; + return length1 < length2 ? -1 : 1; +} + +GuestLocale::String* APS5_VABI WideCollateTransform(GuestLocale::String* result, const GuestLocale::CollateFacet*, const char* first, const char* last) { + if (result == nullptr) throw std::invalid_argument("collate::do_transform: null result"); + const auto* characters = reinterpret_cast(first); + const auto* end = reinterpret_cast(last); + ValidateWideCharacterRange(characters, end, "collate::do_transform"); + const auto length = static_cast(end - characters); + if (length != 0 && std::find(characters, end, static_cast(0)) != end) throw std::runtime_error("collate::do_transform: embedded null characters are not supported"); + const auto bytes = length * GuestWideCharacterSize; + char* data = result->buffer; + std::size_t capacity = GuestWideInlineCapacity; + if (bytes > GuestWideInlineCapacity) { + capacity = bytes | GuestStringCapacityMask; + data = static_cast(AllocateStringStorage(capacity + GuestWideCharacterSize)); + } + if (length != 0) std::memcpy(data, characters, bytes); + data[bytes] = 0; + data[bytes + 1] = 0; + if (data != result->buffer) result->pointer = data; + result->size = bytes; + result->capacity = capacity; + return result; +} + +std::int64_t APS5_VABI WideCollateHash(const GuestLocale::CollateFacet*, const char* first, const char* last) { + const auto* characters = reinterpret_cast(first); + const auto* end = reinterpret_cast(last); + ValidateWideCharacterRange(characters, end, "collate::do_hash"); + std::uint64_t hash = 0xcbf29ce484222325; + for (; characters != end; ++characters) hash = (hash ^ static_cast(*characters)) * 0x100000001b3; + return static_cast(hash); +} + +const GuestLocale::CollateVtable g_wideCollateVtable{{DestroyCollate, DeleteCollate, RetainFacet, ReleaseFacet}, WideCollateCompare, WideCollateTransform, WideCollateHash}; + constexpr std::array MakeClassificationTable() { std::array table{}; for (unsigned int value = 0; value < 256; ++value) { @@ -275,7 +337,7 @@ std::size_t APS5_VABI _ZNSt7collateIwE7_GetcatEPPKNSt6locale5facetEPKS1__nid_pos if (facet != nullptr && *facet == nullptr) { if (locale == nullptr || *locale == nullptr || (*locale)->name == nullptr || std::strcmp((*locale)->name, "C") != 0) throw std::invalid_argument("collate::_Getcat: only the C locale is supported"); auto* collate = static_cast(_Znwm_nid_postfix(sizeof(GuestLocale::CollateFacet))); - *collate = {{&g_collateVtable.facet, 0, 0}, nullptr, nullptr}; + *collate = {{&g_wideCollateVtable.facet, 0, 0}, nullptr, nullptr}; *facet = &collate->base; } return 1; diff --git a/core/libs/tests/GuestCollate.cpp b/core/libs/tests/GuestCollate.cpp index 8e1061053e..07ccf6b0b3 100644 --- a/core/libs/tests/GuestCollate.cpp +++ b/core/libs/tests/GuestCollate.cpp @@ -64,9 +64,27 @@ std::uint64_t Hash(const GuestLocale::CollateFacet* facet, const std::string& te return static_cast(Vtable(&facet->base).hash(facet, text.data(), text.data() + text.size())); } -// The wide facet mirrors the narrow one: the same facet allocation, the same C-locale -// validation and the same vtable, because GuestLocale models one CollateVtable for both. -void CheckWideGetcat(const GuestLocale::Implementation* const* classic, const void* narrowVtable) { +// collate hands the vtable pointers to its 16-bit characters, so the tests below pass the +// characters of a wide string where the vtable declares a range of char and count it in characters. +const char* Characters(const char16_t* text) { + return reinterpret_cast(text); +} + +int CompareWide(const GuestLocale::CollateFacet* facet, const std::u16string& left, const std::u16string& right) { + return Vtable(&facet->base).compare(facet, Characters(left.data()), Characters(left.data() + left.size()), Characters(right.data()), Characters(right.data() + right.size())); +} + +std::uint64_t HashWide(const GuestLocale::CollateFacet* facet, const std::u16string& text) { + return static_cast(Vtable(&facet->base).hash(facet, Characters(text.data()), Characters(text.data() + text.size()))); +} + +GuestLocale::String* TransformWide(const GuestLocale::CollateFacet* facet, GuestLocale::String* result, const std::u16string& text) { + return Vtable(&facet->base).transform(result, facet, Characters(text.data()), Characters(text.data() + text.size())); +} + +// The wide facet mirrors the narrow one in the facet it allocates and the C-locale validation, but it +// carries its own vtable, whose entries work on the guest's 16-bit characters. +void CheckWideGetcat(const GuestLocale::Implementation* const* classic, const GuestLocale::CollateFacet* narrow) { require(_ZNSt7collateIwE7_GetcatEPPKNSt6locale5facetEPKS1__nid_postfix(nullptr, classic) == 1); const std::size_t before = allocations; @@ -95,17 +113,67 @@ void CheckWideGetcat(const GuestLocale::Implementation* const* classic, const vo require(_ZNSt7collateIwE7_GetcatEPPKNSt6locale5facetEPKS1__nid_postfix(&facet, classic) == 1); require(facet != nullptr && allocations == before + 1 && lastSize == sizeof(GuestLocale::CollateFacet) && lastAllocation == facet); const auto* wide = reinterpret_cast(facet); - require(facet->references == 0 && facet->vtable == narrowVtable); + require(facet->references == 0 && facet->vtable != narrow->base.vtable); require(wide->collation == nullptr && wide->wideCollation == nullptr); - require(Compare(wide, "abc", "abd") == -1); - require(Compare(wide, "abd", "abc") == 1); - require(Compare(wide, "abc", "abc") == 0); - require(Compare(wide, "ab", "abc") == -1); - require(Compare(wide, "", "a") == -1); - require(Hash(wide, "foobar") == 0x85944171f73967e8ull); - const char narrow[] = "abc"; - reject([&] { Vtable(facet).compare(wide, narrow + 2, narrow, narrow, narrow + 1); }); + const std::u16string text = u"abc"; + require(CompareWide(wide, text, u"abd") == -1); + require(CompareWide(wide, u"abd", text) == 1); + require(CompareWide(wide, text, text) == 0); + require(CompareWide(wide, u"ab", text) == -1); + require(CompareWide(wide, u"", text) == -1); + require(CompareWide(wide, u"", u"") == 0); + // Characters above 0xff order by their whole value: 0x0100 and 0x0001 compare the other way round + // when the same characters are read as the little-endian bytes 0x00 0x01 and 0x01 0x00. + require(CompareWide(wide, u"\u0100", u"\u0001") == 1); + require(CompareWide(wide, u"\u0001", u"\u0100") == -1); + require(CompareWide(wide, u"\u00ff", u"\u0100") == -1); + require(CompareWide(wide, u"\u00e9", u"z") == 1); + require(CompareWide(wide, u"a\u0101b", u"a\u0100b") == 1); + require(CompareWide(wide, u"a\u0101b", u"a\u0102b") == -1); + const std::u16string embeddedWide(u"a\0b", 3); + require(CompareWide(wide, embeddedWide, u"a") == 1); + require(CompareWide(wide, text, u"abd") == Compare(narrow, "abc", "abd")); + + require(HashWide(wide, u"") == 0xcbf29ce484222325ull); + require(HashWide(wide, text) == 0xe71fa2190541574bull); + require(HashWide(wide, u"a") == 0xaf63dc4c8601ec8cull); + // One round per character, so a wide string of characters below 0x100 hashes like its narrow + // spelling and a character above 0xff hashes like one value instead of the two bytes it holds. + require(HashWide(wide, u"foobar") == 0x85944171f73967e8ull); + require(HashWide(wide, u"foobar") == Hash(narrow, "foobar")); + require(HashWide(wide, u"\u0100") == 0x0af62bd4c860004dfull); + + GuestLocale::String result; + std::memset(&result, 0xcd, sizeof(result)); + require(TransformWide(wide, &result, text) == &result); + require(result.reserved == 0xcdcdcdcdcdcdcdcdull && result.size == 6 && result.capacity == 14 && allocations == before + 1); + const auto* inlineUnits = reinterpret_cast(result.buffer); + require(inlineUnits[0] == u'a' && inlineUnits[1] == u'b' && inlineUnits[2] == u'c' && inlineUnits[3] == 0); + + std::memset(&result, 0xcd, sizeof(result)); + require(Vtable(facet).transform(&result, wide, nullptr, nullptr) == &result); + require(result.size == 0 && result.capacity == 14 && reinterpret_cast(result.buffer)[0] == 0); + + const std::u16string seven(7, u'q'); + require(TransformWide(wide, &result, seven) == &result); + require(result.size == 14 && result.capacity == 14 && allocations == before + 1); + const auto* inlineSeven = reinterpret_cast(result.buffer); + require(inlineSeven[6] == u'q' && inlineSeven[7] == 0); + + const std::u16string eight(8, u'y'); + require(TransformWide(wide, &result, eight) == &result); + require(allocations == before + 2 && lastSize == 33 && result.size == 16 && result.capacity == 31 && result.pointer == lastAllocation); + require(reinterpret_cast(result.pointer)[0] == u'y' && reinterpret_cast(result.pointer)[8] == 0); + std::free(result.pointer); + + const std::u16string embeddedText(u"a\0b", 3); + reject([&] { TransformWide(wide, &result, embeddedText); }); + reject([&] { Vtable(facet).transform(nullptr, wide, Characters(seven.data()), Characters(seven.data() + seven.size())); }); + require(allocations == before + 2); + reject([&] { Vtable(facet).compare(wide, Characters(text.data() + 2), Characters(text.data()), Characters(text.data()), Characters(text.data() + 1)); }); + reject([&] { Vtable(facet).compare(wide, nullptr, Characters(text.data() + 1), Characters(text.data()), Characters(text.data() + 1)); }); + reject([&] { Vtable(facet).hash(wide, Characters(text.data() + 1), Characters(text.data())); }); Vtable(facet).facet.retain(facet); require(facet->references == 1); @@ -115,7 +183,7 @@ void CheckWideGetcat(const GuestLocale::Implementation* const* classic, const vo require(frees == freed); Vtable(facet).facet.deleteObject(facet); require(frees == freed + 1 && lastFree == wide); - require(allocations == before + 1); + require(allocations == before + 2); } } @@ -142,7 +210,6 @@ int main() { require(facet != nullptr && allocations == 1 && lastSize == sizeof(GuestLocale::CollateFacet) && lastAllocation == facet); const auto* collate = reinterpret_cast(facet); require(facet->references == 0 && collate->collation == nullptr && collate->wideCollation == nullptr); - const void* narrowVtable = facet->vtable; require(Compare(collate, "abc", "abd") == -1); require(Compare(collate, "abd", "abc") == 1); @@ -211,12 +278,12 @@ int main() { require(facet->references == 2); require(Vtable(facet).facet.release(facet) == nullptr && facet->references == 1); require(Vtable(facet).facet.release(facet) == facet && facet->references == 0); - Vtable(facet).facet.destroy(facet); - require(frees == 0); - Vtable(facet).facet.deleteObject(facet); - require(frees == 1 && lastFree == collate); + CheckWideGetcat(classic, collate); - CheckWideGetcat(classic, narrowVtable); - require(frees == 2 && allocations == 5); + Vtable(facet).facet.destroy(facet); + require(frees == 1); + Vtable(facet).facet.deleteObject(facet); + require(frees == 2 && lastFree == collate); + require(allocations == 6); return 0; } diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index e814a52bde..b08c9cd3ef 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -58,7 +58,7 @@ - [gethostname](../../../core/libs/prx/libc/src/Hostname.cpp) (libc) reports the fixed guest name `AnyPS5` without querying the host machine name. Buffer copying, null queries and `ENAMETOOLONG` follow [FreeBSD gethostname](https://github.com/freebsd/freebsd-src/blob/main/lib/libc/gen/gethostname.c); the console's configurable hostname and its integration with `sysctl` and name resolution are unverified and not implemented. - [wcsrtombs_s](../../../core/libs/prx/libc/src/Multibyte.cpp) (libc) follows the console's `libc.prx`, but a runtime-constraint violation only returns its error: the default handler there writes the message and a newline to `stderr` before returning, which is not done here. Conversion uses the C locale of `wcrtomb` (`MB_CUR_MAX` 1), so the path where a multibyte character does not fit the remaining space is not reached - [The narrow printf family](../../../core/libs/prx/libc/src/Formatting.cpp) (libc) converts 16-bit guest wide arguments to UTF-8 on both platforms, independently of the host locale. It does not use the guest's single-byte `wcrtomb`; the console's treatment of invalid UTF-16 and locale-specific encoding remains unverified. Linux retains host formatting for formats without wide arguments or `%m`, so a narrow `%p` and a NaN there still follow the host (glibc prints a null pointer as `(nil)`, where FreeBSD prints `0x0`, and a NaN with its sign, `-nan`, and with the `+` and space flags, where FreeBSD prints `nan`); argument positions above 99 (FreeBSD's `NL_ARGMAX`) throw when a format requires the guest formatter -- [collate and collate _Getcat](../../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc) build the facet for the C locale only and throw for any other locale name, the wide entry point following the narrow one it sits next to. The wide facet carries the narrow vtable because [GuestLocale](../../../core/libs/prx/libc/include/GuestLocale.hpp) models one facet layout and one vtable for both element types, so a `collate` comparison, transform or hash runs the narrow `do_compare`, `do_transform` and `do_hash` over the guest's 16-bit characters read as bytes; the console's `libc.prx` exports both `_Getcat` (NIDs BSVJqITGCyI and 2wz4rthdiy8) with identical 0x19d-byte bodies, believed to build the same 0x20-byte facet; the console bodies themselves are unreadable (obfuscated dynsym), the wide vtable is not reproduced here and no console run checks the two. The console's `libc.prx` stores pointers to its collation state tables at offsets 0x10 and 0x18 of the facet; ours leaves them null, as only the facet's own functions read them. `do_transform` throws for input with embedded null characters, which the console drops depending on the output buffer size. Its result capacity (`length | 15`) and the over-aligned storage of results of 4080 characters or more follow the console's `libc.prx` code and are not checked on hardware. No module of the PPSA59322, PPSA76677 or PPSA97358 dumps in this tree imports either `_Getcat`, so neither has been exercised by a title. +- [collate and collate _Getcat](../../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc) build the facet for the C locale only and throw for any other locale name, the wide entry point following the narrow one it sits next to. The wide facet carries its own vtable, whose `compare`, `transform` and `hash` take the guest's 16-bit characters, so the length of a range is a count of characters: `compare` orders by 16-bit value (the C locale, as Dinkumware's `char_traits::compare` does), `transform` copies the characters into the result as the guest's bytes and sizes the result in bytes (two per character, a 16-bit terminator, the 7 characters that still fit next to it and `length * 2 | 15` plus two bytes of storage beyond that), and `hash` mixes one whole character per round, so wide text below 0x100 hashes like its narrow spelling and a character above 0xff hashes as one value instead of the two bytes it holds. The console's `libc.prx` exports both `_Getcat` (NIDs BSVJqITGCyI and 2wz4rthdiy8) with identical 0x19d-byte bodies, believed to build the same 0x20-byte facet; the console bodies themselves are unreadable (obfuscated dynsym, and the text of the relinked `libc.prx` is zeroed), so the wide entries follow the narrow algorithm and the C locale instead of the console's wide code, and no console run checks the two. The console's `libc.prx` stores pointers to its collation state tables at offsets 0x10 and 0x18 of the facet; ours leaves them null, as only the facet's own functions read them. `do_transform` throws for input with embedded null characters, which the console drops depending on the output buffer size. The narrow result capacity (`length | 15`) and the over-aligned storage of results of 4080 characters or more follow the console's `libc.prx` code and are not checked on hardware. No module of the PPSA59322, PPSA76677 or PPSA97358 dumps in this tree imports either `_Getcat`, so neither has been exercised by a title. - [The scanf family](../../../core/libs/prx/libc/src/Formatting.cpp) (libc), the `_s` forms included, on Linux throws for the 16-bit wide conversions (`%ls`, `%lc`, `%l[`, `%S`, `%C`): glibc would store a 32-bit `wchar_t` per character into the guest's 16-bit buffer. Windows hands `%ls`, `%lc` and `%l[` to the host, whose `wchar_t` is 16 bits - [The scanf family](../../../core/libs/prx/libc/include/WindowsScanning.hpp) (libc) on Windows returns 0, as C17 and FreeBSD do, when input fails after only suppressed conversions have completed (`sscanf("12", "%*d%d", &value)`). Linux hands the format to glibc, which returns `EOF` there. On Windows the host stops a `%x`, `%X`, `%i` or `%p` after the `x` of a `0x` that no hex digit follows (or that the width cuts off), and a float conversion (`%e`, `%f`, `%g`, `%a`) after the `e` of an exponent with no digits (`1e`), where C17 and FreeBSD stop before the `x` or the `e` - [Guest path resolution](../../../core/libs/prx/libc/src/General.cpp) falls back to ASCII case-insensitive component lookup, following [shadPS4's host filesystem](https://github.com/shadps4-emu/shadPS4/blob/945dbc3cc3eee80ac3e053b438502ed936fa6bb2/src/core/file_sys/backends/host_fs.cpp). PPSA03747 requests `Skyrim.INI`, `Data/Video` and lowercase archive/plugin names that differ from the dump's filenames. Exact host names take precedence; multiple fallback matches throw instead of selecting an arbitrary file. Existing parent names are resolved when creating a new file, and guest mount aliases use the same ASCII comparison. Results are not cached, so renames and newly created files remain visible. Unicode case folding and PS5 behaviour for case-colliding host entries are unverified; host roots are used as supplied. Explicit native Windows drive and UNC paths bypass guest parent-component validation.. A relinked module's `.guest.prx` is matched the same way when the original module is not beside it: Borderlands 4 (PPSA01494) loads `/app0/prx/libRenoirCore.PS5.prx`, which it ships as `librenoircore.ps5.prx` From 62022e9362a27432e007cea132ecd9ca0909bb2b Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Wed, 14 Oct 2026 22:38:16 -0700 Subject: [PATCH 0216/1000] refactor(agc): keep the private heap lookup and expose a public wrapper LargestDeviceLocalHeap now calls ShaderResources.cpp's private largestDeviceLocalHeap instead of replacing it, so the file's callers stay as they are on main and other changes that call the private helper still build with this one. --- .../Graphics/src/ShaderResources.cpp | 28 +++++++++++-------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index 7a97bde178..36e722f06c 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -481,6 +481,14 @@ std::shared_ptr sampledStorageSource(const Context& context, con std::uint64_t storageCacheBytes(); +std::optional largestDeviceLocalHeap(const VkPhysicalDeviceMemoryProperties& memory) { + std::optional heap; + for (std::uint32_t i = 0; i < std::min(memory.memoryHeapCount, VK_MAX_MEMORY_HEAPS); ++i) { + if ((memory.memoryHeaps[i].flags & VK_MEMORY_HEAP_DEVICE_LOCAL_BIT) != 0 && (!heap || memory.memoryHeaps[i].size > memory.memoryHeaps[*heap].size)) heap = i; + } + return heap; +} + std::uint64_t forcedCacheBudget() { static const std::uint64_t forced = [] { const char* text = std::getenv("APS5_TEXTURE_CACHE_MIB"); @@ -525,7 +533,7 @@ std::uint64_t sampledBudget(const Context& context, TextureCache& cache) { const auto storage = storageCacheBytes(); const auto budget = SampledTextureBudget(context.memory, &reported, sampled + storage); const bool fresh = cache.budgetDevice != context.physical; - if (const auto heap = LargestDeviceLocalHeap(context.memory); heap && (fresh || SampledBudgetReportDue(cache.reportedBudget, budget, now - cache.reportedAt))) { + if (const auto heap = largestDeviceLocalHeap(context.memory); heap && (fresh || SampledBudgetReportDue(cache.reportedBudget, budget, now - cache.reportedAt))) { char was[48] = ""; if (!fresh) std::snprintf(was, sizeof(was), " (was %llu MiB)", static_cast(cache.reportedBudget >> 20u)); std::fprintf(stderr, "[gpu] sampled texture cache budget %llu MiB%s: device-local heap %u has a VK_EXT_memory_budget budget of %llu MiB and uses %llu MiB, %llu MiB of it sampled textures (%llu MiB in the cache) and %llu MiB cached storage images; storage image cache budget %llu MiB\n", static_cast(budget >> 20u), was, *heap, static_cast(reported.heapBudget[*heap] >> 20u), static_cast(reported.heapUsage[*heap] >> 20u), static_cast(sampled >> 20u), static_cast(cache.bytes >> 20u), static_cast(storage >> 20u), static_cast(storageBudget(context) >> 20u)); @@ -977,27 +985,19 @@ std::shared_ptr cachedStorageTexture(const Context& context, std } -std::optional LargestDeviceLocalHeap(const VkPhysicalDeviceMemoryProperties& memory) { - std::optional heap; - for (std::uint32_t i = 0; i < std::min(memory.memoryHeapCount, VK_MAX_MEMORY_HEAPS); ++i) { - if ((memory.memoryHeaps[i].flags & VK_MEMORY_HEAP_DEVICE_LOCAL_BIT) != 0 && (!heap || memory.memoryHeaps[i].size > memory.memoryHeaps[*heap].size)) heap = i; - } - return heap; -} - std::uint64_t TextureCacheBudget(const VkPhysicalDeviceMemoryProperties& memory) { - const auto heap = LargestDeviceLocalHeap(memory); + const auto heap = largestDeviceLocalHeap(memory); return std::max(2048ull << 20u, heap ? memory.memoryHeaps[*heap].size / 4u : 0u); } std::uint64_t StorageTextureBudget(const VkPhysicalDeviceMemoryProperties& memory, const VkPhysicalDeviceMemoryBudgetPropertiesEXT* reported) { - const auto heap = LargestDeviceLocalHeap(memory); + const auto heap = largestDeviceLocalHeap(memory); if (reported == nullptr || !heap || reported->heapBudget[*heap] == 0) return TextureCacheBudget(memory); return std::max(512ull << 20u, std::min(TextureCacheBudget(memory), reported->heapBudget[*heap] / 4u)); } std::uint64_t SampledTextureBudget(const VkPhysicalDeviceMemoryProperties& memory, const VkPhysicalDeviceMemoryBudgetPropertiesEXT* reported, std::uint64_t textureBytes) { - const auto heap = LargestDeviceLocalHeap(memory); + const auto heap = largestDeviceLocalHeap(memory); if (reported == nullptr || !heap || reported->heapBudget[*heap] == 0) return TextureCacheBudget(memory); const auto budget = reported->heapBudget[*heap]; const auto usage = reported->heapUsage[*heap]; @@ -1010,6 +1010,10 @@ bool SampledBudgetReportDue(std::uint64_t reported, std::uint64_t budget, std::c return change > reported / 10u && sinceReport >= std::chrono::seconds(10); } +std::optional LargestDeviceLocalHeap(const VkPhysicalDeviceMemoryProperties& memory) { + return largestDeviceLocalHeap(memory); +} + TextureCacheUse TextureCacheUsage() { TextureCacheUse use; { From 1800d3892bec857b254ca8c55e912e325e1c6711 Mon Sep 17 00:00:00 2001 From: ArjenS-LA Date: Wed, 14 Oct 2026 22:51:15 -0700 Subject: [PATCH 0217/1000] fix(relinker): allow explicit module directories without standard folders --- core/relinker/cli/src/NativeModules.cpp | 2 +- .../relinker/tests/test_guest_module_dir.py | 26 +++++++++++++++++-- docs/user/USAGE.md | 4 ++- 3 files changed, 28 insertions(+), 4 deletions(-) diff --git a/core/relinker/cli/src/NativeModules.cpp b/core/relinker/cli/src/NativeModules.cpp index 4943091c64..d18c127653 100644 --- a/core/relinker/cli/src/NativeModules.cpp +++ b/core/relinker/cli/src/NativeModules.cpp @@ -20,7 +20,7 @@ NativeModules ReadModules(const std::filesystem::path& inputPath, const std::fil const bool hasPlural = std::filesystem::exists(plural); const bool hasPrx = std::filesystem::exists(prx); if (hasSingular && hasPlural) throw Domain::RelinkerException("Both sce_module and sce_modules exist in the guest module parent directory"); - if (!hasSingular && !hasPlural && !hasPrx) throw Domain::RelinkerException(missingModuleMessage); + if (!hasSingular && !hasPlural && !hasPrx && moduleDirectories.empty()) throw Domain::RelinkerException(missingModuleMessage); std::vector directories; if (hasSingular || hasPlural) directories.push_back(hasSingular ? singular : plural); if (hasPrx) directories.push_back(prx); diff --git a/core/relinker/relinker/tests/test_guest_module_dir.py b/core/relinker/relinker/tests/test_guest_module_dir.py index 9fd27ed131..42b83e18bd 100644 --- a/core/relinker/relinker/tests/test_guest_module_dir.py +++ b/core/relinker/relinker/tests/test_guest_module_dir.py @@ -39,8 +39,10 @@ def main(): with tempfile.TemporaryDirectory(prefix="anyps5-module-dir-") as directory: work = Path(directory) - def convert(case, windows, options, executable=None, cwd=None): - (case / "sce_module").mkdir(parents=True, exist_ok=True) + def convert(case, windows, options, executable=None, cwd=None, standard=True): + case.mkdir(parents=True, exist_ok=True) + if standard: + (case / "sce_module").mkdir(exist_ok=True) source = case / "input.elf" source.write_bytes(main_fixture() if executable is None else executable) output = case / ("output.exe" if windows else "output.elf") @@ -49,6 +51,26 @@ def main(): return result, output for windows in (False, True): + for relative in (".", "Media/Plugins"): + case = work / f"{windows}-explicit-only-{relative == '.'}" + modules = case / relative + modules.mkdir(parents=True) + module = modules / "plugin.prx" + original = bytes(module_with_symbol(True)) + module.write_bytes(original) + result, output = convert(case, windows, [], standard=False) + assert result.returncode == 2 and "was not found" in result.stderr, result.stderr + assert not output.exists() + result, output = convert(case, windows, ["--module-dir", relative], standard=False) + assert result.returncode == 0, (result.stdout, result.stderr) + artifact = case / "app0" / relative / "plugin.prx.guest.prx" + assert artifact.read_bytes().startswith(b"MZ" if windows else b"\x7fELF") + assert module.read_bytes() == original + assert not (case / "sce_module").exists() + for invalid in ("missing", "../outside"): + failed, _ = convert(case, windows, ["--module-dir", invalid], standard=False) + assert failed.returncode == 2, (invalid, failed.stdout, failed.stderr) + case = work / f"{windows}-plugins" plugins = case / "Media" / "Plugins" plugins.mkdir(parents=True) diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index b448949fe2..2d3af71b9d 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -8,7 +8,7 @@ The online converter can save Linux output as an uncompressed folder in supporte ## Input and conversion -Use a clean ELF executable. Place its bundled ELF modules in `sce_module/`, `sce_modules/`, or `prx/` beside the input executable, or under the parent directory selected with `--sce-module-path `. The option accepts absolute paths and paths relative to the current working directory; do not include the `sce_module` directory itself. Only files directly inside the module directories are processed. SELF/fSELF containers are skipped with a warning; unwrap them before relinking if they need to be included. `tools/unwrap_self.py ` does that for a decrypted container: it places the segments at the offsets of the SELF segment records and restores `p_offset % p_align == p_vaddr % p_align`, and it refuses a container whose load segments all look encrypted. `prx/` can coexist with either `sce_module/` or `sce_modules/`. Both `sce_module/` and `sce_modules/` present, or all three absent, is an error. +Use a clean ELF executable. Place its bundled ELF modules in `sce_module/`, `sce_modules/`, or `prx/` beside the input executable, or under the parent directory selected with `--sce-module-path `. The option accepts absolute paths and paths relative to the current working directory; do not include the `sce_module` directory itself. Only files directly inside the module directories are processed. SELF/fSELF containers are skipped with a warning; unwrap them before relinking if they need to be included. `tools/unwrap_self.py ` does that for a decrypted container: it places the segments at the offsets of the SELF segment records and restores `p_offset % p_align == p_vaddr % p_align`, and it refuses a container whose load segments all look encrypted. `prx/` can coexist with either `sce_module/` or `sce_modules/`. Both `sce_module/` and `sce_modules/` present is an error. All three absent is an error unless an explicit `--module-dir` is supplied. ```text source/ @@ -21,6 +21,8 @@ source/ relinker [options] ``` +An explicit `--module-dir` also works when the module parent has no `sce_module`, `sce_modules` or `prx` directory. For a homebrew bundle with shared libraries beside its executable, use `--module-dir .`; no empty standard directory is required. Without an explicit module directory, the standard-directory requirement remains. + Linux output: ```sh From f1735f59f7db1d9b017d82b060d6a064dba4ad85 Mon Sep 17 00:00:00 2001 From: David Date: Thu, 15 Oct 2026 09:52:37 +0400 Subject: [PATCH 0218/1000] refactor(relinker): grow a moved branch's displacement with resize With the GS traps in WindowsTlsBuilder::Build, GCC 15.2 inlines this vector insert differently and reports eight -Wstringop-overflow false positives at it. resize appends the same four zero bytes and builds without them. --- core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp b/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp index faac015edb..26d5ba75b8 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp @@ -187,7 +187,7 @@ PeDirectory WindowsTlsBuilder::Build(const std::vector& source, co if (!next.IsTwoByteOpcode && next.Opcode != 0xe9) { movedBytes = next.Opcode == 0xeb ? std::vector{0xe9} : std::vector{0x0f, static_cast(0x80 | (next.Opcode & 0xf))}; branchDisplacementOffset = movedBytes.size(); - movedBytes.insert(movedBytes.end(), 4, 0); + movedBytes.resize(movedBytes.size() + 4); } else { branchDisplacementOffset = next.Length - 4; } From 56d8b4f68d7709a439650cd81b547406f30eea59 Mon Sep 17 00:00:00 2001 From: alfalfa-star Date: Fri, 9 Oct 2026 22:31:45 +0200 Subject: [PATCH 0219/1000] feat(npmanager): implement sceNpGetUserIdByAccountId Account id 0 or a null output returns SCE_NP_ERROR_INVALID_ARGUMENT; otherwise the always signed-out NP returns SCE_NP_ERROR_SIGNED_OUT, as sceNpGetAccountIdA does. PPSA14632 imports it. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceNpManager/Export.cpp | 6 +++--- core/libs/tests/GuestNpManager.cpp | 6 ++++++ docs/dev/TechnicalDebt.md | 2 +- docs/dev/debt/libSceNpManager.md | 1 + 4 files changed, 11 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceNpManager/Export.cpp b/core/libs/prx/libSceNpManager/Export.cpp index a384c22023..f43d909d91 100644 --- a/core/libs/prx/libSceNpManager/Export.cpp +++ b/core/libs/prx/libSceNpManager/Export.cpp @@ -238,9 +238,9 @@ int APS5_VABI sceNpUnregisterPremiumEventCallback(void) { return 0; } -int APS5_VABI sceNpGetUserIdByAccountId() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpGetUserIdByAccountId(uint64_t account_id, int* user_id) { + if (account_id == 0 || !user_id) return SCE_NP_ERROR_INVALID_ARGUMENT; + return SCE_NP_ERROR_SIGNED_OUT; } int APS5_VABI sceNpGetSandboxInfo(void* sandbox_info) { diff --git a/core/libs/tests/GuestNpManager.cpp b/core/libs/tests/GuestNpManager.cpp index eaa8a77d45..7bfefee198 100644 --- a/core/libs/tests/GuestNpManager.cpp +++ b/core/libs/tests/GuestNpManager.cpp @@ -7,6 +7,7 @@ extern "C" { int APS5_VABI sceNpGetNpId(int user_id, NpId* np_id); +int APS5_VABI sceNpGetUserIdByAccountId(std::uint64_t account_id, int* user_id); } extern "C" { @@ -67,5 +68,10 @@ int main() { Require(sceNpGetNpId(0x10000, &npId) == SignedOut, "sceNpGetNpId must report the user as signed out"); Require(std::memcmp(&npId, &untouched, sizeof(npId)) == 0, "sceNpGetNpId must leave the NpId untouched"); Require(sceNpGetNpId(0x10000, nullptr) == InvalidArgument, "sceNpGetNpId must reject a null NpId"); + int userId = 0x5a5a; + Require(sceNpGetUserIdByAccountId(0, &userId) == InvalidArgument, "sceNpGetUserIdByAccountId must reject account id 0"); + Require(sceNpGetUserIdByAccountId(0x1234, nullptr) == InvalidArgument, "sceNpGetUserIdByAccountId must reject a null output"); + Require(sceNpGetUserIdByAccountId(0x1234, &userId) == SignedOut, "sceNpGetUserIdByAccountId must report NP as signed out"); + Require(userId == 0x5a5a, "sceNpGetUserIdByAccountId must leave the user id untouched"); return 0; } diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 2f721015f3..237a10e00d 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -20,7 +20,7 @@ Throughout the project, every function at every stage either **does exactly what - `sceNetCtlGetIfStat` signature and 56-byte layout come from PPSA09030's call site and SharpProspero's `SceNetCtlIfStat`; the device kind is always 0, the byte counters are the host's totals over its up non-loopback hardware interfaces (not "since cleared"), and the not-connected result is assumed to match `sceNetCtlGetState` - Font, Http2, Net, Ssl, SystemService and libkernel exports imported by PPSA12544 and added without an implementation have unknown signatures - The Share, NpTrophy2, NpEntitlementAccess and WebBrowserDialog exports imported by PPSA12544's Unity plugins and added without an implementation have unknown signatures -- Acm, GameLiveStreaming, Net, NpEntitlementAccess, NpManager, VoiceQoS, WebBrowserDialog and libkernel exports imported by PPSA14632 and added without an implementation are declared without parameters +- Acm, GameLiveStreaming, Net, NpEntitlementAccess, VoiceQoS, WebBrowserDialog and libkernel exports imported by PPSA14632 and added without an implementation are declared without parameters ### Functional diff --git a/docs/dev/debt/libSceNpManager.md b/docs/dev/debt/libSceNpManager.md index cb0212dd00..c7b654e7b0 100644 --- a/docs/dev/debt/libSceNpManager.md +++ b/docs/dev/debt/libSceNpManager.md @@ -4,6 +4,7 @@ - [sceNpGetSandboxInfo](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager, NID `ZVZ++68nNI8`) - the name hashes to the imported NID (found with nidhunt, confirmed with `tools/nid_names.py`). STAR WARS: Galactic Racer (PPSA28416) imports it; its OnlineSubsystemPS5 calls it with one pointer to a 0x50-byte stack buffer and reads the buffer as a string (discussions/3086). The returned string, the exact parameter struct and console errors are unknown, so every call throws without reading or writing caller memory. A working implementation belongs with that title's bring-up. - [sceNpSetTimeout](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager) - the signature follows [orbital](https://github.com/AlexAltea/orbital/blob/b321fcdbd700cc02da1cb1ad25412781d5ec73c6/tools/ida/db_types.json), [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0fe263a4760dfbfa973366890061749b4af0de97/src/core/libraries/np/np_manager.cpp) and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Np/NpAccount.cs); the rejected values (all five 0, negative retries, a resolve timeout under 1 s or another timeout under 10 s, 0 meaning the default) are shadPS4's, not checked on a console. An id that was never created or was deleted returns `REQUEST_NOT_FOUND`, as in shadPS4; there is no network, so the timeouts are not stored +- [sceNpGetUserIdByAccountId](../../../core/libs/prx/libSceNpManager/Export.cpp) - the `(account id, user id output)` signature and its errors follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/np/np_manager.cpp): account id 0 or a null output returns `SCE_NP_ERROR_INVALID_ARGUMENT`, and a signed-out NP returns `SCE_NP_ERROR_SIGNED_OUT` without writing the output. NP is always signed out here, so no account id resolves to a user ### Functional From b32c5a50cd14f5c0959b8c4050c744f7335a190b Mon Sep 17 00:00:00 2001 From: akkadbakkad89 Date: Wed, 14 Oct 2026 13:42:59 +0000 Subject: [PATCH 0220/1000] feat(libkernel): implement kevent EV_DISABLE, EV_DISPATCH and EV_FORCEONESHOT A knote registered or modified with EV_DISABLE stays registered but is not polled, fired or reported until an EV_ENABLE change re-arms it, as kern_event.c does with KN_DISABLED. EV_DISPATCH reports an event once and then disables the knote, and EV_FORCEONESHOT registers as oneshot. A negative nevents fails with EINVAL instead of throwing. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- core/libs/prx/libkernel/Equeue/Equeue.cpp | 38 +++++++++---- core/libs/prx/libkernel/Equeue/Equeue.hpp | 1 + core/libs/prx/libkernel/Equeue/src/Kqueue.cpp | 37 +++++++------ core/libs/tests/GuestKqueue.cpp | 54 ++++++++++++++++++- docs/dev/debt/libkernel.md | 4 +- 5 files changed, 105 insertions(+), 29 deletions(-) diff --git a/core/libs/prx/libkernel/Equeue/Equeue.cpp b/core/libs/prx/libkernel/Equeue/Equeue.cpp index 641deca1d4..0d084b3d3b 100644 --- a/core/libs/prx/libkernel/Equeue/Equeue.cpp +++ b/core/libs/prx/libkernel/Equeue/Equeue.cpp @@ -50,7 +50,7 @@ void KernelEqueuePrivate::Close() { void KernelEqueuePrivate::TriggerExpiredTimers(uint64_t nowNs) { for (auto& ev : m_events) { - if (ev.deadlineNs != 0 && ev.deadlineNs <= nowNs) { + if (ev.enabled && ev.deadlineNs != 0 && ev.deadlineNs <= nowNs) { if (ev.event.filter == EVFILT_TIMER) { const uint64_t count = ev.intervalNs == 0 ? (ev.triggered ? 0 : 1) @@ -66,7 +66,7 @@ void KernelEqueuePrivate::TriggerExpiredTimers(uint64_t nowNs) { bool KernelEqueuePrivate::NextTimerWaitMicros(uint64_t nowNs, uint32_t* out) const { uint64_t nearest = std::numeric_limits::max(); for (const auto& ev : m_events) { - if (!ev.triggered && ev.deadlineNs != 0) { + if (ev.enabled && !ev.triggered && ev.deadlineNs != 0) { nearest = std::min(nearest, ev.deadlineNs); } } @@ -82,7 +82,7 @@ bool KernelEqueuePrivate::NextTimerWaitMicros(uint64_t nowNs, uint32_t* out) con bool KernelEqueuePrivate::PollEvents() { bool polled = false; for (auto& ev : m_events) { - if (ev.filter.pollFunc != nullptr) { + if (ev.enabled && ev.filter.pollFunc != nullptr) { ev.triggered = ev.filter.pollFunc(&ev); polled = true; } @@ -99,7 +99,7 @@ void KernelEqueuePrivate::WakePollers() { void KernelEqueuePrivate::WaitForDescriptors(std::unique_lock& lock, std::uint64_t deadlineNanos) { std::vector interests; for (const auto& e : m_events) { - if (e.filter.pollFunc != nullptr) { + if (e.enabled && e.filter.pollFunc != nullptr) { interests.push_back({static_cast(e.event.ident), e.event.filter == EVFILT_WRITE}); } } @@ -121,6 +121,10 @@ int KernelEqueuePrivate::CollectTriggered(KernelEvent* ev, int num) { int ret = 0; for (auto it = m_events.begin(); it != m_events.end() && ret < num;) { auto& e = *it; + if (!e.enabled) { + ++it; + continue; + } bool erase = false; while (e.triggered && ret < num) { ev[ret++] = e.event; @@ -128,6 +132,13 @@ int KernelEqueuePrivate::CollectTriggered(KernelEvent* ev, int num) { erase = true; break; } + if ((e.event.flags & EV_DISPATCH) != 0) { + e.enabled = false; + e.triggered = false; + e.event.fflags = 0; + e.event.data = 0; + break; + } if (e.filter.resetFunc != nullptr) { e.filter.resetFunc(&e); } else if ((e.event.flags & EV_CLEAR) != 0) { @@ -152,7 +163,7 @@ int KernelEqueuePrivate::CollectOncePerWait(KernelEvent* ev, int num) { std::list requeued; for (auto it = m_events.begin(); it != m_events.end() && ret < num;) { auto& e = *it; - if (!e.triggered) { + if (!e.triggered || !e.enabled) { ++it; continue; } @@ -165,7 +176,12 @@ int KernelEqueuePrivate::CollectOncePerWait(KernelEvent* ev, int num) { it = m_events.erase(it); continue; } - if (e.filter.resetFunc != nullptr) { + if ((e.event.flags & EV_DISPATCH) != 0) { + e.enabled = false; + e.triggered = false; + e.event.fflags = 0; + e.event.data = 0; + } else if (e.filter.resetFunc != nullptr) { e.filter.resetFunc(&e); } else if ((e.event.flags & EV_CLEAR) != 0) { e.triggered = false; @@ -281,10 +297,12 @@ int KernelEqueuePrivate::TriggerEvent(uintptr_t ident, int16_t filter, void* tri if (it == m_events.end()) { return SCE_KERNEL_ERROR_ENOENT; } - if (it->filter.triggerFunc != nullptr) { - it->filter.triggerFunc(&*it, triggerData); - } else { - it->triggered = true; + if (it->enabled) { + if (it->filter.triggerFunc != nullptr) { + it->filter.triggerFunc(&*it, triggerData); + } else { + it->triggered = true; + } } m_cond.NotifyOne(); WakePollers(); diff --git a/core/libs/prx/libkernel/Equeue/Equeue.hpp b/core/libs/prx/libkernel/Equeue/Equeue.hpp index 8bc8d0671d..f1fac92cad 100644 --- a/core/libs/prx/libkernel/Equeue/Equeue.hpp +++ b/core/libs/prx/libkernel/Equeue/Equeue.hpp @@ -58,6 +58,7 @@ struct KernelFilter { struct KernelEqueueEvent { bool triggered = false; + bool enabled = true; uint64_t deadlineNs = 0; uint64_t intervalNs = 0; KernelEvent event; diff --git a/core/libs/prx/libkernel/Equeue/src/Kqueue.cpp b/core/libs/prx/libkernel/Equeue/src/Kqueue.cpp index 1696650661..4399aa9c92 100644 --- a/core/libs/prx/libkernel/Equeue/src/Kqueue.cpp +++ b/core/libs/prx/libkernel/Equeue/src/Kqueue.cpp @@ -223,6 +223,13 @@ int ModifyKnote(const KernelEqueueRef& queue, KernelEvent& change) { } return GuestErrno(queue->EditEvent(change.ident, change.filter, [&change](KernelEqueueEvent& event) { event.event.udata = change.udata; + if ((change.flags & EV_ENABLE) != 0) { + event.enabled = true; + } + if ((change.flags & EV_DISABLE) != 0) { + event.enabled = false; + } + event.event.flags = static_cast(event.event.flags | (change.flags & (EV_ONESHOT | EV_CLEAR | EV_DISPATCH))); if (change.filter == EVFILT_USER) { TouchUserKnote(event, change); } else if (change.filter == EVFILT_VNODE) { @@ -241,14 +248,8 @@ int Register(const KernelEqueueRef& queue, int kqueueDescriptor, KernelEvent& ch if (change.filter != EVFILT_READ && change.filter != EVFILT_WRITE && change.filter != EVFILT_VNODE && change.filter != EVFILT_TIMER && change.filter != EVFILT_USER) { NotImplemented_nid_no_patch(("kevent: filter " + std::to_string(change.filter)).c_str()); } - if ((change.flags & EV_DISABLE) != 0) { - NotImplemented_nid_no_patch("kevent: EV_DISABLE"); - } - if ((change.flags & EV_DISPATCH) != 0) { - NotImplemented_nid_no_patch("kevent: EV_DISPATCH"); - } if ((change.flags & EV_FORCEONESHOT) != 0) { - NotImplemented_nid_no_patch("kevent: EV_FORCEONESHOT"); + change.flags = static_cast(change.flags | EV_ONESHOT); } if (change.filter == EVFILT_READ || change.filter == EVFILT_WRITE || change.filter == EVFILT_VNODE) { if (const int error = DescriptorFilterError(kqueueDescriptor, change.ident)) { @@ -260,19 +261,23 @@ int Register(const KernelEqueueRef& queue, int kqueueDescriptor, KernelEvent& ch return GUEST_ENOENT; } const auto flags = static_cast(change.flags & ~KNOTE_CLEARED_FLAGS); + int error; if (change.filter == EVFILT_TIMER) { - return AddTimerKnote(queue, change, flags); - } - if (change.filter == EVFILT_USER) { - return AddUserKnote(queue, change, flags); - } - if (change.filter == EVFILT_VNODE) { + error = AddTimerKnote(queue, change, flags); + } else if (change.filter == EVFILT_USER) { + error = AddUserKnote(queue, change, flags); + } else if (change.filter == EVFILT_VNODE) { const std::uint32_t notes = change.fflags; change.fflags = 0; change.data = 0; - return AddVnodeEvent(queue, static_cast(change.ident), notes, flags, change.udata); + error = AddVnodeEvent(queue, static_cast(change.ident), notes, flags, change.udata); + } else { + error = AddDescriptorKnote(queue, change, flags); } - return AddDescriptorKnote(queue, change, flags); + if (error == 0 && (change.flags & EV_DISABLE) != 0) { + queue->EditEvent(change.ident, change.filter, [](KernelEqueueEvent& event) { event.enabled = false; }); + } + return error; } if ((change.flags & EV_DELETE) != 0) { return GuestErrno(queue->DeleteEvent(change.ident, change.filter)); @@ -338,7 +343,7 @@ int APS5_VABI kevent_nid_postfix(int descriptor, const KernelEvent* changelist, return PosixFailure(GUEST_EBADF); } if (nevents < 0) { - NotImplemented_nid_no_patch("kevent: negative nevents"); + return PosixFailure(GUEST_EINVAL); } if (nchanges > 0 && changelist == nullptr) { return PosixFailure(GUEST_EFAULT); diff --git a/core/libs/tests/GuestKqueue.cpp b/core/libs/tests/GuestKqueue.cpp index 8ac99378e5..b4859bb8dc 100644 --- a/core/libs/tests/GuestKqueue.cpp +++ b/core/libs/tests/GuestKqueue.cpp @@ -57,6 +57,8 @@ static constexpr std::uint16_t EvDisable = 0x0008; static constexpr std::uint16_t EvOneshot = 0x0010; static constexpr std::uint16_t EvClear = 0x0020; static constexpr std::uint16_t EvReceipt = 0x0040; +static constexpr std::uint16_t EvDispatch = 0x0080; +static constexpr std::uint16_t EvForceoneshot = 0x0100; static constexpr std::uint16_t EvError = 0x4000; static constexpr std::uint32_t NoteTrigger = 0x01000000; static constexpr std::uint32_t NoteFfcopy = 0xc0000000; @@ -231,6 +233,53 @@ static void SocketPair() { Require(close_nid_postfix(kq) == 0); } +static void DisableAndDispatch() { + const int kq = kqueue_nid_postfix(); + int pair[2]; + Require(socketpair_nid_postfix(Unix, Stream, 0, pair) == 0); + int marker = 0; + KernelEvent events[4]{}; + const auto ident = static_cast(pair[0]); + + Require(Apply(kq, Change(ident, EvfiltRead, EvAdd | EvDisable)) == 0); + Require(send_nid_postfix(pair[1], "a", 1, 0) == 1); + Require(Poll(kq, events, 4) == 0); + Require(Apply(kq, Change(ident, EvfiltRead, EvAdd | EvEnable)) == 0); + Require(Poll(kq, events, 4) == 1 && events[0].ident == ident && events[0].data == 1); + Require(Apply(kq, Change(ident, EvfiltRead, EvAdd | EvDisable)) == 0); + Require(Poll(kq, events, 4) == 0); + Require(Apply(kq, Change(ident, EvfiltRead, EvDelete)) == 0); + char buffer[4]{}; + Require(recv_nid_postfix(pair[0], buffer, 1, 0) == 1); + + Require(Apply(kq, Change(ident, EvfiltRead, EvAdd | EvDispatch, 0, 0, &marker)) == 0); + Require(send_nid_postfix(pair[1], "bc", 2, 0) == 2); + Require(Wait(kq, events, 4) == 1 && events[0].udata == &marker && events[0].data == 2); + Require(Poll(kq, events, 4) == 0); + Require(send_nid_postfix(pair[1], "d", 1, 0) == 1); + Require(Poll(kq, events, 4) == 0); + Require(Apply(kq, Change(ident, EvfiltRead, EvAdd | EvEnable)) == 0); + Require(Wait(kq, events, 4) == 1 && events[0].data == 3); + Require(Poll(kq, events, 4) == 0); + Require(Apply(kq, Change(ident, EvfiltRead, EvDelete)) == 0); + + Require(Apply(kq, Change(ident, EvfiltRead, EvAdd | EvForceoneshot)) == 0); + Require(Poll(kq, events, 4) == 1 && events[0].data == 3); + Require(Poll(kq, events, 4) == 0); + RequireFailure(Apply(kq, Change(ident, EvfiltRead, EvDelete)), GuestEnoent); + Require(recv_nid_postfix(pair[0], buffer, sizeof(buffer), 0) == 3); + + Require(Apply(kq, Change(9, EvfiltUser, EvAdd | EvDisable)) == 0); + Require(Apply(kq, Change(9, EvfiltUser, 0, NoteTrigger)) == 0); + Require(Poll(kq, events, 4) == 0); + Require(Apply(kq, Change(9, EvfiltUser, EvAdd | EvEnable)) == 0); + Require(Poll(kq, events, 4) == 1 && events[0].filter == EvfiltUser); + Require(Apply(kq, Change(9, EvfiltUser, EvDelete)) == 0); + + Require(close_nid_postfix(pair[0]) == 0 && close_nid_postfix(pair[1]) == 0); + Require(close_nid_postfix(kq) == 0); +} + static void Errors() { const int kq = kqueue_nid_postfix(); KernelEvent events[4]{}; @@ -249,7 +298,9 @@ static void Errors() { RequireFailure(Apply(kq, Change(static_cast(kq), EvfiltRead, EvAdd)), GuestEinval); RequireFailure(Apply(kq, Change(1, 1, EvAdd)), GuestEinval); Require(Throws([kq] { Apply(kq, Change(1, EvfiltProc, EvAdd)); })); - Require(Throws([kq] { Apply(kq, Change(1, EvfiltUser, EvAdd | EvDisable)); })); + Require(Apply(kq, Change(1, EvfiltUser, EvAdd | EvDisable)) == 0); + Require(Apply(kq, Change(1, EvfiltUser, EvDelete)) == 0); + RequireFailure(kevent_nid_postfix(kq, nullptr, 0, events, -1, nullptr), GuestEinval); int pipe[2]; Require(pipe_nid_postfix(pipe) == 0); Require(Throws([kq, &pipe] { Apply(kq, Change(static_cast(pipe[0]), EvfiltRead, EvAdd)); })); @@ -532,6 +583,7 @@ int main() { Timers(); UserEvents(); SocketPair(); + DisableAndDispatch(); Errors(); FileWatch(); #ifndef _WIN32 diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index f16a03c5ab..efd8bcf6ec 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -107,8 +107,8 @@ - [sceKernelMprotect, sceKernelMtypeprotect](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (libkernel) over a range with a gap that is not registered in the allocation tracker protect the registered parts and skip the gap, and return 0, as FreeBSD 11.4's [`vm_map_protect`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/vm/vm_map.c) does (it has no check for holes); a range whose start is unmapped (also when later pages are mapped) returns `SCE_KERNEL_ERROR_EINVAL` (`mprotect`: `EINVAL`) because an existing test pins that for an unmapped start; FreeBSD answers 0 there too (`vm_map_protect` takes the next entry), so this errno is not taken from the source and was not changed. `sceKernelMprotect` threw for a gap before, which ended the title. Every part is checked for a GPU or image pin before the first one is protected; the parts are then protected one after the other, so a host protection failure in a later part leaves the earlier parts changed (FreeBSD's `vm_map_protect` cannot fail in its second pass). The behaviour on the console is not known. None of this was measured on a console. - [JIT shared memory](../../../core/libs/prx/libkernel/DirectMemory/Jit.cpp) (libkernel) - `sceKernelJitCreateSharedMemory`, `sceKernelJitCreateAliasOfSharedMemory` and `sceKernelJitMapSharedMemory` take the signatures of [Orbital](https://github.com/AlexAltea/orbital)'s `tools/ida/db_types.json` (name, length, maximum protection, descriptor out; descriptor, maximum protection, descriptor out; descriptor, protection, address in and out), which ps4sdk, ps5-jar-loader and the call sites in RetroArch PPSA99169, XPSemu PPSA97358 and VLC PPSA85300 match; SharpProspero declares the alias as returning the descriptor. The object is a host anonymous file shared by its descriptors (a `memfd` on Linux, a delete-on-close temporary file mapped as an executable section on Windows), the alias is a duplicate descriptor, and every map maps the whole object, so writes through one view show through the others; a non-null address is a hint, as in `mmap` without `MAP_FIXED`. Not measured on the console: a length that is zero or not a multiple of 16 KiB and protection bits other than CPU read, write and execute are `EINVAL`; an alias or a map asking for more than the descriptor's maximum protection (write and execute counting as read) is `EACCES`, as FreeBSD 11's `vm_map_protect` treats a protection past `max_protection`; a descriptor that is not a JIT object is `EBADF`. The name is ignored, the objects do not count against the flexible memory budget, `mprotect` of a view is not limited to the maximum protection, and `mmap` of the descriptors is unsupported, as for `shm_open`. On Linux the `memfd` is created without `MFD_EXEC`, which Linux 6.3 added and older kernels reject with `EINVAL`, and relies on the protection of the mapping: the flag only sets the file's execute permission bits, and the direct memory `memfd` is mapped executable the same way; a host with `vm.memfd_noexec` set to 2 refuses the creation and the call throws -- [kqueue and kevent](../../../core/libs/prx/libkernel/Equeue/src/Kqueue.cpp) (libkernel) follow FreeBSD 11's [`kern_event.c`](https://github.com/freebsd/freebsd-src/blob/34bd34750187bae02a0086402c78c031c59dd8b3/sys/kern/kern_event.c) (`kqueue_kevent`, `kqueue_register`, `kqueue_scan`, the timer and user filters) over the event queue; nothing is measured on a console. The descriptor is a host null device registered as the kqueue: `close` and `sceKernelClose` close the queue and `dup`/`dup2` of it throw, but `read`, `write` and `fstat` act on the null device instead of failing with `EOPNOTSUPP` or reporting `S_IFIFO`. Read and write events are supported on guest sockets only, plus the timer, user and [vnode](../../../core/libs/prx/libkernel/Equeue/src/FileEvent.cpp) filters; other filters, `EV_DISABLE`, `EV_DISPATCH`, `EV_FORCEONESHOT`, `NOTE_LOWAT`, `EV_CLEAR` on a socket, `EVFILT_READ` on another kqueue, a `NOTE_NSECONDS` period above one second with a fraction (11.0 converts the fraction with the microsecond macro), any change to an existing timer (11.0 keeps the armed expiry and applies the new period after it, 11.4's `filt_timertouch` restarts it) and a negative `nevents` throw. A new user event keeps its `fflags` unmasked and is not triggered by `NOTE_TRIGGER`, because `filt_userattach` reads the already cleared `kn_fflags`. The timer limit `kq_calloutmax` (`ENOMEM`) is not enforced, and closing a kqueue wakes a thread waiting on it in `kevent` with `EBADF`, where `kqueue_drain` makes the close wait for that thread. A level-triggered event is reported once per call and moved behind the other events, as in `kqueue_scan`; the `sceKernel*Equeue` calls keep their own collection loop -- [EVFILT_VNODE, sceKernelAddFileEvent and sceKernelDeleteFileEvent](../../../core/libs/prx/libkernel/Equeue/src/FileEvent.cpp) (libkernel) watch the host file or directory behind the descriptor and turn host changes into FreeBSD 11 vnode hints, which [`filt_vfsvnode`](https://github.com/freebsd/freebsd-src/blob/34bd34750187bae02a0086402c78c031c59dd8b3/sys/kern/vfs_subr.c#L5032) adds whole when they share a bit with the requested `fflags`: a write that grows a file is `NOTE_WRITE | NOTE_EXTEND`, another write `NOTE_WRITE`, a shrinking truncate or an attribute change `NOTE_ATTRIB`, a higher link count `NOTE_LINK`, a lower one or a pending delete `NOTE_DELETE`, a new path `NOTE_RENAME`; in a directory, an entry created, removed or renamed is `NOTE_WRITE`, with `NOTE_LINK` for a subdirectory, and on Linux an entry moved from or to another directory adds `NOTE_EXTEND`; inotify queues the two halves of a rename one after the other, so a watcher woken by the first can read it alone, and a half read without the other is classified only after a `getdents64` on the watched directory, which takes the directory lock that `rename` holds until both halves are queued ([`do_renameat2`](https://github.com/torvalds/linux/blob/v6.8/fs/namei.c#L4975) around `vfs_rename` and its `fsnotify_move`, [`iterate_dir`](https://github.com/torvalds/linux/blob/v6.8/fs/readdir.c#L103)), an ordering taken from the kernel source, not from inotify(7), which promises none; a directory registered with `NOTE_EXTEND` on Windows is accepted, as `vfs_kqfilter` does not check it, but never receives it, because the directory handle cannot tell a move between directories from a removal plus an addition. FreeBSD's hints depend on the filesystem and the console's filesystems are not known: a growing `ftruncate` reports `NOTE_WRITE | NOTE_EXTEND` where `vop_setattr_post` reports `NOTE_ATTRIB`, and on Windows `utimes` on a file reports `NOTE_WRITE`. The three Windows notifications are told apart in the error, which names the `parent`, `entries` or `subdirectories` watch, the path it was given and `GetLastError`, but a `FindFirstChangeNotification` that fails still throws rather than returning an error to the guest: the EVFILT_VNODE attach path is [`vfs_kqfilter`](https://github.com/freebsd/freebsd-src/blob/34bd34750187bae02a0086402c78c031c59dd8b3/sys/kern/vfs_subr.c#L4936), which returns `EINVAL`, `ENOMEM` or 0 after `fget` has turned a descriptor that is not open into `EBADF`, and consults no directory permission, so no FreeBSD errno stands for a host that refuses to notify, and what the console does on such a host is not measured. Only the `parent` watch can fail this way in practice: `entries` and `subdirectories` need the `FILE_LIST_DIRECTORY` that opening the watched directory already took. The parent handle produces no hint of its own, as `RunHostWatch` only calls `FindNextChangeNotification` on it, so a later change may leave the watch on its 50 ms poll instead of throwing; that is not done here. Windows uses `FindFirstChangeNotification` on the parent directory (and on a watched directory itself) and compares `GetFileInformationByHandleEx` snapshots at least every 50 ms, because a write through an open handle is reported only when the handle is flushed or closed, so file hints can arrive 50 ms late, and a file deleted while watched, which the POSIX delete of Windows 10 1903 moves into `$Extend\$Deleted` until its last handle closes, reports `NOTE_DELETE` without `NOTE_RENAME` and keeps its handle without a parent directory watch, as that directory cannot be watched; Linux uses inotify on the file and is not run. `NOTE_REVOKE`, `NOTE_OPEN`, `NOTE_CLOSE`, `NOTE_CLOSE_WRITE`, `NOTE_READ`, unknown bits and descriptors other than files, directories, pipes and sockets throw. `sceKernelAddFileEvent(eq, fd, watch, udata)` and `sceKernelDeleteFileEvent(eq, fd)` take the [OpenOrbis](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain/blob/0a1aaf9d/include/orbis/libkernel.h#L35) and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0/src/SharpProspero/Interop/Kernel/KernelEqueue.cs#L164) signatures; `watch` is taken as the `fflags` and the event is added with `EV_CLEAR`, neither confirmed on a title +- [kqueue and kevent](../../../core/libs/prx/libkernel/Equeue/src/Kqueue.cpp) (libkernel) follow FreeBSD 11's [`kern_event.c`](https://github.com/freebsd/freebsd-src/blob/34bd34750187bae02a0086402c78c031c59dd8b3/sys/kern/kern_event.c) (`kqueue_kevent`, `kqueue_register`, `kqueue_scan`, the timer and user filters) over the event queue; nothing is measured on a console. The descriptor is a host null device registered as the kqueue: `close` and `sceKernelClose` close the queue and `dup`/`dup2` of it throw, but `read`, `write` and `fstat` act on the null device instead of failing with `EOPNOTSUPP` or reporting `S_IFIFO`. Read and write events are supported on guest sockets only, plus the timer, user and [vnode](../../../core/libs/prx/libkernel/Equeue/src/FileEvent.cpp) filters; other filters, `NOTE_LOWAT`, `EV_CLEAR` on a socket, `EVFILT_READ` on another kqueue, a `NOTE_NSECONDS` period above one second with a fraction (11.0 converts the fraction with the microsecond macro) and any change to an existing timer (11.0 keeps the armed expiry and applies the new period after it, 11.4's `filt_timertouch` restarts it) throw. `EV_DISABLE`, `EV_DISPATCH` and `EV_FORCEONESHOT` follow kern_event.c: a disabled knote stays registered, stops polling or firing and is not reported, a trigger raised while it is disabled is kept and reported after `EV_ENABLE`, where `knote` would drop it, and `EV_DISPATCH` reports once and then disables the knote; a negative `nevents` fails with `EINVAL`. A new user event keeps its `fflags` unmasked and is not triggered by `NOTE_TRIGGER`, because `filt_userattach` reads the already cleared `kn_fflags`. The timer limit `kq_calloutmax` (`ENOMEM`) is not enforced, and closing a kqueue wakes a thread waiting on it in `kevent` with `EBADF`, where `kqueue_drain` makes the close wait for that thread. A level-triggered event is reported once per call and moved behind the other events, as in `kqueue_scan`; the `sceKernel*Equeue` calls keep their own collection loop +- [EVFILT_VNODE, sceKernelAddFileEvent and sceKernelDeleteFileEvent](../../../core/libs/prx/libkernel/Equeue/src/FileEvent.cpp) (libkernel) watch the host file or directory behind the descriptor and turn host changes into FreeBSD 11 vnode hints, which [`filt_vfsvnode`](https://github.com/freebsd/freebsd-src/blob/34bd34750187bae02a0086402c78c031c59dd8b3/sys/kern/vfs_subr.c#L5032) adds whole when they share a bit with the requested `fflags`: a write that grows a file is `NOTE_WRITE | NOTE_EXTEND`, another write `NOTE_WRITE`, a shrinking truncate or an attribute change `NOTE_ATTRIB`, a higher link count `NOTE_LINK`, a lower one or a pending delete `NOTE_DELETE`, a new path `NOTE_RENAME`; in a directory, an entry created, removed or renamed is `NOTE_WRITE`, with `NOTE_LINK` for a subdirectory, and on Linux an entry moved from or to another directory adds `NOTE_EXTEND`; inotify queues the two halves of a rename one after the other, so a watcher woken by the first can read it alone, and a half read without the other is classified only after a `getdents64` on the watched directory, which takes the directory lock that `rename` holds until both halves are queued ([`do_renameat2`](https://github.com/torvalds/linux/blob/v6.8/fs/namei.c#L4975) around `vfs_rename` and its `fsnotify_move`, [`iterate_dir`](https://github.com/torvalds/linux/blob/v6.8/fs/readdir.c#L103)), an ordering taken from the kernel source, not from inotify(7), which promises none; a directory registered with `NOTE_EXTEND` on Windows is accepted, as `vfs_kqfilter` does not check it, but never receives it, because the directory handle cannot tell a move between directories from a removal plus an addition. FreeBSD's hints depend on the filesystem and the console's filesystems are not known: a growing `ftruncate` reports `NOTE_WRITE | NOTE_EXTEND` where `vop_setattr_post` reports `NOTE_ATTRIB`, and on Windows `utimes` on a file reports `NOTE_WRITE`. The three Windows notifications are told apart in the error, which names the `parent`, `entries` or `subdirectories` watch, the path it was given and `GetLastError`, but a `FindFirstChangeNotification` that fails still throws rather than returning an error to the guest: the EVFILT_VNODE attach path is [`vfs_kqfilter`](https://github.com/freebsd/freebsd-src/blob/34bd34750187bae02a0086402c78c031c59dd8b3/sys/kern/vfs_subr.c#L4936), which returns `EINVAL`, `ENOMEM` or 0 after `fget` has turned a descriptor that is not open into `EBADF`, and consults no directory permission, so no FreeBSD errno stands for a host that refuses to notify, and what the console does on such a host is not measured. Only the `parent` watch can fail this way in practice: `entries` and `subdirectories` need the `FILE_LIST_DIRECTORY` that opening the watched directory already took. The parent handle produces no hint of its own, as `RunHostWatch` only calls `FindNextChangeNotification` on it, so a later change may leave the watch on its 50 ms poll instead of throwing; that is not done here. Windows uses `FindFirstChangeNotification` on the parent directory (and on a watched directory itself) and compares `GetFileInformationByHandleEx` snapshots at least every 50 ms, because a write through an open handle is reported only when the handle is flushed or closed, so file hints can arrive 50 ms late, and a file deleted while watched, which the POSIX delete of Windows 10 1903 moves into `$Extend\$Deleted` until its last handle closes, reports `NOTE_DELETE` without `NOTE_RENAME` and keeps its handle without a parent directory watch, as that directory cannot be watched; Linux uses inotify on the file and is not run. `NOTE_REVOKE`, `NOTE_OPEN`, `NOTE_CLOSE`, `NOTE_CLOSE_WRITE`, `NOTE_READ`, unknown bits and descriptors other than files, directories, pipes and sockets throw. `sceKernelAddFileEvent(eq, fd, watch, udata)` and `sceKernelDeleteFileEvent(eq, fd)` take the [OpenOrbis](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain/blob/0a1aaf9d/include/orbis/libkernel.h#L35) and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0/src/SharpProspero/Interop/Kernel/KernelEqueue.cs#L164) signatures; `watch` is taken as the `fflags` and the event is added with `EV_CLEAR`, neither confirmed on a title (feat(libkernel): implement kevent EV_DISABLE, EV_DISPATCH and EV_FORCEONESHOT) - [sem_timedwait](../../../core/libs/prx/libkernel/Pthread/Posix/Sem.cpp) (libkernel) consumes an available count before checking the timeout and waits indefinitely for a null timeout, following FreeBSD 11's `sem_timedwait`. Console behavior for null and invalid timeouts when a count is available is unverified. - [tcsendbreak, tcdrain, tcflush, tcflow, tcgetpgrp, tcsetpgrp](../../../core/libs/prx/libkernel/File/src/Terminal.cpp) (libkernel) follow FreeBSD 11.4's [`termios.c`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/gen/termios.c): `tcflush` answers `EINVAL` for a queue other than `TCIFLUSH`, `TCOFLUSH` or `TCIOFLUSH` and `tcflow` for an action outside `TCOOFF`..`TCION` before the descriptor is looked at, and every other call fails as its terminal ioctl does on a descriptor that is not a terminal, with the errors `tcgetattr` gives, so `tcsendbreak` never reaches its 0.4 s pause. FreeBSD's `pipe_ioctl` answers `TIOCGPGRP` and `TIOCSPGRP` on a pipe or FIFO with the descriptor's SIGIO owner, which is not modelled, so `tcgetpgrp` and `tcsetpgrp` throw on a host pipe or FIFO above descriptor 2; descriptors 0 to 2 are taken as non-terminals that are not pipes, whatever the host attaches to them, so a pipe the guest creates on a freed descriptor 0 to 2 gets `ENOTTY` there. The PS5 `libkernel` is assumed to use this FreeBSD code, and `tcdrain` is not a cancellation point here - [Windows file timestamps](../../../core/libs/prx/libkernel/File/include/WindowsFileTime.hpp) use the host filesystem's timestamp range and resolution. `utimes`, `futimes`, and `lutimes` reject timestamps outside nonnegative signed 64-bit Windows file-time ticks with `EINVAL`, including exactly 1601-01-01 00:00:00 UTC, whose zero value means "leave unchanged" to `SetFileTime`. Guest directory descriptors still resolve through their recorded path. Filesystems with coarser timestamp resolution cannot retain every guest microsecond From a7da412a719c5763188bcf7f3b20a00cfe7cdcfd Mon Sep 17 00:00:00 2001 From: akkadbakkad89 Date: Wed, 14 Oct 2026 14:17:12 +0000 Subject: [PATCH 0221/1000] fix(libkernel): follow kqueue_register for EV_FORCEONESHOT and modify flags On a new knote EV_FORCEONESHOT is dropped from the flags; on an existing one it sets EV_ONESHOT and fires the knote, as kqueue_register does at l.1339-1341. A modify no longer ORs EV_ONESHOT, EV_CLEAR or EV_DISPATCH into the knote. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- core/libs/prx/libkernel/Equeue/src/Kqueue.cpp | 8 ++++---- core/libs/tests/GuestKqueue.cpp | 3 +++ docs/dev/debt/libkernel.md | 4 ++-- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libkernel/Equeue/src/Kqueue.cpp b/core/libs/prx/libkernel/Equeue/src/Kqueue.cpp index 4399aa9c92..9bd2979f19 100644 --- a/core/libs/prx/libkernel/Equeue/src/Kqueue.cpp +++ b/core/libs/prx/libkernel/Equeue/src/Kqueue.cpp @@ -229,7 +229,10 @@ int ModifyKnote(const KernelEqueueRef& queue, KernelEvent& change) { if ((change.flags & EV_DISABLE) != 0) { event.enabled = false; } - event.event.flags = static_cast(event.event.flags | (change.flags & (EV_ONESHOT | EV_CLEAR | EV_DISPATCH))); + if ((change.flags & EV_FORCEONESHOT) != 0) { + event.event.flags = static_cast(event.event.flags | EV_ONESHOT); + event.triggered = true; + } if (change.filter == EVFILT_USER) { TouchUserKnote(event, change); } else if (change.filter == EVFILT_VNODE) { @@ -248,9 +251,6 @@ int Register(const KernelEqueueRef& queue, int kqueueDescriptor, KernelEvent& ch if (change.filter != EVFILT_READ && change.filter != EVFILT_WRITE && change.filter != EVFILT_VNODE && change.filter != EVFILT_TIMER && change.filter != EVFILT_USER) { NotImplemented_nid_no_patch(("kevent: filter " + std::to_string(change.filter)).c_str()); } - if ((change.flags & EV_FORCEONESHOT) != 0) { - change.flags = static_cast(change.flags | EV_ONESHOT); - } if (change.filter == EVFILT_READ || change.filter == EVFILT_WRITE || change.filter == EVFILT_VNODE) { if (const int error = DescriptorFilterError(kqueueDescriptor, change.ident)) { return error; diff --git a/core/libs/tests/GuestKqueue.cpp b/core/libs/tests/GuestKqueue.cpp index b4859bb8dc..f0b46d539c 100644 --- a/core/libs/tests/GuestKqueue.cpp +++ b/core/libs/tests/GuestKqueue.cpp @@ -263,6 +263,9 @@ static void DisableAndDispatch() { Require(Poll(kq, events, 4) == 0); Require(Apply(kq, Change(ident, EvfiltRead, EvDelete)) == 0); + Require(Apply(kq, Change(ident, EvfiltRead, EvAdd | EvForceoneshot)) == 0); + Require(Poll(kq, events, 4) == 1 && events[0].data == 3); + Require(Poll(kq, events, 4) == 1 && events[0].data == 3); Require(Apply(kq, Change(ident, EvfiltRead, EvAdd | EvForceoneshot)) == 0); Require(Poll(kq, events, 4) == 1 && events[0].data == 3); Require(Poll(kq, events, 4) == 0); diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index efd8bcf6ec..0abcc952ae 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -107,8 +107,8 @@ - [sceKernelMprotect, sceKernelMtypeprotect](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (libkernel) over a range with a gap that is not registered in the allocation tracker protect the registered parts and skip the gap, and return 0, as FreeBSD 11.4's [`vm_map_protect`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/vm/vm_map.c) does (it has no check for holes); a range whose start is unmapped (also when later pages are mapped) returns `SCE_KERNEL_ERROR_EINVAL` (`mprotect`: `EINVAL`) because an existing test pins that for an unmapped start; FreeBSD answers 0 there too (`vm_map_protect` takes the next entry), so this errno is not taken from the source and was not changed. `sceKernelMprotect` threw for a gap before, which ended the title. Every part is checked for a GPU or image pin before the first one is protected; the parts are then protected one after the other, so a host protection failure in a later part leaves the earlier parts changed (FreeBSD's `vm_map_protect` cannot fail in its second pass). The behaviour on the console is not known. None of this was measured on a console. - [JIT shared memory](../../../core/libs/prx/libkernel/DirectMemory/Jit.cpp) (libkernel) - `sceKernelJitCreateSharedMemory`, `sceKernelJitCreateAliasOfSharedMemory` and `sceKernelJitMapSharedMemory` take the signatures of [Orbital](https://github.com/AlexAltea/orbital)'s `tools/ida/db_types.json` (name, length, maximum protection, descriptor out; descriptor, maximum protection, descriptor out; descriptor, protection, address in and out), which ps4sdk, ps5-jar-loader and the call sites in RetroArch PPSA99169, XPSemu PPSA97358 and VLC PPSA85300 match; SharpProspero declares the alias as returning the descriptor. The object is a host anonymous file shared by its descriptors (a `memfd` on Linux, a delete-on-close temporary file mapped as an executable section on Windows), the alias is a duplicate descriptor, and every map maps the whole object, so writes through one view show through the others; a non-null address is a hint, as in `mmap` without `MAP_FIXED`. Not measured on the console: a length that is zero or not a multiple of 16 KiB and protection bits other than CPU read, write and execute are `EINVAL`; an alias or a map asking for more than the descriptor's maximum protection (write and execute counting as read) is `EACCES`, as FreeBSD 11's `vm_map_protect` treats a protection past `max_protection`; a descriptor that is not a JIT object is `EBADF`. The name is ignored, the objects do not count against the flexible memory budget, `mprotect` of a view is not limited to the maximum protection, and `mmap` of the descriptors is unsupported, as for `shm_open`. On Linux the `memfd` is created without `MFD_EXEC`, which Linux 6.3 added and older kernels reject with `EINVAL`, and relies on the protection of the mapping: the flag only sets the file's execute permission bits, and the direct memory `memfd` is mapped executable the same way; a host with `vm.memfd_noexec` set to 2 refuses the creation and the call throws -- [kqueue and kevent](../../../core/libs/prx/libkernel/Equeue/src/Kqueue.cpp) (libkernel) follow FreeBSD 11's [`kern_event.c`](https://github.com/freebsd/freebsd-src/blob/34bd34750187bae02a0086402c78c031c59dd8b3/sys/kern/kern_event.c) (`kqueue_kevent`, `kqueue_register`, `kqueue_scan`, the timer and user filters) over the event queue; nothing is measured on a console. The descriptor is a host null device registered as the kqueue: `close` and `sceKernelClose` close the queue and `dup`/`dup2` of it throw, but `read`, `write` and `fstat` act on the null device instead of failing with `EOPNOTSUPP` or reporting `S_IFIFO`. Read and write events are supported on guest sockets only, plus the timer, user and [vnode](../../../core/libs/prx/libkernel/Equeue/src/FileEvent.cpp) filters; other filters, `NOTE_LOWAT`, `EV_CLEAR` on a socket, `EVFILT_READ` on another kqueue, a `NOTE_NSECONDS` period above one second with a fraction (11.0 converts the fraction with the microsecond macro) and any change to an existing timer (11.0 keeps the armed expiry and applies the new period after it, 11.4's `filt_timertouch` restarts it) throw. `EV_DISABLE`, `EV_DISPATCH` and `EV_FORCEONESHOT` follow kern_event.c: a disabled knote stays registered, stops polling or firing and is not reported, a trigger raised while it is disabled is kept and reported after `EV_ENABLE`, where `knote` would drop it, and `EV_DISPATCH` reports once and then disables the knote; a negative `nevents` fails with `EINVAL`. A new user event keeps its `fflags` unmasked and is not triggered by `NOTE_TRIGGER`, because `filt_userattach` reads the already cleared `kn_fflags`. The timer limit `kq_calloutmax` (`ENOMEM`) is not enforced, and closing a kqueue wakes a thread waiting on it in `kevent` with `EBADF`, where `kqueue_drain` makes the close wait for that thread. A level-triggered event is reported once per call and moved behind the other events, as in `kqueue_scan`; the `sceKernel*Equeue` calls keep their own collection loop -- [EVFILT_VNODE, sceKernelAddFileEvent and sceKernelDeleteFileEvent](../../../core/libs/prx/libkernel/Equeue/src/FileEvent.cpp) (libkernel) watch the host file or directory behind the descriptor and turn host changes into FreeBSD 11 vnode hints, which [`filt_vfsvnode`](https://github.com/freebsd/freebsd-src/blob/34bd34750187bae02a0086402c78c031c59dd8b3/sys/kern/vfs_subr.c#L5032) adds whole when they share a bit with the requested `fflags`: a write that grows a file is `NOTE_WRITE | NOTE_EXTEND`, another write `NOTE_WRITE`, a shrinking truncate or an attribute change `NOTE_ATTRIB`, a higher link count `NOTE_LINK`, a lower one or a pending delete `NOTE_DELETE`, a new path `NOTE_RENAME`; in a directory, an entry created, removed or renamed is `NOTE_WRITE`, with `NOTE_LINK` for a subdirectory, and on Linux an entry moved from or to another directory adds `NOTE_EXTEND`; inotify queues the two halves of a rename one after the other, so a watcher woken by the first can read it alone, and a half read without the other is classified only after a `getdents64` on the watched directory, which takes the directory lock that `rename` holds until both halves are queued ([`do_renameat2`](https://github.com/torvalds/linux/blob/v6.8/fs/namei.c#L4975) around `vfs_rename` and its `fsnotify_move`, [`iterate_dir`](https://github.com/torvalds/linux/blob/v6.8/fs/readdir.c#L103)), an ordering taken from the kernel source, not from inotify(7), which promises none; a directory registered with `NOTE_EXTEND` on Windows is accepted, as `vfs_kqfilter` does not check it, but never receives it, because the directory handle cannot tell a move between directories from a removal plus an addition. FreeBSD's hints depend on the filesystem and the console's filesystems are not known: a growing `ftruncate` reports `NOTE_WRITE | NOTE_EXTEND` where `vop_setattr_post` reports `NOTE_ATTRIB`, and on Windows `utimes` on a file reports `NOTE_WRITE`. The three Windows notifications are told apart in the error, which names the `parent`, `entries` or `subdirectories` watch, the path it was given and `GetLastError`, but a `FindFirstChangeNotification` that fails still throws rather than returning an error to the guest: the EVFILT_VNODE attach path is [`vfs_kqfilter`](https://github.com/freebsd/freebsd-src/blob/34bd34750187bae02a0086402c78c031c59dd8b3/sys/kern/vfs_subr.c#L4936), which returns `EINVAL`, `ENOMEM` or 0 after `fget` has turned a descriptor that is not open into `EBADF`, and consults no directory permission, so no FreeBSD errno stands for a host that refuses to notify, and what the console does on such a host is not measured. Only the `parent` watch can fail this way in practice: `entries` and `subdirectories` need the `FILE_LIST_DIRECTORY` that opening the watched directory already took. The parent handle produces no hint of its own, as `RunHostWatch` only calls `FindNextChangeNotification` on it, so a later change may leave the watch on its 50 ms poll instead of throwing; that is not done here. Windows uses `FindFirstChangeNotification` on the parent directory (and on a watched directory itself) and compares `GetFileInformationByHandleEx` snapshots at least every 50 ms, because a write through an open handle is reported only when the handle is flushed or closed, so file hints can arrive 50 ms late, and a file deleted while watched, which the POSIX delete of Windows 10 1903 moves into `$Extend\$Deleted` until its last handle closes, reports `NOTE_DELETE` without `NOTE_RENAME` and keeps its handle without a parent directory watch, as that directory cannot be watched; Linux uses inotify on the file and is not run. `NOTE_REVOKE`, `NOTE_OPEN`, `NOTE_CLOSE`, `NOTE_CLOSE_WRITE`, `NOTE_READ`, unknown bits and descriptors other than files, directories, pipes and sockets throw. `sceKernelAddFileEvent(eq, fd, watch, udata)` and `sceKernelDeleteFileEvent(eq, fd)` take the [OpenOrbis](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain/blob/0a1aaf9d/include/orbis/libkernel.h#L35) and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0/src/SharpProspero/Interop/Kernel/KernelEqueue.cs#L164) signatures; `watch` is taken as the `fflags` and the event is added with `EV_CLEAR`, neither confirmed on a title (feat(libkernel): implement kevent EV_DISABLE, EV_DISPATCH and EV_FORCEONESHOT) +- [kqueue and kevent](../../../core/libs/prx/libkernel/Equeue/src/Kqueue.cpp) (libkernel) follow FreeBSD 11's [`kern_event.c`](https://github.com/freebsd/freebsd-src/blob/34bd34750187bae02a0086402c78c031c59dd8b3/sys/kern/kern_event.c) (`kqueue_kevent`, `kqueue_register`, `kqueue_scan`, the timer and user filters) over the event queue; nothing is measured on a console. The descriptor is a host null device registered as the kqueue: `close` and `sceKernelClose` close the queue and `dup`/`dup2` of it throw, but `read`, `write` and `fstat` act on the null device instead of failing with `EOPNOTSUPP` or reporting `S_IFIFO`. Read and write events are supported on guest sockets only, plus the timer, user and [vnode](../../../core/libs/prx/libkernel/Equeue/src/FileEvent.cpp) filters; other filters, `NOTE_LOWAT`, `EV_CLEAR` on a socket, `EVFILT_READ` on another kqueue, a `NOTE_NSECONDS` period above one second with a fraction (11.0 converts the fraction with the microsecond macro) and any change to an existing timer (11.0 keeps the armed expiry and applies the new period after it, 11.4's `filt_timertouch` restarts it) throw. `EV_DISABLE`, `EV_DISPATCH` and `EV_FORCEONESHOT` follow kern_event.c: a disabled knote stays registered, stops polling or firing and is not reported, a trigger raised while it is disabled is kept and reported after `EV_ENABLE`, where `knote` would drop it, `EV_DISPATCH` reports once and then disables the knote, and `EV_FORCEONESHOT` is ignored on a new knote while on an existing one it sets `EV_ONESHOT` and fires the knote; a negative `nevents` fails with `EINVAL`. A new user event keeps its `fflags` unmasked and is not triggered by `NOTE_TRIGGER`, because `filt_userattach` reads the already cleared `kn_fflags`. The timer limit `kq_calloutmax` (`ENOMEM`) is not enforced, and closing a kqueue wakes a thread waiting on it in `kevent` with `EBADF`, where `kqueue_drain` makes the close wait for that thread. A level-triggered event is reported once per call and moved behind the other events, as in `kqueue_scan`; the `sceKernel*Equeue` calls keep their own collection loop +- [EVFILT_VNODE, sceKernelAddFileEvent and sceKernelDeleteFileEvent](../../../core/libs/prx/libkernel/Equeue/src/FileEvent.cpp) (libkernel) watch the host file or directory behind the descriptor and turn host changes into FreeBSD 11 vnode hints, which [`filt_vfsvnode`](https://github.com/freebsd/freebsd-src/blob/34bd34750187bae02a0086402c78c031c59dd8b3/sys/kern/vfs_subr.c#L5032) adds whole when they share a bit with the requested `fflags`: a write that grows a file is `NOTE_WRITE | NOTE_EXTEND`, another write `NOTE_WRITE`, a shrinking truncate or an attribute change `NOTE_ATTRIB`, a higher link count `NOTE_LINK`, a lower one or a pending delete `NOTE_DELETE`, a new path `NOTE_RENAME`; in a directory, an entry created, removed or renamed is `NOTE_WRITE`, with `NOTE_LINK` for a subdirectory, and on Linux an entry moved from or to another directory adds `NOTE_EXTEND`; inotify queues the two halves of a rename one after the other, so a watcher woken by the first can read it alone, and a half read without the other is classified only after a `getdents64` on the watched directory, which takes the directory lock that `rename` holds until both halves are queued ([`do_renameat2`](https://github.com/torvalds/linux/blob/v6.8/fs/namei.c#L4975) around `vfs_rename` and its `fsnotify_move`, [`iterate_dir`](https://github.com/torvalds/linux/blob/v6.8/fs/readdir.c#L103)), an ordering taken from the kernel source, not from inotify(7), which promises none; a directory registered with `NOTE_EXTEND` on Windows is accepted, as `vfs_kqfilter` does not check it, but never receives it, because the directory handle cannot tell a move between directories from a removal plus an addition. FreeBSD's hints depend on the filesystem and the console's filesystems are not known: a growing `ftruncate` reports `NOTE_WRITE | NOTE_EXTEND` where `vop_setattr_post` reports `NOTE_ATTRIB`, and on Windows `utimes` on a file reports `NOTE_WRITE`. The three Windows notifications are told apart in the error, which names the `parent`, `entries` or `subdirectories` watch, the path it was given and `GetLastError`, but a `FindFirstChangeNotification` that fails still throws rather than returning an error to the guest: the EVFILT_VNODE attach path is [`vfs_kqfilter`](https://github.com/freebsd/freebsd-src/blob/34bd34750187bae02a0086402c78c031c59dd8b3/sys/kern/vfs_subr.c#L4936), which returns `EINVAL`, `ENOMEM` or 0 after `fget` has turned a descriptor that is not open into `EBADF`, and consults no directory permission, so no FreeBSD errno stands for a host that refuses to notify, and what the console does on such a host is not measured. Only the `parent` watch can fail this way in practice: `entries` and `subdirectories` need the `FILE_LIST_DIRECTORY` that opening the watched directory already took. The parent handle produces no hint of its own, as `RunHostWatch` only calls `FindNextChangeNotification` on it, so a later change may leave the watch on its 50 ms poll instead of throwing; that is not done here. Windows uses `FindFirstChangeNotification` on the parent directory (and on a watched directory itself) and compares `GetFileInformationByHandleEx` snapshots at least every 50 ms, because a write through an open handle is reported only when the handle is flushed or closed, so file hints can arrive 50 ms late, and a file deleted while watched, which the POSIX delete of Windows 10 1903 moves into `$Extend\$Deleted` until its last handle closes, reports `NOTE_DELETE` without `NOTE_RENAME` and keeps its handle without a parent directory watch, as that directory cannot be watched; Linux uses inotify on the file and is not run. `NOTE_REVOKE`, `NOTE_OPEN`, `NOTE_CLOSE`, `NOTE_CLOSE_WRITE`, `NOTE_READ`, unknown bits and descriptors other than files, directories, pipes and sockets throw. `sceKernelAddFileEvent(eq, fd, watch, udata)` and `sceKernelDeleteFileEvent(eq, fd)` take the [OpenOrbis](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain/blob/0a1aaf9d/include/orbis/libkernel.h#L35) and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0/src/SharpProspero/Interop/Kernel/KernelEqueue.cs#L164) signatures; `watch` is taken as the `fflags` and the event is added with `EV_CLEAR`, neither confirmed on a title (feat(libkernel): implement kevent EV_DISABLE, EV_DISPATCH and EV_FORCEONESHOT) (fix(libkernel): follow kqueue_register for EV_FORCEONESHOT and modify flags) - [sem_timedwait](../../../core/libs/prx/libkernel/Pthread/Posix/Sem.cpp) (libkernel) consumes an available count before checking the timeout and waits indefinitely for a null timeout, following FreeBSD 11's `sem_timedwait`. Console behavior for null and invalid timeouts when a count is available is unverified. - [tcsendbreak, tcdrain, tcflush, tcflow, tcgetpgrp, tcsetpgrp](../../../core/libs/prx/libkernel/File/src/Terminal.cpp) (libkernel) follow FreeBSD 11.4's [`termios.c`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/gen/termios.c): `tcflush` answers `EINVAL` for a queue other than `TCIFLUSH`, `TCOFLUSH` or `TCIOFLUSH` and `tcflow` for an action outside `TCOOFF`..`TCION` before the descriptor is looked at, and every other call fails as its terminal ioctl does on a descriptor that is not a terminal, with the errors `tcgetattr` gives, so `tcsendbreak` never reaches its 0.4 s pause. FreeBSD's `pipe_ioctl` answers `TIOCGPGRP` and `TIOCSPGRP` on a pipe or FIFO with the descriptor's SIGIO owner, which is not modelled, so `tcgetpgrp` and `tcsetpgrp` throw on a host pipe or FIFO above descriptor 2; descriptors 0 to 2 are taken as non-terminals that are not pipes, whatever the host attaches to them, so a pipe the guest creates on a freed descriptor 0 to 2 gets `ENOTTY` there. The PS5 `libkernel` is assumed to use this FreeBSD code, and `tcdrain` is not a cancellation point here - [Windows file timestamps](../../../core/libs/prx/libkernel/File/include/WindowsFileTime.hpp) use the host filesystem's timestamp range and resolution. `utimes`, `futimes`, and `lutimes` reject timestamps outside nonnegative signed 64-bit Windows file-time ticks with `EINVAL`, including exactly 1601-01-01 00:00:00 UTC, whose zero value means "leave unchanged" to `SetFileTime`. Guest directory descriptors still resolve through their recorded path. Filesystems with coarser timestamp resolution cannot retain every guest microsecond From 72d6c9e16bc14c3bafcbe875852de8a1c3ac5fba Mon Sep 17 00:00:00 2001 From: mrcrapto <197606652+mrcrapto@users.noreply.github.com> Date: Sun, 11 Oct 2026 10:05:29 +0400 Subject: [PATCH 0222/1000] fix(agc): discard recorder batches after submission failure --- .../Graphics/include/Recorder.hpp | 2 + .../libSceAgcDriver/Graphics/src/Recorder.cpp | 44 +++++++-- .../tests/RecorderKeptBytes.cpp | 96 ++++++++++++++++++- 3 files changed, 131 insertions(+), 11 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp index 18e72f40da..f046abef5c 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp @@ -750,6 +750,8 @@ private: bool syncThroughUnlocked(std::uint64_t address, std::uint64_t end, int source, const void* site); // `source` is the CountSync source the wait is attributed to. void finish(std::unique_ptr batch, bool wait, int source, bool releaseNow = false); + void discard(std::unique_ptr batch) noexcept; + void removeLabels(Batch& batch); void release(Batch& batch) noexcept; static bool overlaps(const Batch& batch, std::uint64_t address, std::uint64_t end); static std::optional readOverlap(const Batch& batch, std::uint64_t address, std::uint64_t end); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp index 74fbf99462..68211f5a11 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp @@ -3293,6 +3293,13 @@ void Recorder::Submit() { for (const auto& [begin, end] : notedReads(*open)) open->readGeneration = std::max(open->readGeneration, GuestMemory::CollectWrites(begin, end - begin)); } auto batch = std::move(open); + struct DiscardUnsubmitted { + Recorder& recorder; + std::unique_ptr& batch; + ~DiscardUnsubmitted() { + if (batch != nullptr && !batch->submitted) recorder.discard(std::move(batch)); + } + } discardUnsubmitted{*this, batch}; Check(function(endCommandBuffer, "vkEndCommandBuffer")(batch->commands), "vkEndCommandBuffer recorder"); VkSubmitInfo submission{VK_STRUCTURE_TYPE_SUBMIT_INFO}; submission.commandBufferCount = 1; @@ -3309,7 +3316,12 @@ void Recorder::Submit() { submission.pSignalSemaphores = &timeline; } const auto submitStart = std::chrono::steady_clock::now(); - Check(function(queueSubmit, "vkQueueSubmit")(context.queue, 1, &submission, batch->fence), "vkQueueSubmit recorder"); + const auto result = function(queueSubmit, "vkQueueSubmit")(context.queue, 1, &submission, batch->fence); + if (result == VK_ERROR_DEVICE_LOST) { + const auto idle = context.Function("vkDeviceWaitIdle")(context.device); + if (idle != VK_SUCCESS && idle != VK_ERROR_DEVICE_LOST) std::terminate(); + } + Check(result, "vkQueueSubmit recorder"); batch->submitted = true; batch->serial = ++submissions; batch->submittedAt = std::chrono::steady_clock::now(); @@ -3838,18 +3850,34 @@ void Recorder::finish(std::unique_ptr batch, bool wait, int source, bool } // The batch's label entries leave the table (a later label to the same dword already replaced // its entry and belongs to another batch). Correctness never depended on this removal. - if (!batch->labelDwords.empty() || !batch->wideLabels.empty()) { + removeLabels(*batch); + release(*batch); +} + +void Recorder::discard(std::unique_ptr batch) noexcept { + removeLabels(*batch); + if (batch->completionLabelCount != 0) completionLabels.fetch_sub(batch->completionLabelCount, std::memory_order_acq_rel); + if (batch->writeBackCompletionCount != 0) writeBackCompletions.fetch_sub(batch->writeBackCompletionCount, std::memory_order_acq_rel); + if (activeRecorder == this) pendingLabelSince.store(NoPendingLabel, std::memory_order_release); + release(*batch); + try { + publishPendingWrites(); + } catch (...) { + } +} + +void Recorder::removeLabels(Batch& batch) { + if (!batch.labelDwords.empty() || !batch.wideLabels.empty()) { std::lock_guard tableLock(labelTableMutex); - for (const auto dword : batch->labelDwords) { + for (const auto dword : batch.labelDwords) { const auto found = labels.find(dword); - if (found != labels.end() && found->second.batch == batch.get()) labels.erase(found); + if (found != labels.end() && found->second.batch == &batch) labels.erase(found); } - for (const auto entry : batch->wideLabels) wideLabels.erase(entry); + for (const auto entry : batch.wideLabels) wideLabels.erase(entry); recordedLabels.store(labels.size(), std::memory_order_relaxed); } - batch->labelDwords.clear(); - batch->wideLabels.clear(); - release(*batch); + batch.labelDwords.clear(); + batch.wideLabels.clear(); } void Recorder::release(Batch& batch) noexcept { diff --git a/core/libs/prx/libSceAgcDriver/tests/RecorderKeptBytes.cpp b/core/libs/prx/libSceAgcDriver/tests/RecorderKeptBytes.cpp index 4997d6be4d..1158bc94a3 100644 --- a/core/libs/prx/libSceAgcDriver/tests/RecorderKeptBytes.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/RecorderKeptBytes.cpp @@ -1,6 +1,7 @@ #include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include +#include #include #include #include @@ -30,16 +31,27 @@ struct MockDevice { std::map signaled; std::uint64_t submits = 0; std::uint64_t fenceWaits = 0; + std::uint64_t allocatedCommands = 0; + std::uint64_t freedCommands = 0; + VkResult endResult = VK_SUCCESS; + VkResult submitResult = VK_SUCCESS; + bool lostSubmissionPending = false; + std::uint64_t idleWaits = 0; + VkResult idleResult = VK_ERROR_DEVICE_LOST; }; MockDevice mock; VKAPI_ATTR VkResult VKAPI_CALL mockAllocateCommandBuffers(VkDevice, const VkCommandBufferAllocateInfo*, VkCommandBuffer* commands) { *commands = reinterpret_cast(mock.next++); + ++mock.allocatedCommands; return VK_SUCCESS; } -VKAPI_ATTR void VKAPI_CALL mockFreeCommandBuffers(VkDevice, VkCommandPool, std::uint32_t, const VkCommandBuffer*) {} +VKAPI_ATTR void VKAPI_CALL mockFreeCommandBuffers(VkDevice, VkCommandPool, std::uint32_t count, const VkCommandBuffer*) { + Expect(!mock.lostSubmissionPending, "command buffers were freed before the lost submission finished"); + mock.freedCommands += count; +} VKAPI_ATTR VkResult VKAPI_CALL mockCreateFence(VkDevice, const VkFenceCreateInfo*, const VkAllocationCallbacks*, VkFence* fence) { *fence = reinterpret_cast(mock.next++); @@ -48,6 +60,7 @@ VKAPI_ATTR VkResult VKAPI_CALL mockCreateFence(VkDevice, const VkFenceCreateInfo } VKAPI_ATTR void VKAPI_CALL mockDestroyFence(VkDevice, VkFence fence, const VkAllocationCallbacks*) { + Expect(!mock.lostSubmissionPending, "a fence was destroyed before the lost submission finished"); mock.signaled.erase(fence); } @@ -62,6 +75,7 @@ VKAPI_ATTR VkResult VKAPI_CALL mockWaitForFences(VkDevice, std::uint32_t count, } VKAPI_ATTR VkResult VKAPI_CALL mockResetFences(VkDevice, std::uint32_t count, const VkFence* fences) { + Expect(!mock.lostSubmissionPending, "a fence was reset before the lost submission finished"); for (std::uint32_t i = 0; i < count; ++i) mock.signaled.at(fences[i]) = false; return VK_SUCCESS; } @@ -71,12 +85,19 @@ VKAPI_ATTR VkResult VKAPI_CALL mockBeginCommandBuffer(VkCommandBuffer, const VkC } VKAPI_ATTR VkResult VKAPI_CALL mockEndCommandBuffer(VkCommandBuffer) { - return VK_SUCCESS; + return mock.endResult; } VKAPI_ATTR VkResult VKAPI_CALL mockQueueSubmit(VkQueue, std::uint32_t, const VkSubmitInfo*, VkFence) { ++mock.submits; - return VK_SUCCESS; + if (mock.submitResult == VK_ERROR_DEVICE_LOST) mock.lostSubmissionPending = true; + return mock.submitResult; +} + +VKAPI_ATTR VkResult VKAPI_CALL mockDeviceWaitIdle(VkDevice) { + ++mock.idleWaits; + mock.lostSubmissionPending = false; + return mock.idleResult; } VKAPI_ATTR void VKAPI_CALL mockCmdUpdateBuffer(VkCommandBuffer, VkBuffer, VkDeviceSize, VkDeviceSize, const void*) {} @@ -101,6 +122,7 @@ PFN_vkVoidFunction VKAPI_CALL mockProc(VkDevice, const char* name) { {"vkBeginCommandBuffer", reinterpret_cast(mockBeginCommandBuffer)}, {"vkEndCommandBuffer", reinterpret_cast(mockEndCommandBuffer)}, {"vkQueueSubmit", reinterpret_cast(mockQueueSubmit)}, + {"vkDeviceWaitIdle", reinterpret_cast(mockDeviceWaitIdle)}, {"vkCmdUpdateBuffer", reinterpret_cast(mockCmdUpdateBuffer)}, {"vkCmdPipelineBarrier", reinterpret_cast(mockCmdPipelineBarrier)}, {"vkCmdBeginQuery", reinterpret_cast(mockCmdBeginQuery)}, @@ -216,6 +238,68 @@ void SharedOwnerCountsOncePerBatch() { recorder.Sync(); } +void FailedSubmission(VkResult endResult, VkResult submitResult, VkResult idleResult = VK_ERROR_DEVICE_LOST) { + mock = MockDevice{}; + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + alignas(8) std::array memory{}; + const auto address = reinterpret_cast(memory.data()); + bool completed = false; + bool earlierCompleted = false; + bool earlierWrittenBack = false; + { + Recorder recorder(mockContext()); + recorder.Activate(); + recorder.Keep(std::make_shared(0), 17); + recorder.NoteLabel(address, std::span(memory).first(4), 1, 0); + recorder.OnComplete([&] { earlierWrittenBack = true; }); + Expect(recorder.AfterRecordedWork([&] { earlierCompleted = true; }), "the earlier batch did not accept its completion"); + recorder.Submit(); + auto kept = std::shared_ptr(new int(1), [](int* object) { + Expect(!mock.lostSubmissionPending, "kept resources were destroyed before the lost submission finished"); + delete object; + }); + std::weak_ptr released = kept; + recorder.Keep(std::move(kept), Budget); + recorder.NoteLabel(address + 8, std::span(memory).first(4), 2, 0); + recorder.NoteLabel(address + 16, std::span(memory).first(Recorder::LabelTableBytes + 4), 3, 0); + recorder.AfterCompletions(address + 96, std::span(memory).first(4), 4, 0, false); + recorder.OnComplete([&] { completed = true; }); + Expect(recorder.AfterRecordedWork([&] { completed = true; }), "the failed batch did not accept its completion"); + mock.endResult = endResult; + mock.submitResult = submitResult; + mock.idleResult = idleResult; + bool failed = false; + try { + recorder.Submit(); + } catch (const std::exception& error) { + const auto operation = endResult != VK_SUCCESS ? "vkEndCommandBuffer recorder" : "vkQueueSubmit recorder"; + failed = std::string_view(error.what()).find(operation) != std::string_view::npos; + } + Expect(failed, "submission did not report its Vulkan failure"); + Expect(released.expired() && !completed, "a failed batch retained objects or ran its completions"); + Expect(!recorder.Recording() && recorder.Submissions() == 1 && recorder.InFlightBatches() == 1 && recorder.InFlightKeptBytes() == 17, + "the failed batch changed the earlier in-flight submission"); + Expect(recorder.PendingLabels() == 1 && recorder.PendingLabelIn(address, 4), "a failed batch retained labels or removed the earlier batch's label"); + Expect(!recorder.PendingLabelIn(address + 8, 4) && !recorder.PendingLabelIn(address + 16, Recorder::LabelTableBytes + 4), + "a failed batch retained a narrow or wide label"); + Expect(!recorder.CompletionLabelIn(address + 96, 4) && Recorder::PendingCompletionLabels() == 1 && Recorder::PendingWriteBackCompletions() == 1, + "a failed batch retained pending completions or removed earlier ones"); + Expect(!Recorder::PendingLabelSince().has_value(), "a failed batch left its label flush deadline pending"); + Expect(Recorder::SnapshotWriteOverlaps(address, 4) && !Recorder::SnapshotWriteOverlaps(address + 8, 100), + "a failed batch left stale writes in the pending snapshot or removed an earlier write"); + Expect(mock.fenceWaits == 0, "a failed submission waited for an unsignaled fence"); + Expect(mock.idleWaits == (submitResult == VK_ERROR_DEVICE_LOST ? 1u : 0u), "a failed submission did not settle device loss before releasing its resources"); + mock.lostSubmissionPending = false; + mock.endResult = mock.submitResult = VK_SUCCESS; + recorder.Sync(); + Expect(recorder.Idle() && !completed && earlierCompleted && earlierWrittenBack, + "sync ran the failed batch's completions or lost the earlier batch's completions"); + Expect(Recorder::PendingCompletionLabels() == 0 && Recorder::PendingWriteBackCompletions() == 0, + "sync left stale completion counts after submission failure"); + } + Expect(mock.signaled.empty() && mock.allocatedCommands == mock.freedCommands, "a failed submission leaked its fence or command buffer"); +} + } int main() { @@ -225,6 +309,12 @@ int main() { SyncedWithinTwiceTheBudget(); CountFollowsEveryPath(); SharedOwnerCountsOncePerBatch(); + FailedSubmission(VK_ERROR_OUT_OF_HOST_MEMORY, VK_SUCCESS); + FailedSubmission(VK_ERROR_OUT_OF_DEVICE_MEMORY, VK_SUCCESS); + FailedSubmission(VK_SUCCESS, VK_ERROR_OUT_OF_HOST_MEMORY); + FailedSubmission(VK_SUCCESS, VK_ERROR_OUT_OF_DEVICE_MEMORY); + FailedSubmission(VK_SUCCESS, VK_ERROR_DEVICE_LOST); + FailedSubmission(VK_SUCCESS, VK_ERROR_DEVICE_LOST, VK_SUCCESS); } catch (const std::exception& error) { std::fprintf(stderr, "%s\n", error.what()); return 1; From 17ca4531992aa4c2e003a4c0e2f62afb6d913aca Mon Sep 17 00:00:00 2001 From: alfalfa-star Date: Wed, 14 Oct 2026 19:44:03 +0200 Subject: [PATCH 0223/1000] perf(agc): draw an indirect rect list from its records on the GPU ClassifyIndirectDraw sent every indirect rect-list draw to the CPU path (non-vertex path), which reads the records on the CPU. When the GPU writes those records earlier in the frame, the read waits for every batch up to the producer: in Kena (PPSA01802) each such DRAW_INDIRECT waited about 15 ms, about once per frame. A rect list on the vertex path is drawn by its generated tessellation stages from three-vertex patches, with no draw-dependent constants, so the records can be consumed on the GPU like any vertex-path draw. The classifier now keeps a rect list without mesh or tessellation stages on the GPU path, and Graphics::Draw accepts it. APS5_NO_RECT_INDIRECT=1 restores the CPU path. agc_driver_rect_list_indirect submits a direct rect-list draw and the same rectangle from a DRAW_INDIRECT record and requires the same bytes, and a record without vertices to draw nothing; it passes on either path. agc_driver_draw_offset_indices requires an auto indirect rect-list record whose packet names no offset registers to be classified to the GPU. Co-Authored-By: Claude Opus 5.5 --- .../src/Driver/Draw/IndirectDraw.cpp | 4 +- .../prx/libSceAgcDriver/Graphics/src/Draw.cpp | 2 +- .../tests/execution/DrawOffsetIndices.cpp | 18 +- .../tests/execution/RectListIndirect.cpp | 204 ++++++++++++++++++ 4 files changed, 222 insertions(+), 6 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/RectListIndirect.cpp diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/IndirectDraw.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/IndirectDraw.cpp index 21d38fdea8..e52db4829b 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/IndirectDraw.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/IndirectDraw.cpp @@ -31,7 +31,9 @@ std::optional Driver::ClassifyIndirectDraw(const Sha constexpr std::uint32_t NoLocation = 0x280u; const bool meshPath = graphics.stages.mesh.has_value() && !graphics.stages.tessellation && !graphics.rectList; const bool meshGpuSide = meshPath && drawParameters.indexed && indirect.count == 1 && !indirect.countIndirect && indirect.baseVertexLocation == NoLocation && indirect.startInstanceLocation == NoLocation && (!indirect.drawIndexEnabled || indirect.drawIndexLocation == NoLocation); - if ((graphics.stages.path != Graphics::ShaderPath::Vertex || graphics.rectList) && !meshGpuSide) fail(Path::NonVertexPath); + static const bool rectListOnGpu = std::getenv("APS5_NO_RECT_INDIRECT") == nullptr; + const bool rectListGpuSide = rectListOnGpu && graphics.rectList && !graphics.stages.mesh && !graphics.stages.tessellation; + if ((graphics.stages.path != Graphics::ShaderPath::Vertex || (graphics.rectList && !rectListGpuSide)) && !meshGpuSide) fail(Path::NonVertexPath); if (indirect.drawIndexEnabled && indirect.drawIndexSgpr >= 0) fail(Path::DrawIndex); if (indirect.countIndirect && !support.count) fail(Path::FeatureGap); if (indirect.countIndirect && indirect.count > 1 && !support.multi) fail(Path::FeatureGap); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index f38a09b934..d7d4031790 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -1022,7 +1022,7 @@ DrawInputs prepareDrawInputs(const Context& context, const State& state, const P inputs.nothing = true; return inputs; } - } else Require((!state.stages.mesh || draw.indexed) && !state.stages.tessellation && !state.rectList, "indirect draw on a non-vertex path must be resolved by the driver"); + } else Require((!state.stages.mesh || draw.indexed) && !state.stages.tessellation, "indirect draw on a non-vertex path must be resolved by the driver"); inputs.indexBytes = static_cast(draw.indexCount) * draw.indexSize; const auto indexBytes = inputs.indexBytes; APS5_LOG_OUT_DEBUG("Index buffer bytes=%llu", static_cast(indexBytes)); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DrawOffsetIndices.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DrawOffsetIndices.cpp index 6754f0c84d..0fa8c4ebcd 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DrawOffsetIndices.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DrawOffsetIndices.cpp @@ -262,14 +262,15 @@ public: return result; } - AgcDriver::Graphics::State State() const { + AgcDriver::Graphics::State State(bool rectList = false) const { AgcDriver::Graphics::State state{}; state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, 32u, 32u, std::nullopt, std::nullopt}; state.color = {reinterpret_cast(Pixels.data()), {Width, Height}, VK_FORMAT_R8G8B8A8_UNORM, Pixels.size(), 0xe4u}; state.colors = {state.color}; state.hasColorTarget = true; state.renderExtent = {Width, Height}; - state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.topology = rectList ? VK_PRIMITIVE_TOPOLOGY_PATCH_LIST : VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.rectList = rectList; state.viewport = {0.0f, static_cast(Height), static_cast(Width), -static_cast(Height), 0.0f, 1.0f}; state.scissor = {{0, 0}, {Width, Height}}; state.cullMode = VK_CULL_MODE_NONE; @@ -346,8 +347,8 @@ std::string PathName(const std::optional& path) { return path ? "the CPU path (" + std::string(AgcDriver::Graphics::IndirectDrawPathName(*path)) + ")" : std::string("the GPU"); } -std::string Classified(Fixture& fixture, const std::string& what, const ShaderRecompiler::RecompileResult& vertex, const std::vector& userData, AgcDriver::Pm4::DrawParameters& draw, std::optional expectedPath, Rule expectedRule) { - const auto path = AgcDriver::DriverDetail::Driver::ClassifyIndirectDraw(vertex, fixture.State(), Program(userData), fixture.device, draw, false); +std::string Classified(Fixture& fixture, const std::string& what, const ShaderRecompiler::RecompileResult& vertex, const std::vector& userData, AgcDriver::Pm4::DrawParameters& draw, std::optional expectedPath, Rule expectedRule, bool rectList = false) { + const auto path = AgcDriver::DriverDetail::Driver::ClassifyIndirectDraw(vertex, fixture.State(rectList), Program(userData), fixture.device, draw, false); const auto& indirect = *draw.indirect; const bool rules = expectedPath || (indirect.vertexRule == expectedRule && indirect.instanceRule == expectedRule); std::cout << what << ": classified to " << PathName(path) << '\n'; @@ -475,6 +476,15 @@ std::string Run(Fixture& fixture) { fixture.Draw(vertex, draw); failures += fixture.Check(what + ", drawn on the GPU", bufferIndices, 0x51u, 0x62u, true); } + { + const std::string what = "auto indirect rect-list record whose packet names no register for its offsets, user data s16 0x51, s17 0x62"; + const std::array record{Vertices, Instances, 300, 7}; + std::copy(record.begin(), record.end(), fixture.Records() + 32); + const auto vertex = fixture.Vertex(VertexCode, staleUserData); + AgcDriver::Pm4::DrawParameters draw{0, 0, 0, 0, 0, false, 0, 0}; + draw.indirect = AgcDriver::Pm4::DrawParameters::IndirectDraw{reinterpret_cast(fixture.Records() + 32), 0x24u, 16u, 16u, 1u, false, 0u, NoLocation, NoLocation, NoLocation, false, 0u}; + failures += Classified(fixture, what, vertex, staleUserData, draw, std::nullopt, Rule::Constant, true); + } } return failures; } diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/RectListIndirect.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/RectListIndirect.cpp new file mode 100644 index 0000000000..aa000b4764 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/RectListIndirect.cpp @@ -0,0 +1,204 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" +#include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; + +constexpr std::byte Kept{0x40}; + +alignas(256) constexpr std::array VertexCode{ + 0x7e000d05u, 0x080200f4u, 0x10020300u, 0x100202f4u, 0x060202f3u, 0x060400f3u, 0x10040500u, 0x060404f3u, + 0x7e060208u, 0x7e0802f2u, 0x7e0a02ffu, 0x44800000u, 0xf80000d1u, 0x00000005u, 0xf80008cfu, 0x04030201u, 0xbf810000u, 0xbf810000u, 0xbf810000u, 0xbf810000u, +}; +alignas(256) constexpr std::array SlotVertexCode{ + 0x7e000d05u, 0x100c00ffu, 0x3eaaaaabu, 0x7e0c4906u, 0x100e0cffu, 0x40400000u, 0x08000f00u, + 0x080200f4u, 0x10020300u, 0x100202f4u, 0x060202f3u, 0x060400f3u, 0x10040500u, 0x060404f3u, + 0x7e120d08u, 0x101212f4u, 0x060c0d09u, 0x100c0cf0u, 0x100202ffu, 0x3e000000u, 0x06020306u, 0x060202ffu, 0xbf600000u, + 0x7e060208u, 0x7e0802f2u, 0x7e0a02ffu, 0x44800000u, 0xf80000d1u, 0x00000005u, 0xf80008cfu, 0x04030201u, 0xbf810000u, 0xbf810000u, 0xbf810000u, 0xbf810000u, 0xbf810000u, +}; +alignas(256) constexpr std::array PixelCode{0x7e000200u, 0x7e020280u, 0xf800180fu, 0x00010100u, 0xbf810000u, 0xbf810000u, 0xbf810000u, 0xbf810000u}; +alignas(256) std::array DirectMemory{}; +alignas(256) std::array IndirectMemory{}; +alignas(256) std::array EmptyMemory{}; +alignas(256) std::array DirectInstancedMemory{}; +alignas(256) std::array IndirectInstancedMemory{}; +alignas(256) std::array DirectIndexedMemory{}; +alignas(256) std::array IndirectIndexedMemory{}; +alignas(256) std::array Records{3, 1, 0, 0, 0, 1, 0, 0, 6, 2, 0, 0, 6, 1, 0, 0, 0, 0, 0, 0}; +alignas(256) std::array Indices{6, 7, 8, 3, 4, 5, 0, 0}; + +struct Header { + Shader shader{}; + ShaderUserData userData{}; +}; + +alignas(8) Header VertexHeader{}; +alignas(8) Header SlotVertexHeader{}; +alignas(8) Header PixelHeader{}; + +void Registered(Header& header, const std::uint32_t* code, std::size_t bytes, std::uint8_t type) { + header.shader.file_header = 0x34333231u; + header.shader.version = 0x18u; + header.shader.header_size = sizeof(Header); + header.shader.shader_size = static_cast(bytes); + header.shader.code = code; + header.shader.type = type; + header.shader.user_data = &header.userData; + AgcDriverRegisterShader_nid_postfix(&header.shader); +} + +void Append(std::vector& commands, std::uint32_t opcode, std::initializer_list payload) { + commands.push_back(0xc0000000u | (static_cast(payload.size() - 1) << 16u) | (opcode << 8u)); + commands.insert(commands.end(), payload); +} + +std::vector DrawRectangles() { + const auto vertex = reinterpret_cast(VertexCode.data()); + const auto slotVertex = reinterpret_cast(SlotVertexCode.data()); + const auto indices = reinterpret_cast(Indices.data()); + const auto pixel = reinterpret_cast(PixelCode.data()); + const auto records = reinterpret_cast(Records.data()); + const std::vector> context{ + {0x2d5, 0x2000}, + {0x1b6, 0}, {0x207, 0x01200000}, {0x200, 0}, {0x203, 0x800}, + {0x2dc, 0xaa00}, {0x2f8, 0}, {0x292, 2}, {0x293, 0}, + {0x80, 0}, {0x8d, 0}, {0x83, 0xffff}, {0x8c, 0xa}, + {0x2f9, 0x2d}, {0x313, 0x6000}, {0x30e, 0xffffffff}, {0x30f, 0xffffffff}, + {0x206, 0x43f}, {0x204, 0x80000}, {0x205, 0x240}, + {0x8e, 0xf}, {0x8f, 0xf}, {0x202, 0xcc0010}, + {0x1c4, 0}, {0x1c5, 9}, {0x1c3, 4}, {0x31c, 0x28028}, + {0x31b, 0}, {0x31d, 0}, {0x3b0, (63u << 14u) | 3u}, + {0x3b8, 0x9000000}, {0x1e0, 0}, + {0xc, 0}, {0xd, 0x40040}, + {0x81, 0x80000000}, {0x82, 0x40040}, + {0x90, 0x80000000}, {0x91, 0x40040}, + {0x94, 0x80000000}, {0x95, 0x40040}, + {0x10f, std::bit_cast(32.0f)}, {0x110, std::bit_cast(32.0f)}, + {0x111, std::bit_cast(-2.0f)}, {0x112, std::bit_cast(2.0f)}, + {0x113, std::bit_cast(1.0f)}, {0x114, 0}, + {0xb4, 0}, {0xb5, std::bit_cast(1.0f)}, + {0x2fe, 0}, {0x302, 0}, {0x306, 0}, {0x30a, 0}, + }; + const std::vector> shader{ + {0xc8, static_cast(vertex >> 8u)}, {0xc9, static_cast(vertex >> 40u)}, {0x8b, 1u << 1u}, + {0x008, static_cast(pixel >> 8u)}, {0x009, static_cast(pixel >> 40u)}, {0x00b, 1u << 1u}, {0x00c, std::bit_cast(1.0f)}, + }; + std::vector commands; + Append(commands, 0x79, {0x242, 17}); + Append(commands, 0x79, {0x24a, 0}); + for (const auto& [offset, value] : context) Append(commands, 0x69, {offset, value}); + for (const auto& [offset, value] : shader) Append(commands, 0x76, {offset, value}); + Append(commands, 0x11, {1, static_cast(records), static_cast(records >> 32u)}); + const auto target = [&](std::array& memory) { + const auto color = reinterpret_cast(memory.data()); + Append(commands, 0x69, {0x318, static_cast(color >> 8u)}); + Append(commands, 0x69, {0x390, static_cast(color >> 40u)}); + }; + target(DirectMemory); + Append(commands, 0x2d, {3, 2}); + target(IndirectMemory); + Append(commands, 0x24, {0, 0, 0, 2}); + target(EmptyMemory); + Append(commands, 0x24, {16, 0, 0, 2}); + Append(commands, 0x76, {0xc8, static_cast(slotVertex >> 8u)}); + Append(commands, 0x76, {0xc9, static_cast(slotVertex >> 40u)}); + target(DirectInstancedMemory); + Append(commands, 0x2f, {2}); + Append(commands, 0x2d, {6, 2}); + Append(commands, 0x2f, {1}); + target(IndirectInstancedMemory); + Append(commands, 0x24, {32, 0, 0, 2}); + Append(commands, 0x2a, {0}); + target(DirectIndexedMemory); + Append(commands, 0x27, {6, static_cast(indices), static_cast(indices >> 32u), 6, 0}); + Append(commands, 0x26, {static_cast(indices), static_cast(indices >> 32u)}); + Append(commands, 0x13, {6}); + target(IndirectIndexedMemory); + Append(commands, 0x25, {48, 0, 0, 0}); + return commands; +} + +std::size_t Covered(const std::array& memory) { + return static_cast(std::count_if(memory.begin(), memory.end(), [](std::byte value) { return value != Kept; })); +} + +std::string Slots(const std::array& memory) { + std::string slots; + for (std::size_t slot = 0; slot < 4; ++slot) { + std::size_t covered = 0; + for (std::size_t row = 0; row < 4; ++row) { + for (std::size_t x = slot * 16; x < slot * 16 + 16; ++x) { + const auto pixel = (row * 64 + x) * 4; + covered += std::any_of(memory.begin() + static_cast(pixel), memory.begin() + static_cast(pixel + 4), [](std::byte value) { return value != Kept; }) ? 1u : 0u; + } + } + if (!slots.empty()) slots += ' '; + slots += std::to_string(covered); + } + return slots; +} + +void Same(const std::array& indirect, const std::array& direct, const std::string& what) { + for (std::size_t index = 0; index < direct.size(); ++index) { + Require(indirect[index] == direct[index], "byte " + std::to_string(index) + " of the indirect " + what + " is " + std::to_string(static_cast(indirect[index])) + ", the direct draw wrote " + std::to_string(static_cast(direct[index]))); + } +} + +void IndirectRectListsDrawAsDirectOnes() { + DirectMemory.fill(Kept); + IndirectMemory.fill(Kept); + EmptyMemory.fill(Kept); + DirectInstancedMemory.fill(Kept); + IndirectInstancedMemory.fill(Kept); + DirectIndexedMemory.fill(Kept); + IndirectIndexedMemory.fill(Kept); + Registered(VertexHeader, VertexCode.data(), sizeof(VertexCode), 2); + Registered(SlotVertexHeader, SlotVertexCode.data(), sizeof(SlotVertexCode), 2); + Registered(PixelHeader, PixelCode.data(), sizeof(PixelCode), 1); + auto commands = DrawRectangles(); + Packet packet{commands.data(), static_cast(commands.size()), 0, {}}; + Require(sceAgcDriverSubmitDcb(&packet) == 0, "the draw submission failed"); + AgcDriverWaitIdle_nid_postfix(); + const auto direct = Covered(DirectMemory); + Require(direct != 0, "the direct rect-list draw covered nothing"); + Same(IndirectMemory, DirectMemory, "rect-list draw"); + const auto empty = Covered(EmptyMemory); + Require(empty == 0, "the indirect rect-list draw whose record has no vertices covered " + std::to_string(empty) + " bytes, expected none"); + Require(Slots(DirectInstancedMemory) == "32 32 32 32", "the direct rect-list draw of 6 vertices and 2 instances covered pixels " + Slots(DirectInstancedMemory) + " of its four rectangles, expected 32 each"); + Same(IndirectInstancedMemory, DirectInstancedMemory, "rect-list draw of 6 vertices and 2 instances"); + Require(Slots(DirectIndexedMemory) == "0 32 32 0", "the direct indexed rect-list draw covered pixels " + Slots(DirectIndexedMemory) + " of the four rectangles, expected only the second and third"); + Same(IndirectIndexedMemory, DirectIndexedMemory, "indexed rect-list draw"); +} + +} + +int main() { + try { + { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + } + IndirectRectListsDrawAsDirectOnes(); + AgcDriverShutdown_nid_postfix(); + std::puts("indirect rect-list draws drew what the direct draw did"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + AgcDriverShutdown_nid_postfix(); + return 1; + } +} From e088fd8741d21410ce27f7f04995f88fe3202297 Mon Sep 17 00:00:00 2001 From: fogydes Date: Thu, 15 Oct 2026 11:48:51 +0530 Subject: [PATCH 0224/1000] fix(libScePlayGo): let users answer unlisted chunks --- core/libs/prx/libScePlayGo/Export.cpp | 22 ++++++++++- core/libs/tests/GuestPlayGo.cpp | 55 ++++++++++++++++++++++++++- docs/dev/debt/libScePlayGo.md | 2 +- 3 files changed, 76 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libScePlayGo/Export.cpp b/core/libs/prx/libScePlayGo/Export.cpp index 38d6a3621a..2bc5bfc67c 100644 --- a/core/libs/prx/libScePlayGo/Export.cpp +++ b/core/libs/prx/libScePlayGo/Export.cpp @@ -2,6 +2,7 @@ #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include #include #include #include @@ -86,11 +87,30 @@ static const ChunkTable& Chunks() { return chunks; } +enum class UnlistedChunkAnswer { Stop, Installed, Absent }; + +static UnlistedChunkAnswer UnlistedChunks() { + static const UnlistedChunkAnswer answer = [] { + const char* text = std::getenv("APS5_PLAYGO_UNLISTED_CHUNKS"); + if (text == nullptr || text[0] == '\0') return UnlistedChunkAnswer::Stop; + const std::string value(text); + if (value == "installed") return UnlistedChunkAnswer::Installed; + if (value == "absent") return UnlistedChunkAnswer::Absent; + throw std::runtime_error("scePlayGo: APS5_PLAYGO_UNLISTED_CHUNKS must be installed or absent, not \"" + value + "\""); + }(); + return answer; +} + static bool IsKnownChunk(uint16_t id) { const auto& chunks = Chunks(); if (chunks.defined) return chunks.ids.contains(id); if (chunks.scenario || id == 0) return true; - throw std::runtime_error("scePlayGo: sce_sys/playgo-chunk.dat and sce_sys/playgo-scenario.json are missing, so chunk ID " + std::to_string(id) + " cannot be checked"); + switch (UnlistedChunks()) { + case UnlistedChunkAnswer::Installed: return true; + case UnlistedChunkAnswer::Absent: return false; + case UnlistedChunkAnswer::Stop: break; + } + throw std::runtime_error("scePlayGo: sce_sys/playgo-chunk.dat and sce_sys/playgo-scenario.json are missing, so chunk ID " + std::to_string(id) + " cannot be checked; set APS5_PLAYGO_UNLISTED_CHUNKS=installed or =absent to answer for this dump"); } static int ValidateChunks(const uint16_t* chunk_ids, uint32_t number_of_entries) { diff --git a/core/libs/tests/GuestPlayGo.cpp b/core/libs/tests/GuestPlayGo.cpp index 77b345b58c..0d24636d4b 100644 --- a/core/libs/tests/GuestPlayGo.cpp +++ b/core/libs/tests/GuestPlayGo.cpp @@ -35,7 +35,8 @@ static void RequireMissingChunkMetadata(Operation&& operation) { const std::string_view message(error.what()); threw = message.find("playgo-chunk.dat") != std::string_view::npos && message.find("playgo-scenario.json") != std::string_view::npos && - message.find("chunk ID") != std::string_view::npos; + message.find("chunk ID") != std::string_view::npos && + message.find("APS5_PLAYGO_UNLISTED_CHUNKS") != std::string_view::npos; } Require(threw); } @@ -149,6 +150,52 @@ static int CheckRejectedLayout(const char* root, const char* fileName) { return 0; } +static void SetUnlistedChunks(const char* value) { +#ifdef _WIN32 + Require(_putenv_s("APS5_PLAYGO_UNLISTED_CHUNKS", value) == 0); +#else + Require(setenv("APS5_PLAYGO_UNLISTED_CHUNKS", value, 1) == 0); +#endif +} + +static int CheckNoPlayGoFileAnswered(const char* root, const char* answer) { + constexpr int badChunkId = static_cast(0x80B2000C); + SetUnlistedChunks(answer); + std::filesystem::current_path(root); + PlayGoInitParams init{}; + int handle = 0; + Require(scePlayGoInitialize(&init) == 0); + Require(scePlayGoOpen(&handle, nullptr) == 0); + const std::uint16_t unlisted[3] = {0, 1, 999}; + std::int8_t loci[3] = {0, 0, 0}; + const int result = scePlayGoGetLocus(handle, unlisted, 3, loci); + if (std::string_view(answer) == "installed") Require(result == 0 && loci[0] == 3 && loci[1] == 3 && loci[2] == 3); + else Require(result == badChunkId); + const std::uint16_t first = 0; + Require(scePlayGoGetLocus(handle, &first, 1, loci) == 0 && loci[0] == 3); + Require(scePlayGoClose(handle) == 0); + return 0; +} + +static int CheckUnknownUnlistedChunkAnswer(const char* root) { + SetUnlistedChunks("maybe"); + std::filesystem::current_path(root); + PlayGoInitParams init{}; + int handle = 0; + Require(scePlayGoInitialize(&init) == 0); + Require(scePlayGoOpen(&handle, nullptr) == 0); + const std::uint16_t unlisted = 1; + std::int8_t locus = 0; + bool threw = false; + try { + scePlayGoGetLocus(handle, &unlisted, 1, &locus); + } catch (const std::runtime_error& error) { + threw = std::string_view(error.what()).find("APS5_PLAYGO_UNLISTED_CHUNKS must be installed or absent") != std::string_view::npos; + } + Require(threw); + return 0; +} + static void RunLayout(const char* self, const char* mode, std::initializer_list> files) { const auto root = std::filesystem::temp_directory_path() / ("guest_playgo_" + std::to_string(std::random_device{}())); std::filesystem::remove_all(root); @@ -176,6 +223,9 @@ int main(int argc, char** argv) { if (argc == 3) { const std::string_view mode = argv[1]; if (mode == "--no-file") return CheckNoPlayGoFile(argv[2]); + if (mode == "--no-file-installed") return CheckNoPlayGoFileAnswered(argv[2], "installed"); + if (mode == "--no-file-absent") return CheckNoPlayGoFileAnswered(argv[2], "absent"); + if (mode == "--no-file-unknown-answer") return CheckUnknownUnlistedChunkAnswer(argv[2]); if (mode == "--package-chunk-table" || mode == "--scenario-with-chunk-table") return CheckPackageChunkTable(argv[2]); if (mode == "--scenario-only") return CheckScenarioWithoutChunkTable(argv[2]); if (mode == "--ficm-only") return CheckRejectedLayout(argv[2], "playgo-ficm.dat"); @@ -183,6 +233,9 @@ int main(int argc, char** argv) { if (mode == "--invalid-chunk-table") return CheckRejectedLayout(argv[2], "playgo-chunk.dat"); } RunLayout(argv[0], "--no-file", {}); + RunLayout(argv[0], "--no-file-installed", {}); + RunLayout(argv[0], "--no-file-absent", {}); + RunLayout(argv[0], "--no-file-unknown-answer", {}); RunLayout(argv[0], "--package-chunk-table", {{"playgo-chunk.dat", ChunkTableFile()}}); RunLayout(argv[0], "--scenario-with-chunk-table", {{"playgo-chunk.dat", ChunkTableFile()}, {"playgo-scenario.json", "{}"}}); RunLayout(argv[0], "--scenario-only", {{"playgo-scenario.json", "{}"}}); diff --git a/docs/dev/debt/libScePlayGo.md b/docs/dev/debt/libScePlayGo.md index b7dcfacd48..ef94e4f939 100644 --- a/docs/dev/debt/libScePlayGo.md +++ b/docs/dev/debt/libScePlayGo.md @@ -2,5 +2,5 @@ ### Unknown function info -- [libScePlayGo](../../../core/libs/prx/libScePlayGo/Export.cpp) - the chunk set comes from `playgo-chunkdefs.xml` (every listed chunk plus chunks 0 through the default chunk), otherwise from `sce_sys/playgo-chunk.dat`, whose `plgx` header is read as holding the chunk count as a 16-bit value at offset 0x0A and ids 0 to count - 1 are taken as valid. The field was identified on four dumps, where exactly that many 0x20-byte entries follow from offset 0x100 (RIDE 4 PPSA01599: 35, Persona 5 Royal PPSA05110: 25, LEGO Horizon Adventures PPSA14632 and Kena PPSA01802: 1); the rest of the format is not decoded and ids are assumed to be contiguous. Every listed chunk is reported as installed (`LOCAL_FAST`). Without either file, a package with `sce_sys/playgo-scenario.json` (PPSA01748, whose dump lists no chunks) accepts every chunk id as installed while `scePlayGoGetChunkId` lists only chunk 0. A package with no `playgo-*` file in `sce_sys` (PPSA03001, PPSA03490 and PPSA01285 here) accepts chunk 0; querying any other id throws with a message naming the missing `playgo-chunk.dat` and `playgo-scenario.json`, since the incomplete dump cannot establish whether that id exists. A `sce_sys` that holds other `playgo-*` files (`playgo-ficm.dat`, `playgo-hash-table.dat`) without `playgo-chunk.dat` or `playgo-scenario.json`, or a `playgo-chunk.dat` without a `plgx` header and a nonzero count, throws naming the file, because no such dump is known. These answers are unverified on a console, as is the console's answer for an id outside a package's chunk table. +- [libScePlayGo](../../../core/libs/prx/libScePlayGo/Export.cpp) - the chunk set comes from `playgo-chunkdefs.xml` (every listed chunk plus chunks 0 through the default chunk), otherwise from `sce_sys/playgo-chunk.dat`, whose `plgx` header is read as holding the chunk count as a 16-bit value at offset 0x0A and ids 0 to count - 1 are taken as valid. The field was identified on four dumps, where exactly that many 0x20-byte entries follow from offset 0x100 (RIDE 4 PPSA01599: 35, Persona 5 Royal PPSA05110: 25, LEGO Horizon Adventures PPSA14632 and Kena PPSA01802: 1); the rest of the format is not decoded and ids are assumed to be contiguous. Every listed chunk is reported as installed (`LOCAL_FAST`). Without either file, a package with `sce_sys/playgo-scenario.json` (PPSA01748, whose dump lists no chunks) accepts every chunk id as installed while `scePlayGoGetChunkId` lists only chunk 0. A package with no `playgo-*` file in `sce_sys` (PPSA03001, PPSA03490 and PPSA01285 here) accepts chunk 0; querying any other id throws with a message naming the missing `playgo-chunk.dat` and `playgo-scenario.json`, since the incomplete dump cannot establish whether that id exists; `APS5_PLAYGO_UNLISTED_CHUNKS=installed` answers every such id as installed (`LOCAL_FAST`, which PPSA15388 needs to load its saves) and `=absent` answers `SCE_PLAYGO_ERROR_BAD_CHUNK_ID` (which ends PPSA03001's id walk), as the user's choice for that dump. A `sce_sys` that holds other `playgo-*` files (`playgo-ficm.dat`, `playgo-hash-table.dat`) without `playgo-chunk.dat` or `playgo-scenario.json`, or a `playgo-chunk.dat` without a `plgx` header and a nonzero count, throws naming the file, because no such dump is known. These answers are unverified on a console, as is the console's answer for an id outside a package's chunk table. - [libScePlayGo](../../../core/libs/prx/libScePlayGo/Export.cpp) - `scePlayGoGetSupportedOptionalChunk` and `scePlayGoGetOptionalChunk` report every bit set for any `type`, i.e. every optional chunk (language pack) supported and installed, like `scePlayGoGetLanguageMask`. The bit layout and the values of `type` are not known; PPSA01748 calls both with type 0 and hides every text language listed in `playgo-scenario.json` `chunkSupportedLanguages` when the bitmask is 0. The console's answer for a package that ships no optional chunks is unverified. From f9f6872b8d293ee0353e2f2dd95c49840da209ad Mon Sep 17 00:00:00 2001 From: ArjenS-LA Date: Wed, 14 Oct 2026 23:38:52 -0700 Subject: [PATCH 0225/1000] fix(tools): audit ordinary homebrew import names --- docs/user/USAGE.md | 2 ++ tools/import_audit.py | 6 ++---- tools/tests/test_import_audit.py | 31 ++++++++++++++++++++++++++++--- 3 files changed, 32 insertions(+), 7 deletions(-) diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index b448949fe2..c38899b148 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -237,6 +237,8 @@ The report also lists needed libraries that have no file in `--libs` or `--modul | `--names ` | NID and name per line, as in `aerolib.csv` from [`tools/nid_names.py`](../../tools/nid_names.py); names the absent imports. Never downloaded; a name that does not hash to its NID is marked. | | `--json ` | Full result, stamped with the repository's commit and whether its tree was modified. | +Registry `nid` fields may contain either encoded NIDs or ordinary ELF symbol names such as `strcmp`. Ordinary names are matched against literal exports, without hashing them into NIDs. + Exit codes: `0` nothing blocks loading, `1` there are `absent` imports or needed libraries without a file, `2` an input could not be read; the message names the file and the value. A module registry lists `nid`, `library` and `targetOffset` for each relocation against a function that no other bundled module exports, without the `#` suffix of the executable's NIDs. diff --git a/tools/import_audit.py b/tools/import_audit.py index bc819fc9bf..4059b91a3a 100644 --- a/tools/import_audit.py +++ b/tools/import_audit.py @@ -1,7 +1,6 @@ import argparse import hashlib import json -import re import struct import subprocess import sys @@ -14,7 +13,6 @@ import progress ROOT = Path(__file__).resolve().parent.parent PRX = ROOT / "core" / "libs" / "prx" -NID = re.compile(r"[A-Za-z0-9+\-]{11}") NID_POSTFIX = "_nid_postfix" MODULE_SUFFIX = "#" CLASSES = ("implemented", "stub", "absent", "module") @@ -409,8 +407,8 @@ def read_registry(path): if not isinstance(entry, dict) or not isinstance(entry.get("nid"), str) or not isinstance(entry.get("library"), str): raise AuditError(f"{path}: entry {index} needs string fields 'nid' and 'library': {entry!r}") nid = entry["nid"].split("#", 1)[0] - if not NID.fullmatch(nid): - raise AuditError(f"{path}: entry {index} has an invalid NID {entry['nid']!r}") + if not nid or any(character.isspace() or ord(character) < 32 or ord(character) == 127 for character in nid): + raise AuditError(f"{path}: entry {index} has an invalid import symbol {entry['nid']!r}") imports.append((nid, entry["library"], bool(entry.get("weak", False)))) return imports diff --git a/tools/tests/test_import_audit.py b/tools/tests/test_import_audit.py index 2b6bf042f6..993cf3a107 100644 --- a/tools/tests/test_import_audit.py +++ b/tools/tests/test_import_audit.py @@ -178,6 +178,31 @@ class ImportAuditTests(unittest.TestCase): self.work = Workspace() self.addCleanup(self.work.close) + def test_named_imports_use_literal_exports_and_keep_unresolved_names(self): + work = self.work + (work.libs / "libc.prx").write_bytes(exporting("strcmp", "_Z3foov", "AAAAAAAAAAA")) + registry, report = work.root / "homebrew.registry.json", work.root / "audit.json" + write_registry(registry, [("strcmp", ""), ("strcmp", ""), ("_Z3foov", "libc.prx"), + ("AAAAAAAAAAA#A#B", "libc.prx"), ("missing_function", "", True)]) + code, _, err = work.run(registry, "--json", str(report)) + self.assertEqual((code, err), (1, "")) + data = json.loads(report.read_text(encoding="utf-8")) + records = {record["nid"]: record for record in data["imports"]} + self.assertEqual(data["references"], 5) + self.assertEqual(data["unique_by_class"], {"implemented": 3, "stub": 0, "absent": 1, "module": 0}) + self.assertEqual(records["strcmp"]["references"], 2) + self.assertEqual(records["strcmp"]["providers"], ["libc.prx"]) + self.assertTrue(records["missing_function"]["weak"]) + self.assertIsNone(data["stops"][0]["nid"]) + + def test_empty_and_control_character_imports_remain_invalid(self): + registry = self.work.root / "homebrew.registry.json" + for name in ("", "#A#B", "bad\x00name", "bad\nname", "bad\x7fname", "bad name"): + with self.subTest(name=name): + write_registry(registry, [(name, "")]) + with self.assertRaisesRegex(import_audit.AuditError, "invalid import symbol"): + import_audit.read_registry(registry) + def test_failed_json_write_preserves_existing_report_and_leaves_no_partial_report(self): work = self.work (work.libs / "libSceA.prx").write_bytes(exporting("AAAAAAAAAAA")) @@ -1192,7 +1217,7 @@ class ImportAuditTests(unittest.TestCase): with self.assertRaisesRegex(import_audit.AuditError, r"entry 0 needs string fields"): import_audit.read_registry(path) write_registry(path, [("AAAAAAAAAAA", ""), ("bad\x01nid", "")]) - with self.assertRaisesRegex(import_audit.AuditError, r"entry 1 has an invalid NID 'bad\\x01nid'"): + with self.assertRaisesRegex(import_audit.AuditError, r"entry 1 has an invalid import symbol 'bad\\x01nid'"): import_audit.read_registry(path) write_registry(path, [("AAAAAAAAAAA#A#B", "libSceA.prx")], encoding="utf-8-sig") self.assertEqual(import_audit.read_registry(path), [("AAAAAAAAAAA", "libSceA.prx", False)]) @@ -1226,10 +1251,10 @@ class ImportAuditTests(unittest.TestCase): work = self.work (work.libs / "libSceA.prx").write_bytes(exporting("AAAAAAAAAAA")) registry = work.root / "game.json" - write_registry(registry, [("short", "libSceA.prx")]) + write_registry(registry, [("bad name", "libSceA.prx")]) code, out, err = work.run(registry) self.assertEqual((code, out), (2, "")) - self.assertIn("game.json: entry 0 has an invalid NID 'short'", err) + self.assertIn("game.json: entry 0 has an invalid import symbol 'bad name'", err) def test_unreadable_library_and_unwritable_json_exit_two(self): work = self.work From 6a880abf2866ca025ff167deb2b023e0eaf83277 Mon Sep 17 00:00:00 2001 From: Donson Date: Mon, 12 Oct 2026 12:48:06 +0800 Subject: [PATCH 0226/1000] fix(shader): cap SPIR-V scalar replacement --- .../libSceAgcDriver/tests/SpirvOptimizer.cpp | 85 +++++++++++++++++++ .../SpirvBackend/src/SpirvOptimizer.cpp | 6 +- 2 files changed, 89 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/SpirvOptimizer.cpp b/core/libs/prx/libSceAgcDriver/tests/SpirvOptimizer.cpp index 79616003ec..695b81c582 100644 --- a/core/libs/prx/libSceAgcDriver/tests/SpirvOptimizer.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/SpirvOptimizer.cpp @@ -291,6 +291,90 @@ OpFunctionEnd } } +void testLargeFunctionArray() { + constexpr std::uint32_t elementCount = 1000; + const auto source = std::string(R"(OpCapability Shader +OpCapability StorageBuffer8BitAccess +OpExtension "SPV_KHR_8bit_storage" +OpExtension "SPV_KHR_storage_buffer_storage_class" +OpMemoryModel Logical GLSL450 +OpEntryPoint GLCompute %main "main" +OpExecutionMode %main LocalSize 1 1 1 +OpDecorate %input Block +OpMemberDecorate %input 0 Offset 0 +OpDecorate %inputBuffer DescriptorSet 0 +OpDecorate %inputBuffer Binding 0 +OpDecorate %secondInputBuffer DescriptorSet 0 +OpDecorate %secondInputBuffer Binding 1 +OpDecorate %output Block +OpMemberDecorate %output 0 Offset 0 +OpDecorate %outputBuffer DescriptorSet 0 +OpDecorate %outputBuffer Binding 2 +OpDecorate %flag Block +OpMemberDecorate %flag 0 Offset 0 +OpDecorate %flagBuffer DescriptorSet 0 +OpDecorate %flagBuffer Binding 3 +OpDecorate %array ArrayStride 4 +%void = OpTypeVoid +%bool = OpTypeBool +%uint = OpTypeInt 32 0 +%small = OpTypeInt 8 0 +%function = OpTypeFunction %void +%zero = OpConstant %uint 0 +%scope = OpConstant %uint 2 +%semantics = OpConstant %uint 264 +%length = OpConstant %uint )") + std::to_string(elementCount) + R"( +%array = OpTypeArray %uint %length +%arrayPointer = OpTypePointer Function %array +%arrayPointerStorage = OpTypePointer StorageBuffer %array +%input = OpTypeStruct %array +%inputPointer = OpTypePointer StorageBuffer %input +%flag = OpTypeStruct %uint +%flagPointer = OpTypePointer StorageBuffer %flag +%uintPointerStorage = OpTypePointer StorageBuffer %uint +%output = OpTypeStruct %array +%outputPointer = OpTypePointer StorageBuffer %output +%inputBuffer = OpVariable %inputPointer StorageBuffer +%secondInputBuffer = OpVariable %inputPointer StorageBuffer +%outputBuffer = OpVariable %outputPointer StorageBuffer +%flagBuffer = OpVariable %flagPointer StorageBuffer +%main = OpFunction %void None %function +%entry = OpLabel +%local = OpVariable %arrayPointer Function +%flagPointerValue = OpAccessChain %uintPointerStorage %flagBuffer %zero +%flagValue = OpLoad %uint %flagPointerValue +%condition = OpINotEqual %bool %flagValue %zero +OpSelectionMerge %merge None +OpBranchConditional %condition %then %else +%then = OpLabel +%inputArray = OpAccessChain %arrayPointerStorage %inputBuffer %zero +%inputValue = OpLoad %array %inputArray +OpStore %local %inputValue +OpBranch %merge +%else = OpLabel +%secondInputArray = OpAccessChain %arrayPointerStorage %secondInputBuffer %zero +%secondInputValue = OpLoad %array %secondInputArray +OpStore %local %secondInputValue +OpBranch %merge +%merge = OpLabel +OpMemoryBarrier %scope %semantics +%localValue = OpLoad %array %local +%outputArray = OpAccessChain %arrayPointerStorage %outputBuffer %zero +OpStore %outputArray %localValue +OpReturn +OpFunctionEnd +)"; + try { + const auto input = assemble(source); + const auto output = ShaderRecompiler::ValidateAndOptimizeSpirv(input, Vulkan11, Spirv13, false); + spvtools::SpirvTools validator(SPV_ENV_VULKAN_1_1); + check(validator.Validate(output), "large function array optimizer output is invalid"); + check(output.size() < input.size() * 4u, "large function array was scalarized"); + } catch (const std::exception& error) { + check(false, std::string("large function array: ") + error.what()); + } +} + void testInvalidInputIsRejected() { const auto valid = assemble(moduleSource(Types.front(), false, ArithmeticSupport::StorageOnly)); expectRejected(valid, 0x00400000u, Spirv13, "SPIR-V 1.3 with Vulkan 1.0"); @@ -457,6 +541,7 @@ int main() { const char* mode = std::getenv("APS5_SPIRV_OPT"); const bool optimizationEnabled = mode == nullptr || std::string(mode) != "none"; testNarrowConversions(optimizationEnabled); + testLargeFunctionArray(); testSpecializedSelectionExitSpirv16(); testInvalidInputIsRejected(); testSpecializedSelectionExit(); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvOptimizer.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvOptimizer.cpp index 89702e7b2b..856f8bceee 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvOptimizer.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvOptimizer.cpp @@ -10,6 +10,8 @@ namespace ShaderRecompiler { namespace { +constexpr std::uint32_t ScalarReplacementLimit = 64; + bool HasLimitedUseTypes(std::span spirv) { if (spirv.size() < 5) throw std::runtime_error("SPIRV-Tools: incomplete module header"); bool int8 = false, int16 = false, float16 = false; @@ -116,7 +118,7 @@ std::vector ValidateAndOptimizeSpirv(std::span ValidateAndOptimizeSpirv(std::span Date: Thu, 15 Oct 2026 00:35:44 -0700 Subject: [PATCH 0227/1000] fix(libkernel_sys): return 0 for AT_SYMLINK_NOFOLLOW in fchmodat --- core/libs/prx/libkernel_sys/Export.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libkernel_sys/Export.cpp b/core/libs/prx/libkernel_sys/Export.cpp index 99809d6212..875d64f81a 100644 --- a/core/libs/prx/libkernel_sys/Export.cpp +++ b/core/libs/prx/libkernel_sys/Export.cpp @@ -491,8 +491,8 @@ int APS5_VABI fchmodat_nid_postfix(int fd, const char* path, int mode, int flag) const bool symbolic = std::filesystem::is_symlink(std::filesystem::symlink_status(native, status)); #endif if (symbolic) { - NotImplemented_nid_no_patch("fchmodat: AT_SYMLINK_NOFOLLOW on a symbolic link"); - return -1; + RecordWrittenPath_nid_no_patch(native); + return 0; } } #ifdef _WIN32 From 7e34059a432b0ded2720d90fc3b75d6ededd4dae Mon Sep 17 00:00:00 2001 From: QRMOE Date: Thu, 15 Oct 2026 09:42:24 +0200 Subject: [PATCH 0228/1000] test(libc): match getcwd allocation size in application heap check --- core/libs/tests/ApplicationHeap.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/tests/ApplicationHeap.cpp b/core/libs/tests/ApplicationHeap.cpp index d7f755740f..09af512704 100644 --- a/core/libs/tests/ApplicationHeap.cpp +++ b/core/libs/tests/ApplicationHeap.cpp @@ -300,7 +300,7 @@ int main(int argc, char** argv) { require(directory == reinterpret_cast(storage.data()) && directory[0] == '/' && lastSize == std::strlen(directory) + 1); ApplicationHeapFree_nid_no_patch(directory); directory = getcwd_nid_postfix(nullptr, 200); - require(directory == reinterpret_cast(storage.data()) && lastSize == 200); + require(directory == reinterpret_cast(storage.data()) && lastSize == std::strlen(directory) + 1); ApplicationHeapFree_nid_no_patch(directory); fail = true; *__error_nid_postfix() = 0; From 982518169a03cc68910686e5c17f270e67458d93 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Thu, 15 Oct 2026 07:45:49 +0000 Subject: [PATCH 0229/1000] fix(agc): keep a build's BDA table disjoint where its read-only copy overlaps the address space A read-only V# that crosses imports of the cached address space is bound through a copy of its bytes, and that copy can begin inside an import. The BDA table listed the copy and the space's ranges by begin, and a shader takes the last range beginning at or below an address, so the bytes from the copy's begin to the end of that import resolved to the copy. The copy is read-only, so a BDA store or atomic into a writable import faulted with Permission, the GPU side of the rule owner() no longer applies on the CPU. The table now gives every byte one range: the space's ranges, or after a dissolve the direct and mirrored regions, keep the bytes they hold, and a build's copies keep only the bytes nothing live holds. --- .../Graphics/src/GuestBufferMemory.cpp | 71 ++++++++++++++++--- 1 file changed, 63 insertions(+), 8 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index d91dc92ba1..9e2a250b77 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -2577,8 +2577,6 @@ void GuestBufferMemory::dissolveSpace(bool resolve) { std::vector merged; merged.reserve(space->base.size() + regions.size()); if (prepared) { - // Both sorted and disjoint (UploadPrepare merged this build's regions; the space's are - // registered ranges), so the merge keeps the order Descriptor searches. std::merge(space->base.begin(), space->base.end(), std::make_move_iterator(regions.begin()), std::make_move_iterator(regions.end()), std::back_inserter(merged), [](const Region& left, const Region& right) { return left.begin < right.begin; }); std::uint64_t reached = 0; for (const auto& region : merged) { @@ -3659,16 +3657,73 @@ std::uint64_t GuestBufferMemory::servedEnd(const Region& region) { return region.bufferEnd != 0 ? region.bufferEnd : region.end; } +namespace { + +using AddressRange = ShaderRecompiler::BdaAbi::Range; + +void AppendFrom(const AddressRange& range, std::uint64_t begin, std::uint64_t end, std::vector& result) { + if (begin >= end) return; + auto part = range; + part.begin = begin; + part.end = end; + part.deviceAddress += begin - range.begin; + result.push_back(part); +} + +void AppendDisjoint(const std::vector& ranges, std::vector& result) { + std::uint64_t reached = 0; + for (const auto& range : ranges) { + AppendFrom(range, std::max(range.begin, reached), range.end, result); + reached = std::max(reached, range.end); + } +} + +void AppendUncovered(const std::vector& ranges, const std::vector& covering, std::vector& result) { + for (const auto& range : ranges) { + auto cursor = range.begin; + auto cover = std::partition_point(covering.begin(), covering.end(), [&](const AddressRange& candidate) { return candidate.end <= range.begin; }); + for (; cover != covering.end() && cover->begin < range.end && cursor < range.end; ++cover) { + AppendFrom(range, cursor, std::min(cover->begin, range.end), result); + cursor = std::max(cursor, cover->end); + } + AppendFrom(range, cursor, range.end, result); + } +} + +std::vector MergeByBegin(const std::vector& left, const std::vector& right) { + std::vector merged; + merged.reserve(left.size() + right.size()); + std::merge(left.begin(), left.end(), right.begin(), right.end(), std::back_inserter(merged), [](const AddressRange& first, const AddressRange& second) { return first.begin < second.begin; }); + return merged; +} + +} + std::vector GuestBufferMemory::AddressRanges() const { Require(uploaded && !committed, "guest GPU address ranges are not available"); std::vector result; - result.reserve(regions.size() + (space != nullptr ? space->ranges.size() : 0)); - for (const auto& region : regions) result.push_back(addressRange(region)); if (space != nullptr) { - std::vector combined; - combined.reserve(result.size() + space->ranges.size()); - std::merge(space->ranges.begin(), space->ranges.end(), result.begin(), result.end(), std::back_inserter(combined), [](const auto& left, const auto& right) { return left.begin < right.begin; }); - result = std::move(combined); + std::vector own; + own.reserve(regions.size()); + for (const auto& region : regions) own.push_back(addressRange(region)); + std::vector uncovered; + uncovered.reserve(own.size()); + AppendUncovered(own, space->ranges, uncovered); + result = MergeByBegin(space->ranges, uncovered); + } else if (regionsOverlap) { + std::vector live; + std::vector copies; + for (const auto& region : regions) (region.direct != nullptr || region.mirror != nullptr ? live : copies).push_back(addressRange(region)); + std::vector liveParts; + AppendDisjoint(live, liveParts); + std::vector copyParts; + AppendDisjoint(copies, copyParts); + std::vector uncovered; + AppendUncovered(copyParts, liveParts, uncovered); + result = MergeByBegin(liveParts, uncovered); + } else { + result.reserve(regions.size()); + for (const auto& region : regions) result.push_back(addressRange(region)); } if (writes.empty()) return result; auto sortedWrites = writes; From 253fd9512e4352fa70a62478a7c3ee8532ea0ef3 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Thu, 15 Oct 2026 07:45:49 +0000 Subject: [PATCH 0230/1000] test(agc): resolve BDA stores under a read-only V# across imports to the writable import Two writable 64 KiB pieces and a read-only one, one import window each, with a read-only V# across the boundary: through the kept address space, a hit on it and a dissolve after UploadPrepare, the BDA table has no overlapping ranges, stores up to the boundary resolve to the writable import and reads past it find the read-only one. --- .../libSceAgcDriver/tests/BdaResources.cpp | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp index ab710def2b..30f0dce70f 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp @@ -605,6 +605,55 @@ void spaceReadsAcrossImportsMergedTests(const Context& context, const BdaTestAcc ::operator delete(block, std::align_val_t{window}); } +void spaceReadOverWritableImportTableTests(const Context& context) { + constexpr std::size_t piece = 65536; + constexpr std::size_t window = 2 * piece; + void* block = ::operator new(3 * piece, std::align_val_t{window}); + auto* guest = static_cast(block); + const auto first = reinterpret_cast(block); + const auto boundary = first + window; + std::memset(guest, 0x5a, 3 * piece); + auto importing = context; + importing.hostImportAlignment = piece; + SetHostImportWindow(window); + const auto change = [&](std::uint8_t* range, bool add) { + auto* mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix(); + if (add) GuestAllocations::GuestAllocationsAdd_nid_postfix(mutation, range, piece, true, range != guest + 2 * piece, true); + else GuestAllocations::GuestAllocationsRemove_nid_postfix(mutation, range); + GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation); + }; + for (std::size_t index = 0; index < 3; ++index) change(guest + index * piece, true); + const auto lookup = [](const std::vector& ranges, std::uint64_t address) { + const auto found = std::upper_bound(ranges.begin(), ranges.end(), address, [](std::uint64_t value, const auto& range) { return value < range.begin; }); + return found == ranges.begin() ? ranges.end() : std::prev(found); + }; + for (int build = 0; build < 3; ++build) { + GuestBufferMemory leased(importing); + leased.AcquireRegistered(); + leased.AddReadable(boundary - 256, 512); + leased.UploadPrepare(true); + if (build == 2) ClearHostImports(importing.device); + leased.UploadFinish(true); + std::uint32_t adjustment = 0; + const auto view = leased.Descriptor(boundary - 256, 512, adjustment); + Require(view.buffer != VK_NULL_HANDLE && view.range == 512 + adjustment, "a read-only V# across a writable and a read-only import has no descriptor"); + const auto ranges = leased.AddressRanges(); + Require(std::adjacent_find(ranges.begin(), ranges.end(), [](const auto& left, const auto& right) { return right.begin < left.end; }) == ranges.end(), "the BDA table of a build with a read-only V# across imports has overlapping ranges"); + for (const auto address : {first + 16, boundary - 256, boundary - 128, boundary - 16}) { + const auto found = lookup(ranges, address); + Require(found != ranges.end() && address + 16 <= found->end && (found->permissions & ShaderRecompiler::BdaAbi::Write) != 0u, "a BDA store into a writable import resolves to the read-only copy of a V# across it"); + } + for (const auto address : {boundary, boundary + 64, boundary + piece - 32}) { + const auto found = lookup(ranges, address); + Require(found != ranges.end() && address + 16 <= found->end && (found->permissions & ShaderRecompiler::BdaAbi::Read) != 0u, "a BDA read past a read-only V# across imports finds no range"); + } + leased.WriteBack(); + } + for (std::size_t index = 0; index < 3; ++index) change(guest + index * piece, false); + SetHostImportWindow(0); + ::operator delete(block, std::align_val_t{window}); +} + ShaderRecompiler::DescriptorBinding binding(Role role, std::uint32_t slot) { return {ShaderRecompiler::DescriptorKind::StorageBuffer, role, 0, slot, 1, {}, false}; } @@ -1605,6 +1654,7 @@ void RunBdaResourceTests(const Context& context, const BdaTestAccess& access) { spaceReadAcrossImportsTests(context, access); spaceReadAcrossImportsWrittenTests(context); spaceReadsAcrossImportsMergedTests(context, access); + spaceReadOverWritableImportTableTests(context); importJoinTests(context); importWholeRangeOverWindowTests(context); importRefusedWholeRangeTests(context); From f3300a074da4945ada3c04def919be0217631b3e Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Thu, 15 Oct 2026 07:48:13 +0000 Subject: [PATCH 0231/1000] test(agc): prove the merged read-only V# test keeps the address space, and cross access The test of two read-only V#s merged over three imports now asserts that its second build hits the cached address space without a dissolve and that the third leaves it only for the import change. One crossed piece is writable, so a join of the crossed run cannot erase the overlap the test is about. --- core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp index 30f0dce70f..1aa145074e 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp @@ -570,7 +570,7 @@ void spaceReadsAcrossImportsMergedTests(const Context& context, const BdaTestAcc SetHostImportWindow(window); const auto change = [&](std::uint8_t* range, bool add) { auto* mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix(); - if (add) GuestAllocations::GuestAllocationsAdd_nid_postfix(mutation, range, piece, true, false, true); + if (add) GuestAllocations::GuestAllocationsAdd_nid_postfix(mutation, range, piece, true, range == guest + 3 * piece, true); else GuestAllocations::GuestAllocationsRemove_nid_postfix(mutation, range); GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation); }; @@ -582,6 +582,7 @@ void spaceReadsAcrossImportsMergedTests(const Context& context, const BdaTestAcc const std::uint64_t upper = second + 128; const std::uint64_t upperBytes = third + 256 - upper; for (int build = 0; build < 3; ++build) { + const auto before = AddressSpaceCounters(); GuestBufferMemory leased(importing); leased.AcquireRegistered(); leased.AddReadable(lower, lowerBytes); @@ -599,6 +600,8 @@ void spaceReadsAcrossImportsMergedTests(const Context& context, const BdaTestAcc Require(at + bytes <= mapped.size() && mapped[at] == expected(address) && mapped[at + bytes - 1] == expected(address + bytes - 1), "a read-only V# over imports, merged with another one, maps other bytes than its own"); } leased.WriteBack(); + const auto after = AddressSpaceCounters(); + if (after.enabled && build > 0) Require(after.hits == before.hits + 1 && after.dissolvedOverlap == before.dissolvedOverlap && after.dissolvedImports == before.dissolvedImports + (build == 2 ? 1u : 0u), "read-only V#s over imports, merged with each other, left the cached address space other than by the import change"); } for (std::size_t index = 0; index < pieces; ++index) change(guest + index * piece, false); SetHostImportWindow(0); From 6d17a1dea759ca8dc457e5b1085767c00b24691e Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Thu, 15 Oct 2026 07:48:13 +0000 Subject: [PATCH 0232/1000] test(agc): store through a written V# merged with a read-only V# past an import A read-only V# across two imports into unregistered memory, a read-only and a written V# in that memory: UploadPrepare merges the three into one copy that begins inside the first import. The written V# maps its own bytes in that copy, counts as a copied write, and WriteBack lands its store on its bytes alone. --- .../libSceAgcDriver/tests/BdaResources.cpp | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp index 1aa145074e..fc3489e301 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp @@ -608,6 +608,51 @@ void spaceReadsAcrossImportsMergedTests(const Context& context, const BdaTestAcc ::operator delete(block, std::align_val_t{window}); } +void spaceWriteMergedPastImportTests(const Context& context, const BdaTestAccess& access) { + constexpr std::size_t piece = 65536; + constexpr std::size_t window = 2 * piece; + constexpr std::size_t pieces = 5; + void* block = ::operator new(pieces * piece, std::align_val_t{window}); + auto* guest = static_cast(block); + const auto first = reinterpret_cast(block); + const auto second = first + window; + const auto outside = first + 2 * window; + auto importing = context; + importing.hostImportAlignment = piece; + SetHostImportWindow(window); + const auto change = [&](std::uint8_t* range, bool add) { + auto* mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix(); + if (add) GuestAllocations::GuestAllocationsAdd_nid_postfix(mutation, range, piece, true, range == guest + 3 * piece, true); + else GuestAllocations::GuestAllocationsRemove_nid_postfix(mutation, range); + GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation); + }; + for (std::size_t index = 0; index + 1 < pieces; ++index) change(guest + index * piece, true); + const std::uint64_t written = outside + 512; + for (int build = 0; build < 2; ++build) { + std::memset(guest, 0x21, pieces * piece); + GuestBufferMemory leased(importing); + leased.AcquireRegistered(); + leased.AddReadable(second - 256, window + 512); + leased.AddReadable(outside + 128, 640); + leased.AddWritable(written, 256); + leased.Upload(true); + std::uint32_t adjustment = 0; + const auto view = leased.Descriptor(written, 256, adjustment); + Require(view.buffer != VK_NULL_HANDLE && view.range == 256 + adjustment, "a written V# merged with a read-only V# past an import has no descriptor"); + auto mapped = access.bytes(view.buffer); + const auto at = static_cast(view.offset + adjustment); + Require(at + 256 <= mapped.size() && mapped[at] == std::byte{0x21} && mapped[at + 255] == std::byte{0x21}, "a written V# merged with a read-only V# past an import maps other bytes than its own"); + std::memset(mapped.data() + at, 0x7c, 256); + Require(leased.HasCopiedWrites(), "a written V# merged with a read-only V# past an import is not stored back"); + leased.WriteBack(); + const auto offset = static_cast(written - first); + Require(guest[offset] == 0x7c && guest[offset + 255] == 0x7c && guest[offset - 1] == 0x21 && guest[offset + 256] == 0x21, "a write through a V# merged with a read-only V# past an import did not land on its own bytes"); + } + for (std::size_t index = 0; index + 1 < pieces; ++index) change(guest + index * piece, false); + SetHostImportWindow(0); + ::operator delete(block, std::align_val_t{window}); +} + void spaceReadOverWritableImportTableTests(const Context& context) { constexpr std::size_t piece = 65536; constexpr std::size_t window = 2 * piece; @@ -1657,6 +1702,7 @@ void RunBdaResourceTests(const Context& context, const BdaTestAccess& access) { spaceReadAcrossImportsTests(context, access); spaceReadAcrossImportsWrittenTests(context); spaceReadsAcrossImportsMergedTests(context, access); + spaceWriteMergedPastImportTests(context, access); spaceReadOverWritableImportTableTests(context); importJoinTests(context); importWholeRangeOverWindowTests(context); From ace66bd6167f47d87183e0ee0ea09cb34bc47228 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Thu, 15 Oct 2026 08:03:49 +0000 Subject: [PATCH 0233/1000] test(agc): name the overlay statistics test's tolerance check within MinGW's minwindef.h defines near and far as empty macros, so a helper named near stops compiling in any test that comes to include windows.h. The project keeps those names out of its code. --- .../tests/PerformanceOverlayStatistics.cpp | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlayStatistics.cpp b/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlayStatistics.cpp index aaf594a7db..b829a08c01 100644 --- a/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlayStatistics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlayStatistics.cpp @@ -29,7 +29,7 @@ void require(bool condition, const std::string& reason) { if (!condition) throw std::runtime_error(reason); } -bool near(double value, double expected, double tolerance = 1e-9) { +bool within(double value, double expected, double tolerance = 1e-9) { return std::abs(value - expected) <= tolerance; } @@ -39,13 +39,13 @@ void fpsWindowTests() { statistics.AddPresent(Start, 0, 0); require(statistics.Fps() == 0, "one present reads as a frame rate"); for (int i = 1; i <= 120; ++i) statistics.AddPresent(Start + i * 10ms, 0, 0); - require(near(statistics.Fps(), 100.0), "a present every 10 ms reads as " + std::to_string(statistics.Fps()) + " FPS, not 100"); + require(within(statistics.Fps(), 100.0), "a present every 10 ms reads as " + std::to_string(statistics.Fps()) + " FPS, not 100"); statistics.AddPresent(Start + 1700ms, 0, 0); - require(near(statistics.Fps(), 51.0), "after a 500 ms stall the last second holds 52 presents, read as " + std::to_string(statistics.Fps()) + " FPS, not 51"); + require(within(statistics.Fps(), 51.0), "after a 500 ms stall the last second holds 52 presents, read as " + std::to_string(statistics.Fps()) + " FPS, not 51"); statistics.AddPresent(Start + 3700ms, 0, 0); - require(near(statistics.Fps(), 0.5), "after a 2 s stall the last second holds one present, read as " + std::to_string(statistics.Fps()) + " FPS, not the last frame's 0.5"); + require(within(statistics.Fps(), 0.5), "after a 2 s stall the last second holds one present, read as " + std::to_string(statistics.Fps()) + " FPS, not the last frame's 0.5"); statistics.AddPresent(Start + 3750ms, 0, 0); - require(near(statistics.Fps(), 20.0), "two presents 50 ms apart read as " + std::to_string(statistics.Fps()) + " FPS, not 20"); + require(within(statistics.Fps(), 20.0), "two presents 50 ms apart read as " + std::to_string(statistics.Fps()) + " FPS, not 20"); bool refused = false; try { statistics.AddPresent(Start + 3740ms, 0, 0); @@ -66,24 +66,24 @@ void frameRingTests() { statistics.AddPresent(at, 0, 0); } const auto partial = statistics.FrameTimes(); - require(partial.size() == 4 && near(partial[0], 1) && near(partial[3], 4), "the frame times of a short run are not in present order"); + require(partial.size() == 4 && within(partial[0], 1) && within(partial[3], 4), "the frame times of a short run are not in present order"); for (int i = 5; i <= 150; ++i) { at += std::chrono::milliseconds(i); statistics.AddPresent(at, 0, 0); } const auto times = statistics.FrameTimes(); require(times.size() == OverlayStatistics::FrameHistory, "the ring holds " + std::to_string(times.size()) + " frames, not 120"); - for (std::size_t i = 0; i < times.size(); ++i) require(near(times[i], 31.0 + static_cast(i)), "frame " + std::to_string(i) + " of the ring is " + std::to_string(times[i]) + " ms"); - require(near(statistics.LastFrameMs(), 150), "the last frame is not the newest"); + for (std::size_t i = 0; i < times.size(); ++i) require(within(times[i], 31.0 + static_cast(i)), "frame " + std::to_string(i) + " of the ring is " + std::to_string(times[i]) + " ms"); + require(within(statistics.LastFrameMs(), 150), "the last frame is not the newest"); const auto range = statistics.FrameTimeRange(); - require(near(range.minimum, 31) && near(range.maximum, 150) && near(range.average, 90.5), "the graph range of 31..150 ms reads as " + std::to_string(range.minimum) + "/" + std::to_string(range.average) + "/" + std::to_string(range.maximum)); + require(within(range.minimum, 31) && within(range.maximum, 150) && within(range.average, 90.5), "the graph range of 31..150 ms reads as " + std::to_string(range.minimum) + "/" + std::to_string(range.average) + "/" + std::to_string(range.maximum)); } void counterTests() { OverlayStatistics statistics; constexpr std::uint64_t Earlier = 1'000'000'000u; for (std::uint64_t i = 0; i <= 60; ++i) statistics.AddPresent(Start + i * 16ms, Earlier + i * 4'000'000u, Earlier + i * 1'500'000u); - require(near(statistics.GpuMsPerFrame(), 4.0) && near(statistics.DriverMsPerFrame(), 1.5), "4 ms of GPU and 1.5 ms of driver time per frame read as " + std::to_string(statistics.GpuMsPerFrame()) + " and " + std::to_string(statistics.DriverMsPerFrame())); + require(within(statistics.GpuMsPerFrame(), 4.0) && within(statistics.DriverMsPerFrame(), 1.5), "4 ms of GPU and 1.5 ms of driver time per frame read as " + std::to_string(statistics.GpuMsPerFrame()) + " and " + std::to_string(statistics.DriverMsPerFrame())); statistics.AddPresent(Start + 61 * 16ms, 5, 5); require(statistics.GpuMsPerFrame() == 0 && statistics.DriverMsPerFrame() == 0, "a counter that went back is read as time"); } @@ -107,8 +107,8 @@ void lowRateTests() { gpuTotal += Gpu[i]; driverTotal += Driver[i]; slow.AddPresent(Start + (i + 1) * 2s, gpuTotal, driverTotal); - require(near(slow.Fps(), 0.5) && near(slow.LastFrameMs(), 2000), "a present every 2 s reads as " + std::to_string(slow.Fps()) + " FPS and " + std::to_string(slow.LastFrameMs()) + " ms, not 0.5 and 2000"); - require(near(slow.GpuMsPerFrame(), static_cast(Gpu[i]) / 1e6) && near(slow.DriverMsPerFrame(), static_cast(Driver[i]) / 1e6), "frame " + std::to_string(i + 1) + " every 2 s reads " + std::to_string(slow.GpuMsPerFrame()) + " ms of GPU and " + std::to_string(slow.DriverMsPerFrame()) + " ms of driver time, not its own"); + require(within(slow.Fps(), 0.5) && within(slow.LastFrameMs(), 2000), "a present every 2 s reads as " + std::to_string(slow.Fps()) + " FPS and " + std::to_string(slow.LastFrameMs()) + " ms, not 0.5 and 2000"); + require(within(slow.GpuMsPerFrame(), static_cast(Gpu[i]) / 1e6) && within(slow.DriverMsPerFrame(), static_cast(Driver[i]) / 1e6), "frame " + std::to_string(i + 1) + " every 2 s reads " + std::to_string(slow.GpuMsPerFrame()) + " ms of GPU and " + std::to_string(slow.DriverMsPerFrame()) + " ms of driver time, not its own"); } const auto everyTwoSeconds = AgcDriver::ComposeOverlay(slow, true, true); require(everyTwoSeconds.above == std::vector{"PROFILED", "FPS: 0.50", "FRAME: 2000 MS"}, "a present every 2 s shows " + std::to_string(everyTwoSeconds.above.size()) + " lines above the graph, the last '" + everyTwoSeconds.above.back() + "'"); @@ -117,7 +117,7 @@ void lowRateTests() { OverlayStatistics glacial; glacial.AddPresent(Start, 0, 0); for (int i = 1; i <= 5; ++i) glacial.AddPresent(Start + i * 12s, static_cast(i) * 250'000'000u, static_cast(i) * 12'500'000'000u); - require(near(glacial.Fps(), 1000.0 / 12000.0) && near(glacial.LastFrameMs(), 12000) && near(glacial.GpuMsPerFrame(), 250) && near(glacial.DriverMsPerFrame(), 12500), "a present every 12 s reads as " + std::to_string(glacial.Fps()) + " FPS, " + std::to_string(glacial.LastFrameMs()) + " ms, " + std::to_string(glacial.GpuMsPerFrame()) + " and " + std::to_string(glacial.DriverMsPerFrame()) + " ms"); + require(within(glacial.Fps(), 1000.0 / 12000.0) && within(glacial.LastFrameMs(), 12000) && within(glacial.GpuMsPerFrame(), 250) && within(glacial.DriverMsPerFrame(), 12500), "a present every 12 s reads as " + std::to_string(glacial.Fps()) + " FPS, " + std::to_string(glacial.LastFrameMs()) + " ms, " + std::to_string(glacial.GpuMsPerFrame()) + " and " + std::to_string(glacial.DriverMsPerFrame()) + " ms"); const auto everyTwelveSeconds = AgcDriver::ComposeOverlay(glacial, true, true); require(everyTwelveSeconds.above == std::vector{"PROFILED", "FPS: 0.08", "FRAME: 9999 MS"}, "a present every 12 s shows " + std::to_string(everyTwelveSeconds.above.size()) + " lines above the graph, the last '" + everyTwelveSeconds.above.back() + "'"); require(everyTwelveSeconds.below[0] == "MIN/AVG/MAX: 9999/9999/9999" && everyTwelveSeconds.below[1] == "GPU: 250 MS" && everyTwelveSeconds.below[2] == "DRIVER: 9999 MS", "a present every 12 s shows '" + everyTwelveSeconds.below[0] + "', '" + everyTwelveSeconds.below[1] + "' and '" + everyTwelveSeconds.below[2] + "'"); @@ -141,7 +141,7 @@ void lowRateTests() { rising.AddPresent(at, total, 0); }; const auto check = [&](double fps, double gpuMs, const char* shown, const std::string& when) { - require(near(rising.Fps(), fps) && near(rising.GpuMsPerFrame(), gpuMs) && AgcDriver::FormatOverlayRate(rising.Fps()) == shown, when + " reads as " + std::to_string(rising.Fps()) + " FPS ('" + AgcDriver::FormatOverlayRate(rising.Fps()) + "') and " + std::to_string(rising.GpuMsPerFrame()) + " ms of GPU time, not " + std::to_string(fps) + " ('" + shown + "') and " + std::to_string(gpuMs)); + require(within(rising.Fps(), fps) && within(rising.GpuMsPerFrame(), gpuMs) && AgcDriver::FormatOverlayRate(rising.Fps()) == shown, when + " reads as " + std::to_string(rising.Fps()) + " FPS ('" + AgcDriver::FormatOverlayRate(rising.Fps()) + "') and " + std::to_string(rising.GpuMsPerFrame()) + " ms of GPU time, not " + std::to_string(fps) + " ('" + shown + "') and " + std::to_string(gpuMs)); }; step(2s, 40'000'000u); check(0.5, 40, "0.50", "a 2 s frame"); @@ -310,16 +310,16 @@ AgcDriver::OverlayDetails fullDetails() { void packetStatisticsTests() { const auto statistics = countedStatistics(); - require(near(statistics.DrawsPerFrame(), 1240) && near(statistics.DispatchesPerFrame(), 85), "1240 draws and 85 dispatches per frame read as " + std::to_string(statistics.DrawsPerFrame()) + " and " + std::to_string(statistics.DispatchesPerFrame())); + require(within(statistics.DrawsPerFrame(), 1240) && within(statistics.DispatchesPerFrame(), 85), "1240 draws and 85 dispatches per frame read as " + std::to_string(statistics.DrawsPerFrame()) + " and " + std::to_string(statistics.DispatchesPerFrame())); require(statistics.PipelinesInWindow() == 3 && statistics.Latest().pipelines == 1503, "3 pipelines created within the last second of 1503 read as " + std::to_string(statistics.PipelinesInWindow()) + " of " + std::to_string(statistics.Latest().pipelines)); - require(near(statistics.GpuMsPerFrame(), 4.0) && near(statistics.DriverMsPerFrame(), 1.5), "the time counters changed with the packet counters"); + require(within(statistics.GpuMsPerFrame(), 4.0) && within(statistics.DriverMsPerFrame(), 1.5), "the time counters changed with the packet counters"); auto stalled = statistics; auto counters = stalled.Latest(); counters.draws += 900; counters.dispatches += 40; counters.pipelines += 7; stalled.AddPresent(Start + 3000ms, counters); - require(near(stalled.DrawsPerFrame(), 900) && near(stalled.DispatchesPerFrame(), 40) && stalled.PipelinesInWindow() == 7 && stalled.Latest().pipelines == 1510, "a present alone in its second reads as " + std::to_string(stalled.DrawsPerFrame()) + " draws, " + std::to_string(stalled.DispatchesPerFrame()) + " dispatches and " + std::to_string(stalled.PipelinesInWindow()) + " pipelines, not the last frame's 900, 40 and 7"); + require(within(stalled.DrawsPerFrame(), 900) && within(stalled.DispatchesPerFrame(), 40) && stalled.PipelinesInWindow() == 7 && stalled.Latest().pipelines == 1510, "a present alone in its second reads as " + std::to_string(stalled.DrawsPerFrame()) + " draws, " + std::to_string(stalled.DispatchesPerFrame()) + " dispatches and " + std::to_string(stalled.PipelinesInWindow()) + " pipelines, not the last frame's 900, 40 and 7"); auto restarted = statistics; counters = restarted.Latest(); counters.draws = 3; @@ -465,9 +465,9 @@ void paletteTests() { const auto sdr = AgcDriver::OverlayPalette(VK_FORMAT_B8G8R8A8_UNORM, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR); require(sdr[1] == std::array{1.0f, 1.0f, 1.0f, 1.0f} && sdr[0][3] > 0.0f && sdr[0][3] < 1.0f, "the SDR palette is not opaque white text over a translucent panel"); const auto srgb = AgcDriver::OverlayPalette(VK_FORMAT_B8G8R8A8_SRGB, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR); - require(near(srgb[2][1], std::pow((0.8 + 0.055) / 1.055, 2.4), 1e-5) && srgb[1][0] == 1.0f, "an sRGB swapchain gets encoded colors"); + require(within(srgb[2][1], std::pow((0.8 + 0.055) / 1.055, 2.4), 1e-5) && srgb[1][0] == 1.0f, "an sRGB swapchain gets encoded colors"); const auto hdr = AgcDriver::OverlayPalette(VK_FORMAT_A2B10G10R10_UNORM_PACK32, VK_COLOR_SPACE_HDR10_ST2084_EXT); - for (std::size_t channel = 0; channel < 3; ++channel) require(near(hdr[1][channel], 0.5807, 0.0015), "HDR10 white text is not at 203 nits (PQ " + std::to_string(hdr[1][channel]) + ")"); + for (std::size_t channel = 0; channel < 3; ++channel) require(within(hdr[1][channel], 0.5807, 0.0015), "HDR10 white text is not at 203 nits (PQ " + std::to_string(hdr[1][channel]) + ")"); require(std::lround(hdr[0][0] * 1023.0f) == 0 && hdr[0][3] == sdr[0][3], "the HDR10 panel is not black at the same opacity"); } From 7ca97ea137a5d3c61ebaa4022ae9032e99c64b8f Mon Sep 17 00:00:00 2001 From: EdouardSence <103503538+EdouardSence@users.noreply.github.com> Date: Thu, 15 Oct 2026 10:15:22 +0200 Subject: [PATCH 0234/1000] test(agc): lift the host import budget in the sparse dma-buf import test The test imports ranges just above udmabuf's size limit and keeps the first one live. With size_limit_mb=8192 each range is 8257 MiB, so on a host with 32 GB of RAM the default budget (half the physical memory) refuses the second import and HostImportFor returns nullptr instead of reaching the sparse path: "a failure at UDMABUF_CREATE after the sparse path was chosen did not throw". Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAgcDriver/tests/HostImportSparseDmaBuf.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/HostImportSparseDmaBuf.cpp b/core/libs/prx/libSceAgcDriver/tests/HostImportSparseDmaBuf.cpp index a6d05d4dc7..410aba2b8e 100644 --- a/core/libs/prx/libSceAgcDriver/tests/HostImportSparseDmaBuf.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/HostImportSparseDmaBuf.cpp @@ -287,6 +287,7 @@ int main() { } close(udmabuf); setenv("APS5_WRITE_WATCH_IMPORTS", "unwatch", 1); + SetHostImportBudget(~std::uint64_t{0}); GuestArena::GuestArenaSetSharedBacking_nid_postfix(Resolve); try { ImportsALargeRangeAsOneSparseBuffer(); From 70c88b3e0fb15e5b2350bce88367bd03482ee54e Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Thu, 15 Oct 2026 01:48:16 -0700 Subject: [PATCH 0235/1000] docs: say that the overlay's DRIVER line can exceed FRAME with several workers --- docs/user/USAGE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index 65a2a146b5..58e136b1fc 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -209,7 +209,7 @@ F10 shows or hides a performance overlay over the presented image; `ANYPS5_OVERL | graph | The last 120 frame times, the newest on the right. The full height is the longest of them or twice their average, whichever is larger. | | `MIN/AVG/MAX` | The shortest, average and longest of the 120 frame times, in ms. | | `GPU` | GPU time per frame over the last second, or of the last frame below 1 fps, in ms: the span of each recorded batch from its first to its last command, from the timestamp queries of `APS5_PROFILE_GPU=1`. Shown only with that variable. Batches are counted when they complete, so the line trails a sudden change by up to a second, or by a frame below 1 fps. Batches that overlap on different queues each count in full, so the line can exceed `FRAME`. | -| `DRIVER` | CPU time per frame over the last second, or of the last frame below 1 fps, in ms, that the packet workers spent in dispatch and draw packets (the `dispatch` and `draw` time of the `[gpu] worker` line of `APS5_PROFILE_DRAW=1`). Shown only with that variable. | +| `DRIVER` | CPU time per frame over the last second, or of the last frame below 1 fps, in ms, that the packet workers spent in dispatch and draw packets (the `dispatch` and `draw` time of the `[gpu] worker` line of `APS5_PROFILE_DRAW=1`). Shown only with that variable. Every worker's time counts in full, so with several workers busy at once the line can exceed `FRAME`. | | `DRAWS` | Draw packets per frame over the last second, or of the last frame below 1 fps, then dispatch packets per frame (`DISPATCH`), as the packet workers execute them; packets that predication skips are not counted. Shown once the title has submitted either. | | `SHADERS` | Pipelines created in the last second, or during the last frame below 1 fps, then since the start (`TOTAL`): a graphics pipeline for each draw state the pipeline cache had not seen and a compute pipeline for each new dispatch shader. A rise while the game stutters points at pipeline compilation. Shown once a pipeline was created. | | `VRAM` | Usage and budget of the GPU's largest device-local memory heap from `VK_EXT_memory_budget`, read once a second. Not shown when the driver lacks the extension. | From fe56f6056184441a7382f18d53d16ddfc933cb1a Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Thu, 15 Oct 2026 08:50:15 +0000 Subject: [PATCH 0236/1000] fix(shader): bind a T# whose record the CPU evaluates directly when a table cannot express its operation An image table cannot store, update atomically or read PCK and BY2/BY4 through its records (#757): those operations fault and read zeros. #6243 turns a heap T# whose words and record offset the SRT walker evaluates into a selected one-record table, which kept that limitation although such a T# is one known descriptor, as #5188's direct images were. PlanTableColumns now binds such a T# directly, for every operation of the shader on it, when any of them is a store, an atomic, a packed or a BY2/BY4 access. A T# keyed per lane, or by a value the walker cannot evaluate, keeps its table and its unsupported-operation fault. The disk format moves to 37 so that plans cached by #6243's builds are not reused. --- .../Optimization/src/ResourceTracker.cpp | 54 ++++++++++++++++++- core/shader/recompiler/ShaderDiskCache.hpp | 2 +- docs/dev/debt/shader.md | 2 +- 3 files changed, 55 insertions(+), 3 deletions(-) diff --git a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp index 580eac4d16..a83d9ee2f7 100644 --- a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp @@ -1289,6 +1289,7 @@ private: return; } IndexMemoryUsers(); + const auto directImages = DirectImageHandles(); for (auto& block : m_program.Blocks()) { for (IrValue* inst : block->Instructions()) { const auto imageInfo = ImageOpcodeInfoOf(inst->Opcode()); @@ -1300,7 +1301,10 @@ private: continue; } const auto memory = m_program.Resources().memoryInfo[flags.index]; - static_cast(PlanTableColumn(*inst->Argument(0)->Resolve(), false, memory.imageR128)); + IrValue& image = *inst->Argument(0)->Resolve(); + if (std::ranges::none_of(directImages, [&](const IrValue* handle) { return SameHandle(*handle, image); })) { + static_cast(PlanTableColumn(image, false, memory.imageR128)); + } if (imageInfo.needsSampler && inst->ArgumentCount() > 1u && !DirectSampler(*inst->Argument(1)->Resolve())) { static_cast(PlanTableColumn(*inst->Argument(1)->Resolve(), true, false)); } @@ -1344,6 +1348,54 @@ private: return ValidateSource(descriptor, badDword); } + bool DirectImage(const IrValue& handle) { + if (handle.Opcode() != IrOpcode::GetImageResource || handle.ArgumentCount() != 8u) { + return false; + } + DescriptorSource descriptor; + MakeSource(handle, 8u, false, false, descriptor); + std::uint32_t badDword = 0; + return ValidateSource(descriptor, badDword); + } + + std::vector DirectImageHandles() { + std::vector handles; + for (auto& block : m_program.Blocks()) { + for (IrValue* inst : block->Instructions()) { + const auto imageInfo = ImageOpcodeInfoOf(inst->Opcode()); + if (imageInfo.access == ImageAccess::None || inst->ArgumentCount() == 0u) { + continue; + } + const auto flags = inst->Flags(); + if (flags.index >= m_program.Resources().memoryInfo.size()) { + continue; + } + const auto& memory = m_program.Resources().memoryInfo[flags.index]; + const bool tableUnsupported = imageInfo.resourceClass == ImageResourceClass::Storage || memory.imagePacked || memory.imageByElements != 0u; + const IrValue* handle = inst->Argument(0)->Resolve(); + if (tableUnsupported && DirectImage(*handle)) { + handles.push_back(handle); + } + } + } + return handles; + } + + bool SameHandle(const IrValue& left, const IrValue& right) const { + if (&left == &right) { + return true; + } + if (left.Opcode() != right.Opcode() || left.ArgumentCount() != right.ArgumentCount()) { + return false; + } + for (std::size_t index = 0; index < left.ArgumentCount(); index++) { + if (!EquivalentValue(m_program.Resources(), left.Argument(index), right.Argument(index))) { + return false; + } + } + return true; + } + bool IsTablePlanningMemory(std::uint32_t index) const { return std::ranges::find(m_tablePlanningMemory, index) != m_tablePlanningMemory.end(); } diff --git a/core/shader/recompiler/ShaderDiskCache.hpp b/core/shader/recompiler/ShaderDiskCache.hpp index b03d85a809..1cce383ff3 100644 --- a/core/shader/recompiler/ShaderDiskCache.hpp +++ b/core/shader/recompiler/ShaderDiskCache.hpp @@ -13,7 +13,7 @@ namespace ShaderRecompiler::ShaderDiskCache { -inline constexpr std::uint32_t FormatVersion = 36; +inline constexpr std::uint32_t FormatVersion = 37; enum class LoadStatus { Loaded, diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index 61ceaa79aa..b69e3157ae 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -34,7 +34,7 @@ - [Image tables](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) read by `s_load` through a 64-bit pointer hold the keys the key's known bits allow, at most 65536; a key past them, or an entry at a wrapping, unaligned or unmapped address, faults when selected. - An [image table](../../../core/shader/recompiler/Optimization/src/ResourceTracker.cpp) whose key is neither a V#-relative scalar read nor a value the SRT walker evaluates (for example a shifted or masked runtime-offset V# read, a per-lane value made uniform by `v_readfirstlane`, or a loop-carried phi) binds every record of the snapshot domain instead of the selected ones. So does a table read at `(key << n) & mask` with such a key: a key domain would select records by the unmasked key, so that key is not narrowed. A key the walker evaluates selects its one record instead, unless the shader may write the memory the key is read from at run time (the same buffer, or any memory when the read or a write goes through a pointer or a GPU-only descriptor); then the column binds the whole domain too. A write to the key's memory through another V# over the same bytes is not detected. - An [image table](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) T# of an MSAA or FMASK image, or of a color format under a depth compare, faults when selected: tables have no multisampled descriptors and no emulated color compare. -- `image_store`, image atomics, PCK2/PCK4 and MIMG BY2/BY4 through an [image table](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) T# fault (`unsupported operation`) when an active lane executes them; the store is dropped and the load reads zeros. +- `image_store`, image atomics, PCK2/PCK4 and MIMG BY2/BY4 through an [image table](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) T# fault (`unsupported operation`) when an active lane executes them; the store is dropped and the load reads zeros. A T# whose words the SRT walker evaluates is bound directly instead, for every operation of the shader on it (`DirectImageHandles`), so only T#s keyed per lane or by a value the walker cannot evaluate reach a table with these operations; such a direct T# is read when the work is recorded, so a key its own dispatch writes is read before that write. - An [image table](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) S# with a min or max reduction or FORCE_UNNORMALIZED faults when selected, as does an integer, converted or depth-bits table T# paired with a direct min or max S# that filters between texels. - An image-table T#/S# pair with anisotropic `ANISO_OVERRIDE` faults on selection: dynamic mip-level pairing is unimplemented, including otherwise-valid single-level and mipmapped tables. Direct pairs keep their override behavior. Unselected entries, anisotropy without the override and the override without anisotropy stay supported. - Uint and Sint [image table](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvDescriptors.cpp) entries and direct T#s under a table S# sample through a point-filtered copy of the S#: Vulkan leaves linear filtering of integer formats undefined; the console is assumed to point-sample. From e87f3ae920a9ee5d934a91eac61becfd5a6f678d Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Thu, 15 Oct 2026 08:50:15 +0000 Subject: [PATCH 0237/1000] test(shader): store, update atomically and read packed through a T# whose record the CPU evaluates verifySelectedRecordDirectOperations plans image_store, image_atomic_add, image_load_pck and image_load_by2 through a heap T# keyed by an SRT value, which must be bound directly, and through a T# keyed per lane, which must stay a table with its unsupported mark. A T# that is both sampled and stored is bound directly for both, and one that is only sampled still selects its record. RunSelectedStoreTests stores through a record keyed by an SRT value on the GPU and reads the texels back. Before the fix both failed: the T# was planned through an image table, and the store faulted as an unsupported operation. --- .../libSceAgcDriver/tests/ShaderMemory.cpp | 48 +++++++++++++++++++ .../tests/execution/ImageTable.cpp | 33 +++++++++++++ 2 files changed, 81 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 3980d639f4..66e1365008 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -975,6 +975,7 @@ struct TableGuest { std::array srt{}; }; +constexpr std::uint32_t TableFormat16UNorm = 7; constexpr std::uint32_t TableFormat8888UNorm = 56; constexpr std::uint32_t TableFormat32UInt = 20; constexpr std::uint32_t TableFormat16SInt = 12; @@ -1906,6 +1907,52 @@ void verifySelectedRecordOffsetCarry() { require(TableSelected(wrapped.capture->snapshot.tables, 0u, "selected record offset carry: the record past the wrap does not hold one readable selected record") == image && TableCovered(wrapped.regions, words + 0x50u / 4u, sizeof(image)), "selected record offset carry: the record past the wrap was not selected"); } +std::vector WithImageOperation(std::vector code, std::uint32_t word0, bool keepSample) { + const auto sample = std::find(code.begin(), code.end(), 0xf09c0f08u); + require(sample != code.end() && sample + 1 != code.end(), "image operation variant: the program has no image_sample_lz"); + if (keepSample) { + code.insert(sample + 2, {word0, 0x00050000u}); + } else { + sample[0] = word0; + sample[1] = 0x00050000u; + } + return code; +} + +void verifySelectedRecordDirectOperations() { + using namespace ShaderRecompiler; + static TableTexture textures[2]; + alignas(4096) static TableGuest guest; + auto& heap = guest.heap; + std::fill(heap.begin(), heap.end(), std::array{}); + heap[0] = TableT(textures[0].bytes.data(), TableFormat8888UNorm, TableType2D); + heap[1] = TableT(textures[1].bytes.data(), TableFormat32UInt, TableType2D); + heap[2] = heap[1]; + heap[3] = heap[0]; + FillTableSrt(guest, heap.data(), 32u, 4u); + guest.srt[16] = 2u; + const auto userData = TableUserData(guest); + const std::array, 4> operations{{{"image_store", 0xf0200f08u, TableFormat32UInt}, {"image_atomic_add", 0xf0440108u, TableFormat32UInt}, {"image_load_pck", 0xf0080f08u, TableFormat32UInt}, {"image_load_by2", 0xf1080308u, TableFormat16UNorm}}}; + for (const auto& [name, word0, format] : operations) { + const auto what = std::string("selected record direct operations: ") + name; + heap[2] = TableT(textures[1].bytes.data(), format, TableType2D); + const auto code = WithImageOperation(TableSrtKeyCode, word0, false); + const auto request = TableRequest(code, userData); + const auto plan = GetResourcePlan(request); + require(plan->info.images.size() == 1u && plan->info.images[0].table == NoTable && !plan->descriptorSources.at(plan->info.images[0].source).tableColumn.has_value(), (what + " through a T# whose record the CPU evaluates was planned through an image table").c_str()); + const auto compiled = CompileTable(request); + require(TableBinding(*compiled.result, DescriptorRole::ImageTable) == nullptr && compiled.result->imageTableFaults == 0u && TableCovered(compiled.regions, &heap[2], sizeof(heap[2])), (what + " through a T# whose record the CPU evaluates was not bound from its record").c_str()); + const auto wholeCode = WithImageOperation(TableWholeCode, word0, false); + const auto whole = GetResourcePlan(TableRequest(wholeCode, userData)); + require(whole->info.images.size() == 1u && whole->info.images[0].table == 0u && whole->info.images[0].tableOperation == TableOperation::Unsupported, (what + " through a T# keyed per lane left the image table or lost its unsupported mark").c_str()); + } + const auto mixedCode = WithImageOperation(TableSrtKeyCode, 0xf0200f08u, true); + const auto mixed = GetResourcePlan(TableRequest(mixedCode, userData)); + require(mixed->info.images.size() == 2u && std::ranges::all_of(mixed->info.images, [&](const ImageResource& image) { return image.table == NoTable && !mixed->descriptorSources.at(image.source).tableColumn.has_value(); }), "selected record direct operations: a T# sampled and stored was planned through an image table for one of its operations"); + const auto sampled = GetResourcePlan(TableRequest(TableSrtKeyCode, userData)); + require(sampled->info.images.size() == 1u && sampled->info.images[0].table == 0u && sampled->descriptorSources.at(sampled->info.images[0].source).tableColumn->selected, "selected record direct operations: a T# that is only sampled no longer selects its record"); +} + void verifyDescriptorPhis() { using namespace ShaderRecompiler; constexpr std::uint32_t Format8888UNorm = 56; @@ -5198,6 +5245,7 @@ int main(int argc, char** argv) { verifyPointerTableOffsetCarry(); verifySelectedRecordFallbacks(); verifySelectedRecordOffsetCarry(); + verifySelectedRecordDirectOperations(); std::cout << "Image table keys, maps, records and registration passed\n"; return 0; } diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp index 4c0e9d8d54..a6038714bb 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp @@ -204,6 +204,14 @@ alignas(256) constexpr std::array StoreTableCutCode{ 0xbf810000u, }; +constexpr std::uint32_t SelectedStoreValue = 0xc0de5eedu; + +alignas(256) constexpr std::array SelectedStoreCode{ + 0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4000400u, 0xfa000060u, 0xf4200444u, 0x20000000u, + 0x9312b011u, 0xf42c0802u, 0x24000000u, 0x360200bfu, 0x7e040280u, 0x7e0802ffu, SelectedStoreValue, 0xf0208f08u, + 0x00090401u, 0xbf810000u, +}; + alignas(256) constexpr std::array PointerCode{ 0xf4080200u, 0xfa000000u, 0xf4080700u, 0xfa000010u, 0xf4080800u, 0xfa000020u, 0x8f108202u, 0xf4200444u, 0x20000000u, 0x8711ff11u, 0x000000ffu, 0x8f128511u, 0xf40c0900u, 0x24000040u, 0x360200bfu, 0x7e020d01u, @@ -1050,6 +1058,30 @@ void RunStoreTests(AgcDriver::VulkanDevice& device) { Require(UintTexels == saved, "image table store: an unsupported store reached the image"); } +void RunSelectedStoreTests(AgcDriver::VulkanDevice& device) { + alignas(256) static std::array storeTexels{}; + struct Registration { + void* data; + Registration(void* data, std::size_t bytes) : data(data) { GuestAllocations::Mutation().Add(data, bytes, true, true, true); } + ~Registration() { GuestAllocations::Mutation().Remove(data); } + } registered(storeTexels.data(), sizeof(storeTexels)); + for (std::uint32_t texel = 0; texel < storeTexels.size(); ++texel) storeTexels[texel] = 0x5000u + texel; + const auto saved = storeTexels; + SetClassRecords(); + SetRecord(9, PointClamp, Image(storeTexels.data(), Format32UInt)); + Keys = {}; + Keys[3] = 9u; + Srt[24] = 12u; + const auto stored = Run(device, SelectedStoreCode, 1, Lanes, 64); + AgcDriver::Graphics::StorageTexture::FlushPending(reinterpret_cast(storeTexels.data()), sizeof(storeTexels), nullptr, "test"); + device.WaitIdle(); + Require(!Faulted(stored) && stored.imageTableCount == 0u, "image table selected store: an image_store through a T# whose record the CPU evaluates faulted or was bound through an image table:\n" + stored.log); + for (std::uint32_t texel = 0; texel < storeTexels.size(); ++texel) { + const auto expected = texel < Lanes ? SelectedStoreValue : saved[texel]; + Require(storeTexels[texel] == expected, "image table selected store: texel " + std::to_string(texel) + " holds " + std::to_string(storeTexels[texel]) + " after an image_store through a T# whose record the CPU evaluates, expected " + std::to_string(expected)); + } +} + void RunPointerTests(AgcDriver::VulkanDevice& device) { const auto floatImage = Image(FloatTexels.data(), Format32Float); const auto uintImage = Image(UintTexels.data(), Format32UInt); @@ -1379,6 +1411,7 @@ int main() { RunSelectedRecordTests(*device); RunModeTests(*device); RunStoreTests(*device); + RunSelectedStoreTests(*device); RunPointerTests(*device); RunPointerRecordTests(*device); RunSplitRecordTests(*device); From 3b914c84b40d4322b1c15ddb89246b5c6baec4c3 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Thu, 15 Oct 2026 08:50:15 +0000 Subject: [PATCH 0238/1000] test(shader): pin that an S# whose record the CPU evaluates is bound directly A sampler column is planned only when DirectSampler cannot evaluate the S#, so no sampler record is ever selected: an S# read from a heap at an offset the SRT walker evaluates is a direct sampler, and one keyed per lane is a sampler table. --- .../libSceAgcDriver/tests/ShaderMemory.cpp | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 66e1365008..5322874596 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -1953,6 +1953,36 @@ void verifySelectedRecordDirectOperations() { require(sampled->info.images.size() == 1u && sampled->info.images[0].table == 0u && sampled->descriptorSources.at(sampled->info.images[0].source).tableColumn->selected, "selected record direct operations: a T# that is only sampled no longer selects its record"); } +void verifyEvaluableHeapSamplerDirect() { + using namespace ShaderRecompiler; + static TableTexture textures[2]; + alignas(4096) static TableGuest guest; + auto& heap = guest.heap; + std::fill(heap.begin(), heap.end(), std::array{}); + heap[0] = TableT(textures[0].bytes.data(), TableFormat8888UNorm, TableType2D); + heap[1] = TableT(textures[1].bytes.data(), TableFormat32UInt, TableType2D); + heap[2] = heap[1]; + heap[3] = heap[0]; + FillTableSrt(guest, heap.data(), 32u, 4u); + guest.srt[16] = 2u; + const auto userData = TableUserData(guest); + const auto heapSampler = [](std::vector code) { + const auto direct = std::find(code.begin(), code.end(), 0xf4080200u); + require(direct != code.end() && direct[1] == 0xfa000010u, "evaluable heap sampler: the program has no S# load from the SRT"); + code.erase(direct, direct + 2); + const auto shift = std::find(code.begin(), code.end(), 0x8f108510u); + require(shift != code.end(), "evaluable heap sampler: the program has no key shift"); + code.insert(shift + 1, {0xf4280202u, 0x20000000u}); + return code; + }; + const auto evaluatedCode = heapSampler(TableSrtKeyCode); + const auto evaluated = GetResourcePlan(TableRequest(evaluatedCode, userData)); + require(evaluated->info.samplers.size() == 1u && evaluated->info.samplers[0].table == NoTable && !evaluated->descriptorSources.at(evaluated->info.samplers[0].source).tableColumn.has_value(), "evaluable heap sampler: an S# read from a heap at an offset the SRT walker evaluates was planned through a sampler table"); + const auto perLaneCode = heapSampler(TableWholeCode); + const auto perLane = GetResourcePlan(TableRequest(perLaneCode, userData)); + require(perLane->info.samplers.size() == 1u && perLane->info.samplers[0].table != NoTable && perLane->descriptorSources.at(perLane->info.samplers[0].source).tableColumn.has_value(), "evaluable heap sampler: an S# read from a heap at an offset keyed per lane was not planned through a sampler table"); +} + void verifyDescriptorPhis() { using namespace ShaderRecompiler; constexpr std::uint32_t Format8888UNorm = 56; @@ -5246,6 +5276,7 @@ int main(int argc, char** argv) { verifySelectedRecordFallbacks(); verifySelectedRecordOffsetCarry(); verifySelectedRecordDirectOperations(); + verifyEvaluableHeapSamplerDirect(); std::cout << "Image table keys, maps, records and registration passed\n"; return 0; } From 033406265762d56024ebc3d9c45a6b966e956697 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Thu, 15 Oct 2026 08:50:15 +0000 Subject: [PATCH 0239/1000] perf(shader): drop NonUniform from the index of a selected image table record A selected record is one map entry for the whole dispatch, so its descriptor index is dynamically uniform. The emitter decorated it NonUniform like any table index whenever the workgroup spans more than one subgroup, and on every graphics stage, which makes drivers scalarize the access. The element index, its access chain, the loaded image and the sampled image of a selected record are no longer decorated; table samplers and tables keyed per lane keep the decoration. --- .../SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp | 1 + .../recompiler/SpirvBackend/src/SpirvImageEmitter.cpp | 4 +++- .../SpirvBackend/src/SpirvMemory/SpirvDescriptors.cpp | 6 +++--- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp index b85eb66e9f..dfc86a7f4b 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp @@ -83,6 +83,7 @@ struct SpirvTableAccess { std::uint32_t dword3 = 0; std::uint32_t samplerElement = 0; bool active = false; + bool uniform = false; }; struct SpirvEmitterState { diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp index 0328487362..b54e5d2469 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp @@ -1703,7 +1703,9 @@ TableAccessResult EmitTableAccess(SpirvValueEmitContext& ctx, const IrValue& ins result.flags = flags; live = selected; state.tableAccess.element = Binary(state, spv::OpIAdd, u32, LoadTableMap(state, ConstantU32(state, ImageTableAbi::HeaderElementBase)), element); - DecorateTableNonUniform(state, state.tableAccess.element); + const auto& sources = state.program.Resources().descriptorSources; + state.tableAccess.uniform = base.source < sources.size() && sources[base.source].tableColumn.has_value() && sources[base.source].tableColumn->selected; + if (!state.tableAccess.uniform) DecorateTableNonUniform(state, state.tableAccess.element); state.tableAccess.dword3 = dword3; } if (tableSampler) { diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvDescriptors.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvDescriptors.cpp index 10a18de9ff..51c6548c8e 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvDescriptors.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvDescriptors.cpp @@ -156,10 +156,10 @@ std::uint32_t LoadSampledImageDescriptor(SpirvEmitterState& state, std::uint32_t if (found == state.tableImageVariables.end() || !state.tableAccess.active) ExitDescriptorBindingFailure(state, kind, resource, "image table array was not emitted for the mode"); const auto pointer = state.module.AllocateId(); state.module.AddFunction(spv::OpAccessChain, pointerType, pointer, found->variable, state.tableAccess.element); - DecorateTableNonUniform(state, pointer); + if (!state.tableAccess.uniform) DecorateTableNonUniform(state, pointer); const auto image = state.module.AllocateId(); state.module.AddFunction(spv::OpLoad, imageType, image, pointer); - DecorateTableNonUniform(state, image); + if (!state.tableAccess.uniform) DecorateTableNonUniform(state, image); return image; } const auto arrayIndex = ResourceForDescriptor(state, kind, resource); @@ -206,7 +206,7 @@ std::uint32_t MakeSampledImage(SpirvEmitterState& state, std::uint32_t resource, const auto sampledImage = state.module.AllocateId(); const auto sampledType = state.module.Type(spv::OpTypeSampledImage, ImageType(state, imageResource)); state.module.AddFunction(spv::OpSampledImage, sampledType, sampledImage, image, samplerId); - if (imageResource.table != NoTable || state.program.Info().samplers.at(sampler).table != NoTable) DecorateTableNonUniform(state, sampledImage); + if ((imageResource.table != NoTable && !state.tableAccess.uniform) || state.program.Info().samplers.at(sampler).table != NoTable) DecorateTableNonUniform(state, sampledImage); return sampledImage; } From 55a09385ab16d42f8a29ae0dae6627b328507a0b Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Thu, 15 Oct 2026 08:50:15 +0000 Subject: [PATCH 0240/1000] test(shader): check that a selected record's table index is not NonUniform verifySelectedRecordUniformIndex compiles the program keyed by an SRT value and the one keyed per lane with a two-wave workgroup. The selected record's table index carries no NonUniform decoration and the per-lane table's still does. Before the change the selected record's was decorated. --- .../libSceAgcDriver/tests/ShaderMemory.cpp | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 5322874596..2625e6699d 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -1983,6 +1983,38 @@ void verifyEvaluableHeapSamplerDirect() { require(perLane->info.samplers.size() == 1u && perLane->info.samplers[0].table != NoTable && perLane->descriptorSources.at(perLane->info.samplers[0].source).tableColumn.has_value(), "evaluable heap sampler: an S# read from a heap at an offset keyed per lane was not planned through a sampler table"); } +void verifySelectedRecordUniformIndex() { + using namespace ShaderRecompiler; + static TableTexture textures[2]; + alignas(4096) static TableGuest guest; + auto& heap = guest.heap; + std::fill(heap.begin(), heap.end(), std::array{}); + heap[0] = TableT(textures[0].bytes.data(), TableFormat8888UNorm, TableType2D); + heap[1] = TableT(textures[1].bytes.data(), TableFormat32UInt, TableType2D); + heap[2] = heap[1]; + heap[3] = heap[0]; + FillTableSrt(guest, heap.data(), 32u, 4u); + guest.srt[16] = 2u; + const auto userData = TableUserData(guest); + const auto twoWaves = [&](const std::vector& code) { + auto request = TableRequest(code, userData); + request.context.compute->numThreads = {128u, 1u, 1u}; + return CompileTable(request); + }; + const auto nonUniform = [](const TableCompiled& compiled) { + const auto spirv = compiled.result->spirv.Words(); + std::uint32_t count = 0; + for (std::size_t cursor = 5; cursor < spirv.size() && (spirv[cursor] >> 16u) != 0u; cursor += spirv[cursor] >> 16u) { + if ((spirv[cursor] & 0xffffu) == spv::OpDecorate && (spirv[cursor] >> 16u) == 3u && spirv[cursor + 2u] == spv::DecorationNonUniform) ++count; + } + return count; + }; + const auto selected = twoWaves(TableSrtKeyCode); + require(TableBinding(*selected.result, DescriptorRole::ImageTable) != nullptr && nonUniform(selected) == 0u, "selected record uniform index: the image table index of a selected record in a two-wave workgroup is decorated NonUniform"); + const auto whole = twoWaves(TableWholeCode); + require(nonUniform(whole) != 0u, "selected record uniform index: the image table index of a table keyed per lane in a two-wave workgroup is not decorated NonUniform"); +} + void verifyDescriptorPhis() { using namespace ShaderRecompiler; constexpr std::uint32_t Format8888UNorm = 56; @@ -5277,6 +5309,7 @@ int main(int argc, char** argv) { verifySelectedRecordOffsetCarry(); verifySelectedRecordDirectOperations(); verifyEvaluableHeapSamplerDirect(); + verifySelectedRecordUniformIndex(); std::cout << "Image table keys, maps, records and registration passed\n"; return 0; } From 6aacf0bab5434289d60a072f549cdd071444f974 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Thu, 15 Oct 2026 08:50:15 +0000 Subject: [PATCH 0241/1000] fix(shader): clear the key domains of a column deselected because its key may be written CheckTableStores deselects a selected column whose key the dispatch may write: a key read through a pointer or a V# while the shader stores through a pointer, or through a V# the shader writes. The column kept the key domains narrowed from that same key buffer, which the domain rule clears only when the domain's own V# is written, so a shader storing through a pointer still narrowed its whole table on a key it may overwrite. Deselection now clears them as well. --- core/shader/recompiler/Optimization/src/ResourceTracker.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp index a83d9ee2f7..ee01ec50ad 100644 --- a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp @@ -1455,6 +1455,7 @@ private: for (auto& source : m_sources) { if (source.tableColumn.has_value() && source.tableColumn->selected && SelectionWritten(*source.tableColumn)) { source.tableColumn->selected = false; + source.tableColumn->keyDomains.clear(); } if (!source.tableColumn.has_value() || source.tableColumn->keyDomains.empty()) { continue; From 23d6cf6848d1bc8e57fddc2b5df7d366eddd3471 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Thu, 15 Oct 2026 08:50:15 +0000 Subject: [PATCH 0242/1000] test(shader): check that a key a pointer store may write keeps no key domain The key loaded from a constant buffer at an SRT offset has a key domain. With a global_store_dword added, the column is deselected and keeps no key domain. Before the fix it kept the domain. --- core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 2625e6699d..4f85e07306 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -1866,6 +1866,14 @@ void verifySelectedRecordFallbacks() { require(writtenColumn.has_value() && !writtenColumn->selected && writtenColumn->keyDomains.empty(), "selected record fallbacks: a key loaded from a buffer the shader writes selected its record"); const auto written = CompileTable(writtenRequest); require(written.capture->snapshot.tables.tables.at(0).keys == 4u, "selected record fallbacks: a key loaded from a buffer the shader writes did not keep the whole table"); + require(!loadedPlan->descriptorSources.at(loadedPlan->info.images[0].source).tableColumn->keyDomains.empty(), "selected record fallbacks: a key loaded from a constant buffer has no key domain"); + auto pointerWritten = loadedKey; + const auto end = std::find(pointerWritten.begin(), pointerWritten.end(), 0xbf810000u); + require(end != pointerWritten.end(), "selected record fallbacks: the program has no s_endpgm"); + pointerWritten.insert(end, {0xdc708000u, 0x001c0100u}); + const auto pointerPlan = GetResourcePlan(TableRequest(pointerWritten, userData)); + const auto& pointerColumn = pointerPlan->descriptorSources.at(pointerPlan->info.images.at(0).source).tableColumn; + require(pointerPlan->info.bdaWrites && pointerColumn.has_value() && !pointerColumn->selected && pointerColumn->keyDomains.empty(), "selected record fallbacks: a key loaded from a constant buffer in a shader that stores through a pointer selected its record or kept its key domain"); const auto lanePlan = GetResourcePlan(TableRequest(TableWholeCode, userData)); require(!lanePlan->info.images.empty() && !lanePlan->descriptorSources.at(lanePlan->info.images[0].source).tableColumn->selected, "selected record fallbacks: a T# selected per lane left the table"); From a9c2717fd04942f9fb468d8f3b560aed6ec89f0c Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Thu, 15 Oct 2026 01:54:10 -0700 Subject: [PATCH 0243/1000] fix(libSceNet): return 0 from an unbound datagram receive after shutdown or for zero bytes On Windows a receive on a datagram socket with no local address waits for one. A sceNetShutdown of the receive side now ends that wait, and later receives return 0 at once; a zero-length receive returns 0 without waiting. FreeBSD's soreceive_dgram returns 0 in both cases. Before, a thread blocked in such a receive kept waiting after the socket was shut down. --- core/libs/prx/libSceNet/Export.cpp | 32 +++++++++++++++++++++++++----- core/libs/tests/GuestSceNet.cpp | 32 ++++++++++++++++++++++++++++++ docs/dev/debt/libSceNet.md | 2 +- 3 files changed, 60 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index 152a49eb49..6ef62d0b4f 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -246,6 +246,7 @@ struct Sock { bool bound = false; bool listening = false; bool aborted = false; + bool read_shut = false; int rcv_timeout_us = 0; int snd_timeout_us = 0; int reported_error = 0; @@ -496,9 +497,12 @@ bool has_local_address(NativeSocket native) { return ::getsockname(native, reinterpret_cast(&local), &local_length) == 0 || WSAGetLastError() != WSAEINVAL; } -int wait_for_local_address(int s, const Sock& socket) { +constexpr int RECEIVE_NOTHING = -1; + +int wait_for_local_address(int s, const Sock& socket, bool empty) { if (socket.type == NET_SOCK_STREAM || socket.bound) return 0; const bool named = has_local_address(socket.native->value); + if (!named && (socket.read_shut || empty)) return RECEIVE_NOTHING; if (!named && socket.nonblock) return NET_EAGAIN; const auto deadline = socket.rcv_timeout_us > 0 ? std::chrono::steady_clock::now() + std::chrono::microseconds(socket.rcv_timeout_us) @@ -511,6 +515,7 @@ int wait_for_local_address(int s, const Sock& socket) { it->second.bound = true; return 0; } + if (it->second.read_shut) return RECEIVE_NOTHING; const auto now = std::chrono::steady_clock::now(); if (now >= deadline) return NET_EAGAIN; g_cv.WaitUntil(lk, std::min(deadline, now + std::chrono::milliseconds(10))); @@ -876,7 +881,7 @@ int64_t APS5_VABI sceNetRecv(int s, void* buf, size_t len, int flags) { socket = it->second; } #ifdef _WIN32 - if (const int error = wait_for_local_address(s, socket)) return fail(error); + if (const int wait = wait_for_local_address(s, socket, len == 0)) return wait == RECEIVE_NOTHING ? 0 : fail(wait); #endif const GuestArena::HostWrite destination(len == 0 ? nullptr : buf, len); if (!destination.Open()) return fail(NET_EFAULT); @@ -901,7 +906,11 @@ int64_t APS5_VABI sceNetRecvfrom(int s, void* buf, size_t len, int flags, void* socket = it->second; } #ifdef _WIN32 - if (const int error = wait_for_local_address(s, socket)) return fail(error); + if (const int wait = wait_for_local_address(s, socket, len == 0)) { + if (wait != RECEIVE_NOTHING) return fail(wait); + if (from) *fromlen = 0; + return 0; + } #endif const GuestArena::HostWrite destination(len == 0 ? nullptr : buf, len); if (!destination.Open()) return fail(NET_EFAULT); @@ -990,7 +999,13 @@ int64_t APS5_VABI sceNetRecvmsg(int s, NetMsghdr* msg, int flags) { } const bool datagram = socket.type != NET_SOCK_STREAM; #ifdef _WIN32 - if (const int error = wait_for_local_address(s, socket)) return fail(error); + if (const int wait = wait_for_local_address(s, socket, total == 0)) { + if (wait != RECEIVE_NOTHING) return fail(wait); + msg->flags = 0; + if (msg->name) msg->name_length = 0; + msg->control_length = 0; + return 0; + } #endif std::vector buffer; try { @@ -1035,7 +1050,14 @@ int APS5_VABI sceNetShutdown(int s, int how) { if (it == g_socks.end()) return fail(NET_EBADF); socket = it->second; } - return ::shutdown(socket.native->value, how) == 0 ? 0 : fail(native_error()); + const int error = ::shutdown(socket.native->value, how) == 0 ? 0 : native_error(); + if (how != 1) { + std::lock_guard lk(g_mutex); + const auto it = g_socks.find(s); + if (it != g_socks.end() && it->second.native == socket.native) it->second.read_shut = true; + g_cv.NotifyAll(); + } + return error == 0 ? 0 : fail(error); } int APS5_VABI sceNetSetsockopt(int s, int level, int optname, const void* optval, uint32_t optlen) { diff --git a/core/libs/tests/GuestSceNet.cpp b/core/libs/tests/GuestSceNet.cpp index de2230fc0e..9f2d20bcf0 100644 --- a/core/libs/tests/GuestSceNet.cpp +++ b/core/libs/tests/GuestSceNet.cpp @@ -317,6 +317,37 @@ static void CheckUnboundDatagramReceive() { #endif } +static void CheckUnboundDatagramShutdown() { + const int socket = sceNetSocket("unbound-shutdown", 2, 2, 17); + Require(socket >= 0); + auto waiter = std::async(std::launch::async, [socket] { + char ignored[4]; + return sceNetRecvfrom(socket, ignored, sizeof(ignored), 0, nullptr, nullptr); + }); + std::this_thread::sleep_for(std::chrono::milliseconds(20)); + Require(waiter.wait_for(std::chrono::seconds(0)) == std::future_status::timeout); + sceNetShutdown(socket, 0); + Require(waiter.wait_for(std::chrono::seconds(1)) == std::future_status::ready && waiter.get() == 0); + char buffer[4]{}; + Require(sceNetRecv(socket, buffer, sizeof(buffer), 0) == 0); + std::array source{}; + NetIovec in[] = {{buffer, sizeof(buffer)}}; + NetMsghdr receive{source.data(), static_cast(source.size()), in, 1, nullptr, 0, 0}; + Require(sceNetRecvmsg(socket, &receive, 0) == 0 && receive.name_length == 0); +#ifdef _WIN32 + std::uint32_t size = source.size(); + Require(sceNetRecvfrom(socket, buffer, sizeof(buffer), 0, source.data(), &size) == 0 && size == 0); +#endif + Require(sceNetSocketClose(socket) == 0); +#ifdef _WIN32 + const int empty = sceNetSocket("unbound-empty", 2, 2, 17); + Require(empty >= 0); + Require(sceNetRecv(empty, buffer, 0, 0) == 0); + Require(sceNetRecvfrom(empty, buffer, 0, 0, nullptr, nullptr) == 0); + Require(sceNetSocketClose(empty) == 0); +#endif +} + static void CheckEmptyEpoll() { const int epoll = sceNetEpollCreate("empty-epoll", 0); Require(epoll >= 0); @@ -891,6 +922,7 @@ int main() { CheckLinkLocalAllNodesIpv6(); CheckUnboundSocketName(); CheckUnboundDatagramReceive(); + CheckUnboundDatagramShutdown(); CheckUnspecifiedIpv6(); if (!ipv6Unavailable) CheckIpv6SocketsAreIpv6Only(); CheckDatagramDisconnect(); diff --git a/docs/dev/debt/libSceNet.md b/docs/dev/debt/libSceNet.md index cb92dc78d2..b6c761b698 100644 --- a/docs/dev/debt/libSceNet.md +++ b/docs/dev/debt/libSceNet.md @@ -13,6 +13,6 @@ - [sceNetGetsockopt](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) reads `SO_ERROR` from the host socket. Winsock does not clear a socket's pending error when it is read, so on Windows the library reports a pending error once and shows it again only after a new `sceNetConnect`; a second failure with the same code on the same socket without a `sceNetConnect` in between would be hidden. - [sceNetSendmsg and sceNetRecvmsg](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) copy the scatter/gather list through one host buffer and accept only the flags `sceNetSend`/`sceNetRecv` accept. Sending control data throws; a receive never returns control data (`msg_controllen` 0), since no socket option that queues it is supported - [MSG_DONTWAIT](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) in `sceNetSend`, `sceNetSendto`, `sceNetRecv`, `sceNetRecvfrom`, `sceNetRecvmsg` and `sceNetSendmsg` makes that one call non-blocking through the host's `MSG_DONTWAIT`, as FreeBSD 11.4's `soreceive_generic` returns `EWOULDBLOCK` for it (`sys/kern/uipc_socket.c`); the value `0x80` is FreeBSD's (`sys/sys/socket.h`) and [shadPS4's](https://github.com/shadps4-emu/shadPS4/blob/e0a466753a1aacbc8db8253be685bf7ab4d9daae/src/core/libraries/net/net_types.h#L104). Winsock has no per-call non-blocking flag, so on Windows these calls still fail with `EOPNOTSUPP` for it -- [sceNetRecv, sceNetRecvfrom and sceNetRecvmsg](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) on a datagram socket with no local address wait on Windows until a bind, connect or send gives it one, checking the host socket every 10 ms. Winsock fails such a receive with `WSAEINVAL`, where FreeBSD 11.4's `soreceive_dgram` waits for a datagram (`sys/kern/uipc_socket.c`). A non-blocking socket fails with `EAGAIN` at once, and the receive timeout ends the wait with `EAGAIN`; when the address comes first, the host receive that follows waits the whole timeout again, so the call can take up to twice the timeout. An abort or close ends the wait, and the host receive then fails as it did before +- [sceNetRecv, sceNetRecvfrom and sceNetRecvmsg](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) on a datagram socket with no local address wait on Windows until a bind, connect or send gives it one, checking the host socket every 10 ms. Winsock fails such a receive with `WSAEINVAL`, where FreeBSD 11.4's `soreceive_dgram` waits for a datagram (`sys/kern/uipc_socket.c`). A non-blocking socket fails with `EAGAIN` at once, and the receive timeout ends the wait with `EAGAIN`; when the address comes first, the host receive that follows waits the whole timeout again, so the call can take up to twice the timeout. A `sceNetShutdown` of the receive side ends the wait and the receive returns 0, and a zero-length receive returns 0 without waiting, as `soreceive_dgram` does when the socket can't receive more or nothing was asked for. An abort or close ends the wait, and the host receive then fails as it did before - [sceNetResolverAbort and sceNetResolverStartNtoaMultipleRecords](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) follow shadPS4 ([net.cpp](https://github.com/shadps4-emu/shadPS4/blob/811bfcd7d17ed8ae975c477d9ff28abc744e1ca8/src/core/libraries/net/net.cpp), [net_resolver.cpp](https://github.com/shadps4-emu/shadPS4/blob/811bfcd7d17ed8ae975c477d9ff28abc744e1ca8/src/core/libraries/net/net_resolver.cpp)); the `(int rid, int flags)` signature of the abort also matches [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Socket.cs). Unverified on hardware. Lookups run synchronously over the host resolver, which cannot be interrupted: an abort while a lookup runs on another thread returns at once, and that lookup fails with `EINTR` without writing its output only when the host call returns; the abort flags are then ignored, as shadPS4 does for a running lookup. With nothing running, abort flag 1 (NTOA preservation) or 2 (ATON preservation) makes the next `sceNetResolverStartNtoa*` or `sceNetResolverStartAton` call on that resolver fail with `EINTR`, reported by `sceNetResolverGetError`, after its arguments are checked. Unlike shadPS4, which ignores them, other abort flag bits throw, and `sceNetResolverStartNtoaMultipleRecords` throws for any flag, including async (1) and 0x10000 (IP address literals disabled), as `sceNetResolverStartNtoaMultipleRecordsEx` does - [sceNetResolverStartNtoa6](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) resolves a host name to its first IPv6 address through the host resolver (`getaddrinfo` with `AF_INET6`) and writes the 16-byte address; the `(rid, hostname, addr, timeout, retry, flags)` signature follows `sceNetResolverStartNtoa`. A name with no IPv6 address fails with `RESOLVER_ENODNS` (`0x804101E1`) like a failed lookup; whether the console returns IPv4-mapped addresses instead is not verified. Timeout and retry are ignored as in `sceNetResolverStartNtoa`, and any nonzero flags throw because their meaning for this call is not known. Unverified on hardware. From 6575aa217d05e256905b8cecc74af4fda2476de0 Mon Sep 17 00:00:00 2001 From: eljackjoestar-cmd Date: Thu, 15 Oct 2026 08:18:00 +0400 Subject: [PATCH 0244/1000] fix(audio): retain completed AudioOut2 grains until pushed --- .../libSceAudioOut/src/AudioOut2Context.cpp | 2 -- .../tests/AudioOut2PortBuffers.cpp | 21 +++++++++++++++++++ docs/dev/debt/libSceAudioOut.md | 2 +- 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp b/core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp index 74a4a2b1c4..3bfa0a32fd 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp @@ -23,7 +23,6 @@ using Clock = std::chrono::steady_clock; static constexpr std::size_t CONTEXT_MEMORY = 0x10000; static constexpr std::uint32_t DEFAULT_MAX_PORTS = 16; -static constexpr std::size_t ADVANCED_GRAINS_MAX = 16; // Pushes, polls and advances traced individually before the trace falls back to the per-second summary. static constexpr std::uint64_t CALL_TRACE_FULL = 16; // Grains the device waits for before it starts playing (and again whenever it ran dry), so the @@ -264,7 +263,6 @@ int APS5_VABI sceAudioOut2ContextAdvance(AudioOut2ContextHandle ctx) try { if (!context) return SCE_AUDIO_OUT2_ERROR_INVALID_HANDLE; std::lock_guard lock(context->lock); if (context->device != 0) { - if (context->advancedGrains.size() >= ADVANCED_GRAINS_MAX) throw std::runtime_error("AudioOut2: more than 16 completed grains await a push"); if (!context->advancedMode) { while (!context->pendingGrains.empty()) { Render(*context, context->pendingGrains.front()); diff --git a/core/libs/prx/libSceAudioOut/tests/AudioOut2PortBuffers.cpp b/core/libs/prx/libSceAudioOut/tests/AudioOut2PortBuffers.cpp index cac79482e3..177d3a7a46 100644 --- a/core/libs/prx/libSceAudioOut/tests/AudioOut2PortBuffers.cpp +++ b/core/libs/prx/libSceAudioOut/tests/AudioOut2PortBuffers.cpp @@ -207,6 +207,26 @@ void TestAdvanceRetainsVolume() { RequirePlayed(played, {&buffer, &reduced}); } +void TestAdvanceBurstRetainsEveryGrain() { + std::vector> expected; + for (std::uint32_t index = 0; index < 64; ++index) expected.push_back(Grain(0.001f * static_cast(index + 1))); + const auto played = Play([&](AudioOut2ContextHandle context, AudioOut2PortHandle port) { + std::thread producer([&] { + auto buffer = Grain(0.0f); + for (const auto& data : expected) { + buffer = data; + SetData(port, buffer.data()); + Require(sceAudioOut2ContextAdvance(context) == 0); + } + }); + producer.join(); + for (std::size_t index = 0; index < expected.size(); ++index) Require(sceAudioOut2ContextPush(context, 1) == 0); + }, true); + std::vector*> grains; + for (const auto& data : expected) grains.push_back(&data); + RequirePlayed(played, grains); +} + void TestPushingThreadWaitsForAdvance() { const auto first = Grain(0.01f); const auto second = Grain(0.02f); @@ -238,6 +258,7 @@ int main() { SetEnvironment("SDL_AUDIODRIVER", "disk"); TestAdvanceRetainsRewrittenBuffer(); TestAdvanceRetainsVolume(); + TestAdvanceBurstRetainsEveryGrain(); TestPushingThreadWaitsForAdvance(); TestBuffersSetBeforeOnePushPlayInOrder(); TestBurstThenGapKeepsEveryBuffer(); diff --git a/docs/dev/debt/libSceAudioOut.md b/docs/dev/debt/libSceAudioOut.md index 75a1c10469..7719aa2d0d 100644 --- a/docs/dev/debt/libSceAudioOut.md +++ b/docs/dev/debt/libSceAudioOut.md @@ -2,7 +2,7 @@ ### Unknown function info -- [sceAudioOut2ContextAdvance and sceAudioOut2ContextPush](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp) (libSceAudioOut) - with an SDL device, Advance snapshots each port's PCM and gain; Push consumes completed grains in order and a blocking Push waits for Advance when none is ready. Inferred from PPSA26358, which rewrites the same 512-frame, 12-channel float buffer on a producer thread and pushes on another thread. Console buffer ownership, snapshot timing and empty-queue errors are unverified. More than 16 completed grains awaiting Push and a nonblocking Push without a completed grain throw; the limit and errors are runtime restrictions, not console measurements. Context destruction concurrent with active calls remains unsupported. Without an SDL device the existing wall-clock path remains. +- [sceAudioOut2ContextAdvance and sceAudioOut2ContextPush](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp) (libSceAudioOut) - with an SDL device, Advance snapshots each port's PCM and gain; Push consumes completed grains in order and a blocking Push waits for Advance when none is ready. Inferred from PPSA26358, which rewrites the same 512-frame, 12-channel float buffer on a producer thread and pushes on another thread. Console buffer ownership, snapshot timing and empty-queue errors are unverified. Completed grains remain queued until Push consumes them, so memory use grows with an unconsumed PCM backlog. The console backlog limit is unverified. A nonblocking Push without a completed grain throws; this is a runtime restriction, not a console measurement. Context destruction concurrent with active calls remains unsupported. Without an SDL device the existing wall-clock path remains. - [sceAudioOut2UserGetSupportedAttributes](../../../core/libs/prx/libSceAudioOut/src/AudioOut2User.cpp) (libSceAudioOut) - the user handle and two uint32 output pointers follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Audio/AudioOut2.cs) and [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/df5b9b92be7dfd463b47756b707d6d356a9f0550/src/libs/libAudio2.cpp), not a console measurement. The masks describe this runtime: no context attributes and port attributes PCM (bit 0) and gain (bit 1). Null outputs and unknown or destroyed user handles throw; console validation order and error values are unknown. Users are tracked for this query, but port ownership and busy-user destruction are not implemented. The other PPSA23566 imports remain declared without parameters, with unknown signatures: [sceConvertKeycodeGetCharacterFromKeyboardData](../../../core/libs/prx/libSceConvertKeycode/Export.cpp), [sce::Json::MemAllocator::notifyError](../../../core/libs/prx/libSceJson2/Export.cpp), [sceProprietaryVoiceChatHelper{Initialize,Terminate,SetVoiceChatState,GetVoiceChatUsageState}](../../../core/libs/prx/libSceProprietaryVoiceChatHelperPrx/Export.cpp), [sceVoice{GetResourceInfo,ResetPort,EnableChat,DisableChat}](../../../core/libs/prx/libSceVoice/Export.cpp). Names from the shadPS4 aerolib NID list - [sceAudioOutExOpen, sceAudioOutExConfigureOutput, sceAudioOutExClose](../../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) - the HDMI bitstream calls follow [PS5-Audio-Passthrough-Research](https://github.com/sainsaji/PS5-Audio-Passthrough-Research/blob/cf3c921d79bf00db4847f31b68a7bd95a8037cd7/docs/FINDINGS.md#L31-L80), which disassembles firmware 12.70's libSceAudioOut and logs the calls on a console. `sceAudioOutExOpen(user, mode)` returns `0x80260015` for a mode above 10 or outside mask `0x61F`; otherwise it opens a port of internal type 6 in S16 stereo, 256 frames at 48000 Hz for modes 0, 1, 2 and 9 and 1024 frames at 192000 Hz for modes 3, 4 and 10. `sceAudioOutExConfigureOutput(control, flags, mode, target, option)` returns `0x80268009` when `control` is not 0, takes modes 0 to 10 and 0xFF and targets 1 to 3 and 0xFF, and returned 0 on the console; `sceAudioOutExClose` returned 0. The argument registers match [Jelly5](https://github.com/02dnot/Jelly5/blob/552b866d89b2a58b27f7f7b0484dc743ce235526/app/src/jelly5_bitstream.cpp#L26-L28) and PPSA99051's [bitstream_out.cpp](https://github.com/sainsaji/PS5-Audio-Passthrough-Research/blob/cf3c921d79bf00db4847f31b68a7bd95a8037cd7/sample-app/src/platform/ps5/bitstream_out.cpp#L29-L32). Audio reaches the host through SDL as PCM and the host has no HDMI bitstream output, so an Ex port has no host device: `sceAudioOutOutput` paces it at its own rate and drops its IEC 61937 bursts, and `sceAudioOutExConfigureOutput` changes nothing. Nonzero `flags` or `option`, other modes or targets, `sceAudioOutExClose` of a handle that is not an Ex port, and `sceAudioOutGetPortState` on an Ex port throw, since the console's answers are unknown From 7333704c722cba7ebc782d44810cc586a507b5d2 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Thu, 15 Oct 2026 02:13:52 -0700 Subject: [PATCH 0245/1000] test(libSceNet): receive on an unbound datagram socket that another thread binds or connects The test covered a send that names the waiting socket. A bind and a datagram connect from another thread name it too: each case now waits in sceNetRecv on the unbound socket, binds or connects it, and checks that a peer's datagram to the new address reaches the waiting receive. --- core/libs/tests/GuestSceNet.cpp | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/core/libs/tests/GuestSceNet.cpp b/core/libs/tests/GuestSceNet.cpp index 9f2d20bcf0..19d5cf9cae 100644 --- a/core/libs/tests/GuestSceNet.cpp +++ b/core/libs/tests/GuestSceNet.cpp @@ -348,6 +348,32 @@ static void CheckUnboundDatagramShutdown() { #endif } +static void CheckUnboundDatagramLaterAddress() { + for (const bool connect : {false, true}) { + const int waiting = sceNetSocket("unbound-later", 2, 2, 17); + const int peer = sceNetSocket("unbound-later-peer", 2, 2, 17); + Require(waiting >= 0 && peer >= 0); + std::array peerAddress{16, 2, 0, 0, 127, 0, 0, 1}; + Require(sceNetBind_nid_postfix(peer, peerAddress.data(), peerAddress.size()) == 0); + std::uint32_t size = peerAddress.size(); + Require(sceNetGetsockname(peer, peerAddress.data(), &size) == 0); + auto reply = std::async(std::launch::async, [waiting] { + char answer[16]{}; + return sceNetRecv(waiting, answer, sizeof(answer), 0) == 5 && std::strcmp(answer, "pong") == 0; + }); + std::this_thread::sleep_for(std::chrono::milliseconds(20)); + Require(reply.wait_for(std::chrono::seconds(0)) == std::future_status::timeout); + std::array waitingAddress{16, 2, 0, 0, 127, 0, 0, 1}; + Require((connect ? sceNetConnect(waiting, peerAddress.data(), peerAddress.size()) + : sceNetBind_nid_postfix(waiting, waitingAddress.data(), waitingAddress.size())) == 0); + size = waitingAddress.size(); + Require(sceNetGetsockname(waiting, waitingAddress.data(), &size) == 0); + Require(sceNetSendto(peer, "pong", 5, 0, waitingAddress.data(), size) == 5); + Require(reply.wait_for(std::chrono::seconds(1)) == std::future_status::ready && reply.get()); + for (const int descriptor : {waiting, peer}) Require(sceNetSocketClose(descriptor) == 0); + } +} + static void CheckEmptyEpoll() { const int epoll = sceNetEpollCreate("empty-epoll", 0); Require(epoll >= 0); @@ -923,6 +949,7 @@ int main() { CheckUnboundSocketName(); CheckUnboundDatagramReceive(); CheckUnboundDatagramShutdown(); + CheckUnboundDatagramLaterAddress(); CheckUnspecifiedIpv6(); if (!ipv6Unavailable) CheckIpv6SocketsAreIpv6Only(); CheckDatagramDisconnect(); From 42e6737f594459a9d8498346b24c3bdefd74262c Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Thu, 15 Oct 2026 02:13:52 -0700 Subject: [PATCH 0246/1000] perf(libSceNet): end the wait for a local address when a bind, connect or send gives one A receive on an unbound datagram socket checked the host socket every 10 ms, so it went on waiting up to 10 ms after another thread's bind, connect or send had named the socket. sceNetBind now wakes the waiting receives. On Windows a datagram connect or a send marks the socket bound and wakes them once the host socket has its local address; a raw socket that a send leaves without one stays unmarked and keeps waiting. --- core/libs/prx/libSceNet/Export.cpp | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index 6ef62d0b4f..59a8397734 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -497,6 +497,14 @@ bool has_local_address(NativeSocket native) { return ::getsockname(native, reinterpret_cast(&local), &local_length) == 0 || WSAGetLastError() != WSAEINVAL; } +void note_local_address(int s, const Sock& socket) { + if (socket.type == NET_SOCK_STREAM || socket.bound || !has_local_address(socket.native->value)) return; + std::lock_guard lk(g_mutex); + const auto it = g_socks.find(s); + if (it != g_socks.end() && it->second.native == socket.native) it->second.bound = true; + g_cv.NotifyAll(); +} + constexpr int RECEIVE_NOTHING = -1; int wait_for_local_address(int s, const Sock& socket, bool empty) { @@ -752,6 +760,7 @@ int APS5_VABI sceNetBind_nid_postfix(int s, const void* addr, uint32_t addrlen) std::lock_guard lk(g_mutex); auto it = g_socks.find(s); if (it != g_socks.end()) it->second.bound = true; + g_cv.NotifyAll(); return 0; } @@ -862,7 +871,12 @@ int APS5_VABI sceNetConnect(int s, const void* addr, uint32_t addrlen) { NativeLength native_length = 0; if (!guest_to_native_address(addr, addrlen, native, native_length)) return fail(NET_EINVAL); if (native.ss_family != (socket.family == NET_AF_INET ? AF_INET : AF_INET6)) return fail(NET_EAFNOSUPPORT); - if (::connect(socket.native->value, reinterpret_cast(&native), native_length) == 0) return 0; + if (::connect(socket.native->value, reinterpret_cast(&native), native_length) == 0) { +#ifdef _WIN32 + note_local_address(s, socket); +#endif + return 0; + } #ifdef _WIN32 if (WSAGetLastError() == WSAEWOULDBLOCK) return fail(NET_EINPROGRESS); #endif @@ -965,7 +979,11 @@ int64_t APS5_VABI sceNetSendto(int s, const void* buf, size_t len, int flags, co result = ::sendto(socket.native->value, static_cast(buf), static_cast(len), NATIVE_SEND_FLAGS | native_message_flags(flags), reinterpret_cast(&destination), destination_length); } - return result >= 0 ? result : fail(native_error()); + if (result < 0) return fail(native_error()); +#ifdef _WIN32 + note_local_address(s, socket); +#endif + return result; } int64_t APS5_VABI sceNetSendmsg(int s, const NetMsghdr* msg, int flags) { From 8cda582730100820025ac3867b39e18d7ba49952 Mon Sep 17 00:00:00 2001 From: ee6aba54 <340958984+ee6aba54@users.noreply.github.com> Date: Tue, 13 Oct 2026 23:54:36 +0200 Subject: [PATCH 0247/1000] fix(agc-driver): release the texture caches when a device allocation runs out of memory A buffer allocation that still fails after the pool trimmed its own free slots now asks the texture caches for the request plus a reserve (the request again, at least 256 MiB) and retries once more: sampled textures go first, oldest first, then cached storage images, oldest first, with their resource cache entries removed and kept by the active recorder as an eviction from the lookup does. Only bytes whose images die now count; images a recorded batch still holds do not. The caches are skipped when the calling thread holds their mutex (an allocation inside a lookup) or the device lock cannot be taken, so the retry never waits behind a lookup. The recovered line names the released bytes next to the reclaimed ones, as does the failure. Before this, the title PPSA15304 died 460 s into its second section on a 128 MiB buffer allocation while 2.5 GiB of cached storage images were idle. (cherry picked from commit 31b1b227657dd7b9ced8d808a224411673d6de1c) --- .../Execution/src/VulkanDevice.cpp | 1 + .../Graphics/include/BufferPool.hpp | 5 + .../Graphics/include/ShaderResources.hpp | 1 + .../Graphics/src/BufferPool.cpp | 30 +++++- .../Graphics/src/ShaderResources.cpp | 96 ++++++++++++++++++ .../prx/libSceAgcDriver/tests/BufferPool.cpp | 97 +++++++++++++++++++ .../tests/execution/StorageCacheBudget.cpp | 9 ++ 7 files changed, 236 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index ab39aeb323..bb9ab86152 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -1837,6 +1837,7 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : state->functionsReady = true; } Graphics::PrepareImportWatch(graphicsContext()); + Graphics::BufferPool::SetCacheReclaim(Graphics::ReleaseTextureCaches); state->bufferPool = std::make_shared(graphicsContext()); state->emptyBuffer = std::make_unique(graphicsContext(), Graphics::EmptyBufferBytes, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); state->pipelineCache = std::make_unique(graphicsContext(), state->properties); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/BufferPool.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/BufferPool.hpp index de95acd54e..1e8a24b239 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/BufferPool.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/BufferPool.hpp @@ -2,6 +2,7 @@ #define CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_BUFFERPOOL_HPP #include "prx/libSceAgcDriver/Graphics/include/Context.hpp" +#include #include #include #include @@ -55,6 +56,9 @@ class BufferPool { public: explicit BufferPool(const Context& context); static VkDeviceSize DeviceBudget(const VkPhysicalDeviceMemoryProperties& memory); + using CacheReclaim = VkDeviceSize (*)(VkDeviceSize bytes); + static void SetCacheReclaim(CacheReclaim reclaim); + static VkDeviceSize ReclaimTarget(VkDeviceSize bytes); ~BufferPool(); BufferPool(const BufferPool&) = delete; BufferPool& operator=(const BufferPool&) = delete; @@ -126,6 +130,7 @@ private: void evictOldest(Tier& tier, std::vector& evicted); // The retained-slot bound of each tier (APS5_BUFFER_POOL_SLOTS, default `defaultSlots`), read once. static std::size_t MaxSlots(); + static std::atomic& cacheReclaim(); std::size_t maxSlots; VkDevice device; PFN_vkUnmapMemory unmap; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp index 77058182fe..8984da69dd 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp @@ -28,6 +28,7 @@ class Recorder; void FlushCachedTextures(VkDevice device); void ClearCachedTextures(VkDevice device); +std::uint64_t ReleaseTextureCaches(std::uint64_t bytes); std::uint64_t TextureCacheBudget(const VkPhysicalDeviceMemoryProperties& memory); std::uint64_t StorageTextureBudget(const VkPhysicalDeviceMemoryProperties& memory, const VkPhysicalDeviceMemoryBudgetPropertiesEXT* reported); std::uint64_t SampledTextureBudget(const VkPhysicalDeviceMemoryProperties& memory, const VkPhysicalDeviceMemoryBudgetPropertiesEXT* reported, std::uint64_t textureBytes); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp index 6ec77770be..acd629912c 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp @@ -1,6 +1,7 @@ #include "prx/libSceAgcDriver/Execution/include/ProfileOutput.hpp" #include "prx/libSceAgcDriver/Graphics/include/BufferPool.hpp" #include +#include #include #include #include @@ -57,7 +58,9 @@ VkDeviceMemory BufferPool::AllocateMemory(const VkMemoryAllocateInfo& allocation const auto initialResult = result; unsigned attempts = 1; VkDeviceSize reclaimed = 0; - if (result == VK_ERROR_OUT_OF_DEVICE_MEMORY || result == VK_ERROR_OUT_OF_HOST_MEMORY) { + VkDeviceSize released = 0; + const auto exhausted = [](VkResult value) { return value == VK_ERROR_OUT_OF_DEVICE_MEMORY || value == VK_ERROR_OUT_OF_HOST_MEMORY; }; + if (exhausted(result)) { reclaimed = trim(); if (reclaimed != 0) { memory = VK_NULL_HANDLE; @@ -65,16 +68,37 @@ VkDeviceMemory BufferPool::AllocateMemory(const VkMemoryAllocateInfo& allocation ++attempts; } } + if (exhausted(result)) { + if (const auto reclaim = cacheReclaim().load(std::memory_order_acquire); reclaim != nullptr) released = reclaim(ReclaimTarget(allocation.allocationSize)); + if (released != 0) { + memory = VK_NULL_HANDLE; + result = allocateMemory(device, &allocation, nullptr, &memory); + ++attempts; + } + } if (result == VK_SUCCESS) { - if (attempts == 2) std::fprintf(stderr, "[bufferpool] %s recovered: allocation=%llu reclaimed=%llu initialResult=%d attempts=%u\n", operation, static_cast(allocation.allocationSize), static_cast(reclaimed), static_cast(initialResult), attempts); + if (attempts > 1) std::fprintf(stderr, "[bufferpool] %s recovered: allocation=%llu reclaimed=%llu released=%llu initialResult=%d attempts=%u\n", operation, static_cast(allocation.allocationSize), static_cast(reclaimed), static_cast(released), static_cast(initialResult), attempts); return memory; } char details[512]; - std::snprintf(details, sizeof(details), "%s: allocation=%llu memoryType=%u reclaimed=%llu initialResult=%d attempts=%u", operation, static_cast(allocation.allocationSize), allocation.memoryTypeIndex, static_cast(reclaimed), static_cast(initialResult), attempts); + std::snprintf(details, sizeof(details), "%s: allocation=%llu memoryType=%u reclaimed=%llu released=%llu initialResult=%d attempts=%u", operation, static_cast(allocation.allocationSize), allocation.memoryTypeIndex, static_cast(reclaimed), static_cast(released), static_cast(initialResult), attempts); Check(result, details); return VK_NULL_HANDLE; } +std::atomic& BufferPool::cacheReclaim() { + static std::atomic reclaim{nullptr}; + return reclaim; +} + +void BufferPool::SetCacheReclaim(CacheReclaim reclaim) { + cacheReclaim().store(reclaim, std::memory_order_release); +} + +VkDeviceSize BufferPool::ReclaimTarget(VkDeviceSize bytes) { + return bytes + std::max(bytes, VkDeviceSize{256} << 20u); +} + bool BufferPool::SlabEligible(std::size_t capacity, VkDeviceSize alignment, VkDeviceSize size, VkDeviceSize atom) { if (capacity >= classLimit || size > capacity || !std::has_single_bit(capacity)) return false; const auto alignmentOk = alignment == 0 || (std::has_single_bit(alignment) && alignment <= capacity); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index 554709280d..7f4a448f83 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -154,6 +155,26 @@ TextureCache& Textures() { return cache; } +thread_local unsigned sampledCacheHeld = 0; +thread_local unsigned storageCacheHeld = 0; + +struct CacheHold { + unsigned& depth; + explicit CacheHold(unsigned& depth) : depth(depth) { ++depth; } + ~CacheHold() { --depth; } + CacheHold(const CacheHold&) = delete; + CacheHold& operator=(const CacheHold&) = delete; +}; + +bool AcquireCache(std::mutex& mutex) { + const auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(20); + do { + if (mutex.try_lock()) return true; + std::this_thread::yield(); + } while (std::chrono::steady_clock::now() < deadline); + return false; +} + struct UncachedImages { std::mutex mutex; std::vector> images; @@ -704,6 +725,7 @@ std::shared_ptr cachedTexture(const Context& context, std::spansource != nullptr) { @@ -944,6 +966,7 @@ std::shared_ptr cachedStorageTexture(const Context& context, std std::shared_ptr texture; { std::lock_guard lock(cache.mutex); + const CacheHold held(storageCacheHeld); if (resource.mipCount > AllocatedLevels(resource)) { auto allocated = resource; allocated.mipCount = AllocatedLevels(resource); @@ -1095,6 +1118,79 @@ void ClearCachedTextures(VkDevice device) { } } +std::uint64_t ReleaseTextureCaches(std::uint64_t bytes) { + thread_local bool releasing = false; + if (releasing) return 0; + releasing = true; + struct Done { + bool& flag; + ~Done() { flag = false; } + } done{releasing}; + std::uint64_t freed = 0; + std::size_t sampledEntries = 0; + std::size_t storageEntries = 0; + bool sampledSkipped = false; + bool storageSkipped = false; + { + auto& cache = Textures(); + if (sampledCacheHeld != 0 || !AcquireCache(cache.mutex)) { + sampledSkipped = true; + } else { + std::lock_guard lock(cache.mutex, std::adopt_lock); + dropUncachedViews(cache); + while (freed < bytes && !cache.entries.empty()) { + const auto back = std::prev(cache.entries.end()); + const auto accounted = back->accounted; + const bool last = back->texture.use_count() == 1; + eraseTexture(cache, back); + ++cache.counts.evicted; + ++sampledEntries; + if (last) freed += accounted; + } + } + } + if (freed < bytes) { + auto& cache = StorageTextures(); + std::unique_lock gpu(GuestMemory::GpuMutex(), std::defer_lock); + if (storageCacheHeld != 0 || !gpu.try_lock()) { + storageSkipped = true; + } else { + bool more = true; + while (more && freed < bytes) { + std::vector, std::uint64_t>> evicted; + { + if (!AcquireCache(cache.mutex)) { + storageSkipped = true; + break; + } + std::lock_guard lock(cache.mutex, std::adopt_lock); + std::uint64_t batch = 0; + while (batch + freed < bytes && !cache.entries.empty()) { + const auto back = std::prev(cache.entries.end()); + const auto accounted = back->accounted; + evicted.emplace_back(evictStorage(cache, back), accounted); + batch += accounted; + } + more = !cache.entries.empty(); + } + for (auto& [image, accounted] : evicted) { + std::vector> resources; + SharedResourceCache().RemoveImage(image.get(), &resources); + if (auto* recorder = Recorder::Active(); recorder != nullptr && recorder->Recording()) { + for (auto& resource : resources) recorder->Keep(std::move(resource)); + } + resources.clear(); + ++storageEntries; + if (image.use_count() == 1) freed += accounted; + image.reset(); + } + } + } + } + std::fprintf(stderr, "[gpu] texture caches released %llu MiB of the %llu MiB asked for: %zu sampled entries%s, %zu storage images%s\n", static_cast(freed >> 20u), static_cast(bytes >> 20u), sampledEntries, sampledSkipped ? " (cache busy, skipped)" : "", storageEntries, storageSkipped ? " (cache or device busy, skipped)" : ""); + return freed; +} + bool StorageImageCached(const Context& context, const StorageTexture* image) { auto& cache = StorageTextures(); std::lock_guard lock(cache.mutex); diff --git a/core/libs/prx/libSceAgcDriver/tests/BufferPool.cpp b/core/libs/prx/libSceAgcDriver/tests/BufferPool.cpp index f0ac0664ab..5be5aece06 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BufferPool.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BufferPool.cpp @@ -496,6 +496,102 @@ void AllocationFailure(VkResult initial, VkResult final, bool cached) { Expect(mock.liveBytes == 0 && mock.liveBuffers.empty(), "failure cleanup leaked a resource"); } +struct ReclaimMock { + VkDeviceMemory external = VK_NULL_HANDLE; + VkDeviceSize externalBytes = 0; + std::uint64_t requests = 0; + VkDeviceSize asked = 0; +}; + +ReclaimMock reclaimMock; + +VkDeviceMemory ExternalAllocation(VkDeviceSize bytes) { + VkMemoryAllocateInfo info{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; + info.allocationSize = bytes; + VkDeviceMemory memory = VK_NULL_HANDLE; + Expect(mockAllocateMemory(VK_NULL_HANDLE, &info, nullptr, &memory) == VK_SUCCESS, "the external allocation of the reclaim test failed"); + reclaimMock.external = memory; + reclaimMock.externalBytes = bytes; + return memory; +} + +VkDeviceSize mockCacheReclaim(VkDeviceSize bytes) { + ++reclaimMock.requests; + reclaimMock.asked = bytes; + if (reclaimMock.external == VK_NULL_HANDLE) return 0; + mockFreeMemory(VK_NULL_HANDLE, reclaimMock.external, nullptr); + reclaimMock.external = VK_NULL_HANDLE; + return reclaimMock.externalBytes; +} + +void CacheReclaimRecovery() { + Expect(BufferPool::ReclaimTarget(4 * MiB) == 260 * MiB, "a 4 MiB request does not ask the caches for 256 MiB on top of itself"); + Expect(BufferPool::ReclaimTarget(512 * MiB) == 1024 * MiB, "a 512 MiB request does not ask the caches for twice its size"); + constexpr auto usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT; + BufferPool::SetCacheReclaim(mockCacheReclaim); + struct Reset { + ~Reset() { BufferPool::SetCacheReclaim(nullptr); } + } reset; + mock = MockDevice{}; + reclaimMock = ReclaimMock{}; + { + auto context = mockContext(); + { DeviceBuffer idle(context, 2 * MiB, usage); } + ExternalAllocation(10 * MiB); + mock.limit = 12 * MiB; + const auto before = mock.attempts; + try { + DeviceBuffer recovered(context, 4 * MiB, usage); + Expect(mock.attempts == before + 3, "an allocation the pool's own slots could not satisfy did not retry once after the trim and once after the cache release"); + Expect(reclaimMock.requests == 1 && reclaimMock.asked == 260 * MiB, "the caches were not asked once for the request plus its reserve"); + Expect(mock.frees == 2 && mock.liveBytes == 4 * MiB, "the trim or the cache release did not free its memory"); + } catch (const std::exception& error) { + Expect(false, std::string("the cache release did not permit recovery: ") + error.what()); + } + } + Expect(mock.liveBytes == 0 && mock.liveBuffers.empty(), "cache release recovery leaked a resource"); + mock = MockDevice{}; + reclaimMock = ReclaimMock{}; + { + auto context = mockContext(); + ExternalAllocation(10 * MiB); + mock.limit = 12 * MiB; + const auto before = mock.attempts; + try { + DeviceBuffer recovered(context, 4 * MiB, usage); + Expect(mock.attempts == before + 2 && reclaimMock.requests == 1, "an empty pool did not go straight to the cache release and retry once"); + } catch (const std::exception& error) { + Expect(false, std::string("the cache release alone did not permit recovery: ") + error.what()); + } + } + Expect(mock.liveBytes == 0 && mock.liveBuffers.empty(), "cache release recovery with an empty pool leaked a resource"); + mock = MockDevice{}; + reclaimMock = ReclaimMock{}; + { + auto context = mockContext(); + mock.limit = 2 * MiB; + const auto before = mock.attempts; + std::string failure; + try { + DeviceBuffer buffer(context, 4 * MiB, usage); + } catch (const std::exception& error) { + failure = error.what(); + } + Expect(reclaimMock.requests == 1 && mock.attempts == before + 1, "a cache release that freed nothing was retried after"); + Expect(failure.find("Vulkan result -2") != std::string::npos && failure.find("released=0") != std::string::npos, "the failure after an empty cache release lost its result or the released bytes"); + mock.results = {VK_ERROR_DEVICE_LOST}; + mock.limit = 0; + failure.clear(); + try { + DeviceBuffer buffer(context, 4 * MiB, usage); + } catch (const std::exception& error) { + failure = error.what(); + } + Expect(reclaimMock.requests == 1 && failure.find("Vulkan result -4") != std::string::npos, "a device loss asked the caches for memory"); + } + Expect(mock.liveBytes == 0 && mock.liveBuffers.empty(), "the failed cache release leaked a resource"); +} + void SuccessfulAllocationKeepsPool() { mock = MockDevice{}; { @@ -551,6 +647,7 @@ int main() { AllocationFailure(VK_ERROR_OUT_OF_DEVICE_MEMORY, VK_ERROR_DEVICE_LOST, true); SuccessfulAllocationKeepsPool(); PartialSlabDoesNotRetry(); + CacheReclaimRecovery(); HostPlacement(); } catch (const std::exception& error) { std::fprintf(stderr, "FAIL: %s\n", error.what()); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/StorageCacheBudget.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/StorageCacheBudget.cpp index 758781d128..9e05f39278 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/StorageCacheBudget.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/StorageCacheBudget.cpp @@ -126,6 +126,15 @@ int main() { Require(cached == (surface >= 2u), "surface " + std::to_string(surface) + (cached ? " is still cached" : " was evicted") + " after " + std::to_string(capacity + 2u) + " surfaces under a budget of " + std::to_string(capacity) + " of them"); Require(cached == !images[surface].expired(), "surface " + std::to_string(surface) + (cached ? " is cached but its weak image expired" : " was evicted but its resource cache image is still alive")); } + const auto released = AgcDriver::Graphics::ReleaseTextureCaches(held); + Require(released >= held, "a release of one image's bytes freed " + std::to_string(released >> 20u) + " MiB, less than the " + std::to_string(held >> 20u) + " MiB of the oldest image"); + Require(StorageTexture::FindLive(base + 2u * Stride, SurfaceBytes) == nullptr && images[2].expired(), "the oldest cached storage image survived a release of its bytes"); + Require(StorageTexture::FindLive(base + 3u * Stride, SurfaceBytes) != nullptr && !images[3].expired(), "a release of one image's bytes evicted more than the oldest image"); + Require(StorageTexture::FindLive(base + (capacity + 1u) * Stride, SurfaceBytes) != nullptr, "a release of one image's bytes evicted the newest image"); + const auto rest = AgcDriver::Graphics::ReleaseTextureCaches(~std::uint64_t{0}); + Require(rest >= held * (capacity - 1u), "a release of everything freed " + std::to_string(rest >> 20u) + " MiB of the " + std::to_string((held * (capacity - 1u)) >> 20u) + " MiB cached"); + for (std::uint32_t surface = 0; surface < capacity + 2u; ++surface) Require(StorageTexture::FindLive(base + surface * Stride, SurfaceBytes) == nullptr && images[surface].expired(), "surface " + std::to_string(surface) + " survived a release of everything"); + Require(AgcDriver::Graphics::ReleaseTextureCaches(held) == 0, "a release from empty caches reported freed bytes"); } catch (...) { release(); throw; From f157f8a00262a4d2332a2e5a1ab596b6317e726c Mon Sep 17 00:00:00 2001 From: ee6aba54 <340958984+ee6aba54@users.noreply.github.com> Date: Tue, 13 Oct 2026 23:54:48 +0200 Subject: [PATCH 0248/1000] fix(agc-driver): allocate texture and storage image memory through the buffer pool's recovery path Sampled textures, depth copies and storage images allocated their device memory directly and threw on the first VK_ERROR_OUT_OF_DEVICE_MEMORY. They now go through BufferPool::AllocateMemory, so a failed image allocation trims the pool's idle buffers, releases the texture caches and retries like a buffer allocation does, and reports the recovery the same way. (cherry picked from commit d7af5573c12e680703e648accb6e67ead5052f9f) --- core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index ae43856dd3..25f5fee3d8 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -2,6 +2,7 @@ #include "prx/libSceAgcDriver/Execution/include/CaptureTrace.hpp" #include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libSceAgcDriver/Graphics/include/BufferPool.hpp" #include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" #include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" #include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" @@ -290,7 +291,7 @@ Texture::Texture(const Context& context, TextureDetiler& detiler, const GuestTex allocation.allocationSize = requirements.size; allocationBytes = requirements.size; allocation.memoryTypeIndex = context.DeviceMemoryType(requirements.memoryTypeBits, requirements.size, "texture"); - Check(context.Function("vkAllocateMemory")(context.device, &allocation, nullptr, &owned->memory), "vkAllocateMemory texture"); + owned->memory = GetBufferPool(context)->AllocateMemory(allocation, "vkAllocateMemory texture"); Check(context.Function("vkBindImageMemory")(context.device, image, owned->memory, 0), "vkBindImageMemory"); { @@ -581,7 +582,7 @@ Texture::Texture(const Context& context, const std::shared_ptr& allocation.allocationSize = requirements.size; allocationBytes = requirements.size; allocation.memoryTypeIndex = context.MemoryType(requirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); - Check(context.Function("vkAllocateMemory")(context.device, &allocation, nullptr, &owned->memory), "vkAllocateMemory depth copy"); + owned->memory = GetBufferPool(context)->AllocateMemory(allocation, "vkAllocateMemory depth copy"); Check(context.Function("vkBindImageMemory")(context.device, image, owned->memory, 0), "vkBindImageMemory depth copy"); const auto viewLevelCount = std::min(descriptor.lastLevel, descriptor.mipCount - 1u) - descriptor.baseLevel + 1u; @@ -1135,7 +1136,7 @@ StorageTexture::StorageTexture(const Context& context, TextureDetiler& detiler, allocation.allocationSize = requirements.size; memoryBytes = requirements.size; allocation.memoryTypeIndex = context.DeviceMemoryType(requirements.memoryTypeBits, requirements.size, "storage texture"); - Check(context.Function("vkAllocateMemory")(context.device, &allocation, nullptr, &memory), "vkAllocateMemory storage texture"); + memory = GetBufferPool(context)->AllocateMemory(allocation, "vkAllocateMemory storage texture"); Check(context.Function("vkBindImageMemory")(context.device, image, memory, 0), "vkBindImageMemory storage"); } catch (...) { release(); From b31dac8931bf4a3e778f006075bd97905a84f097 Mon Sep 17 00:00:00 2001 From: ee6aba54 <340958984+ee6aba54@users.noreply.github.com> Date: Wed, 14 Oct 2026 00:04:01 +0200 Subject: [PATCH 0249/1000] fix(agc-driver): release clean storage images before ones with pending results Evicting a storage image whose results are still on the GPU stores them to guest memory first, and that store needs a staging buffer: under memory pressure the release itself ran out of device memory and the title died on the nested allocation (PPSA15304 at 125 s on a 64 MiB write-back buffer). The release now takes clean images oldest first and turns to dirty ones only when the clean ones did not free enough; a failure inside the release is reported and ends it, leaving the outer allocation to retry with what was freed. (cherry picked from commit 013b5546da01493cdfd2ac35f237bb7e22d15947) --- .../Graphics/include/Texture.hpp | 1 + .../Graphics/src/ShaderResources.cpp | 63 +++++++++++-------- .../libSceAgcDriver/Graphics/src/Texture.cpp | 6 ++ .../tests/execution/StorageCacheBudget.cpp | 17 ++++- 4 files changed, 58 insertions(+), 29 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp index bc6efaf720..46e965dc8d 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp @@ -185,6 +185,7 @@ public: void MarkDirty(); void MarkSliceDirty(std::uint32_t slice); void Flush(); + bool Dirty() const; // Stores every pending image overlapping the range, except `except`; returns whether any was. // Stores into host-imported memory are recorded (not waited for): a CPU reader syncs afterwards. // Then the unit shadows over the range are published into their imports as `scope` says (see diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index 7f4a448f83..6131da75db 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -1155,35 +1155,46 @@ std::uint64_t ReleaseTextureCaches(std::uint64_t bytes) { if (storageCacheHeld != 0 || !gpu.try_lock()) { storageSkipped = true; } else { - bool more = true; - while (more && freed < bytes) { - std::vector, std::uint64_t>> evicted; - { - if (!AcquireCache(cache.mutex)) { - storageSkipped = true; - break; + try { + bool dirtyToo = false; + while (freed < bytes) { + std::vector, std::uint64_t>> evicted; + { + if (!AcquireCache(cache.mutex)) { + storageSkipped = true; + break; + } + std::lock_guard lock(cache.mutex, std::adopt_lock); + std::uint64_t batch = 0; + for (auto it = cache.entries.end(); it != cache.entries.begin() && batch + freed < bytes;) { + --it; + if (!dirtyToo && it->texture->Dirty()) continue; + const auto accounted = it->accounted; + const auto next = std::next(it); + evicted.emplace_back(evictStorage(cache, it), accounted); + batch += accounted; + it = next; + } + if (evicted.empty()) { + if (dirtyToo || cache.entries.empty()) break; + dirtyToo = true; + continue; + } } - std::lock_guard lock(cache.mutex, std::adopt_lock); - std::uint64_t batch = 0; - while (batch + freed < bytes && !cache.entries.empty()) { - const auto back = std::prev(cache.entries.end()); - const auto accounted = back->accounted; - evicted.emplace_back(evictStorage(cache, back), accounted); - batch += accounted; + for (auto& [image, accounted] : evicted) { + std::vector> resources; + SharedResourceCache().RemoveImage(image.get(), &resources); + if (auto* recorder = Recorder::Active(); recorder != nullptr && recorder->Recording()) { + for (auto& resource : resources) recorder->Keep(std::move(resource)); + } + resources.clear(); + ++storageEntries; + if (image.use_count() == 1) freed += accounted; + image.reset(); } - more = !cache.entries.empty(); - } - for (auto& [image, accounted] : evicted) { - std::vector> resources; - SharedResourceCache().RemoveImage(image.get(), &resources); - if (auto* recorder = Recorder::Active(); recorder != nullptr && recorder->Recording()) { - for (auto& resource : resources) recorder->Keep(std::move(resource)); - } - resources.clear(); - ++storageEntries; - if (image.use_count() == 1) freed += accounted; - image.reset(); } + } catch (const std::exception& error) { + std::fprintf(stderr, "[gpu] texture cache release stopped: %s\n", error.what()); } } } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index 25f5fee3d8..cc82cd1e07 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -3171,6 +3171,12 @@ void StorageTexture::reconcilePending() { BumpPendingSerial(); } +bool StorageTexture::Dirty() const { + auto& pending = Pending(); + std::lock_guard lock(pending.mutex); + return dirty; +} + void StorageTexture::Flush() { { auto& live = Live(); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/StorageCacheBudget.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/StorageCacheBudget.cpp index 9e05f39278..e0845f8f53 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/StorageCacheBudget.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/StorageCacheBudget.cpp @@ -51,7 +51,7 @@ std::array TextureDescriptor(std::uint64_t address) { }; } -std::shared_ptr Store(AgcDriver::VulkanDevice& device, std::uint64_t base, std::uint32_t surface) { +std::shared_ptr Store(AgcDriver::VulkanDevice& device, std::uint64_t base, std::uint32_t surface, bool flush = true) { const auto address = base + surface * Stride; std::vector userData(16, 0u); const auto texture = TextureDescriptor(address); @@ -69,6 +69,7 @@ std::shared_ptr Store(AgcDriver::VulkanDevice& device, std::uint const auto result = ShaderRecompiler::Recompile(request); device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); device.WaitIdle(); + if (!flush) return StorageTexture::FindLive(address, SurfaceBytes); StorageTexture::FlushPending(address, static_cast(SurfaceBytes), nullptr, "test"); device.WaitIdle(); return StorageTexture::FindLive(address, SurfaceBytes); @@ -131,9 +132,19 @@ int main() { Require(StorageTexture::FindLive(base + 2u * Stride, SurfaceBytes) == nullptr && images[2].expired(), "the oldest cached storage image survived a release of its bytes"); Require(StorageTexture::FindLive(base + 3u * Stride, SurfaceBytes) != nullptr && !images[3].expired(), "a release of one image's bytes evicted more than the oldest image"); Require(StorageTexture::FindLive(base + (capacity + 1u) * Stride, SurfaceBytes) != nullptr, "a release of one image's bytes evicted the newest image"); + { + const auto dirty = Store(*device, base, 3, false); + Require(dirty != nullptr && dirty->Dirty(), "a store without a flush left no pending results on surface 3"); + Require(Store(*device, base, 4) != nullptr, "surface 4 is not cached"); + for (std::uint32_t surface = 5; surface < capacity + 2u; ++surface) Require(Store(*device, base, surface) != nullptr, "surface " + std::to_string(surface) + " is not cached"); + } + Require(AgcDriver::Graphics::ReleaseTextureCaches(held) >= held, "a release with a dirty oldest image freed nothing"); + Require(StorageTexture::FindLive(base + 3u * Stride, SurfaceBytes) != nullptr, "a release took the dirty oldest image before a clean one"); + Require(StorageTexture::FindLive(base + 4u * Stride, SurfaceBytes) == nullptr && images[4].expired(), "a release did not take the oldest clean image"); const auto rest = AgcDriver::Graphics::ReleaseTextureCaches(~std::uint64_t{0}); - Require(rest >= held * (capacity - 1u), "a release of everything freed " + std::to_string(rest >> 20u) + " MiB of the " + std::to_string((held * (capacity - 1u)) >> 20u) + " MiB cached"); - for (std::uint32_t surface = 0; surface < capacity + 2u; ++surface) Require(StorageTexture::FindLive(base + surface * Stride, SurfaceBytes) == nullptr && images[surface].expired(), "surface " + std::to_string(surface) + " survived a release of everything"); + Require(rest >= held * (capacity - 3u), "a release of everything freed " + std::to_string(rest >> 20u) + " MiB of the " + std::to_string((held * (capacity - 3u)) >> 20u) + " MiB cached"); + device->WaitIdle(); + for (std::uint32_t surface = 0; surface < capacity + 2u; ++surface) Require(StorageTexture::FindLive(base + surface * Stride, SurfaceBytes) == nullptr && (surface == 3u || images[surface].expired()), "surface " + std::to_string(surface) + " survived a release of everything"); Require(AgcDriver::Graphics::ReleaseTextureCaches(held) == 0, "a release from empty caches reported freed bytes"); } catch (...) { release(); From d6b002887ca29ac29303be7ebb58afff39d65545 Mon Sep 17 00:00:00 2001 From: ee6aba54 <340958984+ee6aba54@users.noreply.github.com> Date: Tue, 13 Oct 2026 23:55:44 +0200 Subject: [PATCH 0250/1000] fix(agc-driver): size the staging pool from the device's reported memory budget The device tier of the buffer pool kept an eighth of the device-local heap (2 GiB on a 16 GiB card) whatever VK_EXT_memory_budget said the process may use. Its budget is now the smaller of an eighth of the heap and an eighth of the reported budget, at least 512 MiB; APS5_STAGING_POOL_MIB still overrides it. (cherry picked from commit b039e1da12d7cedc3bfdf0c1d3c0c354f3cd58aa) --- .../Graphics/include/BufferPool.hpp | 3 ++- .../Graphics/src/BufferPool.cpp | 27 ++++++++++++------- .../prx/libSceAgcDriver/tests/Graphics.cpp | 10 +++++++ 3 files changed, 30 insertions(+), 10 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/BufferPool.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/BufferPool.hpp index 1e8a24b239..4e52e01226 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/BufferPool.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/BufferPool.hpp @@ -55,7 +55,7 @@ struct SlabSlot { class BufferPool { public: explicit BufferPool(const Context& context); - static VkDeviceSize DeviceBudget(const VkPhysicalDeviceMemoryProperties& memory); + static VkDeviceSize DeviceBudget(const VkPhysicalDeviceMemoryProperties& memory, const VkPhysicalDeviceMemoryBudgetPropertiesEXT* reported = nullptr); using CacheReclaim = VkDeviceSize (*)(VkDeviceSize bytes); static void SetCacheReclaim(CacheReclaim reclaim); static VkDeviceSize ReclaimTarget(VkDeviceSize bytes); @@ -120,6 +120,7 @@ private: // when shared; the device tier for device-local memory while it has a budget). Tier& tierFor(std::size_t capacity, VkMemoryPropertyFlags properties); static bool DeviceTierEnabled(); + static VkDeviceSize deviceBudget(const Context& context); static bool DeviceTiered(VkMemoryPropertyFlags properties); static std::size_t NextCapacity(std::size_t capacity); VkDeviceSize destroy(const BufferAllocation& allocation) noexcept; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp index acd629912c..ca001a763d 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp @@ -40,7 +40,7 @@ constexpr VkBufferUsageFlags deviceUsage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | BufferPool::BufferPool(const Context& context) : device(context.device), unmap(context.Function("vkUnmapMemory")), destroyBuffer(context.Function("vkDestroyBuffer")), freeMemory(context.Function("vkFreeMemory")), allocateMemory(context.Function("vkAllocateMemory")), mapMemory(context.Function("vkMapMemory")), maxSlots(std::max(1, std::min(MaxSlots(), context.limits.maxMemoryAllocationCount / 6u))) { smallTier.budget = smallBudget; largeTier.budget = budget; - deviceTier.budget = DeviceTierEnabled() ? DeviceBudget(context.memory) : 0; + deviceTier.budget = deviceBudget(context); } BufferPool::~BufferPool() { @@ -199,18 +199,27 @@ bool BufferPool::DeviceTierEnabled() { return enabled; } -VkDeviceSize BufferPool::DeviceBudget(const VkPhysicalDeviceMemoryProperties& memory) { +VkDeviceSize BufferPool::DeviceBudget(const VkPhysicalDeviceMemoryProperties& memory, const VkPhysicalDeviceMemoryBudgetPropertiesEXT* reported) { static const char* value = std::getenv("APS5_STAGING_POOL_MIB"); if (value != nullptr) return static_cast(std::strtoull(value, nullptr, 10)) << 20u; - VkDeviceSize largest = 0; - bool systemMemory = false; + std::optional largest; for (std::uint32_t heap = 0; heap < memory.memoryHeapCount && heap < VK_MAX_MEMORY_HEAPS; ++heap) { - if ((memory.memoryHeaps[heap].flags & VK_MEMORY_HEAP_DEVICE_LOCAL_BIT) == 0 || memory.memoryHeaps[heap].size <= largest) continue; - largest = memory.memoryHeaps[heap].size; - systemMemory = systemMemoryHeap(memory, heap); + if ((memory.memoryHeaps[heap].flags & VK_MEMORY_HEAP_DEVICE_LOCAL_BIT) != 0 && (!largest || memory.memoryHeaps[heap].size > memory.memoryHeaps[*largest].size)) largest = heap; } - if (systemMemory) return std::max(VkDeviceSize{128} << 20u, largest / 64u); - return std::max(VkDeviceSize{512} << 20u, largest / 8u); + VkDeviceSize size = largest ? memory.memoryHeaps[*largest].size : 0; + if (largest && reported != nullptr && reported->heapBudget[*largest] != 0) size = std::min(size, reported->heapBudget[*largest]); + if (largest && systemMemoryHeap(memory, *largest)) return std::max(VkDeviceSize{128} << 20u, size / 64u); + return std::max(VkDeviceSize{512} << 20u, size / 8u); +} + +VkDeviceSize BufferPool::deviceBudget(const Context& context) { + if (!DeviceTierEnabled()) return 0; + if (context.memoryProperties2 == nullptr || context.physical == VK_NULL_HANDLE) return DeviceBudget(context.memory); + VkPhysicalDeviceMemoryBudgetPropertiesEXT reported{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_BUDGET_PROPERTIES_EXT}; + VkPhysicalDeviceMemoryProperties2 properties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_PROPERTIES_2}; + properties.pNext = &reported; + context.memoryProperties2(context.physical, &properties); + return DeviceBudget(context.memory, &reported); } VkDeviceSize BufferPool::destroy(const BufferAllocation& allocation) noexcept { diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index a54ec69a9f..7091d6016d 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -6477,6 +6477,16 @@ void stagingPoolBudgetTests() { Require(AgcDriver::Graphics::BufferPool::DeviceBudget(memory) == VkDeviceSize{128} << 20u, "the staging pool budget on system memory fell below 128 MiB"); memory.memoryTypes[0] = {VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, 1}; Require(AgcDriver::Graphics::BufferPool::DeviceBudget(memory) == VkDeviceSize{512} << 20u, "a heap with a device-only memory type was taken for system memory"); + memory.memoryHeaps[1].size = VkDeviceSize{16} << 30u; + VkPhysicalDeviceMemoryBudgetPropertiesEXT reported{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_BUDGET_PROPERTIES_EXT}; + Require(AgcDriver::Graphics::BufferPool::DeviceBudget(memory, &reported) == VkDeviceSize{2} << 30u, "a zero reported budget did not fall back to an eighth of the heap"); + reported.heapBudget[0] = VkDeviceSize{1} << 30u; + reported.heapBudget[1] = VkDeviceSize{10} << 30u; + Require(AgcDriver::Graphics::BufferPool::DeviceBudget(memory, &reported) == VkDeviceSize{1280} << 20u, "a 10 GiB budget on the 16 GiB heap does not limit the staging pool to an eighth of the budget"); + reported.heapBudget[1] = VkDeviceSize{2} << 30u; + Require(AgcDriver::Graphics::BufferPool::DeviceBudget(memory, &reported) == VkDeviceSize{512} << 20u, "a 2 GiB budget does not keep the 512 MiB floor"); + reported.heapBudget[1] = VkDeviceSize{40} << 30u; + Require(AgcDriver::Graphics::BufferPool::DeviceBudget(memory, &reported) == VkDeviceSize{2} << 30u, "a budget above the heap size raised the staging pool past an eighth of the heap"); } void meshArgumentTests() { From 896b7377007cae8685ecc74948176f6a9dfb922d Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Thu, 15 Oct 2026 02:34:12 -0700 Subject: [PATCH 0251/1000] fix(libSceNet): keep one receive timeout across the wait for a local address When the address came during the wait, the host receive that followed waited its whole SO_RCVTIMEO again, so a receive could take up to twice the timeout. FreeBSD's soreceive_dgram sleeps once per timeout. Now the receive waits for a datagram with WSAPoll only for what is left of the timeout, and fails with EAGAIN when it runs out, as before. Nothing changes when the socket already had an address or has no timeout. --- core/libs/prx/libSceNet/Export.cpp | 16 ++++++++++++++-- core/libs/tests/GuestSceNet.cpp | 20 ++++++++++++++++++++ docs/dev/debt/libSceNet.md | 2 +- 3 files changed, 35 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index 59a8397734..cab6c60783 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -507,6 +507,17 @@ void note_local_address(int s, const Sock& socket) { constexpr int RECEIVE_NOTHING = -1; +int wait_for_datagram(const Sock& socket, std::chrono::steady_clock::time_point deadline) { + if (deadline == std::chrono::steady_clock::time_point::max()) return 0; + const auto now = std::chrono::steady_clock::now(); + if (now >= deadline) return NET_EAGAIN; + const auto remaining = std::chrono::ceil(deadline - now).count(); + WSAPOLLFD descriptor{}; + descriptor.fd = socket.native->value; + descriptor.events = POLLRDNORM; + return WSAPoll(&descriptor, 1, static_cast(std::min(remaining, INT_MAX))) == 0 ? NET_EAGAIN : 0; +} + int wait_for_local_address(int s, const Sock& socket, bool empty) { if (socket.type == NET_SOCK_STREAM || socket.bound) return 0; const bool named = has_local_address(socket.native->value); @@ -516,12 +527,13 @@ int wait_for_local_address(int s, const Sock& socket, bool empty) { ? std::chrono::steady_clock::now() + std::chrono::microseconds(socket.rcv_timeout_us) : std::chrono::steady_clock::time_point::max(); std::unique_lock lk(g_mutex); - for (;;) { + for (bool waited = false;; waited = true) { const auto it = g_socks.find(s); if (it == g_socks.end() || it->second.native != socket.native || it->second.aborted) return 0; if (named || has_local_address(socket.native->value)) { it->second.bound = true; - return 0; + lk.unlock(); + return waited ? wait_for_datagram(socket, deadline) : 0; } if (it->second.read_shut) return RECEIVE_NOTHING; const auto now = std::chrono::steady_clock::now(); diff --git a/core/libs/tests/GuestSceNet.cpp b/core/libs/tests/GuestSceNet.cpp index 19d5cf9cae..c615c0a91a 100644 --- a/core/libs/tests/GuestSceNet.cpp +++ b/core/libs/tests/GuestSceNet.cpp @@ -374,6 +374,25 @@ static void CheckUnboundDatagramLaterAddress() { } } +static void CheckUnboundDatagramTimeoutAfterAddress() { + const int waiting = sceNetSocket("unbound-timeout", 2, 2, 17); + Require(waiting >= 0); + const int timeout = 500000; + Require(sceNetSetsockopt(waiting, 0xffff, 0x1106, &timeout, sizeof(timeout)) == 0); + auto bound = std::async(std::launch::async, [waiting] { + std::this_thread::sleep_for(std::chrono::milliseconds(300)); + std::array address{16, 2, 0, 0, 127, 0, 0, 1}; + return sceNetBind_nid_postfix(waiting, address.data(), address.size()) == 0; + }); + char buffer[4]; + const auto start = std::chrono::steady_clock::now(); + Require(Failed(sceNetRecv(waiting, buffer, sizeof(buffer), 0), 35)); + const auto elapsed = std::chrono::steady_clock::now() - start; + Require(bound.get()); + Require(elapsed >= std::chrono::milliseconds(450) && elapsed < std::chrono::milliseconds(750)); + Require(sceNetSocketClose(waiting) == 0); +} + static void CheckEmptyEpoll() { const int epoll = sceNetEpollCreate("empty-epoll", 0); Require(epoll >= 0); @@ -950,6 +969,7 @@ int main() { CheckUnboundDatagramReceive(); CheckUnboundDatagramShutdown(); CheckUnboundDatagramLaterAddress(); + CheckUnboundDatagramTimeoutAfterAddress(); CheckUnspecifiedIpv6(); if (!ipv6Unavailable) CheckIpv6SocketsAreIpv6Only(); CheckDatagramDisconnect(); diff --git a/docs/dev/debt/libSceNet.md b/docs/dev/debt/libSceNet.md index b6c761b698..2066dad6f3 100644 --- a/docs/dev/debt/libSceNet.md +++ b/docs/dev/debt/libSceNet.md @@ -13,6 +13,6 @@ - [sceNetGetsockopt](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) reads `SO_ERROR` from the host socket. Winsock does not clear a socket's pending error when it is read, so on Windows the library reports a pending error once and shows it again only after a new `sceNetConnect`; a second failure with the same code on the same socket without a `sceNetConnect` in between would be hidden. - [sceNetSendmsg and sceNetRecvmsg](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) copy the scatter/gather list through one host buffer and accept only the flags `sceNetSend`/`sceNetRecv` accept. Sending control data throws; a receive never returns control data (`msg_controllen` 0), since no socket option that queues it is supported - [MSG_DONTWAIT](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) in `sceNetSend`, `sceNetSendto`, `sceNetRecv`, `sceNetRecvfrom`, `sceNetRecvmsg` and `sceNetSendmsg` makes that one call non-blocking through the host's `MSG_DONTWAIT`, as FreeBSD 11.4's `soreceive_generic` returns `EWOULDBLOCK` for it (`sys/kern/uipc_socket.c`); the value `0x80` is FreeBSD's (`sys/sys/socket.h`) and [shadPS4's](https://github.com/shadps4-emu/shadPS4/blob/e0a466753a1aacbc8db8253be685bf7ab4d9daae/src/core/libraries/net/net_types.h#L104). Winsock has no per-call non-blocking flag, so on Windows these calls still fail with `EOPNOTSUPP` for it -- [sceNetRecv, sceNetRecvfrom and sceNetRecvmsg](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) on a datagram socket with no local address wait on Windows until a bind, connect or send gives it one, checking the host socket every 10 ms. Winsock fails such a receive with `WSAEINVAL`, where FreeBSD 11.4's `soreceive_dgram` waits for a datagram (`sys/kern/uipc_socket.c`). A non-blocking socket fails with `EAGAIN` at once, and the receive timeout ends the wait with `EAGAIN`; when the address comes first, the host receive that follows waits the whole timeout again, so the call can take up to twice the timeout. A `sceNetShutdown` of the receive side ends the wait and the receive returns 0, and a zero-length receive returns 0 without waiting, as `soreceive_dgram` does when the socket can't receive more or nothing was asked for. An abort or close ends the wait, and the host receive then fails as it did before +- [sceNetRecv, sceNetRecvfrom and sceNetRecvmsg](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) on a datagram socket with no local address wait on Windows until a bind, connect or send gives it one, checking the host socket every 10 ms. Winsock fails such a receive with `WSAEINVAL`, where FreeBSD 11.4's `soreceive_dgram` waits for a datagram (`sys/kern/uipc_socket.c`). A non-blocking socket fails with `EAGAIN` at once, and the receive timeout ends the wait with `EAGAIN`; when the address comes first, the call waits for a datagram only for what is left of the timeout. A `sceNetShutdown` of the receive side ends the wait and the receive returns 0, and a zero-length receive returns 0 without waiting, as `soreceive_dgram` does when the socket can't receive more or nothing was asked for. An abort or close ends the wait, and the host receive then fails as it did before - [sceNetResolverAbort and sceNetResolverStartNtoaMultipleRecords](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) follow shadPS4 ([net.cpp](https://github.com/shadps4-emu/shadPS4/blob/811bfcd7d17ed8ae975c477d9ff28abc744e1ca8/src/core/libraries/net/net.cpp), [net_resolver.cpp](https://github.com/shadps4-emu/shadPS4/blob/811bfcd7d17ed8ae975c477d9ff28abc744e1ca8/src/core/libraries/net/net_resolver.cpp)); the `(int rid, int flags)` signature of the abort also matches [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Socket.cs). Unverified on hardware. Lookups run synchronously over the host resolver, which cannot be interrupted: an abort while a lookup runs on another thread returns at once, and that lookup fails with `EINTR` without writing its output only when the host call returns; the abort flags are then ignored, as shadPS4 does for a running lookup. With nothing running, abort flag 1 (NTOA preservation) or 2 (ATON preservation) makes the next `sceNetResolverStartNtoa*` or `sceNetResolverStartAton` call on that resolver fail with `EINTR`, reported by `sceNetResolverGetError`, after its arguments are checked. Unlike shadPS4, which ignores them, other abort flag bits throw, and `sceNetResolverStartNtoaMultipleRecords` throws for any flag, including async (1) and 0x10000 (IP address literals disabled), as `sceNetResolverStartNtoaMultipleRecordsEx` does - [sceNetResolverStartNtoa6](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) resolves a host name to its first IPv6 address through the host resolver (`getaddrinfo` with `AF_INET6`) and writes the 16-byte address; the `(rid, hostname, addr, timeout, retry, flags)` signature follows `sceNetResolverStartNtoa`. A name with no IPv6 address fails with `RESOLVER_ENODNS` (`0x804101E1`) like a failed lookup; whether the console returns IPv4-mapped addresses instead is not verified. Timeout and retry are ignored as in `sceNetResolverStartNtoa`, and any nonzero flags throw because their meaning for this call is not known. Unverified on hardware. From c9d3ff87d9dca340008ff76de7ab7319e282aa92 Mon Sep 17 00:00:00 2001 From: Parth Bhanti Date: Thu, 15 Oct 2026 03:04:18 -0700 Subject: [PATCH 0252/1000] fix(libkernel_sys): handle AT_SYMLINK_NOFOLLOW natively on macOS and document silent stub --- core/libs/prx/libkernel_sys/Export.cpp | 9 +++++++-- docs/dev/debt/libkernel_sys.md | 5 ++++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libkernel_sys/Export.cpp b/core/libs/prx/libkernel_sys/Export.cpp index 875d64f81a..481ff4b823 100644 --- a/core/libs/prx/libkernel_sys/Export.cpp +++ b/core/libs/prx/libkernel_sys/Export.cpp @@ -491,9 +491,14 @@ int APS5_VABI fchmodat_nid_postfix(int fd, const char* path, int mode, int flag) const bool symbolic = std::filesystem::is_symlink(std::filesystem::symlink_status(native, status)); #endif if (symbolic) { - RecordWrittenPath_nid_no_patch(native); - return 0; +#ifdef __APPLE__ + if (::fchmodat(AT_FDCWD, native.c_str(), static_cast(mode & 07777), AT_SYMLINK_NOFOLLOW) != 0) { + return static_cast(Fail(GuestFileError(errno))); } +#endif + RecordWrittenPath_nid_no_patch(native); + return 0; + } } #ifdef _WIN32 if (::_wchmod(native.wstring().c_str(), mode & 07777) != 0) return static_cast(Fail(GuestFileError(errno))); diff --git a/docs/dev/debt/libkernel_sys.md b/docs/dev/debt/libkernel_sys.md index 8fb1dba028..4caf7940b7 100644 --- a/docs/dev/debt/libkernel_sys.md +++ b/docs/dev/debt/libkernel_sys.md @@ -1,10 +1,13 @@ # libkernel_sys technical debt +### Silent stubs +- [fchmodat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) - `AT_SYMLINK_NOFOLLOW` on a symbolic link (on Windows and Linux) + ### Functional - [fchdir](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) changes the guest working directory to the descriptor's directory: on Windows through the path recorded when the descriptor was opened, on Linux through the `/proc/self/fd` link target. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory. FreeBSD `change_dir`'s search-permission check (`EACCES`) is not modelled, and a directory renamed after it was opened is still reached through the recorded path. Console behaviour is unverified - [openat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `open`. Any other relative path is joined to the descriptor's host directory, found the same way as for `fchdir`, so guest path aliases and case-insensitive name matching do not apply to it, and a `..` component is not kept inside the guest root. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory. As with `open`, `O_NOFOLLOW` and `O_NONBLOCK` are ignored. PPSA99003 opens directory entries this way. Console behaviour is unverified -- [fchmodat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `chmod`; any other relative path is joined to the descriptor's host directory the same way as for `openat`, so guest path aliases and case-insensitive name matching do not apply to it and a `..` component is not kept inside the guest root. A flag other than `0` or `AT_SYMLINK_NOFOLLOW` (0x200) returns `EINVAL`; the mode is masked to `07777`. With `AT_SYMLINK_NOFOLLOW` a symbolic link throws, since the host cannot change a link's own mode, while a regular file is changed either way. The mode maps to the host file mode, which on Windows is only the CRT's read-only bit. PPSA99621 calls it through `std::filesystem::permissions`. Console behaviour is unverified +- [fchmodat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `chmod`; any other relative path is joined to the descriptor's host directory the same way as for `openat`, so guest path aliases and case-insensitive name matching do not apply to it and a `..` component is not kept inside the guest root. A flag other than `0` or `AT_SYMLINK_NOFOLLOW` (0x200) returns `EINVAL`; the mode is masked to `07777`. With `AT_SYMLINK_NOFOLLOW` a symbolic link returns success silently on Windows and Linux, since the host cannot change a link's own mode, while a regular file is changed either way. The mode maps to the host file mode, which on Windows is only the CRT's read-only bit. PPSA99621 calls it through `std::filesystem::permissions`. Console behaviour is unverified - [pathconf](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) looks the path up as FreeBSD 11's `kern_pathconf` does (links followed, `ENAMETOOLONG` for a path of 1024 bytes or more or a component over 255) and returns the constants of FreeBSD 11's UFS `ufs_pathconf` for files and directories alike: `_PC_LINK_MAX` 32767, `_PC_NAME_MAX` 255, `_PC_PATH_MAX` and `_PC_SYMLINK_MAX` 1024, `_PC_PIPE_BUF` 512, `_PC_CHOWN_RESTRICTED` and `_PC_NO_TRUNC` 1, `_PC_FILESIZEBITS` 64, `_PC_REC_MAX_XFER_SIZE` -1, `_PC_PRIO_IO` and `_PC_SYNC_IO` 0. Whether the console's filesystems report the same is not verified. It throws for the names whose UFS value depends on the mount (`_PC_ALLOC_SIZE_MIN`, `_PC_REC_INCR_XFER_SIZE`, `_PC_REC_MIN_XFER_SIZE`, `_PC_MIN_HOLE_SIZE`), the kernel options (`_PC_ACL_EXTENDED`, `_PC_ACL_PATH_MAX`, `_PC_ACL_NFS4`, `_PC_MAC_PRESENT`) or the page size (`_PC_REC_XFER_ALIGN`), and on which UFS and `vop_stdpathconf` disagree (`_PC_MAX_CANON`, `_PC_MAX_INPUT`, `_PC_VDISABLE`, `_PC_ASYNC_IO`); other names fail with `EINVAL` - [mkdirat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `mkdir`. Any other relative path is joined to the descriptor's host directory, found the same way as for `fchdir`, so guest path aliases and case-insensitive name matching do not apply to it, and a `..` component is not kept inside the guest root. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory. On Windows, `mode` is ignored. Console behaviour is unverified - [readlink](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) on Windows reads a symbolic link's target from its reparse data and returns it with `/` separators, only when the target is relative, as `symlink` creates them. A symbolic link with an absolute target and a junction throw, as a host path has no guest form. Any other reparse point throws too, where FreeBSD reports `EINVAL` for a file that is not a link From 7d7515b6329d2b9ea9c1e928ae7e199c7543070f Mon Sep 17 00:00:00 2001 From: hypermist Date: Fri, 16 Oct 2026 00:14:25 +1300 Subject: [PATCH 0253/1000] docs(debt): drop the call tracing entry that --trace-imports superseded Co-Authored-By: Claude Opus 5.5 --- docs/dev/debt/libc.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index e92d357f7f..fb1c697e8c 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -69,7 +69,6 @@ - [wcsftime](../../../core/libs/prx/libc/src/Time.cpp) (libc) follows FreeBSD 11.4's [wcsftime.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/locale/wcsftime.c) over this library's `wcsrtombs`, `strftime` and `mbsrtowcs` in the single-byte C locale of [none.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/locale/none.c), with the guest's 16-bit `wchar_t`, without console verification. It has the `strftime` gaps above. A format unit above 255 returns 0 with `EILSEQ`; an empty result, a result that does not fit and a null buffer return 0 with `errno` unchanged, discarding the `ERANGE` or `EINVAL` that the Windows CRT's `strftime` sets. A null format or `tm` throws, where FreeBSD dereferences it (a null `tm` is read there only when the format has a conversion that needs it) - [syslog](../../../core/libs/prx/libc/src/Syslog.cpp) (libc) writes each message to `stderr` as `[syslog:] `, with a newline added when missing, instead of sending it to the console's system log. Priority and facility handling follow FreeBSD's `vsyslog`: `%m` expands to the guest errno's message, unknown bits are reported (as priority 0x23, since FreeBSD passes `LOG_ERR|LOG_CONS|LOG_PERROR|LOG_PID` as the priority) and masked, a missing facility becomes `LOG_USER` or the facility set by `openlog`, and errno is preserved. `openlog` applies its identity and `LOG_PID`; a null identity retains the previous one, and a valid nonzero facility replaces the default. `closelog` clears the identity but retains the options and facility, as in FreeBSD 11; when no identity is set, FreeBSD falls back to the program name, which this backend does not emit. `LOG_PERROR` has no extra effect because stderr is the emulated log sink; `LOG_NDELAY`/`LOG_ODELAY`, `LOG_NOWAIT` and `LOG_CONS` have no daemon, child or console backend to control. `setlogmask` follows [FreeBSD 11](https://github.com/freebsd/freebsd-src/blob/8d2708de5d6112a492744d367cfdfb8fd219227a/lib/libc/gen/syslog.c): the process-wide mask defaults to 0xff, zero queries it, and nonzero values replace it; masked messages are skipped before formatting, including the unknown-priority diagnostic. The console's mask behaviour is unverified - [daemon](../../../core/libs/prx/libc/src/Process.cpp) (libc) throws: forking into the background is not supported. PPSA85300 imports it and fails to load without it. VLC 3.0 calls it only when its `daemon` option is set ([libvlc.c](https://github.com/videolan/vlc/blob/6de05adcbaf2e8b85fe86aad4169393098628119/src/libvlc.c#L190-L197)), and the title starts libVLC with `--ignore-config` and without `--daemon` ([player.cc](https://github.com/ZiZc3/VLC/blob/f3253020a64368153233ede29183014c0b6c8979/src/player.cc#L960-L981)) -- [Call tracing](../../../core/libs/prx/libc/src/specifics/windows/CallTrace.cpp) (`APS5_TRACE_CALLS`) exists on Windows only and records the entry of a call, not its arguments or its result. Nothing passes imports through `TraceImport_nid_no_patch` yet, so no title is traced. One process-wide ring keeps the last 65536 calls, so a thread that calls often pushes the others out. Calls are also counted per traced target since start-up; an import bound to a target already traced under another name is counted under the first name. The thunk saves the integer argument registers and `xmm0` to `xmm7`; arguments in wider vector registers pass only as long as the recording code stays free of AVX instructions, which is not tested - [_WStoul](../../../core/libs/prx/libc/src/Strings.cpp), [_Iswctype](../../../core/libs/prx/libc/src/RuneLocale.cpp), [swscanf_s and swscanf](../../../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. `swscanf` runs the same scanner without the size arguments of `%s`, `%c` and `%[`, as FreeBSD's [swscanf.c](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/lib/libc/stdio/swscanf.c) passes its arguments to `vswscanf`, so everything above except the sizes applies to it; it also keeps the null buffer, format and receiving pointer checks returning `EOF`, where C leaves those undefined. Neither variant is confirmed on a PS5 title - [flock](../../../core/libs/prx/libc/src/FileLock.cpp) (libkernel) on Windows shares one lock state between duplicated descriptors, following FreeBSD 11.4's [flock(2)](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libc/sys/flock.2). Independent opens have separate states; closing a descriptor or reopening a stream drops only its reference. Lock transitions on one open file description are serialized. The lock is a host [LockFileEx](https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-lockfileex) lock on one byte at offset 2^63 - 2, which no file reaches, so it excludes other `flock` calls and leaves reads, writes and mapped views alone, like FreeBSD's advisory locks. Console behavior, signal interruption and concurrent closure of a descriptor while its lock call is pending are unverified. - [std::_Throw_Cpp_error](../../../core/libs/prx/libc/src/exception/StandardExceptions.cpp) (libSceAmpr) follows Dinkumware's `thread0.cpp` as shipped with [Visual C++ 2013](https://github.com/ganboing/crt.vc12/blob/f1d47126bf13d1c9dccaa6c53b72217fd0474968/src/thread0.cpp) and in [microsoft/STL](https://github.com/microsoft/STL/blob/da52dc0fcadbd83690cfd1ed32408d2847439493/stl/src/thread0.cpp), without console verification. `_Cpp_error` codes 0 to 6 throw a guest `std::system_error` whose `error_code` holds the FreeBSD errno of the `errc` value (EBUSY 16, EINVAL 22, ESRCH 3, ENOMEM 12, EPERM 1, EDEADLK 11, EAGAIN 35) and the generic category, and whose `what()` is the table message, `": "` and the generic category's message for that errno, selected by the first matching errno entry in VC12's [`syserror.cpp`](https://github.com/ganboing/crt.vc12/blob/f1d47126bf13d1c9dccaa6c53b72217fd0474968/src/syserror.cpp); another code throws `std::out_of_range` on the host instead of reading past the table. The guest `system_error` is taken to be [VC12's](https://github.com/ganboing/crt.vc12/blob/f1d47126bf13d1c9dccaa6c53b72217fd0474968/src/system_error): the 16-byte `runtime_error` followed by `error_code {int, const error_category*}`, 32 bytes, so its copy constructor and assignment copy the code too. The generic category is a static libc object with VC12's `error_category` virtual order (destructors, `name`, `message`, `default_error_condition`, the two `equivalent`), not the console's `_Error_objects::_Generic_object`, and the new `generic_category()` export returns that object; a title's inline category object in a bundled libc can still have a different identity. Its `message` and direct message export construct the existing [GuestLocale::String](../../../core/libs/prx/libc/include/GuestLocale.hpp) model: a reserved eight-byte word, 16-byte inline buffer, length and capacity, with guest `operator new` storage above 15 bytes. This 40-byte string layout and virtual return ABI have not been verified against a console library. The deleting destructor still throws. `_Syserror_map` covers the 72 distinct Dinkumware `errc` values present in FreeBSD 11.4 [errno.h](https://github.com/freebsd/freebsd-src/blob/releng/11.4/sys/sys/errno.h), returning null for other values; category messages use `unknown error` for those. First-match aliases are `EAGAIN`/`EWOULDBLOCK` -> `operation would block` and `ENOTSUP`/`EOPNOTSUPP` -> `not supported`. STREAMS-only errno assignments absent from that header (`ENODATA`, `ENOSR`, `ENOSTR`, `ETIME`) are not inferred From d7ebcf77120af31d457bfd5c7ec95e301e0dd3df Mon Sep 17 00:00:00 2001 From: Ishmam Ahmed <89687266+IshmamDC217@users.noreply.github.com> Date: Wed, 14 Oct 2026 09:50:57 +0100 Subject: [PATCH 0254/1000] fix(relinker): check PT_LOAD and uncovered PT_SCE_RELRO file ranges ElfReader::ReadProgramHeaders checked that a PT_LOAD's file size does not exceed its memory size, but not that its file range lies inside the input. The Windows and macOS writers reject such a segment later; the Linux patcher keeps the original PT_LOADs and appends its own block at the old end of the file, so a load reaching past the input mapped the relinker's .dynstr, .dynsym and entry stub into the guest segment. A PT_SCE_RELRO that no PT_LOAD covers is turned into a PT_LOAD after the per-header checks, so it skipped them: a file size larger than its memory size was accepted for Linux, and Windows and macOS reported a file-range error instead. Reject a PT_LOAD whose file range exceeds the input, and run the same two checks on an uncovered PT_SCE_RELRO before converting it. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01P9JfvrCsztJtbkd5SATAzL --- .../relinker/src/parsing/ElfReader.cpp | 6 ++++ .../relinker/tests/ElfReaderBoundsTests.cpp | 29 +++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/core/relinker/relinker/src/parsing/ElfReader.cpp b/core/relinker/relinker/src/parsing/ElfReader.cpp index cebff9d83a..27106ec1ac 100644 --- a/core/relinker/relinker/src/parsing/ElfReader.cpp +++ b/core/relinker/relinker/src/parsing/ElfReader.cpp @@ -137,6 +137,8 @@ std::vector ElfReader::ReadProgramHeaders() const { if (ph.Type == PT_LOAD && ph.FileSize > ph.MemorySize) throw RelinkerException("PT_LOAD file size exceeds memory size", ph.Offset); + if (ph.Type == PT_LOAD && !_rangeFits(ph.Offset, ph.FileSize, _fileBuffer.size())) + throw RelinkerException("Segment offset out of bounds", ph.Offset); headers.push_back(ph); offset += header.ProgramHeaderEntrySize; @@ -154,6 +156,10 @@ std::vector ElfReader::ReadProgramHeaders() const { covered = true; } if (!covered) { + if (relro.FileSize > relro.MemorySize) + throw RelinkerException("PT_SCE_RELRO file size exceeds memory size", relro.Offset); + if (!_rangeFits(relro.Offset, relro.FileSize, _fileBuffer.size())) + throw RelinkerException("PT_SCE_RELRO file range exceeds the input file", relro.Offset); relro.Type = PT_LOAD; relro.Flags = PF_R | PF_W; } diff --git a/core/relinker/relinker/tests/ElfReaderBoundsTests.cpp b/core/relinker/relinker/tests/ElfReaderBoundsTests.cpp index 065688d6d0..081ece19d9 100644 --- a/core/relinker/relinker/tests/ElfReaderBoundsTests.cpp +++ b/core/relinker/relinker/tests/ElfReaderBoundsTests.cpp @@ -76,10 +76,39 @@ void RejectAddress(Bytes bytes, const std::uint64_t address) { throw std::runtime_error("Invalid file mapping was accepted"); } +void RejectProgramHeaders(Bytes bytes) { + try { + Relinker::ElfReader(std::move(bytes)).ReadProgramHeaders(); + } catch (const Relinker::RelinkerException&) { + return; + } + throw std::runtime_error("Invalid program header was accepted"); +} + } int main() { const std::vector>> cases = { + {"load file range past end of file", [] { + auto bytes = Fixture(); + Write(bytes, 96, 0xd0); + Write(bytes, 104, 0xd0); + RejectProgramHeaders(std::move(bytes)); + }}, + {"uncovered SCE RELRO file size exceeds memory size", [] { + auto bytes = Fixture(); + Write(bytes, 64, 0x61000010); + Write(bytes, 96, 64); + Write(bytes, 104, 32); + RejectProgramHeaders(std::move(bytes)); + }}, + {"uncovered SCE RELRO file range past end of file", [] { + auto bytes = Fixture(); + Write(bytes, 64, 0x61000010); + Write(bytes, 96, 0xd0); + Write(bytes, 104, 0xd0); + RejectProgramHeaders(std::move(bytes)); + }}, {"section entry size field", [] { const auto sections = Relinker::ElfReader(Fixture()).ReadSectionHeaders(); Require(sections.size() == 3 && sections[1].Name == ".text" && sections[1].EntrySize == 24); From 063cf4894b7c46ccf966b5478a868325f88ec068 Mon Sep 17 00:00:00 2001 From: Ishmam Ahmed <89687266+IshmamDC217@users.noreply.github.com> Date: Wed, 14 Oct 2026 15:17:19 +0100 Subject: [PATCH 0255/1000] fix(relinker): drop the PT_LOAD range check made redundant in translation ReadProgramHeaders now rejects a PT_LOAD whose file range exceeds the input, so TranslateVirtualAddress no longer needs its own check. An uncovered PT_SCE_RELRO outside the file reports the same "Segment offset out of bounds" as a PT_LOAD, and a test pins that a load or uncovered PT_SCE_RELRO ending exactly at the end of the file is still accepted. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01P9JfvrCsztJtbkd5SATAzL --- core/relinker/relinker/src/parsing/ElfReader.cpp | 8 ++------ core/relinker/relinker/tests/ElfReaderBoundsTests.cpp | 10 ++++++++++ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/core/relinker/relinker/src/parsing/ElfReader.cpp b/core/relinker/relinker/src/parsing/ElfReader.cpp index 27106ec1ac..b82abed93f 100644 --- a/core/relinker/relinker/src/parsing/ElfReader.cpp +++ b/core/relinker/relinker/src/parsing/ElfReader.cpp @@ -159,7 +159,7 @@ std::vector ElfReader::ReadProgramHeaders() const { if (relro.FileSize > relro.MemorySize) throw RelinkerException("PT_SCE_RELRO file size exceeds memory size", relro.Offset); if (!_rangeFits(relro.Offset, relro.FileSize, _fileBuffer.size())) - throw RelinkerException("PT_SCE_RELRO file range exceeds the input file", relro.Offset); + throw RelinkerException("Segment offset out of bounds", relro.Offset); relro.Type = PT_LOAD; relro.Flags = PF_R | PF_W; } @@ -267,12 +267,8 @@ std::vector ElfReader::ReadDynamicTags(const ProgramHeader& dynamicH FileByteOffset ElfReader::TranslateVirtualAddress(VirtualAddress address) const { for (const auto& segment : ReadProgramHeaders()) { if (segment.Type == PT_LOAD && address >= segment.MappedAddress && - address - segment.MappedAddress < segment.FileSize) { - if (!_rangeFits(segment.Offset, segment.FileSize, _fileBuffer.size())) { - throw RelinkerException("PT_LOAD file mapping out of bounds", segment.Offset); - } + address - segment.MappedAddress < segment.FileSize) return segment.Offset + (address - segment.MappedAddress); - } } throw RelinkerException("Virtual address not mapped by any PT_LOAD segment", address); diff --git a/core/relinker/relinker/tests/ElfReaderBoundsTests.cpp b/core/relinker/relinker/tests/ElfReaderBoundsTests.cpp index 081ece19d9..ef54dd3c96 100644 --- a/core/relinker/relinker/tests/ElfReaderBoundsTests.cpp +++ b/core/relinker/relinker/tests/ElfReaderBoundsTests.cpp @@ -95,6 +95,16 @@ int main() { Write(bytes, 104, 0xd0); RejectProgramHeaders(std::move(bytes)); }}, + {"load and uncovered SCE RELRO ending at end of file", [] { + for (const std::uint32_t type : {1u, 0x61000010u}) { + auto bytes = Fixture(); + Write(bytes, 64, type); + Write(bytes, 96, 0xc0); + Write(bytes, 104, 0xc0); + const auto headers = Relinker::ElfReader(std::move(bytes)).ReadProgramHeaders(); + Require(headers.size() == 1 && headers[0].Offset + headers[0].FileSize == 0x300); + } + }}, {"uncovered SCE RELRO file size exceeds memory size", [] { auto bytes = Fixture(); Write(bytes, 64, 0x61000010); From b9c91c89ba4b9f56d74bc2bf67c6533ec51152bc Mon Sep 17 00:00:00 2001 From: Niko Date: Tue, 13 Oct 2026 15:21:22 +0200 Subject: [PATCH 0256/1000] ci(build): accept merged pull requests in the relinker author check The eligibility job enabled the relinker matrix for a pull request only when repos.listCommits on main with the author's login returned a commit. That filter misses authors whose commits in main are not matched to their login. On 2026-10-13 it found no commit for 26 authors of open pull requests who have 1 to 44 pull requests merged into main, so their pull requests skip the relinker, against CONTRIBUTING.md ("if its author already has a commit in main"). When listCommits finds nothing, the job now also searches for the author's merged pull requests into main, with the query pr-intake.yml uses. Incomplete search results count as none. A failed search, such as the search API's own rate limit, skips the relinker with a warning instead of failing the job. Both requests now run only when the pull request changes a relinker file, so other pull requests make neither. test_build_eligibility.py runs the step's script in node with a mocked github object and skips when node is missing. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/build.yml | 32 ++++-- CMakeLists.txt | 2 + tools/tests/test_build_eligibility.py | 146 ++++++++++++++++++++++++++ 3 files changed, 171 insertions(+), 9 deletions(-) create mode 100644 tools/tests/test_build_eligibility.py diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 65302caba0..1b96d8fa7d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -140,7 +140,6 @@ jobs: const pulls = []; const paths = []; let truncated = false; - let relinker = manual; for (const pull_number of numbers) { let { data: pr } = await github.rest.pulls.get({ owner, repo, pull_number }); for (let attempt = 0; pr.mergeable === null && attempt < 10; ++attempt) { @@ -160,19 +159,34 @@ jobs: if (pr.mergeable === false) { throw new Error(`Pull request #${pull_number} does not merge cleanly into main; rebase it first`); } - if (!manual) { - const { data: commits } = await github.rest.repos.listCommits({ - owner, repo, sha: 'main', author: pr.user.login, per_page: 1 - }); - relinker = commits.length > 0; - } const files = await github.paginate(github.rest.pulls.listFiles, { owner, repo, pull_number, per_page: 100 }); truncated ||= files.length >= 3000; paths.push(...files.flatMap((file) => [file.filename, file.previous_filename].filter(Boolean))); pulls.push({ number: pull_number, head: pr.head.sha }); } const run = select(paths, truncated); - relinker = relinker && run.relinker; + let relinker = run.relinker; + if (relinker && !manual) { + const author = context.payload.pull_request.user.login; + const { data: commits } = await github.rest.repos.listCommits({ + owner, repo, sha: 'main', author, per_page: 1 + }); + relinker = commits.length > 0; + if (!relinker) { + try { + const { data: mergedPRs } = await github.rest.search.issuesAndPullRequests({ + q: `repo:${owner}/${repo} is:pr is:merged base:main author:${author}`, + per_page: 1 + }); + if (mergedPRs.incomplete_results) { + throw new Error('the search results are incomplete'); + } + relinker = mergedPRs.total_count > 0; + } catch (error) { + core.warning(`Skipping the relinker: could not search the merged pull requests of ${author}: ${error.message}`); + } + } + } const registerContext = run.registerContext; const writeWatch = run.writeWatch; const { data: main } = await github.rest.repos.getBranch({ owner, repo, branch: 'main' }); @@ -188,7 +202,7 @@ jobs: merged.forEach((line) => core.info(line)); await core.summary.addRaw( `${merged.join(', then ')}: full builds ${full ? 'enabled' : 'disabled'}, ` + - `relinker ${relinker ? (full ? (linux ? 'enabled on macOS (the full builds run the Linux and Windows relinker tests)' : 'enabled on Linux and macOS (the full Windows build runs the Windows relinker tests)') : 'enabled') : 'skipped: no relinker file changed or the author has no commit in main'}, ` + + `relinker ${relinker ? (full ? (linux ? 'enabled on macOS (the full builds run the Linux and Windows relinker tests)' : 'enabled on Linux and macOS (the full Windows build runs the Windows relinker tests)') : 'enabled') : 'skipped: no relinker file changed, or no commit or merged pull request in main was found for the author'}, ` + `register context tests ${registerContext ? 'enabled' : 'skipped'}, write watch tests ${writeWatch ? 'enabled' : 'skipped'}` ).write(); diff --git a/CMakeLists.txt b/CMakeLists.txt index 502a2cf90e..24425f7bd8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -123,6 +123,8 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_doc_links.py) add_test(NAME test_registration COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_test_registration.py) + add_test(NAME build_eligibility COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_build_eligibility.py) add_test(NAME check_skips COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_skips.py) add_test(NAME check_main_sync COMMAND ${Python3_EXECUTABLE} diff --git a/tools/tests/test_build_eligibility.py b/tools/tests/test_build_eligibility.py new file mode 100644 index 0000000000..b39ec9fabe --- /dev/null +++ b/tools/tests/test_build_eligibility.py @@ -0,0 +1,146 @@ +import json +import os +import shutil +import subprocess +import tempfile +import unittest +from pathlib import Path + +WORKFLOW = Path(__file__).resolve().parents[2] / ".github/workflows/build.yml" + +HARNESS = r""" +const fs = require('fs'); +const scenario = JSON.parse(fs.readFileSync(process.argv[2], 'utf8')); +const script = fs.readFileSync(process.argv[3], 'utf8'); +const calls = []; +const outputs = {}; +const warnings = []; +let summary = ''; +const github = { + rest: { + pulls: { + get: async () => ({ data: { state: 'open', base: { ref: 'main' }, head: { sha: 'head' }, mergeable: true, user: { login: 'author' } } }), + listFiles: async () => ({ data: [] }) + }, + repos: { + listCommits: async (params) => { + calls.push(['listCommits', params.sha, params.author]); + return { data: scenario.commits }; + }, + getBranch: async () => ({ data: { commit: { sha: 'main' } } }), + getCollaboratorPermissionLevel: async () => ({ data: { permission: 'write' } }) + }, + search: { + issuesAndPullRequests: async (params) => { + calls.push(['search', params.q]); + if (scenario.search === null) { + throw Object.assign(new Error('API rate limit exceeded'), { status: 403 }); + } + return { data: scenario.search }; + } + } + }, + paginate: async () => scenario.files.map((filename) => ({ filename })) +}; +const context = { + eventName: scenario.manual ? 'workflow_dispatch' : 'pull_request', + actor: 'maintainer', + repo: { owner: 'owner', repo: 'project' }, + payload: { pull_request: { number: 7, head: { sha: 'head' }, user: { login: 'author' } } } +}; +const core = { + setOutput: (name, value) => { outputs[name] = value; }, + info: () => {}, + warning: (message) => { warnings.push(String(message)); }, + summary: { addRaw: (text) => { summary += text; return { write: async () => {} }; } } +}; +const AsyncFunction = Object.getPrototypeOf(async () => {}).constructor; +new AsyncFunction('github', 'context', 'core', script)(github, context, core).then( + () => process.stdout.write(JSON.stringify({ outputs, warnings, calls, summary })), + (error) => { console.error(error); process.exit(1); } +); +""" + + +def gate_script(): + lines = WORKFLOW.read_text(encoding="utf-8").splitlines() + start = next(index for index, line in enumerate(lines) if line.strip() == "id: gate") + start = next(index for index in range(start, len(lines)) if lines[index].strip() == "script: |") + 1 + indent = len(lines[start]) - len(lines[start].lstrip()) + script = [] + for line in lines[start:]: + if line.strip() and len(line) - len(line.lstrip()) < indent: + break + script.append(line[indent:]) + return "\n".join(script) + + +class RelinkerEligibilityTests(unittest.TestCase): + def setUp(self): + self.node = shutil.which("node") + if not self.node: + self.skipTest("the eligibility script runs in node") + self.temp = tempfile.TemporaryDirectory() + self.root = Path(self.temp.name) + (self.root / "harness.js").write_text(HARNESS, encoding="utf-8") + (self.root / "gate.js").write_text(gate_script(), encoding="utf-8") + + def tearDown(self): + self.temp.cleanup() + + def run_gate(self, commits, search, files=("core/relinker/main.cpp",), manual=False): + scenario = self.root / "scenario.json" + scenario.write_text(json.dumps({"commits": commits, "search": search, "files": list(files), "manual": manual}), + encoding="utf-8") + environment = dict(os.environ, PR_NUMBER="7", GITHUB_TRIGGERING_ACTOR="maintainer") + result = subprocess.run([self.node, "harness.js", "scenario.json", "gate.js"], cwd=self.root, + capture_output=True, text=True, env=environment, timeout=20) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + return json.loads(result.stdout) + + def test_author_with_a_commit_in_main_runs_the_relinker(self): + result = self.run_gate([{"sha": "a"}], None) + self.assertIs(result["outputs"]["relinker"], True) + self.assertEqual(result["calls"], [["listCommits", "main", "author"]]) + + def test_author_with_only_merged_pull_requests_runs_the_relinker(self): + result = self.run_gate([], {"total_count": 12, "incomplete_results": False, "items": []}) + self.assertIs(result["outputs"]["relinker"], True) + self.assertEqual(result["calls"], [["listCommits", "main", "author"], + ["search", "repo:owner/project is:pr is:merged base:main author:author"]]) + self.assertEqual(result["warnings"], []) + + def test_author_without_merged_pull_requests_skips_the_relinker(self): + result = self.run_gate([], {"total_count": 0, "incomplete_results": False, "items": []}) + self.assertIs(result["outputs"]["relinker"], False) + self.assertEqual(result["warnings"], []) + self.assertIn("no commit or merged pull request in main was found for the author", result["summary"]) + + def test_failed_search_skips_the_relinker_with_a_warning(self): + result = self.run_gate([], None) + self.assertIs(result["outputs"]["relinker"], False) + self.assertEqual(result["outputs"]["sha"], "main") + self.assertEqual(len(result["warnings"]), 1) + self.assertIn("API rate limit exceeded", result["warnings"][0]) + + def test_incomplete_search_skips_the_relinker_with_a_warning(self): + result = self.run_gate([], {"total_count": 3, "incomplete_results": True, "items": []}) + self.assertIs(result["outputs"]["relinker"], False) + self.assertEqual(len(result["warnings"]), 1) + self.assertIn("incomplete", result["warnings"][0]) + + def test_pull_request_without_relinker_files_checks_no_author(self): + result = self.run_gate([], None, files=("core/libs/tests/write_watch/a.cpp",)) + self.assertIs(result["outputs"]["relinker"], False) + self.assertIs(result["outputs"]["write-watch"], True) + self.assertEqual(result["calls"], []) + + def test_manual_run_checks_no_author(self): + result = self.run_gate([], None, manual=True) + self.assertIs(result["outputs"]["relinker"], True) + self.assertEqual(result["calls"], []) + self.assertIs(self.run_gate([], None, files=("README.md",), manual=True)["outputs"]["relinker"], False) + + +if __name__ == "__main__": + unittest.main() From 5b7e3aea8b38e3f627366f53e0cb92757c18760e Mon Sep 17 00:00:00 2001 From: KernelPanicNT <76663133+KernelPanicNT@users.noreply.github.com> Date: Wed, 14 Oct 2026 14:40:58 +0000 Subject: [PATCH 0257/1000] feat(libSceAmpr): implement __cxa_call_unexpected __cxa_call_unexpected takes the in-flight exception's unwind header and never returns, as in libcxxabi. No std::set_unexpected is exported and the exception header's unexpected handler is never set, so the handler is always the default one, which calls std::terminate. It therefore forwards to libc's __cxa_call_terminate, which begins the catch and runs the current terminate handler; a null header terminates in libc's __cxa_begin_catch. libcxxabi's std::terminate would instead run the terminate handler saved in the exception header at throw time; the two differ only if std::set_terminate was called in between, which the debt entry records. The test shares the forked-child helper of the std::_Pad test, which now takes the terminate handler to install. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAmpr/Export.cpp | 6 +++--- core/libs/tests/GuestAmprThreads.cpp | 30 +++++++++++++++++++++++++--- docs/dev/debt/libSceAmpr.md | 1 + 3 files changed, 31 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libSceAmpr/Export.cpp b/core/libs/prx/libSceAmpr/Export.cpp index fba6d9efbe..88f264c00a 100644 --- a/core/libs/prx/libSceAmpr/Export.cpp +++ b/core/libs/prx/libSceAmpr/Export.cpp @@ -271,6 +271,7 @@ void APS5_VABI _Cnd_destroy_nid_postfix(void** handle); int APS5_VABI _Cnd_wait_nid_postfix(void** condition, void** mutex); int APS5_VABI _Cnd_signal_nid_postfix(void** handle); int APS5_VABI wcsrtombs_s_nid_postfix(std::size_t* result, char* destination, std::size_t capacity, const std::uint16_t** source, std::size_t limit, void* state); +[[noreturn]] void APS5_VABI __cxa_call_terminate_nid_postfix(void* exception); int APS5_VABI _Cnd_init_nid_postfix(void** handle); int SignalCondition_nid_no_patch(void** handle); [[noreturn]] void ThrowCppError_nid_no_patch(int code); @@ -994,9 +995,8 @@ int APS5_VABI wcstombs_s_nid_postfix(std::size_t* result, char* destination, std return wcsrtombs_s_nid_postfix(result, destination, capacity, &source, limit, state.data()); } -int APS5_VABI __cxa_call_unexpected_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +[[noreturn]] void APS5_VABI __cxa_call_unexpected_nid_postfix(void* unwindException) { + __cxa_call_terminate_nid_postfix(unwindException); } APS5_EXPORT("0hlfW1O4Aa4", sceAmprLocaleconv); diff --git a/core/libs/tests/GuestAmprThreads.cpp b/core/libs/tests/GuestAmprThreads.cpp index ef98b12fed..264ff62a6d 100644 --- a/core/libs/tests/GuestAmprThreads.cpp +++ b/core/libs/tests/GuestAmprThreads.cpp @@ -1,4 +1,5 @@ #include "SceTypes.hpp" +#include "prx/libc/include/exceptions/Runtime.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include #include @@ -76,6 +77,9 @@ int APS5_VABI _Mtx_unlock_nid_postfix(void** handle); GuestTerminate APS5_VABI _ZSt13set_terminatePFvvE_nid_postfix(GuestTerminate handler); const GuestTypeInfo* APS5_VABI __cxa_current_exception_type_nid_postfix(); void* APS5_VABI __cxa_current_primary_exception_nid_postfix(); +LibcException::Header* APS5_VABI __cxa_init_primary_exception_nid_postfix(void* object, std::type_info* type, void (*destructor)(void*)); +[[noreturn]] void APS5_VABI __cxa_call_unexpected_nid_postfix(void* unwindException); +extern const unsigned char _ZTVN10__cxxabiv117__class_type_infoE_nid_postfix[]; } static constexpr int ThrdSuccess = 0; @@ -487,11 +491,11 @@ static void APS5_VABI ExitWithSystemErrorCode() { } template -static int GuestSystemErrorCode(TCall call) { +static int TerminateExitCode(GuestTerminate handler, TCall call) { const pid_t child = fork(); Require(child >= 0); if (child == 0) { - _ZSt13set_terminatePFvvE_nid_postfix(ExitWithSystemErrorCode); + _ZSt13set_terminatePFvvE_nid_postfix(handler); call(); _exit(2); } @@ -499,6 +503,23 @@ static int GuestSystemErrorCode(TCall call) { Require(waitpid(child, &status, 0) == child); return WIFEXITED(status) ? WEXITSTATUS(status) : -1; } + +static const GuestTypeInfo UnexpectedType{_ZTVN10__cxxabiv117__class_type_infoE_nid_postfix + 16, "10Unexpected"}; + +static void APS5_VABI ExitWithCaughtType() { + const auto* type = __cxa_current_exception_type_nid_postfix(); + _exit(type == &UnexpectedType ? 42 : type == nullptr ? 43 : 1); +} + +static void TestCallUnexpected() { + Require(TerminateExitCode(ExitWithCaughtType, [] { + void* object = __cxa_allocate_exception_nid_postfix(sizeof(int)); + auto* type = reinterpret_cast(const_cast(&UnexpectedType)); + LibcException::Header* header = __cxa_init_primary_exception_nid_postfix(object, type, nullptr); + __cxa_call_unexpected_nid_postfix(&header->unwind); + }) == 42); + Require(TerminateExitCode(ExitWithCaughtType, [] { __cxa_call_unexpected_nid_postfix(nullptr); }) == 43); +} #endif static void TestPadErrors() { @@ -515,7 +536,7 @@ static void TestPadErrors() { Require(Throws([&] { _ZNSt4_Pad7_LaunchEPP7pthread_nid_postfix(&pad, nullptr); })); Require(!pad.started && order.load() == 0); #ifndef _WIN32 - Require(GuestSystemErrorCode([&] { _ZNSt4_Pad8_ReleaseEv_nid_postfix(&pad); }) == GuestEbusy); + Require(TerminateExitCode(ExitWithSystemErrorCode, [&] { _ZNSt4_Pad8_ReleaseEv_nid_postfix(&pad); }) == GuestEbusy); #endif Require(!pad.started); @@ -538,5 +559,8 @@ int main() { TestSpinLock(); TestWcstombs(); TestConditionSignal(); +#ifndef _WIN32 + TestCallUnexpected(); +#endif return 0; } diff --git a/docs/dev/debt/libSceAmpr.md b/docs/dev/debt/libSceAmpr.md index db01df9a8b..68774cc30e 100644 --- a/docs/dev/debt/libSceAmpr.md +++ b/docs/dev/debt/libSceAmpr.md @@ -7,6 +7,7 @@ - [AMPR command sizes](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - commands use the emulator's own encoding with an 8-byte header, so `Nop(n)` takes 8 + 4n bytes where the console record is n dwords including its header (`NopWithData(n)`: (n + 1) dwords). The measure functions agree with the appended sizes, but a title that pads to an exact offset or the end of the buffer with `Nop(remaining / 4)` gets `SCE_AMPR_ERROR_BUFFER_FULL` or a shifted offset. The at-start flag of the `_04_00` writes is ignored; commands run in order, each after the previous one completes - [APR map begin and end](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - as in ampr_emu, `MapBegin`/`MapDirectBegin` map when they run and `MapEnd` only closes the region; while a region is open a second begin and every completion write return `EPERM`, the at-start `_04_00` writes are accepted - [AMPR markers](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - signatures taken from the [ampr_emu](https://github.com/drakmor/ampr_emu) reimplementation, not confirmed on a title: `sceAmprCommandBufferSetMarkerWithColor` takes the color by pointer, the push and measure variants by value. Colors are not recorded +- [__cxa_call_unexpected](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - takes the in-flight exception's unwind header and never returns, as [libcxxabi 18.1.8](https://github.com/llvm/llvm-project/blob/3b5b5c1ec4a3095ab096dd780e84d7ab81f3d7ff/libcxxabi/src/cxa_personality.cpp#L1171-L1211) does. No `std::set_unexpected` is exported and the exception header's unexpected handler is never set, so the handler is always the default one, which calls `std::terminate` ([cxa_default_handlers.cpp](https://github.com/llvm/llvm-project/blob/3b5b5c1ec4a3095ab096dd780e84d7ab81f3d7ff/libcxxabi/src/cxa_default_handlers.cpp#L88-L99)); it therefore forwards to libc's `__cxa_call_terminate`, which begins the catch and runs the current terminate handler. That matches libcxxabi except for which terminate handler runs: there `std::terminate` after `__cxa_begin_catch` ([cxa_exception.cpp L470-L477](https://github.com/llvm/llvm-project/blob/3b5b5c1ec4a3095ab096dd780e84d7ab81f3d7ff/libcxxabi/src/cxa_exception.cpp#L470-L477)) runs the handler saved in the caught exception's header ([cxa_handlers.cpp L72-L93](https://github.com/llvm/llvm-project/blob/3b5b5c1ec4a3095ab096dd780e84d7ab81f3d7ff/libcxxabi/src/cxa_handlers.cpp#L72-L93)), which `__cxa_throw` and `__cxa_init_primary_exception` set to `std::get_terminate()` when the exception is thrown ([cxa_exception.cpp L209-L214](https://github.com/llvm/llvm-project/blob/3b5b5c1ec4a3095ab096dd780e84d7ab81f3d7ff/libcxxabi/src/cxa_exception.cpp#L209-L214), [L279](https://github.com/llvm/llvm-project/blob/3b5b5c1ec4a3095ab096dd780e84d7ab81f3d7ff/libcxxabi/src/cxa_exception.cpp#L279)), while here the current global handler runs, as AnyPS5's own `std::terminate` does. The two differ only if `std::set_terminate` was called between the throw and this call. A null header terminates because this emulator's libc `__cxa_begin_catch` terminates on null (libcxxabi does not define that case). Dynamic exception specifications that would let a replaced unexpected handler throw a permitted type are not modelled ### Functional From e09870006df4cb88fa200eedd0f75d25d1fdf14e Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Thu, 15 Oct 2026 04:35:02 -0700 Subject: [PATCH 0258/1000] refactor(agc): add the overlay's driver time after the worker profile report #6292 moves the APS5_PROFILE_DRAW check in Driver::timed to the top and rewrites the report's condition, the two lines this branch's driver-time line sat between, so the two conflicted. The line now follows the report. It reads the same flag and times, so it counts the same. --- .../Execution/src/Driver/Packets/PacketExecution.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp index ef2313522c..043646aac7 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp @@ -24,11 +24,11 @@ void Driver::timed(double WorkerProfile::*bucket, TWork&& work) { const auto end = std::chrono::steady_clock::now(); profile.*bucket += std::chrono::duration(end - begin).count(); static const bool report = std::getenv("APS5_PROFILE_DRAW") != nullptr; - if (report && bucket != &WorkerProfile::waitMs) PerformanceCounters::AddDriverNanoseconds(static_cast(std::chrono::duration_cast(end - begin).count())); if (report && end - profile.reported > std::chrono::seconds(10)) { profile.reported = end; AgcDriver::ProfilePrint_nid_no_patch( "[gpu] worker at %.0f s: dispatch %.1f s, draw %.1f s, wait %.1f s\n", std::chrono::duration(end - profile.start).count(), profile.dispatchMs / 1000, profile.drawMs / 1000, profile.waitMs / 1000); } + if (report && bucket != &WorkerProfile::waitMs) PerformanceCounters::AddDriverNanoseconds(static_cast(std::chrono::duration_cast(end - begin).count())); } void Driver::execute(const Submission& submission) { From e25bb140f9ac2fa674e9f2d367b6adccfe823b3e Mon Sep 17 00:00:00 2001 From: fdfawef324 <287205748+fdfawef324@users.noreply.github.com> Date: Tue, 13 Oct 2026 23:09:22 -0400 Subject: [PATCH 0259/1000] fix(agc): preserve null images during stencil feedback checks Co-Authored-By: Codex --- .../prx/libSceAgcDriver/Graphics/src/Draw.cpp | 1 + .../tests/execution/StencilNullFeedback.cpp | 265 ++++++++++++++++++ 2 files changed, 266 insertions(+) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/StencilNullFeedback.cpp diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index 7fb4593c6e..1039fa0538 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -2078,6 +2078,7 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& if (binding.role != ShaderRecompiler::DescriptorRole::GuestImages && binding.role != ShaderRecompiler::DescriptorRole::ImageTable) continue; for (std::size_t offset = 0; offset + 8u <= binding.guestDescriptor.size(); offset += 8u) { const auto words = std::span(binding.guestDescriptor).subspan(offset, 8u); + if (words[0] == 0u && (words[1] & 0xffu) == 0u) continue; const auto texture = DecodeTextureResource(words); Require(!DepthSurfaceAt(context, words, texture), "stencil replay refuses sampled depth-surface feedback"); } diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/StencilNullFeedback.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/StencilNullFeedback.cpp new file mode 100644 index 0000000000..3c721781dd --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/StencilNullFeedback.cpp @@ -0,0 +1,265 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace AgcDriver { + +struct VulkanDeviceTestAccess { + static Graphics::Context ContextOf(const VulkanDevice& device) { return device.graphicsContext(); } +}; + +} + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +PFN_vkCmdBindPipeline NativeBind = nullptr; +std::atomic GraphicsBinds{0u}; + +VKAPI_ATTR void VKAPI_CALL CountBind(VkCommandBuffer commands, VkPipelineBindPoint point, VkPipeline pipeline) { + if (point == VK_PIPELINE_BIND_POINT_GRAPHICS) ++GraphicsBinds; + NativeBind(commands, point, pipeline); +} + +struct BindTrace { + AgcDriver::Graphics::DeviceFunctions* functions; + explicit BindTrace(const AgcDriver::VulkanDevice& device) { + const auto context = AgcDriver::VulkanDeviceTestAccess::ContextOf(device); + functions = const_cast(context.functions); + Require(functions != nullptr && functions->cmdBindPipeline != nullptr, "native pipeline bind table is unavailable"); + NativeBind = functions->cmdBindPipeline; + functions->cmdBindPipeline = CountBind; + } + ~BindTrace() { functions->cmdBindPipeline = NativeBind; } +}; + +constexpr std::uint32_t Width = 64; +constexpr std::uint32_t Height = 16; +constexpr std::uint32_t WaveSize = 64; +constexpr std::byte Kept{0x40}; +alignas(256) std::array Pixels{}; +alignas(256) std::array NullBindingTexture{}; +alignas(256) std::array DepthMemory{}; +alignas(256) std::array StencilMemory{}; + +alignas(256) constexpr std::array VertexCode{ + 0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, +}; + +alignas(256) constexpr std::array PlainPixelCode{ + 0x7e0e02f2, 0xf800180f, 0x07070707, 0xbf810000, +}; + +alignas(256) constexpr std::array ColumnPixelCode{ + 0x7e040f00, 0x7e0e02f2, 0xf8000082, 0x00000200, 0xf800180f, 0x07070707, 0xbf810000, +}; + +alignas(256) constexpr std::array RowPixelCode{ + 0x7e040f01, 0x7e0e02f2, 0xf8000082, 0x00000200, 0xf800180f, 0x07070707, 0xbf810000, +}; + +alignas(256) constexpr std::array, 6> Vertices{{ + {-1.0f, -1.0f, 0.25f, 1.0f}, {3.0f, -1.0f, 0.25f, 1.0f}, {-1.0f, 3.0f, 0.25f, 1.0f}, + {-1.0f, -1.0f, 0.75f, 1.0f}, {3.0f, -1.0f, 0.75f, 1.0f}, {-1.0f, 3.0f, 0.75f, 1.0f}, +}}; + +std::array BufferDescriptor(const void* data, std::uint32_t stride, std::uint32_t count) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (stride << 16u), count, 0x01016facu}; +} + +ShaderRecompiler::ShaderPixelStageInfo PixelInfo(std::uint32_t shaderControl) { + const AgcDriver::Registers context{ + {0x1b3, 0x700u}, {0x1b4, 0x700u}, {0x1b6, 0u}, {0x1c5, 9u}, {0x203, shaderControl}, + }; + return AgcDriver::Graphics::DecodePixelStageInfo(context, {0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u}); +} + +void AttachNullImages(ShaderRecompiler::RecompileResult& result) { + namespace Runtime = ShaderRecompiler::RuntimeAbi; + const auto address = static_cast(reinterpret_cast(NullBindingTexture.data())); + const std::array words{static_cast(address >> 8u), static_cast(address >> 40u) | (56u << 20u) | (((Width - 1u) & 3u) << 30u), ((Width - 1u) >> 2u) | ((Height - 1u) << 14u), 0xfacu | (9u << 28u), 0u, 0u, 0u, 0u}; + constexpr auto binding = Runtime::FirstStorageImageBinding + 1u; + ShaderRecompiler::DescriptorBinding images{ShaderRecompiler::DescriptorKind::StorageImage, ShaderRecompiler::DescriptorRole::GuestImages, Runtime::FragmentDescriptorSet, binding, 2u, std::vector(16u)}; + std::copy(words.begin(), words.end(), images.guestDescriptor.begin()); + images.imageShape = ShaderRecompiler::DescriptorImageShape::Image2D; + images.imageWritten = {false, false}; + images.imageAtomic = {false, false}; + images.imageAtomic64 = {false, false}; + result.bindings.push_back(std::move(images)); + auto module = result.spirv.Words(); + auto id = module[3]; + std::uint32_t scalar = 0u, floating = 0u; + std::size_t types = 0u, functions = 0u; + for (std::size_t offset = 5u; offset < module.size(); offset += module[offset] >> 16u) { + const auto opcode = module[offset] & 0xffffu; + if (types == 0u && opcode == spv::OpTypeVoid) types = offset; + if (opcode == spv::OpTypeInt && module[offset + 2u] == 32u && module[offset + 3u] == 0u) scalar = module[offset + 1u]; + if (opcode == spv::OpTypeFloat && module[offset + 2u] == 32u) floating = module[offset + 1u]; + if (opcode == spv::OpFunction) { functions = offset; break; } + } + Require(types != 0u && functions != 0u && floating != 0u, "the null-image fixture has no scalar shader types"); + const bool declareScalar = scalar == 0u; + if (declareScalar) scalar = id++; + const auto count = id++, image = id++, array = id++, pointer = id++, variable = id++; + const auto emit = [](std::vector& output, spv::Op opcode, std::initializer_list operands) { + output.push_back((static_cast(operands.size() + 1u) << 16u) | static_cast(opcode)); + output.insert(output.end(), operands.begin(), operands.end()); + }; + std::vector annotations; + emit(annotations, spv::OpDecorate, {variable, spv::DecorationDescriptorSet, Runtime::FragmentDescriptorSet}); + emit(annotations, spv::OpDecorate, {variable, spv::DecorationBinding, binding}); + emit(annotations, spv::OpDecorate, {variable, spv::DecorationNonWritable}); + std::vector declarations; + if (declareScalar) emit(declarations, spv::OpTypeInt, {scalar, 32u, 0u}); + emit(declarations, spv::OpConstant, {scalar, count, 2u}); + emit(declarations, spv::OpTypeImage, {image, floating, spv::Dim2D, 0u, 0u, 0u, 2u, spv::ImageFormatRgba8}); + emit(declarations, spv::OpTypeArray, {array, image, count}); + emit(declarations, spv::OpTypePointer, {pointer, spv::StorageClassUniformConstant, array}); + emit(declarations, spv::OpVariable, {pointer, variable, spv::StorageClassUniformConstant}); + module.insert(module.begin() + functions, declarations.begin(), declarations.end()); + module.insert(module.begin() + types, annotations.begin(), annotations.end()); + module[3] = id; + result.spirv = std::move(module); +} + + +void Draw(AgcDriver::VulkanDevice& device, std::span pixelCode, std::uint32_t shaderControl, const VkStencilOpState& stencil, std::uint32_t vertices = 3u, std::uint32_t instances = 1u, bool depthFail = false, const std::function& alter = {}, const std::function& alterDraw = {}, std::span> vertexData = Vertices, std::span vertexCode = VertexCode, std::span pixelData = {}, std::span pixelSrt = {}, bool nullImages = false) { + Pixels.fill(Kept); + auto target = device.Target(); + std::vector capabilities(target.supportedCapabilities.begin(), target.supportedCapabilities.end()); + std::erase(capabilities, static_cast(spv::CapabilityStencilExportEXT)); + target.supportedCapabilities = capabilities; + + std::vector vertexUserData(4, 0u); + const auto vertexBuffer = BufferDescriptor(vertexData.data(), 16u, static_cast(vertexData.size())); + std::copy(vertexBuffer.begin(), vertexBuffer.end(), vertexUserData.begin()); + const std::array vertexMemory{{{reinterpret_cast(vertexCode.data()), std::as_bytes(vertexCode)}}}; + ShaderRecompiler::RecompileRequest vertex{ + {ShaderStage::Vertex, reinterpret_cast(vertexCode.data()), vertexCode, 0, {}}, + {WaveSize, 0, vertexUserData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, vertexMemory}, + target, + {0, 0, 0, 64} + }; + vertex.useCache = false; + const auto vertexResult = ShaderRecompiler::Recompile(vertex); + const auto vertexPush = static_cast(vertexResult.pushConstants.size()); + + auto pixelUserData = pixelData.empty() ? std::vector(8, 0u) : std::vector(pixelData.begin(), pixelData.end()); + std::vector pixelMemory{{reinterpret_cast(pixelCode.data()), std::as_bytes(pixelCode)}}; + pixelMemory.insert(pixelMemory.end(), pixelSrt.begin(), pixelSrt.end()); + ShaderRecompiler::RecompileRequest fragment{ + {ShaderStage::Fragment, reinterpret_cast(pixelCode.data()), pixelCode, 0, {}}, + {WaveSize, 0, pixelUserData, std::nullopt, PixelInfo(shaderControl), std::nullopt, pixelMemory}, + target, + PixelPushLayout(vertexPush, target) + }; + fragment.useCache = false; + auto pixelResult = ShaderRecompiler::Recompile(fragment); + if (nullImages) AttachNullImages(pixelResult); + if (!pixelResult.stencilReferences.empty()) { + Require(pixelResult.stencilReferenceDword * sizeof(std::uint32_t) + sizeof(std::uint32_t) == pixelResult.pushConstants.size(), "dynamic reference is not reserved after the stage payload"); + for (std::size_t cursor = 5; cursor < pixelResult.spirv.size();) { + const auto count = pixelResult.spirv[cursor] >> 16u; + Require(count != 0u && count <= pixelResult.spirv.size() - cursor, "invalid materialized replay module"); + Require((pixelResult.spirv[cursor] & 0xffffu) != spv::OpSpecConstant, "expected reference remains a pipeline specialization"); + cursor += count; + } + } + const std::array shaders{{ + {ShaderStage::Vertex, &vertexResult, 0}, + {ShaderStage::Fragment, &pixelResult, PixelPushOffset(vertexPush, target)} + }}; + + AgcDriver::Graphics::State state{}; + state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, WaveSize, WaveSize, std::nullopt, std::nullopt}; + state.color = {reinterpret_cast(Pixels.data()), {Width, Height}, VK_FORMAT_R8G8B8A8_UNORM, Pixels.size(), 0xe4u}; + state.colors = {state.color}; + state.hasColorTarget = true; + state.depth = AgcDriver::Graphics::DepthTarget{reinterpret_cast(DepthMemory.data()), reinterpret_cast(StencilMemory.data()), {Width, Height}, VK_FORMAT_D32_SFLOAT_S8_UINT, 1.0f, 0}; + state.depthTest = depthFail; + state.depthCompare = depthFail ? VK_COMPARE_OP_NEVER : VK_COMPARE_OP_ALWAYS; + state.stencilTest = true; + state.stencilFront = stencil; + state.stencilBack = stencil; + state.renderExtent = {Width, Height}; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.viewport = {0, static_cast(Height), static_cast(Width), -static_cast(Height), 0, 1}; + state.negativeOneToOne = false; + state.scissor = {{0, 0}, {Width, Height}}; + state.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + state.blend.colorWriteMask = 15; + state.blends = {state.blend}; + state.blendConstants = {}; + if (alter) alter(state); + AgcDriver::Pm4::DrawParameters draw{0, vertices, 0, instances, 0, false}; + if (alterDraw) alterDraw(draw); + const auto bindsBefore = GraphicsBinds.load(); + device.Draw(state, draw, shaders); + device.WaitIdle(); + if (!pixelResult.stencilReferences.empty()) Require(GraphicsBinds.load() - bindsBefore == 1u, "stencil replay rebound its shader pipeline within an original draw"); +} + +void Check(const std::function& drawn, std::string_view what) { + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + const auto expected = drawn(x, y) ? std::byte{255} : Kept; + const auto actual = Pixels[(y * Width + x) * 4u]; + Require(actual == expected, std::string(what) + ": pixel (" + std::to_string(x) + ", " + std::to_string(y) + ") is " + std::to_string(static_cast(actual)) + ", expected " + (drawn(x, y) ? "drawn" : "stencil-rejected")); + } + } +} + +} + + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + const BindTrace binds(*device); + constexpr std::uint32_t exported = 0x52u; + constexpr std::uint32_t plain = 0x50u; + constexpr std::array alternating{ + 0x7e040f00u, 0x36060481u, + 0x7c000000u | (0xc2u << 17u) | (3u << 9u) | 0x80u, + 0x7e0802ffu, 0xffu, + (1u << 25u) | (5u << 17u) | (4u << 9u) | 0x80u, + 0x7e0e02f2u, 0xf8000082u, 5u << 8u, + 0xf800180fu, 0x07070707u, 0xbf810000u + }; + const auto replace = [](std::uint32_t reference = 0u, std::uint32_t mask = 0xffu) { return VkStencilOpState{VK_STENCIL_OP_KEEP, VK_STENCIL_OP_REPLACE, VK_STENCIL_OP_KEEP, VK_COMPARE_OP_ALWAYS, 0xffu, mask, reference}; }; + const auto equal = [](std::uint32_t reference, std::uint32_t mask = 0xffu) { return VkStencilOpState{VK_STENCIL_OP_KEEP, VK_STENCIL_OP_KEEP, VK_STENCIL_OP_KEEP, VK_COMPARE_OP_EQUAL, mask, 0u, reference}; }; + const auto all = [](std::uint32_t, std::uint32_t) { return true; }; + const auto even = [](std::uint32_t x, std::uint32_t) { return x % 2u == 0u; }; + const auto odd = [](std::uint32_t x, std::uint32_t) { return x % 2u != 0u; }; + Draw(*device, PlainPixelCode, plain, replace(), 3u, 1u, false, {}, {}, Vertices, VertexCode, {}, {}, true); + Check(all, "an empty typed image element remains supported without stencil export"); + Draw(*device, alternating, exported, replace(), 6u, 2u, false, {}, {}, Vertices, VertexCode, {}, {}, true); + Check(all, "an empty typed image element preserves stencil replay colors"); + Draw(*device, PlainPixelCode, plain, equal(255u)); + Check(even, "an empty typed image element preserves the even stencil reference"); + Draw(*device, PlainPixelCode, plain, equal(0u)); + Check(odd, "an empty typed image element preserves the odd stencil reference"); + std::puts("stencil null feedback tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} From af8a8b28a0170ba235bf5697f8a1c60804c2c326 Mon Sep 17 00:00:00 2001 From: fdfawef324 <287205748+fdfawef324@users.noreply.github.com> Date: Wed, 14 Oct 2026 10:28:41 -0400 Subject: [PATCH 0260/1000] test(agc): copy stencil null fixture words before mutation Co-Authored-By: Codex --- .../prx/libSceAgcDriver/tests/execution/StencilNullFeedback.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/StencilNullFeedback.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/StencilNullFeedback.cpp index 3c721781dd..7101343cf9 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/StencilNullFeedback.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/StencilNullFeedback.cpp @@ -101,7 +101,7 @@ void AttachNullImages(ShaderRecompiler::RecompileResult& result) { images.imageAtomic = {false, false}; images.imageAtomic64 = {false, false}; result.bindings.push_back(std::move(images)); - auto module = result.spirv.Words(); + auto module = result.spirv.Copy(); auto id = module[3]; std::uint32_t scalar = 0u, floating = 0u; std::size_t types = 0u, functions = 0u; From da0d5b2be3d5c32147b34f83fc046b7b30ffaa10 Mon Sep 17 00:00:00 2001 From: fdfawef324 <287205748+fdfawef324@users.noreply.github.com> Date: Wed, 14 Oct 2026 10:31:41 -0400 Subject: [PATCH 0261/1000] test(agc): retain non-null depth feedback refusal Co-Authored-By: Codex --- .../tests/execution/StencilNullFeedback.cpp | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/StencilNullFeedback.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/StencilNullFeedback.cpp index 7101343cf9..befa0444a8 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/StencilNullFeedback.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/StencilNullFeedback.cpp @@ -89,10 +89,11 @@ ShaderRecompiler::ShaderPixelStageInfo PixelInfo(std::uint32_t shaderControl) { return AgcDriver::Graphics::DecodePixelStageInfo(context, {0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u}); } -void AttachNullImages(ShaderRecompiler::RecompileResult& result) { +void AttachNullImages(ShaderRecompiler::RecompileResult& result, bool depthAlias = false) { namespace Runtime = ShaderRecompiler::RuntimeAbi; - const auto address = static_cast(reinterpret_cast(NullBindingTexture.data())); - const std::array words{static_cast(address >> 8u), static_cast(address >> 40u) | (56u << 20u) | (((Width - 1u) & 3u) << 30u), ((Width - 1u) >> 2u) | ((Height - 1u) << 14u), 0xfacu | (9u << 28u), 0u, 0u, 0u, 0u}; + const auto address = static_cast(reinterpret_cast(depthAlias ? DepthMemory.data() : NullBindingTexture.data())); + const auto format = depthAlias ? 22u : 56u; + const std::array words{static_cast(address >> 8u), static_cast(address >> 40u) | (format << 20u) | (((Width - 1u) & 3u) << 30u), ((Width - 1u) >> 2u) | ((Height - 1u) << 14u), 0xfacu | (9u << 28u), 0u, 0u, 0u, 0u}; constexpr auto binding = Runtime::FirstStorageImageBinding + 1u; ShaderRecompiler::DescriptorBinding images{ShaderRecompiler::DescriptorKind::StorageImage, ShaderRecompiler::DescriptorRole::GuestImages, Runtime::FragmentDescriptorSet, binding, 2u, std::vector(16u)}; std::copy(words.begin(), words.end(), images.guestDescriptor.begin()); @@ -127,7 +128,7 @@ void AttachNullImages(ShaderRecompiler::RecompileResult& result) { std::vector declarations; if (declareScalar) emit(declarations, spv::OpTypeInt, {scalar, 32u, 0u}); emit(declarations, spv::OpConstant, {scalar, count, 2u}); - emit(declarations, spv::OpTypeImage, {image, floating, spv::Dim2D, 0u, 0u, 0u, 2u, spv::ImageFormatRgba8}); + emit(declarations, spv::OpTypeImage, {image, floating, spv::Dim2D, 0u, 0u, 0u, 2u, depthAlias ? spv::ImageFormatR32f : spv::ImageFormatRgba8}); emit(declarations, spv::OpTypeArray, {array, image, count}); emit(declarations, spv::OpTypePointer, {pointer, spv::StorageClassUniformConstant, array}); emit(declarations, spv::OpVariable, {pointer, variable, spv::StorageClassUniformConstant}); @@ -138,7 +139,7 @@ void AttachNullImages(ShaderRecompiler::RecompileResult& result) { } -void Draw(AgcDriver::VulkanDevice& device, std::span pixelCode, std::uint32_t shaderControl, const VkStencilOpState& stencil, std::uint32_t vertices = 3u, std::uint32_t instances = 1u, bool depthFail = false, const std::function& alter = {}, const std::function& alterDraw = {}, std::span> vertexData = Vertices, std::span vertexCode = VertexCode, std::span pixelData = {}, std::span pixelSrt = {}, bool nullImages = false) { +void Draw(AgcDriver::VulkanDevice& device, std::span pixelCode, std::uint32_t shaderControl, const VkStencilOpState& stencil, std::uint32_t vertices = 3u, std::uint32_t instances = 1u, bool depthFail = false, const std::function& alter = {}, const std::function& alterDraw = {}, std::span> vertexData = Vertices, std::span vertexCode = VertexCode, std::span pixelData = {}, std::span pixelSrt = {}, bool nullImages = false, bool depthAlias = false) { Pixels.fill(Kept); auto target = device.Target(); std::vector capabilities(target.supportedCapabilities.begin(), target.supportedCapabilities.end()); @@ -170,7 +171,7 @@ void Draw(AgcDriver::VulkanDevice& device, std::span pixelC }; fragment.useCache = false; auto pixelResult = ShaderRecompiler::Recompile(fragment); - if (nullImages) AttachNullImages(pixelResult); + if (nullImages) AttachNullImages(pixelResult, depthAlias); if (!pixelResult.stencilReferences.empty()) { Require(pixelResult.stencilReferenceDword * sizeof(std::uint32_t) + sizeof(std::uint32_t) == pixelResult.pushConstants.size(), "dynamic reference is not reserved after the stage payload"); for (std::size_t cursor = 5; cursor < pixelResult.spirv.size();) { @@ -256,6 +257,16 @@ int main() { Check(even, "an empty typed image element preserves the even stencil reference"); Draw(*device, PlainPixelCode, plain, equal(0u)); Check(odd, "an empty typed image element preserves the odd stencil reference"); + bool refused = false; + try { + Draw(*device, alternating, exported, replace(), 3u, 1u, false, {}, {}, Vertices, VertexCode, {}, {}, true, true); + } catch (const std::exception& error) { + const std::string_view reason(error.what()); + refused = reason.find("stencil replay refuses") != std::string_view::npos && reason.find("feedback") != std::string_view::npos; + if (!refused) throw; + } + Require(refused, "a non-null image aliasing the depth surface bypassed replay feedback checks"); + Require(std::ranges::all_of(Pixels, [](std::byte value) { return value == Kept; }), "refused depth feedback published color results"); std::puts("stencil null feedback tests passed"); return 0; } catch (const std::exception& error) { From dfc71c84181d79169819f9fda3fa7de23dde15ee Mon Sep 17 00:00:00 2001 From: fdfawef324 <287205748+fdfawef324@users.noreply.github.com> Date: Tue, 13 Oct 2026 21:52:20 -0400 Subject: [PATCH 0262/1000] fix(agc): fold bounded vertex lane masks before stencil replay Preserve replay refusal for observable lane-dependent values, non-vertex stages, unknown subgroup widths and varying-size modules. Validate the actual native vertex subgroup contract and retain compiled single-lane targets. Co-Authored-By: Codex --- .../libSceAgcDriver/tests/ConstantBitTest.cpp | 47 +++++++++++++++++++ .../include/Optimization/ConstantFolder.hpp | 2 + .../Optimization/src/ConstantFolder.cpp | 14 ++++++ core/shader/recompiler/Recompiler.cpp | 6 +++ 4 files changed, 69 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/ConstantBitTest.cpp b/core/libs/prx/libSceAgcDriver/tests/ConstantBitTest.cpp index 38131c019d..0d425edf5d 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ConstantBitTest.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ConstantBitTest.cpp @@ -1,5 +1,6 @@ #include "IntermediateRepresentation/IrBuilder.hpp" #include "Optimization/ConstantFolder.hpp" +#include "Optimization/DeadCodeEliminator.hpp" #include #include #include @@ -44,6 +45,41 @@ bool reversesTo(std::uint32_t word, std::uint32_t expected) { return result->HasImmediate() && result->ImmediateBool(); } +bool vertexProjection(std::uint32_t low, std::uint32_t hostSubgroup, std::uint32_t version, bool expected, bool directLane = false, IrShaderStage stage = IrShaderStage::Vertex) { + IrProgram program; + program.Resources().stage = stage; + program.SetWaveSize(64u); + auto& block = program.CreateBlock(); + program.SetEntryBlock(block); + program.BlockOrder().push_back(&block); + IrBuilder builder(program); + builder.SetInsertionPoint(block); + auto& lane = builder.Emit(IrOpcode::LaneId, IrType::U32, {}); + auto& half = builder.Emit(IrOpcode::ULessThan32, IrType::Bool, {&lane, &builder.Constant(32u)}); + auto& word = builder.Emit(IrOpcode::SelectU32, IrType::U32, {&half, &builder.Constant(low), &builder.Constant(0u)}); + auto& amount = builder.Emit(IrOpcode::BitwiseAnd32, IrType::U32, {&lane, &builder.Constant(31u)}); + auto& shifted = builder.Emit(IrOpcode::ShiftRightLogical32, IrType::U32, {&word, &amount}); + auto& bit = builder.Emit(IrOpcode::BitwiseAnd32, IrType::U32, {&shifted, &builder.Constant(1u)}); + auto& test = builder.Emit(IrOpcode::INotEqual32, IrType::Bool, {&bit, &builder.Constant(0u)}); + auto& user = builder.Emit(IrOpcode::Reference, IrType::Void, {&test}); + if (directLane) static_cast(builder.Emit(IrOpcode::ReferenceU32, IrType::Void, {&lane})); + static_cast(builder.Emit(IrOpcode::Return, IrType::Void, {})); + ConstantFolder folder; + const bool folded = folder.FoldVertexLaneBounds(program, hostSubgroup, version); + const bool shouldFold = stage == IrShaderStage::Vertex && hostSubgroup == 32u && version >= 0x00010000u && version < 0x00010600u; + if (folded != shouldFold) return false; + if (!shouldFold && half.Opcode() != IrOpcode::ULessThan32) return false; + if (shouldFold && (!half.Resolve()->HasImmediate() || !half.Resolve()->ImmediateBool())) return false; + folder.Fold(program); + DeadCodeEliminator{}.RemoveIdentities(program); + DeadCodeEliminator{}.Eliminate(program); + const auto* result = user.Argument(0)->Resolve(); + if (expected) return result->HasImmediate() && result->ImmediateBool(); + bool retainedLane = false; + for (const auto* instruction : block.Instructions()) retainedLane = retainedLane || instruction->Opcode() == IrOpcode::LaneId; + return retainedLane; +} + bool run(const char* name, bool passed) { if (!passed) std::fprintf(stderr, "%s\n", name); return passed; @@ -58,6 +94,17 @@ int main() { passed &= run("an all-one word must fold the lane bit test to true", foldsTo(0xffffffffu, true, true)); passed &= run("a mixed word must keep the lane bit test", foldsTo(0x0000ffffu, false, true)); passed &= run("an unmasked lane number must keep the lane bit test", foldsTo(0xffffffffu, true, false)); + passed &= run("a fixed 32-lane vertex subgroup removes a constant lower-half export guard", vertexProjection(0xffffffffu, 32u, 0x00010300u, true)); + passed &= run("a single set bit remains dependent on the vertex lane", vertexProjection(1u, 32u, 0x00010300u, false)); + passed &= run("a partial mask remains dependent on the vertex lane", vertexProjection(0x0000ffffu, 32u, 0x00010300u, false)); + passed &= run("a real vertex lane output stays observable after folding its guard", vertexProjection(0xffffffffu, 32u, 0x00010300u, false, true)); + passed &= run("a 64-lane subgroup keeps a lower-half export guard", vertexProjection(0xffffffffu, 64u, 0x00010300u, false)); + passed &= run("an unknown subgroup keeps a lower-half export guard", vertexProjection(0xffffffffu, 0u, 0x00010300u, false)); + passed &= run("a single-lane request does not borrow the physical 32-lane proof", vertexProjection(0xffffffffu, 1u, 0x00010300u, false)); + passed &= run("SPIR-V 1.6 does not borrow a fixed subgroup-size proof", vertexProjection(0xffffffffu, 32u, 0x00010600u, false)); + passed &= run("an unknown module version keeps the lane guard", vertexProjection(0xffffffffu, 32u, 0u, false)); + passed &= run("the lane-bound optimization does not change compute shaders", vertexProjection(0xffffffffu, 32u, 0x00010300u, false, false, IrShaderStage::Compute)); + passed &= run("the lane-bound optimization does not change pixel shaders", vertexProjection(0xffffffffu, 32u, 0x00010300u, false, false, IrShaderStage::Pixel)); passed &= run("a reversed zero must fold to zero", reversesTo(0x00000000u, 0x00000000u)); passed &= run("a reversed single bit must fold to the opposite end", reversesTo(0x00000001u, 0x80000000u)); passed &= run("a reversed high bit must fold to bit zero", reversesTo(0x80000000u, 0x00000001u)); diff --git a/core/shader/recompiler/Optimization/include/Optimization/ConstantFolder.hpp b/core/shader/recompiler/Optimization/include/Optimization/ConstantFolder.hpp index d2df4ed3cc..6cbf62c460 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/ConstantFolder.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/ConstantFolder.hpp @@ -14,6 +14,8 @@ public: void Fold(IrProgram& program, std::span blocks) const; + [[nodiscard]] bool FoldVertexLaneBounds(IrProgram& program, std::uint32_t hostSubgroupSize, std::uint32_t spirvVersion) const; + [[nodiscard]] bool ComposePackedAncillary(IrProgram& program) const; private: diff --git a/core/shader/recompiler/Optimization/src/ConstantFolder.cpp b/core/shader/recompiler/Optimization/src/ConstantFolder.cpp index 3505f640f4..bf9b33ac6b 100644 --- a/core/shader/recompiler/Optimization/src/ConstantFolder.cpp +++ b/core/shader/recompiler/Optimization/src/ConstantFolder.cpp @@ -846,6 +846,20 @@ void ConstantFolder::Fold(IrProgram& program) const { Fold(program, program.BlockOrder()); } +bool ConstantFolder::FoldVertexLaneBounds(IrProgram& program, std::uint32_t hostSubgroupSize, std::uint32_t spirvVersion) const { + if (program.Resources().stage != IrShaderStage::Vertex || hostSubgroupSize != 32u || spirvVersion < 0x00010000u || spirvVersion >= 0x00010600u) return false; + bool changed = false; + IrBuilder builder(program); + for (IrBlock* block : program.BlockOrder()) { + for (IrValue* value : block->Instructions()) { + if (value->Opcode() != IrOpcode::ULessThan32 || value->ArgumentCount() != 2u || !isLaneIndex(resolveArg(*value, 0)) || !isImmediateU32(resolveArg(*value, 1), 32u)) continue; + replaceWith(*value, builder.ConstantBool(true)); + changed = true; + } + } + return changed; +} + bool ConstantFolder::ComposePackedAncillary(IrProgram& program) const { std::vector words; for (IrBlock* block : program.BlockOrder()) { diff --git a/core/shader/recompiler/Recompiler.cpp b/core/shader/recompiler/Recompiler.cpp index 32dcb23ba2..ccd1b828ae 100644 --- a/core/shader/recompiler/Recompiler.cpp +++ b/core/shader/recompiler/Recompiler.cpp @@ -494,6 +494,12 @@ void prepareInterface(const RecompileRequest& request, IrProgram& program) { const bool nativeSampleOffsets = request.target.nonConstantImageOffsets && std::find(request.target.supportedCapabilities.begin(), request.target.supportedCapabilities.end(), spv::CapabilityImageGatherExtended) != request.target.supportedCapabilities.end(); resourceMaterializer.ApplyStaticInterface(program, nativeSampleOffsets); + constexpr ConstantFolder constantFolder; + if (request.shader.stage == ShaderStage::Vertex && constantFolder.FoldVertexLaneBounds(program, HostSubgroupSize(request), request.target.spirvVersion)) { + constantFolder.Fold(program); + ResolveControlFlowIdentities(program); + } + deadCodeEliminator.RemoveIdentities(program); constexpr RedundantValueEliminator redundantValueEliminator; static_cast(redundantValueEliminator.Eliminate(program)); From d353c6bccf2e95346a18ed892dd31d9acdedf8bf Mon Sep 17 00:00:00 2001 From: fdfawef324 <287205748+fdfawef324@users.noreply.github.com> Date: Wed, 14 Oct 2026 17:05:33 -0400 Subject: [PATCH 0263/1000] fix(shader): fold only constant lower-half lane words of vertex programs FoldVertexLaneBounds replaced every LaneId < 32 compare of a vertex program on a fixed 32-lane host subgroup with true. The half-wave scan matcher of the SPIR-V backend recognises a lane word by the select of its low and high words on that compare. A scan keyed by the entry EXEC ballot lost that shape, so its v_readlane_b32 of lane 63 was no longer read as a half-wave reduction and the recompile failed (agc_shader_memory). Fold the lane word select itself instead, and only when both words are constants. Lane words built from ballots or other values keep their lower-half test. Co-Authored-By: Claude Opus 5.5 --- .../libSceAgcDriver/tests/ConstantBitTest.cpp | 18 +++++++++++++----- .../Optimization/src/ConstantFolder.cpp | 8 +++++--- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/ConstantBitTest.cpp b/core/libs/prx/libSceAgcDriver/tests/ConstantBitTest.cpp index 0d425edf5d..d660dc354d 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ConstantBitTest.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ConstantBitTest.cpp @@ -45,7 +45,7 @@ bool reversesTo(std::uint32_t word, std::uint32_t expected) { return result->HasImmediate() && result->ImmediateBool(); } -bool vertexProjection(std::uint32_t low, std::uint32_t hostSubgroup, std::uint32_t version, bool expected, bool directLane = false, IrShaderStage stage = IrShaderStage::Vertex) { +bool vertexProjection(std::uint32_t low, std::uint32_t hostSubgroup, std::uint32_t version, bool expected, bool directLane = false, IrShaderStage stage = IrShaderStage::Vertex, bool ballotWords = false) { IrProgram program; program.Resources().stage = stage; program.SetWaveSize(64u); @@ -56,7 +56,14 @@ bool vertexProjection(std::uint32_t low, std::uint32_t hostSubgroup, std::uint32 builder.SetInsertionPoint(block); auto& lane = builder.Emit(IrOpcode::LaneId, IrType::U32, {}); auto& half = builder.Emit(IrOpcode::ULessThan32, IrType::Bool, {&lane, &builder.Constant(32u)}); - auto& word = builder.Emit(IrOpcode::SelectU32, IrType::U32, {&half, &builder.Constant(low), &builder.Constant(0u)}); + IrValue* lowWord = &builder.Constant(low); + IrValue* highWord = &builder.Constant(0u); + if (ballotWords) { + auto& entry = builder.Emit(IrOpcode::Ballot, IrType::U32x4, {&builder.ConstantBool(true)}); + lowWord = &builder.Emit(IrOpcode::CompositeExtractU32x4, IrType::U32, {&entry, &builder.Constant(0u)}); + highWord = &builder.Emit(IrOpcode::CompositeExtractU32x4, IrType::U32, {&entry, &builder.Constant(1u)}); + } + auto& word = builder.Emit(IrOpcode::SelectU32, IrType::U32, {&half, lowWord, highWord}); auto& amount = builder.Emit(IrOpcode::BitwiseAnd32, IrType::U32, {&lane, &builder.Constant(31u)}); auto& shifted = builder.Emit(IrOpcode::ShiftRightLogical32, IrType::U32, {&word, &amount}); auto& bit = builder.Emit(IrOpcode::BitwiseAnd32, IrType::U32, {&shifted, &builder.Constant(1u)}); @@ -66,10 +73,10 @@ bool vertexProjection(std::uint32_t low, std::uint32_t hostSubgroup, std::uint32 static_cast(builder.Emit(IrOpcode::Return, IrType::Void, {})); ConstantFolder folder; const bool folded = folder.FoldVertexLaneBounds(program, hostSubgroup, version); - const bool shouldFold = stage == IrShaderStage::Vertex && hostSubgroup == 32u && version >= 0x00010000u && version < 0x00010600u; + const bool shouldFold = !ballotWords && stage == IrShaderStage::Vertex && hostSubgroup == 32u && version >= 0x00010000u && version < 0x00010600u; if (folded != shouldFold) return false; - if (!shouldFold && half.Opcode() != IrOpcode::ULessThan32) return false; - if (shouldFold && (!half.Resolve()->HasImmediate() || !half.Resolve()->ImmediateBool())) return false; + if (!shouldFold && (word.Opcode() != IrOpcode::SelectU32 || word.Argument(0)->Resolve() != &half || half.Opcode() != IrOpcode::ULessThan32)) return false; + if (shouldFold && (!word.Resolve()->HasImmediate() || word.Resolve()->ImmediateU32() != low)) return false; folder.Fold(program); DeadCodeEliminator{}.RemoveIdentities(program); DeadCodeEliminator{}.Eliminate(program); @@ -98,6 +105,7 @@ int main() { passed &= run("a single set bit remains dependent on the vertex lane", vertexProjection(1u, 32u, 0x00010300u, false)); passed &= run("a partial mask remains dependent on the vertex lane", vertexProjection(0x0000ffffu, 32u, 0x00010300u, false)); passed &= run("a real vertex lane output stays observable after folding its guard", vertexProjection(0xffffffffu, 32u, 0x00010300u, false, true)); + passed &= run("a lane word selected from ballot words keeps its lower-half test", vertexProjection(0xffffffffu, 32u, 0x00010300u, false, false, IrShaderStage::Vertex, true)); passed &= run("a 64-lane subgroup keeps a lower-half export guard", vertexProjection(0xffffffffu, 64u, 0x00010300u, false)); passed &= run("an unknown subgroup keeps a lower-half export guard", vertexProjection(0xffffffffu, 0u, 0x00010300u, false)); passed &= run("a single-lane request does not borrow the physical 32-lane proof", vertexProjection(0xffffffffu, 1u, 0x00010300u, false)); diff --git a/core/shader/recompiler/Optimization/src/ConstantFolder.cpp b/core/shader/recompiler/Optimization/src/ConstantFolder.cpp index bf9b33ac6b..52643c8990 100644 --- a/core/shader/recompiler/Optimization/src/ConstantFolder.cpp +++ b/core/shader/recompiler/Optimization/src/ConstantFolder.cpp @@ -849,11 +849,13 @@ void ConstantFolder::Fold(IrProgram& program) const { bool ConstantFolder::FoldVertexLaneBounds(IrProgram& program, std::uint32_t hostSubgroupSize, std::uint32_t spirvVersion) const { if (program.Resources().stage != IrShaderStage::Vertex || hostSubgroupSize != 32u || spirvVersion < 0x00010000u || spirvVersion >= 0x00010600u) return false; bool changed = false; - IrBuilder builder(program); for (IrBlock* block : program.BlockOrder()) { for (IrValue* value : block->Instructions()) { - if (value->Opcode() != IrOpcode::ULessThan32 || value->ArgumentCount() != 2u || !isLaneIndex(resolveArg(*value, 0)) || !isImmediateU32(resolveArg(*value, 1), 32u)) continue; - replaceWith(*value, builder.ConstantBool(true)); + if (value->Opcode() != IrOpcode::SelectU32 || value->ArgumentCount() != 3u) continue; + const auto& half = resolveArg(*value, 0); + auto& low = resolveArg(*value, 1); + if (half.Opcode() != IrOpcode::ULessThan32 || half.ArgumentCount() != 2u || !isLaneIndex(resolveArg(half, 0)) || !isImmediateU32(resolveArg(half, 1), 32u) || !isImmediate(low, IrType::U32) || !isImmediate(resolveArg(*value, 2), IrType::U32)) continue; + replaceWith(*value, low); changed = true; } } From 6541db004b279a4bcaaefd7109474e5388257edf Mon Sep 17 00:00:00 2001 From: fdfawef324 <287205748+fdfawef324@users.noreply.github.com> Date: Wed, 14 Oct 2026 07:21:15 -0400 Subject: [PATCH 0264/1000] fix(agc): disable optional BMP dumps after host I/O failures Use checked host stdio for BMP output and stop requesting dumps after the first write failure. Keep pixel validation, allocation and non-I/O failures on the existing fatal path. Co-Authored-By: Codex --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 11 ++ .../Execution/include/FrameDumpIo.hpp | 60 +++++++ .../Execution/src/VulkanDevice.cpp | 37 ++--- .../prx/libSceAgcDriver/tests/FrameDumpIo.cpp | 149 ++++++++++++++++++ docs/dev/debt/libSceAgcDriver.md | 2 + 5 files changed, 231 insertions(+), 28 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/Execution/include/FrameDumpIo.hpp create mode 100644 core/libs/prx/libSceAgcDriver/tests/FrameDumpIo.cpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 6bdee1b0e9..f0046feede 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -316,6 +316,17 @@ set_target_properties(libSceAgcDriver PROPERTIES CXX_EXTENSIONS OFF) configure_windows_unwind(libSceAgcDriver) if(BUILD_TESTING) + add_test_executable(agc_frame_dump_io_tests tests/FrameDumpIo.cpp) + target_include_directories(agc_frame_dump_io_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(agc_frame_dump_io_tests PRIVATE libc) + configure_windows_unwind(agc_frame_dump_io_tests) + add_test(NAME agc_frame_dump_io COMMAND agc_frame_dump_io_tests) + set_tests_properties(agc_frame_dump_io PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(agc_frame_dump_io PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(agc_write_watch_coverage_tests tests/WriteWatchCoverage.cpp) target_include_directories(agc_write_watch_coverage_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(agc_write_watch_coverage_tests PRIVATE libc) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/FrameDumpIo.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/FrameDumpIo.hpp new file mode 100644 index 0000000000..fd2a0317b9 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Execution/include/FrameDumpIo.hpp @@ -0,0 +1,60 @@ +#pragma once +#include +#include +#include +#include +#include +#include + +namespace AgcDriver { + +struct FrameDumpIoFailure { + const char* operation = nullptr; + int error = 0; + explicit operator bool() const noexcept { return operation != nullptr; } +}; + +inline FrameDumpIoFailure WriteBmpStream_nid_no_patch(std::FILE* file, std::uint32_t width, std::uint32_t height, std::span pixels) noexcept { + const std::uint32_t imageBytes = width * height * 4; + const std::uint32_t header[13] = {0, 0, 54, 40, width, static_cast(-static_cast(height)), 1u | (32u << 16u), 0, imageBytes, 2835, 2835, 0, 0}; + const std::uint16_t magic = 0x4d42; + const std::uint32_t fileBytes = 54 + imageBytes; + const auto write = [&](const void* data, std::size_t size, const char* operation) { + errno = 0; + if (std::fwrite(data, 1, size, file) != size) return FrameDumpIoFailure{operation, errno}; + return FrameDumpIoFailure{}; + }; + if (const auto failure = write(&magic, 2, "header")) return failure; + if (const auto failure = write(&fileBytes, 4, "header")) return failure; + if (const auto failure = write(header + 1, 48, "header")) return failure; + return write(pixels.data(), pixels.size(), "pixels"); +} + +inline FrameDumpIoFailure WriteBmpAndClose_nid_no_patch(std::FILE* file, std::uint32_t width, std::uint32_t height, std::span pixels) noexcept { + auto failure = WriteBmpStream_nid_no_patch(file, width, height, pixels); + errno = 0; + if (std::fclose(file) != 0 && !failure) failure = {"close", errno}; + return failure; +} + +class FrameDumpOutput { +public: + bool Enabled_nid_no_patch() const noexcept { return enabled.load(std::memory_order_relaxed); } + + bool Write_nid_no_patch(const char* path, std::uint32_t width, std::uint32_t height, std::span pixels) noexcept { + if (!Enabled_nid_no_patch()) return false; + errno = 0; + auto* file = std::fopen(path, "wb"); + const auto failure = file != nullptr ? WriteBmpAndClose_nid_no_patch(file, width, height, pixels) : FrameDumpIoFailure{"open", errno}; + if (!failure) return true; + if (enabled.exchange(false, std::memory_order_relaxed)) { + std::fprintf(stderr, "[gpu] optional BMP dumps disabled: %s failed (errno=%d)\n", failure.operation, failure.error); + } + return false; + } + +private: + std::atomic enabled{true}; +}; + +} diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index bb39fa7beb..e8594fb333 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -1,4 +1,5 @@ #include "prx/libSceAgcDriver/Execution/include/ProfileOutput.hpp" +#include "prx/libSceAgcDriver/Execution/include/FrameDumpIo.hpp" #include "prx/libSceAgcDriver/Graphics/include/PipelineSpecialization.hpp" #include "prx/libSceAgcDriver/Execution/include/CaptureTrace.hpp" #include "BdaAbi.hpp" @@ -134,6 +135,7 @@ using ResourceCache = Graphics::ResourceCache; std::uint32_t DumpScale(); void WriteFrameBmp(int index, std::uint32_t fullWidth, std::uint32_t fullHeight, std::span full, std::uint32_t scale); +FrameDumpOutput frameDumpOutput; class FrameDumpWriter { public: @@ -150,6 +152,7 @@ public: std::lock_guard lock(mutex); require(!stopping, "frame dump writer has stopped"); require(pixels.size() <= maxBytes - queuedBytes, "frame dump queue exceeded 64 MiB"); + if (!frameDumpOutput.Enabled_nid_no_patch()) return; if (!worker.joinable()) worker = std::thread([this] { run(); }); const auto bytes = pixels.size(); pending.push_back({index, width, height, scale, std::move(pixels)}); @@ -2719,20 +2722,10 @@ void VulkanDevice::PresentPixels(std::uint32_t width, std::uint32_t height, std: return value ? std::atoi(value) : 0; }(); static int dumped = 0; - if (dumped < dumpLimit) { + if (frameDumpOutput.Enabled_nid_no_patch() && dumped < dumpLimit) { char name[32]; std::snprintf(name, sizeof(name), "frame_%03d.bmp", dumped++); - if (std::FILE* file = std::fopen(name, "wb")) { - const std::uint32_t imageBytes = width * height * 4; - const std::uint32_t header[13] = {0, 0, 54, 40, width, static_cast(-static_cast(height)), 1u | (32u << 16u), 0, imageBytes, 2835, 2835, 0, 0}; - const std::uint16_t magic = 0x4d42; - const std::uint32_t fileBytes = 54 + imageBytes; - std::fwrite(&magic, 2, 1, file); - std::fwrite(&fileBytes, 4, 1, file); - std::fwrite(header + 1, 4, 12, file); - std::fwrite(pixels.data(), 1, pixels.size(), file); - std::fclose(file); - } + frameDumpOutput.Write_nid_no_patch(name, width, height, pixels); } // Tests and tools present pixels synchronously; the game path splits the steps (see Driver::Present). if (!present(width, height, true, pixels)) return; @@ -2760,6 +2753,7 @@ std::uint32_t DumpScale() { // frame_.bmp: every DumpScale()-th pixel of a BGRA8 image, 32-bit top-down. void WriteFrameBmp(int index, std::uint32_t fullWidth, std::uint32_t fullHeight, std::span full, std::uint32_t scale) { require(scale != 0 && full.size() == static_cast(fullWidth) * fullHeight * 4, "invalid frame dump pixels"); + if (!frameDumpOutput.Enabled_nid_no_patch()) return; const std::uint32_t width = (fullWidth + scale - 1) / scale; const std::uint32_t height = (fullHeight + scale - 1) / scale; std::vector pixels(static_cast(width) * height * 4); @@ -2768,20 +2762,7 @@ void WriteFrameBmp(int index, std::uint32_t fullWidth, std::uint32_t fullHeight, } char name[32]; std::snprintf(name, sizeof(name), "frame_%03d.bmp", index); - { - std::ofstream file; - file.exceptions(std::ios::failbit | std::ios::badbit); - file.open(name, std::ios::binary); - const std::uint32_t imageBytes = width * height * 4; - const std::uint32_t header[13] = {0, 0, 54, 40, width, static_cast(-static_cast(height)), 1u | (32u << 16u), 0, imageBytes, 2835, 2835, 0, 0}; - const std::uint16_t magic = 0x4d42; - const std::uint32_t fileBytes = 54 + imageBytes; - file.write(reinterpret_cast(&magic), 2); - file.write(reinterpret_cast(&fileBytes), 4); - file.write(reinterpret_cast(header + 1), 48); - file.write(reinterpret_cast(pixels.data()), static_cast(pixels.size())); - file.close(); - } + frameDumpOutput.Write_nid_no_patch(name, width, height, pixels); } ResidentPresent ResidentPresentable(const Graphics::Context& context, const Graphics::StorageTexture& image, const DisplayBuffer& buffer, VkFilter& filter, std::uint32_t& layer) { @@ -2887,7 +2868,7 @@ bool VulkanDevice::PresentDisplayBuffer(const DisplayBuffer& buffer) { if (passthrough) ensureHdrScaler(DisplayTexelFormat(buffer.pixelFormat)); const auto pixels = passthrough ? ReadDisplayBufferTexels(buffer) : ReadDisplayBuffer(buffer); bool dumpFrame = false; - if (dumps.dumped < dumps.limit && ++dumps.presents % static_cast(dumps.every) == 0) { + if (frameDumpOutput.Enabled_nid_no_patch() && dumps.dumped < dumps.limit && ++dumps.presents % static_cast(dumps.every) == 0) { if (passthrough) { WriteFrameBmp(dumps.dumped++, buffer.width, buffer.height, ReadDisplayBuffer(buffer), DumpScale()); } else if (dumps.cpu) { @@ -2942,7 +2923,7 @@ bool VulkanDevice::PresentDisplayBuffer(const DisplayBuffer& buffer) { } const auto cleared = buffer.dccAddress != 0 && resident == nullptr ? CompressedClearPixel(buffer, bytes) : std::nullopt; bool dumpFrame = false; - if (dumps.dumped < dumps.limit && ++dumps.presents % static_cast(dumps.every) == 0) { + if (frameDumpOutput.Enabled_nid_no_patch() && dumps.dumped < dumps.limit && ++dumps.presents % static_cast(dumps.every) == 0) { if (dumps.cpu || (passthrough && resident == nullptr)) { auto full = cleared ? std::vector(static_cast(buffer.width) * buffer.height * cleared->size()) : ReadDisplayBuffer(buffer); for (std::size_t offset = 0; cleared && offset < full.size(); offset += cleared->size()) std::memcpy(full.data() + offset, cleared->data(), cleared->size()); diff --git a/core/libs/prx/libSceAgcDriver/tests/FrameDumpIo.cpp b/core/libs/prx/libSceAgcDriver/tests/FrameDumpIo.cpp new file mode 100644 index 0000000000..a23ee5531f --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/FrameDumpIo.cpp @@ -0,0 +1,149 @@ +#include "prx/libSceAgcDriver/Execution/include/FrameDumpIo.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef _WIN32 +#include +#include +#else +#include +#endif + +namespace { + +void Check(bool condition, const char* message) { + if (!condition) { + std::fprintf(stderr, "%s\n", message); + std::exit(1); + } +} + +void Close(int descriptor) { +#ifdef _WIN32 + _close(descriptor); +#else + close(descriptor); +#endif +} + +int Read(int descriptor, void* bytes, unsigned count) { +#ifdef _WIN32 + return _read(descriptor, bytes, count); +#else + return static_cast(read(descriptor, bytes, count)); +#endif +} + +std::FILE* Pipe(int& reader) { + int descriptors[2]; +#ifdef _WIN32 + Check(_pipe(descriptors, 4096, _O_BINARY) == 0, "cannot create test pipe"); + auto* file = _fdopen(descriptors[1], "wb"); +#else + Check(pipe(descriptors) == 0, "cannot create test pipe"); + auto* file = fdopen(descriptors[1], "wb"); +#endif + Check(file != nullptr, "cannot open test pipe writer"); + reader = descriptors[0]; + return file; +} + +void SuccessfulBytes() { + auto* file = std::tmpfile(); + Check(file != nullptr, "cannot create BMP test file"); + const std::array pixels{std::byte{1}, std::byte{2}, std::byte{3}, std::byte{4}}; + Check(!AgcDriver::WriteBmpStream_nid_no_patch(file, 2, 2, pixels), "valid BMP write failed"); + Check(std::fflush(file) == 0, "valid BMP flush failed"); + std::rewind(file); + std::array actual{}; + Check(std::fread(actual.data(), 1, actual.size(), file) == actual.size(), "BMP has the wrong byte count"); + const std::array header{ + 0x42, 0x4d, 70, 0, 0, 0, 0, 0, 0, 0, 54, 0, 0, 0, + 40, 0, 0, 0, 2, 0, 0, 0, 0xfe, 0xff, 0xff, 0xff, + 1, 0, 32, 0, 0, 0, 0, 0, 16, 0, 0, 0, + 0x13, 0x0b, 0, 0, 0x13, 0x0b, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; + Check(std::memcmp(actual.data(), header.data(), header.size()) == 0, "BMP header changed"); + Check(std::memcmp(actual.data() + header.size(), pixels.data(), pixels.size()) == 0, "BMP pixel payload changed"); + Check(std::fclose(file) == 0, "BMP test close failed"); +} + +void HeaderFailure() { + int reader; + auto* file = Pipe(reader); + Check(std::setvbuf(file, nullptr, _IONBF, 0) == 0, "cannot disable pipe buffering"); + Close(reader); + const std::array pixels{}; + const auto failure = AgcDriver::WriteBmpAndClose_nid_no_patch(file, 2, 2, pixels); + Check(failure && std::strcmp(failure.operation, "header") == 0, "closed pipe header write was accepted"); +} + +void PixelFailure() { + int reader; + auto* file = Pipe(reader); + Check(std::setvbuf(file, nullptr, _IONBF, 0) == 0, "cannot disable pipe buffering"); + unsigned received = 0; + std::thread consumer([&] { + std::array header{}; + while (received < header.size()) { + const auto count = Read(reader, header.data() + received, static_cast(header.size() - received)); + if (count <= 0) break; + received += static_cast(count); + } + Close(reader); + }); + const std::vector pixels(128 * 128 * 4); + const auto failure = AgcDriver::WriteBmpAndClose_nid_no_patch(file, 128, 128, pixels); + consumer.join(); + Check(received == 54, "pixel failure did not follow a complete BMP header"); + Check(failure && std::strcmp(failure.operation, "pixels") == 0, "closed pipe pixel write was accepted"); +} + +void CloseFailure() { + int reader; + auto* file = Pipe(reader); + std::array buffer{}; + Check(std::setvbuf(file, buffer.data(), _IOFBF, buffer.size()) == 0, "cannot buffer test pipe"); + Close(reader); + const std::array pixels{}; + const auto failure = AgcDriver::WriteBmpAndClose_nid_no_patch(file, 2, 2, pixels); + Check(failure && std::strcmp(failure.operation, "close") == 0, "failed buffered close was accepted"); +} + +void DisableAfterFailure() { + const auto directory = std::filesystem::temp_directory_path() / ("anyps5-bmp-io-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); + Check(std::filesystem::create_directory(directory), "cannot create output-failure fixture"); + AgcDriver::FrameDumpOutput output; + const std::array pixels{}; + Check(output.Enabled_nid_no_patch(), "BMP output was disabled before any failure"); + const auto blocked = directory.string(); + Check(!output.Write_nid_no_patch(blocked.c_str(), 2, 2, pixels), "BMP output accepted a directory as a file"); + Check(!output.Enabled_nid_no_patch(), "failed BMP output remained enabled"); + const auto skipped = directory / "must-not-exist.bmp"; + const auto path = skipped.string(); + for (unsigned i = 0; i < 64; ++i) Check(!output.Write_nid_no_patch(path.c_str(), 2, 2, pixels), "disabled BMP output wrote another file"); + Check(!std::filesystem::exists(skipped), "disabled BMP output created a file"); + Check(std::filesystem::remove(directory), "cannot remove empty fixture directory"); +} + +} + +int main() { +#ifndef _WIN32 + const auto previous = std::signal(SIGPIPE, SIG_IGN); +#endif + SuccessfulBytes(); + HeaderFailure(); + PixelFailure(); + CloseFailure(); + DisableAfterFailure(); +#ifndef _WIN32 + std::signal(SIGPIPE, previous); +#endif + std::puts("frame dump I/O tests passed: bytes/header/pixels/close/disable"); + return 0; +} diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 0b7cad5cbd..dbcf98f954 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -2,6 +2,8 @@ ### Unknown function info +- [Optional BMP frame dumps](../../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) use checked host stdio. An open, header, pixel or close failure prints one diagnostic and disables subsequent BMP dumps for the process; queued frames drain without more file I/O. Pixel validation and allocation failures retain the existing fatal path. The CPU fixture checks identical BMP bytes and real pipe failures; hardware presentation after a storage error is unmeasured. + - [Inactive vertex point size](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) - `PA_CL_VS_OUT_CNTL.USE_VTX_POINT_SIZE` is accepted for lines, triangles and rectangles with `GS_EN` and `HS_EN` clear, when polygon mode is disabled or both faces are filled. Points, patches and absent routing or rasterization registers still throw. Bit 16 selects the misc export's X component as PointSize ([AMD CIK register reference](https://www.amd.com/content/dam/amd/en/documents/radeon-tech-docs/programmer-references/CIK_3D_registers_v2.pdf), PA_CL_VS_OUT_CNTL); ignoring it for non-point rasterization follows [Vulkan PointSize](https://docs.vulkan.org/refpages/latest/refpages/source/PointSize.html). This is inferred, not measured on console hardware. XashPS5 (PPSA19111 v1.3) sets `0x01210000` on a filled triangle draw. - [Native MSAA depth attachment feedback](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) is not implemented. A draw sampling a native multisampled depth view while attaching its source image throws before pipeline creation or render-pass recording, including read-only attachment feedback. Sampling that image in a separate draw or dispatch remains supported. The refusal compares the held native image views and their device and attachment identity; single-sample depth behavior is retained. - [Native multisampled depth sampling](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) accepts numeric D16 or D32, two/four/eight samples, one Z_X slice and mip, and pipe-aligned ITERATE_256 HTILE without stencil or VRS. Sampling eligibility is recorded before the attachment's first host clear: every HTILE word must prove the same initial clear of zero or one, and the complete sample-aware depth and HTILE ranges must be accessible and write-watched. Unsupported initial state or device sampling support retains attachment rendering but sampling throws; eligibility is never retried. Sampling rejects later guest writes, queued external GPU writes, and HTILE clear/fill transitions. Guest publication, arbitrary guest depth import, stencil/raw-bit sampling, arrays, storage access and allocation retirement remain unimplemented. The block footprint follows main's AMD Addrlib thin-block rule; the accepted HTILE clear words and PS5 equivalence are not measured on console hardware. PPSA99902's historical resident eight-sample D32 reader is documented in [#419](https://github.com/boykopovar/AnyPS5/pull/419#issuecomment-6105597729); the subsequent completed HTILE transition needs separate support. From c6e3af325806c418f0fcee6717e025401638b32d Mon Sep 17 00:00:00 2001 From: milescoviello Date: Wed, 14 Oct 2026 05:01:10 +0000 Subject: [PATCH 0265/1000] fix(ngs2): accept empty waveform blocks and matrix levels on a voice without a format A sampler voice set up with an all-zero format has no channels. Adding no waveform blocks to it, or setting a matrix with no levels, threw std::invalid_argument, which ends the title. Accept both on such a voice: the empty add only applies RESET and the CONTINUE state, and the empty matrix clears that matrix, which rendering already treats as unset. Blocks with data, APPEND, and an empty matrix on a voice with a format are still refused. Steins;Gate Re:Boot (PPSA36591) clears a voice this way when a button is pressed at its title screen. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceNgs2.native/src/Voice.cpp | 6 +++++- .../libSceNgs2.native/tests/Ngs2Stream.cpp | 19 +++++++++++++++++++ docs/dev/debt/libSceNgs2.native.md | 1 + 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceNgs2.native/src/Voice.cpp b/core/libs/prx/libSceNgs2.native/src/Voice.cpp index 3cee147407..1ae32f78af 100644 --- a/core/libs/prx/libSceNgs2.native/src/Voice.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Voice.cpp @@ -95,6 +95,10 @@ static Ngs2Port& PortAt(Ngs2Voice& voice, std::uint32_t port) { } static void SetMatrixLevels(Ngs2Voice& voice, std::uint32_t matrixId, const float* levels, std::uint32_t numLevels) { + if (numLevels == 0 && voice.channels == 0 && matrixId < voice.matrices.size()) { + voice.matrices[matrixId].clear(); + return; + } if (matrixId >= voice.matrices.size() || numLevels == 0 || numLevels > NGS2_MAX_CHANNELS * NGS2_MAX_CHANNELS || levels == nullptr) { APS5_INVALID_ARG_EX; } @@ -208,7 +212,7 @@ static void AppendStreamData(Ngs2Voice& voice, const Ngs2SamplerVoiceWaveformBlo static void AddWaveformBlocks(Ngs2Voice& voice, const Ngs2SamplerVoiceWaveformBlocksParam& param) { constexpr std::uint32_t knownFlags = SCE_NGS2_WAVEFORM_BLOCKS_FLAG_CONTINUE | SCE_NGS2_WAVEFORM_BLOCKS_FLAG_APPEND | SCE_NGS2_WAVEFORM_BLOCKS_FLAG_RESET | SCE_NGS2_WAVEFORM_BLOCKS_FLAG_SILENCE; if ((param.flags & ~knownFlags) != 0) throw std::runtime_error("NGS2: waveform block flags " + Ngs2Hex(param.flags) + " are not implemented"); - if (voice.channels == 0 || (param.num_blocks != 0 && (param.blocks == nullptr || param.data == nullptr))) APS5_INVALID_ARG_EX; + if ((param.num_blocks != 0 && (voice.channels == 0 || param.blocks == nullptr || param.data == nullptr)) || (voice.channels == 0 && (param.flags & SCE_NGS2_WAVEFORM_BLOCKS_FLAG_APPEND) != 0)) APS5_INVALID_ARG_EX; const bool reset = (param.flags & SCE_NGS2_WAVEFORM_BLOCKS_FLAG_RESET) != 0; if (!voice.acceptsBlocks && !reset) throw std::invalid_argument("NGS2: the voice waveform was already closed"); if (reset) { diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Stream.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Stream.cpp index 726cf9148b..fa222ce472 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2Stream.cpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Stream.cpp @@ -158,6 +158,24 @@ static void TestSilenceBlock() { Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); } +static void TestNoBlocksForClearedVoice() { + const auto system = CreateSystem(); + const auto rack = CreateRack(system, SCE_NGS2_RACK_ID_SAMPLER); + const auto voice = RackVoice(rack, 0); + Control(voice, SCE_NGS2_SAMPLER_VOICE_PARAM_SETUP, Ngs2SamplerVoiceSetupParam{{}, {}}); + const Ngs2WaveformBlock unused{}; + Control(voice, SCE_NGS2_SAMPLER_VOICE_PARAM_ADD_WAVEFORM_BLOCKS, Ngs2SamplerVoiceWaveformBlocksParam{{}, &Unused, 0, 0, &unused}); + Control(voice, SCE_NGS2_SAMPLER_VOICE_PARAM_ADD_WAVEFORM_BLOCKS, Ngs2SamplerVoiceWaveformBlocksParam{{}, &Unused, SCE_NGS2_WAVEFORM_BLOCKS_FLAG_RESET, 0, &unused}); + const float level = 1.0f; + Control(voice, SCE_NGS2_VOICE_PARAM_MATRIX_LEVELS, Ngs2VoiceMatrixLevelsParam{{}, 0, 0, &level}); + Require(Throws([&] { Control(voice, SCE_NGS2_VOICE_PARAM_MATRIX_LEVELS, Ngs2VoiceMatrixLevelsParam{{}, 1, 0, &level}); })); + Require(Flags(voice) == 0); + const std::vector pcm(4, 16384); + Require(Throws([&] { AddBlock(voice, pcm.data(), pcm.size() * 2, 4, 0); })); + Require(Throws([&] { Control(voice, SCE_NGS2_SAMPLER_VOICE_PARAM_ADD_WAVEFORM_BLOCKS, Ngs2SamplerVoiceWaveformBlocksParam{{}, &Unused, SCE_NGS2_WAVEFORM_BLOCKS_FLAG_APPEND, 0, &unused}); })); + Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); +} + static void TestDrainedStreamReportsEmpty() { const auto system = CreateSystem(); const auto master = Mastering(system, 1); @@ -190,5 +208,6 @@ int main() { TestStreamRejections(); TestSilenceBlock(); TestDrainedStreamReportsEmpty(); + TestNoBlocksForClearedVoice(); return 0; } diff --git a/docs/dev/debt/libSceNgs2.native.md b/docs/dev/debt/libSceNgs2.native.md index 8baad0d337..c2266020fa 100644 --- a/docs/dev/debt/libSceNgs2.native.md +++ b/docs/dev/debt/libSceNgs2.native.md @@ -8,6 +8,7 @@ - [NGS2 ATRAC9 streaming](../../../core/libs/prx/libSceNgs2.native/src/Atrac9.cpp) keeps waveform duration and skip counts across compressed input blocks. The shared `APPEND` flag (2), called `ONLY_DATA` in the compressed-data reference, ignores an ATRAC9 continuation block's sample, skip and repeat counts, following [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/bb98f26433fa9e711c219aac1711fe8ecba5bf3a/src/libs/ngs2.cpp); PPSA03747 confirms a 262044-byte initial buffer declaring 11310532 samples, followed by 16384-byte buffers with flags 3. Incomplete superframes are retained until more input arrives. Input-block completion callbacks allow refills; the final callback of a logical waveform waits for its buffered PCM to drain, preserving the existing complete-block repeat behaviour. This buffering, callback timing, decoded-byte accounting and starvation silence are unverified on hardware. A closed ATRAC9 stream ending before its declared samples throws; fragmented ATRAC9 waveforms with repeats are unsupported. PCM streaming retains its existing append validation and early-close behaviour. - [NGS2 mastering voice gain](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - the param id 0x30000004 is taken from PPSA22520 and the layout from [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `OrbisNgs2MasteringVoiceGainParam`; no reference gives the units or range. The levels are assumed to be linear factors applied when the mastering voice is mixed into the render buffer, so the voice's own samples stay unscaled; the LFE level applies to channel 3 of 6- and 8-channel voices, assuming the L R C LFE order of the AudioOut2 entries above, and the full-band level to the rest. Negative levels are accepted, and a new setup resets both levels to 1 - [NGS2 stereo mastering voice into a surround buffer](../../../core/libs/prx/libSceNgs2.native/src/Render.cpp) (libSceNgs2.native) - PPSA21402 renders a 2-channel mastering voice into a 6- or 8-channel render buffer, which the mixing stage rejected. The console's conversion is unknown: the left and right channels are assumed to go to the first two buffer channels (front left and right) and the remaining channels stay silent, with no centre, LFE or surround content. Any other channel count mismatch still throws +- [NGS2 voices without a format](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - a sampler voice whose setup has no format (all zero) accepts an add of no waveform blocks without `APPEND`, which only applies `RESET` and the `CONTINUE` state, and matrix levels with no levels, which clear that matrix. PPSA36591 sets such a voice up, adds no blocks and sets no levels when a button is pressed at its title screen. Whether the console returns success or an error there is not measured; blocks with data, `APPEND` and an empty matrix on a voice with a format are still refused - [NGS2 silent waveform blocks](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native): flag `0x10` (named `SILENCE` here; no public reference names it) lets a waveform block declare samples without data. PPSA04867 adds one block with flags `0x11`, `data_size` 0 and `num_samples` 16385, then another with flags `0x11` and a fully specified 0x800-byte, 1024-sample mono block, which takes the ordinary path. A dataless block under the flag outputs `num_samples` frames of silence, reports them through `decoded_data_size`, and finishes like a complete block; a dataless block without the flag still throws as not fitting its data. The flag's real name, its interaction with ATRAC9 and repeats, and the console's `waveform_data` for silent blocks are unverified on hardware - [NGS2 streaming PCM waveforms](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - PPSA21402 streams its music by adding one block whose `num_samples` is the whole track and whose `data_size` is a 16 KiB first piece, with `CONTINUE`, and then adds each following piece with flag `0x2` (called `APPEND` here) and the same `num_samples`. No reference describes flag `0x2`. A 16-bit PCM block without skipped samples or repeats whose `num_samples` exceed its data is assumed to start such a stream; flag `0x2` is assumed to append the block's data to the last streaming block, ignoring its `num_samples`; the voice is assumed to output silence without advancing while it has played all appended data and `CONTINUE` is set; and the waveform is assumed to end at `num_samples` or, once `CONTINUE` is cleared, at the end of the appended data. `sceNgs2VoiceGetState` reports the bytes played as `decoded_data_size`, which the title uses to release its queued data, and the read position as `waveform_data`. Appending to a block that is not a stream, or other formats, skipped samples, repeats and partial frames, throw - [NGS2 matrix levels](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - levels are clamped to -4..4 and NaN is kept, as the PS4 firmware 11.00 `libSceNgs2.sprx` stores them; not yet confirmed on the PS5 library. That library rejects a level count of 0 and keeps the matrix; the PS5 error code is unknown, so a count of 0 throws. `sceNgs2VoiceRunCommands` is not in that library, so its matrix levels command is assumed to clamp the same way From a8be2c8fee47836864870fd1fa2459946776d19e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Thu, 15 Oct 2026 13:49:09 +0200 Subject: [PATCH 0266/1000] docs(ngs2): matrix levels debt line names the voice-without-format exception Co-Authored-By: Claude --- docs/dev/debt/libSceNgs2.native.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceNgs2.native.md b/docs/dev/debt/libSceNgs2.native.md index c2266020fa..812d7348b3 100644 --- a/docs/dev/debt/libSceNgs2.native.md +++ b/docs/dev/debt/libSceNgs2.native.md @@ -11,7 +11,7 @@ - [NGS2 voices without a format](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - a sampler voice whose setup has no format (all zero) accepts an add of no waveform blocks without `APPEND`, which only applies `RESET` and the `CONTINUE` state, and matrix levels with no levels, which clear that matrix. PPSA36591 sets such a voice up, adds no blocks and sets no levels when a button is pressed at its title screen. Whether the console returns success or an error there is not measured; blocks with data, `APPEND` and an empty matrix on a voice with a format are still refused - [NGS2 silent waveform blocks](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native): flag `0x10` (named `SILENCE` here; no public reference names it) lets a waveform block declare samples without data. PPSA04867 adds one block with flags `0x11`, `data_size` 0 and `num_samples` 16385, then another with flags `0x11` and a fully specified 0x800-byte, 1024-sample mono block, which takes the ordinary path. A dataless block under the flag outputs `num_samples` frames of silence, reports them through `decoded_data_size`, and finishes like a complete block; a dataless block without the flag still throws as not fitting its data. The flag's real name, its interaction with ATRAC9 and repeats, and the console's `waveform_data` for silent blocks are unverified on hardware - [NGS2 streaming PCM waveforms](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - PPSA21402 streams its music by adding one block whose `num_samples` is the whole track and whose `data_size` is a 16 KiB first piece, with `CONTINUE`, and then adds each following piece with flag `0x2` (called `APPEND` here) and the same `num_samples`. No reference describes flag `0x2`. A 16-bit PCM block without skipped samples or repeats whose `num_samples` exceed its data is assumed to start such a stream; flag `0x2` is assumed to append the block's data to the last streaming block, ignoring its `num_samples`; the voice is assumed to output silence without advancing while it has played all appended data and `CONTINUE` is set; and the waveform is assumed to end at `num_samples` or, once `CONTINUE` is cleared, at the end of the appended data. `sceNgs2VoiceGetState` reports the bytes played as `decoded_data_size`, which the title uses to release its queued data, and the read position as `waveform_data`. Appending to a block that is not a stream, or other formats, skipped samples, repeats and partial frames, throw -- [NGS2 matrix levels](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - levels are clamped to -4..4 and NaN is kept, as the PS4 firmware 11.00 `libSceNgs2.sprx` stores them; not yet confirmed on the PS5 library. That library rejects a level count of 0 and keeps the matrix; the PS5 error code is unknown, so a count of 0 throws. `sceNgs2VoiceRunCommands` is not in that library, so its matrix levels command is assumed to clamp the same way +- [NGS2 matrix levels](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - levels are clamped to -4..4 and NaN is kept, as the PS4 firmware 11.00 `libSceNgs2.sprx` stores them; not yet confirmed on the PS5 library. That library rejects a level count of 0 and keeps the matrix; the PS5 error code is unknown, so a count of 0 throws, except on a sampler voice without a format, where it clears the matrix (see NGS2 voices without a format). `sceNgs2VoiceRunCommands` is not in that library, so its matrix levels command is assumed to clamp the same way - [NGS2 sampler voice state flag EMPTY](../../../core/libs/prx/libSceNgs2.native/src/Render.cpp) (libSceNgs2.native) - a playing sampler voice that still accepts blocks (`CONTINUE`) and has played all queued blocks reports `INUSE|PLAYING|EMPTY` (0x20) and keeps playing, so blocks added later play on. PPSA18391 (engine: Clickteam Fusion) reads `state_flags & 0x22 != 2` from `sceNgs2VoiceGetState` as the end of a sound and only then reuses the voice. When the console sets and clears EMPTY, and whether it also reports it for other voice types, is unverified on hardware. - [NGS2 port matrix](../../../core/libs/prx/libSceNgs2.native/src/Render.cpp) (libSceNgs2.native) - a port without a matrix, or with a selected matrix whose levels were never set, mixes through a default channel map: mono reaches both stereo channels at -3 dB and the 5.1 and 7.1 centre, stereo folds to mono at -3 dB per side, and wider sources fold down with 0.5 and -3 dB gains. The map and the equivalence of an unset matrix with no matrix were measured from the PS4 firmware 11.00 `libSceNgs2.sprx`, not yet from the PS5 library. That library only accepts submixer and mastering voices with 1, 2, 6 or 8 channels, so the map into 3, 4, 5 or 7 channels throws. A matrix set with fewer levels than the destination needs still mixes only the rows it has - [NGS2 custom submixer rack](../../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (libSceNgs2.native, rack 0x4002) - structure layouts from [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `ngs2_custom.h`; module id 0x1f for UserFx2 and 0x1d for UserFx, voice parameter ids `0x40001f00 | module index` and `0x40001d00 | module index`, the process flags (1 on the first process call after setup, 2 after a parameter change, 0 otherwise) and the call order (setup per voice at rack creation, process after the inputs are mixed, cleanup at destruction) come only from [KytyPS5](https://github.com/KytyPS5/KytyPS5) and shadPS4. Only UserFx and UserFx2 modules on a single buffer are accepted: other module ids, more buffers, a module control handler, different input and output channel counts, setup flags and default rack options throw. Each module's state is a separate buffer, not the `state_offset` slice of the voice state, and `sceNgs2VoiceGetState` still throws for this rack. As in KytyPS5, UserFx2 process runs only on grains where an input produced samples, so an effect tail stops with the input and a pending flag 2 waits for the next audible grain From e168591c196f7f2877db3f98ba4a7c011db0b2ee Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Wed, 14 Oct 2026 05:44:37 +0000 Subject: [PATCH 0267/1000] fix(libkernel): honor all guest signal mask words --- .../libs/prx/libkernel/System/src/Signals.cpp | 34 +++++++++++++------ core/libs/tests/GuestSignals.cpp | 18 ++++++++++ 2 files changed, 41 insertions(+), 11 deletions(-) diff --git a/core/libs/prx/libkernel/System/src/Signals.cpp b/core/libs/prx/libkernel/System/src/Signals.cpp index 1437144b9e..2375d9a002 100644 --- a/core/libs/prx/libkernel/System/src/Signals.cpp +++ b/core/libs/prx/libkernel/System/src/Signals.cpp @@ -42,6 +42,7 @@ constexpr int SaSiginfo = 0x40; std::atomic handlers[MaxSignal + 1]{}; static_assert(std::atomic::is_always_lock_free); std::atomic blockedMask{0}; +std::atomic blockedMaskExtra[3]{}; static_assert(std::atomic::is_always_lock_free); std::mutex registration; GuestSigaction dispositions[MaxSignal + 1]{}; @@ -77,7 +78,12 @@ std::uint32_t SignalBit(int guest) { return 1u << ((guest - 1) % 32); } std::uint32_t SignalWord(int guest) { return static_cast((guest - 1) / 32); } bool SignalInSet(const GuestSignalSet& set, int guest) { return (set.bits[SignalWord(guest)] & SignalBit(guest)) != 0; } bool SetHasSignal(const GuestSignalSet& set) { return (set.bits[0] | set.bits[1] | set.bits[2] | set.bits[3]) != 0; } -bool SignalBlocked(int guest) { return guest <= 32 && (blockedMask.load() & SignalBit(guest)) != 0; } +bool SignalBlocked(int guest) { + if (guest < 1 || guest > MaxSignal) return false; + const auto word = SignalWord(guest); + const auto mask = word == 0 ? blockedMask.load() : blockedMaskExtra[word - 1].load(); + return (mask & SignalBit(guest)) != 0; +} struct PendingSignals { std::mutex lock; @@ -282,7 +288,7 @@ void ExpireAlarm(std::chrono::microseconds interval) { handler = dispositions[GuestSigalrm].handler; } if (handler == 1) return; - if ((blockedMask.load() & (1u << (GuestSigalrm - 1))) != 0) + if (SignalBlocked(GuestSigalrm)) throw std::runtime_error("ITIMER_REAL (it_interval " + DescribeInterval(interval) + ") expired while SIGALRM is blocked: pending signals are not implemented"); if (handler == 0) { std::fprintf(stderr, "SIGALRM: the alarm expired with the default disposition; the process terminates\n"); @@ -524,15 +530,22 @@ int APS5_VABI _sigprocmask_nid_postfix(int how, const GuestSignalSet* set, Guest std::lock_guard lock(registration); if (previousSet != nullptr) { previousSet->bits[0] = blockedMask.load(); - previousSet->bits[1] = 0; - previousSet->bits[2] = 0; - previousSet->bits[3] = 0; + for (int word = 1; word < 4; ++word) previousSet->bits[word] = blockedMaskExtra[word - 1].load(); } if (set != nullptr) { switch (how) { - case 1: blockedMask.fetch_or(set->bits[0]); break; - case 2: blockedMask.fetch_and(~set->bits[0]); break; - case 3: blockedMask.store(set->bits[0]); break; + case 1: + blockedMask.fetch_or(set->bits[0]); + for (int word = 1; word < 4; ++word) blockedMaskExtra[word - 1].fetch_or(set->bits[word]); + break; + case 2: + blockedMask.fetch_and(~set->bits[0]); + for (int word = 1; word < 4; ++word) blockedMaskExtra[word - 1].fetch_and(~set->bits[word]); + break; + case 3: + blockedMask.store(set->bits[0]); + for (int word = 1; word < 4; ++word) blockedMaskExtra[word - 1].store(set->bits[word]); + break; default: throw std::invalid_argument("_sigprocmask: invalid how"); } } @@ -608,9 +621,8 @@ int APS5_VABI sigpending_nid_postfix(GuestSignalSet* set) { auto& pending = Pending(); std::lock_guard lock(pending.lock); set->bits[0] = pending.words[0].load(std::memory_order_relaxed) & blockedMask.load(); - set->bits[1] = 0; - set->bits[2] = 0; - set->bits[3] = 0; + for (int word = 1; word < 4; ++word) + set->bits[word] = pending.words[word].load(std::memory_order_relaxed) & blockedMaskExtra[word - 1].load(); return 0; } } diff --git a/core/libs/tests/GuestSignals.cpp b/core/libs/tests/GuestSignals.cpp index d2fe283b8a..ca36fb074b 100644 --- a/core/libs/tests/GuestSignals.cpp +++ b/core/libs/tests/GuestSignals.cpp @@ -275,4 +275,22 @@ int main() { Require(sigpending_nid_postfix(&pendingSignals) == 0); Require(pendingSignals.bits[0] == 0 && pendingSignals.bits[1] == 0 && pendingSignals.bits[2] == 0 && pendingSignals.bits[3] == 0); Require(signal_nid_postfix(15, nullptr) == Callback); + + const GuestSignalSet highSignals{{0, 1u << 7, 1u << 8, 1u << 31}}; + for (int guest : {40, 73, 128}) Require(signal_nid_postfix(guest, Callback) != invalid); + received = 0; + GuestSignalSet previousHighSignals{}; + Require(sigprocmask_nid_postfix(1, &highSignals, &previousHighSignals) == 0); + Require(previousHighSignals.bits[0] == 0 && previousHighSignals.bits[1] == 0 && + previousHighSignals.bits[2] == 0 && previousHighSignals.bits[3] == 0); + for (int guest : {40, 73, 128}) Require(raise_nid_postfix(guest) == 0 && received == 0); + Require(sigpending_nid_postfix(&pendingSignals) == 0 && + std::memcmp(&pendingSignals, &highSignals, sizeof(highSignals)) == 0); + Require(sigprocmask_nid_postfix(2, &highSignals, &previousHighSignals) == 0); + Require(std::memcmp(&previousHighSignals, &highSignals, sizeof(highSignals)) == 0); + Require(received == 128); + Require(sigpending_nid_postfix(&pendingSignals) == 0 && + pendingSignals.bits[0] == 0 && pendingSignals.bits[1] == 0 && + pendingSignals.bits[2] == 0 && pendingSignals.bits[3] == 0); + for (int guest : {40, 73, 128}) Require(signal_nid_postfix(guest, nullptr) == Callback); } From 4ddff7c6e51857cb1c543c93bee3eb3a2b6a54f2 Mon Sep 17 00:00:00 2001 From: esatturan Date: Sat, 10 Oct 2026 01:25:53 +0300 Subject: [PATCH 0268/1000] fix(agc): take depth fast clears from HTILE written by any kernel --- .../Graphics/src/DepthSurface.cpp | 30 +++++++- .../tests/execution/DepthHtileWrite.cpp | 77 +++++++++++++++++++ docs/dev/debt/libSceAgcDriver.md | 1 + 3 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/DepthHtileWrite.cpp diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp index 5e9089b4e2..423953d8e0 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp @@ -8,7 +8,6 @@ #include "prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureDetiler.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" -#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "RdnaDecoder/include/RdnaDecoder/RdnaDescriptorFormat.hpp" #include #include @@ -434,6 +433,8 @@ public: else Recorder::CountBarriers(Recorder::CommandClass::Copy, 3); } + std::uint64_t htileAddress = 0; + std::uint64_t htileGeneration = 0; const Context context; const DepthTarget target; const VkFormat hostFormat; @@ -888,6 +889,32 @@ const DepthSurface* drawnDepth = nullptr; std::atomic surfacesSerial{0}; +std::uint32_t expandedHtileWord(std::uint32_t word, bool stencilInHtile, VkImageAspectFlags cleared) { + if (!stencilInHtile) return (cleared & VK_IMAGE_ASPECT_DEPTH_BIT) != 0 ? 0xfffc000fu : word; + if ((cleared & VK_IMAGE_ASPECT_DEPTH_BIT) != 0) word = (word & 0x3f0u) | 0xfffff00fu; + if ((cleared & VK_IMAGE_ASPECT_STENCIL_BIT) != 0) word |= 0x3f0u; + return word; +} + +void noteHtileWrites(DepthSurface& surface, const DepthTarget& target) { + if (target.htileAddress == 0) return; + const auto tiles = static_cast((target.extent.width + 7u) / 8u) * ((target.extent.height + 7u) / 8u); + const auto bytes = tiles * 4u; + if (!GuestMemory::Accessible(reinterpret_cast(target.htileAddress), bytes, true)) return; + const auto collected = GuestMemory::CollectWrites(target.htileAddress, bytes); + if (surface.htileAddress == target.htileAddress && surface.htileGeneration != 0 && GuestMemory::UnchangedSince(target.htileAddress, bytes, surface.htileGeneration)) return; + std::vector words(tiles); + GuestMemory::Read(target.htileAddress, std::as_writable_bytes(std::span(words)), 4); + surface.htileAddress = target.htileAddress; + surface.htileGeneration = collected; + VkImageAspectFlags cleared = VK_IMAGE_ASPECT_DEPTH_BIT | (target.htileStencil ? VK_IMAGE_ASPECT_STENCIL_BIT : 0u); + for (const auto word : words) cleared &= HtileFillClears(word, target.htileStencil); + if (cleared == 0) return; + surface.pendingClear |= cleared; + for (auto& word : words) word = expandedHtileWord(word, target.htileStencil, cleared); + GuestMemory::Write(target.htileAddress, std::as_bytes(std::span(words)), 4); + surface.htileGeneration = GuestMemory::CollectWrites(target.htileAddress, bytes); +} } std::uint64_t DepthSurfacesSerial() { @@ -1043,6 +1070,7 @@ VkImageView DepthSurfaceView(const Context& context, const DepthTarget& target) if (target.samples != 1u) surface->sampling.refusal = "native multisample depth fast-clear transition is not implemented"; surface->pendingClear |= VK_IMAGE_ASPECT_DEPTH_BIT; } + noteHtileWrites(*surface, target); surface->ApplyFastClear(); if (!created || inherits) surface->written = ++depthWrites; surface->NoteMemory(); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DepthHtileWrite.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DepthHtileWrite.cpp new file mode 100644 index 0000000000..5acbc519b1 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DepthHtileWrite.cpp @@ -0,0 +1,77 @@ +#include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" +#include "DepthFastClearHarness.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include + +namespace { + +using DepthFastClearHarness::Compile; +using DepthFastClearHarness::Covered; +using DepthFastClearHarness::Draw; +using DepthFastClearHarness::DrawOptions; +using DepthFastClearHarness::Height; +using DepthFastClearHarness::Require; +using DepthFastClearHarness::Surface; +using DepthFastClearHarness::Width; + +constexpr std::size_t Tiles = (Width / 8u) * (Height / 8u); + +alignas(4096) std::array Depth{}; +alignas(4096) std::array StencilDepth{}; +alignas(4096) std::array Stencil{}; +alignas(256) std::array Htile{}; + +void Fill(std::uint32_t word, std::size_t count = Tiles) { + std::fill_n(Htile.begin(), count, word); +} + +bool Holds(std::uint32_t word) { + return std::all_of(Htile.begin(), Htile.begin() + Tiles, [&](std::uint32_t value) { return value == word; }); +} + +DrawOptions StencilDraw(VkCompareOp compare, std::uint32_t reference, VkStencilOp op) { + DrawOptions options; + options.depthTest = false; + options.stencilTest = true; + options.stencil = {op, op, op, compare, 0xffu, op == VK_STENCIL_OP_KEEP ? 0u : 0xffu, reference}; + return options; +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + const auto shaders = Compile(*device); + + Fill(0xfffc000fu); + const Surface depth{reinterpret_cast(Depth.data()), 0, reinterpret_cast(Htile.data()), false, VK_FORMAT_D32_SFLOAT}; + Require(Draw(*device, shaders, depth, {.depthWrite = true}) == Covered, "a new depth surface was not cleared to DB_DEPTH_CLEAR (1.0)"); + Require(Draw(*device, shaders, depth, {}) == 0u, "a draw at depth 0.5 passed LESS against the 0.5 written before it"); + Fill(0u, Tiles - 1); + Require(Draw(*device, shaders, depth, {}) == 0u, "an HTILE with one tile left expanded cleared the depth"); + Fill(0u); + Require(Draw(*device, shaders, depth, {.depthWrite = true}) == Covered, "an HTILE written with ZMask 0 on every tile did not clear the depth to DB_DEPTH_CLEAR (1.0)"); + Require(Holds(0xfffc000fu), "the HTILE of the cleared depth was not stored back as expanded"); + Require(Draw(*device, shaders, depth, {}) == 0u, "the depth written after the clear was cleared again"); + + Fill(0xfffff3ffu); + const Surface stencil{reinterpret_cast(StencilDepth.data()), reinterpret_cast(Stencil.data()), reinterpret_cast(Htile.data()), true, VK_FORMAT_D32_SFLOAT_S8_UINT}; + Require(Draw(*device, shaders, stencil, StencilDraw(VK_COMPARE_OP_ALWAYS, 5u, VK_STENCIL_OP_REPLACE)) == Covered, "a stencil write of 5 did not pass ALWAYS"); + Require(Draw(*device, shaders, stencil, StencilDraw(VK_COMPARE_OP_EQUAL, 5u, VK_STENCIL_OP_KEEP)) == Covered, "the stencil plane does not hold the 5 written before"); + Fill(0xfffff00fu); + Require(Draw(*device, shaders, stencil, StencilDraw(VK_COMPARE_OP_EQUAL, 0u, VK_STENCIL_OP_KEEP)) == Covered, "an HTILE written with SMem 0 on every tile did not clear the stencil to DB_STENCIL_CLEAR (0)"); + Require(Holds(0xfffff3ffu), "the HTILE of the cleared stencil was not stored back as expanded"); + Require(Draw(*device, shaders, stencil, StencilDraw(VK_COMPARE_OP_ALWAYS, 9u, VK_STENCIL_OP_REPLACE)) == Covered, "a stencil write of 9 did not pass ALWAYS"); + Require(Draw(*device, shaders, stencil, StencilDraw(VK_COMPARE_OP_EQUAL, 9u, VK_STENCIL_OP_KEEP)) == Covered, "the stencil written after the clear was cleared again"); + std::puts("depth HTILE write tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index afd6b162d7..cc717bf764 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -94,6 +94,7 @@ - [Descriptor pool chain](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - each pool holds two storage buffer descriptors more than a set can take, as a workaround: RADV (Mesa 26.0 and later) lets a `FREE_DESCRIPTOR_SET` pool place a set up to 32 bytes past the pool's buffer ([`util_vma_heap_init`](https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/src/amd/vulkan/radv_descriptor_pool.c) is given `bo_size + RADV_POOL_HEAP_OFFSET` as a size), and with a page-multiple pool size those bytes are an unmapped page - [CMASK fast clears](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver) - each fast-cleared CMASK tile (code 0) is stored as `CB_COLOR_CLEAR_WORD` into its 8x8 texels at the first draw into the target or at its fast-clear eliminate or DCC decompress pass, not as the color block writes the tile, and the CMASK is then stored as expanded (0xF): a texture or copy read of the target after that draw and before the title's eliminate sees the clear value in tiles no draw wrote, where the console keeps their previous bytes. Tile addresses follow the pipe-aligned SW_64KB_Z_X CMASK equation for 16 pipes; other CMASK swizzle modes, a cleared tile of a DCC target and a fast clear of a mipmapped (base mip 0), 3D or array color target throw. A tile with a code other than 0 and 0xF keeps its texels like an expanded tile: PPSA18391 (engine: Clickteam Fusion) fast clears a target whose CMASK it never initialized, so some of its nibbles hold leftover values such as 1 and 10; what the console does with such codes on a single-sample target is unverified on hardware. Single-sample views with `viewMip > 0` skip CMASK fast-clear decoding since CMASK metadata only covers base mip 0 on AMD hardware (Mesa `si_state.c` similarly clears `FAST_CLEAR` for level > 0). - [HTILE depth fast clears](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver) - HTILE is not modeled. Only RADV's `clear_htile_mask` compute kernel (17 GFX10 words, 64x1x1 groups) is recognized, and only when it leaves ZMASK at 0 (`user[4]` and `user[5]` low nibbles both 0); the dispatch still runs, and the host depth image is cleared to `DB_DEPTH_CLEAR` the next time the surface with that `DB_HTILE_DATA_BASE` address is bound. The address match is exact, so a clear of a different HTILE range does not hit. HTILE initialization, the stencil-only update, and fast clears made by the SDK's own paths, CP DMA fills or other kernels are not covered and leave the depth image as it was. The kernel words are the ones the PR author saw RADV submit; they are not compared with Mesa's source. A second kernel, the 9-word dword fill PPSA01802 (Kena: Bridge of Spirits) clears its depth HTILE with (64x1x1 groups, one `buffer_store_format_x` per thread through the stride-4 V# in `user[0..3]`, the value read by `s_buffer_load_dword` through the V# in `user[4..7]`), is taken as a fill of that range when the store format is 32_UINT, 32_SINT or 32_FLOAT and the value V# covers the dword (NUM_RECORDS times its stride, or NUM_RECORDS bytes when the stride is 0) ([`MatchMemoryValueFill`](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp), then the whole-HTILE and ZMask rules of `NoteDepthMetadataFill`). The value is read from guest memory when the dispatch is decoded, so a value that earlier GPU work in the same submission has not written back yet is not seen. When the [fill HLE](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferFill.cpp) takes the dispatch instead (an output descriptor of one 32-bit value per element, a value descriptor that covers the loaded dword, a 16-byte aligned range), the value is read again when the fill is recorded, after the labels queued before it, and the HTILE fill is noted with that value. +- [HTILE written by other kernels](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver) - when a depth target is bound and its HTILE was written since it was last read, the words are read: when every one marks the depth (ZMask 0) or the stencil (SMem 0) cleared, the surface takes that fast clear and the words are stored back as expanded (depth only: ZMask 15 over the full Z range; with stencil: ZMask 15, Z range and SMem, SR1 and SR0 3), so only the title's next clear clears it again. PPSA29141 clears its depth every frame with a compute kernel the fill hooks do not recognize (`InvokeHTileClearComputeShader` in its `ClearMRT` pass); without this its depth kept every earlier frame and moving objects left black trails. The word layouts are Mesa radv's for GFX10. Metadata with some tiles cleared is not modelled, and the compression state draws leave in HTILE is not tracked, so a title reading its HTILE after a draw sees the stored expanded words - [Host imports](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver) on Linux skip ranges that are not mapped read-write, which are copied instead. The NVIDIA driver (595.91.07) refuses `VK_EXT_external_memory_host` imports of read-only or executable mappings (and shared mappings of regular files) with `VK_ERROR_OUT_OF_DEVICE_MEMORY`, after which every `vkAllocateMemory` on the device fails the same way. Read-only ranges are never imported, even on drivers that accept them. Shared mappings of regular files are not checked for. On Windows, an alias of adjacent registered ranges is refused if any page is not a shared view; a range made entirely of shared views is aliased run by run, including across sections or nonconsecutive section offsets. Adjacent registered ranges are joined into one import where they fill a whole 8 MiB aligned window; ranges of a partly filled window, and ranges larger than the window, are imported one at a time. A buffer bound across adjacent ranges that are each larger than the window, or bound exactly over adjacent ranges of any size (from the first one's start to the last one's end), joins them into one import when it is bound, and their own imports are retired; a buffer that crosses any other two imports is copied. A heap grown in pieces re-imports its joined run only when a buffer is bound exactly over all of its pieces, or when a sparse descriptor heap over it is bound: a heap a shader binds whole while only the pieces from its base are mapped is bound in place from the import of that run, its views ending at the import's end as the copy's do, once the previous bind of that address saw the same last mapped byte (a heap that grows between every two of its binds stays copied; one bound more often than it grows is joined again after each growth) and at most 64 times per joined run base; past that, with a message, it is not joined again: it is bound in place while an existing import covers its run and copied otherwise, until no registered range starts at that base or the device changes. Ranges larger than the window are taken to be registered whole and never grown in pieces: a title that grows a heap in pieces larger than 8 MiB re-imports the joined run each time a bound buffer crosses into a new piece. The 8 MiB window is a choice: unbounded runs were re-imported whole on every growth of a heap mapped in small pieces, and the NVIDIA driver (610.57.04, 615.78.08) then refused imports or lost the device. Not measured on RADV. - [Cube texture descriptors](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp) with equal `BASE_ARRAY` and `DEPTH` expose six faces starting at `BASE_ARRAY`. Measured with `image_load`, `image_sample` and `image_get_resinfo` on RX 6600 (gfx1032), using 64x64 R32_UINT faces and base slices 0, 5 and 6. Other ranges containing fewer than six faces or incomplete cube arrays still throw: hardware load bounds and filtered-sample addressing differ. Out-of-range cube-array sampling remains unimplemented. Samplers with `DISABLE_CUBE_WRAP` are created with `VK_SAMPLER_CREATE_NON_SEAMLESS_CUBE_MAP_BIT_EXT`, the flag RADV maps to that bit (Mesa `src/amd/vulkan/radv_sampler.c`, `src/amd/common/ac_descriptors.c`); without `VK_EXT_non_seamless_cube_map` a cube texture sampled through such a sampler throws. PS5 equivalence has not been measured on console hardware. - [Array layer views](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp) (libSceAgcDriver) - `ArrayLayersServeView` lets a 2D texture at a 2D array image's base address, or a 2D array texture over its first layers, view that image (`Texture::CanCopyFrom`). Mesa-built homebrew (ps5-opengl) writes the view's last slice plus one to `CB_COLOR_ATTRIB3.MIP0_DEPTH`, so a slice-0 render target is a two-layer array image, and the title samples it as a plain 2D texture (SuperTuxKart PPSA69421 drew a black 3D scene and minimap without this). The per-layer layout of a mipmapped array is assumed equal to a standalone surface's; it is not measured on the console. A 2D texture at a later slice's address still gets an image of its own. From 3e61fcf1f93cb9a598462145647e870206ffaec4 Mon Sep 17 00:00:00 2001 From: AlxcBTW <187763517+AlxcBTW@users.noreply.github.com> Date: Tue, 13 Oct 2026 10:04:56 +0200 Subject: [PATCH 0269/1000] fix(agc): read and store back the whole pipe-aligned HTILE outside the surfaces lock The bind-time HTILE read covered one word per 8x8 tile from DB_HTILE_DATA_BASE, but main models the HTILE as the pipe-aligned SW_64KB_Z_X metadata (HtileMetadataBytes, the extent HtileFillCovers takes): at 1920x1080, 1856 of the 32400 tile words lie past the first tiles*4 bytes, so they kept ZMask 0 after a store-back. The read and the store-back now cover the whole metadata and run before the surfaces mutex is taken, since both wait for recorded work over the range. The tests' HTILE arrays hold the whole metadata. PPSA25344 (Unreal Engine 5) decodes its depth through HTILE in its Nanite depth export after the depth prepass and takes ZMask 0 as a tile holding the clear value; the new agc_driver_depth_fast_clear_submit case checks that a draw after a fast clear leaves every word expanded. --- .../Graphics/src/DepthSurface.cpp | 50 ++++++++++++------- .../tests/execution/DepthFastClear.cpp | 2 +- .../tests/execution/DepthFastClearSubmit.cpp | 16 ++++++ .../tests/execution/DepthHtileWrite.cpp | 5 +- docs/dev/debt/libSceAgcDriver.md | 2 +- 5 files changed, 54 insertions(+), 21 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp index 423953d8e0..08ca7c5f7b 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include #include #include @@ -433,8 +434,6 @@ public: else Recorder::CountBarriers(Recorder::CommandClass::Copy, 3); } - std::uint64_t htileAddress = 0; - std::uint64_t htileGeneration = 0; const Context context; const DepthTarget target; const VkFormat hostFormat; @@ -896,24 +895,40 @@ std::uint32_t expandedHtileWord(std::uint32_t word, bool stencilInHtile, VkImage return word; } -void noteHtileWrites(DepthSurface& surface, const DepthTarget& target) { - if (target.htileAddress == 0) return; - const auto tiles = static_cast((target.extent.width + 7u) / 8u) * ((target.extent.height + 7u) / 8u); - const auto bytes = tiles * 4u; - if (!GuestMemory::Accessible(reinterpret_cast(target.htileAddress), bytes, true)) return; +std::mutex& htileReadsMutex() { + static std::mutex mutex; + return mutex; +} + +std::map& htileReads() { + static auto* reads = new std::map(); + return *reads; +} + +VkImageAspectFlags storeBackClearedHtile(const Context& context, const DepthTarget& target) { + if (target.htileAddress == 0 || target.samples != 1u) return 0; + const auto bytes = static_cast(HtileMetadataBytes(target.extent)); + if (!GuestMemory::Accessible(reinterpret_cast(target.htileAddress), bytes, true)) return 0; const auto collected = GuestMemory::CollectWrites(target.htileAddress, bytes); - if (surface.htileAddress == target.htileAddress && surface.htileGeneration != 0 && GuestMemory::UnchangedSince(target.htileAddress, bytes, surface.htileGeneration)) return; - std::vector words(tiles); + auto* recorder = RecorderFor(context); + if (recorder == nullptr || !recorder->PendingWriteOverlaps(target.htileAddress, bytes)) { + std::lock_guard lock(htileReadsMutex()); + const auto found = htileReads().find(target.htileAddress); + if (found != htileReads().end() && found->second != 0 && GuestMemory::UnchangedSince(target.htileAddress, bytes, found->second)) return 0; + } + std::vector words(bytes / 4u); GuestMemory::Read(target.htileAddress, std::as_writable_bytes(std::span(words)), 4); - surface.htileAddress = target.htileAddress; - surface.htileGeneration = collected; VkImageAspectFlags cleared = VK_IMAGE_ASPECT_DEPTH_BIT | (target.htileStencil ? VK_IMAGE_ASPECT_STENCIL_BIT : 0u); for (const auto word : words) cleared &= HtileFillClears(word, target.htileStencil); - if (cleared == 0) return; - surface.pendingClear |= cleared; - for (auto& word : words) word = expandedHtileWord(word, target.htileStencil, cleared); - GuestMemory::Write(target.htileAddress, std::as_bytes(std::span(words)), 4); - surface.htileGeneration = GuestMemory::CollectWrites(target.htileAddress, bytes); + auto generation = collected; + if (cleared != 0) { + for (auto& word : words) word = expandedHtileWord(word, target.htileStencil, cleared); + GuestMemory::Write(target.htileAddress, std::as_bytes(std::span(words)), 4); + generation = GuestMemory::CollectWrites(target.htileAddress, bytes); + } + std::lock_guard lock(htileReadsMutex()); + htileReads()[target.htileAddress] = generation; + return cleared; } } @@ -1017,6 +1032,7 @@ float HostDepthBiasConstant(const Context& context, VkFormat format, float const } VkImageView DepthSurfaceView(const Context& context, const DepthTarget& target) { + const auto htileCleared = storeBackClearedHtile(context, target); std::lock_guard gpu(GuestMemory::GpuMutex()); std::unique_lock lock(surfacesMutex()); const bool cleared = takeClearedHtile(target); @@ -1070,7 +1086,7 @@ VkImageView DepthSurfaceView(const Context& context, const DepthTarget& target) if (target.samples != 1u) surface->sampling.refusal = "native multisample depth fast-clear transition is not implemented"; surface->pendingClear |= VK_IMAGE_ASPECT_DEPTH_BIT; } - noteHtileWrites(*surface, target); + surface->pendingClear |= htileCleared; surface->ApplyFastClear(); if (!created || inherits) surface->written = ++depthWrites; surface->NoteMemory(); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DepthFastClear.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DepthFastClear.cpp index 7d119f12f3..181a10cebc 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DepthFastClear.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DepthFastClear.cpp @@ -17,7 +17,7 @@ using DepthFastClearHarness::Surface; using DepthFastClearHarness::Width; alignas(4096) std::array Depth{}; -alignas(256) std::array Htile{}; +alignas(256) std::array Htile{}; alignas(16) std::array FillValue{}; constexpr std::array MemoryValueFillKernel{0xd7460000u, 0x04010c08u, 0xf4201a82u, 0xfa000000u, 0xbf8cc07fu, 0x7e02026au, 0xe0102000u, 0x80000100u, 0xbf810000u}; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DepthFastClearSubmit.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DepthFastClearSubmit.cpp index 0ecf904715..3b9caf1112 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DepthFastClearSubmit.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DepthFastClearSubmit.cpp @@ -162,6 +162,21 @@ void DispatchBufferPatternFillClearsDepth(AgcDriver::VulkanDevice& device, const Require(Draw(device, shaders, surface, {.depthWrite = true}) == Covered, "a compute fill with the buffer pattern 0 over the HTILE did not clear the depth to DB_DEPTH_CLEAR (1.0)"); } +void DrawAfterFastClearExpandsHtile(AgcDriver::VulkanDevice& device, const Shaders& shaders) { + for (const bool stencil : {false, true}) { + auto& htile = NewBlock(); + const Surface surface{NewBlock().Address(), stencil ? NewBlock().Address() : 0, htile.Address(), stencil, stencil ? VK_FORMAT_D32_SFLOAT_S8_UINT : VK_FORMAT_D32_SFLOAT}; + Require(Draw(device, shaders, surface, {.depthWrite = true}) == Covered, "a new depth surface was not cleared to DB_DEPTH_CLEAR (1.0)"); + DmaFill(htile, HtileFillBytes, stencil ? 0xf0u : 0u); + Require(Draw(device, shaders, surface, {.depthWrite = true}) == Covered, "a fast clear through the HTILE did not clear the depth to DB_DEPTH_CLEAR (1.0)"); + std::vector words(HtileFillBytes / 4u); + AgcDriver::GuestMemory::Read(htile.Address(), std::as_writable_bytes(std::span(words)), 4); + const auto expanded = stencil ? 0xfffff3ffu : 0xfffc000fu; + for (const auto word : words) Require(word == expanded, stencil ? "a draw after a depth and stencil fast clear left HTILE words other than expanded (0xfffff3ff)" : "a draw after a depth fast clear left HTILE words other than expanded (0xfffc000f): a kernel decoding the depth through ZMask 0 reads the clear value, not the drawn depth"); + Require(Draw(device, shaders, surface, {}) == 0u, "the depth drawn after the fast clear was cleared again"); + } +} + void DepthOnlyFillKeepsStencil(AgcDriver::VulkanDevice& device, const Shaders& shaders) { const auto& htile = NewBlock(); const Surface surface{NewBlock().Address(), NewBlock().Address(), htile.Address(), true, VK_FORMAT_D32_SFLOAT_S8_UINT}; @@ -185,6 +200,7 @@ int main() { const auto shaders = Compile(*device); DmaFillClearsDepth(*device, shaders); DepthOnlyFillKeepsStencil(*device, shaders); + DrawAfterFastClearExpandsHtile(*device, shaders); const bool computeRan = StoresFills(*device, NewBlock()); if (computeRan) { DispatchFillClearsDepth(*device, shaders); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DepthHtileWrite.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DepthHtileWrite.cpp index 5acbc519b1..d0d71cf49e 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DepthHtileWrite.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DepthHtileWrite.cpp @@ -13,16 +13,17 @@ using DepthFastClearHarness::Covered; using DepthFastClearHarness::Draw; using DepthFastClearHarness::DrawOptions; using DepthFastClearHarness::Height; +using DepthFastClearHarness::HtileBytes; using DepthFastClearHarness::Require; using DepthFastClearHarness::Surface; using DepthFastClearHarness::Width; -constexpr std::size_t Tiles = (Width / 8u) * (Height / 8u); +constexpr std::size_t Tiles = HtileBytes / 4u; alignas(4096) std::array Depth{}; alignas(4096) std::array StencilDepth{}; alignas(4096) std::array Stencil{}; -alignas(256) std::array Htile{}; +alignas(256) std::array Htile{}; void Fill(std::uint32_t word, std::size_t count = Tiles) { std::fill_n(Htile.begin(), count, word); diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index cc717bf764..bb4a248259 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -94,7 +94,7 @@ - [Descriptor pool chain](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - each pool holds two storage buffer descriptors more than a set can take, as a workaround: RADV (Mesa 26.0 and later) lets a `FREE_DESCRIPTOR_SET` pool place a set up to 32 bytes past the pool's buffer ([`util_vma_heap_init`](https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/src/amd/vulkan/radv_descriptor_pool.c) is given `bo_size + RADV_POOL_HEAP_OFFSET` as a size), and with a page-multiple pool size those bytes are an unmapped page - [CMASK fast clears](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver) - each fast-cleared CMASK tile (code 0) is stored as `CB_COLOR_CLEAR_WORD` into its 8x8 texels at the first draw into the target or at its fast-clear eliminate or DCC decompress pass, not as the color block writes the tile, and the CMASK is then stored as expanded (0xF): a texture or copy read of the target after that draw and before the title's eliminate sees the clear value in tiles no draw wrote, where the console keeps their previous bytes. Tile addresses follow the pipe-aligned SW_64KB_Z_X CMASK equation for 16 pipes; other CMASK swizzle modes, a cleared tile of a DCC target and a fast clear of a mipmapped (base mip 0), 3D or array color target throw. A tile with a code other than 0 and 0xF keeps its texels like an expanded tile: PPSA18391 (engine: Clickteam Fusion) fast clears a target whose CMASK it never initialized, so some of its nibbles hold leftover values such as 1 and 10; what the console does with such codes on a single-sample target is unverified on hardware. Single-sample views with `viewMip > 0` skip CMASK fast-clear decoding since CMASK metadata only covers base mip 0 on AMD hardware (Mesa `si_state.c` similarly clears `FAST_CLEAR` for level > 0). - [HTILE depth fast clears](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver) - HTILE is not modeled. Only RADV's `clear_htile_mask` compute kernel (17 GFX10 words, 64x1x1 groups) is recognized, and only when it leaves ZMASK at 0 (`user[4]` and `user[5]` low nibbles both 0); the dispatch still runs, and the host depth image is cleared to `DB_DEPTH_CLEAR` the next time the surface with that `DB_HTILE_DATA_BASE` address is bound. The address match is exact, so a clear of a different HTILE range does not hit. HTILE initialization, the stencil-only update, and fast clears made by the SDK's own paths, CP DMA fills or other kernels are not covered and leave the depth image as it was. The kernel words are the ones the PR author saw RADV submit; they are not compared with Mesa's source. A second kernel, the 9-word dword fill PPSA01802 (Kena: Bridge of Spirits) clears its depth HTILE with (64x1x1 groups, one `buffer_store_format_x` per thread through the stride-4 V# in `user[0..3]`, the value read by `s_buffer_load_dword` through the V# in `user[4..7]`), is taken as a fill of that range when the store format is 32_UINT, 32_SINT or 32_FLOAT and the value V# covers the dword (NUM_RECORDS times its stride, or NUM_RECORDS bytes when the stride is 0) ([`MatchMemoryValueFill`](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp), then the whole-HTILE and ZMask rules of `NoteDepthMetadataFill`). The value is read from guest memory when the dispatch is decoded, so a value that earlier GPU work in the same submission has not written back yet is not seen. When the [fill HLE](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferFill.cpp) takes the dispatch instead (an output descriptor of one 32-bit value per element, a value descriptor that covers the loaded dword, a 16-byte aligned range), the value is read again when the fill is recorded, after the labels queued before it, and the HTILE fill is noted with that value. -- [HTILE written by other kernels](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver) - when a depth target is bound and its HTILE was written since it was last read, the words are read: when every one marks the depth (ZMask 0) or the stencil (SMem 0) cleared, the surface takes that fast clear and the words are stored back as expanded (depth only: ZMask 15 over the full Z range; with stencil: ZMask 15, Z range and SMem, SR1 and SR0 3), so only the title's next clear clears it again. PPSA29141 clears its depth every frame with a compute kernel the fill hooks do not recognize (`InvokeHTileClearComputeShader` in its `ClearMRT` pass); without this its depth kept every earlier frame and moving objects left black trails. The word layouts are Mesa radv's for GFX10. Metadata with some tiles cleared is not modelled, and the compression state draws leave in HTILE is not tracked, so a title reading its HTILE after a draw sees the stored expanded words +- [HTILE written by other kernels](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver) - when a depth target is bound and its HTILE was written since it was last read, the words over its whole pipe-aligned metadata (`HtileMetadataBytes`, the extent `HtileFillCovers` takes) are read, outside the surfaces mutex since the read waits for recorded work: when every one marks the depth (ZMask 0) or the stencil (SMem 0) cleared, the surface takes that fast clear and the words are stored back as expanded (depth only: ZMask 15 over the full Z range; with stencil: ZMask 15, Z range and SMem, SR1 and SR0 3), so only the title's next clear clears it again. PPSA29141 clears its depth every frame with a compute kernel the fill hooks do not recognize (`InvokeHTileClearComputeShader` in its `ClearMRT` pass); without this its depth kept every earlier frame and moving objects left black trails. PPSA25344 (Unreal Engine 5) reads its HTILE after the depth prepass in its Nanite depth export, which takes a tile with ZMask 0 as holding the clear value; without the expanded words the export put Nanite depth over the prepass and the Nanite material pass painted over the meshes in front. The word layouts are Mesa radv's for GFX10. Metadata with some tiles cleared is not modelled, and the compression state draws leave in HTILE is not tracked, so a title reading its HTILE after a draw sees the stored expanded words - [Host imports](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver) on Linux skip ranges that are not mapped read-write, which are copied instead. The NVIDIA driver (595.91.07) refuses `VK_EXT_external_memory_host` imports of read-only or executable mappings (and shared mappings of regular files) with `VK_ERROR_OUT_OF_DEVICE_MEMORY`, after which every `vkAllocateMemory` on the device fails the same way. Read-only ranges are never imported, even on drivers that accept them. Shared mappings of regular files are not checked for. On Windows, an alias of adjacent registered ranges is refused if any page is not a shared view; a range made entirely of shared views is aliased run by run, including across sections or nonconsecutive section offsets. Adjacent registered ranges are joined into one import where they fill a whole 8 MiB aligned window; ranges of a partly filled window, and ranges larger than the window, are imported one at a time. A buffer bound across adjacent ranges that are each larger than the window, or bound exactly over adjacent ranges of any size (from the first one's start to the last one's end), joins them into one import when it is bound, and their own imports are retired; a buffer that crosses any other two imports is copied. A heap grown in pieces re-imports its joined run only when a buffer is bound exactly over all of its pieces, or when a sparse descriptor heap over it is bound: a heap a shader binds whole while only the pieces from its base are mapped is bound in place from the import of that run, its views ending at the import's end as the copy's do, once the previous bind of that address saw the same last mapped byte (a heap that grows between every two of its binds stays copied; one bound more often than it grows is joined again after each growth) and at most 64 times per joined run base; past that, with a message, it is not joined again: it is bound in place while an existing import covers its run and copied otherwise, until no registered range starts at that base or the device changes. Ranges larger than the window are taken to be registered whole and never grown in pieces: a title that grows a heap in pieces larger than 8 MiB re-imports the joined run each time a bound buffer crosses into a new piece. The 8 MiB window is a choice: unbounded runs were re-imported whole on every growth of a heap mapped in small pieces, and the NVIDIA driver (610.57.04, 615.78.08) then refused imports or lost the device. Not measured on RADV. - [Cube texture descriptors](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp) with equal `BASE_ARRAY` and `DEPTH` expose six faces starting at `BASE_ARRAY`. Measured with `image_load`, `image_sample` and `image_get_resinfo` on RX 6600 (gfx1032), using 64x64 R32_UINT faces and base slices 0, 5 and 6. Other ranges containing fewer than six faces or incomplete cube arrays still throw: hardware load bounds and filtered-sample addressing differ. Out-of-range cube-array sampling remains unimplemented. Samplers with `DISABLE_CUBE_WRAP` are created with `VK_SAMPLER_CREATE_NON_SEAMLESS_CUBE_MAP_BIT_EXT`, the flag RADV maps to that bit (Mesa `src/amd/vulkan/radv_sampler.c`, `src/amd/common/ac_descriptors.c`); without `VK_EXT_non_seamless_cube_map` a cube texture sampled through such a sampler throws. PS5 equivalence has not been measured on console hardware. - [Array layer views](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp) (libSceAgcDriver) - `ArrayLayersServeView` lets a 2D texture at a 2D array image's base address, or a 2D array texture over its first layers, view that image (`Texture::CanCopyFrom`). Mesa-built homebrew (ps5-opengl) writes the view's last slice plus one to `CB_COLOR_ATTRIB3.MIP0_DEPTH`, so a slice-0 render target is a two-layer array image, and the title samples it as a plain 2D texture (SuperTuxKart PPSA69421 drew a black 3D scene and minimap without this). The per-layer layout of a mipmapped array is assumed equal to a standalone surface's; it is not measured on the console. A 2D texture at a later slice's address still gets an image of its own. From cc022df3485a459466830b043b6064fc606a9158 Mon Sep 17 00:00:00 2001 From: AlxcBTW <187763517+AlxcBTW@users.noreply.github.com> Date: Tue, 13 Oct 2026 20:03:19 +0200 Subject: [PATCH 0270/1000] fix(agc): take a slice of a depth array from its own HTILE only when its words are uniform A fill or clear the driver recognizes over an HTILE drops the range's last-read generation, so the next bind reads the words and stores them back even when the write tracker did not see the fill's stores. A depth view of slice 1 or later reads the HTILE at its own address (DB_HTILE_DATA_BASE + slice * HtileMetadataBytes); the HTILE word layout is only modelled for slice 0, so such a slice takes the clear and the store-back only when all its words are equal and keeps its fill-time fast clears otherwise. DepthTarget::slice carries DB_DEPTH_VIEW SLICE_START. agc_driver_depth_fast_clear_submit: an unrecognized kernel clears the HTILE on two frames in a row and both clears are taken; slice 1 with words that are not all equal is refused and stored back untouched; slice 1 with equal words takes the clear. agc_driver_graphics checks the decoded slice. --- .../Graphics/src/DepthSurface.cpp | 11 +++- .../prx/libSceAgcDriver/tests/Graphics.cpp | 5 ++ .../tests/execution/DepthFastClearHarness.hpp | 2 + .../tests/execution/DepthFastClearSubmit.cpp | 54 +++++++++++++++++++ docs/dev/debt/libSceAgcDriver.md | 2 +- 5 files changed, 72 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp index 08ca7c5f7b..f986cf60f3 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp @@ -905,6 +905,12 @@ std::map& htileReads() { return *reads; } +void forgetHtileReads(std::uint64_t address, std::size_t bytes) { + std::lock_guard lock(htileReadsMutex()); + auto& reads = htileReads(); + reads.erase(reads.lower_bound(address), reads.lower_bound(address + bytes)); +} + VkImageAspectFlags storeBackClearedHtile(const Context& context, const DepthTarget& target) { if (target.htileAddress == 0 || target.samples != 1u) return 0; const auto bytes = static_cast(HtileMetadataBytes(target.extent)); @@ -918,7 +924,8 @@ VkImageAspectFlags storeBackClearedHtile(const Context& context, const DepthTarg } std::vector words(bytes / 4u); GuestMemory::Read(target.htileAddress, std::as_writable_bytes(std::span(words)), 4); - VkImageAspectFlags cleared = VK_IMAGE_ASPECT_DEPTH_BIT | (target.htileStencil ? VK_IMAGE_ASPECT_STENCIL_BIT : 0u); + const bool uniform = std::all_of(words.begin(), words.end(), [&](std::uint32_t word) { return word == words.front(); }); + VkImageAspectFlags cleared = target.firstSlice == 0 || uniform ? VK_IMAGE_ASPECT_DEPTH_BIT | (target.htileStencil ? VK_IMAGE_ASPECT_STENCIL_BIT : 0u) : 0u; for (const auto word : words) cleared &= HtileFillClears(word, target.htileStencil); auto generation = collected; if (cleared != 0) { @@ -1129,6 +1136,7 @@ void NoteMemoryValueFill(const MemoryValueFill& fill) { void NoteHtileDepthClear(std::uint64_t htileAddress, std::size_t bytes) { std::lock_guard lock(surfacesMutex()); + forgetHtileReads(htileAddress, bytes); bool existing = false; for (const auto& surface : surfaces()) { if (!surface->active->load(std::memory_order_acquire)) continue; @@ -1264,6 +1272,7 @@ bool HtileFillCovers(std::uint64_t htile, VkExtent2D extent, std::uint64_t addre void NoteDepthMetadataFill(std::uint64_t address, std::size_t bytes, std::uint32_t pattern) { std::lock_guard lock(surfacesMutex()); + forgetHtileReads(address, bytes); for (const auto& surface : surfaces()) { if (!surface->active->load(std::memory_order_acquire)) continue; const auto& target = surface->target; diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index af140b274b..86533c050c 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -1331,6 +1331,11 @@ void DisabledColorTests() { queue.context[0x00b] = std::bit_cast(1.0f); const auto depthOnly = AgcDriver::Graphics::DecodeState(queue); Require(!depthOnly.hasColorTarget && depthOnly.depth && depthOnly.depthTest && depthOnly.depthWrite && depthOnly.renderExtent.height == 64, "COLOR_INVALID discarded a depth-only draw"); + Require(depthOnly.depth->firstSlice == 0, "a depth view of slice 0 decoded another slice"); + queue.context[0x002] = 3; + const auto thirdSlice = AgcDriver::Graphics::DecodeState(queue); + Require(thirdSlice.depth && thirdSlice.depth->firstSlice == 3 && thirdSlice.depth->address == depthOnly.depth->address + 3u * AgcDriver::Graphics::DepthSliceBytes(depthOnly.depth->extent, 4u), "a depth view of slice 3 lost its slice or its Z offset"); + queue.context[0x002] = 0; for (const auto colorControl : {0x0u, 0xcc0000u}) { queue = makeState(); queue.context[0x202] = colorControl; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DepthFastClearHarness.hpp b/core/libs/prx/libSceAgcDriver/tests/execution/DepthFastClearHarness.hpp index 1e4e5bcb3d..3e21bfbceb 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DepthFastClearHarness.hpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DepthFastClearHarness.hpp @@ -39,6 +39,7 @@ struct Surface { std::uint64_t htile; bool htileStencil; VkFormat format; + std::uint32_t slice = 0; }; struct DrawOptions { @@ -116,6 +117,7 @@ inline std::uint32_t Draw(AgcDriver::VulkanDevice& device, const Shaders& compil state.colors = {state.color}; state.hasColorTarget = true; state.depth = AgcDriver::Graphics::DepthTarget{surface.depth, surface.stencil, {Width, Height}, surface.format, 1.0f, 0, surface.htile, surface.htileStencil}; + state.depth->firstSlice = surface.slice; state.depthTest = options.depthTest; state.depthWrite = options.depthWrite; state.depthCompare = options.depthCompare; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DepthFastClearSubmit.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DepthFastClearSubmit.cpp index 3b9caf1112..e685156997 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DepthFastClearSubmit.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DepthFastClearSubmit.cpp @@ -11,6 +11,7 @@ #endif #include #endif +#include #include #include #include @@ -20,6 +21,7 @@ #include #include #include +#include #include namespace { @@ -113,6 +115,8 @@ void DispatchFill(const GuestBlock& code, const GuestBlock& htile, std::uint32_t }); } +constexpr std::array UnknownFillKernel{0xbf800000u, 0xd7460004u, 0x04010c08u, 0x7e000204u, 0x7e020205u, 0x7e040206u, 0x7e060207u, 0xe01c2000u, 0x80000004u, 0xbf810000u}; + bool StoresFills(AgcDriver::VulkanDevice& device, const GuestBlock& probe) { std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); std::array pattern{}; @@ -177,6 +181,53 @@ void DrawAfterFastClearExpandsHtile(AgcDriver::VulkanDevice& device, const Shade } } +void UnknownKernelClearsTwice(AgcDriver::VulkanDevice& device, const Shaders& shaders) { + const auto& htile = NewBlock(); + auto& code = NewBlock(); + std::memcpy(code.Data(), UnknownFillKernel.data(), sizeof(UnknownFillKernel)); + const Surface surface{NewBlock().Address(), 0, htile.Address(), false, VK_FORMAT_D32_SFLOAT}; + Require(Draw(device, shaders, surface, {.depthWrite = true}) == Covered, "a new depth surface was not cleared to DB_DEPTH_CLEAR (1.0)"); + for (const char* frame : {"first", "second"}) { + DispatchFill(code, htile, HtileFillBytes); + Require(Draw(device, shaders, surface, {.depthWrite = true}) == Covered, std::string("the ") + frame + " clear an unrecognized kernel wrote into the HTILE was not taken"); + Require(Draw(device, shaders, surface, {}) == 0u, std::string("the depth written after the ") + frame + " kernel clear was lost"); + } +} + +std::uint32_t* WordsAt(GuestBlock& block, std::size_t offset) { return reinterpret_cast(block.Data() + offset); } + +bool AllWords(const std::uint32_t* words, std::uint32_t value) { return std::all_of(words, words + HtileFillBytes / 4u, [&](std::uint32_t word) { return word == value; }); } + +void NonUniformSliceIsRefused(AgcDriver::VulkanDevice& device, const Shaders& shaders) { + auto& htile = NewBlock(); + const Surface first{NewBlock().Address(), 0, htile.Address(), false, VK_FORMAT_D32_SFLOAT, 0}; + const Surface second{NewBlock().Address(), 0, htile.Address() + HtileFillBytes, false, VK_FORMAT_D32_SFLOAT, 1}; + Require(Draw(device, shaders, first, {.depthWrite = true}) == Covered, "slice 0 was not cleared to DB_DEPTH_CLEAR (1.0)"); + Require(Draw(device, shaders, second, {.depthWrite = true}) == Covered, "slice 1 was not cleared to DB_DEPTH_CLEAR (1.0)"); + auto* sliceOne = WordsAt(htile, HtileFillBytes); + std::fill(sliceOne, sliceOne + HtileFillBytes / 4u, 0u); + sliceOne[0] = 0xffffffffu; + AgcDriver::GuestMemory::MarkWritten(htile.Address() + HtileFillBytes, HtileFillBytes); + Require(Draw(device, shaders, second, {}) == 0u, "slice 1 took a clear from HTILE words that are not all equal"); + Require(sliceOne[0] == 0xffffffffu && std::all_of(sliceOne + 1, sliceOne + HtileFillBytes / 4u, [](std::uint32_t word) { return word == 0u; }), "a bind of slice 1 stored back HTILE words that are not all equal"); + Require(AllWords(WordsAt(htile, 0), 0xfffc000fu), "a bind of slice 1 changed slice 0's HTILE"); +} + +void UniformSliceDecodes(AgcDriver::VulkanDevice& device, const Shaders& shaders) { + auto& htile = NewBlock(); + const Surface first{NewBlock().Address(), 0, htile.Address(), false, VK_FORMAT_D32_SFLOAT, 0}; + const Surface second{NewBlock().Address(), 0, htile.Address() + HtileFillBytes, false, VK_FORMAT_D32_SFLOAT, 1}; + Require(Draw(device, shaders, first, {.depthWrite = true}) == Covered, "slice 0 was not cleared to DB_DEPTH_CLEAR (1.0)"); + Require(Draw(device, shaders, second, {.depthWrite = true}) == Covered, "slice 1 was not cleared to DB_DEPTH_CLEAR (1.0)"); + Require(Draw(device, shaders, second, {}) == 0u, "the depth drawn into slice 1 was lost"); + auto* sliceOne = WordsAt(htile, HtileFillBytes); + std::fill(sliceOne, sliceOne + HtileFillBytes / 4u, 0u); + AgcDriver::GuestMemory::MarkWritten(htile.Address() + HtileFillBytes, HtileFillBytes); + Require(Draw(device, shaders, second, {.depthWrite = true}) == Covered, "slice 1 did not take the clear from HTILE words that are all equal"); + Require(AllWords(sliceOne, 0xfffc000fu), "slice 1's bind did not store its cleared HTILE back as expanded"); + Require(Draw(device, shaders, second, {}) == 0u, "the depth drawn into slice 1 after the clear was lost"); +} + void DepthOnlyFillKeepsStencil(AgcDriver::VulkanDevice& device, const Shaders& shaders) { const auto& htile = NewBlock(); const Surface surface{NewBlock().Address(), NewBlock().Address(), htile.Address(), true, VK_FORMAT_D32_SFLOAT_S8_UINT}; @@ -202,9 +253,12 @@ int main() { DepthOnlyFillKeepsStencil(*device, shaders); DrawAfterFastClearExpandsHtile(*device, shaders); const bool computeRan = StoresFills(*device, NewBlock()); + NonUniformSliceIsRefused(*device, shaders); + UniformSliceDecodes(*device, shaders); if (computeRan) { DispatchFillClearsDepth(*device, shaders); DispatchBufferPatternFillClearsDepth(*device, shaders); + UnknownKernelClearsTwice(*device, shaders); } else std::printf("compute fill not run: the device does not store fills into imported guest memory\n"); LibcRunShutdown_nid_postfix(); diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index bb4a248259..dd3f294fa3 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -94,7 +94,7 @@ - [Descriptor pool chain](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - each pool holds two storage buffer descriptors more than a set can take, as a workaround: RADV (Mesa 26.0 and later) lets a `FREE_DESCRIPTOR_SET` pool place a set up to 32 bytes past the pool's buffer ([`util_vma_heap_init`](https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/src/amd/vulkan/radv_descriptor_pool.c) is given `bo_size + RADV_POOL_HEAP_OFFSET` as a size), and with a page-multiple pool size those bytes are an unmapped page - [CMASK fast clears](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver) - each fast-cleared CMASK tile (code 0) is stored as `CB_COLOR_CLEAR_WORD` into its 8x8 texels at the first draw into the target or at its fast-clear eliminate or DCC decompress pass, not as the color block writes the tile, and the CMASK is then stored as expanded (0xF): a texture or copy read of the target after that draw and before the title's eliminate sees the clear value in tiles no draw wrote, where the console keeps their previous bytes. Tile addresses follow the pipe-aligned SW_64KB_Z_X CMASK equation for 16 pipes; other CMASK swizzle modes, a cleared tile of a DCC target and a fast clear of a mipmapped (base mip 0), 3D or array color target throw. A tile with a code other than 0 and 0xF keeps its texels like an expanded tile: PPSA18391 (engine: Clickteam Fusion) fast clears a target whose CMASK it never initialized, so some of its nibbles hold leftover values such as 1 and 10; what the console does with such codes on a single-sample target is unverified on hardware. Single-sample views with `viewMip > 0` skip CMASK fast-clear decoding since CMASK metadata only covers base mip 0 on AMD hardware (Mesa `si_state.c` similarly clears `FAST_CLEAR` for level > 0). - [HTILE depth fast clears](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver) - HTILE is not modeled. Only RADV's `clear_htile_mask` compute kernel (17 GFX10 words, 64x1x1 groups) is recognized, and only when it leaves ZMASK at 0 (`user[4]` and `user[5]` low nibbles both 0); the dispatch still runs, and the host depth image is cleared to `DB_DEPTH_CLEAR` the next time the surface with that `DB_HTILE_DATA_BASE` address is bound. The address match is exact, so a clear of a different HTILE range does not hit. HTILE initialization, the stencil-only update, and fast clears made by the SDK's own paths, CP DMA fills or other kernels are not covered and leave the depth image as it was. The kernel words are the ones the PR author saw RADV submit; they are not compared with Mesa's source. A second kernel, the 9-word dword fill PPSA01802 (Kena: Bridge of Spirits) clears its depth HTILE with (64x1x1 groups, one `buffer_store_format_x` per thread through the stride-4 V# in `user[0..3]`, the value read by `s_buffer_load_dword` through the V# in `user[4..7]`), is taken as a fill of that range when the store format is 32_UINT, 32_SINT or 32_FLOAT and the value V# covers the dword (NUM_RECORDS times its stride, or NUM_RECORDS bytes when the stride is 0) ([`MatchMemoryValueFill`](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp), then the whole-HTILE and ZMask rules of `NoteDepthMetadataFill`). The value is read from guest memory when the dispatch is decoded, so a value that earlier GPU work in the same submission has not written back yet is not seen. When the [fill HLE](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferFill.cpp) takes the dispatch instead (an output descriptor of one 32-bit value per element, a value descriptor that covers the loaded dword, a 16-byte aligned range), the value is read again when the fill is recorded, after the labels queued before it, and the HTILE fill is noted with that value. -- [HTILE written by other kernels](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver) - when a depth target is bound and its HTILE was written since it was last read, the words over its whole pipe-aligned metadata (`HtileMetadataBytes`, the extent `HtileFillCovers` takes) are read, outside the surfaces mutex since the read waits for recorded work: when every one marks the depth (ZMask 0) or the stencil (SMem 0) cleared, the surface takes that fast clear and the words are stored back as expanded (depth only: ZMask 15 over the full Z range; with stencil: ZMask 15, Z range and SMem, SR1 and SR0 3), so only the title's next clear clears it again. PPSA29141 clears its depth every frame with a compute kernel the fill hooks do not recognize (`InvokeHTileClearComputeShader` in its `ClearMRT` pass); without this its depth kept every earlier frame and moving objects left black trails. PPSA25344 (Unreal Engine 5) reads its HTILE after the depth prepass in its Nanite depth export, which takes a tile with ZMask 0 as holding the clear value; without the expanded words the export put Nanite depth over the prepass and the Nanite material pass painted over the meshes in front. The word layouts are Mesa radv's for GFX10. Metadata with some tiles cleared is not modelled, and the compression state draws leave in HTILE is not tracked, so a title reading its HTILE after a draw sees the stored expanded words +- [HTILE written by other kernels](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver) - when a depth target is bound and its HTILE was written since it was last read, the words over its whole pipe-aligned metadata (`HtileMetadataBytes`, the extent `HtileFillCovers` takes) are read, outside the surfaces mutex since the read waits for recorded work: when every one marks the depth (ZMask 0) or the stencil (SMem 0) cleared, the surface takes that fast clear and the words are stored back as expanded (depth only: ZMask 15 over the full Z range; with stencil: ZMask 15, Z range and SMem, SR1 and SR0 3), so only the title's next clear clears it again. PPSA29141 clears its depth every frame with a compute kernel the fill hooks do not recognize (`InvokeHTileClearComputeShader` in its `ClearMRT` pass); without this its depth kept every earlier frame and moving objects left black trails. PPSA25344 (Unreal Engine 5) reads its HTILE after the depth prepass in its Nanite depth export, which takes a tile with ZMask 0 as holding the clear value; without the expanded words the export put Nanite depth over the prepass and the Nanite material pass painted over the meshes in front. The word layouts are Mesa radv's for GFX10. Metadata with some tiles cleared is not modelled, and the compression state draws leave in HTILE is not tracked, so a title reading its HTILE after a draw sees the stored expanded words. A recognized HTILE fill or clear makes the next bind read the words again. A slice other than 0 of a depth array is read at its own HTILE (`DB_HTILE_DATA_BASE` plus the slice times `HtileMetadataBytes`) but the HTILE word layout of slices 1 and up is not modelled (PPSA05684's tile masks need X8 and Y8 to take Z0 and Z1 as well, slices 4 and up unmeasured), so such a slice takes the clear only when all its words are equal and otherwise keeps the fill-time fast clears. A depth-only HTILE whose tiles all have ZMask 0 but different Min/Max Z (per-tile clear values, PPSA05684's cascades) is taken as a whole-surface clear to `DB_DEPTH_CLEAR`. - [Host imports](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver) on Linux skip ranges that are not mapped read-write, which are copied instead. The NVIDIA driver (595.91.07) refuses `VK_EXT_external_memory_host` imports of read-only or executable mappings (and shared mappings of regular files) with `VK_ERROR_OUT_OF_DEVICE_MEMORY`, after which every `vkAllocateMemory` on the device fails the same way. Read-only ranges are never imported, even on drivers that accept them. Shared mappings of regular files are not checked for. On Windows, an alias of adjacent registered ranges is refused if any page is not a shared view; a range made entirely of shared views is aliased run by run, including across sections or nonconsecutive section offsets. Adjacent registered ranges are joined into one import where they fill a whole 8 MiB aligned window; ranges of a partly filled window, and ranges larger than the window, are imported one at a time. A buffer bound across adjacent ranges that are each larger than the window, or bound exactly over adjacent ranges of any size (from the first one's start to the last one's end), joins them into one import when it is bound, and their own imports are retired; a buffer that crosses any other two imports is copied. A heap grown in pieces re-imports its joined run only when a buffer is bound exactly over all of its pieces, or when a sparse descriptor heap over it is bound: a heap a shader binds whole while only the pieces from its base are mapped is bound in place from the import of that run, its views ending at the import's end as the copy's do, once the previous bind of that address saw the same last mapped byte (a heap that grows between every two of its binds stays copied; one bound more often than it grows is joined again after each growth) and at most 64 times per joined run base; past that, with a message, it is not joined again: it is bound in place while an existing import covers its run and copied otherwise, until no registered range starts at that base or the device changes. Ranges larger than the window are taken to be registered whole and never grown in pieces: a title that grows a heap in pieces larger than 8 MiB re-imports the joined run each time a bound buffer crosses into a new piece. The 8 MiB window is a choice: unbounded runs were re-imported whole on every growth of a heap mapped in small pieces, and the NVIDIA driver (610.57.04, 615.78.08) then refused imports or lost the device. Not measured on RADV. - [Cube texture descriptors](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp) with equal `BASE_ARRAY` and `DEPTH` expose six faces starting at `BASE_ARRAY`. Measured with `image_load`, `image_sample` and `image_get_resinfo` on RX 6600 (gfx1032), using 64x64 R32_UINT faces and base slices 0, 5 and 6. Other ranges containing fewer than six faces or incomplete cube arrays still throw: hardware load bounds and filtered-sample addressing differ. Out-of-range cube-array sampling remains unimplemented. Samplers with `DISABLE_CUBE_WRAP` are created with `VK_SAMPLER_CREATE_NON_SEAMLESS_CUBE_MAP_BIT_EXT`, the flag RADV maps to that bit (Mesa `src/amd/vulkan/radv_sampler.c`, `src/amd/common/ac_descriptors.c`); without `VK_EXT_non_seamless_cube_map` a cube texture sampled through such a sampler throws. PS5 equivalence has not been measured on console hardware. - [Array layer views](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp) (libSceAgcDriver) - `ArrayLayersServeView` lets a 2D texture at a 2D array image's base address, or a 2D array texture over its first layers, view that image (`Texture::CanCopyFrom`). Mesa-built homebrew (ps5-opengl) writes the view's last slice plus one to `CB_COLOR_ATTRIB3.MIP0_DEPTH`, so a slice-0 render target is a two-layer array image, and the title samples it as a plain 2D texture (SuperTuxKart PPSA69421 drew a black 3D scene and minimap without this). The per-layer layout of a mipmapped array is assumed equal to a standalone surface's; it is not measured on the console. A 2D texture at a later slice's address still gets an image of its own. From 0a7193b5cdb77dbbec8c877cae43db87742226b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Thu, 15 Oct 2026 13:53:00 +0200 Subject: [PATCH 0271/1000] fix(libkernel): sigsuspend swaps and restores all four mask words --- core/libs/prx/libkernel/System/src/Signals.cpp | 1 + core/libs/tests/GuestSignals.cpp | 17 +++++++++++++++++ docs/dev/debt/libkernel.md | 2 +- 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libkernel/System/src/Signals.cpp b/core/libs/prx/libkernel/System/src/Signals.cpp index 2375d9a002..23f4e7f977 100644 --- a/core/libs/prx/libkernel/System/src/Signals.cpp +++ b/core/libs/prx/libkernel/System/src/Signals.cpp @@ -609,6 +609,7 @@ int APS5_VABI sigsuspend_nid_postfix(const GuestSignalSet* set) { { std::lock_guard lock(registration); previous.bits[0] = blockedMask.exchange(set->bits[0]); + for (int word = 1; word < 4; ++word) previous.bits[word] = blockedMaskExtra[word - 1].exchange(set->bits[word]); } WaitForDeliverableSignal(); _sigprocmask_nid_postfix(GuestSigsetmask, &previous, nullptr); diff --git a/core/libs/tests/GuestSignals.cpp b/core/libs/tests/GuestSignals.cpp index ca36fb074b..016c4c5a0f 100644 --- a/core/libs/tests/GuestSignals.cpp +++ b/core/libs/tests/GuestSignals.cpp @@ -198,6 +198,23 @@ int main() { Require(received == 30); Require(sigprocmask_nid_postfix(1, nullptr, &previous) == 0 && previous.bits[0] == user.bits[0]); Require(sigprocmask_nid_postfix(2, &user, nullptr) == 0); + const GuestSignalSet highBlock{{1u << 29, 1u << 7, 0, 0}}; + const GuestSignalSet suspendMask{{0, 0, 1u << 8, 0}}; + Require(sigprocmask_nid_postfix(3, &highBlock, nullptr) == 0); + Require(signal_nid_postfix(73, Callback) != invalid); + received = 0; + SuspendWait highWait{&suspendMask, 0, 0}; + void* highSuspender = nullptr; + Require(scePthreadCreate(&highSuspender, nullptr, SuspendEntry, &highWait, nullptr) == 0); + SuspendSender highSender{highSuspender, -1}; + void* highSenderThread = nullptr; + Require(scePthreadCreate(&highSenderThread, nullptr, SuspendSend, &highSender, nullptr) == 0); + Require(scePthreadJoin(highSenderThread, nullptr) == 0 && highSender.result == 0); + Require(scePthreadJoin(highSuspender, nullptr) == 0); + Require(highWait.result == -1 && highWait.error == 4 && received == 30); + Require(sigprocmask_nid_postfix(1, nullptr, &previous) == 0 && std::memcmp(&previous, &highBlock, sizeof(highBlock)) == 0); + Require(sigprocmask_nid_postfix(3, &empty, nullptr) == 0); + Require(signal_nid_postfix(73, nullptr) == Callback); signal_nid_postfix(30, nullptr); Require(GuestAlarm_nid_no_patch(0) == 0); diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index f16a03c5ab..bae12b09e4 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -174,7 +174,7 @@ - [mincore](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) (libkernel) follows FreeBSD 11's `sys_mincore` (`sys/vm/vm_mmap.c`), not measured on the console: the address is rounded down to the page, length 0 returns 0, a null vector over a mapped range returns `EFAULT`, an overflowing range or any unmapped page in the range (as reported by `sceKernelVirtualQuery`) returns `ENOMEM`. For valid ranges, a page reports `MINCORE_INCORE` (0x1) when `sceKernelVirtualQuery` reports it committed and 0 when it is only reserved; actual residency, referenced and modified bits are not modelled. Length 0 returns 0 when the start page is mapped and `ENOMEM` otherwise, and the range and mapping checks come before the null vector `EFAULT`, as in `kern_mincore` Java 25 LAN / DNS (PPSA99869) imports it - [mlockall](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) (libkernel) returns `EINVAL` for zero or unknown flags. Valid `MCL_CURRENT` throws because it would lock host stacks, heaps, modules and driver mappings along with guest memory; `MCL_FUTURE` throws because later mappings are not tracked for locking. On main, XPSemu (PPSA97358) fails to load at this import; its `MCL_CURRENT | MCL_FUTURE` calls execute only when `-overcommit mem-lock=on` is enabled. The call has not been observed in a run with this implementation. - [sigtimedwait](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) waits on a process-wide pending set instead of throwing: a signal raised while blocked stays pending and is taken by `sigwait`, `sigwaitinfo` or `sigtimedwait`, delivered to the handler when `_sigprocmask` unblocks it, or delivered by `pause`. The set covers all 128 signals and the lowest number that is pending and in the set is selected (FreeBSD specifies the lowest number only among real-time signals). Selection, `EAGAIN` on timeout, `EINVAL` for a `tv_nsec` outside 0 to 1000000000 while nothing is pending and `NULL` timeout blocking indefinitely follow FreeBSD's [sigtimedwait(2)](https://man.freebsd.org/cgi/man.cgi?query=sigtimedwait). `sigwait` returns 0 with the number in its output, as POSIX specifies, and `sigwaitinfo` and `sigtimedwait` return the number, as FreeBSD does. The guest `siginfo_t` follows FreeBSD 11's `sys/sys/siginfo.h`: 80 bytes, `si_signo` at 0, `si_code` at 8, `si_pid` at 12, `si_uid` at 16, `si_addr` at 24, `si_value` at 32 and `si_band` at 40; only `si_signo`, `si_code` (`SI_USER`) and `si_pid` (the guest pid) are written, `si_uid` stays 0 because no uid is modelled, and the console's layout is not verified. PPSA85300 imports `sigtimedwait` and fails to load without it; its only call site is VLC's `vlc_writev` ([filesystem.c](https://github.com/videolan/vlc/blob/6de05adcbaf2e8b85fe86aad4169393098628119/src/posix/filesystem.c#L243-L278)), which polls with a zero timeout to drain a `SIGPIPE` only after `writev` fails with `EPIPE`. The waits are not cancellation points and an unblocked caught signal does not interrupt them with `EINTR`, where FreeBSD's `sigwaitinfo` and `sigtimedwait` may return it. `SIGPIPE` has no host equivalent and is never raised by a host write here (see `read, write and vector I/O`), so VLC's drain finds nothing -- [pause, sigsuspend, sigwait, sigwaitinfo](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) take the pending set of `sigtimedwait` instead of throwing. `pause` waits for a pending signal that is not blocked and whose disposition can be delivered, runs its handler on the calling thread and returns -1 with `EINTR`; an ignored signal is discarded and the wait continues, and it never returns for a signal whose handler cannot run. A signal whose registered handler has `SA_SIGINFO` still throws, and one whose disposition is the default action and that has no host equivalent (every signal except 2, 4, 6, 8, 11 and 15) is neither delivered nor discarded: it stays pending until a wait takes it, where FreeBSD runs the default action, which terminates the process for most of them. PPSA68091, PPSA99169, PPSA99420 and PPSA97358 import them and fail to load without them. PPSA68091 calls `pause` at one site (0x8F7266), in a loop that waits while a global holds another thread's `errno` address. PPSA99169 only stores the address of `pause` in a table of symbol addresses (0x30DB1). PPSA99420 calls `pause` at 0x3BD80F6 and 0x3C9E220, `sigwait` at 0x3C9E4C7 and `sigwaitinfo` at 0x3C9E588. PPSA97358 calls `sigwait` from QEMU's `sigwait_compat` thread ([compatfd.c](https://github.com/ZiZc3/XPSemu/blob/8eec07b4e31f66dce8e0b05c1ca1281af303e933/util/compatfd.c#L28-L53)), which `qemu_signal_init` ([main-loop.c](https://github.com/ZiZc3/XPSemu/blob/8eec07b4e31f66dce8e0b05c1ca1281af303e933/util/main-loop.c#L110-L147)) starts for `SIGIO`, `SIGALRM` and `SIGBUS` when the main loop is created, so it reaches the wait at start-up, and from the vCPU thread of the dummy accelerator ([dummy-cpus.c](https://github.com/ZiZc3/XPSemu/blob/8eec07b4e31f66dce8e0b05c1ca1281af303e933/accel/dummy-cpus.c#L48-L56)). `sigsuspend` swaps the process-wide mask of `_sigprocmask` for its argument, waits as `pause` does through the same helper, then restores the mask through `_sigprocmask`, and returns -1 with `EINTR`; a null set throws, as the console faults in libthr instead of returning `EFAULT`. Like `_sigprocmask`, only the first 32 signals are blocked: words 1 to 3 of the argument are not applied, so signals 33 to 128 stay deliverable during the wait; widening the shared mask is a follow-up. Java 25 LAN / DNS (PPSA99869, #5501) imports it and fails to load without it, after `sysctlnametomib`, `fegetenv` and `mincore`. None of the five waits is a cancellation point +- [pause, sigsuspend, sigwait, sigwaitinfo](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) take the pending set of `sigtimedwait` instead of throwing. `pause` waits for a pending signal that is not blocked and whose disposition can be delivered, runs its handler on the calling thread and returns -1 with `EINTR`; an ignored signal is discarded and the wait continues, and it never returns for a signal whose handler cannot run. A signal whose registered handler has `SA_SIGINFO` still throws, and one whose disposition is the default action and that has no host equivalent (every signal except 2, 4, 6, 8, 11 and 15) is neither delivered nor discarded: it stays pending until a wait takes it, where FreeBSD runs the default action, which terminates the process for most of them. PPSA68091, PPSA99169, PPSA99420 and PPSA97358 import them and fail to load without them. PPSA68091 calls `pause` at one site (0x8F7266), in a loop that waits while a global holds another thread's `errno` address. PPSA99169 only stores the address of `pause` in a table of symbol addresses (0x30DB1). PPSA99420 calls `pause` at 0x3BD80F6 and 0x3C9E220, `sigwait` at 0x3C9E4C7 and `sigwaitinfo` at 0x3C9E588. PPSA97358 calls `sigwait` from QEMU's `sigwait_compat` thread ([compatfd.c](https://github.com/ZiZc3/XPSemu/blob/8eec07b4e31f66dce8e0b05c1ca1281af303e933/util/compatfd.c#L28-L53)), which `qemu_signal_init` ([main-loop.c](https://github.com/ZiZc3/XPSemu/blob/8eec07b4e31f66dce8e0b05c1ca1281af303e933/util/main-loop.c#L110-L147)) starts for `SIGIO`, `SIGALRM` and `SIGBUS` when the main loop is created, so it reaches the wait at start-up, and from the vCPU thread of the dummy accelerator ([dummy-cpus.c](https://github.com/ZiZc3/XPSemu/blob/8eec07b4e31f66dce8e0b05c1ca1281af303e933/accel/dummy-cpus.c#L48-L56)). `sigsuspend` swaps the process-wide mask of `_sigprocmask` for its argument, waits as `pause` does through the same helper, then restores the mask through `_sigprocmask`, and returns -1 with `EINTR`; a null set throws, as the console faults in libthr instead of returning `EFAULT`. All four words of the argument replace the mask and are restored afterwards, as in `_sigprocmask`. Java 25 LAN / DNS (PPSA99869, #5501) imports it and fails to load without it, after `sysctlnametomib`, `fegetenv` and `mincore`. None of the five waits is a cancellation point - [getsid](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) returns the calling process ID for pid 0 and the calling process, and `ESRCH` for other process IDs, assuming the single guest process is its own session leader. PPSA74126 imports it and fails to load without it - [sigaction](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) records the handler, flags and mask of every signal from 1 to 128 and reports them back, following FreeBSD's `sigaction` (`EINVAL` for other signals, and for a handler on `SIGKILL` or `SIGSTOP`); `signal` records its handler with `SA_RESTART`, as FreeBSD's libc does. Only handlers without `SA_SIGINFO` on the signals `signal` already delivers (`SIGINT`, `SIGILL`, `SIGABRT`, `SIGFPE`, `SIGSEGV`, `SIGTERM`) run, the same way `signal` handlers do; their mask and flags are not applied. `SA_SIGINFO` handlers are recorded but never called, since that needs FreeBSD's `siginfo_t` and `ucontext_t`, and neither are the handlers of other signals (`SIGBUS`, `SIGHUP`, ...), whether they are set by `sigaction` or by `signal`, which records them too (every number from 1 to 128, with `SA_RESTART` and an empty mask replacing what `sigaction` stored; `SIGKILL` and `SIGSTOP` accept only the default handler in both calls, as in FreeBSD's `kern_sigaction`). `raise` and `kill` accept every number from 1 to 128 now: a blocked one stays pending on the set of `sigtimedwait`, an unblocked one whose handler is registered runs it on the calling thread, and an unblocked one that has no host equivalent and no handler still fails with `EINVAL`, where FreeBSD runs its default action. `raise` of an `SA_SIGINFO` handler still throws instead of terminating - [kill, killpg](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) follow FreeBSD's `kern_kill` and `killpg1` for the only guest process: `EINVAL` for signals outside 0 to 128 and for negative `killpg` process groups, signal 0 only checks that the target exists, and its own pid, 0 and minus its own pid (the guest is taken to lead its own process group) signal it through `raise`, so they share `raise`'s limits above. Any other pid, and -1, which skips the caller, report `ESRCH`, so `EPERM` is never reported From 139e3b86ed70b9ceba367ad23f369b889b14f71e Mon Sep 17 00:00:00 2001 From: AlxcBTW <187763517+AlxcBTW@users.noreply.github.com> Date: Thu, 15 Oct 2026 13:55:39 +0200 Subject: [PATCH 0272/1000] fix(agc): route native-owned depth binds before the HTILE read and store-back A multisampled depth target is created as, or already is, the native depth owner, and a target whose HTILE overlaps a resident depth guard aliases one. Neither may have its HTILE read or rewritten by the generic store-back: the expanded words would be a guest write over the owner's range. DepthSurfaceView decides this before the store-back, which still runs before GpuMutex() and the surfaces mutex are taken. NativeMultisampleDepth binds the owner and a single-sample alias of its HTILE and checks the words stay untouched. --- .../Graphics/src/DepthSurface.cpp | 12 +++++-- .../execution/NativeMultisampleDepth.cpp | 31 +++++++++++++++++++ docs/dev/debt/libSceAgcDriver.md | 2 +- 3 files changed, 42 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp index f986cf60f3..2320243816 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp @@ -911,8 +911,16 @@ void forgetHtileReads(std::uint64_t address, std::size_t bytes) { reads.erase(reads.lower_bound(address), reads.lower_bound(address + bytes)); } +bool RoutesToNativeDepthOwner(const DepthTarget& target) { + if (target.samples != 1u) return true; + if (target.htileAddress == 0 || !anyDepthGuard().load(std::memory_order_acquire)) return false; + const auto bytes = static_cast(HtileMetadataBytes(target.extent)); + std::shared_lock lock(depthGuardMutex()); + return std::ranges::any_of(depthGuards(), [&](const auto& range) { return range.active->load(std::memory_order_acquire) && target.htileAddress < range.address + range.bytes && range.address < target.htileAddress + bytes; }); +} + VkImageAspectFlags storeBackClearedHtile(const Context& context, const DepthTarget& target) { - if (target.htileAddress == 0 || target.samples != 1u) return 0; + if (target.htileAddress == 0) return 0; const auto bytes = static_cast(HtileMetadataBytes(target.extent)); if (!GuestMemory::Accessible(reinterpret_cast(target.htileAddress), bytes, true)) return 0; const auto collected = GuestMemory::CollectWrites(target.htileAddress, bytes); @@ -1039,7 +1047,7 @@ float HostDepthBiasConstant(const Context& context, VkFormat format, float const } VkImageView DepthSurfaceView(const Context& context, const DepthTarget& target) { - const auto htileCleared = storeBackClearedHtile(context, target); + const auto htileCleared = RoutesToNativeDepthOwner(target) ? VkImageAspectFlags{0} : storeBackClearedHtile(context, target); std::lock_guard gpu(GuestMemory::GpuMutex()); std::unique_lock lock(surfacesMutex()); const bool cleared = takeClearedHtile(target); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp index 73f8217fd4..ba48535dcf 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp @@ -729,6 +729,36 @@ void CheckDepthTarget(Context context, VkFormat format, std::uint32_t fragments, std::printf("Native depth format=%u samples=%u clear=%u: imported, rasterized twice, sampled and refused conflicts\n", static_cast(format), fragments, one); } +void CheckHtileRouting(Context context, VkFormat format, std::uint32_t fragments) { + const VkExtent2D extent{5u, 3u}; + const auto bytes = static_cast(MultisampledDepthBytes_nid_no_patch(extent, format == VK_FORMAT_D16_UNORM ? 2u : 4u, fragments)); + TargetMemory memory(bytes), metadata(65536u), single(65536u); + const auto metadataBytes = HtileMetadataBytes(extent); + std::memset(metadata.Data(), 0, metadataBytes); + const std::vector cleared(metadata.Data(), metadata.Data() + metadataBytes); + const auto untouched = [&] { return std::equal(cleared.begin(), cleared.end(), metadata.Data()); }; + DepthTarget target{memory.Address(), 0u, extent, format, 0.0f, 0u, metadata.Address(), false, fragments}; + target.depthSwizzle = 24u; + target.iterate256 = target.htilePipeAligned = true; + Recorder recorder(context); + recorder.Activate(); + struct Cleanup { + Recorder& recorder; + Context context; + ~Cleanup() { recorder.Sync(); ClearCachedTextures(context.device); ClearDepthSurfaces(context.device); } + } cleanup{recorder, context}; + const auto view = DepthSurfaceView(context, target); + Require(HasResidentDepthGuards_nid_no_patch(), "the native depth owner installed no guest guard"); + Require(untouched(), "the first bind of a native depth target stored expanded HTILE words back"); + Require(DepthSurfaceView(context, target) == view && untouched(), "rebinding a native-owned depth target went through the generic HTILE store-back"); + auto alias = target; + alias.samples = 1u; + alias.address = single.Address(); + TargetRefuses([&] { DepthSurfaceView(context, alias); }, "layout or metadata changed"); + Require(untouched(), "a single-sample bind over a native owner's HTILE stored expanded words into it"); + std::printf("Native depth format=%u samples=%u: binds over the owner's HTILE left its words alone\n", static_cast(format), fragments); +} + void CheckEligibility(Context context) { constexpr VkExtent2D extent{129u, 67u}; constexpr std::uint32_t samples = 4u; @@ -1002,6 +1032,7 @@ int main(int argc, char** argv) { for (const auto samples : {2u, 4u, 8u}) { if (!DepthSupported(context, format, samples)) { unavailable += 2u; continue; } for (const bool one : {false, true}) { CheckDepthTarget(context, format, samples, one); ++checked; } + CheckHtileRouting(context, format, samples); } } CheckEligibility(context); diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index dd3f294fa3..847b8614b4 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -94,7 +94,7 @@ - [Descriptor pool chain](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - each pool holds two storage buffer descriptors more than a set can take, as a workaround: RADV (Mesa 26.0 and later) lets a `FREE_DESCRIPTOR_SET` pool place a set up to 32 bytes past the pool's buffer ([`util_vma_heap_init`](https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/src/amd/vulkan/radv_descriptor_pool.c) is given `bo_size + RADV_POOL_HEAP_OFFSET` as a size), and with a page-multiple pool size those bytes are an unmapped page - [CMASK fast clears](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver) - each fast-cleared CMASK tile (code 0) is stored as `CB_COLOR_CLEAR_WORD` into its 8x8 texels at the first draw into the target or at its fast-clear eliminate or DCC decompress pass, not as the color block writes the tile, and the CMASK is then stored as expanded (0xF): a texture or copy read of the target after that draw and before the title's eliminate sees the clear value in tiles no draw wrote, where the console keeps their previous bytes. Tile addresses follow the pipe-aligned SW_64KB_Z_X CMASK equation for 16 pipes; other CMASK swizzle modes, a cleared tile of a DCC target and a fast clear of a mipmapped (base mip 0), 3D or array color target throw. A tile with a code other than 0 and 0xF keeps its texels like an expanded tile: PPSA18391 (engine: Clickteam Fusion) fast clears a target whose CMASK it never initialized, so some of its nibbles hold leftover values such as 1 and 10; what the console does with such codes on a single-sample target is unverified on hardware. Single-sample views with `viewMip > 0` skip CMASK fast-clear decoding since CMASK metadata only covers base mip 0 on AMD hardware (Mesa `si_state.c` similarly clears `FAST_CLEAR` for level > 0). - [HTILE depth fast clears](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver) - HTILE is not modeled. Only RADV's `clear_htile_mask` compute kernel (17 GFX10 words, 64x1x1 groups) is recognized, and only when it leaves ZMASK at 0 (`user[4]` and `user[5]` low nibbles both 0); the dispatch still runs, and the host depth image is cleared to `DB_DEPTH_CLEAR` the next time the surface with that `DB_HTILE_DATA_BASE` address is bound. The address match is exact, so a clear of a different HTILE range does not hit. HTILE initialization, the stencil-only update, and fast clears made by the SDK's own paths, CP DMA fills or other kernels are not covered and leave the depth image as it was. The kernel words are the ones the PR author saw RADV submit; they are not compared with Mesa's source. A second kernel, the 9-word dword fill PPSA01802 (Kena: Bridge of Spirits) clears its depth HTILE with (64x1x1 groups, one `buffer_store_format_x` per thread through the stride-4 V# in `user[0..3]`, the value read by `s_buffer_load_dword` through the V# in `user[4..7]`), is taken as a fill of that range when the store format is 32_UINT, 32_SINT or 32_FLOAT and the value V# covers the dword (NUM_RECORDS times its stride, or NUM_RECORDS bytes when the stride is 0) ([`MatchMemoryValueFill`](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp), then the whole-HTILE and ZMask rules of `NoteDepthMetadataFill`). The value is read from guest memory when the dispatch is decoded, so a value that earlier GPU work in the same submission has not written back yet is not seen. When the [fill HLE](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferFill.cpp) takes the dispatch instead (an output descriptor of one 32-bit value per element, a value descriptor that covers the loaded dword, a 16-byte aligned range), the value is read again when the fill is recorded, after the labels queued before it, and the HTILE fill is noted with that value. -- [HTILE written by other kernels](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver) - when a depth target is bound and its HTILE was written since it was last read, the words over its whole pipe-aligned metadata (`HtileMetadataBytes`, the extent `HtileFillCovers` takes) are read, outside the surfaces mutex since the read waits for recorded work: when every one marks the depth (ZMask 0) or the stencil (SMem 0) cleared, the surface takes that fast clear and the words are stored back as expanded (depth only: ZMask 15 over the full Z range; with stencil: ZMask 15, Z range and SMem, SR1 and SR0 3), so only the title's next clear clears it again. PPSA29141 clears its depth every frame with a compute kernel the fill hooks do not recognize (`InvokeHTileClearComputeShader` in its `ClearMRT` pass); without this its depth kept every earlier frame and moving objects left black trails. PPSA25344 (Unreal Engine 5) reads its HTILE after the depth prepass in its Nanite depth export, which takes a tile with ZMask 0 as holding the clear value; without the expanded words the export put Nanite depth over the prepass and the Nanite material pass painted over the meshes in front. The word layouts are Mesa radv's for GFX10. Metadata with some tiles cleared is not modelled, and the compression state draws leave in HTILE is not tracked, so a title reading its HTILE after a draw sees the stored expanded words. A recognized HTILE fill or clear makes the next bind read the words again. A slice other than 0 of a depth array is read at its own HTILE (`DB_HTILE_DATA_BASE` plus the slice times `HtileMetadataBytes`) but the HTILE word layout of slices 1 and up is not modelled (PPSA05684's tile masks need X8 and Y8 to take Z0 and Z1 as well, slices 4 and up unmeasured), so such a slice takes the clear only when all its words are equal and otherwise keeps the fill-time fast clears. A depth-only HTILE whose tiles all have ZMask 0 but different Min/Max Z (per-tile clear values, PPSA05684's cascades) is taken as a whole-surface clear to `DB_DEPTH_CLEAR`. +- [HTILE written by other kernels](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver) - when a depth target is bound and its HTILE was written since it was last read, the words over its whole pipe-aligned metadata (`HtileMetadataBytes`, the extent `HtileFillCovers` takes) are read, outside the surfaces mutex since the read waits for recorded work: when every one marks the depth (ZMask 0) or the stencil (SMem 0) cleared, the surface takes that fast clear and the words are stored back as expanded (depth only: ZMask 15 over the full Z range; with stencil: ZMask 15, Z range and SMem, SR1 and SR0 3), so only the title's next clear clears it again. PPSA29141 clears its depth every frame with a compute kernel the fill hooks do not recognize (`InvokeHTileClearComputeShader` in its `ClearMRT` pass); without this its depth kept every earlier frame and moving objects left black trails. PPSA25344 (Unreal Engine 5) reads its HTILE after the depth prepass in its Nanite depth export, which takes a tile with ZMask 0 as holding the clear value; without the expanded words the export put Nanite depth over the prepass and the Nanite material pass painted over the meshes in front. The word layouts are Mesa radv's for GFX10. Metadata with some tiles cleared is not modelled, and the compression state draws leave in HTILE is not tracked, so a title reading its HTILE after a draw sees the stored expanded words. A recognized HTILE fill or clear makes the next bind read the words again. A slice other than 0 of a depth array is read at its own HTILE (`DB_HTILE_DATA_BASE` plus the slice times `HtileMetadataBytes`) but the HTILE word layout of slices 1 and up is not modelled (PPSA05684's tile masks need X8 and Y8 to take Z0 and Z1 as well, slices 4 and up unmeasured), so such a slice takes the clear only when all its words are equal and otherwise keeps the fill-time fast clears. A depth-only HTILE whose tiles all have ZMask 0 but different Min/Max Z (per-tile clear values, PPSA05684's cascades) is taken as a whole-surface clear to `DB_DEPTH_CLEAR`. A multisampled depth target, and any target whose HTILE overlaps a resident native depth guard, skips this read and store-back and goes to the native multisample depth owner, which keeps its own refusal for a fast-clear transition. - [Host imports](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver) on Linux skip ranges that are not mapped read-write, which are copied instead. The NVIDIA driver (595.91.07) refuses `VK_EXT_external_memory_host` imports of read-only or executable mappings (and shared mappings of regular files) with `VK_ERROR_OUT_OF_DEVICE_MEMORY`, after which every `vkAllocateMemory` on the device fails the same way. Read-only ranges are never imported, even on drivers that accept them. Shared mappings of regular files are not checked for. On Windows, an alias of adjacent registered ranges is refused if any page is not a shared view; a range made entirely of shared views is aliased run by run, including across sections or nonconsecutive section offsets. Adjacent registered ranges are joined into one import where they fill a whole 8 MiB aligned window; ranges of a partly filled window, and ranges larger than the window, are imported one at a time. A buffer bound across adjacent ranges that are each larger than the window, or bound exactly over adjacent ranges of any size (from the first one's start to the last one's end), joins them into one import when it is bound, and their own imports are retired; a buffer that crosses any other two imports is copied. A heap grown in pieces re-imports its joined run only when a buffer is bound exactly over all of its pieces, or when a sparse descriptor heap over it is bound: a heap a shader binds whole while only the pieces from its base are mapped is bound in place from the import of that run, its views ending at the import's end as the copy's do, once the previous bind of that address saw the same last mapped byte (a heap that grows between every two of its binds stays copied; one bound more often than it grows is joined again after each growth) and at most 64 times per joined run base; past that, with a message, it is not joined again: it is bound in place while an existing import covers its run and copied otherwise, until no registered range starts at that base or the device changes. Ranges larger than the window are taken to be registered whole and never grown in pieces: a title that grows a heap in pieces larger than 8 MiB re-imports the joined run each time a bound buffer crosses into a new piece. The 8 MiB window is a choice: unbounded runs were re-imported whole on every growth of a heap mapped in small pieces, and the NVIDIA driver (610.57.04, 615.78.08) then refused imports or lost the device. Not measured on RADV. - [Cube texture descriptors](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp) with equal `BASE_ARRAY` and `DEPTH` expose six faces starting at `BASE_ARRAY`. Measured with `image_load`, `image_sample` and `image_get_resinfo` on RX 6600 (gfx1032), using 64x64 R32_UINT faces and base slices 0, 5 and 6. Other ranges containing fewer than six faces or incomplete cube arrays still throw: hardware load bounds and filtered-sample addressing differ. Out-of-range cube-array sampling remains unimplemented. Samplers with `DISABLE_CUBE_WRAP` are created with `VK_SAMPLER_CREATE_NON_SEAMLESS_CUBE_MAP_BIT_EXT`, the flag RADV maps to that bit (Mesa `src/amd/vulkan/radv_sampler.c`, `src/amd/common/ac_descriptors.c`); without `VK_EXT_non_seamless_cube_map` a cube texture sampled through such a sampler throws. PS5 equivalence has not been measured on console hardware. - [Array layer views](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp) (libSceAgcDriver) - `ArrayLayersServeView` lets a 2D texture at a 2D array image's base address, or a 2D array texture over its first layers, view that image (`Texture::CanCopyFrom`). Mesa-built homebrew (ps5-opengl) writes the view's last slice plus one to `CB_COLOR_ATTRIB3.MIP0_DEPTH`, so a slice-0 render target is a two-layer array image, and the title samples it as a plain 2D texture (SuperTuxKart PPSA69421 drew a black 3D scene and minimap without this). The per-layer layout of a mipmapped array is assumed equal to a standalone surface's; it is not measured on the console. A 2D texture at a later slice's address still gets an image of its own. From 6a9b3d6fad39f38be5b70a41aa83c2740591820f Mon Sep 17 00:00:00 2001 From: fdfawef324 Date: Thu, 15 Oct 2026 07:56:13 -0400 Subject: [PATCH 0273/1000] docs(agc): record the BMP open failure measured on presentation VideoOut flips on an RTX 3060 now show that a first frame dump that cannot be opened no longer stops presentation. Header, pixel and close failures remain measured only by the CPU fixture. Co-Authored-By: Claude Opus 5.5 --- docs/dev/debt/libSceAgcDriver.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index dbcf98f954..466c951843 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -2,7 +2,7 @@ ### Unknown function info -- [Optional BMP frame dumps](../../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) use checked host stdio. An open, header, pixel or close failure prints one diagnostic and disables subsequent BMP dumps for the process; queued frames drain without more file I/O. Pixel validation and allocation failures retain the existing fatal path. The CPU fixture checks identical BMP bytes and real pipe failures; hardware presentation after a storage error is unmeasured. +- [Optional BMP frame dumps](../../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) use checked host stdio. An open, header, pixel or close failure prints one diagnostic and disables subsequent BMP dumps for the process; queued frames drain without more file I/O. Pixel validation and allocation failures retain the existing fatal path. The CPU fixture checks identical BMP bytes and real pipe failures. VideoOut flips on an RTX 3060 continue after a first dump that cannot be opened; header, pixel and close failures during presentation are unmeasured. - [Inactive vertex point size](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) - `PA_CL_VS_OUT_CNTL.USE_VTX_POINT_SIZE` is accepted for lines, triangles and rectangles with `GS_EN` and `HS_EN` clear, when polygon mode is disabled or both faces are filled. Points, patches and absent routing or rasterization registers still throw. Bit 16 selects the misc export's X component as PointSize ([AMD CIK register reference](https://www.amd.com/content/dam/amd/en/documents/radeon-tech-docs/programmer-references/CIK_3D_registers_v2.pdf), PA_CL_VS_OUT_CNTL); ignoring it for non-point rasterization follows [Vulkan PointSize](https://docs.vulkan.org/refpages/latest/refpages/source/PointSize.html). This is inferred, not measured on console hardware. XashPS5 (PPSA19111 v1.3) sets `0x01210000` on a filled triangle draw. - [Native MSAA depth attachment feedback](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) is not implemented. A draw sampling a native multisampled depth view while attaching its source image throws before pipeline creation or render-pass recording, including read-only attachment feedback. Sampling that image in a separate draw or dispatch remains supported. The refusal compares the held native image views and their device and attachment identity; single-sample depth behavior is retained. From 5fec3f600b88e88fe93e1f712150da081ebe941c Mon Sep 17 00:00:00 2001 From: Romans Pokrovskis Date: Wed, 14 Oct 2026 02:05:59 +0300 Subject: [PATCH 0274/1000] test(agc): exercise completed native HTILE clears --- .../execution/NativeMultisampleDepth.cpp | 44 +++++++++++++++++-- 1 file changed, 41 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp index 73f8217fd4..0c43b550c3 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include #include #include @@ -599,17 +600,18 @@ void CheckDrawFeedback() { std::puts("Native depth feedback refused through the draw resource path"); } -void CheckDepthTarget(Context context, VkFormat format, std::uint32_t fragments, bool one) { +void CheckDepthTarget(Context context, VkFormat format, std::uint32_t fragments, bool one, bool clearing = false, std::optional depthOnlyOverride = std::nullopt) { const VkExtent2D extent{5u, 3u}; const auto bytes = static_cast(MultisampledDepthBytes_nid_no_patch(extent, format == VK_FORMAT_D16_UNORM ? 2u : 4u, fragments)); TargetMemory memory(bytes), metadata(65536u); const auto metadataBytes = HtileMetadataBytes(extent); - const std::uint32_t pattern = one ? 0xfffffff0u : 0u; + const bool depthOnly = depthOnlyOverride.value_or(one); + const std::uint32_t pattern = one ? (depthOnly ? 0xfffffff0u : 0xfffc00f0u) : 0u; for (std::size_t i = 0u; i < metadataBytes / 4u; ++i) std::memcpy(metadata.Data() + i * 4u, &pattern, 4u); DepthTarget target{memory.Address(), 0u, extent, format, one ? 1.0f : 0.0f, 0u, metadata.Address(), false, fragments}; target.depthSwizzle = 24u; target.pipeBankXor = one ? 0x8c00u : 0u; - target.htileDepthOnly = one; + target.htileDepthOnly = depthOnly; target.iterate256 = target.htilePipeAligned = true; Recorder recorder(context); recorder.Activate(); @@ -680,6 +682,29 @@ void CheckDepthTarget(Context context, VkFormat format, std::uint32_t fragments, batch.SubmitAndWait(); check(true, offset); } + if (clearing) { + for (std::uint32_t iteration = 0u; iteration < 3u; ++iteration) { + { + CommandBatch batch(context); + raster.Run(batch.Handle(), static_cast(iteration) / 16.0f); + batch.SubmitAndWait(); + } + NoteHtileDepthClear(target.htileAddress, static_cast(metadataBytes)); + AgcDriver::GuestMemory::StoreOwnBytes(target.htileAddress, static_cast(metadataBytes), [&] { + for (std::size_t i = 0u; i < metadataBytes / 4u; ++i) std::memcpy(metadata.Data() + i * 4u, &pattern, 4u); + }); + Require(DepthSurfaceHolds(context, DecodeTextureResource(words), texture.get()), "completed native depth clear lost its sampled view"); + recorder.Sync(); + Require(DepthSurfaceView(context, target) == view && CachedSampledTexture(context, words) == texture, "completed native depth clear replaced an attachment or cached view"); + std::memset(output.Bytes().data(), 0xa5, output.Bytes().size()); + CommandBatch batch(context); + fetch.Run(batch.Handle(), count); + RecordMemoryBarrier(context, batch.Handle(), VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_HOST_READ_BIT); + batch.SubmitAndWait(); + check(false, 0.0f); + } + std::printf("Native depth completed clear format=%d samples=%u depthOnly=%u value=%u cycles=3\n", format, fragments, depthOnly, one); + } } Require(std::all_of(memory.Data(), memory.Data() + bytes, [](auto value) { return value == std::byte{0xa5}; }), "native depth sampling silently published guest memory"); std::array probe{}; @@ -985,6 +1010,19 @@ int main(int argc, char** argv) { const auto context = device->GetContext(); const std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); Require(DepthSupported(context, VK_FORMAT_D32_SFLOAT, 4u), "the native depth regression requires sampled D32 with four samples"); + if (argc == 2 && std::string_view(argv[1]) == "--clears") { + std::uint32_t checked = 0u, unavailable = 0u; + for (const auto format : {VK_FORMAT_D16_UNORM, VK_FORMAT_D32_SFLOAT}) { + for (const auto samples : {2u, 4u, 8u}) { + if (!DepthSupported(context, format, samples)) { unavailable += 4u; continue; } + for (const bool depthOnly : {false, true}) { + for (const bool one : {false, true}) { CheckDepthTarget(context, format, samples, one, true, depthOnly); ++checked; } + } + } + } + std::printf("Native depth completed clear cases checked=%u unavailable=%u\n", checked, unavailable); + return 0; + } Require(argc == 1 || (argc == 2 && std::string_view(argv[1]) == "--retirement"), "invalid native depth fixture arguments"); if (argc == 2) { std::uint32_t checked = 0u, unavailable = 0u; From 99e60030989458da0b9a58ec716cc4ebf42e12d1 Mon Sep 17 00:00:00 2001 From: Romans Pokrovskis Date: Wed, 14 Oct 2026 02:10:44 +0300 Subject: [PATCH 0275/1000] feat(agc): prove complete tracked driver store coverage --- .../Execution/include/GuestMemory.hpp | 1 + .../Execution/src/GuestMemory.cpp | 48 ++++++++- .../libSceAgcDriver/tests/WriteTracking.cpp | 98 +++++++++++++++++++ 3 files changed, 143 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp index 037486c8bd..2388684997 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp @@ -81,6 +81,7 @@ std::uint64_t MarkWritten(std::uint64_t address, std::size_t bytes); std::uint64_t StoreOwnBytes(std::uint64_t address, std::size_t bytes, const std::function& store); std::uint64_t StoreOwnBytes(std::uint64_t address, std::span source); bool StoredOver(std::uint64_t address, std::size_t bytes, std::uint64_t generation); +bool StoredAllSince_nid_no_patch(std::uint64_t address, std::size_t bytes, std::uint64_t generation); // Collect epoch: within one epoch a range already collected is not walked again, CollectWrites // returns the current generation instead. A guest write landing between two collects of the same // epoch is seen by the next epoch, which is the ordering real hardware gives a CPU write made while a diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp index 8850453b34..65af7b6a26 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp @@ -883,6 +883,20 @@ struct WriteTracker { std::uint32_t dropped = 0; std::uint32_t next = 0; std::array pieces{}; + + bool covers(std::uint32_t begin, std::uint32_t end, std::uint64_t generation) const { + if (generation == 0 || begin >= end || dropped > generation) return false; + if (whole > generation) return true; + while (begin < end) { + auto covered = begin; + for (const auto& piece : pieces) { + if (piece.generation > generation && piece.begin <= begin && piece.end > covered) covered = piece.end; + } + if (covered == begin) return false; + begin = covered; + } + return true; + } }; std::unordered_map driverPieces; @@ -909,10 +923,9 @@ struct WriteTracker { const auto from = static_cast(std::max(address, begin) - begin); const auto to = static_cast(std::min(end, begin + WriteBlockBytes) - begin); if (from == 0 && to == WriteBlockBytes) { - if (const auto found = driverPieces.find(block); found != driverPieces.end()) { - found->second = DriverPieces{}; - found->second.whole = stampGeneration; - } + auto& entry = driverPieces[block]; + entry = DriverPieces{}; + entry.whole = stampGeneration; return; } auto& entry = driverPieces[block]; @@ -1529,6 +1542,33 @@ bool StoredOver(std::uint64_t address, std::size_t bytes, std::uint64_t generati return false; } +bool StoredAllSince_nid_no_patch(std::uint64_t address, std::size_t bytes, std::uint64_t generation) { + if (generation == 0 || bytes == 0 || bytes > std::numeric_limits::max() - address) return false; + auto& tracker = Tracker(); + const auto lock = lockTracker(tracker); + tracker.initialize(); + if (!tracker.watched || !tracker.covers(address, bytes)) return false; + const auto end = address + bytes; + if (!tracker.unstampedStores.empty()) { + const auto first = address / WriteBlockBytes; + const auto last = (end - 1u) / WriteBlockBytes; + if (last - first >= WriteTracker::UnstampedSlots) { + if (std::ranges::any_of(tracker.unstampedStores, [&](auto stamp) { return stamp > generation; })) return false; + } else { + for (auto block = first; block <= last; ++block) if (tracker.unstampedStores[block % WriteTracker::UnstampedSlots] > generation) return false; + } + } + for (auto block = tracker.blockOf(address); block <= tracker.blockOf(end - 1u); ++block) { + const auto found = tracker.driverPieces.find(block); + if (found == tracker.driverPieces.end()) return false; + const auto start = tracker.blockBegin(block); + const auto from = static_cast(std::max(address, start) - start); + const auto to = static_cast(std::min(end, start + WriteBlockBytes) - start); + if (!found->second.covers(from, to, generation)) return false; + } + return true; +} + std::uint64_t TrackerGeneration() { auto& tracker = Tracker(); // Under the mutex: a collect bumps the generation before its walk, and a walk still running diff --git a/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp b/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp index 86da33486d..d69035db8c 100644 --- a/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -100,6 +101,102 @@ void CheckSharedBlock() { if (!Watched(outside, unwatched.size())) Require(StoredOver(outside, unwatched.size(), TrackerGeneration()), "an unwatched range reads as not stored over"); } +void CheckStoredAllSince() { + constexpr std::size_t bytes = 4 * Block; + auto* memory = static_cast(AllocateWatched(bytes)); + struct Cleanup { + void* memory; + ~Cleanup() { +#ifdef _WIN32 + GuestArena::GuestArenaReset_nid_postfix(memory, bytes); + GuestArena::GuestArenaRelease_nid_postfix(memory, bytes); +#else + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(memory, bytes); + munmap(memory, bytes); +#endif + } + } cleanup{memory}; + const auto base = reinterpret_cast(memory); + std::memset(memory, 0x11, bytes); + auto before = CollectWritesUncached(base, bytes); + Require(before != 0, "the complete-store block is not collected"); + Require(!StoredAllSince_nid_no_patch(base, 64, before), "a range with no driver store has complete coverage"); + + MarkWritten(base + Block, Block); + Require(StoredAllSince_nid_no_patch(base + Block, Block, before), "the first whole-block driver store has no complete coverage"); + Require(StoredAllSince_nid_no_patch(base + Block + 1, Block - 2, before), "a whole-block store lost its interior coverage"); + const auto whole = TrackerGeneration(); + Require(!StoredAllSince_nid_no_patch(base + Block, Block, whole), "a whole-block store at the baseline is taken as newer"); + + before = TrackerGeneration(); + static_cast(memory)[2 * Block + 7] = 0x22; + CollectWritesUncached(base + 2 * Block, Block); + Require(!StoredAllSince_nid_no_patch(base + 2 * Block, Block, before), "CPU-only stores prove complete driver coverage"); + + before = TrackerGeneration(); + const std::array dword{}; + Write(base + 16, dword); + Require(StoredAllSince_nid_no_patch(base + 16, dword.size(), before), "a tracked dword store is absent from its exact range"); + Require(!StoredAllSince_nid_no_patch(base, 64, before), "one dword store proves complete surface coverage"); + Require(!StoredAllSince_nid_no_patch(base + 15, 5, before) && !StoredAllSince_nid_no_patch(base + 16, 5, before), "a dword store covers an unstored boundary byte"); + const auto dwordStored = TrackerGeneration(); + Require(!StoredAllSince_nid_no_patch(base + 16, dword.size(), dwordStored), "a dword store at the baseline is taken as newer"); + MarkWritten(base, 64); + Require(StoredAllSince_nid_no_patch(base, 64, before), "a complete non-block-aligned range store is absent"); + + before = TrackerGeneration(); + MarkWritten(base + 512, 16); + MarkWritten(base + 544, 16); + Require(!StoredAllSince_nid_no_patch(base + 512, 48, before), "split driver stores prove coverage across a hole"); + MarkWritten(base + 528, 16); + Require(StoredAllSince_nid_no_patch(base + 512, 48, before), "out-of-order split driver stores did not form complete coverage"); + Require(!StoredAllSince_nid_no_patch(base + 512, 49, before), "split coverage includes an unstored final byte"); + + before = TrackerGeneration(); + MarkWritten(base + 1024 + 8, 24); + MarkWritten(base + 1024, 16); + Require(StoredAllSince_nid_no_patch(base + 1024, 32, before), "overlapping driver intervals did not form complete coverage"); + + before = TrackerGeneration(); + MarkWritten(base + Block - 4, 8); + Require(StoredAllSince_nid_no_patch(base + Block - 4, 8, before), "a tracked store across a block boundary lost coverage"); + Require(!StoredAllSince_nid_no_patch(base + Block - 5, 9, before) && !StoredAllSince_nid_no_patch(base + Block - 4, 9, before), "a boundary store covers an unstored adjacent byte"); + + before = TrackerGeneration(); + for (unsigned piece = 0; piece < 6; ++piece) MarkWritten(base + static_cast(piece) * 64, 4); + Require(!StoredAllSince_nid_no_patch(base, 4, before), "dropped driver evidence proves complete coverage"); + MarkWritten(base, Block); + Require(StoredAllSince_nid_no_patch(base, Block, before), "a new whole-block store did not replace dropped evidence"); + + before = TrackerGeneration(); + MarkWritten(base, 3 * Block); + Require(StoredAllSince_nid_no_patch(base, 3 * Block, before), "complete multi-block stores lost coverage"); + Require(StoredAllSince_nid_no_patch(base + 1, 3 * Block - 2, before) && StoredAllSince_nid_no_patch(base + 3 * Block - 1, 1, before), "complete multi-block stores lost edge bytes"); + const auto complete = TrackerGeneration(); + Require(!StoredAllSince_nid_no_patch(base, 3 * Block, complete), "stores older than the baseline prove complete coverage"); + + before = TrackerGeneration(); + MarkWritten(base + 2 * Block, Block); + const auto driverStored = TrackerGeneration(); + static_cast(memory)[2 * Block + 7] = 0x33; + CollectWritesUncached(base + 2 * Block, Block); + Require(StoredAllSince_nid_no_patch(base + 2 * Block, Block, before), "a later CPU store erased the proof that the driver stored every byte"); + Require(!UnchangedSinceCollected(base + 2 * Block, Block, driverStored), "the separate CPU-write check missed a store after complete driver coverage"); + + Require(!StoredAllSince_nid_no_patch(base, 0, before) && !StoredAllSince_nid_no_patch(base, 1, 0), "empty or unknown-baseline complete coverage is accepted"); + Require(!StoredAllSince_nid_no_patch(std::numeric_limits::max(), 2, before), "overflowing complete coverage is accepted"); + Unwatch(base + 3 * Block, Block); + Require(MarkWritten(base + 3 * Block, Block) == 0, "an unwatched whole store acquired trusted driver coverage"); + Require(!StoredAllSince_nid_no_patch(base + 3 * Block, Block, before), "unwatched stores prove complete coverage"); + Require(!StoredAllSince_nid_no_patch(base + 3 * Block - 1, 2, before), "a range entering unwatched memory has complete coverage"); + std::array outside{}; + const auto outsideAddress = reinterpret_cast(outside.data()); + if (!Watched(outsideAddress, outside.size())) { + MarkWritten(outsideAddress, outside.size()); + Require(!StoredAllSince_nid_no_patch(outsideAddress, outside.size(), before), "an outside untracked range has complete coverage"); + } +} + void CheckOwnStore() { void* memory = AllocateWatched(2 * Block); const auto base = reinterpret_cast(memory); @@ -834,6 +931,7 @@ int main() { } #endif CheckSharedBlock(); + CheckStoredAllSince(); CheckOwnStore(); CheckUnwatch(); CheckNewestStamp(); From 7d0dd69f0ac8189f5bfb6d6b24f56f88951f16dd Mon Sep 17 00:00:00 2001 From: Romans Pokrovskis Date: Wed, 14 Oct 2026 02:18:54 +0300 Subject: [PATCH 0276/1000] feat(agc): import completion-proven native HTILE clears --- .../Graphics/include/DepthSurface.hpp | 3 +- .../Graphics/src/DepthSurface.cpp | 52 +++++++++++++++++-- .../execution/NativeMultisampleDepth.cpp | 2 +- 3 files changed, 52 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp index cddc2e5478..2f54bee302 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp @@ -28,7 +28,8 @@ void RetireDepthSurfaces(VkDevice device, std::uint64_t address, std::uint64_t b bool DepthSurfaceAt(std::uint64_t address); bool DepthSurfaceAt(const Context& context, std::span words, const GuestTextureResource& resource); std::uint64_t HtileDepthClearAddress(std::span code, std::span userData, const std::array& numThreads); -void NoteHtileDepthClear(std::uint64_t htileAddress, std::size_t bytes); +std::size_t HtileDepthClearBytes_nid_no_patch(std::span code, std::span userData, const ShaderRecompiler::ShaderComputeStageInfo& compute, std::span packet, std::uint64_t indirectArguments); +void NoteHtileDepthClear(std::uint64_t htileAddress, std::size_t bytes, std::size_t nativeBytes = 0u); struct MemoryValueFill { std::uint64_t address; std::size_t bytes; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp index 5e9089b4e2..53fe36b538 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp @@ -197,8 +197,9 @@ public: DepthSurface(const DepthSurface&) = delete; DepthSurface& operator=(const DepthSurface&) = delete; - void CheckCurrent() const { + void CheckCurrent() { if (target.samples == 1u) return; + const auto clearLease = clearGeneration == 0u ? GuestAllocations::Lease{} : GuestAllocations::GuestAllocationsAcquireAll_nid_postfix(); Require(active->load(std::memory_order_acquire), "the native multisample depth owner was released"); Require(sampling.refusal.empty(), sampling.refusal); for (const auto& range : sampling.initial) { @@ -206,6 +207,26 @@ public: if (const auto* recorder = RecorderFor(context)) Require(!recorder->PendingWriteOverlaps(range.address, range.bytes), "external GPU writes over native multisample depth are not implemented"); GuestMemory::CollectWritesUncached(range.address, range.bytes); } + if (clearGeneration != 0u) { + const auto& depth = sampling.initial[0]; + const auto& metadata = sampling.initial[1]; + Require(GuestMemory::UnchangedSince(depth.address, depth.bytes, depth.generation), "guest writes over native multisample depth are not implemented"); + Require(GuestMemory::UnchangedSinceCollected(metadata.address, metadata.bytes, clearGeneration), "CPU writes raced native multisample depth clear"); + Require(GuestMemory::StoredAllSince_nid_no_patch(metadata.address, metadata.bytes, clearGeneration), "native multisample depth clear has no complete tracked HTILE store"); + Require(clearDepth == 0.0f || clearDepth == 1.0f, "native multisample depth clear value is not implemented"); + auto current = sampling.initial; + current[1].generation = GuestMemory::CollectWritesUncached(metadata.address, metadata.bytes); + std::vector words(metadata.bytes / sizeof(std::uint32_t)); + std::memcpy(words.data(), reinterpret_cast(metadata.address), metadata.bytes); + for (const auto word : words) Require(htileProvesClear(word, target.htileDepthOnly, clearDepth), "native multisample depth completed HTILE does not prove its clear value"); + for (const auto& range : current) GuestMemory::CollectWritesUncached(range.address, range.bytes); + Require(GuestMemory::UnchangedSinceAll(current), "guest writes raced native multisample depth clear"); + Require(GuestMemory::UnchangedSinceCollected(metadata.address, metadata.bytes, clearGeneration), "CPU writes raced native multisample depth clear"); + RecordClear(VK_IMAGE_ASPECT_DEPTH_BIT); + sampling.initial = current; + clearGeneration = 0u; + pendingClear = 0u; + } Require(GuestMemory::UnchangedSinceAll(sampling.initial), "guest writes over native multisample depth are not implemented"); } @@ -224,6 +245,10 @@ public: const auto aspects = pendingClear & (VK_IMAGE_ASPECT_DEPTH_BIT | (target.stencilAddress != 0 ? VK_IMAGE_ASPECT_STENCIL_BIT : 0u)); pendingClear = 0; if (aspects == 0) return; + RecordClear(aspects); + } + + void RecordClear(VkImageAspectFlags aspects) { written = ++depthWrites; auto* recorder = RecorderFor(context); std::unique_ptr batch; @@ -236,7 +261,10 @@ public: context.Function("vkCmdClearDepthStencilImage")(commands, image, VK_IMAGE_LAYOUT_GENERAL, &clear, 1, &range); RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, access); if (batch) batch->SubmitAndWait(); - else Recorder::CountBarriers(Recorder::CommandClass::Draw, 2); + else { + Recorder::CountBarriers(Recorder::CommandClass::Draw, 2); + recorder->Keep(owned); + } } bool SampledAccepts(std::span words, const GuestTextureResource& resource) const { @@ -444,6 +472,7 @@ public: bool sampleable = true; bool samplingActive = false; DepthSamplingState sampling; + std::uint64_t clearGeneration = 0u; std::shared_ptr owned; private: @@ -1083,11 +1112,28 @@ void NoteMemoryValueFill(const MemoryValueFill& fill) { NoteDepthMetadataFill(fill.address, fill.bytes, value); } -void NoteHtileDepthClear(std::uint64_t htileAddress, std::size_t bytes) { +std::size_t HtileDepthClearBytes_nid_no_patch(std::span code, std::span userData, const ShaderRecompiler::ShaderComputeStageInfo& compute, std::span packet, std::uint64_t indirectArguments) { + if (userData.size() != 6u || packet.size() < 5u || indirectArguments != 0u || compute.PartialGroups() || !compute.groupIdEnable[0] || compute.threadIdComponentCount == 0u || compute.threadIdComponentCount > 3u) return 0u; + if (packet[1] == 0u || packet[2] != 1u || packet[3] != 1u || (packet[4] & 0x23u) != 1u) return 0u; + const auto address = HtileDepthClearAddress(code, userData, compute.numThreads); + if (address == 0u || (address & 15u) != 0u) return 0u; + const auto bytes = static_cast(packet[1]) * 1024u; + return address <= std::numeric_limits::max() - bytes ? static_cast(bytes) : 0u; +} + +void NoteHtileDepthClear(std::uint64_t htileAddress, std::size_t bytes, std::size_t nativeBytes) { std::lock_guard lock(surfacesMutex()); bool existing = false; for (const auto& surface : surfaces()) { if (!surface->active->load(std::memory_order_acquire)) continue; + if (surface->samplingActive && surface->target.htileAddress == htileAddress) { + const auto& metadata = surface->sampling.initial[1]; + if (nativeBytes != bytes || nativeBytes < metadata.bytes || !GuestMemory::Accessible(reinterpret_cast(metadata.address), metadata.bytes) || !GuestMemory::Watched(metadata.address, metadata.bytes)) surface->sampling.refusal = "native multisample depth incomplete or unproven fast-clear transition is not implemented"; + else if (surface->clearGeneration != 0u) surface->sampling.refusal = "overlapping native multisample depth clear transitions are not implemented"; + else surface->clearGeneration = GuestMemory::CollectWritesUncached(metadata.address, metadata.bytes); + existing = true; + continue; + } if (!HtileFillCovers(surface->target.htileAddress, surface->target.extent, htileAddress, bytes)) continue; if (surface->target.samples != 1u) surface->sampling.refusal = "native multisample depth fast-clear transition is not implemented"; surface->pendingClear |= VK_IMAGE_ASPECT_DEPTH_BIT; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp index 0c43b550c3..06ef301547 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp @@ -689,7 +689,7 @@ void CheckDepthTarget(Context context, VkFormat format, std::uint32_t fragments, raster.Run(batch.Handle(), static_cast(iteration) / 16.0f); batch.SubmitAndWait(); } - NoteHtileDepthClear(target.htileAddress, static_cast(metadataBytes)); + NoteHtileDepthClear(target.htileAddress, static_cast(metadataBytes), static_cast(metadataBytes)); AgcDriver::GuestMemory::StoreOwnBytes(target.htileAddress, static_cast(metadataBytes), [&] { for (std::size_t i = 0u; i < metadataBytes / 4u; ++i) std::memcpy(metadata.Data() + i * 4u, &pattern, 4u); }); From 764f0ace02c5e7daf67b3e43d09f157a29189054 Mon Sep 17 00:00:00 2001 From: Romans Pokrovskis Date: Wed, 14 Oct 2026 02:31:26 +0300 Subject: [PATCH 0277/1000] test(agc): exercise native HTILE producers and clear refusals --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 4 + .../execution/NativeMultisampleDepth.cpp | 77 ++++++++++++++++++- docs/dev/debt/libSceAgcDriver.md | 3 +- 3 files changed, 79 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index cdec1eeb14..2cf6fadeab 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1527,8 +1527,12 @@ if(BUILD_TESTING) set_tests_properties(agc_driver_native_multisample_depth PROPERTIES TIMEOUT 30) add_test(NAME agc_driver_native_depth_retirement COMMAND agc_driver_native_multisample_depth_tests --retirement) set_tests_properties(agc_driver_native_depth_retirement PROPERTIES TIMEOUT 30 ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + add_test(NAME agc_driver_native_depth_clears COMMAND agc_driver_native_multisample_depth_tests --clears) + add_test(NAME agc_driver_native_depth_clear_refusals COMMAND agc_driver_native_multisample_depth_tests --clear-refusals) + set_tests_properties(agc_driver_native_depth_clears agc_driver_native_depth_clear_refusals PROPERTIES TIMEOUT 30 ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") if(WIN32) set_tests_properties(agc_driver_native_depth_retirement PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + set_tests_properties(agc_driver_native_depth_clears agc_driver_native_depth_clear_refusals PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() set_tests_properties(agc_sparse_buffer_holes PROPERTIES TIMEOUT 30) set_tests_properties(agc_driver_sparse_buffer_holes PROPERTIES TIMEOUT 30) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp index 06ef301547..845b511ae3 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp @@ -600,7 +600,50 @@ void CheckDrawFeedback() { std::puts("Native depth feedback refused through the draw resource path"); } -void CheckDepthTarget(Context context, VkFormat format, std::uint32_t fragments, bool one, bool clearing = false, std::optional depthOnlyOverride = std::nullopt) { +enum class ClearFault { None, PartialStore, CpuWrite, DepthWrite, BadMetadata, UnprovenDispatch, PendingWrite, Overlap }; + +void CheckClearDispatch() { + const std::array code{ + 0xd7460000, 0x04010c06, 0x34000084, 0xdc388000, 0x04020000, + 0xbf8c3f70, 0xd7710008, 0x00120805, 0xd7710009, 0x00120a05, + 0xd771000a, 0x00120c05, 0xd771000b, 0x00120e05, + 0xdc788000, 0x00020800, 0xbf810000, + }; + const std::array userData{0u, 0u, 65536u, 0u, 0xf0u, 0xf0u}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{64u, 1u, 1u}, 0u, {true, false, false}, false, 1u}; + const std::array packet{0u, 32u, 1u, 1u, 1u}; + Require(HtileDepthClearBytes_nid_no_patch(code, userData, compute, packet, 0u) == 32768u, "complete HTILE mask dispatch was not recognized"); + Require(HtileDepthClearBytes_nid_no_patch(code, userData, compute, packet, 65536u) == 0u, "indirect HTILE clear was accepted"); + Require(HtileDepthClearBytes_nid_no_patch(code, userData, compute, std::span(packet).first(4u), 0u) == 0u, "short HTILE dispatch packet was accepted"); + Require(HtileDepthClearBytes_nid_no_patch(code, std::span(userData).first(5u), compute, packet, 0u) == 0u, "short HTILE user data was accepted"); + auto changedCode = code; + changedCode[0] ^= 1u; + Require(HtileDepthClearBytes_nid_no_patch(changedCode, userData, compute, packet, 0u) == 0u, "another HTILE producer was accepted"); + for (std::uint32_t fault = 0u; fault < 14u; ++fault) { + auto changedCompute = compute; + auto changedPacket = packet; + auto changedUserData = userData; + switch (fault) { + case 0u: changedCompute.partialThreads[0] = 1u; break; + case 1u: changedCompute.groupIdEnable[0] = false; break; + case 2u: changedCompute.threadIdComponentCount = 0u; break; + case 3u: changedCompute.threadIdComponentCount = 4u; break; + case 4u: changedCompute.numThreads[0] = 32u; break; + case 5u: changedCompute.numThreads[1] = 2u; break; + case 6u: changedPacket[1] = 0u; break; + case 7u: changedPacket[2] = 2u; break; + case 8u: changedPacket[3] = 2u; break; + case 9u: changedPacket[4] = 0u; break; + case 10u: changedPacket[4] = 0x21u; break; + case 11u: changedUserData[2] = 65537u; break; + case 12u: changedUserData[4] = 0u; break; + case 13u: changedUserData[2] = 0xfffffff0u; changedUserData[3] = 0xffffffffu; break; + } + Require(HtileDepthClearBytes_nid_no_patch(code, changedUserData, changedCompute, changedPacket, 0u) == 0u, "unproven HTILE mask dispatch was accepted"); + } +} + +void CheckDepthTarget(Context context, VkFormat format, std::uint32_t fragments, bool one, bool clearing = false, std::optional depthOnlyOverride = std::nullopt, ClearFault fault = ClearFault::None) { const VkExtent2D extent{5u, 3u}; const auto bytes = static_cast(MultisampledDepthBytes_nid_no_patch(extent, format == VK_FORMAT_D16_UNORM ? 2u : 4u, fragments)); TargetMemory memory(bytes), metadata(65536u); @@ -689,10 +732,21 @@ void CheckDepthTarget(Context context, VkFormat format, std::uint32_t fragments, raster.Run(batch.Handle(), static_cast(iteration) / 16.0f); batch.SubmitAndWait(); } - NoteHtileDepthClear(target.htileAddress, static_cast(metadataBytes), static_cast(metadataBytes)); - AgcDriver::GuestMemory::StoreOwnBytes(target.htileAddress, static_cast(metadataBytes), [&] { - for (std::size_t i = 0u; i < metadataBytes / 4u; ++i) std::memcpy(metadata.Data() + i * 4u, &pattern, 4u); + const bool clearOne = iteration == 1u ? !one : one; + target.clearDepth = clearOne ? 1.0f : 0.0f; + Require(DepthSurfaceView(context, target) == view, "updating the native clear value replaced its attachment"); + const std::uint32_t clearPattern = clearOne ? (depthOnly ? 0xfffffff0u : 0xfffc00f0u) : 0u; + const auto nativeBytes = fault == ClearFault::UnprovenDispatch ? 0u : static_cast(metadataBytes); + NoteHtileDepthClear(target.htileAddress, static_cast(metadataBytes), nativeBytes); + if (fault == ClearFault::Overlap) NoteHtileDepthClear(target.htileAddress, static_cast(metadataBytes), nativeBytes); + const auto storedBytes = fault == ClearFault::PartialStore ? 4u : static_cast(metadataBytes); + AgcDriver::GuestMemory::StoreOwnBytes(target.htileAddress, storedBytes, [&] { + for (std::size_t i = 0u; i < storedBytes / 4u; ++i) std::memcpy(metadata.Data() + i * 4u, &clearPattern, 4u); + if (fault == ClearFault::BadMetadata) metadata.Data()[0] = std::byte{0x0f}; }); + if (fault == ClearFault::CpuWrite) metadata.Data()[metadataBytes - 1u] ^= std::byte{0x01}; + if (fault == ClearFault::DepthWrite) memory.Data()[bytes - 1u] ^= std::byte{0x01}; + if (fault == ClearFault::PendingWrite) QueueTargetByteWrite(context, recorder, target.htileAddress, std::byte{0}); Require(DepthSurfaceHolds(context, DecodeTextureResource(words), texture.get()), "completed native depth clear lost its sampled view"); recorder.Sync(); Require(DepthSurfaceView(context, target) == view && CachedSampledTexture(context, words) == texture, "completed native depth clear replaced an attachment or cached view"); @@ -1010,7 +1064,22 @@ int main(int argc, char** argv) { const auto context = device->GetContext(); const std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); Require(DepthSupported(context, VK_FORMAT_D32_SFLOAT, 4u), "the native depth regression requires sampled D32 with four samples"); + if (argc == 2 && std::string_view(argv[1]) == "--clear-refusals") { + for (const auto& [fault, expected] : std::array{ + std::pair{ClearFault::PartialStore, "no complete tracked HTILE store"}, + std::pair{ClearFault::CpuWrite, "CPU writes raced"}, + std::pair{ClearFault::DepthWrite, "guest writes"}, + std::pair{ClearFault::BadMetadata, "does not prove its clear value"}, + std::pair{ClearFault::UnprovenDispatch, "incomplete or unproven"}, + std::pair{ClearFault::PendingWrite, "external GPU writes"}, + std::pair{ClearFault::Overlap, "overlapping native multisample depth"}}) { + TargetRefuses([&] { CheckDepthTarget(context, VK_FORMAT_D32_SFLOAT, 4u, false, true, false, fault); }, expected); + std::printf("Native depth clear fault=%u refused: %s\n", static_cast(fault), expected); + } + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--clears") { + CheckClearDispatch(); std::uint32_t checked = 0u, unavailable = 0u; for (const auto format : {VK_FORMAT_D16_UNORM, VK_FORMAT_D32_SFLOAT}) { for (const auto samples : {2u, 4u, 8u}) { diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index afd6b162d7..9d39702700 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -3,9 +3,10 @@ ### Unknown function info - [Inactive vertex point size](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) - `PA_CL_VS_OUT_CNTL.USE_VTX_POINT_SIZE` is accepted for lines, triangles and rectangles with `GS_EN` and `HS_EN` clear, when polygon mode is disabled or both faces are filled. Points, patches and absent routing or rasterization registers still throw. Bit 16 selects the misc export's X component as PointSize ([AMD CIK register reference](https://www.amd.com/content/dam/amd/en/documents/radeon-tech-docs/programmer-references/CIK_3D_registers_v2.pdf), PA_CL_VS_OUT_CNTL); ignoring it for non-point rasterization follows [Vulkan PointSize](https://docs.vulkan.org/refpages/latest/refpages/source/PointSize.html). This is inferred, not measured on console hardware. XashPS5 (PPSA19111 v1.3) sets `0x01210000` on a filled triangle draw. +- [Native multisampled depth clears](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) accept the recognized HTILE mask kernel's direct full-range dispatch after its GPU writes complete. Every metadata byte needs tracked driver-store coverage since notification, no intervening CPU or depth write, and HTILE words proving the current clear value of zero or one. Partial, overlapping, indirect and unproven producers remain unsupported. Existing sampled views retain the native image; guest depth publication and metadata-fill transitions remain unimplemented. Windows shared imports retain write tracking through separate aliases; private host imports that lose tracking and copied or mirrored HTILE uploads remain unsupported. Clear-word encoding and console equivalence remain unmeasured on PS5 hardware. - [Native depth allocation retirement](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) covers sampling-eligible multisampled owners only. Successful release notifications must cumulatively cover both complete depth and HTILE ranges; protection changes, failed releases and partial coverage keep the original guards. Retired owners leave lookup and metadata routing, while held views and recorded work retain their images. Retirement of single-sample or sampling-ineligible depth attachments and console equivalence remain unimplemented or unmeasured. The regression uses real allocation-registry operations with a no-op host-release callback, so it does not validate the host unmap backend. - [Native MSAA depth attachment feedback](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) is not implemented. A draw sampling a native multisampled depth view while attaching its source image throws before pipeline creation or render-pass recording, including read-only attachment feedback. Sampling that image in a separate draw or dispatch remains supported. The refusal compares the held native image views and their device and attachment identity; single-sample depth behavior is retained. -- [Native multisampled depth sampling](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) accepts numeric D16 or D32, two/four/eight samples, one Z_X slice and mip, and pipe-aligned ITERATE_256 HTILE without stencil or VRS. Sampling eligibility is recorded before the attachment's first host clear: every HTILE word must prove the same initial clear of zero or one, and the complete sample-aware depth and HTILE ranges must be accessible and write-watched. Unsupported initial state or device sampling support retains attachment rendering but sampling throws; eligibility is never retried. Sampling rejects later guest writes, queued external GPU writes, and HTILE clear/fill transitions. Guest publication, arbitrary guest depth import, stencil/raw-bit sampling, arrays and storage access remain unimplemented. The block footprint follows main's AMD Addrlib thin-block rule; the accepted HTILE clear words and PS5 equivalence are not measured on console hardware. PPSA99902's historical resident eight-sample D32 reader is documented in [#419](https://github.com/boykopovar/AnyPS5/pull/419#issuecomment-6105597729); the subsequent completed HTILE transition needs separate support. +- [Native multisampled depth sampling](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) accepts numeric D16 or D32, two/four/eight samples, one Z_X slice and mip, and pipe-aligned ITERATE_256 HTILE without stencil or VRS. Sampling eligibility is recorded before the attachment's first host clear: every HTILE word must prove the same initial clear of zero or one, and the complete sample-aware depth and HTILE ranges must be accessible and write-watched. Unsupported initial state or device sampling support retains attachment rendering but sampling throws; eligibility is never retried. Sampling rejects later guest writes, queued external GPU writes, and unproven HTILE clear/fill transitions. Guest publication, arbitrary guest depth import, stencil/raw-bit sampling, arrays and storage access remain unimplemented. The block footprint follows main's AMD Addrlib thin-block rule; the accepted HTILE clear words and PS5 equivalence are not measured on console hardware. PPSA99902's historical resident eight-sample D32 reader is documented in [#419](https://github.com/boykopovar/AnyPS5/pull/419#issuecomment-6105597729). - [sceAgcDriverSetSubmitValidationMode, sceAgcDriverGetSubmitValidationMode, sceAgcDriverSetSubmitValidationConfig, sceAgcDriverGetSubmitValidationConfig, sceAgcDriverSetValidationErrorOutputFrequency, sceAgcDriverGetShaderDebuggingStatus](../../../core/libs/prx/libSceAgcDriver/Unimplemented.cpp) (libSceAgcDriver) - the five validation calls return `0x8A6C1000` without reading their arguments or writing caller memory, and `sceAgcDriverGetShaderDebuggingStatus` returns 1, as [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/game_compat.c#L250-L291) reads them from Sony's libSceAgcDriver.sprx: each validation export is `mov eax, 0x8a6c1000; ret` in the copy Dragon Quest VII Reimagined bundles (0x7bb0-0x7bf0), and the debugging status is 1 in the firmware 5.50 library (0x6740). The parameters (a 32-bit mode or frequency, a mode output, a configuration pointer) follow OpenAGC and are never read. Whether later retail firmware enables submit validation or shader debugging is unverified; no title in this repository is known to call them - [Indices far past every vertex fetch](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver) - when a draw's highest index is at least 64Ki vertices past the largest per-vertex record count, every index past that count is moved to it in a copy of the indices, so the zero-padded vertex copies are not sized to the original index (PPSA04159 draws a list of `0xffffffff` with primitive restart off, which asked for 160 GiB). Each attribute fetched by vertex index reads zeros there as at the original index (out-of-bounds select 0 or 1); a draw with another out-of-bounds select keeps its indices. The vertex index the shader sees changes for the moved indices, which a shader that reads it directly would notice - [Depth clear draws](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - a draw with `DB_RENDER_CONTROL.DEPTH_CLEAR_ENABLE` is taken to write `DB_DEPTH_CLEAR` to every pixel it covers whatever the depth test, depth bounds and depth bias say: it is drawn with an always-passing depth write and the viewport depth range pinned to the clear value. The draw is rasterized as submitted, so its coverage is the draw's own. Whether the DB still applies the depth test or HiZ to such a draw is not measured From 06b787f25d5ffd19ddb07f6b6865a43040c0b72a Mon Sep 17 00:00:00 2001 From: Romans Pokrovskis Date: Wed, 14 Oct 2026 02:33:37 +0300 Subject: [PATCH 0278/1000] test(agc): refuse a mask with an uncleared HTILE key --- .../libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp index 845b511ae3..b9082dfcbc 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp @@ -636,7 +636,7 @@ void CheckClearDispatch() { case 9u: changedPacket[4] = 0u; break; case 10u: changedPacket[4] = 0x21u; break; case 11u: changedUserData[2] = 65537u; break; - case 12u: changedUserData[4] = 0u; break; + case 12u: changedUserData[4] = 1u; break; case 13u: changedUserData[2] = 0xfffffff0u; changedUserData[3] = 0xffffffffu; break; } Require(HtileDepthClearBytes_nid_no_patch(code, changedUserData, changedCompute, changedPacket, 0u) == 0u, "unproven HTILE mask dispatch was accepted"); From 9e27d0b62379cce7be9e09e11b4e0371fe1d55d1 Mon Sep 17 00:00:00 2001 From: Romans Pokrovskis Date: Wed, 14 Oct 2026 03:14:16 +0300 Subject: [PATCH 0279/1000] fix(agc): bound native HTILE clears to the kernel offset span --- core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp | 2 +- .../tests/execution/NativeMultisampleDepth.cpp | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp index 53fe36b538..74838e758f 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp @@ -1114,7 +1114,7 @@ void NoteMemoryValueFill(const MemoryValueFill& fill) { std::size_t HtileDepthClearBytes_nid_no_patch(std::span code, std::span userData, const ShaderRecompiler::ShaderComputeStageInfo& compute, std::span packet, std::uint64_t indirectArguments) { if (userData.size() != 6u || packet.size() < 5u || indirectArguments != 0u || compute.PartialGroups() || !compute.groupIdEnable[0] || compute.threadIdComponentCount == 0u || compute.threadIdComponentCount > 3u) return 0u; - if (packet[1] == 0u || packet[2] != 1u || packet[3] != 1u || (packet[4] & 0x23u) != 1u) return 0u; + if (packet[1] == 0u || packet[1] > 0x400000u || packet[2] != 1u || packet[3] != 1u || (packet[4] & 0x23u) != 1u) return 0u; const auto address = HtileDepthClearAddress(code, userData, compute.numThreads); if (address == 0u || (address & 15u) != 0u) return 0u; const auto bytes = static_cast(packet[1]) * 1024u; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp index b9082dfcbc..3f5abe12c9 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp @@ -613,6 +613,12 @@ void CheckClearDispatch() { const ShaderRecompiler::ShaderComputeStageInfo compute{{64u, 1u, 1u}, 0u, {true, false, false}, false, 1u}; const std::array packet{0u, 32u, 1u, 1u, 1u}; Require(HtileDepthClearBytes_nid_no_patch(code, userData, compute, packet, 0u) == 32768u, "complete HTILE mask dispatch was not recognized"); + for (const auto groups : {0x3fffffu, 0x400000u, 0x400001u}) { + auto boundary = packet; + boundary[1] = groups; + const auto expected = groups > 0x400000u ? 0u : static_cast(groups) * 1024u; + Require(HtileDepthClearBytes_nid_no_patch(code, userData, compute, boundary, 0u) == expected, "HTILE mask extent crossed its 32-bit byte-offset bound"); + } Require(HtileDepthClearBytes_nid_no_patch(code, userData, compute, packet, 65536u) == 0u, "indirect HTILE clear was accepted"); Require(HtileDepthClearBytes_nid_no_patch(code, userData, compute, std::span(packet).first(4u), 0u) == 0u, "short HTILE dispatch packet was accepted"); Require(HtileDepthClearBytes_nid_no_patch(code, std::span(userData).first(5u), compute, packet, 0u) == 0u, "short HTILE user data was accepted"); From c9a96a548cd7e63161f069433cdfc56cbae26ac6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Thu, 15 Oct 2026 14:00:53 +0200 Subject: [PATCH 0280/1000] fix(agc): pass the native HTILE clear extent after the pipeline drain --- .../libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp index de522dba8d..366bb010ed 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp @@ -87,7 +87,8 @@ void Driver::dispatch(QueueState& queue, std::span packet, if (DrawPipeline::Active()) DrawPipeline::Queue0().Drain(DrawPipeline::DrainReason::Packet, 0x15); if (const auto htile = Graphics::HtileDepthClearAddress(std::span(snapshot.code).subspan(codeOffset), userData, compute.numThreads); htile != 0) { const auto threads = (packet[4] & 0x20u) != 0 ? static_cast(packet[1]) : static_cast(packet[1]) * compute.numThreads[0]; - Graphics::NoteHtileDepthClear(htile, static_cast(threads * 16u)); + const auto nativeBytes = Graphics::HtileDepthClearBytes_nid_no_patch(std::span(snapshot.code).subspan(codeOffset), userData, compute, packet, indirectArguments); + Graphics::NoteHtileDepthClear(htile, static_cast(threads * 16u), nativeBytes); } if (!partialLastGroup && indirectArguments == 0 && copyBuffer(queue, submission.queue, packet, std::span(snapshot.code).subspan(codeOffset), userData, compute, localDevice, address)) { pendingDispatchPhases().outcome = DispatchOutcome::CopyHle; From ad78fbcdf78d5a93f0ca3f9d79eff68adad4b38b Mon Sep 17 00:00:00 2001 From: JJenkx Date: Thu, 15 Oct 2026 08:16:22 -0400 Subject: [PATCH 0281/1000] fix(libkernel): stabilize virtual-query layout across hosts --- core/libs/SceTypes.hpp | 21 +++++++++++++-------- core/libs/tests/GuestMemory.cpp | 31 +++++++++++++++++++++++++++++++ docs/dev/debt/libkernel.md | 1 + 3 files changed, 45 insertions(+), 8 deletions(-) diff --git a/core/libs/SceTypes.hpp b/core/libs/SceTypes.hpp index 1e46d81df2..1338fe31b9 100644 --- a/core/libs/SceTypes.hpp +++ b/core/libs/SceTypes.hpp @@ -160,18 +160,23 @@ struct VirtualQueryInfo { std::uint64_t offset; std::int32_t protection; std::int32_t memory_type; - std::uint32_t is_flexible : 1; - std::uint32_t is_direct : 1; - std::uint32_t is_stack : 1; - std::uint32_t is_pooled : 1; - std::uint32_t is_committed : 1; - std::uint32_t is_gpu_prt : 1; - std::uint32_t amm_usage : 1; - std::uint32_t reserved : 1; + std::uint8_t is_flexible : 1; + std::uint8_t is_direct : 1; + std::uint8_t is_stack : 1; + std::uint8_t is_pooled : 1; + std::uint8_t is_committed : 1; + std::uint8_t is_gpu_prt : 1; + std::uint8_t amm_usage : 1; + std::uint8_t reserved : 1; char name[32]; std::uint8_t gpu_mask_id; std::uint8_t reserved2; }; +static_assert(sizeof(VirtualQueryInfo) == 72); +static_assert(alignof(VirtualQueryInfo) == 8); +static_assert(offsetof(VirtualQueryInfo, name) == 33); +static_assert(offsetof(VirtualQueryInfo, gpu_mask_id) == 65); +static_assert(offsetof(VirtualQueryInfo, reserved2) == 66); struct KernelSemaPrivate; struct KernelEventFlagPrivate; diff --git a/core/libs/tests/GuestMemory.cpp b/core/libs/tests/GuestMemory.cpp index a7460faf47..d0a859c5fe 100644 --- a/core/libs/tests/GuestMemory.cpp +++ b/core/libs/tests/GuestMemory.cpp @@ -2362,6 +2362,36 @@ static void CheckVirtualQuerySplitFlexibleRanges() { Require(sceKernelMunmap(bytes + page * 2, page) == 0); } +static void CheckVirtualQueryWireLayout() { + constexpr std::size_t page = 0x4000; + constexpr char name[] = "query wire layout"; + void* mapped = nullptr; + Require(sceKernelMapNamedFlexibleMemory(&mapped, page, 3, 0, name) == 0); + struct { + std::array before; + VirtualQueryInfo info; + std::array after; + } result{}; + result.before.fill(0xa5); + result.after.fill(0x5a); + Require(sceKernelVirtualQuery(mapped, 0, &result.info, 72) == 0); + const auto* bytes = reinterpret_cast(&result.info); + std::uintptr_t start = 0, end = 0; + std::int32_t protection = 0; + std::memcpy(&start, bytes, sizeof(start)); + std::memcpy(&end, bytes + 8, sizeof(end)); + std::memcpy(&protection, bytes + 24, sizeof(protection)); + Require(start == reinterpret_cast(mapped) && end == start + page); + Require(protection == 3 && bytes[32] == 0x11); // flexible and committed + Require(std::memcmp(bytes + 33, name, sizeof(name)) == 0); + Require(bytes[65] == 0 && bytes[66] == 0); + Require(std::all_of(bytes + 67, bytes + 72, [](auto byte) { return byte == 0; })); + Require(std::all_of(result.before.begin(), result.before.end(), [](auto byte) { return byte == 0xa5; })); + Require(std::all_of(result.after.begin(), result.after.end(), [](auto byte) { return byte == 0x5a; })); + Require(sceKernelClearVirtualRangeName(mapped, page) == 0); + Require(sceKernelMunmap(mapped, page) == 0); +} + static void CheckVirtualQueryForNonReadableGuestRange() { constexpr std::size_t page = 0x4000; auto* mapped = static_cast(mmap_nid_postfix(nullptr, page, 3, 0x1002, -1, 0)); @@ -2557,6 +2587,7 @@ int main(int argc, char** argv) { CheckVirtualQueryPartialMunmap(); CheckMunmapOverUnmappedPages(); CheckPageTableStats(); + CheckVirtualQueryWireLayout(); CheckNamedAndHintedMappings(); CheckInternalNamedFlexibleMapping(); CheckGuestModuleUsesFlexibleMemory(); diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index b93b7ccea5..f8e4dac0de 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -197,3 +197,4 @@ - [chflags](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) follows FreeBSD 11.4's [`kern_chflagsat`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/vfs_syscalls.c#L2366-L2383) and UFS's [`ufs_setattr`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/ufs/ufs/ufs_vnops.c#L509-L556) for a root caller: the path lookup reports `EFAULT`, `ENOENT` and `ENOTDIR`, a flag outside the 17 UFS stores (`VNOVAL` included) `EOPNOTSUPP`, and `SF_SNAPSHOT` `EPERM`. Neither host keeps BSD file flags, so every file is taken to have none (`stat` reports `st_flags` 0): flags 0 succeed without touching the file, and any other flag throws, naming the flags. `UF_READONLY`, `UF_HIDDEN`, `UF_SYSTEM` and `UF_ARCHIVE` are FreeBSD's names for the Windows attributes but are not mapped to them: UFS stores them without enforcing them, `FILE_ATTRIBUTE_READONLY` already carries the owner write bit of `chmod` on Windows, and Linux has no equivalent a process can set without `CAP_LINUX_IMMUTABLE`. The console's filesystem types, `EROFS` on read-only mounts and the change-time update are not modelled - [getpriority](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) follows FreeBSD 11.4's [`sys_getpriority`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_resource.c#L93-L165) for the only guest process, which is taken to run at nice 0: `PRIO_PROCESS` and `PRIO_PGRP` with 0 or its own pid (the guest is taken to lead its own process group, see `kill`) and `PRIO_USER` with 0 or its user id 0 return 0, any other id fails with `ESRCH` and any other `which` with `EINVAL`. As in FreeBSD's libc, where `getpriority` is the bare system call, errno is left unchanged on success. The nice value of a title process on the console is unverified, and the host nice of the process or of the threads `APS5_THREAD_PRIORITY=high` raises is not reported. `setpriority` accepts the same target ids (any other id fails with `ESRCH`, any other `which` with `EINVAL`), clamps the value to FreeBSD's `PRIO_MIN`-`PRIO_MAX` (-20 to 20) and stores it as the guest process's nice value, which `getpriority` returns; the host scheduling class is not changed, and whether a title process may lower the value without privilege is unverified - [rtprio_thread, pthread_setconcurrency, pthread_getconcurrency](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - `rtprio_thread` follows FreeBSD 11.4's `rtprio_thread(2)`. Every guest thread here runs under `SCHED_FIFO` or `SCHED_RR` (priority 256-767), whose rtprio class is real-time; the console's mapping from that priority to the rtprio `prio` range (0-31) is unmeasured, so `RTP_LOOKUP` and `RTP_SET` throw for such a thread rather than report an impossible priority. A thread that is not real-time reports `RTP_PRIO_NORMAL` (3) with 0, and `RTP_SET` accepts only that value. Another function fails with `EINVAL`, a null `rtprio` with `EFAULT`, a thread other than the calling one throws, and an id that is not a live guest thread with `ESRCH`. `pthread_setconcurrency` stores 0 or more and returns `EINVAL` for a negative level (the error itself, as FreeBSD's libthr does, with errno untouched); `pthread_getconcurrency` returns the level, which is a hint and changes nothing about the host's concurrency. Morrowind (`mshivam019/morrowind-ps5`) imports all three at start-up +- [sceKernelVirtualQuery](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) returns [VirtualQueryInfo](../../../core/libs/SceTypes.hpp) with a one-byte flag allocation unit so Windows and SysV hosts return the same 72-byte representation, with the name at byte 33 and GPU mask at byte 65. This preserves the existing SysV representation; the console offsets beyond the address, protection and size fields observed in Astro Bot's AGC calls are not hardware-measured. Size and offset assertions plus a raw-byte named-mapping regression guard the host ABI. From c836b3b55b65e8d5c514dbccb04487069141ed0e Mon Sep 17 00:00:00 2001 From: fogydes Date: Wed, 14 Oct 2026 13:00:03 +0530 Subject: [PATCH 0282/1000] fix(libkernel): protect app metadata state across threads and validate icon stream read --- .../libkernel/AppMetadata/src/AppMetadata.cpp | 9 +- core/libs/prx/libkernel/CMakeLists.txt | 10 ++ core/libs/tests/GuestAppIcon.cpp | 126 ++++++++++++++++++ 3 files changed, 143 insertions(+), 2 deletions(-) create mode 100644 core/libs/tests/GuestAppIcon.cpp diff --git a/core/libs/prx/libkernel/AppMetadata/src/AppMetadata.cpp b/core/libs/prx/libkernel/AppMetadata/src/AppMetadata.cpp index e5f172fcea..c2c9b2d2c6 100644 --- a/core/libs/prx/libkernel/AppMetadata/src/AppMetadata.cpp +++ b/core/libs/prx/libkernel/AppMetadata/src/AppMetadata.cpp @@ -6,12 +6,14 @@ #include #include #include +#include #include #include #include namespace { +std::mutex g_metadataMutex; bool g_titleLoaded = false; bool g_iconAttempted = false; bool g_iconLoaded = false; @@ -32,6 +34,7 @@ void copyToFixedBuffer(char* destination, std::size_t destinationSize, const std } void ensureTitleLoaded() { + std::lock_guard lock(g_metadataMutex); if (g_titleLoaded) return; const auto resolvedPath = ResolvePath_nid_no_patch(AppMetadataParamJsonGuestPath); if (!std::filesystem::exists(resolvedPath)) throw std::runtime_error("param.json not found"); @@ -68,6 +71,7 @@ const bool g_dataMounted = [] { }(); void ensureIconLoaded() { + std::lock_guard lock(g_metadataMutex); if (g_iconAttempted) return; g_iconAttempted = true; const auto resolvedPath = ResolvePath_nid_no_patch(AppMetadataIconGuestPath); @@ -76,10 +80,11 @@ void ensureIconLoaded() { if (!file.is_open()) throw std::runtime_error("failed to open icon0.png"); file.seekg(0, std::ios::end); const auto fileSize = file.tellg(); + if (file.fail() || fileSize <= 0) throw std::runtime_error("icon0.png is empty or unreadable"); file.seekg(0, std::ios::beg); std::vector bytes(static_cast(fileSize)); - if (!bytes.empty()) file.read(reinterpret_cast(bytes.data()), static_cast(bytes.size())); - if (bytes.empty()) throw std::runtime_error("icon0.png is empty"); + file.read(reinterpret_cast(bytes.data()), static_cast(bytes.size())); + if (!file || file.gcount() != static_cast(bytes.size())) throw std::runtime_error("failed to read icon0.png"); constexpr std::uint8_t PngSignature[8] = {0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A}; if (bytes.size() < 8 || std::memcmp(bytes.data(), PngSignature, sizeof(PngSignature)) != 0) throw std::runtime_error("icon0.png is not a valid PNG file"); g_iconBytes = std::move(bytes); diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index fc45573cab..129c763ea7 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -356,6 +356,16 @@ if(BUILD_TESTING) set_tests_properties(guest_kernel_is_stack PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_app_icon_tests ${LIBS_INCLUDE_DIR}/tests/GuestAppIcon.cpp) + target_include_directories(guest_app_icon_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_app_icon_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_app_icon_tests) + add_test(NAME guest_app_icon COMMAND guest_app_icon_tests) + set_tests_properties(guest_app_icon PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_app_icon PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_app_info_tests ${LIBS_INCLUDE_DIR}/tests/GuestAppInfo.cpp) target_include_directories(guest_app_info_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_app_info_tests PRIVATE libkernel libc) diff --git a/core/libs/tests/GuestAppIcon.cpp b/core/libs/tests/GuestAppIcon.cpp new file mode 100644 index 0000000000..0657ac60f2 --- /dev/null +++ b/core/libs/tests/GuestAppIcon.cpp @@ -0,0 +1,126 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libkernel/AppMetadata/include/AppMetadata.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +void Require(bool value) { + if (!value) std::abort(); +} + +int CheckNoIcon(const char* root) { + std::filesystem::current_path(root); + Require(!HasAppIcon_nid_postfix()); + bool threw = false; + try { + GetAppIconData_nid_postfix(); + } catch (const std::runtime_error& error) { + threw = std::string_view(error.what()).find("icon not available") != std::string_view::npos; + } + Require(threw); + return 0; +} + +int CheckValidIcon(const char* root) { + std::filesystem::current_path(root); + Require(HasAppIcon_nid_postfix()); + const auto icon = GetAppIconData_nid_postfix(); + Require(icon.bytes != nullptr); + Require(icon.size == 16); + Require(icon.bytes[0] == 0x89 && icon.bytes[1] == 'P' && icon.bytes[2] == 'N' && icon.bytes[3] == 'G'); + return 0; +} + +int CheckEmptyIcon(const char* root) { + std::filesystem::current_path(root); + bool threw = false; + try { + HasAppIcon_nid_postfix(); + } catch (const std::runtime_error& error) { + threw = std::string_view(error.what()).find("icon0.png is empty or unreadable") != std::string_view::npos; + } + Require(threw); + return 0; +} + +int CheckCorruptPng(const char* root) { + std::filesystem::current_path(root); + bool threw = false; + try { + HasAppIcon_nid_postfix(); + } catch (const std::runtime_error& error) { + threw = std::string_view(error.what()).find("icon0.png is not a valid PNG file") != std::string_view::npos; + } + Require(threw); + return 0; +} + +int CheckConcurrentAccess(const char* root) { + std::filesystem::current_path(root); + std::vector threads; + for (int i = 0; i < 8; ++i) { + threads.emplace_back([] { + Require(HasAppIcon_nid_postfix()); + const auto icon = GetAppIconData_nid_postfix(); + Require(icon.size == 16); + }); + } + for (auto& t : threads) t.join(); + return 0; +} + +void RunCase(const char* self, const char* mode, std::initializer_list> files) { + const auto root = std::filesystem::temp_directory_path() / ("guest_app_icon_" + std::to_string(std::random_device{}())); + std::filesystem::remove_all(root); + std::filesystem::create_directories(root / "app0" / "sce_sys"); + for (const auto& [name, content] : files) { + std::ofstream stream(root / "app0" / "sce_sys" / name, std::ios::binary); + stream.write(content.data(), static_cast(content.size())); + } + const std::string arguments = "\"" + std::string(self) + "\" " + mode + " \"" + root.string() + "\""; +#ifdef _WIN32 + const std::string command = "\"" + arguments + "\""; +#else + const std::string command = arguments; +#endif + const int status = std::system(command.c_str()); + std::filesystem::remove_all(root); + Require(status == 0); +} + +} + +int main(int argc, char** argv) { + if (argc == 3) { + const std::string_view mode = argv[1]; + if (mode == "--no-icon") return CheckNoIcon(argv[2]); + if (mode == "--valid-icon") return CheckValidIcon(argv[2]); + if (mode == "--empty-icon") return CheckEmptyIcon(argv[2]); + if (mode == "--corrupt-png") return CheckCorruptPng(argv[2]); + if (mode == "--concurrent") return CheckConcurrentAccess(argv[2]); + return 1; + } + + const std::string validPng("\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00", 16); + const std::string corruptPng("\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f", 16); + const std::string emptyPng(""); + + RunCase(argv[0], "--no-icon", {}); + RunCase(argv[0], "--valid-icon", {{"icon0.png", validPng}}); + RunCase(argv[0], "--empty-icon", {{"icon0.png", emptyPng}}); + RunCase(argv[0], "--corrupt-png", {{"icon0.png", corruptPng}}); + RunCase(argv[0], "--concurrent", {{"icon0.png", validPng}}); + + return 0; +} From 54633848acd89ea1ed77bbd9243712119b4d44e6 Mon Sep 17 00:00:00 2001 From: GalaxyGamingFR <153150716+GalaxyGamingFR@users.noreply.github.com> Date: Wed, 14 Oct 2026 01:45:08 -0700 Subject: [PATCH 0283/1000] fix(tools): keep loose-list continuations with their debt entry Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_012zyXkUzTZGh3nBwNMtrLaW --- tools/debt_split.py | 7 +++++-- tools/tests/test_debt_split.py | 13 +++++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/tools/debt_split.py b/tools/debt_split.py index 5c27562a4e..512a69f35e 100644 --- a/tools/debt_split.py +++ b/tools/debt_split.py @@ -17,6 +17,7 @@ def parse(text): items = [] section = None current = None + blanks = [] for line in text.splitlines(): heading = HEADING.match(line) if heading: @@ -26,13 +27,15 @@ def parse(text): elif line.startswith("- "): current = ["entry", section, [line]] items.append(current) + blanks = [] elif current and (line.startswith(" ") or line.startswith("\t")): - current[2].append(line) + current[2] += blanks + [line] + blanks = [] elif line.strip(): current = None items.append(["text", section, [line]]) else: - current = None + blanks.append(line) return [(kind, section, "\n".join(lines)) for kind, section, lines in items] diff --git a/tools/tests/test_debt_split.py b/tools/tests/test_debt_split.py index 8ffc27c4ca..003d86b50d 100644 --- a/tools/tests/test_debt_split.py +++ b/tools/tests/test_debt_split.py @@ -182,6 +182,19 @@ class DebtSplitTests(unittest.TestCase): "\n".join(entry.replace("../../", "../../../") for entry in (first, second, A_STUB)) + "\n\n### Functional\n\n" + A_FUNCTIONAL.replace("../../", "../../../") + "\n") self.assertEqual(self.read(INDEX), self.git("show", "main:" + INDEX).stdout) + def test_indented_continuation_after_a_blank_line_moves_with_its_entry(self): + note = " - sub note about sceAOne" + self.write(INDEX, "# Project technical debt\n\n### Silent stubs\n\n" + A_STUB + "\n\n" + note + "\n") + self.split() + self.assertIn(note, self.read("docs/dev/debt/libSceA.md")) + self.assertNotIn(note, self.read(INDEX)) + + def test_unindented_paragraph_after_a_blank_line_stays_in_the_index(self): + paragraph = "A plain paragraph after the stubs" + self.write(INDEX, "# Project technical debt\n\n### Silent stubs\n\n" + A_STUB + "\n\n" + paragraph + "\n") + self.split() + self.assertIn(paragraph, self.read(INDEX)) + self.assertNotIn(paragraph, self.read("docs/dev/debt/libSceA.md")) if __name__ == "__main__": From 1f128cc0b5f673151ea1c436ac10f8a5e75bb552 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Thu, 15 Oct 2026 14:16:54 +0200 Subject: [PATCH 0284/1000] style(tools): blank line between tests Co-Authored-By: Claude --- tools/tests/test_debt_split.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tools/tests/test_debt_split.py b/tools/tests/test_debt_split.py index 003d86b50d..e207c496ad 100644 --- a/tools/tests/test_debt_split.py +++ b/tools/tests/test_debt_split.py @@ -182,6 +182,7 @@ class DebtSplitTests(unittest.TestCase): "\n".join(entry.replace("../../", "../../../") for entry in (first, second, A_STUB)) + "\n\n### Functional\n\n" + A_FUNCTIONAL.replace("../../", "../../../") + "\n") self.assertEqual(self.read(INDEX), self.git("show", "main:" + INDEX).stdout) + def test_indented_continuation_after_a_blank_line_moves_with_its_entry(self): note = " - sub note about sceAOne" self.write(INDEX, "# Project technical debt\n\n### Silent stubs\n\n" + A_STUB + "\n\n" + note + "\n") From da1a7393685e9cdf2e70ae5df3de75ca442a1d68 Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Wed, 14 Oct 2026 07:35:19 +0000 Subject: [PATCH 0285/1000] test(agc): cover repeated untracked storage writes after sampling --- .../tests/execution/StoreOverUntracked.cpp | 31 ++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/StoreOverUntracked.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/StoreOverUntracked.cpp index 3431424ec1..f98e62ee55 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/StoreOverUntracked.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/StoreOverUntracked.cpp @@ -60,6 +60,10 @@ alignas(256) constexpr std::array FillCode{ 0xd7460004, 0x04010c08, 0x7e000204, 0x7e020205, 0x7e040206, 0x7e060207, 0xe01c2000, 0x80000004, 0xbf810000, }; +alignas(256) constexpr std::array ReadCode{ + 0x7e020280, 0x7e04020c, 0xf0001108, 0x00010300, 0xbf8c3f70, 0x34080082, 0xe0701000, 0x80000304, 0xbf810000, +}; + std::uint64_t AddressOf(const void* data) { return static_cast(reinterpret_cast(data)); } @@ -120,6 +124,20 @@ void Fill(const std::uint32_t* texels) { Dispatch(FillCode, 64, (static_cast(userData.size()) << 1u) | 0x80u, userData, static_cast(SurfaceBytes / 1024u)); } +std::array BufferDescriptor(const void* data, std::size_t bytes) { + const auto address = AddressOf(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), static_cast(bytes), 0x31016facu}; +} + +void ReadImage(const std::uint32_t* texels, std::uint32_t* output) { + std::array userData{}; + const auto buffer = BufferDescriptor(output, SurfaceBytes); + const auto texture = TextureDescriptor(texels); + std::copy(buffer.begin(), buffer.end(), userData.begin()); + std::copy(texture.begin(), texture.end(), userData.begin() + 4); + Dispatch(ReadCode, 32, static_cast(userData.size()) << 1u, userData, 1); +} + class Surface { public: explicit Surface(bool watched = false, std::size_t lead = 0) : watched(watched), regionBytes(lead == 0 ? SurfaceBytes : SurfaceBytes + Block) { @@ -217,7 +235,7 @@ void Settle(THold& hold, const Surface& surface, std::uint32_t result) { hold.unlock(); } -enum class Kind { ReadAfter, ReadBefore, Filled, Copied, LeftWatch, WrittenThenLeftWatch, ImportLeftWatch, NeighbourCopied, NeighbourLeftWatch }; +enum class Kind { ReadAfter, ReadBefore, Filled, Copied, LeftWatch, WrittenThenLeftWatch, ImportLeftWatch, NeighbourCopied, NeighbourLeftWatch, UntrackedRepeat }; // One 64 KiB block of the surface: the image of its last block, and the source copied into it. constexpr std::uint32_t BlockSide = 128; @@ -313,6 +331,16 @@ Outcome Run(const Case& test) { Settle(hold, *source, Filled); } Store(surface.Texels()); + if (test.kind == Kind::UntrackedRepeat) { + Settle(hold, surface, Stored); + Surface output; + ReadImage(surface.Texels(), output.Texels()); + hold.lock(); + const auto image = StorageTexture::FindLive(surface.Address(), SurfaceBytes); + Require(image != nullptr, "store over untracked: the sampled surface has no live storage image"); + image->MarkDirty(); + hold.unlock(); + } if (test.kind == Kind::Filled || leaves) { Settle(hold, surface, Stored); if (leaves) { @@ -438,6 +466,7 @@ int main() { {"a surface whose import left the write watch", Kind::ImportLeftWatch, 0xe000}, {"an image copy into another block of the surface", Kind::NeighbourCopied, 0xe000}, {"a surface whose first block left the write watch with the page before it", Kind::NeighbourLeftWatch, 0xe000}, + {"untracked storage results written again after an image read", Kind::UntrackedRepeat, 0xe000}, }}; bool kept = true; for (const auto& test : cases) { From 9c765fa0fa7a2cde10042d13f16e80d2ca14df9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Thu, 15 Oct 2026 14:11:00 +0200 Subject: [PATCH 0286/1000] test(agc): keep the sampled output surface alive until the queued read finishes --- .../libSceAgcDriver/tests/execution/StoreOverUntracked.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/StoreOverUntracked.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/StoreOverUntracked.cpp index f98e62ee55..3ec2340226 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/StoreOverUntracked.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/StoreOverUntracked.cpp @@ -312,6 +312,7 @@ Outcome RunSecondStore(const char* name) { } Outcome Run(const Case& test) { + std::optional output; std::optional source; if (test.kind == Kind::Copied || test.kind == Kind::NeighbourCopied) source.emplace(); const bool neighbour = test.kind == Kind::NeighbourLeftWatch; @@ -333,8 +334,8 @@ Outcome Run(const Case& test) { Store(surface.Texels()); if (test.kind == Kind::UntrackedRepeat) { Settle(hold, surface, Stored); - Surface output; - ReadImage(surface.Texels(), output.Texels()); + output.emplace(); + ReadImage(surface.Texels(), output->Texels()); hold.lock(); const auto image = StorageTexture::FindLive(surface.Address(), SurfaceBytes); Require(image != nullptr, "store over untracked: the sampled surface has no live storage image"); From d17895ed846bf1a5804443b1281b60d85e561c2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Thu, 15 Oct 2026 14:26:21 +0200 Subject: [PATCH 0287/1000] test(agc): size the case table for the neighbour case added on main --- .../prx/libSceAgcDriver/tests/execution/StoreOverUntracked.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/StoreOverUntracked.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/StoreOverUntracked.cpp index 3ec2340226..63fbbb4539 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/StoreOverUntracked.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/StoreOverUntracked.cpp @@ -457,7 +457,7 @@ int main() { Surface probe; Require(AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::DecodeTextureResource(TextureDescriptor(probe.Texels()))).guestBytes == SurfaceBytes, "store over untracked: unexpected surface size"); } - constexpr std::array cases{{ + constexpr std::array cases{{ {"another block read after the CPU write", Kind::ReadAfter, Block + Page}, {"another block read before the CPU write", Kind::ReadBefore, 0xe000}, {"a fill of the surface", Kind::Filled, 0xe000}, From 939b3d8a9fa79900e34b4868bc4dcef338d164ef Mon Sep 17 00:00:00 2001 From: GalaxyGamingFR <153150716+GalaxyGamingFR@users.noreply.github.com> Date: Wed, 14 Oct 2026 02:48:57 -0700 Subject: [PATCH 0288/1000] fix(conventions): check comments in GLSL shaders The comment rule only scanned C++, Python and CMake files, so a comment added to a .comp, .frag or .vert shader passed the check. TextureDetile.comp and ColorTransfer.comp carry such comments today. Shaders are now masked with the same C-style lexer and report comment on changed lines, with the same #endif allowance and the same technical-debt exemption as C++. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_012zyXkUzTZGh3nBwNMtrLaW --- tools/check_conventions.py | 11 +++++++++++ tools/tests/test_check_conventions_comments.py | 14 ++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/tools/check_conventions.py b/tools/check_conventions.py index 45b427a200..2e06a65a24 100644 --- a/tools/check_conventions.py +++ b/tools/check_conventions.py @@ -191,6 +191,16 @@ class Check: if match and not match.group(1).startswith("3rdparty/"): self.report("system-dependency", ".gitmodules", number, match.group(1)) + def shaders(self): + if any(is_debt(path) for path in self.status): + return + for path, lines in self.files(lambda p: p.suffix in (".comp", ".frag", ".vert")): + masked = cpp_text(self.show(path), preserve_comments=True).split("\n") + for number, _ in lines: + bare = masked[number - 1] + if ("//" in bare or "/*" in bare) and not ALLOWED_COMMENT.match(bare): + self.report("comment", path, number) + def submodules(self): result = subprocess.run(["git", "config", "--no-includes", "-z", "--blob", f"{self.head}:.gitmodules", "--list"], capture_output=True, encoding="utf-8", errors="replace") @@ -456,6 +466,7 @@ if __name__ == "__main__": check = Check(args.base, args.head) check.code() check.build() + check.shaders() check.repository() check.docs() check.workflows() diff --git a/tools/tests/test_check_conventions_comments.py b/tools/tests/test_check_conventions_comments.py index 86ecc8f605..9f72e1a16d 100644 --- a/tools/tests/test_check_conventions_comments.py +++ b/tools/tests/test_check_conventions_comments.py @@ -362,6 +362,20 @@ class TestFixtureExportTests(unittest.TestCase): self.assertIn("core/libs/prx/libSceAudioOut/src/Clock.cpp:1: error: [duplicate-export]", result.stdout) self.assertIn("sceKernelGetProcessTime is also in libkernel", result.stdout) + def test_comment_in_a_shader_is_reported(self): + for path in ("core/libs/prx/libSceAgcDriver/Graphics/shaders/Fill.comp", "core/libs/prx/libSceAgcDriver/tests/shaders/Fill.frag"): + for line in (" // fill the target", " /* fill the target */"): + with self.subTest(path=path, line=line): + result = self.check(path, f"#version 450\nvoid main() {{\n{line}\n}}\n") + self.assertEqual(result.returncode, 1, result.stdout + result.stderr) + self.assertIn(f"{path}:3: error: [comment]", result.stdout) + + def test_shader_without_comments_passes(self): + result = self.check("core/libs/prx/libSceAgcDriver/Graphics/shaders/Fill.vert", + '#version 450\n#define TEXT "a//b"\n#ifdef X\n#endif // X\nvoid main() {}\n') + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertIn("All conventions checks passed", result.stdout) + class PullRequestTemplateTests(unittest.TestCase): def body_findings(self, body): From 1b94074869f20a8d283fa1d8baaa9eccebdddbd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Thu, 15 Oct 2026 14:39:47 +0200 Subject: [PATCH 0289/1000] fix(conventions): also check comments in .glsl shaders Co-Authored-By: Claude --- tools/check_conventions.py | 2 +- tools/tests/test_check_conventions_comments.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/check_conventions.py b/tools/check_conventions.py index 2e06a65a24..000a1de445 100644 --- a/tools/check_conventions.py +++ b/tools/check_conventions.py @@ -194,7 +194,7 @@ class Check: def shaders(self): if any(is_debt(path) for path in self.status): return - for path, lines in self.files(lambda p: p.suffix in (".comp", ".frag", ".vert")): + for path, lines in self.files(lambda p: p.suffix in (".comp", ".frag", ".vert", ".glsl")): masked = cpp_text(self.show(path), preserve_comments=True).split("\n") for number, _ in lines: bare = masked[number - 1] diff --git a/tools/tests/test_check_conventions_comments.py b/tools/tests/test_check_conventions_comments.py index 9f72e1a16d..fe186dc114 100644 --- a/tools/tests/test_check_conventions_comments.py +++ b/tools/tests/test_check_conventions_comments.py @@ -363,7 +363,7 @@ class TestFixtureExportTests(unittest.TestCase): self.assertIn("sceKernelGetProcessTime is also in libkernel", result.stdout) def test_comment_in_a_shader_is_reported(self): - for path in ("core/libs/prx/libSceAgcDriver/Graphics/shaders/Fill.comp", "core/libs/prx/libSceAgcDriver/tests/shaders/Fill.frag"): + for path in ("core/libs/prx/libSceAgcDriver/Graphics/shaders/Fill.comp", "core/libs/prx/libSceAgcDriver/tests/shaders/Fill.frag", "core/libs/prx/libSceAgcDriver/Graphics/shaders/Fill.glsl"): for line in (" // fill the target", " /* fill the target */"): with self.subTest(path=path, line=line): result = self.check(path, f"#version 450\nvoid main() {{\n{line}\n}}\n") From 33d93315f7b6215ee6c58d9b46176d22c6b5c44a Mon Sep 17 00:00:00 2001 From: fogydes Date: Wed, 14 Oct 2026 14:10:44 +0530 Subject: [PATCH 0290/1000] fix(libSceRtc): use safe tick addition in local clock and validate null pointer checks --- core/libs/prx/libSceRtc/Export.cpp | 6 ++++-- core/libs/tests/GuestRtc.cpp | 13 +++++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceRtc/Export.cpp b/core/libs/prx/libSceRtc/Export.cpp index 0744be6f86..b9119c2159 100644 --- a/core/libs/prx/libSceRtc/Export.cpp +++ b/core/libs/prx/libSceRtc/Export.cpp @@ -372,8 +372,10 @@ int APS5_VABI sceRtcGetCurrentClock(RtcDateTime* time, int time_zone_minutes) { int APS5_VABI sceRtcGetCurrentClockLocalTime(RtcDateTime* time) { if (!time) return SCE_RTC_ERROR_INVALID_POINTER; - const std::uint64_t utc = currentTick(); - *time = fromTick(utc + static_cast(localOffsetSeconds(utc) * TICKS_PER_SECOND)); + RtcTick local{}; + const RtcTick utc{currentTick()}; + if (const int result = addTicks(&local, &utc, localOffsetSeconds(utc.tick), TICKS_PER_SECOND); result != 0) return result; + *time = fromTick(local.tick); return 0; } diff --git a/core/libs/tests/GuestRtc.cpp b/core/libs/tests/GuestRtc.cpp index 96273f2b73..0cfa49baa9 100644 --- a/core/libs/tests/GuestRtc.cpp +++ b/core/libs/tests/GuestRtc.cpp @@ -17,6 +17,8 @@ int APS5_VABI sceRtcGetTickResolution(void); int APS5_VABI sceRtcGetTick(const RtcDateTime*, RtcTick*); int APS5_VABI sceRtcSetTick(RtcDateTime*, const RtcTick*); int APS5_VABI sceRtcGetCurrentTick(RtcTick*); +int APS5_VABI sceRtcGetCurrentClock(RtcDateTime*, int); +int APS5_VABI sceRtcGetCurrentClockLocalTime(RtcDateTime*); int APS5_VABI sceRtcConvertUtcToLocalTime(const RtcTick*, RtcTick*); int APS5_VABI sceRtcConvertLocalTimeToUtc(const RtcTick*, RtcTick*); int APS5_VABI sceRtcGetTime_t(const RtcDateTime*, std::int64_t*); @@ -71,8 +73,12 @@ int main() { Require(sceRtcGetTickResolution() == 1000000); Require(sceRtcIsLeapYear(2000) == 1 && sceRtcIsLeapYear(1900) == 0 && sceRtcIsLeapYear(2024) == 1); Require(sceRtcIsLeapYear(0) == invalidYear); + Require(sceRtcIsLeapYear(10000) == 1); + Require(sceRtcIsLeapYear(9999) == 0 && sceRtcIsLeapYear(9996) == 1); Require(sceRtcGetDaysInMonth(2023, 2) == 28 && sceRtcGetDaysInMonth(2024, 2) == 29 && sceRtcGetDaysInMonth(2024, 4) == 30); Require(sceRtcGetDaysInMonth(2024, 13) == invalidMonth); + Require(sceRtcGetDaysInMonth(10000, 1) == 31); + Require(sceRtcGetDaysInMonth(9999, 1) == 31); Require(sceRtcGetDayOfWeek(1, 1, 1) == 1 && sceRtcGetDayOfWeek(2026, 9, 26) == 6); Require(sceRtcGetDayOfWeek(2023, 2, 29) == invalidDay); @@ -98,6 +104,13 @@ int main() { tick.tick = maxTick + 1; Require(sceRtcSetTick(&converted, &tick) == invalidValue); + Require(sceRtcGetCurrentClock(nullptr, 0) == invalidPointer); + Require(sceRtcGetCurrentClock(&converted, 0) == 0 && sceRtcCheckValid(&converted) == 0); + Require(sceRtcGetCurrentClock(&converted, 60) == 0 && sceRtcCheckValid(&converted) == 0); + Require(sceRtcGetCurrentClock(&converted, -300) == 0 && sceRtcCheckValid(&converted) == 0); + Require(sceRtcGetCurrentClockLocalTime(nullptr) == invalidPointer); + Require(sceRtcGetCurrentClockLocalTime(&converted) == 0 && sceRtcCheckValid(&converted) == 0); + RtcDateTime epoch{1970, 1, 1, 0, 0, 0, 0}; std::int64_t seconds = -1; Require(sceRtcGetTime_t(&epoch, &seconds) == 0 && seconds == 0); From ceb963c6ddedec7cc92b5cdb5ed233db98e12c75 Mon Sep 17 00:00:00 2001 From: fogydes Date: Wed, 14 Oct 2026 13:08:21 +0530 Subject: [PATCH 0291/1000] fix(libc): check allocation result in vasprintf and guard null pointer in strndup --- core/libs/prx/libc/CMakeLists.txt | 9 ++ core/libs/prx/libc/src/AllocatingStrings.cpp | 1 + core/libs/prx/libc/src/Formatting.cpp | 4 + core/libs/tests/GuestAllocatingStrings.cpp | 128 +++++++++++++++++++ docs/dev/debt/libc.md | 1 + 5 files changed, 143 insertions(+) create mode 100644 core/libs/tests/GuestAllocatingStrings.cpp diff --git a/core/libs/prx/libc/CMakeLists.txt b/core/libs/prx/libc/CMakeLists.txt index 1fe621e9eb..e7b458d900 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -61,6 +61,15 @@ if(WIN32) endif() if(BUILD_TESTING) + add_test_executable(guest_allocating_strings_tests ${LIBS_INCLUDE_DIR}/tests/GuestAllocatingStrings.cpp) + target_include_directories(guest_allocating_strings_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_allocating_strings_tests PRIVATE libc) + configure_windows_unwind(guest_allocating_strings_tests) + add_test(NAME guest_allocating_strings COMMAND guest_allocating_strings_tests) + if(WIN32) + set_tests_properties(guest_allocating_strings PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_array_callbacks_tests ${LIBS_INCLUDE_DIR}/tests/GuestArrayCallbacks.cpp) target_include_directories(guest_array_callbacks_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_array_callbacks_tests PRIVATE libc) diff --git a/core/libs/prx/libc/src/AllocatingStrings.cpp b/core/libs/prx/libc/src/AllocatingStrings.cpp index 2e91cc2159..1605fbd125 100644 --- a/core/libs/prx/libc/src/AllocatingStrings.cpp +++ b/core/libs/prx/libc/src/AllocatingStrings.cpp @@ -11,6 +11,7 @@ extern "C" { int APS5_VABI vasprintf_nid_postfix(char** destination, const char* format, VaList* args); char* APS5_VABI strndup_nid_postfix(const char* source, std::size_t limit) { + if (!source) { errno = 22; return nullptr; } std::size_t length = 0; while (length < limit && source[length] != '\0') ++length; try { diff --git a/core/libs/prx/libc/src/Formatting.cpp b/core/libs/prx/libc/src/Formatting.cpp index bc08460a33..503383aa50 100644 --- a/core/libs/prx/libc/src/Formatting.cpp +++ b/core/libs/prx/libc/src/Formatting.cpp @@ -229,6 +229,10 @@ int APS5_VABI vasprintf_nid_postfix(char** destination, const char* format, VaLi const char* source = guestWide ? guestText.c_str() : text; #endif auto* output = static_cast(ApplicationHeapAllocate_nid_no_patch(static_cast(count) + 1)); + if (!output) { + errno = 12; + return -1; + } std::memcpy(output, source, static_cast(count) + 1); *destination = output; return count; diff --git a/core/libs/tests/GuestAllocatingStrings.cpp b/core/libs/tests/GuestAllocatingStrings.cpp new file mode 100644 index 0000000000..165a4515f8 --- /dev/null +++ b/core/libs/tests/GuestAllocatingStrings.cpp @@ -0,0 +1,128 @@ +#include "prx/libc/include/ApplicationHeap.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include "SceTypes.hpp" +#include +#include +#include +#include +#include +#include + +extern "C" { +char* APS5_VABI strndup_nid_postfix(const char*, std::size_t); +char* APS5_VABI strdup_nid_postfix(const char*); +int APS5_VABI asprintf_nid_postfix(char**, const char*, ...); +int APS5_VABI vasprintf_nid_postfix(char**, const char*, VaList*); +void APS5_VABI free_nid_postfix(void*); +int* APS5_VABI __error_nid_postfix(); +} + +namespace { + +void Require(bool value) { + if (!value) std::abort(); +} + +void* APS5_VABI Allocate(std::size_t size) { return std::malloc(size); } +void APS5_VABI Free(void* ptr) { std::free(ptr); } +void* APS5_VABI Reallocate(void* ptr, std::size_t size) { return std::realloc(ptr, size); } +void* APS5_VABI Calloc(std::size_t count, std::size_t size) { return std::calloc(count, size); } +void* APS5_VABI Align(std::size_t, std::size_t size) { return std::malloc(size); } +void* APS5_VABI Realign(void* ptr, std::size_t size, std::size_t) { return std::realloc(ptr, size); } +int APS5_VABI PosixAlign(void** ptr, std::size_t, std::size_t size) { + *ptr = std::malloc(size); + return *ptr ? 0 : 12; +} + +void InitializeHeap() { + alignas(16) static std::array process{}; + alignas(16) static std::array libc{}; + alignas(16) static std::array replacement{}; + const auto write = [](auto& data, std::size_t offset, auto value) { + std::memcpy(data.data() + offset, &value, sizeof(value)); + }; + write(process, 0, std::uint64_t{0x40}); + write(process, 8, std::uint32_t{0x4942524f}); + write(process, 0x38, libc.data()); + write(libc, 0, std::uint64_t{0x38}); + write(libc, 0x30, replacement.data()); + write(replacement, 0, std::uint64_t{0x78}); + write(replacement, 8, std::uint64_t{2}); + write(replacement, 0x20, reinterpret_cast(&Allocate)); + write(replacement, 0x28, reinterpret_cast(&Free)); + write(replacement, 0x30, reinterpret_cast(&Calloc)); + write(replacement, 0x38, reinterpret_cast(&Reallocate)); + write(replacement, 0x40, reinterpret_cast(&Align)); + write(replacement, 0x48, reinterpret_cast(&Realign)); + write(replacement, 0x50, reinterpret_cast(&PosixAlign)); + ApplicationHeapInitialize_nid_no_patch(process.data()); +} + +int CallVasprintf(char** destination, const char* format, ...) { +#ifdef _WIN32 + __builtin_sysv_va_list args; + __builtin_sysv_va_start(args, format); +#else + std::va_list args; + va_start(args, format); +#endif + const int result = vasprintf_nid_postfix(destination, format, reinterpret_cast(args)); +#ifdef _WIN32 + __builtin_sysv_va_end(args); +#else + va_end(args); +#endif + return result; +} + +} + +int main() { + *__error_nid_postfix() = 0; + Require(strndup_nid_postfix(nullptr, 0) == nullptr); + Require(*__error_nid_postfix() == 22); + + *__error_nid_postfix() = 0; + Require(strndup_nid_postfix(nullptr, 32) == nullptr); + Require(*__error_nid_postfix() == 22); + + *__error_nid_postfix() = 0; + Require(strdup_nid_postfix(nullptr) == nullptr); + Require(*__error_nid_postfix() == 22); + + *__error_nid_postfix() = 0; + Require(vasprintf_nid_postfix(nullptr, "format", nullptr) == -1); + Require(*__error_nid_postfix() == 22); + + char* allocated = nullptr; + *__error_nid_postfix() = 0; + Require(CallVasprintf(&allocated, nullptr) == -1); + Require(*__error_nid_postfix() == 22); + Require(allocated == nullptr); + + *__error_nid_postfix() = 0; + Require(vasprintf_nid_postfix(&allocated, "format", nullptr) == -1); + Require(*__error_nid_postfix() == 22); + Require(allocated == nullptr); + + InitializeHeap(); + + char* dup1 = strndup_nid_postfix("hello world", 5); + Require(dup1 != nullptr); + Require(std::strcmp(dup1, "hello") == 0); + free_nid_postfix(dup1); + + char* dup2 = strdup_nid_postfix("testing 1 2 3"); + Require(dup2 != nullptr); + Require(std::strcmp(dup2, "testing 1 2 3") == 0); + free_nid_postfix(dup2); + + allocated = nullptr; + const int count = asprintf_nid_postfix(&allocated, "value=%d %s", 42, "ok"); + Require(count == 11); + Require(allocated != nullptr); + Require(std::strcmp(allocated, "value=42 ok") == 0); + free_nid_postfix(allocated); + + return 0; +} diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index e92d357f7f..f565ad51ea 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -30,6 +30,7 @@ - [set_constraint_handler_s](../../../core/libs/prx/libc/src/WideConversion.cpp) (libc, re-exported by libSceAmpr) - the default handler is assumed to abort; each module keeps its own handler state, so a handler set through one library is not seen by the other. `ctime_s` and `asctime_s` follow the Dinkumware 26-byte format with `EINVAL` for a buffer under 26 bytes, a null pointer or an out-of-range weekday/month; `ctime_s` uses the guest time zone of `localtime_s`, and the console's exact checks are not verified - [__udivti3](../../../core/libs/prx/libc/src/RuntimeSupport.cpp) (libc, re-exported by libSceAmpr) - the hardware traps on a zero divisor; this throws instead - [__umodti3](../../../core/libs/prx/libc/src/RuntimeSupport.cpp) (libc) - the hardware traps on a zero divisor; this throws instead +- [strndup and strdup](../../../core/libs/prx/libc/src/AllocatingStrings.cpp) (libc) return null with `errno` set to `EINVAL` (22) when `source` is null, preventing a host segfault on invalid guest input. POSIX.1-2008 and FreeBSD 11.4 libc leave `strndup(NULL)` and `strdup(NULL)` undefined (faulting); exact console behavior on hardware is unverified. ### Functional From bc3f81c198f33943f0adb57676bd2af05c816b0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Thu, 15 Oct 2026 14:44:16 +0200 Subject: [PATCH 0292/1000] test(libc): pin vasprintf allocation failure; move debt line off the section end Co-Authored-By: Claude --- core/libs/tests/GuestAllocatingStrings.cpp | 12 +++++++++++- docs/dev/debt/libc.md | 2 +- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/core/libs/tests/GuestAllocatingStrings.cpp b/core/libs/tests/GuestAllocatingStrings.cpp index 165a4515f8..8bf053914b 100644 --- a/core/libs/tests/GuestAllocatingStrings.cpp +++ b/core/libs/tests/GuestAllocatingStrings.cpp @@ -23,7 +23,9 @@ void Require(bool value) { if (!value) std::abort(); } -void* APS5_VABI Allocate(std::size_t size) { return std::malloc(size); } +bool failAllocation = false; + +void* APS5_VABI Allocate(std::size_t size) { return failAllocation ? nullptr : std::malloc(size); } void APS5_VABI Free(void* ptr) { std::free(ptr); } void* APS5_VABI Reallocate(void* ptr, std::size_t size) { return std::realloc(ptr, size); } void* APS5_VABI Calloc(std::size_t count, std::size_t size) { return std::calloc(count, size); } @@ -124,5 +126,13 @@ int main() { Require(std::strcmp(allocated, "value=42 ok") == 0); free_nid_postfix(allocated); + failAllocation = true; + allocated = nullptr; + *__error_nid_postfix() = 0; + Require(asprintf_nid_postfix(&allocated, "value=%d", 1) == -1); + Require(*__error_nid_postfix() == 12); + Require(allocated == nullptr); + failAllocation = false; + return 0; } diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index f565ad51ea..e5f4fa441b 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -26,11 +26,11 @@ - [swprintf_s and vswprintf_s](../../../core/libs/prx/libc/src/FormattingWide.cpp) (libc) - follow C11 K.3.9.1.4 with `RSIZE_MAX` assumed to be `SIZE_MAX >> 1`: the complete output, including the terminating null, must fit in the buffer, otherwise it is a runtime-constraint violation that sets `buffer[0]` to zero and returns a negative value, and the buffer is not partially written. Argument and encoding checks match `snwprintf_s` - [vsnwprintf_s](../../../core/libs/prx/libc/src/FormattingWide.cpp) (libc) - `va_list` counterpart of `snwprintf_s` (C11 K.3.9.1.3): truncates to the buffer and returns the full length, with the same constraints and argument checks - [vsnprintf_s and vsscanf_s](../../../core/libs/prx/libc/src/Formatting.cpp) (libc) - `vsnprintf_s` forwards to `vsnprintf` semantics: the C11 K.3.7.1.5 runtime constraints (null buffer, `size == 0`, `%n`, null `%s`) are not checked, so `size == 0` returns the would-be length instead of a negative value, and no constraint handler is invoked. `vsscanf_s` returns the count of items converted before an undersized `%s`/`%c`/`%[` destination (0 when it is the first), not EOF. Neither is confirmed on a PS5 title +- [strndup and strdup](../../../core/libs/prx/libc/src/AllocatingStrings.cpp) (libc) return null with `errno` set to `EINVAL` (22) when `source` is null, preventing a host segfault on invalid guest input. POSIX.1-2008 and FreeBSD 11.4 libc leave `strndup(NULL)` and `strdup(NULL)` undefined (faulting); exact console behavior on hardware is unverified. - [localeconv](../../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc, re-exported by libSceAmpr) - the 0x90-byte Dinkumware struct (empty monetary strings, `CHAR_MAX` for the 14 `char` fields, `decimal_point` ".", `_False`/`_True` "false"/"true") is read from the `libc.prx` that Astro's Playroom ships. It is a fixed C-locale table: whether the console changes it with `setlocale` is not known, and `_Frac_grouping`, `_Frac_sep`, `_No`, `_Yes` are empty strings as in that dump - [set_constraint_handler_s](../../../core/libs/prx/libc/src/WideConversion.cpp) (libc, re-exported by libSceAmpr) - the default handler is assumed to abort; each module keeps its own handler state, so a handler set through one library is not seen by the other. `ctime_s` and `asctime_s` follow the Dinkumware 26-byte format with `EINVAL` for a buffer under 26 bytes, a null pointer or an out-of-range weekday/month; `ctime_s` uses the guest time zone of `localtime_s`, and the console's exact checks are not verified - [__udivti3](../../../core/libs/prx/libc/src/RuntimeSupport.cpp) (libc, re-exported by libSceAmpr) - the hardware traps on a zero divisor; this throws instead - [__umodti3](../../../core/libs/prx/libc/src/RuntimeSupport.cpp) (libc) - the hardware traps on a zero divisor; this throws instead -- [strndup and strdup](../../../core/libs/prx/libc/src/AllocatingStrings.cpp) (libc) return null with `errno` set to `EINVAL` (22) when `source` is null, preventing a host segfault on invalid guest input. POSIX.1-2008 and FreeBSD 11.4 libc leave `strndup(NULL)` and `strdup(NULL)` undefined (faulting); exact console behavior on hardware is unverified. ### Functional From 22102002321dcbc59fdf27536f3aebff24b7602d Mon Sep 17 00:00:00 2001 From: fogydes Date: Wed, 14 Oct 2026 14:56:19 +0530 Subject: [PATCH 0293/1000] fix(libkernel): reject unknown event flag creation attributes --- .../prx/libkernel/EventFlag/src/EventFlag.cpp | 1 + core/libs/tests/GuestKernelErrors.cpp | 80 +++++++++++++++++++ 2 files changed, 81 insertions(+) diff --git a/core/libs/prx/libkernel/EventFlag/src/EventFlag.cpp b/core/libs/prx/libkernel/EventFlag/src/EventFlag.cpp index 7d4ebe2ea3..1e52870311 100644 --- a/core/libs/prx/libkernel/EventFlag/src/EventFlag.cpp +++ b/core/libs/prx/libkernel/EventFlag/src/EventFlag.cpp @@ -66,6 +66,7 @@ int APS5_VABI sceKernelCreateEventFlag(KernelEventFlag* ef, const char* name, ui (void)param; if (!ef || !name) return SCE_KERNEL_ERROR_EINVAL; if ((attr & EVF_ATTR_SINGLE) && (attr & EVF_ATTR_MULTI)) return SCE_KERNEL_ERROR_EINVAL; + if ((attr & ~0x33u) != 0) return SCE_KERNEL_ERROR_EINVAL; auto* flag = new (std::nothrow) KernelEventFlagPrivate(); if (!flag) return SCE_KERNEL_ERROR_ENOMEM; flag->_name = name; diff --git a/core/libs/tests/GuestKernelErrors.cpp b/core/libs/tests/GuestKernelErrors.cpp index 6a76e356e3..44bb942f03 100644 --- a/core/libs/tests/GuestKernelErrors.cpp +++ b/core/libs/tests/GuestKernelErrors.cpp @@ -29,16 +29,32 @@ int APS5_VABI scePthreadMutexDestroy(PthreadMutex* mutex); int APS5_VABI scePthreadMutexLock(PthreadMutex* mutex); int APS5_VABI scePthreadMutexUnlock(PthreadMutex* mutex); int APS5_VABI scePthreadMutexTrylock(PthreadMutex* mutex); +int APS5_VABI sceKernelCreateEventFlag(KernelEventFlag* ef, const char* name, std::uint32_t attr, std::uint64_t init_pattern, const void* param); +int APS5_VABI sceKernelDeleteEventFlag(KernelEventFlag ef); +int APS5_VABI sceKernelSetEventFlag(KernelEventFlag ef, std::uint64_t bit_pattern); +int APS5_VABI sceKernelClearEventFlag(KernelEventFlag ef, std::uint64_t bit_pattern); +int APS5_VABI sceKernelCancelEventFlag(KernelEventFlag ef, std::uint64_t set_pattern, int* num_wait_threads); +int APS5_VABI sceKernelPollEventFlag(KernelEventFlag ef, std::uint64_t bit_pattern, std::uint32_t wait_mode, std::uint64_t* result_pat); +int APS5_VABI sceKernelWaitEventFlag(KernelEventFlag ef, std::uint64_t bit_pattern, std::uint32_t wait_mode, std::uint64_t* result_pat, KernelUseconds* timeout); } static constexpr int SCE_OK = 0; +static constexpr int SCE_KERNEL_ERROR_EPERM = static_cast(0x80020001); static constexpr int SCE_KERNEL_ERROR_ENOENT = static_cast(0x80020002); +static constexpr int SCE_KERNEL_ERROR_ESRCH = static_cast(0x80020003); static constexpr int SCE_KERNEL_ERROR_EBADF = static_cast(0x80020009); static constexpr int SCE_KERNEL_ERROR_EDEADLK = static_cast(0x8002000B); static constexpr int SCE_KERNEL_ERROR_EFAULT = static_cast(0x8002000E); static constexpr int SCE_KERNEL_ERROR_EINVAL = static_cast(0x80020016); static constexpr int SCE_KERNEL_ERROR_EBUSY = static_cast(0x80020010); static constexpr int SCE_KERNEL_ERROR_ETIMEDOUT = static_cast(0x8002003C); +static constexpr int SCE_KERNEL_ERROR_ECANCELED = static_cast(0x80020055); +static constexpr std::uint32_t EVF_ATTR_SINGLE = 0x10; +static constexpr std::uint32_t EVF_ATTR_MULTI = 0x20; +static constexpr std::uint32_t EVF_WAITMODE_AND = 0x01; +static constexpr std::uint32_t EVF_WAITMODE_OR = 0x02; +static constexpr std::uint32_t EVF_WAITMODE_CLEAR_ALL = 0x10; +static constexpr std::uint32_t EVF_WAITMODE_CLEAR_PAT = 0x20; static constexpr int MUTEX_TYPE_ERRORCHECK = 1; static constexpr int PRIO_NONE = 0; static constexpr int PRIO_INHERIT = 1; @@ -156,4 +172,68 @@ int main() { Require(pthread_mutex_lock_nid_postfix(&staticAdaptive) == POSIX_EDEADLK); Require(pthread_mutex_unlock_nid_postfix(&staticAdaptive) == 0); Require(pthread_mutex_destroy_nid_postfix(&staticAdaptive) == 0); + + KernelEventFlag ef = nullptr; + Require(sceKernelCreateEventFlag(nullptr, "test", EVF_ATTR_MULTI, 0, nullptr) == SCE_KERNEL_ERROR_EINVAL); + Require(sceKernelCreateEventFlag(&ef, nullptr, EVF_ATTR_MULTI, 0, nullptr) == SCE_KERNEL_ERROR_EINVAL); + Require(sceKernelCreateEventFlag(&ef, "test_zero_attr", 0, 0, nullptr) == SCE_OK); + Require(sceKernelDeleteEventFlag(ef) == SCE_OK); + Require(sceKernelCreateEventFlag(&ef, "test_fifo", 0x01, 0, nullptr) == SCE_OK); + Require(sceKernelDeleteEventFlag(ef) == SCE_OK); + Require(sceKernelCreateEventFlag(&ef, "test_single_fifo", EVF_ATTR_SINGLE | 0x01u, 0, nullptr) == SCE_OK); + Require(sceKernelDeleteEventFlag(ef) == SCE_OK); + Require(sceKernelCreateEventFlag(&ef, "test", EVF_ATTR_SINGLE | EVF_ATTR_MULTI, 0, nullptr) == SCE_KERNEL_ERROR_EINVAL); + Require(sceKernelCreateEventFlag(&ef, "test", EVF_ATTR_MULTI | 0x1000u, 0, nullptr) == SCE_KERNEL_ERROR_EINVAL); + Require(sceKernelDeleteEventFlag(nullptr) == SCE_KERNEL_ERROR_ESRCH); + Require(sceKernelSetEventFlag(nullptr, 1) == SCE_KERNEL_ERROR_ESRCH); + Require(sceKernelClearEventFlag(nullptr, 1) == SCE_KERNEL_ERROR_ESRCH); + Require(sceKernelPollEventFlag(nullptr, 1, EVF_WAITMODE_OR, nullptr) == SCE_KERNEL_ERROR_ESRCH); + Require(sceKernelWaitEventFlag(nullptr, 1, EVF_WAITMODE_OR, nullptr, nullptr) == SCE_KERNEL_ERROR_ESRCH); + Require(sceKernelCancelEventFlag(nullptr, 0, nullptr) == SCE_KERNEL_ERROR_ESRCH); + + Require(sceKernelCreateEventFlag(&ef, "flag", EVF_ATTR_MULTI, 0x15, nullptr) == SCE_OK); + std::uint64_t pattern = 0; + Require(sceKernelPollEventFlag(ef, 0, EVF_WAITMODE_OR, &pattern) == SCE_KERNEL_ERROR_EINVAL); + Require(sceKernelPollEventFlag(ef, 1, 0, &pattern) == SCE_KERNEL_ERROR_EINVAL); + Require(sceKernelPollEventFlag(ef, 1, EVF_WAITMODE_AND | EVF_WAITMODE_OR, &pattern) == SCE_KERNEL_ERROR_EINVAL); + Require(sceKernelPollEventFlag(ef, 1, EVF_WAITMODE_AND | EVF_WAITMODE_CLEAR_ALL | EVF_WAITMODE_CLEAR_PAT, &pattern) == SCE_KERNEL_ERROR_EINVAL); + + Require(sceKernelPollEventFlag(ef, 0x20, EVF_WAITMODE_OR, &pattern) == SCE_KERNEL_ERROR_EBUSY); + Require(pattern == 0x15); + Require(sceKernelPollEventFlag(ef, 0x11, EVF_WAITMODE_AND, &pattern) == SCE_OK); + Require(pattern == 0x15); + + Require(sceKernelClearEventFlag(ef, ~std::uint64_t{0x01}) == SCE_OK); + Require(sceKernelPollEventFlag(ef, 0x01, EVF_WAITMODE_OR, &pattern) == SCE_KERNEL_ERROR_EBUSY); + Require(pattern == 0x14); + + Require(sceKernelSetEventFlag(ef, 0x02) == SCE_OK); + Require(sceKernelPollEventFlag(ef, 0x06, EVF_WAITMODE_OR | EVF_WAITMODE_CLEAR_PAT, &pattern) == SCE_OK); + Require(pattern == 0x16); + Require(sceKernelPollEventFlag(ef, 0x02, EVF_WAITMODE_OR, &pattern) == SCE_KERNEL_ERROR_EBUSY); + Require(pattern == 0x10); + + Require(sceKernelPollEventFlag(ef, 0x10, EVF_WAITMODE_OR | EVF_WAITMODE_CLEAR_ALL, &pattern) == SCE_OK); + Require(pattern == 0x10); + Require(sceKernelPollEventFlag(ef, 0x10, EVF_WAITMODE_OR, &pattern) == SCE_KERNEL_ERROR_EBUSY); + Require(pattern == 0); + + KernelUseconds efTimeout = 1000; + Require(sceKernelWaitEventFlag(ef, 0x01, EVF_WAITMODE_OR, &pattern, &efTimeout) == SCE_KERNEL_ERROR_ETIMEDOUT); + Require(pattern == 0); + + int waitThreads = -1; + Require(sceKernelCancelEventFlag(ef, 0x42, &waitThreads) == SCE_OK); + Require(waitThreads == 0); + Require(sceKernelPollEventFlag(ef, 0x42, EVF_WAITMODE_AND, &pattern) == SCE_OK); + Require(pattern == 0x42); + + Require(sceKernelClearEventFlag(ef, 0) == SCE_OK); + Require(sceKernelPollEventFlag(ef, 0x42, EVF_WAITMODE_AND, &pattern) == SCE_KERNEL_ERROR_EBUSY); + Require(pattern == 0); + + Require(sceKernelDeleteEventFlag(ef) == SCE_OK); + + Require(sceKernelCreateEventFlag(&ef, "single", EVF_ATTR_SINGLE | 0x02u, 0, nullptr) == SCE_OK); + Require(sceKernelDeleteEventFlag(ef) == SCE_OK); } From 57ce03944d19d7b4ba010865b544d71e114d6ba3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Thu, 15 Oct 2026 14:48:02 +0200 Subject: [PATCH 0294/1000] fix(agc): keep the draw key trace map out of module teardown on Windows Co-Authored-By: Claude --- .../Execution/src/Driver/Draw/DrawRegisterKey.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp index 0464d26b60..d07b070444 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp @@ -1,5 +1,6 @@ #include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawRegisterKey.hpp" +#include "prx/libc/include/HostThreadLocal.hpp" #include #include #include @@ -130,7 +131,8 @@ std::uint64_t Driver::drawRegisterKey(const QueueState& queue, const ShaderRegis void Driver::traceDrawKeyLookup(const std::array& components, bool cacheMiss) { if (!traceDrawCache()) return; struct Previous { std::array parts{}; }; - thread_local std::unordered_map previous; + struct PreviousStorage {}; + auto& previous = HostThreadLocal, PreviousStorage>(); const auto shaderSet = components[4]; if (previous.size() >= 2048 && previous.find(shaderSet) == previous.end()) previous.clear(); const auto [it, inserted] = previous.try_emplace(shaderSet); From 6b14169a45031d7cfe4ed5eeca307b810d29fc23 Mon Sep 17 00:00:00 2001 From: Joey Fisher Date: Thu, 15 Oct 2026 06:50:57 -0600 Subject: [PATCH 0295/1000] fix(agc): resolve a GPU-read indirect dispatch at the gate when the run writes its arguments The dispatch look-ahead stopped before every DISPATCH_INDIRECT whose arguments an earlier dispatch of the run may write, or whose earlier dispatches have unknown writes. That stop protects a CPU read of the arguments at the resolve, which only the thread-dimensions, misaligned and APS5_NO_GPU_INDIRECT paths make. A GPU-read indirect dispatch never reads its arguments on the CPU, so the stop left it and every dispatch behind it to be captured when the worker reached them, after the GPU mutex waits between dispatches. That is the late read the group capture exists to avoid: by then the title had re-armed the gate and reused its table slots, and the capture read descriptors the title had not written yet. The look-ahead now stops only before an indirect dispatch whose arguments it would read on the CPU; the path choice is shared with dispatchIndirect. AgcDriverDispatchesAdoptedAhead_nid_postfix counts the dispatches adopted from the look-ahead so a test can tell a gate-time capture from a late one. Two tests check through it that both dispatches of a gated group are captured at the gate: one whose writer stores the indirect arguments through a registered binding, and one whose writer stores them through a binding misaligned in its host import. --- .../Execution/include/Driver.hpp | 2 + .../Execution/include/Driver/Driver.hpp | 3 + .../src/Driver/Dispatch/Dispatch.cpp | 1 + .../src/Driver/Dispatch/DispatchAhead.cpp | 6 +- .../src/Driver/Dispatch/IndirectDispatch.cpp | 14 +- .../Execution/src/Driver/EntryPoints.cpp | 4 + ...IndirectArgumentsMisalignedWriterAhead.cpp | 120 ++++++++++++++++++ .../GpuIndirectArgumentsWrittenAhead.cpp | 101 +++++++++++++++ 8 files changed, 245 insertions(+), 6 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/GpuIndirectArgumentsMisalignedWriterAhead.cpp create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/GpuIndirectArgumentsWrittenAhead.cpp diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver.hpp index 76b1a346fc..25bb3d485a 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver.hpp @@ -1,6 +1,7 @@ #ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_DRIVER_HPP #define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_DRIVER_HPP +#include #include #include "SceTypes.hpp" #include "SceShaders.hpp" @@ -17,6 +18,7 @@ extern "C" void AgcDriverLockVulkanLoader_nid_postfix(); extern "C" void AgcDriverUnlockVulkanLoader_nid_postfix(); extern "C" void AgcDriverShutdown_nid_postfix(); extern "C" void AgcDriverSuspendPoint_nid_postfix(); +extern "C" std::uint64_t AgcDriverDispatchesAdoptedAhead_nid_postfix(); extern "C" void AgcDriverRegisterShader_nid_postfix(const Shader* shader); extern "C" void AgcDriverResolveShaderAbi_nid_postfix(const Shader* shader, std::span context, std::span primitive); extern "C" void AgcDriverResolveGraphicsStagesAbi_nid_postfix(std::span stages, std::span context, std::span primitive); diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp index 93f2402920..de29bd8ed2 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp @@ -49,6 +49,7 @@ public: ~Driver(); void Shutdown(); void WaitIdle(); + std::uint64_t DispatchesAdoptedAhead() const; void CheckFailure(); void ReportFailure(std::exception_ptr error); void Submit(const Packet* packet, std::uint32_t queue); @@ -176,6 +177,7 @@ private: static bool traceCopy(); void traceCopyPending(const char* what, std::uint64_t address, std::size_t bytes, std::uint64_t source, std::uint64_t destination, std::size_t copyBytes, const std::shared_ptr& localDevice); static void countIndirect(int path, double readMs); + static int indirectArgumentsPath(std::span packet, std::uint64_t arguments); void dispatchIndirect(QueueState& queue, std::span packet, const Submission& submission); static bool& sampledRead(); static bool writeEvidenceEnabled(); @@ -379,6 +381,7 @@ private: std::atomic evidenceReads{0}; std::atomic evidenceValidations{0}; + std::atomic dispatchesAdoptedAhead{0}; std::mutex writtenBuffersMutex; std::deque writtenBuffers; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp index c00249bc0c..2b5ef08729 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp @@ -232,6 +232,7 @@ void Driver::dispatch(QueueState& queue, std::span packet, cached = ahead.cached; validated = ahead.validated; ++groupStats.adopted; + ++dispatchesAdoptedAhead; const auto age = std::chrono::duration(std::chrono::steady_clock::now() - ahead.resolvedAt).count(); groupStats.ageMs += age; groupStats.ageMaxMs = std::max(groupStats.ageMaxMs, age); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchAhead.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchAhead.cpp index 441cbdf3c7..91cb9cf4c0 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchAhead.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchAhead.cpp @@ -35,6 +35,10 @@ bool& Driver::gateOpened() { return opened; } +std::uint64_t Driver::DispatchesAdoptedAhead() const { + return dispatchesAdoptedAhead.load(std::memory_order_relaxed); +} + void Driver::clearResolvedAhead() { auto& resolved = resolvedAhead(); if (resolved.empty()) return; @@ -146,7 +150,7 @@ void Driver::resolveGroupAhead(const Submission& submission, const QueueState& l const auto argumentsEnd = arguments + 3u * sizeof(std::uint32_t); const bool written = std::any_of(writes.begin(), writes.end(), [&](const auto& range) { return range.first < argumentsEnd && arguments < range.second; }) || std::any_of(labels.begin(), labels.end(), [&](const RunLabel& label) { return label.address < argumentsEnd && arguments < label.address + label.size; }); - if (unknownWrites || written) { + if ((unknownWrites || written) && indirectArgumentsPath(packet, arguments) != IndirectGpu) { ++stats.writtenBefore; break; } diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/IndirectDispatch.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/IndirectDispatch.cpp index 9be8199572..f567a93e2e 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/IndirectDispatch.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/IndirectDispatch.cpp @@ -23,14 +23,18 @@ void Driver::countIndirect(int path, double readMs) { std::fprintf(stderr, "[indirect] gpu-side %llu, cpu-side %llu (thread dimensions %llu, fill kernel %llu, pending image results %llu, pending label or copied write %llu, not imported %llu, misaligned %llu, disabled %llu, device memory LDS %llu); CPU argument reads took %.1f s\n", static_cast(counts[IndirectGpu]), static_cast(cpu), static_cast(counts[IndirectThreadDimensions]), static_cast(counts[IndirectFillKernel]), static_cast(counts[IndirectPendingImage]), static_cast(counts[IndirectCopiedWrite]), static_cast(counts[IndirectNotImported]), static_cast(counts[IndirectMisaligned]), static_cast(counts[IndirectDisabled]), static_cast(counts[IndirectWorkgroupMemory]), readWaitedMs / 1000); } -void Driver::dispatchIndirect(QueueState& queue, std::span packet, const Submission& submission) { +int Driver::indirectArgumentsPath(std::span packet, std::uint64_t arguments) { static const bool gpuIndirect = std::getenv("APS5_NO_GPU_INDIRECT") == nullptr; + if (!gpuIndirect) return IndirectDisabled; + if ((packet.back() & 0x20u) != 0) return IndirectThreadDimensions; + if (arguments % 4 != 0) return IndirectMisaligned; + return IndirectGpu; +} + +void Driver::dispatchIndirect(QueueState& queue, std::span packet, const Submission& submission) { const auto arguments = Pm4::DispatchArgumentAddress(packet, queue); const auto initiator = packet.back(); - int path = IndirectGpu; - if (!gpuIndirect) path = IndirectDisabled; - else if ((initiator & 0x20u) != 0) path = IndirectThreadDimensions; - else if (arguments % 4 != 0) path = IndirectMisaligned; + const int path = indirectArgumentsPath(packet, arguments); if (path != IndirectGpu) { if (!resolvingAhead()) { const auto& resolved = resolvedAhead(); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/EntryPoints.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/EntryPoints.cpp index 5853975c5f..01693be19c 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/EntryPoints.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/EntryPoints.cpp @@ -99,6 +99,10 @@ extern "C" std::uint32_t AgcDriverReserveEopFlip_nid_postfix(const AgcDriver::Fl return AgcDriver::DriverDetail::Driver::Get().ReserveEopFlip(info); } +extern "C" std::uint64_t AgcDriverDispatchesAdoptedAhead_nid_postfix() { + return AgcDriver::DriverDetail::Driver::Get().DispatchesAdoptedAhead(); +} + extern "C" void AgcDriverRegisterVideoOutput_nid_postfix(std::uint32_t handle, const std::shared_ptr& output) { AgcDriver::RegisterVideoOutput(handle, output); } diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/GpuIndirectArgumentsMisalignedWriterAhead.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/GpuIndirectArgumentsMisalignedWriterAhead.cpp new file mode 100644 index 0000000000..704f0fa843 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/GpuIndirectArgumentsMisalignedWriterAhead.cpp @@ -0,0 +1,120 @@ +#include "VulkanTestDevice.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Submit/include/Acb.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +void Require(bool condition, const std::string& message) { + if (!condition) throw std::runtime_error(message); +} + +alignas(256) std::array WriterCode{0xf4080200u, 0xfa000000u, 0xbf8cc07fu, 0x7e000202u, 0x7e020203u, 0x7e040204u, 0xe07c0000u, 0x80020000u, 0xbf810000u}; +alignas(256) std::array CounterCode{0xf4080200u, 0xfa000000u, 0xbf8cc07fu, 0x7e000281u, 0xe0c80000u, 0x80020000u, 0xbf810000u}; + +constexpr std::size_t BlockBytes = 65536; +constexpr std::uint32_t ArgumentsOffset = 4; +alignas(BlockBytes) std::array Block{}; +alignas(256) std::array WriterDescriptor{}; +alignas(256) std::array Counter{}; +alignas(256) std::array CounterDescriptor{}; +alignas(256) volatile std::uint32_t Gate = 1; +alignas(256) volatile std::uint32_t Done = 0; + +std::uint32_t Low(const volatile void* pointer) { + return static_cast(reinterpret_cast(pointer)); +} + +std::uint32_t High(const volatile void* pointer) { + return static_cast(static_cast(reinterpret_cast(pointer)) >> 32u); +} + +std::array Descriptor(const void* pointer, std::uint32_t bytes) { + return {Low(pointer), High(pointer) & 0xffffu, bytes, 0x31016facu}; +} + +std::uint32_t Argument(std::size_t index) { + std::uint32_t value = 0; + std::memcpy(&value, Block.data() + ArgumentsOffset + index * sizeof(value), sizeof(value)); + return value; +} + +void SetArguments(std::uint32_t x, std::uint32_t y, std::uint32_t z) { + const std::array values{x, y, z}; + std::memcpy(Block.data() + ArgumentsOffset, values.data(), sizeof(values)); +} + +void SetShaderRegister(std::vector& words, std::uint32_t offset, std::uint32_t value) { + words.insert(words.end(), {0xc0017600u, offset, value}); +} + +void Program(std::vector& words, const void* code, std::initializer_list userData) { + const auto address = static_cast(reinterpret_cast(code)); + SetShaderRegister(words, 0x20c, static_cast(address >> 8u)); + SetShaderRegister(words, 0x20d, static_cast(address >> 40u) & 0xffu); + for (const std::uint32_t offset : {0x207u, 0x208u, 0x209u}) SetShaderRegister(words, offset, 1); + SetShaderRegister(words, 0x212, 0); + SetShaderRegister(words, 0x213, static_cast(userData.size()) << 1u); + std::uint32_t offset = 0x240; + for (const auto value : userData) SetShaderRegister(words, offset++, value); +} + +} + +int main() { + try { + auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + device.reset(); + GuestAllocations::Mutation().Add(Block.data(), BlockBytes, true, true, true); + SetArguments(1, 1, 1); + const auto* arguments = Block.data() + ArgumentsOffset; + WriterDescriptor = Descriptor(arguments, 16); + CounterDescriptor = Descriptor(Counter.data(), sizeof(Counter)); + std::vector warmUp; + Program(warmUp, CounterCode.data(), {Low(CounterDescriptor.data()), High(CounterDescriptor.data())}); + warmUp.insert(warmUp.end(), {0xc0031500u, 1, 1, 1, 0x8041u}); + warmUp.insert(warmUp.end(), {0xc0033700u, 0x00100200u, Low(&Done), High(&Done), 2u}); + Packet first{warmUp.data(), static_cast(warmUp.size()), 0, {}}; + Require(sceAgcDriverSubmitAcb(0x20, &first) == 0, "warm-up submit failed"); + AgcDriverWaitIdle_nid_postfix(); + Require(Done == 2 && Counter[0] == 1, "the warm-up dispatch did not run"); + Counter[0] = 0; + const auto adoptedBefore = AgcDriverDispatchesAdoptedAhead_nid_postfix(); + std::vector words; + words.insert(words.end(), {0xc0053c00u, 0x13u, Low(&Gate), High(&Gate), 1u, 0xffffffffu, 0x19u}); + Program(words, WriterCode.data(), {Low(WriterDescriptor.data()), High(WriterDescriptor.data()), 2u, 3u, 4u}); + words.insert(words.end(), {0xc0031500u, 1, 1, 1, 0x8041u}); + Program(words, CounterCode.data(), {Low(CounterDescriptor.data()), High(CounterDescriptor.data())}); + words.insert(words.end(), {0xc0021600u, Low(arguments), High(arguments), 0x8041u}); + words.insert(words.end(), {0xc0033700u, 0x00100200u, Low(&Done), High(&Done), 1u}); + Packet packet{words.data(), static_cast(words.size()), 0, {}}; + Require(sceAgcDriverSubmitAcb(0x20, &packet) == 0, "compute submit failed"); + AgcDriverWaitIdle_nid_postfix(); + Require(Done == 1, "the submission did not finish"); + Require(Argument(0) == 2 && Argument(1) == 3 && Argument(2) == 4, "the first dispatch did not write the indirect arguments through its misaligned binding"); + Require(Counter[0] == 24, "an indirect dispatch whose arguments the dispatch before it writes through a misaligned binding ran " + std::to_string(Counter[0]) + " groups, expected 24"); + const auto adopted = AgcDriverDispatchesAdoptedAhead_nid_postfix() - adoptedBefore; + Require(adopted == 2, "the gated group adopted " + std::to_string(adopted) + " dispatches resolved at the gate, expected 2: a GPU-read indirect dispatch whose registered arguments an earlier dispatch writes through a misaligned binding must still be captured at the gate"); + GuestAllocations::Mutation().Remove(Block.data()); + std::cout << "gpu indirect arguments misaligned writer ahead test passed\n"; + AgcDriverShutdown_nid_postfix(); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + try { + AgcDriverShutdown_nid_postfix(); + } catch (const std::exception& shutdownError) { + std::cerr << shutdownError.what() << '\n'; + } + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/GpuIndirectArgumentsWrittenAhead.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/GpuIndirectArgumentsWrittenAhead.cpp new file mode 100644 index 0000000000..7f0640e1fc --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/GpuIndirectArgumentsWrittenAhead.cpp @@ -0,0 +1,101 @@ +#include "VulkanTestDevice.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Submit/include/Acb.hpp" +#include +#include +#include +#include +#include +#include +#include + +namespace { + +void Require(bool condition, const std::string& message) { + if (!condition) throw std::runtime_error(message); +} + +alignas(256) std::array WriterCode{0xf4080200u, 0xfa000000u, 0xbf8cc07fu, 0x7e000202u, 0x7e020203u, 0x7e040204u, 0xe07c0000u, 0x80020000u, 0xbf810000u}; +alignas(256) std::array CounterCode{0xf4080200u, 0xfa000000u, 0xbf8cc07fu, 0x7e000281u, 0xe0c80000u, 0x80020000u, 0xbf810000u}; + +alignas(256) std::array Arguments{1, 1, 1, 0}; +alignas(256) std::array ArgumentsDescriptor{}; +alignas(256) std::array Counter{}; +alignas(256) std::array CounterDescriptor{}; +alignas(256) volatile std::uint32_t Gate = 1; +alignas(256) volatile std::uint32_t Done = 0; + +std::uint32_t Low(const volatile void* pointer) { + return static_cast(reinterpret_cast(pointer)); +} + +std::uint32_t High(const volatile void* pointer) { + return static_cast(static_cast(reinterpret_cast(pointer)) >> 32u); +} + +std::array Descriptor(const void* pointer, std::uint32_t bytes) { + return {Low(pointer), High(pointer) & 0xffffu, bytes, 0x31016facu}; +} + +void SetShaderRegister(std::vector& words, std::uint32_t offset, std::uint32_t value) { + words.insert(words.end(), {0xc0017600u, offset, value}); +} + +void Program(std::vector& words, const void* code, std::initializer_list userData) { + const auto address = static_cast(reinterpret_cast(code)); + SetShaderRegister(words, 0x20c, static_cast(address >> 8u)); + SetShaderRegister(words, 0x20d, static_cast(address >> 40u) & 0xffu); + for (const std::uint32_t offset : {0x207u, 0x208u, 0x209u}) SetShaderRegister(words, offset, 1); + SetShaderRegister(words, 0x212, 0); + SetShaderRegister(words, 0x213, static_cast(userData.size()) << 1u); + std::uint32_t offset = 0x240; + for (const auto value : userData) SetShaderRegister(words, offset++, value); +} + +} + +int main() { + try { + auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + device.reset(); + ArgumentsDescriptor = Descriptor(Arguments.data(), sizeof(Arguments)); + CounterDescriptor = Descriptor(Counter.data(), sizeof(Counter)); + std::vector warmUp; + Program(warmUp, CounterCode.data(), {Low(CounterDescriptor.data()), High(CounterDescriptor.data())}); + warmUp.insert(warmUp.end(), {0xc0031500u, 1, 1, 1, 0x8041u}); + warmUp.insert(warmUp.end(), {0xc0033700u, 0x00100200u, Low(&Done), High(&Done), 2u}); + Packet first{warmUp.data(), static_cast(warmUp.size()), 0, {}}; + Require(sceAgcDriverSubmitAcb(0x20, &first) == 0, "warm-up submit failed"); + AgcDriverWaitIdle_nid_postfix(); + Require(Done == 2 && Counter[0] == 1, "the warm-up dispatch did not run"); + Counter[0] = 0; + const auto adoptedBefore = AgcDriverDispatchesAdoptedAhead_nid_postfix(); + std::vector words; + words.insert(words.end(), {0xc0053c00u, 0x13u, Low(&Gate), High(&Gate), 1u, 0xffffffffu, 0x19u}); + Program(words, WriterCode.data(), {Low(ArgumentsDescriptor.data()), High(ArgumentsDescriptor.data()), 2u, 3u, 4u}); + words.insert(words.end(), {0xc0031500u, 1, 1, 1, 0x8041u}); + Program(words, CounterCode.data(), {Low(CounterDescriptor.data()), High(CounterDescriptor.data())}); + words.insert(words.end(), {0xc0021600u, Low(Arguments.data()), High(Arguments.data()), 0x8041u}); + words.insert(words.end(), {0xc0033700u, 0x00100200u, Low(&Done), High(&Done), 1u}); + Packet packet{words.data(), static_cast(words.size()), 0, {}}; + Require(sceAgcDriverSubmitAcb(0x20, &packet) == 0, "compute submit failed"); + AgcDriverWaitIdle_nid_postfix(); + Require(Done == 1, "the submission did not finish"); + Require(Arguments[0] == 2 && Arguments[1] == 3 && Arguments[2] == 4, "the first dispatch did not write the indirect arguments"); + Require(Counter[0] == 24, "an indirect dispatch whose arguments the dispatch before it writes ran " + std::to_string(Counter[0]) + " groups, expected 24"); + const auto adopted = AgcDriverDispatchesAdoptedAhead_nid_postfix() - adoptedBefore; + Require(adopted == 2, "the gated group adopted " + std::to_string(adopted) + " dispatches resolved at the gate, expected 2: a GPU-read indirect dispatch whose arguments an earlier dispatch writes must still be captured at the gate"); + std::cout << "gpu indirect arguments written ahead test passed\n"; + AgcDriverShutdown_nid_postfix(); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + try { + AgcDriverShutdown_nid_postfix(); + } catch (const std::exception& shutdownError) { + std::cerr << shutdownError.what() << '\n'; + } + return 1; + } +} From 47f7118241ba3d999e6a48f6f51fb119b297e90d Mon Sep 17 00:00:00 2001 From: umang Date: Wed, 14 Oct 2026 15:56:20 +0530 Subject: [PATCH 0296/1000] fix(agc): sample a depth surface's stencil plane through an R8_UNORM view Vulkan cannot view a stencil aspect as UNORM, so an 8-bit UNORM view of a stencil plane threw. Copy the stencil aspect through a buffer into an R8_UNORM image of the surface's extent and sample that. The copy is kept on the surface and refreshed when the surface was written since, or while it is the bound depth target. Co-Authored-By: Claude Opus 5.5 --- .../Graphics/src/DepthSurface.cpp | 107 +++++++++++++++++- .../tests/DepthSurfaceReuse.cpp | 73 +++++++++++- 2 files changed, 175 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp index 74838e758f..1cdeaf4b80 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp @@ -267,16 +267,20 @@ public: } } + bool UnormStencilView(const GuestTextureResource& resource) const { + return target.samples == 1 && target.stencilAddress != 0 && resource.baseAddress == target.stencilAddress && ResolveTextureFormat(resource.format) == VK_FORMAT_R8_UNORM; + } + bool SampledAccepts(std::span words, const GuestTextureResource& resource) const { const bool stencil = target.stencilAddress != 0 && resource.baseAddress == target.stencilAddress; const bool d16 = target.format == VK_FORMAT_D16_UNORM || target.format == VK_FORMAT_D16_UNORM_S8_UINT; const auto expected = stencil ? VK_FORMAT_R8_UINT : d16 ? VK_FORMAT_R16_UNORM : VK_FORMAT_R32_SFLOAT; const bool depthBits = !stencil && words.size() >= 4 && ShaderRecompiler::DepthBitsTextureWidth(words[1], words[3]) == (d16 ? 16u : 32u); const bool oneSlice = target.samples != 1 ? resource.dimension == TextureDimension::k2DMsaa : resource.dimension == TextureDimension::k2D || (resource.dimension == TextureDimension::k2DArray && resource.depthOrLastArray == 0); - return (ResolveTextureFormat(resource.format) == expected || depthBits) && oneSlice && resource.width == target.extent.width && resource.height == target.extent.height && resource.baseLevel == 0 && resource.lastLevel == 0 && resource.baseArray == 0; + return (ResolveTextureFormat(resource.format) == expected || depthBits || UnormStencilView(resource)) && oneSlice && resource.width == target.extent.width && resource.height == target.extent.height && resource.baseLevel == 0 && resource.lastLevel == 0 && resource.baseArray == 0; } - std::shared_ptr Sampled(std::span words, const GuestTextureResource& resource, VkComponentMapping components) { + std::shared_ptr Sampled(std::span words, const GuestTextureResource& resource, VkComponentMapping components, bool drawn = false) { CheckCurrent(); CheckResource(resource); std::array key{}; @@ -298,6 +302,9 @@ public: Require(resource.samples == target.samples, "the sampled depth descriptor's sample count differs from its native target"); Require(hostFormat == target.format, "sampling a native multisampled depth plane that the device stores in another depth format is not implemented"); } + if (stencil && UnormStencilView(resource) && SampledAccepts(words, resource)) { + return unormStencil(key, components, resource.dimension == TextureDimension::k2DArray ? VK_IMAGE_VIEW_TYPE_2D_ARRAY : VK_IMAGE_VIEW_TYPE_2D, drawn); + } if (!SampledAccepts(words, resource)) { char text[448]; std::snprintf(text, sizeof(text), "AGC graphics: sampling the %s plane of depth surface 0x%llx (%ux%u, vk format %d) as a %ux%u texture of guest format %u (vk %d), tile mode %u, dimension %d, levels %u-%u, slice %u is not implemented (T# %08x %08x %08x %08x %08x %08x %08x %08x)", @@ -334,7 +341,8 @@ public: } bool Holds(const Texture* texture) const { - return std::any_of(textures.begin(), textures.end(), [&](const auto& entry) { return entry.second.get() == texture; }); + const auto held = [&](const auto& entry) { return entry.second.get() == texture; }; + return std::any_of(textures.begin(), textures.end(), held) || std::any_of(stencilCopy.textures.begin(), stencilCopy.textures.end(), held); } std::uint64_t written = ++depthWrites; @@ -481,11 +489,101 @@ private: std::vector> replaced; std::unique_ptr staging; }; + struct StencilCopy { + VkImage image = VK_NULL_HANDLE; + VkDeviceMemory memory = VK_NULL_HANDLE; + std::unique_ptr staging; + std::map, std::shared_ptr> textures; + std::uint64_t written = 0; + bool filled = false; + }; std::map, std::shared_ptr> textures; PlaneStorage depthPlane; PlaneStorage stencilPlane; std::uint64_t depthMemory = 0; std::uint64_t stencilMemory = 0; + StencilCopy stencilCopy; + + std::shared_ptr unormStencil(const std::array& key, VkComponentMapping components, VkImageViewType viewType, bool drawn) { + if (stencilCopy.image == VK_NULL_HANDLE) { + VkImageCreateInfo info{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO}; + info.imageType = VK_IMAGE_TYPE_2D; + info.format = VK_FORMAT_R8_UNORM; + info.extent = {target.extent.width, target.extent.height, 1}; + info.mipLevels = 1; + info.arrayLayers = 1; + info.samples = VK_SAMPLE_COUNT_1_BIT; + info.tiling = VK_IMAGE_TILING_OPTIMAL; + info.usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT; + info.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + info.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + try { + Check(context.Function("vkCreateImage")(context.device, &info, nullptr, &stencilCopy.image), "vkCreateImage stencil copy"); + VkMemoryRequirements requirements{}; + context.Function("vkGetImageMemoryRequirements")(context.device, stencilCopy.image, &requirements); + VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; + allocation.allocationSize = requirements.size; + allocation.memoryTypeIndex = context.MemoryType(requirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); + Check(context.Function("vkAllocateMemory")(context.device, &allocation, nullptr, &stencilCopy.memory), "vkAllocateMemory stencil copy"); + Check(context.Function("vkBindImageMemory")(context.device, stencilCopy.image, stencilCopy.memory, 0), "vkBindImageMemory stencil copy"); + stencilCopy.staging = std::make_unique(context, static_cast(target.extent.width) * target.extent.height, VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT); + } catch (...) { + releaseStencilCopy(); + throw; + } + } + if (drawn || !stencilCopy.filled || stencilCopy.written != written) { + copyStencil(); + stencilCopy.written = drawn ? 0 : written; + } + auto& texture = stencilCopy.textures[key]; + if (texture == nullptr) { + texture = std::make_shared(context, stencilCopy.image, VK_FORMAT_R8_UNORM, VK_IMAGE_ASPECT_COLOR_BIT, components, viewType); + texture->MarkRefreshedPerUse(); + } + return texture; + } + + void copyStencil() { + auto* recorder = RecorderFor(context); + std::unique_ptr batch; + if (recorder == nullptr) batch = std::make_unique(context); + const auto commands = recorder != nullptr ? recorder->Commands() : batch->Handle(); + const auto barrier = context.Resolved(&DeviceFunctions::cmdPipelineBarrier, "vkCmdPipelineBarrier"); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT); + VkBufferImageCopy region{0, 0, 0, {VK_IMAGE_ASPECT_STENCIL_BIT, 0, 0, 1}, {0, 0, 0}, {target.extent.width, target.extent.height, 1}}; + context.Function("vkCmdCopyImageToBuffer")(commands, image, VK_IMAGE_LAYOUT_GENERAL, stencilCopy.staging->Handle(), 1, ®ion); + VkImageMemoryBarrier toTransfer{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; + toTransfer.srcAccessMask = VK_ACCESS_SHADER_READ_BIT; + toTransfer.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + toTransfer.oldLayout = stencilCopy.filled ? VK_IMAGE_LAYOUT_GENERAL : VK_IMAGE_LAYOUT_UNDEFINED; + toTransfer.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + toTransfer.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + toTransfer.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + toTransfer.image = stencilCopy.image; + toTransfer.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + const VkBufferMemoryBarrier staged{VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER, nullptr, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT, VK_QUEUE_FAMILY_IGNORED, VK_QUEUE_FAMILY_IGNORED, stencilCopy.staging->Handle(), 0, VK_WHOLE_SIZE}; + barrier(commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT | VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 1, &staged, 1, &toTransfer); + region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + context.Function("vkCmdCopyBufferToImage")(commands, stencilCopy.staging->Handle(), stencilCopy.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ®ion); + VkImageMemoryBarrier toGeneral = toTransfer; + toGeneral.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + toGeneral.dstAccessMask = VK_ACCESS_SHADER_READ_BIT; + toGeneral.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + toGeneral.newLayout = VK_IMAGE_LAYOUT_GENERAL; + barrier(commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 0, nullptr, 0, nullptr, 1, &toGeneral); + stencilCopy.filled = true; + if (batch) batch->SubmitAndWait(); + else Recorder::CountBarriers(Recorder::CommandClass::Copy, 3); + } + + void releaseStencilCopy() noexcept { + stencilCopy.textures.clear(); + stencilCopy.staging.reset(); + if (stencilCopy.image) context.Function("vkDestroyImage")(context.device, stencilCopy.image, nullptr); + if (stencilCopy.memory) context.Function("vkFreeMemory")(context.device, stencilCopy.memory, nullptr); + stencilCopy = {}; + } std::uint64_t depthBytes() const { const bool d16 = target.format == VK_FORMAT_D16_UNORM || target.format == VK_FORMAT_D16_UNORM_S8_UINT; @@ -532,6 +630,7 @@ private: void release() noexcept { depthPlane = {}; stencilPlane = {}; + releaseStencilCopy(); textures.clear(); if (view) context.Function("vkDestroyImageView")(context.device, view, nullptr); if (owned != nullptr) owned.reset(); @@ -1200,7 +1299,7 @@ std::shared_ptr DepthSurfaceTexture(const Context& context, std::spanApplyFastClear(); - if (sampled) return found->Sampled(words, resource, components); + if (sampled) return found->Sampled(words, resource, components, found == drawnDepth); Require(found->hostFormat == base.format, "sampling several slices or an integer view of a depth plane that the device stores in another depth format is not implemented"); const auto layers = cube ? 6u : layered ? resource.depthOrLastArray + 1u : 1u; if (!planes) { diff --git a/core/libs/prx/libSceAgcDriver/tests/DepthSurfaceReuse.cpp b/core/libs/prx/libSceAgcDriver/tests/DepthSurfaceReuse.cpp index a427dfcc29..538f42295e 100644 --- a/core/libs/prx/libSceAgcDriver/tests/DepthSurfaceReuse.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/DepthSurfaceReuse.cpp @@ -10,12 +10,14 @@ #include #include #include +#include #include namespace { using namespace AgcDriver::Graphics; +constexpr std::uint32_t FormatR8Unorm = 1; constexpr std::uint32_t FormatR8Uint = 5; constexpr std::uint32_t FormatR16Unorm = 7; constexpr std::uint32_t FormatR16Uint = 11; @@ -43,6 +45,8 @@ VkImage lastImage = VK_NULL_HANDLE; std::vector clearedImages; VkImageUsageFlags lastImageUsage = 0; std::uint32_t imagesDestroyed = 0; +std::vector> imageReads; +std::vector> imageWrites; VKAPI_ATTR VkResult VKAPI_CALL createImage(VkDevice, const VkImageCreateInfo* info, const VkAllocationCallbacks*, VkImage* image) { lastImageFormat = info->format; @@ -114,6 +118,33 @@ VKAPI_ATTR void VKAPI_CALL cmdCopyImage(VkCommandBuffer, VkImage source, VkImage for (std::uint32_t i = 0; i < count; ++i) imageCopies.emplace_back(source, destination, regions[i].dstSubresource.aspectMask); } +VKAPI_ATTR void VKAPI_CALL cmdCopyImageToBuffer(VkCommandBuffer, VkImage source, VkImageLayout, VkBuffer, std::uint32_t count, const VkBufferImageCopy* regions) { + for (std::uint32_t i = 0; i < count; ++i) imageReads.emplace_back(source, regions[i].imageSubresource.aspectMask); +} + +VKAPI_ATTR void VKAPI_CALL cmdCopyBufferToImage(VkCommandBuffer, VkBuffer, VkImage destination, VkImageLayout, std::uint32_t count, const VkBufferImageCopy* regions) { + for (std::uint32_t i = 0; i < count; ++i) imageWrites.emplace_back(destination, regions[i].imageSubresource.aspectMask); +} + +VKAPI_ATTR VkResult VKAPI_CALL createBuffer(VkDevice, const VkBufferCreateInfo*, const VkAllocationCallbacks*, VkBuffer* buffer) { + *buffer = newHandle(); + return VK_SUCCESS; +} + +VKAPI_ATTR void VKAPI_CALL bufferMemoryRequirements(VkDevice, VkBuffer, VkMemoryRequirements* requirements) { + *requirements = {65536, 256, 1}; +} + +VKAPI_ATTR VkResult VKAPI_CALL bindBufferMemory(VkDevice, VkBuffer, VkDeviceMemory, VkDeviceSize) { + return VK_SUCCESS; +} + +VKAPI_ATTR void VKAPI_CALL destroyBuffer(VkDevice, VkBuffer, const VkAllocationCallbacks*) {} +VKAPI_ATTR VkResult VKAPI_CALL mapMemory(VkDevice, VkDeviceMemory, VkDeviceSize, VkDeviceSize, VkMemoryMapFlags, void**) { + return VK_ERROR_MEMORY_MAP_FAILED; +} +VKAPI_ATTR void VKAPI_CALL unmapMemory(VkDevice, VkDeviceMemory) {} + VKAPI_ATTR void VKAPI_CALL formatProperties(VkPhysicalDevice, VkFormat, VkFormatProperties* properties) { *properties = {}; properties->optimalTilingFeatures = VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT | VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT; @@ -163,7 +194,15 @@ PFN_vkVoidFunction VKAPI_CALL deviceProc(VkDevice, const char* name) { {"vkFreeMemory", reinterpret_cast(freeMemory)}, {"vkCmdPipelineBarrier", reinterpret_cast(cmdPipelineBarrier)}, {"vkCmdClearDepthStencilImage", reinterpret_cast(cmdClearDepthStencilImage)}, - {"vkCmdCopyImage", reinterpret_cast(cmdCopyImage)} + {"vkCmdCopyImage", reinterpret_cast(cmdCopyImage)}, + {"vkCmdCopyImageToBuffer", reinterpret_cast(cmdCopyImageToBuffer)}, + {"vkCmdCopyBufferToImage", reinterpret_cast(cmdCopyBufferToImage)}, + {"vkCreateBuffer", reinterpret_cast(createBuffer)}, + {"vkGetBufferMemoryRequirements", reinterpret_cast(bufferMemoryRequirements)}, + {"vkBindBufferMemory", reinterpret_cast(bindBufferMemory)}, + {"vkDestroyBuffer", reinterpret_cast(destroyBuffer)}, + {"vkMapMemory", reinterpret_cast(mapMemory)}, + {"vkUnmapMemory", reinterpret_cast(unmapMemory)} }; const auto it = table.find(name); return it == table.end() ? nullptr : it->second; @@ -197,6 +236,37 @@ std::shared_ptr lookup(const Context& context, const View& view) { return DepthSurfaceTexture(context, words, DecodeTextureResource(words), VkComponentMapping{}); } +void TestStencilUnormView(const Context& context) { + constexpr std::uint64_t depth = 0x74000000; + constexpr std::uint64_t stencil = 0x74800000; + constexpr std::uint64_t other = 0x75000000; + const auto before = createdImages.size(); + DepthSurfaceView(context, {depth, stencil, {320, 180}, VK_FORMAT_D32_SFLOAT_S8_UINT, 1.0f, 0}); + Require(createdImages.size() == before + 1, "binding a new depth surface must create one image"); + const auto surface = createdImages[before]; + DepthSurfaceView(context, {other, 0, {64, 64}, VK_FORMAT_D32_SFLOAT, 1.0f, 0}); + imageReads.clear(); + imageWrites.clear(); + Require(lookup(context, {stencil, FormatR8Uint, 320, 180}) != nullptr && imageReads.empty() && imageWrites.empty(), "an R8_UINT view of a stencil plane must sample it without a copy"); + const auto first = lookup(context, {stencil, FormatR8Unorm, 320, 180}); + Require(first != nullptr && lastImageFormat == VK_FORMAT_R8_UNORM, "an R8_UNORM view of a stencil plane must sample an R8_UNORM copy of it"); + const auto copy = createdImages.back(); + const std::vector> read{{surface, VK_IMAGE_ASPECT_STENCIL_BIT}}; + const std::vector> written{{copy, VK_IMAGE_ASPECT_COLOR_BIT}}; + Require(imageReads == read && imageWrites == written, "the R8_UNORM copy must be filled from the stencil aspect of the depth image"); + Require(lookup(context, {stencil, FormatR8Unorm, 320, 180}) == first && imageReads.size() == 1, "sampling the stencil plane again with no write in between must reuse the copy"); + DepthSurfaceView(context, {depth, stencil, {320, 180}, VK_FORMAT_D32_SFLOAT_S8_UINT, 1.0f, 0}); + lookup(context, {stencil, FormatR8Unorm, 320, 180}); + lookup(context, {stencil, FormatR8Unorm, 320, 180}); + Require(imageReads.size() == 3 && imageWrites.size() == 3, "sampling the stencil plane of the bound depth surface must copy it each time"); + DepthSurfaceView(context, {other, 0, {64, 64}, VK_FORMAT_D32_SFLOAT, 1.0f, 0}); + lookup(context, {stencil, FormatR8Unorm, 320, 180}); + lookup(context, {stencil, FormatR8Unorm, 320, 180}); + Require(imageReads.size() == 4 && imageWrites.size() == 4, "after the surface is unbound, its stencil copy must be refreshed once"); + Require(createdImages.back() == copy, "refreshing the stencil copy must reuse its image"); + ClearDepthSurfaces(context.device); +} + void TestD16StencilFallback(const Context& context) { Require(HostDepthFormat(context, VK_FORMAT_D16_UNORM_S8_UINT) == VK_FORMAT_D16_UNORM_S8_UINT && HostDepthBiasConstant(context, VK_FORMAT_D16_UNORM_S8_UINT, 2.0f) == 2.0f, "a device with D16_UNORM_S8_UINT must keep it and its depth bias"); static int withoutTag = 0; @@ -314,6 +384,7 @@ void RunDepthSurfaceReuseTests() { TestD16StencilFallback(context); TestMultisampleDepthOwnership(context); TestMultisampleStencilAliases(context); + TestStencilUnormView(context); constexpr std::uint64_t depth32 = 0x40000000; constexpr std::uint64_t depth16 = 0x50000000; constexpr std::uint64_t depthStencil = 0x60000000; From f7ec358ec4c8f63c0c39a8a31d327389410083c6 Mon Sep 17 00:00:00 2001 From: Get-fix <100056769+Get-fix@users.noreply.github.com> Date: Thu, 15 Oct 2026 15:57:49 +0300 Subject: [PATCH 0297/1000] test(relinker): convert the SCE module extent fixtures for every target The two fixtures were converted for Windows only. They now run for Linux, Windows and macOS. The syscall scan of a module with data after its code fails on all three without the fix, and the loader path without PT_INTERP fails on Windows and macOS, so the macOS code analysis stays under test. --- .../relinker/tests/test_sce_module_code_extent.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/core/relinker/relinker/tests/test_sce_module_code_extent.py b/core/relinker/relinker/tests/test_sce_module_code_extent.py index 28a19c721e..9395a04db3 100644 --- a/core/relinker/relinker/tests/test_sce_module_code_extent.py +++ b/core/relinker/relinker/tests/test_sce_module_code_extent.py @@ -123,22 +123,22 @@ def sce_module(): return image -def run_module(relinker, work, name, module): +def run_module(relinker, work, name, module, flags): case = work / name (case / "sce_module").mkdir(parents=True) (case / "input.elf").write_bytes(importing_executable([])) (case / "sce_module" / "libExample.prx").write_bytes(module) - result = subprocess.run([str(relinker), "--windows", str(case / "input.elf"), str(case / "output.exe")], + result = subprocess.run([str(relinker), *flags, str(case / "input.elf"), str(case / "output.exe")], capture_output=True, text=True, timeout=30) assert result.returncode == 0 and (case / "app0" / "sce_module" / "libExample.prx.guest.prx").exists(), ( name, result.stdout, result.stderr) -def run(relinker, work, name, image, expected_error=None): +def run(relinker, work, name, image, flags, expected_error=None): source = work / (name + ".elf") output = work / (name + ".exe") source.write_bytes(image) - result = subprocess.run([str(relinker), "--skip-sce-module", "--windows", str(source), str(output)], + result = subprocess.run([str(relinker), "--skip-sce-module", *flags, str(source), str(output)], capture_output=True, text=True, timeout=20) if expected_error is None: assert result.returncode == 0 and output.exists(), (name, result.stdout, result.stderr) @@ -151,8 +151,9 @@ def main(): relinker = Path(sys.argv[1]).resolve() with tempfile.TemporaryDirectory(prefix="anyps5-sce-module-code-extent-") as directory: work = Path(directory) - run(relinker, work, "sce-loader-path-without-interp-header", fixture(rodata_address=False, interp_header=False)) - run_module(relinker, work, "sce-module-data-after-code", sce_module()) + for system, flags in (("linux", []), ("windows", ["--windows"]), ("macos", ["--macos"])): + run(relinker, work, "sce-loader-path-without-interp-header-" + system, fixture(rodata_address=False, interp_header=False), flags) + run_module(relinker, work, "sce-module-data-after-code-" + system, sce_module(), flags) print("SCE module code extent tests passed") From cb5b3e22a24e947256b1e2ce1742a6866d981a41 Mon Sep 17 00:00:00 2001 From: Nithishvelan M Date: Wed, 14 Oct 2026 13:11:49 +0530 Subject: [PATCH 0298/1000] fix(libSceMsgDialog.native): return error codes instead of throwing in initialize and terminate --- .../prx/libSceMsgDialog.native/Export.cpp | 59 +++++++++---------- core/libs/tests/GuestMsgDialogNative.cpp | 5 ++ docs/dev/debt/libSceMsgDialog.native.md | 2 +- 3 files changed, 34 insertions(+), 32 deletions(-) diff --git a/core/libs/prx/libSceMsgDialog.native/Export.cpp b/core/libs/prx/libSceMsgDialog.native/Export.cpp index edb76df0f7..339bf0049a 100644 --- a/core/libs/prx/libSceMsgDialog.native/Export.cpp +++ b/core/libs/prx/libSceMsgDialog.native/Export.cpp @@ -1,6 +1,5 @@ #include #include -#include #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" @@ -11,6 +10,7 @@ std::atomic g_status{0}; constexpr int COMMON_DIALOG_STATUS_RUNNING = 2; constexpr int COMMON_DIALOG_STATUS_FINISHED = 3; constexpr int COMMON_DIALOG_RESULT_OK = 0; +constexpr int COMMON_DIALOG_ERROR_ALREADY_INITIALIZED = static_cast(0x80B80004u); constexpr int COMMON_DIALOG_ERROR_NOT_INITIALIZED = static_cast(0x80B80003u); constexpr int COMMON_DIALOG_ERROR_NOT_FINISHED = static_cast(0x80B80005u); constexpr int COMMON_DIALOG_ERROR_NOT_RUNNING = static_cast(0x80B8000Bu); @@ -23,41 +23,38 @@ extern "C" { int SceMsgDialogNativeModuleLoaded_nid_no_patch = 1; int APS5_VABI sceMsgDialogClose(void) { - int expected = COMMON_DIALOG_STATUS_RUNNING; - if (!g_status.compare_exchange_strong(expected, COMMON_DIALOG_STATUS_FINISHED)) return COMMON_DIALOG_ERROR_NOT_RUNNING; - return 0; + int expected = COMMON_DIALOG_STATUS_RUNNING; + if (!g_status.compare_exchange_strong(expected, COMMON_DIALOG_STATUS_FINISHED)) return COMMON_DIALOG_ERROR_NOT_RUNNING; + return 0; } int APS5_VABI sceMsgDialogGetResult(MsgDialogResult* result) { - const int status = g_status.load(); - if (status == 0) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; - if (result == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; - if (status != COMMON_DIALOG_STATUS_FINISHED) return COMMON_DIALOG_ERROR_NOT_FINISHED; - *result = MsgDialogResult{}; - result->result = COMMON_DIALOG_RESULT_OK; - result->button_id = BUTTON_ID_OK; - return 0; + const int status = g_status.load(); + if (status == 0) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + if (result == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; + if (status != COMMON_DIALOG_STATUS_FINISHED) return COMMON_DIALOG_ERROR_NOT_FINISHED; + *result = MsgDialogResult{}; + result->result = COMMON_DIALOG_RESULT_OK; + result->button_id = BUTTON_ID_OK; + return 0; } int APS5_VABI sceMsgDialogInitialize(void) { int expected = 0; - if (!g_status.compare_exchange_strong(expected, 1)) throw std::logic_error("sceMsgDialogInitialize: already initialized"); + if (!g_status.compare_exchange_strong(expected, 1)) return COMMON_DIALOG_ERROR_ALREADY_INITIALIZED; return 0; } int APS5_VABI sceMsgDialogOpen(const void* param) { - const int status = g_status.load(); - if (status == 0) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; - if (param == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; - g_status = COMMON_DIALOG_STATUS_FINISHED; - return 0; + const int status = g_status.load(); + if (status == 0) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + if (param == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; + g_status = COMMON_DIALOG_STATUS_FINISHED; + return 0; } int APS5_VABI sceMsgDialogTerminate(void) { - int expected = 1; - if (g_status.compare_exchange_strong(expected, 0)) return 0; - expected = COMMON_DIALOG_STATUS_FINISHED; - if (!g_status.compare_exchange_strong(expected, 0)) throw std::logic_error("sceMsgDialogTerminate: not initialized or still running"); + if (g_status.exchange(0) == 0) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; return 0; } @@ -66,21 +63,21 @@ int APS5_VABI sceMsgDialogUpdateStatus(void) { } int APS5_VABI sceMsgDialogProgressBarInc(int target, std::uint32_t delta) { - (void)target; - (void)delta; - return g_status.load() == COMMON_DIALOG_STATUS_RUNNING ? 0 : COMMON_DIALOG_ERROR_NOT_RUNNING; + (void)target; + (void)delta; + return g_status.load() == COMMON_DIALOG_STATUS_RUNNING ? 0 : COMMON_DIALOG_ERROR_NOT_RUNNING; } int APS5_VABI sceMsgDialogProgressBarSetMsg(int target, const char* msg) { - (void)target; - (void)msg; - return g_status.load() == COMMON_DIALOG_STATUS_RUNNING ? 0 : COMMON_DIALOG_ERROR_NOT_RUNNING; + (void)target; + (void)msg; + return g_status.load() == COMMON_DIALOG_STATUS_RUNNING ? 0 : COMMON_DIALOG_ERROR_NOT_RUNNING; } int APS5_VABI sceMsgDialogProgressBarSetValue(int target, std::uint32_t rate) { - (void)target; - (void)rate; - return g_status.load() == COMMON_DIALOG_STATUS_RUNNING ? 0 : COMMON_DIALOG_ERROR_NOT_RUNNING; + (void)target; + (void)rate; + return g_status.load() == COMMON_DIALOG_STATUS_RUNNING ? 0 : COMMON_DIALOG_ERROR_NOT_RUNNING; } int APS5_VABI sceMsgDialogGetStatus(void) { diff --git a/core/libs/tests/GuestMsgDialogNative.cpp b/core/libs/tests/GuestMsgDialogNative.cpp index dd43d10b4c..bc806a3cd8 100644 --- a/core/libs/tests/GuestMsgDialogNative.cpp +++ b/core/libs/tests/GuestMsgDialogNative.cpp @@ -22,6 +22,8 @@ void Require(bool value) { if (!value) std::abort(); } constexpr int kStatusInitialized = 1; constexpr int kStatusFinished = 3; +constexpr int kErrAlreadyInitialized = static_cast(0x80B80004u); +constexpr int kErrNotInitialized = static_cast(0x80B80003u); constexpr int kErrNotRunning = static_cast(0x80B8000Bu); constexpr int kButtonIdOk = 1; @@ -30,11 +32,13 @@ constexpr int kButtonIdOk = 1; int main() { std::uint8_t param[0x60]{}; + Require(sceMsgDialogTerminate() == kErrNotInitialized); Require(sceMsgDialogClose() == kErrNotRunning); Require(sceMsgDialogUpdateStatus() == 0); Require(sceMsgDialogGetStatus() == 0); Require(sceMsgDialogInitialize() == 0); + Require(sceMsgDialogInitialize() == kErrAlreadyInitialized); Require(sceMsgDialogClose() == kErrNotRunning); Require(sceMsgDialogUpdateStatus() == kStatusInitialized); Require(sceMsgDialogGetStatus() == kStatusInitialized); @@ -57,6 +61,7 @@ int main() { Require(result.button_id == kButtonIdOk); Require(sceMsgDialogTerminate() == 0); + Require(sceMsgDialogTerminate() == kErrNotInitialized); Require(sceMsgDialogClose() == kErrNotRunning); Require(sceMsgDialogGetStatus() == 0); } diff --git a/docs/dev/debt/libSceMsgDialog.native.md b/docs/dev/debt/libSceMsgDialog.native.md index 8f8116bec7..511522085f 100644 --- a/docs/dev/debt/libSceMsgDialog.native.md +++ b/docs/dev/debt/libSceMsgDialog.native.md @@ -2,4 +2,4 @@ ### Silent stubs -- [libSceMsgDialog.native](../../../core/libs/prx/libSceMsgDialog.native/Export.cpp) - dialogs finish at `sceMsgDialogOpen` without UI and answer with button 1, so `sceMsgDialogClose` returns `NOT_RUNNING`; its result before `sceMsgDialogInitialize` follows libSceMsgDialog +- [libSceMsgDialog.native](../../../core/libs/prx/libSceMsgDialog.native/Export.cpp) - dialogs finish at `sceMsgDialogOpen` without UI and answer with button 1, so `sceMsgDialogClose` returns `NOT_RUNNING`; its result before `sceMsgDialogInitialize` follows libSceMsgDialog. `sceMsgDialogTerminate` resets running or finished dialogs and returns OK, rejecting uninitialized calls with `COMMON_DIALOG_ERROR_NOT_INITIALIZED` following shadPS4. From b139ac9b9fc6462e4239f6f10ce1532951c2bae7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Thu, 15 Oct 2026 14:59:51 +0200 Subject: [PATCH 0299/1000] ci(build): build up to 40 pull requests together Co-Authored-By: Claude --- .github/workflows/build.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1b96d8fa7d..f18d548971 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -9,7 +9,7 @@ on: workflow_dispatch: inputs: pr: - description: Pull request numbers to build together, separated by spaces or commas (up to 8) + description: Pull request numbers to build together, separated by spaces or commas (up to 40) required: true type: string linux: @@ -127,8 +127,8 @@ jobs: throw new Error('Only collaborators with write access can start or rerun builds'); } } - if (!/^[1-9][0-9]*([ ,]+[1-9][0-9]*){0,7}$/.test(process.env.PR_NUMBER.trim())) { - throw new Error('Enter up to 8 positive pull request numbers separated by spaces or commas'); + if (!/^[1-9][0-9]*([ ,]+[1-9][0-9]*){0,39}$/.test(process.env.PR_NUMBER.trim())) { + throw new Error('Enter up to 40 positive pull request numbers separated by spaces or commas'); } } const numbers = manual From 9b92c55edf4b199d0cdad945766c36b95ea46757 Mon Sep 17 00:00:00 2001 From: gguerra26 Date: Wed, 14 Oct 2026 11:41:05 +0100 Subject: [PATCH 0300/1000] fix(relinker): lower ANDN in Rosetta conversion --- core/relinker/CMakeLists.txt | 8 + .../include/codegen/x86/AndnLowering.hpp | 22 ++ .../src/x86/Amd64OnlyInstructionMatcher.cpp | 7 + .../relinker/codegen/src/x86/AndnLowering.cpp | 138 ++++++++++ .../codegen/tests/AndnLoweringTests.cpp | 245 ++++++++++++++++++ .../relinker/tests/test_to_rosetta.py | 18 +- docs/dev/debt/libkernel.md | 2 +- docs/dev/debt/relinker.md | 4 +- docs/user/USAGE.md | 2 +- 9 files changed, 439 insertions(+), 7 deletions(-) create mode 100644 core/relinker/codegen/include/codegen/x86/AndnLowering.hpp create mode 100644 core/relinker/codegen/src/x86/AndnLowering.cpp create mode 100644 core/relinker/codegen/tests/AndnLoweringTests.cpp diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index 784fc11985..d227fc017d 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -51,6 +51,7 @@ add_library(relinker_core STATIC ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/WideStoreOperands.cpp ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/WideStoreLowering.cpp ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/ScalarCompareOperands.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/AndnLowering.cpp ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/parsing/ElfReader.cpp ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/analysis/SyscallScanner.cpp ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/analysis/CallSiteResolver.cpp @@ -83,6 +84,11 @@ target_compile_features(relinker_core PUBLIC cxx_std_20) set_target_properties(relinker_core PROPERTIES CXX_EXTENSIONS OFF) if(BUILD_TESTING) + add_test_executable(andn_lowering_tests ${CMAKE_CURRENT_SOURCE_DIR}/codegen/tests/AndnLoweringTests.cpp) + target_link_libraries(andn_lowering_tests PRIVATE relinker_core) + add_test(NAME andn_lowering COMMAND andn_lowering_tests) + set_tests_properties(andn_lowering PROPERTIES TIMEOUT 20) + add_test_executable(relinker_conversion_tests ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/ConversionTests.cpp) target_link_libraries(relinker_conversion_tests PRIVATE relinker_core) add_test(NAME relinker_conversion COMMAND relinker_conversion_tests) @@ -169,6 +175,7 @@ if(WIN32) ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/WideStoreOperands.cpp ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/WideStoreLowering.cpp ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/ScalarCompareOperands.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/AndnLowering.cpp ) target_include_directories(amd64_only_windows_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/include ${CMAKE_CURRENT_SOURCE_DIR}/domain/include ${CMAKE_CURRENT_SOURCE_DIR}/io/include ${CMAKE_CURRENT_SOURCE_DIR}/codegen/include ${CMAKE_CURRENT_SOURCE_DIR}/../libs/prx/libc/src/specifics/windows) if(BUILD_TESTING) @@ -353,6 +360,7 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/WideStoreOperands.cpp ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/WideStoreLowering.cpp ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/ScalarCompareOperands.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/AndnLowering.cpp ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/src/linux/LinuxElfPatcher.cpp ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/src/general/SegmentFilter.cpp ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/src/general/EntryStubBuilder.cpp diff --git a/core/relinker/codegen/include/codegen/x86/AndnLowering.hpp b/core/relinker/codegen/include/codegen/x86/AndnLowering.hpp new file mode 100644 index 0000000000..00ecfe2cb6 --- /dev/null +++ b/core/relinker/codegen/include/codegen/x86/AndnLowering.hpp @@ -0,0 +1,22 @@ +#ifndef CODEGEN_X86_ANDNLOWERING_HPP +#define CODEGEN_X86_ANDNLOWERING_HPP + +#include +#include + +namespace Codegen { + +struct AndnOperands { + std::uint8_t Destination; + std::uint8_t InvertedSource; + std::uint8_t Source; + bool Wide; + std::optional Memory; +}; + +[[nodiscard]] std::optional DecodeAndn(const std::uint8_t* data, std::size_t length); +void EmitAndn(StubBodyBuilder& body, const AndnOperands& operands); + +} + +#endif diff --git a/core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp b/core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp index 112fed6de7..7b6175bdff 100644 --- a/core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp +++ b/core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -217,6 +218,8 @@ Amd64OnlyMatch Amd64OnlyInstructionMatcher::_matchClzero(const DecodedInstructio std::optional Amd64OnlyInstructionMatcher::_matchRosetta(const DecodedInstruction& instr, std::span trailing) const { if (_target != Amd64OnlyTarget::Rosetta) return std::nullopt; + if (const auto andn = DecodeAndn(instr.Data, instr.Length)) + return _trampoline("ANDN", instr.Length, _outOfLine(instr.Length, trailing, [&](StubBodyBuilder& body) { EmitAndn(body, *andn); })); if (instr.IsRdseed()) { std::vector replacement(instr.Data, instr.Data + instr.Length); replacement[instr.OpcodeOffset() + 2] &= static_cast(~(1u << X64OpcodeConstants::ModRmRegShift)); @@ -261,6 +264,10 @@ const char* Amd64OnlyInstructionMatcher::_emitOutOfLine(StubBodyBuilder& body, c body.Raw(_rdpidReplacement(instr)); return kRdpid.Name; } + if (const auto andn = _target == Amd64OnlyTarget::Rosetta ? DecodeAndn(instr.Data, instr.Length) : std::nullopt) { + EmitAndn(body, *andn); + return "ANDN"; + } if (const auto wide = _target == Amd64OnlyTarget::Rosetta ? DecodeVexWideStore(instr.Data, instr.Length) : std::nullopt) { _wideStoreLowering.EmitOutOfLine(body, *wide); return kWideStore.Name; diff --git a/core/relinker/codegen/src/x86/AndnLowering.cpp b/core/relinker/codegen/src/x86/AndnLowering.cpp new file mode 100644 index 0000000000..a99b92e4f6 --- /dev/null +++ b/core/relinker/codegen/src/x86/AndnLowering.cpp @@ -0,0 +1,138 @@ +#include +#include +#include +#include +#include + +namespace Codegen { + +namespace { + +constexpr std::int32_t kFrameSize = 144; +constexpr std::uint8_t kStack = 4; + +bool _prefix(std::uint8_t value) { + return value == 0x2E || value == 0x36 || value == 0x3E || value == 0x26 || value == 0x64 || value == 0x65 || value == 0x67; +} + +void _append32(std::vector& bytes, std::int32_t value) { + for (unsigned shift = 0; shift < 32; shift += 8) + bytes.push_back(static_cast(static_cast(value) >> shift)); +} + +std::uint8_t _rex(bool wide, std::uint8_t reg, std::uint8_t rm) { + return static_cast(0x40 | (wide ? 8 : 0) | ((reg & 8) >> 1) | ((rm & 8) >> 3)); +} + +void _register(StubBodyBuilder& body, std::uint8_t opcode, bool wide, std::uint8_t reg, std::uint8_t rm) { + const std::array bytes{_rex(wide, reg, rm), opcode, static_cast(0xC0 | ((reg & 7) << 3) | (rm & 7))}; + body.Raw(bytes); +} + +void _stack(StubBodyBuilder& body, std::uint8_t opcode, bool wide, std::uint8_t reg, std::int32_t displacement) { + std::vector bytes{_rex(wide, reg, kStack), opcode, static_cast(0x84 | ((reg & 7) << 3)), 0x24}; + _append32(bytes, displacement); + body.Raw(bytes); +} + +std::uint8_t _scratch(const AndnOperands& operands) { + std::array used{}; + used[kStack] = used[operands.Destination] = used[operands.InvertedSource] = true; + if (!operands.Memory) { + used[operands.Source] = true; + } else { + const auto& memory = *operands.Memory; + if (memory.Rm == 4) { + const auto base = static_cast((memory.Sib & 7) | ((memory.RexIndexBase & 1) << 3)); + const auto index = static_cast(((memory.Sib >> 3) & 7) | ((memory.RexIndexBase & 2) << 2)); + if (memory.Mod != 0 || (memory.Sib & 7) != 5) used[base] = true; + if (index != kStack) used[index] = true; + } else if (!memory.RipRelative) { + used[memory.Rm | ((memory.RexIndexBase & 1) << 3)] = true; + } + } + for (const auto reg : {11, 10, 9, 8, 0, 1, 2, 3, 5, 6, 7, 12, 13, 14, 15}) + if (!used[reg]) return static_cast(reg); + throw CodegenException("ANDN has no scratch register"); +} + +void _memory(StubBodyBuilder& body, const AndnOperands& operands, std::uint8_t scratch) { + const auto& memory = *operands.Memory; + auto displacement = static_cast(memory.Displacement); + if (memory.StackBase) displacement += kFrameSize; + const bool address32 = std::find(memory.Prefixes.begin(), memory.Prefixes.end(), 0x67) != memory.Prefixes.end(); + if (!address32 && displacement > std::numeric_limits::max()) + throw CodegenException("ANDN stack displacement does not fit after spilling"); + const auto mod = memory.StackBase ? 2 : memory.Mod; + auto bytes = memory.Prefixes; + bytes.push_back(static_cast(_rex(operands.Wide, scratch, 0) | memory.RexIndexBase)); + bytes.push_back(0x23); + bytes.push_back(static_cast((mod << 6) | ((scratch & 7) << 3) | memory.Rm)); + if (memory.Rm == 4) bytes.push_back(memory.Sib); + const auto offset = bytes.size(); + if (mod == 1) bytes.push_back(static_cast(displacement)); + if (mod == 2 || (mod == 0 && (memory.RipRelative || (memory.Rm == 4 && (memory.Sib & 7) == 5)))) + _append32(bytes, static_cast(displacement)); + if (memory.RipRelative) + body.RipRelative(bytes, offset, static_cast(memory.InstructionLength) + memory.Displacement); + else + body.Raw(bytes); +} + +} + +std::optional DecodeAndn(const std::uint8_t* data, const std::size_t length) { + std::size_t vex = 0; + while (vex < length && _prefix(data[vex])) ++vex; + if (vex + 5 > length || data[vex] != 0xC4 || (data[vex + 1] & 0x1F) != 2 || data[vex + 3] != 0xF2) + return std::nullopt; + if ((data[vex + 2] & 7) != 0) + throw CodegenException("ANDN requires VEX.L=0 and VEX.pp=0"); + const auto extensions = static_cast(~data[vex + 1]); + const auto modrm = data[vex + 4]; + AndnOperands operands{ + static_cast(((modrm >> 3) & 7) | ((extensions & 0x80) >> 4)), + static_cast((~data[vex + 2] >> 3) & 15), + static_cast((modrm & 7) | ((extensions & 0x20) >> 2)), + (data[vex + 2] & 0x80) != 0, std::nullopt}; + if ((modrm >> 6) == 3) { + if (vex + 5 != length) throw CodegenException("ANDN register operand has trailing bytes"); + } else { + const auto rex = static_cast(((extensions & 0x20) >> 5) | ((extensions & 0x40) >> 5)); + operands.Memory = DecodeMemoryOperand(data, length, vex + 4, rex, {data, data + vex}); + if (vex + 4 + operands.Memory->EncodedSize != length) + throw CodegenException("ANDN memory operand has trailing bytes"); + } + return operands; +} + +void EmitAndn(StubBodyBuilder& body, const AndnOperands& operands) { + const auto scratch = _scratch(operands); + _stack(body, 0x8D, true, kStack, -kFrameSize); + _stack(body, 0x89, true, scratch, 0); + if (!operands.Memory && operands.Source == kStack) { + _stack(body, 0x8D, true, scratch, kFrameSize); + _stack(body, 0x89, true, scratch, 8); + } + if (operands.InvertedSource == kStack) + _stack(body, 0x8D, true, scratch, kFrameSize); + else + _register(body, 0x8B, operands.Wide, scratch, operands.InvertedSource); + _register(body, 0xF7, operands.Wide, 2, scratch); + if (operands.Memory) + _memory(body, operands, scratch); + else if (operands.Source == kStack) + _stack(body, 0x23, operands.Wide, scratch, 8); + else + _register(body, 0x23, operands.Wide, scratch, operands.Source); + if (operands.Destination == kStack) + _stack(body, 0x89, operands.Wide, scratch, 8); + else + _register(body, 0x8B, operands.Wide, operands.Destination, scratch); + _stack(body, 0x8B, true, scratch, 0); + _stack(body, 0x8D, true, kStack, kFrameSize); + if (operands.Destination == kStack) + _stack(body, 0x8B, operands.Wide, kStack, 8 - kFrameSize); +} + +} diff --git a/core/relinker/codegen/tests/AndnLoweringTests.cpp b/core/relinker/codegen/tests/AndnLoweringTests.cpp new file mode 100644 index 0000000000..6925e2e0b5 --- /dev/null +++ b/core/relinker/codegen/tests/AndnLoweringTests.cpp @@ -0,0 +1,245 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#if (defined(__linux__) || defined(__APPLE__)) && defined(__x86_64__) +#include +#endif + +namespace { + +using Bytes = std::vector; + +void require(bool condition, const char* message) { + if (!condition) throw std::runtime_error(message); +} + +Bytes instruction(bool wide, std::uint8_t dst, std::uint8_t first, std::uint8_t second) { + return {0xC4, static_cast(0xE2 ^ ((dst & 8) << 4) ^ ((second & 8) << 2)), + static_cast((wide ? 0x80 : 0) | ((~first & 15) << 3)), 0xF2, + static_cast(0xC0 | ((dst & 7) << 3) | (second & 7))}; +} + +void decoding() { + for (bool wide : {false, true}) { + for (std::uint8_t dst = 0; dst < 16; ++dst) { + for (std::uint8_t first = 0; first < 16; ++first) { + for (std::uint8_t second = 0; second < 16; ++second) { + const auto bytes = instruction(wide, dst, first, second); + const auto operands = Codegen::DecodeAndn(bytes.data(), bytes.size()); + require(operands && operands->Wide == wide && operands->Destination == dst && operands->InvertedSource == first && operands->Source == second && !operands->Memory, "ANDN register decoding changed an operand"); + } + } + } + } + const Bytes memory{0x64, 0x67, 0xC4, 0x02, 0xA8, 0xF2, 0x8C, 0xAC, 0x10, 0, 0, 0}; + const auto operands = Codegen::DecodeAndn(memory.data(), memory.size()); + require(operands && operands->Destination == 9 && operands->InvertedSource == 10 && operands->Memory && operands->Memory->RexIndexBase == 3 && operands->Memory->Prefixes == Bytes{0x64, 0x67}, "ANDN memory extensions or prefixes were lost"); + for (const Bytes bytes : {Bytes{0x90}, Bytes{0xC4, 0xE2, 0xF0}, Bytes{0xC4, 0xE1, 0xF0, 0xF2, 0xC0}}) + require(!Codegen::DecodeAndn(bytes.data(), bytes.size()), "Unrelated or truncated opcode matched ANDN"); + for (const Bytes bytes : {Bytes{0xC4, 0xE2, 0xF4, 0xF2, 0xC0}, Bytes{0xC4, 0xE2, 0xF1, 0xF2, 0xC0}, Bytes{0xC4, 0xE2, 0xF0, 0xF2, 0x84}}) { + bool rejected = false; + try { (void)Codegen::DecodeAndn(bytes.data(), bytes.size()); } + catch (const Codegen::CodegenException&) { rejected = true; } + require(rejected, "Malformed ANDN was accepted"); + } + const Bytes overflow{0xC4, 0xE2, 0xF0, 0xF2, 0x84, 0x24, 0xFF, 0xFF, 0xFF, 0x7F}; + bool rejected = false; + try { + Codegen::StubBodyBuilder body; + Codegen::EmitAndn(body, *Codegen::DecodeAndn(overflow.data(), overflow.size())); + } catch (const Codegen::CodegenException&) { rejected = true; } + require(rejected, "An overflowing stack displacement was silently wrapped"); +} + +void matching() { + const auto bytes = instruction(true, 0, 1, 6); + const auto intel = Codegen::MakeAmd64OnlyInstructionMatcher(); + const auto rosetta = Codegen::MakeAmd64OnlyInstructionMatcher(Codegen::Amd64OnlyTarget::Rosetta); + require(!intel->Match(bytes.data(), bytes.size()), "Intel mode rewrote ANDN"); + const auto match = rosetta->Match(bytes.data(), bytes.size()); + require(match && match->InstructionName == "ANDN" && match->Lowering == Codegen::Amd64OnlyLowering::Trampoline && !match->Optional, "Rosetta mode did not require an ANDN trampoline"); + const std::array, 2> sequence{bytes, bytes}; + require(rosetta->MatchSequence(sequence, {}).has_value(), "An ANDN sequence could not move out of line"); +} + +#if (defined(__linux__) || defined(__APPLE__)) && defined(__x86_64__) + +struct State { + std::array Input{}; + std::array Output{}; + std::uint64_t Flags = 0; + std::array RedZone{}; +}; + +class Execution { +public: + Execution(const Bytes& site, std::uint64_t memoryValue) { + auto operands = Codegen::DecodeAndn(site.data(), site.size()); + require(operands.has_value(), "Execution site was not ANDN"); + for (auto reg : {3, 5, 12, 13, 14, 15}) push(static_cast(reg)); + memory(0x8D, 4, 4, -128); + rip({0x48, 0x89, 0x3D}, 0); + rip({0x48, 0x89, 0x25}, 8); + memory(0x89, 4, 7, 32); + bytes.insert(bytes.end(), {0x48, 0xB8}); + append64(kSentinel); + for (int displacement = -128; displacement < 0; displacement += 8) + memory(0x89, 0, 4, displacement); + memory(0x8B, 0, 7, 16 * 8); + memory(0x89, 0, 4, 64); + for (std::uint8_t reg = 0; reg < 16; ++reg) + if (reg != 4 && reg != 7) memory(0x8B, reg, 7, reg * 8); + memory(0x8B, 7, 7, 7 * 8); + const auto siteOffset = bytes.size(); + if (operands->Memory && operands->Memory->RipRelative) { + auto relocated = site; + const auto displacement = static_cast(4096 + 24 - (siteOffset + site.size())); + std::memcpy(relocated.data() + relocated.size() - 4, &displacement, sizeof(displacement)); + operands = Codegen::DecodeAndn(relocated.data(), relocated.size()); + } + Codegen::StubBodyBuilder builder; + Codegen::EmitAndn(builder, *operands); + auto body = builder.Finish(); + const auto stubOffset = bytes.size(); + Codegen::ApplyStubRelocations(body.Bytes, body.Relocations, siteOffset, stubOffset, 0); + bytes.insert(bytes.end(), body.Bytes.begin(), body.Bytes.begin() + static_cast(body.ReturnBranchOffset)); + rip({0x48, 0x89, 0x25}, 16); + rip({0x48, 0x8B, 0x25}, 8); + memory(0x8D, 4, 4, -256); + for (std::uint8_t reg = 0; reg < 16; ++reg) if (reg != 4) push(reg); + bytes.push_back(0x9C); + rip({0x48, 0x8B, 0x3D}, 0); + memory(0x8B, 0, 4, 0); + memory(0x89, 0, 7, 32 * 8); + int offset = 8; + for (int reg = 15; reg >= 0; --reg) { + if (reg == 4) continue; + memory(0x8B, 0, 4, offset); + memory(0x89, 0, 7, (16 + reg) * 8); + offset += 8; + } + rip({0x48, 0x8B, 0x05}, 16); + memory(0x89, 0, 7, 20 * 8); + for (int slot = 0; slot < 16; ++slot) { + memory(0x8B, 0, 4, 256 + 128 - 128 + slot * 8); + memory(0x89, 0, 7, (33 + slot) * 8); + } + rip({0x48, 0x8B, 0x25}, 8); + memory(0x8D, 4, 4, 128); + for (auto reg : {15, 14, 13, 12, 5, 3}) pop(static_cast(reg)); + bytes.push_back(0xC3); + require(bytes.size() < 4096, "Execution wrapper exceeded its code page"); + code = static_cast(mmap(nullptr, 8192, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0)); + require(code != MAP_FAILED, "Cannot allocate execution test pages"); + std::memcpy(code, bytes.data(), bytes.size()); + std::memcpy(code + 4096 + 24, &memoryValue, sizeof(memoryValue)); + require(mprotect(code, 4096, PROT_READ | PROT_EXEC) == 0, "Cannot make ANDN test code executable"); + } + + ~Execution() { if (code != MAP_FAILED) munmap(code, 8192); } + + void Run(State& state) const { reinterpret_cast(code)(&state); } + const void* Memory() const { return code + 4096 + 24; } + static constexpr std::uint64_t kSentinel = 0xBADC0FFEE0DDF00Dull; + +private: + Bytes bytes; + std::uint8_t* code = reinterpret_cast(MAP_FAILED); + + void append32(std::int32_t value) { + for (unsigned shift = 0; shift < 32; shift += 8) bytes.push_back(static_cast(static_cast(value) >> shift)); + } + void append64(std::uint64_t value) { + for (unsigned shift = 0; shift < 64; shift += 8) bytes.push_back(static_cast(value >> shift)); + } + void rip(std::initializer_list opcode, int slot) { + bytes.insert(bytes.end(), opcode); + append32(static_cast(4096 + slot - (bytes.size() + 4))); + } + void push(std::uint8_t reg) { + if (reg >= 8) bytes.push_back(0x41); + bytes.push_back(static_cast(0x50 | (reg & 7))); + } + void pop(std::uint8_t reg) { + if (reg >= 8) bytes.push_back(0x41); + bytes.push_back(static_cast(0x58 | (reg & 7))); + } + void memory(std::uint8_t opcode, std::uint8_t reg, std::uint8_t base, int displacement) { + bytes.insert(bytes.end(), {static_cast(0x48 | ((reg & 8) >> 1) | ((base & 8) >> 3)), opcode, + static_cast(0x80 | ((reg & 7) << 3) | (base & 7))}); + if ((base & 7) == 4) bytes.push_back(0x24); + append32(displacement); + } +}; + +void check(const State& state, const Codegen::AndnOperands& operands, std::uint64_t second) { + const auto mask = operands.Wide ? ~std::uint64_t{0} : 0xFFFFFFFFull; + const auto expected = (~state.Input[operands.InvertedSource] & second) & mask; + for (std::size_t reg = 0; reg < 16; ++reg) + require(state.Output[reg] == (reg == operands.Destination ? expected : state.Input[reg]), "ANDN changed a source/scratch register or produced an incorrect result"); + const auto flags = (expected == 0 ? 0x40u : 0u) | ((expected >> (operands.Wide ? 63 : 31)) != 0 ? 0x80u : 0u); + require((state.Flags & 0x8C1u) == flags, "ANDN did not preserve its defined flag semantics"); + for (const auto value : state.RedZone) require(value == Execution::kSentinel, "ANDN overwrote the caller's red zone"); +} + +State initialState() { + State state; + for (std::size_t reg = 0; reg < 16; ++reg) state.Input[reg] = 0xABCD123456789000ull + reg * 0x1234567ull; + return state; +} + +void execution() { + for (bool wide : {false, true}) { + for (std::uint8_t dst = 0; dst < 16; ++dst) { + for (const auto pair : {std::pair{1, 6}, {dst, 10}, {11, dst}, {dst, dst}, {4, 9}, {9, 4}}) { + const auto site = instruction(wide, dst, pair.first, pair.second); + Execution executable(site, 0); + for (int variant = 0; variant < 3; ++variant) { + auto state = initialState(); + if (variant == 1) state.Input.fill(0); + if (variant == 2) state.Input.fill(~std::uint64_t{0}); + executable.Run(state); + check(state, *Codegen::DecodeAndn(site.data(), site.size()), state.Input[pair.second]); + } + } + } + for (const Bytes tail : {Bytes{0x06}, Bytes{0x46, 0x10}, Bytes{0x84, 0x24, 0x40, 0, 0, 0}, Bytes{0x04, 0xCA}, Bytes{0x85, 0x10, 0, 0, 0}, Bytes{0x05, 0, 0, 0, 0}}) { + Bytes site{0xC4, 0xE2, static_cast(wide ? 0xF0 : 0x70), 0xF2}; + site.insert(site.end(), tail.begin(), tail.end()); + constexpr std::uint64_t value = 0xFEDCBA9876543210ull; + Execution executable(site, value); + auto state = initialState(); + state.Input[6] = reinterpret_cast(executable.Memory()) - (tail[0] == 0x46 ? 16 : 0); + state.Input[2] = reinterpret_cast(executable.Memory()) - 3 * 8; + state.Input[1] = 3; + state.Input[5] = reinterpret_cast(executable.Memory()) - 16; + state.Output[0] = value; + executable.Run(state); + check(state, *Codegen::DecodeAndn(site.data(), site.size()), value); + } + } +} +#else +void execution() { std::cout << "ANDN execution checks require a Linux/macOS x86-64 build\n"; } +#endif + +} + +int main() { + try { + decoding(); + matching(); + execution(); + std::cout << "ANDN lowering checks passed\n"; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/relinker/relinker/tests/test_to_rosetta.py b/core/relinker/relinker/tests/test_to_rosetta.py index 5d35e01866..72bc4640d8 100644 --- a/core/relinker/relinker/tests/test_to_rosetta.py +++ b/core/relinker/relinker/tests/test_to_rosetta.py @@ -16,17 +16,17 @@ SITES = RDSEED + CLWB + b"\xc3" LOWERED = RDRAND + NOP4 + b"\xc3" -def executable(): +def executable(code=SITES): image = fixture() struct.pack_into("` | Load the Linux executable at this fixed address, given in decimal or hexadecimal (`0x`): every virtual address of the image is offset by it, so virtual address 0 is placed at the address, as the console places an executable. Requires a 64 KiB-aligned address from `0x10000` through `0x7fffffff0000` that is a multiple of every `PT_LOAD` alignment, and an image that ends in the user address space. The executable is written as `ET_EXEC`, without ASLR. Bundled modules keep their normal placement. Conflicts with `--windows`, `--macos` and `--lazy-binding`. | | `--to-intel` | Convert supported AMD-only instructions in the executable and bundled modules. Unsupported instructions or unreachable conversion stubs cause an error. | -| `--to-rosetta` | Convert as `--to-intel` does and also lower what Rosetta lacks: RDSEED becomes RDRAND, CLWB a NOP, and RDPID returns processor 0. Implies `--to-intel`. Independent of `--macos`: `--macos` alone leaves these instructions as they are. Conflicts with `--windows`. | +| `--to-rosetta` | Convert as `--to-intel` does and also lower what Rosetta lacks: ANDN becomes scalar NOT/AND, RDSEED becomes RDRAND, CLWB a NOP, and RDPID returns processor 0. Implies `--to-intel`. Independent of `--macos`: `--macos` alone leaves these instructions as they are. Conflicts with `--windows`. | | `unused-filter=0` | Keep all imported NID references. | | `unused-filter=1` | Filter unused non-PLT imports using control-flow and GOT access analysis, treating address-taken code as reachable; preserve PLT imports. | | `unused-filter=2` | Apply strict unused-import analysis and compact the PLT. Unsupported analysis cases cause an error. | From 868d8bd24c457380ed5cfe227318ad9e21ad40af Mon Sep 17 00:00:00 2001 From: alosta Date: Wed, 14 Oct 2026 12:39:47 +0100 Subject: [PATCH 0301/1000] fix(libSceNgs2): support listener transforms in sceNgs2GeomCalcListener Restore world-to-listener matrix construction instead of rejecting non-default orientation, translation or flags. - Build an orthonormal row-major matrix from position, orient_front and orient_up; apply the right-handed axis flip for flag 1. - Store the negated dotted translation in each matrix row. - Validate unknown flags, invalid or non-finite input, short orientation axes, and unrepresentable translation without changing the output. - Preserve finite velocity and positive finite sound speed. - Update NGS2 geometry tests and the libSceNgs2 technical debt note. AI assistance: yes --- core/libs/prx/libSceNgs2.native/src/Geom.cpp | 49 ++++++++++-- .../prx/libSceNgs2.native/tests/Ngs2Geom.cpp | 79 ++++++++----------- docs/dev/debt/libSceNgs2.native.md | 2 +- 3 files changed, 75 insertions(+), 55 deletions(-) diff --git a/core/libs/prx/libSceNgs2.native/src/Geom.cpp b/core/libs/prx/libSceNgs2.native/src/Geom.cpp index f8ab93148e..f6049c0968 100644 --- a/core/libs/prx/libSceNgs2.native/src/Geom.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Geom.cpp @@ -1,26 +1,59 @@ +#include #include #include +#include #include "prx/libc/include/General.hpp" #include "Ngs2Internal.hpp" static constexpr float DEFAULT_SOUND_SPEED = 343.0f; +namespace { + +using Vector = std::array; + +Vector ReadVector(const Ngs2GeomVector& value) { + if (!std::isfinite(value.x) || !std::isfinite(value.y) || !std::isfinite(value.z)) APS5_INVALID_ARG_EX; + return {value.x, value.y, value.z}; +} + +double Dot(const Vector& left, const Vector& right) { + return left[0] * right[0] + left[1] * right[1] + left[2] * right[2]; +} + +Vector Unit(Vector value) { + const double length = std::hypot(value[0], value[1], value[2]); + if (!(length > 0.0)) APS5_INVALID_ARG_EX; + for (auto& component : value) component /= length; + return value; +} + +} + int Ngs2GeomCalcListener(const Ngs2GeomListenerParam* param, Ngs2GeomListenerWork* outWork, std::uint32_t flags) { if (param == nullptr) APS5_INVALID_ARG_EX; if (outWork == nullptr) return SCE_NGS2_ERROR_INVALID_OUT_ADDRESS; - if (flags != 0) throw std::runtime_error("NGS2: listener coordinate flags are unsupported"); - if (param->position.x != 0.0f || param->position.y != 0.0f || param->position.z != 0.0f) - throw std::runtime_error("NGS2: listener translation is unsupported"); - if (param->orient_front.x != 0.0f || param->orient_front.y != 0.0f || param->orient_front.z != 1.0f || - param->orient_up.x != 0.0f || param->orient_up.y != 1.0f || param->orient_up.z != 0.0f) - throw std::runtime_error("NGS2: non-default listener orientation is unsupported"); - if (!std::isfinite(param->velocity.x) || !std::isfinite(param->velocity.y) || !std::isfinite(param->velocity.z)) APS5_INVALID_ARG_EX; + if (flags > 1) throw std::runtime_error("NGS2: unknown listener coordinate flags"); if (!std::isfinite(param->sound_speed) || param->sound_speed <= 0.0f) APS5_INVALID_ARG_EX; + const Vector position = ReadVector(param->position); + ReadVector(param->velocity); + const Vector front = Unit(ReadVector(param->orient_front)); + Vector up = Unit(ReadVector(param->orient_up)); + Vector right = Unit({up[1] * front[2] - up[2] * front[1], up[2] * front[0] - up[0] * front[2], up[0] * front[1] - up[1] * front[0]}); + up = {front[1] * right[2] - front[2] * right[1], front[2] * right[0] - front[0] * right[2], front[0] * right[1] - front[1] * right[0]}; + if (flags == 1) for (auto& component : right) component = -component; + const std::array axes{right, up, front}; Ngs2GeomListenerWork work{}; - for (std::size_t i = 0; i < 4; i++) work.matrix[i][i] = 1.0f; + for (std::size_t row = 0; row < axes.size(); row++) { + for (std::size_t column = 0; column < 3; column++) work.matrix[row][column] = static_cast(axes[row][column]); + const double translation = -Dot(axes[row], position); + if (std::abs(translation) > std::numeric_limits::max()) APS5_INVALID_ARG_EX; + work.matrix[row][3] = static_cast(translation); + } + work.matrix[3][3] = 1.0f; work.velocity = param->velocity; work.sound_speed = param->sound_speed; + work.coordinate = flags; *outWork = work; return SCE_NGS2_OK; } diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Geom.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Geom.cpp index 91a377793b..67d472244e 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2Geom.cpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Geom.cpp @@ -60,22 +60,15 @@ static Ngs2GeomListenerParam Listener() { static void RequireMatrix(const Ngs2GeomListenerWork& work, const float (&expected)[4][4]) { for (std::size_t row = 0; row < 4; row++) { - for (std::size_t column = 0; column < 4; column++) Require(work.matrix[row][column] == expected[row][column]); + for (std::size_t column = 0; column < 4; column++) Require(std::abs(work.matrix[row][column] - expected[row][column]) < 0.00001f); } } -static void TestDefaultListener() { +static void TestListenerTransform() { auto param = Listener(); - Ngs2GeomListenerWork work; - std::memset(&work, 0x5a, sizeof(work)); - Require(sceNgs2GeomCalcListener(¶m, &work, 0) == SCE_NGS2_OK); - RequireMatrix(work, {{1, 0, 0, 0}, {0, 1, 0, 0}, {0, 0, 1, 0}, {0, 0, 0, 1}}); - Require(work.velocity.x == 0.0f && work.velocity.y == 0.0f && work.velocity.z == 0.0f); - Require(work.sound_speed == SoundSpeed && work.coordinate == 0); - Require(work.reserved[0] == 0 && work.reserved[1] == 0 && work.reserved[2] == 0); - param.velocity = {2.0f, -3.0f, 4.0f}; param.sound_speed = 1500.0f; + Ngs2GeomListenerWork work; std::memset(&work, 0x5a, sizeof(work)); Require(sceNgs2GeomCalcListener(¶m, &work, 0) == SCE_NGS2_OK); RequireMatrix(work, {{1, 0, 0, 0}, {0, 1, 0, 0}, {0, 0, 1, 0}, {0, 0, 0, 1}}); @@ -83,11 +76,25 @@ static void TestDefaultListener() { Require(work.sound_speed == 1500.0f && work.coordinate == 0); Require(work.reserved[0] == 0 && work.reserved[1] == 0 && work.reserved[2] == 0); - param.position = {-0.0f, -0.0f, -0.0f}; - param.orient_front = {-0.0f, -0.0f, 1.0f}; - param.orient_up = {-0.0f, 1.0f, -0.0f}; + param.position = {3.0f, 4.0f, 5.0f}; Require(sceNgs2GeomCalcListener(¶m, &work, 0) == SCE_NGS2_OK); - RequireMatrix(work, {{1, 0, 0, 0}, {0, 1, 0, 0}, {0, 0, 1, 0}, {0, 0, 0, 1}}); + RequireMatrix(work, {{1, 0, 0, -3}, {0, 1, 0, -4}, {0, 0, 1, -5}, {0, 0, 0, 1}}); + + param.orient_front = {2.0f, 0.0f, 0.0f}; + param.orient_up = {1.0f, 3.0f, 0.0f}; + Require(sceNgs2GeomCalcListener(¶m, &work, 0) == SCE_NGS2_OK); + RequireMatrix(work, {{0, 0, -1, 5}, {0, 1, 0, -4}, {1, 0, 0, -3}, {0, 0, 0, 1}}); + + param.orient_front = {0.0f, 0.0f, -2.0f}; + param.orient_up = {0.0f, 3.0f, 0.0f}; + Require(sceNgs2GeomCalcListener(¶m, &work, 1) == SCE_NGS2_OK); + RequireMatrix(work, {{1, 0, 0, -3}, {0, 1, 0, -4}, {0, 0, -1, 5}, {0, 0, 0, 1}}); + Require(work.coordinate == 1); + + param.orient_front = {0.0f, 0.0f, std::numeric_limits::max()}; + param.orient_up = {0.0f, std::numeric_limits::min(), 0.0f}; + Require(sceNgs2GeomCalcListener(¶m, &work, 0) == SCE_NGS2_OK); + RequireMatrix(work, {{1, 0, 0, -3}, {0, 1, 0, -4}, {0, 0, 1, -5}, {0, 0, 0, 1}}); } static void RejectListener(const Ngs2GeomListenerParam* param, std::uint32_t flags) { @@ -105,30 +112,12 @@ static void RejectListener(const Ngs2GeomListenerParam* param, std::uint32_t fla Require(std::memcmp(before, &work, sizeof(work)) == 0); } -static void TestUnsupportedListener() { - auto param = Listener(); - for (const auto flags : {1u, 2u, 0xffffffffu}) RejectListener(¶m, flags); - for (const auto vector : {&Ngs2GeomListenerParam::position, &Ngs2GeomListenerParam::orient_front, &Ngs2GeomListenerParam::orient_up}) { - for (const auto component : {&Ngs2GeomVector::x, &Ngs2GeomVector::y, &Ngs2GeomVector::z}) { - for (const float value : {-1.0f, 0.0f, 1.0f, 2.0f, std::numeric_limits::min(), std::numeric_limits::max()}) { - param = Listener(); - auto& field = (param.*vector).*component; - if (field == value) continue; - field = value; - RejectListener(¶m, 0); - } - param = Listener(); - auto& field = (param.*vector).*component; - field = std::nextafter(field, 2.0f); - RejectListener(¶m, 0); - } - } -} - static void TestInvalidListener() { auto param = Listener(); Require(sceNgs2GeomCalcListener(¶m, nullptr, 0) == SCE_NGS2_ERROR_INVALID_OUT_ADDRESS); RejectListener(nullptr, 0); + RejectListener(¶m, 2); + RejectListener(¶m, 0xffffffffu); for (const float value : {0.0f, -1.0f, std::numeric_limits::infinity(), std::numeric_limits::quiet_NaN()}) { param.sound_speed = value; RejectListener(¶m, 0); @@ -142,24 +131,22 @@ static void TestInvalidListener() { param = Listener(); param.orient_up = {}; RejectListener(¶m, 0); - for (const auto vector : {&Ngs2GeomListenerParam::position, &Ngs2GeomListenerParam::orient_front, - &Ngs2GeomListenerParam::orient_up, &Ngs2GeomListenerParam::velocity}) { - for (const auto component : {&Ngs2GeomVector::x, &Ngs2GeomVector::y, &Ngs2GeomVector::z}) { - for (const float value : {std::numeric_limits::infinity(), -std::numeric_limits::infinity(), std::numeric_limits::quiet_NaN()}) { - param = Listener(); - (param.*vector).*component = value; - RejectListener(¶m, 0); - } - } - } + param = Listener(); + param.position.x = std::numeric_limits::infinity(); + RejectListener(¶m, 0); + param = Listener(); + param.velocity.z = std::numeric_limits::quiet_NaN(); + RejectListener(¶m, 0); + param = Listener(); + param.orient_front.y = std::numeric_limits::quiet_NaN(); + RejectListener(¶m, 0); } int main() { TestLayouts(); TestResetListenerParam(); TestResetSourceParam(); - TestDefaultListener(); - TestUnsupportedListener(); + TestListenerTransform(); TestInvalidListener(); return 0; } diff --git a/docs/dev/debt/libSceNgs2.native.md b/docs/dev/debt/libSceNgs2.native.md index 812d7348b3..ebcb495ef1 100644 --- a/docs/dev/debt/libSceNgs2.native.md +++ b/docs/dev/debt/libSceNgs2.native.md @@ -25,6 +25,6 @@ - [sceNgs2VoiceControl sampler filter](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - param `0x1000000a`, its layout and meaning follow KytyPS5 only: location 1 type 1 is an RBJ low-pass biquad at the system sample rate after resampling, scaled by `level`, a set `channel_mask` bit bypasses that channel, type 0 turns the filter off. Other locations and types throw. The voice keeps playing a filter tail after its last block until the history is zero, with outputs below 1e-20 flushed to zero (a KytyPS5 choice); the console's tail length is unknown - [sceNgs2PanInit / sceNgs2PanGetVolumeMatrix](../../../core/libs/prx/libSceNgs2.native/src/Pan.cpp) (libSceNgs2.native) - follow the PS4 firmware 11.00 `libSceNgs2.sprx`, read and checked against its output for 2, 4, 5 and 7 speakers into 1, 2, 6 and 8 channels; not yet confirmed on the PS5 library. Speakers are taken in the order L R, L R Ls Rs, L R C Ls Rs or L R C Ls Rs Lb Rb and kept in a fixed circular order, not sorted; without angles the defaults are 90/270, 40/110/250/320, 0/40/110/250/320 and 0/40/110/150/210/250/320 degrees. As in that library, defaults converted to radians keep the wrap-around entry in degrees, so angles past the last speaker pan almost fully to it. Error codes are not known for the PS5, so invalid arguments throw, including a speaker angle equal to the unit angle, on which that library loops forever. Angles so large that adding or subtracting a turn leaves them unchanged throw for the same reason - [sceNgs2GeomResetListenerParam, sceNgs2GeomResetSourceParam](../../../core/libs/prx/libSceNgs2.native/src/Geom.cpp) (libSceNgs2.native) - the structures (12, 16, 16, 60, 96, 108, 32 and 308 bytes) come from [KytyPS5](https://github.com/KytyPS5/KytyPS5)'s `ngs2.cpp` and match [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `ngs2_geom.h` field for field, but only KytyPS5 gives the behaviour - shadPS4 logs and returns success without writing the output. The defaults (orient front +Z, orient up +Y, sound speed 343, cone 1/360/1/360, rolloff 0/1000000/1/1, doppler factor, full-band, LFE and maximum level 1, two speakers and matrix format 2) are unverified on a PS5 title; KytyPS5 and iStark/PS5PCEM agree on them. PPSA15552 (Dead Cells) calls `sceNgs2GeomResetSourceParam` after creating its racks. A null output address returns `INVALID_OUT_ADDRESS` as elsewhere in the library rather than throwing as KytyPS5 does. `sceNgs2GeomApply` stays unimplemented -- [sceNgs2GeomCalcListener](../../../core/libs/prx/libSceNgs2.native/src/Geom.cpp) supports only a listener at the origin with front +Z, up +Y and flags 0. The identity matrix and preserved finite velocity and positive finite sound speed follow [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/df5b9b92be7dfd463b47756b707d6d356a9f0550/src/libs/ngs2.cpp) and [PS5PCEM](https://github.com/iStark/PS5PCEM/blob/d2534bac6325a2c61c0fa88977e5321074304484/src/hle/ngs2_geom.zig) for that case, not a PS5 measurement. Other orientations (including non-unit vectors), translations and flags throw without changing the output; the matrix convention, handedness flags and console error codes remain unverified. Nonfinite input and nonpositive sound speed also throw. Dead Cells (PPSA15552) reaches this call after the custom sampler rack in #4334, but its arguments have not been captured here. Source attenuation, panning and Doppler in `sceNgs2GeomApply` remain unimplemented +- [sceNgs2GeomCalcListener](../../../core/libs/prx/libSceNgs2.native/src/Geom.cpp) builds an orthonormal world-to-listener transform. The row-major matrix, front-axis priority and flag 1 for right-handed input follow the model described by [PS5PCEM](https://github.com/iStark/PS5PCEM/blob/d2534bac6325a2c61c0fa88977e5321074304484/docs/development/dead-cells-ngs2-2026-10-11.md); they have not been measured on PS5 hardware. Velocity stays in world coordinates and positive finite sound speed is preserved. Unknown flags, invalid orientation, nonfinite input and an unrepresentable translation throw without changing the output; console error codes and handling of nonpositive sound speed are unknown. Dead Cells (PPSA15552) reaches this call after the custom sampler rack in #4334. This does not implement source attenuation, panning or Doppler in `sceNgs2GeomApply`. - [NGS2 custom sampler rack](../../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (libSceNgs2.native, rack 0x4001) - implements bounded support for the custom sampler rack and its built-in module 0x13 (Custom Voice Filter), unblocking PPSA15552 (Dead Cells) startup. Structure layouts come from [shadPS4's ngs2_custom.h](https://raw.githubusercontent.com/shadps4-emu/shadPS4/main/src/core/libraries/ngs2/ngs2_custom.h) (OrbisNgs2CustomVoiceFilterParam, 40 bytes) and live trace correlation from [prosper's hle_audio.cpp](https://raw.githubusercontent.com/mattias800/prosper/main/prosper/src/hle/audio/hle_audio.cpp) (0x40001300 command and parameters like 10000.0f and 0.707f). Routes parameter updates via the 0x40001300 command family into the Biquad filter engine, supporting only filter type 1; other filter types and unmodeled configurations throw. - [sceNgs2VoiceGetState custom sampler](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native, rack 0x4001) - only the size and one field come from a title. PPSA15552 (Dead Cells) calls it on custom sampler voices with a 48-byte buffer at two sites (eboot `0x1740f90`, `0x1741009`) and reads only the 64-bit value at offset 16: the first site keeps it and the second subtracts the kept value, so it is filled with the voice's decoded sample count. Other sizes return `INVALID_OUT_SIZE` as for the other racks. The state flags fill the common voice state; the remaining fields stay zero. shadPS4's `OrbisNgs2CustomSamplerVoiceState` is about 80 bytes and does not match the size this title passes, so the field names and the console's value are unverified From 2f25a54b7b888d3856da3047ef39c909c19250ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Thu, 15 Oct 2026 15:03:32 +0200 Subject: [PATCH 0302/1000] docs(ngs2): cite the PS5PCEM source that exists and describe the orientation handling as implemented Co-Authored-By: Claude --- docs/dev/debt/libSceNgs2.native.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceNgs2.native.md b/docs/dev/debt/libSceNgs2.native.md index ebcb495ef1..25765ec1d8 100644 --- a/docs/dev/debt/libSceNgs2.native.md +++ b/docs/dev/debt/libSceNgs2.native.md @@ -25,6 +25,6 @@ - [sceNgs2VoiceControl sampler filter](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - param `0x1000000a`, its layout and meaning follow KytyPS5 only: location 1 type 1 is an RBJ low-pass biquad at the system sample rate after resampling, scaled by `level`, a set `channel_mask` bit bypasses that channel, type 0 turns the filter off. Other locations and types throw. The voice keeps playing a filter tail after its last block until the history is zero, with outputs below 1e-20 flushed to zero (a KytyPS5 choice); the console's tail length is unknown - [sceNgs2PanInit / sceNgs2PanGetVolumeMatrix](../../../core/libs/prx/libSceNgs2.native/src/Pan.cpp) (libSceNgs2.native) - follow the PS4 firmware 11.00 `libSceNgs2.sprx`, read and checked against its output for 2, 4, 5 and 7 speakers into 1, 2, 6 and 8 channels; not yet confirmed on the PS5 library. Speakers are taken in the order L R, L R Ls Rs, L R C Ls Rs or L R C Ls Rs Lb Rb and kept in a fixed circular order, not sorted; without angles the defaults are 90/270, 40/110/250/320, 0/40/110/250/320 and 0/40/110/150/210/250/320 degrees. As in that library, defaults converted to radians keep the wrap-around entry in degrees, so angles past the last speaker pan almost fully to it. Error codes are not known for the PS5, so invalid arguments throw, including a speaker angle equal to the unit angle, on which that library loops forever. Angles so large that adding or subtracting a turn leaves them unchanged throw for the same reason - [sceNgs2GeomResetListenerParam, sceNgs2GeomResetSourceParam](../../../core/libs/prx/libSceNgs2.native/src/Geom.cpp) (libSceNgs2.native) - the structures (12, 16, 16, 60, 96, 108, 32 and 308 bytes) come from [KytyPS5](https://github.com/KytyPS5/KytyPS5)'s `ngs2.cpp` and match [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `ngs2_geom.h` field for field, but only KytyPS5 gives the behaviour - shadPS4 logs and returns success without writing the output. The defaults (orient front +Z, orient up +Y, sound speed 343, cone 1/360/1/360, rolloff 0/1000000/1/1, doppler factor, full-band, LFE and maximum level 1, two speakers and matrix format 2) are unverified on a PS5 title; KytyPS5 and iStark/PS5PCEM agree on them. PPSA15552 (Dead Cells) calls `sceNgs2GeomResetSourceParam` after creating its racks. A null output address returns `INVALID_OUT_ADDRESS` as elsewhere in the library rather than throwing as KytyPS5 does. `sceNgs2GeomApply` stays unimplemented -- [sceNgs2GeomCalcListener](../../../core/libs/prx/libSceNgs2.native/src/Geom.cpp) builds an orthonormal world-to-listener transform. The row-major matrix, front-axis priority and flag 1 for right-handed input follow the model described by [PS5PCEM](https://github.com/iStark/PS5PCEM/blob/d2534bac6325a2c61c0fa88977e5321074304484/docs/development/dead-cells-ngs2-2026-10-11.md); they have not been measured on PS5 hardware. Velocity stays in world coordinates and positive finite sound speed is preserved. Unknown flags, invalid orientation, nonfinite input and an unrepresentable translation throw without changing the output; console error codes and handling of nonpositive sound speed are unknown. Dead Cells (PPSA15552) reaches this call after the custom sampler rack in #4334. This does not implement source attenuation, panning or Doppler in `sceNgs2GeomApply`. +- [sceNgs2GeomCalcListener](../../../core/libs/prx/libSceNgs2.native/src/Geom.cpp) builds an orthonormal world-to-listener transform. The row-major matrix, front-axis priority and flag 1 for right-handed input follow [PS5PCEM](https://github.com/iStark/PS5PCEM/blob/d2534bac6325a2c61c0fa88977e5321074304484/src/hle/ngs2_geom.zig); they have not been measured on PS5 hardware. Velocity stays in world coordinates and positive finite sound speed is preserved. Orientation vectors are normalised and up is made perpendicular to front, so non-unit and slightly tilted vectors are accepted; unknown flags, zero or parallel orientation vectors, nonfinite input and an unrepresentable translation throw without changing the output; console error codes are unknown, and a nonpositive sound speed throws where PS5PCEM and [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/df5b9b92be7dfd463b47756b707d6d356a9f0550/src/libs/ngs2.cpp) default it to 343. KytyPS5 always writes the identity matrix, so the transform rests on PS5PCEM alone. Dead Cells (PPSA15552) reaches this call after the custom sampler rack in #4334. This does not implement source attenuation, panning or Doppler in `sceNgs2GeomApply`. - [NGS2 custom sampler rack](../../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (libSceNgs2.native, rack 0x4001) - implements bounded support for the custom sampler rack and its built-in module 0x13 (Custom Voice Filter), unblocking PPSA15552 (Dead Cells) startup. Structure layouts come from [shadPS4's ngs2_custom.h](https://raw.githubusercontent.com/shadps4-emu/shadPS4/main/src/core/libraries/ngs2/ngs2_custom.h) (OrbisNgs2CustomVoiceFilterParam, 40 bytes) and live trace correlation from [prosper's hle_audio.cpp](https://raw.githubusercontent.com/mattias800/prosper/main/prosper/src/hle/audio/hle_audio.cpp) (0x40001300 command and parameters like 10000.0f and 0.707f). Routes parameter updates via the 0x40001300 command family into the Biquad filter engine, supporting only filter type 1; other filter types and unmodeled configurations throw. - [sceNgs2VoiceGetState custom sampler](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native, rack 0x4001) - only the size and one field come from a title. PPSA15552 (Dead Cells) calls it on custom sampler voices with a 48-byte buffer at two sites (eboot `0x1740f90`, `0x1741009`) and reads only the 64-bit value at offset 16: the first site keeps it and the second subtracts the kept value, so it is filled with the voice's decoded sample count. Other sizes return `INVALID_OUT_SIZE` as for the other racks. The state flags fill the common voice state; the remaining fields stay zero. shadPS4's `OrbisNgs2CustomSamplerVoiceState` is about 80 bytes and does not match the size this title passes, so the field names and the console's value are unverified From 758e69b56789d76761ca707452bab9a490f6895e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Ortellado?= Date: Wed, 14 Oct 2026 09:14:59 -0300 Subject: [PATCH 0303/1000] fix(tools): preserve main sync reports on limited encodings --- tools/check_main_sync.py | 2 ++ tools/tests/test_check_main_sync.py | 35 +++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/tools/check_main_sync.py b/tools/check_main_sync.py index f089a321d9..fa3265d310 100644 --- a/tools/check_main_sync.py +++ b/tools/check_main_sync.py @@ -1,6 +1,7 @@ import argparse import re import subprocess +import sys from pathlib import Path PULL_REQUEST = re.compile(r"Merge pull request #(\d+)") @@ -105,6 +106,7 @@ if __name__ == "__main__": data = report(args.repo, base, args.head) except RuntimeError as error: parser.error(str(error)) + sys.stdout.reconfigure(errors="backslashreplace") print(render(data)) if data["conflicts"]: raise SystemExit(1) diff --git a/tools/tests/test_check_main_sync.py b/tools/tests/test_check_main_sync.py index 14f70e94fc..26e72e7cc0 100644 --- a/tools/tests/test_check_main_sync.py +++ b/tools/tests/test_check_main_sync.py @@ -132,6 +132,41 @@ class CheckMainSyncTests(unittest.TestCase): self.assertEqual(result.returncode, 2) self.assertIn("no common ancestor", result.stderr) + def check_unicode_output(self, conflict): + name = "sp ace \u00e9 \u6e2c \U0001f680.txt" + self.repo.write(name, "one\ntwo\nthree\n") + self.repo.commit("add unicode path") + self.branch("feature") + self.repo.write(name, "one\nmine\nthree\n" if conflict else "zero\none\ntwo\nthree\n") + self.repo.commit("feature edit") + self.repo.git("checkout", "-q", "main") + self.repo.git("checkout", "-q", "-b", "theirs") + self.repo.write(name, "one\ntheirs\nthree\n" if conflict else "one\ntwo\nthree\nfour\n") + self.repo.commit("their edit") + self.repo.git("checkout", "-q", "main") + self.repo.merge_pull(9, "theirs") + expected = {"ascii": r"sp ace \xe9 \u6e2c \U0001f680.txt", + "cp1252": "sp ace \u00e9 " + r"\u6e2c \U0001f680.txt", + "utf-8": name} + for encoding, shown in expected.items(): + for strict in ((False,) if conflict else (False, True)): + with self.subTest(encoding=encoding, strict=strict): + result = subprocess.run([sys.executable, str(TOOL), "--repo", str(self.repo.path), + "--base", "main", "--head", "feature", *(["--strict"] if strict else [])], + capture_output=True, text=True, encoding=encoding, + env={**os.environ, "PYTHONIOENCODING": encoding + ":strict"}) + self.assertEqual(result.returncode, 1 if conflict or strict else 0, result.stderr) + self.assertIn("merge: CONFLICT" if conflict else "merge: clean", result.stdout) + self.assertIn(f" {shown}\n", result.stdout.replace("\r", "")) + self.assertIn("merged pull requests that touched them: #9", result.stdout) + self.assertNotIn("Traceback", result.stderr) + + def test_clean_unicode_report_preserves_exit_status_and_output_encoding(self): + self.check_unicode_output(False) + + def test_conflicting_unicode_report_preserves_exit_status_and_output_encoding(self): + self.check_unicode_output(True) + def test_missing_base_is_an_error_not_a_pass(self): result = subprocess.run([sys.executable, str(TOOL), "--repo", self.directory.name, "--base", "upstream/main"], capture_output=True, text=True) self.assertEqual(result.returncode, 2) From d4cce869eb12c4dc52537bf2a6cbb0b76e4f57f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Ortellado?= Date: Thu, 15 Oct 2026 10:11:54 -0300 Subject: [PATCH 0304/1000] test(tools): pin check_main_sync subprocess encoding --- tools/tests/test_check_main_sync.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tools/tests/test_check_main_sync.py b/tools/tests/test_check_main_sync.py index 26e72e7cc0..4a13a56e0a 100644 --- a/tools/tests/test_check_main_sync.py +++ b/tools/tests/test_check_main_sync.py @@ -38,7 +38,12 @@ class Repo: def run_tool(path, *args): - return subprocess.run([sys.executable, str(TOOL), "--repo", str(path), "--base", "main", *args], capture_output=True, text=True) + return subprocess.run( + [sys.executable, str(TOOL), "--repo", str(path), "--base", "main", *args], + capture_output=True, + encoding="utf-8", + env={**os.environ, "PYTHONIOENCODING": "utf-8"}, + ) class CheckMainSyncTests(unittest.TestCase): From 474ec771bf7026f792c08ee7321954c11d6f06cf Mon Sep 17 00:00:00 2001 From: SandstromPL Date: Wed, 14 Oct 2026 17:33:06 +0530 Subject: [PATCH 0305/1000] fix(agc): accept a min or max reduction sampler with anisotropic filtering A guest S# with FILTER_MODE min or max and an anisotropic XY filter threw "combines a min or max reduction with anisotropic filtering", which stops High on Life (PPSA15388) in its home base. It now decodes to a Vulkan sampler with both anisotropy and the reduction mode, as a weighted-average anisotropic S# already does. RADV programs the same S# for such a Vulkan sampler. Tamalero's gfx1030 measurement on #5156 covers a sample without derivatives through a 2x anisotropic S# over a single-level texture: min or max with anisotropic bilinear returns the bilinear minimum or maximum, and min with anisotropic point the point result. The execution test checks the bilinear case on the host GPU; the debt entry records what is not measured. Co-Authored-By: Claude Opus 5.5 --- .../Graphics/src/GuestSamplerResource.cpp | 1 - .../tests/GuestSamplerResource.cpp | 33 ++++++++++++++++--- .../tests/execution/ImageSampleReduction.cpp | 7 +++- docs/dev/debt/libSceAgcDriver.md | 2 +- 4 files changed, 36 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp index ac536cc811..413eeced6a 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp @@ -116,7 +116,6 @@ GuestSamplerResource DecodeSamplerResource(std::span words, Require(mipFilter != 2u || reductionMode == VK_SAMPLER_REDUCTION_MODE_WEIGHTED_AVERAGE_EXT, "guest sampler descriptor combines a min or max reduction with a linear mip filter, which is not implemented"); const auto aniso = isAnisoFilter(xyMagFilter) || isAnisoFilter(xyMinFilter); - Require(!aniso || reductionMode == VK_SAMPLER_REDUCTION_MODE_WEIGHTED_AVERAGE_EXT, "guest sampler descriptor combines a min or max reduction with anisotropic filtering, which is not implemented"); auto anisoRatio = 1.0f; if (aniso) { if (maxAnisoRatio > 4u) Require(false, "guest sampler descriptor uses an unknown anisotropy ratio " + std::to_string(maxAnisoRatio)); diff --git a/core/libs/prx/libSceAgcDriver/tests/GuestSamplerResource.cpp b/core/libs/prx/libSceAgcDriver/tests/GuestSamplerResource.cpp index 84542f15f4..7f5b21682c 100644 --- a/core/libs/prx/libSceAgcDriver/tests/GuestSamplerResource.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/GuestSamplerResource.cpp @@ -94,6 +94,8 @@ struct SamplerCapture { const void* next = nullptr; VkSamplerReductionMode reductionMode = VK_SAMPLER_REDUCTION_MODE_MAX_ENUM; VkSamplerCreateFlags flags = 0; + bool anisotropyEnable = false; + float maxAnisotropy = 0.0f; }; SamplerCapture samplerCapture; @@ -107,6 +109,8 @@ VKAPI_ATTR VkResult VKAPI_CALL captureCreateSampler(VkDevice, const VkSamplerCre samplerCapture.created = true; samplerCapture.next = info->pNext; samplerCapture.flags = info->flags; + samplerCapture.anisotropyEnable = info->anisotropyEnable == VK_TRUE; + samplerCapture.maxAnisotropy = info->maxAnisotropy; const auto* reduction = static_cast(info->pNext); if (reduction != nullptr && reduction->sType == VK_STRUCTURE_TYPE_SAMPLER_REDUCTION_MODE_CREATE_INFO_EXT && reduction->pNext == nullptr) samplerCapture.reductionMode = reduction->reductionMode; *sampler = fakeHandle(0x5a); @@ -174,14 +178,30 @@ void RunSamplerReductionTests(const Fields& base) { badFilterMode.filterMode = 3; rejectFields(badFilterMode, "unknown reduction filter mode 3"); Fields anisoReduction = base; - anisoReduction.filterMode = 1; - anisoReduction.xyMagFilter = 3; - anisoReduction.xyMinFilter = 3; anisoReduction.maxAnisoRatio = 2; anisoReduction.mipFilter = 1; - rejectFields(anisoReduction, "min or max reduction with anisotropic filtering"); + context.samplerAnisotropy = true; + context.limits.maxSamplerAnisotropy = 16.0f; + for (std::uint32_t mode = 1; mode < modes.size(); ++mode) { + for (const std::uint32_t filter : {2u, 3u}) { + anisoReduction.filterMode = mode; + anisoReduction.xyMagFilter = filter; + anisoReduction.xyMinFilter = filter; + const auto decoded = DecodeSamplerResource(pack(anisoReduction)); + Require(decoded.reductionMode == modes.at(mode) && decoded.anisotropyEnable && decoded.maxAnisotropy == 4.0f, "anisotropic min or max sampler decoded incorrectly"); + const auto created = createSampler(context, decoded); + Require(created.reductionMode == modes.at(mode), "an anisotropic min or max sampler did not chain its reduction mode"); + Require(created.anisotropyEnable && created.maxAnisotropy == 4.0f, "an anisotropic min or max sampler lost its anisotropy"); + Require(Sampler(context, decoded).RequiresFilterMinmax() == (filter == 3u), "anisotropic sampler min/max format requirement is wrong"); + } + } + context.samplerAnisotropy = false; + context.limits.maxSamplerAnisotropy = 1.0f; anisoReduction.filterMode = 0; Require(DecodeSamplerResource(pack(anisoReduction)).anisotropyEnable, "anisotropic weighted-average sampler was rejected"); + anisoReduction.filterMode = 1; + anisoReduction.mipFilter = 2; + rejectFields(anisoReduction, "min or max reduction with a linear mip filter"); auto compared = captured; compared.compareEnable = true; @@ -235,6 +255,11 @@ void RunSamplerReductionTests(const Fields& base) { maxLinearMip.filterMode = 2; maxLinearMip.mipFilter = 2; reject([&] { pointFiltered(pack(maxLinearMip)); }, "needs point filtering"); + Fields minAnisoPoint = minPoint; + minAnisoPoint.xyMagFilter = 2; + minAnisoPoint.xyMinFilter = 2; + minAnisoPoint.maxAnisoRatio = 1; + reject([&] { pointFiltered(pack(minAnisoPoint)); }, "needs point filtering"); } void RunNonSeamlessCubeTests(const Fields& base) { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleReduction.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleReduction.cpp index d0418ce365..a6536f8c9a 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleReduction.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleReduction.cpp @@ -31,6 +31,8 @@ constexpr std::uint32_t ReductionMin = 1; constexpr std::uint32_t ReductionMax = 2; constexpr std::uint32_t FilterPoint = 0; constexpr std::uint32_t FilterBilinear = 1; +constexpr std::uint32_t FilterAnisoBilinear = 3; +constexpr std::uint32_t Aniso2x = 1; constexpr std::uint32_t MipNone = 0; constexpr std::uint32_t MipPoint = 1; constexpr std::uint32_t MipLinear = 2; @@ -67,6 +69,7 @@ struct Sampler { std::uint32_t reduction; std::uint32_t filter; std::uint32_t mip; + std::uint32_t anisoRatio = 0; }; std::array BufferDescriptor(const void* data, std::uint32_t bytes) { @@ -89,7 +92,7 @@ std::array TextureDescriptor(std::uint32_t format, const void* } std::array SamplerDescriptor(const Sampler& sampler) { - return {0x92u | (sampler.reduction << 29u), 0x00fff000u, (sampler.filter << 20u) | (sampler.filter << 22u) | (1u << 24u) | (sampler.mip << 26u), 0u}; + return {0x92u | (sampler.anisoRatio << 9u) | (sampler.reduction << 29u), 0x00fff000u, (sampler.filter << 20u) | (sampler.filter << 22u) | (1u << 24u) | (sampler.mip << 26u), 0u}; } void FillTexels() { @@ -301,6 +304,8 @@ int main() { Run(*device, {ReductionMax, FilterPoint, MipPoint}, "max, point, point mip"); Run(*device, {ReductionMin, FilterBilinear, MipNone}, "min, bilinear, no mip"); Run(*device, {ReductionMax, FilterPoint, MipNone}, "max, point, no mip"); + Run(*device, {ReductionMin, FilterAnisoBilinear, MipPoint, Aniso2x}, "min, 2x anisotropic bilinear, point mip"); + Run(*device, {ReductionMax, FilterAnisoBilinear, MipPoint, Aniso2x}, "max, 2x anisotropic bilinear, point mip"); Gather(*device, {ReductionMin, FilterBilinear, MipPoint}, "gather4_lz, min"); Gather(*device, {ReductionMax, FilterBilinear, MipPoint}, "gather4_lz, max"); RunWithColors(*device, loaded, LoadedCode, false, "min 32_FLOAT, image_load 8_8_8_8"); diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 3edcde24ff..663a69598b 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -127,7 +127,7 @@ - [Local programs](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp) (libSceAgcDriver) that the hull header leaves at address 0 are prepared at their first draw, one artifact per tessellation configuration, instead of at registration. - [Packed 16-bit textures](../../../core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp) (libSceAgcDriver) - `5_6_5`, `1_5_5_5` and `4_4_4_4` are viewed as R5G6B5, A1R5G5B5 and B4G4R4A4 (the 4-bit format every Vulkan device can sample; R4G4B4A4 is optional and lavapipe lacks it) with a swizzle that reads X from the low bits, and a DST_SEL of the missing W of `5_6_5` reads X. Measured on RDNA2 (gfx1035), not on the console: through `image_load` and `image_sample_lz`, X, Y, Z (and W of `1_5_5_5` and `4_4_4_4`) come from the low, middle and high fields, and W of `5_6_5` returns X (the low 5 bits), not 1. - [Min/max S# reduction](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp) (libSceAgcDriver) - `FILTER_MODE` 1 and 2 use Vulkan's min and max reduction (the same `SQ_IMG_FILTER_MODE` values RADV programs). Measured on RDNA2 (gfx1035), not on the console: only texels of non-zero weight are reduced, and the weight is taken after rounding to the 8-bit subtexel grid (a sample within 1/512 texel of a centre reduces that texel alone); a driver with other `subTexelPrecisionBits` reduces a different set there. Gathers ignore the mode. -- [Min/max S#](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp) (libSceAgcDriver, shader recompiler) - throws with anisotropy, a linear mip filter, depth or color-texture compare, or bilinear filtering of an image the recompiler point-samples (sint, converted, depth-bits). +- [Min/max S#](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp) (libSceAgcDriver, shader recompiler) - throws with a linear mip filter, depth or color-texture compare, or bilinear or anisotropic filtering of an image the recompiler point-samples (sint, converted, depth-bits). With anisotropy the host sampler reduces over its own footprint; [RADV](https://github.com/openbsd/xenocara/blob/1b39033df8d928ec247ca64448c4a63c72302db7/lib/mesa/src/amd/vulkan/radv_sampler.c#L185-L232) programs the same S# for such a Vulkan sampler. Only a sample without derivatives is measured: on gfx1030, a 2x anisotropic S# over a single-level texture returns the bilinear minimum or maximum, and min with anisotropic point the point result ([#5156](https://github.com/boykopovar/AnyPS5/pull/5156#issuecomment-6155265616)). A real anisotropic footprint and max with anisotropic point are not measured, and Vulkan leaves nearest filtering with anisotropy to the host driver. - [FORCE_DEGAMMA](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Sampler.cpp) (libSceAgcDriver) - an S# that forces sRGB decoding adds nothing to an sRGB texture, as in shadPS4 (PS4). Measured on RDNA2 (gfx1035), not on the console: `8`, `8_8` and `8_8_8_8` sRGB textures sample bit for bit the same with and without the bit (point and bilinear; BC formats not measured). Other textures throw; on the same hardware their UNORM `8`, `8_8` and `8_8_8_8` forms sample bit for bit as the sRGB format (decoded before filtering). - [Tessellation factor ring](../../../core/libs/prx/libSceAgcDriver/State/src/Configuration.cpp) (libSceAgcDriver) - `sceAgcDriverSetTFRing` records the ring and `sceAgcDriverGetTFRing` reports it. Before a title sets one, it reports the console's default, 0xFF0000000 and 128 KiB, as measured in [PS5_Vulkan's hardware findings](https://github.com/mihawk-99/PS5_Vulkan/blob/fde9e3798c2ec1d8fccc90d5067cd6060490607f/docs/HARDWARE_FINDINGS.md). Nothing is written to the recorded ring: tessellated draws run as Vulkan tessellation stages, which keep their factors on the host, so a title that reads tessellation factors from the ring finds none. A null base or a zero size throws; [ps5-opengl's AGC runtime](https://github.com/blackbearreloaded/ps5-opengl/blob/67c873f985eaefe0a72f713529e5632eaa01139c/src/platform/ps5_agc_native_runtime.c) calls `sceAgcDriverSetTFRing(0, 0)` in its cleanup, and what that does on the console is unknown. - [VertexBufferReadSize](../../../core/libs/prx/libSceAgcDriver/Graphics/include/VertexInput.hpp) and the [vertex staging copy](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver) - a vertex fetch whose index is at or past the descriptor's `num_records` no longer throws for `OOB_SELECT` 0 and 1. The copy still spans the whole fetch, but the bytes from `num_records * stride` onward read as zeros, so the out-of-range vertex is `(0, 0, 0, 0)`. Measured with `buffer_load_format_xyzw` (`idxen`) through the [hardware oracle](../HW_ORACLE.md) on RDNA2 gfx1036 for PPSA01325's word 3 `0x0004dfac` (`OOB_SELECT` 0) in #2241: indices 3, 4 and 5 over 3 records each return `00000000 00000000 00000000 00000000`, W included, unlike Vulkan `robustBufferAccess`, which returns `(0, 0, 0, 1)`. The zeros come from the record check, not from memory: stored values placed past the records are ignored, and a `num_records` 6 control returns them. `RESOURCE_LEVEL` 1, `OOB_SELECT` 1 and FORMAT 74 `32_32_32_FLOAT` give the same zeros, in wave32 and wave64; gfx1013 (the console) is not measured. `OOB_SELECT` 2 returns the stored values past the records and `OOB_SELECT` 3 compares `num_records` against the byte offset, both measured on the same setup in #2241; the two keep throwing past `num_records` here, since each needs its own rule. Zero-stride descriptors (`stride` 0) keep their separate byte-count check and are not covered here. Each out-of-range fetch is copied alone and outside the draw-snapshot cache, which is neither read nor written for it: a reused snapshot would carry another draw's bytes into the zero region, and a stored zero-padded copy would hand zeros to a later in-range draw at the same address. The valid prefix is still flushed for GPU writes before it is read. From 17ed1218a4aad341a3879614733127ae2a18838b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Thu, 15 Oct 2026 15:17:18 +0200 Subject: [PATCH 0306/1000] docs(debt): record the measured min/max anisotropic sampler results Co-Authored-By: Claude --- docs/dev/debt/libSceAgcDriver.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 663a69598b..b50ea8e8db 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -127,7 +127,7 @@ - [Local programs](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp) (libSceAgcDriver) that the hull header leaves at address 0 are prepared at their first draw, one artifact per tessellation configuration, instead of at registration. - [Packed 16-bit textures](../../../core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp) (libSceAgcDriver) - `5_6_5`, `1_5_5_5` and `4_4_4_4` are viewed as R5G6B5, A1R5G5B5 and B4G4R4A4 (the 4-bit format every Vulkan device can sample; R4G4B4A4 is optional and lavapipe lacks it) with a swizzle that reads X from the low bits, and a DST_SEL of the missing W of `5_6_5` reads X. Measured on RDNA2 (gfx1035), not on the console: through `image_load` and `image_sample_lz`, X, Y, Z (and W of `1_5_5_5` and `4_4_4_4`) come from the low, middle and high fields, and W of `5_6_5` returns X (the low 5 bits), not 1. - [Min/max S# reduction](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp) (libSceAgcDriver) - `FILTER_MODE` 1 and 2 use Vulkan's min and max reduction (the same `SQ_IMG_FILTER_MODE` values RADV programs). Measured on RDNA2 (gfx1035), not on the console: only texels of non-zero weight are reduced, and the weight is taken after rounding to the 8-bit subtexel grid (a sample within 1/512 texel of a centre reduces that texel alone); a driver with other `subTexelPrecisionBits` reduces a different set there. Gathers ignore the mode. -- [Min/max S#](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp) (libSceAgcDriver, shader recompiler) - throws with a linear mip filter, depth or color-texture compare, or bilinear or anisotropic filtering of an image the recompiler point-samples (sint, converted, depth-bits). With anisotropy the host sampler reduces over its own footprint; [RADV](https://github.com/openbsd/xenocara/blob/1b39033df8d928ec247ca64448c4a63c72302db7/lib/mesa/src/amd/vulkan/radv_sampler.c#L185-L232) programs the same S# for such a Vulkan sampler. Only a sample without derivatives is measured: on gfx1030, a 2x anisotropic S# over a single-level texture returns the bilinear minimum or maximum, and min with anisotropic point the point result ([#5156](https://github.com/boykopovar/AnyPS5/pull/5156#issuecomment-6155265616)). A real anisotropic footprint and max with anisotropic point are not measured, and Vulkan leaves nearest filtering with anisotropy to the host driver. +- [Min/max S#](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp) (libSceAgcDriver, shader recompiler) - throws with a linear mip filter, depth or color-texture compare, or bilinear or anisotropic filtering of an image the recompiler point-samples (sint, converted, depth-bits). With anisotropy the host sampler reduces over its own footprint; [RADV](https://github.com/openbsd/xenocara/blob/1b39033df8d928ec247ca64448c4a63c72302db7/lib/mesa/src/amd/vulkan/radv_sampler.c#L185-L232) programs the same S# for such a Vulkan sampler. Measured on RDNA2 (gfx1030 in [#5156](https://github.com/boykopovar/AnyPS5/pull/5156#issuecomment-6155265616), gfx1035 in #5821), not on the console: without derivatives (`image_sample_lz`), min or max with anisotropic bilinear returns the bilinear minimum or maximum and with anisotropic point the point result, at every ratio field 0 to 4; with derivatives (`image_sample_d`, a 4:1 footprint), anisotropic bilinear at ratio field 2 or 4 reduces over the texels of all its anisotropic taps (the minimum or maximum along the major axis), not one bilinear footprint. A host that is not AMD reduces over its own anisotropic footprint, and Vulkan leaves nearest filtering with anisotropy to the host driver. - [FORCE_DEGAMMA](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Sampler.cpp) (libSceAgcDriver) - an S# that forces sRGB decoding adds nothing to an sRGB texture, as in shadPS4 (PS4). Measured on RDNA2 (gfx1035), not on the console: `8`, `8_8` and `8_8_8_8` sRGB textures sample bit for bit the same with and without the bit (point and bilinear; BC formats not measured). Other textures throw; on the same hardware their UNORM `8`, `8_8` and `8_8_8_8` forms sample bit for bit as the sRGB format (decoded before filtering). - [Tessellation factor ring](../../../core/libs/prx/libSceAgcDriver/State/src/Configuration.cpp) (libSceAgcDriver) - `sceAgcDriverSetTFRing` records the ring and `sceAgcDriverGetTFRing` reports it. Before a title sets one, it reports the console's default, 0xFF0000000 and 128 KiB, as measured in [PS5_Vulkan's hardware findings](https://github.com/mihawk-99/PS5_Vulkan/blob/fde9e3798c2ec1d8fccc90d5067cd6060490607f/docs/HARDWARE_FINDINGS.md). Nothing is written to the recorded ring: tessellated draws run as Vulkan tessellation stages, which keep their factors on the host, so a title that reads tessellation factors from the ring finds none. A null base or a zero size throws; [ps5-opengl's AGC runtime](https://github.com/blackbearreloaded/ps5-opengl/blob/67c873f985eaefe0a72f713529e5632eaa01139c/src/platform/ps5_agc_native_runtime.c) calls `sceAgcDriverSetTFRing(0, 0)` in its cleanup, and what that does on the console is unknown. - [VertexBufferReadSize](../../../core/libs/prx/libSceAgcDriver/Graphics/include/VertexInput.hpp) and the [vertex staging copy](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver) - a vertex fetch whose index is at or past the descriptor's `num_records` no longer throws for `OOB_SELECT` 0 and 1. The copy still spans the whole fetch, but the bytes from `num_records * stride` onward read as zeros, so the out-of-range vertex is `(0, 0, 0, 0)`. Measured with `buffer_load_format_xyzw` (`idxen`) through the [hardware oracle](../HW_ORACLE.md) on RDNA2 gfx1036 for PPSA01325's word 3 `0x0004dfac` (`OOB_SELECT` 0) in #2241: indices 3, 4 and 5 over 3 records each return `00000000 00000000 00000000 00000000`, W included, unlike Vulkan `robustBufferAccess`, which returns `(0, 0, 0, 1)`. The zeros come from the record check, not from memory: stored values placed past the records are ignored, and a `num_records` 6 control returns them. `RESOURCE_LEVEL` 1, `OOB_SELECT` 1 and FORMAT 74 `32_32_32_FLOAT` give the same zeros, in wave32 and wave64; gfx1013 (the console) is not measured. `OOB_SELECT` 2 returns the stored values past the records and `OOB_SELECT` 3 compares `num_records` against the byte offset, both measured on the same setup in #2241; the two keep throwing past `num_records` here, since each needs its own rule. Zero-stride descriptors (`stride` 0) keep their separate byte-count check and are not covered here. Each out-of-range fetch is copied alone and outside the draw-snapshot cache, which is neither read nor written for it: a reused snapshot would carry another draw's bytes into the zero region, and a stored zero-padded copy would hand zeros to a later in-range draw at the same address. The valid prefix is still flushed for GPU writes before it is read. From ed33660a8091e1c30cc7c384e5513891f1af5f8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Ortellado?= Date: Wed, 14 Oct 2026 09:41:21 -0300 Subject: [PATCH 0307/1000] fix(tools): include folded-only threads in trace digests --- tools/tests/test_trace_digest.py | 38 ++++++++++++++++++++++++++++++++ tools/trace_digest.py | 3 ++- 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/tools/tests/test_trace_digest.py b/tools/tests/test_trace_digest.py index c065c6518e..d9dd888b29 100644 --- a/tools/tests/test_trace_digest.py +++ b/tools/tests/test_trace_digest.py @@ -59,6 +59,44 @@ class TraceDigestTests(unittest.TestCase): def tearDown(self): self.directory.cleanup() + def test_folded_only_threads_are_reported_in_json_and_markdown(self): + baseline = {thread["thread"]: thread for thread in self.result()["threads"]} + for name, total in (("shader compiler", 6.0), ("tid 3", 6.0), ("shader compiler", 0.0)): + with self.subTest(name=name, total=total): + events = EVENTS + [ + {"name": "thread_name", "ph": "M", "ts": 0, "pid": 7, "tid": 3, "args": {"name": name}}, + {"name": "thread_name", "ph": "M", "ts": 0, "pid": 7, "tid": 4, "args": {"name": "idle"}}, + {"name": f"{name} folded", "ph": "C", "ts": 0, "pid": 7, "tid": 3, "args": {"ms": total / 2}}, + {"name": f"{name} folded", "ph": "C", "ts": 20000, "pid": 7, "tid": 3, "args": {"ms": total / 2}}, + ] + with gzip.open(self.trace, "wt", encoding="utf-8") as handle: + json.dump({"traceEvents": events}, handle) + output = io.StringIO() + with contextlib.redirect_stdout(output): + self.assertEqual(trace_digest.main([str(self.trace), "--json"]), 0) + result = json.loads(output.getvalue()) + threads = {thread["thread"]: thread for thread in result["threads"]} + self.assertEqual(set(threads), set(baseline) | {name}) + self.assertEqual(threads[name]["folded_ms_per_frame"], total / 3) + self.assertEqual(threads[name]["busy_ms_per_frame"], 0) + self.assertEqual(threads[name]["wait_ms_per_frame"], 0) + self.assertEqual(threads[name]["top"], []) + for thread_name, thread in baseline.items(): + self.assertEqual(threads[thread_name], thread) + output = io.StringIO() + with contextlib.redirect_stdout(output): + trace_digest.main([str(self.trace)]) + self.assertIn(f"| {name} | 0.0 (0 %) | 0.0 (0 %) |", output.getvalue()) + + def test_folded_only_thread_without_frame_boundaries(self): + trace = trace_digest.Trace({"traceEvents": [ + {"name": "compiler folded", "ph": "C", "ts": 0, "tid": 3, "args": {"ms": 1.5}}, + ]}) + thread = trace_digest.digest(trace)["threads"][0] + self.assertEqual(thread["thread"], "tid 3") + self.assertEqual(thread["folded_ms_per_frame"], 1.5) + self.assertEqual(thread["untraced_ms_per_frame"], 0) + def result(self): return trace_digest.digest(trace_digest.load_trace(self.trace), trace_digest.load_summary(None, self.trace), trace_digest.load_stacks(self.stacks), worst=2, top=3) diff --git a/tools/trace_digest.py b/tools/trace_digest.py index 4dbc84f7cf..6644bdfbe2 100644 --- a/tools/trace_digest.py +++ b/tools/trace_digest.py @@ -119,7 +119,8 @@ def digest(trace, summary=None, stacks=None, worst=5, top=10): threads = [] totals = collections.Counter() waits = [] - for tid, slices in sorted(trace.slices.items()): + for tid in sorted(trace.slices.keys() | trace.folded.keys()): + slices = trace.slices.get(tid, []) traced = sum(dur for _, dur, _, depth, _ in slices if depth == 0) waited = sum(dur for _, dur, name, _, nested in slices if is_wait(name) and not nested) leaders = collections.Counter() From e8b7361a1cd13eb1e0f23554b867e38c34cbfbf4 Mon Sep 17 00:00:00 2001 From: hugodearaujo Date: Thu, 15 Oct 2026 10:19:46 -0300 Subject: [PATCH 0308/1000] fix(agc): settle dma-buf imports on Linux on the settle queue The kernel maps a dma-buf import's pages for the GPU at the first submission that uses it. On RADV that took about 2 s for a 3 GiB import, in the second frame of Hades (PPSA03355), and its staging pool ran out while the frame was held. Linux now makes the settle queue from #6060 when the device imports dma-buf memory, and settles every dma-buf import of 1 MiB or more right after it is made with settleImport's 4-byte copy. The settle queue needs neither the GPU mutex nor the main queue's work to finish. A dma-buf import's buffer is shared with the settle queue's family instead of aliased, since a sparse import is bound to many memory objects. Where the sparse queue already takes that family, the settle queue is a second queue of it. --- .../Execution/src/VulkanDevice.cpp | 71 +++++++++++-------- .../Graphics/include/GuestBufferMemory.hpp | 2 + .../Graphics/src/GuestBufferMemory.cpp | 63 ++++++++++++---- .../tests/execution/SparseDmaBufImport.cpp | 14 ++++ docs/dev/ARCHITECTURE.md | 2 +- docs/dev/debt/libSceAgcDriver.md | 1 + 6 files changed, 109 insertions(+), 44 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index bb39fa7beb..9309df0543 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -1423,29 +1423,37 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : queueInfo.queueFamilyIndex = family; queueInfo.queueCount = 1; queueInfo.pQueuePriorities = &priority; -#ifdef _WIN32 const float priorities[2]{1.0f, 1.0f}; bool settleQueue = false; std::uint32_t settleFamily = family; - std::uint32_t families = 0; - auto getFamilies = state->InstanceFunction("vkGetPhysicalDeviceQueueFamilyProperties"); - getFamilies(selected, &families, nullptr); - std::vector queues(families); - getFamilies(selected, &families, queues.data()); - if (family < families && queues[family].queueCount >= 2) { - queueInfo.queueCount = 2; - queueInfo.pQueuePriorities = priorities; - settleQueue = true; - } else { - const auto copies = [&](std::uint32_t index) { return index != family && queues[index].queueCount != 0 && (queues[index].queueFlags & (VK_QUEUE_TRANSFER_BIT | VK_QUEUE_COMPUTE_BIT | VK_QUEUE_GRAPHICS_BIT)) != 0; }; - for (std::uint32_t i = 0; i < families && !settleQueue; ++i) { - if (copies(i) && (queues[i].queueFlags & (VK_QUEUE_COMPUTE_BIT | VK_QUEUE_GRAPHICS_BIT)) == 0) settleFamily = i, settleQueue = true; - } - for (std::uint32_t i = 0; i < families && !settleQueue; ++i) { - if (copies(i)) settleFamily = i, settleQueue = true; + std::uint32_t settleIndex = 0; + std::vector queues; +#ifdef _WIN32 + const bool settles = true; +#else + const bool settles = state->dmaBufImport; +#endif + if (settles) { + std::uint32_t families = 0; + auto getFamilies = state->InstanceFunction("vkGetPhysicalDeviceQueueFamilyProperties"); + getFamilies(selected, &families, nullptr); + queues.resize(families); + getFamilies(selected, &families, queues.data()); + if (family < families && queues[family].queueCount >= 2) { + queueInfo.queueCount = 2; + queueInfo.pQueuePriorities = priorities; + settleQueue = true; + settleIndex = 1; + } else { + const auto copies = [&](std::uint32_t index) { return index != family && queues[index].queueCount != 0 && (queues[index].queueFlags & (VK_QUEUE_TRANSFER_BIT | VK_QUEUE_COMPUTE_BIT | VK_QUEUE_GRAPHICS_BIT)) != 0; }; + for (std::uint32_t i = 0; i < families && !settleQueue; ++i) { + if (copies(i) && (queues[i].queueFlags & (VK_QUEUE_COMPUTE_BIT | VK_QUEUE_GRAPHICS_BIT)) == 0) settleFamily = i, settleQueue = true; + } + for (std::uint32_t i = 0; i < families && !settleQueue; ++i) { + if (copies(i)) settleFamily = i, settleQueue = true; + } } } -#endif VkDeviceCreateInfo deviceInfo{VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO}; deviceInfo.queueCreateInfoCount = 1; deviceInfo.pQueueCreateInfos = &queueInfo; @@ -1479,7 +1487,7 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : // Guest shaders routinely read past descriptor ranges; robust access turns that into zeros // instead of a GPU fault that loses the device. enabled.robustBufferAccess = available.robustBufferAccess; - VkDeviceQueueCreateInfo queueInfos[2]{queueInfo, {VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO}}; + VkDeviceQueueCreateInfo queueInfos[3]{queueInfo, {VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO}, {VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO}}; if (available.sparseBinding && state->dmaBufImport) { std::uint32_t families = 0; auto getFamilies = state->InstanceFunction("vkGetPhysicalDeviceQueueFamilyProperties"); @@ -1507,19 +1515,22 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : enabled.sparseBinding = state->sparseBinding ? VK_TRUE : VK_FALSE; } deviceInfo.pQueueCreateInfos = queueInfos; -#ifdef _WIN32 if (settleQueue && settleFamily != family) { - if (deviceInfo.queueCreateInfoCount == 1) { - queueInfos[1] = {VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO}; - queueInfos[1].queueFamilyIndex = settleFamily; - queueInfos[1].queueCount = 1; - queueInfos[1].pQueuePriorities = &priority; - deviceInfo.queueCreateInfoCount = 2; + if (deviceInfo.queueCreateInfoCount == 2 && queueInfos[1].queueFamilyIndex == settleFamily) { + settleQueue = queues[settleFamily].queueCount >= 2; + if (settleQueue) { + queueInfos[1].queueCount = 2; + queueInfos[1].pQueuePriorities = priorities; + settleIndex = 1; + } } else { - settleQueue = false; + auto& settleInfo = queueInfos[deviceInfo.queueCreateInfoCount++]; + settleInfo = {VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO}; + settleInfo.queueFamilyIndex = settleFamily; + settleInfo.queueCount = 1; + settleInfo.pQueuePriorities = &priority; } } -#endif // Indirect draw records with a non-zero first instance, and several records per call. enabled.drawIndirectFirstInstance = available.drawIndirectFirstInstance; enabled.multiDrawIndirect = available.multiDrawIndirect; @@ -1804,12 +1815,10 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : } state->DeviceFunction("vkGetDeviceQueue")(state->device, family, 0, &state->queue); state->queueFamily = family; -#ifdef _WIN32 if (settleQueue) { state->settleFamily = settleFamily; - state->DeviceFunction("vkGetDeviceQueue")(state->device, settleFamily, settleFamily == family ? 1 : 0, &state->settleQueue); + state->DeviceFunction("vkGetDeviceQueue")(state->device, settleFamily, settleIndex, &state->settleQueue); } -#endif if (state->sparseFamily != VK_QUEUE_FAMILY_IGNORED) state->DeviceFunction("vkGetDeviceQueue")(state->device, state->sparseFamily, 0, &state->sparseQueue); if (checkpoints) state->checkpointMarker = state->DeviceFunction("vkCmdSetCheckpointNV"); if (deviceFault || checkpoints) { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp index 9efb6aac24..150431a79d 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp @@ -71,6 +71,8 @@ void SetHostImportWindow(std::uint64_t bytes); void SetHostImportRangeWindow(std::uint64_t bytes); void SetHostImportBudget(std::uint64_t bytes); std::uint64_t HostImportLiveBytes(); +// Tests: how many imports settleImport has used on the settle queue. +std::uint64_t HostImportsSettled(); bool ImportMappedRanges(const Context& context, const GuestAllocations::Mapped& ranges, std::uint64_t generation, bool adoptDevice); // Whether an existing import covers [address, address + bytes), without reconciling the imports // with the registry or making one (HostImportFor may take a registry lease): a hint for choices diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index 16604afe97..1020d477e7 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -184,7 +184,6 @@ struct HostImports { bool unwatchImports = false; bool unwatchDmaBufImports = false; bool watchPrivateImports = false; -#ifdef _WIN32 VkDevice settleDevice = VK_NULL_HANDLE; VkBuffer settleTarget = VK_NULL_HANDLE; VkDeviceMemory settleMemory = VK_NULL_HANDLE; @@ -193,7 +192,6 @@ struct HostImports { PFN_vkDestroyFence destroyFence = nullptr; PFN_vkDestroyBuffer settleDestroyBuffer = nullptr; PFN_vkFreeMemory settleFreeMemory = nullptr; -#endif std::uint64_t useCounter = 0; std::uint64_t buildFloor = 0; }; @@ -272,6 +270,28 @@ bool releaseParkedImports(HostImports& state, std::uint64_t begin, std::uint64_t return !released.empty(); } +#ifndef _WIN32 +struct ImportFamilies { + std::array indices{}; + std::uint32_t count = 0; +}; + +// A dma-buf import's buffer is used on the main queue, on the settle queue (settleImport) and, when +// sparse, on the sparse queue. +void shareDmaBufImport(const Context& context, VkBufferCreateInfo& info, ImportFamilies& families, bool sparse) { + const auto add = [&](std::uint32_t family) { + if (std::find(families.indices.begin(), families.indices.begin() + families.count, family) == families.indices.begin() + families.count) families.indices[families.count++] = family; + }; + add(context.queueFamily); + if (context.settleQueue != VK_NULL_HANDLE) add(context.settleFamily); + if (sparse && context.sparseQueue != VK_NULL_HANDLE) add(context.sparseFamily); + const bool concurrent = families.count > 1; + info.sharingMode = concurrent ? VK_SHARING_MODE_CONCURRENT : VK_SHARING_MODE_EXCLUSIVE; + info.queueFamilyIndexCount = concurrent ? families.count : 0u; + info.pQueueFamilyIndices = concurrent ? families.indices.data() : nullptr; +} +#endif + const char* bindImport(const Context& context, HostImport& entry, VkExternalMemoryHandleTypeFlagBits handleType, const void* import, std::uint32_t importTypes, bool& allocated, VkResult& failure) { const auto failed = [&](const char* step, VkResult result) -> const char* { if (entry.buffer) context.Function("vkDestroyBuffer")(context.device, entry.buffer, nullptr); @@ -288,6 +308,10 @@ const char* bindImport(const Context& context, HostImport& entry, VkExternalMemo // INDIRECT_BUFFER: DISPATCH_INDIRECT group counts are read in place (VulkanDevice::DispatchIndirect). info.usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT | VK_BUFFER_USAGE_VERTEX_BUFFER_BIT | VK_BUFFER_USAGE_INDEX_BUFFER_BIT; info.sharingMode = VK_SHARING_MODE_EXCLUSIVE; +#ifndef _WIN32 + ImportFamilies families; + if (handleType == VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT) shareDmaBufImport(context, info, families, false); +#endif if (const auto result = context.Function("vkCreateBuffer")(context.device, &info, nullptr, &entry.buffer); result != VK_SUCCESS) return failed("vkCreateBuffer", result); VkMemoryRequirements requirements{}; context.Function("vkGetBufferMemoryRequirements")(context.device, entry.buffer, &requirements); @@ -466,7 +490,8 @@ const char* createChunkedDmaBufImport(const Context& context, HostImport& entry, VkBufferCreateInfo info{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, &external}; info.size = entry.bytes; info.usage = ChunkImportUsage; - info.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + ImportFamilies families; + shareDmaBufImport(context, info, families, false); VkBuffer buffer = VK_NULL_HANDLE; if (const auto result = context.Function("vkCreateBuffer")(context.device, &info, nullptr, &buffer); result != VK_SUCCESS) { failure = result; @@ -519,11 +544,8 @@ VkMemoryRequirements dmaBufBufferRequirements(const Context& context, const Host info.flags = flags; info.size = size; info.usage = usage; - const std::uint32_t families[] = {context.queueFamily, context.sparseFamily}; - const bool concurrent = (flags & VK_BUFFER_CREATE_SPARSE_BINDING_BIT) != 0 && context.sparseQueue != VK_NULL_HANDLE; - info.sharingMode = concurrent ? VK_SHARING_MODE_CONCURRENT : VK_SHARING_MODE_EXCLUSIVE; - info.queueFamilyIndexCount = concurrent ? 2u : 0u; - info.pQueueFamilyIndices = concurrent ? families : nullptr; + ImportFamilies families; + shareDmaBufImport(context, info, families, (flags & VK_BUFFER_CREATE_SPARSE_BINDING_BIT) != 0); VkBuffer buffer = VK_NULL_HANDLE; if (const auto result = context.Function("vkCreateBuffer")(context.device, &info, nullptr, &buffer); result != VK_SUCCESS) throwSparseImport(entry, "vkCreateBuffer", result); VkMemoryRequirements requirements{}; @@ -650,9 +672,10 @@ ImportWatchRequest importWatchRequest() { } #endif -#ifdef _WIN32 constexpr std::uint64_t settleFloorBytes = 0x100000; +std::atomic settledImports{0}; + const char* settleImport(const Context& context, HostImports& state, const HostImport& entry, VkResult& result) { std::unique_lock queueLock(*context.settleQueueMutex); if (state.settleDevice != context.device) { @@ -703,6 +726,7 @@ const char* settleImport(const Context& context, HostImports& state, const HostI VkBuffer source = entry.buffer; VkBuffer alias = VK_NULL_HANDLE; const auto run = [&]() -> const char* { +#ifdef _WIN32 if (context.settleFamily != context.queueFamily) { VkExternalMemoryBufferCreateInfo external{VK_STRUCTURE_TYPE_EXTERNAL_MEMORY_BUFFER_CREATE_INFO}; external.handleTypes = VK_EXTERNAL_MEMORY_HANDLE_TYPE_HOST_ALLOCATION_BIT_EXT; @@ -713,6 +737,7 @@ const char* settleImport(const Context& context, HostImports& state, const HostI if ((result = context.Function("vkBindBufferMemory")(context.device, alias, entry.memory, 0)) != VK_SUCCESS) return "vkBindBufferMemory (settle alias)"; source = alias; } +#endif if ((result = context.Function("vkResetFences")(context.device, 1, &state.settleFence)) != VK_SUCCESS) return "vkResetFences (settle)"; if ((result = context.Function("vkResetCommandBuffer")(state.settleCommands, 0)) != VK_SUCCESS) return "vkResetCommandBuffer (settle)"; VkCommandBufferBeginInfo begin{VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO}; @@ -731,9 +756,9 @@ const char* settleImport(const Context& context, HostImports& state, const HostI const char* step = run(); if (step != nullptr) context.Function("vkQueueWaitIdle")(context.settleQueue); if (alias != VK_NULL_HANDLE) context.Function("vkDestroyBuffer")(context.device, alias, nullptr); + if (step == nullptr) settledImports.fetch_add(1, std::memory_order_relaxed); return step; } -#endif void decideImportWatch(const Context& context, HostImports& state) { if (state.watchDevice == context.device) return; @@ -1167,6 +1192,18 @@ const HostImport* importRunAt(const Context& context, HostImports& state, Import return step == nullptr; }, watchedDmaBuf); } + if (step == nullptr && entry.dmaBuf && bytes >= settleFloorBytes && context.settleQueue != VK_NULL_HANDLE && context.settleQueueMutex != nullptr) { + static const bool traceSettle = std::getenv("APS5_TRACE_HOST_IMPORT") != nullptr; + const auto start = std::chrono::steady_clock::now(); + if ((step = settleImport(context, state, entry, result)) != nullptr) { + destroyImport(context, entry); + entry.buffer = VK_NULL_HANDLE; + entry.memory = VK_NULL_HANDLE; + entry.chunk.reset(); + } else if (traceSettle) { + std::fprintf(stderr, "[gpu] settled the dma-buf import of 0x%llx+0x%llx on the settle queue in %.2f ms\n", static_cast(base), static_cast(bytes), std::chrono::duration(std::chrono::steady_clock::now() - start).count()); + } + } #endif entry.unwatched = step == nullptr && !(entry.dmaBuf ? watchedDmaBuf : watchedHostPointer); if (step != nullptr) { @@ -2292,7 +2329,6 @@ void ClearHostImports(VkDevice device) { auto& state = Imports(); std::lock_guard lock(state.mutex); state.parked.erase(device); -#ifdef _WIN32 if (state.settleDevice == device) { if (state.settleFence != VK_NULL_HANDLE) state.destroyFence(device, state.settleFence, nullptr); if (state.settleTarget != VK_NULL_HANDLE) state.settleDestroyBuffer(device, state.settleTarget, nullptr); @@ -2303,7 +2339,6 @@ void ClearHostImports(VkDevice device) { state.settleCommands = VK_NULL_HANDLE; state.settleFence = VK_NULL_HANDLE; } -#endif if (state.device != device) return; for (const auto& [address, entry] : state.imports) { state.destroyBuffer(state.device, entry.buffer, nullptr); @@ -2555,6 +2590,10 @@ std::uint64_t HostImportLiveBytes() { return state.liveBytes; } +std::uint64_t HostImportsSettled() { + return settledImports.load(std::memory_order_relaxed); +} + const HostImport* HostImportFor(const Context& context, std::uint64_t address, std::size_t bytes) { if (context.hostImportAlignment == 0 || bytes == 0 || bytes > std::numeric_limits::max() - address) return nullptr; ensurePinWaiter(); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SparseDmaBufImport.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SparseDmaBufImport.cpp index 6c271a9f48..33e26c12d6 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/SparseDmaBufImport.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SparseDmaBufImport.cpp @@ -24,6 +24,14 @@ int main() { return VulkanTestSkipped; } #else +namespace AgcDriver { + +struct VulkanDeviceTestAccess { + static Graphics::Context ContextOf(const VulkanDevice& device) { return device.graphicsContext(); } +}; + +} + namespace { using AgcDriver::Graphics::Require; @@ -107,6 +115,8 @@ int main() { auto* small = mapping + bytes; constexpr std::uint64_t SmallBytes = std::uint64_t{1} << 20u; + const bool settles = AgcDriver::VulkanDeviceTestAccess::ContextOf(*device).settleQueue != VK_NULL_HANDLE; + const auto settledBefore = AgcDriver::Graphics::HostImportsSettled(); device->ImportGuestMemory(Register(small, SmallBytes), GuestAllocations::GuestAllocationsGeneration_nid_postfix(), false); const bool smallImported = Imported(device->Device(), reinterpret_cast(small), static_cast(SmallBytes)); GuestAllocations::Mutation().Remove(small); @@ -114,14 +124,18 @@ int main() { std::printf("skipped, the device does not import dma-buf memory\n"); return VulkanTestSkipped; } + if (!settles) std::printf("note: the device has no settle queue, so dma-buf imports are not settled\n"); + Require(!settles || AgcDriver::Graphics::HostImportsSettled() == settledBefore + 1, "sparse import: the 1 MiB dma-buf import made at map time was not settled on the settle queue"); const auto boundary = ChunkBytes; std::memset(mapping + boundary - 65536, 0x11, 131072); const auto ranges = Register(mapping, bytes); const auto address = reinterpret_cast(mapping); + const auto settledSmall = AgcDriver::Graphics::HostImportsSettled(); const bool atMap = device->ImportGuestMemory(ranges, GuestAllocations::GuestAllocationsGeneration_nid_postfix(), false); Require(Imported(device->Device(), address, static_cast(bytes)), "sparse import: a range larger than udmabuf's size limit was not imported"); Require(atMap, "sparse import: the import made without the GPU lock was refused"); + Require(!settles || AgcDriver::Graphics::HostImportsSettled() == settledSmall + 1, "sparse import: the sparse dma-buf import made at map time was not settled on the settle queue"); bool filled = false; { diff --git a/docs/dev/ARCHITECTURE.md b/docs/dev/ARCHITECTURE.md index 54b4476de1..a8a2b6cd7f 100644 --- a/docs/dev/ARCHITECTURE.md +++ b/docs/dev/ARCHITECTURE.md @@ -107,7 +107,7 @@ flowchart LR - A draw whose `PA_SC_AA_CONFIG` selects 2, 4 or 8 samples renders into host-only multisampled images. [`MultisampleTarget.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) keeps one image per device, surface address, format, extent and sample count (`MultisampleTargetView`); the pipeline, render pass and library keys carry `State::samples`, and a multisampled depth target is a multisampled depth surface. The guest sample planes and FMASK are never read or written, so the registry also guards the target's guest range: a CPU read (`GuestMemory::SetReadGuard`), a DMA copy, and a sampled or storage image read of it throw. A `CB_COLOR_CONTROL` resolve mode over a multisampled CB0 runs as a render pass whose resolve attachment is CB1's resident image (`ResolveMultisampleTarget`). A zero fill of the target's CMASK (`NoteColorMetadataFill`, from fill kernels and CP DMA fills) clears the image at its next use. - A texture over a depth surface samples the depth image itself when it views one 2D slice of the plane's own format (`DepthSurfaceTexture` in [`DepthSurface.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp)). A view of several slices (a 2D array or a cube) or an integer view of a D16 plane is served by a `DepthPlaneCopy`, an image the slices are copied into at every use: from the depth surface bound at each slice's address, else from guest memory. Such a texture is never kept in the resource templates (`RefreshedPerUse`). - Guest memory reaches the GPU through host imports (`VK_EXT_external_memory_host`) where the driver accepts them, and through copies otherwise ([`GuestBufferMemory.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp)). A registered range larger than the import window (64 MiB) is imported per window for buffer and fill accesses and whole for an address-based build. Live imports stay under a budget, with the least recently used import retired first (see TechnicalDebt, Host import windows and budget). -- On Windows, a host import of private `MEM_WRITE_WATCH` memory (singly mapped direct memory) stays on `GetWriteWatch` when `ProbeImportWriteProtection` shows the driver reports nothing written after the second GPU read and a GPU write, and a CPU store is collected (`decideImportWatch`). The driver marks every page of such an import written, asynchronously, until its first GPU use completes, so `settleImport` in [`GuestBufferMemory.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) copies 4 bytes out of the new import on a second queue of the graphics family, or on a queue of another copy-capable family when the graphics family has only one (`Context::settleQueue`, own pool and mutex) inside `ImportWatched`'s window and waits for it, only for imports of 1 MiB or more (a smaller import is compared, so a title that maps, imports and unmaps a small buffer around every submission does not block its mapping thread); the main queue can't be used there because `importAllocation` holds the imports lock and `GpuMutex` comes before it in the lock order. Imports through a section alias keep tracking on the guest views, and every other Windows host import is compared. A device without such a queue keeps the comparisons. +- On Windows, a host import of private `MEM_WRITE_WATCH` memory (singly mapped direct memory) stays on `GetWriteWatch` when `ProbeImportWriteProtection` shows the driver reports nothing written after the second GPU read and a GPU write, and a CPU store is collected (`decideImportWatch`). The driver marks every page of such an import written, asynchronously, until its first GPU use completes, so `settleImport` in [`GuestBufferMemory.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) copies 4 bytes out of the new import on a second queue of the graphics family, or on a queue of another copy-capable family when the graphics family has only one (`Context::settleQueue`, own pool and mutex) inside `ImportWatched`'s window and waits for it, only for imports of 1 MiB or more (a smaller import is compared, so a title that maps, imports and unmaps a small buffer around every submission does not block its mapping thread); the main queue can't be used there because `importAllocation` holds the imports lock and `GpuMutex` comes before it in the lock order. Imports through a section alias keep tracking on the guest views, and every other Windows host import is compared. A device without such a queue keeps the comparisons. On Linux the settle queue is made when the device imports dma-buf memory, and every dma-buf import of 1 MiB or more is settled right after it is made, for another reason: the kernel maps an import's pages for the GPU at the first submission that uses it, which took about 2 s for a 3 GiB import on RADV and held up the title's frame. The import's buffer is shared with the settle queue's family instead of aliased, since a sparse import is bound to many memory objects. ### Runtime image and sampler tables diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 6d427f202d..9bc8012517 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -66,6 +66,7 @@ - [Pending storage-image results over memory outside the write watch](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) leave to the CPU the 64 KiB blocks whose bytes differ from the ones saved at the upload, the fill, the image copy, the previous store or the moment their range left the watch (`Unwatch`, or a host import that does not stay watched), and the blocks the watch saw the CPU write before that moment. A store made on the GPU saves its bytes through a host-visible copy that becomes the saved bytes when its batch completes; a store or a refresh that needs them earlier waits for that batch. A unit without a write generation saves the guest bytes when results land in it outside the watch. One whose range leaves the watch while its results are pending, and ranges that leave the watch through a failed collect, have no saved bytes; storing such results throws. - Windows host-pointer imports of the guest mapping itself are compared instead of write-watched, except writable imports of private `MEM_WRITE_WATCH` memory of 1 MiB or more when the import probe keeps the write watch and a settle queue exists (see Pages under an import window): a driver can mark imported pages dirty after the synchronous import window. Shared memory imported through a separate writable alias retains protection-based tracking on the guest views; driver access to the import alias does not disarm those views. [GuestMemory](../../../core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp) excludes every overlapping 64 KiB tracker block while its backing is retained. A full block replaced by fresh private backing regains tracking and gets a new generation; protection changes, partial replacements and shared remappings do not restore it. Excluded blocks keep their write-watch marks as a hint only (`UntrackedHint`): a storage image or a sampled texture snapshot over them compares its bytes again after a mark in its range, a driver store, a mapping change or a recorded GPU write instead of at every collect epoch or lookup, and a mark the driver makes without a CPU store costs one comparison. This preserves CPU edits through byte comparisons but limits snapshot reuse on excluded blocks to draws of one collect epoch and disables unit shadows for them; restoring tracking on import retirement without replacing the backing remains unimplemented. - [Pages under an import window](../../../core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp) (libSceAgcDriver) whose write protection was resolved while a host import that leaves its range watched was made are compared with the bytes they held before the import and count as stored over only when those differ. A CPU store of the bytes already there within the window cannot be told apart from an import that resolves the write protection without storing, as the NVIDIA driver (610.57.04) does for every imported page, so such a store does not win over pending results. Where host-pointer and dma-buf imports differ in whether they stay watched, a failed host-pointer import and the dma-buf import after it are two windows, and the pages the failed one resolved count as stored over. On Windows, the NVIDIA driver (617.42) marks every page of a host import of private `MEM_WRITE_WATCH` memory written, asynchronously, until the import's first GPU use completes. Such an import stays watched only after a 4-byte read of it on the settle queue has completed inside the window. Each one waits for that read, which keeps its window open for one round trip on the settle queue, so a same-value CPU store from another thread in that time counts as unchanged too. An import made where no settle queue exists, and an import of under 1 MiB, is compared instead: the settle blocks the importing thread, and a title that imports a 64 KiB buffer around every submission lost about 19 % of its frame rate to that wait (Dripl1x, #6060). Settling without blocking the importing thread (submit the read, keep the import compared until its fence signals, then switch it to the write watch) is not implemented. +- [Settled dma-buf imports](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver): on Linux a dma-buf import of 1 MiB or more is read by a 4-byte copy on the settle queue right after it is made (`settleImport`), so the kernel maps its pages for the GPU there and not in the first submission that uses it (about 2 s for a 3 GiB import on RADV). The import's buffer is shared with the settle queue's family. An import made from a submission settles under the GPU mutex, so its frame still pays for it; a smaller import, and every import on a device with no settle queue (one without a copy-capable queue besides the main one), pays in its first submission - [Sparse dma-buf imports](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver): on Linux a shared-backed range larger than one 64 MiB udmabuf buffer is imported as one sparse buffer bound to the 64 MiB buffers, on a queue of its own when the device has a second family that binds sparse memory. The path is decided before anything is imported, and a failure after that throws. A range takes the single udmabuf import instead, which udmabuf's `size_limit_mb` refuses above 64 MiB by default so the range is copied, when its offset in the backing file is not a multiple of the buffer's sparse block size (64 KiB on RADV), when the sparse buffer does not accept every memory type a chunk import can have, when the backing file ends inside its last chunk, or when it is imported at map time on a device with no second family (the bind then needs the GPU mutex); the NVIDIA proprietary driver imports no dma-buf memory - [Unnormalized S# coordinates](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp) (libSceAgcDriver) - `FORCE_UNNORMALIZED` uses a Vulkan `unnormalizedCoordinates` sampler, so the S# LOD range, LOD bias and mip filter are not applied, which is exact only on a one-level view. - An [unnormalized S#](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp) (libSceAgcDriver) maps clamp mode 0 (repeat) on X and Y to clamp-to-edge as required by Vulkan unnormalized coordinates, and assigns the magnification filter when minification and magnification filters differ (since unnormalized coordinates sample at LOD 0). Anisotropy, TRUNC_COORD, MC_COORD_TRUNC and other clamp modes still throw. Secondary LOD bias (LOD_BIAS_SEC, bits [19:14]) is accepted and ignored, matching RDNA2 hardware measurements across explicit and implicit samples. High on Life (PPSA15388, #4736) uses secondary LOD bias and unnormalized coordinates with clamp mode 0 and differing min/mag filters during home base and city rendering. From e0b9572e3bcf3f36e88d08eafee0120a44ad24de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Thu, 15 Oct 2026 15:20:44 +0200 Subject: [PATCH 0309/1000] ci(windows): find the CA bundle of Git for Windows under ucrt64 too Git for Windows 2.56 (runner image 20261002) moved from mingw64 to ucrt64, so the fixed SSL_CERT_FILE path no longer exists and the WinLibs CMake fails the FFmpeg download with status 60 on every Windows build. Take the bundle from whichever of ucrt64 and mingw64 exists. Co-Authored-By: Claude --- .github/workflows/build.yml | 8 +++++++- .github/workflows/release.yml | 13 +++++++++---- docs/dev/BUILD.md | 2 +- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f18d548971..328669a882 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -364,7 +364,6 @@ jobs: env: WINLIBS_RELEASE: 15.2.0posix-14.0.0-ucrt-r7 WINLIBS_ZIP: winlibs-x86_64-posix-seh-gcc-15.2.0-mingw-w64ucrt-14.0.0-r7.zip - SSL_CERT_FILE: C:\Program Files\Git\mingw64\etc\ssl\certs\ca-bundle.crt defaults: run: shell: bash @@ -391,6 +390,13 @@ jobs: if ! git -c user.name=ci -c user.email=ci@localhost merge --no-ff --no-edit "refs/remotes/pull/${numbers[i]}"; then echo "::error::Pull request #${numbers[i]} does not merge cleanly into main and the pull requests before it; rebase it first"; exit 1; fi done git submodule update --init --recursive + - name: Point the WinLibs CMake at the CA bundle of Git for Windows + run: | + for prefix in ucrt64 mingw64; do + bundle="C:/Program Files/Git/$prefix/etc/ssl/certs/ca-bundle.crt" + if [ -f "$bundle" ]; then echo "SSL_CERT_FILE=$bundle" >> "$GITHUB_ENV"; exit 0; fi + done + echo "::error::No CA bundle found in Git for Windows"; exit 1 - id: toolchain uses: actions/cache@v6 with: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1cc1d9aa9d..7fccd9c6a6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -42,6 +42,15 @@ jobs: - uses: actions/checkout@v7 with: submodules: recursive + - name: Point the WinLibs CMake at the CA bundle of Git for Windows + if: matrix.platform == 'windows' + shell: bash + run: | + for prefix in ucrt64 mingw64; do + bundle="C:/Program Files/Git/$prefix/etc/ssl/certs/ca-bundle.crt" + if [ -f "$bundle" ]; then echo "SSL_CERT_FILE=$bundle" >> "$GITHUB_ENV"; exit 0; fi + done + echo "::error::No CA bundle found in Git for Windows"; exit 1 - id: toolchain if: matrix.platform == 'windows' uses: actions/cache@v6 @@ -49,8 +58,6 @@ jobs: path: C:\winlibs key: ${{ env.WINLIBS_ZIP }} - if: matrix.platform == 'windows' && steps.toolchain.outputs.cache-hit != 'true' - env: - SSL_CERT_FILE: 'C:\Program Files\Git\mingw64\etc\ssl\certs\ca-bundle.crt' run: | curl -sSfL -o "$RUNNER_TEMP/winlibs.zip" "https://github.com/brechtsanders/winlibs_mingw/releases/download/$WINLIBS_RELEASE/$WINLIBS_ZIP" 7z x "$RUNNER_TEMP/winlibs.zip" -oC:/winlibs @@ -69,8 +76,6 @@ jobs: - name: Configure Windows if: matrix.platform == 'windows' shell: cmd - env: - SSL_CERT_FILE: 'C:\Program Files\Git\mingw64\etc\ssl\certs\ca-bundle.crt' run: cmake -S . -B build %CONFIGURE% - name: Build project and tests (Linux) if: matrix.platform == 'linux' diff --git a/docs/dev/BUILD.md b/docs/dev/BUILD.md index 30a99a2804..2dfcffda8d 100644 --- a/docs/dev/BUILD.md +++ b/docs/dev/BUILD.md @@ -32,7 +32,7 @@ git submodule update --init --recursive - Fedora: if CMake reports `Could NOT find Threads`, install `glibc-static` and `libstdc++-static` with `sudo dnf install glibc-static libstdc++-static`, then rerun configuration. The project enables static executable linking, and FFmpeg's thread check needs these libraries. - Windows: only MinGW-w64 GCC 15.2.0 (WinLibs `x86_64-ucrt-posix-seh`, release [`15.2.0posix-14.0.0-ucrt-r7`](https://github.com/brechtsanders/winlibs_mingw/releases/tag/15.2.0posix-14.0.0-ucrt-r7), archive `winlibs-x86_64-posix-seh-gcc-15.2.0-mingw-w64ucrt-14.0.0-r7.zip`) is currently supported. Add its `mingw64/bin` directory to `PATH` before configuring. - Windows antivirus software can quarantine the executables that the relinker tests create in the temp directory, which fails `optional_plt`, `empty_tls`, `windows_address_space` and `windows_icon`. Set `TEMP` and `TMP` to a directory the scanner excludes before running `ctest`. -- FFmpeg binaries are downloaded during configuration unless `FFMPEG_PREBUILT_DIR` is set. With the WinLibs CMake, the download fails with status 60 (`SSL peer certificate or SSH remote key was not OK`) unless `SSL_CERT_FILE` names a CA bundle, for example `C:\Program Files\Git\mingw64\etc\ssl\certs\ca-bundle.crt` from Git for Windows, as in CI. +- FFmpeg binaries are downloaded during configuration unless `FFMPEG_PREBUILT_DIR` is set. With the WinLibs CMake, the download fails with status 60 (`SSL peer certificate or SSH remote key was not OK`) unless `SSL_CERT_FILE` names a CA bundle, for example `C:\Program Files\Git\ucrt64\etc\ssl\certs\ca-bundle.crt` from Git for Windows 2.56 and later (`mingw64` instead of `ucrt64` before 2.56), as in CI. ## Commands From aea1dd601afd98233c22eb45131a80306b00976b Mon Sep 17 00:00:00 2001 From: MrMha <114929652+MrMha@users.noreply.github.com> Date: Thu, 15 Oct 2026 17:21:14 +0400 Subject: [PATCH 0310/1000] feat(libc): provide the runtime scripting allocator hooks Use the registered application allocator and the source-backed alignment/size signature. Add callback ownership and failure checks; record unmeasured console behavior. --- core/libs/prx/libc/src/ApplicationHeap.cpp | 14 ++++++++++++++ core/libs/tests/ApplicationHeap.cpp | 13 +++++++++++++ docs/dev/debt/libc.md | 2 ++ 3 files changed, 29 insertions(+) diff --git a/core/libs/prx/libc/src/ApplicationHeap.cpp b/core/libs/prx/libc/src/ApplicationHeap.cpp index 6acd014d08..34b89291d4 100644 --- a/core/libs/prx/libc/src/ApplicationHeap.cpp +++ b/core/libs/prx/libc/src/ApplicationHeap.cpp @@ -202,6 +202,20 @@ void* ApplicationHeapAlign_nid_no_patch(std::size_t alignment, std::size_t bytes return pointer; } +extern "C" void* APS5_VABI scriptingGetMem_nid_postfix(std::size_t alignment, std::size_t bytes) { + alignment = std::max(alignment, 16); + if ((alignment & (alignment - 1)) != 0) return nullptr; + try { + return ApplicationHeapAlign_nid_no_patch(alignment, bytes); + } catch (const std::bad_alloc&) { + return nullptr; + } +} + +extern "C" void APS5_VABI scriptingFreeMem_nid_postfix(void* pointer) { + ApplicationHeapFree_nid_no_patch(pointer); +} + void* ApplicationHeapRealign_nid_no_patch(void* pointer, std::size_t bytes, std::size_t alignment) { if (bytes == 0) { ApplicationHeapFree_nid_no_patch(pointer); diff --git a/core/libs/tests/ApplicationHeap.cpp b/core/libs/tests/ApplicationHeap.cpp index a4c2856b60..c55bf3fb4e 100644 --- a/core/libs/tests/ApplicationHeap.cpp +++ b/core/libs/tests/ApplicationHeap.cpp @@ -36,6 +36,8 @@ char* APS5_VABI strndup_nid_postfix(const char*, std::size_t); char* APS5_VABI getcwd_nid_postfix(char*, std::size_t); int* APS5_VABI __error_nid_postfix(); int APS5_VABI atexit_nid_postfix(void (APS5_VABI*)()); +void* APS5_VABI scriptingGetMem_nid_postfix(std::size_t, std::size_t); +void APS5_VABI scriptingFreeMem_nid_postfix(void*); } namespace { @@ -385,4 +387,15 @@ int main(int argc, char** argv) { *__error_nid_postfix() = 0; require(getcwd_nid_postfix(nullptr, 0) == nullptr && *__error_nid_postfix() == 12); fail = false; + const auto beforeScripting = frees; + scriptingFreeMem_nid_postfix(nullptr); + require(frees == beforeScripting); + require(scriptingGetMem_nid_postfix(0, 31) == storage.data() && lastAlignment == 16 && lastSize == 31); + require(scriptingGetMem_nid_postfix(64, 37) == storage.data() && lastAlignment == 64 && lastSize == 37); + scriptingFreeMem_nid_postfix(storage.data()); + require(frees == beforeScripting + 1); + require(scriptingGetMem_nid_postfix(24, 80) == nullptr && lastSize == 37); + fail = true; + require(scriptingGetMem_nid_postfix(16, 64) == nullptr); + fail = false; } diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index e92d357f7f..500450621c 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -3,6 +3,8 @@ ### Unknown function info - [fdopen](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) validates descriptor access modes as FreeBSD 11.4 [fdopen.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/stdio/fdopen.c) does: a requested mode incompatible with a descriptor not opened for both reading and writing returns null with `EINVAL`, leaving the descriptor open. Windows reads the handle's granted access through the same host helper as `fcntl(F_GETFL)`; a closed descriptor returns null with `EBADF`. Console behavior is unverified. +- [scriptingGetMem, scriptingFreeMem](../../../core/libs/prx/libc/src/ApplicationHeap.cpp) (libc) - runtime allocator hooks for Unity titles. The allocation signature `(alignment, size)`, minimum alignment 16 and null result for a non-power-of-two alignment follow [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/df5b9b92be7dfd463b47756b707d6d356a9f0550/src/libs/libKernel.cpp); the free hook's one-pointer signature follows [Raeen's recorded contract](https://github.com/Whoisraeen/Raeen/blob/2db56923204b89719916b95e29c2f3c4e1d9f90b/docs/unity-dlsym-blocker.md). Both use the registered application allocator. No console measurement; null allocation returns null and other allocator errors throw. + - [_Mtx_trylock](../../../core/libs/prx/libc/src/Threads.cpp) (libc, NID `k6pGNMwJB08`) - returns busy for a non-recursive mutex already owned by the caller, assumed to match `_Mtx_lock`'s ownership handling; exact console behavior is unverified. - [nearbyintf](../../../core/libs/prx/libc/src/Math.cpp) (libc, NID `c+4r-T-tEIc`) uses the host rounding mode, not the console's; console rounding mode state remains unverified. - [_Thrd_sleep](../../../core/libs/prx/libc/src/Threads.cpp) (libc, NID `jfRI3snge3o`) follows the Dinkumware threads library shipped with [Visual C++ 2013](https://github.com/adityaramesh/concurrency_runtime/tree/master/VS2013_SP2/crt/src/thr) (`xtime.c`) and C11 Annex K, without console verification. It reads a 16-byte xtime (64-bit seconds and nanoseconds) on the guest `CLOCK_REALTIME`, normalizes the nanoseconds and sleeps through `nanosleep`, retrying on `EINTR`, so it is a cancellation point; a null xtime or one whose normalization overflows throws. The console's `_Thrd_sleep` (disassembled from a title's `libc.prx`) also takes an optional `xtime*` of remaining time and returns `int`; here the sleep is never cut short, so it returns 0 and leaves the remaining time untouched. From 99500f9d4508e1d8bed7222e204c0db8d0eb4c7f Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Thu, 15 Oct 2026 00:44:30 +0000 Subject: [PATCH 0311/1000] fix(shader): budget pixel lane proof for NHL 26 replay --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 4 +++- .../libSceAgcDriver/tests/ShaderMemory.cpp | 19 +++++++++++++++++++ .../tests/data/nhl26-wave64-pixel-lane63.req | 1 + .../src/SpirvAlu/SpirvAluEmitterMath.cpp | 2 +- 4 files changed, 24 insertions(+), 2 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/data/nhl26-wave64-pixel-lane63.req diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 051af9c6e5..99f035d482 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1498,8 +1498,10 @@ if(BUILD_TESTING) add_test(NAME agc_shader_layout_pair_off COMMAND agc_shader_memory_tests --layout-pair) set_tests_properties(agc_shader_layout_pair PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") set_tests_properties(agc_shader_layout_pair_off PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_NO_PERF_LAYOUT_PAIR=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + add_test(NAME agc_shader_entry_lane_pixel_replay COMMAND agc_shader_memory_tests --entry-lane-5398) + set_tests_properties(agc_shader_entry_lane_pixel_replay PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") if(WIN32) - set_tests_properties(agc_shader_bindless agc_shader_srt_read_patching agc_shader_type_zero_image_materialization agc_shader_frontend_pair agc_shader_frontend_pair_off agc_shader_layout_pair agc_shader_layout_pair_off PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + set_tests_properties(agc_shader_bindless agc_shader_srt_read_patching agc_shader_type_zero_image_materialization agc_shader_frontend_pair agc_shader_frontend_pair_off agc_shader_layout_pair agc_shader_layout_pair_off agc_shader_entry_lane_pixel_replay PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() foreach(agcTest IN ITEMS agc_vertex_input_tests agc_shared_write_tests agc_driver_tests agc_shader_memory_tests agc_structurizer_tests agc_spirv_module_tests agc_dpp_write_tests agc_masked_select_tests agc_ir_reclaim_tests agc_vector_window_tests agc_constant_bit_test_tests agc_integer_narrowing_tests agc_scalar_select_mask_tests agc_vop1_control_decode_tests agc_validate_program_tests agc_pass_hazards_tests agc_shader_disk_cache_tests agc_shader_device_profile_tests agc_placeholder_images_tests agc_shader_control_flow_tests agc_driver_flip_tests agc_driver_pm4_tests agc_driver_graphics_tests agc_driver_bda_device_tests agc_driver_mesh_tests agc_driver_cmask_fast_clear_tests agc_formatted_buffer_once_tests agc_formatted_buffer_byte_stores_tests ${agcExecutionTests} agc_fma_mix_once_tests) if(agcTest STREQUAL "agc_driver_constant_store_tests" OR agcTest STREQUAL "agc_driver_flat_apertures_tests" OR agcTest STREQUAL "agc_driver_pipeline_optimized_readback_tests") diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index a3294a30fe..81ace209fe 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -28,11 +28,13 @@ #include #include #include +#include #include #include #include #include #include +#include #include #include #include @@ -2418,6 +2420,18 @@ void verifyHalfWaveReduction() { expectFailure([&] { static_cast(pixel(umin, 8u, true)); }, "v_permlanex16_b32 from lanes 16-31 is outside the 8-lane host subgroup", "half-wave reduction: the scan built on an 8-lane subgroup"); } +void verifyNhl26Wave64PixelLane63Replay() { + using namespace ShaderRecompiler; + std::ifstream file("core/libs/prx/libSceAgcDriver/tests/data/nhl26-wave64-pixel-lane63.req", std::ios::binary); + require(file.good(), "NHL 26 wave64 pixel request fixture could not be opened"); + std::ostringstream serialized; + serialized << file.rdbuf(); + const auto request = RequestSerializer{}.Deserialize(serialized.str()); + require(request.request.shader.stage == ShaderStage::Fragment && request.request.context.waveSize == 64u && request.request.target.subgroupSize == 32u, "NHL 26 lane-63 fixture no longer describes a wave64 pixel shader on a 32-lane host"); + const auto handle = PrepareShader(request.request); + require(!GetPreparedArtifact(*handle).spirv.Words().empty(), "NHL 26 wave64 pixel shader did not produce SPIR-V"); +} + void verifyNarrowVertexWave() { using namespace ShaderRecompiler; static constexpr std::array capabilities{spv::CapabilityGroupNonUniform, spv::CapabilityGroupNonUniformBallot, spv::CapabilityGroupNonUniformShuffle, spv::CapabilityGroupNonUniformArithmetic, spv::CapabilityClipDistance}; @@ -5130,6 +5144,11 @@ int main(int argc, char** argv) { std::cout << "Both push-constant layouts of a pixel shader compile from one program unchanged\n"; return 0; } + if (argc == 2 && std::string_view(argv[1]) == "--entry-lane-5398") { + verifyNhl26Wave64PixelLane63Replay(); + std::cout << "The NHL 26 wave64 pixel lane-63 shader compiles for a 32-lane host\n"; + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--bindless") { verifyImageTableKeys(); verifyImageTables(); diff --git a/core/libs/prx/libSceAgcDriver/tests/data/nhl26-wave64-pixel-lane63.req b/core/libs/prx/libSceAgcDriver/tests/data/nhl26-wave64-pixel-lane63.req new file mode 100644 index 0000000000..f0ed10c761 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/data/nhl26-wave64-pixel-lane63.req @@ -0,0 +1 @@ +NVNQQRcAAAAFABCucxEAAADkAgAAAAAAAAMAoL8CEQh+AxEKfgABDPQgAAD6AAMM9AAAAPoDDwx+AAYI9KAAAPp/wIy/CAMA8AQAAQAIAQDwBAQDAAwHKPRAAAD6DAUk9FAAAPoMATD0AAAA+v8HJrB/wIy/HwwKShQEBBAVBgYQjBog9EwBAPoCA/y+gQoKNn/AjL+Aage/AwBB1fQGzgN+gKSFAgCiyAUNCn4CAabIgAIQfvICVH7BAlZ+8QoKBoACWH6AAk5+gAJKfoACRn4fCgQ+gAJMfoACSH6AAkR+gAJAfgIAQdX0BM4DgAJCfoACPn6AAjx+gAI6foACOH6AAjZ+gAI0foACMn6AAjB+gAIufoACLH6AAip+gAIofoACJn6AAiR+gAIifoACIH6AAh5+gAIcfoACGn6AAhh+gAIWfoACFH6AAhJ+fgSCvgwKKPRwAQD6fgSgvn4Eor5xP4y/AQBv1wEhAQT/AAw2/w8AAHA/jL8tAEHVBCVMADAAQdUEDRwAMQBB1QQVLAAHAEjVARkxAgYNAn75DIJ9JoaGBi0AH9URBgJABgBB1QQdPAAHDQh+AQBB1f8CzgMBCAA6+Q6CfSaKhgYQBFo+MAAf1QUGAkAFAEHV/wjOAwEIADr5AlwG8waGJjEAH9UJBgJALVUIfgYAH9UNBgJA+QpeBvIGhjb5XAJ8BQA2BgMAAdUuXSpA+VwABgUGNhYEBGA+LgAB1S9fGiAIBGI+BQcGAgwEDD4wAEHVBGFyAAFdAgIxAEHVBGN2ADIAQdUEDXoAAQMEEAMHBD4AAQQ+Al0IfgQDAhAEBwQQBAEKEAMHQdUEBwYE+QoIfICEhgYDAx/VAAkCAANVCH4EAwYQBAUAEAIDQdUEBc4DAQNB1QQDzgOABgZ8+QAGfICGhgb5BAQCAgYGFgEAAdUBAxpAAgAB1QMFEgAAAAHVAAMSAAGAS9XwBMIDAIBL1fAAwgMBAEHV/wLCAwDwf0UAAEHV/wDCAwDwf0UBDwJ+AA8Efv8CADb/DwAAAgBv1wIZBQQADQJ+AgBv14AwCQT5AIJ9JoSGBgEAQdX/As4DAQgAOgIASNUCGTEC+QIABvMGhiYCDQZ+/wSCff8HAAACAEHV/wbOAwEIADr5AAYCAAYGFvkECAbyBoY2+QACfAIANgb5AAAGAgY2FgQAAdUECRIgAgcEAgEJAgIBAwYQAgUGPgABBj4DXQZ+AwNeEAMFWhADAVwQMwAB1YBekgA0AAHVgFqSADUAAdWAXJIAEwGIvyxRjH1+av6KImqiig8BiL8sUwBKAAEI9IAAAPp/wIy/ACAw4AAAAYB+KOq+cD+Mv8EABAL6BAQmAhEB//oEBCYCEgH/+gQEJgIUAf/6BAQmAhgB/wEQeNcCgwUDAgMEJmoE/r4EAGDXAj8BAAUAYNcCfwEABAWcgxwAjH35BG0ChAaGhoJspH3rAIi/+RAGfPKIhgYc/2qTcAAAAAABCPRwAAD6f8CMv8IFIPRMAADUf8CMvxdWgn0IauqHImqiin5q/orcAIi/hAJsfoFYWEoc/x2TcAAAAAADCPRwAAD6f8CMvwYEKPRQAAA6BgEs9AAAADoGBij0IAAAOoYaJPRgAAA6f8CMvwQAQdUTasoEAQBB1RNoxgQAAEHVE2bCBBACBH4FAEHVBggeAAYAQdUKCC4ABwBB1RoIbgADAEHVBQIWBAkCDD4ZAg4+BAAGPgQAQdUIABoEBgBB1RgAHgT5Bgh8gISGBgAAV9XwBsYD+QgIfICGhgb5DAh8gIiGBgEAV9XwDMYDBwAB1WoEEgADAQoIAgBX1fAIxgOGGiD0aAAAOgYDBgj/DgwgbxKDOgQFCAgGVQx+BgsKEBECDH4GAAHVawwaAP8MDCBvEoM6BlUMfgYJCBAFCwwQEgIKfgQJDD5/wIy/BQAB1WoKIgD/CgogbxKDOgVVCn4FBwYQAwcMPvIMKHyLAIi/8hAICAADCPRwAAD6cAAcuPAEBAZ/wIy/BgYk9EAAADiACAggF1aCffACAgbwAAAGBmcKfhcCVn6GBiT0MAAAOAAECPSQAAD6AAEM9EAAAPoqCVQCf8CMvxkMCH6GGiD0bAAAOMYaIPQ4AAA4f8CMv2pUbhDyCAYGhhog9EgAADg3AB/VNwsCQAMFBBAIbxAGBAUGHhsMBH7yBgYgAgcGBn/AjL9qDAR+AwBL1fAGxgPyBAgG/wYGBgAAgD4EAwIQOQBB1SkGwgMCAwQeawwCfikGcBDyBAQgAQUEBhgMAn4CAEvV8ATGA/ICBgb/BAQGAACAPgMBABA6AEHVKgTCAwEBAh4aDAB+KgR2EPICAiAAAwAGAABL1fAAxgP/AAAGAACAPigAeBA9AEHVKADCAxIHnPA8AIEAOToAABIPnPA8BIEAOTsAAHE/jL83ARY+NwUSPjcDFD5wP4y/NwlKPjcPOj43DUI+NwtIPhIPnPA9AIEAOToAABIPnPA8BIEAODsAAHE/jL83ATA+NwcYPjcFID43Ayg+cD+MvzcJTj43Dzw+Nw1APjcLTD4SD5zwPQCBADg6AAASD5zwPASBADg6AABxP4y/NwEyPjcHGj43BSI+NwMqPhIPnPA9AIEAOTsAAHE/jL83CUY+Nw84PjcNPj43C0Q+cD+MvzcBND43Bxw+NwUkPjcDLD4SD5zwPQCBADg7AABwP4y/NwE2PjcHHj43BSY+NwMuPiIE/r6EbKR9AgCIv4ACbH7s/oK/IAT+viAE6r7/EAAgrMUnN/IQUB4AVQB+AFFSECdTThAlU0oQI1NGECZTChAkUwYQIlMCECBTRBAhU0AQH1M+EB5TDBAdUwgQHFMEEBtTNhAaUzQQGVMyEBhTABAXUy4QFlMsEBVTKhAUUw4QE1MmEBJTJBARUyIQEFMgEA9THhAOUxwQDVMaEAxTGBALUxYQClMUEAlTEhDyECh8KgCIv/JQEAgACQj0YAAA+n/AjL8SATD0AAAA+hIFMPRAAAD6Egoo9IAAAPp/wIy/BBBOPgUQSj4GEEY+CBAKPgkQBj4KEAI+DBBEPg0QQD4OED4+EBAMPhEQCD4SEAQ+FBA2PhUQND4WEDI+FxAAPhgQLj4ZECw+GhAqPhsQDj4cECY+HRAkPh4QIj4fECA+IBAePiEQHD4iEBo+IxAYPigQFj4pEBQ+KhASPmoE/r4AAQj0oAAA+i9fOhAnNzwIJTVCCCMzOAgPXxAQHQAf1S5dAkD0NjZWDl8cEPQ0NFYNXxoQ9DIyVh0XPD4dFUI+HRM4Pi03RD4tNUA+LhE8Pi4dQj4uGzg+LTM+PhdfRD4vCzw+LwdCPhZfQD4vAzg+FV8+Pi4NPD4TXUQ+LglCPhJdQD4uBTg+EV0+Pn/AjL/CGiD0WAAA+oIaIPRgAAD6Ils8Pn/AjL/51AQQawaGhiBbQj4fWzg+AA8AXhAZCF4CPQoQAkMGEAI5AhAFBwReAQAv1QEBAQACBP6+DxQA+AIBAAAfDAD4AAQAAAAAgb9zbDAwDgEAACYBAAAAAAAAZQ8gIRAiICRAAABXJRAAAF0pEAAAYSkQAACmAQQQQEBCIEsQTBBNEE4Qb2B5EHoQhRAAAG4AAGQPICEQJEAAAGQPICEQIiAkQAAAWQAARCRAAABBglEAASAAPYCgQ8BBAGoRYEAARaXnYOFB0klYOt1Co8l4Mg+RLmLKTFaXlxlbZ/PwCw0dRQCUAiaWAAABXvmBgAAloAEFW6UBJakBASCsAQggsAEHUAKgAQClARC4AQGQAQCpARC/AQCAARKsARDIAQBwEKwBENEBAGAGrAEQ2QEAQBGwARDhAQAgBbQBEOkBAAAAtAEbtAHxAbABRxsAoAEb+QGAAghEhQIqiwIBhQIBDvhV4gMBYz2Qi6BAIIiEnncAAAAAAAAAAAAAYmFyZWZvb3T700E4AAAAAAEAAAAwCgAAAAAAACYBAAABoyTtCILnDgAAAAAAAAAAABCmUBEAAACYAQAAAAAAADEyMzQYAAAAEBGmUBEAAAAAEK5zEQAAAMgQplARAAAAmBCmUBEAAABgEKZQEQAAAJAQplARAAAAAAAAAAAAAACYAQAAkAsAAAAAAAAOAAAAAQAAAAAAAAAwAAEJBgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA8AQAAAAAAACAAAABCucxEJAAAAAAAAAAYAAABgCwAhBgAAAPvTQTgKAAAATwEsAgsAAAAEAAAAxAEAAAAAAADFAQAARAAAALMBAAACAwAAtAEAAAIDAAC2AQAAAQAAALgBAAAAAAABAwIAABAAAACPAAAA/wAAABADAAAAAAAASBGmUBEAAABgEaZQEQAAAIgRplARAAAAiBGmUBEAAACQEaZQEQAAACwAAAALABMAAAABAAEAAAD/////////////AAD/////////////AAAgAP//////////KAA4gP///////////////////////zyAMABAgAAARIAAAAAAAABIgAAAAAAAAEAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAABAQAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIDAAABAAEBAAAAAAAAAAAAAAEAAAAEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAACAAAAAAAAAAAQrnMRAAAAkAsAAAAAAAADAKC/AhEIfgMRCn4AAQz0IAAA+gADDPQAAAD6Aw8MfgAGCPSgAAD6f8CMvwgDAPAEAAEACAEA8AQEAwAMByj0QAAA+gwFJPRQAAD6DAEw9AAAAPr/Byawf8CMvx8MCkoUBAQQFQYGEIwaIPRMAQD6AgP8voEKCjZ/wIy/gGoHvwMAQdX0Bs4DfoCkhQIAosgFDQp+AgGmyIACEH7yAlR+wQJWfvEKCgaAAlh+gAJOfoACSn6AAkZ+HwoEPoACTH6AAkh+gAJEfoACQH4CAEHV9ATOA4ACQn6AAj5+gAI8foACOn6AAjh+gAI2foACNH6AAjJ+gAIwfoACLn6AAix+gAIqfoACKH6AAiZ+gAIkfoACIn6AAiB+gAIefoACHH6AAhp+gAIYfoACFn6AAhR+gAISfn4Egr4MCij0cAEA+n4EoL5+BKK+cT+MvwEAb9cBIQEE/wAMNv8PAABwP4y/LQBB1QQlTAAwAEHVBA0cADEAQdUEFSwABwBI1QEZMQIGDQJ++QyCfSaGhgYtAB/VEQYCQAYAQdUEHTwABw0IfgEAQdX/As4DAQgAOvkOgn0mioYGEARaPjAAH9UFBgJABQBB1f8IzgMBCAA6+QJcBvMGhiYxAB/VCQYCQC1VCH4GAB/VDQYCQPkKXgbyBoY2+VwCfAUANgYDAAHVLl0qQPlcAAYFBjYWBARgPi4AAdUvXxogCARiPgUHBgIMBAw+MABB1QRhcgABXQICMQBB1QRjdgAyAEHVBA16AAEDBBADBwQ+AAEEPgJdCH4EAwIQBAcEEAQBChADB0HVBAcGBPkKCHyAhIYGAwMf1QAJAgADVQh+BAMGEAQFABACA0HVBAXOAwEDQdUEA84DgAYGfPkABnyAhoYG+QQEAgIGBhYBAAHVAQMaQAIAAdUDBRIAAAAB1QADEgABgEvV8ATCAwCAS9XwAMIDAQBB1f8CwgMA8H9FAABB1f8AwgMA8H9FAQ8CfgAPBH7/AgA2/w8AAAIAb9cCGQUEAA0CfgIAb9eAMAkE+QCCfSaEhgYBAEHV/wLOAwEIADoCAEjVAhkxAvkCAAbzBoYmAg0Gfv8Egn3/BwAAAgBB1f8GzgMBCAA6+QAGAgAGBhb5BAgG8gaGNvkAAnwCADYG+QAABgIGNhYEAAHVBAkSIAIHBAIBCQICAQMGEAIFBj4AAQY+A10GfgMDXhADBVoQAwFcEDMAAdWAXpIANAAB1YBakgA1AAHVgFySABMBiL8sUYx9fmr+iiJqoooPAYi/LFMASgABCPSAAAD6f8CMvwAgMOAAAAGAfijqvnA/jL/BAAQC+gQEJgIRAf/6BAQmAhIB//oEBCYCFAH/+gQEJgIYAf8BEHjXAoMFAwIDBCZqBP6+BABg1wI/AQAFAGDXAn8BAAQFnIMcAIx9+QRtAoQGhoaCbKR96wCIv/kQBnzyiIYGHP9qk3AAAAAAAQj0cAAA+n/AjL/CBSD0TAAA1H/AjL8XVoJ9CGrqhyJqoop+av6K3ACIv4QCbH6BWFhKHP8dk3AAAAAAAwj0cAAA+n/AjL8GBCj0UAAAOgYBLPQAAAA6BgYo9CAAADqGGiT0YAAAOn/AjL8EAEHVE2rKBAEAQdUTaMYEAABB1RNmwgQQAgR+BQBB1QYIHgAGAEHVCgguAAcAQdUaCG4AAwBB1QUCFgQJAgw+GQIOPgQABj4EAEHVCAAaBAYAQdUYAB4E+QYIfICEhgYAAFfV8AbGA/kICHyAhoYG+QwIfICIhgYBAFfV8AzGAwcAAdVqBBIAAwEKCAIAV9XwCMYDhhog9GgAADoGAwYI/w4MIG8SgzoEBQgIBlUMfgYLChARAgx+BgAB1WsMGgD/DAwgbxKDOgZVDH4GCQgQBQsMEBICCn4ECQw+f8CMvwUAAdVqCiIA/woKIG8SgzoFVQp+BQcGEAMHDD7yDCh8iwCIv/IQCAgAAwj0cAAA+nAAHLjwBAQGf8CMvwYGJPRAAAA4gAgIIBdWgn3wAgIG8AAABgZnCn4XAlZ+hgYk9DAAADgABAj0kAAA+gABDPRAAAD6KglUAn/AjL8ZDAh+hhog9GwAADjGGiD0OAAAOH/AjL9qVG4Q8ggGBoYaIPRIAAA4NwAf1TcLAkADBQQQCG8QBgQFBh4bDAR+8gYGIAIHBgZ/wIy/agwEfgMAS9XwBsYD8gQIBv8GBgYAAIA+BAMCEDkAQdUpBsIDAgMEHmsMAn4pBnAQ8gQEIAEFBAYYDAJ+AgBL1fAExgPyAgYG/wQEBgAAgD4DAQAQOgBB1SoEwgMBAQIeGgwAfioEdhDyAgIgAAMABgAAS9XwAMYD/wAABgAAgD4oAHgQPQBB1SgAwgMSB5zwPACBADk6AAASD5zwPASBADk7AABxP4y/NwEWPjcFEj43AxQ+cD+MvzcJSj43Dzo+Nw1CPjcLSD4SD5zwPQCBADk6AAASD5zwPASBADg7AABxP4y/NwEwPjcHGD43BSA+NwMoPnA/jL83CU4+Nw88PjcNQD43C0w+Eg+c8D0AgQA4OgAAEg+c8DwEgQA4OgAAcT+MvzcBMj43Bxo+NwUiPjcDKj4SD5zwPQCBADk7AABxP4y/NwlGPjcPOD43DT4+NwtEPnA/jL83ATQ+NwccPjcFJD43Ayw+Eg+c8D0AgQA4OwAAcD+MvzcBNj43Bx4+NwUmPjcDLj4iBP6+hGykfQIAiL+AAmx+7P6CvyAE/r4gBOq+/xAAIKzFJzfyEFAeAFUAfgBRUhAnU04QJVNKECNTRhAmUwoQJFMGECJTAhAgU0QQIVNAEB9TPhAeUwwQHVMIEBxTBBAbUzYQGlM0EBlTMhAYUwAQF1MuEBZTLBAVUyoQFFMOEBNTJhASUyQQEVMiEBBTIBAPUx4QDlMcEA1TGhAMUxgQC1MWEApTFBAJUxIQ8hAofCoAiL/yUBAIAAkI9GAAAPp/wIy/EgEw9AAAAPoSBTD0QAAA+hIKKPSAAAD6f8CMvwQQTj4FEEo+BhBGPggQCj4JEAY+ChACPgwQRD4NEEA+DhA+PhAQDD4REAg+EhAEPhQQNj4VEDQ+FhAyPhcQAD4YEC4+GRAsPhoQKj4bEA4+HBAmPh0QJD4eECI+HxAgPiAQHj4hEBw+IhAaPiMQGD4oEBY+KRAUPioQEj5qBP6+AAEI9KAAAPovXzoQJzc8CCU1QggjMzgID18QEB0AH9UuXQJA9DY2Vg5fHBD0NDRWDV8aEPQyMlYdFzw+HRVCPh0TOD4tN0Q+LTVAPi4RPD4uHUI+Lhs4Pi0zPj4XX0Q+Lws8Pi8HQj4WX0A+LwM4PhVfPj4uDTw+E11EPi4JQj4SXUA+LgU4PhFdPj5/wIy/whog9FgAAPqCGiD0YAAA+iJbPD5/wIy/+dQEEGsGhoYgW0I+H1s4PgAPAF4QGQheAj0KEAJDBhACOQIQBQcEXgEAL9UBAQEAAgT+vg8UAPgCAQAAHwwA+AAEAAAAAIG/c2wwMA4BAAAmAQAAAAAAAGUPICEQIiAkQAAAVyUQAABdKRAAAGEpEAAApgEEEEBAQiBLEEwQTRBOEG9geRB6EIUQAABuAABkDyAhECRAAABkDyAhECIgJEAAAFkAAEQkQAAAQYJRAAEgAD2AoEPAQQBqEWBAAEWl52DhQdJJWDrdQqPJeDIPkS5iykxWl5cZW2fz8AsNHUUAlAImlgAAAV75gYAAJaABBVulASWpAQEgrAEIILABB1ACoAEApQEQuAEBkAEAqQEQvwEAgAESrAEQyAEAcBCsARDRAQBgBqwBENkBAEARsAEQ4QEAIAW0ARDpAQAAALQBG7QB8QGwAUcbAKABG/kBgAIIRIUCKosCAYUCAQ74VeIDAWM9kIugQCCIhJ53AAAAAAAAAAAAAGJhcmVmb290+9NBOAAAAAABAAAAMAoAAAAAAAAmAQAAAaMk7QiC5w4AAAAAAAAAAAAQplARAAAAmAEAAAAAAAAxMjM0GAAAABARplARAAAAABCucxEAAADIEKZQEQAAAJgQplARAAAAYBCmUBEAAACQEKZQEQAAAAAAAAAAAAAAmAEAAJALAAAAAAAADgAAAAEAAAAAAAAAMAABCQYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPAEAAAAAAAAgAAAAQrnMRCQAAAAAAAAAGAAAAYAsAIQYAAAD700E4CgAAAE8BLAILAAAABAAAAMQBAAAAAAAAxQEAAEQAAACzAQAAAgMAALQBAAACAwAAtgEAAAEAAAC4AQAAAAAAAQMCAAAQAAAAjwAAAP8AAAAQAwAAAAAAAEgRplARAAAAYBGmUBEAAACIEaZQEQAAAIgRplARAAAAkBGmUBEAAAAsAAAACwATAAAAAQABAAAA/////////////wAA/////////////wAAIAD//////////ygAOID///////////////////////88gDAAQIAAAESAAAAAAAAASIAAAAAAAAABwAAAAAEAAAAQQAAABAEAIAAAAAEAAAAjAAAAAAAAAAEAAAACAAAAAwAAAAkAAAAKAAAACwAAAAwAAAAZAAAAGwAAAB0AAAAfAAAAIAAAACEAAAAqAAAAMAAAADcAAAA4AAAAPQAAAD8AAABAAAAAQQAAAGARAABwEQAAchEAAHMRAACYEwAAvxMAAIYUAACjFAAApBQAALUUAAC7FAAAvRQAAOMUAAACFQAACgAAAAAAAAAbAAAAAAAAAFNQVl9FWFRfZGVzY3JpcHRvcl9pbmRleGluZyEAAAAAAAAAU1BWX0VYVF9mcmFnbWVudF9zaGFkZXJfaW50ZXJsb2NrEwAAAAAAAABTUFZfRVhUX21lc2hfc2hhZGVyGgAAAAAAAABTUFZfRVhUX3NoYWRlcl9pbWFnZV9pbnQ2NCMAAAAAAAAAU1BWX0VYVF9zaGFkZXJfdmlld3BvcnRfaW5kZXhfbGF5ZXIUAAAAAAAAAFNQVl9LSFJfOGJpdF9zdG9yYWdlFgAAAAAAAABTUFZfS0hSX2Zsb2F0X2NvbnRyb2xzIwAAAAAAAABTUFZfS0hSX2ZyYWdtZW50X3NoYWRlcl9iYXJ5Y2VudHJpYx8AAAAAAAAAU1BWX0tIUl9waHlzaWNhbF9zdG9yYWdlX2J1ZmZlchQAAAAAAAAAU1BWX0tIUl9zaGFkZXJfY2xvY2sBAAQAAAAEAABAAAAAAAQAAADAAAABgAAAAIAAAACAAAAAgAAAAABwAAAAAQAAAAEAAIAAAAAAgAAAIAAAACAAAAABIAAAAIAAAACAAAAAeAAAAHgQAACAAAAAgAAAAAAAAAAAAAAAgAAAAIAAAAAAAQECAAAAAAABQA0DAAEB \ No newline at end of file diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp index 757a979fa5..0a499678c5 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp @@ -389,7 +389,7 @@ private: bool everyInvocationEnters; std::unordered_set phis; - std::uint32_t budget = 512u; + std::uint32_t budget = 2048u; }; bool EveryLaneBit(const IrValue* bit, bool constantTrue) { From d65cc07a1b609b836be77af25916861e2fc66f1f Mon Sep 17 00:00:00 2001 From: AlxcBTW <187763517+AlxcBTW@users.noreply.github.com> Date: Thu, 15 Oct 2026 14:09:34 +0200 Subject: [PATCH 0312/1000] perf(agc): load the pipeline cache file only on the windowed device and save without re-reading an unchanged file The headless device is replaced by the windowed one at window attach and then only lingers for in-flight workers, but it parsed its own full copy of the cache file. It now starts empty (it still merges what it compiles into the file). Load reuses the file buffer as the initial data instead of copying it, and save() writes the live data directly when the file still carries the header this object loaded or wrote, skipping the read-back and the merge cache. --- .../Execution/src/VulkanDevice.cpp | 2 +- .../Graphics/include/PipelineCache.hpp | 2 +- .../Graphics/src/PipelineCache.cpp | 20 +++++++++-------- .../prx/libSceAgcDriver/tests/Recorder.cpp | 22 +++++++++++++++++++ 4 files changed, 35 insertions(+), 11 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index bb39fa7beb..0044028bfb 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -1839,7 +1839,7 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : Graphics::PrepareImportWatch(graphicsContext()); state->bufferPool = std::make_shared(graphicsContext()); state->emptyBuffer = std::make_unique(graphicsContext(), Graphics::EmptyBufferBytes, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); - state->pipelineCache = std::make_unique(graphicsContext(), state->properties); + state->pipelineCache = std::make_unique(graphicsContext(), state->properties, window != nullptr || compileOnly); state->detiler = std::make_unique(graphicsContext()); state->textureCache = std::make_unique(graphicsContext()); state->colorTransfer = std::make_unique(graphicsContext()); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/PipelineCache.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/PipelineCache.hpp index 92506617e4..73b999b4d6 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/PipelineCache.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/PipelineCache.hpp @@ -18,7 +18,7 @@ class PipelineCache { public: static constexpr int SaveIntervalSeconds = 10; - PipelineCache(const Context& context, const VkPhysicalDeviceProperties& properties); + PipelineCache(const Context& context, const VkPhysicalDeviceProperties& properties, bool loadFromFile = true); ~PipelineCache(); PipelineCache(const PipelineCache&) = delete; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineCache.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineCache.cpp index 8b5d426e59..ebfa65095e 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineCache.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineCache.cpp @@ -35,7 +35,7 @@ std::mutex& pathMutex(const std::filesystem::path& path) { } -PipelineCache::PipelineCache(const Context& context, const VkPhysicalDeviceProperties& properties) : context(context), properties(properties) { +PipelineCache::PipelineCache(const Context& context, const VkPhysicalDeviceProperties& properties, bool loadFromFile) : context(context), properties(properties) { static_assert(sizeof(FileHeader) == 24); const auto directory = ShaderRecompiler::ShaderCacheDirectory(); if (!directory.empty()) { @@ -52,7 +52,7 @@ PipelineCache::PipelineCache(const Context& context, const VkPhysicalDevicePrope std::vector initialData; if (!path.empty()) { std::lock_guard pathLock(pathMutex(path)); - load(initialData); + if (loadFromFile) load(initialData); } VkPipelineCacheCreateInfo info{VK_STRUCTURE_TYPE_PIPELINE_CACHE_CREATE_INFO}; info.initialDataSize = initialData.size(); @@ -145,7 +145,7 @@ std::optional PipelineCache::readHeader() const { } void PipelineCache::load(std::vector& initialData) { - const auto file = readFile(); + auto file = readFile(); if (file.state == FileState::Missing) return; if (file.state == FileState::Unreadable) { std::fprintf(stderr, "[pipeline-cache] cannot read %s; starting empty\n", path.string().c_str()); @@ -155,8 +155,8 @@ void PipelineCache::load(std::vector& initialData) { std::fprintf(stderr, "[pipeline-cache] ignoring %s: %s\n", path.string().c_str(), file.invalidReason); return; } - const auto data = file.Payload(); - initialData.assign(data.begin(), data.end()); + initialData = std::move(file.contents); + initialData.erase(initialData.begin(), initialData.begin() + sizeof(FileHeader)); loadedBytes = initialData.size(); settledBytes = initialData.size(); settledFile = file.header; @@ -171,20 +171,22 @@ void PipelineCache::save(bool final) { auto synchronizationLock = synchronization.LockForSnapshot(); if (getData(context.device, cache, &liveBytes, nullptr) != VK_SUCCESS || liveBytes == 0) return; } - if (liveBytes == settledBytes && readHeader() == settledFile) return; + const auto onDisk = readHeader(); + if (liveBytes == settledBytes && onDisk == settledFile) return; + const bool fileUnchanged = settledFile.has_value() && onDisk == settledFile; const auto started = std::chrono::steady_clock::now(); const auto settle = [&](const std::optional& file) { settledBytes = liveBytes; settledFile = file; }; - const auto disk = readFile(); + const auto disk = fileUnchanged ? File{} : readFile(); if (disk.state == FileState::Unreadable) { std::fprintf(stderr, "[pipeline-cache] cannot read %s; not saving over it\n", path.string().c_str()); settle(readHeader()); return; } if (disk.state == FileState::Invalid) std::fprintf(stderr, "[pipeline-cache] replacing %s: %s\n", path.string().c_str(), disk.invalidReason); - std::size_t floor = 0; + std::size_t floor = fileUnchanged ? std::max(loadedBytes, settledFile->dataBytes) : 0; VkPipelineCache merged = VK_NULL_HANDLE; struct DestroyMerged { const Context& context; @@ -231,7 +233,7 @@ void PipelineCache::save(bool final) { } if (size < floor) { std::fprintf(stderr, "[pipeline-cache] keeping %s: the merged cache has %.1f KiB, less than the %.1f KiB on disk or loaded at start\n", path.string().c_str(), static_cast(size) / 1024.0, static_cast(floor) / 1024.0); - settle(disk.header); + settle(fileUnchanged ? *settledFile : disk.header); return; } const FileHeader header{FileMagic, FileFormat, size, ShaderRecompiler::HashBytes(std::span(file).subspan(sizeof(FileHeader)))}; diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 040c2dd195..662ce7d21c 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -7429,6 +7429,28 @@ int pipelineCacheFileTests(const Device& device) { PipelineCache idle(context, properties); } Require(contents(path) == stored, "an object that added no pipelines rewrote the cache file"); + { + PipelineCache unseeded(context, properties, false); + Require(!holds(unseeded.Handle(), 0), "a cache created without the file already held a pipeline of the file"); + } + { + PipelineCache reloaded(context, properties); + Require(holds(reloaded.Handle(), 0) && holds(reloaded.Handle(), 1), "an unseeded cache dropped pipelines from the cache file when it saved"); + } + std::filesystem::remove_all(directory); + { + PipelineCache writer(context, properties); + build(writer.Handle(), 0); + } + { + PipelineCache extender(context, properties); + Require(holds(extender.Handle(), 0), "the cache file lost a pipeline"); + build(extender.Handle(), 1); + } + { + PipelineCache reloaded(context, properties); + Require(holds(reloaded.Handle(), 0) && holds(reloaded.Handle(), 1), "a cache that saved over an unchanged file lost pipelines"); + } { std::ofstream corrupt(path, std::ios::binary | std::ios::trunc); corrupt << "not a pipeline cache"; From b0c283daa8b3ffce6a20f7b385d6793fb5e6c8a7 Mon Sep 17 00:00:00 2001 From: Crimson <144462348+CrimsonMac@users.noreply.github.com> Date: Wed, 14 Oct 2026 21:41:20 +0300 Subject: [PATCH 0313/1000] docs(build): note Bitdefender Advanced Threat Defense hanging guest_raise_exception --- docs/dev/BUILD.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/dev/BUILD.md b/docs/dev/BUILD.md index 2dfcffda8d..23a097e7f9 100644 --- a/docs/dev/BUILD.md +++ b/docs/dev/BUILD.md @@ -32,6 +32,7 @@ git submodule update --init --recursive - Fedora: if CMake reports `Could NOT find Threads`, install `glibc-static` and `libstdc++-static` with `sudo dnf install glibc-static libstdc++-static`, then rerun configuration. The project enables static executable linking, and FFmpeg's thread check needs these libraries. - Windows: only MinGW-w64 GCC 15.2.0 (WinLibs `x86_64-ucrt-posix-seh`, release [`15.2.0posix-14.0.0-ucrt-r7`](https://github.com/brechtsanders/winlibs_mingw/releases/tag/15.2.0posix-14.0.0-ucrt-r7), archive `winlibs-x86_64-posix-seh-gcc-15.2.0-mingw-w64ucrt-14.0.0-r7.zip`) is currently supported. Add its `mingw64/bin` directory to `PATH` before configuring. - Windows antivirus software can quarantine the executables that the relinker tests create in the temp directory, which fails `optional_plt`, `empty_tls`, `windows_address_space` and `windows_icon`. Set `TEMP` and `TMP` to a directory the scanner excludes before running `ctest`. +- Bitdefender Advanced Threat Defense hooks Windows calls that `sceKernelRaiseException` makes while its target thread is suspended. When the target is suspended inside ntdll's exception dispatch while it holds an ntdll lock, the hook waits for that lock and `guest_raise_exception` hangs until its timeout. A scanning exception for the build directory does not prevent it; add `guest_raise_exception_tests.exe` to the Advanced Threat Defense exceptions. - FFmpeg binaries are downloaded during configuration unless `FFMPEG_PREBUILT_DIR` is set. With the WinLibs CMake, the download fails with status 60 (`SSL peer certificate or SSH remote key was not OK`) unless `SSL_CERT_FILE` names a CA bundle, for example `C:\Program Files\Git\ucrt64\etc\ssl\certs\ca-bundle.crt` from Git for Windows 2.56 and later (`mingw64` instead of `ucrt64` before 2.56), as in CI. ## Commands From c712a6bebd38a7971cbbeb80981e7bf7d3762e50 Mon Sep 17 00:00:00 2001 From: lakhantheboss <61951686+lakhantheboss@users.noreply.github.com> Date: Thu, 15 Oct 2026 19:03:09 +0530 Subject: [PATCH 0314/1000] fix(web): reset interface state and input values on change input --- demo/web/app.mjs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/demo/web/app.mjs b/demo/web/app.mjs index 79fc9f901d..bb48086a67 100644 --- a/demo/web/app.mjs +++ b/demo/web/app.mjs @@ -195,8 +195,19 @@ async function selectFile(selected, files) { worker.postMessage(files ? { type: "inspect", files } : { type: "inspect", file }); } -$("choose").onclick = () => $("file").click(); -$("change").onclick = () => { $("drop").hidden = false; }; +$("choose").onclick = () => { $("file").value = ""; $("file").click(); }; +$("change").onclick = () => { + if (working) return; + if (downloadUrl) { URL.revokeObjectURL(downloadUrl); downloadUrl = null; } + file = null; + summary = null; + $("file").value = ""; + $("folder").value = ""; + for (const id of ["selection", "warnings", "error", "done", "download"]) $(id).hidden = true; + $("acknowledge").checked = false; + $("drop").hidden = false; + update(); +}; $("choose-folder").onclick = () => { $("folder").value = ""; $("folder").click(); }; $("folder").onchange = () => { const files = [...$("folder").files].map(file => ({ path: file.webkitRelativePath, file })); From 63db31fdd18b3d1f51d2001fc44b451a72afa245 Mon Sep 17 00:00:00 2001 From: simonbronk <275178677+simonbronk@users.noreply.github.com> Date: Wed, 14 Oct 2026 15:00:21 +0200 Subject: [PATCH 0315/1000] fix(shader): read explicit-LOD and level-zero 2D-array samples through 1D T#s as measured Co-Authored-By: Claude Opus 5.5 --- .../execution/ImagePlaneAddressOnLine.cpp | 54 +++++++++++++++++++ .../Optimization/src/ResourceMaterializer.cpp | 2 +- .../Optimization/src/ResourceTracker.cpp | 4 +- docs/dev/debt/shader.md | 2 +- 4 files changed, 59 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImagePlaneAddressOnLine.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImagePlaneAddressOnLine.cpp index 465df1cabf..6e3186841c 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ImagePlaneAddressOnLine.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImagePlaneAddressOnLine.cpp @@ -47,6 +47,18 @@ alignas(256) constexpr std::array ExplicitLodCode{ 0x80000d01, 0xe0701028, 0x80000e01, 0xe070102c, 0x80000f01, 0xbf810000, }; +alignas(256) constexpr std::array ArrayExplicitLodCode{ + 0x34020087, 0xe0301000, 0x80000201, 0xe0301004, 0x80000301, 0xe0301008, 0x80000401, 0xe030100c, + 0x80000501, 0xbf8c3f70, 0xf0900f28, 0x00610c02, 0xbf8c3f70, 0xe0701020, 0x80000c01, 0xe0701024, + 0x80000d01, 0xe0701028, 0x80000e01, 0xe070102c, 0x80000f01, 0xbf810000, +}; + +alignas(256) constexpr std::array ArrayLevelZeroCode{ + 0x34020087, 0xe0301000, 0x80000201, 0xe0301004, 0x80000301, 0xe0301008, 0x80000401, 0xe030100c, + 0x80000501, 0xbf8c3f70, 0xf09c0f28, 0x00610c02, 0xbf8c3f70, 0xe0701020, 0x80000c01, 0xe0701024, + 0x80000d01, 0xe0701028, 0x80000e01, 0xe070102c, 0x80000f01, 0xbf810000, +}; + alignas(256) constexpr std::array LineLodCode{ 0x34020087, 0xe0301000, 0x80000201, 0xe0301008, 0x80000301, 0xbf8c3f70, 0xf0900f00, 0x00610c02, 0xbf8c3f70, 0xe0701020, 0x80000c01, 0xe0701024, 0x80000d01, 0xe0701028, 0x80000e01, 0xe070102c, 0x80000f01, 0xbf810000, @@ -78,6 +90,7 @@ constexpr std::array SamplerCases{{ constexpr std::array LodU{0.03125f, 0.33125f, 0.96875f, 0.0f, 1.0f, -0.2f, 1.3f, 0.6f}; constexpr std::array Lods{0.0f, 1.0f, 2.0f, 1.25f, 2.75f, 3.6f, 6.0f, -1.0f}; +constexpr std::array Slices{0.0f, 1.0f, 2.7f, -1.0f, 5.0f}; constexpr std::array LineV{0.0f, 0.5f, 1.0f, 1.5f, -0.25f, 3.0f, 0.25f, 0.75f}; constexpr std::array LineY{0u, 1u, 2u, 3u, 0xffffffffu, 7u, 0u, 5u}; @@ -250,6 +263,24 @@ void FillLodInput() { } } +void FillArrayInput(bool levelZero) { + Buffer.fill(0xdeadbeefu); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto lod = levelZero ? 0.0f : LaneLod(tid); + const std::array words{Bits(LaneU(tid)), Bits(LineV[(tid / 2u) % LineV.size()]), Bits(Slices[tid % Slices.size()]), Bits(lod)}; + std::copy(words.begin(), words.end(), &Buffer[tid * Words]); + } +} + +void FillLineInput(bool levelZero) { + Buffer.fill(0xdeadbeefu); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto lod = levelZero ? 0.0f : LaneLod(tid); + const std::array words{Bits(LaneU(tid)), 0u, Bits(lod), 0u}; + std::copy(words.begin(), words.end(), &Buffer[tid * Words]); + } +} + std::array SampleResults() { std::array results{}; for (std::uint32_t tid = 0; tid < Threads; ++tid) std::copy_n(&Buffer[tid * Words + SampleResult], 4u, &results[tid * 4u]); @@ -296,6 +327,28 @@ void CheckExplicitLod(AgcDriver::VulkanDevice& device) { } } } + +void CheckArray(AgcDriver::VulkanDevice& device) { + FillMippedLine(); + for (const bool levelZero : {false, true}) { + for (const auto& sampler : SamplerCases) { + const auto samplerWords = SamplerDescriptor(sampler); + FillLineInput(levelZero); + Run(device, LineLodCode, MippedLineDescriptor(), samplerWords); + const auto reference = SampleResults(); + FillArrayInput(levelZero); + Run(device, levelZero ? std::span(ArrayLevelZeroCode) : std::span(ArrayExplicitLodCode), MippedLineDescriptor(), samplerWords); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto what = std::string(levelZero ? "image_sample_lz" : "image_sample_l") + " 2d array on a 1D texture with " + sampler.name + ", u " + std::to_string(LaneU(tid)) + ", v " + std::to_string(LineV[(tid / 2u) % LineV.size()]) + ", slice " + std::to_string(Slices[tid % Slices.size()]); + for (std::uint32_t component = 0; component < 4u; ++component) { + const auto value = Buffer[tid * Words + SampleResult + component]; + const auto expected = reference[tid * 4u + component]; + Require(value == expected, what + ": thread " + std::to_string(tid) + " component " + std::to_string(component) + " has bits " + std::to_string(value) + ", image_sample_l 1d returned bits " + std::to_string(expected)); + } + } + } + } +} } int main() { @@ -311,6 +364,7 @@ int main() { Run(*device, Code, PlaneDescriptor()); CheckPlane(); CheckExplicitLod(*device); + CheckArray(*device); std::puts("image plane address on line tests passed"); return 0; } catch (const std::exception& error) { diff --git a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp index eb27dd66ea..6823ae7a2a 100644 --- a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp @@ -1261,7 +1261,7 @@ std::vector ResourceMaterializer::RuntimeImageModes(const ImageRe face.cube = true; modes.push_back(face); } - if (image.dimension == RdnaImageDimension::Dim2D && image.flatLineCompatible && !storage && !depth && conversion == IrBufferFormat::Invalid && packed == IrBufferFormat::Invalid) { + if ((image.dimension == RdnaImageDimension::Dim2D || image.dimension == RdnaImageDimension::Dim2DArray) && image.flatLineCompatible && !storage && !depth && conversion == IrBufferFormat::Invalid && packed == IrBufferFormat::Invalid) { auto line = mode; line.dimension = RdnaImageDimension::Dim1D; modes.push_back(line); diff --git a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp index 82f21af388..3900471a2f 100644 --- a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp @@ -1514,7 +1514,9 @@ private: const auto lineSampleFlags = memory.imageSampleFlags & ~RdnaImageSampleFlagCompare; const bool flatLineSample = op == IrOpcode::ImageSampleRaw && (lineSampleFlags == RdnaImageSampleFlagLevelZero || lineSampleFlags == 0u); const bool flatLineExplicitLod = op == IrOpcode::ImageSampleRaw && memory.imageSampleFlags == RdnaImageSampleFlagLod; - image.flatLineCompatible = image.flatLineCompatible && (flatLineLoad || flatLineSample || flatLineExplicitLod) && !memory.imagePacked && memory.imageByElements == 0u; + const bool flatLineLevelZero = op == IrOpcode::ImageSampleRaw && memory.imageSampleFlags == RdnaImageSampleFlagLevelZero; + const bool flatLineAccess = image.dimension == RdnaImageDimension::Dim2DArray ? flatLineLevelZero || flatLineExplicitLod : flatLineLoad || flatLineSample || flatLineExplicitLod; + image.flatLineCompatible = image.flatLineCompatible && flatLineAccess && !memory.imagePacked && memory.imageByElements == 0u; image.flatVolumeLineCompatible = image.flatVolumeLineCompatible && (flatLineLoad || flatLineSample) && !memory.imagePacked && memory.imageByElements == 0u; if ((memory.imageSampleFlags & RdnaImageSampleFlagCompare) != 0u) { constexpr auto unsupported = RdnaImageSampleFlagLod | RdnaImageSampleFlagDerivative; diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index 228fc28011..f5dba607db 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -70,7 +70,7 @@ - A value read through a null or unmapped pointer still throws at record time if a [uniform fill or table base](../../../core/shader/recompiler/Optimization/src/SrtWalker/SrtDescriptorEvaluation.cpp) needs it, or if the pointer comes from user data and is not null, even if the console skips the read. - A [V# or flat SRT word](../../../core/shader/recompiler/Optimization/src/SrtWalker/SrtEvaluator.cpp) read through a null pointer taken from user data is zero and counts as a [poisoned SRT read](../../../core/shader/recompiler/Recompiler.cpp), so a dispatch that reads it is not cached. The console faults if the program executes the read; PPSA28416 branches around it with `s_cbranch_execz`. The fault is not modeled. - A [V#, T# or S#](../../../core/shader/recompiler/Optimization/src/SrtWalker/SrtDescriptorEvaluation.cpp) loaded through a null or unmapped pointer read from memory is zero when the work is recorded, and the capture throws if no flat SRT slot records that read. -- [2D accesses through a 1D T#](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) read the texel the 1D addressing selects and ignore the second coordinate, as simonbbby measured on RDNA2 (gfx1036) in #2237 for `image_sample_lz` and mip-less `image_load` with point filtering and wrap, clamp and border addressing; `image_sample_l` was measured the same way on gfx1036 against `image_sample_l` with DIM 1D through the same T#, for u inside, on and past the edges, v of 0, 0.5, 1, -0.25 and 3, LOD -1 to 6 on a 5-level R32_FLOAT texture, point, linear and linear-mip filtering, wrap, clamp and border addressing and HEIGHT or DEPTH 3: all 4725 rows matched. The PS5's gfx1013 is not measured. A plain `image_sample` (implicit LOD) through such a pair takes its LOD from the derivatives of the first coordinate alone, measured on RDNA2 (gfx1036) with the [hardware oracle](../HW_ORACLE.md) on the quads of a compute kernel (wave32 and wave64, IEEE off, DX10 clamp on, denormals preserved, round to nearest even): on a 16-texel, 5-mip 32_UINT T# with point filters it picks the same mip and texel as DIM 1D for first-coordinate steps of 0, 1, 2, 4 and 8 texels per lane along x or along y, whatever the second coordinate does (normalised steps up to 16 per lane), while the same rows on a 16x16 2D T# move the mip with the second coordinate. Outside the pixel stage a plain `image_sample` is emitted with LOD 0 for every image type, so this pair reads mip 0 there, where the kernel measurement shows the quad's derivatives being used. `image_sample_c_lz` and plain `image_sample_c` through such a pair compare against the texel the DIM 1D comparison with the same reference and first coordinate reads, measured on RDNA1 (gfx1010, RX 5700 XT) with the [hardware oracle](../HW_ORACLE.md) template run through the Windows driver's comgr and HIP runtime (wave32 and wave64, IEEE off, DX10 clamp on, denormals preserved, round to nearest even): 8-texel 32_FLOAT and 16_UNORM T#s, also with HEIGHT 3, point and bilinear filters, wrap, clamp and opaque-white border addressing, less and gequal functions, second coordinates 0, 0.5, 1, 1.5, -0.25 and 3; on a 64-texel, 7-mip 32_FLOAT T# `image_sample_c` picks its LOD from the first coordinate's steps alone (1, 4 and 16 texels per lane give mips 0, 2 and 4; second-coordinate steps up to 64 texels leave it unchanged). Only comparisons the runtime does natively (32_FLOAT, 16_UNORM) reach the 1D view; comparisons the runtime emulates for other color formats still throw through such a pair. Other 2D accesses through a 1D T# (storage, depth, packed, BY loads, mips other than explicit LOD, explicit LOD with comparison, bias, offsets, derivatives) still throw. +- [2D accesses through a 1D T#](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) read the texel the 1D addressing selects and ignore the second coordinate, as simonbbby measured on RDNA2 (gfx1036) in #2237 for `image_sample_lz` and mip-less `image_load` with point filtering and wrap, clamp and border addressing; `image_sample_l` was measured the same way on gfx1036 against `image_sample_l` with DIM 1D through the same T#, for u inside, on and past the edges, v of 0, 0.5, 1, -0.25 and 3, LOD -1 to 6 on a 5-level R32_FLOAT texture, point, linear and linear-mip filtering, wrap, clamp and border addressing and HEIGHT or DEPTH 3: all 4725 rows matched. The PS5's gfx1013 is not measured. A plain `image_sample` (implicit LOD) through such a pair takes its LOD from the derivatives of the first coordinate alone, measured on RDNA2 (gfx1036) with the [hardware oracle](../HW_ORACLE.md) on the quads of a compute kernel (wave32 and wave64, IEEE off, DX10 clamp on, denormals preserved, round to nearest even): on a 16-texel, 5-mip 32_UINT T# with point filters it picks the same mip and texel as DIM 1D for first-coordinate steps of 0, 1, 2, 4 and 8 texels per lane along x or along y, whatever the second coordinate does (normalised steps up to 16 per lane), while the same rows on a 16x16 2D T# move the mip with the second coordinate. Outside the pixel stage a plain `image_sample` is emitted with LOD 0 for every image type, so this pair reads mip 0 there, where the kernel measurement shows the quad's derivatives being used. `image_sample_c_lz` and plain `image_sample_c` through such a pair compare against the texel the DIM 1D comparison with the same reference and first coordinate reads, measured on RDNA1 (gfx1010, RX 5700 XT) with the [hardware oracle](../HW_ORACLE.md) template run through the Windows driver's comgr and HIP runtime (wave32 and wave64, IEEE off, DX10 clamp on, denormals preserved, round to nearest even): 8-texel 32_FLOAT and 16_UNORM T#s, also with HEIGHT 3, point and bilinear filters, wrap, clamp and opaque-white border addressing, less and gequal functions, second coordinates 0, 0.5, 1, 1.5, -0.25 and 3; on a 64-texel, 7-mip 32_FLOAT T# `image_sample_c` picks its LOD from the first coordinate's steps alone (1, 4 and 16 texels per lane give mips 0, 2 and 4; second-coordinate steps up to 64 texels leave it unchanged). Only comparisons the runtime does natively (32_FLOAT, 16_UNORM) reach the 1D view; comparisons the runtime emulates for other color formats still throw through such a pair. 2D-array `image_sample_l` and `image_sample_lz` through a 1D T# read as DIM 1D too, ignoring v and the slice: measured on gfx1036 with the hardware oracle (template assembled by the AMD Windows driver's `amd_comgr_2.dll` and launched through its `amdhip64_6.dll`) against the same sample with DIM 1D on a 16-texel 8_8_8_8 UNORM T# with 1 or 5 levels, HEIGHT or DEPTH 3, for u inside, on and past the edges, v of 0, 0.5, 1, -0.25 and 3, slices 0, 1, 2.7, -1 and 5, LOD -1 to 6, and the five filter and address cases above: all 48000 rows matched. A 2D-array `image_load` through a 1D T# is not the same: on gfx1036 it returns the DIM 1D texel only at slice 0, and zeros at slices 1, 2, 5 and -1 even with DEPTH 3, so it, the plain `image_sample` and the other 2D-array accesses still throw. Other 2D accesses through a 1D T# (storage, depth, packed, BY loads, mips other than explicit LOD, explicit LOD with comparison, bias, offsets, derivatives) still throw. - [3D image accesses bound to a 1D T#](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) follow a [hardware oracle](../HW_ORACLE.md) measurement on RDNA1 (RX 5700 XT, gfx1010; wave32 and wave64, IEEE off, DX10 clamp on, denormals preserved, round to nearest even), not the PS5's gfx1013, on 8x1 R8G8B8A8_UNORM linear 1D T#s with HEIGHT and DEPTH 0 or 3 and point filters. `image_sample_lz` reads the texel u selects for v and r in {0, 0.5, 1, 1.5, -0.25, 3}, with wrap, clamp and border addressing, and v and r never produce the border; u wraps, clamps and borders as with DIM 1D. Mip-less `image_load` reads the x texel for any y in {0, 1, 2, 3, -1} when z is 0, returns zero for z in {1, 2, 3, -1}, whatever DEPTH says, and returns zero for x past the width. A plain `image_sample` (implicit LOD) on the quads of a compute kernel takes its texel and LOD from u alone: on a 64-texel, 7-mip 1D T# steps of 1, 4 and 16 texels per lane pick mips 0, 2 and 4, as DIM 1D `image_sample_l` does, and steps in v or r alone stay at mip 0. Outside the pixel stage a plain `image_sample` is emitted with LOD 0, as for 2D accesses through a 1D T#. Every other 3D access of such an image (`image_load` with mips, storage, explicit LOD, bias, derivatives, offsets, comparison, gathers, queries) keeps throwing. SONIC SUPERSTARS (PPSA06888) samples a 1x1 1D T# this way with plain `image_sample` and `image_load`. - [Emulated comparison of a 2D-array access through a 2D T#](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) samples the 2D image and ignores the slice, as the native modes of a 2D-array access through a 2D T# already do. This follows the T# type deciding the addressing; it is not measured on hardware for comparison samples - [Flat aperture bases](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (core/shader/recompiler) - the layout follows the gfx9+ one in the AMD KFD driver's `kfd_flat_memory.c` (`MAKE_LDS_APP_BASE_V9`, `MAKE_LDS_APP_LIMIT`): each aperture is a 4 GB window whose base is the high dword, so a flat address is routed by its whole high dword, as measured on RDNA2 (gfx1035: `src_shared_base` 0x00010000_00000000, `src_shared_limit` 0x00010000_ffffffff). The PS5's base values, high dwords 0x80000000 (shared, `SharedApertureHigh`) and 0x70000000 (private, `PrivateApertureHigh`), are inferred from GTA V's traversal addresses, not measured on a console or read from SH_MEM_BASES. The scalar aperture sources read the same two values From fd90df41b6eff4fa6812a6d615c44a0b8d52754e Mon Sep 17 00:00:00 2001 From: pushkarreddyy <212133856+pushkarreddyy@users.noreply.github.com> Date: Wed, 14 Oct 2026 19:10:52 +0530 Subject: [PATCH 0316/1000] feat(libSceAgc): implement sceAgcGetDefaultCxStateFlat --- .../Command/include/RegisterDefaults.hpp | 1 + .../Command/src/RegisterDefaults.cpp | 39 +++++++++++++++++++ .../Misc/include/RegisterDefaults.hpp | 5 +++ .../libSceAgc/Misc/src/RegisterDefaults.cpp | 4 ++ core/libs/prx/libSceAgc/Unimplemented.cpp | 5 --- core/libs/prx/libSceAgc/tests/Commands.cpp | 17 +++++++- docs/dev/debt/libSceAgc.md | 1 + 7 files changed, 65 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libSceAgc/Command/include/RegisterDefaults.hpp b/core/libs/prx/libSceAgc/Command/include/RegisterDefaults.hpp index a072cf5f5c..abd502cc9d 100644 --- a/core/libs/prx/libSceAgc/Command/include/RegisterDefaults.hpp +++ b/core/libs/prx/libSceAgc/Command/include/RegisterDefaults.hpp @@ -6,6 +6,7 @@ namespace Agc::Command { void* GetRegisterDefaults(std::uint32_t version, bool internal, const char* function); +int GetDefaultCxStateFlat(void* destination, std::uint32_t size); } diff --git a/core/libs/prx/libSceAgc/Command/src/RegisterDefaults.cpp b/core/libs/prx/libSceAgc/Command/src/RegisterDefaults.cpp index ca1d966c3f..189b8fb9fa 100644 --- a/core/libs/prx/libSceAgc/Command/src/RegisterDefaults.cpp +++ b/core/libs/prx/libSceAgc/Command/src/RegisterDefaults.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include @@ -44,6 +45,33 @@ struct Storage { std::mutex defaultsMutex; std::array, 2> storage; +struct AgcContextState { + std::uint32_t data[512]; +}; + +static_assert(sizeof(AgcContextState) == 2048); + +std::mutex defaultStateMutex; +AgcContextState defaultState{}; +bool defaultStateInitialized = false; + +void InitDefaultCxState() { + std::memset(&defaultState, 0, sizeof(defaultState)); + const auto* source = agcPublicRegDefaultsByVersion[8]; + for (std::size_t bank = 0; bank < 4; ++bank) { + const auto& b = source->banks[bank]; + if (b.registers == nullptr) continue; + for (std::uint32_t r = 0; r < b.registerCount; ++r) { + const auto offset = b.registers[r].offset; + const auto value = b.registers[r].value; + if (offset < 512) { + defaultState.data[offset] = value; + } + } + } + defaultStateInitialized = true; +} + } void* GetRegisterDefaults(std::uint32_t version, bool internal, const char* function) { @@ -76,4 +104,15 @@ void* GetRegisterDefaults(std::uint32_t version, bool internal, const char* func return &destination.defaults; } +int GetDefaultCxStateFlat(void* destination, std::uint32_t size) { + if (destination == nullptr) return static_cast(0x80890001); + std::lock_guard lock(defaultStateMutex); + if (!defaultStateInitialized) { + InitDefaultCxState(); + } + const auto copyBytes = std::min(size, static_cast(sizeof(AgcContextState))); + std::memcpy(destination, defaultState.data, copyBytes); + return 0; +} + } diff --git a/core/libs/prx/libSceAgc/Misc/include/RegisterDefaults.hpp b/core/libs/prx/libSceAgc/Misc/include/RegisterDefaults.hpp index cb296a8167..c7eb4fea76 100644 --- a/core/libs/prx/libSceAgc/Misc/include/RegisterDefaults.hpp +++ b/core/libs/prx/libSceAgc/Misc/include/RegisterDefaults.hpp @@ -1,4 +1,9 @@ #ifndef CORE_LIBS_PRX_LIBSCEAGC_MISC_INCLUDE_REGISTERDEFAULTS_HPP #define CORE_LIBS_PRX_LIBSCEAGC_MISC_INCLUDE_REGISTERDEFAULTS_HPP +#include "SceTypes.hpp" +#include +extern "C" { +int APS5_VABI sceAgcGetDefaultCxStateFlat(void* out_state, std::uint32_t size); +} #endif diff --git a/core/libs/prx/libSceAgc/Misc/src/RegisterDefaults.cpp b/core/libs/prx/libSceAgc/Misc/src/RegisterDefaults.cpp index d5ff3372f1..487bc16fca 100644 --- a/core/libs/prx/libSceAgc/Misc/src/RegisterDefaults.cpp +++ b/core/libs/prx/libSceAgc/Misc/src/RegisterDefaults.cpp @@ -26,4 +26,8 @@ void* APS5_VABI sceAgcGetRegisterDefaults2Internal(std::uint32_t version) { return Agc::Command::GetRegisterDefaults(version, true, __func__); } +int APS5_VABI sceAgcGetDefaultCxStateFlat(void* out_state, std::uint32_t size) { + return Agc::Command::GetDefaultCxStateFlat(out_state, size); +} + } diff --git a/core/libs/prx/libSceAgc/Unimplemented.cpp b/core/libs/prx/libSceAgc/Unimplemented.cpp index dad824f489..7a7d7c52cb 100644 --- a/core/libs/prx/libSceAgc/Unimplemented.cpp +++ b/core/libs/prx/libSceAgc/Unimplemented.cpp @@ -30,11 +30,6 @@ int APS5_VABI sceAgcDcbSetMarkerSpan() { return 0; } -int APS5_VABI sceAgcGetDefaultCxStateFlat() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceAgcSetSemaphoreMemory() { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/prx/libSceAgc/tests/Commands.cpp b/core/libs/prx/libSceAgc/tests/Commands.cpp index 7c90e82644..5e56da52a6 100644 --- a/core/libs/prx/libSceAgc/tests/Commands.cpp +++ b/core/libs/prx/libSceAgc/tests/Commands.cpp @@ -21,6 +21,7 @@ extern "C" int APS5_VABI sceAgcInit(std::uint32_t version); extern "C" void* APS5_VABI sceAgcGetRegisterDefaults(); extern "C" void* APS5_VABI sceAgcGetRegisterDefaultsInternal(); extern "C" void* APS5_VABI sceAgcGetRegisterDefaults2Internal(std::uint32_t version); +extern "C" int APS5_VABI sceAgcGetDefaultCxStateFlat(void* out_state, std::uint32_t size); extern "C" int APS5_VABI sceAgcInit_0090(std::uint32_t* state, std::uint32_t version); extern "C" std::uint32_t* APS5_VABI sceAgcDcbDrawIndexAuto(CommandBuffer* buf, std::uint32_t indexCount, std::uint64_t modifier); extern "C" std::uint32_t* APS5_VABI sceAgcDcbDrawIndexIndirect(CommandBuffer* buf, std::uint32_t dataOffsetInBytes, std::uint64_t modifier); @@ -637,8 +638,20 @@ void testDefaults() { auto* internalDefaults = sceAgcGetRegisterDefaultsInternal(); check(internalDefaults != nullptr && internalDefaults == Agc::Command::GetRegisterDefaults(0, true, __func__) && internalDefaults == sceAgcGetRegisterDefaults2Internal(0), "internal register defaults are not the baseline internal table"); check(internalDefaults != sceAgcGetRegisterDefaults(), "internal register defaults returned the public table"); - expectFailure([] { Agc::Command::GetRegisterDefaults(14, false, __func__); }); - expectFailure([] { Agc::Command::GetRegisterDefaults(0xffffffffu, true, __func__); }); + std::array flatCxBuffer{}; + check(sceAgcGetDefaultCxStateFlat(nullptr, sizeof(flatCxBuffer)) == SCE_AGC_ERROR_INVALID_ARGUMENT, "null destination was accepted"); + check(sceAgcGetDefaultCxStateFlat(flatCxBuffer.data(), 0) == 0, "zero size copy failed"); + check(sceAgcGetDefaultCxStateFlat(flatCxBuffer.data(), sizeof(flatCxBuffer)) == 0, "flat CX state default retrieval failed"); + check(flatCxBuffer[0x001] == 0x11000100u, "flat CX state register 0x001 mismatch"); + check(flatCxBuffer[0x007] == 0x0000ffffu, "flat CX state register 0x007 mismatch"); + check(flatCxBuffer[0x12d] == 0x3f800000u, "flat CX state register 0x12d mismatch"); + check(flatCxBuffer[0x1e0] == 0x20010001u, "flat CX state register 0x1e0 mismatch"); + std::array overBuffer{}; + overBuffer[512] = 0x12345678u; + check(sceAgcGetDefaultCxStateFlat(overBuffer.data(), sizeof(overBuffer)) == 0, "oversized copy failed"); + check(overBuffer[512] == 0x12345678u, "oversized copy overflowed buffer"); + check(Agc::Command::GetDefaultCxStateFlat(nullptr, sizeof(flatCxBuffer)) == SCE_AGC_ERROR_INVALID_ARGUMENT, "null command destination was accepted"); + check(Agc::Command::GetDefaultCxStateFlat(flatCxBuffer.data(), sizeof(flatCxBuffer)) == 0, "command flat CX state retrieval failed"); } void testEventWrite() { diff --git a/docs/dev/debt/libSceAgc.md b/docs/dev/debt/libSceAgc.md index 81b3f5e718..f9cdf5ff9f 100644 --- a/docs/dev/debt/libSceAgc.md +++ b/docs/dev/debt/libSceAgc.md @@ -30,6 +30,7 @@ - [sceAgcDcbSetZPassPredicationEnableGetSize, sceAgcDcbSetPredicationDisableGetSize, sceAgcDcbSetBoolPredicationEnableGetSize](../../../core/libs/prx/libSceAgc/DcbState/src/Predication.cpp) (libSceAgc) - the builders are inline in the SDK and not exported; the size is assumed to be that of the SET_PREDICATION packet `sceAgcDcbSetPredication` writes (16 bytes) - [vieBRwlh1Lw](../../../core/libs/prx/libSceAgc/Shader/src/InterpolantMapping.cpp) (libSceAgc) follows [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/game_compat.c) only, not confirmed on hardware: the `dbOlWdppb4o` transform without the identity tail, so it writes one register per pixel shader input and none for a pixel shader without inputs, and a null pixel shader throws. For an `is_f16` 1 or 3 input whose `default_value` and `default_value_hi` differ it throws: OpenAGC takes `DEFAULT_VAL` from `default_value_hi`, while KytyPS5, sharpemu and `dbOlWdppb4o` here take it from `default_value` - [sceAgcAcbWaitUntilSafeForRendering](../../../core/libs/prx/libSceAgc/Acb/src/Control.cpp) (libSceAgc) - `(buffer, videoOutHandle, displayBufferIndex)`, the declared arguments, which PPSA12544 (01.000.010) passes inside `waitUntilSafeForRendering` and `waitUntilSafeForRendering VR` markers on an asynchronous compute command buffer, ignoring the result. It writes the same rendering-wait packet as `sceAgcDcbWaitUntilSafeForRendering`, which the driver now accepts on compute queues too; the wait blocks only that queue's worker. Whether the console encodes the compute variant differently is not known +- [sceAgcGetDefaultCxStateFlat](../../../core/libs/prx/libSceAgc/Misc/src/RegisterDefaults.cpp) (libSceAgc) - copies up to `size` bytes (clamped to 2048 bytes, the 512-dword `AgcContextState`) from the default context state initialized from the version 8 public register default groups (CX, SH, UC), indexed by register offset, following OpenAGC `src/context_state.c`; returns `0x80890001` on null destination; version selection without `sceAgcInit` is unverified ### Functional From 6ebab0e73befdfd3aa4b69d2a6d1e3ca1df4dd1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Thu, 15 Oct 2026 15:36:49 +0200 Subject: [PATCH 0317/1000] test(libSceAgc): declare the invalid-argument constant, restore the register defaults version checks --- core/libs/prx/libSceAgc/tests/Commands.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgc/tests/Commands.cpp b/core/libs/prx/libSceAgc/tests/Commands.cpp index 5e56da52a6..1f86645fe8 100644 --- a/core/libs/prx/libSceAgc/tests/Commands.cpp +++ b/core/libs/prx/libSceAgc/tests/Commands.cpp @@ -638,8 +638,9 @@ void testDefaults() { auto* internalDefaults = sceAgcGetRegisterDefaultsInternal(); check(internalDefaults != nullptr && internalDefaults == Agc::Command::GetRegisterDefaults(0, true, __func__) && internalDefaults == sceAgcGetRegisterDefaults2Internal(0), "internal register defaults are not the baseline internal table"); check(internalDefaults != sceAgcGetRegisterDefaults(), "internal register defaults returned the public table"); + constexpr int kInvalidArgument = static_cast(0x80890001u); std::array flatCxBuffer{}; - check(sceAgcGetDefaultCxStateFlat(nullptr, sizeof(flatCxBuffer)) == SCE_AGC_ERROR_INVALID_ARGUMENT, "null destination was accepted"); + check(sceAgcGetDefaultCxStateFlat(nullptr, sizeof(flatCxBuffer)) == kInvalidArgument, "null destination was accepted"); check(sceAgcGetDefaultCxStateFlat(flatCxBuffer.data(), 0) == 0, "zero size copy failed"); check(sceAgcGetDefaultCxStateFlat(flatCxBuffer.data(), sizeof(flatCxBuffer)) == 0, "flat CX state default retrieval failed"); check(flatCxBuffer[0x001] == 0x11000100u, "flat CX state register 0x001 mismatch"); @@ -650,8 +651,10 @@ void testDefaults() { overBuffer[512] = 0x12345678u; check(sceAgcGetDefaultCxStateFlat(overBuffer.data(), sizeof(overBuffer)) == 0, "oversized copy failed"); check(overBuffer[512] == 0x12345678u, "oversized copy overflowed buffer"); - check(Agc::Command::GetDefaultCxStateFlat(nullptr, sizeof(flatCxBuffer)) == SCE_AGC_ERROR_INVALID_ARGUMENT, "null command destination was accepted"); + check(Agc::Command::GetDefaultCxStateFlat(nullptr, sizeof(flatCxBuffer)) == kInvalidArgument, "null command destination was accepted"); check(Agc::Command::GetDefaultCxStateFlat(flatCxBuffer.data(), sizeof(flatCxBuffer)) == 0, "command flat CX state retrieval failed"); + expectFailure([] { Agc::Command::GetRegisterDefaults(14, false, __func__); }); + expectFailure([] { Agc::Command::GetRegisterDefaults(0xffffffffu, true, __func__); }); } void testEventWrite() { From 090c0dad15122bd004a6a4b246b1209b59a0498c Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Thu, 15 Oct 2026 06:39:33 -0700 Subject: [PATCH 0318/1000] fix(shader): count a selected record's dwords past its buffer as read Since 682b07eb0, an image whose dword 3 wasn't captured is poisoned (OutsideSnapshot). A record selected past the end of its heap reads nothing there, so it was poisoned instead of selecting a null image, and agc_shader_bindless and agc_driver_image_table failed after the merge of main. A scalar buffer load returns zeros past the end of the buffer, so those dwords now count as read zeros, and the dwords the selection reads count as captured. Every other path keeps main's rule. --- .../recompiler/Optimization/src/ResourceMaterializer.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp index 505e0aead6..0cc812e22b 100644 --- a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp @@ -580,13 +580,17 @@ private: std::uint64_t high = 0; for (std::uint32_t dword = 0; dword < column.dwordCount; dword++) { const auto position = ImageTableAbi::ScalarBufferDword(offset, column.offset + dword * 4u, size); - if (!position.has_value()) continue; + if (!position.has_value()) { + value.availableDwordMask |= static_cast(1u << dword); + continue; + } const auto address = base + *position; if (!PageAccessible(address, base, base + size)) { records.states.front() = RecordState::Unmapped; return; } value.dwords[dword] = Read(address); + value.availableDwordMask |= static_cast(1u << dword); low = std::min(low, address); high = std::max(high, address + sizeof(std::uint32_t)); } From 6799d8775101bae803893a092fc22faf87377049 Mon Sep 17 00:00:00 2001 From: Nithishvelan M Date: Wed, 14 Oct 2026 19:18:24 +0530 Subject: [PATCH 0319/1000] fix(libSceCdlgPlayerReview): return error codes instead of throwing in initialize and terminate --- core/libs/prx/libSceCdlgPlayerReview/Export.cpp | 10 ++++++---- core/libs/tests/GuestPlayerReviewDialog.cpp | 4 ++++ 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceCdlgPlayerReview/Export.cpp b/core/libs/prx/libSceCdlgPlayerReview/Export.cpp index b19768c25a..b2f07c88e7 100644 --- a/core/libs/prx/libSceCdlgPlayerReview/Export.cpp +++ b/core/libs/prx/libSceCdlgPlayerReview/Export.cpp @@ -1,6 +1,5 @@ #include #include -#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" @@ -11,9 +10,11 @@ constexpr int COMMON_DIALOG_STATUS_NONE = 0; constexpr int COMMON_DIALOG_STATUS_RUNNING = 2; constexpr int COMMON_DIALOG_STATUS_FINISHED = 3; constexpr int COMMON_DIALOG_RESULT_USER_CANCELED = 1; +constexpr int COMMON_DIALOG_ERROR_ALREADY_INITIALIZED = static_cast(0x80B80002u); constexpr int COMMON_DIALOG_ERROR_NOT_INITIALIZED = static_cast(0x80B80003u); constexpr int COMMON_DIALOG_ERROR_NOT_FINISHED = static_cast(0x80B80005u); constexpr int COMMON_DIALOG_ERROR_BUSY = static_cast(0x80B80007u); +constexpr int COMMON_DIALOG_ERROR_NOT_RUNNING = static_cast(0x80B8000Bu); constexpr int COMMON_DIALOG_ERROR_ARG_NULL = static_cast(0x80B8000Du); } @@ -21,7 +22,7 @@ extern "C" { int APS5_VABI scePlayerReviewDialogInitialize(void) { int expected = 0; - if (!g_status.compare_exchange_strong(expected, 1)) throw std::logic_error("scePlayerReviewDialogInitialize: already initialized"); + if (!g_status.compare_exchange_strong(expected, 1)) return COMMON_DIALOG_ERROR_ALREADY_INITIALIZED; return 0; } @@ -29,8 +30,9 @@ int APS5_VABI scePlayerReviewDialogTerminate(void) { int expected = 1; if (g_status.compare_exchange_strong(expected, 0)) return 0; expected = COMMON_DIALOG_STATUS_FINISHED; - if (!g_status.compare_exchange_strong(expected, 0)) throw std::logic_error("scePlayerReviewDialogTerminate: not initialized or still running"); - return 0; + if (g_status.compare_exchange_strong(expected, 0)) return 0; + if (g_status.load() == 0) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + return COMMON_DIALOG_ERROR_NOT_RUNNING; } int APS5_VABI scePlayerReviewDialogOpen(const void* param) { diff --git a/core/libs/tests/GuestPlayerReviewDialog.cpp b/core/libs/tests/GuestPlayerReviewDialog.cpp index 7093323415..b7689c2cc6 100644 --- a/core/libs/tests/GuestPlayerReviewDialog.cpp +++ b/core/libs/tests/GuestPlayerReviewDialog.cpp @@ -18,6 +18,7 @@ constexpr int COMMON_DIALOG_STATUS_NONE = 0; constexpr int COMMON_DIALOG_STATUS_INITIALIZED = 1; constexpr int COMMON_DIALOG_STATUS_FINISHED = 3; constexpr int COMMON_DIALOG_RESULT_USER_CANCELED = 1; +constexpr int COMMON_DIALOG_ERROR_ALREADY_INITIALIZED = static_cast(0x80B80002u); constexpr int COMMON_DIALOG_ERROR_NOT_INITIALIZED = static_cast(0x80B80003u); constexpr int COMMON_DIALOG_ERROR_NOT_FINISHED = static_cast(0x80B80005u); constexpr int COMMON_DIALOG_ERROR_ARG_NULL = static_cast(0x80B8000Du); @@ -29,16 +30,19 @@ void Require(bool value) { if (!value) std::abort(); } int main() { std::uint8_t param[64] = {}; std::int32_t result[8] = {}; + Require(scePlayerReviewDialogTerminate() == COMMON_DIALOG_ERROR_NOT_INITIALIZED); Require(scePlayerReviewDialogGetStatus() == COMMON_DIALOG_STATUS_NONE); Require(scePlayerReviewDialogOpen(param) == COMMON_DIALOG_ERROR_NOT_INITIALIZED); Require(scePlayerReviewDialogClose() == COMMON_DIALOG_ERROR_NOT_INITIALIZED); Require(scePlayerReviewDialogGetResult(result) == COMMON_DIALOG_ERROR_NOT_INITIALIZED); Require(scePlayerReviewDialogInitialize() == 0); + Require(scePlayerReviewDialogInitialize() == COMMON_DIALOG_ERROR_ALREADY_INITIALIZED); Require(scePlayerReviewDialogGetStatus() == COMMON_DIALOG_STATUS_INITIALIZED); Require(scePlayerReviewDialogUpdateStatus() == COMMON_DIALOG_STATUS_INITIALIZED); Require(scePlayerReviewDialogGetResult(result) == COMMON_DIALOG_ERROR_NOT_FINISHED); Require(scePlayerReviewDialogTerminate() == 0); + Require(scePlayerReviewDialogTerminate() == COMMON_DIALOG_ERROR_NOT_INITIALIZED); Require(scePlayerReviewDialogGetStatus() == COMMON_DIALOG_STATUS_NONE); Require(scePlayerReviewDialogInitialize() == 0); From 61a9ff5ee09229ff4da8522d220b13876c6ddb82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Thu, 15 Oct 2026 15:43:36 +0200 Subject: [PATCH 0320/1000] fix(libSceCdlgPlayerReview): ALREADY_INITIALIZED is 0x80B80004 Co-Authored-By: Claude --- core/libs/prx/libSceCdlgPlayerReview/Export.cpp | 2 +- core/libs/tests/GuestPlayerReviewDialog.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceCdlgPlayerReview/Export.cpp b/core/libs/prx/libSceCdlgPlayerReview/Export.cpp index b2f07c88e7..7113ee245a 100644 --- a/core/libs/prx/libSceCdlgPlayerReview/Export.cpp +++ b/core/libs/prx/libSceCdlgPlayerReview/Export.cpp @@ -10,7 +10,7 @@ constexpr int COMMON_DIALOG_STATUS_NONE = 0; constexpr int COMMON_DIALOG_STATUS_RUNNING = 2; constexpr int COMMON_DIALOG_STATUS_FINISHED = 3; constexpr int COMMON_DIALOG_RESULT_USER_CANCELED = 1; -constexpr int COMMON_DIALOG_ERROR_ALREADY_INITIALIZED = static_cast(0x80B80002u); +constexpr int COMMON_DIALOG_ERROR_ALREADY_INITIALIZED = static_cast(0x80B80004u); constexpr int COMMON_DIALOG_ERROR_NOT_INITIALIZED = static_cast(0x80B80003u); constexpr int COMMON_DIALOG_ERROR_NOT_FINISHED = static_cast(0x80B80005u); constexpr int COMMON_DIALOG_ERROR_BUSY = static_cast(0x80B80007u); diff --git a/core/libs/tests/GuestPlayerReviewDialog.cpp b/core/libs/tests/GuestPlayerReviewDialog.cpp index b7689c2cc6..3287ddd4d9 100644 --- a/core/libs/tests/GuestPlayerReviewDialog.cpp +++ b/core/libs/tests/GuestPlayerReviewDialog.cpp @@ -18,7 +18,7 @@ constexpr int COMMON_DIALOG_STATUS_NONE = 0; constexpr int COMMON_DIALOG_STATUS_INITIALIZED = 1; constexpr int COMMON_DIALOG_STATUS_FINISHED = 3; constexpr int COMMON_DIALOG_RESULT_USER_CANCELED = 1; -constexpr int COMMON_DIALOG_ERROR_ALREADY_INITIALIZED = static_cast(0x80B80002u); +constexpr int COMMON_DIALOG_ERROR_ALREADY_INITIALIZED = static_cast(0x80B80004u); constexpr int COMMON_DIALOG_ERROR_NOT_INITIALIZED = static_cast(0x80B80003u); constexpr int COMMON_DIALOG_ERROR_NOT_FINISHED = static_cast(0x80B80005u); constexpr int COMMON_DIALOG_ERROR_ARG_NULL = static_cast(0x80B8000Du); From 5e2e1352804326d4442dba54b410c2e7311b7f37 Mon Sep 17 00:00:00 2001 From: Nithishvelan M Date: Wed, 14 Oct 2026 13:21:49 +0530 Subject: [PATCH 0321/1000] fix(libSceSaveDataDialog.native): validate dialog state in close, get result and terminate --- .../libSceSaveDataDialog.native/Export.cpp | 9 +++++++++ .../SaveDataDialog.hpp | 2 ++ core/libs/tests/GuestSaveDataDialog.cpp | 19 +++++++++++++++++++ docs/dev/debt/libSceSaveDataDialog.native.md | 2 +- 4 files changed, 31 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceSaveDataDialog.native/Export.cpp b/core/libs/prx/libSceSaveDataDialog.native/Export.cpp index 3c834f7d9c..d042e8c8e7 100644 --- a/core/libs/prx/libSceSaveDataDialog.native/Export.cpp +++ b/core/libs/prx/libSceSaveDataDialog.native/Export.cpp @@ -33,6 +33,9 @@ int APS5_VABI sceSaveDataDialogUpdateStatus(void) { } int APS5_VABI sceSaveDataDialogGetResult(void* result) { + if (g_status != SAVE_DATA_DIALOG_STATUS_FINISHED) { + return SAVE_DATA_DIALOG_ERROR_NOT_FINISHED; + } if (result == nullptr) { return SAVE_DATA_DIALOG_ERROR_ARG_NULL; } @@ -76,6 +79,9 @@ int APS5_VABI sceSaveDataDialogOpen(const void* param) { int APS5_VABI sceSaveDataDialogClose(const void* closeParam) { (void)closeParam; + if (g_status != SAVE_DATA_DIALOG_STATUS_RUNNING) { + return SAVE_DATA_DIALOG_ERROR_NOT_RUNNING; + } g_status = SAVE_DATA_DIALOG_STATUS_FINISHED; return SAVE_DATA_DIALOG_OK; } @@ -85,6 +91,9 @@ int APS5_VABI sceSaveDataDialogIsReadyToDisplay(void) { } int APS5_VABI sceSaveDataDialogTerminate(void) { + if (g_status == SAVE_DATA_DIALOG_STATUS_NONE) { + return SAVE_DATA_DIALOG_ERROR_NOT_INITIALIZED; + } g_status = SAVE_DATA_DIALOG_STATUS_NONE; g_mode = 0; g_user_data = nullptr; diff --git a/core/libs/prx/libSceSaveDataDialog.native/SaveDataDialog.hpp b/core/libs/prx/libSceSaveDataDialog.native/SaveDataDialog.hpp index 3259b8d482..7a8eda2bd0 100644 --- a/core/libs/prx/libSceSaveDataDialog.native/SaveDataDialog.hpp +++ b/core/libs/prx/libSceSaveDataDialog.native/SaveDataDialog.hpp @@ -7,7 +7,9 @@ constexpr int SAVE_DATA_DIALOG_OK = 0; constexpr int SAVE_DATA_DIALOG_ERROR_NOT_INITIALIZED = static_cast(0x80B80003); constexpr int SAVE_DATA_DIALOG_ERROR_ALREADY_INITIALIZED = static_cast(0x80B80004); +constexpr int SAVE_DATA_DIALOG_ERROR_NOT_FINISHED = static_cast(0x80B80005); constexpr int SAVE_DATA_DIALOG_ERROR_INVALID_STATE = static_cast(0x80B80006); +constexpr int SAVE_DATA_DIALOG_ERROR_NOT_RUNNING = static_cast(0x80B8000B); constexpr int SAVE_DATA_DIALOG_ERROR_ARG_NULL = static_cast(0x80B8000D); constexpr int SAVE_DATA_DIALOG_STATUS_NONE = 0; diff --git a/core/libs/tests/GuestSaveDataDialog.cpp b/core/libs/tests/GuestSaveDataDialog.cpp index 5d8e063710..2a44ff62d5 100644 --- a/core/libs/tests/GuestSaveDataDialog.cpp +++ b/core/libs/tests/GuestSaveDataDialog.cpp @@ -34,7 +34,16 @@ static void RequireRunning() { } int main() { + Require(sceSaveDataDialogGetStatus() == SAVE_DATA_DIALOG_STATUS_NONE, "initial status"); + Require(sceSaveDataDialogTerminate() == SAVE_DATA_DIALOG_ERROR_NOT_INITIALIZED, "terminate before init"); + Require(sceSaveDataDialogClose(nullptr) == SAVE_DATA_DIALOG_ERROR_NOT_RUNNING, "close before init"); + SaveDataDialogResult uninitResult{}; + Require(sceSaveDataDialogGetResult(&uninitResult) == SAVE_DATA_DIALOG_ERROR_NOT_FINISHED, "get result before init"); + Require(sceSaveDataDialogInitialize() == 0, "initialize"); + Require(sceSaveDataDialogInitialize() == SAVE_DATA_DIALOG_ERROR_ALREADY_INITIALIZED, "double init"); + Require(sceSaveDataDialogClose(nullptr) == SAVE_DATA_DIALOG_ERROR_NOT_RUNNING, "close while initialized not running"); + Require(sceSaveDataDialogGetResult(&uninitResult) == SAVE_DATA_DIALOG_ERROR_NOT_FINISHED, "get result before open"); SaveDataDirName names[2]{{}, {"SDU1"}}; SaveDataDialogItems items{}; @@ -57,6 +66,8 @@ int main() { RequireRunning(); Require(sceSaveDataDialogProgressBarSetValue(0, 100) == 0, "complete progress"); RequireRunning(); + Require(sceSaveDataDialogGetResult(&uninitResult) == SAVE_DATA_DIALOG_ERROR_NOT_FINISHED, + "get result while running"); Require(sceSaveDataDialogClose(nullptr) == 0, "close save progress"); Require(sceSaveDataDialogUpdateStatus() == SAVE_DATA_DIALOG_STATUS_FINISHED, "closing save progress must finish it"); @@ -69,10 +80,18 @@ int main() { "save result must preserve its mode, directory and caller context"); } + Require(sceSaveDataDialogOpen(¶m) == 0, "open save progress for terminate test"); + RequireRunning(); + Require(sceSaveDataDialogTerminate() == 0, "terminate while running"); + Require(sceSaveDataDialogGetStatus() == SAVE_DATA_DIALOG_STATUS_NONE, "terminated state"); + Require(sceSaveDataDialogTerminate() == SAVE_DATA_DIALOG_ERROR_NOT_INITIALIZED, "terminate after terminate"); + + Require(sceSaveDataDialogInitialize() == 0, "re-initialize"); param.mode = 1; Require(sceSaveDataDialogOpen(¶m) == 0, "open selection dialog"); Require(sceSaveDataDialogUpdateStatus() == SAVE_DATA_DIALOG_STATUS_FINISHED, "selection dialog must retain the existing automatic response"); Require(sceSaveDataDialogTerminate() == 0, "terminate"); + Require(sceSaveDataDialogTerminate() == SAVE_DATA_DIALOG_ERROR_NOT_INITIALIZED, "terminate when already terminated"); Require(sceSaveDataDialogGetStatus() == SAVE_DATA_DIALOG_STATUS_NONE, "terminated state"); } diff --git a/docs/dev/debt/libSceSaveDataDialog.native.md b/docs/dev/debt/libSceSaveDataDialog.native.md index a06b238913..ee6bb8dc2d 100644 --- a/docs/dev/debt/libSceSaveDataDialog.native.md +++ b/docs/dev/debt/libSceSaveDataDialog.native.md @@ -2,4 +2,4 @@ ### Silent stubs -- [libSceSaveDataDialog.native](../../../core/libs/prx/libSceSaveDataDialog.native/Export.cpp) shows no UI and ignores progress values. Progress-bar mode (5) stays running until the caller closes it, matching Skyrim PPSA03747's save sequence; other modes still finish immediately and return button 1 and the first nonempty directory name. Console dialog timing and selection behavior remain unverified. +- [libSceSaveDataDialog.native](../../../core/libs/prx/libSceSaveDataDialog.native/Export.cpp) shows no UI and ignores progress values. Progress-bar mode (5) stays running until the caller closes it, matching Skyrim PPSA03747's save sequence; other modes still finish immediately and return button 1 and the first nonempty directory name. Console dialog timing and selection behavior remain unverified. Lifecycle state error codes follow shadPS4: `sceSaveDataDialogClose` returns `SAVE_DATA_DIALOG_ERROR_NOT_RUNNING` whenever not running, `sceSaveDataDialogGetResult` returns `SAVE_DATA_DIALOG_ERROR_NOT_FINISHED` whenever not finished (checked before null pointer), and `sceSaveDataDialogTerminate` closes running dialogs and returns OK, rejecting uninitialized calls with `SAVE_DATA_DIALOG_ERROR_NOT_INITIALIZED`. From e485f6ae09b90b24f852601f5a828dbedd2b4e61 Mon Sep 17 00:00:00 2001 From: Nithishvelan M Date: Wed, 14 Oct 2026 19:22:34 +0530 Subject: [PATCH 0322/1000] fix(libSceIme): return error codes instead of throwing on duplicate open or close --- core/libs/prx/libSceIme/Export.cpp | 5 +++-- core/libs/tests/GuestImeKeyboard.cpp | 4 ++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceIme/Export.cpp b/core/libs/prx/libSceIme/Export.cpp index d686445211..4ef0877667 100644 --- a/core/libs/prx/libSceIme/Export.cpp +++ b/core/libs/prx/libSceIme/Export.cpp @@ -15,6 +15,7 @@ namespace { constexpr int ErrorBusy = static_cast(0x80bc0001u); +constexpr int ErrorAlreadyOpened = static_cast(0x80bc0001u); constexpr int ErrorNotOpened = static_cast(0x80bc0002u); constexpr int ErrorConnectionFailed = static_cast(0x80bc0004u); constexpr int ErrorInvalidText = static_cast(0x80bc0006u); @@ -346,7 +347,7 @@ int APS5_VABI sceImeGetPanelSize(const Param* param, uint32_t* width, uint32_t* int APS5_VABI sceImeKeyboardClose(int32_t user_id) { std::lock_guard lock(g_keyboardMutex); - if (g_openKeyboards.erase(user_id) == 0) throw std::logic_error("sceImeKeyboardClose: keyboard not open for user " + std::to_string(user_id)); + if (g_openKeyboards.erase(user_id) == 0) return ErrorNotOpened; return 0; } @@ -369,7 +370,7 @@ int APS5_VABI sceImeKeyboardGetResourceId(int32_t user_id, KeyboardResourceIdArr int APS5_VABI sceImeKeyboardOpen(int32_t user_id, const KeyboardParam* param) { if (!param) APS5_INVALID_ARG_EX; std::lock_guard lock(g_keyboardMutex); - if (!g_openKeyboards.insert(user_id).second) throw std::logic_error("sceImeKeyboardOpen: keyboard already open for user " + std::to_string(user_id)); + if (!g_openKeyboards.insert(user_id).second) return ErrorAlreadyOpened; return 0; } diff --git a/core/libs/tests/GuestImeKeyboard.cpp b/core/libs/tests/GuestImeKeyboard.cpp index cd3dbaf247..180a47106f 100644 --- a/core/libs/tests/GuestImeKeyboard.cpp +++ b/core/libs/tests/GuestImeKeyboard.cpp @@ -12,6 +12,7 @@ int APS5_VABI sceImeKeyboardSetMode(int32_t user_id, uint32_t mode); static_assert(sizeof(KeyboardInfo) == 36); +constexpr int AlreadyOpened = static_cast(0x80bc0001u); constexpr int NotOpened = static_cast(0x80bc0002u); constexpr int InvalidUserId = static_cast(0x80bc0010u); constexpr int NoResourceId = static_cast(0x80bc0023u); @@ -35,6 +36,7 @@ static void CheckClosed() { Require(sceImeKeyboardGetInfo(0, nullptr) == InvalidAddress, "null info"); Require(sceImeKeyboardGetInfo(0, &info) == NotOpened, "info without an open keyboard"); RequireUntouched(info); + Require(sceImeKeyboardClose(1) == NotOpened, "close without an open keyboard"); Require(sceImeKeyboardSetMode(-1, 0) == InvalidUserId, "set mode invalid user"); Require(sceImeKeyboardSetMode(1, 0) == NotOpened, "set mode without an open keyboard"); Require(sceImeKeyboardSetMode(1, 0x80) == NotOpened, "set mode checks the mode before the keyboard"); @@ -43,6 +45,7 @@ static void CheckClosed() { static void CheckOpened() { KeyboardParam param{}; Require(sceImeKeyboardOpen(1, ¶m) == 0, "keyboard open failed"); + Require(sceImeKeyboardOpen(1, ¶m) == AlreadyOpened, "duplicate open accepted"); KeyboardInfo info; std::memset(&info, 0xa5, sizeof(info)); Require(sceImeKeyboardGetInfo(0, nullptr) == InvalidAddress, "null info with an open keyboard"); @@ -57,6 +60,7 @@ static void CheckOpened() { Require(sceImeKeyboardSetMode(1, 0x80) == InvalidMode, "mode bit 7 accepted"); Require(sceImeKeyboardSetMode(1, 0x80000001u) == InvalidMode, "mode bit 31 accepted"); Require(sceImeKeyboardClose(1) == 0, "keyboard close failed"); + Require(sceImeKeyboardClose(1) == NotOpened, "second close accepted"); Require(sceImeKeyboardGetInfo(0, &info) == NotOpened, "info after close"); Require(sceImeKeyboardSetMode(1, 0) == NotOpened, "set mode after close"); } From 04b1e9b0b706d4010534e16485fd982bf726246a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Thu, 15 Oct 2026 15:45:56 +0200 Subject: [PATCH 0323/1000] fix(libSceIme): reuse ErrorBusy for duplicate keyboard open and record the behaviour in technical debt --- core/libs/prx/libSceIme/Export.cpp | 3 +-- docs/dev/debt/libSceIme.md | 1 + 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceIme/Export.cpp b/core/libs/prx/libSceIme/Export.cpp index 4ef0877667..8d2f2748ee 100644 --- a/core/libs/prx/libSceIme/Export.cpp +++ b/core/libs/prx/libSceIme/Export.cpp @@ -15,7 +15,6 @@ namespace { constexpr int ErrorBusy = static_cast(0x80bc0001u); -constexpr int ErrorAlreadyOpened = static_cast(0x80bc0001u); constexpr int ErrorNotOpened = static_cast(0x80bc0002u); constexpr int ErrorConnectionFailed = static_cast(0x80bc0004u); constexpr int ErrorInvalidText = static_cast(0x80bc0006u); @@ -370,7 +369,7 @@ int APS5_VABI sceImeKeyboardGetResourceId(int32_t user_id, KeyboardResourceIdArr int APS5_VABI sceImeKeyboardOpen(int32_t user_id, const KeyboardParam* param) { if (!param) APS5_INVALID_ARG_EX; std::lock_guard lock(g_keyboardMutex); - if (!g_openKeyboards.insert(user_id).second) return ErrorAlreadyOpened; + if (!g_openKeyboards.insert(user_id).second) return ErrorBusy; return 0; } diff --git a/docs/dev/debt/libSceIme.md b/docs/dev/debt/libSceIme.md index 8511d88e3d..76178899b3 100644 --- a/docs/dev/debt/libSceIme.md +++ b/docs/dev/debt/libSceIme.md @@ -11,5 +11,6 @@ ### Functional +- [sceImeKeyboardOpen, sceImeKeyboardClose](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - opening a keyboard that is already open for the same user returns `0x80bc0001` (busy) and closing one that is not open returns `0x80bc0002`, as [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/ime.cpp) does; it tracks one keyboard for all users, while the open state is kept per user id here, and the PS5 result is unverified - [sceImeKeyboardGetInfo](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) returns `0x80bc0023` (no resource id) for every id while a keyboard is open, since the IME keyboard never reports a connected device. [sceImeKeyboardSetMode](../../../core/libs/prx/libSceIme/Export.cpp) validates the mode bits but the mode has no effect, as no IME keyboard events are delivered. - [sceImeKeyboardGetResourceId](../../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) reports that no USB keyboard is connected (`0x80bc0004`, as [shadPS4](https://github.com/shadps4-emu/shadPS4) does) because IME keyboard events are not delivered to the handler, although [libSceKeyboard](../../../core/libs/prx/libSceKeyboard/src/keyboard_impl.cpp) exposes the host keyboard From 65e95f7139b083cb57da40f590232c5373a7c10c Mon Sep 17 00:00:00 2001 From: Ondra97 <47064087+Ondra97@users.noreply.github.com> Date: Wed, 14 Oct 2026 14:46:57 +0200 Subject: [PATCH 0324/1000] fix(libkernel): serialize Windows vectored file transfers --- core/libs/prx/libkernel/File/src/Stdio.cpp | 19 ++-- core/libs/tests/GuestFileVector.cpp | 104 +++++++++++++++++++++ docs/dev/debt/libkernel.md | 2 +- 3 files changed, 118 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index 5fd51f5386..5ded00a7ba 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -198,7 +198,7 @@ void File::SharePositionedTransfer(int from, int to) { if (!source) source = std::make_shared(); g_positionedLocks[to] = source; } -std::int64_t NativePositioned_nid_no_patch(int descriptor, void* buf, std::size_t nbytes, std::int64_t offset, bool write) { +static std::int64_t NativePositionedTransfer(int descriptor, void* buf, std::size_t nbytes, std::int64_t offset, bool write) { if (nbytes > static_cast(std::numeric_limits::max())) { throw std::runtime_error("NativePositioned: nbytes exceeds platform limit"); } @@ -211,7 +211,6 @@ std::int64_t NativePositioned_nid_no_patch(int descriptor, void* buf, std::size_ errno = EBADF; return -1; } - const auto positioned = File::LockPositionedTransfer(descriptor); LARGE_INTEGER position{}; if (::GetFileType(handle) != FILE_TYPE_DISK || !::SetFilePointerEx(handle, LARGE_INTEGER{}, &position, FILE_CURRENT)) { errno = !File::DirectoryDescriptorPath(descriptor) ? ESPIPE : write ? EBADF : EISDIR; @@ -234,17 +233,24 @@ std::int64_t NativePositioned_nid_no_patch(int descriptor, void* buf, std::size_ } return done; } -static std::int64_t NativeTransfer(int descriptor, void* buf, std::size_t nbytes, bool write) { +std::int64_t NativePositioned_nid_no_patch(int descriptor, void* buf, std::size_t nbytes, std::int64_t offset, bool write) { + const auto positioned = File::LockPositionedTransfer(descriptor); + return NativePositionedTransfer(descriptor, buf, nbytes, offset, write); +} +static std::int64_t NativeTransferBuffer(int descriptor, void* buf, std::size_t nbytes, bool write) { if (nbytes > static_cast(std::numeric_limits::max())) { throw std::runtime_error("NativeTransfer: nbytes exceeds platform limit"); } - const auto positioned = File::LockPositionedTransfer(descriptor); if (write) { const auto result = File::WriteSharedMemory_nid_no_patch(descriptor, buf, nbytes); if (result != -2) return result; } return write ? ::_write(descriptor, buf, static_cast(nbytes)) : ::_read(descriptor, buf, static_cast(nbytes)); } +static std::int64_t NativeTransfer(int descriptor, void* buf, std::size_t nbytes, bool write) { + const auto positioned = File::LockPositionedTransfer(descriptor); + return NativeTransferBuffer(descriptor, buf, nbytes, write); +} static bool NativeIsDisk(int descriptor) { const auto handle = reinterpret_cast(::_get_osfhandle(descriptor)); return handle != INVALID_HANDLE_VALUE && ::GetFileType(handle) == FILE_TYPE_DISK; @@ -1073,9 +1079,10 @@ static std::int64_t TransferIovecs(int d, const KernelIovec* iov, int iovcnt, co return result < 0 ? SceKernelError(*__error_nid_postfix()) : result; } if (!write && File::IsRandomDevice(d)) return ReadRandomIovecs(d, iov, iovcnt); + const auto positioned = File::LockPositionedTransfer(d); if (total == 0) { char none = 0; - const auto result = offset != nullptr ? NativePositioned_nid_no_patch(d, &none, 0, *offset, write) : NativeTransfer(d, &none, 0, write); + const auto result = offset != nullptr ? NativePositionedTransfer(d, &none, 0, *offset, write) : NativeTransferBuffer(d, &none, 0, write); return result < 0 ? SceErrorFromErrno(errno) : 0; } if (!write && offset == nullptr && File::DirectoryDescriptorPath(d)) return SceErrorFromErrno(GUEST_EISDIR); @@ -1085,7 +1092,7 @@ static std::int64_t TransferIovecs(int d, const KernelIovec* iov, int iovcnt, co auto* base = static_cast(iov[i].base); for (std::size_t position = 0; position < iov[i].length;) { const auto chunk = std::min(iov[i].length - position, std::numeric_limits::max()); - const auto result = offset != nullptr ? NativePositioned_nid_no_patch(d, base + position, chunk, *offset + done, write) : NativeTransfer(d, base + position, chunk, write); + const auto result = offset != nullptr ? NativePositionedTransfer(d, base + position, chunk, *offset + done, write) : NativeTransferBuffer(d, base + position, chunk, write); if (result < 0) return done != 0 ? done : SceErrorFromErrno(errno); done += result; position += static_cast(result); diff --git a/core/libs/tests/GuestFileVector.cpp b/core/libs/tests/GuestFileVector.cpp index 76f392261f..e2f2a9dfc4 100644 --- a/core/libs/tests/GuestFileVector.cpp +++ b/core/libs/tests/GuestFileVector.cpp @@ -1,5 +1,7 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include "prx/libkernel/File/include/FileFlags.hpp" +#include +#include #include #include #include @@ -10,6 +12,8 @@ #include #include #include +#include +#include #ifdef _WIN32 #include #include @@ -30,6 +34,7 @@ struct GuestIovec { extern "C" { int APS5_VABI sceKernelOpen(const char*, int, std::uint16_t); int APS5_VABI sceKernelClose(int); +int APS5_VABI dup_nid_postfix(int); std::int64_t APS5_VABI sceKernelRead(int, void*, std::size_t); std::int64_t APS5_VABI sceKernelPread(int, void*, std::size_t, std::int64_t); int APS5_VABI sceKernelLseek(int, std::int64_t, int); @@ -69,10 +74,109 @@ static std::string Contents(const std::filesystem::path& path) { return std::string(std::istreambuf_iterator(stream), std::istreambuf_iterator()); } +static void ConcurrentRecords(const std::filesystem::path& path) { + constexpr std::size_t Workers = 4; + constexpr std::size_t Records = 256; + constexpr std::size_t Parts = 32; + constexpr std::size_t RecordBytes = Parts * sizeof(std::uint16_t); + { std::ofstream stream(path, std::ios::binary); Require(static_cast(stream)); } + std::array descriptors{}; + descriptors[0] = sceKernelOpen(path.string().c_str(), SCE_KERNEL_O_RDWR, 0); + Require(descriptors[0] >= 0); + for (std::size_t worker = 1; worker < Workers; ++worker) { + descriptors[worker] = dup_nid_postfix(descriptors[0]); + Require(descriptors[worker] >= 0); + } + std::barrier start(static_cast(Workers)); + std::array writers; + for (std::size_t worker = 0; worker < Workers; ++worker) { + writers[worker] = std::thread([&, worker] { + std::uint16_t label = 0; + std::array pieces; + pieces.fill({&label, sizeof(label)}); + start.arrive_and_wait(); + for (std::size_t record = 0; record < Records; ++record) { + label = static_cast(worker * Records + record); + const auto written = worker % 2 == 0 ? sceKernelWritev(descriptors[worker], pieces.data(), static_cast(pieces.size())) + : writev_nid_postfix(descriptors[worker], pieces.data(), static_cast(pieces.size())); + Require(written == RecordBytes); + } + }); + } + for (auto& writer : writers) writer.join(); + const auto bytes = Contents(path); + Require(bytes.size() == Workers * Records * RecordBytes); + std::array seen{}; + for (std::size_t offset = 0; offset < bytes.size(); offset += RecordBytes) { + std::array record; + std::memcpy(record.data(), bytes.data() + offset, RecordBytes); + Require(record[0] < seen.size()); + for (const auto label : record) { + if (label != record[0]) std::fprintf(stderr, "writev interleaved a record at byte %zu: %u versus %u\n", offset, static_cast(record[0]), static_cast(label)); + Require(label == record[0]); + } + ++seen[record[0]]; + } + for (const auto count : seen) Require(count == 1); + Require(sceKernelLseek(descriptors[0], 0, SEEK_SET) == 0); + std::array, Workers> labels; + std::array readers; + for (std::size_t worker = 0; worker < Workers; ++worker) { + readers[worker] = std::thread([&, worker] { + std::array record{}; + std::array pieces; + for (std::size_t part = 0; part < Parts; ++part) pieces[part] = {&record[part], sizeof(record[part])}; + start.arrive_and_wait(); + for (;;) { + const auto read = worker % 2 == 0 ? sceKernelReadv(descriptors[worker], pieces.data(), static_cast(pieces.size())) + : readv_nid_postfix(descriptors[worker], pieces.data(), static_cast(pieces.size())); + if (read == 0) break; + Require(read == RecordBytes); + for (const auto label : record) Require(label == record[0]); + labels[worker].push_back(record[0]); + } + }); + } + for (auto& reader : readers) reader.join(); + seen.fill(0); + for (const auto& records : labels) for (const auto label : records) { + Require(label < seen.size()); + ++seen[label]; + } + for (const auto count : seen) Require(count == 1); + Require(sceKernelLseek(descriptors[0], 0, SEEK_CUR) == static_cast(bytes.size())); + std::array positioned; + for (std::size_t worker = 0; worker < Workers; ++worker) { + positioned[worker] = std::thread([&, worker] { + std::array record{}; + std::array pieces; + for (std::size_t part = 0; part < Parts; ++part) pieces[part] = {&record[part], sizeof(record[part])}; + start.arrive_and_wait(); + for (std::size_t iteration = 0; iteration < Records; ++iteration) { + std::int64_t transferred; + if (worker % 2 == 0) { + record.fill(static_cast(0x8000 + worker * Records + iteration)); + transferred = worker == 0 ? sceKernelPwritev(descriptors[worker], pieces.data(), static_cast(pieces.size()), 0) + : pwritev_nid_postfix(descriptors[worker], pieces.data(), static_cast(pieces.size()), 0); + } else { + transferred = worker == 1 ? sceKernelPreadv(descriptors[worker], pieces.data(), static_cast(pieces.size()), 0) + : preadv_nid_postfix(descriptors[worker], pieces.data(), static_cast(pieces.size()), 0); + for (const auto label : record) Require(label == record[0]); + } + Require(transferred == RecordBytes); + } + }); + } + for (auto& transfer : positioned) transfer.join(); + Require(sceKernelLseek(descriptors[0], 0, SEEK_CUR) == static_cast(bytes.size())); + for (const auto descriptor : descriptors) Require(sceKernelClose(descriptor) == 0); +} + int main() { const auto root = std::filesystem::path("anyps5-file-vector-test-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); Require(std::filesystem::create_directory(root)); + ConcurrentRecords(root / "concurrent.bin"); const auto path = root / "data.bin"; { std::ofstream stream(path, std::ios::binary); stream << "0123456789"; } diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index b93b7ccea5..75cd9cdf1a 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -125,7 +125,7 @@ - [Shared memory](../../../core/libs/prx/libkernel/File/src/SharedMemory.cpp) (libkernel) on Linux implements anonymous and named objects, bounded descriptor writes and seeks, and `shm_unlink` with effective read/write permission checks from FreeBSD 11. Named paths up to 1023 bytes use `/anyps5-shm-` plus SHA-1; collisions are not detected, and the namespace is shared across processes in the host mount namespace. Named creation applies the guest umask without changing the host mask. Linux procfs and `/dev/shm` are required. Operations use an advisory directory lock; external host operations bypass it, unrelated objects serialize, and descriptor operations require two spare host descriptors and compare device/inode with the initial descriptor identity on both the pinned-object and ordinary-file fallback paths. [Guest mappings](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) support shared and private mappings of these descriptors through the allocation tracker and pin a duplicate descriptor during validation and mapping; mapping requires one spare host descriptor; file-backed mappings use the write-watch scan fallback because aliases and descriptor writes bypass per-mapping tracking. Named objects and file-backed mappings on Windows remain unsupported. Invalid non-null name pointers, console behaviour and title compatibility remain unverified. - [sceKernelAddReadEvent and sceKernelAddWriteEvent](../../../core/libs/prx/libkernel/Equeue/Equeue.cpp) (libkernel): a write event reports the socket's send buffer size (`SO_SNDBUF`) in `data`, not the free space left in it, as Windows has no query for that space. Only guest sockets are supported; other descriptors and a low watermark above 1 throw - [close](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) and [setsockopt](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) reach a [libSceNet](../../../core/libs/prx/libSceNet/Export.cpp) socket through hooks libSceNet registers, as `select` polls them; the other libkernel socket calls do not. libSceNet numbers its sockets from 32 in its own table, so a CRT file descriptor and a socket can share a number; `close` then closes the socket. On the console both are one kernel descriptor space -- [sceKernelPread, sceKernelPwrite, sceKernelPreadv, sceKernelPwritev](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) on Windows transfer at the given offset with an overlapped `ReadFile`/`WriteFile` and restore the descriptor's file pointer afterwards, so a concurrent `read`/`write` on the same descriptor can see the moved pointer in between, unlike on the console. Handles that are not disk files return `ESPIPE`. `readv` on a pipe or console handle stops after the first iovec read, a short read the console is assumed to allow; each iovec is transferred separately, so a failure after the first transfer returns the bytes already done +- [sceKernelPread, sceKernelPwrite, sceKernelPreadv, sceKernelPwritev](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) on Windows transfer at the given offset with an overlapped `ReadFile`/`WriteFile` and restore the descriptor's file pointer afterwards. Guest scalar transfers, positioned transfers, vectored transfers and seeks share a position lock across duplicated descriptors; a vector holds it for the whole operation. Independently opened descriptors and other processes do not share that lock, so vector transfers over their overlapping file ranges are not coordinated. Handles that are not disk files return `ESPIPE`. A pipe or console `readv` can return a short read when no more input is available; each iovec is transferred separately, so a failure after the first transfer returns the bytes already done - [sceKernelAioSubmitReadCommands, sceKernelAioSubmitWriteCommands and their Multiple variants](../../../core/libs/prx/libkernel/Aio/src/Aio.cpp) (libkernel) on Windows transfer through the same positioned path as `sceKernelPread`/`sceKernelPwrite` and restore the descriptor's file pointer afterwards; positioned transfers, plain transfers and seeks of one descriptor are serialized per descriptor (shared across duplicated descriptors), so a concurrent transfer observes a stable file pointer - [sceKernelAioSetParam](../../../core/libs/prx/libkernel/Aio/src/Aio.cpp) (libkernel) writes the five values into the 20-byte scheduling struct the first argument points to, following the declarations in [Orbital](https://github.com/AlexAltea/orbital/blob/cc408367da6a6ebbf5ac1ac559fefeadc73b2831/tools/ida/db_types.json), [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Kernel/KernelAio.cs) and [ampr_emu](https://github.com/drakmor/ampr_emu/blob/cfa85df379f6eeeb165d7badf9b648e266fe77b7/ps5/include/sce/_fs.h). Whether the console checks the values is unknown, so a null struct, a scheduling window or delayed count limit outside 0..128, a split flag other than 0 or 1 and a split size or chunk size above 0x1000000 (SharpProspero's maximums) throw. The scheduling settings are not used: `sceKernelAioInitializeImpl` ignores the struct and requests run synchronously on submit - [sceKernelGetFsSandboxRandomWord](../../../core/libs/prx/libkernel/System/src/Sandbox.cpp) (libkernel) returns a fixed directory name, `gkbHtXc3Wq`. On the console the name is the process's randomized path (syscall 602, `randomized_path`), and the sandbox exposes system files under it, such as `common/lib` and `common/cert`. Nothing is mounted under the name here, so `/gkbHtXc3Wq/common/...` resolves under the guest root like any other path and does not exist From 0aba57d29462365de0f394b2454919bc6d165f1e Mon Sep 17 00:00:00 2001 From: Neithennn <151066966+Neithennn@users.noreply.github.com> Date: Thu, 15 Oct 2026 15:52:10 +0200 Subject: [PATCH 0325/1000] test(agc): keep waiting for the neighbour copy case's result The NeighbourCopied case returned Missed as soon as the surface image was live with nothing pending, although its result had not reached guest memory yet. It now waits until the result is pending or stored, as HoldPending does, and only a stored result is a miss. Fixes #6383 Co-Authored-By: Claude Opus 5.5 --- .../tests/execution/StoreOverUntracked.cpp | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/StoreOverUntracked.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/StoreOverUntracked.cpp index 3431424ec1..a4ebf5d925 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/StoreOverUntracked.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/StoreOverUntracked.cpp @@ -358,13 +358,10 @@ Outcome Run(const Case& test) { const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(10); while (true) { hold.lock(); - if (StorageTexture::FindLive(address, SurfaceBytes) != nullptr) { - if (StorageTexture::AnyPending()) break; - hold.unlock(); - return Outcome::Missed; - } + if (StorageTexture::FindLive(address, SurfaceBytes) != nullptr && StorageTexture::AnyPending()) break; hold.unlock(); - Require(std::chrono::steady_clock::now() < deadline, "store over untracked: the surface's storage image was never created"); + if (Holds(surface.Texels(), Stored)) return Outcome::Missed; + Require(std::chrono::steady_clock::now() < deadline, "store over untracked: the surface's result neither became pending nor reached guest memory"); std::this_thread::yield(); } const auto from = StorageTexture::FindLive(source->Address(), Block); From cbb6ddc885c07c8a3c6ff3542505e9c30ff6b51c Mon Sep 17 00:00:00 2001 From: Ondra97 <47064087+Ondra97@users.noreply.github.com> Date: Thu, 15 Oct 2026 15:54:29 +0200 Subject: [PATCH 0326/1000] test(libkernel): scope overlapping vector I/O checks to Windows --- core/libs/tests/GuestFileVector.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/libs/tests/GuestFileVector.cpp b/core/libs/tests/GuestFileVector.cpp index e2f2a9dfc4..f864d0e069 100644 --- a/core/libs/tests/GuestFileVector.cpp +++ b/core/libs/tests/GuestFileVector.cpp @@ -145,6 +145,7 @@ static void ConcurrentRecords(const std::filesystem::path& path) { } for (const auto count : seen) Require(count == 1); Require(sceKernelLseek(descriptors[0], 0, SEEK_CUR) == static_cast(bytes.size())); +#ifdef _WIN32 std::array positioned; for (std::size_t worker = 0; worker < Workers; ++worker) { positioned[worker] = std::thread([&, worker] { @@ -169,6 +170,7 @@ static void ConcurrentRecords(const std::filesystem::path& path) { } for (auto& transfer : positioned) transfer.join(); Require(sceKernelLseek(descriptors[0], 0, SEEK_CUR) == static_cast(bytes.size())); +#endif for (const auto descriptor : descriptors) Require(sceKernelClose(descriptor) == 0); } From 345fca11db1aa5c639b495b441da2f5c4b9e42f1 Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Thu, 15 Oct 2026 13:59:17 +0000 Subject: [PATCH 0327/1000] fix(tools): recognize markdown fences with CRLF endings --- tools/check_conventions.py | 2 +- tools/tests/test_check_conventions_doc_links.py | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/tools/check_conventions.py b/tools/check_conventions.py index 000a1de445..98c6f108b0 100644 --- a/tools/check_conventions.py +++ b/tools/check_conventions.py @@ -50,7 +50,7 @@ SYSTEM_DEPENDENCY = re.compile(r"\b(find_package\s*\((?!\s*(Python3|Threads|Git) TITLE = re.compile(r"\b(PPSA|CUSA)\d{5}\b") EXTENSION = re.compile(r"__attribute__|__declspec|#\s*pragma\s+(?!once\b)") LINK = re.compile(r"\[[^\]]+\]\(([^)\s]+)") -FENCE = re.compile(r"^ {0,3}(```|~~~).*?^ {0,3}\1[ \t]*$|^ {0,3}(?:```|~~~).*\Z", re.S | re.M) +FENCE = re.compile(r"^ {0,3}(```|~~~).*?^ {0,3}\1[ \t]*\r?$|^ {0,3}(?:```|~~~).*\Z", re.S | re.M) INLINE = re.compile(r"`[^`\n]*`") SECTION = re.compile(r"^###(?!#)\s*(.+?)\s*$", re.M) KEY = re.compile(r"^( *)([A-Za-z0-9_-]+|'[^']*'|\"[^\"]*\"):(?:\s+(.*?))?\s*$") diff --git a/tools/tests/test_check_conventions_doc_links.py b/tools/tests/test_check_conventions_doc_links.py index d230f88a30..3f847eae90 100644 --- a/tools/tests/test_check_conventions_doc_links.py +++ b/tools/tests/test_check_conventions_doc_links.py @@ -17,6 +17,10 @@ class DocLinkTests(unittest.TestCase): text = f"{FENCE}\n[](int a)\n{FENCE}\nsee [guide](../README.md)\n" self.assertEqual(check_conventions.links("docs/x.md", text), [("../README.md", "README.md")]) + def test_real_link_after_crlf_code_block_is_kept(self): + text = f"{FENCE}\r\ncode\r\n{FENCE}\r\nsee [guide](../README.md)\r\n" + self.assertEqual(check_conventions.links("docs/x.md", text), [("../README.md", "README.md")]) + def test_empty_link_text_is_ignored(self): self.assertEqual(check_conventions.links("docs/x.md", "![](img.png) and [](empty.md)"), []) From bc2211ac858a5ab5b773f20f1b19438679749257 Mon Sep 17 00:00:00 2001 From: fma-rc <188743521+fma-rc@users.noreply.github.com> Date: Thu, 15 Oct 2026 15:57:33 +0200 Subject: [PATCH 0328/1000] fix(tools): link overlapping pull requests without cross-referencing them --- tools/pr_overlap.py | 8 ++++++-- tools/tests/test_pr_overlap.py | 9 +++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/tools/pr_overlap.py b/tools/pr_overlap.py index b82912cd76..6a558f828d 100644 --- a/tools/pr_overlap.py +++ b/tools/pr_overlap.py @@ -188,6 +188,10 @@ def spans(conflicts, blob): for s, e in ranges) for path, ranges in sorted((conflicts or {}).items())) +def pull(blob, number): + return f'[PR {number}]({blob.split("/blob/", 1)[0].replace("https://github.com/", "https://www.github.com/", 1)}/pull/{number})' + + def report(rows, blob): if not rows: return "" @@ -204,7 +208,7 @@ def report(rows, blob): "|---|---|---|---|", ]) for number, found in sorted(confirmed.items()): - lines.append(f'| #{number} | {spans(found.get("conflicts"), blob)} | {cell(found.get("exports"))} | {cell(found["added"])} |') + lines.append(f'| {pull(blob, number)} | {spans(found.get("conflicts"), blob)} | {cell(found.get("exports"))} | {cell(found["added"])} |') if uncompared: if confirmed: lines.append("") @@ -216,7 +220,7 @@ def report(rows, blob): "|---|---|", ]) for number, files in sorted(uncompared.items()): - lines.append(f"| #{number} | {cell(files)} |") + lines.append(f"| {pull(blob, number)} | {cell(files)} |") return "\n".join(lines) + "\n" diff --git a/tools/tests/test_pr_overlap.py b/tools/tests/test_pr_overlap.py index e76ab38727..dad871a5e2 100644 --- a/tools/tests/test_pr_overlap.py +++ b/tools/tests/test_pr_overlap.py @@ -1,5 +1,6 @@ import json import os +import re import shutil import subprocess import sys @@ -216,6 +217,14 @@ class ConflictTests(unittest.TestCase): self.assertIn("`shared.cpp`", report) self.assertTrue(report.startswith(pr_overlap.MARKER)) + def test_report_links_pull_requests_without_cross_references(self): + rows = {2: {"added": set(), "conflicts": {"other.cpp": [(1, 1)]}}, + 3: {"added": set(), "uncompared": {"shared.cpp"}}} + report = pr_overlap.report(rows, "https://github.com/owner/project/blob/base") + self.assertIsNone(re.search(r"#\d|https://github\.com/owner/project/(pull|issues)/", report)) + self.assertIn("[PR 2](https://www.github.com/owner/project/pull/2)", report) + self.assertIn("[PR 3](https://www.github.com/owner/project/pull/3)", report) + class UnicodePathTests(unittest.TestCase): def test_scans_and_reports_conflicts_with_git_path_quoting_enabled(self): From 2a65acd86c3d44321702a3b5aeb1caabc8aadedc Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Thu, 15 Oct 2026 14:05:06 +0000 Subject: [PATCH 0329/1000] fix(relinker): guard TLS instruction byte access at EOF --- .../src/windows/WindowsTlsBuilder.cpp | 2 +- .../tests/test_tls_function_coverage.py | 21 +++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp b/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp index 2a017013c4..43a3989b01 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp @@ -143,7 +143,7 @@ PeDirectory WindowsTlsBuilder::Build(const std::vector& source, co const bool wide = (info.RexPrefix & 8) != 0; const bool isAlu = isAluReadOpcode(bytes[position]); const auto address = supportedPrefixes && !loadValue && !aluRead && (bytes[position] == 0x8b || isAlu) && (wide || !hasOperandSizePrefix) ? registerAddress(bytes, position, info.Length, info.RexPrefix) : std::nullopt; - const auto addressRegister = static_cast(((bytes[position + 1] >> 3) & 7) | ((info.RexPrefix & 4) << 1)); + const auto addressRegister = address ? static_cast(((bytes[position + 1] >> 3) & 7) | ((info.RexPrefix & 4) << 1)) : std::uint8_t{4}; if (address && addressRegister != 4) { std::vector moved; auto length = info.Length; diff --git a/core/relinker/relinker/tests/test_tls_function_coverage.py b/core/relinker/relinker/tests/test_tls_function_coverage.py index 8061ee1638..6c30aada25 100644 --- a/core/relinker/relinker/tests/test_tls_function_coverage.py +++ b/core/relinker/relinker/tests/test_tls_function_coverage.py @@ -491,6 +491,25 @@ def make_image(transfer, metadata, extent=None, body=None): return image +def make_eof_segment_prefix_image(): + image = bytearray(0x1000) + image[:16] = b"\x7fELF\x02\x01\x01" + bytes(9) + struct.pack_into(" Date: Wed, 14 Oct 2026 08:52:16 -0300 Subject: [PATCH 0330/1000] fix(relinker): reject core and untyped ELF conversion inputs --- .../src/pipeline/RelinkerPipeline.cpp | 7 +++ .../relinker/tests/test_elf_header.py | 50 +++++++++++++++---- 2 files changed, 48 insertions(+), 9 deletions(-) diff --git a/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp b/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp index 8e082aa3e9..9b476bd32b 100644 --- a/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp +++ b/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp @@ -19,7 +19,9 @@ using namespace Elfpatcher; namespace { +constexpr std::uint16_t kEtNone = 0; constexpr std::uint16_t kEtRel = 1; +constexpr std::uint16_t kEtCore = 4; } @@ -57,6 +59,11 @@ std::string RelinkerPipeline::relocationTypeName(std::uint32_t type) { RelinkResult RelinkerPipeline::Relink() { if (elfReader->ReadHeader().Type == kEtRel) throw RelinkerException("ET_REL relocatable inputs are not supported"); + const auto inputType = elfReader->ReadHeader().Type; + if (inputType == kEtNone) + throw RelinkerException("ET_NONE inputs do not specify an ELF object type"); + if (inputType == kEtCore) + throw RelinkerException("ET_CORE core dumps are not executable images"); auto programHeaders = elfReader->ReadProgramHeaders(); diff --git a/core/relinker/relinker/tests/test_elf_header.py b/core/relinker/relinker/tests/test_elf_header.py index 9b42382448..d345aa292d 100644 --- a/core/relinker/relinker/tests/test_elf_header.py +++ b/core/relinker/relinker/tests/test_elf_header.py @@ -73,10 +73,50 @@ def structural_cases(relinker, directory): assert not output.exists() and not registry.exists(), (case.name, "output written") +def object_type_cases(relinker, directory): + cases = ((0, "ET_NONE inputs do not specify an ELF object type"), + (1, "ET_REL relocatable inputs are not supported"), + (4, "ET_CORE core dumps are not executable images"), + (2, None), (3, None), (0xFE10, None), (0xFE18, None)) + for mode, flags in enumerate(MODES): + for filter_level in (0, 1, 2): + for object_type, error in cases: + if object_type == 2 and ("--windows" in flags or "--macos" in flags): + continue + case = Path(directory) / f"type-{mode}-{filter_level}-{object_type}" + case.mkdir() + source = case / "input.elf" + output = case / "output.out" + registry = case / "output.registry.json" + data = main_fixture() + if filter_level == 2: + struct.pack_into(" Date: Thu, 15 Oct 2026 10:20:38 -0300 Subject: [PATCH 0331/1000] refactor(relinker): read ELF header once during validation --- core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp b/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp index 9b476bd32b..cf7f7a80ae 100644 --- a/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp +++ b/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp @@ -57,9 +57,9 @@ std::string RelinkerPipeline::relocationTypeName(std::uint32_t type) { } RelinkResult RelinkerPipeline::Relink() { - if (elfReader->ReadHeader().Type == kEtRel) - throw RelinkerException("ET_REL relocatable inputs are not supported"); const auto inputType = elfReader->ReadHeader().Type; + if (inputType == kEtRel) + throw RelinkerException("ET_REL relocatable inputs are not supported"); if (inputType == kEtNone) throw RelinkerException("ET_NONE inputs do not specify an ELF object type"); if (inputType == kEtCore) From ad64fa844b181905362960be2cc6738c269c3a67 Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Thu, 15 Oct 2026 14:13:26 +0000 Subject: [PATCH 0332/1000] fix(tools): keep duplicate export paths unquoted --- tools/check_conventions.py | 2 +- tools/tests/test_check_conventions_wrapped_declarations.py | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/tools/check_conventions.py b/tools/check_conventions.py index 000a1de445..777bf34157 100644 --- a/tools/check_conventions.py +++ b/tools/check_conventions.py @@ -170,7 +170,7 @@ class Check: if not exports: return pattern = r"(^|[^[:alnum:]_])APS5_VABI[[:space:]]+(" + "|".join(exports) + r")[[:space:]]*\(" - result = subprocess.run(["git", "grep", "-nE", "--full-name", pattern, self.head, "--", ":(top)core/libs/prx"], capture_output=True, encoding="utf-8", errors="replace") + result = subprocess.run(["git", "-c", "core.quotePath=false", "grep", "-nE", "--full-name", pattern, self.head, "--", ":(top)core/libs/prx"], capture_output=True, encoding="utf-8", errors="replace") hits = [line.split(":", 3)[1:] for line in filter(None, result.stdout.split("\n"))] self.read(path for path, _, _ in hits) for path, number, text in hits: diff --git a/tools/tests/test_check_conventions_wrapped_declarations.py b/tools/tests/test_check_conventions_wrapped_declarations.py index 258fc2916a..c05d890dbb 100644 --- a/tools/tests/test_check_conventions_wrapped_declarations.py +++ b/tools/tests/test_check_conventions_wrapped_declarations.py @@ -47,6 +47,11 @@ class WrappedDeclarationTests(unittest.TestCase): self.assertNotIn("duplicate-export", result.stdout, result.stdout + result.stderr) self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + def test_non_ascii_declaration_path_does_not_crash_duplicate_scan(self): + result = self.check("caf\u00e9.cpp", "int APS5_VABI sceFooBar(int a, int b);\n") + self.assertNotIn("duplicate-export", result.stdout, result.stdout + result.stderr) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + def test_cut_helpers_in_different_libraries_are_not_duplicate_nids(self): helper = "int APS5_VABI Support_nid_no_patch_cut() { return 3; }\n" result = self.check("Export.cpp", helper, helper) From c7ef90592722b1527331ece55caa8c4745906007 Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Thu, 15 Oct 2026 14:14:25 +0000 Subject: [PATCH 0333/1000] fix(tools): hide labels that do not fit treemap slivers --- tools/progress.py | 3 ++- tools/tests/test_progress_render_sliver.py | 5 ++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/tools/progress.py b/tools/progress.py index 8a4f5995f3..cc58eb0ede 100644 --- a/tools/progress.py +++ b/tools/progress.py @@ -306,7 +306,8 @@ def treemap(title, data, left): for i, cell in enumerate(cells(total, x + 1, y + 1, w - 2, h - 2)): parts.append(rect(*cell, DONE_COLOR if i < group["done"] else TODO_COLOR)) parts.append(f'') - label = group["label"][:int((w - 10) / 7)] + label_length = int((w - 10) / 7) + label = group["label"][:label_length] if label_length > 0 else "" if (label == group["label"] or len(label) >= 3) and h > 22: parts.append(text(x + 5, y + 16, label, 12)) parts.append("") diff --git a/tools/tests/test_progress_render_sliver.py b/tools/tests/test_progress_render_sliver.py index 4fa8b21948..cf08a54344 100644 --- a/tools/tests/test_progress_render_sliver.py +++ b/tools/tests/test_progress_render_sliver.py @@ -13,7 +13,10 @@ class ProgressRenderSliverTests(unittest.TestCase): {"name": "libA", "label": "libA", "done": 300, "todo": 0}, {"name": "libB", "label": "libB", "done": 1, "todo": 0}]} shaders = {"done": 0, "total": 0, "percent": 0, "groups": []} - self.assertIn("libA", svg) + self.assertNotIn(">lib", svg) def test_cells_without_area(self): self.assertEqual(progress.cells(5, 0, 0, 0, 10), []) From 01d3594c203cc541944cc08ba61056f2a43a773d Mon Sep 17 00:00:00 2001 From: billythemenace2 Date: Thu, 15 Oct 2026 15:16:56 +0100 Subject: [PATCH 0334/1000] fix(agc): treat a TYPE 1 T# in a read-only static image slot as null A static, read-only image slot whose T# has TYPE 1 is now bound as a null image, like TYPE 0 since #5473 and #4779. On gfx1010, image_sample_lz, image_sample_b, image_load and image_get_resinfo through a format-22 TYPE 1 T# return zeros without a fault, and gfx1035 reads it as zeros too. Written, atomic and table slots still throw, and the reserved types 2-7 stay refused. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../Graphics/src/GuestTextureResource.cpp | 2 +- .../tests/GuestTextureResource.cpp | 4 + .../libSceAgcDriver/tests/ShaderMemory.cpp | 84 +++++++++++-------- .../Optimization/src/ResourceMaterializer.cpp | 2 +- docs/dev/debt/libSceAgcDriver.md | 4 +- 5 files changed, 55 insertions(+), 41 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp index 13093680d6..9b92e9b857 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp @@ -55,7 +55,7 @@ bool IsNullTextureResource(std::span words) { Require(words.size() == 8, "guest texture descriptor must contain 8 dwords"); const auto base40 = (static_cast(words[0]) | (static_cast(words[1]) << 32u)) & 0xffffffffffull; const auto type = (words[3] >> 28u) & 0xfu; - return base40 == 0 || type == 0; + return base40 == 0 || type <= 1; } GuestTextureResource DecodeTextureResource(std::span words) { diff --git a/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp b/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp index 6a8e0d8e7a..34c28d4149 100644 --- a/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp @@ -104,6 +104,10 @@ void RunGuestTextureResourceTests() { Fields nullType = base; nullType.typeRaw = 0; Require(IsNullTextureResource(pack(nullType)), "a type-0 texture with a nonzero base was not classified as null"); + nullType.typeRaw = 1; + Require(IsNullTextureResource(pack(nullType)), "a type-1 texture with a nonzero base was not classified as null"); + nullType.typeRaw = 2; + Require(!IsNullTextureResource(pack(nullType)), "a reserved type-2 texture was classified as null"); auto result = DecodeTextureResource(pack(base)); Require(result.baseAddress == (base.base40 << 8u), "decoded base address changed"); Require(result.width == 16 && result.height == 16, "decoded width or height changed"); diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index a3294a30fe..a36f753f23 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -678,46 +678,48 @@ void verifySignedSrtComparison() { void verifyTypeZeroImageMaterialization() { using namespace ShaderRecompiler; - IrResourcePlan plan; - plan.srtPlanComplete = true; - plan.resourceTrackingComplete = true; - plan.userDataCount = 0u; - plan.materializationSources = {0u}; + for (const std::uint32_t type : {0u, 1u}) { + IrResourcePlan plan; + plan.srtPlanComplete = true; + plan.resourceTrackingComplete = true; + plan.userDataCount = 0u; + plan.materializationSources = {0u}; - const std::array words{0x12345678u, (56u << 20u) | 0x100u, 0x0000c000u, 0x00000facu, 1u, 2u, 3u, 4u}; - std::vector> values; - DescriptorSource source; - source.dwordCount = static_cast(words.size()); - for (std::uint32_t i = 0u; i < words.size(); ++i) { - auto value = std::make_unique(IrOpcode::Void, IrType::U32, i); - value->SetImmediateU32(words[i]); - source.dwords[i] = value.get(); - values.push_back(std::move(value)); - } - plan.descriptorSources.push_back(source); + const std::array words{0x12345678u, (56u << 20u) | 0x100u, 0x0000c000u, 0x00000facu | (type << 28u), 1u, 2u, 3u, 4u}; + std::vector> values; + DescriptorSource source; + source.dwordCount = static_cast(words.size()); + for (std::uint32_t i = 0u; i < words.size(); ++i) { + auto value = std::make_unique(IrOpcode::Void, IrType::U32, i); + value->SetImmediateU32(words[i]); + source.dwords[i] = value.get(); + values.push_back(std::move(value)); + } + plan.descriptorSources.push_back(source); - ImageResource image; - image.source = 0u; - image.resourceClass = ImageResourceClass::Sampled; - image.dimension = RdnaImageDimension::Dim2D; - image.read = true; - plan.info.images.push_back(image); - plan.info.runtimeImageModes.push_back(ResourceMaterializer::RuntimeImageModes(image)); + ImageResource image; + image.source = 0u; + image.resourceClass = ImageResourceClass::Sampled; + image.dimension = RdnaImageDimension::Dim2D; + image.read = true; + plan.info.images.push_back(image); + plan.info.runtimeImageModes.push_back(ResourceMaterializer::RuntimeImageModes(image)); - ResourceSnapshot snapshot; - ResourceMaterializer{}.Materialize(plan, SrtRuntime{}, snapshot); - require(snapshot.images.size() == 1u && snapshot.images[0].dwordCount == 8u && snapshot.images[0].dwords == words, - "TYPE=0 image descriptor with a nonzero base was rejected or altered during materialization"); + ResourceSnapshot snapshot; + ResourceMaterializer{}.Materialize(plan, SrtRuntime{}, snapshot); + require(snapshot.images.size() == 1u && snapshot.images[0].dwordCount == 8u && snapshot.images[0].dwords == words, + type == 0u ? "TYPE=0 image descriptor with a nonzero base was rejected or altered during materialization" : "TYPE=1 image descriptor with a nonzero base was rejected or altered during materialization"); - for (const bool atomic : {false, true}) { - image.resourceClass = ImageResourceClass::Storage; - image.read = false; - image.written = !atomic; - image.atomic = atomic; - plan.info.images[0] = image; - plan.info.runtimeImageModes[0] = ResourceMaterializer::RuntimeImageModes(image); - expectFailure([&] { ResourceMaterializer{}.Materialize(plan, SrtRuntime{}, snapshot); }, "runtime image descriptor is invalid", - atomic ? "TYPE=0 atomic image descriptor was accepted without hardware evidence" : "TYPE=0 writable image descriptor was accepted without hardware evidence"); + for (const bool atomic : {false, true}) { + image.resourceClass = ImageResourceClass::Storage; + image.read = false; + image.written = !atomic; + image.atomic = atomic; + plan.info.images[0] = image; + plan.info.runtimeImageModes[0] = ResourceMaterializer::RuntimeImageModes(image); + expectFailure([&] { ResourceMaterializer{}.Materialize(plan, SrtRuntime{}, snapshot); }, "runtime image descriptor is invalid", + atomic ? "TYPE=0 or 1 atomic image descriptor was accepted without hardware evidence" : "TYPE=0 or 1 writable image descriptor was accepted without hardware evidence"); + } } } @@ -869,7 +871,7 @@ void verifyGuardedDescriptorSources() { static Texture texture; const auto base = static_cast(reinterpret_cast(texture.bytes.data())); const std::array valid{static_cast(base >> 8u), static_cast((base >> 40u) & 0xffu) | (Format8888UNorm << 20u) | (3u << 30u), 3u << 14u, 0xfacu | (Type2D << 28u), 0u, 0u, 0u, 0u}; - const std::array invalid{0x12345678u, 0xffffffffu, 0xffffffffu, 1u << 28u, 0u, 0u, 0u, 0u}; + const std::array invalid{0x12345678u, 0xffffffffu, 0xffffffffu, 2u << 28u, 0u, 0u, 0u, 0u}; const std::array sampler{0u, 0u, 0u, 0u}; std::array output{}; const auto outputAddress = static_cast(reinterpret_cast(output.data())); @@ -915,6 +917,14 @@ void verifyGuardedDescriptorSources() { AgcDriver::ShaderMemory memory({}); expectFailure([&] { static_cast(memory.Capture(request)); }, "runtime image descriptor is invalid", "guarded sources: an invalid T# behind a taken branch was accepted"); } + auto typeOne = valid; + typeOne[3] = 0xfacu | (1u << 28u); + fill(typeOne, 1u); + { + AgcDriver::ShaderMemory memory({}); + const auto capture = memory.Capture(request); + require(capture->snapshot.images.size() == 1u && std::equal(typeOne.begin(), typeOne.end(), capture->snapshot.images[0].dwords.begin()), "guarded sources: a read-only TYPE 1 T# behind a taken branch was refused or altered"); + } fill(valid, 1u); { AgcDriver::ShaderMemory memory({}); diff --git a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp index eb27dd66ea..a764e0a1ef 100644 --- a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp @@ -78,7 +78,7 @@ ImageType rawImageType(const DescriptorValue& descriptor) { bool nullImageDescriptor(const ImageResource& image, const DescriptorValue& descriptor) { if (nullImageDescriptor(descriptor)) return true; - return rawImageType(descriptor) == static_cast(0u) && image.table == NoTable && image.read && !image.written && !image.atomic; + return static_cast(rawImageType(descriptor)) <= 1u && image.table == NoTable && image.read && !image.written && !image.atomic; } IrBufferFormat rawImageFormat(const DescriptorValue& descriptor) { diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 3edcde24ff..c891eb7be0 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -35,7 +35,7 @@ ### Functional - [Initial stencil state](../../../core/libs/prx/libSceAgcDriver/Execution/include/QueueState.hpp) uses zero for `DB_STENCIL_CONTROL`, `DB_STENCILREFMASK` and `DB_STENCILREFMASK_BF` (context `0x10b`-`0x10d`), including after `CLEAR_STATE`. The public V0, V4, V5 and V7-V11 [register-default tables](../../../core/libs/prx/libSceAgc/Command/include/RegisterDefaultsData.hpp) supply these values. Linux's [GFX10 clear-state table](https://github.com/torvalds/linux/blob/master/drivers/gpu/drm/amd/amdgpu/clearstate_gfx10.h) agrees on the control register but uses `0x01000000` for the reference masks; the console reset values have not been measured. Planet Coaster (PPSA01736 v01.017) enables stencil tests without writing the control register. -- [Type-0 texture descriptors](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp) (`TYPE` = 0 in dword 3) with a nonzero base are bound as null images, like a zero base. The behaviour comes from a gfx1036 (RDNA2) measurement in issue #4139 (image sample, load and resinfo return zero, no fault); it was not measured on the console's gfx1013. +- [Type-0 and type-1 texture descriptors](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp) (`TYPE` = 0 or 1 in dword 3) with a nonzero base are bound as null images, like a zero base. TYPE 0 comes from a gfx1036 (RDNA2) measurement in issue #4139 (image sample, load and resinfo return zero, no fault). TYPE 1 comes from a gfx1010 (RDNA1) measurement in #4139 and #5828 (`image_sample_lz`, `image_sample_b`, `image_load` and `image_get_resinfo` return zero, no fault) with format 22 only; TYPE 1 is not measured on RDNA2, with other formats or with depth-compare reads (`image_sample_c*`). Neither type was measured on the console's gfx1013. - [Multisampled color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) lose their guest-read guards and cannot be reused after a successful allocation removal or unmap covers their full guarded range. Retired Vulkan images remain allocated until device teardown to preserve outstanding references. Partial unmaps keep the guard; partial allocation reuse and guest access to live multisampled data remain unsupported. - [Stencil reference export replay](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) on devices without `VK_EXT_shader_stencil_export` supports at most sixteen values proved from constant, select and phi exports, with an export proved on every kept path. It records instances, then their triangles, then the reference classes of each triangle. One pipeline filters fragments at shader termination against a dynamic push value before late stencil and depth tests. The [Vulkan stencil-reference replacement rule](https://docs.vulkan.org/refpages/latest/refpages/source/FragStencilRefEXT.html) discards bits above the attachment's stencil width; supported stencil attachments have eight bits. Numeric subgroup observations throw except bounded own-lane mask plumbing and group control with kept-lane and derivative-helper coverage proved. WQM inputs must exclude helper contributions, since [non-quad subgroup operations may treat helpers as inactive](https://docs.vulkan.org/glslext/latest/glslext/khr/GL_KHR_shader_subgroup.html). Readonly guarded SRT reads may report internal host faults; these draws wait and check faults before publishing results, and loops throw. Indirect draws, restart, strips, fans, tessellation, mesh, multisampling, early tests, ordered pixel execution, guest shader memory writes, other fault-producing memory access, clocks, primitive ID inputs, attachment feedback and pipeline face/channel splits throw. An exhausted stage push range throws. Equivalence to console ordering has not been measured on console hardware. @@ -59,7 +59,7 @@ - [Graphics pipeline libraries](../../../core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp) (libSceAgcDriver): a pipeline is fast-linked at the draw and relinked with `VK_PIPELINE_CREATE_LINK_TIME_OPTIMIZATION_BIT_EXT` on a background thread, and binds the optimized one from the next draw on. Draws recorded before that use the fast link, whose vertex stage keeps the outputs the fragment stage does not read: on RADV (Radeon 660M, Mesa 26.2.4) 346 of 483 Astro's Playroom vertex shaders paired with a fragment stage that reads none of them used more VGPRs (median 8 more, up to 2.5 times), and on an RTX 4090 13 of 15 PPSA36082 vertex shaders up to 68 instead of 38 (#2242). The optimized link compiles to the same code as a whole pipeline - [Prebuilt pipeline libraries](../../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) (libSceAgcDriver): each registered vertex or pixel shader in registration form gets its pre-rasterization or fragment-shader library built on one background thread, with a fixed state the draw may not use: triangle list, counter-clockwise front face, no depth clamp, depth bias or conservative rasterization, first provoking vertex, one sample. Libraries are kept until the device is torn down; on Astro's Playroom 1948 of them added 0.44 GB of peak memory on RADV. - [`APS5_THREAD_PRIORITY=rt`](../../../core/libs/prx/libSceAgcDriver/Execution/src/ThreadPriority.cpp) (libSceAgcDriver) lowers the process's hard `RLIMIT_RTTIME` when it is above `APS5_THREAD_RTTIME_US` or rtkit's `RTTimeUSecMax`, because [rtkit](https://github.com/heftig/rtkit/blob/c295fa849f52b487be6433e69e08b46251950399/rtkit-daemon.c#L592-L637) (`verify_process_rttime`) reads the hard limit from `/proc//limits` and refuses real-time scheduling when it is higher. An unprivileged process cannot raise the hard limit again, so it stays lowered for the process and its threads even when rtkit then refuses the request (burst limit, user or start-time checks) and the thread throws. rtkit's `MaxRealtimePriority` and `RTTimeUSecMax` are read first, so an unreachable rtkit or a priority above its maximum leaves the limit untouched. The lowered limit only matters for threads that later run under a real-time policy -- AMD image descriptors with `TYPE=0` are treated as null for read-only image slots even when the base and other words are nonzero: `image_load` on a gfx1035 with such a T# returns 0,0,0,0, and the same descriptors were seen in direct reads in PPSA01341 on an RTX 4070. Writable and atomic image slots and entries of an image table keep faulting because there is no hardware measurement for them, and reserved `TYPE=1` also faults (gfx1035 reads it as zeros too); gfx1013 behavior is unverified +- AMD image descriptors with `TYPE=0` are treated as null for read-only image slots even when the base and other words are nonzero: `image_load` on a gfx1035 with such a T# returns 0,0,0,0, and the same descriptors were seen in direct reads in PPSA01341 on an RTX 4070. Writable and atomic image slots and entries of an image table keep faulting because there is no hardware measurement for them, and the reserved types 2-7 also fault. `TYPE=1` is treated as null in the same read-only slots: gfx1035 reads it as zeros too, and gfx1010 returns zeros for `image_sample_lz`, `image_sample_b`, `image_load` and `image_get_resinfo` with format 22 (#5828); it is not measured on RDNA2, with other formats or with depth-compare reads (`image_sample_c*`). gfx1013 behavior is unverified - On a device without VK_EXT_robustness2's `nullDescriptor`, such as MoltenVK, a null image descriptor in a typed heap is a [placeholder image](../../../core/libs/prx/libSceAgcDriver/Graphics/src/PlaceholderImages.cpp) of that heap's type, 1×1 and cleared to zero, which every null descriptor of the heap shares. Reads give zeros, as `nullDescriptor` allows, but stores and atomics to it are kept, so a later read of a null storage image can return them, and a depth comparison compares against depth 0. A heap whose placeholder the device cannot create, for example 1D or 3D depth, 64-bit atomics without 64-bit image atomics, or multisampled storage without `shaderStorageImageMultisample`, throws when a null descriptor reaches it. - [Draw input snapshots](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver): `PrepareDrawBindings` caps read-only input buffer snapshots at 64 MiB (overridable via `APS5_DRAW_SNAPSHOT_MAX_MIB`). Larger inputs (streaming heaps, bindless pools) or draws exceeding the cumulative budget skip the per-draw snapshot and bind in place, avoiding multi-gigabyte CPU copy stalls (such as Ratchet & Clank: Rift Apart PPSA01474 stalling at frame ~380 copying 36 GB every 5 s). If the title rewrites those large ranges on the CPU before GPU completion, the in-place binding observes the modification, as verified by `drawSnapshotCapTests`. - [Pending storage-image results](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) are stored when something reads their memory, not when the GPU work that wrote them completes. A title that waits for that work, frees the image and reuses its memory can have the new data overwritten by the late store (#1970). A title's own CPU reads are not seen either, so a title that reads the results after waiting for a label reads stale memory. `APS5_STORE_AT_FLIP=1` stores every pending result at each flip and `APS5_STORE_AT_LABEL=1` before each label the GPU writes; both store every pending image, not only the ones the waited-for work wrote, and neither is on by default. From e37e5f0aba0cd73c4aca8b2336a69767cc4e508e Mon Sep 17 00:00:00 2001 From: AlxcBTW <187763517+AlxcBTW@users.noreply.github.com> Date: Thu, 15 Oct 2026 16:19:42 +0200 Subject: [PATCH 0335/1000] fix(agc): merge with the cache file unless this object's live cache holds it; test the unseeded double save --- .../Graphics/include/PipelineCache.hpp | 2 ++ .../Graphics/src/PipelineCache.cpp | 7 ++++++- core/libs/prx/libSceAgcDriver/tests/Recorder.cpp | 15 +++++++++++++++ 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/PipelineCache.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/PipelineCache.hpp index 73b999b4d6..bff387e234 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/PipelineCache.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/PipelineCache.hpp @@ -25,6 +25,7 @@ public: PipelineCache& operator=(const PipelineCache&) = delete; VkPipelineCache Handle() const { return cache; } PipelineCacheSynchronization& Synchronization() { return synchronization; } + void SaveNow(); private: struct FileHeader { @@ -54,6 +55,7 @@ private: VkPipelineCache cache = VK_NULL_HANDLE; PipelineCacheSynchronization synchronization; std::filesystem::path path; + bool holdsFile = false; std::size_t loadedBytes = 0; std::size_t settledBytes = 0; std::optional settledFile; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineCache.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineCache.cpp index ebfa65095e..a3cf852d6a 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineCache.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineCache.cpp @@ -160,6 +160,7 @@ void PipelineCache::load(std::vector& initialData) { loadedBytes = initialData.size(); settledBytes = initialData.size(); settledFile = file.header; + holdsFile = true; std::fprintf(stderr, "[pipeline-cache] loaded %.1f KiB from %s\n", static_cast(initialData.size()) / 1024.0, path.string().c_str()); } @@ -173,7 +174,7 @@ void PipelineCache::save(bool final) { } const auto onDisk = readHeader(); if (liveBytes == settledBytes && onDisk == settledFile) return; - const bool fileUnchanged = settledFile.has_value() && onDisk == settledFile; + const bool fileUnchanged = holdsFile && settledFile.has_value() && onDisk == settledFile; const auto started = std::chrono::steady_clock::now(); const auto settle = [&](const std::optional& file) { settledBytes = liveBytes; @@ -246,6 +247,10 @@ void PipelineCache::save(bool final) { if (final || profiling()) std::fprintf(stderr, "[pipeline-cache] saved %.1f KiB to %s in %.1f ms%s%s\n", static_cast(size) / 1024.0, path.string().c_str(), std::chrono::duration(std::chrono::steady_clock::now() - started).count(), merged != VK_NULL_HANDLE ? " (merged with the file)" : "", final ? " (teardown)" : ""); } +void PipelineCache::SaveNow() { + save(false); +} + void PipelineCache::run() { std::unique_lock lock(mutex); while (!wake.wait_for(lock, std::chrono::seconds(SaveIntervalSeconds), [&] { return stopping; })) { diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 662ce7d21c..1e25b74f60 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -7442,6 +7442,21 @@ int pipelineCacheFileTests(const Device& device) { PipelineCache writer(context, properties); build(writer.Handle(), 0); } + { + PipelineCache unseeded(context, properties, false); + unseeded.SaveNow(); + build(unseeded.Handle(), 1); + unseeded.SaveNow(); + } + { + PipelineCache reloaded(context, properties); + Require(holds(reloaded.Handle(), 0) && holds(reloaded.Handle(), 1), "an unseeded cache that saved twice over an unchanged file lost pipelines of the file"); + } + std::filesystem::remove_all(directory); + { + PipelineCache writer(context, properties); + build(writer.Handle(), 0); + } { PipelineCache extender(context, properties); Require(holds(extender.Handle(), 0), "the cache file lost a pipeline"); From 402a1d0556785ae7616ec98c25ceb670c88f51f6 Mon Sep 17 00:00:00 2001 From: Itamar Medyoni Date: Wed, 14 Oct 2026 22:14:21 +0300 Subject: [PATCH 0336/1000] test(agc): delete mapped shader cache entries with POSIX semantics on Windows The disk cache keeps loaded and persisted entries mapped for the life of the process. On Windows 10 DeleteFileW refuses a file that still has a mapped view with ERROR_ACCESS_DENIED, so agc_shader_disk_cache failed in its final remove_all after every check had passed and left its directory in the temp folder. Delete each file with FILE_DISPOSITION_FLAG_POSIX_SEMANTICS before removing the directories, as WriteFileAtomically already renames with POSIX semantics. The mapped views stay valid, and the test still fails when a file or the directory cannot be removed. Linux is unchanged. --- .../libSceAgcDriver/tests/ShaderDiskCache.cpp | 28 +++++++++++++++++-- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp index 3c7743b54e..8be16e84ae 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp @@ -56,6 +56,30 @@ void setEnvironment(const char* name, const std::string& value) { #endif } +void removeCacheDirectory(const std::filesystem::path& directory) { + std::error_code error; +#ifdef _WIN32 + std::vector files; + for (const auto& entry : std::filesystem::recursive_directory_iterator(directory)) { + if (!entry.is_directory()) files.push_back(entry.path()); + } + constexpr auto dispositionInfoEx = static_cast(21); + constexpr DWORD deleteFile = 0x00000001, posixSemantics = 0x00000002; + for (const auto& file : files) { + const auto handle = CreateFileW(file.c_str(), DELETE, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); + if (handle == INVALID_HANDLE_VALUE) error.assign(static_cast(GetLastError()), std::system_category()); + require(!error, "cannot open " + file.string() + " for deletion: " + error.message()); + struct Close { HANDLE file; ~Close() { CloseHandle(file); } } close{handle}; + DWORD flags = deleteFile | posixSemantics; + if (!SetFileInformationByHandle(handle, dispositionInfoEx, &flags, sizeof(flags))) error.assign(static_cast(GetLastError()), std::system_category()); + require(!error, "cannot delete " + file.string() + ": " + error.message()); + } +#endif + std::filesystem::remove_all(directory, error); + require(!error, "cannot remove the shader cache test directory: " + error.message()); +} + bool sameBinding(const DescriptorBinding& left, const DescriptorBinding& right) { return left.kind == right.kind && left.role == right.role && left.descriptorSet == right.descriptorSet && left.binding == right.binding && left.count == right.count && left.guestDescriptor == right.guestDescriptor && left.readOnly == right.readOnly && left.imageShape == right.imageShape && left.samplerDepthCompare == right.samplerDepthCompare && left.imageWritten == right.imageWritten && left.imageDepthCompare == right.imageDepthCompare && left.imageAtomic == right.imageAtomic && left.imageShapes == right.imageShapes && left.bufferAtomic == right.bufferAtomic && left.bufferWritten == right.bufferWritten && left.bufferRead == right.bufferRead && left.samplerUnnormalized == right.samplerUnnormalized && left.samplerIntegerBorder == right.samplerIntegerBorder && left.imageUnnormalized == right.imageUnnormalized && left.imageUnnormalizedBaseLevel == right.imageUnnormalizedBaseLevel && left.imageSamplers == right.imageSamplers; } @@ -2128,9 +2152,7 @@ int main(int argc, char** argv) { verifyBuiltinSpecialization(); verifySpecializationLiveness(); ShaderRecompiler::ShaderDiskCache::Flush(); - std::error_code error; - std::filesystem::remove_all(directory, error); - require(!error, "cannot remove the shader cache test directory: " + error.message()); + removeCacheDirectory(directory); std::cout << "shader disk cache tests passed\n"; return 0; } catch (const std::exception& error) { From 553690c0d34af422cfa2e7cdfbf51a41b7aa2670 Mon Sep 17 00:00:00 2001 From: Scott Schuster Date: Sun, 11 Oct 2026 13:53:49 -0500 Subject: [PATCH 0337/1000] perf(recompiler): report which user-data words a resource capture only reads through The SRT evaluator records every raw read with the user-data word its address is based on, and marks a word used as a plain value. The result also names the user-data word behind each shader-data dword. Co-Authored-By: Claude Opus 5.5 --- .../include/Optimization/BindingAllocator.hpp | 1 + .../include/Optimization/ResourceProgram.hpp | 1 + .../include/Optimization/SrtWalker.hpp | 2 ++ .../src/DescriptorBindingBuilder.cpp | 4 ++++ .../src/SrtWalker/SrtEvaluator.cpp | 15 ++++++++++++++- core/shader/recompiler/Recompiler.cpp | 6 ++++-- core/shader/recompiler/Recompiler.hpp | 1 + core/shader/recompiler/UserDataUse.hpp | 19 +++++++++++++++++++ 8 files changed, 46 insertions(+), 3 deletions(-) create mode 100644 core/shader/recompiler/UserDataUse.hpp diff --git a/core/shader/recompiler/Optimization/include/Optimization/BindingAllocator.hpp b/core/shader/recompiler/Optimization/include/Optimization/BindingAllocator.hpp index 4b3755e656..7396e8e96d 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/BindingAllocator.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/BindingAllocator.hpp @@ -19,6 +19,7 @@ struct BindingAllocationResult : CompiledBindingLayout { std::vector specialization; std::vector bindings; std::vector pushConstants; + std::vector shaderDataUserWords; }; class BindingAllocator { diff --git a/core/shader/recompiler/Optimization/include/Optimization/ResourceProgram.hpp b/core/shader/recompiler/Optimization/include/Optimization/ResourceProgram.hpp index 77328b8672..f83da69525 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/ResourceProgram.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/ResourceProgram.hpp @@ -38,6 +38,7 @@ struct ResourceCapture { // The walk's read addresses when the plan has pure flat slots (the leaf of each pure slot, // and every other read sorted and deduplicated); empty otherwise. SrtReadTrace readTrace; + SrtUserDataUse userDataUse; // APS5_PROFILE_DRAW: what CaptureResources spent resolving the source (the stage inputs, the // key over the code, the plan) before the walk. std::uint64_t sourceNanoseconds = 0; diff --git a/core/shader/recompiler/Optimization/include/Optimization/SrtWalker.hpp b/core/shader/recompiler/Optimization/include/Optimization/SrtWalker.hpp index 46dc84574f..8d345285de 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/SrtWalker.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/SrtWalker.hpp @@ -2,6 +2,7 @@ #define CORE_SHADER_RECOMPILIER_OPTIMIZATION_INCLUDE_OPTIMIZATION_SRTWALKER_HPP #include "IntermediateRepresentation/IrProgram.hpp" +#include "UserDataUse.hpp" #include #include @@ -35,6 +36,7 @@ struct SrtRuntime { SrtMemoryProbe accessible = nullptr; SrtMemoryProbe pendingWrite = nullptr; SrtMemoryBulkReader readWords = nullptr; + SrtUserDataUse* userDataUse = nullptr; }; enum class RuntimeValueType { diff --git a/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp b/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp index d988925a91..f0350d9109 100644 --- a/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp +++ b/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp @@ -525,6 +525,10 @@ void DescriptorBindingBuilder::Populate(BindingAllocationResult& allocation, con const auto& layout = compiled.layout; const bool needsShaderData = layout.UsesPushData() || plan.storageData; auto shaderData = needsShaderData ? ShaderDataDwordsFor(layout, userDataBase, snapshot, partialThreads, plan.imageMetadata, plan.runtimeState) : std::vector{}; + allocation.shaderDataUserWords.clear(); + if (needsShaderData) { + for (const auto reg : layout.userDataRegisters) allocation.shaderDataUserWords.push_back(reg - userDataBase); + } std::vector bindings; bindings.reserve(plan.bindings.size()); std::size_t writtenHere = 0; diff --git a/core/shader/recompiler/Optimization/src/SrtWalker/SrtEvaluator.cpp b/core/shader/recompiler/Optimization/src/SrtWalker/SrtEvaluator.cpp index 190e6af479..d9a5fbccf7 100644 --- a/core/shader/recompiler/Optimization/src/SrtWalker/SrtEvaluator.cpp +++ b/core/shader/recompiler/Optimization/src/SrtWalker/SrtEvaluator.cpp @@ -194,7 +194,18 @@ bool BasicEvaluator::EvaluateRawRead(const TNode& inst, std::uint64_t& re std::uint64_t low = 0; std::uint64_t high = 0; std::uint64_t offset = 0; - if (!Arg(handle, 0, low) || !Arg(handle, 1, high) || !Arg(inst, 1, offset)) { + std::int32_t origin = -1; + const auto baseWord = [&] { + const TNode source = handle.Argument(0); + if (_runtime.userDataUse == nullptr || source.HasImmediate() || source.Opcode() != IrOpcode::GetUserData) return Arg(handle, 0, low); + const auto reg = RegIndex(static_cast(source.Argument(0).RegisterIndex())); + if (reg < _program.userDataBase || reg - _program.userDataBase >= _runtime.userData.size() || reg - _program.userDataBase >= SrtUserDataUse::TrackedWords) return Arg(handle, 0, low); + origin = static_cast(reg - _program.userDataBase); + low = _runtime.userData[reg - _program.userDataBase]; + _runtime.userDataUse->asBase |= std::uint64_t {1} << origin; + return true; + }; + if (!baseWord() || !Arg(handle, 1, high) || !Arg(inst, 1, offset)) { return false; } const auto base = ((high << 32u) | static_cast(low)) & AddressMask; @@ -229,6 +240,7 @@ bool BasicEvaluator::EvaluateRawRead(const TNode& inst, std::uint64_t& re result = 0; return true; } + if (_runtime.userDataUse != nullptr) _runtime.userDataUse->reads.emplace_back(address, origin); if (_inaccessible != nullptr && _runtime.accessible != nullptr && !_runtime.accessible(_runtime.userContext, address, sizeof(std::uint32_t)) && LoadedFromMemory(*handle.Value())) { *_inaccessible = {inst.Value(), address}; return false; @@ -407,6 +419,7 @@ bool BasicEvaluator::EvaluateInst(const TNode& inst, std::uint64_t& resul if (reg < _program.userDataBase || reg - _program.userDataBase >= _runtime.userData.size()) { return false; } + if (_runtime.userDataUse != nullptr) _runtime.userDataUse->asValue |= reg - _program.userDataBase < SrtUserDataUse::TrackedWords ? std::uint64_t {1} << (reg - _program.userDataBase) : ~std::uint64_t {0}; result = _runtime.userData[reg - _program.userDataBase]; return true; } diff --git a/core/shader/recompiler/Recompiler.cpp b/core/shader/recompiler/Recompiler.cpp index 32dcb23ba2..7a67b3e3b1 100644 --- a/core/shader/recompiler/Recompiler.cpp +++ b/core/shader/recompiler/Recompiler.cpp @@ -992,6 +992,7 @@ RecompileResult materializeResult(const CompiledVariant& variant, const Recompil std::copy_n(runtimeSelectors.begin(), layout.runtimeStateDwords, data->guestDescriptor.begin() + first); } } + result.shaderDataUserWords = std::move(bindings.shaderDataUserWords); result.poisonedSrtReads = static_cast(snapshot.srtPoison.size()) + snapshot.nullRootReads; if (!tables.map.empty()) { result.imageTablePoison.reserve(tables.poison.size()); @@ -1326,11 +1327,12 @@ namespace { // traced into the capture (ResourceCapture::readTrace). void materializeCapture(ResourceCapture& capture, const SrtRuntime& runtime) { const auto& plan = *capture.plan; + SrtRuntime traced = runtime; + traced.userDataUse = &capture.userDataUse; if (std::none_of(plan.pureFlatSlots.begin(), plan.pureFlatSlots.end(), [](std::uint8_t pure) { return pure != 0u; })) { - ResourceMaterializer{}.Materialize(plan, runtime, capture.snapshot); + ResourceMaterializer{}.Materialize(plan, traced, capture.snapshot); return; } - SrtRuntime traced = runtime; traced.readTrace = &capture.readTrace; ResourceMaterializer{}.Materialize(plan, traced, capture.snapshot); auto& other = capture.readTrace.otherReads; diff --git a/core/shader/recompiler/Recompiler.hpp b/core/shader/recompiler/Recompiler.hpp index 157ab11feb..ff93352beb 100644 --- a/core/shader/recompiler/Recompiler.hpp +++ b/core/shader/recompiler/Recompiler.hpp @@ -579,6 +579,7 @@ struct ShaderInvocation { struct RecompileResult : CompiledShaderArtifact, ShaderInvocation { bool cacheHit = false; std::uint32_t workgroupMemoryDwords = 0; + std::vector shaderDataUserWords; [[nodiscard]] std::uint64_t PipelineVariantId() const { return specializationId != 0 ? specializationId : variantId; } }; diff --git a/core/shader/recompiler/UserDataUse.hpp b/core/shader/recompiler/UserDataUse.hpp new file mode 100644 index 0000000000..c193714270 --- /dev/null +++ b/core/shader/recompiler/UserDataUse.hpp @@ -0,0 +1,19 @@ +#ifndef CORE_SHADER_RECOMPILER_USERDATAUSE_HPP +#define CORE_SHADER_RECOMPILER_USERDATAUSE_HPP + +#include +#include +#include + +namespace ShaderRecompiler { + +struct SrtUserDataUse { + static constexpr std::uint32_t TrackedWords = 64; + std::uint64_t asBase = 0; + std::uint64_t asValue = 0; + std::vector> reads; +}; + +} + +#endif From 6603a9d09639a4fdcac7bf975876750fa66282c2 Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Wed, 14 Oct 2026 13:51:32 +0000 Subject: [PATCH 0338/1000] docs(dev): list tracing environment switches --- docs/dev/TRACING.md | 71 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 docs/dev/TRACING.md diff --git a/docs/dev/TRACING.md b/docs/dev/TRACING.md new file mode 100644 index 0000000000..54b9422419 --- /dev/null +++ b/docs/dev/TRACING.md @@ -0,0 +1,71 @@ +# Tracing + +Set a switch before starting the emulator. Unless noted, any value enables it. Most output goes to the emulator log or standard error. Performance captures are written to files as described in [Build](BUILD.md#performance-trace). Traces can be large and can affect timing. + +## Kernel and libraries + +| Switch | Output | +| --- | --- | +| `APS5_TRACE_AFFINITY` | Thread role, host thread ID, requested CPU mask and whether pinning succeeded. | +| `APS5_TRACE_AJM` | Submitted audio jobs, decoded output and unimplemented AJM calls. | +| `APS5_TRACE_APR` | APR file reads, including path, offset, size and destination. | +| `APS5_TRACE_AUDIOOUT` | Audio output port parameters when a port is opened. | +| `APS5_TRACE_AUDIOOUT2` | AudioOut2 context and port parameters, attributes, pushes, queue polls and periodic queue summaries. | +| `APS5_TRACE_CALLS` | On Windows, records calls to the imports selected by this comma-separated library list (or `all`); crash and abort reports include the last 64 calls and most-called imports. The executable must be converted with `--trace-imports`; see [Usage](../user/USAGE.md#tracing-system-library-calls). | +| `APS5_TRACE_EXIT` | Guest `exit` and `_Exit` calls and process-exit backtraces. | +| `APS5_TRACE_FIBER` | Fiber initialization, switches and returns. | +| `APS5_TRACE_MEMORY` | Direct-memory allocation, mapping and protection operations. | +| `APS5_TRACE_PREPARATION_WAIT` | Shader preparation tasks moved to the front of the queue to satisfy a waiting caller. | +| `APS5_TRACE_SSE4A` | Each emulated SSE4A instruction once per guest instruction address (Windows). | +| `APS5_TRACE_SYNC` | Event-flag operations and their wait and wake results. | +| `APS5_TRACE_TIMERS` | Timing measurements for the host wait primitives used by the emulator. | +| `APS5_TRACE_USLEEP` | Every 2,000 guest `usleep` calls, with caller addresses and requested delays. | +| `APS5_TRACE_VIDEODEC` | Video decoder API calls and decode results. | +| `APS5_TRACE_WAITS` | Blocking waits aggregated by guest call site, including timeout counts and longest waits, every three seconds. | + +## GPU and memory + +| Switch | Output | +| --- | --- | +| `APS5_TRACE_BIGBUF` | Each copied or written-back guest-memory range of at least 1 MiB. | +| `APS5_TRACE_BINDLESS` | Bindless resource materialization and descriptor selection. | +| `APS5_TRACE_BDA` | Shader memory accesses that make a program use the buffer-device-address path. | +| `APS5_TRACE_BUFFER_WRITTEN` | Guest buffer elements classified as writable or read-only after descriptor population. | +| `APS5_TRACE_CAPSYNC` | Waits for GPU writes needed by CPU accesses, including submission and range details. | +| `APS5_TRACE_CONTEXT_STATE` | PM4 context-state operations and registers written while context state is pushed. | +| `APS5_TRACE_COPY_HLE` | Pending host-side copies, their ranges, GPU batches and known writers. | +| `APS5_TRACE_DCC_KEYS` | Display and storage-image DCC key reads, writes, fills and clears. | +| `APS5_TRACE_DISPATCH_CACHE` | Compute dispatch cache hits, misses and changed user-data words. | +| `APS5_TRACE_DISPATCH_IO` | Compute dispatch resources; value `2` also prints small user-data buffers. | +| `APS5_TRACE_DRAWS` | Draw inputs and per-draw timing phases. With `APS5_PROFILE_DRAW`, also prints the detailed draw lines. | +| `APS5_TRACE_FILL_COVER` | Initial fill/image coverage mismatches and refused image-layer clears. | +| `APS5_TRACE_FLUSH` | Storage images flushed to guest memory and ranges left for the CPU. | +| `APS5_TRACE_GPU` | Queue submissions, packet execution, waits and display-buffer scanouts. | +| `APS5_TRACE_HOST_IMPORT` | Host-import mapping attempts, memory-map details, failures and retirement. | +| `APS5_TRACE_INDIRECT_DRAWS` | Indirect draw packets that pass the draw precheck. | +| `APS5_TRACE_LABEL` | `RELEASE_MEM` packets that target the address in this hexadecimal value. | +| `APS5_TRACE_LATE_LABELS` | GPU label writes observed after their waiting submission was handled. | +| `APS5_TRACE_QUERY` | Every sixteenth guest-memory page query, including cost and caller address. | +| `APS5_TRACE_RESOURCE_KEY_MISSES` | Resource-content key changes that cause draw or dispatch resource-cache misses. | +| `APS5_TRACE_SHADOW` | Unit-shadow publish, seed, eviction and retirement events; a number limits publish output. | +| `APS5_TRACE_SKIPS` | GPU operations skipped by diagnostics, with a bounded number of recompile-request dumps per reason. | +| `APS5_TRACE_STAGING` | Distinct guest-buffer ranges admitted to the write-staging window. | +| `APS5_TRACE_TEXTURES` | Sampled texture addresses, dimensions, format and nonzero-byte counts. | +| `APS5_TRACE_UNREADABLE` | Call sites that check inaccessible guest-memory ranges or fail to read them. | +| `APS5_TRACE_UPLOAD` | Reasons storage-image data is uploaded again. | +| `APS5_TRACE_BARRIERS` | Set to a positive integer to print that many skipped barrier decisions and their ranges. | + +## Performance captures + +See [Build](BUILD.md#performance-trace) for the trace format, summary fields and capture limits. + +| Switch | Output | +| --- | --- | +| `APS5_TRACE_FRAMES=:` | Records a window of presents by zero-based frame number. | +| `APS5_TRACE_SECONDS=:` | Records a window of presents by elapsed seconds after the first present. Cannot be set with `APS5_TRACE_FRAMES`. | +| `APS5_TRACE_SUMMARY_EVERY=` | Writes periodic summary lines without recording trace events. Can be combined with either window switch. | +| `APS5_TRACE_DIR=` | Writes performance capture and summary files to this directory instead of the working directory. | + +## Shader registers + +`APS5_TRACE_SHADER_REGS=all` prints register snapshots for every shader. Set it to a shader code address in hexadecimal to print one shader's registers. From 99d4d230a90b67890cf53d96e541e50d8a88c7d0 Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Thu, 15 Oct 2026 14:29:57 +0000 Subject: [PATCH 0339/1000] docs(dev): sync trace switches with current code --- docs/dev/TRACING.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/dev/TRACING.md b/docs/dev/TRACING.md index 54b9422419..8297f4ab04 100644 --- a/docs/dev/TRACING.md +++ b/docs/dev/TRACING.md @@ -28,7 +28,6 @@ Set a switch before starting the emulator. Unless noted, any value enables it. M | Switch | Output | | --- | --- | | `APS5_TRACE_BIGBUF` | Each copied or written-back guest-memory range of at least 1 MiB. | -| `APS5_TRACE_BINDLESS` | Bindless resource materialization and descriptor selection. | | `APS5_TRACE_BDA` | Shader memory accesses that make a program use the buffer-device-address path. | | `APS5_TRACE_BUFFER_WRITTEN` | Guest buffer elements classified as writable or read-only after descriptor population. | | `APS5_TRACE_CAPSYNC` | Waits for GPU writes needed by CPU accesses, including submission and range details. | @@ -37,11 +36,14 @@ Set a switch before starting the emulator. Unless noted, any value enables it. M | `APS5_TRACE_DCC_KEYS` | Display and storage-image DCC key reads, writes, fills and clears. | | `APS5_TRACE_DISPATCH_CACHE` | Compute dispatch cache hits, misses and changed user-data words. | | `APS5_TRACE_DISPATCH_IO` | Compute dispatch resources; value `2` also prints small user-data buffers. | +| `APS5_TRACE_DRAW_CACHE` | Draw-cache lookups, hits, misses, and key differences, reported every ten seconds. | | `APS5_TRACE_DRAWS` | Draw inputs and per-draw timing phases. With `APS5_PROFILE_DRAW`, also prints the detailed draw lines. | +| `APS5_TRACE_DRAW_PROGRAMS` | One line per draw listing selected shader stages, roles, code addresses and entry addresses. | | `APS5_TRACE_FILL_COVER` | Initial fill/image coverage mismatches and refused image-layer clears. | | `APS5_TRACE_FLUSH` | Storage images flushed to guest memory and ranges left for the CPU. | | `APS5_TRACE_GPU` | Queue submissions, packet execution, waits and display-buffer scanouts. | | `APS5_TRACE_HOST_IMPORT` | Host-import mapping attempts, memory-map details, failures and retirement. | +| `APS5_TRACE_IMAGE_TABLE` | Image-table resource materialization and descriptor selection. | | `APS5_TRACE_INDIRECT_DRAWS` | Indirect draw packets that pass the draw precheck. | | `APS5_TRACE_LABEL` | `RELEASE_MEM` packets that target the address in this hexadecimal value. | | `APS5_TRACE_LATE_LABELS` | GPU label writes observed after their waiting submission was handled. | @@ -64,6 +66,7 @@ See [Build](BUILD.md#performance-trace) for the trace format, summary fields and | `APS5_TRACE_FRAMES=:` | Records a window of presents by zero-based frame number. | | `APS5_TRACE_SECONDS=:` | Records a window of presents by elapsed seconds after the first present. Cannot be set with `APS5_TRACE_FRAMES`. | | `APS5_TRACE_SUMMARY_EVERY=` | Writes periodic summary lines without recording trace events. Can be combined with either window switch. | +| `APS5_TRACE_EVENTS=` | Sets the per-thread event-buffer capacity for a frame or seconds window (1–4,194,304; default 262,144). | | `APS5_TRACE_DIR=` | Writes performance capture and summary files to this directory instead of the working directory. | ## Shader registers From 45625022e75c45aff1ca5aa2402fea238e28ab85 Mon Sep 17 00:00:00 2001 From: Romans Pokrovskis Date: Thu, 15 Oct 2026 11:00:43 +0300 Subject: [PATCH 0340/1000] fix(libc): defer resident alias demotion during a host write Retain Pipodi safety guard from closed PR5851 without its resident alias expansion or automatic AGC alias routing. Main still refuses aliases over partial resident runs. Five unmodified-main regression runs fail before this guard; positive validation pending. Fork proposal only; no duplicate upstream PR intended. (cherry picked from commit 078a5740b40c3b5b6384eec8f92363360c75b758) --- core/libs/prx/libc/include/WindowsMappings.hpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/core/libs/prx/libc/include/WindowsMappings.hpp b/core/libs/prx/libc/include/WindowsMappings.hpp index 56c8851fbe..9a167663fa 100644 --- a/core/libs/prx/libc/include/WindowsMappings.hpp +++ b/core/libs/prx/libc/include/WindowsMappings.hpp @@ -261,6 +261,9 @@ public: if (found == views.end() || !found->second.resident) continue; const auto run = std::prev(residentRuns.upper_bound(address + done)); if (run->first < address || run->second.end > address + bytes) throw refuse("a resident run of direct memory extends outside the range"); + for (auto page = views.find(run->first); page != views.end() && page->first < run->second.end; ++page) { + if (page->second.hostWrites != 0) return nullptr; + } } for (std::size_t done = 0; done < bytes; done += pageBytes) { const auto found = views.find(address + done); From 50748f79f2429e0a95aebc346a6ff872b7c83e50 Mon Sep 17 00:00:00 2001 From: Romans Pokrovskis Date: Thu, 15 Oct 2026 10:54:27 +0300 Subject: [PATCH 0341/1000] test(libc): retain the open-host-write alias regression Retain the regression originally supplied by Pipodi in closed PR5851 (22091410a), adapted here through e1c6af65b. Current-main-equivalent libc/kernel source returns an alias during the open HostWrite and fails this assertion five times. This test is a reproduction, not a new implementation or an upstream PR. (cherry picked from commit 312170cd0c461104174d655911de876be341cf6a) --- core/libs/tests/GuestMemory.cpp | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/core/libs/tests/GuestMemory.cpp b/core/libs/tests/GuestMemory.cpp index a7460faf47..a69d8d8ce7 100644 --- a/core/libs/tests/GuestMemory.cpp +++ b/core/libs/tests/GuestMemory.cpp @@ -1207,6 +1207,28 @@ static void CheckAliasAcrossDirectAllocations() { Require(sceKernelReleaseDirectMemory(first, page * 3) == 0); } +static void CheckAliasLeavesResidentRunsUnderAHostWrite() { + constexpr std::size_t page = 0x4000; + std::int64_t phys = 0; + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, page * 2, 0, 0, &phys) == 0); + void* mapped = nullptr; + Require(sceKernelMapDirectMemory(&mapped, page * 2, 3, 0, phys, 0) == 0); + auto* bytes = static_cast(mapped); + bytes[5] = 0x11; + { + const GuestArena::HostWrite open(static_cast(mapped) + page, page); + Require(open.Open()); + Require(GuestArena::GuestArenaMapAlias_nid_postfix(reinterpret_cast(mapped), page * 2) == nullptr); + bytes[page + 5] = 0x22; + } + auto* alias = static_cast(GuestArena::GuestArenaMapAlias_nid_postfix(reinterpret_cast(mapped), page * 2)); + Require(alias != nullptr && alias[5] == 0x11 && alias[page + 5] == 0x22); + GuestArena::GuestArenaUnmapAlias_nid_postfix(const_cast(alias)); + Require(bytes[5] == 0x11 && bytes[page + 5] == 0x22); + Require(sceKernelMunmap(mapped, page * 2) == 0); + Require(sceKernelReleaseDirectMemory(phys, page * 2) == 0); +} + static void CheckAliasRefusesPagesThatAreNotSharedViews() { constexpr std::size_t page = 0x4000; std::int64_t phys = 0; @@ -2593,6 +2615,7 @@ int main(int argc, char** argv) { CheckAliasAcrossDirectAllocations(); CheckAliasOfReorderedSharedPages(); CheckAliasRefusesPagesThatAreNotSharedViews(); + CheckAliasLeavesResidentRunsUnderAHostWrite(); #endif CheckUnhintedKernelMappingsLandInTheWindow(); #if defined(__linux__) From f9cacfadc9549a80a6b07c1a108ffbd606cf70a7 Mon Sep 17 00:00:00 2001 From: Scott Schuster Date: Sun, 11 Oct 2026 13:53:49 -0500 Subject: [PATCH 0342/1000] perf(agc): find cached draws across moved user-data pointers A draw whose user-data words differ only in pointers the capture read through looks up an alias key built from the contents behind them, shifts the cached variant's runs by the pointer delta, validates them by value and patches the forwarded shader-data words. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 1 + .../include/Driver/Dispatch/DispatchCache.hpp | 3 + .../include/Driver/Draw/DrawCache.hpp | 32 +++ .../Execution/include/Driver/Driver.hpp | 15 +- .../Execution/src/Driver/Draw/Draw.cpp | 57 +++- .../Execution/src/Driver/Draw/DrawCache.cpp | 17 +- .../Execution/src/Driver/Draw/DrawCapture.cpp | 24 +- .../Execution/src/Driver/Draw/DrawDecode.cpp | 1 + .../src/Driver/Draw/DrawRegisterKey.cpp | 67 ++++- .../src/Driver/Draw/DrawRelocate.cpp | 268 ++++++++++++++++++ 10 files changed, 471 insertions(+), 14 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRelocate.cpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 051af9c6e5..eaf464acc4 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -154,6 +154,7 @@ set(agcDriverSources ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/Driver/Draw/DrawPipeline.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/Driver/Draw/DrawPrecheck.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/Driver/Draw/DrawRegisterKey.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/Driver/Draw/DrawRelocate.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/Driver/Draw/DrawTiming.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/Driver/Draw/IndirectDraw.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/Driver/EntryPoints.cpp diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Dispatch/DispatchCache.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Dispatch/DispatchCache.hpp index 110894795c..03005f73f4 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Dispatch/DispatchCache.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Dispatch/DispatchCache.hpp @@ -40,6 +40,9 @@ struct DispatchVariant { std::uint32_t flatBinding = NoFlatBinding; std::uint32_t pushOffset = 0; + std::vector runBases; + std::uint64_t relocatableWords = 0; + std::vector> shaderDataPatches; std::shared_ptr vertexInfo; std::shared_ptr memory; diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawCache.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawCache.hpp index ee5ce7844f..23b6231072 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawCache.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawCache.hpp @@ -1,6 +1,7 @@ #ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_DRIVER_DRAWCACHE_HPP #define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_DRIVER_DRAWCACHE_HPP +#include "UserDataUse.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Dispatch/DispatchCache.hpp" #include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" #include @@ -30,6 +31,7 @@ struct DrawProgram { bool merged = false; bool mergedPointerRequired = false; std::uint32_t mergedPointer = 0; + std::uint32_t hiddenUserWords = 0; }; struct DrawDecode { @@ -118,6 +120,36 @@ struct StageCapture { std::vector regions; std::uint64_t forgetSerial = 0; std::uint32_t pushOffset = 0; + ShaderRecompiler::SrtUserDataUse userDataUse; +}; + +struct RelocatableUserWord { + std::uint32_t reg; + std::vector> reads; +}; + +struct DrawRelocation { + std::vector words; + std::vector banned; +}; + +struct DrawUserPatch { + std::size_t program; + std::uint32_t index, value; +}; + +struct DrawRelocationPending { + std::uint64_t key = 0, alias = 0, identity = 0; +}; + +struct DrawRelocationTrial { + static constexpr std::uint32_t Window = 1024, MinimumHitShare = 16, IdleDraws = 16384, ProbationAdmissions = 2; + std::uint32_t lookups = 0, hits = 0, idle = 0, admissions = 0; + bool productive = false; +}; + +struct DrawRelocationCounters { + std::uint64_t lookups = 0, donors = 0, refused = 0, hits = 0, evicted = 0; }; bool DecodeProgramsCurrent(const DrawDecode& decode, const ShaderRegistry& registry); diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp index a2dad48ca7..636ba52a4b 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp @@ -141,8 +141,16 @@ private: void maybeReportDrawCache(bool profile); void reportDrawCache(DrawEntryCounters& counters, bool profile); std::string profileDrawCacheLines(const DrawEntryCounters& counters); - static std::uint64_t drawRegisterKey(const QueueState& queue, const ShaderRegistry& registry, std::uint64_t deviceSerial, std::uint64_t* shape = nullptr, std::array* components = nullptr); + static std::uint64_t drawRegisterKey(const QueueState& queue, const ShaderRegistry& registry, std::uint64_t deviceSerial, std::uint64_t* shape = nullptr, std::array* components = nullptr, const DrawRelocation* relocation = nullptr, std::uint64_t* alias = nullptr); void traceDrawKeyLookup(const std::array& components, bool cacheMiss); + static std::uint64_t drawIdentityKey(const QueueState& queue, const ShaderRegistry& registry); + static bool drawRelocationEnabled(); + static std::uint64_t relocatedContentKey(const RelocatableUserWord& word, std::uint32_t value, bool& readable); + static void tagVariantRuns(DispatchVariant& variant, std::span regions, const ShaderRecompiler::SrtUserDataUse& use, bool eligible, std::uint32_t hiddenWords); + static void applyShaderDataPatches(DispatchVariant& variant); + void learnPendingRelocation(); + void relocateDrawEntry(const QueueState& queue, std::uint64_t alias, std::shared_ptr& entry, std::shared_ptr& decode, std::vector& patches); + void reportDrawRelocation(); static bool sameVertexInfo(const ShaderRecompiler::ShaderVertexStageInfo& a, const ShaderRecompiler::ShaderVertexStageInfo& b); static bool sameDecode(const DrawDecode& a, const DrawDecode& b); std::shared_ptr decodeDraw(const QueueState& queue, const Submission& submission); @@ -346,6 +354,11 @@ private: std::unordered_map drawShapes; std::unordered_set traceShapes, traceKeysSeen, traceKeysEvicted; std::list drawOrder; + std::unordered_map> drawRelocations; + std::unordered_map> drawAliases; + DrawRelocationPending drawRelocationPending; + std::unordered_map drawRelocationTrials; + DrawRelocationCounters drawRelocationCounters; std::mutex drawCacheMutex; std::uint64_t drawCacheHits = 0, drawCacheEvictions = 0, drawCacheVariants = 0, drawCacheVariantBytes = 0; std::optional drawKeySightings; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp index a6421e61c9..19cc880d2a 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp @@ -77,10 +77,26 @@ DrawVerdict Driver::draw(QueueState& queue, std::span packe std::shared_ptr decode; std::shared_ptr dataEntry; std::uint64_t shapeKey = 0; + std::uint64_t drawIdentity = 0, drawAlias = 0; + std::vector userPatches; if (registerKey) { const auto keyStart = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; - drawKey = drawRegisterKey(queue, *submission.shaders, localDevice->Serial(), &shapeKey, traceDrawCache() ? &drawKeyComponents : nullptr); - std::lock_guard cacheLock(drawCacheMutex); + std::shared_ptr relocation; + bool aliasFirst = false; + if (drawRelocationEnabled()) { + drawIdentity = drawIdentityKey(queue, *submission.shaders); + std::lock_guard cacheLock(drawCacheMutex); + learnPendingRelocation(); + const auto known = drawRelocations.find(drawIdentity); + if (known != drawRelocations.end()) { + auto& trial = drawRelocationTrials[drawIdentity]; + if (trial.idle != 0) --trial.idle; + else relocation = known->second; + aliasFirst = relocation != nullptr && trial.productive; + } + } + drawKey = drawRegisterKey(queue, *submission.shaders, localDevice->Serial(), &shapeKey, traceDrawCache() ? &drawKeyComponents : nullptr, aliasFirst ? relocation.get() : nullptr, aliasFirst ? &drawAlias : nullptr); + std::unique_lock cacheLock(drawCacheMutex); ++drawEntryCounters.lookups; ++drawEntryCounters.registerKeyLookups; if (profile) drawEntryCounters.keyUs += std::chrono::duration(std::chrono::steady_clock::now() - keyStart).count(); @@ -90,6 +106,22 @@ DrawVerdict Driver::draw(QueueState& queue, std::span packe decode = entry->decode; } else { ++drawEntryCounters.absent; + if (relocation != nullptr && !aliasFirst) { + cacheLock.unlock(); + drawRegisterKey(queue, *submission.shaders, localDevice->Serial(), nullptr, nullptr, relocation.get(), &drawAlias); + cacheLock.lock(); + } + if (drawIdentity != 0) drawRelocationPending = {drawKey, drawAlias, drawIdentity}; + if (drawAlias != 0) relocateDrawEntry(queue, drawAlias, entry, decode, userPatches); + if (relocation != nullptr) { + auto& trial = drawRelocationTrials[drawIdentity]; + ++trial.lookups; + if (trial.lookups >= DrawRelocationTrial::Window) { + trial.productive = trial.hits * DrawRelocationTrial::MinimumHitShare >= trial.lookups; + if (!trial.productive) trial.idle = DrawRelocationTrial::IdleDraws; + trial.lookups = trial.hits = 0; + } + } if (traceDrawCache()) { auto& counters = drawEntryCounters; if (traceKeysEvicted.contains(drawKey)) ++counters.absentEvicted; @@ -97,7 +129,7 @@ DrawVerdict Driver::draw(QueueState& queue, std::span packe else if (traceShapes.contains(shapeKey)) ++counters.absentUserWords; else ++counters.absentNewRegisters; } - if (drawDataHits() && !drawParameters.indirect) { + if (decode == nullptr && drawDataHits() && !drawParameters.indirect) { if (const auto shape = drawShapes.find(shapeKey); shape != drawShapes.end()) { const auto kept = drawCache.find(shape->second); if (kept != drawCache.end() && kept->second->decode != nullptr && kept->second->shape == shapeKey) dataEntry = kept->second; @@ -113,6 +145,7 @@ DrawVerdict Driver::draw(QueueState& queue, std::span packe maybeReportDrawCache(profile); } if (decode != nullptr && !DecodeProgramsCurrent(*decode, *submission.shaders)) { + userPatches.clear(); std::lock_guard cacheLock(drawCacheMutex); eraseDrawEntry(drawKey, entry); ++drawEntryCounters.absent; @@ -144,6 +177,7 @@ DrawVerdict Driver::draw(QueueState& queue, std::span packe const auto& graphics = decode->state; auto pixel = decode->pixel; std::vector programs = decode->programs; + for (const auto& patch : userPatches) programs[patch.program].userData[patch.index] = patch.value; if (const auto skipped = SkippedShader(); skipped != 0 && std::any_of(programs.begin(), programs.end(), [skipped](const DrawProgram& program) { return program.binary.codeAddress == skipped; })) return drawn(); const auto setMeshIndexBuffer = [&](const Pm4::DrawParameters& parameters) { if (!graphics.stages.mesh) return; @@ -238,6 +272,23 @@ DrawVerdict Driver::draw(QueueState& queue, std::span packe bool verifyHit = false; const bool stageEntries = useDrawEntries && !(graphics.stages.tessellation && !drawParameters.indirect && drawParameters.instanceCount > 1u); lookupDraw(submission, localDevice, graphics, pixel, programs, roles, vertexInfos, stageEntries, registerKey, profile, drawKey, entry, matched, matchedRegions, drawHit, verifyHit, phaseTiming, phaseMs, dataEntry, dataCandidates, stageData); + for (auto& variant : matched) { + if (variant != nullptr) applyShaderDataPatches(*variant); + } + const auto relocatedDecode = decode; + if (!userPatches.empty()) { + std::lock_guard cacheLock(drawCacheMutex); + if (drawHit) { + ++drawRelocationCounters.hits; + ++drawRelocationTrials[drawIdentity].hits; + } + if (profile && drawRelocationCounters.lookups >= 100000) reportDrawRelocation(); + } + if (!userPatches.empty() && !drawHit) { + auto current = std::make_shared(*decode); + for (const auto& patch : userPatches) current->programs[patch.program].userData[patch.index] = patch.value; + decode = std::move(current); + } const bool patchedStages = std::any_of(stageData.begin(), stageData.end(), [](const StageDataHit& hit) { return hit.compiled != nullptr; }); std::uint64_t stageDataVerified = 0; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCache.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCache.cpp index e7b4e66382..e65aeb32db 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCache.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCache.cpp @@ -102,7 +102,22 @@ bool Driver::admitDrawKey(std::uint64_t key, std::uint64_t shape) { std::lock_guard cacheLock(drawCacheMutex); if (drawCache.contains(key)) return true; if (!drawKeySightings) drawKeySightings.emplace(drawCacheEntries() * DrawKeySightings::Ways); - const bool seen = drawKeySightings->SeenBefore(key); + bool seen = drawKeySightings->SeenBefore(key); + const auto& pending = drawRelocationPending; + if (pending.key == key && pending.identity != 0) { + if (pending.alias != 0) { + const auto trial = drawRelocationTrials.find(pending.identity); + if (drawKeySightings->SeenBefore(pending.alias ^ 0x9e3779b97f4a7c15ull) && trial != drawRelocationTrials.end()) { + auto& state = trial->second; + if (state.productive || state.hits != 0 || state.admissions < DrawRelocationTrial::ProbationAdmissions) { + if (!state.productive && state.hits == 0) ++state.admissions; + seen = true; + } + } + } else if (!drawRelocations.contains(pending.identity)) { + if (drawKeySightings->SeenBefore(pending.identity ^ 0xc2b2ae3d27d4eb4full)) seen = true; + } + } if (seen || shape == 0 || drawShapes.contains(shape)) return seen; if (!drawShapeSightings) drawShapeSightings.emplace(drawCacheEntries() * DrawKeySightings::Ways); return drawShapeSightings->SeenBefore(shape); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCapture.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCapture.cpp index 5c049b74a6..c9eac0720a 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCapture.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCapture.cpp @@ -5,6 +5,7 @@ #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Execution/include/Pm4.hpp" #include "Optimization/ResourceProgram.hpp" +#include #include #include #include @@ -55,6 +56,7 @@ std::shared_ptr Driver::materializeDraw }(); stageCapture.regions = shaderMemory.TakeRecentRegions(); + stageCapture.userDataUse = capture->userDataUse; recompiled[i] = true; memory = shaderMemory.Regions(); @@ -156,13 +158,7 @@ void Driver::cacheDrawStages(bool useDrawEntries, bool drawHit, const Pm4::DrawP std::vector regions(stageCapture.regions.begin(), stageCapture.regions.end()); for (const auto& read : decodeReads[i]) regions.push_back({read.address, std::as_bytes(std::span(read.bytes))}); std::stable_sort(regions.begin(), regions.end(), [](const ShaderRecompiler::MemoryRegion& a, const ShaderRecompiler::MemoryRegion& b) { return a.guestAddress < b.guestAddress; }); - for (const auto& region : regions) { - variant->runs.emplace_back(region.guestAddress, region.guestAddress + region.bytes.size()); - const auto count = region.bytes.size() / sizeof(std::uint32_t); - const auto offset = variant->words.size(); - variant->words.resize(offset + count); - std::memcpy(variant->words.data() + offset, region.bytes.data(), count * sizeof(std::uint32_t)); - } + tagVariantRuns(*variant, regions, stageCapture.userDataUse, decodeReads[i].empty() && (!vertexInfos[i] || vertexInfos[i]->resourcesNum == 0), programs[i].hiddenUserWords); if (drawStageDataHits() && stageCapture.capture != nullptr && !stageCapture.capture->readTrace.leaves.empty()) recordStageDataPositions(*variant, *stageCapture.capture, decodeReads[i]); if (traceDrawCache() && entry != nullptr && matched[i] == nullptr && i < entry->stages.size()) { const auto& variants = entry->stages[i]; @@ -174,7 +170,19 @@ void Driver::cacheDrawStages(bool useDrawEntries, bool drawHit, const Pm4::DrawP ++differingRunsChanged; } } - if (verifyHit && matched[i] != nullptr && (matched[i]->runs != variant->runs || matched[i]->words != variant->words)) { + const auto sameContent = [](const DispatchVariant& a, const DispatchVariant& b) { + const auto flatten = [](const DispatchVariant& source) { + std::vector> flat; + std::size_t word = 0; + for (const auto& [begin, end] : source.runs) { + for (auto address = begin; address < end && word < source.words.size(); address += sizeof(std::uint32_t)) flat.emplace_back(address, source.words[word++]); + } + std::sort(flat.begin(), flat.end()); + return flat; + }; + return (a.runs == b.runs && a.words == b.words) || flatten(a) == flatten(b); + }; + if (verifyHit && matched[i] != nullptr && (!sameContent(*matched[i], *variant) || matched[i]->compiled->pushConstants != variant->compiled->pushConstants)) { ++mismatches; static std::atomic reports{0}; if (reports.fetch_add(1) < 20) std::fprintf(stderr, "[draw-cache] verify: stage %zu (program 0x%llx) of a hit captured differently: %zu runs / %zu words matched, %zu / %zu fresh\n", i, static_cast(programs[i].binary.codeAddress), matched[i]->runs.size(), matched[i]->words.size(), variant->runs.size(), variant->words.size()); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawDecode.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawDecode.cpp index 9079eb1c16..598176196b 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawDecode.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawDecode.cpp @@ -35,6 +35,7 @@ void InitializeMerged(const QueueState& queue, DrawProgram& program, std::uint32 program.mergedPointerRequired = pointerRequired; program.firstUserSgpr = 0; program.userData.insert(program.userData.begin(), 8, 0); + program.hiddenUserWords = 8; Graphics::NoteRegisterRead(Graphics::RegisterBank::Shader, pointerBase); Graphics::NoteRegisterRead(Graphics::RegisterBank::Shader, pointerBase + 1); if (staticAbi) return; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp index d07b070444..7ad098a233 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp @@ -33,6 +33,20 @@ const auto& registerMasks() { return masks; } +template +void forEachKeyedUserWord(const QueueState& queue, bool allUserWords, TVisit&& visit) { + const auto words = [&](std::uint32_t base, std::uint32_t count) { + for (auto it = queue.shader.lower_bound(base); it != queue.shader.end() && it->first < base + count; ++it) visit(it->first, it->second); + }; + for (const auto base : {0x00cu, 0x08cu, 0x10cu}) { + const auto resources = queue.shader.find(base - 1); + const auto count = resources == queue.shader.end() ? 0u : ((resources->second >> 1u) & 0x1fu) | (((resources->second >> 27u) & 1u) << 5u); + words(base, allUserWords ? 32u : std::min(count, 32u)); + } + words(0x082u, 2); + words(0x102u, 2); +} + } std::uint64_t RegisterStateKey(const QueueState& queue, bool allUserWords, bool incremental, std::uint64_t* shapeKey, RegisterStateKeyParts* parts) { @@ -86,13 +100,59 @@ std::uint64_t DrawShaderSetKey(const QueueState& queue, const ShaderRegistry& re return key; } -std::uint64_t Driver::drawRegisterKey(const QueueState& queue, const ShaderRegistry& registry, std::uint64_t deviceSerial, std::uint64_t* shape, std::array* components) { +std::uint64_t Driver::drawIdentityKey(const QueueState& queue, const ShaderRegistry& registry) { + std::uint64_t key = 0x84222325cbf29ce4ull; + const auto mix = [&](std::uint64_t value) { + key ^= value; + key *= 0x100000001b3ull; + }; + for (const auto base : {0x008u, 0x088u, 0x0c8u, 0x108u, 0x148u}) { + const auto low = queue.shader.find(base); + const auto high = queue.shader.find(base + 1); + if (low == queue.shader.end() || high == queue.shader.end()) { + mix(0); + continue; + } + mix(low->second); + mix(high->second); + const auto resources = queue.shader.find(base + 3); + mix(resources != queue.shader.end() ? resources->second : 0); + const auto address = (static_cast(low->second) << 8u) | (static_cast(high->second & 0xffu) << 40u); + auto it = registry.upper_bound(address); + if (it == registry.begin()) { + mix(1); + continue; + } + --it; + for (const auto& snapshot : it->second) mix(reinterpret_cast(snapshot.get())); + mix(address - it->first); + } + return key == 0 ? 1 : key; +} + +std::uint64_t Driver::drawRegisterKey(const QueueState& queue, const ShaderRegistry& registry, std::uint64_t deviceSerial, std::uint64_t* shape, std::array* components, const DrawRelocation* relocation, std::uint64_t* alias) { + const bool aliased = alias != nullptr && relocation != nullptr && !relocation->words.empty(); + bool readable = true; + std::uint64_t other = 0x6c62272e07bb0142ull; + const auto mixOther = [&](std::uint64_t value) { + other ^= value; + other *= 0x100000001b3ull; + }; static const bool allUserWords = std::getenv("APS5_DRAW_KEY_ALL_USER_WORDS") != nullptr; static const bool verify = std::getenv("APS5_VERIFY_REGISTER_KEYS") != nullptr; std::uint64_t shapeKey = 0; RegisterStateKeyParts parts; auto key = RegisterStateKey(queue, allUserWords, true, &shapeKey, components != nullptr ? &parts : nullptr); if (verify && key != RegisterStateKey(queue, allUserWords, false)) throw std::runtime_error("incremental register fingerprint disagrees with full state"); + if (aliased) { + mixOther(shapeKey); + forEachKeyedUserWord(queue, allUserWords, [&](std::uint32_t offset, std::uint32_t value) { + mixOther(offset); + const auto word = std::find_if(relocation->words.begin(), relocation->words.end(), [&](const RelocatableUserWord& candidate) { return candidate.reg == offset; }); + mixOther(word != relocation->words.end() ? relocatedContentKey(*word, value, readable) : value); + }); + mixOther(deviceSerial); + } const auto mix = [&](std::uint64_t value) { key ^= value; key *= 0x100000001b3ull; @@ -123,8 +183,13 @@ std::uint64_t Driver::drawRegisterKey(const QueueState& queue, const ShaderRegis --it; for (const auto& snapshot : it->second) mix(reinterpret_cast(snapshot.get())); mix(address - it->first); + if (aliased) { + for (const auto& snapshot : it->second) mixOther(reinterpret_cast(snapshot.get())); + mixOther(address - it->first); + } } if (shape != nullptr) *shape = shapeKey; + if (aliased && readable) *alias = other == 0 ? 1 : other; return key; } diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRelocate.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRelocate.cpp new file mode 100644 index 0000000000..caff5d1bd1 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRelocate.cpp @@ -0,0 +1,268 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/ProfileOutput.hpp" +#include +#include +#include +#include +#include +#include + +namespace AgcDriver::DriverDetail { + +namespace { + +constexpr std::size_t MaxAliases = 16384; +constexpr std::size_t MaxContentWords = 64; + +std::uint64_t AlignedWord(std::uint32_t word) { + return word & ~std::uint32_t {3}; +} + +} + +bool Driver::drawRelocationEnabled() { + static const bool enabled = std::getenv("APS5_NO_DRAW_RELOCATION") == nullptr; + return enabled; +} + +std::uint64_t Driver::relocatedContentKey(const RelocatableUserWord& word, std::uint32_t value, bool& readable) { + std::uint64_t hash = 0x9e3779b97f4a7c15ull; + const auto mix = [&](std::uint64_t mixed) { + hash ^= mixed; + hash *= 0x100000001b3ull; + }; + mix(value & 3u); + if (value == 0) { + readable = false; + return hash; + } + std::array content; + for (const auto& [offset, words] : word.reads) { + mix(static_cast(offset)); + mix(words); + for (std::size_t done = 0; done < words; done += MaxContentWords) { + const auto count = std::min(words - done, MaxContentWords); + const auto address = AlignedWord(value) + static_cast(offset) + done * sizeof(std::uint32_t); + if (GuestMemory::CopyMapped(address, std::as_writable_bytes(std::span(content).first(count))) != GuestMemory::Compare::Equal) { + readable = false; + return hash; + } + for (std::size_t i = 0; i < count; ++i) mix(content[i]); + } + } + return hash; +} + +void Driver::tagVariantRuns(DispatchVariant& variant, std::span regions, const ShaderRecompiler::SrtUserDataUse& use, bool eligible, std::uint32_t hiddenWords) { + std::uint64_t relocatable = eligible && drawRelocationEnabled() ? use.asBase & ~use.asValue : 0; + if (hiddenWords != 0) relocatable &= hiddenWords >= 64 ? 0 : ~((std::uint64_t {1} << hiddenWords) - 1); + std::vector> reads; + const auto covered = [&](std::uint64_t address) { + return std::any_of(regions.begin(), regions.end(), [&](const ShaderRecompiler::MemoryRegion& region) { return address >= region.guestAddress && address - region.guestAddress + sizeof(std::uint32_t) <= region.bytes.size(); }); + }; + if (relocatable != 0) { + reads = use.reads; + std::sort(reads.begin(), reads.end()); + reads.erase(std::unique(reads.begin(), reads.end()), reads.end()); + const auto drop = [&](std::int32_t origin) { + if (origin >= 0) relocatable &= ~(std::uint64_t {1} << origin); + }; + for (std::size_t i = 0; i < reads.size(); ++i) { + if (i + 1 < reads.size() && reads[i].first == reads[i + 1].first) { + drop(reads[i].second); + drop(reads[i + 1].second); + } + if (reads[i].second >= 0 && ((reads[i].first & 3u) != 0 || !covered(reads[i].first))) drop(reads[i].second); + } + } + const auto origin = [&](std::uint64_t address) -> std::int8_t { + const auto found = std::lower_bound(reads.begin(), reads.end(), std::make_pair(address, std::numeric_limits::min())); + if (found == reads.end() || found->first != address || found->second < 0) return -1; + return (relocatable >> found->second & 1u) != 0 ? static_cast(found->second) : std::int8_t {-1}; + }; + variant.relocatableWords = relocatable; + for (const auto& region : regions) { + const auto count = region.bytes.size() / sizeof(std::uint32_t); + const auto offset = variant.words.size(); + variant.words.resize(offset + count); + std::memcpy(variant.words.data() + offset, region.bytes.data(), count * sizeof(std::uint32_t)); + if (relocatable == 0) { + variant.runs.emplace_back(region.guestAddress, region.guestAddress + region.bytes.size()); + continue; + } + std::size_t start = 0; + auto current = count != 0 ? origin(region.guestAddress) : std::int8_t {-1}; + for (std::size_t word = 1; word <= count; ++word) { + const auto next = word < count ? origin(region.guestAddress + word * sizeof(std::uint32_t)) : std::int8_t {-2}; + if (next == current) continue; + variant.runs.emplace_back(region.guestAddress + start * sizeof(std::uint32_t), region.guestAddress + word * sizeof(std::uint32_t)); + variant.runBases.push_back(current); + start = word; + current = next; + } + } +} + +namespace { + +template +std::size_t dropEveryOther(TMap& map) { + std::size_t dropped = 0; + bool drop = false; + for (auto it = map.begin(); it != map.end();) { + drop = !drop; + if (!drop) { + ++it; + continue; + } + it = map.erase(it); + ++dropped; + } + return dropped; +} + +} + +void Driver::learnPendingRelocation() { + const auto pending = drawRelocationPending; + drawRelocationPending = {}; + if (pending.key == 0) return; + const auto inserted = drawCache.find(pending.key); + if (inserted == drawCache.end() || inserted->second->decode == nullptr) return; + const auto& entry = *inserted->second; + const auto& programs = entry.decode->programs; + if (entry.stages.size() != programs.size()) return; + const auto known = drawRelocations.find(pending.identity); + auto shape = std::make_shared(); + if (known != drawRelocations.end()) *shape = *known->second; + bool changed = known == drawRelocations.end(); + for (std::size_t i = 0; i < programs.size(); ++i) { + if (entry.stages[i].empty()) continue; + const auto& variant = *entry.stages[i].front(); + const auto& program = programs[i]; + for (std::uint32_t index = program.hiddenUserWords; index < program.userData.size() && index < 64; ++index) { + const auto reg = program.userDataBase + index - program.hiddenUserWords; + const auto held = std::find_if(shape->words.begin(), shape->words.end(), [&](const RelocatableUserWord& word) { return word.reg == reg; }); + if ((variant.relocatableWords >> index & 1u) == 0) { + if (held == shape->words.end()) continue; + shape->words.erase(held); + shape->banned.push_back(reg); + changed = true; + continue; + } + if (held != shape->words.end() || std::find(shape->banned.begin(), shape->banned.end(), reg) != shape->banned.end()) continue; + RelocatableUserWord word{reg, {}}; + const auto base = AlignedWord(program.userData[index]); + for (std::size_t run = 0; run < variant.runs.size() && run < variant.runBases.size(); ++run) { + if (variant.runBases[run] != static_cast(index)) continue; + word.reads.emplace_back(static_cast(variant.runs[run].first - base), static_cast((variant.runs[run].second - variant.runs[run].first) / sizeof(std::uint32_t))); + } + shape->words.push_back(std::move(word)); + changed = true; + } + } + if (changed) { + std::sort(shape->words.begin(), shape->words.end(), [](const RelocatableUserWord& a, const RelocatableUserWord& b) { return a.reg < b.reg; }); + drawRelocations[pending.identity] = std::move(shape); + } + if (pending.alias == 0) return; + if (drawAliases.size() >= MaxAliases) drawRelocationCounters.evicted += dropEveryOther(drawAliases); + drawAliases[pending.alias] = inserted->second; +} + +void Driver::relocateDrawEntry(const QueueState& queue, std::uint64_t alias, std::shared_ptr& entry, std::shared_ptr& decode, std::vector& patches) { + auto& counters = drawRelocationCounters; + ++counters.lookups; + const auto found = drawAliases.find(alias); + if (found == drawAliases.end() || found->second->decode == nullptr) return; + const auto donor = found->second; + const auto& programs = donor->decode->programs; + if (donor->stages.size() != programs.size()) return; + struct Move { + std::size_t program; + std::uint32_t index, from, to; + }; + std::vector moves; + for (std::size_t i = 0; i < programs.size(); ++i) { + const auto& program = programs[i]; + for (std::uint32_t index = program.hiddenUserWords; index < program.userData.size(); ++index) { + const auto current = readUserData(queue.shader, program.userDataBase + index - program.hiddenUserWords); + if (current != program.userData[index]) moves.push_back({i, index, program.userData[index], current}); + } + } + if (moves.empty()) return; + ++counters.donors; + auto relocated = std::make_shared(); + relocated->decode = donor->decode; + relocated->stages.resize(donor->stages.size()); + for (std::size_t i = 0; i < donor->stages.size(); ++i) { + const bool moved = std::any_of(moves.begin(), moves.end(), [&](const Move& move) { return move.program == i; }); + if (!moved) { + relocated->stages[i] = donor->stages[i]; + continue; + } + for (const auto& variant : donor->stages[i]) { + if (variant == nullptr || variant->compiled == nullptr) continue; + const bool allowed = std::all_of(moves.begin(), moves.end(), [&](const Move& move) { return move.program != i || (move.index < 64 && (variant->relocatableWords >> move.index & 1u) != 0 && move.from != 0 && move.to != 0 && ((move.from ^ move.to) & 3u) == 0); }); + if (!allowed) { + ++counters.refused; + continue; + } + auto copy = std::make_shared(); + copy->runs = variant->runs; + copy->runBases = variant->runBases; + copy->relocatableWords = variant->relocatableWords; + copy->words = variant->words; + copy->forgetSerial = variant->forgetSerial; + copy->compiled = variant->compiled; + copy->shader = variant->shader; + copy->dataPositions = variant->dataPositions; + copy->dataSlots = variant->dataSlots; + copy->flatBinding = variant->flatBinding; + copy->pushOffset = variant->pushOffset; + copy->vertexInfo = variant->vertexInfo; + for (const auto& move : moves) { + if (move.program != i) continue; + const auto delta = AlignedWord(move.to) - AlignedWord(move.from); + for (std::size_t run = 0; run < copy->runs.size() && run < copy->runBases.size(); ++run) { + if (copy->runBases[run] != static_cast(move.index)) continue; + copy->runs[run].first += delta; + copy->runs[run].second += delta; + } + const auto& forwarded = variant->compiled->shaderDataUserWords; + for (std::uint32_t slot = 0; slot < forwarded.size(); ++slot) { + if (forwarded[slot] == move.index) copy->shaderDataPatches.emplace_back(slot, move.to); + } + } + relocated->stages[i].push_back(std::move(copy)); + } + } + patches.clear(); + for (const auto& move : moves) patches.push_back({move.program, move.index, move.to}); + entry = std::move(relocated); + decode = donor->decode; +} + +void Driver::applyShaderDataPatches(DispatchVariant& variant) { + if (variant.shaderDataPatches.empty()) return; + auto rebased = std::make_shared(*variant.compiled); + for (const auto& [slot, value] : variant.shaderDataPatches) { + const auto at = static_cast(slot) * sizeof(std::uint32_t); + if (at + sizeof(std::uint32_t) <= rebased->pushConstants.size()) std::memcpy(rebased->pushConstants.data() + at, &value, sizeof(value)); + for (auto& binding : rebased->bindings) { + if (binding.role == ShaderRecompiler::DescriptorRole::ShaderData && slot < binding.guestDescriptor.size()) binding.guestDescriptor[slot] = value; + } + } + variant.compiled = std::move(rebased); + variant.shaderDataPatches.clear(); +} + +void Driver::reportDrawRelocation() { + auto& counters = drawRelocationCounters; + AgcDriver::ProfilePrint_nid_no_patch("[draw-relocate] %llu alias lookups: %llu donors with moved user words, %llu refusals, %llu hits through a relocated entry; %zu shader sets learned, %zu aliases held (%llu dropped at the limit)\n", static_cast(counters.lookups), static_cast(counters.donors), static_cast(counters.refused), static_cast(counters.hits), drawRelocations.size(), drawAliases.size(), static_cast(counters.evicted)); + counters = {}; +} + +} From 9a0e449f6be9aab999e209e28a314720fb4e0196 Mon Sep 17 00:00:00 2001 From: Scott Schuster Date: Sun, 11 Oct 2026 14:11:50 -0500 Subject: [PATCH 0343/1000] test(agc): the draw-entry verification compares contents and forwarded user words Co-Authored-By: Claude Opus 5.5 --- .../Execution/src/Driver/Draw/DrawCapture.cpp | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCapture.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCapture.cpp index c9eac0720a..bcef0bb6d0 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCapture.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCapture.cpp @@ -182,7 +182,14 @@ void Driver::cacheDrawStages(bool useDrawEntries, bool drawHit, const Pm4::DrawP }; return (a.runs == b.runs && a.words == b.words) || flatten(a) == flatten(b); }; - if (verifyHit && matched[i] != nullptr && (!sameContent(*matched[i], *variant) || matched[i]->compiled->pushConstants != variant->compiled->pushConstants)) { + const auto sameForwardedWords = [](const ShaderRecompiler::RecompileResult& a, const ShaderRecompiler::RecompileResult& b, std::uint32_t hidden) { + if (a.pushConstants.size() != b.pushConstants.size() || a.shaderDataUserWords != b.shaderDataUserWords) return false; + for (std::size_t slot = 0; slot < a.shaderDataUserWords.size() && (slot + 1) * sizeof(std::uint32_t) <= a.pushConstants.size(); ++slot) { + if (a.shaderDataUserWords[slot] >= hidden && std::memcmp(a.pushConstants.data() + slot * sizeof(std::uint32_t), b.pushConstants.data() + slot * sizeof(std::uint32_t), sizeof(std::uint32_t)) != 0) return false; + } + return true; + }; + if (verifyHit && matched[i] != nullptr && (!sameContent(*matched[i], *variant) || !sameForwardedWords(*matched[i]->compiled, *variant->compiled, programs[i].hiddenUserWords))) { ++mismatches; static std::atomic reports{0}; if (reports.fetch_add(1) < 20) std::fprintf(stderr, "[draw-cache] verify: stage %zu (program 0x%llx) of a hit captured differently: %zu runs / %zu words matched, %zu / %zu fresh\n", i, static_cast(programs[i].binary.codeAddress), matched[i]->runs.size(), matched[i]->words.size(), variant->runs.size(), variant->words.size()); From 21b3e60b8b7d5b027cc207c211d2b336af067abd Mon Sep 17 00:00:00 2001 From: Scott Schuster Date: Mon, 12 Oct 2026 23:44:40 -0500 Subject: [PATCH 0344/1000] test(agc): a draw whose user-data pointer moved reuses the cached draw A pixel shader loads its colour through a user-data pointer. Drawn from a third address with the same contents it hits the cached draw; with other contents it does not; the addresses seen before still hit exactly. AI-assisted (Claude Code). Co-Authored-By: Claude Opus 5.5 --- .../tests/execution/DrawCacheRelocation.cpp | 169 ++++++++++++++++++ 1 file changed, 169 insertions(+) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/DrawCacheRelocation.cpp diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DrawCacheRelocation.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DrawCacheRelocation.cpp new file mode 100644 index 0000000000..358d6a2c99 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DrawCacheRelocation.cpp @@ -0,0 +1,169 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" +#include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using AgcDriver::DriverDetail::DrawMiss; + +alignas(256) constexpr std::array VertexCode{0x7e000208u, 0x7e020280u, 0x7e040280u, 0x7e0602f2u, 0xf80008cfu, 0x03020100u, 0xbf810000u, 0xbf810000u}; +alignas(256) constexpr std::array SecondVertexCode = VertexCode; +alignas(256) constexpr std::array PixelCode{0xf4000100u, 0xfa000000u, 0x7e000204u, 0x7e020280u, 0xf800180fu, 0x00010100u, 0xbf810000u, 0xbf810000u}; +alignas(16) std::array Other{0.5f, 0.0f, 0.0f, 0.0f}; +alignas(256) std::array ColorMemory{}; + +struct Header { + Shader shader{}; + ShaderUserData userData{}; +}; + +alignas(8) Header VertexHeader{}; +alignas(8) Header PixelHeader{}; +alignas(8) Header SecondVertexHeader{}; + +const Shader* Registered(Header& header, const std::uint32_t* code, std::size_t bytes, std::uint8_t type) { + header.shader.file_header = 0x34333231u; + header.shader.version = 0x18u; + header.shader.header_size = sizeof(Header); + header.shader.shader_size = static_cast(bytes); + header.shader.code = code; + header.shader.type = type; + header.shader.user_data = &header.userData; + AgcDriverRegisterShader_nid_postfix(&header.shader); + return &header.shader; +} + +void Append(std::vector& commands, std::uint32_t opcode, std::initializer_list payload) { + commands.push_back(0xc0000000u | (static_cast(payload.size() - 1) << 16u) | (opcode << 8u)); + commands.insert(commands.end(), payload); +} + +std::vector DrawFrom(std::initializer_list sources, const std::uint32_t* vertexCode = VertexCode.data()) { + const auto color = reinterpret_cast(ColorMemory.data()); + const auto vertex = reinterpret_cast(vertexCode); + const auto pixel = reinterpret_cast(PixelCode.data()); + const std::vector> context{ + {0x2d5, 0x2000}, + {0x1b6, 0}, {0x207, 0}, {0x200, 0}, {0x203, 0x800}, + {0x2dc, 0xaa00}, {0x2f8, 0}, {0x292, 2}, {0x293, 0}, + {0x80, 0}, {0x8d, 0}, {0x83, 0xffff}, {0x8c, 0xa}, + {0x2f9, 0x2d}, {0x313, 0x6000}, {0x30e, 0xffffffff}, {0x30f, 0xffffffff}, + {0x206, 0x43f}, {0x204, 0x80000}, {0x205, 0x240}, + {0x8e, 0xf}, {0x8f, 0xf}, {0x202, 0xcc0010}, + {0x1c4, 0}, {0x1c5, 9}, {0x1c3, 4}, {0x31c, 0x28028}, + {0x31b, 0}, {0x31d, 0}, {0x3b0, (63u << 14u) | 3u}, + {0x3b8, 0x9000000}, {0x1e0, 0}, + {0xc, 0}, {0xd, 0x40040}, + {0x81, 0x80000000}, {0x82, 0x40040}, + {0x90, 0x80000000}, {0x91, 0x40040}, + {0x94, 0x80000000}, {0x95, 0x40040}, + {0x318, static_cast(color >> 8u)}, {0x390, static_cast(color >> 40u)}, + {0x10f, std::bit_cast(32.0f)}, {0x110, std::bit_cast(32.0f)}, + {0x111, std::bit_cast(-2.0f)}, {0x112, std::bit_cast(2.0f)}, + {0x113, std::bit_cast(1.0f)}, {0x114, 0}, + {0xb4, 0}, {0xb5, std::bit_cast(1.0f)}, + }; + const std::vector> shader{ + {0xc8, static_cast(vertex >> 8u)}, {0xc9, static_cast(vertex >> 40u)}, {0x8b, 1u << 1u}, {0x8c, 0}, + {0x008, static_cast(pixel >> 8u)}, {0x009, static_cast(pixel >> 40u)}, {0x00b, 2u << 1u}, + }; + std::vector commands; + Append(commands, 0x79, {0x242, 4}); + for (const auto& [offset, value] : context) Append(commands, 0x69, {offset, value}); + for (const auto& [offset, value] : shader) Append(commands, 0x76, {offset, value}); + for (const auto* source : sources) { + const auto address = reinterpret_cast(source); + Append(commands, 0x76, {0x00c, static_cast(address)}); + Append(commands, 0x76, {0x00d, static_cast(address >> 32u)}); + Append(commands, 0x2d, {3, 2}); + } + return commands; +} + +void MovedDataHitsTheCache(bool libraries) { + Registered(VertexHeader, VertexCode.data(), sizeof(VertexCode), 2); + Registered(PixelHeader, PixelCode.data(), sizeof(PixelCode), 1); + const auto mode = std::string(libraries ? "with" : "without") + " graphics pipeline libraries"; + const auto draw = [&](std::initializer_list sources) { + auto commands = DrawFrom(sources); + Packet packet{commands.data(), static_cast(commands.size()), 0, {}}; + Require(sceAgcDriverSubmitDcb(&packet) == 0, "the draw submission failed"); + AgcDriverWaitIdle_nid_postfix(); + return AgcDriver::DriverDetail::Driver::Get().DrawCacheCounters(); + }; + static std::array, 8> same{}; + for (auto& entry : same) entry = {1.0f, 0.0f, 0.0f, 0.0f}; + AgcDriver::DriverDetail::DrawEntryCounters counters{}; + for (std::size_t i = 0; i < 4; ++i) counters = draw({same[i].data()}); + Require(counters.lookups == 4 && counters.hits == 0 && counters.inserts == 2, mode + ": the draws that teach the moved pointer and its contents made " + std::to_string(counters.hits) + " hits and " + std::to_string(counters.inserts) + " inserts, expected 0 and 2"); + for (std::size_t i = 4; i < same.size(); ++i) counters = draw({same[i].data()}); + Require(counters.lookups == 8 && counters.hits == 4 && counters.inserts == 2, mode + ": draws whose data moved again did not reuse the cached draw (" + std::to_string(counters.hits) + " hits in " + std::to_string(counters.lookups) + " lookups)"); + const auto other = draw({Other.data()}); + Require(other.lookups == 9 && other.hits == 4, mode + ": a draw whose moved data holds another value reused the cached draw"); + const auto again = draw({same[0].data()}); + Require(again.lookups == 10 && again.hits == 5, mode + ": a draw at an address seen before did not reuse the cached draw (" + std::to_string(again.hits) + " hits)"); +} + +void UnrelocatedAliasesStayOut(bool libraries) { + Registered(SecondVertexHeader, SecondVertexCode.data(), sizeof(SecondVertexCode), 2); + Registered(PixelHeader, PixelCode.data(), sizeof(PixelCode), 1); + const auto mode = std::string(libraries ? "with" : "without") + " graphics pipeline libraries"; + const auto draw = [&](const float* source) { + auto commands = DrawFrom({source}, SecondVertexCode.data()); + Packet packet{commands.data(), static_cast(commands.size()), 0, {}}; + Require(sceAgcDriverSubmitDcb(&packet) == 0, "the draw submission failed"); + AgcDriverWaitIdle_nid_postfix(); + return AgcDriver::DriverDetail::Driver::Get().DrawCacheCounters(); + }; + alignas(16) static std::array repeated{1.0f, 0.0f, 0.0f, 0.0f}; + alignas(16) static std::array, 2>, 12> twice{}; + for (std::size_t i = 0; i < twice.size(); ++i) { + for (auto& entry : twice[i]) entry = {0.25f + 0.03125f * static_cast(i), 0.0f, 0.0f, 0.0f}; + } + draw(repeated.data()); + const auto start = draw(repeated.data()); + AgcDriver::DriverDetail::DrawEntryCounters counters = start; + for (const auto& pair : twice) { + for (const auto& entry : pair) draw(entry.data()); + counters = draw(repeated.data()); + } + const auto inserts = counters.inserts - start.inserts; + Require(inserts <= AgcDriver::DriverDetail::DrawRelocationTrial::ProbationAdmissions, mode + ": draws whose alias repeats but never relocates were cached (" + std::to_string(inserts) + " inserts for " + std::to_string(twice.size()) + " aliases)"); + Require(counters.hits - start.hits == twice.size(), mode + ": the exact key of a repeated draw lost its hits next to draws whose alias never relocates (" + std::to_string(counters.hits - start.hits) + " hits in " + std::to_string(twice.size()) + " draws)"); +} + +} + +int main() { + try { + bool libraries = false; + { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + libraries = device->GraphicsPipelineLibraries(); + } + if (!libraries && std::getenv("APS5_NO_GPL") == nullptr) return VulkanTestSkipped; + MovedDataHitsTheCache(libraries); + UnrelocatedAliasesStayOut(libraries); + AgcDriverShutdown_nid_postfix(); + std::cout << "A draw whose data moved reused the cached draw " << (libraries ? "with" : "without") << " graphics pipeline libraries\n"; + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + AgcDriverShutdown_nid_postfix(); + return 1; + } +} From 2ea25c53f77da7d681b2838884fc62d5611eb27e Mon Sep 17 00:00:00 2001 From: Scott Schuster Date: Tue, 13 Oct 2026 22:52:26 -0500 Subject: [PATCH 0345/1000] fix(recompiler): the shader disk cache carries a result's shader-data user words Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp | 2 ++ core/shader/recompiler/ShaderDiskCache.cpp | 6 ++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp index 3c7743b54e..66fe7adaeb 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp @@ -101,6 +101,7 @@ void requireSameResult(const RecompileResult& left, const RecompileResult& right require(left.specialization == right.specialization, prefix + "specialization constants differ"); require(left.workgroupMemoryDwords == right.workgroupMemoryDwords, prefix + "workgroup memory stride differs"); require(left.poisonedSrtReads == right.poisonedSrtReads, prefix + "poisoned SRT read counts differ"); + require(left.shaderDataUserWords == right.shaderDataUserWords, prefix + "shader-data user words differ"); require(left.imageTableRanges == right.imageTableRanges && left.imageTableShader == right.imageTableShader && left.imageTableFaults == right.imageTableFaults && left.imageTablePoison.size() == right.imageTablePoison.size(), prefix + "image table invocation data differs"); require(left.imageTableReadRanges == right.imageTableReadRanges, prefix + "image table read ranges differ"); for (std::size_t i = 0; i < left.imageTablePoison.size(); ++i) { @@ -189,6 +190,7 @@ RecompileResult sampleResult() { result.stencilReplaySafe = true; result.stencilReferenceDword = 23u; result.poisonedSrtReads = 3; + result.shaderDataUserWords = {2u, 0xffffffffu, 9u}; result.barycentricEmulation = {true, false, true}; result.variantId = 99; result.imageTablePoison = {{{1u, 2u, 3u, 4u, 5u, 6u, 7u, 8u}, 8u, 2u, 6u}, {{9u, 0u, 0u, 0u, 0u, 0u, 0u, 0u}, 4u, 0xffffffffu, 19u}}; diff --git a/core/shader/recompiler/ShaderDiskCache.cpp b/core/shader/recompiler/ShaderDiskCache.cpp index 0656a8fb3f..de5f1e4f0a 100644 --- a/core/shader/recompiler/ShaderDiskCache.cpp +++ b/core/shader/recompiler/ShaderDiskCache.cpp @@ -86,7 +86,7 @@ namespace ShaderRecompiler::ShaderDiskCache { #if defined(__linux__) && defined(__x86_64__) && defined(__GLIBCXX__) static_assert(sizeof(CompiledShaderArtifact) == 264, "CompiledShaderArtifact changed: update the artifact encoder"); static_assert(sizeof(ShaderInvocation) == 200, "ShaderInvocation changed: update the invocation encoder"); -static_assert(sizeof(RecompileResult) == 472, "RecompileResult changed: update EncodeResult and DecodeResult"); +static_assert(sizeof(RecompileResult) == 496, "RecompileResult changed: update EncodeResult and DecodeResult"); static_assert(sizeof(DescriptorBinding) == 592, "DescriptorBinding changed: update the binding encoder"); static_assert(sizeof(VertexAttribute) == 32, "VertexAttribute changed: update the attribute encoder"); static_assert(sizeof(VertexInput) == 16, "VertexInput changed: update the vertex input encoder"); @@ -101,7 +101,7 @@ static_assert(sizeof(StageInput) == 56, "StageInput changed: update the info enc static_assert(sizeof(StageOutput) == 48, "StageOutput changed: update the info encoder"); static_assert(sizeof(IrBindingLayout) == 80, "IrBindingLayout changed: update the layout encoder"); static_assert(sizeof(IrDescriptorBinding) == 32, "IrDescriptorBinding changed: update the layout encoder"); -static_assert(sizeof(BindingAllocationResult) == 160, "BindingAllocationResult changed: update the allocation encoder"); +static_assert(sizeof(BindingAllocationResult) == 184, "BindingAllocationResult changed: update the allocation encoder"); static_assert(sizeof(CompiledBindingLayout) == 88, "CompiledBindingLayout changed: update the allocation encoder"); static_assert(sizeof(BindingLayout) == 16, "BindingLayout changed: update BuildKey"); static_assert(sizeof(IrResourcePlan) == 712, "IrResourcePlan changed: update EncodePlan and DecodePlan"); @@ -1694,6 +1694,7 @@ void EncodeResult(const RecompileResult& result, std::vector& out) { encodeArtifact(writer, result); encodeInvocation(writer, result); writer.Value(result.workgroupMemoryDwords); + writer.Values(std::span(result.shaderDataUserWords)); } bool DecodeResult(std::span bytes, RecompileResult& result) { @@ -1701,6 +1702,7 @@ bool DecodeResult(std::span bytes, RecompileResult& result) { decodeArtifact(reader, result); decodeInvocation(reader, result); reader.Value(result.workgroupMemoryDwords); + reader.Values(result.shaderDataUserWords); result.cacheHit = false; return reader.Done() && result.runtimeAbiVersion == RuntimeAbi::Version; } From bca123a7b9597408ba7e6d0d1fc6b89872831a37 Mon Sep 17 00:00:00 2001 From: MrMha <114929652+MrMha@users.noreply.github.com> Date: Thu, 15 Oct 2026 18:54:04 +0400 Subject: [PATCH 0346/1000] fix: reject aliased release note input and outputs --- tools/prepare_release_notes.py | 5 +++++ tools/tests/test_package_release.py | 25 +++++++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/tools/prepare_release_notes.py b/tools/prepare_release_notes.py index ed4c2e5562..55d3345e1e 100644 --- a/tools/prepare_release_notes.py +++ b/tools/prepare_release_notes.py @@ -19,6 +19,11 @@ def release_body(notes): def prepare_notes(source, body, full): + paths = (source, body, full) + for index, first in enumerate(paths): + for second in paths[index + 1:]: + if first.resolve() == second.resolve() or (first.exists() and second.exists() and first.samefile(second)): + raise ValueError("Release note input and outputs must be distinct files") notes = json.loads(source.read_text(encoding="utf-8"))["body"] if not isinstance(notes, str): raise ValueError("Generated release notes body must be a string") diff --git a/tools/tests/test_package_release.py b/tools/tests/test_package_release.py index 0de3c558e8..b648507ff9 100644 --- a/tools/tests/test_package_release.py +++ b/tools/tests/test_package_release.py @@ -384,6 +384,31 @@ class ReleaseAssetsTests(unittest.TestCase): class ReleaseNotesTests(unittest.TestCase): + def test_aliased_paths_preserve_input_and_existing_outputs(self): + for pair in ((0, 1), (0, 2), (1, 2)): + for alias in ("same", "symlink", "hardlink"): + with self.subTest(pair=pair, alias=alias), tempfile.TemporaryDirectory() as directory: + paths = [Path(directory) / name for name in ("notes.json", "body.md", "full.md")] + paths[0].write_text(json.dumps({"body": "release notes"}), encoding="utf-8") + paths[1].write_bytes(b"previous body") + paths[2].write_bytes(b"previous full") + first, second = pair + paths[second].unlink() + if alias == "same": + paths[second] = paths[first] + elif alias == "symlink": + try: + paths[second].symlink_to(paths[first]) + except OSError as error: + self.skipTest(str(error)) + else: + paths[second].hardlink_to(paths[first]) + previous = [path.read_bytes() for path in paths] + with self.assertRaisesRegex(ValueError, "distinct files"): + prepare_release_notes.prepare_notes(*paths) + self.assertEqual([path.read_bytes() for path in paths], previous) + + def test_short_notes_are_unchanged_including_unicode_at_limit(self): for notes in ("", "## What's Changed\n* Fix\n", "я" * 125000): self.assertEqual(prepare_release_notes.release_body(notes), notes) From c94e015c92ff83914e47d052260267dd6d798bb3 Mon Sep 17 00:00:00 2001 From: MrMha <114929652+MrMha@users.noreply.github.com> Date: Thu, 15 Oct 2026 18:54:23 +0400 Subject: [PATCH 0347/1000] fix: bound expanded library archives before CRC validation --- tools/build_web.py | 5 ++++- tools/tests/test_build_web.py | 18 ++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/tools/build_web.py b/tools/build_web.py index 0d8bb92408..7af1cc6992 100644 --- a/tools/build_web.py +++ b/tools/build_web.py @@ -9,6 +9,7 @@ import urllib.request import zipfile ROOT = Path(__file__).resolve().parent.parent +MAX_EXPANDED_BYTES = 512 * 1024 * 1024 def copy_sources(output): @@ -70,10 +71,12 @@ def publish_libraries(output): if name in names: raise ValueError(f"Duplicate library archive path: {file.filename}") names.add(name) + expanded = sum(file.file_size for file in files) + if expanded > MAX_EXPANDED_BYTES: + raise ValueError("Release archive exceeds maximum expanded size") corrupt = archive.testzip() if corrupt: raise ValueError(f"Corrupt release entry: {corrupt}") - expanded = sum(file.file_size for file in files) path = f"libraries/{platform}-{checksum}.zip" temporary.replace(output / path) manifest["platforms"][platform] = {"path": path, "size": size, "expandedBytes": expanded, "sha256": checksum} diff --git a/tools/tests/test_build_web.py b/tools/tests/test_build_web.py index 872c9f03eb..47c057babc 100644 --- a/tools/tests/test_build_web.py +++ b/tools/tests/test_build_web.py @@ -24,6 +24,24 @@ def library_zip(entries): class LibraryPublicationTests(unittest.TestCase): + def test_expanded_limit_is_checked_before_decompression(self): + payload = library_zip([("libs/libc.prx", b"a" * 4097)]) + with patch.object(build_web, "MAX_EXPANDED_BYTES", 4096, create=True), \ + patch.object(zipfile.ZipFile, "testzip", side_effect=AssertionError("archive decompressed")): + with self.assertRaisesRegex(ValueError, "expanded size"): + self.publish(payload, payload) + self.assertEqual(self.manifest.read_bytes(), self.previous) + self.assertEqual(list(self.libraries.iterdir()), [self.existing]) + + + def test_expanded_limit_boundary_can_be_published(self): + payload = library_zip([("libs/libc.prx", b"a" * 4096)]) + with patch.object(build_web, "MAX_EXPANDED_BYTES", 4096, create=True): + self.publish(payload, payload) + report = json.loads(self.manifest.read_text(encoding="utf-8")) + self.assertTrue(all(asset["expandedBytes"] == 4096 for asset in report["platforms"].values())) + + def setUp(self): temporary = tempfile.TemporaryDirectory() self.addCleanup(temporary.cleanup) From a80a81d80c0fa2083f726eca7836eedaeca5558e Mon Sep 17 00:00:00 2001 From: MrMha <114929652+MrMha@users.noreply.github.com> Date: Thu, 15 Oct 2026 18:54:47 +0400 Subject: [PATCH 0348/1000] fix: suppress Clang compilation fragments during syntax checks --- tools/check_compile.py | 4 ++-- tools/tests/test_check_compile.py | 16 ++++++++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/tools/check_compile.py b/tools/check_compile.py index 1ec01a433c..ddab62af88 100644 --- a/tools/check_compile.py +++ b/tools/check_compile.py @@ -10,7 +10,7 @@ from pathlib import Path CPP = (".cpp", ".cc", ".cxx", ".c") DROP = ("-c", "-MD", "-MMD", "-MP") -DROP_WITH_VALUE = ("-o", "-MT", "-MF") +DROP_WITH_VALUE = ("-o", "-MT", "-MF", "-MJ") def capture(command, cwd=None): @@ -79,7 +79,7 @@ def syntax_only(entry, source): if argument in DROP_WITH_VALUE: index += 2 continue - if argument in DROP: + if argument in DROP or argument.startswith("-MJ"): index += 1 continue if not argument.startswith("-"): diff --git a/tools/tests/test_check_compile.py b/tools/tests/test_check_compile.py index b691d60d45..0df2dc4d46 100644 --- a/tools/tests/test_check_compile.py +++ b/tools/tests/test_check_compile.py @@ -1,5 +1,6 @@ import json import os +import shutil import subprocess import sys import tempfile @@ -13,6 +14,21 @@ TOOL = Path(__file__).resolve().parents[1] / "check_compile.py" class CheckCompileTests(unittest.TestCase): + def test_syntax_check_preserves_existing_clang_compilation_fragment(self): + compiler = shutil.which("clang") + if compiler is None: + self.skipTest("Clang is required to check compilation-fragment side effects") + source = self.source("unit.c") + fragment = self.build / "fragment.json" + for option in (f'-MJ "{fragment}"', f'-MJ"{fragment}"'): + with self.subTest(option=option): + fragment.write_bytes(b"previous compilation fragment") + self.manifest([self.entry("unit.c", f'"{compiler}" -c {option} "{source}"')]) + result = self.tool("unit.c") + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(fragment.read_bytes(), b"previous compilation fragment") + + def setUp(self): holder = tempfile.TemporaryDirectory() self.addCleanup(holder.cleanup) From 911708633e3b4d6a67d2d072747de412f02a964b Mon Sep 17 00:00:00 2001 From: MrMha <114929652+MrMha@users.noreply.github.com> Date: Thu, 15 Oct 2026 18:55:05 +0400 Subject: [PATCH 0349/1000] fix: retain enabled CTest registrations across configurations --- tools/check_test_registration.py | 13 ++++++++----- tools/tests/test_test_registration.py | 14 ++++++++++++++ 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/tools/check_test_registration.py b/tools/check_test_registration.py index 6a4b16d4c4..da156692b5 100644 --- a/tools/check_test_registration.py +++ b/tools/check_test_registration.py @@ -94,21 +94,24 @@ def registered(build): if directory in visited or not file.is_file(): continue visited.add(directory) - tests, disabled = [], {} + tests, latest = [], {} for command, arguments in commands(file.read_text(encoding="utf-8", errors="replace")): if command == "subdirs": pending.extend(directory / argument for argument in arguments) elif command == "add_test" and arguments: - tests.append((arguments[0], arguments[1:])) + test = [arguments[1:], False] + tests.append(test) + latest[arguments[0]] = test elif command == "set_tests_properties" and "PROPERTIES" in arguments: properties = arguments.index("PROPERTIES") values = arguments[properties + 1:] for key, value in zip(values[::2], values[1::2]): if key == "DISABLED": for name in arguments[:properties]: - disabled[name] = value.upper() in ("1", "ON", "YES", "TRUE", "Y") - for test, arguments in tests: - if disabled.get(test, False): + if name in latest: + latest[name][1] = value.upper() in ("1", "ON", "YES", "TRUE", "Y") + for arguments, disabled in tests: + if disabled: continue for index, argument in enumerate(arguments): if index and (argument.startswith("-") or re.match(r"^[A-Za-z_]\w*=", argument)): diff --git a/tools/tests/test_test_registration.py b/tools/tests/test_test_registration.py index a5de76dba1..b53754fcb7 100644 --- a/tools/tests/test_test_registration.py +++ b/tools/tests/test_test_registration.py @@ -12,6 +12,20 @@ import check_test_registration class RegistrationTests(unittest.TestCase): + def test_disabled_property_is_local_to_each_configuration(self): + for configurations in (("OFF", "TRUE"), ("TRUE", "OFF"), ("TRUE", "TRUE")): + with self.subTest(configurations=configurations): + content = 'if(CTEST_CONFIGURATION_TYPE STREQUAL "Debug")\n' + for index, disabled in enumerate(configurations): + if index: + content += 'else()\n' + content += f'add_test("bounds" "/build/tests/config{index}/buffer_bounds_tests")\n' + content += f'set_tests_properties("bounds" PROPERTIES DISABLED {disabled})\n' + content += 'endif()\n' + names = self.registered(content) + self.assertEqual("buffer_bounds_tests" in names, "OFF" in configurations) + + def registered(self, content): with tempfile.TemporaryDirectory() as directory: build = Path(directory) From 1490e27fd7240963a1d2d9054c5b28bd2e58bc51 Mon Sep 17 00:00:00 2001 From: MrMha <114929652+MrMha@users.noreply.github.com> Date: Thu, 15 Oct 2026 18:56:27 +0400 Subject: [PATCH 0350/1000] fix: require boolean weak flags in import registries --- tools/import_audit.py | 5 ++++- tools/tests/test_import_audit.py | 18 ++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/tools/import_audit.py b/tools/import_audit.py index 037dba8fe3..4a53489429 100644 --- a/tools/import_audit.py +++ b/tools/import_audit.py @@ -411,7 +411,10 @@ def read_registry(path): nid = entry["nid"].split("#", 1)[0] if not NID.fullmatch(nid): raise AuditError(f"{path}: entry {index} has an invalid NID {entry['nid']!r}") - imports.append((nid, entry["library"], bool(entry.get("weak", False)))) + weak = entry.get("weak", False) + if not isinstance(weak, bool): + raise AuditError(f"{path}: entry {index} needs a boolean 'weak' field") + imports.append((nid, entry["library"], weak)) return imports diff --git a/tools/tests/test_import_audit.py b/tools/tests/test_import_audit.py index 9a0359e654..3fd57c11d4 100644 --- a/tools/tests/test_import_audit.py +++ b/tools/tests/test_import_audit.py @@ -174,6 +174,24 @@ class Workspace: class ImportAuditTests(unittest.TestCase): + def test_registry_weak_flags_are_booleans(self): + registry = self.work.root / "game.json" + output = self.work.root / "audit.json" + output.write_bytes(b"previous report") + for weak in ("false", "true", 0, 1, None, [], {}): + with self.subTest(weak=weak): + registry.write_text(json.dumps([{"nid": "AAAAAAAAAAA", "library": "libSceA.prx", "weak": weak}]), encoding="utf-8") + with self.assertRaisesRegex(import_audit.AuditError, "boolean 'weak'"): + import_audit.read_registry(registry) + code, out, err = self.work.run(registry, "--json", str(output)) + self.assertEqual((code, out), (2, "")) + self.assertIn("boolean 'weak'", err) + self.assertEqual(output.read_bytes(), b"previous report") + for fields, expected in (({}, False), ({"weak": False}, False), ({"weak": True}, True)): + registry.write_text(json.dumps([dict(nid="AAAAAAAAAAA", library="libSceA.prx", **fields)]), encoding="utf-8") + self.assertEqual(import_audit.read_registry(registry), [("AAAAAAAAAAA", "libSceA.prx", expected)]) + + def setUp(self): self.work = Workspace() self.addCleanup(self.work.close) From 9ab52fefc46c94e4b489775d5476bcf3a78af29c Mon Sep 17 00:00:00 2001 From: MrMha <114929652+MrMha@users.noreply.github.com> Date: Thu, 15 Oct 2026 18:57:14 +0400 Subject: [PATCH 0351/1000] fix: require fresh exact-size output for every oracle dispatch --- tools/hw-oracle/hw_oracle.py | 4 ++++ tools/hw-oracle/test_hw_oracle.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/tools/hw-oracle/hw_oracle.py b/tools/hw-oracle/hw_oracle.py index 8424ab09bf..88225e74b5 100644 --- a/tools/hw-oracle/hw_oracle.py +++ b/tools/hw-oracle/hw_oracle.py @@ -120,8 +120,12 @@ def run(body, rows, extra=b"", wave64=False, coarse=False, *, ieee, denorm32, de for start in range(0, len(padded), rows_per_dispatch): chunk = padded[start:start + rows_per_dispatch] (work / "in.bin").write_bytes(b"".join(struct.pack("<4I", *r) for r in chunk) + extra) + (work / "out.bin").unlink(missing_ok=True) subprocess.run([oracle(), str(code), str(len(chunk)), str(work / "in.bin"), str(work / "out.bin"), "16"], check=True, env=env, stdout=sys.stderr) data = (work / "out.bin").read_bytes() + expected = len(chunk) * 64 + if len(data) != expected: + raise ValueError(f"Oracle output size: expected {expected} bytes, got {len(data)}") out += [struct.unpack_from("<16I", data, lane * 64) for lane in range(len(chunk))] return out[:len(rows)] diff --git a/tools/hw-oracle/test_hw_oracle.py b/tools/hw-oracle/test_hw_oracle.py index 31fa5240e0..69f0db6479 100644 --- a/tools/hw-oracle/test_hw_oracle.py +++ b/tools/hw-oracle/test_hw_oracle.py @@ -79,6 +79,35 @@ class OracleCacheTests(unittest.TestCase): class InputRowsTests(unittest.TestCase): + def test_successful_dispatch_without_output_cannot_reuse_previous_results(self): + with tempfile.TemporaryDirectory() as directory: + runner = Path(directory) / "runner.py" + runner.write_text( + "import sys\nfrom pathlib import Path\n" + "output = Path(sys.argv[3])\n" + "marker = output.with_suffix('.seen')\n" + "if not marker.exists():\n" + " output.write_bytes(bytes(int(sys.argv[1]) * 64))\n" + " marker.touch()\n", encoding="utf-8") + for wave64 in (False, True): + with self.subTest(wave64=wave64), patch.object(hw_oracle, "assemble", return_value=runner), \ + patch.object(hw_oracle, "oracle", return_value=sys.executable): + with self.assertRaises(FileNotFoundError): + hw_oracle.run("s_nop 0", [(0, 0, 0, 0)] * 1025, wave64=wave64, **MODES) + + + def test_oracle_output_must_match_the_padded_dispatch_size(self): + for delta in (-1, 1, 64): + def dispatch(command, **kwargs): + Path(command[4]).write_bytes(bytes(int(command[2]) * 64 + delta)) + + with self.subTest(delta=delta), patch.object(hw_oracle, "assemble", return_value="kernel.co"), \ + patch.object(hw_oracle, "oracle", return_value="oracle"), \ + patch.object(hw_oracle.subprocess, "run", side_effect=dispatch): + with self.assertRaisesRegex(ValueError, "Oracle output size"): + hw_oracle.run("s_nop 0", [(0, 0, 0, 0)], **MODES) + + def test_invalid_rows_fail_before_assembler_or_gpu_lookup(self): for row in ((1, 2, 3), (1, 2, 3, 4, 5), (-1, 0, 0, 0), (0x100000000, 0, 0, 0), (1.5, 0, 0, 0), ("1", 0, 0, 0)): From 857c35eb8792ffaaf532da945695ff2c5ca47e8d Mon Sep 17 00:00:00 2001 From: MrMha <114929652+MrMha@users.noreply.github.com> Date: Thu, 15 Oct 2026 18:58:30 +0400 Subject: [PATCH 0352/1000] fix: reject invalid collapsed stack sample weights --- tools/tests/test_trace_digest.py | 14 ++++++++++++++ tools/trace_digest.py | 16 +++++++++++++--- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/tools/tests/test_trace_digest.py b/tools/tests/test_trace_digest.py index d9dd888b29..96c79009f2 100644 --- a/tools/tests/test_trace_digest.py +++ b/tools/tests/test_trace_digest.py @@ -234,6 +234,20 @@ class TraceDigestTests(unittest.TestCase): self.assertAlmostEqual(thread["wait_ms_per_frame"], 4.0) self.assertEqual(thread["top"], [{"name": "GpuMutex.hold.draw", "ms_per_frame": 6.0}]) + def test_invalid_stack_weights_fail_without_a_digest(self): + for row in ("worker;draw -1", "worker;draw 0", "worker;draw nope", "worker;;draw 1", " 1"): + with self.subTest(row=row): + self.stacks.write_text("# header\n" + row + "\n", encoding="utf-8") + output, errors = io.StringIO(), io.StringIO() + with contextlib.redirect_stdout(output), contextlib.redirect_stderr(errors): + with self.assertRaises(SystemExit) as exit: + trace_digest.main([str(self.trace), "--stacks", str(self.stacks), "--json"]) + self.assertEqual(exit.exception.code, 2) + self.assertEqual(output.getvalue(), "") + self.assertIn(str(self.stacks) + ":2:", errors.getvalue()) + self.assertNotIn("Traceback", errors.getvalue()) + + def test_stacks_self_and_total(self): stacks = self.result()["stacks"] self.assertEqual(stacks["samples"], 5) diff --git a/tools/trace_digest.py b/tools/trace_digest.py index 6644bdfbe2..f0faa81372 100644 --- a/tools/trace_digest.py +++ b/tools/trace_digest.py @@ -89,11 +89,17 @@ def load_summary(path, trace_path): def load_stacks(path): stacks = [] - for line in Path(path).read_text(encoding="utf-8").splitlines(): + for number, line in enumerate(Path(path).read_text(encoding="utf-8").splitlines(), 1): if line.startswith("#") or not line.strip(): continue frames, _, count = line.rpartition(" ") - stacks.append((frames.split(";"), int(count))) + try: + weight = int(count) + except ValueError as error: + raise ValueError(f"{path}:{number}: sample count must be a positive integer") from error + if weight <= 0 or any(not frame.strip() for frame in frames.split(";")): + raise ValueError(f"{path}:{number}: expected nonempty frames and a positive sample count") + stacks.append((frames.split(";"), weight)) return stacks @@ -309,8 +315,12 @@ def main(arguments=None): result = digest_intervals(load_intervals(options.trace)) sys.stdout.write(json.dumps(result, indent=1) + "\n" if options.json else markdown_intervals(result)) return 0 + try: + stacks = load_stacks(options.stacks) if options.stacks else None + except (OSError, ValueError) as error: + parser.error(str(error)) result = digest(load_trace(options.trace), load_summary(options.summary, options.trace), - load_stacks(options.stacks) if options.stacks else None, options.frames, options.top) + stacks, options.frames, options.top) sys.stdout.write(json.dumps(result, indent=1) + "\n" if options.json else markdown(result)) return 0 From 03a9073104eadf7044c681ef7b98d465556c9390 Mon Sep 17 00:00:00 2001 From: MrMha <114929652+MrMha@users.noreply.github.com> Date: Thu, 15 Oct 2026 18:58:49 +0400 Subject: [PATCH 0353/1000] fix: reject negative trace digest list limits --- tools/tests/test_trace_digest.py | 20 ++++++++++++++++++++ tools/trace_digest.py | 11 +++++++++-- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/tools/tests/test_trace_digest.py b/tools/tests/test_trace_digest.py index d9dd888b29..6575a5067e 100644 --- a/tools/tests/test_trace_digest.py +++ b/tools/tests/test_trace_digest.py @@ -272,6 +272,26 @@ class TraceDigestTests(unittest.TestCase): def test_no_batch_section_without_batch_ids(self): self.assertNotIn("largest_gpu_batches", self.result()) + def test_negative_list_limits_are_rejected_and_zero_is_supported(self): + for option in ("--frames", "--top"): + with self.subTest(option=option): + output, errors = io.StringIO(), io.StringIO() + with contextlib.redirect_stdout(output), contextlib.redirect_stderr(errors): + with self.assertRaises(SystemExit) as exit: + trace_digest.main([str(self.trace), option, "-1", "--json"]) + self.assertEqual(exit.exception.code, 2) + self.assertEqual(output.getvalue(), "") + self.assertIn(option, errors.getvalue()) + self.assertIn("nonnegative", errors.getvalue()) + output = io.StringIO() + with contextlib.redirect_stdout(output): + self.assertEqual(trace_digest.main([str(self.trace), "--frames", "0", "--top", "0", "--json"]), 0) + result = json.loads(output.getvalue()) + self.assertEqual(result["slowest_frames"], []) + self.assertEqual(result["top_slices_ms_per_frame"], []) + self.assertEqual(result["longest_waits"], []) + + def test_command_line_markdown_and_json(self): output = io.StringIO() with contextlib.redirect_stdout(output): diff --git a/tools/trace_digest.py b/tools/trace_digest.py index 6644bdfbe2..0b6620b5cc 100644 --- a/tools/trace_digest.py +++ b/tools/trace_digest.py @@ -296,13 +296,20 @@ def markdown_intervals(result): return "\n".join(lines) + "\n" +def nonnegative_integer(value): + number = int(value) + if number < 0: + raise argparse.ArgumentTypeError("must be a nonnegative integer") + return number + + def main(arguments=None): parser = argparse.ArgumentParser(description=DESCRIPTION) parser.add_argument("trace", help="aps5-trace-*.json.gz (or .json), or a .jsonl summary file with APS5_TRACE_SUMMARY_EVERY lines") parser.add_argument("--summary", help="file with the summary line (default: aps5-summary.jsonl next to the trace)") parser.add_argument("--stacks", help="collapsed-stack file of APS5_SAMPLE_WORKER (.collapsed.txt)") - parser.add_argument("--frames", type=int, default=5, help="slowest frames to list (default 5)") - parser.add_argument("--top", type=int, default=10, help="entries per list (default 10)") + parser.add_argument("--frames", type=nonnegative_integer, default=5, help="slowest frames to list (default 5)") + parser.add_argument("--top", type=nonnegative_integer, default=10, help="entries per list (default 10)") parser.add_argument("--json", action="store_true", help="print the digest as JSON") options = parser.parse_args(arguments) if options.trace.endswith(".jsonl"): From 3929c9a49b0f76c6b401976952082066526e38b4 Mon Sep 17 00:00:00 2001 From: MrMha <114929652+MrMha@users.noreply.github.com> Date: Thu, 15 Oct 2026 18:59:10 +0400 Subject: [PATCH 0354/1000] fix: escape trace names in Markdown table cells --- tools/tests/test_trace_digest.py | 17 +++++++++++++++++ tools/trace_digest.py | 9 +++++++-- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/tools/tests/test_trace_digest.py b/tools/tests/test_trace_digest.py index d9dd888b29..df4ba548f0 100644 --- a/tools/tests/test_trace_digest.py +++ b/tools/tests/test_trace_digest.py @@ -176,6 +176,23 @@ class TraceDigestTests(unittest.TestCase): self.assertAlmostEqual(largest[("Driver.Present", "presenter")], 16.0) self.assertAlmostEqual(largest[("Driver.Present.inflight_wait", "presenter")], 14.5) + def test_markdown_thread_names_cannot_split_table_cells_or_rows(self): + name = "worker|\nrow & more" + phase = "phase|\r\nnext" + trace = trace_digest.Trace({"traceEvents": [ + {"ph": "M", "name": "thread_name", "tid": 1, "args": {"name": name}}, + complete(1, phase, 0, 1000), + ]}) + result = trace_digest.digest(trace) + self.assertEqual(result["threads"][0]["thread"], name) + self.assertEqual(result["threads"][0]["top"][0]["name"], phase) + rows = [line for line in trace_digest.markdown(result).splitlines() if line.startswith("|")] + self.assertEqual(len(rows), 3) + self.assertTrue(all(line.count("|") == 7 for line in rows), rows) + self.assertIn("worker|<gpu>
row & more", rows[2]) + self.assertIn("phase|<tag>
next", rows[2]) + + def test_threads_split_busy_and_wait(self): threads = {thread["thread"]: thread for thread in self.result()["threads"]} worker = threads["queue worker 0x0"] diff --git a/tools/trace_digest.py b/tools/trace_digest.py index 6644bdfbe2..6e2cf5c21c 100644 --- a/tools/trace_digest.py +++ b/tools/trace_digest.py @@ -2,6 +2,7 @@ import argparse import bisect import collections import gzip +import html import json import sys from pathlib import Path @@ -188,6 +189,10 @@ def digest(trace, summary=None, stacks=None, worst=5, top=10): return result +def table_cell(value): + return html.escape(str(value)).replace("|", "|").replace("\r\n", "\n").replace("\r", "\n").replace("\n", "
") + + def markdown(result): lines = [] summary = result["summary"] @@ -217,8 +222,8 @@ def markdown(result): lines.append("|---|---|---|---|---|---|") for thread in result["threads"]: largest = ", ".join(f"{entry['name']} {entry['ms_per_frame']:.1f}" for entry in thread["top"]) - lines.append(f"| {thread['thread']} | {thread['busy_ms_per_frame']:.1f} ({thread['busy_pct']:.0f} %) | {thread['wait_ms_per_frame']:.1f} ({thread['wait_pct']:.0f} %) " - f"| {thread['untraced_ms_per_frame']:.1f} | {thread['folded_ms_per_frame']:.1f} | {largest} |") + lines.append(f"| {table_cell(thread['thread'])} | {thread['busy_ms_per_frame']:.1f} ({thread['busy_pct']:.0f} %) | {thread['wait_ms_per_frame']:.1f} ({thread['wait_pct']:.0f} %) " + f"| {thread['untraced_ms_per_frame']:.1f} | {thread['folded_ms_per_frame']:.1f} | {table_cell(largest)} |") lines.append("") lines.append("**Largest slices** (ms per frame, inclusive, all threads): " + ", ".join(f"{entry['name']} {entry['ms']:.1f}" for entry in result["top_slices_ms_per_frame"])) lines.append("") From 8f894967660870e26c94a92c745a19c5da4e6e0e Mon Sep 17 00:00:00 2001 From: MrMha <114929652+MrMha@users.noreply.github.com> Date: Thu, 15 Oct 2026 18:59:32 +0400 Subject: [PATCH 0355/1000] fix: preserve strong references when grouping duplicate imports --- tools/import_audit.py | 2 +- tools/tests/test_import_audit.py | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/tools/import_audit.py b/tools/import_audit.py index 037dba8fe3..c7abfcf11b 100644 --- a/tools/import_audit.py +++ b/tools/import_audit.py @@ -451,7 +451,7 @@ def audit(imports, exports, stubs, libraries, modules, module_imports=None, modu for nid, library, weak in imports: record = records.setdefault((nid, library), {"nid": nid, "library": library, "references": 0, "weak": weak}) record["references"] += 1 - record["weak"] = weak + record["weak"] = record["weak"] and weak for module, (symbols, needed) in (module_imports or {}).items(): for nid in symbols: record = records.setdefault((nid, None, module), {"nid": nid, "library": None, "module": module, "references": 0, "weak": False}) diff --git a/tools/tests/test_import_audit.py b/tools/tests/test_import_audit.py index 9a0359e654..2399410b53 100644 --- a/tools/tests/test_import_audit.py +++ b/tools/tests/test_import_audit.py @@ -739,6 +739,22 @@ class ImportAuditTests(unittest.TestCase): self.assertEqual(data["stops"], [{"registry": str(registry), "nid": None, "library": None, "name": None}]) self.assertIn(f" {registry}: none", out) + def test_duplicate_import_strength_is_order_independent(self): + for strengths in ((False, True), (True, False), (False, False), (True, True)): + with self.subTest(strengths=strengths): + imports = [("AAAAAAAAAAA", "libSceA.prx", weak) for weak in strengths] + records, _ = import_audit.audit(imports, {}, {}, set(), set()) + self.assertEqual(len(records), 1) + self.assertEqual(records[0]["references"], 2) + self.assertEqual(records[0]["weak"], all(strengths)) + stop = import_audit.current_stop(imports, records) + if all(strengths): + self.assertIsNone(stop) + else: + self.assertIs(stop, records[0]) + self.assertFalse(stop["weak"]) + + def test_a_weak_unresolved_import_is_not_a_stop(self): work = self.work (work.libs / "libSceA.prx").write_bytes(exporting("AAAAAAAAAAA")) From 831c5830037fe55ef7af76f8450d1c72f06a9a6b Mon Sep 17 00:00:00 2001 From: Rick <99503318+0xRickx@users.noreply.github.com> Date: Tue, 13 Oct 2026 14:43:39 +0200 Subject: [PATCH 0356/1000] fix(libkernel): refuse a closed loopback TCP port at once on Windows Windows answers a SYN to a closed port with a RST and then retransmits the SYN, so connect to a closed loopback port takes about 2 s before ECONNREFUSED; FreeBSD fails at once. PS5VR (PPSA99177) makes about 33 such connects to 127.0.0.1:17600 and reaches its home screen 45 s late. When no socket listens on the destination port (GetExtendedTcpTable), a loopback connect now sets SIO_TCP_INITIAL_RTO with no SYN retransmissions and fails at once. A port with a listener keeps the retransmissions: Windows also answers a SYN to a full accept backlog with a RST, and the retransmission connects once the server accepts, as the console's does after FreeBSD drops the SYN. guest_refused_connect requires ECONNREFUSED in under a second for a closed port through libkernel connect and sceNetConnect, and a second connect to a listener with backlog 1 to succeed once the server accepts the first, through both. Co-Authored-By: Claude Opus 5.5 (1M context) --- core/libs/prx/libSceNet/CMakeLists.txt | 10 ++ core/libs/prx/libSceNet/Export.cpp | 7 ++ core/libs/prx/libkernel/CMakeLists.txt | 2 + .../Socket/include/SocketConnect.hpp | 6 + .../libkernel/Socket/src/SocketConnect.cpp | 47 ++++++++ core/libs/prx/libkernel/Socket/src/Udp.cpp | 4 + core/libs/tests/GuestRefusedConnect.cpp | 108 ++++++++++++++++++ 7 files changed, 184 insertions(+) create mode 100644 core/libs/prx/libkernel/Socket/include/SocketConnect.hpp create mode 100644 core/libs/prx/libkernel/Socket/src/SocketConnect.cpp create mode 100644 core/libs/tests/GuestRefusedConnect.cpp diff --git a/core/libs/prx/libSceNet/CMakeLists.txt b/core/libs/prx/libSceNet/CMakeLists.txt index 98776d4b5e..4a1c9fdb5e 100644 --- a/core/libs/prx/libSceNet/CMakeLists.txt +++ b/core/libs/prx/libSceNet/CMakeLists.txt @@ -15,6 +15,16 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() endforeach() + add_test_executable(guest_refused_connect_tests ${LIBS_INCLUDE_DIR}/tests/GuestRefusedConnect.cpp) + target_include_directories(guest_refused_connect_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_refused_connect_tests PRIVATE libSceNet libkernel libc) + configure_windows_unwind(guest_refused_connect_tests) + add_test(NAME guest_refused_connect COMMAND guest_refused_connect_tests) + set_tests_properties(guest_refused_connect PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_refused_connect PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() if(WIN32) add_test_executable(guest_socket_host_write_tests ${LIBS_INCLUDE_DIR}/tests/GuestSocketHostWrite.cpp) target_include_directories(guest_socket_host_write_tests PRIVATE ${LIBS_INCLUDE_DIR}) diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index ac697a4120..284a9076f9 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -859,6 +859,10 @@ static int disconnect_datagram(const Sock& socket) { return 0; } +#ifdef _WIN32 +extern "C" void RefuseClosedLoopbackAtOnce_nid_no_patch(SOCKET socket, const sockaddr* address); +#endif + int APS5_VABI sceNetConnect(int s, const void* addr, uint32_t addrlen) { Sock socket; { @@ -875,6 +879,9 @@ int APS5_VABI sceNetConnect(int s, const void* addr, uint32_t addrlen) { sockaddr_storage native{}; NativeLength native_length = 0; if (!guest_to_native_address(addr, addrlen, native, native_length)) return fail(NET_EINVAL); +#ifdef _WIN32 + if (socket.type == NET_SOCK_STREAM) RefuseClosedLoopbackAtOnce_nid_no_patch(socket.native->value, reinterpret_cast(&native)); +#endif if (native.ss_family != (socket.family == NET_AF_INET ? AF_INET : AF_INET6)) return fail(NET_EAFNOSUPPORT); if (::connect(socket.native->value, reinterpret_cast(&native), native_length) == 0) return 0; #ifdef _WIN32 diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index ef8dcfb233..798334ca3e 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -47,6 +47,7 @@ add_library(libkernel SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/Socket/src/Udp.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Socket/src/SocketOptions.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Socket/src/AddressConversion.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/Socket/src/SocketConnect.cpp ${CMAKE_CURRENT_SOURCE_DIR}/EventFlag/src/EventFlag.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Module/src/Module.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Module/src/DynamicLoader.cpp @@ -81,6 +82,7 @@ target_link_libraries(libkernel PRIVATE ${CMAKE_DL_LIBS}) target_link_libraries(libkernel PRIVATE libc) if(WIN32) target_link_libraries(libkernel PRIVATE ws2_32 shell32 advapi32 wbemuuid ole32 oleaut32) + target_link_libraries(libkernel PRIVATE iphlpapi) target_compile_definitions(libkernel PRIVATE _WIN32_WINNT=0x0602) endif() configure_windows_unwind(libkernel) diff --git a/core/libs/prx/libkernel/Socket/include/SocketConnect.hpp b/core/libs/prx/libkernel/Socket/include/SocketConnect.hpp new file mode 100644 index 0000000000..51ebf73520 --- /dev/null +++ b/core/libs/prx/libkernel/Socket/include/SocketConnect.hpp @@ -0,0 +1,6 @@ +#pragma once +#ifdef _WIN32 +#include + +extern "C" void RefuseClosedLoopbackAtOnce_nid_no_patch(SOCKET socket, const sockaddr* address); +#endif diff --git a/core/libs/prx/libkernel/Socket/src/SocketConnect.cpp b/core/libs/prx/libkernel/Socket/src/SocketConnect.cpp new file mode 100644 index 0000000000..245fd5e527 --- /dev/null +++ b/core/libs/prx/libkernel/Socket/src/SocketConnect.cpp @@ -0,0 +1,47 @@ +#ifdef _WIN32 +#include "prx/libkernel/Socket/include/SocketConnect.hpp" +#include +#include +#include +#include + +namespace { + +bool IsLoopback(const sockaddr* address) { + if (address->sa_family == AF_INET) return (ntohl(reinterpret_cast(address)->sin_addr.s_addr) >> 24) == 127; + if (address->sa_family == AF_INET6) return IN6_IS_ADDR_LOOPBACK(&reinterpret_cast(address)->sin6_addr); + return false; +} + +u_short Port(const sockaddr* address) { + return address->sa_family == AF_INET ? reinterpret_cast(address)->sin_port : reinterpret_cast(address)->sin6_port; +} + +template +bool ListenerOnPort(ULONG family, u_short port) { + std::vector table; + ULONG size = 0; + DWORD result = ERROR_INSUFFICIENT_BUFFER; + while (result == ERROR_INSUFFICIENT_BUFFER) { + table.resize(size); + result = GetExtendedTcpTable(table.data(), &size, FALSE, family, TCP_TABLE_OWNER_PID_LISTENER, 0); + } + if (result != NO_ERROR) return true; + const auto* rows = reinterpret_cast(table.data()); + for (DWORD i = 0; i < rows->dwNumEntries; ++i) { + if (static_cast(rows->table[i].dwLocalPort) == port) return true; + } + return false; +} + +} + +extern "C" void RefuseClosedLoopbackAtOnce_nid_no_patch(SOCKET socket, const sockaddr* address) { + if (!IsLoopback(address)) return; + const u_short port = Port(address); + if (ListenerOnPort(AF_INET, port) || ListenerOnPort(AF_INET6, port)) return; + TCP_INITIAL_RTO_PARAMETERS parameters{TCP_INITIAL_RTO_UNSPECIFIED_RTT, TCP_INITIAL_RTO_NO_SYN_RETRANSMISSIONS}; + DWORD returned = 0; + WSAIoctl(socket, SIO_TCP_INITIAL_RTO, ¶meters, sizeof(parameters), nullptr, 0, &returned, nullptr, nullptr); +} +#endif diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index 7588023bb7..f45edc6aa5 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -28,6 +28,7 @@ #include "prx/libkernel/File/include/File.hpp" #include "prx/libkernel/Time/include/TimedWait.hpp" #include "prx/libkernel/Socket/include/SocketPoll.hpp" +#include "prx/libkernel/Socket/include/SocketConnect.hpp" #include #include #include @@ -956,6 +957,9 @@ int APS5_VABI connect_nid_postfix(int descriptor, const void* address, std::uint sockaddr_storage native{}; socklen_t size; if (!Address(address, length, native, size)) return -1; +#ifdef _WIN32 + if (socket->type == 1) RefuseClosedLoopbackAtOnce_nid_no_patch(socket->value, reinterpret_cast(&native)); +#endif if (native.ss_family != (socket->family == 2 ? AF_INET : AF_INET6)) return Fail(47); { std::lock_guard lock(socket->modeMutex); diff --git a/core/libs/tests/GuestRefusedConnect.cpp b/core/libs/tests/GuestRefusedConnect.cpp new file mode 100644 index 0000000000..409f8ad97c --- /dev/null +++ b/core/libs/tests/GuestRefusedConnect.cpp @@ -0,0 +1,108 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI socket_nid_postfix(int, int, int); +int APS5_VABI bind_nid_postfix(int, const void*, std::uint32_t); +int APS5_VABI connect_nid_postfix(int, const void*, std::uint32_t); +int APS5_VABI listen_nid_postfix(int, int); +int APS5_VABI accept_nid_postfix(int, void*, std::uint32_t*); +int APS5_VABI getsockname_nid_postfix(int, void*, std::uint32_t*); +int APS5_VABI close_nid_postfix(int); +int* APS5_VABI __error_nid_postfix(); +int APS5_VABI sceNetInit_nid_postfix(); +int APS5_VABI sceNetSocket(const char*, int, int, int); +int APS5_VABI sceNetConnect(int, const void*, std::uint32_t); +int APS5_VABI sceNetSocketClose(int); +int* APS5_VABI sceNetErrnoLoc(); +} + +namespace { + +constexpr int GuestInet = 2; +constexpr int GuestStream = 1; +constexpr int GuestConnectionRefused = 61; +constexpr auto RefusalLimit = std::chrono::milliseconds(1000); + +void Require(bool value) { if (!value) std::abort(); } + +std::array ClosedLoopbackPort() { + std::array address{16, 2, 0, 0, 127, 0, 0, 1}; + const int descriptor = socket_nid_postfix(GuestInet, GuestStream, 0); + Require(descriptor >= 0); + Require(bind_nid_postfix(descriptor, address.data(), address.size()) == 0); + std::uint32_t length = address.size(); + Require(getsockname_nid_postfix(descriptor, address.data(), &length) == 0); + Require(close_nid_postfix(descriptor) == 0); + Require(address[2] != 0 || address[3] != 0); + return address; +} + +template +bool ConnectsOnceBacklogDrains(TConnect connect) { + std::array address{16, 2, 0, 0, 127, 0, 0, 1}; + const int listener = socket_nid_postfix(GuestInet, GuestStream, 0); + Require(listener >= 0); + Require(bind_nid_postfix(listener, address.data(), address.size()) == 0); + std::uint32_t length = address.size(); + Require(getsockname_nid_postfix(listener, address.data(), &length) == 0); + Require(listen_nid_postfix(listener, 1) == 0); + const int queued = socket_nid_postfix(GuestInet, GuestStream, 0); + Require(queued >= 0 && connect_nid_postfix(queued, address.data(), address.size()) == 0); + std::thread acceptor([listener] { + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + const int accepted = accept_nid_postfix(listener, nullptr, nullptr); + Require(accepted >= 0 && close_nid_postfix(accepted) == 0); + }); + const bool connected = connect(address); + acceptor.join(); + Require(close_nid_postfix(queued) == 0 && close_nid_postfix(listener) == 0); + return connected; +} + +template +bool RefusedAtOnce(TConnect connect) { + const auto start = std::chrono::steady_clock::now(); + const bool refused = connect(); + return refused && std::chrono::steady_clock::now() - start < RefusalLimit; +} + +} + +int main() { + Require(sceNetInit_nid_postfix() == 0); + const auto address = ClosedLoopbackPort(); + Require(RefusedAtOnce([&] { + const int descriptor = socket_nid_postfix(GuestInet, GuestStream, 0); + Require(descriptor >= 0); + const bool refused = connect_nid_postfix(descriptor, address.data(), address.size()) == -1 && *__error_nid_postfix() == GuestConnectionRefused; + Require(close_nid_postfix(descriptor) == 0); + return refused; + })); + Require(RefusedAtOnce([&] { + const int descriptor = sceNetSocket("refused", GuestInet, GuestStream, 0); + Require(descriptor >= 0); + const bool refused = sceNetConnect(descriptor, address.data(), address.size()) == static_cast(0x80410100u | GuestConnectionRefused) + && *sceNetErrnoLoc() == GuestConnectionRefused; + Require(sceNetSocketClose(descriptor) == 0); + return refused; + })); + Require(ConnectsOnceBacklogDrains([](const std::array& listening) { + const int descriptor = socket_nid_postfix(GuestInet, GuestStream, 0); + Require(descriptor >= 0); + const bool connected = connect_nid_postfix(descriptor, listening.data(), listening.size()) == 0; + Require(close_nid_postfix(descriptor) == 0); + return connected; + })); + Require(ConnectsOnceBacklogDrains([](const std::array& listening) { + const int descriptor = sceNetSocket("backlog", GuestInet, GuestStream, 0); + Require(descriptor >= 0); + const bool connected = sceNetConnect(descriptor, listening.data(), listening.size()) == 0; + Require(sceNetSocketClose(descriptor) == 0); + return connected; + })); +} From 451f11eafe5eae2bfcd9ab582499ab2ced6bda3d Mon Sep 17 00:00:00 2001 From: fma-rc <188743521+fma-rc@users.noreply.github.com> Date: Thu, 15 Oct 2026 08:45:47 +0200 Subject: [PATCH 0357/1000] fix(libkernel): report a 256 GiB AMM range starting on a 64 GiB boundary --- core/libs/prx/libkernel/Apr/src/Apr.cpp | 16 +++++++++------- core/libs/tests/GuestAmmPlacement.cpp | 5 +++-- docs/dev/debt/libkernel.md | 2 +- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/core/libs/prx/libkernel/Apr/src/Apr.cpp b/core/libs/prx/libkernel/Apr/src/Apr.cpp index 22e9358fc2..7208a99967 100644 --- a/core/libs/prx/libkernel/Apr/src/Apr.cpp +++ b/core/libs/prx/libkernel/Apr/src/Apr.cpp @@ -260,8 +260,10 @@ TCommand _read(const Apr::CommandBufferObject& buffer, std::uint32_t cursor) { return command; } -constexpr std::uint64_t AmmRangeBytes = 32ull << 30; -constexpr std::uint64_t AmmRangeAlignment = 2ull << 20; +constexpr std::uint64_t AmmRangeBytes = 256ull << 30; +constexpr std::uint64_t AmmMultimapRangeBytes = 32ull << 30; +constexpr std::uint64_t AmmReservedBytes = AmmRangeBytes + AmmMultimapRangeBytes; +constexpr std::uint64_t AmmRangeAlignment = 64ull << 30; constexpr int GuestMapFixed = 0x10; constexpr int GuestMapNoOverwrite = 0x80; @@ -292,11 +294,11 @@ std::uintptr_t _ammBase(AmmState& state) { std::uintptr_t arenaBase = 0; std::size_t arenaBytes = 0; GuestArena::GuestArenaRange_nid_postfix(&arenaBase, &arenaBytes); - if (arenaBase == 0 || arenaBytes < 2 * AmmRangeBytes) throw std::runtime_error("AMM: the guest address space arena cannot hold the virtual address range"); - const auto top = GuestArena::GuestArenaHighestFree_nid_no_patch(2 * AmmRangeBytes, AmmRangeAlignment); + if (arenaBase == 0 || arenaBytes < AmmReservedBytes) throw std::runtime_error("AMM: the guest address space arena cannot hold the virtual address range"); + const auto top = GuestArena::GuestArenaHighestFree_nid_no_patch(AmmReservedBytes, AmmRangeAlignment); if (top == 0) throw std::runtime_error("AMM: the guest address space arena has no free virtual address range"); void* address = reinterpret_cast(top); - if (DoReserveVirtual(&address, 2 * AmmRangeBytes, GuestMapFixed | GuestMapNoOverwrite, 0) != 0) throw std::runtime_error("AMM: cannot reserve the virtual address range"); + if (DoReserveVirtual(&address, AmmReservedBytes, GuestMapFixed | GuestMapNoOverwrite, 0) != 0) throw std::runtime_error("AMM: cannot reserve the virtual address range"); state.base = reinterpret_cast(address); } return state.base; @@ -304,7 +306,7 @@ std::uintptr_t _ammBase(AmmState& state) { void _ammCheckRange(AmmState& state, std::uint64_t address, std::uint64_t size) { const auto base = _ammBase(state); - if (address < base || size > 2 * AmmRangeBytes || address - base > 2 * AmmRangeBytes - size) throw std::runtime_error("AMM: range outside the AMM virtual address range"); + if (address < base || size > AmmReservedBytes || address - base > AmmReservedBytes - size) throw std::runtime_error("AMM: range outside the AMM virtual address range"); } int _ammHostProtection(std::int32_t protection) { @@ -704,7 +706,7 @@ void AmmVirtualAddressRanges_nid_no_patch(std::uint64_t* start, std::uint64_t* e if (start) *start = base; if (end) *end = base + AmmRangeBytes; if (multimapStart) *multimapStart = base + AmmRangeBytes; - if (multimapEnd) *multimapEnd = base + 2 * AmmRangeBytes; + if (multimapEnd) *multimapEnd = base + AmmReservedBytes; } struct AprQueue { diff --git a/core/libs/tests/GuestAmmPlacement.cpp b/core/libs/tests/GuestAmmPlacement.cpp index e52fe22b59..db65b897bf 100644 --- a/core/libs/tests/GuestAmmPlacement.cpp +++ b/core/libs/tests/GuestAmmPlacement.cpp @@ -28,9 +28,10 @@ int main() { std::uint64_t multimapStart = 0; std::uint64_t multimapEnd = 0; Require(sceAmprAmmGetVirtualAddressRanges(&start, &end, &multimapStart, &multimapEnd) == 0, "AMM ranges were not reported"); - Require(end == multimapStart && multimapEnd - start == 64ull << 30, "AMM ranges are not two adjacent 32 GiB ranges"); + Require(end - start == 256ull << 30 && end == multimapStart && multimapEnd - multimapStart == 32ull << 30, "AMM ranges are not a 256 GiB range and an adjacent 32 GiB multimap range"); Require(start >= arenaBase && multimapEnd <= arenaEnd - occupied, "AMM ranges overlap the used top of the guest arena"); - Require(GuestArena::GuestArenaHighestFree_nid_no_patch(64ull << 30, 2ull << 20) < start, "AMM ranges are not at the top of the free guest arena"); + Require(start % (64ull << 30) == 0, "the AMM range does not start on a 64 GiB boundary"); + Require(GuestArena::GuestArenaHighestFree_nid_no_patch(multimapEnd - start, 64ull << 30) < start, "AMM ranges are not at the top of the free guest arena"); std::puts("AMM ranges placed below the used top of the guest arena"); return 0; } catch (const std::exception& error) { diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 58a0a37e9d..9183fb0717 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -30,7 +30,7 @@ - [APR reads past the end of a file](../../../core/libs/prx/libkernel/Apr/src/Apr.cpp) (libkernel) - a read that reaches the current end of the file on disk copies the bytes the file has and fills the rest of the destination with zeros; a read that comes back short before that end still throws. PPSA18605 resolves its settings file once, rewrites it one byte shorter and reads it again with the size that resolve returned. The console's behaviour for a read past the end of a file is unverified: it may zero-fill, leave the destination untouched or fail the command - [AMPR counter access](../../../core/libs/prx/libkernel/Apr/src/Apr.cpp) (libSceAmpr) - signatures and checks of `WaitOnCounter_04_00`, `WriteCounter_04_00`, `ConstructNop` and `ConstructMarker` taken from the [ampr_emu](https://github.com/drakmor/ampr_emu) reimplementation, which hands counter commands to the console and does not show their effect. The counters are taken as 32 bits each: the 8-byte access of counter n spans n (low) and n + 1 (high), the 2- and 1-byte accesses select a half or a byte of the counter by its offset, waits compare at the access width (signed and wrapped compares included), the AND mask applies to both sides and the atomic writes wrap within the access. The `ConstructNop` type is not recorded - [APR gather and scatter reads](../../../core/libs/prx/libkernel/Apr/src/Apr.cpp) (libSceAmpr) - signatures, argument checks and the read cursor taken from the [ampr_emu](https://github.com/drakmor/ampr_emu) reimplementation, not confirmed on a title: each read leaves the file, the next file offset and the next destination; a gather reads that file at a new offset into the next destination, a scatter continues in the file into a new destination, a gather-scatter takes both. Recording one needs a read file since the last `ResetGatherScatterState`; like ampr_emu, `sceAmprCommandBufferReset` keeps that state, and a gather or scatter with no read before it in the submitted buffer throws instead of returning `SCE_AMPR_ERROR_APR_INVALIDGATHERSCATTERSTATE` -- [AMM map, map direct and unmap](../../../core/libs/prx/libkernel/Apr/src/Apr.cpp) (libSceAmpr) - signatures and the protection mask (CPU, GPU, AMPR and ACP read and write) taken from the [ampr_emu](https://github.com/drakmor/ampr_emu) reimplementation, which hands the remaining checks and the mapping to the console kernel; the mapper model taken from [PS5PCEM](https://github.com/iStark/PS5PCEM): `sceAmprAmmGiveDirectMemory` usage 1 gives the allocated direct memory to the pool plain maps take pages from, usage 0 only allocates it. Not confirmed on a title. The AMM virtual address ranges are a 32 GiB range and a 32 GiB multimap range reserved on first use at the top of the guest arena, above the addresses that maps without `MAP_FIXED` are given lowest first and that titles derive their next hints from; the console's addresses are not known (ampr_emu takes them from `sceKernelMapperGetParam`), and a reservation that cannot be made there throws. Maps outside them throw. Addresses, sizes and direct offsets must be 16 KiB aligned. The memory type and the GPU mask are ignored, AMPR and ACP access maps as CPU access, unmapped pages go back to the pool and stay reserved. Submission runs the buffer before returning, the submit result is always 0 and a failing command throws; the AMM measures return errors sign-extended, as ampr_emu does. A map returns its target to the reservation before releasing the old pages, so pool exhaustion leaves that target reserved; mapping failure also restores the reservation before returning newly taken pages to the pool. The console's map failure and rollback behavior is unverified. `sceAmprAmmGetVirtualAddressRanges` throws for a null output +- [AMM map, map direct and unmap](../../../core/libs/prx/libkernel/Apr/src/Apr.cpp) (libSceAmpr) - signatures and the protection mask (CPU, GPU, AMPR and ACP read and write) taken from the [ampr_emu](https://github.com/drakmor/ampr_emu) reimplementation, which hands the remaining checks and the mapping to the console kernel; the mapper model taken from [PS5PCEM](https://github.com/iStark/PS5PCEM): `sceAmprAmmGiveDirectMemory` usage 1 gives the allocated direct memory to the pool plain maps take pages from, usage 0 only allocates it. Not confirmed on a title. The AMM virtual address ranges are a 256 GiB range (PPSA02015 places its heaps 64 GiB above the start of the range, so 32 GiB is too small, and reached 84 GiB above it in its first 159 s; KytyPS5 reports 512 GiB, but every 64 GiB-aligned 544 GiB window in the guest arena covers 448 to 608 GiB, and on Windows one host thread stack there is enough to block it) and a 32 GiB multimap range reserved on first use at the top of the guest arena with the start on a 64 GiB boundary (PPSA02015 tells its pool pointers from its heap pointers by address bits 28 to 35, which needs such a start for its layout), above the addresses that maps without `MAP_FIXED` are given lowest first and that titles derive their next hints from; the console's addresses are not known (ampr_emu takes them from `sceKernelMapperGetParam`), and a reservation that cannot be made there throws. Maps outside them throw. Addresses, sizes and direct offsets must be 16 KiB aligned. The memory type and the GPU mask are ignored, AMPR and ACP access maps as CPU access, unmapped pages go back to the pool and stay reserved. Submission runs the buffer before returning, the submit result is always 0 and a failing command throws; the AMM measures return errors sign-extended, as ampr_emu does. A map returns its target to the reservation before releasing the old pages, so pool exhaustion leaves that target reserved; mapping failure also restores the reservation before returning newly taken pages to the pool. The console's map failure and rollback behavior is unverified. `sceAmprAmmGetVirtualAddressRanges` throws for a null output - [AMM remap, multimap and protection changes](../../../core/libs/prx/libkernel/Apr/src/Apr.cpp) (libSceAmpr) - signatures and the protection and mask checks taken from ampr_emu; the effects are inferred from the names, not confirmed on a title. A remap moves every page of a fully mapped range to the new address with the given protection and leaves the old range reserved; a multimap maps the same pages at a second address, anywhere in the AMM ranges, and a pool page goes back to the pool once its last mapping is unmapped; a multimap onto its own source range throws. A protection change sets the masked bits on every page of a fully mapped range; the memory type is recorded and ignored. A command on a range that is not fully mapped throws - [AMM PRT ranges](../../../core/libs/prx/libkernel/Apr/src/Apr.cpp) (libSceAmpr) - signatures, checks and their order taken from ampr_emu, where `MapAsPrt` is a map with the PRT flag and protection 0 and `AllocatePaForPrt` a memory type and protection change with mask 1019, both checking the buffer memory after its size; the effects are inferred from the names, not confirmed on a title. Unbacked PRT pages are zero-filled pages with GPU read access, so CPU reads see zeros instead of faulting and GPU writes fault instead of being dropped. `AllocatePaForPrt` backs each unbacked page with a pool page and sets the type and protection of backed ones; `RemapIntoPrt` moves a fully mapped range into a PRT range, its opcode argument (1011 when 0, as in ampr_emu) is recorded and ignored; `UnmapToPrt` returns the pages to the pool and zero-fills them again. A plain map, unmap or remap over a PRT range ends it; `AllocatePaForPrt`, `RemapIntoPrt` or `UnmapToPrt` outside a PRT range throws - [AMM kernel write commands](../../../core/libs/prx/libkernel/Apr/src/Apr.cpp) (libkernel) - signatures of `sceKernelWriteModifyMtypeProtectCommand` and `sceKernelWriteRemapIntoPrtCommand` taken from [ampr_emu's libkernel hook targets](https://github.com/drakmor/ampr_emu/blob/cfa85df379f6eeeb165d7badf9b648e266fe77b7/include/ampr_libkernel_hook_targets.h), which forward to the real kernel; the measure mode (`dst` null reports the size, `outSize` null skips the report) follows ampr_emu's measure-then-record calls, not confirmed on a title. The range, protection and mask checks mirror the `libSceAmpr` siblings, and the recorded bytes match them exactly so the submitted buffer executes on the existing paths From c28e0ac37020317f918e1edbf8ea6bde3b28319a Mon Sep 17 00:00:00 2001 From: Yossi Yadgar Date: Wed, 14 Oct 2026 10:16:00 +0300 Subject: [PATCH 0358/1000] fix(libSceSystemService): give LoadExec's next executable the host standard error A guest dup2 onto descriptor 2 keeps a close-on-exec duplicate of the host's standard error (#4792), so the runtime's own output stays on the console while the title's stderr goes where the title pointed it. The duplicate did not survive LoadExec's execv, and the next executable started with descriptor 2 still on the title's redirect: its runtime output, terminate report included, went there. RetroArch (PPSA99169) points stderr at app0/trace.txt and then starts its picker. The picker's [gpu] and sceKernelDebugOutText lines landed in trace.txt, and its abort on an unconverted es-de.bin showed up nowhere; the message was found only in the core dump. Before execv, the kept descriptor now goes back onto descriptor 2. --- core/libs/prx/libSceSystemService/Export.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/core/libs/prx/libSceSystemService/Export.cpp b/core/libs/prx/libSceSystemService/Export.cpp index 6b5bb3bb44..19bb3e3048 100644 --- a/core/libs/prx/libSceSystemService/Export.cpp +++ b/core/libs/prx/libSceSystemService/Export.cpp @@ -183,6 +183,9 @@ void StartHostExecutable(HostLaunch& launch) { std::vector argv; for (auto& argument : launch.arguments) argv.push_back(argument.data()); argv.push_back(nullptr); + if (const int host = HostStandardError_nid_no_patch(); host >= 0 && ::dup2(host, 2) != 2) { + throw std::runtime_error("sceSystemServiceLoadExec: cannot give the host standard error back to descriptor 2: " + std::string(std::strerror(errno))); + } ::execv(launch.executable.c_str(), argv.data()); throw std::runtime_error("sceSystemServiceLoadExec: execv failed: " + std::string(std::strerror(errno))); } From 67f08d9f279b8c944e97354b3ced824e0f52d6c4 Mon Sep 17 00:00:00 2001 From: Yossi Yadgar Date: Wed, 14 Oct 2026 10:16:00 +0300 Subject: [PATCH 0359/1000] test(libSceSystemService): check stderr after LoadExec of a redirected title A forked child with stderr on a pipe moves the guest's descriptor 2 to a second pipe with dup2, then starts itself again through sceSystemServiceLoadExec("/app0/eboot.bin"). What the new image writes to stderr must reach the first pipe and nothing the second. It fails with the previous execv, where the text goes to the guest's pipe. --- .../prx/libSceSystemService/CMakeLists.txt | 7 +++ ...uestSystemServiceLoadExecStandardError.cpp | 57 +++++++++++++++++++ 2 files changed, 64 insertions(+) create mode 100644 core/libs/tests/GuestSystemServiceLoadExecStandardError.cpp diff --git a/core/libs/prx/libSceSystemService/CMakeLists.txt b/core/libs/prx/libSceSystemService/CMakeLists.txt index d7ff11174c..66e1725b37 100644 --- a/core/libs/prx/libSceSystemService/CMakeLists.txt +++ b/core/libs/prx/libSceSystemService/CMakeLists.txt @@ -48,6 +48,13 @@ if(BUILD_TESTING) PASS_REGULAR_EXPRESSION "Guest relaunch completed" TIMEOUT 30) set_tests_properties(guest_nested_exit PROPERTIES PASS_REGULAR_EXPRESSION "Guest nested exit completed" TIMEOUT 30) + if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + add_test_executable(guest_system_load_exec_standard_error_tests ${LIBS_INCLUDE_DIR}/tests/GuestSystemServiceLoadExecStandardError.cpp) + target_include_directories(guest_system_load_exec_standard_error_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_system_load_exec_standard_error_tests PRIVATE libSceSystemService libkernel libc) + add_test(NAME guest_system_load_exec_standard_error COMMAND guest_system_load_exec_standard_error_tests) + set_tests_properties(guest_system_load_exec_standard_error PROPERTIES TIMEOUT 20) + endif() if(WIN32) set_tests_properties(guest_system_exit guest_system_exit_parameters guest_immediate_exit guest_return_from_main guest_quick_exit guest_system_relaunch guest_nested_exit PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") diff --git a/core/libs/tests/GuestSystemServiceLoadExecStandardError.cpp b/core/libs/tests/GuestSystemServiceLoadExecStandardError.cpp new file mode 100644 index 0000000000..5992a2f94d --- /dev/null +++ b/core/libs/tests/GuestSystemServiceLoadExecStandardError.cpp @@ -0,0 +1,57 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceSystemServiceLoadExec(const char*, const char* const*); +int APS5_VABI dup2_nid_postfix(int, int); +} + +static void Require(bool value) { if (!value) std::abort(); } + +static std::string Drain(int descriptor) { + std::string text; + char buffer[4096]; + for (ssize_t count; (count = ::read(descriptor, buffer, sizeof(buffer))) > 0;) text.append(buffer, static_cast(count)); + ::close(descriptor); + return text; +} + +int main(int argc, char** argv) { + if (argc == 2 && std::strcmp(argv[1], "--after-load-exec") == 0) { + std::fputs("started\n", stderr); + return 0; + } + Require(argc == 1); + int host[2] = {-1, -1}; + int guest[2] = {-1, -1}; + Require(::pipe(host) == 0); + Require(::pipe(guest) == 0); + const pid_t child = ::fork(); + Require(child >= 0); + if (child == 0) { + ::close(host[0]); + ::close(guest[0]); + if (::dup2(host[1], 2) != 2) ::_exit(70); + ::close(host[1]); + if (dup2_nid_postfix(guest[1], 2) != 2) ::_exit(71); + ::close(guest[1]); + const char* const arguments[] = {"--after-load-exec", nullptr}; + sceSystemServiceLoadExec("/app0/eboot.bin", arguments); + ::_exit(72); + } + ::close(host[1]); + ::close(guest[1]); + const std::string hostText = Drain(host[0]); + const std::string guestText = Drain(guest[0]); + int status = 0; + Require(::waitpid(child, &status, 0) == child); + Require(WIFEXITED(status) && WEXITSTATUS(status) == 0); + Require(hostText == "started\n"); + Require(guestText.empty()); + return 0; +} From 0d9af44626c9c7ae7285721bf1af47fec7f7315b Mon Sep 17 00:00:00 2001 From: Yossi Yadgar Date: Wed, 14 Oct 2026 10:16:00 +0300 Subject: [PATCH 0360/1000] docs(debt): note which descriptors LoadExec passes on Linux --- docs/dev/debt/libSceSystemService.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/dev/debt/libSceSystemService.md b/docs/dev/debt/libSceSystemService.md index 3e5bcecac2..550ae166ac 100644 --- a/docs/dev/debt/libSceSystemService.md +++ b/docs/dev/debt/libSceSystemService.md @@ -18,3 +18,4 @@ - [sceSystemServiceLoadExec](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - besides `"exit"`, it starts the title's own `/app0/eboot.bin` or another executable under `/app0` that the relinker converted with `--executable`, which is looked up beside the running one (`/app0/picker/picker.bin` as `picker.picker.bin` with the running executable's extension). The main executable is the one `APS5_EBOOT` names, which the first call sets to its own path. After the guest shutdown the old process ends without running guest exit handlers or host destructors. `"exit"` ends the process after the host shutdown without running the title's `atexit` and `__cxa_atexit` handlers, since the console's shell closes the title instead; a line the title writes just before through its own thread, such as a forwarded standard error line, may not be printed. [VLC for PS5](https://github.com/ZiZc3/VLC/blob/86bb7d474a303f62bb3c2fc12f728db4846085b8/src/platform_ps5.c#L565-L571) (PPSA85300) relaunches itself with no arguments; RetroArch (PPSA99169) starts its frontend picker with `{"", NULL}`, and the picker starts `eboot.bin` with `{"--ps5-mode=retroarch", NULL}` or `/app0/es-de/es-de.bin` ([picker.cpp](https://github.com/mihawk-99/PS5_RetroArch/blob/bead80725cc11e3c5c65bd9424b0a4494ae36d45/frontends/picker/picker.cpp#L444-L446)), so the list holds only arguments. A list with a first entry becomes the whole guest `argv`, with no program name in front ([LoadExec.cpp](../../../core/libs/prx/libSceSystemService/LoadExec.cpp)): a [PS5 RetroArch console transcript](https://github.com/mihawk-99/PS5_RetroArch/blob/bead80725cc11e3c5c65bd9424b0a4494ae36d45/evidence/loadexec-second-image/relaunch-test.jsonl) records a restart with one argument entering with `argc=1`, `argv[0]="--ps5-relaunch=1"`, and a launch from the home screen with `argc=1`, `argv[0]=""` ([relaunch_ps5.cpp](https://github.com/mihawk-99/PS5_RetroArch/blob/bead80725cc11e3c5c65bd9424b0a4494ae36d45/src/relaunch_ps5.cpp#L161-L164)). For such a list Linux replaces the process with `fexecve`: the PID and the standard streams stay, the other descriptors are closed and the working directory becomes the executable's. Windows starts a suspended process that inherits only the standard streams and a handle of the old process, and its entry stub waits for the old process to end before it loads the title's libraries, so a shell or script that started the title sees it end. A null list or one without a first entry keeps the earlier launch: the new executable gets only its own path as its command line (`CreateProcessW` on Windows, `execv` on Linux), so its `argv[0]` is the host path, and the old process ends without waiting for it. What the console passes for such a list is not known, nor are its argument limits and error codes. Paths outside `/app0`, unconverted executables and macOS hosts throw - [Running application ID](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - `sceSystemServiceGetAppIdOfRunningBigApp` reports 1 as the running title's app ID. The value is AnyPS5's own: the console's shell assigns app IDs and their values are not known here, so other calls that take an app ID must use the same one. `sceSystemServiceKillApp` with that ID and the arguments -1, 0 and 0, which [SwanStationPS5](https://github.com/darkxex/SwanStationPS5/blob/4cdc981d1a64ffb7f2af73f7a27a9e91a3cb5eb4/src/main.c#L1050-L1058) passes to close itself and [ps5-payload-dev's hbldr](https://github.com/ps5-payload-dev/websrv/blob/46374edee6627885abac717de24da7a1d73af6f1/src/ps5/hbldr.c#L425-L435) passes to close the running game, ends the title as its own `exit(0)` would: the guest's shutdown and `atexit` handlers run and the process exits with status 0. On the console the call returns and the shell then ends the title, and whether those handlers run there is unknown; here there is no shell, so the call does not return. Another app ID or other arguments throw. +- [LoadExec descriptors](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - on Linux the guest's descriptors stay open across `sceSystemServiceLoadExec`'s `execv`, except that descriptor 2 first gets back the host standard error a guest redirect kept, so the runtime's output does not follow the title's redirect (RetroArch's trace file) into the next executable. Which descriptors the console passes to the new process is not known From 45cb1f91a074ea4dd08a7de8f7e16ffe60aef3aa Mon Sep 17 00:00:00 2001 From: Yossi Yadgar Date: Thu, 15 Oct 2026 18:17:21 +0300 Subject: [PATCH 0361/1000] fix(libSceSystemService): give the host standard error back before LoadExec's fexecve A LoadExec with an argument list replaces the process through Replace (fexecve), which runs before StartHostExecutable, so the next image still wrote to the title's redirect. Descriptor 2 now gets the kept host descriptor there too, after the shutdown and the flush and before close_range closes it. --- core/libs/prx/libSceSystemService/LoadExec.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/core/libs/prx/libSceSystemService/LoadExec.cpp b/core/libs/prx/libSceSystemService/LoadExec.cpp index cc359f4b53..360e818297 100644 --- a/core/libs/prx/libSceSystemService/LoadExec.cpp +++ b/core/libs/prx/libSceSystemService/LoadExec.cpp @@ -1,4 +1,5 @@ #include "LoadExec.hpp" +#include "prx/libc/include/General.hpp" #include "prx/libc/include/Shutdown.hpp" #include #include @@ -153,6 +154,8 @@ Handle Duplicate(HANDLE source, DWORD access) { pointers.push_back(nullptr); LibcRunShutdown_nid_postfix(); std::fflush(nullptr); + if (const int host = HostStandardError_nid_no_patch(); host >= 0 && dup2(host, 2) != 2) + throw std::system_error(errno, std::generic_category(), "LoadExec: giving the host standard error back to descriptor 2"); std::filesystem::current_path(target.parent_path()); if ((descriptor > 3 && close_range(3, static_cast(descriptor - 1), 0) != 0) || close_range(static_cast(std::max(3, descriptor + 1)), ~0u, 0) != 0) From c9ae2c4191c5c51610a937971942db7764d8e3ef Mon Sep 17 00:00:00 2001 From: Yossi Yadgar Date: Thu, 15 Oct 2026 18:17:21 +0300 Subject: [PATCH 0362/1000] test(libSceSystemService): check stderr after LoadExec on both exec paths The re-executed image is told apart by an environment variable. With an argument list (fexecve) it sees argc 1 and the marker as argv[0]; with a null list (execv) it sees only its path. --- ...uestSystemServiceLoadExecStandardError.cpp | 39 +++++++++++++------ 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/core/libs/tests/GuestSystemServiceLoadExecStandardError.cpp b/core/libs/tests/GuestSystemServiceLoadExecStandardError.cpp index 5992a2f94d..ec83ac1c10 100644 --- a/core/libs/tests/GuestSystemServiceLoadExecStandardError.cpp +++ b/core/libs/tests/GuestSystemServiceLoadExecStandardError.cpp @@ -11,9 +11,16 @@ int APS5_VABI sceSystemServiceLoadExec(const char*, const char* const*); int APS5_VABI dup2_nid_postfix(int, int); } -static void Require(bool value) { if (!value) std::abort(); } +namespace { -static std::string Drain(int descriptor) { +constexpr const char* Marker = "APS5_LOAD_EXEC_STANDARD_ERROR"; +constexpr const char* AfterLoadExec = "--after-load-exec"; + +void Require(bool value) { + if (!value) std::abort(); +} + +std::string Drain(int descriptor) { std::string text; char buffer[4096]; for (ssize_t count; (count = ::read(descriptor, buffer, sizeof(buffer))) > 0;) text.append(buffer, static_cast(count)); @@ -21,12 +28,7 @@ static std::string Drain(int descriptor) { return text; } -int main(int argc, char** argv) { - if (argc == 2 && std::strcmp(argv[1], "--after-load-exec") == 0) { - std::fputs("started\n", stderr); - return 0; - } - Require(argc == 1); +void Run(const char* mode, const char* const* arguments) { int host[2] = {-1, -1}; int guest[2] = {-1, -1}; Require(::pipe(host) == 0); @@ -40,9 +42,9 @@ int main(int argc, char** argv) { ::close(host[1]); if (dup2_nid_postfix(guest[1], 2) != 2) ::_exit(71); ::close(guest[1]); - const char* const arguments[] = {"--after-load-exec", nullptr}; + if (::setenv(Marker, mode, 1) != 0) ::_exit(72); sceSystemServiceLoadExec("/app0/eboot.bin", arguments); - ::_exit(72); + ::_exit(73); } ::close(host[1]); ::close(guest[1]); @@ -51,7 +53,22 @@ int main(int argc, char** argv) { int status = 0; Require(::waitpid(child, &status, 0) == child); Require(WIFEXITED(status) && WEXITSTATUS(status) == 0); - Require(hostText == "started\n"); + Require(hostText == std::string("started ") + mode + "\n"); Require(guestText.empty()); +} + +} + +int main(int argc, char** argv) { + if (const char* mode = std::getenv(Marker); mode != nullptr) { + if (std::strcmp(mode, "list") == 0) Require(argc == 1 && std::strcmp(argv[0], AfterLoadExec) == 0); + else Require(std::strcmp(mode, "none") == 0 && argc == 1); + std::fprintf(stderr, "started %s\n", mode); + return 0; + } + const char* const arguments[] = {AfterLoadExec, nullptr}; + Run("list", arguments); + Run("none", nullptr); + std::puts("LoadExec standard error tests passed"); return 0; } From 6473318a6bccb955b540e40021970631a45bc4b9 Mon Sep 17 00:00:00 2001 From: Yossi Yadgar Date: Thu, 15 Oct 2026 18:17:21 +0300 Subject: [PATCH 0363/1000] docs(debt): note which descriptors each LoadExec path passes on Linux --- docs/dev/debt/libSceSystemService.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceSystemService.md b/docs/dev/debt/libSceSystemService.md index 550ae166ac..6908a39b03 100644 --- a/docs/dev/debt/libSceSystemService.md +++ b/docs/dev/debt/libSceSystemService.md @@ -18,4 +18,4 @@ - [sceSystemServiceLoadExec](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - besides `"exit"`, it starts the title's own `/app0/eboot.bin` or another executable under `/app0` that the relinker converted with `--executable`, which is looked up beside the running one (`/app0/picker/picker.bin` as `picker.picker.bin` with the running executable's extension). The main executable is the one `APS5_EBOOT` names, which the first call sets to its own path. After the guest shutdown the old process ends without running guest exit handlers or host destructors. `"exit"` ends the process after the host shutdown without running the title's `atexit` and `__cxa_atexit` handlers, since the console's shell closes the title instead; a line the title writes just before through its own thread, such as a forwarded standard error line, may not be printed. [VLC for PS5](https://github.com/ZiZc3/VLC/blob/86bb7d474a303f62bb3c2fc12f728db4846085b8/src/platform_ps5.c#L565-L571) (PPSA85300) relaunches itself with no arguments; RetroArch (PPSA99169) starts its frontend picker with `{"", NULL}`, and the picker starts `eboot.bin` with `{"--ps5-mode=retroarch", NULL}` or `/app0/es-de/es-de.bin` ([picker.cpp](https://github.com/mihawk-99/PS5_RetroArch/blob/bead80725cc11e3c5c65bd9424b0a4494ae36d45/frontends/picker/picker.cpp#L444-L446)), so the list holds only arguments. A list with a first entry becomes the whole guest `argv`, with no program name in front ([LoadExec.cpp](../../../core/libs/prx/libSceSystemService/LoadExec.cpp)): a [PS5 RetroArch console transcript](https://github.com/mihawk-99/PS5_RetroArch/blob/bead80725cc11e3c5c65bd9424b0a4494ae36d45/evidence/loadexec-second-image/relaunch-test.jsonl) records a restart with one argument entering with `argc=1`, `argv[0]="--ps5-relaunch=1"`, and a launch from the home screen with `argc=1`, `argv[0]=""` ([relaunch_ps5.cpp](https://github.com/mihawk-99/PS5_RetroArch/blob/bead80725cc11e3c5c65bd9424b0a4494ae36d45/src/relaunch_ps5.cpp#L161-L164)). For such a list Linux replaces the process with `fexecve`: the PID and the standard streams stay, the other descriptors are closed and the working directory becomes the executable's. Windows starts a suspended process that inherits only the standard streams and a handle of the old process, and its entry stub waits for the old process to end before it loads the title's libraries, so a shell or script that started the title sees it end. A null list or one without a first entry keeps the earlier launch: the new executable gets only its own path as its command line (`CreateProcessW` on Windows, `execv` on Linux), so its `argv[0]` is the host path, and the old process ends without waiting for it. What the console passes for such a list is not known, nor are its argument limits and error codes. Paths outside `/app0`, unconverted executables and macOS hosts throw - [Running application ID](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - `sceSystemServiceGetAppIdOfRunningBigApp` reports 1 as the running title's app ID. The value is AnyPS5's own: the console's shell assigns app IDs and their values are not known here, so other calls that take an app ID must use the same one. `sceSystemServiceKillApp` with that ID and the arguments -1, 0 and 0, which [SwanStationPS5](https://github.com/darkxex/SwanStationPS5/blob/4cdc981d1a64ffb7f2af73f7a27a9e91a3cb5eb4/src/main.c#L1050-L1058) passes to close itself and [ps5-payload-dev's hbldr](https://github.com/ps5-payload-dev/websrv/blob/46374edee6627885abac717de24da7a1d73af6f1/src/ps5/hbldr.c#L425-L435) passes to close the running game, ends the title as its own `exit(0)` would: the guest's shutdown and `atexit` handlers run and the process exits with status 0. On the console the call returns and the shell then ends the title, and whether those handlers run there is unknown; here there is no shell, so the call does not return. Another app ID or other arguments throw. -- [LoadExec descriptors](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - on Linux the guest's descriptors stay open across `sceSystemServiceLoadExec`'s `execv`, except that descriptor 2 first gets back the host standard error a guest redirect kept, so the runtime's output does not follow the title's redirect (RetroArch's trace file) into the next executable. Which descriptors the console passes to the new process is not known +- [LoadExec descriptors](../../../core/libs/prx/libSceSystemService/LoadExec.cpp) (libSceSystemService) - on Linux, a call with an argument list replaces the process with `fexecve`, which first closes every descriptor above 2; a call without one uses `execv`, and the guest's descriptors stay open across it. On both paths descriptor 2 first gets back the host standard error a guest redirect kept, so the runtime's output does not follow the title's redirect (RetroArch's trace file, VLC's log) into the next executable. Which descriptors the console passes to the new process is not known From 2852249eb92d63d4cbfcacc6ed65fe4241963593 Mon Sep 17 00:00:00 2001 From: Yossi Yadgar Date: Thu, 15 Oct 2026 01:16:36 +0300 Subject: [PATCH 0364/1000] fix(libc): open closed standard descriptors on /dev/null when libc loads --- core/libs/prx/libc/src/StandardStreams.cpp | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libc/src/StandardStreams.cpp b/core/libs/prx/libc/src/StandardStreams.cpp index 8d136d05b4..c18cf23b37 100644 --- a/core/libs/prx/libc/src/StandardStreams.cpp +++ b/core/libs/prx/libc/src/StandardStreams.cpp @@ -4,10 +4,26 @@ #ifdef _WIN32 #include #else +#include #include #endif -namespace { FileStream input{stdin}; } +namespace { +#ifndef _WIN32 +const bool standardDescriptorsOpen = [] { + for (int descriptor = 0; descriptor <= 2; ++descriptor) { + if (::fcntl(descriptor, F_GETFD) != -1 || errno != EBADF) continue; + const int opened = ::open("/dev/null", descriptor == 0 ? O_RDONLY : O_WRONLY); + if (opened > descriptor) { + ::dup2(opened, descriptor); + ::close(opened); + } + } + return true; +}(); +#endif +FileStream input{stdin}; +} extern "C" { FileStream* __stdinp_nid_postfix = &input; FileStream* __stdoutp_nid_postfix = &_Stdout_nid_postfix; From 7c4400e926195c8b84d7f1be8d95a32f5c9c4fe1 Mon Sep 17 00:00:00 2001 From: Yossi Yadgar Date: Thu, 15 Oct 2026 01:16:36 +0300 Subject: [PATCH 0365/1000] test(libc): start a guest process with its standard descriptors closed --- core/libs/prx/libkernel/CMakeLists.txt | 5 ++ .../tests/GuestClosedStandardDescriptors.cpp | 66 +++++++++++++++++++ 2 files changed, 71 insertions(+) create mode 100644 core/libs/tests/GuestClosedStandardDescriptors.cpp diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 21cf02d628..1a87daace5 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -265,6 +265,11 @@ if(BUILD_TESTING) target_link_libraries(guest_host_standard_error_tests PRIVATE libkernel libc) add_test(NAME guest_host_standard_error COMMAND guest_host_standard_error_tests) set_tests_properties(guest_host_standard_error PROPERTIES TIMEOUT 10) + add_test_executable(guest_closed_standard_descriptors_tests ${LIBS_INCLUDE_DIR}/tests/GuestClosedStandardDescriptors.cpp) + target_include_directories(guest_closed_standard_descriptors_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_closed_standard_descriptors_tests PRIVATE libkernel libc) + add_test(NAME guest_closed_standard_descriptors COMMAND guest_closed_standard_descriptors_tests) + set_tests_properties(guest_closed_standard_descriptors PROPERTIES TIMEOUT 10) endif() add_test_executable(guest_truncate_tests ${LIBS_INCLUDE_DIR}/tests/GuestTruncate.cpp) target_include_directories(guest_truncate_tests PRIVATE ${LIBS_INCLUDE_DIR}) diff --git a/core/libs/tests/GuestClosedStandardDescriptors.cpp b/core/libs/tests/GuestClosedStandardDescriptors.cpp new file mode 100644 index 0000000000..4cb0deece1 --- /dev/null +++ b/core/libs/tests/GuestClosedStandardDescriptors.cpp @@ -0,0 +1,66 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI fcntl_nid_postfix(int, int, ...); +} + +namespace { + +constexpr int GuestGetFlags = 3; + +int Child(const char* closed) { + struct stat null{}; + if (::stat("/dev/null", &null) != 0) return 70; + for (int descriptor = 0; descriptor <= 2; ++descriptor) { + struct stat status{}; + if (::fstat(descriptor, &status) != 0) return 71 + descriptor; + if (closed[descriptor] != '1') continue; + if (!S_ISCHR(status.st_mode) || status.st_rdev != null.st_rdev) return 74 + descriptor; + if ((::fcntl(descriptor, F_GETFL) & O_ACCMODE) != (descriptor == 0 ? O_RDONLY : O_WRONLY)) return 77 + descriptor; + } + if (closed[0] == '1' && fcntl_nid_postfix(0, GuestGetFlags) < 0) return 80; + return 0; +} + +int RunClosed(const char* self, bool closeInput, bool closeOutput, bool closeError) { + const pid_t child = ::fork(); + if (child < 0) return -1; + if (child == 0) { + if (closeInput) ::close(0); + if (closeOutput) ::close(1); + if (closeError) ::close(2); + char closed[] = {closeInput ? '1' : '0', closeOutput ? '1' : '0', closeError ? '1' : '0', '\0'}; + char* const arguments[] = {const_cast(self), const_cast("--closed-child"), closed, nullptr}; + ::execv(self, arguments); + ::_exit(69); + } + int status = 0; + if (::waitpid(child, &status, 0) != child) return -1; + return WIFEXITED(status) ? WEXITSTATUS(status) : 128 + WTERMSIG(status); +} + +} + +int main(int argc, char** argv) { + if (argc > 2 && std::strcmp(argv[1], "--closed-child") == 0 && std::strlen(argv[2]) == 3) return Child(argv[2]); + const struct { bool input, output, error; const char* name; } cases[] = { + {true, false, false, "stdin"}, + {false, true, false, "stdout"}, + {true, true, true, "all three"}, + }; + for (const auto& test : cases) { + const int result = RunClosed(argv[0], test.input, test.output, test.error); + if (result != 0) { + std::fprintf(stderr, "closed standard descriptors, %s: child exited with %d\n", test.name, result); + return 1; + } + } + std::puts("closed standard descriptors are reopened on /dev/null"); + return 0; +} From a7ef31e103a08f45ead7697b378d6773baee4e8b Mon Sep 17 00:00:00 2001 From: Yossi Yadgar Date: Thu, 15 Oct 2026 01:16:36 +0300 Subject: [PATCH 0366/1000] docs(debt): note how closed standard descriptors are reopened --- docs/dev/debt/libc.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 50359fc84c..84d23a1f95 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -39,6 +39,7 @@ - Windows direct-memory backing reserves a paging-file section. A [direct memory page](../../../core/libs/prx/libc/include/WindowsMappings.hpp) mapped at one guest address is private write-watched memory at that address and is not committed in the section; a page mapped at a second address moves its whole resident run into the section (committed there) and every address becomes a view of it, tracked by write protection. An unmapped resident page that is not all zeros is kept in a private reservation until it is mapped again. Committed section contents remain across unmap/remap and are released when the backing and its remaining views are destroyed. A host import alias over part of a resident run is refused. Linux retains its existing backing. Peak gameplay memory and Windows versions other than the tested host remain unverified. - [getc_unlocked, putc_unlocked, getchar_unlocked, putchar_unlocked](../../../core/libs/prx/libc/src/StandardStreams.cpp) (libc) - byte I/O uses the host unlocked operations; guest EOF/error flags and macro counters follow `FileStream`. Linux status queries use `feof_unlocked`/`ferror_unlocked` because [glibc `feof`](https://github.com/bminor/glibc/blob/d2097651cc57834dbfcaa102ddfacae0d86cfb66/libio/feof.c) and [`ferror`](https://github.com/bminor/glibc/blob/d2097651cc57834dbfcaa102ddfacae0d86cfb66/libio/ferror.c) can acquire the stream lock. Windows uses [the CRT no-lock byte operations](https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/fgetc-nolock-fgetwc-nolock) and `feof`/`ferror`, verified under a held stream lock on UCRT. Console behavior and other host CRTs remain unverified. - [fgetws, getwc, getwchar, putwc, putwchar](../../../core/libs/prx/libc/src/FormattingWide.cpp) (libc) - wide stream I/O encodes and decodes wide characters as UTF-8 on the underlying byte stream; `fgetws` reads characters into the destination buffer until newline or EOF and null-terminates, returning null on stream error. Console wide stream buffering and encoding details remain unverified. +- [Closed standard descriptors](../../../core/libs/prx/libc/src/StandardStreams.cpp) (libc) - on Linux and macOS, a descriptor 0, 1 or 2 that is closed when libc loads is opened on `/dev/null` (read-only for 0, write-only for 1 and 2) by a static initializer of libc, which runs before the libraries that depend on libc initialize and before the title starts. Within libc this relies on no static initializer opening a descriptor before it, which `guest_closed_standard_descriptors` checks. glibc does the same for set-user-ID programs ([`check_fds.c`](https://sourceware.org/git/?p=glibc.git;a=blob;f=csu/check_fds.c)). Without it the first descriptor a library opens, the write watch's `userfaultfd` for one, becomes the guest's standard input or output. What the console gives a title as its standard descriptors is not measured, and on Windows a process started without standard handles is not handled. - [ftrylockfile](../../../core/libs/prx/libc/src/RuntimeSupport.cpp) (libc) - tries the host CRT stream lock that `flockfile`, `funlockfile` and each locking host stdio call on the stream take, so it returns -1 while another thread holds the stream through `flockfile` or is inside such a call, and 0 when it acquires the lock or already owns it (recursively, released by as many `funlockfile` calls), as FreeBSD 11 [`_ftrylockfile`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/stdio/_flock_stub.c#L89-L104) does. A libc export holds the lock only for each host call it makes, not across several host calls or while it updates the guest `FILE` prefix. Linux uses glibc `ftrylockfile`. UCRT has no try variant of `_lock_file`, so Windows calls `TryEnterCriticalSection` on the `CRITICAL_SECTION` that `_lock_file` enters, found through the private UCRT stream layout (`__crt_stdio_stream_data` in `ucrt/inc/corecrt_internal_stdio.h`, unchanged from Windows SDK 10.0.19041 to 10.0.26100); another CRT or a change of that layout breaks it. A null or closed stream throws where FreeBSD dereferences it - [Wide-character conversions](../../../core/libs/prx/libc/src/Multibyte.cpp) (libc) - The wide `wcsto*` functions parse the ASCII prefix of the guest string, as the C locale accepts only ASCII white space and digits. [`wcsrtombs`](../../../core/libs/prx/libc/src/Multibyte.cpp) uses the guest's single-byte C locale through `wcrtomb`; other encodings and shift states are not supported - [realpath](../../../core/libs/prx/libc/src/RealPath.cpp) (libc) follows FreeBSD 11's [`realpath.c`](https://github.com/freebsd/freebsd-src/blob/stable/11/lib/libc/stdlib/realpath.c) except for symlinks. The console's guest filesystem has none, so a host symlink under the guest root is followed for existence and type but not expanded, and `..` drops the previous component lexically, as `Resolve` does for every guest path. Through a host link `linkdir -> sub/deep`, `linkdir/../x` gives `x` beside the link, not beside its target. From ae920cb2063de6ba203057462c468bed20b2d661 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Thu, 15 Oct 2026 07:42:13 +0000 Subject: [PATCH 0367/1000] fix(agc): stamp the watched blocks of a partly watched store MarkWritten and StoreOwnBytes return 0 for a range that is not wholly watched, and they stamped none of its blocks. The watched blocks such a store wrote kept their old stamps, so UnchangedSince and StoredOver over them still read as unchanged after a GPU write that crossed into an unwatched import. A vouched shader table column or a cached range then kept bytes that had been replaced. Both now stamp each block whose part of the store starts or ends in watched memory, as the wholly watched path stamps every block. They still return 0 and note the store as unstamped. This costs one or two coverage lookups per block, and only on this path. --- .../Execution/src/GuestMemory.cpp | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp index d0859ab061..d78b40d178 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp @@ -918,6 +918,23 @@ struct WriteTracker { } } + void stampWatchedPart(std::uint64_t address, std::uint64_t end, std::uint32_t stampGeneration) { +#ifdef _WIN32 + address = std::max(address, base); + end = std::min(end, base + size); +#else + end = std::min(end, LeafCount * LeafBlocks * WriteBlockBytes); +#endif + if (address >= end) return; + for (auto block = blockOf(address); block <= blockOf(end - 1); ++block) { + const auto first = std::max(address, blockBegin(block)); + const auto stop = std::min(end, blockBegin(block) + WriteBlockBytes); + if (!covers(first, 1) && !covers(stop - 1, 1)) continue; + stamp(block, stampGeneration, StampKind::Driver); + noteDriverStore(block, address, end, stampGeneration); + } + } + void noteDriverStore(std::uint64_t block, std::uint64_t address, std::uint64_t end, std::uint32_t stampGeneration) { const auto begin = blockBegin(block); const auto from = static_cast(std::max(address, begin) - begin); @@ -1447,6 +1464,7 @@ std::uint64_t storeOwn(std::uint64_t address, std::size_t bytes, const std::func ++tracker.generation; if (!tracker.watched || !tracker.covers(stored.first, static_cast(stored.second - stored.first))) { tracker.noteUnstampedStore(stored.first, stored.second); + if (tracker.watched) tracker.stampWatchedPart(stored.first, stored.second, tracker.generation); return 0; } for (auto block = tracker.blockOf(stored.first); block <= tracker.blockOf(stored.second - 1); ++block) { @@ -1492,6 +1510,7 @@ std::uint64_t MarkWritten(std::uint64_t address, std::size_t bytes) { ++tracker.generation; if (!tracker.watched || !tracker.covers(address, bytes)) { tracker.noteUnstampedStore(address, address + bytes); + if (tracker.watched) tracker.stampWatchedPart(address, address + bytes, tracker.generation); return 0; } { From 7fc9e201df072a02896bca976d314d2ebc646d0e Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Thu, 15 Oct 2026 07:42:13 +0000 Subject: [PATCH 0368/1000] test(agc): check the stamps of a store into partly watched memory A driver store and an own store across a watched block and an unwatched one must stamp the watched block and no other. On Linux, a store across the halves of a split block must stamp it, and a store into its unwatched half alone must not. --- .../libSceAgcDriver/tests/WriteTracking.cpp | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp b/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp index d69035db8c..39a0fa20ed 100644 --- a/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp @@ -280,6 +280,39 @@ void CheckNewestStamp() { Require(NewestStamp(base + 3 * Block - 8, Block + 8) > partial, "a CPU store into the watched part of a partly watched range did not advance its stamp"); } +void CheckPartlyWatchedStore() { + void* memory = AllocateWatched(3 * Block); + const auto base = reinterpret_cast(memory); + const auto stored = base + Block; + const auto unwatched = base + 2 * Block; + std::memset(memory, 0x11, 3 * Block); + BumpCollectEpoch(); + Unwatch(unwatched, Block); + const auto before = CollectWrites(base, 2 * Block); + Require(before != 0 && UnchangedSince(base, 2 * Block, before), "the watched blocks were not collected"); + Require(MarkWritten(unwatched - 64, 128) == 0, "a driver store into partly watched memory is trusted"); + Require(!UnchangedSince(stored, Block, before), "a driver store into partly watched memory left its watched block unchanged"); + Require(StoredOver(unwatched - 64, 64, before), "a driver store into partly watched memory is not seen over its watched bytes"); + Require(!StoredOver(stored, Block - 64, before), "a driver store into partly watched memory is seen over watched bytes it did not store"); + Require(UnchangedSince(base, Block, before), "a driver store into partly watched memory stamped a block it did not store into"); + const auto beforeOwn = TrackerGeneration(); + Require(StoreOwnBytes(unwatched - 64, 128, [&] { std::memset(reinterpret_cast(unwatched - 64), 0x22, 128); }) == 0, "an own store into partly watched memory is trusted"); + Require(!UnchangedSince(stored, Block, beforeOwn), "an own store into partly watched memory left its watched block unchanged"); + Require(UnchangedSince(base, Block, beforeOwn), "an own store into partly watched memory stamped a block it did not store into"); +#ifndef _WIN32 + void* split = AllocateWatched(Block); + const auto splitBase = reinterpret_cast(split); + const auto watchedHalf = splitBase + Block / 2; + std::memset(split, 0x11, Block); + Unwatch(splitBase, Block / 2); + const auto beforeSplit = CollectWrites(watchedHalf, Block / 2); + Require(beforeSplit != 0 && UnchangedSince(watchedHalf, Block / 2, beforeSplit), "the watched half of a split block was not collected"); + Require(MarkWritten(splitBase + 64, 128) == 0 && UnchangedSince(watchedHalf, Block / 2, beforeSplit), "a driver store into the unwatched half of a split block stamped its watched half"); + Require(MarkWritten(watchedHalf - 64, 128) == 0, "a driver store across the halves of a split block is trusted"); + Require(!UnchangedSince(watchedHalf, Block / 2, beforeSplit), "a driver store across the halves of a split block left its watched half unchanged"); +#endif +} + #ifdef _WIN32 void CheckDeferredSharedImports() { constexpr std::size_t bytes = 4 * Block; @@ -935,6 +968,7 @@ int main() { CheckOwnStore(); CheckUnwatch(); CheckNewestStamp(); + CheckPartlyWatchedStore(); #ifdef _WIN32 CheckDeferredSharedImports(); CheckPrivateMappingReuse(); From fa9ac37ce72387c32c0ac4447a367d39d38fc8e2 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Thu, 15 Oct 2026 08:51:39 -0700 Subject: [PATCH 0369/1000] feat(libc): ask whether any part of a range is write-watched GuestWriteWatchCovers says whether a range is watched in full. The Linux and macOS watches unregister page ranges, so a range can hold watched pages between unwatched ones, and code that must act on every watched byte of a store has had no way to find them short of asking page by page. GuestWriteWatchOverlaps answers with one lookup in the registered ranges, like Covers. The macOS write-watch test and agc_write_tracking on Linux check it around watched pages between unwatched ones, at both edges and for an empty range. --- .../libSceAgcDriver/tests/WriteTracking.cpp | 21 ++++++++++++++++ .../libs/prx/libc/include/GuestWriteWatch.hpp | 1 + core/libs/prx/libc/src/GuestWriteWatch.cpp | 24 +++++++++++++++++++ core/libs/tests/WriteWatch.cpp | 18 +++++++++++++- core/libs/tests/write_watch/CMakeLists.txt | 2 +- 5 files changed, 64 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp b/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp index 39a0fa20ed..8ae2885dca 100644 --- a/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp @@ -9,6 +9,7 @@ #include #else #include +#include #endif #include #include @@ -313,6 +314,23 @@ void CheckPartlyWatchedStore() { #endif } +#ifndef _WIN32 +void CheckWatchOverlaps() { + const auto page = static_cast(sysconf(_SC_PAGESIZE)); + void* memory = AllocateWatched(Block); + const auto base = reinterpret_cast(memory); + Require(GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(memory, page), "cannot unwatch the block's first page"); + Require(GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(reinterpret_cast(base + 3 * page), Block - 3 * page), "cannot unwatch the block's last pages"); + Require(!GuestWriteWatch::GuestWriteWatchCovers_nid_postfix(base, Block), "a block with unwatched pages is covered"); + Require(GuestWriteWatch::GuestWriteWatchOverlaps_nid_postfix(base, Block), "a block with watched pages inside does not overlap the watch"); + Require(GuestWriteWatch::GuestWriteWatchOverlaps_nid_postfix(base + page - 1, 2), "a range ending in a watched page does not overlap the watch"); + Require(GuestWriteWatch::GuestWriteWatchOverlaps_nid_postfix(base + 3 * page - 1, 2), "a range starting in a watched page does not overlap the watch"); + Require(!GuestWriteWatch::GuestWriteWatchOverlaps_nid_postfix(base, page), "an unwatched page before the watched ones overlaps the watch"); + Require(!GuestWriteWatch::GuestWriteWatchOverlaps_nid_postfix(base + 3 * page, Block - 3 * page), "unwatched pages after the watched ones overlap the watch"); + Require(!GuestWriteWatch::GuestWriteWatchOverlaps_nid_postfix(base + page, 0), "an empty range overlaps the watch"); +} +#endif + #ifdef _WIN32 void CheckDeferredSharedImports() { constexpr std::size_t bytes = 4 * Block; @@ -969,6 +987,9 @@ int main() { CheckUnwatch(); CheckNewestStamp(); CheckPartlyWatchedStore(); +#ifndef _WIN32 + CheckWatchOverlaps(); +#endif #ifdef _WIN32 CheckDeferredSharedImports(); CheckPrivateMappingReuse(); diff --git a/core/libs/prx/libc/include/GuestWriteWatch.hpp b/core/libs/prx/libc/include/GuestWriteWatch.hpp index 95d5df1fc5..9afcf582db 100644 --- a/core/libs/prx/libc/include/GuestWriteWatch.hpp +++ b/core/libs/prx/libc/include/GuestWriteWatch.hpp @@ -12,6 +12,7 @@ bool GuestWriteWatchAvailable_nid_postfix(); void GuestWriteWatchRegister_nid_postfix(const void* pointer, std::size_t bytes); bool GuestWriteWatchUnregister_nid_postfix(const void* pointer, std::size_t bytes); bool GuestWriteWatchCovers_nid_postfix(std::uintptr_t address, std::size_t bytes); +bool GuestWriteWatchOverlaps_nid_postfix(std::uintptr_t address, std::size_t bytes); bool GuestWriteWatchCollect_nid_postfix(std::uintptr_t address, std::size_t bytes, void (*written)(void* context, std::uintptr_t begin, std::uintptr_t end), void* context); void GuestWriteWatchBeginHostWrite_nid_postfix(const void* pointer, std::size_t bytes); void GuestWriteWatchEndHostWrite_nid_postfix(const void* pointer, std::size_t bytes); diff --git a/core/libs/prx/libc/src/GuestWriteWatch.cpp b/core/libs/prx/libc/src/GuestWriteWatch.cpp index e155b3ed21..52907e1cb0 100644 --- a/core/libs/prx/libc/src/GuestWriteWatch.cpp +++ b/core/libs/prx/libc/src/GuestWriteWatch.cpp @@ -90,6 +90,13 @@ public: return covers(begin, end); } + bool Overlaps(std::uintptr_t begin, std::uintptr_t end) { + if (!Available()) return false; + std::shared_lock lock(_lock); + const auto next = _ranges.upper_bound(begin); + return (next != _ranges.begin() && std::prev(next)->second > begin) || (next != _ranges.end() && next->first < end); + } + bool Collect(std::uintptr_t begin, std::uintptr_t end, void (*written)(void*, std::uintptr_t, std::uintptr_t), void* context) { if (!Available()) return false; begin &= ~(PageBytes - 1); @@ -379,6 +386,13 @@ public: return covers(begin, end); } + bool Overlaps(std::uintptr_t begin, std::uintptr_t end) { + if (!_available) return false; + const Lock lock(_lock); + const auto next = _ranges.upper_bound(begin); + return (next != _ranges.begin() && std::prev(next)->second.end > begin) || (next != _ranges.end() && next->first < end); + } + enum class Take { All, Armed, Fresh }; bool Collect(std::uintptr_t begin, std::uintptr_t end, void (*written)(void*, std::uintptr_t, std::uintptr_t), void* context, Take take = Take::All) { @@ -660,6 +674,16 @@ bool GuestWriteWatchCovers_nid_postfix(std::uintptr_t address, std::size_t bytes #endif } +bool GuestWriteWatchOverlaps_nid_postfix(std::uintptr_t address, std::size_t bytes) { +#ifdef APS5_GUEST_WRITE_WATCH + return bytes != 0 && address + bytes > address && Watch::Get().Overlaps(address, address + bytes); +#else + static_cast(address); + static_cast(bytes); + return false; +#endif +} + bool GuestWriteWatchCollect_nid_postfix(std::uintptr_t address, std::size_t bytes, void (*written)(void* context, std::uintptr_t begin, std::uintptr_t end), void* context) { #ifdef APS5_GUEST_WRITE_WATCH if (bytes == 0 || address + bytes < address) return false; diff --git a/core/libs/tests/WriteWatch.cpp b/core/libs/tests/WriteWatch.cpp index 2093126aa4..0588e469d0 100644 --- a/core/libs/tests/WriteWatch.cpp +++ b/core/libs/tests/WriteWatch.cpp @@ -231,6 +231,21 @@ void CheckProtection() { Require(!GuestWriteWatchCovers_nid_postfix(region.Address(2), region.page), "an unregistered range is covered"); } +void CheckOverlaps() { + Region region(6); + Require(!GuestWriteWatchOverlaps_nid_postfix(region.Address(0), region.bytes), "an unregistered range overlaps the watch"); + GuestWriteWatchRegister_nid_postfix(region.base, region.bytes); + Require(GuestWriteWatchUnregister_nid_postfix(region.base, region.page), "cannot unregister the first page"); + Require(GuestWriteWatchUnregister_nid_postfix(region.base + 3 * region.page, 3 * region.page), "cannot unregister the last pages"); + Require(!GuestWriteWatchCovers_nid_postfix(region.Address(0), region.bytes), "a range around unregistered pages is covered"); + Require(GuestWriteWatchOverlaps_nid_postfix(region.Address(0), region.bytes), "a range around registered pages does not overlap the watch"); + Require(GuestWriteWatchOverlaps_nid_postfix(region.Address(1) - 1, 2), "a range ending in a registered page does not overlap the watch"); + Require(GuestWriteWatchOverlaps_nid_postfix(region.Address(3) - 1, 2), "a range starting in a registered page does not overlap the watch"); + Require(!GuestWriteWatchOverlaps_nid_postfix(region.Address(0), region.page), "an unregistered page before the registered ones overlaps the watch"); + Require(!GuestWriteWatchOverlaps_nid_postfix(region.Address(3), 3 * region.page), "unregistered pages after the registered ones overlap the watch"); + Require(!GuestWriteWatchOverlaps_nid_postfix(region.Address(1), 0), "an empty range overlaps the watch"); +} + void CheckThreads() { constexpr std::size_t Threads = 4; constexpr std::size_t PagesPerThread = 64; @@ -269,7 +284,7 @@ void CheckThreads() { int main(int argc, char** argv) { try { - Require(argc == 2, "expected collect, fresh, armed, register, mapped, host, empty, protection or threads"); + Require(argc == 2, "expected collect, fresh, armed, register, mapped, host, empty, protection, overlaps or threads"); const std::string_view mode(argv[1]); if (mode == "collect") CheckCollect(); else if (mode == "fresh") CheckFresh(); @@ -279,6 +294,7 @@ int main(int argc, char** argv) { else if (mode == "host") CheckHostWrite(); else if (mode == "empty") CheckEmptyRanges(); else if (mode == "protection") CheckProtection(); + else if (mode == "overlaps") CheckOverlaps(); else if (mode == "threads") CheckThreads(); else throw std::runtime_error("unknown write watch test"); std::printf("write watch %s passed\n", argv[1]); diff --git a/core/libs/tests/write_watch/CMakeLists.txt b/core/libs/tests/write_watch/CMakeLists.txt index 7e55f2bb41..ba692f336c 100644 --- a/core/libs/tests/write_watch/CMakeLists.txt +++ b/core/libs/tests/write_watch/CMakeLists.txt @@ -15,7 +15,7 @@ set_target_properties(write_watch_tests PROPERTIES CXX_EXTENSIONS OFF RUNTIME_OUTPUT_DIRECTORY "$<1:${ANYPS5_TEST_OUTPUT_DIRECTORY}>") -foreach(mode collect fresh armed register mapped host empty protection threads) +foreach(mode collect fresh armed register mapped host empty protection overlaps threads) add_test(NAME write_watch_${mode} COMMAND write_watch_tests ${mode}) set_tests_properties(write_watch_${mode} PROPERTIES TIMEOUT 30) endforeach() From 89778ecdac1daec593e82e81075b4a26a0de7f10 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Thu, 15 Oct 2026 08:52:55 -0700 Subject: [PATCH 0370/1000] fix(agc): stamp a block whose watched pages lie inside a partly watched store stampWatchedPart stamped a block when the store's part of it started or ended in watched memory. That is exact on Windows, where the watch excludes whole 64 KiB blocks and the arena's host regions are 64 KiB holes. On Linux and macOS the watch unregisters page ranges, so a block can be watched only between two unwatched ranges, and a store across all three left its watched pages with their old stamps. Off Windows the block is now stamped when the store's part of it overlaps any watched page (GuestWriteWatchOverlaps). A store that overlaps no watched page at all returns after that one lookup, where it made two coverage lookups per block before. The test watches only the middle pages of a block: a driver store and an own store around them must stamp it, a store into an unwatched end must not. --- .../Execution/src/GuestMemory.cpp | 13 ++++++++++-- .../libSceAgcDriver/tests/WriteTracking.cpp | 20 +++++++++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp index d78b40d178..210311a8ad 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp @@ -918,18 +918,27 @@ struct WriteTracker { } } + bool blockPartWatched(std::uint64_t first, std::uint64_t stop) const { +#ifdef _WIN32 + return covers(first, 1) || covers(stop - 1, 1); +#else + return GuestWriteWatch::GuestWriteWatchOverlaps_nid_postfix(static_cast(first), static_cast(stop - first)); +#endif + } + void stampWatchedPart(std::uint64_t address, std::uint64_t end, std::uint32_t stampGeneration) { #ifdef _WIN32 address = std::max(address, base); end = std::min(end, base + size); + if (address >= end) return; #else end = std::min(end, LeafCount * LeafBlocks * WriteBlockBytes); + if (address >= end || !GuestWriteWatch::GuestWriteWatchOverlaps_nid_postfix(static_cast(address), static_cast(end - address))) return; #endif - if (address >= end) return; for (auto block = blockOf(address); block <= blockOf(end - 1); ++block) { const auto first = std::max(address, blockBegin(block)); const auto stop = std::min(end, blockBegin(block) + WriteBlockBytes); - if (!covers(first, 1) && !covers(stop - 1, 1)) continue; + if (!blockPartWatched(first, stop)) continue; stamp(block, stampGeneration, StampKind::Driver); noteDriverStore(block, address, end, stampGeneration); } diff --git a/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp b/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp index 8ae2885dca..e72b4db89f 100644 --- a/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp @@ -329,6 +329,25 @@ void CheckWatchOverlaps() { Require(!GuestWriteWatch::GuestWriteWatchOverlaps_nid_postfix(base + 3 * page, Block - 3 * page), "unwatched pages after the watched ones overlap the watch"); Require(!GuestWriteWatch::GuestWriteWatchOverlaps_nid_postfix(base + page, 0), "an empty range overlaps the watch"); } + +void CheckWatchedMiddleStore() { + const auto page = static_cast(sysconf(_SC_PAGESIZE)); + void* memory = AllocateWatched(Block); + const auto base = reinterpret_cast(memory); + const auto middle = base + page; + std::memset(memory, 0x11, Block); + Unwatch(base, page); + Unwatch(base + 3 * page, Block - 3 * page); + const auto before = CollectWrites(middle, 2 * page); + Require(before != 0 && UnchangedSince(middle, 2 * page, before), "the watched middle of a block was not collected"); + Require(MarkWritten(base + 3 * page + 64, 128) == 0 && UnchangedSince(middle, 2 * page, before), "a driver store into an unwatched end of a block stamped its watched middle"); + Require(MarkWritten(base + page / 2, 3 * page) == 0, "a driver store around the watched middle of a block is trusted"); + Require(!UnchangedSince(middle, 2 * page, before), "a driver store around the watched middle of a block left it unchanged"); + Require(StoredOver(middle, 2 * page, before), "a driver store around the watched middle of a block is not seen over it"); + const auto beforeOwn = TrackerGeneration(); + Require(StoreOwnBytes(base + page / 2, 3 * page, [&] { std::memset(reinterpret_cast(base + page / 2), 0x22, 3 * page); }) == 0, "an own store around the watched middle of a block is trusted"); + Require(!UnchangedSince(middle, 2 * page, beforeOwn), "an own store around the watched middle of a block left it unchanged"); +} #endif #ifdef _WIN32 @@ -989,6 +1008,7 @@ int main() { CheckPartlyWatchedStore(); #ifndef _WIN32 CheckWatchOverlaps(); + CheckWatchedMiddleStore(); #endif #ifdef _WIN32 CheckDeferredSharedImports(); From 285ac78c06111634ec1cb52b87e31350e651ebad Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Thu, 15 Oct 2026 08:53:56 -0700 Subject: [PATCH 0371/1000] test(agc): check a store across a watched import and one that left the watch Since #6060 a settled Windows host import of private write-watched memory stays watched, and its bytes are vouched by the write tracker's stamps instead of compared. A GPU write that runs from such an import into a neighbouring import that left the watch reaches the tracker as one MarkWritten over a partly watched range. The test imports two blocks that stay watched next to one that leaves the watch, and checks that a driver store and an own store across the boundary stamp the watched block they wrote, are seen over exactly its stored bytes, and leave the other watched block unchanged. --- .../libSceAgcDriver/tests/WriteTracking.cpp | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp b/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp index e72b4db89f..1602df9621 100644 --- a/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp @@ -800,6 +800,24 @@ void ProtectPage(std::uint64_t address, bool readable) { GuestAllocations::GuestAllocationsInvalidate_nid_postfix(static_cast(address), 4096); } +void CheckStoreAcrossImports() { + void* memory = AllocateWatched(3 * Block); + const auto base = reinterpret_cast(memory); + const auto boundary = base + 2 * Block; + std::memset(memory, 0x11, 3 * Block); + Require(ImportWatched(base, 2 * Block, [] { return true; }) && Watched(base, 2 * Block), "an import that stays watched left the watch"); + Require(ImportWatched(boundary, Block, [] { return true; }, false) && !Watched(boundary, Block), "an import that leaves the watch stayed watched"); + const auto before = CollectWrites(base, 2 * Block); + Require(before != 0 && UnchangedSince(base, 2 * Block, before), "the watched import was not collected"); + Require(MarkWritten(boundary - 64, 128) == 0, "a GPU write across a watched import and an unwatched one is trusted"); + Require(!UnchangedSince(base + Block, Block, before), "a GPU write across a watched import and an unwatched one left the watched import's last block unchanged"); + Require(StoredOver(boundary - 64, 64, before) && !StoredOver(base + Block, Block - 64, before), "a GPU write across a watched import and an unwatched one is not seen over exactly its watched bytes"); + Require(UnchangedSince(base, Block, before), "a GPU write across a watched import and an unwatched one stamped a block it did not write"); + const auto beforeOwn = TrackerGeneration(); + Require(StoreOwnBytes(boundary - 64, 128, [&] { std::memset(reinterpret_cast(boundary - 64), 0x22, 128); }) == 0, "an own store across a watched import and an unwatched one is trusted"); + Require(!UnchangedSince(base + Block, Block, beforeOwn) && UnchangedSince(base, Block, beforeOwn), "an own store across a watched import and an unwatched one did not stamp exactly the watched block it wrote"); +} + void CheckUnwatchedImport() { void* memory = AllocateWatched(3 * Block); const auto base = reinterpret_cast(memory); @@ -1021,6 +1039,7 @@ int main() { CheckImportWindow(); CheckImportWindowZeroPages(); CheckImportWindowOtherThread(); + CheckStoreAcrossImports(); CheckUnwatchedImport(); CheckCommittedRanges(); } catch (const std::exception& error) { From 4fe92c9c23c7bd861d63664d4efde3033b6226f6 Mon Sep 17 00:00:00 2001 From: Romans Pokrovskis Date: Thu, 15 Oct 2026 18:54:28 +0300 Subject: [PATCH 0372/1000] test(agc): align indirect argument storage at runtime --- .../execution/GpuIndirectArgumentsMisalignedWriterAhead.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/GpuIndirectArgumentsMisalignedWriterAhead.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/GpuIndirectArgumentsMisalignedWriterAhead.cpp index 704f0fa843..12eb8b6c06 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/GpuIndirectArgumentsMisalignedWriterAhead.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/GpuIndirectArgumentsMisalignedWriterAhead.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -22,7 +23,8 @@ alignas(256) std::array CounterCode{0xf4080200u, 0xfa000000u, constexpr std::size_t BlockBytes = 65536; constexpr std::uint32_t ArgumentsOffset = 4; -alignas(BlockBytes) std::array Block{}; +std::array BlockStorage{}; +const std::span Block{reinterpret_cast((reinterpret_cast(BlockStorage.data()) + BlockBytes - 1) & ~(BlockBytes - 1)), BlockBytes}; alignas(256) std::array WriterDescriptor{}; alignas(256) std::array Counter{}; alignas(256) std::array CounterDescriptor{}; @@ -71,6 +73,7 @@ void Program(std::vector& words, const void* code, std::initializ int main() { try { + Require(reinterpret_cast(Block.data()) % BlockBytes == 0, "the indirect arguments block is not 64 KiB aligned"); auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; device.reset(); From 1a059f7af11694ad60cbefc2258209e09b3cfb1d Mon Sep 17 00:00:00 2001 From: akkadbakkad89 Date: Thu, 15 Oct 2026 16:10:36 +0000 Subject: [PATCH 0373/1000] test(libc): cover the whole fmin and fmax family in guest_fminf --- core/libs/tests/GuestFminf.cpp | 80 ++++++++++++++++++++++------------ 1 file changed, 53 insertions(+), 27 deletions(-) diff --git a/core/libs/tests/GuestFminf.cpp b/core/libs/tests/GuestFminf.cpp index 75dd90152b..742fad398f 100644 --- a/core/libs/tests/GuestFminf.cpp +++ b/core/libs/tests/GuestFminf.cpp @@ -5,42 +5,68 @@ #include extern "C" { +double APS5_VABI fmin_nid_postfix(double, double); float APS5_VABI fminf_nid_postfix(float, float); +long double APS5_VABI fminl_nid_postfix(long double, long double); +double APS5_VABI fmax_nid_postfix(double, double); +float APS5_VABI fmaxf_nid_postfix(float, float); +long double APS5_VABI fmaxl_nid_postfix(long double, long double); } -static void Check(bool condition, int line) { +static void Check(bool condition, const char* name, int line) { if (!condition) { - std::fprintf(stderr, "fminf check failed at line %d\n", line); + std::fprintf(stderr, "%s check failed at line %d\n", name, line); std::abort(); } } -#define Require(value) Check((value), __LINE__) +#define Require(value) Check((value), name, __LINE__) + +template +static void CheckMinAndMax(T (APS5_VABI *minimum)(T, T), T (APS5_VABI *maximum)(T, T), const char* name) { + const auto nan = std::numeric_limits::quiet_NaN(); + const auto infinity = std::numeric_limits::infinity(); + + Require(minimum(T(3.0), T(-2.0)) == T(-2.0)); + Require(minimum(T(-2.0), T(3.0)) == T(-2.0)); + Require(minimum(T(1.5), T(2.5)) == T(1.5)); + Require(maximum(T(3.0), T(-2.0)) == T(3.0)); + Require(maximum(T(-2.0), T(3.0)) == T(3.0)); + Require(maximum(T(1.5), T(2.5)) == T(2.5)); + + Require(minimum(nan, T(4.0)) == T(4.0)); + Require(minimum(T(4.0), nan) == T(4.0)); + Require(std::isnan(minimum(nan, nan))); + Require(maximum(nan, T(4.0)) == T(4.0)); + Require(maximum(T(4.0), nan) == T(4.0)); + Require(std::isnan(maximum(nan, nan))); + + Require(std::signbit(minimum(T(-0.0), T(+0.0)))); + Require(std::signbit(minimum(T(+0.0), T(-0.0)))); + Require(std::signbit(minimum(T(-0.0), T(-0.0)))); + Require(!std::signbit(minimum(T(+0.0), T(+0.0)))); + Require(!std::signbit(maximum(T(-0.0), T(+0.0)))); + Require(!std::signbit(maximum(T(+0.0), T(-0.0)))); + Require(std::signbit(maximum(T(-0.0), T(-0.0)))); + Require(!std::signbit(maximum(T(+0.0), T(+0.0)))); + + Require(minimum(infinity, -infinity) == -infinity); + Require(minimum(-infinity, infinity) == -infinity); + Require(minimum(infinity, T(5.0)) == T(5.0)); + Require(minimum(T(5.0), infinity) == T(5.0)); + Require(minimum(-infinity, T(5.0)) == -infinity); + Require(minimum(T(5.0), -infinity) == -infinity); + Require(maximum(infinity, -infinity) == infinity); + Require(maximum(-infinity, infinity) == infinity); + Require(maximum(infinity, T(5.0)) == infinity); + Require(maximum(T(5.0), infinity) == infinity); + Require(maximum(-infinity, T(5.0)) == T(5.0)); + Require(maximum(T(5.0), -infinity) == T(5.0)); +} int main() { - const auto nan = std::numeric_limits::quiet_NaN(); - const auto infinity = std::numeric_limits::infinity(); - - Require(fminf_nid_postfix(3.0f, -2.0f) == -2.0f); - Require(fminf_nid_postfix(-2.0f, 3.0f) == -2.0f); - Require(fminf_nid_postfix(1.5f, 2.5f) == 1.5f); - - Require(fminf_nid_postfix(nan, 4.0f) == 4.0f); - Require(fminf_nid_postfix(4.0f, nan) == 4.0f); - Require(std::isnan(fminf_nid_postfix(nan, nan))); - - const auto minNegZero = fminf_nid_postfix(-0.0f, +0.0f); - const auto minPosZero = fminf_nid_postfix(+0.0f, -0.0f); - Require(minNegZero == 0.0f && std::signbit(minNegZero)); - Require(minPosZero == 0.0f && std::signbit(minPosZero)); - Require(std::signbit(fminf_nid_postfix(-0.0f, -0.0f))); - Require(!std::signbit(fminf_nid_postfix(+0.0f, +0.0f))); - - Require(fminf_nid_postfix(infinity, -infinity) == -infinity); - Require(fminf_nid_postfix(-infinity, infinity) == -infinity); - Require(fminf_nid_postfix(infinity, 5.0f) == 5.0f); - Require(fminf_nid_postfix(5.0f, infinity) == 5.0f); - Require(fminf_nid_postfix(-infinity, 5.0f) == -infinity); - Require(fminf_nid_postfix(5.0f, -infinity) == -infinity); + CheckMinAndMax(fmin_nid_postfix, fmax_nid_postfix, "fmin/fmax"); + CheckMinAndMax(fminf_nid_postfix, fmaxf_nid_postfix, "fminf/fmaxf"); + CheckMinAndMax(fminl_nid_postfix, fmaxl_nid_postfix, "fminl/fmaxl"); return 0; } From 2321f1ac7fe6ba95054ca4fd10089192b847398c Mon Sep 17 00:00:00 2001 From: fdfawef324 <287205748+fdfawef324@users.noreply.github.com> Date: Thu, 15 Oct 2026 12:36:14 -0400 Subject: [PATCH 0374/1000] fix(libSceAudioOut): count completed AudioOut2 grains in the queue level sceAudioOut2ContextGetQueueLevel left out the grains that sceAudioOut2ContextAdvance completed and no push had consumed yet. While pushes filled the 40 ms cushion the level stayed at 0, so a title that renders as many grains as the level leaves room for and pushes them on another thread rendered a full queue again after each push. PPSA23226's Wwise sink does that and stopped once more than 16 completed grains awaited a push. Co-Authored-By: Claude Opus 5.5 --- .../libSceAudioOut/src/AudioOut2Context.cpp | 2 +- .../tests/AudioOut2PortBuffers.cpp | 39 ++++++++++++++++++- docs/dev/debt/libSceAudioOut.md | 1 + 3 files changed, 39 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp b/core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp index 74a4a2b1c4..242eeb0a53 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp @@ -323,7 +323,7 @@ int APS5_VABI sceAudioOut2ContextGetQueueLevel(AudioOut2ContextHandle ctx, uint3 auto* context = FromHandle(ctx); if (!context) return SCE_AUDIO_OUT2_ERROR_INVALID_HANDLE; std::lock_guard lock(context->lock); - const auto level = QueueLevel(*context, Clock::now()); + const auto level = std::min(context->queueDepth, QueueLevel(*context, Clock::now()) + static_cast(context->advancedGrains.size())); if (queue_level) *queue_level = level; if (available_queue) *available_queue = context->queueDepth - level; context->queueLevelPolls++; diff --git a/core/libs/prx/libSceAudioOut/tests/AudioOut2PortBuffers.cpp b/core/libs/prx/libSceAudioOut/tests/AudioOut2PortBuffers.cpp index cac79482e3..bfa3139087 100644 --- a/core/libs/prx/libSceAudioOut/tests/AudioOut2PortBuffers.cpp +++ b/core/libs/prx/libSceAudioOut/tests/AudioOut2PortBuffers.cpp @@ -1,5 +1,6 @@ #include "SceTypes.hpp" +#include #include #include #include @@ -21,6 +22,7 @@ int APS5_VABI sceAudioOut2ContextResetParam(AudioOut2ContextParam*); int APS5_VABI sceAudioOut2ContextCreate(const AudioOut2ContextParam*, void*, std::size_t, AudioOut2ContextHandle*); int APS5_VABI sceAudioOut2ContextDestroy(AudioOut2ContextHandle); int APS5_VABI sceAudioOut2ContextAdvance(AudioOut2ContextHandle); +int APS5_VABI sceAudioOut2ContextGetQueueLevel(AudioOut2ContextHandle, std::uint32_t*, std::uint32_t*); int APS5_VABI sceAudioOut2ContextPush(AudioOut2ContextHandle, std::uint32_t); int APS5_VABI sceAudioOut2PortCreate(AudioOut2ContextHandle, const AudioOut2PortParam*, AudioOut2PortHandle*); int APS5_VABI sceAudioOut2PortDestroy(AudioOut2PortHandle); @@ -54,7 +56,7 @@ void SetData(AudioOut2PortHandle port, const void* data) { Require(sceAudioOut2PortSetAttributes(port, &attribute, 1) == 0); } -std::vector Play(const std::function& feed, bool advanced = false) { +std::vector Play(const std::function& feed, bool advanced = false, std::uint32_t queueDepth = 1) { const auto path = std::filesystem::temp_directory_path() / ("anyps5_audio_out2_port_buffers-" + std::to_string(std::random_device{}()) + ".raw"); std::filesystem::remove(path); SetEnvironment("SDL_DISKAUDIOFILE", path.string()); @@ -62,7 +64,7 @@ std::vector Play(const std::function> expected; + for (std::uint32_t index = 0; index < grains; index++) expected.push_back(Grain(0.001f * static_cast(index + 1))); + std::uint32_t mostAwaiting = 0; + const auto played = Play([&](AudioOut2ContextHandle context, AudioOut2PortHandle port) { + std::uint32_t advanced = 0; + std::uint32_t pushed = 0; + while (pushed < grains) { + std::uint32_t available = 0; + Require(sceAudioOut2ContextGetQueueLevel(context, nullptr, &available) == 0); + for (std::uint32_t index = 0; index < available && advanced < grains; index++) { + SetData(port, expected[advanced].data()); + Require(sceAudioOut2ContextAdvance(context) == 0); + advanced++; + } + mostAwaiting = std::max(mostAwaiting, advanced - pushed); + if (advanced == pushed) { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + continue; + } + Require(sceAudioOut2ContextPush(context, 1) == 0); + pushed++; + } + }, true, queueDepth); + Require(mostAwaiting <= queueDepth); + std::vector*> order; + for (const auto& data : expected) order.push_back(&data); + RequirePlayed(played, order); +} + } int main() { @@ -243,5 +277,6 @@ int main() { TestBurstThenGapKeepsEveryBuffer(); TestAdvancingPortWithoutNewBufferPlaysSilence(); TestRewrittenBufferRepeats(); + TestRenderingSizedByLevelStaysWithinQueueDepth(); return 0; } diff --git a/docs/dev/debt/libSceAudioOut.md b/docs/dev/debt/libSceAudioOut.md index 75a1c10469..8a33ea26f7 100644 --- a/docs/dev/debt/libSceAudioOut.md +++ b/docs/dev/debt/libSceAudioOut.md @@ -25,4 +25,5 @@ - [sceAudioOut2EnableChat](../../../core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp) (libSceAudioOut) takes no arguments and returns 0, as declared by [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Audio/AudioOut2.cs#L460-L464) ("Allows chat audio alongside the application's own"). The host mixes no chat audio and AudioOut2 has no chat port, so the call only records that chat audio is allowed. Whether the console fails it without a chat session, when repeated or before `sceAudioOut2Initialize` is unknown. [sceAudioOut2DisableChat](../../../core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp) is the counterpart, imported by PPSA18216: it takes no arguments, returns 0 and records that chat audio is no longer allowed; its signature is not confirmed by a console measurement, and whether the console fails it without a chat session or when repeated is unknown. - [sceAudioOut2Set3DLatency](../../../core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp) and [sceAudioOut2MasteringInit, sceAudioOut2MasteringTerm and sceAudioOut2MasteringSetParam](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Mastering.cpp) (libSceAudioOut) have no effect: there is no 3D rendering or mastering stage. - [AudioOut2 contexts](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp) (libSceAudioOut) with an SDL device and no `sceAudioOut2ContextAdvance` calls read a grain's port buffers at the next push, at least 100 us after the grain was pushed, and play the mix behind a 40 ms cushion that the queue level leaves out. When the hardware reads the buffers is not known. The timing follows a title whose mixer fills a buffer after pushing it, reuses it two pushes later, and may still be writing it when the next push follows within microseconds. The last grain pushed before a title stops pushing is never read or played. +- [sceAudioOut2ContextGetQueueLevel](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp) (libSceAudioOut) with an SDL device counts the grains that `sceAudioOut2ContextAdvance` completed and no push has consumed as queued, together with the pushed grains beyond the 40 ms cushion, up to `queue_depth`. Whether the console counts them is unknown. Pushes that fill the cushion leave the level at 0, so a title that renders as many grains as the level leaves room for and pushes them on another thread rendered a full queue again after each of those pushes: PPSA23226's Wwise sink passes the available count to the sound engine as the number of frames to render, advances once per frame and pushes on a separate thread, and exceeded 16 completed grains. Counted, the completed grains keep such a title at most `queue_depth` grains ahead of its pushes. - [sceAudioOutSysGetHdmiMonitorInfo](../../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) reports an HDMI sink without a monitor name or audio descriptors: it zeroes the 0x180 bytes and returns 0, since audio reaches the host through SDL as PCM and there is no EDID to describe. The arguments `(type, info, size)`, the required size 0x180 and the fields titles read (monitor name at +0x0B, 13 bytes; descriptor count as a 32-bit value at +0x94; 8-byte descriptors `[coding type, channels, rate mask, 0, extra, 0, 0, 0]` from +0x98) come from [PS5-Audio-Passthrough-Research](https://github.com/sainsaji/PS5-Audio-Passthrough-Research/blob/c3be9c2afcb8f7838784ead0fe1c12a2ceea965a/docs/FINDINGS.md) (firmware 12.70, one monitor). What a console without an HDMI audio sink returns, an error or no descriptors, is unknown. The other bytes, among them the header at +0x90 (`01 09 4f 00` on that monitor), have no known meaning and are written as 0. No LPCM descriptor is reported for the host device, as the rate mask and extra byte of one are not known. A null buffer returns `0x80260004` like the other AudioOut calls; the console's code is not measured. Monitor type 0x10, which the console also accepts, other types and sizes other than 0x180 throw: their results and error codes are unknown. From 7e29ba6fba9d509ef8fcd1a905367a5ece529076 Mon Sep 17 00:00:00 2001 From: notaxeltv Date: Wed, 14 Oct 2026 21:55:33 +0200 Subject: [PATCH 0375/1000] feat(libSceLoginService): let PPSA34015 load without a missing module FC 27 stops on Windows with Failed to load module / error 126 because this PRX is absent. Export Initialize and Terminate as no-ops and keep RequestDevices throwing until the buffer layout is known. Co-authored-by: Cursor --- .../prx/libSceLoginService/CMakeLists.txt | 15 +++++++++ core/libs/prx/libSceLoginService/Export.cpp | 21 +++++++++++++ core/libs/tests/GuestLoginService.cpp | 31 +++++++++++++++++++ docs/dev/debt/libSceLoginService.md | 9 ++++++ 4 files changed, 76 insertions(+) create mode 100644 core/libs/prx/libSceLoginService/CMakeLists.txt create mode 100644 core/libs/prx/libSceLoginService/Export.cpp create mode 100644 core/libs/tests/GuestLoginService.cpp create mode 100644 docs/dev/debt/libSceLoginService.md diff --git a/core/libs/prx/libSceLoginService/CMakeLists.txt b/core/libs/prx/libSceLoginService/CMakeLists.txt new file mode 100644 index 0000000000..5993f09b49 --- /dev/null +++ b/core/libs/prx/libSceLoginService/CMakeLists.txt @@ -0,0 +1,15 @@ +add_library(libSceLoginService SHARED EXCLUDE_FROM_ALL Export.cpp) +target_include_directories(libSceLoginService PRIVATE ${LIBS_INCLUDE_DIR}) +configure_windows_unwind(libSceLoginService) + +if(BUILD_TESTING) + add_test_executable(guest_login_service_tests ${LIBS_INCLUDE_DIR}/tests/GuestLoginService.cpp) + target_include_directories(guest_login_service_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_login_service_tests PRIVATE libSceLoginService libc) + configure_windows_unwind(guest_login_service_tests) + add_test(NAME guest_login_service COMMAND guest_login_service_tests) + if(WIN32) + set_tests_properties(guest_login_service PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceLoginService/Export.cpp b/core/libs/prx/libSceLoginService/Export.cpp new file mode 100644 index 0000000000..43a1b1dcdf --- /dev/null +++ b/core/libs/prx/libSceLoginService/Export.cpp @@ -0,0 +1,21 @@ +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceLoginServiceInitialize(void) { + return 0; +} + +int APS5_VABI sceLoginServiceTerminate(void) { + return 0; +} + +int APS5_VABI sceLoginServiceRequestDevices(void* param) { + (void)param; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/tests/GuestLoginService.cpp b/core/libs/tests/GuestLoginService.cpp new file mode 100644 index 0000000000..a1fa20d143 --- /dev/null +++ b/core/libs/tests/GuestLoginService.cpp @@ -0,0 +1,31 @@ +#include "prx/libc/include/general/VabiMacros.hpp" + +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceLoginServiceInitialize(void); +int APS5_VABI sceLoginServiceTerminate(void); +int APS5_VABI sceLoginServiceRequestDevices(void* param); +} + +static void Require(bool value) { + if (!value) std::abort(); +} + +int main() { + Require(sceLoginServiceInitialize() == 0); + Require(sceLoginServiceTerminate() == 0); + unsigned char buffer[0x80]{}; + try { + sceLoginServiceRequestDevices(buffer); + } catch (const std::runtime_error& error) { + if (std::string(error.what()) == "sceLoginServiceRequestDevices not implemented") return 0; + std::fprintf(stderr, "sceLoginServiceRequestDevices threw: %s\n", error.what()); + return 1; + } + std::fprintf(stderr, "sceLoginServiceRequestDevices returned instead of reporting that it is not implemented\n"); + return 1; +} diff --git a/docs/dev/debt/libSceLoginService.md b/docs/dev/debt/libSceLoginService.md new file mode 100644 index 0000000000..f33713e90d --- /dev/null +++ b/docs/dev/debt/libSceLoginService.md @@ -0,0 +1,9 @@ +# libSceLoginService technical debt + +### Silent stubs + +- [sceLoginServiceInitialize, sceLoginServiceTerminate](../../../core/libs/prx/libSceLoginService/Export.cpp) (libSceLoginService) - return success and keep no login-service state: AnyPS5 has no PSN login service. EA SPORTS FC 27 (PPSA34015) loads the module with `sceSysmoduleLoadModule(0xe3)` and calls `sceLoginServiceInitialize` with no arguments; a missing `libSceLoginService.prx` stops Windows start-up with `Failed to load module` / `GetLastError: 126` + +### Unknown function info + +- [sceLoginServiceRequestDevices](../../../core/libs/prx/libSceLoginService/Export.cpp) (libSceLoginService) - PPSA34015 passes one pointer to a stack buffer (`lea rdi, [rbp-0x80]`) then calls `sceLoginServiceTerminate`. The buffer layout, other arguments and console errors are unknown, so every call throws without reading or writing caller memory From 9800f3ab92c058e649c50c771a131fe641106d96 Mon Sep 17 00:00:00 2001 From: billybobpicklio Date: Thu, 15 Oct 2026 12:45:54 -0400 Subject: [PATCH 0376/1000] fix(agc): keep each set's image table writes in its own draw snapshot set The vertex and fragment image tables both sit at ImageTableBinding, in sets 0 and 1. PrepareDrawBindings matched the recorded writes to their bindings by binding number alone, so the set 0 table also took the fragment table's writes: they went into the snapshot's set 0, past its variable descriptor count and into the next set in the pool, and the snapshot's set 1 table was left unwritten. Match a write by its original set as well. The recorder test device now enables the descriptor indexing features image tables need. Co-Authored-By: Claude Opus 5.5 --- .../Graphics/src/ShaderResources.cpp | 3 +- .../prx/libSceAgcDriver/tests/Recorder.cpp | 107 ++++++++++++++++++ 2 files changed, 109 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index 15a0f3c2d2..8ba6f9b60a 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -4606,7 +4606,8 @@ std::shared_ptr ShaderResources::PrepareDrawBindi for (std::size_t index = 0; index < selected.size(); ++index) snapshotIndices[selected[index]] = index; auto nextWrite = writes.begin(); for (const auto& binding : bindings) { - while (nextWrite != writes.end() && nextWrite->dstBinding == binding.layout.binding) { + const auto original = setOf(binding) == 0 ? _set : fragment.set; + while (nextWrite != writes.end() && nextWrite->dstBinding == binding.layout.binding && nextWrite->dstSet == original) { auto& write = *nextWrite++; write.dstSet = setOf(binding) == 0 ? result->allocation.set : result->fragmentAllocation.set; if (binding.allocations.empty()) continue; diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 040c2dd195..47569eb2be 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -28,6 +28,7 @@ #include "prx/libc/include/GuestWriteWatch.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include "ResidentPresent.hpp" +#include "ImageTableAbi.hpp" #include "SampleLod_spv.h" #include "BufferUpdate_spv.h" #include "SampleArray_spv.h" @@ -264,6 +265,28 @@ public: extensionsEnabled.insert(extensionsEnabled.end(), libraryExtensions.begin(), libraryExtensions.end()); address.pNext = &library; } + VkPhysicalDeviceDescriptorIndexingFeaturesEXT indexing{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DESCRIPTOR_INDEXING_FEATURES_EXT}; + if (hasExtension(VK_EXT_DESCRIPTOR_INDEXING_EXTENSION_NAME)) { + VkPhysicalDeviceFeatures2 features{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2, &indexing}; + function("vkGetPhysicalDeviceFeatures2")(context.physical, &features); + context.descriptorIndexing = indexing.shaderSampledImageArrayNonUniformIndexing == VK_TRUE && indexing.shaderStorageImageArrayNonUniformIndexing == VK_TRUE; + context.partiallyBound = indexing.descriptorBindingPartiallyBound == VK_TRUE; + context.variableDescriptorCount = indexing.descriptorBindingVariableDescriptorCount == VK_TRUE; + context.runtimeDescriptorArray = indexing.runtimeDescriptorArray == VK_TRUE; + } + indexing = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DESCRIPTOR_INDEXING_FEATURES_EXT}; + if (context.descriptorIndexing && context.partiallyBound && context.variableDescriptorCount && context.runtimeDescriptorArray) { + extensionsEnabled.push_back(VK_EXT_DESCRIPTOR_INDEXING_EXTENSION_NAME); + indexing.shaderSampledImageArrayNonUniformIndexing = VK_TRUE; + indexing.shaderStorageImageArrayNonUniformIndexing = VK_TRUE; + indexing.descriptorBindingPartiallyBound = VK_TRUE; + indexing.descriptorBindingVariableDescriptorCount = VK_TRUE; + indexing.runtimeDescriptorArray = VK_TRUE; + indexing.pNext = address.pNext; + address.pNext = &indexing; + } else { + context.descriptorIndexing = context.partiallyBound = context.variableDescriptorCount = context.runtimeDescriptorArray = false; + } VkPhysicalDeviceRobustness2FeaturesEXT robustness{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ROBUSTNESS_2_FEATURES_EXT}; if (hasExtension(VK_EXT_ROBUSTNESS_2_EXTENSION_NAME)) { VkPhysicalDeviceFeatures2 features{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2, &robustness}; @@ -292,6 +315,7 @@ public: std::cout << "Vulkan device: " << properties.deviceName << '\n'; context.limits = properties.limits; context.bufferDeviceAddress = true; + context.imageTableCapacity = ImageTableCapacity(context); context.formatProperties = function("vkGetPhysicalDeviceFormatProperties"); context.imageFormatProperties = function("vkGetPhysicalDeviceImageFormatProperties"); context.Function("vkGetDeviceQueue")(context.device, family, 0, &context.queue); @@ -2434,6 +2458,7 @@ public: struct Contents { std::map, VkDescriptorBufferInfo> buffers; std::map, VkDescriptorImageInfo> images; + std::map, std::uint32_t> written; }; explicit DescriptorUpdates(Context& context) : context(context), previous(context.functions), update(context.Resolved(&DeviceFunctions::updateDescriptorSets, "vkUpdateDescriptorSets")) { @@ -2452,6 +2477,7 @@ private: Contents contents; for (std::uint32_t i = 0; i < count; ++i) { const auto& write = writes[i]; + contents.written[{write.dstSet, write.dstBinding}] += write.descriptorCount; for (std::uint32_t element = 0; element < write.descriptorCount; ++element) { const auto key = std::pair{write.dstBinding, write.dstArrayElement + element}; if (write.descriptorType == VK_DESCRIPTOR_TYPE_STORAGE_BUFFER) contents.buffers[key] = write.pBufferInfo[element]; @@ -2522,6 +2548,85 @@ void drawDescriptorWriteTests(const Device& device, Recorder& recorder) { recorder.Activate(); } +void drawSnapshotTableSetTests(const Device& device, Recorder& recorder) { + namespace Abi = ShaderRecompiler::ImageTableAbi; + using ShaderRecompiler::DescriptorKind; + using ShaderRecompiler::DescriptorRole; + const auto& base = device.GetContext(); + TextureDetiler detiler(base); + auto context = base; + context.detiler = &detiler; + if (context.imageTableCapacity < 2u) { + std::cout << "image tables unavailable: draw snapshot table sets not tested\n"; + return; + } + constexpr std::uint32_t width = 4; + constexpr std::uint32_t height = 4; + alignas(256) static std::array texels{}; + texels.fill(1.0f); + const auto address = static_cast(reinterpret_cast(texels.data())); + const std::array texture{static_cast(address >> 8u), static_cast((address >> 40u) & 0xffu) | (22u << 20u) | (((width - 1u) & 3u) << 30u), ((width - 1u) >> 2u) | ((height - 1u) << 14u), 0xfacu | (9u << 28u), 0u, 0u, 0u, 0u}; + constexpr auto bindingCount = static_cast(ShaderRecompiler::RuntimeAbi::Binding::Count); + const auto addTable = [&](ShaderRecompiler::RecompileResult& program, ShaderRecompiler::RuntimeAbi::Stage stage, std::uint32_t set, std::uint32_t elements) { + ShaderRecompiler::DescriptorBinding map{}; + map.kind = DescriptorKind::StorageBuffer; + map.role = DescriptorRole::ImageTableMap; + map.descriptorSet = set; + map.binding = static_cast(stage) * bindingCount + static_cast(ShaderRecompiler::RuntimeAbi::Binding::ImageTableMap); + map.count = 1; + map.guestDescriptor.assign(Abi::HeaderWords + elements, 0u); + map.guestDescriptor[Abi::HeaderVersion] = Abi::Version; + map.guestDescriptor[Abi::HeaderElementCount] = elements; + map.guestDescriptor[Abi::HeaderElementFaults] = Abi::HeaderWords; + map.guestDescriptor[Abi::HeaderSamplerFaults] = Abi::HeaderWords + elements; + program.bindings.push_back(map); + ShaderRecompiler::DescriptorBinding images{}; + images.kind = DescriptorKind::SampledImage; + images.role = DescriptorRole::ImageTable; + images.descriptorSet = set; + images.binding = ShaderRecompiler::RuntimeAbi::ImageTableBinding; + images.count = elements; + for (std::uint32_t element = 0; element < elements; ++element) images.guestDescriptor.insert(images.guestDescriptor.end(), texture.begin(), texture.end()); + images.imageShapes.assign(elements, ShaderRecompiler::DescriptorImageShape::Image2D); + images.imageDepthCompare.assign(elements, false); + images.imageSamplers.assign(elements, 0u); + program.bindings.push_back(std::move(images)); + }; + DescriptorUpdates updates(context); + { + DescriptorCache cache(context); + context.descriptorCache = &cache; + Recorder snapshots(context); + snapshots.Activate(); + ShaderRecompiler::RecompileResult vertex, fragment; + ShaderRecompiler::DescriptorBinding data{}; + data.kind = DescriptorKind::StorageBuffer; + data.role = DescriptorRole::FlattenedSrt; + data.binding = static_cast(ShaderRecompiler::RuntimeAbi::Binding::FlattenedSrt); + data.count = 1; + data.guestDescriptor = {1, 2, 3, 4}; + vertex.bindings.push_back(data); + addTable(vertex, ShaderRecompiler::RuntimeAbi::Stage::Main, 0u, 1u); + addTable(fragment, ShaderRecompiler::RuntimeAbi::Stage::Fragment, ShaderRecompiler::RuntimeAbi::FragmentDescriptorSet, 2u); + const ColorTarget target{}; + ShaderResources resources(context, vertex, fragment, target, 0, 0); + Require(resources.SplitSets(), "a fragment image table did not get its own descriptor set"); + const ShaderResources::MovedBuffer moved{0, 0, 16, {5, 6, 7, 8}}; + const auto draw = resources.PrepareDrawBindings(snapshots, std::span(&moved, 1)); + Require(draw != nullptr && draw->snapshots.size() == 1, "moved shader data did not create a draw snapshot"); + const auto& written = updates.calls.back().written; + const auto tableWrites = [&](VkDescriptorSet set) { + const auto it = written.find({set, ShaderRecompiler::RuntimeAbi::ImageTableBinding}); + return it == written.end() ? 0u : it->second; + }; + Require(tableWrites(draw->allocation.set) == 1u, "the draw snapshot wrote the wrong number of image table descriptors into the vertex set"); + Require(tableWrites(draw->fragmentAllocation.set) == 2u, "the draw snapshot wrote the wrong number of image table descriptors into the fragment set"); + snapshots.Sync(); + } + ClearCachedTextures(context.device); + recorder.Activate(); +} + void drawSnapshotReuseTests(const Device& device, Recorder& recorder) { const auto& context = device.GetContext(); constexpr std::size_t bytes = 65536; @@ -8199,6 +8304,7 @@ int main(int argc, char** argv) { } if (argc == 2 && std::string_view(argv[1]) == "--draw-descriptors-only") { drawDescriptorWriteTests(device, recorder); + drawSnapshotTableSetTests(device, recorder); std::cout << "Draw descriptor writes and snapshot overrides passed\n"; return 0; } @@ -8256,6 +8362,7 @@ int main(int argc, char** argv) { misalignedRegionTests(device, recorder); drawSnapshotReuseTests(device, recorder); drawDescriptorWriteTests(device, recorder); + drawSnapshotTableSetTests(device, recorder); drawSnapshotCapTests(device, recorder); drawSnapshotEvictionTests(device); unwrittenSnapshotTests(device); From 4cdd2f48340c30e22bfea17c6fb0477fca9edfa8 Mon Sep 17 00:00:00 2001 From: milescoviello Date: Wed, 14 Oct 2026 16:20:24 +0000 Subject: [PATCH 0377/1000] fix(agc): accept a copy's sample selection on a draw that copies nothing DB_RENDER_CONTROL.COPY_CENTROID and COPY_SAMPLE select the sample that DEPTH_COPY or STENCIL_COPY copies. Forspoken (PPSA03984) draws with DB_RENDER_CONTROL 0xc0, DEPTH_COMPRESS_DISABLE and COPY_CENTROID with neither copy bit set, and stopped at 43 s because both DepthMaintenanceRejection and decodeDepth took the selection for a copy. Accept the selection fields when no copy is enabled; draws that copy are still rejected. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp | 6 +++--- core/libs/prx/libSceAgcDriver/tests/Graphics.cpp | 11 ++++++++--- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index c16eb7847a..bca927bdeb 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -256,7 +256,7 @@ VkStencilOpState stencilFace(std::uint32_t compare, std::uint32_t ops, std::uint } void decodeDepth(const Registers& cx, std::uint32_t depthControl, bool testValExport, State& result) { - zero(cx, 0x000, 0x00001f9cu, "depth copy, resummarize or decompress draws (DB_RENDER_CONTROL)"); + zero(cx, 0x000, 0x0000101cu, "depth copy, resummarize or decompress draws (DB_RENDER_CONTROL)"); const bool depthClear = (read(cx, 0x000) & 1u) != 0; const bool stencilClear = (read(cx, 0x000) & 2u) != 0; const auto view = read(cx, 0x002); @@ -912,10 +912,10 @@ bool DepthResummarizeDrawsNothing(const QueueState& queue) { std::string DepthMaintenanceRejection(const QueueState& queue) { if (DepthResummarizeDrawsNothing(queue)) return {}; const auto control = find(queue.context, 0x000); - if (control == queue.context.end() || (control->second & ~0x2063u) == 0) return {}; + if (control == queue.context.end() || (control->second & ~0x2fe3u) == 0) return {}; const auto depthControl = find(queue.context, 0x200); const bool colorMasks = find(queue.context, 0x8e) != queue.context.end() && find(queue.context, 0x8f) != queue.context.end(); - if ((control->second & ~0x2063u) == 0x10u && depthControl != queue.context.end() && (depthControl->second & 3u) == 0 && colorMasks && ColorWriteMask(queue.context) == 0) return {}; + if ((control->second & ~0x2fe3u) == 0x10u && depthControl != queue.context.end() && (depthControl->second & 3u) == 0 && colorMasks && ColorWriteMask(queue.context) == 0) return {}; return zeroMessage(0x000, control->second, "DB_RENDER_CONTROL depth copy, resummarize or decompress"); } diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index c7d2efd679..1fd80556df 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -1886,7 +1886,12 @@ void DepthStencilTests() { queue.context[0x000] = 0x23; state = AgcDriver::Graphics::DecodeState(queue); Require(state.depthWrite && state.depthCompare == VK_COMPARE_OP_ALWAYS && clears(state.stencilFront), "a combined depth and stencil clear draw does not clear both planes"); - for (const auto control : {4u, 8u}) { + for (const auto select : {0xc0u, 0xf00u}) { + queue.context[0x000] = 0x23u | select; + state = AgcDriver::Graphics::DecodeState(queue); + Require(state.depthWrite && state.depthCompare == VK_COMPARE_OP_ALWAYS && clears(state.stencilFront), "a copy sample selection without a copy changed a clear draw"); + } + for (const auto control : {4u, 8u, 0x84u, 0x108u}) { queue.context[0x000] = 0x22u | control; expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "DB_RENDER_CONTROL"); } @@ -2044,7 +2049,7 @@ void memoryValueFillTests() { } void depthMaintenanceTests() { - for (const auto mode : {0x4u, 0x8u, 0x80u, 0x100u, 0x1000u, 0x4000u}) { + for (const auto mode : {0x4u, 0x8u, 0x84u, 0x108u, 0xf88u, 0x1000u, 0x4000u}) { for (const auto clear : {0u, 1u, 2u, 3u}) { auto queue = makeState(); queue.context[0x000] = mode | clear; @@ -2058,7 +2063,7 @@ void depthMaintenanceTests() { expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, reason); } } - for (const auto control : {0u, 1u, 2u, 3u, 0x20u, 0x40u, 0x2000u, 0x2063u}) { + for (const auto control : {0u, 1u, 2u, 3u, 0x20u, 0x40u, 0x80u, 0xc0u, 0x100u, 0xf00u, 0x2000u, 0x2063u, 0x2fe3u}) { auto queue = makeState(); queue.context[0x000] = control; Require(AgcDriver::Graphics::DepthMaintenanceRejection(queue).empty(), "ordinary depth controls were mistaken for maintenance"); From 92f170c6738d7b45015e93aaaa2e2e7980b00b9c Mon Sep 17 00:00:00 2001 From: milescoviello Date: Wed, 14 Oct 2026 17:51:44 +0000 Subject: [PATCH 0378/1000] fix(agc): treat a view past a depth surface's first level as memory A depth surface is one level: the driver rejects draws that render a depth mip. A sampled or storage view at the surface's address and extent that starts at a later level, or a storage access to a later level, does not touch that level, so it is not the depth plane. DepthSurfaceTexture and DepthSurfaceStorage still sent such views to the surface, which rejected the level. Forspoken (PPSA03984) builds a depth pyramid from its 1984x1116 D32 depth surface with a compute shader that samples one level of a five-level R32F view and stores the next, and stopped there. Both now leave such views to the texture and storage caches, as they already do for a view of another extent or format. Co-Authored-By: Claude Opus 5.5 --- .../libSceAgcDriver/Graphics/src/DepthSurface.cpp | 14 +++++--------- .../libSceAgcDriver/tests/DepthSurfaceReuse.cpp | 8 +++++++- .../tests/execution/DepthStorage.cpp | 8 +++++++- 3 files changed, 19 insertions(+), 11 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp index b15945ee89..eeabf5ec29 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp @@ -380,7 +380,7 @@ public: return ResolveTextureFormat(resource.format) == expected || depthBits; } - void RequireStorable(const GuestTextureResource& resource, std::uint32_t mip) const { + void RequireStorable(const GuestTextureResource& resource) const { const bool stencil = target.stencilAddress != 0 && resource.baseAddress == target.stencilAddress; Require(target.samples == 1, "storage image access to a multisampled depth surface is not implemented"); if (!stencil && hostFormat != target.format) { @@ -388,11 +388,6 @@ public: std::snprintf(text, sizeof(text), "AGC graphics: storage image access to the depth plane of depth surface 0x%llx (vk format %d) stored as vk format %d is not implemented", static_cast(target.address), static_cast(target.format), static_cast(hostFormat)); throw std::runtime_error(text); } - if (mip != 0 || resource.baseLevel != 0) { - char text[224]; - std::snprintf(text, sizeof(text), "AGC graphics: storage image access to level %u of the %s plane of depth surface 0x%llx is not implemented", mip, stencil ? "stencil" : "depth", static_cast(target.address)); - throw std::runtime_error(text); - } } std::shared_ptr Storage(std::span words, const GuestTextureResource& resource, std::uint32_t mip) { @@ -404,7 +399,7 @@ public: std::snprintf(text, sizeof(text), "AGC graphics: storage image access to depth/stencil surface 0x%llx is not implemented (storage=%ux%u guest format=%u mip=%u dimension=%d tile=%d)", static_cast(resource.baseAddress), resource.width, resource.height, resource.format, mip, static_cast(resource.dimension), static_cast(resource.tileMode)); throw std::runtime_error(text); } - RequireStorable(resource, mip); + RequireStorable(resource); ApplyFastClear(); auto& plane = stencil ? stencilPlane : depthPlane; if (plane.storage == nullptr || plane.storage->Descriptor().format != resource.format || plane.storage->Descriptor().tileMode != resource.tileMode || plane.storage->Descriptor().baseAddress != resource.baseAddress || plane.storage->Descriptor().dimension != resource.dimension) { @@ -1343,7 +1338,7 @@ std::shared_ptr DepthSurfaceTexture(const Context& context, std::spanSampled(words, resource, components); } - if (resource.width != base.extent.width || resource.height != base.extent.height) return nullptr; + if (resource.width != base.extent.width || resource.height != base.extent.height || resource.baseLevel != 0) return nullptr; const bool d16 = base.format == VK_FORMAT_D16_UNORM || base.format == VK_FORMAT_D16_UNORM_S8_UINT; const auto format = ResolveTextureFormat(resource.format); const bool depthBits = !stencil && words.size() >= 4 && ShaderRecompiler::DepthBitsTextureWidth(words[1], words[3]) == (d16 ? 16u : 32u); @@ -1468,7 +1463,7 @@ std::shared_ptr layeredDepthStorage(const Context& context, Dept const auto layers = resource.depthOrLastArray + 1u; const bool d16 = target.format == VK_FORMAT_D16_UNORM || target.format == VK_FORMAT_D16_UNORM_S8_UINT; const auto bytesPerTexel = d16 ? 2u : 4u; - surface.RequireStorable(resource, mip); + surface.RequireStorable(resource); const auto geometry = DescribeSurface(resource); if (layers > 1 && geometry.GuestLayerOffset(1) != DepthSliceBytes(target.extent, bytesPerTexel)) { char text[256]; @@ -1487,6 +1482,7 @@ std::shared_ptr layeredDepthStorage(const Context& context, Dept } std::shared_ptr DepthSurfaceStorage(const Context& context, std::span words, const GuestTextureResource& resource, std::uint32_t mip) { + if (mip != 0 || resource.baseLevel != 0) return nullptr; std::lock_guard lock(surfacesMutex()); const auto& list = surfaces(); const auto found = std::find_if(list.rbegin(), list.rend(), [&](const auto& surface) { diff --git a/core/libs/prx/libSceAgcDriver/tests/DepthSurfaceReuse.cpp b/core/libs/prx/libSceAgcDriver/tests/DepthSurfaceReuse.cpp index 538f42295e..ebb60efe01 100644 --- a/core/libs/prx/libSceAgcDriver/tests/DepthSurfaceReuse.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/DepthSurfaceReuse.cpp @@ -216,6 +216,9 @@ struct View { std::uint32_t tileMode = TileDepth64KB; std::uint32_t type = Type2D; std::uint32_t lastArray = 0; + std::uint32_t baseLevel = 0; + std::uint32_t lastLevel = 0; + std::uint32_t maxMip = 0; }; std::array tsharp(const View& view) { @@ -226,8 +229,9 @@ std::array tsharp(const View& view) { words[0] = static_cast(base); words[1] = static_cast((base >> 32u) & 0xffu) | ((view.format & 0x1ffu) << 20u) | ((widthMinus1 & 0x3u) << 30u); words[2] = ((widthMinus1 >> 2u) & 0xfffu) | ((heightMinus1 & 0x3fffu) << 14u); - words[3] = 4u | (5u << 3u) | (6u << 6u) | (7u << 9u) | ((view.tileMode & 0x1fu) << 20u) | ((view.type & 0xfu) << 28u); + words[3] = 4u | (5u << 3u) | (6u << 6u) | (7u << 9u) | ((view.baseLevel & 0xfu) << 12u) | ((view.lastLevel & 0xfu) << 16u) | ((view.tileMode & 0x1fu) << 20u) | ((view.type & 0xfu) << 28u); words[4] = view.lastArray & 0x1fffu; + words[5] = (view.maxMip & 0xfu) << 4u; return words; } @@ -403,6 +407,8 @@ void RunDepthSurfaceReuseTests() { Require(lookup(context, {depth32, FormatR32Float, 512, 512, TileDepth64KB, Type2DArray, 5}) == nullptr, "a 2D array view of another extent over a depth surface must be read as reused memory"); Require(lookup(context, {depth32, FormatR32Uint, 384, 384, TileRenderTarget64KB}) == nullptr, "an R32 uint view without a depth layout must be read as reused memory"); Require(lookup(context, {depth16, FormatR32Float, 256, 256}) == nullptr, "an R32F view of a D16 surface's own extent must be read as reused memory"); + Require(lookup(context, {depth32, FormatR32Float, 384, 384, TileDepth64KB, Type2D, 0, 0, 0, 4}) != nullptr, "the first level of a mipmapped R32F view of a D32 surface's own extent must sample its depth plane"); + Require(lookup(context, {depth32, FormatR32Float, 384, 384, TileDepth64KB, Type2D, 0, 1, 1, 4}) == nullptr, "a view that starts past the first level of a D32 surface's own extent must be read as reused memory"); Require(lookup(context, {stencil, FormatR8Uint, 320, 180}) != nullptr, "an 8-bit view of a stencil plane's own extent must sample the stencil plane"); Require(lookup(context, {stencil, FormatR8G8B8A8Unorm, 320, 180, TileRenderTarget64KB}) == nullptr, "an RGBA8 view of a stencil plane's own extent must be read as reused memory"); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DepthStorage.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DepthStorage.cpp index f83e70edb5..237d3f7646 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DepthStorage.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DepthStorage.cpp @@ -199,7 +199,13 @@ void Run(Context context, std::uint8_t* block) { Require(DepthSurfaceStorage(context, {}, PlaneResource(address, Format32Float, Side / 2), 0) == nullptr, "a storage view of another extent over the depth memory was taken for the depth surface"); Require(DepthSurfaceTexture(context, {}, PlaneResource(address, Format32Float, Side / 2), {}) == nullptr, "a sampled view of another extent over the depth memory was taken for the depth surface"); Require(DepthSurfaceStorage(context, {}, PlaneResource(address, Format8Uint), 0) == nullptr, "an 8-bit storage view of the depth plane was taken for the depth surface"); - ExpectFailure([&] { static_cast(DepthSurfaceStorage(context, {}, PlaneResource(address, Format32Float), 1)); }, "not implemented", "a storage view of another level of the depth plane was accepted"); + Require(DepthSurfaceStorage(context, {}, PlaneResource(address, Format32Float), 1) == nullptr, "a storage access to a later level over the depth memory was taken for the depth surface"); + auto later = PlaneResource(address, Format32Float); + later.mipCount = 2; + later.baseLevel = 1; + later.lastLevel = 1; + Require(DepthSurfaceStorage(context, {}, later, 0) == nullptr, "a storage view of a later level over the depth memory was taken for the depth surface"); + Require(DepthSurfaceTexture(context, {}, later, {}) == nullptr, "a sampled view of a later level over the depth memory was taken for the depth surface"); const auto depth = DepthSurfaceStorage(context, {}, PlaneResource(address, Format32Float), 0); Require(depth != nullptr, "the depth plane has no storage image"); submit(recorder); From 49919d7f32f3115d82b9f8321c443d09ebbd0b2e Mon Sep 17 00:00:00 2001 From: Alex Collini Date: Thu, 15 Oct 2026 18:56:30 +0200 Subject: [PATCH 0379/1000] fix(agc): drop resource-cache entries that hold released sampled textures A resource-cache entry keeps every sampled texture of its descriptor sets alive. The sampled texture cache passes over textures something else holds and, past its ceiling, drops them anyway; textures it makes without caching are never in it. Entries holding either kept them alive with no budget counting them, bounded only by the 1,024-entry and 65,536-element counts, until device memory ran out. The texture cache now marks such textures released. ResourceCache::Insert refuses descriptor sets holding one and hands the entries that hold one back to the caller, like RemoveImage does for evicted storage images. Co-Authored-By: Claude Opus 5.5 --- .../Graphics/include/ShaderResources.hpp | 2 + .../Graphics/include/Texture.hpp | 3 ++ .../Graphics/src/ShaderResources.cpp | 32 +++++++++++++- .../tests/execution/SampledCacheBudget.cpp | 43 +++++++++++++++++++ docs/dev/debt/libSceAgcDriver.md | 1 + 5 files changed, 80 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp index efefda3a7f..409580699e 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp @@ -196,6 +196,7 @@ public: std::vector> DeviceReads() const { return guestMemory.DeviceReads(); } bool ReadsImage(const StorageTexture* image) const; bool HasCachedStorageImages() const; + bool HoldsReleasedTexture() const; std::uint32_t TableElements() const { return tableElements[0] + tableElements[1]; } const std::vector& LayoutKey() const { return layoutKey; } const std::vector& FragmentLayoutKey() const { return fragmentLayoutKey; } @@ -634,6 +635,7 @@ public: private: void erase(const Key& key); void noteMiss(const Key& key); + void dropReleasedTextures(std::vector>* evicted); struct KeyHash { std::size_t operator()(const Key& key) const noexcept { std::uint64_t hash = 14695981039346656037ull; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp index bc6efaf720..3ee21eb426 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp @@ -109,6 +109,8 @@ public: PerUseCopy CopyKind() const { return perUseCopy; } bool RefreshedPerUse() const { return perUseCopy == PerUseCopy::RemadePerUse; } void MarkRefreshedPerUse() { perUseCopy = PerUseCopy::RemadePerUse; } + bool Released() const { return released.load(std::memory_order_acquire); } + void MarkReleased() { released.store(true, std::memory_order_release); } private: void release() noexcept; @@ -142,6 +144,7 @@ private: std::shared_ptr storageSource; std::unique_ptr upload; PerUseCopy perUseCopy = PerUseCopy::None; + std::atomic released{false}; bool copiedOnce = false; std::uint64_t copiedVersion = 0; VkFormat depthFormat = VK_FORMAT_UNDEFINED; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index 15a0f3c2d2..dced3d585e 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -172,6 +172,16 @@ void noteUncached(const std::shared_ptr& image) { uncached.any.store(true, std::memory_order_release); } +std::atomic& TexturesReleased() { + static std::atomic released{false}; + return released; +} + +void noteReleased(const std::shared_ptr& texture) { + texture->MarkReleased(); + TexturesReleased().store(true, std::memory_order_release); +} + bool SamplerCacheDisabled() { static const bool disabled = std::getenv("APS5_NO_SAMPLER_CACHE") != nullptr; return disabled; @@ -203,6 +213,7 @@ void eraseTexture(TextureCache& cache, std::list::iterator it) { } } } + if (it->texture.use_count() > 1) noteReleased(it->texture); cache.index.erase(it->key); cache.entries.erase(it); ++cache.erasures; @@ -797,6 +808,7 @@ std::shared_ptr cachedTexture(const Context& context, std::span resources, std::vector>* evicted) { std::lock_guard lock(mutex); - if (!resources->HasCachedStorageImages()) return; + dropReleasedTextures(evicted); + if (!resources->HasCachedStorageImages() || resources->HoldsReleasedTexture()) return; if (const auto found = index.find(key); found != index.end()) { if (evicted != nullptr) evicted->push_back(std::move(found->second->second)); entries.erase(found->second); @@ -3316,6 +3329,19 @@ void ResourceCache::RemoveImage(const StorageTexture* image, std::vector>* evicted) { + if (!TexturesReleased().exchange(false, std::memory_order_acq_rel)) return; + for (auto it = entries.begin(); it != entries.end();) { + if (!it->second->HoldsReleasedTexture()) { + ++it; + continue; + } + if (evicted != nullptr) evicted->push_back(std::move(it->second)); + index.erase(it->first); + it = entries.erase(it); + } +} + bool ResourceCache::Touch(const Key& key) { resourceCacheTouches.fetch_add(1, std::memory_order_relaxed); std::lock_guard lock(mutex); @@ -4719,6 +4745,10 @@ bool ShaderResources::HasCachedStorageImages() const { return std::none_of(textures.begin(), textures.end(), [](const auto& texture) { return texture != nullptr && texture->StorageSource() != nullptr && !texture->StorageSource()->Cached(); }); } +bool ShaderResources::HoldsReleasedTexture() const { + return std::any_of(textures.begin(), textures.end(), [](const auto& texture) { return texture != nullptr && texture->Released(); }); +} + bool ShaderResources::ReadsImage(const StorageTexture* image) const { if (image == nullptr) return false; for (const auto& texture : textures) { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp index 9d1978dbb2..d8a6076a3a 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp @@ -424,6 +424,48 @@ void heldTests(const Context& context) { Require(TextureCacheUsage().sampledEntries == capacity && TextureCacheUsage().sampledBytes <= Budget, "released textures were not evicted down to the budget"); } +void releasedTests(Context context) { + PlaceholderImages placeholders(context); + context.placeholderImages = &placeholders; + TextureCache textures(context); + context.textureCache = &textures; + ClearCachedTextures(context.device); + auto& cache = SharedResourceCache(); + cache.Clear(); + try { + Block block(context); + auto pinned = ImageResources(context, block, 0, 1, true); + Require(pinned->SampledTextures().size() == 1 && !pinned->SampledTextures().front()->ViewsStorageImage(), "the pinned descriptor set does not hold a sampled snapshot"); + const std::weak_ptr texture = pinned->SampledTextures().front(); + const std::weak_ptr pinnedSet = pinned; + cache.Insert({1}, pinned); + Require(cache.Find({1}) == pinned, "the resource cache rejected a descriptor set whose texture is cached"); + pinned.reset(); + ClearCachedTextures(context.device); + Require(!texture.expired() && texture.lock()->Released(), "the sampled texture cache dropped a texture the resource cache holds without marking it released"); + auto live = ImageResources(context, block, 1, 1, true); + std::vector> displaced; + cache.Insert({2}, live, &displaced); + Require(cache.Find({1}) == nullptr && cache.Find({2}) == live && cache.Size() == 1, "the resource cache kept an entry holding a released texture"); + Require(displaced.size() == 1 && displaced.front() == pinnedSet.lock(), "the entry holding a released texture was not handed back to the caller"); + displaced.clear(); + Require(pinnedSet.expired() && texture.expired(), "the resource cache kept a released texture alive"); + auto stale = ImageResources(context, block, 2, 1, true); + ClearCachedTextures(context.device); + Require(stale->SampledTextures().front()->Released(), "a held texture the sampled cache dropped is not marked released"); + cache.Insert({3}, stale, &displaced); + Require(cache.Find({3}) == nullptr, "a descriptor set holding a released texture entered the resource cache"); + Require(cache.Size() == 0 && displaced.size() == 1 && displaced.front() == live, "the live entry whose texture was released stayed in the resource cache"); + displaced.clear(); + } catch (...) { + cache.Clear(); + throw; + } + cache.Clear(); + ClearCachedTextures(context.device); + std::puts("resource cache released texture tests passed"); +} + void rescanTests(const Context& context) { constexpr std::uint32_t SmallSide = 128; constexpr std::size_t SmallBytes = std::size_t{SmallSide} * SmallSide * 4u; @@ -638,6 +680,7 @@ int main(int argc, char** argv) { reportedBudgetTests(context); snapshotTests(context); heldTests(context); + releasedTests(context); rescanTests(context); viewEvictionTests(context); viewTests(context); diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 3edcde24ff..4dce0d2b6e 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -150,3 +150,4 @@ - [Flip packets raise the graphics end-of-pipe event](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp) (libSceAgcDriver) - a flip delivers the queue's end-of-pipe event (`sceAgcDriverAddEqEvent`) once the work recorded before it completes, as a `RELEASE_MEM` with `INT_SEL` does. Minecraft (PPSA17221) registers the graphics event, never requests an interrupt in its releases, and waits on the event after every flip while holding its render lock, so without this it never presents a second frame. Whether the console raises the event from the flip itself or from an interrupt its flip sequence contains is not measured; a title that waits only on release interrupts sees one extra event per flip, with `data` counting it. - [Host import windows and budget](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver): a registered range larger than 64 MiB is imported one 64 MiB window at a time for buffer and fill accesses; an access across window edges imports the cells it touches, grown to the union of the range's imports it overlaps. An address-based build (`AcquireRegistered`) imports the range whole and replaces its windows. Live imports stay under a budget: `APS5_HOST_IMPORT_MIB`, else the larger of 6 GiB and half the physical memory, at most the largest host heap less 2 GiB (imports and type-3 mirrors share it; #167's rule). A new import beyond it retires the least recently used imports not used in the last 256 lookups; one that would not fit even then is refused before anything is retired, and imports made when a range is mapped never evict. A refused import is tried again at its next use. The 64 MiB window, the 256-lookup protection and the 2 GiB reserve are choices, not measured. - [Registration preparation at submit](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp) (libSceAgcDriver) - `Driver::Submit` waits for every registration-time shader preparation queued before the call, not only those of the shaders the commands use, so a submission behind a long queue of unrelated preparations waits for all of them on the submitting thread. Waiting there instead of in the queue worker keeps the GPU side of a submission short: a title that sees a submission take seconds treats the GPU as hung (Goat Simulator 3, PPSA04159, then stops submitting and its garbage collector fails a 10 s timeout). On the console shaders are compiled ahead of time, so neither wait exists; how long a title tolerates a slow submit call is not known. +- [Resource cache entries holding released textures](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - a sampled texture the texture cache drops while something else holds it, or makes without caching (`admitTexture` refusing a first use), is marked released, and `ResourceCache::Insert` refuses descriptor sets that hold one and removes the entries that do. The removal waits for the next insert: until then `Find` still returns those entries and their textures stay alive. One process-wide flag starts the sweep, so while the sampled cache is over its budget every insert scans all entries (up to 1,024, each with its textures); that cost is not measured. A draw or dispatch whose textures the cache refused builds its descriptor sets again on each use for as long as the cache stays full; the count bounds (`APS5_RESOURCE_CACHE_ENTRIES`, `APS5_RESOURCE_CACHE_TABLE_ELEMENTS`) still bound nothing by bytes, and sampled textures made outside the texture cache (the disabled cache, depth-surface copies) are never marked. Measured only on Ghost of Yōtei (PPSA26344), and before this change with the env bounds lowered as a stand-in: with 256 entries and 16,384 table elements, sampled textures held outside the cache fell from up to 7,830 MiB to 2,541 MiB at the wolf scene and device-local heap 0 stayed under its 23 GB budget. From 511db2e8ffc7549758e0b4ab42a57292588153c8 Mon Sep 17 00:00:00 2001 From: notaxeltv Date: Wed, 14 Oct 2026 23:34:52 +0200 Subject: [PATCH 0380/1000] feat(libc): export locale::classic for the C locale Absent PPSA34015 import; return a stable facade over the classic _Locimp. Co-authored-by: Cursor --- core/libs/prx/libc/src/LocaleSupport.cpp | 11 +++++++++++ core/libs/tests/GuestLocale.cpp | 5 +++++ 2 files changed, 16 insertions(+) diff --git a/core/libs/prx/libc/src/LocaleSupport.cpp b/core/libs/prx/libc/src/LocaleSupport.cpp index a464f6881f..dfb0d8e1fb 100644 --- a/core/libs/prx/libc/src/LocaleSupport.cpp +++ b/core/libs/prx/libc/src/LocaleSupport.cpp @@ -333,6 +333,17 @@ GuestLocale::Implementation* APS5_VABI _ZNSt6locale16_GetgloballocaleEv_nid_post return &g_classicLocale; } +namespace { +struct GuestLocaleFacade { + GuestLocale::Implementation* implementation; +}; +GuestLocaleFacade g_classicLocaleFacade{&g_classicLocale}; +} + +GuestLocaleFacade* APS5_VABI _ZNSt6locale7classicEv_nid_postfix() { + return &g_classicLocaleFacade; +} + std::size_t APS5_VABI _ZNSt7collateIwE7_GetcatEPPKNSt6locale5facetEPKS1__nid_postfix(GuestLocale::Facet** facet, const GuestLocale::Implementation* const* locale) { if (facet != nullptr && *facet == nullptr) { if (locale == nullptr || *locale == nullptr || (*locale)->name == nullptr || std::strcmp((*locale)->name, "C") != 0) throw std::invalid_argument("collate::_Getcat: only the C locale is supported"); diff --git a/core/libs/tests/GuestLocale.cpp b/core/libs/tests/GuestLocale.cpp index 93cf43ff11..04ddac5f38 100644 --- a/core/libs/tests/GuestLocale.cpp +++ b/core/libs/tests/GuestLocale.cpp @@ -13,6 +13,8 @@ extern "C" { extern GuestLocale::Implementation* _ZSt21_sceLibcClassicLocale_nid_postfix; GuestLocale::Implementation* APS5_VABI _ZNSt6locale16_GetgloballocaleEv_nid_postfix(); GuestLocale::Implementation* APS5_VABI _ZNSt6locale5_InitEv_nid_postfix(); +struct GuestLocaleFacade { GuestLocale::Implementation* implementation; }; +GuestLocaleFacade* APS5_VABI _ZNSt6locale7classicEv_nid_postfix(); void APS5_VABI _ZNSt6locale5facet9_RegisterEv_nid_postfix(GuestLocale::Facet* self); void APS5_VABI _ZNSt8_LocinfoC1EPKc_nid_postfix(GuestLocale::LocinfoStorage* self, const char* localeName); void APS5_VABI _ZNSt8_LocinfoD1Ev_nid_postfix(GuestLocale::LocinfoStorage* self); @@ -49,6 +51,9 @@ static void CheckGuestCalls() { Require(locale != nullptr && locale == _ZNSt6locale16_GetgloballocaleEv_nid_postfix()); Require(_ZNSt6locale5_InitEv_nid_postfix() == locale); Require(_ZNSt6locale5_InitEv_nid_postfix() == locale); + auto* classic = _ZNSt6locale7classicEv_nid_postfix(); + Require(classic != nullptr && classic->implementation == locale); + Require(_ZNSt6locale7classicEv_nid_postfix() == classic); const std::byte* table = nullptr; std::memcpy(&table, locale, sizeof(table)); void (APS5_VABI *retain)(GuestLocale::Facet*) = nullptr; From 5c378ece37c71dc2178f2c5a32235a4d7bb9f718 Mon Sep 17 00:00:00 2001 From: Zinebyte <197974009+Zinebyte@users.noreply.github.com> Date: Wed, 14 Oct 2026 18:12:39 -0400 Subject: [PATCH 0381/1000] fix(relinker): preserve weak imports in macOS guest modules --- core/relinker/CMakeLists.txt | 6 + .../src/macos/MacOsGuestModuleWriter.cpp | 2 +- .../tests/test_macos_guest_weak_imports.py | 142 ++++++++++++++++++ 3 files changed, 149 insertions(+), 1 deletion(-) create mode 100644 core/relinker/relinker/tests/test_macos_guest_weak_imports.py diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index 784fc11985..436f43913c 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -717,3 +717,9 @@ if(BUILD_TESTING) add_test(NAME reloc_table_bounds COMMAND reloc_table_bounds_tests $) endif() endif() + +if(BUILD_TESTING AND Python3_Interpreter_FOUND) + add_test(NAME macos_guest_weak_imports COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_macos_guest_weak_imports.py $) + set_property(TEST macos_guest_weak_imports APPEND PROPERTY LABELS relinker) +endif() diff --git a/core/relinker/elfpatcher/src/macos/MacOsGuestModuleWriter.cpp b/core/relinker/elfpatcher/src/macos/MacOsGuestModuleWriter.cpp index bd5928a33a..74e31d19d3 100644 --- a/core/relinker/elfpatcher/src/macos/MacOsGuestModuleWriter.cpp +++ b/core/relinker/elfpatcher/src/macos/MacOsGuestModuleWriter.cpp @@ -83,7 +83,7 @@ std::vector GuestModuleWriter::WriteMacOs(const Relinker::GuestIma if (symbol.Name.empty()) throw Domain::RelinkerException("Empty guest import", target); image.WritePointer(target, 0); const auto name = guest.UsePlatformTlsResolver && symbol.Name == "vNe1w4diLCs" ? std::string("Aps5GuestTlsGetAddr_nid_no_patch") : DynamicName(guest, symbolIndex); - input.Binds.push_back({"_" + name, rva, static_cast(addend)}); + input.Binds.push_back({"_" + name, rva, static_cast(addend), (symbol.Info >> 4) == 2}); } } else throw Domain::RelinkerException("Unsupported macOS guest relocation " + std::to_string(type), target); } diff --git a/core/relinker/relinker/tests/test_macos_guest_weak_imports.py b/core/relinker/relinker/tests/test_macos_guest_weak_imports.py new file mode 100644 index 0000000000..7a0f2dc432 --- /dev/null +++ b/core/relinker/relinker/tests/test_macos_guest_weak_imports.py @@ -0,0 +1,142 @@ +import os +import platform +import shutil +import struct +import subprocess +import sys +import tempfile +from pathlib import Path + +from test_guest_intel_trampolines import main_fixture +from test_guest_symbol_names import guest_module, string_table +from test_macos_output import MachO + +STB_GLOBAL = 1 +STB_WEAK = 2 +OPTIONAL = 'AAAAAAAAAAA' +REQUIRED = 'BBBBBBBBBBB' +ENTRY = 'ProbeOptional' +HOST = 'libWeakHost.prx' + + +def macos_guest_module(binding): + image = guest_module([(OPTIONAL, False), (REQUIRED, False), (ENTRY, True)]) + strings, offsets = string_table((OPTIONAL, REQUIRED, ENTRY, HOST)) + image[0x800:0x800 + len(strings)] = strings + struct.pack_into('\n' + '#include \n' + '\n' + 'int main(int argc, char **argv) {\n' + ' if (argc == 1) return 0;\n' + ' void *module = dlopen(argv[1], RTLD_NOW | RTLD_LOCAL);\n' + ' if (!module) { fprintf(stderr, "%s\\n", dlerror()); return 1; }\n' + ' dlerror();\n' + ' int (*entry)(void) = (int (*)(void))dlsym(module, argv[2]);\n' + ' const char *error = dlerror();\n' + ' if (error) { fprintf(stderr, "%s\\n", error); return 2; }\n' + ' printf("%d\\n", entry());\n' + ' return 0;\n' + '}\n' + ) + result = subprocess.run(['xcrun', 'clang', '-arch', 'x86_64', str(source), '-o', str(probe)], + capture_output=True, text=True, timeout=20) + assert result.returncode == 0, (result.stdout, result.stderr) + command = (['/usr/bin/arch', '-x86_64'] if platform.machine() == 'arm64' else []) + [str(probe)] + result = subprocess.run(command, capture_output=True, text=True, timeout=10) + if result.returncode != 0 and 'Bad CPU type in executable' in result.stderr: + assert os.environ.get('GITHUB_ACTIONS') != 'true', 'macOS CI must execute the native x86_64 weak-import probe' + print('macOS guest weak imports: x86_64 execution unavailable') + return None + assert result.returncode == 0, (result.returncode, result.stdout, result.stderr) + kernel_source, kernel = work / 'kernel.c', work / 'libkernel.prx' + kernel_source.write_text('int Anyps5TestKernel(void) { return 0; }\n') + result = subprocess.run(['xcrun', 'clang', '-arch', 'x86_64', '-dynamiclib', str(kernel_source), + '-Wl,-install_name,@rpath/libkernel.prx', '-o', str(kernel)], + capture_output=True, text=True, timeout=20) + assert result.returncode == 0, (result.stdout, result.stderr) + return command, kernel + + +def check_macos_guest_weak_imports(relinker, work): + native = macos_native_probe(work) + for name, binding, optional, required, expected, missing in ( + ('weak-missing', STB_WEAK, False, True, 42, None), + ('weak-present', STB_WEAK, True, True, 77, None), + ('required-missing', STB_WEAK, True, False, None, REQUIRED), + ('global-missing', STB_GLOBAL, False, True, None, OPTIONAL)): + case = work / ('macos-guest-' + name) + modules, libraries = case / 'sce_module', case / 'libs' + modules.mkdir(parents=True) + libraries.mkdir() + (modules / 'optional.prx').write_bytes(macos_guest_module(binding)) + source, output = case / 'input.elf', case / 'output' + source.write_bytes(main_fixture()) + result = subprocess.run([str(relinker), '--macos', str(source), str(output)], + capture_output=True, text=True, timeout=30) + assert result.returncode == 0, (name, result.stdout, result.stderr) + converted = case / 'app0/sce_module/optional.prx.guest.prx' + image = converted.read_bytes() + macho = MachO(image) + offset, size = macho.dyld_info[2:4] + stream = image[offset:offset + size] + for symbol, opcode in ((OPTIONAL, 0x41 if binding == STB_WEAK else 0x40), (REQUIRED, 0x40)): + at = stream.find(('_' + symbol).encode() + b'\0') + assert at > 0 and stream[at - 1] == opcode, (name, symbol, stream) + assert '@rpath/' + HOST in macho.dylibs, (name, macho.dylibs) + if native: + command, kernel = native + shutil.copyfile(kernel, libraries / 'libkernel.prx') + provider_source, host = case / 'provider.c', libraries / HOST + provider_source.write_text((f'int {OPTIONAL}(void) {{ return 77; }}\n' if optional else '') + + (f'int {REQUIRED}(void) {{ return 42; }}\n' if required else '')) + result = subprocess.run(['xcrun', 'clang', '-arch', 'x86_64', '-dynamiclib', str(provider_source), + '-Wl,-install_name,@rpath/' + HOST, '-o', str(host)], + capture_output=True, text=True, timeout=20) + assert result.returncode == 0, (name, result.stdout, result.stderr) + run = subprocess.run([*command, str(converted), ENTRY + '#guest'], + capture_output=True, text=True, timeout=10) + if expected is None: + assert run.returncode != 0 and missing in run.stderr, (name, run.returncode, run.stdout, run.stderr) + else: + assert run.returncode == 0 and run.stdout.strip() == str(expected), (name, run.returncode, run.stdout, run.stderr) + if name == 'weak-missing': + host.unlink() + run = subprocess.run([*command, str(converted), ENTRY + '#guest'], + capture_output=True, text=True, timeout=10) + assert run.returncode != 0 and HOST in run.stderr, (run.returncode, run.stdout, run.stderr) + if native: + print('macOS guest weak imports: five native x86_64 loader controls passed') + + +def main(): + relinker = Path(sys.argv[1]).resolve() + with tempfile.TemporaryDirectory(prefix='anyps5-macos-guest-weak-') as directory: + check_macos_guest_weak_imports(relinker, Path(directory)) + print('macOS guest weak import tests passed') + + +if __name__ == '__main__': + main() From 349420af7fb2ed7c738d0eefb54d0d4744172026 Mon Sep 17 00:00:00 2001 From: Alex Collini Date: Thu, 15 Oct 2026 19:28:43 +0200 Subject: [PATCH 0382/1000] docs(agc): record the measured effect of dropping entries with released textures Co-Authored-By: Claude Opus 5.5 --- docs/dev/debt/libSceAgcDriver.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 4dce0d2b6e..5662ba6a3e 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -150,4 +150,4 @@ - [Flip packets raise the graphics end-of-pipe event](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp) (libSceAgcDriver) - a flip delivers the queue's end-of-pipe event (`sceAgcDriverAddEqEvent`) once the work recorded before it completes, as a `RELEASE_MEM` with `INT_SEL` does. Minecraft (PPSA17221) registers the graphics event, never requests an interrupt in its releases, and waits on the event after every flip while holding its render lock, so without this it never presents a second frame. Whether the console raises the event from the flip itself or from an interrupt its flip sequence contains is not measured; a title that waits only on release interrupts sees one extra event per flip, with `data` counting it. - [Host import windows and budget](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver): a registered range larger than 64 MiB is imported one 64 MiB window at a time for buffer and fill accesses; an access across window edges imports the cells it touches, grown to the union of the range's imports it overlaps. An address-based build (`AcquireRegistered`) imports the range whole and replaces its windows. Live imports stay under a budget: `APS5_HOST_IMPORT_MIB`, else the larger of 6 GiB and half the physical memory, at most the largest host heap less 2 GiB (imports and type-3 mirrors share it; #167's rule). A new import beyond it retires the least recently used imports not used in the last 256 lookups; one that would not fit even then is refused before anything is retired, and imports made when a range is mapped never evict. A refused import is tried again at its next use. The 64 MiB window, the 256-lookup protection and the 2 GiB reserve are choices, not measured. - [Registration preparation at submit](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp) (libSceAgcDriver) - `Driver::Submit` waits for every registration-time shader preparation queued before the call, not only those of the shaders the commands use, so a submission behind a long queue of unrelated preparations waits for all of them on the submitting thread. Waiting there instead of in the queue worker keeps the GPU side of a submission short: a title that sees a submission take seconds treats the GPU as hung (Goat Simulator 3, PPSA04159, then stops submitting and its garbage collector fails a 10 s timeout). On the console shaders are compiled ahead of time, so neither wait exists; how long a title tolerates a slow submit call is not known. -- [Resource cache entries holding released textures](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - a sampled texture the texture cache drops while something else holds it, or makes without caching (`admitTexture` refusing a first use), is marked released, and `ResourceCache::Insert` refuses descriptor sets that hold one and removes the entries that do. The removal waits for the next insert: until then `Find` still returns those entries and their textures stay alive. One process-wide flag starts the sweep, so while the sampled cache is over its budget every insert scans all entries (up to 1,024, each with its textures); that cost is not measured. A draw or dispatch whose textures the cache refused builds its descriptor sets again on each use for as long as the cache stays full; the count bounds (`APS5_RESOURCE_CACHE_ENTRIES`, `APS5_RESOURCE_CACHE_TABLE_ELEMENTS`) still bound nothing by bytes, and sampled textures made outside the texture cache (the disabled cache, depth-surface copies) are never marked. Measured only on Ghost of Yōtei (PPSA26344), and before this change with the env bounds lowered as a stand-in: with 256 entries and 16,384 table elements, sampled textures held outside the cache fell from up to 7,830 MiB to 2,541 MiB at the wolf scene and device-local heap 0 stayed under its 23 GB budget. +- [Resource cache entries holding released textures](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - a sampled texture the texture cache drops while something else holds it, or makes without caching (`admitTexture` refusing a first use), is marked released, and `ResourceCache::Insert` refuses descriptor sets that hold one and removes the entries that do. The removal waits for the next insert: until then `Find` still returns those entries and their textures stay alive. One process-wide flag starts the sweep, so while the sampled cache is over its budget every insert scans all entries (up to 1,024, each with its textures); that cost is not measured. A draw or dispatch whose textures the cache refused builds its descriptor sets again on each use for as long as the cache stays full; the count bounds (`APS5_RESOURCE_CACHE_ENTRIES`, `APS5_RESOURCE_CACHE_TABLE_ELEMENTS`) still bound nothing by bytes, and sampled textures made outside the texture cache (the disabled cache, depth-surface copies) are never marked. Measured only on Ghost of Yōtei (PPSA26344) on one RTX 4090: with the default bounds, sampled textures held outside the cache peaked at 2,836 MiB (7,830 MiB before) and device-local heap 0 stayed under its 22.9 GB budget for the whole run (26,357 MiB before, which aborted with `VK_ERROR_OUT_OF_DEVICE_MEMORY` at the wolf scene). Whether the extra descriptor-set builds cost frame time elsewhere is not measured. From 8c3458759891a7770d369b018f720750498b1e02 Mon Sep 17 00:00:00 2001 From: Get-fix <100056769+Get-fix@users.noreply.github.com> Date: Sun, 11 Oct 2026 17:46:30 +0300 Subject: [PATCH 0383/1000] fix(relinker): search libc.prx for libkernel-only Windows executables AnyPS5 exports some libkernel functions, such as __error, from libc.prx, which libkernel.prx links. On Linux they resolve through that dependency. On Windows, the import stubs search only the listed modules, and libc.prx was listed only when libSceLibcInternal.prx was needed. A PS4 homebrew that needs libkernel.prx but no C library (the OpenOrbis graphics sample) failed with "unresolved ELF import 9BcDykPmo1I". libc.prx is now also listed when libkernel.prx is needed. PS5 executables need libSceLibcInternal.prx and convert unchanged. --- .../relinker/elfpatcher/src/windows/WindowsImportBuilder.cpp | 2 +- .../elfpatcher/tests/WindowsDependencyDiagnosticsTests.cpp | 5 +++-- docs/dev/debt/relinker.md | 1 + 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/core/relinker/elfpatcher/src/windows/WindowsImportBuilder.cpp b/core/relinker/elfpatcher/src/windows/WindowsImportBuilder.cpp index 3901ef1c17..ed4f72c88e 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsImportBuilder.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsImportBuilder.cpp @@ -52,7 +52,7 @@ std::vector WindowsImportBuilder::ReadLibraries(const Domain::SysVD // AnyPS5 implements the C runtime in libc.prx. Windows GetProcAddress // does not search a module's dependencies as ELF symbol lookup does. // Keep the requested module first, then make its shared runtime visible. - if (unique.contains("libSceLibcInternal.prx") && !unique.contains("libc.prx")) + if ((unique.contains("libSceLibcInternal.prx") || unique.contains("libkernel.prx")) && !unique.contains("libc.prx")) result.push_back("libc.prx"); return result; } diff --git a/core/relinker/elfpatcher/tests/WindowsDependencyDiagnosticsTests.cpp b/core/relinker/elfpatcher/tests/WindowsDependencyDiagnosticsTests.cpp index d23e355840..2635ae20e5 100644 --- a/core/relinker/elfpatcher/tests/WindowsDependencyDiagnosticsTests.cpp +++ b/core/relinker/elfpatcher/tests/WindowsDependencyDiagnosticsTests.cpp @@ -46,13 +46,14 @@ void checkRuntimeDependencies() { throw std::runtime_error("Incorrect Windows runtime dependency visibility"); }; check({}, {}); - check({"libkernel.prx"}, {"libkernel.prx"}); + check({"libkernel.prx"}, {"libkernel.prx", "libc.prx"}); + check({"libkernel.prx", "libc.prx"}, {"libkernel.prx", "libc.prx"}); check({"libSceLibcInternal.prx", "libkernel.prx"}, {"libSceLibcInternal.prx", "libkernel.prx", "libc.prx"}); check({"libc.prx", "libSceLibcInternal.prx"}, {"libc.prx", "libSceLibcInternal.prx"}); check({"libSceVideoOut.prx", "libkernel.prx", "libSceVideoOut.prx"}, - {"libSceVideoOut.prx", "libkernel.prx"}); + {"libSceVideoOut.prx", "libkernel.prx", "libc.prx"}); check({"libSceVideoOut.prx", "libSceVideoOut.prx"}, {"libSceVideoOut.prx"}); } diff --git a/docs/dev/debt/relinker.md b/docs/dev/debt/relinker.md index afce9f8d50..ab68d2155e 100644 --- a/docs/dev/debt/relinker.md +++ b/docs/dev/debt/relinker.md @@ -22,3 +22,4 @@ - `--trace-imports` ([entry stub](../../../core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp)) exists for Windows output only. Only imports bound through a PLT slot, or through a GOT slot whose symbol is a function, are traced; a function reached through any other relocation, or looked up at run time with `sceKernelDlsym`, is not, and neither are weak imports or the imports of modules loaded at run time - [ANDN lowering](../../../core/relinker/codegen/src/x86/AndnLowering.cpp) uses scalar NOT and AND for 32- and 64-bit register and memory operands under `--to-rosetta`, following the ANDN entry in [Intel SDM volume 2A](https://cdrdv2-public.intel.com/812383/253666-sdm-vol-2a.pdf). SF and ZF follow the result, CF and OF clear, and AF and PF are undefined. The scratch register is saved below the 128-byte red zone. A 64-bit stack-relative displacement that exceeds signed disp32 after reserving the spill frame throws; other BMI instructions are not lowered. The M1 test covers register overlap, RSP operands, memory addressing, RIP relocation, flags and red-zone preservation; precise fault delivery inside a spill sequence has not been verified. +- On macOS, [libc.prx](../../../core/relinker/elfpatcher/src/macos/MacOsMachOPatcher.cpp) is a dependency only when `libSceLibcInternal.prx` is needed, so a program that needs only `libkernel.prx` cannot resolve the libkernel functions that AnyPS5 exports from libc.prx, such as `__error`. Windows also lists libc.prx for `libkernel.prx`. From d6fe6551b2fd8e4417fb638c9a09a2c1c3210b72 Mon Sep 17 00:00:00 2001 From: ee6aba54 <340958984+ee6aba54@users.noreply.github.com> Date: Thu, 15 Oct 2026 20:13:28 +0200 Subject: [PATCH 0384/1000] fix(agc-driver): keep storage images with pending results out of the memory release Storing a storage image's pending results needs a staging buffer, and under memory pressure that buffer comes from the heap whose allocation just failed. The release turned to dirty images once the clean ones did not cover the request, so with the device and shared heaps full it failed on a 4 MiB write-back buffer at every call (PPSA26344 on Windows). StorageTexture::Flush clears the pending state before it stores, so each failed store also dropped that image's results. The release now takes only sampled entries and storage images without pending results, and its line reports how many images with pending results it kept. --- .../Graphics/src/ShaderResources.cpp | 16 ++++++++-------- .../tests/execution/StorageCacheBudget.cpp | 13 ++++++++++++- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index 6131da75db..bda50b2579 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -1129,6 +1129,7 @@ std::uint64_t ReleaseTextureCaches(std::uint64_t bytes) { std::uint64_t freed = 0; std::size_t sampledEntries = 0; std::size_t storageEntries = 0; + std::size_t storageKept = 0; bool sampledSkipped = false; bool storageSkipped = false; { @@ -1156,7 +1157,6 @@ std::uint64_t ReleaseTextureCaches(std::uint64_t bytes) { storageSkipped = true; } else { try { - bool dirtyToo = false; while (freed < bytes) { std::vector, std::uint64_t>> evicted; { @@ -1166,20 +1166,20 @@ std::uint64_t ReleaseTextureCaches(std::uint64_t bytes) { } std::lock_guard lock(cache.mutex, std::adopt_lock); std::uint64_t batch = 0; + storageKept = 0; for (auto it = cache.entries.end(); it != cache.entries.begin() && batch + freed < bytes;) { --it; - if (!dirtyToo && it->texture->Dirty()) continue; + if (it->texture->Dirty()) { + ++storageKept; + continue; + } const auto accounted = it->accounted; const auto next = std::next(it); evicted.emplace_back(evictStorage(cache, it), accounted); batch += accounted; it = next; } - if (evicted.empty()) { - if (dirtyToo || cache.entries.empty()) break; - dirtyToo = true; - continue; - } + if (evicted.empty()) break; } for (auto& [image, accounted] : evicted) { std::vector> resources; @@ -1198,7 +1198,7 @@ std::uint64_t ReleaseTextureCaches(std::uint64_t bytes) { } } } - std::fprintf(stderr, "[gpu] texture caches released %llu MiB of the %llu MiB asked for: %zu sampled entries%s, %zu storage images%s\n", static_cast(freed >> 20u), static_cast(bytes >> 20u), sampledEntries, sampledSkipped ? " (cache busy, skipped)" : "", storageEntries, storageSkipped ? " (cache or device busy, skipped)" : ""); + std::fprintf(stderr, "[gpu] texture caches released %llu MiB of the %llu MiB asked for: %zu sampled entries%s, %zu storage images%s, %zu with pending results kept\n", static_cast(freed >> 20u), static_cast(bytes >> 20u), sampledEntries, sampledSkipped ? " (cache busy, skipped)" : "", storageEntries, storageSkipped ? " (cache or device busy, skipped)" : "", storageKept); return freed; } diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/StorageCacheBudget.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/StorageCacheBudget.cpp index e0845f8f53..09c9c0b2da 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/StorageCacheBudget.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/StorageCacheBudget.cpp @@ -144,7 +144,18 @@ int main() { const auto rest = AgcDriver::Graphics::ReleaseTextureCaches(~std::uint64_t{0}); Require(rest >= held * (capacity - 3u), "a release of everything freed " + std::to_string(rest >> 20u) + " MiB of the " + std::to_string((held * (capacity - 3u)) >> 20u) + " MiB cached"); device->WaitIdle(); - for (std::uint32_t surface = 0; surface < capacity + 2u; ++surface) Require(StorageTexture::FindLive(base + surface * Stride, SurfaceBytes) == nullptr && (surface == 3u || images[surface].expired()), "surface " + std::to_string(surface) + " survived a release of everything"); + for (std::uint32_t surface = 0; surface < capacity + 2u; ++surface) { + if (surface == 3u) continue; + Require(StorageTexture::FindLive(base + surface * Stride, SurfaceBytes) == nullptr && images[surface].expired(), "surface " + std::to_string(surface) + " survived a release of everything"); + } + { + const auto kept = StorageTexture::FindLive(base + 3u * Stride, SurfaceBytes); + Require(kept != nullptr && kept->Dirty(), "a release of everything took the image with pending results"); + } + StorageTexture::FlushPending(base + 3u * Stride, static_cast(SurfaceBytes), nullptr, "test"); + device->WaitIdle(); + Require(AgcDriver::Graphics::ReleaseTextureCaches(~std::uint64_t{0}) >= held, "a release did not take the image once its results were stored"); + Require(StorageTexture::FindLive(base + 3u * Stride, SurfaceBytes) == nullptr, "surface 3 survived a release after its results were stored"); Require(AgcDriver::Graphics::ReleaseTextureCaches(held) == 0, "a release from empty caches reported freed bytes"); } catch (...) { release(); From 0be11ff1a39e0b94c11f93f2f70a348cd1204a7d Mon Sep 17 00:00:00 2001 From: MrMha <114929652+MrMha@users.noreply.github.com> Date: Thu, 15 Oct 2026 22:20:53 +0400 Subject: [PATCH 0385/1000] fix: keep release-note alias tests compatible with Python 3.9 --- tools/tests/test_package_release.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/tests/test_package_release.py b/tools/tests/test_package_release.py index b648507ff9..6a0877c18c 100644 --- a/tools/tests/test_package_release.py +++ b/tools/tests/test_package_release.py @@ -402,7 +402,7 @@ class ReleaseNotesTests(unittest.TestCase): except OSError as error: self.skipTest(str(error)) else: - paths[second].hardlink_to(paths[first]) + os.link(paths[first], paths[second]) previous = [path.read_bytes() for path in paths] with self.assertRaisesRegex(ValueError, "distinct files"): prepare_release_notes.prepare_notes(*paths) From 96070188b9f0d8dba01777d185807a9a51f0e113 Mon Sep 17 00:00:00 2001 From: Sam Date: Wed, 14 Oct 2026 07:55:29 +0100 Subject: [PATCH 0386/1000] perf(agc): allow shader preparation on first use APS5_LAZY_SHADER_PREPARE=1 keeps registration validation and decoding but skips queueing the preparation. Draws and dispatches prepare a deferred snapshot on first use and cache it; explicit ABI resolution still prepares its stages. Titles that register thousands of shaders at boot no longer hold every prepared shader in memory before the first frame. Cover lazy dispatch, metadata and failure cases in both asynchronous and synchronous modes. --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 9 +- .../src/Driver/Shaders/ShaderRegistry.cpp | 6 +- .../tests/execution/PreparedShaders.cpp | 97 ++++++++++++++++++- docs/dev/ARCHITECTURE.md | 1 + 4 files changed, 109 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 051af9c6e5..66d89a4130 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1730,7 +1730,7 @@ if(BUILD_TESTING) endif() endforeach() endforeach() - foreach(preparedMode IN ITEMS indirect uncached debug debug_failure deferred deferred_undecodable registered_workgroup registered_workgroup_sync) + foreach(preparedMode IN ITEMS indirect uncached debug debug_failure deferred deferred_undecodable registered_workgroup registered_workgroup_sync lazy lazy_sync) set(preparedTest "agc_driver_prepared_shaders_${preparedMode}") if(preparedMode STREQUAL "indirect") add_test(NAME ${preparedTest} COMMAND agc_driver_prepared_shaders_tests --indirect) @@ -1738,6 +1738,8 @@ if(BUILD_TESTING) add_test(NAME ${preparedTest} COMMAND agc_driver_prepared_shaders_tests --deferred) elseif(preparedMode STREQUAL "deferred_undecodable") add_test(NAME ${preparedTest} COMMAND agc_driver_prepared_shaders_tests --deferred-undecodable) + elseif(preparedMode STREQUAL "lazy" OR preparedMode STREQUAL "lazy_sync") + add_test(NAME ${preparedTest} COMMAND agc_driver_prepared_shaders_tests --lazy) elseif(preparedMode STREQUAL "registered_workgroup" OR preparedMode STREQUAL "registered_workgroup_sync") add_test(NAME ${preparedTest} COMMAND agc_driver_prepared_shaders_tests --registered-workgroup) elseif(preparedMode STREQUAL "debug_failure") @@ -1751,6 +1753,11 @@ if(BUILD_TESTING) set_property(TEST ${preparedTest} APPEND PROPERTY ENVIRONMENT "APS5_NO_DISPATCH_CACHE=1") elseif(preparedMode STREQUAL "registered_workgroup_sync") set_property(TEST ${preparedTest} APPEND PROPERTY ENVIRONMENT "APS5_SYNC_SHADER_PREPARE=1") + elseif(preparedMode STREQUAL "lazy" OR preparedMode STREQUAL "lazy_sync") + set_property(TEST ${preparedTest} APPEND PROPERTY ENVIRONMENT "APS5_LAZY_SHADER_PREPARE=1" "APS5_SUSPEND_DRAIN=1") + if(preparedMode STREQUAL "lazy_sync") + set_property(TEST ${preparedTest} APPEND PROPERTY ENVIRONMENT "APS5_SYNC_SHADER_PREPARE=1") + endif() elseif(preparedMode STREQUAL "debug" OR preparedMode STREQUAL "debug_failure") set_property(TEST ${preparedTest} APPEND PROPERTY ENVIRONMENT "APS5_PROFILE_DRAW=1" "APS5_NO_UNLOCKED_DEVICE=1") endif() diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp index b9a7b3ed9c..9d64334596 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp @@ -1514,7 +1514,11 @@ void Driver::RegisterShader(const Shader* shader) { registered.context = snapshot.registeredState->context; registered.userConfig = snapshot.registeredState->userConfig; std::shared_ptr preparation; - if (AsyncRegistrationPrepare()) { + static const bool lazy = std::getenv("APS5_LAZY_SHADER_PREPARE") != nullptr; + if (lazy) { + static_cast(PlanRegistered(snapshot, *localDevice, registered, true, &snapshot.prepared->deferred)); + snapshot.prepared->deferred = true; + } else if (AsyncRegistrationPrepare()) { preparation = PlanRegistered(snapshot, *localDevice, registered, true, &snapshot.prepared->deferred); if (preparation != nullptr) preparation->prebuildDevice = localDevice; if (preparation != nullptr && (snapshot.type == 0 || snapshot.type == 1)) BuildRegisteredAbiKey(registered, *localDevice, preparation->abiKey); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp index 777dd733f4..5109c06012 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp @@ -5,6 +5,8 @@ #include "prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver.hpp" #include "prx/libSceAgcDriver/Execution/include/ShaderPreparation.hpp" +#include "../NarrowConstantStoreFixture.hpp" +#include "prx/libc/include/GuestAllocations.hpp" #include "prx/libSceAgcDriver/Submit/include/Acb.hpp" #include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" #include @@ -27,6 +29,12 @@ #include #include #include +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#endif namespace { @@ -600,7 +608,7 @@ void RegisteredVertexWorkgroup(const ShaderRecompiler::SpirvTarget& target, bool header.specials.vgt_shader_stages_en.value = 0; ExpectFailure([&] { AgcDriverRegisterShader_nid_postfix(&header.shader); }, "missing AGC user-data header"); header.shader.user_data = &header.userData; - if (target.subgroupSize < 64u && std::getenv("APS5_SYNC_SHADER_PREPARE") != nullptr) { + if (target.subgroupSize < 64u && std::getenv("APS5_SYNC_SHADER_PREPARE") != nullptr && std::getenv("APS5_LAZY_SHADER_PREPARE") == nullptr) { ExpectFailure([&] { AgcDriverRegisterShader_nid_postfix(&header.shader); }, "v_readlane_b32 of lane 63 is outside"); } std::cout << "registered NGG preparation deferral passed; subgroup=" << target.subgroupSize << '\n'; @@ -612,6 +620,85 @@ void RegisteredVertexWorkgroup(const ShaderRecompiler::SpirvTarget& target, bool std::cout << "registered NGG wave64 preparation passed; subgroup=" << target.subgroupSize << '\n'; } +void LazyRegistration() { + using namespace NarrowConstantStoreFixture; + Require(std::getenv("APS5_LAZY_SHADER_PREPARE") != nullptr, "lazy preparation is not enabled"); + struct Header { + Shader shader{}; + std::array registers{}; + ShaderSpecialRegs specials{}; + } header; + const auto address = reinterpret_cast(Code.data()); + header.shader.file_header = 0x34333231u; + header.shader.version = 0x18; + header.shader.header_size = sizeof(header); + header.shader.shader_size = sizeof(Code); + header.shader.code = Code.data(); + header.shader.sh_registers = header.registers.data(); + header.shader.num_sh_registers = header.registers.size(); + header.shader.specials = &header.specials; + header.specials.dispatch_modifier = 0x8000; + header.registers = {{{0x20c, static_cast(address >> 8u)}, {0x20d, static_cast(address >> 40u)}, {0x207, Threads}, {0x208, 1}, {0x209, 1}, {0x212, 0}, {0x213, 4}}}; + AgcDriverRegisterShader_nid_postfix(&header.shader); + ++header.registers[0].value; + ExpectFailure([&] { AgcDriverRegisterShader_nid_postfix(&header.shader); }, "entry point is outside shader code"); + --header.registers[0].value; + header.registers[2].value = 0; + ExpectFailure([&] { AgcDriverRegisterShader_nid_postfix(&header.shader); }, "must be nonzero"); + header.registers[2].value = Threads; + header.shader.num_sh_registers = 255; + ExpectFailure([&] { AgcDriverRegisterShader_nid_postfix(&header.shader); }, "truncated shader metadata"); + header.shader.num_sh_registers = header.registers.size(); + constexpr std::size_t outputBytes = 65536; +#ifdef _WIN32 + auto* allocation = static_cast(VirtualAlloc(nullptr, outputBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); +#else + auto* allocation = static_cast(std::aligned_alloc(outputBytes, outputBytes)); +#endif + Require(allocation != nullptr, "cannot allocate lazy shader test output"); + const auto release = [](std::uint8_t* block) { + GuestAllocations::Mutation().Remove(block); +#ifdef _WIN32 + VirtualFree(block, 0, MEM_RELEASE); +#else + std::free(block); +#endif + }; + const std::unique_ptr ownedOutput(allocation, release); + const std::span output(allocation, outputBytes); + GuestAllocations::Mutation().Add(output.data(), output.size(), true, true, true); + const auto outputAddress = reinterpret_cast(output.data()); + std::vector commands; + for (const auto reg : header.registers) commands.insert(commands.end(), {0xc0017600u, reg.offset, reg.value}); + commands.insert(commands.end(), {0xc0027600u, 0x240, static_cast(outputAddress), static_cast(outputAddress >> 32u), 0xc0031500u, 1, 1, 1, 0x8041}); + Packet packet{commands.data(), static_cast(commands.size()), 0, {}}; + for (unsigned iteration = 0; iteration < 2; ++iteration) { + std::ranges::fill(output, Fill); + sceAgcDriverSubmitAcb(0x20, &packet); + AgcDriverWaitIdle_nid_postfix(); + AgcDriverSuspendPoint_nid_postfix(); + AgcDriverWaitIdle_nid_postfix(); + for (std::size_t offset = 0; offset < output.size(); ++offset) { + const auto expected = offset < Threads * LaneBytes ? ExpectedLane[offset % LaneBytes] : Fill; + if (output[offset] != expected) throw std::runtime_error("lazy dispatch " + std::to_string(iteration) + " byte " + std::to_string(offset) + " is " + std::to_string(output[offset]) + ", expected " + std::to_string(expected)); + } + } + AgcDriverResolveShaderAbi_nid_postfix(&header.shader, {}, {}); + alignas(256) static const std::array invalid{0xbf9f0000u}; + const auto invalidAddress = reinterpret_cast(invalid.data()); + header.shader.code = invalid.data(); + header.shader.shader_size = sizeof(invalid); + header.registers[0].value = static_cast(invalidAddress >> 8u); + header.registers[1].value = static_cast(invalidAddress >> 40u); + AgcDriverRegisterShader_nid_postfix(&header.shader); + ExpectFailure([&] { AgcDriverResolveShaderAbi_nid_postfix(&header.shader, {}, {}); }, "s_code_end"); + commands[2] = header.registers[0].value; + commands[5] = header.registers[1].value; + sceAgcDriverSubmitAcb(0x20, &packet); + ExpectFailure([] { AgcDriverWaitIdle_nid_postfix(); }, "s_code_end"); + ExpectFailure([] { AgcDriverShutdown_nid_postfix(); }, "s_code_end"); +} + void DeferredRegistration() { alignas(256) std::array code{0xbe842104u, 0xbf810000u}; struct Header { @@ -672,7 +759,13 @@ void DeferredUndecodableRegistration() { int main(int argc, char** argv) { try { - Require(argc == 1 || (argc == 2 && (std::string_view(argv[1]) == "--indirect" || std::string_view(argv[1]) == "--fail-before-registration" || std::string_view(argv[1]) == "--deferred" || std::string_view(argv[1]) == "--deferred-undecodable" || std::string_view(argv[1]) == "--registered-workgroup")), "invalid test arguments"); + Require(argc == 1 || (argc == 2 && (std::string_view(argv[1]) == "--indirect" || std::string_view(argv[1]) == "--fail-before-registration" || std::string_view(argv[1]) == "--deferred" || std::string_view(argv[1]) == "--deferred-undecodable" || std::string_view(argv[1]) == "--registered-workgroup" || std::string_view(argv[1]) == "--lazy")), "invalid test arguments"); + if (argc == 2 && std::string_view(argv[1]) == "--lazy") { + if (!OpenVulkanTestDevice()) return VulkanTestSkipped; + LazyRegistration(); + std::cout << "lazy shader preparation tests passed\n"; + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--registered-workgroup") { const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; diff --git a/docs/dev/ARCHITECTURE.md b/docs/dev/ARCHITECTURE.md index 54b4476de1..9370533583 100644 --- a/docs/dev/ARCHITECTURE.md +++ b/docs/dev/ARCHITECTURE.md @@ -100,6 +100,7 @@ flowchart LR - Queue 0's packet worker decodes, captures and prepares a draw and hands its recording (`Graphics::Draw` under the GPU lock) to a committer thread in [`DrawPipeline.cpp`](../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp), so the next packet is decoded while the previous draw is recorded. Each queued item carries the guest ranges it will write (`DrawWriteRanges`: written buffers and storage images, color targets with their DCC and CMASK, depth and stencil). A later packet that reads or writes one of those ranges, or that the pipeline can't order, drains it first; labels, submission ends, buffer fills and (with `APS5_PIPELINE_DMA=1`) DMA packets are queued as items instead. An exception thrown by a commit is rethrown on the worker at its next enqueue or drain, as an inline draw error would be. On by default; `APS5_PIPELINED_DRAWS=0` records every draw inline, and `APS5_EXACT_DRAW_WRITES=0` widens the color and depth ranges to estimates. - The shader registry maps a code address to every header registered for that code, in registration order. Static, graphics and rectangle ABI resolution pick the header at the bound address (or one equal to it, `SameHeader`, which also takes a hull header fused by `sceAgcFuseShaderHalves` (type 3) as the registered back half (type 7) it copies, the fused registers aside; graphics ABI resolution then uses the fused header's shader registers); draws and dispatches pick the program by the stage's binary type (`RegisteredProgram`), and a front entry matching both type 2 and type 4 throws. Within a type the last registration wins; re-registering an identical earlier header makes its snapshot current again without preparing it again, and changed code drops the older headers. - The snapshot is published as pending. `SourceHandleFor`, `InvocationFor` and `ShaderPreparationTransaction::Edit` / `Read` wait for the pending job before reading `PreparedShaders`, and rethrow its failure. `APS5_SYNC_SHADER_PREPARE=1` prepares on the registering thread instead. +- `APS5_LAZY_SHADER_PREPARE=1` retains registration validation and decoding but discards the temporary plan without queueing preparation. The snapshot is deferred, not pending; draws and dispatches prepare and cache it on first use. Explicit ABI resolution still prepares its requested stages. This opt-in mode takes precedence over synchronous registration preparation and trades background CPU/memory use for possible first-use stalls; disk caches and shader output are unchanged. - On devices with graphics pipeline libraries (`SpirvTarget::registrationModules`), a vertex or pixel artifact is emitted in registration form: the buffer V# words, image modes, swizzles and mip ranges, depth-compare state, export mappings and vertex component selectors it used to specialize on per draw are read at run time (image `ResourceMetadata` and a few state dwords in the shader data), descriptor heaps keep every element in place (an unused one gets a null or placeholder descriptor), and the pixel stage's bindings live in descriptor set 1 so each stage's library depends only on its own set layout (`INDEPENDENT_SETS`). An artifact above `registrationWordLimit` SPIR-V words is re-emitted specialized, and `CompiledShaderArtifact::registrationModule` records which form it has. For artifacts in registration form, `RegistrationResult` materializes the module once, and `PrebuildStageLibrary` builds its pre-rasterization or fragment-shader library on a separate one-thread pool (`APS5_PREBUILD_THREADS`) with a fixed default state, pairing it with a stub stage. A later draw with that state finds both libraries in the store and only links. - A draw or dispatch whose state matches no artifact prepared at registration (a title that builds its static ABI state itself, without `sceAgcDriverResolve*Abi`) gets one prepared at use by `SourceHandleFor` or `InvocationFor`, logged once per artifact and kept in the snapshot for the next use. - A compute shader whose `s_swappc_b64` calls a user-data pointer cannot be linked at registration, because the callee's address is only known at dispatch. `InvocationFor` in [`ShaderRegistry.cpp`](../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp) reads each callee from guest memory, appends one copy per call after the caller (`LinkUserDataCalls`) and prepares that linked code, so a different callee gets its own prepared entry. From c8bedff055a1643cfb3f03fa19f0f7491961d3aa Mon Sep 17 00:00:00 2001 From: Sam <85789460+Sam-mon7@users.noreply.github.com> Date: Thu, 15 Oct 2026 17:55:30 +0100 Subject: [PATCH 0387/1000] test(agc): check that lazy registration defers preparation The lazy mode of agc_driver_prepared_shaders only checked dispatch output and failure reporting, which eager preparation also produces. Read the registered snapshot through Driver::RegisteredShaders: registration leaves it deferred with no prepared artifact, and the first dispatch prepares it. --- .../Execution/include/Driver/Driver.hpp | 1 + .../src/Driver/Shaders/ShaderRegistry.cpp | 5 +++++ .../tests/execution/PreparedShaders.cpp | 18 ++++++++++++++++++ 3 files changed, 24 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp index 434f0d992b..6d440f6e31 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp @@ -75,6 +75,7 @@ public: }; DispatchCacheFootprint DispatchCacheSize(); static std::uint64_t DispatchCacheBytes(); + std::shared_ptr RegisteredShaders(); private: friend class SampledReadScope; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp index 9d64334596..ab8df700c0 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp @@ -1462,6 +1462,11 @@ ShaderSnapshot PrepareNullPixelProgram(const VulkanDevice& device) { return null; } +std::shared_ptr Driver::RegisteredShaders() { + std::lock_guard lock(mutex); + return shaders; +} + void Driver::RegisterShader(const Shader* shader) { PerformanceContext timingContext(FrameTiming::Preparation()); PerformanceTimer timing("Shader.Register"); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp index 5109c06012..e242f0a0b9 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp @@ -4,6 +4,7 @@ #include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp" #include "prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" #include "prx/libSceAgcDriver/Execution/include/ShaderPreparation.hpp" #include "../NarrowConstantStoreFixture.hpp" #include "prx/libc/include/GuestAllocations.hpp" @@ -649,6 +650,19 @@ void LazyRegistration() { header.shader.num_sh_registers = 255; ExpectFailure([&] { AgcDriverRegisterShader_nid_postfix(&header.shader); }, "truncated shader metadata"); header.shader.num_sh_registers = header.registers.size(); + const auto registered = [&] { + const auto registry = AgcDriver::DriverDetail::Driver::Get().RegisteredShaders(); + Require(registry != nullptr, "lazy registration published no shader registry"); + auto snapshot = AgcDriver::DriverDetail::RegisteredHeader(*registry, &header.shader); + Require(snapshot != nullptr, "lazy registration published no snapshot"); + return snapshot; + }; + const auto lazySnapshot = registered(); + { + AgcDriver::DriverDetail::ShaderPreparationTransaction transaction; + const auto& state = transaction.Read(*lazySnapshot); + Require(state.deferred && state.entries.empty(), "lazy registration prepared the shader before its first dispatch"); + } constexpr std::size_t outputBytes = 65536; #ifdef _WIN32 auto* allocation = static_cast(VirtualAlloc(nullptr, outputBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); @@ -683,6 +697,10 @@ void LazyRegistration() { if (output[offset] != expected) throw std::runtime_error("lazy dispatch " + std::to_string(iteration) + " byte " + std::to_string(offset) + " is " + std::to_string(output[offset]) + ", expected " + std::to_string(expected)); } } + { + AgcDriver::DriverDetail::ShaderPreparationTransaction transaction; + Require(!transaction.Read(*lazySnapshot).entries.empty(), "the first dispatch did not prepare the lazily registered shader"); + } AgcDriverResolveShaderAbi_nid_postfix(&header.shader, {}, {}); alignas(256) static const std::array invalid{0xbf9f0000u}; const auto invalidAddress = reinterpret_cast(invalid.data()); From 2b567cda34c74de777f858d4815e73b13f2a9949 Mon Sep 17 00:00:00 2001 From: Anthony <132519619+Wild-Badger@users.noreply.github.com> Date: Thu, 15 Oct 2026 14:28:32 -0300 Subject: [PATCH 0388/1000] fix(libc): count wide collate string elements --- core/libs/prx/libc/src/LocaleSupport.cpp | 14 +++++------- core/libs/tests/GuestCollate.cpp | 28 ++++++++++++++++++------ docs/dev/debt/libc.md | 2 +- 3 files changed, 27 insertions(+), 17 deletions(-) diff --git a/core/libs/prx/libc/src/LocaleSupport.cpp b/core/libs/prx/libc/src/LocaleSupport.cpp index a464f6881f..bbec9d28c7 100644 --- a/core/libs/prx/libc/src/LocaleSupport.cpp +++ b/core/libs/prx/libc/src/LocaleSupport.cpp @@ -157,12 +157,8 @@ std::int64_t APS5_VABI CollateHash(const GuestLocale::CollateFacet*, const char* const GuestLocale::CollateVtable g_collateVtable{{DestroyCollate, DeleteCollate, RetainFacet, ReleaseFacet}, CollateCompare, CollateTransform, CollateHash}; -// The guest's wchar_t is 16 bits, so the wide facet needs its own entries: collate reaches the -// same slots with ranges of 16-bit characters, whose length is a count of characters and not of bytes. -// The C locale orders them by value, as Dinkumware's char_traits::compare does, mixes one -// whole character per hash round, and copies them into the result, whose size and capacity are bytes. constexpr std::size_t GuestWideCharacterSize = 2; -constexpr std::size_t GuestWideInlineCapacity = sizeof(GuestLocale::String::buffer) - GuestWideCharacterSize; +constexpr std::size_t GuestWideInlineCapacity = sizeof(GuestLocale::String::buffer) / GuestWideCharacterSize - 1; void ValidateWideCharacterRange(const std::uint16_t* first, const std::uint16_t* last, const char* function) { if ((first == nullptr) != (last == nullptr) || last < first) throw std::invalid_argument(std::string(function) + ": invalid character range"); @@ -195,15 +191,15 @@ GuestLocale::String* APS5_VABI WideCollateTransform(GuestLocale::String* result, const auto bytes = length * GuestWideCharacterSize; char* data = result->buffer; std::size_t capacity = GuestWideInlineCapacity; - if (bytes > GuestWideInlineCapacity) { - capacity = bytes | GuestStringCapacityMask; - data = static_cast(AllocateStringStorage(capacity + GuestWideCharacterSize)); + if (length > GuestWideInlineCapacity) { + capacity = length | GuestWideInlineCapacity; + data = static_cast(AllocateStringStorage((capacity + 1) * GuestWideCharacterSize)); } if (length != 0) std::memcpy(data, characters, bytes); data[bytes] = 0; data[bytes + 1] = 0; if (data != result->buffer) result->pointer = data; - result->size = bytes; + result->size = length; result->capacity = capacity; return result; } diff --git a/core/libs/tests/GuestCollate.cpp b/core/libs/tests/GuestCollate.cpp index 07ccf6b0b3..7dec7aabab 100644 --- a/core/libs/tests/GuestCollate.cpp +++ b/core/libs/tests/GuestCollate.cpp @@ -147,30 +147,44 @@ void CheckWideGetcat(const GuestLocale::Implementation* const* classic, const Gu GuestLocale::String result; std::memset(&result, 0xcd, sizeof(result)); require(TransformWide(wide, &result, text) == &result); - require(result.reserved == 0xcdcdcdcdcdcdcdcdull && result.size == 6 && result.capacity == 14 && allocations == before + 1); + require(result.reserved == 0xcdcdcdcdcdcdcdcdull && result.size == 3 && result.capacity == 7 && allocations == before + 1); const auto* inlineUnits = reinterpret_cast(result.buffer); require(inlineUnits[0] == u'a' && inlineUnits[1] == u'b' && inlineUnits[2] == u'c' && inlineUnits[3] == 0); std::memset(&result, 0xcd, sizeof(result)); require(Vtable(facet).transform(&result, wide, nullptr, nullptr) == &result); - require(result.size == 0 && result.capacity == 14 && reinterpret_cast(result.buffer)[0] == 0); + require(result.size == 0 && result.capacity == 7 && reinterpret_cast(result.buffer)[0] == 0); const std::u16string seven(7, u'q'); require(TransformWide(wide, &result, seven) == &result); - require(result.size == 14 && result.capacity == 14 && allocations == before + 1); + require(result.size == 7 && result.capacity == 7 && allocations == before + 1); const auto* inlineSeven = reinterpret_cast(result.buffer); require(inlineSeven[6] == u'q' && inlineSeven[7] == 0); const std::u16string eight(8, u'y'); require(TransformWide(wide, &result, eight) == &result); - require(allocations == before + 2 && lastSize == 33 && result.size == 16 && result.capacity == 31 && result.pointer == lastAllocation); + require(allocations == before + 2 && lastSize == 32 && result.size == 8 && result.capacity == 15 && result.pointer == lastAllocation); require(reinterpret_cast(result.pointer)[0] == u'y' && reinterpret_cast(result.pointer)[8] == 0); std::free(result.pointer); + for (const std::size_t length : {2039, 2040}) { + const std::u16string large(length, u'\u0100'); + require(TransformWide(wide, &result, large) == &result); + require(result.size == length && result.capacity == (length | 7)); + const auto* units = reinterpret_cast(result.pointer); + require(std::u16string(units, result.size) == large && units[result.size] == 0); + require(lastSize == (length == 2039 ? 4080 : 4096 + 0x27)); + if (length == 2040) { + require(reinterpret_cast(units) % 0x20 == 0); + require(reinterpret_cast(units)[-1] == lastAllocation); + } else require(result.pointer == lastAllocation); + std::free(lastAllocation); + } + const std::u16string embeddedText(u"a\0b", 3); reject([&] { TransformWide(wide, &result, embeddedText); }); reject([&] { Vtable(facet).transform(nullptr, wide, Characters(seven.data()), Characters(seven.data() + seven.size())); }); - require(allocations == before + 2); + require(allocations == before + 4); reject([&] { Vtable(facet).compare(wide, Characters(text.data() + 2), Characters(text.data()), Characters(text.data()), Characters(text.data() + 1)); }); reject([&] { Vtable(facet).compare(wide, nullptr, Characters(text.data() + 1), Characters(text.data()), Characters(text.data() + 1)); }); reject([&] { Vtable(facet).hash(wide, Characters(text.data() + 1), Characters(text.data())); }); @@ -183,7 +197,7 @@ void CheckWideGetcat(const GuestLocale::Implementation* const* classic, const Gu require(frees == freed); Vtable(facet).facet.deleteObject(facet); require(frees == freed + 1 && lastFree == wide); - require(allocations == before + 2); + require(allocations == before + 4); } } @@ -284,6 +298,6 @@ int main() { require(frees == 1); Vtable(facet).facet.deleteObject(facet); require(frees == 2 && lastFree == collate); - require(allocations == 6); + require(allocations == 8); return 0; } diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 50359fc84c..06d783bdf1 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -62,7 +62,7 @@ - [gethostname](../../../core/libs/prx/libc/src/Hostname.cpp) (libc) reports the fixed guest name `AnyPS5` without querying the host machine name. Buffer copying, null queries and `ENAMETOOLONG` follow [FreeBSD gethostname](https://github.com/freebsd/freebsd-src/blob/main/lib/libc/gen/gethostname.c); the console's configurable hostname and its integration with `sysctl` and name resolution are unverified and not implemented. - [wcsrtombs_s](../../../core/libs/prx/libc/src/Multibyte.cpp) (libc) follows the console's `libc.prx`, but a runtime-constraint violation only returns its error: the default handler there writes the message and a newline to `stderr` before returning, which is not done here. Conversion uses the C locale of `wcrtomb` (`MB_CUR_MAX` 1), so the path where a multibyte character does not fit the remaining space is not reached - [The narrow printf family](../../../core/libs/prx/libc/src/Formatting.cpp) (libc) converts 16-bit guest wide arguments to UTF-8 on both platforms, independently of the host locale. It does not use the guest's single-byte `wcrtomb`; the console's treatment of invalid UTF-16 and locale-specific encoding remains unverified. Linux retains host formatting for formats without wide arguments or `%m`, so a narrow `%p` and a NaN there still follow the host (glibc prints a null pointer as `(nil)`, where FreeBSD prints `0x0`, and a NaN with its sign, `-nan`, and with the `+` and space flags, where FreeBSD prints `nan`); argument positions above 99 (FreeBSD's `NL_ARGMAX`) throw when a format requires the guest formatter -- [collate and collate _Getcat](../../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc) build the facet for the C locale only and throw for any other locale name, the wide entry point following the narrow one it sits next to. The wide facet carries its own vtable, whose `compare`, `transform` and `hash` take the guest's 16-bit characters, so the length of a range is a count of characters: `compare` orders by 16-bit value (the C locale, as Dinkumware's `char_traits::compare` does), `transform` copies the characters into the result as the guest's bytes and sizes the result in bytes (two per character, a 16-bit terminator, the 7 characters that still fit next to it and `length * 2 | 15` plus two bytes of storage beyond that), and `hash` mixes one whole character per round, so wide text below 0x100 hashes like its narrow spelling and a character above 0xff hashes as one value instead of the two bytes it holds. The console's `libc.prx` exports both `_Getcat` (NIDs BSVJqITGCyI and 2wz4rthdiy8) with identical 0x19d-byte bodies, believed to build the same 0x20-byte facet; the console bodies themselves are unreadable (obfuscated dynsym, and the text of the relinked `libc.prx` is zeroed), so the wide entries follow the narrow algorithm and the C locale instead of the console's wide code, and no console run checks the two. The console's `libc.prx` stores pointers to its collation state tables at offsets 0x10 and 0x18 of the facet; ours leaves them null, as only the facet's own functions read them. `do_transform` throws for input with embedded null characters, which the console drops depending on the output buffer size. The narrow result capacity (`length | 15`) and the over-aligned storage of results of 4080 characters or more follow the console's `libc.prx` code and are not checked on hardware. No module of the PPSA59322, PPSA76677 or PPSA97358 dumps in this tree imports either `_Getcat`, so neither has been exercised by a title. +- [collate and collate _Getcat](../../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc) build the facet for the C locale only and throw for any other locale name, the wide entry point following the narrow one it sits next to. The wide facet carries its own vtable, whose `compare`, `transform` and `hash` take the guest's 16-bit characters, so the length of a range is a count of characters: `compare` orders by 16-bit value (the C locale, as Dinkumware's `char_traits::compare` does), `transform` copies the characters into the result and records size and capacity in 16-bit elements (inline capacity 7, heap capacity `length | 7`, `(capacity + 1) * 2` bytes including the terminator), following Dinkumware's [VC12 `basic_string`](https://github.com/ganboing/crt.vc12/blob/f1d47126bf13d1c9dccaa6c53b72217fd0474968/src/xstring#L500-L531), and `hash` mixes one whole character per round, so wide text below 0x100 hashes like its narrow spelling and a character above 0xff hashes as one value instead of the two bytes it holds. The console's `libc.prx` exports both `_Getcat` (NIDs BSVJqITGCyI and 2wz4rthdiy8) with identical 0x19d-byte bodies, believed to build the same 0x20-byte facet; the console bodies themselves are unreadable (obfuscated dynsym, and the text of the relinked `libc.prx` is zeroed), so the wide entries follow the narrow algorithm and the C locale instead of the console's wide code, and no console run checks the two. The console's `libc.prx` stores pointers to its collation state tables at offsets 0x10 and 0x18 of the facet; ours leaves them null, as only the facet's own functions read them. `do_transform` throws for input with embedded null characters, which the console drops depending on the output buffer size. The narrow result capacity (`length | 15`) and the over-aligned storage of results of 4080 characters or more follow the console's `libc.prx` code and are not checked on hardware. No module of the PPSA59322, PPSA76677 or PPSA97358 dumps in this tree imports either `_Getcat`, so neither has been exercised by a title. - [The scanf family](../../../core/libs/prx/libc/src/Formatting.cpp) (libc), the `_s` forms included, on Linux throws for the 16-bit wide conversions (`%ls`, `%lc`, `%l[`, `%S`, `%C`): glibc would store a 32-bit `wchar_t` per character into the guest's 16-bit buffer. Windows hands `%ls`, `%lc` and `%l[` to the host, whose `wchar_t` is 16 bits - [The scanf family](../../../core/libs/prx/libc/include/WindowsScanning.hpp) (libc) on Windows returns 0, as C17 and FreeBSD do, when input fails after only suppressed conversions have completed (`sscanf("12", "%*d%d", &value)`). Linux hands the format to glibc, which returns `EOF` there. On Windows the host stops a `%x`, `%X`, `%i` or `%p` after the `x` of a `0x` that no hex digit follows (or that the width cuts off), and a float conversion (`%e`, `%f`, `%g`, `%a`) after the `e` of an exponent with no digits (`1e`), where C17 and FreeBSD stop before the `x` or the `e` - [Guest path resolution](../../../core/libs/prx/libc/src/General.cpp) falls back to ASCII case-insensitive component lookup, following [shadPS4's host filesystem](https://github.com/shadps4-emu/shadPS4/blob/945dbc3cc3eee80ac3e053b438502ed936fa6bb2/src/core/file_sys/backends/host_fs.cpp). PPSA03747 requests `Skyrim.INI`, `Data/Video` and lowercase archive/plugin names that differ from the dump's filenames. Exact host names take precedence; multiple fallback matches throw instead of selecting an arbitrary file. Existing parent names are resolved when creating a new file, and guest mount aliases use the same ASCII comparison. Results are not cached, so renames and newly created files remain visible. Unicode case folding and PS5 behaviour for case-colliding host entries are unverified; host roots are used as supplied. Explicit native Windows drive and UNC paths bypass guest parent-component validation.. A relinked module's `.guest.prx` is matched the same way when the original module is not beside it: Borderlands 4 (PPSA01494) loads `/app0/prx/libRenoirCore.PS5.prx`, which it ships as `librenoircore.ps5.prx` From 4c64d39af51a9b1819f866e896db03469db8d638 Mon Sep 17 00:00:00 2001 From: MrMha <114929652+MrMha@users.noreply.github.com> Date: Thu, 15 Oct 2026 22:33:01 +0400 Subject: [PATCH 0389/1000] fix: escape phase names in trace interval tables --- tools/tests/test_trace_digest.py | 24 ++++++++++++++++++++++++ tools/trace_digest.py | 2 +- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/tools/tests/test_trace_digest.py b/tools/tests/test_trace_digest.py index df4ba548f0..dffd4da9b3 100644 --- a/tools/tests/test_trace_digest.py +++ b/tools/tests/test_trace_digest.py @@ -394,5 +394,29 @@ class IntervalDigestTests(unittest.TestCase): trace_digest.main([str(self.path)]) self.assertIn("no interval lines", output.getvalue()) + def test_interval_phase_names_preserve_json_and_markdown_table_structure(self): + for name, escaped in (("Draw|&", "Draw|<pass>&"), + ("Draw\r\npass", "Draw
pass"), + ("Draw\rpass\nend", "Draw
pass
end")): + with self.subTest(name=name): + line = interval_line(0.0, 30.0, 20.0, 5.0, top_phases_ms_per_frame={name: 7.5}) + self.path.write_text(json.dumps(line) + "\n", encoding="utf-8") + output = io.StringIO() + with contextlib.redirect_stdout(output): + self.assertEqual(trace_digest.main([str(self.path), "--json"]), 0) + row = json.loads(output.getvalue())["intervals"][0] + self.assertEqual(row["top_phase"], {"name": name, "ms_per_frame": 7.5}) + self.assertEqual(row["frame_ms"], line["frame_ms"]) + self.assertEqual(row["gpu_busy_ms_per_present"], 5.0) + self.assertEqual((row["start_s"], row["end_s"], row["presents"]), (0.0, 30.0, 1800)) + output = io.StringIO() + with contextlib.redirect_stdout(output): + self.assertEqual(trace_digest.main([str(self.path)]), 0) + rows = [line for line in output.getvalue().splitlines() if line.startswith("|")] + self.assertEqual(len(rows), 3) + self.assertTrue(all(line.count("|") == 11 for line in rows), rows) + cells = [cell.strip() for cell in rows[2].split("|")[1:-1]] + self.assertEqual(cells, ["1", "0-30", "1800", "16.6", "18.0", "20.0", "25.0", "5.0", "50 %", escaped + " 7.5"]) + if __name__ == "__main__": unittest.main() diff --git a/tools/trace_digest.py b/tools/trace_digest.py index 6e2cf5c21c..abfa8702a4 100644 --- a/tools/trace_digest.py +++ b/tools/trace_digest.py @@ -294,7 +294,7 @@ def markdown_intervals(result): worker = f"{row['busiest_worker']['pct']:.0f} %" if row["busiest_worker"] else "-" phase = f"{row['top_phase']['name']} {row['top_phase']['ms_per_frame']:.1f}" if row["top_phase"] else "-" lines.append(f"| {index} | {row['start_s']:.0f}-{row['end_s']:.0f}{' (dropped)' if row['trace_dropped'] else ''} | {row['presents']} | {frame['p50']:.1f} | {frame['p90']:.1f} " - f"| {frame['p99']:.1f} | {frame['max']:.1f} | {row['gpu_busy_ms_per_present']:.1f} | {worker} | {phase} |") + f"| {frame['p99']:.1f} | {frame['max']:.1f} | {row['gpu_busy_ms_per_present']:.1f} | {worker} | {table_cell(phase)} |") p99 = [row["frame_ms"]["p99"] for row in rows] gpu = [row["gpu_busy_ms_per_present"] for row in rows] lines += ["", "```", f"p99 ms {sparkline(p99)} 0-{max(p99):.1f}", f"gpu ms {sparkline(gpu)} 0-{max(gpu):.1f}", "```"] From ced03054555132053f4da5737cc53c835d03dacb Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 15 Oct 2026 21:32:41 +0300 Subject: [PATCH 0390/1000] fix(libkernel): return EINVAL for invalid mutex priority attributes --- core/libs/prx/libkernel/Pthread/Posix/Mutex.cpp | 6 +++--- core/libs/tests/GuestPthreadMutexattr.cpp | 14 ++++++++++++-- docs/dev/debt/libkernel.md | 2 +- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libkernel/Pthread/Posix/Mutex.cpp b/core/libs/prx/libkernel/Pthread/Posix/Mutex.cpp index fbe5145666..110c589cc2 100644 --- a/core/libs/prx/libkernel/Pthread/Posix/Mutex.cpp +++ b/core/libs/prx/libkernel/Pthread/Posix/Mutex.cpp @@ -105,7 +105,7 @@ int APS5_VABI pthread_mutexattr_setpshared_nid_postfix(PthreadMutexattr* attr, i } int APS5_VABI pthread_mutexattr_setprotocol_nid_postfix(PthreadMutexattr* attr, int protocol) { - if (!attr || !*attr) throw std::runtime_error("pthread_mutexattr_setprotocol: null attr"); + if (!attr || !*attr) return PosixThread::GUEST_EINVAL; if (protocol < 0 || protocol > POSIX_PRIO_PROTECT) return POSIX_EINVAL; (*attr)->protocol = protocol; (*attr)->ceiling = sched_get_priority_max_nid_postfix(POSIX_SCHED_RR); @@ -113,14 +113,14 @@ int APS5_VABI pthread_mutexattr_setprotocol_nid_postfix(PthreadMutexattr* attr, } int APS5_VABI pthread_mutexattr_setprioceiling_nid_postfix(PthreadMutexattr* attr, int prioceiling) { - if (!attr || !*attr) throw std::runtime_error("pthread_mutexattr_setprioceiling: null attr"); + if (!attr || !*attr) return PosixThread::GUEST_EINVAL; if ((*attr)->protocol != POSIX_PRIO_PROTECT) return POSIX_EINVAL; (*attr)->ceiling = prioceiling; return 0; } int APS5_VABI pthread_mutexattr_getprioceiling_nid_postfix(const PthreadMutexattr* attr, int* prioceiling) { - if (!attr || !*attr) throw std::runtime_error("pthread_mutexattr_getprioceiling: null attr"); + if (!attr || !*attr) return PosixThread::GUEST_EINVAL; if (!prioceiling) throw std::runtime_error("pthread_mutexattr_getprioceiling: null prioceiling"); if ((*attr)->protocol != POSIX_PRIO_PROTECT) return POSIX_EINVAL; *prioceiling = (*attr)->ceiling; diff --git a/core/libs/tests/GuestPthreadMutexattr.cpp b/core/libs/tests/GuestPthreadMutexattr.cpp index a201ff6d92..75ba65971c 100644 --- a/core/libs/tests/GuestPthreadMutexattr.cpp +++ b/core/libs/tests/GuestPthreadMutexattr.cpp @@ -113,11 +113,22 @@ static void CheckProcessSharing() { Require(pthread_mutexattr_setpshared_nid_postfix(&attr, 0) == GUEST_EINVAL); } +static void CheckInvalidPriorityAttribute(PthreadMutexattr* attr) { + int ceiling = -1; + Require(pthread_mutexattr_setprotocol_nid_postfix(attr, PRIO_PROTECT) == GUEST_EINVAL); + Require(pthread_mutexattr_setprotocol_nid_postfix(attr, -1) == GUEST_EINVAL); + Require(pthread_mutexattr_setprioceiling_nid_postfix(attr, 300) == GUEST_EINVAL); + Require(pthread_mutexattr_getprioceiling_nid_postfix(attr, &ceiling) == GUEST_EINVAL && ceiling == -1); + Require(pthread_mutexattr_getprioceiling_nid_postfix(attr, nullptr) == GUEST_EINVAL); +} + int main() { CheckType(); CheckProcessSharing(); CheckTimedlockInvalidDeadline(); PthreadMutexattr attr = nullptr; + CheckInvalidPriorityAttribute(nullptr); + CheckInvalidPriorityAttribute(&attr); Require(pthread_mutexattr_init_nid_postfix(&attr) == 0); int ceiling = -1; Require(pthread_mutexattr_getprioceiling_nid_postfix(&attr, &ceiling) == GUEST_EINVAL && ceiling == -1); @@ -139,7 +150,6 @@ int main() { Require(pthread_mutexattr_getprioceiling_nid_postfix(&attr, &ceiling) == GUEST_EINVAL); Require(Throws([&] { pthread_mutexattr_getprioceiling_nid_postfix(&attr, nullptr); })); Require(pthread_mutexattr_destroy_nid_postfix(&attr) == 0); - Require(Throws([&] { pthread_mutexattr_setprioceiling_nid_postfix(&attr, 300); })); - Require(Throws([&] { pthread_mutexattr_getprioceiling_nid_postfix(&attr, &ceiling); })); + CheckInvalidPriorityAttribute(&attr); return 0; } diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 924649e640..b75b6c4316 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -4,7 +4,7 @@ - [sceKernelGetSocPowerConsumption](../../../core/libs/prx/libkernel/System/src/ThermalSensors.cpp) (libkernel) - returns `EINVAL` and writes nothing, as `sceKernelGetSocSensorTemperature` does when the host has no sensor: the host's package power is not read (Linux RAPL `energy_uj` is readable by root only, and Windows has no general source). The prototype is not documented. PS5LM Plus (PPSA99581) passes a zeroed 128-byte buffer and, on success, reads its first 32-bit word as milliwatts, accepting 1000 to 350000; on failure it shows 0 W - [scePthreadMutexattrSetprotocol](../../../core/libs/prx/libkernel/Pthread/src/Mutex.cpp) (libkernel) - accepts 0 to 2 as a PS5 (firmware 13.60) does and returns `EINVAL` otherwise, but the protocol is not stored or applied: priority inheritance and protection are not modelled. On the console a null or uninitialized mutex attribute returns `EINVAL` from the SCE attribute calls, `scePthreadMutexInit` takes an uninitialized attribute as the default one, and a destroyed mutex returns `EINVAL` from `scePthreadMutexDestroy`; a null mutex pointer crashed the payload and still throws -- [pthread_mutexattr_setprotocol, pthread_mutexattr_setprioceiling, pthread_mutexattr_getprioceiling](../../../core/libs/prx/libkernel/Pthread/Posix/Mutex.cpp) (libkernel) - the protocol and the priority ceiling are stored and reported as in FreeBSD's [libthr](https://github.com/freebsd/freebsd-src/blob/main/lib/libthr/thread/thr_mutexattr.c) (the ceiling is set or read only under `PTHREAD_PRIO_PROTECT`, and setting a protocol resets it to the highest `SCHED_RR` priority), but mutexes do not apply them: a priority-inherit or priority-protect mutex locks like a normal one and does not change its owner's priority +- [pthread_mutexattr_setprotocol, pthread_mutexattr_setprioceiling, pthread_mutexattr_getprioceiling](../../../core/libs/prx/libkernel/Pthread/Posix/Mutex.cpp) (libkernel) - the protocol and the priority ceiling are stored and reported as in [FreeBSD 11 libthr](https://github.com/freebsd/freebsd-src/blob/releng/11.0/lib/libthr/thread/thr_mutexattr.c#L192-L245) (the ceiling is set or read only under `PTHREAD_PRIO_PROTECT`, and setting a protocol resets it to the highest `SCHED_RR` priority). A null, uninitialized or destroyed attribute returns `EINVAL` before the other arguments are checked; this error behavior is unverified on the console. Mutexes do not apply the protocol or ceiling: a priority-inherit or priority-protect mutex locks like a normal one and does not change its owner's priority - [sceKernelGetMainSocId and sceKernelGetCpumode](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) both return 0, and [sceKernelGetCpuFrequency](../../../core/libs/prx/libkernel/Time/Time.cpp) returns the host TSC frequency measured for `sceKernelGetTscFrequency`, not the console CPU clock. The PS5's SoC id and CPU mode are unknown; shadPS4's PS4 values are [0x710f10/0x740f30 and 0, 2 or 5 from the param.sfo CPU-mode flags](https://github.com/shadps4-emu/shadPS4/blob/e0a466753a1a/src/core/libraries/kernel/process.cpp). PPSA99203 only logs them (`[console] main SoC id: result=%#x out=%#x`, `CPU: %llu Hz, cpumode %d`), passing a pointer that `sceKernelGetMainSocId` leaves untouched - [sceKernelEnableDmemAliasing](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) returns 0 and changes nothing: direct memory can always be mapped more than once here, as `DoMapDirect` in [DirectMemory.cpp](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) only checks that the physical pages are allocated, so there is no aliasing to turn on. [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/e0a466753a1aacbc8db8253be685bf7ab4d9daae/src/core/libraries/kernel/memory.cpp#L39-L43) (PS4) sets a flag there that lifts the `EBUSY` its [`sceKernelMapDirectMemory2`](https://github.com/shadps4-emu/shadPS4/blob/e0a466753a1aacbc8db8253be685bf7ab4d9daae/src/core/libraries/kernel/memory.cpp#L299-L301), and [`sceKernelMapDirectMemory` for SDK 2.50 and later](https://github.com/shadps4-emu/shadPS4/blob/e0a466753a1aacbc8db8253be685bf7ab4d9daae/src/core/libraries/kernel/memory.cpp#L237-L242), return for direct memory that is [already mapped](https://github.com/shadps4-emu/shadPS4/blob/e0a466753a1aacbc8db8253be685bf7ab4d9daae/src/core/memory.cpp#L548-L553); that check is not applied here. The no-argument `int` signature follows shadPS4 and ProsperoEden (PPSA99008), which calls it before mapping the same direct memory again. Whether the PS5 refuses such a mapping without the call is unverified - [sceKernelInternalMemoryGetAvailableSize](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) reports 0 bytes and returns 0: what the console's internal memory pool holds and how large it is are unknown. The `int (size_t*)` signature is from [xUtilty](https://github.com/Backporter/xUtilty/blob/328a856af1ef/source/SystemWrapper.cpp#L85) (PS4). PPSA99203 calls it once beside `sceKernelAvailableFlexibleMemorySize` while starting its GS renderer and never reads the result ([GS.cpp](https://github.com/Swordpdf/PS5SX2/blob/b4cf55e87c34/pcsx2/GS/GS.cpp#L233-L247)) From 6f100d3f61d5b0f7c1ecaa23d1af1e215f88ca33 Mon Sep 17 00:00:00 2001 From: javierUDP Date: Thu, 15 Oct 2026 15:40:26 -0300 Subject: [PATCH 0391/1000] test(relinker): execute the AMD64-only conversion stubs on macOS The execution part of the AMD64-only converter tests was compiled for Linux x86-64 only, so the SSE4A, SHA-1, SHA-256, CLZERO and reciprocal stubs were never run on a macOS x86-64 build, unlike the ANDN lowering tests. Rosetta maps no memory below 4 GiB, so the CLZERO case that needs a 32-bit address is left out when that mapping is unavailable. --- .../codegen/tests/Amd64OnlyConverterTests.cpp | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp b/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp index c97cea6a0b..fa6930c21a 100644 --- a/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp +++ b/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp @@ -24,7 +24,7 @@ #include #include #include -#ifdef __linux__ +#if defined(__linux__) || defined(__APPLE__) #include #endif #include @@ -1289,7 +1289,7 @@ void linuxRipRelativePlacement() { } -#if defined(__linux__) && defined(__x86_64__) +#if (defined(__linux__) || defined(__APPLE__)) && defined(__x86_64__) std::uint64_t extrqReference(std::uint64_t value, std::uint64_t control) { const auto length = static_cast(control & 0x3f); const auto index = static_cast((control >> 8) & 0x3f); @@ -1304,6 +1304,12 @@ std::uint64_t insertqReference(std::uint64_t destination, std::uint64_t value, s return (destination & ~(mask << index)) | ((value & mask) << index); } +void* mapBuffer(const bool low) { + const int flags = MAP_PRIVATE | MAP_ANONYMOUS | (low ? MAP_32BIT : 0); + auto* buffer = mmap(nullptr, 4096, PROT_READ | PROT_WRITE, flags, -1, 0); + return buffer == MAP_FAILED ? nullptr : buffer; +} + constexpr std::uint64_t kStubScratch[2] = {0x0123456789abcdefull, 0xfedcba9876543210ull}; std::uint32_t rotr(const std::uint32_t value, const unsigned count) { @@ -1580,8 +1586,10 @@ void clzeroExecution() { } for (const auto& item : cases) { require(item.Match.has_value(), item.Name); - auto* buffer = static_cast(mmap(nullptr, 4096, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | (item.Low ? MAP_32BIT : 0), -1, 0)); - require(buffer != MAP_FAILED, "cannot map the CLZERO buffer"); + auto* buffer = static_cast(mapBuffer(item.Low)); + if (buffer == nullptr && item.Low) + continue; + require(buffer != nullptr, "cannot map the CLZERO buffer"); for (const std::size_t offset : {std::size_t{0x80}, std::size_t{0xAD}, std::size_t{0xFF}}) { std::memset(buffer, 0xA5, 4096); const auto address = reinterpret_cast(buffer + offset); From ad85ed13ac67d9134b7ada42a68bf59fa3664646 Mon Sep 17 00:00:00 2001 From: AlxcBTW <187763517+AlxcBTW@users.noreply.github.com> Date: Thu, 15 Oct 2026 14:50:17 +0200 Subject: [PATCH 0392/1000] perf(agc): format and write steady-state log lines off the queue worker Texture creation logged one stdout line per texture and per mip through WriteLogLine, which formatted with the mingw printf and flushed every line on the queue worker (about 786k lines in a 25 minute cave run). AsyncLog keeps a bounded queue and a logger thread. AGC_LOG_DEFERRED captures numeric arguments on the caller and formats them on the logger thread; WriteLogLine hands finished lines to a per-module sink in libc/LogMacros.hpp that AgcDriver installs, so all AgcDriver stdout lines keep their order and are written one flush per batch. A full queue blocks the caller, so no line is dropped. Flush runs on driver failure, shutdown and at exit. APS5_LOG_SYNC=1 restores the synchronous path. Identical "deferred write-back failed" notes are printed three times per 10 s and then counted into one "repeated N more times" line. A "[log] 10 s" line reports lines, bytes, caller time and logger time (APS5_LOG_STATS=0 hides it). --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 11 + .../Execution/include/AsyncLog.hpp | 376 ++++++++++++++++++ .../Execution/src/Driver/Lifecycle.cpp | 4 + .../libSceAgcDriver/Graphics/src/Recorder.cpp | 3 +- .../libSceAgcDriver/Graphics/src/Texture.cpp | 13 +- .../prx/libSceAgcDriver/tests/AsyncLog.cpp | 69 ++++ .../prx/libc/include/general/LogMacros.hpp | 35 +- 7 files changed, 495 insertions(+), 16 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/Execution/include/AsyncLog.hpp create mode 100644 core/libs/prx/libSceAgcDriver/tests/AsyncLog.cpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 251d38d6a1..d1caac1390 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -348,6 +348,17 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() + add_test_executable(agc_async_log_tests tests/AsyncLog.cpp) + target_include_directories(agc_async_log_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(agc_async_log_tests PRIVATE libc) + configure_windows_unwind(agc_async_log_tests) + add_test(NAME agc_async_log COMMAND agc_async_log_tests) + set_tests_properties(agc_async_log PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(agc_async_log PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(agc_stack_walk_tests tests/StackWalk.cpp) target_include_directories(agc_stack_walk_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(agc_stack_walk_tests PRIVATE libc) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/AsyncLog.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/AsyncLog.hpp new file mode 100644 index 0000000000..8834650ea2 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Execution/include/AsyncLog.hpp @@ -0,0 +1,376 @@ +#pragma once + +#include "prx/libc/include/general/LogMacros.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace AgcDriver { + +struct LogLiteral { + const char* text; +}; + +namespace AsyncLogDetail { + +template +inline constexpr bool Deferrable = std::is_arithmetic_v || std::is_enum_v || std::is_same_v; + +inline const char* Unwrap(LogLiteral value) { + return value.text; +} + +template +T Unwrap(T value) { + return value; +} + +class Formatter { +public: + virtual ~Formatter() = default; + virtual void Format(std::string& out) const = 0; +}; + +template +class Bound final : public Formatter { +public: + Bound(const char* format, TArgs... args) : format(format), values(args...) {} + + void Format(std::string& out) const override { + std::apply([&](const auto&... value) { Append(out, value...); }, values); + } + +private: + void Append(std::string& out, const auto&... value) const { + char small[512]; + const int size = std::snprintf(small, sizeof(small), format, Unwrap(value)...); + if (size < 0) return; + if (static_cast(size) < sizeof(small)) { + out.append(small, static_cast(size)); + return; + } + std::string large(static_cast(size) + 1, '\0'); + std::snprintf(large.data(), large.size(), format, Unwrap(value)...); + out.append(large.data(), static_cast(size)); + } + + const char* format; + std::tuple values; +}; + +struct Entry { + std::FILE* stream = nullptr; + long long micros = -1; + std::string text; + std::unique_ptr deferred; + std::size_t cost = 0; +}; + +} + +class AsyncLog { +public: + using Clock = std::chrono::steady_clock; + + static AsyncLog& Get() { + static AsyncLog* const instance = new AsyncLog(); + return *instance; + } + + int Write(std::FILE* stream, const char* text, std::size_t length) { + const auto start = Clock::now(); + if (!asynchronous) return WriteNow(stream, text, length); + AsyncLogDetail::Entry entry; + entry.stream = stream; + entry.text.assign(text, length); + entry.cost = sizeof(entry) + length; + Enqueue(std::move(entry)); + callerNanoseconds.fetch_add(Elapsed(start), std::memory_order_relaxed); + return 0; + } + + template + void Defer(std::FILE* stream, const char* format, TArgs... args) { + static_assert((AsyncLogDetail::Deferrable && ...), "deferred log arguments are numbers, enums or LogLiteral"); + const auto start = Clock::now(); + const auto micros = LogTimestampMicros(); + auto bound = std::make_unique>(format, args...); + if (!asynchronous) { + std::string text; + if (micros >= 0) AppendTimestamp(text, micros); + bound->Format(text); + WriteNow(stream, text.data(), text.size()); + return; + } + AsyncLogDetail::Entry entry; + entry.stream = stream; + entry.micros = micros; + entry.deferred = std::move(bound); + entry.cost = sizeof(entry) + sizeof(AsyncLogDetail::Bound); + Enqueue(std::move(entry)); + callerNanoseconds.fetch_add(Elapsed(start), std::memory_order_relaxed); + } + + bool Admit(const std::string& line) { + const auto now = Clock::now(); + std::string summary; + { + std::lock_guard lock(coalesceMutex); + auto found = windows.find(line); + if (found == windows.end()) { + if (windows.size() >= CoalesceKeys) return true; + windows.emplace(line, Window{now, 1, 0}); + return true; + } + auto& window = found->second; + if (now - window.first < CoalesceWindow) { + if (++window.seen <= PrintedRepeats) return true; + ++window.suppressed; + coalescedTotal.fetch_add(1, std::memory_order_relaxed); + return false; + } + summary = Summary(found->first, window.suppressed); + window = Window{now, 1, 0}; + } + if (!summary.empty()) WriteNow(stderr, summary.data(), summary.size()); + return true; + } + + void Flush() { + if (!asynchronous) return; + std::unique_lock lock(mutex); + idle.wait_for(lock, std::chrono::seconds(2), [&] { return pending.empty() && !writing; }); + } + +private: + struct Window { + Clock::time_point first; + std::uint64_t seen; + std::uint64_t suppressed; + }; + + static constexpr std::size_t Capacity = 16u * 1024u * 1024u; + static constexpr std::size_t CoalesceKeys = 128; + static constexpr std::uint64_t PrintedRepeats = 3; + static constexpr std::chrono::seconds CoalesceWindow{10}; + static constexpr std::chrono::seconds StatsWindow{10}; + static constexpr std::chrono::milliseconds Tick{500}; + + AsyncLog() { + asynchronous = std::getenv("APS5_LOG_SYNC") == nullptr; + const char* stats = std::getenv("APS5_LOG_STATS"); + statsEnabled = stats == nullptr || std::string(stats) != "0"; + lastStats = Clock::now(); + if (!asynchronous) return; + LogSink().store(&AsyncLog::Sink, std::memory_order_release); + std::atexit([] { Get().Flush(); }); + std::thread([this] { Run(); }).detach(); + } + + static int Sink(std::FILE* stream, const char* text, std::size_t length) { + return Get().Write(stream, text, length); + } + + static std::uint64_t Elapsed(Clock::time_point start) { + return static_cast(std::chrono::duration_cast(Clock::now() - start).count()); + } + + static void AppendTimestamp(std::string& out, long long micros) { + char prefix[64]; + const int size = FormatLogTimestamp(prefix, sizeof(prefix), micros); + if (size > 0) out.append(prefix, static_cast(size)); + } + + static std::string Summary(const std::string& line, std::uint64_t suppressed) { + if (suppressed == 0) return {}; + std::string text = line; + while (!text.empty() && (text.back() == '\n' || text.back() == '\r')) text.pop_back(); + text += " (repeated " + std::to_string(suppressed) + " more times in the last 10 s)\n"; + return text; + } + + static int WriteNow(std::FILE* stream, const char* text, std::size_t length) { + std::fwrite(text, 1, length, stream); + return std::fflush(stream); + } + + void Enqueue(AsyncLogDetail::Entry&& entry) { + bool notify = false; + { + std::unique_lock lock(mutex); + space.wait(lock, [&] { return bytes == 0 || bytes + entry.cost <= Capacity; }); + bytes += entry.cost; + peak = std::max(peak, bytes); + pending.push_back(std::move(entry)); + notify = sleeping; + sleeping = false; + } + if (notify) wake.notify_one(); + } + + void Run() noexcept { + std::deque batch; + std::string buffer; + for (;;) { + { + std::unique_lock lock(mutex); + sleeping = true; + wake.wait_for(lock, Tick, [&] { return !pending.empty(); }); + sleeping = false; + batch.swap(pending); + bytes = 0; + writing = !batch.empty(); + } + space.notify_all(); + if (!batch.empty()) { + const auto start = Clock::now(); + try { + Drain(batch, buffer); + } catch (...) { + buffer.clear(); + } + batch.clear(); + loggerNanoseconds.fetch_add(Elapsed(start), std::memory_order_relaxed); + { + std::lock_guard lock(mutex); + writing = false; + } + idle.notify_all(); + } + try { + Housekeeping(); + } catch (...) { + } + } + } + + void Drain(std::deque& batch, std::string& buffer) { + std::FILE* current = nullptr; + buffer.clear(); + std::uint64_t outLines = 0; + std::uint64_t errLines = 0; + std::uint64_t written = 0; + const auto emit = [&] { + if (current != nullptr && !buffer.empty()) { + written += buffer.size(); + WriteNow(current, buffer.data(), buffer.size()); + } + buffer.clear(); + }; + for (auto& entry : batch) { + if (entry.stream != current) { + emit(); + current = entry.stream; + } + if (entry.deferred) { + if (entry.micros >= 0) AppendTimestamp(buffer, entry.micros); + entry.deferred->Format(buffer); + } else { + buffer += entry.text; + } + (entry.stream == stderr ? errLines : outLines) += 1; + if (buffer.size() >= 65536) emit(); + } + emit(); + stdoutLines.fetch_add(outLines, std::memory_order_relaxed); + stderrLines.fetch_add(errLines, std::memory_order_relaxed); + loggedBytes.fetch_add(written, std::memory_order_relaxed); + } + + void Housekeeping() { + const auto now = Clock::now(); + std::vector summaries; + { + std::lock_guard lock(coalesceMutex); + for (auto it = windows.begin(); it != windows.end();) { + if (now - it->second.first >= CoalesceWindow) { + const auto summary = Summary(it->first, it->second.suppressed); + if (!summary.empty()) summaries.push_back(summary); + it = windows.erase(it); + } else { + ++it; + } + } + } + for (const auto& summary : summaries) WriteNow(stderr, summary.data(), summary.size()); + if (now - lastStats < StatsWindow) return; + const double seconds = std::chrono::duration(now - lastStats).count(); + lastStats = now; + const auto outLines = stdoutLines.exchange(0, std::memory_order_relaxed); + const auto errLines = stderrLines.exchange(0, std::memory_order_relaxed); + const auto written = loggedBytes.exchange(0, std::memory_order_relaxed); + const auto caller = callerNanoseconds.exchange(0, std::memory_order_relaxed); + const auto logger = loggerNanoseconds.exchange(0, std::memory_order_relaxed); + const auto coalesced = coalescedTotal.exchange(0, std::memory_order_relaxed); + std::size_t high = 0; + { + std::lock_guard lock(mutex); + high = peak; + peak = bytes; + } + if (!statsEnabled || outLines + errLines + coalesced == 0) return; + char line[512]; + const int size = std::snprintf(line, sizeof(line), "[log] %.1f s: %llu lines (%llu stdout, %llu stderr), %.1f KiB written, %llu repeats coalesced; callers spent %.2f ms queueing and formatting (%.3f%% of one thread), logger thread %.2f ms, queue peak %.1f KiB\n", seconds, static_cast(outLines + errLines), static_cast(outLines), static_cast(errLines), static_cast(written) / 1024.0, static_cast(coalesced), static_cast(caller) / 1e6, static_cast(caller) / 1e9 / seconds * 100.0, static_cast(logger) / 1e6, static_cast(high) / 1024.0); + if (size > 0) WriteNow(stderr, line, std::min(static_cast(size), sizeof(line) - 1)); + } + + bool asynchronous = true; + bool statsEnabled = true; + std::mutex mutex; + std::condition_variable wake; + std::condition_variable space; + std::condition_variable idle; + std::deque pending; + std::size_t bytes = 0; + std::size_t peak = 0; + bool sleeping = false; + bool writing = false; + std::mutex coalesceMutex; + std::map windows; + Clock::time_point lastStats; + std::atomic stdoutLines{0}; + std::atomic stderrLines{0}; + std::atomic loggedBytes{0}; + std::atomic callerNanoseconds{0}; + std::atomic loggerNanoseconds{0}; + std::atomic coalescedTotal{0}; +}; + +inline void InstallAsyncLog_nid_no_patch() { + static_cast(AsyncLog::Get()); +} + +inline void FlushAsyncLog_nid_no_patch() { + AsyncLog::Get().Flush(); +} + +template +void LogCoalescedStderr_nid_no_patch(const char* format, TArgs... args) { + char small[1024]; + const int size = std::snprintf(small, sizeof(small), format, args...); + if (size < 0) return; + std::string line = static_cast(size) < sizeof(small) ? std::string(small, static_cast(size)) : std::string(static_cast(size), '\0'); + if (static_cast(size) >= sizeof(small)) std::snprintf(line.data(), line.size() + 1, format, args...); + if (!AsyncLog::Get().Admit(line)) return; + std::fwrite(line.data(), 1, line.size(), stderr); + std::fflush(stderr); +} + +} + +#define AGC_LOG_DEFERRED(stream, fmt, ...) \ +::AgcDriver::AsyncLog::Get().Defer(stream, "[%s:%d %s] " fmt "\n", ::AgcDriver::LogLiteral{_APS5_FILE_}, __LINE__, ::AgcDriver::LogLiteral{TrimNidPostfix(__func__)}, __VA_ARGS__) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp index 8fdd044922..6dc5164943 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp @@ -1,3 +1,4 @@ +#include "prx/libSceAgcDriver/Execution/include/AsyncLog.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp" @@ -38,6 +39,7 @@ void PrintFailure(std::exception_ptr error) { } catch (...) { std::fprintf(stderr, "[gpu] driver failure with a non-standard exception\n"); } + FlushAsyncLog_nid_no_patch(); std::fflush(stderr); } @@ -119,11 +121,13 @@ void Driver::stop() { replacedDevices.clear(); Graphics::ShutdownGuestBufferWorkers(); PerformanceTrace::StopOutput(); + FlushAsyncLog_nid_no_patch(); StopProfileOutput_nid_no_patch(); stopped = true; } Driver::Driver() { + InstallAsyncLog_nid_no_patch(); static_cast(GuestMemory::TrackerGeneration()); ShaderMemory::SetWaitedMsProvider(&Graphics::Recorder::ThreadWaitedMs); try { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp index b494d51ffe..921505763e 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp @@ -1,3 +1,4 @@ +#include "prx/libSceAgcDriver/Execution/include/AsyncLog.hpp" #include "prx/libSceAgcDriver/Execution/include/ProfileOutput.hpp" #include "prx/libSceAgcDriver/Execution/include/CaptureTrace.hpp" #include "prx/libc/include/HostThreadLocal.hpp" @@ -3828,7 +3829,7 @@ void Recorder::finish(std::unique_ptr batch, bool wait, int source, bool try { action(); } catch (const std::exception& error) { - std::fprintf(stderr, "[gpu] deferred write-back failed: %s\n", error.what()); + AgcDriver::LogCoalescedStderr_nid_no_patch("[gpu] deferred write-back failed: %s\n", error.what()); } } } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index e71366c10b..2523a53bbf 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -1,3 +1,4 @@ +#include "prx/libSceAgcDriver/Execution/include/AsyncLog.hpp" #include "prx/libSceAgcDriver/Execution/include/ProfileOutput.hpp" #include "prx/libSceAgcDriver/Execution/include/CaptureTrace.hpp" #include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" @@ -259,9 +260,9 @@ Texture::Texture(const Context& context, TextureDetiler& detiler, const GuestTex const auto& mips = geometry.mips; const auto arrayLayers = geometry.layers; const auto elementBytes = BytesPerElement(descriptor.format); - APS5_LOG_OUT("Texture address=0x%llx %ux%u mips=%u layers=%u dim=%d tile=%d format=%u vk=%d element=%u", static_cast(descriptor.baseAddress), descriptor.width, descriptor.height, descriptor.mipCount, arrayLayers, + AGC_LOG_DEFERRED(stdout, "Texture address=0x%llx %ux%u mips=%u layers=%u dim=%d tile=%d format=%u vk=%d element=%u", static_cast(descriptor.baseAddress), descriptor.width, descriptor.height, descriptor.mipCount, arrayLayers, static_cast(descriptor.dimension), static_cast(descriptor.tileMode), descriptor.format, static_cast(vkFormat), elementBytes); - for (const auto& mip : mips) APS5_LOG_OUT(" mip %ux%u tiled=0x%llx+0x%llx linear=0x%llx+0x%llx blocksPerRow=%u pitch=%u tail=%d", mip.width, mip.height, static_cast(mip.tiledOffset), static_cast(mip.tiledSize), static_cast(mip.linearOffset), static_cast(mip.linearSize), mip.blocksPerRow, mip.pitchBytes, mip.tail ? 1 : 0); + for (const auto& mip : mips) AGC_LOG_DEFERRED(stdout, " mip %ux%u tiled=0x%llx+0x%llx linear=0x%llx+0x%llx blocksPerRow=%u pitch=%u tail=%d", mip.width, mip.height, static_cast(mip.tiledOffset), static_cast(mip.tiledSize), static_cast(mip.linearOffset), static_cast(mip.linearSize), mip.blocksPerRow, mip.pitchBytes, mip.tail ? 1 : 0); const auto guestBytes = geometry.guestBytes; Require(snapshot.size() == guestBytes, "texture snapshot size mismatch"); @@ -504,7 +505,7 @@ Texture::Texture(const Context& context, const std::shared_ptr& copyLevels = descriptor.mipCount; copyLayers = geometry.imageLayers; copyElementBytes = BytesPerElement(descriptor.format); - APS5_LOG_OUT("Texture address=0x%llx %ux%u mips=%u layers=%u block copy of storage image (guest format %u -> vk %d)", static_cast(descriptor.baseAddress), descriptor.width, descriptor.height, descriptor.mipCount, copyLayers, source->Descriptor().format, static_cast(blockFormat)); + AGC_LOG_DEFERRED(stdout, "Texture address=0x%llx %ux%u mips=%u layers=%u block copy of storage image (guest format %u -> vk %d)", static_cast(descriptor.baseAddress), descriptor.width, descriptor.height, descriptor.mipCount, copyLayers, source->Descriptor().format, static_cast(blockFormat)); VkImageCreateInfo imageInfo{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO}; imageInfo.imageType = VK_IMAGE_TYPE_2D; @@ -560,7 +561,7 @@ Texture::Texture(const Context& context, const std::shared_ptr& copyLevels = descriptor.mipCount; copyLayers = geometry.imageLayers; copyElementBytes = BytesPerElement(descriptor.format); - APS5_LOG_OUT("Texture address=0x%llx %ux%u mips=%u layers=%u depth copy of storage image (vk=%d -> %d)", static_cast(descriptor.baseAddress), descriptor.width, descriptor.height, descriptor.mipCount, copyLayers, static_cast(colorFormat), static_cast(depthFormat)); + AGC_LOG_DEFERRED(stdout, "Texture address=0x%llx %ux%u mips=%u layers=%u depth copy of storage image (vk=%d -> %d)", static_cast(descriptor.baseAddress), descriptor.width, descriptor.height, descriptor.mipCount, copyLayers, static_cast(colorFormat), static_cast(depthFormat)); VkImageCreateInfo imageInfo{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO}; imageInfo.flags = descriptor.dimension == TextureDimension::kCube ? VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT : 0u; @@ -747,7 +748,7 @@ Texture::Texture(const Context& context, const std::shared_ptr& Require(source != nullptr && CanCopyFrom(*source, descriptor), "storage image does not match the sampled texture"); const auto vkFormat = SampledTextureFormat(context, descriptor.format); const auto geometry = DescribeSurface(descriptor); - APS5_LOG_OUT("Texture address=0x%llx %ux%u mips=%u viewed from storage image (vk=%d)", static_cast(descriptor.baseAddress), descriptor.width, descriptor.height, descriptor.mipCount, static_cast(vkFormat)); + AGC_LOG_DEFERRED(stdout, "Texture address=0x%llx %ux%u mips=%u viewed from storage image (vk=%d)", static_cast(descriptor.baseAddress), descriptor.width, descriptor.height, descriptor.mipCount, static_cast(vkFormat)); // Storage images stay in the general layout; the view samples them there. layout = VK_IMAGE_LAYOUT_GENERAL; const auto viewLevelCount = std::min(descriptor.lastLevel, descriptor.mipCount - 1u) - descriptor.baseLevel + 1u; @@ -1087,7 +1088,7 @@ StorageTexture::StorageTexture(const Context& context, TextureDetiler& detiler, Require(mipLevel < descriptor.mipCount, "storage texture mip level is outside the texture"); const auto vkFormat = StorageFormatFor(context, ResolveTextureFormat(descriptor.format)); storageFormat = vkFormat; - APS5_LOG_OUT("StorageTexture address=0x%llx %ux%u mips=%u mip=%u layers=%u base=%u dim=%d tile=%d format=%u vk=%d", static_cast(descriptor.baseAddress), descriptor.width, descriptor.height, descriptor.mipCount, mipLevel, + AGC_LOG_DEFERRED(stdout, "StorageTexture address=0x%llx %ux%u mips=%u mip=%u layers=%u base=%u dim=%d tile=%d format=%u vk=%d", static_cast(descriptor.baseAddress), descriptor.width, descriptor.height, descriptor.mipCount, mipLevel, descriptor.depthOrLastArray, descriptor.baseArray, static_cast(descriptor.dimension), static_cast(descriptor.tileMode), descriptor.format, static_cast(vkFormat)); geometry = DescribeSurface(descriptor); mips = geometry.mips; diff --git a/core/libs/prx/libSceAgcDriver/tests/AsyncLog.cpp b/core/libs/prx/libSceAgcDriver/tests/AsyncLog.cpp new file mode 100644 index 0000000000..280ef53467 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/AsyncLog.cpp @@ -0,0 +1,69 @@ +#include "prx/libSceAgcDriver/Execution/include/AsyncLog.hpp" +#include +#include +#include + +namespace { + +void Check(bool condition, const char* message) { + if (!condition) throw std::runtime_error(message); +} + +std::string ReadAll(std::FILE* file) { + std::fflush(file); + std::rewind(file); + std::string text; + char buffer[4096]; + for (std::size_t read = 0; (read = std::fread(buffer, 1, sizeof(buffer), file)) != 0;) text.append(buffer, read); + return text; +} + +void OrderPerThread() { + std::FILE* file = std::tmpfile(); + Check(file != nullptr, "temporary file"); + auto& log = AgcDriver::AsyncLog::Get(); + std::string expected; + for (int i = 0; i < 2000; ++i) { + log.Defer(file, "deferred %d %s %llu\n", i, AgcDriver::LogLiteral{"literal"}, static_cast(i) * 3ull); + expected += "deferred " + std::to_string(i) + " literal " + std::to_string(static_cast(i) * 3ull) + "\n"; + const std::string plain = "plain " + std::to_string(i) + "\n"; + log.Write(file, plain.data(), plain.size()); + expected += plain; + } + log.Flush(); + Check(ReadAll(file) == expected, "deferred and written lines changed order or content"); + std::fclose(file); +} + +void LongLine() { + std::FILE* file = std::tmpfile(); + Check(file != nullptr, "temporary file"); + const std::string longText(3000, 'x'); + AgcDriver::AsyncLog::Get().Defer(file, "%s|%d\n", AgcDriver::LogLiteral{longText.c_str()}, 7); + AgcDriver::AsyncLog::Get().Flush(); + Check(ReadAll(file) == longText + "|7\n", "a line longer than the stack buffer changed"); + std::fclose(file); +} + +void Coalescing() { + auto& log = AgcDriver::AsyncLog::Get(); + const std::string line = "[gpu] coalescing test line\n"; + int printed = 0; + for (int i = 0; i < 50; ++i) printed += log.Admit(line) ? 1 : 0; + Check(printed == 3, "identical lines in one window were not limited to the first three"); + Check(log.Admit("[gpu] another line\n"), "a different line was suppressed"); +} + +} + +int main() { + try { + OrderPerThread(); + LongLine(); + Coalescing(); + } catch (const std::exception& error) { + std::cerr << error.what() << std::endl; + return 1; + } + return 0; +} diff --git a/core/libs/prx/libc/include/general/LogMacros.hpp b/core/libs/prx/libc/include/general/LogMacros.hpp index 46ba77734e..00e511b24b 100644 --- a/core/libs/prx/libc/include/general/LogMacros.hpp +++ b/core/libs/prx/libc/include/general/LogMacros.hpp @@ -1,6 +1,7 @@ #ifndef CORE_LIBS_PRX_LIBC_INCLUDE_GENERAL_LOGMACROS_HPP #define CORE_LIBS_PRX_LIBC_INCLUDE_GENERAL_LOGMACROS_HPP +#include #include #include #include @@ -8,6 +9,25 @@ #include #include +using LogSinkFn = int (*)(std::FILE* stream, const char* text, std::size_t length); + +inline std::atomic& LogSink() { + static std::atomic sink{nullptr}; + return sink; +} + +inline long long LogTimestampMicros() { + static const bool timestamps = std::getenv("APS5_LOG_TIMESTAMPS") != nullptr; + if (!timestamps) return -1; + using Clock = std::chrono::steady_clock; + static const auto started = Clock::now(); + return static_cast(std::chrono::duration_cast(Clock::now() - started).count()); +} + +inline int FormatLogTimestamp(char* out, std::size_t capacity, long long micros) { + return std::snprintf(out, capacity, "[+%llu.%06llu s] ", static_cast(micros / 1000000), static_cast(micros % 1000000)); +} + #ifdef __MINGW_PRINTF_FORMAT [[gnu::format(__MINGW_PRINTF_FORMAT, 2, 3)]] #else @@ -16,14 +36,9 @@ inline int WriteLogLine(std::FILE* stream, const char* format, ...) { char small[1024]; std::size_t prefixLength = 0; - static const bool timestamps = std::getenv("APS5_LOG_TIMESTAMPS") != nullptr; - if (timestamps) { - using Clock = std::chrono::steady_clock; - static const auto started = Clock::now(); - const auto elapsed = std::chrono::duration_cast(Clock::now() - started).count(); - const auto seconds = static_cast(elapsed / 1000000); - const auto microseconds = static_cast(elapsed % 1000000); - const int written = std::snprintf(small, sizeof(small), "[+%llu.%06llu s] ", seconds, microseconds); + const long long micros = LogTimestampMicros(); + if (micros >= 0) { + const int written = FormatLogTimestamp(small, sizeof(small), micros); if (written < 0 || written >= static_cast(sizeof(small))) return -1; prefixLength = static_cast(written); } @@ -43,7 +58,9 @@ inline int WriteLogLine(std::FILE* stream, const char* format, ...) { va_end(again); if (length < 0) return length; const auto totalLength = prefixLength + static_cast(length); - std::fwrite(large.empty() ? small : large.data(), 1, totalLength, stream); + const char* text = large.empty() ? small : large.data(); + if (const auto sink = LogSink().load(std::memory_order_acquire)) return sink(stream, text, totalLength); + std::fwrite(text, 1, totalLength, stream); return std::fflush(stream); } From 4e264726035e33b477e13a7021c6bbfe26782734 Mon Sep 17 00:00:00 2001 From: AlxcBTW <187763517+AlxcBTW@users.noreply.github.com> Date: Thu, 15 Oct 2026 16:19:45 +0200 Subject: [PATCH 0393/1000] fix(agc): Flush waits until the logger has written every queued line --- core/libs/prx/libSceAgcDriver/Execution/include/AsyncLog.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/AsyncLog.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/AsyncLog.hpp index 8834650ea2..a81897f807 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/AsyncLog.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/AsyncLog.hpp @@ -153,7 +153,7 @@ public: void Flush() { if (!asynchronous) return; std::unique_lock lock(mutex); - idle.wait_for(lock, std::chrono::seconds(2), [&] { return pending.empty() && !writing; }); + idle.wait(lock, [&] { return pending.empty() && !writing; }); } private: From 99ada24cc5fd94567af17ea1ec7d674cf4b7ac72 Mon Sep 17 00:00:00 2001 From: AlxcBTW <187763517+AlxcBTW@users.noreply.github.com> Date: Thu, 15 Oct 2026 20:41:45 +0200 Subject: [PATCH 0394/1000] test(agc): Flush waits for a held stream and a full queue blocks the caller Flush must not return while the logger still waits for a stream that another thread holds (the regression reported on the pull request: it gave up after two seconds), and a full queue must block its caller instead of dropping a line. AsyncLog::Capacity becomes public so the test sizes its load from it. --- .../Execution/include/AsyncLog.hpp | 3 +- .../prx/libSceAgcDriver/tests/AsyncLog.cpp | 148 ++++++++++++++++++ 2 files changed, 150 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/AsyncLog.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/AsyncLog.hpp index a81897f807..0b135abd59 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/AsyncLog.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/AsyncLog.hpp @@ -86,6 +86,8 @@ class AsyncLog { public: using Clock = std::chrono::steady_clock; + static constexpr std::size_t Capacity = 16u * 1024u * 1024u; + static AsyncLog& Get() { static AsyncLog* const instance = new AsyncLog(); return *instance; @@ -163,7 +165,6 @@ private: std::uint64_t suppressed; }; - static constexpr std::size_t Capacity = 16u * 1024u * 1024u; static constexpr std::size_t CoalesceKeys = 128; static constexpr std::uint64_t PrintedRepeats = 3; static constexpr std::chrono::seconds CoalesceWindow{10}; diff --git a/core/libs/prx/libSceAgcDriver/tests/AsyncLog.cpp b/core/libs/prx/libSceAgcDriver/tests/AsyncLog.cpp index 280ef53467..64edda20f0 100644 --- a/core/libs/prx/libSceAgcDriver/tests/AsyncLog.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/AsyncLog.cpp @@ -1,10 +1,18 @@ #include "prx/libSceAgcDriver/Execution/include/AsyncLog.hpp" +#include +#include +#include +#include #include #include #include +#include namespace { +using namespace std::chrono_literals; +using Clock = AgcDriver::AsyncLog::Clock; + void Check(bool condition, const char* message) { if (!condition) throw std::runtime_error(message); } @@ -18,6 +26,76 @@ std::string ReadAll(std::FILE* file) { return text; } +void ClearLogEnvironment() { +#ifdef _WIN32 + _putenv("APS5_LOG_SYNC="); + _putenv("APS5_LOG_TIMESTAMPS="); +#else + unsetenv("APS5_LOG_SYNC"); + unsetenv("APS5_LOG_TIMESTAMPS"); +#endif +} + +void LockStream(std::FILE* file) { +#ifdef _WIN32 + _lock_file(file); +#else + flockfile(file); +#endif +} + +void UnlockStream(std::FILE* file) { +#ifdef _WIN32 + _unlock_file(file); +#else + funlockfile(file); +#endif +} + +class HeldStream { +public: + explicit HeldStream(std::FILE* file) : file(file) { + holder = std::thread([this] { + LockStream(this->file); + held.store(true); + while (!releaseRequested.load()) std::this_thread::sleep_for(1ms); + released.store(true); + UnlockStream(this->file); + }); + while (!held.load()) std::this_thread::sleep_for(1ms); + } + + HeldStream(const HeldStream&) = delete; + HeldStream& operator=(const HeldStream&) = delete; + + ~HeldStream() { + Release(); + holder.join(); + } + + void Release() { + releaseRequested.store(true); + } + + bool Released() const { + return released.load(); + } + +private: + std::FILE* file; + std::atomic held{false}; + std::atomic releaseRequested{false}; + std::atomic released{false}; + std::thread holder; +}; + +std::string LargeLine(std::size_t index, std::size_t bytes) { + std::string line = "large " + std::to_string(index) + " "; + line.resize(bytes - 1, 'x'); + line += '\n'; + return line; +} + void OrderPerThread() { std::FILE* file = std::tmpfile(); Check(file != nullptr, "temporary file"); @@ -54,13 +132,83 @@ void Coalescing() { Check(log.Admit("[gpu] another line\n"), "a different line was suppressed"); } +void FlushWaitsForHeldStream() { + std::FILE* file = std::tmpfile(); + Check(file != nullptr, "temporary file"); + auto& log = AgcDriver::AsyncLog::Get(); + { + HeldStream held(file); + log.Defer(file, "held %d %s\n", 7, AgcDriver::LogLiteral{"line"}); + std::thread releaser([&] { + std::this_thread::sleep_for(2400ms); + held.Release(); + }); + log.Flush(); + const bool waited = held.Released(); + releaser.join(); + Check(waited, "Flush returned while the logger still waited for the stream"); + } + Check(ReadAll(file) == "held 7 line\n", "the line queued behind a held stream changed"); + std::fclose(file); +} + +void FullQueueBlocksCaller() { + std::FILE* file = std::tmpfile(); + Check(file != nullptr, "temporary file"); + auto& log = AgcDriver::AsyncLog::Get(); + constexpr std::size_t lineBytes = 1024 * 1024; + constexpr std::size_t lines = 2 * AgcDriver::AsyncLog::Capacity / lineBytes + 4; + constexpr std::size_t filled = AgcDriver::AsyncLog::Capacity / lineBytes / 2; + std::atomic queued{0}; + bool blocked = false; + { + HeldStream held(file); + std::thread producer([&] { + for (std::size_t i = 0; i < lines; ++i) { + const std::string line = LargeLine(i, lineBytes); + log.Write(file, line.data(), line.size()); + queued.store(i + 1); + } + }); + auto last = queued.load(); + auto changedAt = Clock::now(); + const auto deadline = changedAt + 10s; + while (last < lines && Clock::now() < deadline && (last < filled || Clock::now() - changedAt < 500ms)) { + std::this_thread::sleep_for(5ms); + const auto now = queued.load(); + if (now != last) { + last = now; + changedAt = Clock::now(); + } + } + blocked = last >= filled && last < lines && Clock::now() - changedAt >= 500ms; + held.Release(); + producer.join(); + } + Check(blocked, "the caller was not blocked by a full queue"); + log.Flush(); + std::fflush(file); + std::rewind(file); + std::string line; + for (std::size_t i = 0; i < lines; ++i) { + const std::string expected = LargeLine(i, lineBytes); + line.resize(expected.size()); + Check(std::fread(line.data(), 1, line.size(), file) == line.size() && line == expected, "a line written through a full queue changed or went missing"); + } + Check(std::fgetc(file) == EOF, "a full queue wrote extra output"); + std::fclose(file); +} + } int main() { try { + ClearLogEnvironment(); OrderPerThread(); LongLine(); Coalescing(); + FlushWaitsForHeldStream(); + FullQueueBlocksCaller(); } catch (const std::exception& error) { std::cerr << error.what() << std::endl; return 1; From b82d688ed837ef03356678339ca53f51716809fd Mon Sep 17 00:00:00 2001 From: AlxcBTW <187763517+AlxcBTW@users.noreply.github.com> Date: Thu, 15 Oct 2026 20:41:59 +0200 Subject: [PATCH 0395/1000] fix(agc): flush the log queue, bounded, where the process ends without exit handlers Lines queued behind the logger were lost when the process ended through abort(), std::terminate, the Windows crash report or a guest _Exit, because only driver failure, Driver::stop and a normal exit flushed the queue. libc keeps one end flush (LibcSetEndFlush_nid_no_patch) and runs it from those paths. Its first registration chains a SIGABRT handler in front of the previous one, so every abort() and std::terminate in the process is covered, on Linux as well; with the default disposition the handler restores it and raises again, so raise(SIGABRT) still ends the process. Guest abort() and _Exit, and ReportCrash for the first fatal exception, run the flush directly. AsyncLog registers a bounded flush (FlushFor, 2 s) that polls two counters and takes no lock, so a dying process cannot hang on a blocked stream or on the queue lock; after its first timeout it returns at once. Flush counts queued and finished entries instead of watching the queue and the writer flag, so it waits for the lines queued before the call and not for a moment in which no thread is logging. agc_async_log_fatal runs the test executable as a child that queues lines behind a held stream and ends in abort(), std::terminate(), raise(SIGABRT) (POSIX) or an access violation (Windows), and once more with the stream never released. The lines of the first four must arrive in order, the last child must end after the bound, and every child has a watchdog. agc_async_log gains the bounded flush case. --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 5 +- .../Execution/include/AsyncLog.hpp | 32 ++++- .../prx/libSceAgcDriver/tests/AsyncLog.cpp | 122 +++++++++++++++++- core/libs/prx/libc/include/Shutdown.hpp | 2 + core/libs/prx/libc/src/Process.cpp | 35 +++++ .../src/specifics/windows/CrashReport.cpp | 2 + 6 files changed, 191 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index d1caac1390..29b578d311 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -353,9 +353,10 @@ if(BUILD_TESTING) target_link_libraries(agc_async_log_tests PRIVATE libc) configure_windows_unwind(agc_async_log_tests) add_test(NAME agc_async_log COMMAND agc_async_log_tests) - set_tests_properties(agc_async_log PROPERTIES TIMEOUT 20) + add_test(NAME agc_async_log_fatal COMMAND agc_async_log_tests fatal) + set_tests_properties(agc_async_log agc_async_log_fatal PROPERTIES TIMEOUT 20) if(WIN32) - set_tests_properties(agc_async_log PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(agc_async_log agc_async_log_fatal PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/AsyncLog.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/AsyncLog.hpp index 0b135abd59..383e554f61 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/AsyncLog.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/AsyncLog.hpp @@ -1,5 +1,6 @@ #pragma once +#include "prx/libc/include/Shutdown.hpp" #include "prx/libc/include/general/LogMacros.hpp" #include #include @@ -155,7 +156,19 @@ public: void Flush() { if (!asynchronous) return; std::unique_lock lock(mutex); - idle.wait(lock, [&] { return pending.empty() && !writing; }); + const auto target = queued.load(std::memory_order_relaxed); + idle.wait(lock, [&] { return finished.load(std::memory_order_relaxed) >= target; }); + } + + bool FlushFor(std::chrono::milliseconds limit) { + if (!asynchronous) return true; + const auto target = queued.load(std::memory_order_acquire); + const auto deadline = Clock::now() + limit; + while (finished.load(std::memory_order_acquire) < target) { + if (Clock::now() >= deadline) return false; + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + return true; } private: @@ -170,6 +183,7 @@ private: static constexpr std::chrono::seconds CoalesceWindow{10}; static constexpr std::chrono::seconds StatsWindow{10}; static constexpr std::chrono::milliseconds Tick{500}; + static constexpr std::chrono::milliseconds EndFlushLimit{2000}; AsyncLog() { asynchronous = std::getenv("APS5_LOG_SYNC") == nullptr; @@ -180,6 +194,13 @@ private: LogSink().store(&AsyncLog::Sink, std::memory_order_release); std::atexit([] { Get().Flush(); }); std::thread([this] { Run(); }).detach(); + LibcSetEndFlush_nid_no_patch(&AsyncLog::FlushAtEnd); + } + + static void FlushAtEnd() { + auto& log = Get(); + if (log.abandoned.load(std::memory_order_acquire)) return; + if (!log.FlushFor(EndFlushLimit)) log.abandoned.store(true, std::memory_order_release); } static int Sink(std::FILE* stream, const char* text, std::size_t length) { @@ -217,6 +238,7 @@ private: bytes += entry.cost; peak = std::max(peak, bytes); pending.push_back(std::move(entry)); + queued.fetch_add(1, std::memory_order_release); notify = sleeping; sleeping = false; } @@ -234,11 +256,11 @@ private: sleeping = false; batch.swap(pending); bytes = 0; - writing = !batch.empty(); } space.notify_all(); if (!batch.empty()) { const auto start = Clock::now(); + const auto count = batch.size(); try { Drain(batch, buffer); } catch (...) { @@ -248,7 +270,7 @@ private: loggerNanoseconds.fetch_add(Elapsed(start), std::memory_order_relaxed); { std::lock_guard lock(mutex); - writing = false; + finished.fetch_add(count, std::memory_order_release); } idle.notify_all(); } @@ -339,7 +361,9 @@ private: std::size_t bytes = 0; std::size_t peak = 0; bool sleeping = false; - bool writing = false; + std::atomic queued{0}; + std::atomic finished{0}; + std::atomic abandoned{false}; std::mutex coalesceMutex; std::map windows; Clock::time_point lastStats; diff --git a/core/libs/prx/libSceAgcDriver/tests/AsyncLog.cpp b/core/libs/prx/libSceAgcDriver/tests/AsyncLog.cpp index 64edda20f0..0325795a41 100644 --- a/core/libs/prx/libSceAgcDriver/tests/AsyncLog.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/AsyncLog.cpp @@ -1,12 +1,29 @@ #include "prx/libSceAgcDriver/Execution/include/AsyncLog.hpp" #include #include +#include #include #include +#include +#include +#include #include +#include #include #include +#include #include +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#include +#else +#include +#endif namespace { @@ -26,6 +43,11 @@ std::string ReadAll(std::FILE* file) { return text; } +std::string ReadFile(const std::filesystem::path& path) { + std::ifstream stream(path, std::ios::binary); + return std::string(std::istreambuf_iterator(stream), std::istreambuf_iterator()); +} + void ClearLogEnvironment() { #ifdef _WIN32 _putenv("APS5_LOG_SYNC="); @@ -152,6 +174,22 @@ void FlushWaitsForHeldStream() { std::fclose(file); } +void BoundedFlushGivesUp() { + std::FILE* file = std::tmpfile(); + Check(file != nullptr, "temporary file"); + auto& log = AgcDriver::AsyncLog::Get(); + Check(log.FlushFor(0ms), "FlushFor waited with nothing queued"); + { + HeldStream held(file); + log.Defer(file, "bounded %d\n", 1); + Check(!log.FlushFor(100ms), "FlushFor reported a line written while its stream was held"); + held.Release(); + Check(log.FlushFor(10s), "FlushFor missed the line written after the stream was released"); + } + Check(ReadAll(file) == "bounded 1\n", "the line queued behind a held stream changed"); + std::fclose(file); +} + void FullQueueBlocksCaller() { std::FILE* file = std::tmpfile(); Check(file != nullptr, "temporary file"); @@ -199,15 +237,97 @@ void FullQueueBlocksCaller() { std::fclose(file); } +constexpr int ChildLines = 50; + +[[noreturn]] void EndChild(const std::string& mode, const char* output) { + std::FILE* file = std::fopen(output, "wb"); + Check(file != nullptr, "child output file"); +#ifdef _WIN32 + SetErrorMode(SEM_FAILCRITICALERRORS | SEM_NOGPFAULTERRORBOX); + _set_abort_behavior(0, _WRITE_ABORT_MSG | _CALL_REPORTFAULT); +#else + const rlimit noCore{0, 0}; + setrlimit(RLIMIT_CORE, &noCore); +#endif + std::thread([] { + std::this_thread::sleep_for(8s); + std::_Exit(99); + }).detach(); + AgcDriver::InstallAsyncLog_nid_no_patch(); + auto& log = AgcDriver::AsyncLog::Get(); + HeldStream held(file); + for (int i = 0; i < ChildLines; ++i) { + log.Defer(file, "deferred %d\n", i); + WriteLogLine(file, "plain %d\n", i); + } + std::thread releaser([&] { + std::this_thread::sleep_for(100ms); + if (mode != "blocked") held.Release(); + }); + if (mode == "terminate") std::terminate(); +#ifdef _WIN32 + if (mode == "fault") RaiseException(EXCEPTION_ACCESS_VIOLATION, EXCEPTION_NONCONTINUABLE, 0, nullptr); +#else + if (mode == "raise") { + std::raise(SIGABRT); + std::_Exit(0); + } +#endif + std::abort(); } -int main() { +std::string Quote(const std::string& text) { + return "\"" + text + "\""; +} + +void LinesSurviveEnd(const std::string& self, const std::string& mode) { + const auto output = std::filesystem::temp_directory_path() / ("aps5-async-log-" + mode + "-" + std::to_string(Clock::now().time_since_epoch().count()) + ".txt"); + std::string command = Quote(self) + " child " + mode + " " + Quote(output.string()); +#ifdef _WIN32 + command = Quote(command); +#endif + const auto start = Clock::now(); + const int status = std::system(command.c_str()); + const auto lived = Clock::now() - start; + std::error_code error; + const bool started = std::filesystem::exists(output, error); + const std::string text = started ? ReadFile(output) : std::string(); + std::filesystem::remove(output, error); + Check(started, "the child did not start"); + Check(status != 0, "the child did not end abnormally"); + Check(lived < 6s, "the child took too long to end"); + if (mode == "blocked") { + Check(lived >= 1900ms, "the child ended before the flush gave up"); + Check(text.empty(), "the child wrote through a held stream"); + return; + } + std::string expected; + for (int i = 0; i < ChildLines; ++i) expected += "deferred " + std::to_string(i) + "\nplain " + std::to_string(i) + "\n"; + Check(text == expected, "lines queued before the process ended were lost or changed"); +} + +} + +int main(int argc, char** argv) { try { ClearLogEnvironment(); + if (argc == 4 && std::strcmp(argv[1], "child") == 0) EndChild(argv[2], argv[3]); + if (argc == 2 && std::strcmp(argv[1], "fatal") == 0) { + LinesSurviveEnd(argv[0], "abort"); + LinesSurviveEnd(argv[0], "terminate"); +#ifdef _WIN32 + LinesSurviveEnd(argv[0], "fault"); +#else + LinesSurviveEnd(argv[0], "raise"); +#endif + LinesSurviveEnd(argv[0], "blocked"); + return 0; + } OrderPerThread(); LongLine(); Coalescing(); FlushWaitsForHeldStream(); + BoundedFlushGivesUp(); FullQueueBlocksCaller(); } catch (const std::exception& error) { std::cerr << error.what() << std::endl; diff --git a/core/libs/prx/libc/include/Shutdown.hpp b/core/libs/prx/libc/include/Shutdown.hpp index 4bfe737f75..af9ae548d1 100644 --- a/core/libs/prx/libc/include/Shutdown.hpp +++ b/core/libs/prx/libc/include/Shutdown.hpp @@ -14,5 +14,7 @@ extern "C" [[noreturn]] void LibcAwaitExit_nid_postfix(); extern "C" [[noreturn]] void LibcExit_nid_no_patch(int code); extern "C" [[noreturn]] void LibcExitWithoutHandlers_nid_no_patch(int code); extern "C" [[noreturn]] void LibcTerminate_nid_no_patch(int code); +extern "C" void LibcSetEndFlush_nid_no_patch(void (*flush)()); +extern "C" void LibcRunEndFlush_nid_no_patch(); #endif diff --git a/core/libs/prx/libc/src/Process.cpp b/core/libs/prx/libc/src/Process.cpp index d4bca7d3f2..8bf2797336 100644 --- a/core/libs/prx/libc/src/Process.cpp +++ b/core/libs/prx/libc/src/Process.cpp @@ -1,3 +1,4 @@ +#include #include #include #include @@ -98,6 +99,38 @@ extern "C" void LibcRunShutdown_nid_postfix() { namespace { +std::atomic endFlush{nullptr}; +void (*previousAbortHandler)(int) = SIG_DFL; + +void FlushOnAbort(int number) { + LibcRunEndFlush_nid_no_patch(); + if (previousAbortHandler == SIG_IGN) return; + if (previousAbortHandler != SIG_DFL && previousAbortHandler != SIG_ERR) { + previousAbortHandler(number); + return; + } +#ifndef _WIN32 + std::signal(number, SIG_DFL); + std::raise(number); +#endif +} + +} + +extern "C" void LibcSetEndFlush_nid_no_patch(void (*flush)()) { + if (flush == nullptr) throw std::runtime_error("libc: invalid end flush registration"); + void (*registered)() = nullptr; + if (!endFlush.compare_exchange_strong(registered, flush) && registered != flush) throw std::runtime_error("libc: end flush already registered"); + static std::once_flag installed; + std::call_once(installed, [] { previousAbortHandler = std::signal(SIGABRT, FlushOnAbort); }); +} + +extern "C" void LibcRunEndFlush_nid_no_patch() { + if (const auto flush = endFlush.load(std::memory_order_acquire)) flush(); +} + +namespace { + void StartExit() { exitRequested.store(true); if (exitStarted.exchange(true)) { @@ -122,6 +155,7 @@ extern "C" { } [[noreturn]] void LibcTerminate_nid_no_patch(int code) { + LibcRunEndFlush_nid_no_patch(); #ifdef _WIN32 TerminateProcess(GetCurrentProcess(), static_cast(code)); #endif @@ -159,6 +193,7 @@ void APS5_VABI exit_nid_postfix(int code) { ) { (void)arg0; (void)arg1; (void)arg2; (void)arg3; (void)arg4; (void)arg5; + LibcRunEndFlush_nid_no_patch(); std::fprintf(stderr, "[libc] abort() called from %p\n", __builtin_return_address(0)); std::fflush(nullptr); std::abort(); diff --git a/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp b/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp index 6491da364c..13c9ee44ec 100644 --- a/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp +++ b/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp @@ -3,6 +3,7 @@ #include #include "prx/libc/include/CallTrace.hpp" #include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/Shutdown.hpp" #include #include #include @@ -304,6 +305,7 @@ LONG WINAPI ReportCrash(EXCEPTION_POINTERS* info) { // Another thread is already reporting; let it finish before this fault ends the process. Sleep(INFINITE); } + LibcRunEndFlush_nid_no_patch(); const auto* context = info->ContextRecord; char line[MAX_PATH + 64]; char threadName[128] = ""; From e44ab157e05084528940b0546b0d713462042689 Mon Sep 17 00:00:00 2001 From: AlxcBTW <187763517+AlxcBTW@users.noreply.github.com> Date: Thu, 15 Oct 2026 20:41:59 +0200 Subject: [PATCH 0396/1000] docs: describe the log queue, its flush points and the stream order in ARCHITECTURE.md The logger thread, the bounded queue that blocks its caller, the LogSink hook, APS5_LOG_SYNC and APS5_LOG_STATS, where the queue is flushed and what stays outside it: direct fprintf(stderr) and printf calls are written at once, so they can appear before older queued lines. --- docs/dev/ARCHITECTURE.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/docs/dev/ARCHITECTURE.md b/docs/dev/ARCHITECTURE.md index 54b4476de1..0a9a669545 100644 --- a/docs/dev/ARCHITECTURE.md +++ b/docs/dev/ARCHITECTURE.md @@ -109,6 +109,25 @@ flowchart LR - Guest memory reaches the GPU through host imports (`VK_EXT_external_memory_host`) where the driver accepts them, and through copies otherwise ([`GuestBufferMemory.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp)). A registered range larger than the import window (64 MiB) is imported per window for buffer and fill accesses and whole for an address-based build. Live imports stay under a budget, with the least recently used import retired first (see TechnicalDebt, Host import windows and budget). - On Windows, a host import of private `MEM_WRITE_WATCH` memory (singly mapped direct memory) stays on `GetWriteWatch` when `ProbeImportWriteProtection` shows the driver reports nothing written after the second GPU read and a GPU write, and a CPU store is collected (`decideImportWatch`). The driver marks every page of such an import written, asynchronously, until its first GPU use completes, so `settleImport` in [`GuestBufferMemory.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) copies 4 bytes out of the new import on a second queue of the graphics family, or on a queue of another copy-capable family when the graphics family has only one (`Context::settleQueue`, own pool and mutex) inside `ImportWatched`'s window and waits for it, only for imports of 1 MiB or more (a smaller import is compared, so a title that maps, imports and unmaps a small buffer around every submission does not block its mapping thread); the main queue can't be used there because `importAllocation` holds the imports lock and `GpuMutex` comes before it in the lock order. Imports through a section alias keep tracking on the guest views, and every other Windows host import is compared. A device without such a queue keeps the comparisons. +### Log output + +```mermaid +flowchart LR + callers["Any thread:
WriteLogLine,
AGC_LOG_DEFERRED"] --> queue["Queue, 16 MiB:
a full queue blocks the caller"] + queue --> logger["Logger thread:
format, join lines per stream,
fwrite and flush"] + logger --> streams["stdout, stderr"] + exits["Abort, terminate, crash report,
_Exit"] -- "bounded flush, 2 s" --> queue +``` + +`WriteLogLine` ([`LogMacros.hpp`](../../core/libs/prx/libc/include/general/LogMacros.hpp), behind `APS5_LOG_OUT` and `APS5_LOG_ERR`) formats a line on the calling thread and hands it to the module's `LogSink()` when one is installed; without a sink it writes and flushes it there. `Driver` installs [`AsyncLog`](../../core/libs/prx/libSceAgcDriver/Execution/include/AsyncLog.hpp) as the sink of `libSceAgcDriver`, so the queue workers no longer wait for the console or the log file for those lines. `APS5_LOG_SYNC` (any value) skips the installation and restores the synchronous path. + +- **Queue.** One logger thread drains a queue that holds at most 16 MiB of waiting lines; the batch the logger is writing comes on top. A caller that finds the queue full blocks until the logger has taken the pending batch, so no line is dropped. The logger writes entries in queue order and joins consecutive entries of one stream; it writes them with one `fwrite` and flush when the text reaches 64 KiB, the stream changes or the batch ends. `AGC_LOG_DEFERRED` queues the format string with numeric, enum and `LogLiteral` arguments and formats them on the logger thread; `APS5_LOG_TIMESTAMPS` stamps a line when it is queued. +- **Order.** Lines that pass through the sink, on stdout and stderr alike, appear in call order. A direct `fprintf(stderr, ...)` or `printf` does not pass through it and is written at once, so it can appear before older queued lines; the coalesced `deferred write-back failed` notes are of this kind. `APS5_LOG_SYNC=1` keeps one order for all of them. +- **Flush.** `Flush` waits without a limit until every line queued before the call has been written, including while another thread holds the stream. It runs when the driver fails, in `Driver::stop` and at exit. `AsyncLog` also registers a bounded flush (`FlushFor`, 2 s) with `LibcSetEndFlush_nid_no_patch`; libc runs it where the process ends without those: `abort()` and `std::terminate` (a `SIGABRT` handler that the first registration chains in front of the previous one, so `raise(SIGABRT)` still ends the process), guest `abort()`, the Windows crash report of a fatal exception and guest `_Exit`. A title that installs its own `SIGABRT` disposition replaces that handler. The bounded flush polls two counters and takes no lock, so a thread that dies holding the queue lock or a stream cannot stall it. After 2 s it gives up, later calls return at once, and the lines still queued are lost. +- **Statistics.** A `[log]` line every 10 s in which lines were logged reports lines, bytes, the time spent by the callers and by the logger, and the queue peak; `APS5_LOG_STATS=0` hides it. Identical `deferred write-back failed` notes print three times per 10 s and are then counted in one `repeated N more times` line. + +`agc_async_log` checks the order of deferred and written lines, long lines, the repeat limit, a `Flush` that waits for a held stream, a bounded flush that gives up, and a full queue that blocks its caller. `agc_async_log_fatal` starts the test executable as a child that queues lines behind a held stream and then calls `abort()`, calls `std::terminate()`, raises an access violation (Windows) or `SIGABRT` (elsewhere), or aborts without ever releasing the stream; the first four must leave every line in the file in order, the last must still end after the bound. + ### Runtime image and sampler tables A T# or S# that a shader loads from a buffer at a runtime offset (`key * stride + addend`, or `(key << n) & mask`) is not a direct descriptor. [`ResourceTracker.cpp`](../../core/shader/recompiler/Optimization/src/ResourceTracker.cpp) plans it as a table column (`TableColumn`): the heap V#'s descriptor source, the record stride and addend, the descriptor's offset in the record, and the key domains that may narrow it. Accesses with the same heap and record shape share one column. The shader keeps computing the offset and indexes a table binding: images use one variable-count `SAMPLED_IMAGE` array (`RuntimeAbi::ImageTableBinding`, in the fragment set when pipeline libraries split the sets), samplers a `SAMPLER` array (`Binding::SamplerTable`). Devices without `runtimeDescriptorArray`, `descriptorBindingPartiallyBound` and `descriptorBindingVariableDescriptorCount` refuse such shaders. From 244f41ed24de04038f2a8ddbc0bbe7252ac82cd6 Mon Sep 17 00:00:00 2001 From: Scott Schuster Date: Thu, 15 Oct 2026 14:06:12 -0500 Subject: [PATCH 0397/1000] fix(agc): a relocation trial counts the hits the verification captured again With APS5_VERIFY_DRAW_ENTRIES=1 every hit is captured again, so drawHit is false and no relocated hit counted for its shader set's trial, which judged every set unproductive and turned the relocation off under the very mode meant to check it. Co-Authored-By: Claude Fable 5.1 --- .../libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp index 19cc880d2a..5dea7922ea 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp @@ -278,7 +278,7 @@ DrawVerdict Driver::draw(QueueState& queue, std::span packe const auto relocatedDecode = decode; if (!userPatches.empty()) { std::lock_guard cacheLock(drawCacheMutex); - if (drawHit) { + if (drawHit || verifyHit) { ++drawRelocationCounters.hits; ++drawRelocationTrials[drawIdentity].hits; } From 7d9a308044d059723e273d42df4887e01adcff2d Mon Sep 17 00:00:00 2001 From: Muhammad Wasif Ali Wasif <76695420+wachi35676@users.noreply.github.com> Date: Wed, 14 Oct 2026 01:36:30 +0500 Subject: [PATCH 0398/1000] fix(nid): reject ELF exports that patch to different symbols under one name Two defined ELF exports that are renamed to the same name, such as foo and foo_nid_disambig1, were both written as one name and the patcher exited 0, leaving a lookup that can reach only one of them. The PE and Mach-O back ends already reject this. The ELF patcher now remembers the address (st_value and st_shndx) of each patched export name and throws "duplicate patched export name" only when the name comes from a symbol at a different address. Aliases of one address are kept: the Linux libc defines __cxa_throw and __cxa_throw_nid_no_patch_cut as aliases of __cxa_throw_nid_postfix, so both become __cxa_throw, and any lookup of that name resolves to the same code. The Linux libc also exported __gxx_personality_v0 twice at different addresses: the forwarding function in Personality.cpp and the __gxx_personality_v0_nid_no_patch_cut alias of the guest personality. Drop that alias, so the name keeps only the forwarding function, as in the PE build. Co-Authored-By: Claude Opus 5.5 --- core/libs/CMakeLists.txt | 2 + core/libs/nid/src/ElfNidPatcher.cpp | 5 ++ core/libs/nid/tests/test_elf_duplicates.py | 69 ++++++++++++++++++++ core/libs/prx/libc/src/exception/Exports.cpp | 1 - 4 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 core/libs/nid/tests/test_elf_duplicates.py diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index 8385138261..580602db2f 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -221,6 +221,8 @@ if(BUILD_TESTING) if(Python3_Interpreter_FOUND) add_test(NAME nid_gnu_hash COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/nid/tests/test_gnu_hash.py $) + add_test(NAME nid_elf_duplicates COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/nid/tests/test_elf_duplicates.py $) add_test(NAME nid_patcher_macho COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/nid/tests/test_macho_nids.py $) add_test(NAME nid_patcher_native_paths COMMAND ${Python3_EXECUTABLE} diff --git a/core/libs/nid/src/ElfNidPatcher.cpp b/core/libs/nid/src/ElfNidPatcher.cpp index b5470147d9..7a6b8c3d1e 100644 --- a/core/libs/nid/src/ElfNidPatcher.cpp +++ b/core/libs/nid/src/ElfNidPatcher.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -408,6 +409,7 @@ void ElfNidPatcher::PatchNids(std::vector& elf, const std::string& }; std::vector uses; + std::unordered_map> patchedExports; for (std::size_t i = 1u; i < symCount; ++i) { const std::size_t symOffset = dynSymOffset + i * sizeof(Elf64_Sym); const auto sym = Read(elf, symOffset); @@ -424,6 +426,9 @@ void ElfNidPatcher::PatchNids(std::vector& elf, const std::string& const auto it = nidMap.find(symName); if (it != nidMap.end()) newValue = it->second; + const auto [existing, inserted] = patchedExports.try_emplace(newValue, sym.st_value, sym.st_shndx); + if (!inserted && existing->second != std::pair{sym.st_value, sym.st_shndx}) + throw std::runtime_error("duplicate patched export name: " + newValue); } else if (binding != kStbLocal && sym.st_shndx == kShnUndef) { const bool isUnversioned = versymOffset == 0u || (Read(elf, versymOffset + i * sizeof(std::uint16_t)) <= 1u); diff --git a/core/libs/nid/tests/test_elf_duplicates.py b/core/libs/nid/tests/test_elf_duplicates.py new file mode 100644 index 0000000000..504cf33bdc --- /dev/null +++ b/core/libs/nid/tests/test_elf_duplicates.py @@ -0,0 +1,69 @@ +import base64 +import hashlib +import sys +import unittest +from pathlib import Path + +from test_symbol_versions import build_image, patch, read_dynsym, IMPORTS, NID_NAME, SHN_UNDEF, STT_FUNC + + +def nid(name): + digest = hashlib.sha1(name.encode() + bytes.fromhex("518d64a635ded8c1e6b039b1c3e55230")).digest() + return base64.b64encode(digest[:8][::-1]).decode().rstrip("=").replace("/", "-") + + +def image(names, addresses=None): + if addresses is None: + addresses = [0x1200 + index * 0x20 for index in range(len(names))] + entries = [(None, 1, SHN_UNDEF, 0, 0)] + entries += [(name, 1, SHN_UNDEF, 0, 0) for name in IMPORTS] + entries += [(name, 1, 9, STT_FUNC, address) for name, address in zip(names, addresses)] + return bytes(build_image(entries, False)) + + +class ElfDuplicateTests(unittest.TestCase): + def test_exports_patched_to_one_name_are_rejected(self): + cases = ( + ["sceAlphaComputeLength", "sceAlphaComputeLength_nid_disambig1", "sceBetaCopyString"], + ["sceAlphaComputeLength_nid_postfix", "sceAlphaComputeLength_nid_disambig12", "sceBetaCopyString"], + ["sceBetaCopyString", "sceAlphaComputeLength_nid_no_patch", "sceAlphaComputeLength_nid_no_patch_nid_no_patch_cut"], + ) + for names in cases: + with self.subTest(names=names): + before = image(names) + result, after = patch(PATCHER, before, "duplicates.so") + self.assertEqual(result.returncode, 2, (result.returncode, result.stderr)) + self.assertIn(b"duplicate patched export name", result.stderr) + self.assertEqual(after, before) + + def test_aliases_of_one_address_are_patched(self): + names = ["sceAlphaComputeLength", "sceAlphaComputeLength_nid_postfix", "sceAlphaComputeLength_nid_no_patch_cut", "sceBetaCopyString"] + result, after = patch(PATCHER, image(names, [0x1200, 0x1200, 0x1200, 0x1220]), "aliases.so") + self.assertEqual(result.returncode, 0, result.stderr) + patched = [(entry[0], entry[2]) for entry in read_dynsym(after) if entry[1] == 9] + self.assertEqual(sorted(name for name, address in patched if address == 0x1200), + sorted(["sceAlphaComputeLength", "sceAlphaComputeLength", nid("sceAlphaComputeLength")])) + self.assertEqual([name for name, address in patched if address == 0x1220], [nid("sceBetaCopyString")]) + self.assertTrue(NID_NAME.match(nid("sceAlphaComputeLength"))) + + def test_aliases_of_different_addresses_are_rejected(self): + names = ["sceAlphaComputeLength", "sceAlphaComputeLength_nid_postfix", "sceAlphaComputeLength_nid_no_patch_cut"] + before = image(names, [0x1200, 0x1200, 0x1220]) + result, after = patch(PATCHER, before, "aliases.so") + self.assertEqual(result.returncode, 2, (result.returncode, result.stderr)) + self.assertIn(b"duplicate patched export name: sceAlphaComputeLength", result.stderr) + self.assertEqual(after, before) + + def test_distinct_exports_are_still_patched(self): + names = ["sceAlphaComputeLength_nid_disambig1", "sceBetaCopyString_nid_postfix", "sceGammaKeep_nid_no_patch_cut"] + result, after = patch(PATCHER, image(names), "distinct.so") + self.assertEqual(result.returncode, 0, result.stderr) + patched = [entry[0] for entry in read_dynsym(after) if entry[1] == 9] + self.assertEqual(len(patched), 3) + self.assertEqual(len(set(patched)), 3) + self.assertIn("sceGammaKeep", patched) + + +if __name__ == "__main__": + PATCHER = Path(sys.argv.pop(1)).resolve() + unittest.main() diff --git a/core/libs/prx/libc/src/exception/Exports.cpp b/core/libs/prx/libc/src/exception/Exports.cpp index dbc0c7acbc..4f6575b340 100644 --- a/core/libs/prx/libc/src/exception/Exports.cpp +++ b/core/libs/prx/libc/src/exception/Exports.cpp @@ -18,7 +18,6 @@ GCC_GLOBAL_ALIAS(__cxa_throw_nid_no_patch_cut, __cxa_throw_nid_postfix); GCC_GLOBAL_ALIAS(__cxa_begin_catch_nid_no_patch_cut, __cxa_begin_catch_nid_postfix); GCC_GLOBAL_ALIAS(__cxa_end_catch_nid_no_patch_cut, __cxa_end_catch_nid_postfix); GCC_GLOBAL_ALIAS(__cxa_rethrow_nid_no_patch_cut, __cxa_rethrow_nid_postfix); -GCC_GLOBAL_ALIAS(__gxx_personality_v0_nid_no_patch_cut, __gxx_personality_v0_nid_postfix); GCC_GLOBAL_ALIAS(_Unwind_Resume_nid_no_patch_cut, _Unwind_Resume_nid_postfix); GCC_GLOBAL_ALIAS(__cxa_free_exception, __cxa_free_exception_nid_postfix); GCC_GLOBAL_ALIAS(_ZSt9terminatev, _ZSt9terminatev_nid_postfix); From 7ef0bf154174c40fb5e6069c45483f85137630fc Mon Sep 17 00:00:00 2001 From: al3w0f205 Date: Thu, 15 Oct 2026 10:32:43 -0500 Subject: [PATCH 0399/1000] fix(tools): allow unwrap_self to place zero-size segments without a PT_LOAD host When a decrypted SELF contains a non-PT_LOAD segment (such as PT_TLS for pure TBSS) with p_filesz == 0 whose virtual address lies outside any PT_LOAD memory range, unwrap_self previously refused the container with 'segment N vaddr 0x... is in no PT_LOAD'. Allow zero-size segments without a matching PT_LOAD host to be placed with source offset 0 and new offset 0, preserving valid ELF headers for titles like Unity/IL2CPP PS5 builds. Also allow file-empty PT_LOAD segments with stale out-of-file source offsets to skip the segment-extent bounds check, preserving memory sizes, addresses and alignments without reading container metadata. Co-authored-by: simonbronk <275178677+simonbronk@users.noreply.github.com> --- tools/tests/test_unwrap_self.py | 46 +++++++++++++++++++++++++++++++++ tools/unwrap_self.py | 7 ++++- 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/tools/tests/test_unwrap_self.py b/tools/tests/test_unwrap_self.py index 44e1897ccb..192b85a758 100644 --- a/tools/tests/test_unwrap_self.py +++ b/tools/tests/test_unwrap_self.py @@ -13,6 +13,7 @@ ELF_HEADER_OFFSET = 0x1A0 PROGRAM_TABLE_OFFSET = 0x40 PT_LOAD = 1 PT_DYNAMIC = 2 +PT_TLS = 7 FIRST_REGION = 0x400 HEADER = b"\x7fELF\x02\x01\x01\x09" + b"\x00" * 56 @@ -173,6 +174,51 @@ class UnwrapTest(unittest.TestCase): self.assertEqual(result.returncode, 0, result.stderr) self.assertTrue(output.exists()) + def test_file_empty_load_with_stale_offset_preserves_memory(self): + for offset in (0, 0x1900, 0xffffffffffffffff): + with self.subTest(offset=offset): + segments = default_segments() + [(PT_LOAD, 6, offset, 0x4000, 0, PAGE)] + container = bytearray(build_container(segments, default_records(), default_payloads())) + struct.pack_into(" len(data) or header["size"] > len(data) - header["source"]: raise Refused("segment %d does not fit the file" % header["index"]) @@ -219,7 +224,7 @@ def unwrap(path, output, verbose=False): result[region["new"]:region["new"] + region["size"]] = data[region["old"]:region["old"] + region["size"]] for header in headers: - if header["size"] == 0 and header["type"] == PT_LOAD: + if header["size"] == 0 and (header["type"] == PT_LOAD or header["source"] == 0): header["new"] = 0 continue region = region_for(header["source"], header["size"]) From 7777219ee175a6ad60e7cbe945b8be04aa86b6fe Mon Sep 17 00:00:00 2001 From: JJenkx Date: Thu, 15 Oct 2026 15:25:34 -0400 Subject: [PATCH 0400/1000] feat(libkernel): support cpuset affinity by guest thread ID --- .../prx/libkernel/Pthread/Posix/Thread.cpp | 22 +++---- .../prx/libkernel/Pthread/include/Pthread.hpp | 2 + .../libs/prx/libkernel/Pthread/src/Thread.cpp | 22 +++++++ core/libs/tests/GuestAvailableCpumask.cpp | 58 +++++++++++++++---- docs/dev/debt/libkernel.md | 4 +- 5 files changed, 83 insertions(+), 25 deletions(-) diff --git a/core/libs/prx/libkernel/Pthread/Posix/Thread.cpp b/core/libs/prx/libkernel/Pthread/Posix/Thread.cpp index 7660915456..32c3f7e406 100644 --- a/core/libs/prx/libkernel/Pthread/Posix/Thread.cpp +++ b/core/libs/prx/libkernel/Pthread/Posix/Thread.cpp @@ -61,11 +61,10 @@ int APS5_VABI cpuset_getaffinity_nid_postfix(int level, int which, std::int64_t if (size < sizeof(KernelCpumask) || size > MaximumSize) return fail(GuestErange); if (!mask) return fail(GuestEfault); auto* self = scePthreadSelf(); + KernelCpumask affinity = 0; if (which == WhichThread) { - if (id != -1 && id != static_cast(self->tid)) { - if (LiveGuestThreadExists(id)) NotImplemented_nid_no_patch("cpuset_getaffinity other than the calling thread or process"); - return fail(GuestEsrch); - } + const auto tid = id == -1 ? self->tid : id; + if (!GetLiveGuestThreadAffinity_nid_no_patch(tid, affinity)) return fail(GuestEsrch); } else if (which == WhichProcess) { if (id != -1 && id != getpid_nid_postfix()) return fail(GuestEsrch); } else if (which >= 3 && which <= 6) { @@ -75,9 +74,7 @@ int APS5_VABI cpuset_getaffinity_nid_postfix(int level, int which, std::int64_t } if (level < 1 || level > 3) return fail(GuestEinval); if (level != LevelWhich) NotImplemented_nid_no_patch("cpuset_getaffinity other than the calling thread or process"); - KernelCpumask affinity = 0; - if (which == WhichThread) scePthreadGetaffinity(self, &affinity); - else affinity = LiveGuestThreadsAffinity(); + if (which == WhichProcess) affinity = LiveGuestThreadsAffinity(); std::memset(mask, 0, size); std::memcpy(mask, &affinity, sizeof(affinity)); return 0; @@ -99,10 +96,7 @@ int APS5_VABI cpuset_setaffinity_nid_postfix(int level, int which, std::int64_t } auto* self = scePthreadSelf(); if (which == WhichThread) { - if (id != -1 && id != static_cast(self->tid)) { - if (LiveGuestThreadExists(id)) NotImplemented_nid_no_patch("cpuset_setaffinity other than the calling thread or process"); - return fail(GuestEsrch); - } + if (id != -1 && id != self->tid && !LiveGuestThreadExists(id)) return fail(GuestEsrch); } else if (which == WhichProcess) { if (id != -1 && id != getpid_nid_postfix()) return fail(GuestEsrch); } else if (which >= 3 && which <= 6) { @@ -116,8 +110,10 @@ int APS5_VABI cpuset_setaffinity_nid_postfix(int level, int which, std::int64_t std::memcpy(&requested, bytes, sizeof(requested)); const KernelCpumask affinity = requested & sceKernelGetAvailableCpumask(); if (affinity == 0) return fail(GuestEdeadlk); - if (which == WhichThread) scePthreadSetaffinity(self, affinity); - else SetLiveGuestThreadsAffinity(affinity); + if (which == WhichThread) { + const auto tid = id == -1 ? self->tid : id; + if (!SetLiveGuestThreadAffinity_nid_no_patch(tid, affinity)) return fail(GuestEsrch); + } else SetLiveGuestThreadsAffinity(affinity); return 0; } diff --git a/core/libs/prx/libkernel/Pthread/include/Pthread.hpp b/core/libs/prx/libkernel/Pthread/include/Pthread.hpp index 1252760c57..f398ca7c2e 100644 --- a/core/libs/prx/libkernel/Pthread/include/Pthread.hpp +++ b/core/libs/prx/libkernel/Pthread/include/Pthread.hpp @@ -161,5 +161,7 @@ void SetLiveGuestThreadsAffinity(KernelCpumask mask); KernelCpumask LiveGuestThreadsAffinity(); bool GuestThreadIsLive(const PthreadPrivate* thread); bool LiveGuestThreadExists(std::int64_t tid); +bool GetLiveGuestThreadAffinity_nid_no_patch(std::int64_t tid, KernelCpumask& mask); +bool SetLiveGuestThreadAffinity_nid_no_patch(std::int64_t tid, KernelCpumask mask); #endif diff --git a/core/libs/prx/libkernel/Pthread/src/Thread.cpp b/core/libs/prx/libkernel/Pthread/src/Thread.cpp index b7e180b1cf..deff80344a 100644 --- a/core/libs/prx/libkernel/Pthread/src/Thread.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Thread.cpp @@ -375,6 +375,28 @@ bool GuestThreadIsLive(const PthreadPrivate* thread) { return false; } +bool GetLiveGuestThreadAffinity_nid_no_patch(std::int64_t tid, KernelCpumask& mask) { + auto& registry = CpuClocks(); + std::lock_guard lock(registry.lock); + for (const auto& [id, thread] : registry.threads) { + if (thread->tid != tid || thread->_finished.load(std::memory_order_acquire)) continue; + mask = thread->affinity.load(std::memory_order_relaxed); + return true; + } + return false; +} + +bool SetLiveGuestThreadAffinity_nid_no_patch(std::int64_t tid, KernelCpumask mask) { + auto& registry = CpuClocks(); + std::lock_guard lock(registry.lock); + for (const auto& [id, thread] : registry.threads) { + if (thread->tid != tid || thread->_finished.load(std::memory_order_acquire)) continue; + thread->affinity.store(mask, std::memory_order_relaxed); + return true; + } + return false; +} + bool LiveGuestThreadExists(std::int64_t tid) { auto& registry = CpuClocks(); std::lock_guard lock(registry.lock); diff --git a/core/libs/tests/GuestAvailableCpumask.cpp b/core/libs/tests/GuestAvailableCpumask.cpp index 76b09054a6..31775180e5 100644 --- a/core/libs/tests/GuestAvailableCpumask.cpp +++ b/core/libs/tests/GuestAvailableCpumask.cpp @@ -28,6 +28,7 @@ Pthread APS5_VABI pthread_self_nid_postfix(void); int APS5_VABI pthread_getaffinity_np_nid_postfix(Pthread thread, std::size_t cpusetSize, void* cpuset); int APS5_VABI pthread_setaffinity_np_nid_postfix(Pthread thread, std::size_t cpusetSize, const void* cpuset); int APS5_VABI scePthreadGetthreadid(void); +int APS5_VABI sceKernelGetCurrentCpu(void); } static constexpr int SCE_OK = 0; @@ -68,10 +69,28 @@ static void* APS5_VABI Worker(void* arg) { return nullptr; } +struct RemoteThread { + std::atomic tid{0}; + std::future* release = nullptr; + KernelCpumask observed = 0; + KernelCpumask inherited = 0; + int cpu = -1; +}; + +static void* APS5_VABI RecordOwnAffinity(void* arg) { + Require(scePthreadGetaffinity(scePthreadSelf(), static_cast(arg)) == SCE_OK); + return nullptr; +} + static void* APS5_VABI RecordThreadId(void* arg) { - auto* data = static_cast*, std::future*>*>(arg); - data->first->store(scePthreadGetthreadid(), std::memory_order_release); - data->second->get(); + auto& data = *static_cast(arg); + data.tid.store(scePthreadGetthreadid(), std::memory_order_release); + data.release->get(); + Require(scePthreadGetaffinity(scePthreadSelf(), &data.observed) == SCE_OK); + data.cpu = sceKernelGetCurrentCpu(); + Pthread child = nullptr; + Require(scePthreadCreate(&child, nullptr, RecordOwnAffinity, &data.inherited, nullptr) == SCE_OK); + Require(scePthreadJoin(child, nullptr) == SCE_OK); return nullptr; } @@ -312,15 +331,34 @@ int main() { Require(throws([&] { cpuset_setaffinity_nid_postfix(3, 4, -1, 8, processSet); })); std::promise holdOther; auto heldOther = holdOther.get_future(); - std::atomic otherTid{0}; + RemoteThread otherArg; + otherArg.release = &heldOther; Pthread otherThread = nullptr; - std::pair otherArg{&otherTid, &heldOther}; Require(scePthreadCreate(&otherThread, nullptr, RecordThreadId, &otherArg, nullptr) == SCE_OK); - while (otherTid.load(std::memory_order_acquire) == 0) std::this_thread::yield(); - Require(throws([&] { cpuset_getaffinity_nid_postfix(3, 1, otherTid.load(), 8, processSet); })); - Require(throws([&] { cpuset_setaffinity_nid_postfix(3, 1, otherTid.load(), 8, processSet); })); + while (otherArg.tid.load(std::memory_order_acquire) == 0) std::this_thread::yield(); + const int otherTid = otherArg.tid.load(std::memory_order_acquire); + std::uint64_t remoteSet[5] = {~0ull, ~0ull, ~0ull, ~0ull, ~0ull}; + Require(cpuset_getaffinity_nid_postfix(3, 1, otherTid, GUEST_CPUSET_BYTES, remoteSet) == 0); + Require(remoteSet[0] == available && remoteSet[1] == 0 && remoteSet[2] == 0 && remoteSet[3] == 0 && remoteSet[4] == ~0ull); + const std::uint64_t remoteMask[4] = {last | (1ull << 40), 0, 0, 0}; + Require(cpuset_setaffinity_nid_postfix(3, 1, otherTid, sizeof(remoteMask), remoteMask) == 0); + Require(ReadCpuset(otherThread) == last); + Require(ReadCpuset(pthread_self_nid_postfix()) == available); + Require(cpuset_getaffinity_nid_postfix(3, 1, otherTid, 8, remoteSet) == 0 && remoteSet[0] == last); + const std::uint64_t invalidRemote[4] = {first, 0, 0, 1}; + Require(cpuset_setaffinity_nid_postfix(3, 1, otherTid, sizeof(invalidRemote), invalidRemote) == -1 && *__error_nid_postfix() == GUEST_EINVAL); + const std::uint64_t unavailableRemote[1] = {1ull << 40}; + Require(cpuset_setaffinity_nid_postfix(3, 1, otherTid, sizeof(unavailableRemote), unavailableRemote) == -1 && *__error_nid_postfix() == GUEST_EDEADLK); + Require(cpuset_setaffinity_nid_postfix(3, 1, otherTid, 8, nullptr) == -1 && *__error_nid_postfix() == GUEST_EFAULT); + Require(cpuset_setaffinity_nid_postfix(3, 1, otherTid, 4, remoteMask) == -1 && *__error_nid_postfix() == GUEST_ERANGE); + Require(ReadCpuset(otherThread) == last); + Require(cpuset_getaffinity_nid_postfix(99, 1, otherTid, 8, remoteSet) == -1 && *__error_nid_postfix() == GUEST_EINVAL); + Require(cpuset_setaffinity_nid_postfix(99, 1, otherTid, 8, remoteMask) == -1 && *__error_nid_postfix() == GUEST_EINVAL); holdOther.set_value(); Require(scePthreadJoin(otherThread, nullptr) == SCE_OK); - Require(cpuset_getaffinity_nid_postfix(3, 1, otherTid.load(), 8, processSet) == -1 && *__error_nid_postfix() == GUEST_ESRCH); - Require(cpuset_setaffinity_nid_postfix(3, 1, otherTid.load(), 8, processSet) == -1 && *__error_nid_postfix() == GUEST_ESRCH); + Require(otherArg.observed == last && otherArg.inherited == last); + Require(otherArg.cpu >= 0 && otherArg.cpu < PS5_LOGICAL_CPUS && (last & (KernelCpumask{1} << otherArg.cpu)) != 0); + remoteSet[0] = ~0ull; + Require(cpuset_getaffinity_nid_postfix(3, 1, otherTid, 8, remoteSet) == -1 && *__error_nid_postfix() == GUEST_ESRCH && remoteSet[0] == ~0ull); + Require(cpuset_setaffinity_nid_postfix(3, 1, otherTid, 8, remoteMask) == -1 && *__error_nid_postfix() == GUEST_ESRCH); } diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 924649e640..71a6ae7326 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -145,8 +145,8 @@ - [sigpending](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) reports the intersection of the process-wide pending set of `sigtimedwait` and the blocked mask of `_sigprocmask`, as FreeBSD's `sys_sigpending` intersects the pending list with `td_sigmask`, and returns -1 with `EFAULT` for a null set. Because `_sigprocmask` keeps only the first 32 signals, signals 33 to 128 are never blocked and never appear in the set, and setting `SIG_IGN` (or `SIG_DFL` on a signal whose default action is to ignore) does not clear a signal that was already marked pending. PPSA99420 imports it - [scePthreadJoin, scePthreadCreate](../../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) - on a PS5 (firmware 13.60) joining a thread a second time returns `ESRCH` (`0x80020003`); here a joined thread is released, so a second join reads freed memory. A null entry crashed the payload and still throws. Joining the calling thread returns `EDEADLK` and a zeroed attribute creates a default thread, as on the console - [Thread cancellation](../../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) follows FreeBSD libthr (states and types 0/1 and 0/2, `PTHREAD_CANCELED` 1, the `_pthread_cleanup_info` node layout), not measured on the console. A request is acted on only in `scePthreadTestcancel`, the condition variable, `sem` and `scePthreadSem` waits, `scePthreadJoin` and the sleeps, and their POSIX names; file and socket calls are not cancellation points. A sleep is not interrupted: the thread exits when it ends. The asynchronous type acts like the deferred one, except at the two set calls. -- [cpuset_setaffinity](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - only the calling thread or the calling process is handled; other levels (ROOT/CPUSET), other scopes (CPUSET/JAIL) and other threads throw as unmodelled, invalid levels or scopes return `EINVAL` and unknown thread or process IDs return `ESRCH`, following FreeBSD's `cpuset_setaffinity`. Bits outside `sceKernelGetAvailableCpumask` are dropped, and a mask with none of those bits fails with `EDEADLK`, as FreeBSD does when the new set has no CPU in common with the parent set -- [cpuset_getaffinity](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - only the calling thread or the calling process is handled; other levels (ROOT/CPUSET), other scopes (CPUSET/JAIL) and other threads throw as unmodelled, invalid levels or scopes return `EINVAL` and unknown thread or process IDs return `ESRCH`, following FreeBSD's `cpuset_getaffinity`. For the process the mask is the union of the affinities of the live guest threads, as [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/sys/kern/kern_cpuset.c#L1150-L1156) ORs the mask of every thread in the process; threads the guest did not create through libkernel are not included, and the console result is unverified +- [cpuset_setaffinity](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - the calling thread, another live guest thread selected by its guest TID, or the calling process is handled; other levels (ROOT/CPUSET) and other scopes (CPUSET/JAIL) throw as unmodelled, invalid levels or scopes return `EINVAL` and unknown thread or process IDs return `ESRCH`, following [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/sys/kern/kern_cpuset.c#L1281-L1286) thread selection. Bits outside `sceKernelGetAvailableCpumask` are dropped, and a mask with none of those bits fails with `EDEADLK`, as FreeBSD does when the new set has no CPU in common with the parent set. Guest-TID lookup and the update hold the thread registry lock so a join cannot release the target during access. Affinity is recorded for guest CPU reporting and child inheritance, not applied to host scheduling; PS5 behaviour has not been measured +- [cpuset_getaffinity](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - the calling thread, another live guest thread selected by its guest TID, or the calling process is handled; other levels (ROOT/CPUSET) and other scopes (CPUSET/JAIL) throw as unmodelled, invalid levels or scopes return `EINVAL` and unknown thread or process IDs return `ESRCH`, following [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/sys/kern/kern_cpuset.c#L426-L437) guest-TID selection, not measured on PS5. For the process the mask is the union of the affinities of the live guest threads, as [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/sys/kern/kern_cpuset.c#L1150-L1156) ORs the mask of every thread in the process; threads the guest did not create through libkernel are not included, and the console result is unverified - [pthread_setaffinity_np](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - follows [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_affinity.c#L41-L62) libthr over `kern_cpuset_setaffinity` for the calling thread and any live guest thread: a size other than the 32-byte `cpuset_t` returns `ERANGE`, an unknown or finished thread `ESRCH`. Bits outside `sceKernelGetAvailableCpumask` are dropped and a mask with none of those bits returns `EDEADLK`, as `cpuset_setaffinity` does; the affinity is recorded on the guest thread and not applied to the host thread, and the console result is unverified - [dup and dup2](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) throw for the combinations they do not model: `dup2` of a guest socket onto descriptors 0 to 2, whose host streams libc writes without the socket table, and on Windows duplicating a directory descriptor. Guest sockets share one socket object between the duplicates; the numbers come from the host descriptor table like those of files. The errno values for descriptor exhaustion are not verified against the console. - [waitpid](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) returns -1 with `ECHILD` (`EINVAL` for unknown options): nothing here creates a child process; child processes would need a real `waitpid`. From a0347cfed51bc2807d6e771072e7013f1019340a Mon Sep 17 00:00:00 2001 From: billybobpicklio Date: Wed, 14 Oct 2026 09:22:03 -0400 Subject: [PATCH 0401/1000] feat(agc): draw line-strip output of NGG geometry programs as mesh lines A geometry program with VGT_GS_OUT_PRIM_TYPE 1 was refused as an unsupported output assembly. Each primitive export of an NGG line-strip program is one line whose indices are packed like a triangle's, so the mesh module now declares OutputLinesEXT with PrimitiveLineIndicesEXT for it. A subgroup gets VGT_GS_MAX_VERT_OUT - 1 lines per input primitive and at least GE_MAX_OUTPUT_PER_SUBGROUP line slots; fast launch keeps using PRIM_AMP_FACTOR. The output size is part of the cache, draw, validation and pipeline keys, and the request serialization version becomes 31. Triangle-strip geometry is unchanged. Co-Authored-By: Claude Opus 5.5 --- .../Execution/src/Driver/Draw/DrawLookup.cpp | 2 +- .../src/Driver/Draw/DrawRegisterKey.cpp | 2 +- .../prx/libSceAgcDriver/Graphics/src/Draw.cpp | 1 + .../libSceAgcDriver/Graphics/src/Pipeline.cpp | 1 + .../Graphics/src/ShaderValidation.cpp | 13 +++--- .../libSceAgcDriver/Graphics/src/State.cpp | 14 +++--- .../prx/libSceAgcDriver/tests/Graphics.cpp | 41 +++++++++++++++++ core/libs/prx/libSceAgcDriver/tests/Mesh.cpp | 44 +++++++++++++++++++ .../libSceAgcDriver/tests/ShaderMemory.cpp | 12 ++++- core/shader/recompiler/CacheKey.hpp | 2 +- .../ControlFlow/src/RequestSerializer.cpp | 6 ++- .../Optimization/ShaderStageInputInfo.hpp | 1 + core/shader/recompiler/Recompiler.hpp | 1 + .../src/SpirvMemory/SpirvModuleSetup.cpp | 2 +- .../SpirvBackend/src/SpirvModuleEmitter.cpp | 14 +++--- .../src/ShaderInputInfoBuilder.cpp | 3 +- .../tests/RequestSerializerTests.cpp | 14 +++--- docs/dev/debt/libSceAgcDriver.md | 1 + 18 files changed, 140 insertions(+), 34 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp index 366793d496..c64a860a7a 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp @@ -22,7 +22,7 @@ void Driver::lookupDraw(const Submission& submission, const std::shared_ptr& x, const std::optional& y) { if (x.has_value() != y.has_value()) return false; if (!x) return true; - return x->inputPrimitive == y->inputPrimitive && x->primitivesPerGroup == y->primitivesPerGroup && x->verticesPerGroup == y->verticesPerGroup && x->maxVertices == y->maxVertices && x->maxPrimitives == y->maxPrimitives && x->threadsPerGroup == y->threadsPerGroup && x->ldsSizeDwords == y->ldsSizeDwords && x->provokingVertex == y->provokingVertex && x->esgsItemSize == y->esgsItemSize; + return x->inputPrimitive == y->inputPrimitive && x->primitivesPerGroup == y->primitivesPerGroup && x->verticesPerGroup == y->verticesPerGroup && x->maxVertices == y->maxVertices && x->maxPrimitives == y->maxPrimitives && x->threadsPerGroup == y->threadsPerGroup && x->ldsSizeDwords == y->ldsSizeDwords && x->provokingVertex == y->provokingVertex && x->esgsItemSize == y->esgsItemSize && x->outputPrimitiveSize == y->outputPrimitiveSize; }; const auto sameTess = [](const std::optional& x, const std::optional& y) { if (x.has_value() != y.has_value()) return false; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index 7fb4593c6e..6e567b224f 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -658,6 +658,7 @@ bool ValidationKey(const Context& context, std::span shade add(mesh.threadsPerGroup); add(mesh.ldsSizeDwords); add(mesh.provokingVertex); + add(mesh.outputPrimitiveSize); } add(state.stages.tessellation.has_value()); if (state.stages.tessellation) { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp index 9fd5790a24..0a10e1c8d6 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp @@ -872,6 +872,7 @@ void pipelineKey(std::vector& key, const Context& context, const Stat append(key, mesh.threadsPerGroup); append(key, mesh.ldsSizeDwords); append(key, mesh.provokingVertex); + append(key, mesh.outputPrimitiveSize); } append(key, state.stages.tessellation.has_value()); if (state.stages.tessellation) { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp index 9c7dcb033e..a0c0001a35 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp @@ -47,7 +47,7 @@ struct Module { bool position = false; std::uint32_t clipDistances = 0; std::uint32_t cullDistances = 0; - bool primitiveIndices = false; + std::uint32_t primitiveIndices = 0; bool fragmentBarycentric = false; bool layerOutput = false; std::map> modes; @@ -134,8 +134,9 @@ struct Module { Require(input && ((value == spv::BuiltInPosition && signature == "f32x4") || (value == spv::BuiltInTessCoord && stage == Stage::TessellationEvaluation && signature == "f32x3") || ((value == spv::BuiltInInvocationId || value == spv::BuiltInPrimitiveId || value == spv::BuiltInPatchVertices) && (signature == "i32" || signature == "u32"))), "unsupported tessellation built-in"); return; } - if (!input && value == spv::BuiltInPrimitiveTriangleIndicesEXT) primitiveIndices = true; - Require((input && ((value == spv::BuiltInWorkgroupId || value == spv::BuiltInLocalInvocationId || value == spv::BuiltInGlobalInvocationId || value == spv::BuiltInNumWorkgroups) && signature == "u32x3")) || (input && value == spv::BuiltInLocalInvocationIndex && signature == "u32") || (!input && value == spv::BuiltInPrimitiveTriangleIndicesEXT && signature == "u32x3") || (!input && value == spv::BuiltInCullPrimitiveEXT && signature == "bool") || (!input && value == spv::BuiltInLayer && signature == "u32"), "unsupported mesh built-in"); + if (!input && value == spv::BuiltInPrimitiveLineIndicesEXT) primitiveIndices = 2u; + if (!input && value == spv::BuiltInPrimitiveTriangleIndicesEXT) primitiveIndices = 3u; + Require((input && ((value == spv::BuiltInWorkgroupId || value == spv::BuiltInLocalInvocationId || value == spv::BuiltInGlobalInvocationId || value == spv::BuiltInNumWorkgroups) && signature == "u32x3")) || (input && value == spv::BuiltInLocalInvocationIndex && signature == "u32") || (!input && value == spv::BuiltInPrimitiveLineIndicesEXT && signature == "u32x2") || (!input && value == spv::BuiltInPrimitiveTriangleIndicesEXT && signature == "u32x3") || (!input && value == spv::BuiltInCullPrimitiveEXT && signature == "bool") || (!input && value == spv::BuiltInLayer && signature == "u32"), "unsupported mesh built-in"); return; } const auto signature = Signature(type); @@ -437,7 +438,7 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi mode(spv::ExecutionModeLocalSize, {paired ? config.threadsPerGroup / 2u : config.threadsPerGroup, 1, 1}); mode(spv::ExecutionModeOutputVertices, {config.maxVertices}); mode(spv::ExecutionModeOutputPrimitivesEXT, {config.maxPrimitives}); - mode(spv::ExecutionModeOutputTrianglesEXT, {}); + mode(config.outputPrimitiveSize == 2u ? spv::ExecutionModeOutputLinesEXT : spv::ExecutionModeOutputTrianglesEXT, {}); Require(module.modes.size() == 4, "unsupported mesh execution mode"); } else if (control) { Require(state.rectList || state.stages.tessellation.has_value(), "tessellation configuration is missing"); @@ -486,7 +487,7 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi const auto length = module.constants.find(outer[3]); Require(length != module.constants.end() && length->second != 0, "invalid per-vertex interface array length"); if (mesh) { - const bool primitive = decoration.perPrimitive || (decoration.builtin && (*decoration.builtin == spv::BuiltInPrimitiveTriangleIndicesEXT || *decoration.builtin == spv::BuiltInCullPrimitiveEXT)); + const bool primitive = decoration.perPrimitive || (decoration.builtin && (*decoration.builtin == spv::BuiltInPrimitiveLineIndicesEXT || *decoration.builtin == spv::BuiltInPrimitiveTriangleIndicesEXT || *decoration.builtin == spv::BuiltInCullPrimitiveEXT)); Require(length->second == (primitive ? state.stages.mesh->maxPrimitives : state.stages.mesh->maxVertices), "mesh interface array disagrees with output limits"); } else if (geometry) { Require(length->second == 3u, "geometry interface array must hold the three vertices of a triangle"); @@ -594,7 +595,7 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi Require(descriptors.size() == shader.bindings.size(), "recompiler binding metadata contains undeclared resources"); Require(push == (!shader.pushConstants.empty() || mesh || (control && !rectListControl)), "recompiler push constant metadata disagrees with SPIR-V"); for (const auto id : module.interface) Require(module.variables.contains(id), "entry point interface contains an unknown variable"); - if (mesh) Require(module.primitiveIndices, "mesh shader does not export primitive indices"); + if (mesh) Require(module.primitiveIndices == state.stages.mesh->outputPrimitiveSize, "mesh shader does not export primitive indices"); if (stage == Stage::Vertex || mesh || evaluation || geometry) Require(module.position, "vertex shader does not export position"); return module; } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index c16eb7847a..78fb6c50ea 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -876,7 +876,10 @@ ShaderStages DecodeShaderStages(const QueueState& queue, bool vertexWorkgroup) { validate(!fastLaunch || (!vertexWorkgroup && primitive == 1 && vertices == 1 && primitives == 1), "fast launch other than one point per subgroup is unsupported"); const auto maxVertices = read(queue.context, 0x1ff); const auto verticesPerPrimitive = vertexWorkgroup ? 3u : read(queue.context, 0x2ce); - validate((primitive == 1 || primitive == 2 || primitive == 4 || primitive == 5 || primitive == 6) && read(queue.context, 0x29b) == 2 && verticesPerPrimitive >= 3, "unsupported geometry input or output assembly"); + const auto outputType = read(queue.context, 0x29b); + validate(outputType == 2 || (outputType == 1 && !vertexWorkgroup), "VGT_GS_OUT_PRIM_TYPE other than a triangle strip, or a line strip from a geometry program, is unsupported"); + const auto outputSize = outputType == 1 ? 2u : 3u; + validate((primitive == 1 || primitive == 2 || primitive == 4 || primitive == 5 || primitive == 6) && verticesPerPrimitive >= outputSize, "unsupported geometry input or output assembly"); const auto inputSize = primitive == 1 ? 1u : primitive == 2 ? 2u : 3u; validate(vertices >= inputSize && maxVertices != 0 && (!vertexWorkgroup || maxVertices >= inputSize) && maxVertices <= 256 && verticesPerPrimitive <= 256, "invalid geometry subgroup output"); const auto inputStep = primitive == 5 || primitive == 6 ? 1u : inputSize; @@ -886,11 +889,11 @@ ShaderStages DecodeShaderStages(const QueueState& queue, bool vertexWorkgroup) { const auto resources = read(queue.shader, 0x8b, RegisterBank::Shader); const auto esgsItemSize = read(queue.context, 0x2ab); validate(esgsItemSize != 0 && esgsItemSize * vertices <= 0xffffu, "invalid VGT_ESGS_RING_ITEMSIZE"); - const auto amplification = fastLaunch ? read(queue.context, 0x2d3) & 0x1ffu : verticesPerPrimitive - 2u; + const auto amplification = fastLaunch ? read(queue.context, 0x2d3) & 0x1ffu : verticesPerPrimitive - outputSize + 1u; validate(amplification != 0, "fast launch subgroup amplifies to no primitives"); - const auto maxPrimitives = primitives * amplification; + const auto maxPrimitives = outputSize == 2u && !fastLaunch ? std::max(primitives * amplification, maxVertices) : primitives * amplification; const auto threads = std::max({(groupPrimitives - 1u) * inputStep + inputSize, primitives, maxVertices, maxPrimitives}); - result.mesh = ShaderRecompiler::MeshConfiguration{primitive, groupPrimitives, (groupPrimitives - 1u) * inputStep + inputSize, maxVertices, maxPrimitives, ((threads + result.vertexWaveSize - 1u) / result.vertexWaveSize) * result.vertexWaveSize, ((resources >> 19u) & 0xffu) * 128u, 0, esgsItemSize}; + result.mesh = ShaderRecompiler::MeshConfiguration{primitive, groupPrimitives, (groupPrimitives - 1u) * inputStep + inputSize, maxVertices, maxPrimitives, ((threads + result.vertexWaveSize - 1u) / result.vertexWaveSize) * result.vertexWaveSize, ((resources >> 19u) & 0xffu) * 128u, 0, esgsItemSize, outputSize}; } return result; } @@ -1054,7 +1057,7 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) { const auto raster = read(cx, 0x205); // Bits 5-10 give the front/back polygon type (2 = filled triangles), which POLY_MODE (bit 3) turns on // explicitly; KEEP_TOGETHER_ENABLE (bit 24) only affects primitive distribution across the chip. - const bool pointsOrLines = result.topology == VK_PRIMITIVE_TOPOLOGY_POINT_LIST || result.topology == VK_PRIMITIVE_TOPOLOGY_LINE_LIST || result.topology == VK_PRIMITIVE_TOPOLOGY_LINE_STRIP; + const bool pointsOrLines = result.topology == VK_PRIMITIVE_TOPOLOGY_POINT_LIST || result.topology == VK_PRIMITIVE_TOPOLOGY_LINE_LIST || result.topology == VK_PRIMITIVE_TOPOLOGY_LINE_STRIP || (result.stages.mesh.has_value() && result.stages.mesh->outputPrimitiveSize == 2u); const auto lineOffset = (raster & 0x1800u) != 0 && !pointsOrLines ? 0x2000u : 0u; const auto rasterMode = raster & ~0x7u & ~(1u << 24u) & ~0x1800u & ~lineOffset & ~(1u << 19u) & ~(1u << 16u); if (rasterMode != 0 && rasterMode != 0x240u && rasterMode != 0x248u) throw std::runtime_error("AGC graphics: " + zeroMessage(0x205, raster, "polygon mode, depth bias or nonstandard rasterization")); @@ -1320,6 +1323,7 @@ std::array QuadSampleMasks(const State& state) { ShaderRecompiler::RasterPrimitive RasterPrimitiveOf(const State& state) { if (state.rectList) return ShaderRecompiler::RasterPrimitive::Rectangle; + if (state.stages.mesh.has_value() && state.stages.mesh->outputPrimitiveSize == 2u) return ShaderRecompiler::RasterPrimitive::Line; if (state.stages.mesh.has_value() || state.stages.tessellation.has_value()) return ShaderRecompiler::RasterPrimitive::Triangle; switch (state.topology) { case VK_PRIMITIVE_TOPOLOGY_POINT_LIST: return ShaderRecompiler::RasterPrimitive::Point; diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index c7d2efd679..bf2df04ece 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -816,6 +816,10 @@ void ShaderStageTests() { Require(state.stages.path == AgcDriver::Graphics::ShaderPath::Geometry && state.stages.vertexWaveSize == 32 && state.stages.mesh, "a fast-launch draw did not decode as wave32 geometry"); const auto& mesh = *state.stages.mesh; Require(mesh.inputPrimitive == 1 && mesh.primitivesPerGroup == 1 && mesh.verticesPerGroup == 1 && mesh.maxVertices == 32 && mesh.maxPrimitives == 64 && mesh.threadsPerGroup == 64 && mesh.ldsSizeDwords == 128, "a fast-launch subgroup did not decode as one point amplified by PRIM_AMP_FACTOR"); + fast.context[0x29b] = 1; + const auto fastLines = AgcDriver::Graphics::DecodeState(fast); + Require(fastLines.stages.mesh->outputPrimitiveSize == 2 && fastLines.stages.mesh->maxPrimitives == 64 && AgcDriver::Graphics::RasterPrimitiveOf(fastLines) == ShaderRecompiler::RasterPrimitive::Line, "a fast-launch line strip did not take its line count from PRIM_AMP_FACTOR"); + fast.context[0x29b] = 2; fast.context[0x2d5] = 0x502030; expectFailure([&] { AgcDriver::Graphics::DecodeState(fast); }, "unsupported geometry routing, fast launch or wave-ID state"); fast.context[0x2d5] = 0x482030; @@ -828,6 +832,40 @@ void ShaderStageTests() { fast.context[0x2d3] = 0; expectFailure([&] { AgcDriver::Graphics::DecodeState(fast); }, "amplifies to no primitives"); } + { + auto lines = makeState(); + lines.userConfig[0x242] = 1; + lines.context[0x2d5] = 0x2030; + lines.userConfig[0x25b] = 0x6633; + lines.context[0x1ff] = 255; + lines.context[0x2ce] = 5; + lines.context[0x29b] = 1; + lines.context[0x2ab] = 4; + lines.shader[0x8a] = 3u << 29u; + lines.shader[0x8b] = 3u << 16u; + auto state = AgcDriver::Graphics::DecodeState(lines); + Require(state.stages.path == AgcDriver::Graphics::ShaderPath::Geometry && state.stages.mesh && AgcDriver::Graphics::RasterPrimitiveOf(state) == ShaderRecompiler::RasterPrimitive::Line, "a geometry program that outputs line strips did not decode as geometry rasterized as lines"); + auto mesh = *state.stages.mesh; + Require(mesh.inputPrimitive == 1 && mesh.primitivesPerGroup == 51 && mesh.verticesPerGroup == 51 && mesh.maxVertices == 255 && mesh.maxPrimitives == 255 && mesh.threadsPerGroup == 256 && mesh.outputPrimitiveSize == 2, "a line-strip subgroup did not decode as 51 points with a line slot per output vertex"); + lines.context[0x29b] = 2; + state = AgcDriver::Graphics::DecodeState(lines); + mesh = *state.stages.mesh; + Require(mesh.maxPrimitives == 153 && mesh.threadsPerGroup == 256 && mesh.outputPrimitiveSize == 3 && AgcDriver::Graphics::RasterPrimitiveOf(state) == ShaderRecompiler::RasterPrimitive::Triangle, "the triangle-strip subgroup of the same draw changed"); + lines.context[0x29b] = 1; + lines.context[0x2ce] = 1; + expectFailure([&] { AgcDriver::Graphics::DecodeState(lines); }, "unsupported geometry input or output assembly"); + lines.context[0x2ce] = 5; + for (const auto output : {0u, 3u, 0x80000001u, 0x101u}) { + lines.context[0x29b] = output; + expectFailure([&] { AgcDriver::Graphics::DecodeState(lines); }, "VGT_GS_OUT_PRIM_TYPE other than a triangle strip, or a line strip from a geometry program, is unsupported"); + } + lines.userConfig[0x242] = 4; + lines.context[0x205] = 0x2a40; + lines.context[0x29b] = 2; + Require(AgcDriver::Graphics::DecodeState(lines).stages.mesh->primitivesPerGroup == 17, "triangle input with a triangle-strip output and a triangle polygon offset did not decode"); + lines.context[0x29b] = 1; + expectFailure([&] { AgcDriver::Graphics::DecodeState(lines); }, "polygon mode, depth bias or nonstandard rasterization"); + } queue.context[0x2ab] = 0; expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "invalid VGT_ESGS_RING_ITEMSIZE"); queue.context[0x2ab] = 4; @@ -947,6 +985,9 @@ void NggVertexWorkgroupTests() { queue.context[0x2d5] = stages; expectFailure([&] { AgcDriver::Graphics::DecodeState(queue, true); }, "requires PRIMGEN_EN without GS_EN, PRIMGEN_PASSTHRU_EN or tessellation"); } + queue.context[0x2d5] = 0x12010; + queue.context[0x29b] = 1; + expectFailure([&] { AgcDriver::Graphics::DecodeState(queue, true); }, "VGT_GS_OUT_PRIM_TYPE other than a triangle strip, or a line strip from a geometry program, is unsupported"); } constexpr std::array IdentityExports{0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u}; diff --git a/core/libs/prx/libSceAgcDriver/tests/Mesh.cpp b/core/libs/prx/libSceAgcDriver/tests/Mesh.cpp index 9a9b548f49..1e59006cd6 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Mesh.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Mesh.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include namespace { @@ -41,6 +42,13 @@ alignas(256) constexpr std::array FastLaunchCode{ 0xbf810000, }; +alignas(256) constexpr std::array LineStripCode{ + 0xd7650000, 0x000100c1, 0x7e000505, 0x93018300, 0x4a020001, 0xe0382000, 0x80020201, 0xe0382010, + 0x80020601, 0xb07c2003, 0xbf900009, 0xbf8c3f70, 0x7d880083, 0xbefe036a, 0xf80008cf, 0x05040302, + 0xf800020f, 0x09080706, 0x7d880082, 0xbefe036a, 0x161400ff, 0x00000401, 0x4a1414ff, 0x00000400, + 0xf8000941, 0x0000000a, 0xbf810000, +}; + alignas(256) constexpr std::array PixelCode{ 0xc8020002, 0xc8060102, 0xc80a0202, 0xc80e0302, 0xf800180f, 0x03020100, 0xbf810000, }; @@ -85,6 +93,13 @@ alignas(256) std::array Indices{}; alignas(256) std::array Ordered{}; alignas(256) std::array Strip{}; +constexpr std::uint32_t LineStrips = 4; +alignas(256) std::array LineStripVertices{}; + +std::uint32_t LineStripRow(std::uint32_t strip) { + return 16u + 28u * strip; +} + constexpr std::uint32_t FanRim = 8; constexpr float FanStep = 6.28318530718f / FanRim; alignas(256) std::array Fan{}; @@ -279,6 +294,17 @@ int main() { const float y = i % 2u == 0u ? -1.0f : 0.0f; Strip[i] = {{x, y, 0.5f, 1.0f}, stripColor}; } + for (std::uint32_t strip = 0; strip < LineStrips; ++strip) { + const auto color = TriangleColor(strip); + const std::array rgba{color[0] / 255.0f, color[1] / 255.0f, color[2] / 255.0f, 1.0f}; + const float top = 1.0f - (static_cast(LineStripRow(strip)) + 0.5f) / (Height / 2.0f); + const float bottom = top - 20.0f / (Height / 2.0f); + const float left = 24.5f / (Width / 2.0f) - 1.0f; + const float right = 168.5f / (Width / 2.0f) - 1.0f; + LineStripVertices[3 * strip] = {{left, top, 0.5f, 1.0f}, rgba}; + LineStripVertices[3 * strip + 1] = {{right, top, 0.5f, 1.0f}, rgba}; + LineStripVertices[3 * strip + 2] = {{right, bottom, 0.5f, 1.0f}, rgba}; + } Fan[0] = {{0.0f, 0.0f, 0.5f, 1.0f}, {1.0f, 1.0f, 1.0f, 1.0f}}; for (std::uint32_t k = 1; k <= FanRim; ++k) { const auto color = TriangleColor(k); @@ -360,6 +386,24 @@ int main() { } Require(fastLaunchIndexedRefused, "an indexed fast-launch draw was drawn"); + ClearPixels(); + auto lineSubgroup = FastLaunchSubgroup; + lineSubgroup.maxPrimitives = 2u; + lineSubgroup.outputPrimitiveSize = 2u; + DrawMesh(device, {lineSubgroup, {0, LineStrips, 0, 1, 0, false}, VertexBufferDescriptor(LineStripVertices.data(), static_cast(LineStripVertices.size())), ShaderRecompiler::MeshDrawPushOffsetBytes, false, 32u, VK_CULL_MODE_NONE, LineStripCode, 0x482030u}); + const auto linePixel = [](std::uint32_t x, std::uint32_t y) { return (static_cast(y) * Width + x) * 4u; }; + for (std::uint32_t strip = 0; strip < LineStrips; ++strip) { + const auto row = LineStripRow(strip); + const auto color = TriangleColor(strip); + const auto where = "line strip " + std::to_string(strip) + ": "; + for (std::uint32_t x = 32; x <= 160; x += 8) Require(PixelIs(linePixel(x, row), color), where + "horizontal line pixel (" + std::to_string(x) + ", " + std::to_string(row) + ") is " + PixelText(linePixel(x, row))); + for (std::uint32_t y = row + 4u; y <= row + 16u; y += 4) Require(PixelIs(linePixel(168, y), color), where + "vertical line pixel (168, " + std::to_string(y) + ") is " + PixelText(linePixel(168, y))); + for (const auto& [x, y, what] : {std::tuple{96u, row - 3u, "above the horizontal line"}, std::tuple{96u, row + 3u, "below the horizontal line"}, std::tuple{150u, row + 5u, "inside the triangle of the strip's three vertices"}, std::tuple{96u, row + 10u, "on the edge closing the strip"}, std::tuple{96u, row + 24u, "between this strip and the next"}, std::tuple{8u, row, "left of the strip"}, std::tuple{184u, row + 10u, "right of the strip"}}) { + Require(PixelIs(linePixel(x, y), Background), where + "pixel (" + std::to_string(x) + ", " + std::to_string(y) + ") " + what + " is " + PixelText(linePixel(x, y))); + } + } + Require(PixelIs(0, Background) && PixelIs(Pixels.size() - 4u, Background), "line strips: the corners changed"); + constexpr std::size_t recordBlockBytes = 65536; auto* record = static_cast(::operator new(recordBlockBytes, std::align_val_t{65536})); { diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index a3294a30fe..5b7c940a01 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -2673,6 +2673,14 @@ void verifyMeshConfiguration() { other.graphics = GraphicsCompileContext{0u, {}, otherMesh, std::nullopt, {}}; RecompileCacheKey::Build(other, key); require(key != first && RecompileCacheKey::ContextHash(request) != RecompileCacheKey::ContextHash(other), "the cache keys ignore the mesh configuration"); + require(mesh.outputPrimitiveSize == 3u && replay.request.graphics->mesh->outputPrimitiveSize == 3u, "a mesh configuration does not output triangles by default"); + otherMesh = mesh; + otherMesh.outputPrimitiveSize = 2u; + other.graphics = GraphicsCompileContext{0u, {}, otherMesh, std::nullopt, {}}; + const auto lineReplay = RequestSerializer{}.Deserialize(RequestSerializer{}.Serialize(other)); + require(lineReplay.request.graphics.has_value() && lineReplay.request.graphics->mesh.has_value() && lineReplay.request.graphics->mesh->outputPrimitiveSize == 2u, "the mesh output primitive size was lost in serialization"); + RecompileCacheKey::Build(other, key); + require(key != first && RecompileCacheKey::ContextHash(request) != RecompileCacheKey::ContextHash(other), "the cache keys ignore the mesh output primitive size"); RecompileRequest hull{}; hull.shader = {ShaderStage::TessellationControl, 0x10000u, code, 0, {}}; hull.context.waveSize = 64; @@ -3049,7 +3057,7 @@ void verifyPixelRequestSerialization() { minimal.context.pixel->layerExported = true; minimal.context.pixel->ditheredAlphaToCoverage = true; const auto encoded = serializer.Serialize(minimal); - require(requestPrefix(encoded, 8u) == "NVNQQR4AAAA=", "new requests did not use serialization version 30"); + require(requestPrefix(encoded, 8u) == "NVNQQR8AAAA=", "new requests did not use serialization version 31"); const std::array fallbacks{0x40u, 0x1ac4u}; auto withFallbacks = minimal; withFallbacks.context.linkedCallFallbacks = fallbacks; @@ -3167,7 +3175,7 @@ void verifyPixelRequestSerialization() { for (std::size_t bytes = 0; bytes < 45u; ++bytes) { expectFailure([&] { static_cast(serializer.Deserialize(requestPrefix(encoded, mappingOffset + bytes))); }, "truncated data", "a truncated pixel mapping, packing, dual-source, alpha-to-coverage, stencil reference export flag, primitive class, layer export, dither or front-face flag, quad pixel mask or quad sample mask was accepted"); } - for (const auto unsupported : {"NVNQQQAAAAA=", "NVNQQR8AAAA="}) { + for (const auto unsupported : {"NVNQQQAAAAA=", "NVNQQSAAAAA="}) { expectFailure([&] { static_cast(serializer.Deserialize(unsupported)); }, "serialization version", "an unsupported request version was accepted"); } static constexpr std::string_view version18Request{ diff --git a/core/shader/recompiler/CacheKey.hpp b/core/shader/recompiler/CacheKey.hpp index 9a05433674..033a416692 100644 --- a/core/shader/recompiler/CacheKey.hpp +++ b/core/shader/recompiler/CacheKey.hpp @@ -104,7 +104,7 @@ private: const auto* mesh = request.graphics && request.graphics->mesh ? &*request.graphics->mesh : nullptr; append(key, mesh != nullptr); if (mesh == nullptr) return; - for (const auto value : {mesh->inputPrimitive, mesh->primitivesPerGroup, mesh->verticesPerGroup, mesh->maxVertices, mesh->maxPrimitives, mesh->threadsPerGroup, mesh->ldsSizeDwords, mesh->provokingVertex, mesh->esgsItemSize}) append(key, value); + for (const auto value : {mesh->inputPrimitive, mesh->primitivesPerGroup, mesh->verticesPerGroup, mesh->maxVertices, mesh->maxPrimitives, mesh->threadsPerGroup, mesh->ldsSizeDwords, mesh->provokingVertex, mesh->esgsItemSize, mesh->outputPrimitiveSize}) append(key, value); } static void appendTessellation(std::vector& key, const RecompileRequest& request) { diff --git a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp index 20c4e29f51..3bee798ecb 100644 --- a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp +++ b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp @@ -518,6 +518,7 @@ void writeMeshConfiguration(Writer& writer, const MeshConfiguration& configurati writer.WriteU32(configuration.ldsSizeDwords); writer.WriteU32(configuration.provokingVertex); writer.WriteU32(configuration.esgsItemSize); + writer.WriteU32(configuration.outputPrimitiveSize); } MeshConfiguration readMeshConfiguration(Reader& reader, std::uint32_t version) { @@ -531,6 +532,7 @@ MeshConfiguration readMeshConfiguration(Reader& reader, std::uint32_t version) { configuration.ldsSizeDwords = reader.ReadU32(); configuration.provokingVertex = reader.ReadU32(); configuration.esgsItemSize = version >= 4u ? reader.ReadU32() : 4u; + if (version >= 31u) configuration.outputPrimitiveSize = reader.ReadU32(); return configuration; } @@ -776,7 +778,7 @@ std::string RequestSerializer::Serialize(const RecompileRequest& request) const std::string buffer; Writer writer(buffer); writer.WriteU32(0x41505335u); - writer.WriteU32(30u); + writer.WriteU32(31u); writeShaderBinary(writer, request.shader); writeGuestContext(writer, request.context); writeSpirvTarget(writer, request.target); @@ -809,7 +811,7 @@ DeserializedRequest RequestSerializer::Deserialize(std::string_view text) const Reader reader(decoded); if (reader.ReadU32() != 0x41505335u) throw std::runtime_error("invalid recompile request signature"); const auto version = reader.ReadU32(); - if (version < 1u || version > 30u) throw std::runtime_error("unsupported recompile request serialization version"); + if (version < 1u || version > 31u) throw std::runtime_error("unsupported recompile request serialization version"); DeserializedRequest result{}; result.request.shader = readShaderBinary(reader, result.shaderCode, result.shaderHeader); result.request.context = readGuestContext(reader, result, version); diff --git a/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp b/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp index 72fe2675ee..2ba98418bd 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp @@ -92,6 +92,7 @@ struct ShaderMeshInputInfo: ShaderWorkgroupInputInfo { std::uint32_t maxPrimitives = 0; std::uint32_t provokingVertex = 0; std::uint32_t esgsItemSize = 0; + std::uint32_t outputPrimitiveSize = 3; [[nodiscard]] std::uint32_t InputPrimitiveSize() const { return inputPrimitive == 1u ? 1u : inputPrimitive == 2u ? 2u : 3u; diff --git a/core/shader/recompiler/Recompiler.hpp b/core/shader/recompiler/Recompiler.hpp index 157ab11feb..7be06968e0 100644 --- a/core/shader/recompiler/Recompiler.hpp +++ b/core/shader/recompiler/Recompiler.hpp @@ -302,6 +302,7 @@ struct MeshConfiguration { std::uint32_t ldsSizeDwords; std::uint32_t provokingVertex; std::uint32_t esgsItemSize = 0; + std::uint32_t outputPrimitiveSize = 3; }; struct TessellationConfiguration { diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp index 78de6340b4..e6efb53722 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp @@ -240,7 +240,7 @@ void DefineModule(SpirvEmitterState& state) { state.meshGuestFunc = state.module.AllocateId(); state.module.EmitCapability(spv::CapabilityMeshShadingEXT); state.module.EmitExtension("SPV_EXT_mesh_shader"); - state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeOutputTrianglesEXT); + state.module.AddExecutionMode(state.mainFunc, mesh.outputPrimitiveSize == 2u ? spv::ExecutionModeOutputLinesEXT : spv::ExecutionModeOutputTrianglesEXT); state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeOutputVertices, mesh.maxVertices); state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeOutputPrimitivesEXT, mesh.maxPrimitives); } diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp index 774e16aa09..2c92bf42a0 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp @@ -767,11 +767,11 @@ void DefineMeshOutputs(SpirvEmitterState& state) { } state.meshAllocation = MeshArray(state, spv::StorageClassWorkgroup, TypeU32(state), 2u); state.meshPrimitiveData = MeshArray(state, spv::StorageClassPrivate, TypeU32(state), state.laneCount); - state.meshPrimitives = MeshArray(state, spv::StorageClassOutput, TypeU32Vector(state, 3u), mesh.maxPrimitives); + state.meshPrimitives = MeshArray(state, spv::StorageClassOutput, TypeU32Vector(state, mesh.outputPrimitiveSize), mesh.maxPrimitives); state.meshCull = MeshArray(state, spv::StorageClassOutput, TypeBool(state), mesh.maxPrimitives); state.interfaceVariables.push_back(state.meshPrimitives); state.interfaceVariables.push_back(state.meshCull); - state.module.AddAnnotation(spv::OpDecorate, state.meshPrimitives, spv::DecorationBuiltIn, spv::BuiltInPrimitiveTriangleIndicesEXT); + state.module.AddAnnotation(spv::OpDecorate, state.meshPrimitives, spv::DecorationBuiltIn, mesh.outputPrimitiveSize == 2u ? spv::BuiltInPrimitiveLineIndicesEXT : spv::BuiltInPrimitiveTriangleIndicesEXT); state.module.AddAnnotation(spv::OpDecorate, state.meshCull, spv::DecorationBuiltIn, spv::BuiltInCullPrimitiveEXT); state.module.AddAnnotation(spv::OpDecorate, state.meshCull, spv::DecorationPerPrimitiveEXT); if (state.numWorkgroupsVariable == 0u) { @@ -837,16 +837,16 @@ void EmitMeshEntryPoint(SpirvEmitterState& state) { EmitIfCondition(state, isPrimitive, [&]() { const auto packed = MeshLoad(state, state.meshPrimitiveData, spv::StorageClassPrivate, TypeU32(state), ConstantU32(state, half)); std::array vertex {}; - for (std::uint32_t component = 0; component < 3u; component++) { + for (std::uint32_t component = 0; component < mesh.outputPrimitiveSize; component++) { const auto field = state.module.AllocateId(); state.module.AddFunction(spv::OpBitFieldUExtract, TypeU32(state), field, packed, ConstantU32(state, component * 10u), ConstantU32(state, 9u)); vertex.at(component) = state.module.AllocateId(); state.module.AddFunction(spv::OpExtInst, TypeU32(state), vertex.at(component), GlslStd450(state), GLSLstd450UMin, field, lastVertex); } - const auto triangle = state.module.AllocateId(); - state.module.AddFunction(spv::OpCompositeConstruct, TypeU32Vector(state, 3u), triangle, vertex[0], vertex[1], vertex[2]); - const auto trianglePointer = MeshElement(state, state.meshPrimitives, spv::StorageClassOutput, TypeU32Vector(state, 3u), index); - state.module.AddFunction(spv::OpStore, trianglePointer, triangle); + const auto indices = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeConstruct, TypeU32Vector(state, mesh.outputPrimitiveSize), indices, std::span(vertex.data(), mesh.outputPrimitiveSize)); + const auto indicesPointer = MeshElement(state, state.meshPrimitives, spv::StorageClassOutput, TypeU32Vector(state, mesh.outputPrimitiveSize), index); + state.module.AddFunction(spv::OpStore, indicesPointer, indices); const auto nullBit = EmitBinaryU32(state, spv::OpBitwiseAnd, packed, ConstantU32(state, 0x80000000u)); const auto culled = state.module.AllocateId(); state.module.AddFunction(spv::OpINotEqual, TypeBool(state), culled, nullBit, ConstantU32(state, 0u)); diff --git a/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp b/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp index 5de4543f7b..992dcf3388 100644 --- a/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp +++ b/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp @@ -217,7 +217,8 @@ ShaderStageInputInfo BuildShaderStageInputInfo(ShaderStageKind stage, const Gues target.maxPrimitives = mesh->maxPrimitives; target.provokingVertex = mesh->provokingVertex; target.esgsItemSize = mesh->esgsItemSize; - if (target.threadsNum[0] == 0u || target.maxVertices == 0u || target.maxPrimitives == 0u || target.provokingVertex > 2u) throw std::runtime_error("ShaderInputInfoBuilder: invalid mesh configuration"); + target.outputPrimitiveSize = mesh->outputPrimitiveSize; + if (target.threadsNum[0] == 0u || target.maxVertices == 0u || target.maxPrimitives == 0u || (target.outputPrimitiveSize != 2u && target.outputPrimitiveSize != 3u) || target.provokingVertex >= target.outputPrimitiveSize) throw std::runtime_error("ShaderInputInfoBuilder: invalid mesh configuration"); } ShaderStageInputInfo result; result.vertex = &vertexStorage; diff --git a/core/shader/recompiler/tests/RequestSerializerTests.cpp b/core/shader/recompiler/tests/RequestSerializerTests.cpp index b8d4b0e10e..54b249f255 100644 --- a/core/shader/recompiler/tests/RequestSerializerTests.cpp +++ b/core/shader/recompiler/tests/RequestSerializerTests.cpp @@ -206,7 +206,7 @@ int main() { Require(request.graphics->draw.indexAddress == 0xabcdef3400u && request.graphics->draw.indexCount == 57u && request.graphics->draw.indexElementBytes == 4u && request.graphics->draw.instanceCount == 9u); Require(request.target.float16DenormPreserve == preserve); auto upgraded = bytes; - upgraded[4] = 30u; + upgraded[4] = 31u; if (fixture.pixel) upgraded.insert(upgraded.begin() + 258u, {0u, 0u, 0u}); if (fixture.pixel) InsertPixelDefaultsAfterFrontFace(upgraded); if (fixture.tessellation) upgraded.insert(upgraded.end() - 35u, 5u, 0u); @@ -245,7 +245,7 @@ int main() { layerRejected = std::string_view(error.what()) == "RequestSerializer: truncated data"; } Require(layerRejected); - layered[4] = 30u; + layered[4] = 31u; layered.insert(layered.begin() + 259u, {0u, 0u}); InsertPixelDefaultsAfterFrontFace(layered); if (fixture.tessellation) layered.insert(layered.end() - 35u, 5u, 0u); @@ -270,7 +270,7 @@ int main() { Require(replay.request.context.pixel->rasterPrimitive == ShaderRecompiler::RasterPrimitive::Rectangle); Require(replay.request.context.vertex->paClVsOutCntl == 0x00050a05u); Require(replay.request.target.float16DenormPreserve == preserve); - bytes[4] = 30u; + bytes[4] = 31u; bytes.insert(bytes.begin() + 259u, {0u, 0u}); InsertPixelDefaultsAfterFrontFace(bytes); bytes.insert(bytes.end(), sizeof(std::uint64_t), 0u); @@ -394,7 +394,7 @@ int main() { Require(request.graphics->draw.indexAddress == 0xabcdef3400u && request.graphics->draw.indexCount == 57u && request.graphics->draw.indexElementBytes == 4u && request.graphics->draw.instanceCount == 9u); Require(request.target.float16DenormPreserve == preserve); auto upgraded = bytes; - upgraded[4] = 30u; + upgraded[4] = 31u; if (fixture.pixel) upgraded.insert(upgraded.begin() + 259u, {0u, 0u}); if (fixture.pixel) InsertPixelDefaultsAfterFrontFace(upgraded); upgraded.insert(upgraded.end(), sizeof(std::uint64_t), 0u); @@ -407,7 +407,7 @@ int main() { const auto replay25 = serializer.Deserialize(EncodeBase64(main25)); Require(!fixture.pixel || (replay25.request.context.pixel->layerExported == layer && replay25.request.context.pixel->ditheredAlphaToCoverage == dither && replay25.request.context.pixel->reversedBlend == 0u)); Require(replay25.request.target.float16DenormPreserve == preserve && (!fixture.tessellation || replay25.request.graphics->tessellation->primitiveGroupSize == 37u)); - main25[4] = 30u; + main25[4] = 31u; if (fixture.pixel) { main25.insert(main25.begin() + 260u, 0u); InsertPixelDefaultsAfterFrontFace(main25); @@ -426,9 +426,9 @@ int main() { const auto replay = serializer.Deserialize(encoded); Require(replay.request.target.float16DenormPreserve == preserve); auto bytes = DecodeBase64(encoded); - Require(bytes.size() >= 8u && bytes[4] == 30u && bytes[5] == 0u && bytes[6] == 0u && bytes[7] == 0u); + Require(bytes.size() >= 8u && bytes[4] == 31u && bytes[5] == 0u && bytes[6] == 0u && bytes[7] == 0u); auto future = bytes; - future[4] = 31u; + future[4] = 32u; bool futureRejected = false; try { static_cast(serializer.Deserialize(EncodeBase64(future))); diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 09bd4b9432..b009e17be1 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -17,6 +17,7 @@ - [Indices far past every vertex fetch](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver) - when a draw's highest index is at least 64Ki vertices past the largest per-vertex record count, every index past that count is moved to it in a copy of the indices, so the zero-padded vertex copies are not sized to the original index (PPSA04159 draws a list of `0xffffffff` with primitive restart off, which asked for 160 GiB). Each attribute fetched by vertex index reads zeros there as at the original index (out-of-bounds select 0 or 1); a draw with another out-of-bounds select keeps its indices. The vertex index the shader sees changes for the moved indices, which a shader that reads it directly would notice - [Depth clear draws](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - a draw with `DB_RENDER_CONTROL.DEPTH_CLEAR_ENABLE` is taken to write `DB_DEPTH_CLEAR` to every pixel it covers whatever the depth test, depth bounds and depth bias say: it is drawn with an always-passing depth write and the viewport depth range pinned to the clear value. The draw is rasterized as submitted, so its coverage is the draw's own. Whether the DB still applies the depth test or HiZ to such a draw is not measured - [GS_FAST_LAUNCH geometry](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - `VGT_SHADER_STAGES_EN.GS_FAST_LAUNCH = 1` with a point list and `GE_CNTL` groups of one vertex and one primitive decodes as one point per subgroup, with the primitive limit taken from `GE_NGG_SUBGRP_CNTL.PRIM_AMP_FACTOR` and the GS/ES VGPR counts not required. Follows how RADV drives mesh shaders on gfx10.3 (`ac_nir_lower_ngg.c`, the workgroup index comes from the vertex id of the first thread), not measured on the console. Fast launch 2, other group sizes or input primitives and indexed fast-launch draws still throw. PPSA03671's fast-launch draws decode, but the title stops earlier in a wave64 vertex program, so they have not reached the GPU yet. +- [Line-strip geometry output](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - a geometry program with `VGT_GS_OUT_PRIM_TYPE = 1` (line strip) runs as a mesh shader with `OutputLinesEXT`: each primitive export is one line whose two vertex indices are packed like the three of a triangle, as Mesa's NGG lowering exports line strips (`ac_nir_lower_ngg_gs.c`), and each input primitive gets `VGT_GS_MAX_VERT_OUT - 1` lines where a triangle strip gets `- 2` triangles. A subgroup gets at least `GE_MAX_OUTPUT_PER_SUBGROUP` line slots, because Mesa's lowering exports one primitive, possibly null, per emitted vertex; whether the console's compiler does the same is not known, and triangle strips keep `- 2` per input. A fast-launch subgroup takes its line count from `GE_NGG_SUBGRP_CNTL.PRIM_AMP_FACTOR` as for triangles. Line output counts as lines for the polygon offset decode, as on the vertex path. The lines are drawn 1 pixel wide with Vulkan's default line rasterization, like lines on the vertex path: `PA_SU_LINE_CNTL` is not read, and neither its width nor the console's line rules were measured. Points, rectangle lists, per-stream types and line strips from a vertex program on the mesh path still throw. - [Alpha-to-coverage](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - only `DB_ALPHA_TO_MASK = 0xaa01` is accepted: the enable bit with the four quad offsets at 2 and no `OFFSET_ROUND`, the undithered setting [radeonsi](https://gitlab.freedesktop.org/mesa/mesa/-/blob/bffd91947efc1932da99aca69fc540d6d4201b0c/src/gallium/drivers/radeonsi/si_state.c#L371-L381) programs. Draws are single-sampled, so the pixel shader [writes](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) `SampleMask` 0 when its MRT0 alpha is below one half or NaN and all ones otherwise, and runs with late fragment tests. That rule was measured on a Radeon 660M (gfx1035, RADV with `RADV_DEBUG=noatocdithering`, which programs the same register) for 0, -0, 0.25, the two floats below one half, one half, the two above, 0.75, 1, 2, -1, both infinities and both NaNs, and is pinned in [AlphaToCoverage.cpp](../../../core/libs/prx/libSceAgcDriver/tests/execution/AlphaToCoverage.cpp); it is not measured on gfx1013. Vulkan's `alphaToCoverageEnable` is not used: the NVIDIA driver 615.78.08 dithers single-sample coverage (an alpha of 0.25 writes half the pixels of an RTX 3050 Laptop target). Other offsets, `OFFSET_ROUND`, a sample-mask export, and an MRT0 that is missing, swizzled, packed or exported without a floating-point alpha throw. A draw without a color target on MRT0 keeps alpha-to-coverage from its MRT0 export, as [radeonsi exports MRT0 as `32_AR` for alpha-to-coverage without a color buffer](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-24.0.0/src/gallium/drivers/radeonsi/si_state_shaders.cpp#L2425-L2431). A draw that enables alpha-to-coverage with no MRT0 export still throws: what the console covers then is not measured. Default dithered `DB_ALPHA_TO_MASK = 0x18701` additionally uses a per-quad sample-mask table [measured on gfx1036](https://github.com/boykopovar/AnyPS5/pull/4451#issuecomment-6124848921) for eight standard samples with FP16_ABGR or 32_ABGR exports; other sample counts, custom locations, EQAA combinations and dithered UNORM16/SNORM16 exports throw. The dithered rule is not measured on gfx1013. - [MSAA_ENABLE on a single-sample target](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - `PA_SC_MODE_CNTL_0.MSAA_ENABLE` with `PA_SC_AA_CONFIG.MSAA_NUM_SAMPLES = 0` is drawn like the same state without the bit when sample 0 of the four `PA_SC_AA_SAMPLE_LOCS_PIXEL_*_0` registers is at the pixel center and the draw rasterizes filled triangles with alpha-to-coverage and conservative rasterization off. [RADV](https://gitlab.freedesktop.org/mesa/mesa/-/blob/745709a000482a4a24c91faef5c5a40bd653a863/src/amd/vulkan/radv_cmd_buffer.c#L13606-13609) programs this state for one-sample custom sample locations on GFX10 and later, where the bit makes the rasterizer use the programmed location of sample 0 ([061bc615](https://gitlab.freedesktop.org/mesa/mesa/-/commit/061bc6151a11597f720535f00e79a5752e24aff6)); that the bit changes nothing else for such a draw is inferred from that and is not measured on hardware. Other locations of sample 0, absent location registers, points, lines, rectangles, alpha-to-coverage and conservative rasterization throw - [sceAgcDriverGetEqContextId](../../../core/libs/prx/libSceAgcDriver/Eq/src/Query.cpp) (libSceAgcDriver) - returns `ident`: an end-of-pipe interrupt is delivered only to the registration whose id equals the raising queue, so `ident` is that context's id (titles register 0 or 1 for graphics and `0x20 + 8 * pipe + queue` for compute). The console's value is not known; KytyPS5 returns the event's `data`, which it fills with the interrupt's context id, whereas `data` here is the EV_CLEAR trigger count. Other event filters throw From a63e77e9fc91fefeef1a1dd8429ebe22e7aae101 Mon Sep 17 00:00:00 2001 From: JAB-dev <68183649+JAB-dev@users.noreply.github.com> Date: Thu, 15 Oct 2026 22:08:20 +0200 Subject: [PATCH 0402/1000] fix(libSceSysmodule): report the region of the queried page from a cached unwind region VirtualQuery describes the pages from the queried address to the end of the run of equal pages, so the reported seg0 begins at the queried page. The cached entry returned the first lookup's page for every later address in the run. It now returns the queried page and the bytes up to the end of the cached run, which is what VirtualQuery returns. Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libSceSysmodule/Export.cpp | 4 +++- core/libs/tests/GuestSysmodule.cpp | 9 +++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceSysmodule/Export.cpp b/core/libs/prx/libSceSysmodule/Export.cpp index 889ee2cfad..e9c5f23e1b 100644 --- a/core/libs/prx/libSceSysmodule/Export.cpp +++ b/core/libs/prx/libSceSysmodule/Export.cpp @@ -91,7 +91,9 @@ bool fillCachedImageRegion(std::uint64_t addr, std::uintptr_t image, ModuleInfoF gRegions.erase(found); return false; } - writeUnwindRegion(found->first, region.end - found->first, region.name, info); + constexpr std::uint64_t page = 4096; + const std::uint64_t begin = addr & ~(page - 1); + writeUnwindRegion(begin, region.end - begin, region.name, info); return true; } #endif diff --git a/core/libs/tests/GuestSysmodule.cpp b/core/libs/tests/GuestSysmodule.cpp index 5f7cac0296..1589f86f03 100644 --- a/core/libs/tests/GuestSysmodule.cpp +++ b/core/libs/tests/GuestSysmodule.cpp @@ -130,6 +130,15 @@ void CheckUnwindInfo() { RequireUnwindMatchesRegion(reinterpret_cast(&RequireUnwindMatchesRegion)); RequireUnwindMatchesRegion(reinterpret_cast(&sceSysmoduleGetModuleInfoForUnwind)); RequireUnwindMatchesRegion(reinterpret_cast(&GetModuleHandleW)); + const auto* host = reinterpret_cast(&GetModuleHandleW); + MEMORY_BASIC_INFORMATION hostRegion{}; + Require(VirtualQuery(host, &hostRegion, sizeof(hostRegion)) != 0); + for (const std::size_t step : {std::size_t{0x1000}, std::size_t{0x3000}, std::size_t{0x10000}}) { + if (hostRegion.RegionSize > step) RequireUnwindMatchesRegion(host + step); + } + RequireUnwindMatchesRegion(host + 0x2000); + RequireUnwindMatchesRegion(host); + if (reinterpret_cast(host) >= 0x2000 + reinterpret_cast(hostRegion.BaseAddress)) RequireUnwindMatchesRegion(host - 0x2000); static int data = 1; static const char text[] = "unwind region"; RequireUnwindMatchesRegion(&data); From d67e7b7389e7d66a65c3208f1bdd935d1332d5f2 Mon Sep 17 00:00:00 2001 From: javierUDP Date: Thu, 15 Oct 2026 17:10:59 -0300 Subject: [PATCH 0403/1000] test(relinker): link the converter tests with a small macOS zero page The CLZERO case that needs a 32-bit address found no mapping because a 64-bit Mach-O executable reserves 4 GiB as `__PAGEZERO`, not because Rosetta withholds low memory: the same `mmap` succeeds once the executable is linked with a smaller zero page. The Apple x86-64 build links the test that way and the case runs with the other three, so the skip it had is gone and the Linux mapping failure stays fatal. The arm64 build keeps the 4 GiB zero page, which it requires to start. --- core/relinker/CMakeLists.txt | 3 +++ .../codegen/tests/Amd64OnlyConverterTests.cpp | 12 ++---------- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index d227fc017d..51dc17dbad 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -375,6 +375,9 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/include ${CMAKE_CURRENT_SOURCE_DIR}/io/include ) + if(APPLE AND (CMAKE_OSX_ARCHITECTURES STREQUAL "x86_64" OR (NOT CMAKE_OSX_ARCHITECTURES AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64"))) + target_link_options(amd64_only_converter_tests PRIVATE "LINKER:-pagezero_size,0x10000") + endif() add_test(NAME amd64_only_converter COMMAND amd64_only_converter_tests) add_test_executable(call_site_resolver_tests ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/CallSiteResolverTests.cpp diff --git a/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp b/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp index fa6930c21a..b88e1323d8 100644 --- a/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp +++ b/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp @@ -1304,12 +1304,6 @@ std::uint64_t insertqReference(std::uint64_t destination, std::uint64_t value, s return (destination & ~(mask << index)) | ((value & mask) << index); } -void* mapBuffer(const bool low) { - const int flags = MAP_PRIVATE | MAP_ANONYMOUS | (low ? MAP_32BIT : 0); - auto* buffer = mmap(nullptr, 4096, PROT_READ | PROT_WRITE, flags, -1, 0); - return buffer == MAP_FAILED ? nullptr : buffer; -} - constexpr std::uint64_t kStubScratch[2] = {0x0123456789abcdefull, 0xfedcba9876543210ull}; std::uint32_t rotr(const std::uint32_t value, const unsigned count) { @@ -1586,10 +1580,8 @@ void clzeroExecution() { } for (const auto& item : cases) { require(item.Match.has_value(), item.Name); - auto* buffer = static_cast(mapBuffer(item.Low)); - if (buffer == nullptr && item.Low) - continue; - require(buffer != nullptr, "cannot map the CLZERO buffer"); + auto* buffer = static_cast(mmap(nullptr, 4096, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | (item.Low ? MAP_32BIT : 0), -1, 0)); + require(buffer != MAP_FAILED, "cannot map the CLZERO buffer"); for (const std::size_t offset : {std::size_t{0x80}, std::size_t{0xAD}, std::size_t{0xFF}}) { std::memset(buffer, 0xA5, 4096); const auto address = reinterpret_cast(buffer + offset); From b8db74198ec7303b471f0242c52c0f80fdce5d8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Thu, 15 Oct 2026 22:15:16 +0200 Subject: [PATCH 0404/1000] Revert "fix(agc): validate mesh index descriptors on cached draws" (#5791) agc_driver_mesh_index_cache aborts on the linux job (lavapipe, LLVM 20.1.2: "LLVM ERROR: Cannot emit physreg copy instruction") on every main push build since the merge (first red: 38777177302). The PR was merged on a windows-only run after a linux-red batch was split. Co-Authored-By: Claude --- .../include/Driver/Dispatch/DispatchCache.hpp | 1 - .../Execution/src/Driver/Draw/DrawCache.cpp | 2 +- .../Execution/src/Driver/Draw/DrawCapture.cpp | 3 - .../Execution/src/Driver/Draw/DrawLookup.cpp | 6 - core/libs/prx/libSceAgcDriver/tests/Mesh.cpp | 17 +- .../tests/MeshGeometryFixture.hpp | 27 ---- .../tests/execution/MeshIndexCache.cpp | 145 ------------------ 7 files changed, 16 insertions(+), 185 deletions(-) delete mode 100644 core/libs/prx/libSceAgcDriver/tests/MeshGeometryFixture.hpp delete mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/MeshIndexCache.cpp diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Dispatch/DispatchCache.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Dispatch/DispatchCache.hpp index 41d3bce67a..110894795c 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Dispatch/DispatchCache.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Dispatch/DispatchCache.hpp @@ -29,7 +29,6 @@ struct DispatchVariant { std::vector words; std::uint64_t forgetSerial = 0; std::shared_ptr compiled; - std::array meshIndexDescriptor{}; std::shared_ptr shader; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCache.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCache.cpp index 2120be06c4..e7b4e66382 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCache.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCache.cpp @@ -125,7 +125,7 @@ void Driver::insertDrawEntry(std::uint64_t key, std::vectorstages[i]; - const auto present = std::find_if(variants.begin(), variants.end(), [&](const std::shared_ptr& kept) { return kept->pushOffset == fresh[i]->pushOffset && kept->meshIndexDescriptor == fresh[i]->meshIndexDescriptor && kept->runs == fresh[i]->runs && kept->words == fresh[i]->words; }); + const auto present = std::find_if(variants.begin(), variants.end(), [&](const std::shared_ptr& kept) { return kept->pushOffset == fresh[i]->pushOffset && kept->runs == fresh[i]->runs && kept->words == fresh[i]->words; }); if (present != variants.end()) { ++counters.present; fresh[i] = *present; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCapture.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCapture.cpp index 2cab9c30e7..5c049b74a6 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCapture.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCapture.cpp @@ -148,9 +148,6 @@ void Driver::cacheDrawStages(bool useDrawEntries, bool drawHit, const Pm4::DrawP if (stageCapture.compiled == nullptr || !CacheableResult(*stageCapture.compiled)) continue; auto variant = std::make_shared(); variant->compiled = stageCapture.compiled; - if (programs[i].binary.stage == ShaderRecompiler::ShaderStage::Mesh) { - std::copy_n(programs[i].userData.begin() + ShaderRecompiler::MeshIndexBufferUserWord, variant->meshIndexDescriptor.size(), variant->meshIndexDescriptor.begin()); - } variant->shader = programs[i].snapshot; variant->forgetSerial = stageCapture.forgetSerial; variant->pushOffset = stageCapture.pushOffset; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp index 366793d496..8308914ff2 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp @@ -1,7 +1,6 @@ #include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" -#include namespace AgcDriver::DriverDetail { @@ -108,11 +107,6 @@ void Driver::lookupDraw(const Submission& submission, const std::shared_ptrpushOffset != cursor) continue; - if (programs[i].binary.stage == ShaderRecompiler::ShaderStage::Mesh && !std::equal(variant->meshIndexDescriptor.begin(), variant->meshIndexDescriptor.end(), programs[i].userData.begin() + ShaderRecompiler::MeshIndexBufferUserWord)) { - if (!anyLayout) outcome = EntryOutcome::Differing; - anyLayout = true; - continue; - } auto& regions = matchedRegions[i]; regions.clear(); appendEntryRegions(*variant, regions); diff --git a/core/libs/prx/libSceAgcDriver/tests/Mesh.cpp b/core/libs/prx/libSceAgcDriver/tests/Mesh.cpp index 9a9b548f49..ac2fff1057 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Mesh.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Mesh.cpp @@ -1,6 +1,5 @@ #include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" #include "Recompiler.hpp" -#include "MeshGeometryFixture.hpp" #include "execution/VulkanTestDevice.hpp" #include "prx/libc/include/GuestAllocations.hpp" #include @@ -25,7 +24,21 @@ constexpr std::uint32_t Height = 128; constexpr std::array Background{16, 24, 40, 255}; alignas(256) std::array Pixels{}; -constexpr auto& GeometryCode = AgcDriver::Test::MeshGeometryCode; +alignas(256) constexpr std::array GeometryCode{ + 0x8f6a9003, 0x94fe6ac1, 0xbf88000b, 0xd7650006, 0x000100c1, 0xd7660006, 0x00020cc1, 0x93ebff03, + 0x00040018, 0xd7460006, 0x04190c6b, 0x340c0c82, 0xd8340000, 0x00000506, 0xbf8cc07f, 0xbefe04c1, + 0xbf8a0000, 0x938dff02, 0x00090016, 0x9382ff03, 0x00040018, 0x938cff03, 0x00080008, 0xd7650009, + 0x000100c1, 0xd7660009, 0x000212c1, 0xd746000a, 0x04250c02, 0x7da8120c, 0xbf88002e, 0x361600ff, + 0x0000ffff, 0x2c180090, 0x361a02ff, 0x0000ffff, 0xd8d80000, 0x0b00000b, 0xd8d80000, 0x0c00000c, + 0xd8d80000, 0x0d00000d, 0xbf8cc07f, 0xe0382000, 0x8002100b, 0xe0382010, 0x8002140b, 0xe0382000, + 0x8002180c, 0xe0382010, 0x80021c0c, 0xe0382000, 0x8002200d, 0xe0382010, 0x8002240d, 0x161c1483, + 0x161e14ff, 0x00000060, 0xbf8c3f70, 0xdb7c0400, 0x0000100f, 0xdb7c0410, 0x0000140f, 0xdb7c0420, + 0x0000180f, 0xdb7c0430, 0x00001c0f, 0xdb7c0440, 0x0000200f, 0xdb7c0450, 0x0000240f, 0x4a501c81, + 0x4a521c82, 0x3450508a, 0x34525294, 0xd772002a, 0x04a6510e, 0xbf8cc07f, 0xbefe04c1, 0xbf8a0000, + 0x930e830d, 0xbf078002, 0xbf850003, 0x8f0f8c0d, 0x887c0f0e, 0xbf900009, 0x7da8140d, 0xbf880002, + 0xf8000941, 0x0000002a, 0xbefe04c1, 0x7da8140e, 0xbf88000a, 0x34561485, 0xdbfc0400, 0x2c00002b, + 0xdbfc0410, 0x3000002b, 0xbf8cc07f, 0xf80008cf, 0x2f2e2d2c, 0xf800020f, 0x33323130, 0xbf810000, +}; alignas(256) constexpr auto Wave32GeometryCode = [] { auto code = GeometryCode; diff --git a/core/libs/prx/libSceAgcDriver/tests/MeshGeometryFixture.hpp b/core/libs/prx/libSceAgcDriver/tests/MeshGeometryFixture.hpp deleted file mode 100644 index 0c59ec020a..0000000000 --- a/core/libs/prx/libSceAgcDriver/tests/MeshGeometryFixture.hpp +++ /dev/null @@ -1,27 +0,0 @@ -#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_TESTS_MESHGEOMETRYFIXTURE_HPP -#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_TESTS_MESHGEOMETRYFIXTURE_HPP - -#include -#include - -namespace AgcDriver::Test { - -alignas(256) inline constexpr std::array MeshGeometryCode{ - 0x8f6a9003, 0x94fe6ac1, 0xbf88000b, 0xd7650006, 0x000100c1, 0xd7660006, 0x00020cc1, 0x93ebff03, - 0x00040018, 0xd7460006, 0x04190c6b, 0x340c0c82, 0xd8340000, 0x00000506, 0xbf8cc07f, 0xbefe04c1, - 0xbf8a0000, 0x938dff02, 0x00090016, 0x9382ff03, 0x00040018, 0x938cff03, 0x00080008, 0xd7650009, - 0x000100c1, 0xd7660009, 0x000212c1, 0xd746000a, 0x04250c02, 0x7da8120c, 0xbf88002e, 0x361600ff, - 0x0000ffff, 0x2c180090, 0x361a02ff, 0x0000ffff, 0xd8d80000, 0x0b00000b, 0xd8d80000, 0x0c00000c, - 0xd8d80000, 0x0d00000d, 0xbf8cc07f, 0xe0382000, 0x8002100b, 0xe0382010, 0x8002140b, 0xe0382000, - 0x8002180c, 0xe0382010, 0x80021c0c, 0xe0382000, 0x8002200d, 0xe0382010, 0x8002240d, 0x161c1483, - 0x161e14ff, 0x00000060, 0xbf8c3f70, 0xdb7c0400, 0x0000100f, 0xdb7c0410, 0x0000140f, 0xdb7c0420, - 0x0000180f, 0xdb7c0430, 0x00001c0f, 0xdb7c0440, 0x0000200f, 0xdb7c0450, 0x0000240f, 0x4a501c81, - 0x4a521c82, 0x3450508a, 0x34525294, 0xd772002a, 0x04a6510e, 0xbf8cc07f, 0xbefe04c1, 0xbf8a0000, - 0x930e830d, 0xbf078002, 0xbf850003, 0x8f0f8c0d, 0x887c0f0e, 0xbf900009, 0x7da8140d, 0xbf880002, - 0xf8000941, 0x0000002a, 0xbefe04c1, 0x7da8140e, 0xbf88000a, 0x34561485, 0xdbfc0400, 0x2c00002b, - 0xdbfc0410, 0x3000002b, 0xbf8cc07f, 0xf80008cf, 0x2f2e2d2c, 0xf800020f, 0x33323130, 0xbf810000, -}; - -} - -#endif diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/MeshIndexCache.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/MeshIndexCache.cpp deleted file mode 100644 index e03f45cd77..0000000000 --- a/core/libs/prx/libSceAgcDriver/tests/execution/MeshIndexCache.cpp +++ /dev/null @@ -1,145 +0,0 @@ -#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" -#include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" -#include "../MeshGeometryFixture.hpp" -#include "VulkanTestDevice.hpp" -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace { - -using AgcDriver::Graphics::Require; -using AgcDriver::DriverDetail::DrawMiss; - -constexpr std::uint32_t Width = 64; -constexpr std::uint32_t Height = 64; -alignas(256) std::array Pixels{}; -alignas(256) constexpr std::array Indices{0, 1, 2, 3, 4, 5, 0, 0, 0, 0}; -alignas(256) constexpr std::array RightIndices{3, 4, 5, 0}; -alignas(256) constexpr std::array, 6> Vertices{{ - {-0.9f, -0.8f, 0, 1, 1, 1, 1, 1}, {-0.1f, -0.8f, 0, 1, 1, 1, 1, 1}, {-0.5f, 0.8f, 0, 1, 1, 1, 1, 1}, - {0.1f, -0.8f, 0, 1, 1, 1, 1, 1}, {0.9f, -0.8f, 0, 1, 1, 1, 1, 1}, {0.5f, 0.8f, 0, 1, 1, 1, 1, 1} -}}; -alignas(256) constexpr std::array PixelCode{0x7e0e02f2u, 0xf800180fu, 0x07070707u, 0xbf810000u}; - -struct Header { - Shader shader{}; - ShaderUserData users{}; -}; - -Header GeometryHeader{}; -Header PixelHeader{}; - -void Register(Header& header, const std::uint32_t* code, std::size_t size, std::uint8_t type) { - header.shader.file_header = 0x34333231u; - header.shader.version = 0x18u; - header.shader.header_size = sizeof(header); - header.shader.shader_size = static_cast(size); - header.shader.code = code; - header.shader.type = type; - header.shader.user_data = &header.users; - AgcDriverRegisterShader_nid_postfix(&header.shader); -} - -void Write(std::vector& commands, std::uint32_t opcode, std::uint32_t offset, std::uint32_t value) { - commands.insert(commands.end(), {0xc0010000u | (opcode << 8u), offset, value}); -} - -std::vector Setup() { - const auto color = reinterpret_cast(Pixels.data()); - const auto geometry = reinterpret_cast(AgcDriver::Test::MeshGeometryCode.data()); - const auto pixel = reinterpret_cast(PixelCode.data()); - const auto vertices = reinterpret_cast(Vertices.data()); - const std::vector> context{ - {0x2d5, 0x20}, {0x1ff, 12}, {0x2ce, 3}, {0x29b, 2}, {0x2ab, 4}, {0x260, 0}, - {0x1b6, 0}, {0x207, 0}, {0x200, 0}, {0x203, 0x800}, - {0x2dc, 0xaa00}, {0x2f8, 0}, {0x292, 2}, {0x293, 0}, - {0x80, 0}, {0x8d, 0}, {0x83, 0xffff}, {0x8c, 0xa}, - {0x2f9, 0x2d}, {0x313, 0x6000}, {0x30e, 0xffffffff}, {0x30f, 0xffffffff}, - {0x206, 0x43f}, {0x204, 0x80000}, {0x205, 0x240}, - {0x8e, 0xf}, {0x8f, 0xf}, {0x202, 0xcc0010}, - {0x1c4, 0}, {0x1c5, 9}, {0x1c3, 4}, {0x31c, 0x28028}, - {0x31b, 0}, {0x31d, 0}, {0x3b0, ((Height - 1u) << 14u) | (Width - 1u)}, - {0x3b8, 0x9000000}, {0x1e0, 0}, {0xc, 0}, {0xd, (Height << 16u) | Width}, - {0x81, 0x80000000}, {0x82, (Height << 16u) | Width}, - {0x90, 0x80000000}, {0x91, (Height << 16u) | Width}, - {0x94, 0x80000000}, {0x95, (Height << 16u) | Width}, - {0x318, static_cast(color >> 8u)}, {0x390, static_cast(color >> 40u)}, - {0x10f, std::bit_cast(32.0f)}, {0x110, std::bit_cast(32.0f)}, - {0x111, std::bit_cast(32.0f)}, {0x112, std::bit_cast(32.0f)}, - {0x113, std::bit_cast(1.0f)}, {0x114, 0}, - {0xb4, 0}, {0xb5, std::bit_cast(1.0f)} - }; - const std::vector> shaders{ - {0xc8, static_cast(geometry >> 8u)}, {0xc9, static_cast(geometry >> 40u)}, - {0x8a, 0x600c0000u}, {0x8b, (8u << 19u) | (3u << 16u) | (4u << 1u)}, - {0x8c, static_cast(vertices)}, {0x8d, static_cast(vertices >> 32u) | (32u << 16u)}, - {0x8e, static_cast(Vertices.size())}, {0x8f, 0x01016facu}, - {0x8, static_cast(pixel >> 8u)}, {0x9, static_cast(pixel >> 40u)}, {0xb, 0} - }; - std::vector commands; - Write(commands, 0x79, 0x242, 4); - Write(commands, 0x79, 0x25b, (12u << 9u) | 4u); - for (const auto& [offset, value] : context) Write(commands, 0x69, offset, value); - for (const auto& [offset, value] : shaders) Write(commands, 0x76, offset, value); - commands.insert(commands.end(), {0xc0002a00u, 0u}); - return commands; -} - -void Draw(const std::uint16_t* indices, std::uint32_t count, bool left, bool right) { - Pixels.fill(std::byte{0}); - auto commands = Setup(); - const auto address = reinterpret_cast(indices); - commands.insert(commands.end(), {0xc0042700u, count, static_cast(address), static_cast(address >> 32u), count, 0u}); - Packet packet{commands.data(), static_cast(commands.size()), 0, {}}; - Require(sceAgcDriverSubmitDcb(&packet) == 0, "mesh submission failed"); - AgcDriverWaitIdle_nid_postfix(); - const auto lit = [](std::uint32_t x) { return std::to_integer(Pixels[(24u * Width + x) * 4u]) > 200u; }; - Require(lit(16) == left && lit(48) == right, "cached mesh draw used another draw's indices: count=" + std::to_string(count)); -} - -} - -int main() { - try { - { - const auto device = OpenVulkanTestDevice(); - if (!device || !TargetHasCapability(device->Target(), spv::CapabilityMeshShadingEXT)) return VulkanTestSkipped; - } - Register(GeometryHeader, AgcDriver::Test::MeshGeometryCode.data(), sizeof(AgcDriver::Test::MeshGeometryCode), 2); - Register(PixelHeader, PixelCode.data(), sizeof(PixelCode), 1); - for (unsigned i = 0; i < 3; ++i) Draw(Indices.data(), 3, true, false); - const auto warm = AgcDriver::DriverDetail::Driver::Get().DrawCacheCounters(); - Require(warm.hits != 0, "mesh warm-up did not reach the draw cache"); - const auto front = static_cast(DrawMiss::FrontDiffering); - const auto layout = static_cast(DrawMiss::Layout); - Draw(Indices.data(), 6, true, true); - const auto range = AgcDriver::DriverDetail::Driver::Get().DrawCacheCounters(); - Require(range.misses[front] == warm.misses[front] + 1 && range.misses[layout] == warm.misses[layout], "an index-range change was not counted as a differing front stage"); - Draw(Indices.data(), 6, true, true); - const auto rangeReuse = AgcDriver::DriverDetail::Driver::Get().DrawCacheCounters(); - Require(rangeReuse.hits == range.hits + 1, "the changed index range was not retained for cache reuse"); - Draw(RightIndices.data(), 3, false, true); - const auto address = AgcDriver::DriverDetail::Driver::Get().DrawCacheCounters(); - Require(address.misses[front] == rangeReuse.misses[front] + 1 && address.misses[layout] == rangeReuse.misses[layout], "an index-address change was not counted as a differing front stage"); - Draw(RightIndices.data(), 3, false, true); - const auto addressReuse = AgcDriver::DriverDetail::Driver::Get().DrawCacheCounters(); - Require(addressReuse.hits == address.hits + 1, "the changed index address was not retained for cache reuse"); - Draw(Indices.data(), 3, true, false); - const auto earlier = AgcDriver::DriverDetail::Driver::Get().DrawCacheCounters(); - Require(earlier.hits == addressReuse.hits + 1, "returning to an earlier index descriptor did not hit the cache"); - AgcDriverShutdown_nid_postfix(); - std::cout << "Cached mesh draws retained their index ranges and addresses\n"; - return 0; - } catch (const std::exception& error) { - std::cerr << error.what() << '\n'; - AgcDriverShutdown_nid_postfix(); - return 1; - } -} From fbe49d6a68b95e52a65c9a09c42e00fc893a8df0 Mon Sep 17 00:00:00 2001 From: JAB-dev <68183649+JAB-dev@users.noreply.github.com> Date: Thu, 15 Oct 2026 22:22:20 +0200 Subject: [PATCH 0405/1000] test(libSceSysmodule): look up an earlier page of a cached unwind region after a later one The backward lookup added with the region fix compared the address with a page it could not reach and never ran. The test now queries an ntdll address three pages in, then the first page, then the pages between, and compares each with VirtualQuery. The lookups after the first fail on the cache before the region fix. Co-Authored-By: Claude Sonnet 5.5 --- core/libs/tests/GuestSysmodule.cpp | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/core/libs/tests/GuestSysmodule.cpp b/core/libs/tests/GuestSysmodule.cpp index 1589f86f03..dba31b2f7b 100644 --- a/core/libs/tests/GuestSysmodule.cpp +++ b/core/libs/tests/GuestSysmodule.cpp @@ -138,7 +138,15 @@ void CheckUnwindInfo() { } RequireUnwindMatchesRegion(host + 0x2000); RequireUnwindMatchesRegion(host); - if (reinterpret_cast(host) >= 0x2000 + reinterpret_cast(hostRegion.BaseAddress)) RequireUnwindMatchesRegion(host - 0x2000); + const auto* anchor = reinterpret_cast(&RtlPcToFileHeader); + MEMORY_BASIC_INFORMATION anchorRegion{}; + Require(VirtualQuery(anchor, &anchorRegion, sizeof(anchorRegion)) != 0); + if (anchorRegion.RegionSize > 0x4000) { + RequireUnwindMatchesRegion(anchor + 0x3000); + RequireUnwindMatchesRegion(anchor); + RequireUnwindMatchesRegion(anchor + 0x1000); + RequireUnwindMatchesRegion(anchor + 0x2000); + } static int data = 1; static const char text[] = "unwind region"; RequireUnwindMatchesRegion(&data); From 3f8337e73035b420ac61e6c378733cd6f572eb49 Mon Sep 17 00:00:00 2001 From: mattogepeto Date: Thu, 15 Oct 2026 22:33:13 +0200 Subject: [PATCH 0406/1000] fix(libSceSysmodule): test and document unknown module ids sceSysmoduleIsLoaded, sceSysmoduleLoadModule and sceSysmoduleUnloadModule return success for an id missing from the module table, because titles that load or unload such an id aborted otherwise (b4db9fdd6, a2520aa07). Nothing tested or recorded it, and two commented-out throws were left in the code. Remove the commented-out throws, list the three functions under Silent stubs in the library's technical debt file, and test that unknown ids succeed without changing state while the internal variants still throw. Written with AI assistance (Claude). Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libSceSysmodule/Export.cpp | 2 -- core/libs/tests/GuestSysmodule.cpp | 26 ++++++++++++++++++++++++ docs/dev/debt/libSceSysmodule.md | 5 +++++ 3 files changed, 31 insertions(+), 2 deletions(-) create mode 100644 docs/dev/debt/libSceSysmodule.md diff --git a/core/libs/prx/libSceSysmodule/Export.cpp b/core/libs/prx/libSceSysmodule/Export.cpp index 8ed9239477..1cae4a26d9 100644 --- a/core/libs/prx/libSceSysmodule/Export.cpp +++ b/core/libs/prx/libSceSysmodule/Export.cpp @@ -122,7 +122,6 @@ int APS5_VABI sceSysmoduleIsLoaded(std::uint16_t id) { } if (!findModuleName(id)) { APS5_LOG_OUT("unknown id: %u", static_cast(id)); - // throw std::runtime_error(std::string("sceSysmoduleIsLoaded: unknown id ") + std::to_string(id)); return 0; } std::lock_guard lock(gMutex); @@ -139,7 +138,6 @@ int APS5_VABI sceSysmoduleLoadModule(std::uint16_t id) { } if (!findModuleName(id)) { APS5_LOG_OUT("unknown id: %u", static_cast(id)); - // throw std::runtime_error(std::string("sceSysmoduleLoadModule: unknown id ") + std::to_string(id)); return 0; } std::lock_guard lock(gMutex); diff --git a/core/libs/tests/GuestSysmodule.cpp b/core/libs/tests/GuestSysmodule.cpp index 4c7f86455c..f3f5ea9b60 100644 --- a/core/libs/tests/GuestSysmodule.cpp +++ b/core/libs/tests/GuestSysmodule.cpp @@ -1,5 +1,6 @@ #include "SceTypes.hpp" #include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libSceSysmodule/ModuleTable.hpp" #include #include #include @@ -103,9 +104,34 @@ void CheckUnwindInfo() { } #endif +std::uint16_t UnknownModuleId() { + for (std::uint32_t id = 0xffff; id != 0; --id) { + if (kModuleTable.find(id) == kModuleTable.end()) return static_cast(id); + } + std::abort(); +} + +void CheckUnknownIds() { + const std::uint16_t id = UnknownModuleId(); + const std::uint32_t internalId = id | kInternalFlag; + Require(kModuleTable.find(internalId) == kModuleTable.end()); + Require(sceSysmoduleIsLoaded(id) == 0); + Require(sceSysmoduleLoadModule(id) == 0); + Require(sceSysmoduleIsLoaded(id) == 0); + Require(sceSysmoduleUnloadModule(id) == 0); + Require(sceSysmoduleUnloadModule(id) == 0); + Require(sceSysmoduleIsLoaded(id) == 0); + Require(ThrowsRuntimeError([internalId] { sceSysmoduleLoadModuleInternal(internalId); })); + Require(ThrowsRuntimeError([internalId] { sceSysmoduleUnloadModuleInternal(internalId); })); + int result = 0x5a5a5a5a; + Require(ThrowsRuntimeError([internalId, &result] { sceSysmoduleLoadModuleInternalWithArg(internalId, 0, nullptr, 0, &result); })); + Require(result == 0x5a5a5a5a); +} + } int main() { + CheckUnknownIds(); Require(sceSysmoduleIsLoaded(0) == kInvalidValue); Require(sceSysmoduleLoadModule(0) == kInvalidValue); Require(sceSysmoduleUnloadModule(0) == kInvalidValue); diff --git a/docs/dev/debt/libSceSysmodule.md b/docs/dev/debt/libSceSysmodule.md new file mode 100644 index 0000000000..93c18eb81f --- /dev/null +++ b/docs/dev/debt/libSceSysmodule.md @@ -0,0 +1,5 @@ +# libSceSysmodule technical debt + +### Silent stubs + +- [sceSysmoduleIsLoaded, sceSysmoduleLoadModule, sceSysmoduleUnloadModule](../../../core/libs/prx/libSceSysmodule/Export.cpp) (libSceSysmodule) accept a module id missing from the module table: they log it and return 0 without loading anything, so `sceSysmoduleIsLoaded` reports an unknown module as loaded and unloading it succeeds, because titles that load or unload such an id aborted otherwise. The internal variants (`sceSysmoduleLoadModuleInternal`, `sceSysmoduleUnloadModuleInternal`, `sceSysmoduleLoadModuleInternalWithArg`, `sceSysmoduleGetModuleHandleInternal`) throw for an unknown id. What the console returns for an id it does not know is not verified. From 07dd28f448ba1b30503019322d32bc146d1a087d Mon Sep 17 00:00:00 2001 From: Tamalero <11452991+Tamalero@users.noreply.github.com> Date: Thu, 15 Oct 2026 13:40:53 -0700 Subject: [PATCH 0407/1000] fix(libSceVideoOut): keep mouse clicks and motion off the pad while the title reads the mouse The default bindings map the left and right mouse buttons to Square and R2, and mouse mode maps motion to the right stick. A title that opens the mouse with sceMouseOpen therefore gets every click twice: as a mouse button and as a pad press. PPSA15388 treats the pad press as a switch to gamepad input, re-centres its cursor and drops the click, so menu items cannot be clicked. While libSceMouse has a handle open, KeyboardMouse ignores mouse motion and mouse-button presses. Releases still pass, so a button held before the title opened the mouse is let go. Keys and the wheel are unchanged, as with the IME guard next to it. Co-Authored-By: Claude Opus 5.5 --- .../prx/libScePad/include/KeyboardMouse.hpp | 2 ++ .../prx/libSceVideoOut/src/KeyboardMouse.cpp | 7 +++- core/libs/prx/libSceVideoOut/src/PadInput.cpp | 2 ++ .../libSceVideoOut/tests/KeyboardMouse.cpp | 36 +++++++++++++++++++ 4 files changed, 46 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libScePad/include/KeyboardMouse.hpp b/core/libs/prx/libScePad/include/KeyboardMouse.hpp index 68e501a384..53336f6a47 100644 --- a/core/libs/prx/libScePad/include/KeyboardMouse.hpp +++ b/core/libs/prx/libScePad/include/KeyboardMouse.hpp @@ -48,6 +48,7 @@ public: InputRequests HandleEvent(const SDL_Event& event, Clock::time_point now); bool Advance(Clock::time_point now); void SetMouseMode(bool enabled); + void SetTitleReadsMouse(bool reads); void Release(); bool MouseMode() const; PadInputState Compose(const PadInputState& controller) const; @@ -61,6 +62,7 @@ private: Clock::time_point lastAdvance{}; bool advanced = false; bool mouseMode = false; + bool titleReadsMouse = false; }; std::uint8_t StickByte(double deflection); diff --git a/core/libs/prx/libSceVideoOut/src/KeyboardMouse.cpp b/core/libs/prx/libSceVideoOut/src/KeyboardMouse.cpp index d8cf0e3d7c..62ef4ae145 100644 --- a/core/libs/prx/libSceVideoOut/src/KeyboardMouse.cpp +++ b/core/libs/prx/libSceVideoOut/src/KeyboardMouse.cpp @@ -104,6 +104,10 @@ void Pad::KeyboardMouse::SetMouseMode(bool enabled) { mouse.Reset(); } +void Pad::KeyboardMouse::SetTitleReadsMouse(bool reads) { + titleReadsMouse = reads; +} + void Pad::KeyboardMouse::Release() { std::fill(pressed.begin(), pressed.end(), false); std::fill(wheelReleaseTimes.begin(), wheelReleaseTimes.end(), Clock::time_point{}); @@ -118,7 +122,7 @@ Pad::InputRequests Pad::KeyboardMouse::HandleEvent(const SDL_Event& event, Clock return requests; } if (event.type == SDL_MOUSEMOTION) { - if (mouseMode) mouse.Move(event.motion.xrel, event.motion.yrel); + if (mouseMode && !titleReadsMouse) mouse.Move(event.motion.xrel, event.motion.yrel); return requests; } if (event.type == SDL_MOUSEWHEEL) { @@ -138,6 +142,7 @@ Pad::InputRequests Pad::KeyboardMouse::HandleEvent(const SDL_Event& event, Clock const bool mouseButton = event.type == SDL_MOUSEBUTTONDOWN || event.type == SDL_MOUSEBUTTONUP; if (!keyboard && !mouseButton) return requests; if (keyboard && event.key.repeat != 0) return requests; + if (event.type == SDL_MOUSEBUTTONDOWN && titleReadsMouse) return requests; const bool down = event.type == SDL_KEYDOWN || event.type == SDL_MOUSEBUTTONDOWN; for (std::size_t index = 0; index < configuration.bindings.size(); ++index) { const auto& binding = configuration.bindings[index]; diff --git a/core/libs/prx/libSceVideoOut/src/PadInput.cpp b/core/libs/prx/libSceVideoOut/src/PadInput.cpp index 06352b8775..25ccd5af4a 100644 --- a/core/libs/prx/libSceVideoOut/src/PadInput.cpp +++ b/core/libs/prx/libSceVideoOut/src/PadInput.cpp @@ -9,6 +9,7 @@ #include "SDL.h" #include "prx/libSceVideoOut/include/PadInput.hpp" +#include "prx/libSceMouse/include/MouseState.hpp" #include "prx/libSceVideoOut/include/DisplayWindow.hpp" #include "prx/libScePad/include/PadState.hpp" #include "prx/libScePad/include/PadInputTypes.hpp" @@ -224,6 +225,7 @@ void PadInput::HandleEvent(const SDL_Event& event, DisplayWindow& window) { event.type == SDL_MOUSEBUTTONDOWN || event.type == SDL_MOUSEBUTTONUP; if (!input || ignoreInput) return; if (event.type == SDL_KEYDOWN && ImeCapturesKeyboard_nid_postfix()) return; + keyboardMouse.SetTitleReadsMouse(MouseIsOpen_nid_no_patch()); const auto requests = keyboardMouse.HandleEvent(event, std::chrono::steady_clock::now()); if (requests.toggleFullscreen && window.Handle() != nullptr && event.key.windowID == SDL_GetWindowID(window.Handle())) window.ToggleFullscreen(); if (requests.toggleMouse) setMouseMode(!keyboardMouse.MouseMode()); diff --git a/core/libs/prx/libSceVideoOut/tests/KeyboardMouse.cpp b/core/libs/prx/libSceVideoOut/tests/KeyboardMouse.cpp index 93715508cb..7dbe86259b 100644 --- a/core/libs/prx/libSceVideoOut/tests/KeyboardMouse.cpp +++ b/core/libs/prx/libSceVideoOut/tests/KeyboardMouse.cpp @@ -286,6 +286,41 @@ void testToggleRequests() { check(keyboardMouse.HandleEvent(KeyEvent(SDL_SCANCODE_F11, true), now).toggleFullscreen, "F11 asks to toggle fullscreen"); } + +void testTitleReadsMouse() { + const auto square = static_cast(Pad::PadButton::Square); + const auto r2 = static_cast(Pad::PadButton::R2); + auto now = Pad::KeyboardMouse::Clock::now(); + Pad::KeyboardMouse keyboardMouse(defaults()); + keyboardMouse.HandleEvent(PadSimulation::ButtonEvent(SDL_BUTTON_LEFT, true), now); + check((keyboardMouse.Compose(PadInputState{}).buttons & square) != 0, "a left click presses Square while the title does not read the mouse"); + keyboardMouse.SetTitleReadsMouse(true); + keyboardMouse.HandleEvent(PadSimulation::ButtonEvent(SDL_BUTTON_LEFT, false), now); + check((keyboardMouse.Compose(PadInputState{}).buttons & square) == 0, "releasing a button pressed before the title read the mouse left Square held"); + keyboardMouse.HandleEvent(PadSimulation::ButtonEvent(SDL_BUTTON_LEFT, true), now); + keyboardMouse.HandleEvent(PadSimulation::ButtonEvent(SDL_BUTTON_RIGHT, true), now); + const auto clicked = keyboardMouse.Compose(PadInputState{}); + check((clicked.buttons & (square | r2)) == 0 && clicked.analogButtonsR2 == 0, "a click while the title reads the mouse pressed a pad button"); + check(!keyboardMouse.HandleEvent(PadSimulation::ButtonEvent(SDL_BUTTON_MIDDLE, true), now).toggleMouse, "a middle click while the title reads the mouse asked to toggle mouse mode"); + keyboardMouse.HandleEvent(KeyEvent(SDL_SCANCODE_W, true), now); + check(keyboardMouse.Compose(PadInputState{}).sticks[1] == 0, "keys stopped reaching the pad while the title reads the mouse"); + keyboardMouse.SetMouseMode(true); + keyboardMouse.Advance(now); + for (int step = 1; step <= 40; ++step) { + now += std::chrono::milliseconds(8); + keyboardMouse.HandleEvent(PadSimulation::MotionEvent(200, 0), now); + keyboardMouse.Advance(now); + } + check(keyboardMouse.Compose(PadInputState{}).sticks[2] == 128, "motion tilted the right stick while the title reads the mouse"); + keyboardMouse.SetTitleReadsMouse(false); + for (int step = 1; step <= 40; ++step) { + now += std::chrono::milliseconds(8); + keyboardMouse.HandleEvent(PadSimulation::MotionEvent(200, 0), now); + keyboardMouse.Advance(now); + } + check(keyboardMouse.Compose(PadInputState{}).sticks[2] > 128, "motion in mouse mode no longer tilts the stick once the title stops reading the mouse"); +} + } int main() { @@ -305,6 +340,7 @@ int main() { {"wheel and keys on focus loss", testWheelFocusLoss}, {"focus loss releases the mouse", testFocusLossReleasesMouse}, {"toggle requests", testToggleRequests}, + {"title reads the mouse", testTitleReadsMouse}, }; for (const auto& [name, test] : tests) { test(); From b40d527b337c423602c846c09c7ba8ee264af07a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Thu, 15 Oct 2026 22:43:15 +0200 Subject: [PATCH 0408/1000] test(agc): zero the whole pending key store block agc_pending_key_stores cleared only the first 256 bytes of its 64 KiB block and then asked for the keys of 512 bytes. The other 256 were whatever the heap held: when they were not all equal the range read as mixed (correctly) and the check that it is unknown failed, so the test passed or failed with the allocator's state (main builds after #5784). Co-Authored-By: Claude --- core/libs/prx/libSceAgcDriver/tests/Recorder.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 040c2dd195..53f2a2b7eb 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -5550,7 +5550,7 @@ void pendingKeyStoreTests(const Device& device, Recorder& recorder) { return; } recorder.Sync(); - std::memset(keys, 0x00, keyCount); + std::memset(keys, 0x00, bytes); MarkDccUncompressed(context, address, surfaceBytes); Require(recorder.PendingWriteOverlaps(address, keyCount) && keys[0] == 0x00, "the uncompressed key store did not stay pending"); Require(CurrentDccKeys(address, surfaceBytes) == DccKeys::Uncompressed, "the keys after a pending uncompressed store do not read as uncompressed"); From d92ebfa8827f899699558331d5e7f0097e025483 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Thu, 15 Oct 2026 22:43:15 +0200 Subject: [PATCH 0409/1000] fix(agc): validate mesh index descriptors on cached draws (reapply #5791) Reapplies #5791 (reverted by #6552). agc_driver_mesh_index_cache now skips on llvmpipe like agc_driver_mesh does: llvmpipe aborts compiling these mesh programs (LLVM ERROR: Cannot emit physreg copy), which is what turned main's linux job red. Co-Authored-By: Claude --- .../include/Driver/Dispatch/DispatchCache.hpp | 1 + .../Execution/src/Driver/Draw/DrawCache.cpp | 2 +- .../Execution/src/Driver/Draw/DrawCapture.cpp | 3 + .../Execution/src/Driver/Draw/DrawLookup.cpp | 6 + core/libs/prx/libSceAgcDriver/tests/Mesh.cpp | 17 +- .../tests/MeshGeometryFixture.hpp | 27 ++++ .../tests/execution/MeshIndexCache.cpp | 150 ++++++++++++++++++ 7 files changed, 190 insertions(+), 16 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/MeshGeometryFixture.hpp create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/MeshIndexCache.cpp diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Dispatch/DispatchCache.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Dispatch/DispatchCache.hpp index 110894795c..41d3bce67a 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Dispatch/DispatchCache.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Dispatch/DispatchCache.hpp @@ -29,6 +29,7 @@ struct DispatchVariant { std::vector words; std::uint64_t forgetSerial = 0; std::shared_ptr compiled; + std::array meshIndexDescriptor{}; std::shared_ptr shader; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCache.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCache.cpp index e7b4e66382..2120be06c4 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCache.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCache.cpp @@ -125,7 +125,7 @@ void Driver::insertDrawEntry(std::uint64_t key, std::vectorstages[i]; - const auto present = std::find_if(variants.begin(), variants.end(), [&](const std::shared_ptr& kept) { return kept->pushOffset == fresh[i]->pushOffset && kept->runs == fresh[i]->runs && kept->words == fresh[i]->words; }); + const auto present = std::find_if(variants.begin(), variants.end(), [&](const std::shared_ptr& kept) { return kept->pushOffset == fresh[i]->pushOffset && kept->meshIndexDescriptor == fresh[i]->meshIndexDescriptor && kept->runs == fresh[i]->runs && kept->words == fresh[i]->words; }); if (present != variants.end()) { ++counters.present; fresh[i] = *present; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCapture.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCapture.cpp index 5c049b74a6..2cab9c30e7 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCapture.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCapture.cpp @@ -148,6 +148,9 @@ void Driver::cacheDrawStages(bool useDrawEntries, bool drawHit, const Pm4::DrawP if (stageCapture.compiled == nullptr || !CacheableResult(*stageCapture.compiled)) continue; auto variant = std::make_shared(); variant->compiled = stageCapture.compiled; + if (programs[i].binary.stage == ShaderRecompiler::ShaderStage::Mesh) { + std::copy_n(programs[i].userData.begin() + ShaderRecompiler::MeshIndexBufferUserWord, variant->meshIndexDescriptor.size(), variant->meshIndexDescriptor.begin()); + } variant->shader = programs[i].snapshot; variant->forgetSerial = stageCapture.forgetSerial; variant->pushOffset = stageCapture.pushOffset; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp index 8308914ff2..366793d496 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp @@ -1,6 +1,7 @@ #include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include namespace AgcDriver::DriverDetail { @@ -107,6 +108,11 @@ void Driver::lookupDraw(const Submission& submission, const std::shared_ptrpushOffset != cursor) continue; + if (programs[i].binary.stage == ShaderRecompiler::ShaderStage::Mesh && !std::equal(variant->meshIndexDescriptor.begin(), variant->meshIndexDescriptor.end(), programs[i].userData.begin() + ShaderRecompiler::MeshIndexBufferUserWord)) { + if (!anyLayout) outcome = EntryOutcome::Differing; + anyLayout = true; + continue; + } auto& regions = matchedRegions[i]; regions.clear(); appendEntryRegions(*variant, regions); diff --git a/core/libs/prx/libSceAgcDriver/tests/Mesh.cpp b/core/libs/prx/libSceAgcDriver/tests/Mesh.cpp index ac2fff1057..9a9b548f49 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Mesh.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Mesh.cpp @@ -1,5 +1,6 @@ #include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" #include "Recompiler.hpp" +#include "MeshGeometryFixture.hpp" #include "execution/VulkanTestDevice.hpp" #include "prx/libc/include/GuestAllocations.hpp" #include @@ -24,21 +25,7 @@ constexpr std::uint32_t Height = 128; constexpr std::array Background{16, 24, 40, 255}; alignas(256) std::array Pixels{}; -alignas(256) constexpr std::array GeometryCode{ - 0x8f6a9003, 0x94fe6ac1, 0xbf88000b, 0xd7650006, 0x000100c1, 0xd7660006, 0x00020cc1, 0x93ebff03, - 0x00040018, 0xd7460006, 0x04190c6b, 0x340c0c82, 0xd8340000, 0x00000506, 0xbf8cc07f, 0xbefe04c1, - 0xbf8a0000, 0x938dff02, 0x00090016, 0x9382ff03, 0x00040018, 0x938cff03, 0x00080008, 0xd7650009, - 0x000100c1, 0xd7660009, 0x000212c1, 0xd746000a, 0x04250c02, 0x7da8120c, 0xbf88002e, 0x361600ff, - 0x0000ffff, 0x2c180090, 0x361a02ff, 0x0000ffff, 0xd8d80000, 0x0b00000b, 0xd8d80000, 0x0c00000c, - 0xd8d80000, 0x0d00000d, 0xbf8cc07f, 0xe0382000, 0x8002100b, 0xe0382010, 0x8002140b, 0xe0382000, - 0x8002180c, 0xe0382010, 0x80021c0c, 0xe0382000, 0x8002200d, 0xe0382010, 0x8002240d, 0x161c1483, - 0x161e14ff, 0x00000060, 0xbf8c3f70, 0xdb7c0400, 0x0000100f, 0xdb7c0410, 0x0000140f, 0xdb7c0420, - 0x0000180f, 0xdb7c0430, 0x00001c0f, 0xdb7c0440, 0x0000200f, 0xdb7c0450, 0x0000240f, 0x4a501c81, - 0x4a521c82, 0x3450508a, 0x34525294, 0xd772002a, 0x04a6510e, 0xbf8cc07f, 0xbefe04c1, 0xbf8a0000, - 0x930e830d, 0xbf078002, 0xbf850003, 0x8f0f8c0d, 0x887c0f0e, 0xbf900009, 0x7da8140d, 0xbf880002, - 0xf8000941, 0x0000002a, 0xbefe04c1, 0x7da8140e, 0xbf88000a, 0x34561485, 0xdbfc0400, 0x2c00002b, - 0xdbfc0410, 0x3000002b, 0xbf8cc07f, 0xf80008cf, 0x2f2e2d2c, 0xf800020f, 0x33323130, 0xbf810000, -}; +constexpr auto& GeometryCode = AgcDriver::Test::MeshGeometryCode; alignas(256) constexpr auto Wave32GeometryCode = [] { auto code = GeometryCode; diff --git a/core/libs/prx/libSceAgcDriver/tests/MeshGeometryFixture.hpp b/core/libs/prx/libSceAgcDriver/tests/MeshGeometryFixture.hpp new file mode 100644 index 0000000000..0c59ec020a --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/MeshGeometryFixture.hpp @@ -0,0 +1,27 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_TESTS_MESHGEOMETRYFIXTURE_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_TESTS_MESHGEOMETRYFIXTURE_HPP + +#include +#include + +namespace AgcDriver::Test { + +alignas(256) inline constexpr std::array MeshGeometryCode{ + 0x8f6a9003, 0x94fe6ac1, 0xbf88000b, 0xd7650006, 0x000100c1, 0xd7660006, 0x00020cc1, 0x93ebff03, + 0x00040018, 0xd7460006, 0x04190c6b, 0x340c0c82, 0xd8340000, 0x00000506, 0xbf8cc07f, 0xbefe04c1, + 0xbf8a0000, 0x938dff02, 0x00090016, 0x9382ff03, 0x00040018, 0x938cff03, 0x00080008, 0xd7650009, + 0x000100c1, 0xd7660009, 0x000212c1, 0xd746000a, 0x04250c02, 0x7da8120c, 0xbf88002e, 0x361600ff, + 0x0000ffff, 0x2c180090, 0x361a02ff, 0x0000ffff, 0xd8d80000, 0x0b00000b, 0xd8d80000, 0x0c00000c, + 0xd8d80000, 0x0d00000d, 0xbf8cc07f, 0xe0382000, 0x8002100b, 0xe0382010, 0x8002140b, 0xe0382000, + 0x8002180c, 0xe0382010, 0x80021c0c, 0xe0382000, 0x8002200d, 0xe0382010, 0x8002240d, 0x161c1483, + 0x161e14ff, 0x00000060, 0xbf8c3f70, 0xdb7c0400, 0x0000100f, 0xdb7c0410, 0x0000140f, 0xdb7c0420, + 0x0000180f, 0xdb7c0430, 0x00001c0f, 0xdb7c0440, 0x0000200f, 0xdb7c0450, 0x0000240f, 0x4a501c81, + 0x4a521c82, 0x3450508a, 0x34525294, 0xd772002a, 0x04a6510e, 0xbf8cc07f, 0xbefe04c1, 0xbf8a0000, + 0x930e830d, 0xbf078002, 0xbf850003, 0x8f0f8c0d, 0x887c0f0e, 0xbf900009, 0x7da8140d, 0xbf880002, + 0xf8000941, 0x0000002a, 0xbefe04c1, 0x7da8140e, 0xbf88000a, 0x34561485, 0xdbfc0400, 0x2c00002b, + 0xdbfc0410, 0x3000002b, 0xbf8cc07f, 0xf80008cf, 0x2f2e2d2c, 0xf800020f, 0x33323130, 0xbf810000, +}; + +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/MeshIndexCache.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/MeshIndexCache.cpp new file mode 100644 index 0000000000..c449f905e8 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/MeshIndexCache.cpp @@ -0,0 +1,150 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" +#include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" +#include "../MeshGeometryFixture.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using AgcDriver::DriverDetail::DrawMiss; + +constexpr std::uint32_t Width = 64; +constexpr std::uint32_t Height = 64; +alignas(256) std::array Pixels{}; +alignas(256) constexpr std::array Indices{0, 1, 2, 3, 4, 5, 0, 0, 0, 0}; +alignas(256) constexpr std::array RightIndices{3, 4, 5, 0}; +alignas(256) constexpr std::array, 6> Vertices{{ + {-0.9f, -0.8f, 0, 1, 1, 1, 1, 1}, {-0.1f, -0.8f, 0, 1, 1, 1, 1, 1}, {-0.5f, 0.8f, 0, 1, 1, 1, 1, 1}, + {0.1f, -0.8f, 0, 1, 1, 1, 1, 1}, {0.9f, -0.8f, 0, 1, 1, 1, 1, 1}, {0.5f, 0.8f, 0, 1, 1, 1, 1, 1} +}}; +alignas(256) constexpr std::array PixelCode{0x7e0e02f2u, 0xf800180fu, 0x07070707u, 0xbf810000u}; + +struct Header { + Shader shader{}; + ShaderUserData users{}; +}; + +Header GeometryHeader{}; +Header PixelHeader{}; + +void Register(Header& header, const std::uint32_t* code, std::size_t size, std::uint8_t type) { + header.shader.file_header = 0x34333231u; + header.shader.version = 0x18u; + header.shader.header_size = sizeof(header); + header.shader.shader_size = static_cast(size); + header.shader.code = code; + header.shader.type = type; + header.shader.user_data = &header.users; + AgcDriverRegisterShader_nid_postfix(&header.shader); +} + +void Write(std::vector& commands, std::uint32_t opcode, std::uint32_t offset, std::uint32_t value) { + commands.insert(commands.end(), {0xc0010000u | (opcode << 8u), offset, value}); +} + +std::vector Setup() { + const auto color = reinterpret_cast(Pixels.data()); + const auto geometry = reinterpret_cast(AgcDriver::Test::MeshGeometryCode.data()); + const auto pixel = reinterpret_cast(PixelCode.data()); + const auto vertices = reinterpret_cast(Vertices.data()); + const std::vector> context{ + {0x2d5, 0x20}, {0x1ff, 12}, {0x2ce, 3}, {0x29b, 2}, {0x2ab, 4}, {0x260, 0}, + {0x1b6, 0}, {0x207, 0}, {0x200, 0}, {0x203, 0x800}, + {0x2dc, 0xaa00}, {0x2f8, 0}, {0x292, 2}, {0x293, 0}, + {0x80, 0}, {0x8d, 0}, {0x83, 0xffff}, {0x8c, 0xa}, + {0x2f9, 0x2d}, {0x313, 0x6000}, {0x30e, 0xffffffff}, {0x30f, 0xffffffff}, + {0x206, 0x43f}, {0x204, 0x80000}, {0x205, 0x240}, + {0x8e, 0xf}, {0x8f, 0xf}, {0x202, 0xcc0010}, + {0x1c4, 0}, {0x1c5, 9}, {0x1c3, 4}, {0x31c, 0x28028}, + {0x31b, 0}, {0x31d, 0}, {0x3b0, ((Height - 1u) << 14u) | (Width - 1u)}, + {0x3b8, 0x9000000}, {0x1e0, 0}, {0xc, 0}, {0xd, (Height << 16u) | Width}, + {0x81, 0x80000000}, {0x82, (Height << 16u) | Width}, + {0x90, 0x80000000}, {0x91, (Height << 16u) | Width}, + {0x94, 0x80000000}, {0x95, (Height << 16u) | Width}, + {0x318, static_cast(color >> 8u)}, {0x390, static_cast(color >> 40u)}, + {0x10f, std::bit_cast(32.0f)}, {0x110, std::bit_cast(32.0f)}, + {0x111, std::bit_cast(32.0f)}, {0x112, std::bit_cast(32.0f)}, + {0x113, std::bit_cast(1.0f)}, {0x114, 0}, + {0xb4, 0}, {0xb5, std::bit_cast(1.0f)} + }; + const std::vector> shaders{ + {0xc8, static_cast(geometry >> 8u)}, {0xc9, static_cast(geometry >> 40u)}, + {0x8a, 0x600c0000u}, {0x8b, (8u << 19u) | (3u << 16u) | (4u << 1u)}, + {0x8c, static_cast(vertices)}, {0x8d, static_cast(vertices >> 32u) | (32u << 16u)}, + {0x8e, static_cast(Vertices.size())}, {0x8f, 0x01016facu}, + {0x8, static_cast(pixel >> 8u)}, {0x9, static_cast(pixel >> 40u)}, {0xb, 0} + }; + std::vector commands; + Write(commands, 0x79, 0x242, 4); + Write(commands, 0x79, 0x25b, (12u << 9u) | 4u); + for (const auto& [offset, value] : context) Write(commands, 0x69, offset, value); + for (const auto& [offset, value] : shaders) Write(commands, 0x76, offset, value); + commands.insert(commands.end(), {0xc0002a00u, 0u}); + return commands; +} + +void Draw(const std::uint16_t* indices, std::uint32_t count, bool left, bool right) { + Pixels.fill(std::byte{0}); + auto commands = Setup(); + const auto address = reinterpret_cast(indices); + commands.insert(commands.end(), {0xc0042700u, count, static_cast(address), static_cast(address >> 32u), count, 0u}); + Packet packet{commands.data(), static_cast(commands.size()), 0, {}}; + Require(sceAgcDriverSubmitDcb(&packet) == 0, "mesh submission failed"); + AgcDriverWaitIdle_nid_postfix(); + const auto lit = [](std::uint32_t x) { return std::to_integer(Pixels[(24u * Width + x) * 4u]) > 200u; }; + Require(lit(16) == left && lit(48) == right, "cached mesh draw used another draw's indices: count=" + std::to_string(count)); +} + +} + +int main() { + try { + { + const auto device = OpenVulkanTestDevice(); + if (!device || !TargetHasCapability(device->Target(), spv::CapabilityMeshShadingEXT)) return VulkanTestSkipped; + if (device->DeviceName().starts_with("llvmpipe")) { + std::puts("skipped, the device has no mesh shader compiler that handles these programs (llvmpipe)"); + return VulkanTestSkipped; + } + } + Register(GeometryHeader, AgcDriver::Test::MeshGeometryCode.data(), sizeof(AgcDriver::Test::MeshGeometryCode), 2); + Register(PixelHeader, PixelCode.data(), sizeof(PixelCode), 1); + for (unsigned i = 0; i < 3; ++i) Draw(Indices.data(), 3, true, false); + const auto warm = AgcDriver::DriverDetail::Driver::Get().DrawCacheCounters(); + Require(warm.hits != 0, "mesh warm-up did not reach the draw cache"); + const auto front = static_cast(DrawMiss::FrontDiffering); + const auto layout = static_cast(DrawMiss::Layout); + Draw(Indices.data(), 6, true, true); + const auto range = AgcDriver::DriverDetail::Driver::Get().DrawCacheCounters(); + Require(range.misses[front] == warm.misses[front] + 1 && range.misses[layout] == warm.misses[layout], "an index-range change was not counted as a differing front stage"); + Draw(Indices.data(), 6, true, true); + const auto rangeReuse = AgcDriver::DriverDetail::Driver::Get().DrawCacheCounters(); + Require(rangeReuse.hits == range.hits + 1, "the changed index range was not retained for cache reuse"); + Draw(RightIndices.data(), 3, false, true); + const auto address = AgcDriver::DriverDetail::Driver::Get().DrawCacheCounters(); + Require(address.misses[front] == rangeReuse.misses[front] + 1 && address.misses[layout] == rangeReuse.misses[layout], "an index-address change was not counted as a differing front stage"); + Draw(RightIndices.data(), 3, false, true); + const auto addressReuse = AgcDriver::DriverDetail::Driver::Get().DrawCacheCounters(); + Require(addressReuse.hits == address.hits + 1, "the changed index address was not retained for cache reuse"); + Draw(Indices.data(), 3, true, false); + const auto earlier = AgcDriver::DriverDetail::Driver::Get().DrawCacheCounters(); + Require(earlier.hits == addressReuse.hits + 1, "returning to an earlier index descriptor did not hit the cache"); + AgcDriverShutdown_nid_postfix(); + std::cout << "Cached mesh draws retained their index ranges and addresses\n"; + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + AgcDriverShutdown_nid_postfix(); + return 1; + } +} From 1cfa32a86b22558eaebfdd2f31689d4dbbd10ad4 Mon Sep 17 00:00:00 2001 From: GalaxyGamingFR <153150716+GalaxyGamingFR@users.noreply.github.com> Date: Thu, 15 Oct 2026 13:50:02 -0700 Subject: [PATCH 0410/1000] fix(check_compile): take changed units from the merge base changed() diffed the working tree against the tip of the base, so files changed only on upstream main since the branch point were reported and compiled. It now diffs against the merge base of the base and HEAD. A regression test covers a base-only change. Co-Authored-By: Claude Opus 5.5 --- tools/check_compile.py | 5 ++++- tools/tests/test_check_compile.py | 22 ++++++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/tools/check_compile.py b/tools/check_compile.py index 1ec01a433c..71bb8a836b 100644 --- a/tools/check_compile.py +++ b/tools/check_compile.py @@ -58,7 +58,10 @@ def index(build): def changed(root, base): - result = capture(["git", "-C", str(root), "diff", "--name-only", "--diff-filter=ACMR", "-z", base]) + fork = capture(["git", "-C", str(root), "merge-base", base, "HEAD"]) + if fork.returncode != 0: + raise ValueError(f"git merge-base {base}: {fork.stderr.strip()}") + result = capture(["git", "-C", str(root), "diff", "--name-only", "--diff-filter=ACMR", "-z", fork.stdout.strip()]) if result.returncode != 0: raise ValueError(f"git diff {base}: {result.stderr.strip()}") untracked = capture(["git", "-C", str(root), "ls-files", "--others", "--exclude-standard", "-z"]) diff --git a/tools/tests/test_check_compile.py b/tools/tests/test_check_compile.py index b691d60d45..90d85a9602 100644 --- a/tools/tests/test_check_compile.py +++ b/tools/tests/test_check_compile.py @@ -170,6 +170,28 @@ class CheckCompileTests(unittest.TestCase): self.assertEqual(result.returncode, 1) self.assertIn("changed.cpp:1:1: error: no", result.stderr) + def test_files_changed_only_on_the_base_since_the_branch_point_are_not_checked(self): + script = self.compiler("import sys\nsys.stderr.write(sys.argv[-1] + ': error: no\\n')\nsys.exit(1)\n") + self.manifest([self.entry(name, f'"{sys.executable}" "{script}" "{self.root / name}"') + for name in ("mine.cpp", "other.cpp")]) + git = ["git", "-C", str(self.root)] + self.source("other.cpp") + subprocess.run([*git, "add", "other.cpp"], check=True) + subprocess.run([*git, "commit", "-qm", "add"], check=True) + subprocess.run([*git, "branch", "-M", "main"], check=True) + subprocess.run([*git, "checkout", "-qb", "side"], check=True) + self.source("mine.cpp") + subprocess.run([*git, "add", "mine.cpp"], check=True) + subprocess.run([*git, "commit", "-qm", "mine"], check=True) + subprocess.run([*git, "checkout", "-q", "main"], check=True) + self.source("other.cpp", "int y;\n") + subprocess.run([*git, "commit", "-qam", "other"], check=True) + subprocess.run([*git, "checkout", "-q", "side"], check=True) + result = self.tool("--base", "main") + self.assertEqual(result.returncode, 1) + self.assertIn("mine.cpp: error: no", result.stderr) + self.assertNotIn("other.cpp", result.stderr) + def test_no_changed_translation_unit_is_not_a_failure(self): self.source("clean.cpp") subprocess.run(["git", "-C", str(self.root), "add", "clean.cpp"], check=True) From 50d8db46eadb412bd0e06c01ab290251e266a805 Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Thu, 15 Oct 2026 22:56:35 +0200 Subject: [PATCH 0411/1000] docs(usage): state that the shader cache directory has no size limit The Shader cache section says only that subdirectories of other versions are removed after 14 days. housekeeping() in ShaderDiskCache.cpp removes nothing from the current version, and no size or entry limit applies there. Co-Authored-By: Claude Sonnet 5.5 --- docs/user/USAGE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index 57f4ba8cdd..4914299f1a 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -177,7 +177,7 @@ HDR output is available on Windows only. Titles see it when the monitor showing ### Shader cache -Recompiled shaders and the Vulkan pipeline cache are kept in a `shader_cache/` directory beside the output executable, so later runs skip recompilation. Set `ANYPS5_SHADER_CACHE_DIR` to use another directory, or set `ANYPS5_NO_SHADER_CACHE` to any non-empty value other than `0` to disable both caches. Shaders are stored in a subdirectory named after the recompiler's source version; subdirectories of other versions unused for 14 days are removed the first time a run looks up a shader. The pipeline cache is one `vk-pipelines-*.bin` file per GPU and driver version. Deleting the directory is safe: it is rebuilt on the next run. +Recompiled shaders and the Vulkan pipeline cache are kept in a `shader_cache/` directory beside the output executable, so later runs skip recompilation. Set `ANYPS5_SHADER_CACHE_DIR` to use another directory, or set `ANYPS5_NO_SHADER_CACHE` to any non-empty value other than `0` to disable both caches. Shaders are stored in a subdirectory named after the recompiler's source version; subdirectories of other versions unused for 14 days are removed the first time a run looks up a shader. The pipeline cache is one `vk-pipelines-*.bin` file per GPU and driver version. Entries of the current version are never removed and the directory has no size limit, so a title that compiles many shaders can add several GB within minutes. Deleting the directory is safe: it is rebuilt on the next run. ### Storing GPU results at each flip From a70dea62e7facfee1146154767551f49a241f13b Mon Sep 17 00:00:00 2001 From: GalaxyGamingFR <153150716+GalaxyGamingFR@users.noreply.github.com> Date: Thu, 15 Oct 2026 13:50:13 -0700 Subject: [PATCH 0412/1000] fix(check_main_sync): fetch an explicit --base before resolving it main() resolved the base before running --fetch, so an explicit base on a remote that was never fetched failed with "no base ref found" and never fetched. The fetch is now a local helper and runs first when --base is given; the default base still resolves first, then fetches. Co-Authored-By: Claude Opus 5.5 --- tools/check_main_sync.py | 11 ++++++++--- tools/tests/test_check_main_sync.py | 8 ++++++++ 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/tools/check_main_sync.py b/tools/check_main_sync.py index fa3265d310..12d5b78a32 100644 --- a/tools/check_main_sync.py +++ b/tools/check_main_sync.py @@ -97,12 +97,17 @@ if __name__ == "__main__": parser.add_argument("--strict", action="store_true", help="also fail when the branch is behind the base") args = parser.parse_args() try: - base = resolve_base(args.repo, args.base) - if args.fetch: - remote, _, branch = base.partition("/") + def fetch(ref): + remote, _, branch = ref.partition("/") if not branch: parser.error("--fetch needs a remote-tracking base such as upstream/main") git(args.repo, "fetch", "--quiet", remote, branch) + + if args.fetch and args.base: + fetch(args.base) + base = resolve_base(args.repo, args.base) + if args.fetch and not args.base: + fetch(base) data = report(args.repo, base, args.head) except RuntimeError as error: parser.error(str(error)) diff --git a/tools/tests/test_check_main_sync.py b/tools/tests/test_check_main_sync.py index 4a13a56e0a..d893e70b0e 100644 --- a/tools/tests/test_check_main_sync.py +++ b/tools/tests/test_check_main_sync.py @@ -177,6 +177,14 @@ class CheckMainSyncTests(unittest.TestCase): self.assertEqual(result.returncode, 2) self.assertIn("no base ref found", result.stderr) + def test_explicit_base_is_fetched_before_it_is_resolved(self): + with tempfile.TemporaryDirectory() as other: + subprocess.run(["git", "clone", "-q", self.directory.name, other], check=True, capture_output=True) + self.repo.git("remote", "add", "upstream", other) + result = subprocess.run([sys.executable, str(TOOL), "--repo", self.directory.name, "--base", "upstream/main", "--fetch"], capture_output=True, text=True) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertIn("merge: clean", result.stdout) + if __name__ == "__main__": unittest.main() From 4b8ccd2e425bd08923c8b41a626cfbe45d405009 Mon Sep 17 00:00:00 2001 From: Hashaam Zafar Date: Thu, 15 Oct 2026 14:04:10 -0700 Subject: [PATCH 0413/1000] test(agc): write more pixels than a pipe holds in the closed-pipe case PixelFailure wrote a 128x128 payload, exactly the 64 KiB a default Linux pipe holds. Once the consumer read the 54-byte header, the whole payload fit in the pipe, so the write could finish before the reader closed and the expected EPIPE never came. agc_frame_dump_io failed in 147 of 300 runs on Linux. A 512x512 payload exceeds the default pipe capacity on Linux and macOS, so the write always reaches the closed reader. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAgcDriver/tests/FrameDumpIo.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/FrameDumpIo.cpp b/core/libs/prx/libSceAgcDriver/tests/FrameDumpIo.cpp index a23ee5531f..ec52e5f1d9 100644 --- a/core/libs/prx/libSceAgcDriver/tests/FrameDumpIo.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/FrameDumpIo.cpp @@ -96,8 +96,8 @@ void PixelFailure() { } Close(reader); }); - const std::vector pixels(128 * 128 * 4); - const auto failure = AgcDriver::WriteBmpAndClose_nid_no_patch(file, 128, 128, pixels); + const std::vector pixels(512 * 512 * 4); + const auto failure = AgcDriver::WriteBmpAndClose_nid_no_patch(file, 512, 512, pixels); consumer.join(); Check(received == 54, "pixel failure did not follow a complete BMP header"); Check(failure && std::strcmp(failure.operation, "pixels") == 0, "closed pipe pixel write was accepted"); From 266e8de2ccb409e353a8ace6f74bf3cad6d3a57c Mon Sep 17 00:00:00 2001 From: David Date: Fri, 16 Oct 2026 01:05:09 +0400 Subject: [PATCH 0414/1000] test(libc): wait on a _Cnd_timedwait deadline beyond the nanosecond range A deadline of 10^10 seconds does not fit a nanosecond count but fits the host clock where system_clock counts microseconds, as on macOS. There the wait must block until it is woken; where the clock cannot hold the deadline, _Cnd_timedwait must throw the overflow error as for any other deadline past the clock's range. --- core/libs/tests/GuestLibcCndTimedWait.cpp | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/core/libs/tests/GuestLibcCndTimedWait.cpp b/core/libs/tests/GuestLibcCndTimedWait.cpp index 07bb8719c3..467d2e6ed5 100644 --- a/core/libs/tests/GuestLibcCndTimedWait.cpp +++ b/core/libs/tests/GuestLibcCndTimedWait.cpp @@ -99,6 +99,26 @@ int main() { } signaler.join(); + constexpr std::int64_t farSeconds = 10000000000; + const Xtime farTime{farSeconds, 999999999}; + constexpr auto clockSeconds = std::chrono::duration_cast(std::chrono::system_clock::duration::max()).count(); + if (farSeconds < clockSeconds) { + bool farReady = false; + std::thread farSignaler([&] { + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + Require(_Mtx_lock_nid_postfix(&mutex) == success); + farReady = true; + Require(_Cnd_broadcast_nid_postfix(&condition) == success); + Require(_Mtx_unlock_nid_postfix(&mutex) == success); + }); + while (!farReady) { + Require(_Cnd_timedwait_nid_postfix(&condition, &mutex, &farTime) == success); + } + farSignaler.join(); + } else { + RequireThrows([&] { _Cnd_timedwait_nid_postfix(&condition, &mutex, &farTime); }); + } + Require(_Mtx_unlock_nid_postfix(&mutex) == success); RequireThrows([&] { _Cnd_timedwait_nid_postfix(&condition, &mutex, nullptr); }); const Xtime overflowCarryTime{std::numeric_limits::max(), 1000000000}; From 2854da23aaf726b5e8def47cfb5fb1a4a9de5f0d Mon Sep 17 00:00:00 2001 From: David Date: Fri, 16 Oct 2026 01:05:09 +0400 Subject: [PATCH 0415/1000] fix(libc): keep the _Cnd_timedwait deadline in the host clock's unit The deadline was built as seconds plus nanoseconds, a nanosecond duration, and converted implicitly to a system_clock time point. Where system_clock counts microseconds, as on macOS, that conversion is lossy and does not compile, so libc did not build there. The seconds now stay in the clock's unit and only the nanoseconds are rounded up to it, so the wait never ends before the requested time. With a nanosecond system_clock nothing changes. Converting the nanosecond sum instead would compile but overflow above 2^63 nanoseconds, which the clock range check above allows on macOS: a 10^10 second deadline then wraps into the past and the wait times out at once. --- core/libs/prx/libc/src/Threads.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libc/src/Threads.cpp b/core/libs/prx/libc/src/Threads.cpp index b1d85f9faf..a5cc181812 100644 --- a/core/libs/prx/libc/src/Threads.cpp +++ b/core/libs/prx/libc/src/Threads.cpp @@ -246,7 +246,8 @@ int APS5_VABI _Cnd_timedwait_nid_postfix(void** conditionHandle, void** mutexHan bool timeout = false; try { const auto deadlineTime = std::chrono::system_clock::time_point( - std::chrono::seconds(deadline.tv_sec) + std::chrono::nanoseconds(deadline.tv_nsec)); + std::chrono::seconds(deadline.tv_sec) + + std::chrono::ceil(std::chrono::nanoseconds(deadline.tv_nsec))); timeout = (condition->native.wait_until(lock, deadlineTime) == std::cv_status::timeout); } catch (const std::system_error&) { lock.release(); From 21ff40a95cab7e3f6f3f0db12854f07ea2d936af Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Thu, 15 Oct 2026 23:25:54 +0200 Subject: [PATCH 0416/1000] docs(input): list the default keyboard and mouse bindings INPUT_MAPPING.md names the actions and says that omitted actions keep their built-in bindings, but not what those bindings are. The table is read from InputMapping in core/libs/prx/libScePad/include/InputMapping.hpp. Co-Authored-By: Claude Sonnet 5.5 --- docs/user/INPUT_MAPPING.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/docs/user/INPUT_MAPPING.md b/docs/user/INPUT_MAPPING.md index 3032533866..882ce0a000 100644 --- a/docs/user/INPUT_MAPPING.md +++ b/docs/user/INPUT_MAPPING.md @@ -18,6 +18,31 @@ Game-controller bindings are enabled automatically and are not affected by keybo Supported actions are `Cross`, `Circle`, `Triangle`, `Square`, `L1`, `R1`, `L2`, `R2`, `L3`, `R3`, `Options`, `Up`, `Right`, `Down`, `Left`, `LeftStickLeft`, `LeftStickRight`, `LeftStickUp`, `LeftStickDown`, `RightStickLeft`, `RightStickRight`, `RightStickUp`, `RightStickDown`, `TouchLeft`, `TouchRight`, `ToggleMouse`, and `ToggleFullscreen`. +### Default bindings + +Without an `anyps5-input.ini`, or for actions the file does not mention, these bindings apply: + +| Action | Keyboard and mouse | +|--------|--------------------| +| `Cross` | Return, Space | +| `Circle` | C | +| `Triangle` | I | +| `Square` | Left mouse button | +| `L1` | Q | +| `R1` | E, Left Alt, Right Alt | +| `R2` | Right mouse button | +| `L3` | Left Shift, Right Shift | +| `R3` | Left Ctrl, Right Ctrl | +| `Options` | Escape | +| `Up`, `Right`, `Down`, `Left` | Arrow keys; the mouse wheel also presses `Up` (wheel up) and `Down` (wheel down) | +| `LeftStickLeft`, `LeftStickRight`, `LeftStickUp`, `LeftStickDown` | A, D, W, S | +| `RightStickLeft`, `RightStickRight`, `RightStickUp`, `RightStickDown` | F, H, T, G | +| `TouchLeft`, `TouchRight` | Backspace, Tab | +| `ToggleMouse` | Middle mouse button | +| `ToggleFullscreen` | F11 | + +`L2` has no built-in keyboard or mouse binding. + For example, this changes Cross to F or Space, moves the left stick to IJKL, uses the mouse buttons for Square and R2, and keeps all other built-in bindings: ```ini From 42191c0807cc0a3a1748996e660cacd3f6da59ea Mon Sep 17 00:00:00 2001 From: Hashaam Zafar Date: Thu, 15 Oct 2026 14:26:53 -0700 Subject: [PATCH 0417/1000] test(libc): keep intentional aborts away from the system crash handler guest_host_standard_error and exception_runtime fork children that end in SIGABRT on purpose. When kernel.core_pattern pipes cores to a handler, as Ubuntu's apport and systemd-coredump do, the kernel starts it for every dumpable crash even with RLIMIT_CORE at 0. apport takes about 1.1 s per crash here, so guest_host_standard_error spent 2.3 s waiting and timed out at its 10 s limit under parallel ctest load. Marking those children non-dumpable on Linux skips the handler: 2.29 s to 0.01 s and 1.17 s to 0.01 s, with the same SIGABRT and message checks. Co-Authored-By: Claude Opus 5.5 --- core/libs/tests/ExceptionRuntime.cpp | 6 ++++++ core/libs/tests/GuestHostStandardError.cpp | 14 +++++++++++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/core/libs/tests/ExceptionRuntime.cpp b/core/libs/tests/ExceptionRuntime.cpp index 43dc8405c0..42079527da 100644 --- a/core/libs/tests/ExceptionRuntime.cpp +++ b/core/libs/tests/ExceptionRuntime.cpp @@ -14,6 +14,9 @@ #include #include #include +#ifdef __linux__ +#include +#endif #include extern "C" unsigned __cxa_uncaught_exceptions_nid_postfix(); @@ -172,6 +175,9 @@ void CheckDefaultTerminateMessage() { if (child == 0) { close(channel[0]); dup2(channel[1], 2); +#ifdef __linux__ + assert(prctl(PR_SET_DUMPABLE, 0) == 0); +#endif throw std::runtime_error(message); } close(channel[1]); diff --git a/core/libs/tests/GuestHostStandardError.cpp b/core/libs/tests/GuestHostStandardError.cpp index 72e2ea7025..bf4df157ce 100644 --- a/core/libs/tests/GuestHostStandardError.cpp +++ b/core/libs/tests/GuestHostStandardError.cpp @@ -11,6 +11,9 @@ #include #include #include +#ifdef __linux__ +#include +#endif extern "C" { FileStream* APS5_VABI freopen_nid_postfix(const char*, const char*, FileStream*); @@ -25,6 +28,12 @@ extern FileStream* __stderrp_nid_postfix; static void Require(bool value) { if (!value) std::abort(); } +static void SkipCrashHandler() { +#ifdef __linux__ + Require(::prctl(PR_SET_DUMPABLE, 0) == 0); +#endif +} + enum class Redirect { Duplicate, Reopen }; struct Outcome { @@ -56,7 +65,10 @@ static Outcome Run(Redirect redirect, const std::filesystem::path& probe, bool t ::close(host[1]); RedirectGuest(redirect, probe); std::fputs("host\n", stderr); - if (terminate) throw std::runtime_error("report kept"); + if (terminate) { + SkipCrashHandler(); + throw std::runtime_error("report kept"); + } if (redirect == Redirect::Reopen && fflush_nid_postfix(__stderrp_nid_postfix) != 0) ::_exit(71); ::_exit(0); } From 881facc4fa50a49d4148b4f385b280b62886d144 Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Thu, 15 Oct 2026 21:35:59 +0000 Subject: [PATCH 0418/1000] fix(agc): refuse native MSAA index buffer reads --- .../prx/libSceAgcDriver/Graphics/src/Draw.cpp | 1 + .../execution/SampleMultisampleTarget.cpp | 79 ++++++++++++++++++- docs/dev/debt/libSceAgcDriver.md | 2 +- 3 files changed, 78 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index 7fb4593c6e..e44bdee878 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -1031,6 +1031,7 @@ DrawInputs prepareDrawInputs(const Context& context, const State& state, const P if (draw.indexed) GuestMemory::CheckRange(reinterpret_cast(draw.indexAddress), static_cast(indexBytes), draw.indexSize); APS5_LOG_CHARS_OUT_DEBUG("Index buffer range OK"); Require(!draw.indexed || !state.hasColorTarget || draw.indexAddress + indexBytes <= state.color.address || state.color.address + state.color.bytes <= draw.indexAddress, "index buffer aliases the render target"); + if (draw.indexed) RequireNoMultisampleTarget(draw.indexAddress, static_cast(indexBytes), "an index buffer read"); APS5_LOG_CHARS_OUT_DEBUG("ValidateShaders"); if (recipe != nullptr) { inputs.fragmentOutputs = recipe->fragmentOutputs; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp index f3d7e2bf44..94101e7a81 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp @@ -3,6 +3,7 @@ #include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp" #include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp" #include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" #include "prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp" #include "prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp" #include "prx/libSceAgcDriver/Graphics/include/State.hpp" @@ -25,9 +26,19 @@ #include #include #include +#include #include #include #include +#include + +namespace AgcDriver { + +struct VulkanDeviceTestAccess { + static Graphics::Context ContextOf(const VulkanDevice& device) { return device.graphicsContext(); } +}; + +} namespace { @@ -142,7 +153,7 @@ AgcDriver::Graphics::State TargetState(const AgcDriver::Graphics::ColorTarget& c return state; } -void Draw(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::State& state) { +void Draw(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::State& state, std::uint64_t indexAddress = 0, std::uint32_t indexSize = 2u) { const auto target = device.Target(); constexpr std::uint32_t waveSize = 64; std::vector vertexUserData(4, 0u); @@ -181,7 +192,7 @@ void Draw(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::State& sta {ShaderStage::Vertex, &vertexResult, 0}, {ShaderStage::Fragment, pixelResult.get(), PixelPushOffset(vertexPush, target)} }}; - const AgcDriver::Pm4::DrawParameters draw{0, static_cast(Triangle.size()), 0, 1, 0, false}; + const AgcDriver::Pm4::DrawParameters draw{indexAddress, static_cast(Triangle.size()), indexAddress != 0 ? indexSize : 0u, 1, 0, indexAddress != 0}; device.Draw(state, draw, shaders); } @@ -293,6 +304,62 @@ void CheckUniform(const std::vector>& loaded, const std::ar } } +void CheckNativeIndices(AgcDriver::VulkanDevice& device, const Block& source, const Block& destination) { + const auto context = AgcDriver::VulkanDeviceTestAccess::ContextOf(device); + if (context.hostImportAlignment == 0) { + std::puts("native index alias checks not run: host imports unavailable"); + return; + } + const auto colorBytes = AgcDriver::Graphics::MultisampledColorBytes(DecodeTarget(source)); + Require(colorBytes <= source.bytes && source.bytes - colorBytes >= BlockBytes, "the native-index fixture has no neighboring block"); + const auto neighbor = source.Address() + colorBytes; + AgcDriver::Graphics::RequireNoMultisampleTarget(neighbor, 12u, "an index neighbor control"); + std::string guarded; + try { + AgcDriver::Graphics::RequireNoMultisampleTarget(source.Address(), 48u, "a draw-input control"); + } catch (const std::exception& error) { + guarded = error.what(); + } + Require(guarded.find("whose samples are only on the host") != std::string::npos, "the draw-input source is not guarded as a native target"); + { + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + const auto* native = AgcDriver::Graphics::HostImportFor(context, source.Address(), 12u); + const auto* adjacent = AgcDriver::Graphics::HostImportFor(context, neighbor, 12u); + Require(native != nullptr && native == adjacent, "the native indices and their neighbor do not share a host import"); + } + const auto state = TargetState(DecodeTarget(destination)); + std::uint32_t missed = 0; + for (const std::uint32_t indexSize : {2u, 4u}) { + if (indexSize == 2u) { + const std::array indices{0u, 1u, 2u}; + std::memcpy(source.data + colorBytes, indices.data(), sizeof(indices)); + } else { + const std::array indices{0u, 1u, 2u}; + std::memcpy(source.data + colorBytes, indices.data(), sizeof(indices)); + } + Draw(device, state, neighbor, indexSize); + device.WaitIdle(); + CheckSamples(state, LoadSamples(device, destination), {}); + for (const bool edge : {false, true}) { + const auto address = edge ? neighbor - indexSize : source.Address(); + std::string refusal; + try { + Draw(device, state, address, indexSize); + device.WaitIdle(); + } catch (const std::exception& error) { + refusal = error.what(); + } + const bool refused = refusal.find("whose samples are only on the host") != std::string::npos; + std::printf("native index %u %s: refused=%u neighbor=passed detail=%s\n", indexSize * 8u, edge ? "edge" : "base", refused ? 1u : 0u, refusal.c_str()); + Require(refusal.empty() || refused, "the native index input failed for another reason: " + refusal); + missed += refused ? 0u : 1u; + } + } + CheckUniform(LoadSamples(device, source), Ones, "after native draw-input alias attempts"); + std::puts("native index controls: shared-import=1 source-samples=1024 neighbor-triangle=passed"); + Require(missed == 0, "native indices missed " + std::to_string(missed) + " resident-image refusals"); +} + void StoreWords(const Block& block, std::uint64_t bytes, std::uint32_t word) { for (std::uint64_t offset = 0; offset < bytes; offset += sizeof(word)) std::memcpy(block.data + offset, &word, sizeof(word)); } @@ -315,7 +382,7 @@ void RequireRefused(AgcDriver::VulkanDevice& device, const std::array& device; + ~ReleaseIndexDevice() { device.reset(); } + } releaseIndexDevice{device}; + CheckNativeIndices(*device, block, destination); return 0; } catch (const std::exception& error) { std::cerr << error.what() << '\n'; diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index e7c93807bf..3262f2531a 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -111,7 +111,7 @@ - [Multisampled color fills](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) (libSceAgcDriver) - a multisampled target's samples are on the host, so a fill of its color memory is applied at its next use: a uniform fill over the whole of it clears the host image to that value, and a pending CMASK fast clear takes precedence over the fill and over a fill that would throw. Its color memory is a single mip and slice of addrlib's block dimensions for its sample count (ComputeThinBlockDimension). A fill over part of it, a fill reaching the start of a target whose footprint is not computed (linear), and a fill value with no exact clear value in the format (sRGB channels other than 0 and 1) throw at the next use. A fill before the target's first use is not seen (the host image starts at 0), nor are copies or shader writes into its color memory. A uniform compute fill over exactly the whole color memory with an exact clear value is not stored into guest memory, since only the host image holds the samples. A target is retired only when its whole guarded range is unmapped or its allocation removed, so if a title reuses that memory for other data while it stays mapped, a read the guard does not check (a buffer read through a V# or a pointer) sees the bytes from before the fill. - [Multisampled texture no draw rendered to](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) (libSceAgcDriver) - sampling a multisampled texture whose surface no draw rendered to creates the host target from the T# and clears it to the color a single 32-bit value repeated over the whole footprint encodes, padding up to whole tile blocks included. Inferred, not measured on the console: that the samples of such a surface are the guest color memory, as a title that clears its canvas on the CPU (ps5-opengl `ps5_clear_msaa4_color`) relies on. Memory that is not one repeated value, tile modes other than Standard4KB, Standard64KB and RenderTarget, unmapped memory and values with no exact clear color throw. CPU writes to the surface after its host target exists are not seen by the host. - [Multisampled targets in CB passes](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a CB eliminate or DCC pass over a multisampled target throws, since its texels are on the host and only a CB resolve and an FMASK decompress (`CB_COLOR_CONTROL` mode 5) are modeled. The FMASK decompress only applies the pending CMASK fast clear to the host image; it requires a CMASK address, and the target's DCC and FMASK contents are not interpreted. DCC over a multisampled target is accepted only in that pass and throws everywhere else. A CB resolve into a fast-clear destination throws: the resolve writes the destination's texels but not its CMASK, which would still read as cleared. -- [Multisampled target guest access](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) (libSceAgcDriver) - the memory a multisampled color target throws on (CPU read, DMA copy, texture or storage image read) uses fragment-aware 64 KiB render-target blocks. [AMD Addrlib's GFX10 thin-block rule](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/core/addrlib2.cpp#L1680-L1712) gives the block width and height from element bytes and fragment count; its [surface-size calculation](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/gfx10/gfx10addrlib.cpp#L4000-L4005) rounds both extents to those blocks and multiplies by element bytes and fragments. Thus a 1920x1080 4x RGBA8 surface occupies 0x1fe0000 bytes, leaving the next allocation at +0x2000000 outside the guard. The 2x and 8x block orientations differ from treating the fragments as a larger element. Console address-layout equivalence has not been measured. Other tile modes retain the conservative `max(CB bytes * samples, width * height * element bytes * samples)` bound, which may still reject a distinct allocation. Targets are kept per device for the device's lifetime, one per surface address, format, extent and sample count, and are not evicted. +- [Multisampled target guest access](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) (libSceAgcDriver) - the memory a multisampled color target throws on (CPU read, index buffer read, DMA copy, texture or storage image read) uses fragment-aware 64 KiB render-target blocks. [AMD Addrlib's GFX10 thin-block rule](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/core/addrlib2.cpp#L1680-L1712) gives the block width and height from element bytes and fragment count; its [surface-size calculation](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/gfx10/gfx10addrlib.cpp#L4000-L4005) rounds both extents to those blocks and multiplies by element bytes and fragments. Thus a 1920x1080 4x RGBA8 surface occupies 0x1fe0000 bytes, leaving the next allocation at +0x2000000 outside the guard. The 2x and 8x block orientations differ from treating the fragments as a larger element. Console address-layout equivalence has not been measured. Other tile modes retain the conservative `max(CB bytes * samples, width * height * element bytes * samples)` bound, which may still reject a distinct allocation. Targets are kept per device for the device's lifetime, one per surface address, format, extent and sample count, and are not evicted. - [CB resolve sample count](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a draw with CB_COLOR_CONTROL in resolve mode is a CB resolve when either PA_SC_AA_CONFIG or CB_COLOR0_ATTRIB.NUM_SAMPLES is multisampled, and takes its sample count from CB0. Inferred, not from documentation: PPSA36082 resolves its 4x targets with no sample count in PA_SC_AA_CONFIG, and radeonsi sets PA_SC_AA_CONFIG to 0 for a multisampled framebuffer when rasterizer multisampling and line smoothing are off. - [Custom MSAA sample locations](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) support a uniform 1x1 pattern for 2, 4 or 8 samples through VK_EXT_sample_locations. Patterns varying across the guest's 2x2 pixel grid use a 2x2 sample location grid, anchored at the framebuffer origin like the guest's while the window offset is zero, on devices whose `maxSampleLocationGridSize` for the sample count is a multiple of 2x2, and throw on others. The RTX 5070 reports 2x4 for 2 samples, 2x2 for 4 and 1x2 for 8; an AMD Radeon integrated GPU reports 2x2 for each. Devices without the required sample count, four subpixel bits or the full guest coordinate range refuse custom patterns. Depth images remain in GENERAL between draws, and render passes cannot continue across pattern changes. Guest offsets retain the existing decoder's signed four-bit, sixteenth-pixel interpretation; PS5 coverage and interpolation equivalence have not been measured on hardware. - [DCC display buffers](../../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) are presented only when all keys of the console's DCC extent agree on uncompressed or a fast-clear code, padding keys included; other keys, and register-clear keys over a pending image, throw. From 46d49131e777b8cd80d06c6581603aa1e8542f00 Mon Sep 17 00:00:00 2001 From: Felipe Arce Date: Wed, 14 Oct 2026 04:19:45 -0300 Subject: [PATCH 0419/1000] feat(libkernel): support EVFILT_READ on the read end of a pipe kevent threw for any descriptor that is not a guest socket. The read end of a pipe now gets the descriptor knote, following FreeBSD 11's filt_piperead: data is the number of buffered bytes and EV_EOF is set once the write end is closed. GuestSockets::Ready falls back to the new PipeReadState for a descriptor that is not a socket, and WaitAny waits on the pipe with ppoll on Linux or in 1 ms WSAPoll slices on Windows, only when the queue holds a pipe. On Windows the equeue waker now starts Winsock itself, since a queue can wait on a pipe before any socket exists. --- core/libs/prx/libkernel/Equeue/src/Kqueue.cpp | 10 +++-- .../Socket/include/SocketRuntime.hpp | 1 + core/libs/prx/libkernel/Socket/src/Udp.cpp | 45 ++++++++++++++++++- core/libs/tests/GuestKqueue.cpp | 41 ++++++++++++++++- docs/dev/debt/libkernel.md | 3 +- 5 files changed, 93 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libkernel/Equeue/src/Kqueue.cpp b/core/libs/prx/libkernel/Equeue/src/Kqueue.cpp index 9bd2979f19..ae13538695 100644 --- a/core/libs/prx/libkernel/Equeue/src/Kqueue.cpp +++ b/core/libs/prx/libkernel/Equeue/src/Kqueue.cpp @@ -135,14 +135,18 @@ int AddDescriptorKnote(const KernelEqueueRef& queue, KernelEvent& change, std::u } NotImplemented_nid_no_patch("kevent: EVFILT_READ on a kqueue descriptor"); } - if (!GuestSockets::IsOpen(descriptor)) { - NotImplemented_nid_no_patch(change.filter == EVFILT_READ ? "kevent: EVFILT_READ on a non-socket descriptor" : "kevent: EVFILT_WRITE on a non-socket descriptor"); + const bool socket = GuestSockets::IsOpen(descriptor); + std::int64_t data = 0; + bool eof = false; + const bool pipe = !socket && change.filter == EVFILT_READ && GuestSockets::PipeReadState(descriptor, &data, &eof); + if (!socket && !pipe) { + NotImplemented_nid_no_patch(change.filter == EVFILT_READ ? "kevent: EVFILT_READ on a descriptor that is neither a socket nor the read end of a pipe" : "kevent: EVFILT_WRITE on a non-socket descriptor"); } if ((change.fflags & NOTE_LOWAT) != 0) { NotImplemented_nid_no_patch("kevent: NOTE_LOWAT"); } if ((flags & EV_CLEAR) != 0) { - NotImplemented_nid_no_patch("kevent: EV_CLEAR on a socket"); + NotImplemented_nid_no_patch(pipe ? "kevent: EV_CLEAR on a pipe" : "kevent: EV_CLEAR on a socket"); } auto event = EqueueDescriptorEvent(descriptor, change.filter, flags, change.udata); change.fflags = 0; diff --git a/core/libs/prx/libkernel/Socket/include/SocketRuntime.hpp b/core/libs/prx/libkernel/Socket/include/SocketRuntime.hpp index a39295ecd6..c5425e4350 100644 --- a/core/libs/prx/libkernel/Socket/include/SocketRuntime.hpp +++ b/core/libs/prx/libkernel/Socket/include/SocketRuntime.hpp @@ -31,6 +31,7 @@ struct Interest { std::uintptr_t CurrentWaker(); void Wake(std::uintptr_t waker); bool WaitAny(const std::vector& interests, std::uint64_t deadlineNanos); +bool PipeReadState(int descriptor, std::int64_t* data, bool* eof); std::int64_t Readv(int descriptor, const Iovec* iov, int iovcnt); std::int64_t Writev(int descriptor, const Iovec* iov, int iovcnt); } diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index 7588023bb7..b1f1fa4420 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -122,6 +122,10 @@ struct Socket { struct Waker { NativeSocket value = Invalid; Waker() { +#ifdef _WIN32 + static const int startup = [] { WSADATA data{}; return WSAStartup(MAKEWORD(2, 2), &data); }(); + if (startup) throw std::runtime_error("equeue waker: WSAStartup failed, error " + std::to_string(startup)); +#endif value = ::socket(AF_INET, SOCK_DGRAM, 0); sockaddr_in address{}; address.sin_family = AF_INET; @@ -335,6 +339,7 @@ bool GuestSockets::WaitAny(const std::vector& interests, std::uint64_t std::vector entries; constexpr SHORT ReadMask = POLLRDNORM; constexpr SHORT WriteMask = POLLWRNORM; + bool pipes = false; #else std::vector entries; constexpr short ReadMask = POLLIN; @@ -344,7 +349,14 @@ bool GuestSockets::WaitAny(const std::vector& interests, std::uint64_t std::lock_guard lock(socketsMutex); for (const auto& interest : interests) { const auto found = sockets.find(interest.descriptor); - if (found == sockets.end()) continue; + if (found == sockets.end()) { +#ifdef _WIN32 + pipes = true; +#else + entries.push_back({interest.descriptor, ReadMask, 0}); +#endif + continue; + } held.push_back(found->second); entries.push_back({found->second->value, interest.write ? WriteMask : ReadMask, 0}); } @@ -362,6 +374,7 @@ bool GuestSockets::WaitAny(const std::vector& interests, std::uint64_t if (remaining < 1000000) return false; timeout = static_cast(std::min(remaining / 1000000, INT_MAX)); } + if (pipes && (timeout < 0 || timeout > 1)) timeout = 1; if (WSAPoll(entries.data(), static_cast(entries.size()), timeout) == SOCKET_ERROR) throw std::runtime_error("equeue wait: WSAPoll failed, guest errno " + std::to_string(NativeError())); #else @@ -701,7 +714,7 @@ bool GuestSockets::Ready(int descriptor, bool write, std::int64_t* data, bool* e { std::lock_guard lock(socketsMutex); const auto found = sockets.find(descriptor); - if (found == sockets.end()) return false; + if (found == sockets.end()) return !write && PipeReadState(descriptor, data, eof) && (*data > 0 || *eof); socket = found->second; } #ifdef _WIN32 @@ -730,6 +743,34 @@ bool GuestSockets::Ready(int descriptor, bool write, std::int64_t* data, bool* e return true; } +bool GuestSockets::PipeReadState(int descriptor, std::int64_t* data, bool* eof) { +#ifdef _WIN32 + const auto handle = reinterpret_cast(::_get_osfhandle(descriptor)); + if (handle == INVALID_HANDLE_VALUE || GetFileType(handle) != FILE_TYPE_PIPE) return false; + static const auto query = reinterpret_cast( + reinterpret_cast(GetProcAddress(GetModuleHandleW(L"ntdll.dll"), "NtQueryInformationFile"))); + if (!query) throw std::runtime_error("kevent: NtQueryInformationFile is unavailable"); + PipeIoStatus status{}; + ACCESS_MASK access = 0; + PipeLocalInformation pipe{}; + if (query(handle, &status, &access, sizeof(access), FileAccessInformationClass) < 0 || + query(handle, &status, &pipe, sizeof(pipe), FilePipeLocalInformationClass) < 0) + throw std::runtime_error("kevent: querying pipe descriptor " + std::to_string(descriptor) + " failed"); + if (!(access & FILE_READ_DATA)) return false; + *data = pipe.readDataAvailable; + *eof = pipe.state == PipeClosingState; +#else + if (!IsPipe(descriptor, 0) || (::fcntl(descriptor, F_GETFL) & O_ACCMODE) == O_WRONLY) return false; + pollfd entry{descriptor, POLLIN, 0}; + int available = 0; + if (::poll(&entry, 1, 0) < 0 || ::ioctl(descriptor, FIONREAD, &available) != 0) + throw std::runtime_error("kevent: querying pipe descriptor " + std::to_string(descriptor) + " failed"); + *data = available; + *eof = (entry.revents & POLLHUP) != 0; +#endif + return true; +} + int GuestSockets::Poll(KernelSocketPoll::Entry* entries, int count, int timeoutMilliseconds) { std::vector> held; std::vector native; diff --git a/core/libs/tests/GuestKqueue.cpp b/core/libs/tests/GuestKqueue.cpp index f0b46d539c..593c82d059 100644 --- a/core/libs/tests/GuestKqueue.cpp +++ b/core/libs/tests/GuestKqueue.cpp @@ -23,6 +23,7 @@ int APS5_VABI pipe_nid_postfix(int*); int APS5_VABI socketpair_nid_postfix(int, int, int, int*); std::int64_t APS5_VABI send_nid_postfix(int, const void*, std::uint64_t, int); std::int64_t APS5_VABI recv_nid_postfix(int, void*, std::uint64_t, int); +std::int64_t APS5_VABI read_nid_postfix(int, void*, std::uint64_t); int APS5_VABI open_nid_postfix(const char*, int, int); int APS5_VABI mkdir_nid_postfix(const char*, unsigned short); int APS5_VABI rmdir_nid_postfix(const char*); @@ -60,6 +61,7 @@ static constexpr std::uint16_t EvReceipt = 0x0040; static constexpr std::uint16_t EvDispatch = 0x0080; static constexpr std::uint16_t EvForceoneshot = 0x0100; static constexpr std::uint16_t EvError = 0x4000; +static constexpr std::uint16_t EvEof = 0x8000; static constexpr std::uint32_t NoteTrigger = 0x01000000; static constexpr std::uint32_t NoteFfcopy = 0xc0000000; static constexpr std::uint32_t NoteFfor = 0x80000000; @@ -208,6 +210,40 @@ static void UserEvents() { Require(close_nid_postfix(kq) == 0); } +static void PipeRead() { + const int kq = kqueue_nid_postfix(); + int pipe[2]; + Require(pipe_nid_postfix(pipe) == 0); + const auto readEnd = static_cast(pipe[0]); + int marker = 0; + KernelEvent events[4]{}; + Require(Apply(kq, Change(readEnd, EvfiltRead, EvAdd, 0, 0, &marker)) == 0); + Require(Poll(kq, events, 4) == 0); + Require(write_nid_postfix(pipe[1], "abc", 3) == 3); + Require(Poll(kq, events, 4) == 1); + Require(events[0].ident == readEnd && events[0].filter == EvfiltRead && events[0].data == 3 && events[0].udata == &marker && (events[0].flags & EvEof) == 0); + char buffer[4]{}; + Require(read_nid_postfix(pipe[0], buffer, sizeof(buffer)) == 3); + Require(Poll(kq, events, 4) == 0); + std::thread writer([&pipe] { + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + Require(write_nid_postfix(pipe[1], "d", 1) == 1); + }); + const KernelTimespec limit{5, 0}; + const auto start = std::chrono::steady_clock::now(); + Require(kevent_nid_postfix(kq, nullptr, 0, events, 4, &limit) == 1); + const auto waited = std::chrono::steady_clock::now() - start; + writer.join(); + Require(waited >= std::chrono::milliseconds(40) && waited < std::chrono::seconds(4)); + Require(events[0].ident == readEnd && events[0].data == 1); + Require(close_nid_postfix(pipe[1]) == 0); + Require(Poll(kq, events, 4) == 1 && events[0].data == 1 && (events[0].flags & EvEof) != 0); + Require(read_nid_postfix(pipe[0], buffer, sizeof(buffer)) == 1); + Require(Poll(kq, events, 4) == 1 && events[0].data == 0 && (events[0].flags & EvEof) != 0); + Require(close_nid_postfix(pipe[0]) == 0); + Require(close_nid_postfix(kq) == 0); +} + static void SocketPair() { const int kq = kqueue_nid_postfix(); int pair[2]; @@ -306,7 +342,9 @@ static void Errors() { RequireFailure(kevent_nid_postfix(kq, nullptr, 0, events, -1, nullptr), GuestEinval); int pipe[2]; Require(pipe_nid_postfix(pipe) == 0); - Require(Throws([kq, &pipe] { Apply(kq, Change(static_cast(pipe[0]), EvfiltRead, EvAdd)); })); + Require(Throws([kq, &pipe] { Apply(kq, Change(static_cast(pipe[1]), EvfiltRead, EvAdd)); })); + Require(Throws([kq, &pipe] { Apply(kq, Change(static_cast(pipe[1]), EvfiltWrite, EvAdd)); })); + Require(Throws([kq, &pipe] { Apply(kq, Change(static_cast(pipe[0]), EvfiltRead, EvAdd | EvClear)); })); Require(close_nid_postfix(pipe[0]) == 0 && close_nid_postfix(pipe[1]) == 0); const KernelEvent skipped = Change(1, 0, EvAdd); const KernelTimespec zero{0, 0}; @@ -584,6 +622,7 @@ static void Timeout() { int main() { CreateAndClose(); Timers(); + PipeRead(); UserEvents(); SocketPair(); DisableAndDispatch(); diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 5396a00c3a..e7e3c7eba9 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -108,7 +108,7 @@ - [sceKernelMprotect, sceKernelMtypeprotect](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (libkernel) over a range with a gap that is not registered in the allocation tracker protect the registered parts and skip the gap, and return 0, as FreeBSD 11.4's [`vm_map_protect`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/vm/vm_map.c) does (it has no check for holes); a range whose start is unmapped (also when later pages are mapped) returns `SCE_KERNEL_ERROR_EINVAL` (`mprotect`: `EINVAL`) because an existing test pins that for an unmapped start; FreeBSD answers 0 there too (`vm_map_protect` takes the next entry), so this errno is not taken from the source and was not changed. `sceKernelMprotect` threw for a gap before, which ended the title. Every part is checked for a GPU or image pin before the first one is protected; the parts are then protected one after the other, so a host protection failure in a later part leaves the earlier parts changed (FreeBSD's `vm_map_protect` cannot fail in its second pass). The behaviour on the console is not known. None of this was measured on a console. - [sceKernelMprotect protection bits](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - bits outside `0x3F7` are dropped instead of refused, as FreeBSD 11's [`sys_mprotect`](https://github.com/freebsd/freebsd-src/blob/34bd34750187bae02a0086402c78c031c59dd8b3/sys/vm/vm_mmap.c#L606) does with `VM_PROT_ALL`. Onimusha: Way of the Sword (PPSA27836 01.000.001) maps direct memory with `0xf2` and then protects it with `0xfffffff2`. Not measured on a console; the map calls and `sceKernelMtypeprotect` still refuse undefined bits - [JIT shared memory](../../../core/libs/prx/libkernel/DirectMemory/Jit.cpp) (libkernel) - `sceKernelJitCreateSharedMemory`, `sceKernelJitCreateAliasOfSharedMemory` and `sceKernelJitMapSharedMemory` take the signatures of [Orbital](https://github.com/AlexAltea/orbital)'s `tools/ida/db_types.json` (name, length, maximum protection, descriptor out; descriptor, maximum protection, descriptor out; descriptor, protection, address in and out), which ps4sdk, ps5-jar-loader and the call sites in RetroArch PPSA99169, XPSemu PPSA97358 and VLC PPSA85300 match; SharpProspero declares the alias as returning the descriptor. The object is a host anonymous file shared by its descriptors (a `memfd` on Linux, a delete-on-close temporary file mapped as an executable section on Windows), the alias is a duplicate descriptor, and every map maps the whole object, so writes through one view show through the others; a non-null address is a hint, as in `mmap` without `MAP_FIXED`. Not measured on the console: a length that is zero or not a multiple of 16 KiB and protection bits other than CPU read, write and execute are `EINVAL`; an alias or a map asking for more than the descriptor's maximum protection (write and execute counting as read) is `EACCES`, as FreeBSD 11's `vm_map_protect` treats a protection past `max_protection`; a descriptor that is not a JIT object is `EBADF`. The name is ignored, the objects do not count against the flexible memory budget, `mprotect` of a view is not limited to the maximum protection, and `mmap` of the descriptors is unsupported, as for `shm_open`. On Linux the `memfd` is created without `MFD_EXEC`, which Linux 6.3 added and older kernels reject with `EINVAL`, and relies on the protection of the mapping: the flag only sets the file's execute permission bits, and the direct memory `memfd` is mapped executable the same way; a host with `vm.memfd_noexec` set to 2 refuses the creation and the call throws -- [kqueue and kevent](../../../core/libs/prx/libkernel/Equeue/src/Kqueue.cpp) (libkernel) follow FreeBSD 11's [`kern_event.c`](https://github.com/freebsd/freebsd-src/blob/34bd34750187bae02a0086402c78c031c59dd8b3/sys/kern/kern_event.c) (`kqueue_kevent`, `kqueue_register`, `kqueue_scan`, the timer and user filters) over the event queue; nothing is measured on a console. The descriptor is a host null device registered as the kqueue: `close` and `sceKernelClose` close the queue and `dup`/`dup2` of it throw, but `read`, `write` and `fstat` act on the null device instead of failing with `EOPNOTSUPP` or reporting `S_IFIFO`. Read and write events are supported on guest sockets only, plus the timer, user and [vnode](../../../core/libs/prx/libkernel/Equeue/src/FileEvent.cpp) filters; other filters, `NOTE_LOWAT`, `EV_CLEAR` on a socket, `EVFILT_READ` on another kqueue, a `NOTE_NSECONDS` period above one second with a fraction (11.0 converts the fraction with the microsecond macro) and any change to an existing timer (11.0 keeps the armed expiry and applies the new period after it, 11.4's `filt_timertouch` restarts it) throw. `EV_DISABLE`, `EV_DISPATCH` and `EV_FORCEONESHOT` follow kern_event.c: a disabled knote stays registered, stops polling or firing and is not reported, a trigger raised while it is disabled is kept and reported after `EV_ENABLE`, where `knote` would drop it, `EV_DISPATCH` reports once and then disables the knote, and `EV_FORCEONESHOT` is ignored on a new knote while on an existing one it sets `EV_ONESHOT` and fires the knote; a negative `nevents` fails with `EINVAL`. A new user event keeps its `fflags` unmasked and is not triggered by `NOTE_TRIGGER`, because `filt_userattach` reads the already cleared `kn_fflags`. The timer limit `kq_calloutmax` (`ENOMEM`) is not enforced, and closing a kqueue wakes a thread waiting on it in `kevent` with `EBADF`, where `kqueue_drain` makes the close wait for that thread. A level-triggered event is reported once per call and moved behind the other events, as in `kqueue_scan`; the `sceKernel*Equeue` calls keep their own collection loop +- [kqueue and kevent](../../../core/libs/prx/libkernel/Equeue/src/Kqueue.cpp) (libkernel) follow FreeBSD 11's [`kern_event.c`](https://github.com/freebsd/freebsd-src/blob/34bd34750187bae02a0086402c78c031c59dd8b3/sys/kern/kern_event.c) (`kqueue_kevent`, `kqueue_register`, `kqueue_scan`, the timer and user filters) over the event queue; nothing is measured on a console. The descriptor is a host null device registered as the kqueue: `close` and `sceKernelClose` close the queue and `dup`/`dup2` of it throw, but `read`, `write` and `fstat` act on the null device instead of failing with `EOPNOTSUPP` or reporting `S_IFIFO`. Read and write events are supported on guest sockets only, plus `EVFILT_READ` on the read end of a pipe (see the kevent on a pipe entry) and the timer, user and [vnode](../../../core/libs/prx/libkernel/Equeue/src/FileEvent.cpp) filters; other filters, `NOTE_LOWAT`, `EV_CLEAR` on a socket or a pipe, `EVFILT_READ` on another kqueue, a `NOTE_NSECONDS` period above one second with a fraction (11.0 converts the fraction with the microsecond macro) and any change to an existing timer (11.0 keeps the armed expiry and applies the new period after it, 11.4's `filt_timertouch` restarts it) throw. `EV_DISABLE`, `EV_DISPATCH` and `EV_FORCEONESHOT` follow kern_event.c: a disabled knote stays registered, stops polling or firing and is not reported, a trigger raised while it is disabled is kept and reported after `EV_ENABLE`, where `knote` would drop it, `EV_DISPATCH` reports once and then disables the knote, and `EV_FORCEONESHOT` is ignored on a new knote while on an existing one it sets `EV_ONESHOT` and fires the knote; a negative `nevents` fails with `EINVAL`. A new user event keeps its `fflags` unmasked and is not triggered by `NOTE_TRIGGER`, because `filt_userattach` reads the already cleared `kn_fflags`. The timer limit `kq_calloutmax` (`ENOMEM`) is not enforced, and closing a kqueue wakes a thread waiting on it in `kevent` with `EBADF`, where `kqueue_drain` makes the close wait for that thread. A level-triggered event is reported once per call and moved behind the other events, as in `kqueue_scan`; the `sceKernel*Equeue` calls keep their own collection loop - [EVFILT_VNODE, sceKernelAddFileEvent and sceKernelDeleteFileEvent](../../../core/libs/prx/libkernel/Equeue/src/FileEvent.cpp) (libkernel) watch the host file or directory behind the descriptor and turn host changes into FreeBSD 11 vnode hints, which [`filt_vfsvnode`](https://github.com/freebsd/freebsd-src/blob/34bd34750187bae02a0086402c78c031c59dd8b3/sys/kern/vfs_subr.c#L5032) adds whole when they share a bit with the requested `fflags`: a write that grows a file is `NOTE_WRITE | NOTE_EXTEND`, another write `NOTE_WRITE`, a shrinking truncate or an attribute change `NOTE_ATTRIB`, a higher link count `NOTE_LINK`, a lower one or a pending delete `NOTE_DELETE`, a new path `NOTE_RENAME`; in a directory, an entry created, removed or renamed is `NOTE_WRITE`, with `NOTE_LINK` for a subdirectory, and on Linux an entry moved from or to another directory adds `NOTE_EXTEND`; inotify queues the two halves of a rename one after the other, so a watcher woken by the first can read it alone, and a half read without the other is classified only after a `getdents64` on the watched directory, which takes the directory lock that `rename` holds until both halves are queued ([`do_renameat2`](https://github.com/torvalds/linux/blob/v6.8/fs/namei.c#L4975) around `vfs_rename` and its `fsnotify_move`, [`iterate_dir`](https://github.com/torvalds/linux/blob/v6.8/fs/readdir.c#L103)), an ordering taken from the kernel source, not from inotify(7), which promises none; a directory registered with `NOTE_EXTEND` on Windows is accepted, as `vfs_kqfilter` does not check it, but never receives it, because the directory handle cannot tell a move between directories from a removal plus an addition. FreeBSD's hints depend on the filesystem and the console's filesystems are not known: a growing `ftruncate` reports `NOTE_WRITE | NOTE_EXTEND` where `vop_setattr_post` reports `NOTE_ATTRIB`, and on Windows `utimes` on a file reports `NOTE_WRITE`. The three Windows notifications are told apart in the error, which names the `parent`, `entries` or `subdirectories` watch, the path it was given and `GetLastError`, but a `FindFirstChangeNotification` that fails still throws rather than returning an error to the guest: the EVFILT_VNODE attach path is [`vfs_kqfilter`](https://github.com/freebsd/freebsd-src/blob/34bd34750187bae02a0086402c78c031c59dd8b3/sys/kern/vfs_subr.c#L4936), which returns `EINVAL`, `ENOMEM` or 0 after `fget` has turned a descriptor that is not open into `EBADF`, and consults no directory permission, so no FreeBSD errno stands for a host that refuses to notify, and what the console does on such a host is not measured. Only the `parent` watch can fail this way in practice: `entries` and `subdirectories` need the `FILE_LIST_DIRECTORY` that opening the watched directory already took. The parent handle produces no hint of its own, as `RunHostWatch` only calls `FindNextChangeNotification` on it, so a later change may leave the watch on its 50 ms poll instead of throwing; that is not done here. Windows uses `FindFirstChangeNotification` on the parent directory (and on a watched directory itself) and compares `GetFileInformationByHandleEx` snapshots at least every 50 ms, because a write through an open handle is reported only when the handle is flushed or closed, so file hints can arrive 50 ms late, and a file deleted while watched, which the POSIX delete of Windows 10 1903 moves into `$Extend\$Deleted` until its last handle closes, reports `NOTE_DELETE` without `NOTE_RENAME` and keeps its handle without a parent directory watch, as that directory cannot be watched; Linux uses inotify on the file and is not run. `NOTE_REVOKE`, `NOTE_OPEN`, `NOTE_CLOSE`, `NOTE_CLOSE_WRITE`, `NOTE_READ`, unknown bits and descriptors other than files, directories, pipes and sockets throw. `sceKernelAddFileEvent(eq, fd, watch, udata)` and `sceKernelDeleteFileEvent(eq, fd)` take the [OpenOrbis](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain/blob/0a1aaf9d/include/orbis/libkernel.h#L35) and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0/src/SharpProspero/Interop/Kernel/KernelEqueue.cs#L164) signatures; `watch` is taken as the `fflags` and the event is added with `EV_CLEAR`, neither confirmed on a title (feat(libkernel): implement kevent EV_DISABLE, EV_DISPATCH and EV_FORCEONESHOT) (fix(libkernel): follow kqueue_register for EV_FORCEONESHOT and modify flags) - [sem_timedwait](../../../core/libs/prx/libkernel/Pthread/Posix/Sem.cpp) (libkernel) consumes an available count before checking the timeout and waits indefinitely for a null timeout, following FreeBSD 11's `sem_timedwait`. Console behavior for null and invalid timeouts when a count is available is unverified. - [tcsendbreak, tcdrain, tcflush, tcflow, tcgetpgrp, tcsetpgrp](../../../core/libs/prx/libkernel/File/src/Terminal.cpp) (libkernel) follow FreeBSD 11.4's [`termios.c`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/gen/termios.c): `tcflush` answers `EINVAL` for a queue other than `TCIFLUSH`, `TCOFLUSH` or `TCIOFLUSH` and `tcflow` for an action outside `TCOOFF`..`TCION` before the descriptor is looked at, and every other call fails as its terminal ioctl does on a descriptor that is not a terminal, with the errors `tcgetattr` gives, so `tcsendbreak` never reaches its 0.4 s pause. FreeBSD's `pipe_ioctl` answers `TIOCGPGRP` and `TIOCSPGRP` on a pipe or FIFO with the descriptor's SIGIO owner, which is not modelled, so `tcgetpgrp` and `tcsetpgrp` throw on a host pipe or FIFO above descriptor 2; descriptors 0 to 2 are taken as non-terminals that are not pipes, whatever the host attaches to them, so a pipe the guest creates on a freed descriptor 0 to 2 gets `ENOTTY` there. The PS5 `libkernel` is assumed to use this FreeBSD code, and `tcdrain` is not a cancellation point here @@ -168,6 +168,7 @@ - [Socket descriptor numbers](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel, libSceNet) - `socket`, `accept`, `socketpair`, `dup`, `sceNetSocket` and `sceNetAccept` take the lowest free number of the host descriptor table that files use, as FreeBSD 11.4 `fdalloc` takes the lowest free entry of the one process table (`kern_descrip.c`), so every number stays below `FD_SETSIZE` (1024) while fewer than 1024 descriptors are open. The number holds a zero-access `NUL` handle on Windows and a duplicate of the host socket on Linux, and the socket tables map it to the socket. The host table also holds the runtime's own files, so the numbers differ from the console's; the first number `socket` and `sceNetSocket` return on a console and its `getdtablesize` are not measured. Paths that bypass the socket tables see the placeholder: libc `fdopen` on a socket opens it, and libkernel `close` of a libSceNet socket releases the number but leaves the libSceNet socket open. Palworld (PPSA23126), The Mound (PPSA26223) and Dying Light 2 (PPSA02262) set socket bits in a 1024-bit `fd_set` - [fstat on a guest socket](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) fills the fields FreeBSD 11's soo_stat and pru_sense_null fill and zeroes the rest: S_IFSOCK, the read bits while the receive side is open or data is queued, the write bits while the send side is open, st_size and st_blksize. Only this process's shutdown closes a side here; a peer's shutdown, close or reset does not clear the bits as FreeBSD's socantrcvmore and socantsendmore do. st_size is the host's FIONREAD, which for a Linux datagram socket is the size of the next datagram where FreeBSD counts every queued byte, and st_blksize is the host's SO_SNDBUF, which Linux reports as twice the size that was set. A socketpair end reports st_dev and st_ino 0, where FreeBSD's uipc_sense reports NODEV and a per-socket number - [poll](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) waits on guest sockets and pipes, through `WSAPoll` on Windows and `poll` on Linux; the `pollfd` layout and flag values are FreeBSD's. A libSceNet socket joins the same host wait through the native socket libSceNet lends to libkernel. `WSAPoll` rejects `POLLPRI`, so on Windows a socket's `POLLPRI` is asked as `POLLRDBAND` and `POLLIN` as `POLLRDNORM`; Winsock reports urgent data until it is read with `MSG_OOB`, where FreeBSD 11.4's `sopoll_generic` stops once the normal stream passes the mark. Guest `send` and `recv` refuse `MSG_OOB` with `EOPNOTSUPP`, so urgent data can only come from an outside peer and Windows keeps reporting `POLLPRI` after the stream passes the mark. A pipe follows FreeBSD's `pipe_poll`: the read end is readable when data is buffered, either end reports `POLLHUP` once the other end is closed, with the requested `POLLIN` and `POLLRDNORM` bits and without `POLLOUT`, which `pollscan` clears, and the write end is writable when `PIPE_BUF` (512) bytes are free on Windows, or when the host kernel says so on Linux. `POLLOUT` on a read end throws: FreeBSD opens both ends for reading and writing and reports it writable, and host pipes are one-way. Windows pipes cannot be waited on, so while a pipe is polled `poll` checks it with `NtQueryInformationFile` and waits on the sockets in slices of 1 ms. A closed descriptor reports `POLLNVAL`, and an open descriptor that is neither a guest socket, a libSceNet socket nor a pipe (files, stdio) throws, as do event bits other than `POLLIN`, `POLLPRI`, `POLLOUT`, `POLLRDNORM`, `POLLRDBAND` and `POLLWRBAND` (the `POLLERR`, `POLLHUP` and `POLLNVAL` bits are ignored in `events`), an infinite timeout with no socket or pipe to wait on. A closed socket descriptor reports `POLLNVAL`; the console's limit on the descriptor count is unknown and not checked +- [kevent on a pipe](../../../core/libs/prx/libkernel/Equeue/src/Kqueue.cpp) (libkernel) - `EVFILT_READ` on the read end of a pipe follows FreeBSD 11's [`filt_piperead`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/sys_pipe.c#L1787-L1806): `data` is the number of buffered bytes, and `EV_EOF` is set once the write end is closed. Windows pipes cannot be waited on, so while a kqueue waits on a pipe it checks it with `NtQueryInformationFile` and waits on its sockets in slices of 1 ms; a wait with no pipe is not sliced. `EVFILT_READ` on the write end of a pipe throws: FreeBSD's `pipe` opens both ends for reading and writing ([`kern_pipe`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/sys_pipe.c#L422)), and host pipes are one-way. `EVFILT_WRITE` on a pipe and `NOTE_LOWAT` on a pipe, which `filt_piperead` ignores, throw too. - [Socket error numbers](../../../core/libs/prx/libkernel/Socket/include/SocketError.hpp) (libkernel) - a host socket error becomes the FreeBSD 11 `errno.h` number of the same name; the numbers are not checked on the console. Winsock `WSAESHUTDOWN` becomes `EPIPE`, as a send after `shutdown` does on FreeBSD, and a host error with no FreeBSD socket counterpart becomes `EIO`. A nonblocking `connect` in progress reports `EINPROGRESS` on Linux but `EAGAIN` on Windows (`WSAEWOULDBLOCK`), where FreeBSD reports `EINPROGRESS`. On Windows a refused nonblocking `connect` is never reported (`SO_ERROR` is unsupported), so a repeated one keeps reporting `EALREADY` where FreeBSD reports `ECONNREFUSED`. A receive after `shutdown(SHUT_RD)` reports `EPIPE` on Windows where FreeBSD returns 0 - [getsockopt, setsockopt](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) on a guest socket handle the SOL_SOCKET level and `TCP_NODELAY`, after FreeBSD 11.4's [`sosetopt` and `sogetopt`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c): `SO_REUSEADDR`, `SO_KEEPALIVE`, `SO_DONTROUTE`, `SO_BROADCAST`, `SO_OOBINLINE` and `SO_LINGER` act on the host socket, and `SO_ERROR`, `SO_TYPE`, `SO_ACCEPTCONN`, `SO_SNDBUF` and `SO_RCVBUF` are read. A boolean option reads back as its own bit, as FreeBSD returns `so_options & option`. Winsock does not clear a socket's pending error when it is read, so on Windows the library reports a pending error once and shows it again only after a new `connect`; a second failure with the same code on the same socket without a `connect` in between would be hidden. `SO_SNDBUF` and `SO_RCVBUF` read the host's value, which is not the size FreeBSD reports (Linux doubles it). `SO_NOSIGPIPE` is kept per socket and reads back as its bit. On Linux a `send`, `sendto`, `sendmsg` or `write` on that socket passes `MSG_NOSIGNAL` to the host, as [`kern_sendit`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_syscalls.c#L890-L895) and [`soo_write`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/sys_socket.c#L147-L151) leave out the signal. An accepted socket takes it from its listener, as [`sonewconn`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L603) copies the options. `SO_NBIO` (`0x1200`) is the console's own option, not FreeBSD's. It switches the socket between blocking and non-blocking as `FIONBIO` does and reads back as 0 or 1, as `sceNetSetsockopt` and `sceNetGetsockopt` treat it here; what the console reads back for it is not measured. `TCP_NODELAY` (`IPPROTO_TCP`, 1) is set on the host socket and reads back as `TF_NODELAY` (4), as [`tcp_default_ctloutput`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/netinet/tcp_usrreq.c#L1860-L1861) returns it. The `IPPROTO_TCP` level on a datagram or `AF_UNIX` socket fails with `EINVAL`, as [`ip_ctloutput`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/netinet/ip_output.c#L943-L944) and [`uipc_ctloutput`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_usrreq.c#L1228-L1229) answer it, and an accepted socket takes `TCP_NODELAY` through the host. `SO_SNDTIMEO` and `SO_RCVTIMEO` follow [`sosetopt`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L2657-L2689) and [`sogetopt`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L2846-L2850). A 16-byte `timeval` with a negative field or 1000000 microseconds or more fails with `EDOM`, and more than `INT32_MAX` seconds becomes the longest wait. The value is kept and read back through FreeBSD's 32.32 fixed-point conversion ([`tvtosbt`, `sbttotv`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/sys/time.h#L178-L286)), so half a second reads back as 499999 microseconds and one microsecond as 0. A `recv`, `recvfrom`, `recvmsg`, `read` or `readv`, and a `send`, `sendto`, `sendmsg`, `write` or `writev`, on a blocking socket then waits that long for the host to report the socket readable or writable, and fails with `EWOULDBLOCK`; a call with `MSG_DONTWAIT` does not wait. An accepted socket takes both from its listener ([`sonewconn`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L629-L630)). The wait covers only the first transfer: once the socket is writable the host send runs without a limit, where FreeBSD stops a send that cannot finish and returns the bytes sent so far, and a receive whose data another thread takes between the wait and the host call blocks again. `sendfile` ignores the send timeout. The host wait has millisecond steps, so a shorter timeout lasts up to a millisecond longer. A null value fails with `EINVAL`, where FreeBSD's [`kern_setsockopt`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_syscalls.c#L1414-L1415) returns `EFAULT`. Every other SOL_SOCKET option (`SO_DEBUG`, `SO_USELOOPBACK`, `SO_REUSEPORT` and `SO_TIMESTAMP`, which FreeBSD stores and no title has been seen to set, the low-water marks among them), every other `IPPROTO_TCP` option (`TCP_MAXSEG`, `TCP_KEEPIDLE`, `TCP_KEEPINTVL` and `TCP_KEEPCNT` among them) and every other level (`IPPROTO_IP`, `IPPROTO_IPV6`) fail with `ENOPROTOOPT`. None of this was measured on a console. - [connect to AF_UNSPEC](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) dissolves a datagram association as FreeBSD 11.4's `udp_disconnect()` does, which resets the local address to the wildcard address and keeps the port. On Linux a socket bound to an explicit port keeps its local address as well (`127.0.0.1:` stays `127.0.0.1:`), because a bound socket cannot be bound again; one bound to port 0 is bound again to the wildcard address with the port it had From 0365a367f9b087581c96f54873a38a4cd95f45e9 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Thu, 15 Oct 2026 15:04:17 -0700 Subject: [PATCH 0420/1000] fix(agc): close only the shader preparation pools that exist at shutdown CloseRegistrationPreparation took RegistrationPreparePool::Instance(), which creates the pool and starts its detached workers (half the hardware threads, at least two) when nothing was queued for preparation yet. In a process that prepared nothing in the background, the driver's shutdown started the workers only to close the pool, and the process exited while they were still starting. Their first waits take the global lock of winpthreads' statically initialized condition variables; ExitProcess killed them holding it, and the destructor of the next static condition variable that was never waited on spun on it forever at DLL detach (~DeviceUseGate in ~Driver, or one of libkernel's). On GitHub's Windows runners agc_interpolant_mapping hung that way in 15 of 60 rounds of eight test runs. Shutdown now closes only the pools that exist, and a pool first used after it starts no workers, so later preparations run on the caller as they did on a closed pool. --- .../include/Driver/Shaders/ShaderRegistry.hpp | 1 + .../src/Driver/Shaders/ShaderRegistry.cpp | 42 +++++++++++++++++-- .../tests/execution/ShutdownThreads.cpp | 36 ++++++++++++++++ 3 files changed, 75 insertions(+), 4 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/ShutdownThreads.cpp diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp index 60a9fee237..aeb0d3f279 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp @@ -118,6 +118,7 @@ void PrioritizeRegistrationPreparation(PreparedShaders& prepared); void AwaitRegistrationPreparation(); void SubmitLibraryPrebuild(std::function task); void CloseRegistrationPreparation(); +unsigned RegistrationPreparationThreads(); void ResolvePreparedGraphics(const ShaderSnapshot& front, const std::shared_ptr& fragment, std::uint32_t primitiveType, const ShaderRecompiler::SpirvTarget& target); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp index b9a7b3ed9c..1d4be06830 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp @@ -11,6 +11,7 @@ #include "CompiledVariant.hpp" #include "Optimization/ResourceProgram.hpp" #include +#include #include #include #include @@ -933,7 +934,8 @@ class RegistrationPreparePool { public: static RegistrationPreparePool& Instance() { static auto* pool = [] { - auto* created = new RegistrationPreparePool(std::max(2u, std::thread::hardware_concurrency() / 2u)); + std::lock_guard lifecycle(Lifecycle()); + auto* created = new RegistrationPreparePool(Closing() ? 0u : std::max(2u, std::thread::hardware_concurrency() / 2u)); PreparationSlot().store(created); return created; }(); @@ -947,7 +949,8 @@ public: static RegistrationPreparePool& Prebuild() { static auto* pool = [] { const char* text = std::getenv("APS5_PREBUILD_THREADS"); - auto* created = new RegistrationPreparePool(text != nullptr ? std::max(1u, static_cast(std::strtoul(text, nullptr, 10))) : 1u); + std::lock_guard lifecycle(Lifecycle()); + auto* created = new RegistrationPreparePool(Closing() ? 0u : text != nullptr ? std::max(1u, static_cast(std::strtoul(text, nullptr, 10))) : 1u); PrebuildSlot().store(created); return created; }(); @@ -958,7 +961,33 @@ public: return PrebuildSlot().load(); } + static unsigned LiveWorkers() { + return liveWorkers.load(std::memory_order_acquire); + } + + static void CloseAll() { + std::array pools{}; + { + std::lock_guard lifecycle(Lifecycle()); + Closing() = true; + pools = {Existing(), ExistingPrebuild()}; + } + for (auto* pool : pools) { + if (pool != nullptr) pool->Close(); + } + } + private: + static std::mutex& Lifecycle() { + static std::mutex lifecycle; + return lifecycle; + } + + static bool& Closing() { + static bool closing = false; + return closing; + } + static std::atomic& PreparationSlot() { static std::atomic slot{nullptr}; return slot; @@ -1017,6 +1046,7 @@ private: if (pthread_create(&thread, &attributes, &RegistrationPreparePool::Main, this) == 0) { pthread_detach(thread); ++threads; + liveWorkers.fetch_add(1, std::memory_order_acq_rel); } } pthread_attr_destroy(&attributes); @@ -1063,6 +1093,7 @@ private: std::condition_variable idle; std::deque tasks; unsigned threads = 0; + inline static std::atomic liveWorkers{0}; unsigned running = 0; std::uint64_t submitted = 0; std::set unfinished; @@ -1120,8 +1151,11 @@ void SubmitLibraryPrebuild(std::function task) { } void CloseRegistrationPreparation() { - RegistrationPreparePool::Instance().Close(); - if (auto* prebuild = RegistrationPreparePool::ExistingPrebuild()) prebuild->Close(); + RegistrationPreparePool::CloseAll(); +} + +unsigned RegistrationPreparationThreads() { + return RegistrationPreparePool::LiveWorkers(); } std::vector PrepareGraphicsStages(const DrawDecode& decoded, const ShaderRecompiler::SpirvTarget& target) { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ShutdownThreads.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ShutdownThreads.cpp new file mode 100644 index 0000000000..f06398b9f2 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ShutdownThreads.cpp @@ -0,0 +1,36 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp" +#include +#include +#include + +namespace { + +void Require(bool condition, const char* message) { + if (!condition) throw std::runtime_error(message); +} + +void ClosingStartsNoPreparation() { + using namespace AgcDriver::DriverDetail; + Require(RegistrationPreparationThreads() == 0, "a shader preparation thread ran before anything was prepared"); + CloseRegistrationPreparation(); + Require(RegistrationPreparationThreads() == 0, "closing the shader preparation started preparation threads"); + const auto caller = std::this_thread::get_id(); + bool ranHere = false; + SubmitRegistrationPreparation([&] { ranHere = std::this_thread::get_id() == caller; }); + SubmitLibraryPrebuild([&] { ranHere = ranHere && std::this_thread::get_id() == caller; }); + Require(ranHere, "a preparation submitted after closing ran on another thread"); + Require(RegistrationPreparationThreads() == 0, "a preparation submitted after closing started preparation threads"); +} + +} + +int main() { + try { + ClosingStartsNoPreparation(); + std::cout << "shutdown threads tests passed\n"; + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} From afb1d0aad8d62a9408c11861098401aa8986e83f Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Thu, 15 Oct 2026 15:05:12 -0700 Subject: [PATCH 0421/1000] fix(agc): join the shader preparation workers when their pool closes Closing a preparation pool waited until its queue was empty, but its detached workers stayed parked on the queue until ExitProcess killed them. A pool first used shortly before shutdown still had workers starting at exit, inside the first waits that take winpthreads' global condition-variable lock, which the next DLL-detach destructor of an unwaited static condition variable then waits for forever. The workers are now joinable and leave once their pool is closed and idle, and Close joins them, so after the driver's shutdown no preparation worker is running or starting. --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 5 ++++ .../src/Driver/Shaders/ShaderRegistry.cpp | 23 +++++++++++++++---- .../tests/execution/ShutdownThreads.cpp | 23 +++++++++++++++++-- 3 files changed, 44 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 251d38d6a1..b9451afbff 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1769,6 +1769,11 @@ if(BUILD_TESTING) set_tests_properties(${preparedTest} PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() endforeach() + add_test(NAME agc_driver_shutdown_threads_joined COMMAND agc_driver_shutdown_threads_tests --joined) + set_tests_properties(agc_driver_shutdown_threads_joined PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" TIMEOUT 30) + if(WIN32) + set_tests_properties(agc_driver_shutdown_threads_joined PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() add_test(NAME agc_driver_compute_user_data_calls_registered COMMAND agc_driver_compute_user_data_calls_tests --registered) set_tests_properties(agc_driver_compute_user_data_calls_registered PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") if(WIN32) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp index 1d4be06830..9c5c54cd44 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp @@ -1031,9 +1031,17 @@ public: } void Close() { - std::unique_lock lock(mutex); - closed = true; - idle.wait(lock, [&] { return tasks.empty() && running == 0; }); + std::vector joined; + { + std::unique_lock lock(mutex); + closed = true; + idle.wait(lock, [&] { return tasks.empty() && running == 0; }); + joined.swap(workers); + } + ready.notify_all(); + for (const auto worker : joined) { + if (!pthread_equal(worker, pthread_self())) pthread_join(worker, nullptr); + } } private: @@ -1044,7 +1052,7 @@ private: for (unsigned index = 0; index < count; ++index) { pthread_t thread; if (pthread_create(&thread, &attributes, &RegistrationPreparePool::Main, this) == 0) { - pthread_detach(thread); + workers.push_back(thread); ++threads; liveWorkers.fetch_add(1, std::memory_order_acq_rel); } @@ -1059,7 +1067,11 @@ private: Task task; { std::unique_lock lock(pool.mutex); - pool.ready.wait(lock, [&] { return !pool.tasks.empty(); }); + pool.ready.wait(lock, [&] { return !pool.tasks.empty() || pool.closed; }); + if (pool.tasks.empty()) { + liveWorkers.fetch_sub(1, std::memory_order_acq_rel); + return nullptr; + } task = std::move(pool.tasks.front()); pool.tasks.pop_front(); ++pool.running; @@ -1092,6 +1104,7 @@ private: std::condition_variable ready; std::condition_variable idle; std::deque tasks; + std::vector workers; unsigned threads = 0; inline static std::atomic liveWorkers{0}; unsigned running = 0; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ShutdownThreads.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ShutdownThreads.cpp index f06398b9f2..baebc13884 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ShutdownThreads.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ShutdownThreads.cpp @@ -1,6 +1,8 @@ #include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp" +#include #include #include +#include #include namespace { @@ -22,11 +24,28 @@ void ClosingStartsNoPreparation() { Require(RegistrationPreparationThreads() == 0, "a preparation submitted after closing started preparation threads"); } +void ClosingJoinsPreparation() { + using namespace AgcDriver::DriverDetail; + std::promise prepared; + std::promise prebuilt; + auto preparedOn = prepared.get_future(); + auto prebuiltOn = prebuilt.get_future(); + SubmitRegistrationPreparation([&] { prepared.set_value(std::this_thread::get_id()); }); + SubmitLibraryPrebuild([&] { prebuilt.set_value(std::this_thread::get_id()); }); + const auto caller = std::this_thread::get_id(); + Require(preparedOn.get() != caller && prebuiltOn.get() != caller, "a queued preparation ran on the caller instead of a preparation thread"); + Require(RegistrationPreparationThreads() != 0, "a queued preparation started no preparation thread"); + CloseRegistrationPreparation(); + Require(RegistrationPreparationThreads() == 0, "closing the shader preparation left its threads running"); } -int main() { +} + +int main(int argc, char** argv) { try { - ClosingStartsNoPreparation(); + const std::string_view mode = argc > 1 ? argv[1] : ""; + if (mode == "--joined") ClosingJoinsPreparation(); + else ClosingStartsNoPreparation(); std::cout << "shutdown threads tests passed\n"; return 0; } catch (const std::exception& error) { From 0ba4f056cdcc3705fc7c1dbfbffc753cf1858a99 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Thu, 15 Oct 2026 15:09:33 -0700 Subject: [PATCH 0422/1000] fix(agc): stop and join the compare helpers and the pipeline library worker at shutdown Two more detached driver threads were never stopped: the helpers of the parallel compare pool, started at the first large compare, and the pipeline library's job thread, started at the first link-time optimized pipeline. A process that exited right after starting them killed them inside the first waits that take winpthreads' global condition-variable lock, as with the preparation workers. The driver's shutdown now stops both and joins them, after the device is reset. A compare in progress finishes first and later compares run on the caller. The library worker finishes the job it is running, the jobs still queued are dropped and counted off their device, and later pipelines are not optimized in the background. --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 5 +- .../Execution/src/Driver/Lifecycle.cpp | 4 ++ .../Graphics/include/ParallelCompare.hpp | 2 + .../Graphics/include/PipelineLibrary.hpp | 3 + .../Graphics/src/ParallelCompare.cpp | 67 ++++++++++++++++++- .../Graphics/src/PipelineLibrary.cpp | 38 +++++++++-- .../execution/PipelineOptimizedReadback.cpp | 3 + .../tests/execution/ShutdownThreads.cpp | 25 +++++++ 8 files changed, 137 insertions(+), 10 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index b9451afbff..f636f2299b 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1770,9 +1770,10 @@ if(BUILD_TESTING) endif() endforeach() add_test(NAME agc_driver_shutdown_threads_joined COMMAND agc_driver_shutdown_threads_tests --joined) - set_tests_properties(agc_driver_shutdown_threads_joined PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" TIMEOUT 30) + add_test(NAME agc_driver_shutdown_threads_compare_helpers COMMAND agc_driver_shutdown_threads_tests --compare-helpers) + set_tests_properties(agc_driver_shutdown_threads_joined agc_driver_shutdown_threads_compare_helpers PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" TIMEOUT 30) if(WIN32) - set_tests_properties(agc_driver_shutdown_threads_joined PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + set_tests_properties(agc_driver_shutdown_threads_joined agc_driver_shutdown_threads_compare_helpers PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() add_test(NAME agc_driver_compute_user_data_calls_registered COMMAND agc_driver_compute_user_data_calls_tests --registered) set_tests_properties(agc_driver_compute_user_data_calls_registered PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp index 8fdd044922..2d30731154 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp @@ -6,6 +6,8 @@ #include "prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp" #include "prx/libSceAgcDriver/Execution/include/ProfileOutput.hpp" #include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ParallelCompare.hpp" +#include "prx/libSceAgcDriver/Graphics/include/PipelineLibrary.hpp" #include "prx/libc/include/GuestAllocations.hpp" #include #include @@ -118,6 +120,8 @@ void Driver::stop() { device.Reset(); replacedDevices.clear(); Graphics::ShutdownGuestBufferWorkers(); + Graphics::StopPipelineLibraryWorker(); + Graphics::StopCompareHelpers(); PerformanceTrace::StopOutput(); StopProfileOutput_nid_no_patch(); stopped = true; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ParallelCompare.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ParallelCompare.hpp index a996ab230b..483b2f45cc 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ParallelCompare.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ParallelCompare.hpp @@ -16,6 +16,8 @@ struct CompareSpan { void CompareSpans(std::span spans, std::span equal); void CompareSpansFrom(std::span spans, std::span equal, std::size_t parallelBytes); unsigned CompareHelpers(); +void StopCompareHelpers(); +unsigned CompareHelpersRunning(); } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/PipelineLibrary.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/PipelineLibrary.hpp index b2ca74e468..0c30c450b7 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/PipelineLibrary.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/PipelineLibrary.hpp @@ -41,6 +41,9 @@ void WaitForOptimizedPipelines(VkDevice device); void ClearPipelineLibraries(VkDevice device); +void StopPipelineLibraryWorker(); +unsigned PipelineLibraryWorkers(); + struct PipelineLibraryCounters { std::array built{}; std::uint64_t linked = 0; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ParallelCompare.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ParallelCompare.cpp index b4fce71845..7f83917b88 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ParallelCompare.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ParallelCompare.cpp @@ -5,14 +5,21 @@ #include #include #include +#include namespace AgcDriver::Graphics { namespace { +std::atomic runningHelpers{0}; + class ComparePool { public: explicit ComparePool(unsigned count) { - for (unsigned i = 0; i < count; ++i) std::thread([this] { work(); }).detach(); + stopping = count == 0; + for (unsigned i = 0; i < count; ++i) { + workers.emplace_back([this] { work(); }); + runningHelpers.fetch_add(1, std::memory_order_acq_rel); + } } bool Run(std::span spans, std::span equal) { @@ -20,6 +27,7 @@ public: if (!caller.owns_lock()) return false; { std::unique_lock lock(mutex); + if (stopping) return false; finished.wait(lock, [&] { return working == 0; }); jobSpans = spans; jobEqual = equal; @@ -36,6 +44,21 @@ public: return true; } + void Stop() { + std::vector joined; + { + std::lock_guard caller(callers); + std::lock_guard lock(mutex); + stopping = true; + joined.swap(workers); + } + wake.notify_all(); + for (auto& worker : joined) { + if (worker.get_id() == std::this_thread::get_id()) worker.detach(); + else worker.join(); + } + } + private: void drain(std::span spans, std::span equal) { std::size_t compared = 0; @@ -51,7 +74,8 @@ private: std::uint64_t seen = 0; std::unique_lock lock(mutex); for (;;) { - wake.wait(lock, [&] { return generation != seen; }); + wake.wait(lock, [&] { return generation != seen || stopping; }); + if (stopping) break; seen = generation; const auto spans = jobSpans; const auto equal = jobEqual; @@ -62,6 +86,7 @@ private: --working; if (working == 0) finished.notify_all(); } + runningHelpers.fetch_sub(1, std::memory_order_acq_rel); } std::mutex callers; @@ -74,10 +99,32 @@ private: std::span jobEqual; std::atomic next{0}; std::atomic done{0}; + std::vector workers; + bool stopping = false; }; +std::mutex& poolLifecycle() { + static std::mutex lifecycle; + return lifecycle; +} + +bool& poolStopped() { + static bool stopped = false; + return stopped; +} + +std::atomic& createdPool() { + static std::atomic created{nullptr}; + return created; +} + ComparePool* sharedPool() { - static ComparePool* const pool = new ComparePool(CompareHelpers()); + static ComparePool* const pool = [] { + std::lock_guard lifecycle(poolLifecycle()); + auto* created = new ComparePool(poolStopped() ? 0u : CompareHelpers()); + createdPool().store(created, std::memory_order_release); + return created; + }(); return pool; } @@ -97,6 +144,20 @@ void CompareSpansFrom(std::span spans, std::spanStop(); +} + +unsigned CompareHelpersRunning() { + return runningHelpers.load(std::memory_order_acquire); +} + void CompareSpans(std::span spans, std::span equal) { CompareSpansFrom(spans, equal, std::size_t{1} << 20u); } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp index bd2a12ed32..9a897f8436 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp @@ -1,6 +1,7 @@ #include "prx/libSceAgcDriver/Graphics/include/PipelineLibrary.hpp" #include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp" #include +#include #include #include #include @@ -52,9 +53,12 @@ struct LibraryStore { std::condition_variable work; std::deque>> jobs; std::map pending; - bool started = false; + std::thread worker; + bool stopping = false; }; +std::atomic runningWorkers{0}; + LibraryStore& Libraries() { static auto* store = new LibraryStore(); return *store; @@ -63,7 +67,8 @@ LibraryStore& Libraries() { void runJobs(LibraryStore& store) { std::unique_lock lock(store.mutex); for (;;) { - store.work.wait(lock, [&] { return !store.jobs.empty(); }); + store.work.wait(lock, [&] { return !store.jobs.empty() || store.stopping; }); + if (store.stopping) break; auto [device, job] = std::move(store.jobs.front()); store.jobs.pop_front(); lock.unlock(); @@ -71,12 +76,14 @@ void runJobs(LibraryStore& store) { lock.lock(); if (--store.pending[device] == 0) store.idle.notify_all(); } + runningWorkers.fetch_sub(1, std::memory_order_acq_rel); } void enqueue(LibraryStore& store, VkDevice device, std::function job) { - if (!store.started) { - std::thread(runJobs, std::ref(store)).detach(); - store.started = true; + if (store.stopping) return; + if (!store.worker.joinable()) { + store.worker = std::thread(runJobs, std::ref(store)); + runningWorkers.fetch_add(1, std::memory_order_acq_rel); } ++store.pending[device]; store.jobs.emplace_back(device, std::move(job)); @@ -303,6 +310,27 @@ void ClearPipelineLibraries(VkDevice device) { store.devices.erase(found); } +void StopPipelineLibraryWorker() { + auto& store = Libraries(); + std::thread worker; + { + std::lock_guard lock(store.mutex); + store.stopping = true; + for (const auto& [device, job] : store.jobs) --store.pending[device]; + store.jobs.clear(); + worker = std::move(store.worker); + } + store.idle.notify_all(); + store.work.notify_all(); + if (!worker.joinable()) return; + if (worker.get_id() == std::this_thread::get_id()) worker.detach(); + else worker.join(); +} + +unsigned PipelineLibraryWorkers() { + return runningWorkers.load(std::memory_order_acquire); +} + PipelineLibraryCounters PipelineLibraryCountersOf(VkDevice device) { auto& store = Libraries(); std::lock_guard lock(store.mutex); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PipelineOptimizedReadback.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PipelineOptimizedReadback.cpp index e4e66f07f3..f2b56411c0 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PipelineOptimizedReadback.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PipelineOptimizedReadback.cpp @@ -268,6 +268,9 @@ int main(int argc, char** argv) { OptimizationDrain drain{context.device}; std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); Run(*device, context, trace, mode == "color32" ? 32u : 64u, masked ? VK_FORMAT_UNDEFINED : (mode == "d16" ? VK_FORMAT_D16_UNORM : VK_FORMAT_D32_SFLOAT), optimize); + if (optimize) Require(AgcDriver::Graphics::PipelineLibraryWorkers() == 1, "the optimized pipeline was not built on the pipeline library worker"); + AgcDriver::Graphics::StopPipelineLibraryWorker(); + Require(AgcDriver::Graphics::PipelineLibraryWorkers() == 0, "stopping the pipeline library left its worker running"); return 0; } catch (const std::exception& error) { std::cerr << error.what() << '\n'; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ShutdownThreads.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ShutdownThreads.cpp index baebc13884..a29d4e2e3a 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ShutdownThreads.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ShutdownThreads.cpp @@ -1,9 +1,14 @@ #include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ParallelCompare.hpp" +#include +#include +#include #include #include #include #include #include +#include namespace { @@ -39,12 +44,32 @@ void ClosingJoinsPreparation() { Require(RegistrationPreparationThreads() == 0, "closing the shader preparation left its threads running"); } +void StoppingJoinsCompareHelpers() { + using namespace AgcDriver::Graphics; + const std::vector first(std::size_t{2} << 20u, std::byte{1}); + auto second = first; + second.back() = std::byte{2}; + const auto half = first.size() / 2u; + const std::array spans{{{first.data(), second.data(), half}, {first.data() + half, second.data() + half, half}}}; + std::array equal{}; + CompareSpans(spans, equal); + Require(equal[0] == 1u && equal[1] == 0u, "the compare helpers compared the spans wrongly"); + Require(CompareHelpersRunning() == CompareHelpers(), "a parallel compare started no compare helpers"); + StopCompareHelpers(); + Require(CompareHelpersRunning() == 0, "stopping the compare helpers left them running"); + equal = {}; + CompareSpans(spans, equal); + Require(equal[0] == 1u && equal[1] == 0u, "a compare after the helpers stopped compared the spans wrongly"); + Require(CompareHelpersRunning() == 0, "a compare after the helpers stopped started them again"); +} + } int main(int argc, char** argv) { try { const std::string_view mode = argc > 1 ? argv[1] : ""; if (mode == "--joined") ClosingJoinsPreparation(); + else if (mode == "--compare-helpers") StoppingJoinsCompareHelpers(); else ClosingStartsNoPreparation(); std::cout << "shutdown threads tests passed\n"; return 0; From 083f1cc9d87f34036ed083224102bf23d8168562 Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Thu, 15 Oct 2026 22:24:04 +0000 Subject: [PATCH 0423/1000] test(libkernel): drop backup privileges in denied parent watch An elevated Windows process can bypass the parent directory DACL while holding SeBackupPrivilege or SeRestorePrivilege. Remove both privileges before DeniedParentWatch so the test exercises the denied-open path reliably. Co-Authored-By: Adam Franke <10361523+frankea@users.noreply.github.com> --- core/libs/tests/GuestKqueue.cpp | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/core/libs/tests/GuestKqueue.cpp b/core/libs/tests/GuestKqueue.cpp index f0b46d539c..a0ca835daa 100644 --- a/core/libs/tests/GuestKqueue.cpp +++ b/core/libs/tests/GuestKqueue.cpp @@ -540,7 +540,24 @@ static void RequireContextInsensitive(std::string message, std::string context) } } +static void DropPrivilege(HANDLE token, const wchar_t* name) { + TOKEN_PRIVILEGES privileges{}; + privileges.PrivilegeCount = 1; + privileges.Privileges[0].Attributes = SE_PRIVILEGE_REMOVED; + Require(::LookupPrivilegeValueW(nullptr, name, &privileges.Privileges[0].Luid) != FALSE); + Require(::AdjustTokenPrivileges(token, FALSE, &privileges, 0, nullptr, nullptr) != FALSE); +} + +static void DropBackupPrivileges() { + HANDLE token = nullptr; + Require(::OpenProcessToken(::GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &token) != FALSE); + DropPrivilege(token, L"SeBackupPrivilege"); + DropPrivilege(token, L"SeRestorePrivilege"); + Require(::CloseHandle(token) != FALSE); +} + static void DeniedParentWatch() { + DropBackupPrivileges(); const auto path = TemporaryName("denied"); Require(mkdir_nid_postfix(path.c_str(), 0755) == 0); const int file = open_nid_postfix((path + "/watched.bin").c_str(), ReadWrite | Create, 0644); From 0627bc6bde2041cd24b2d0a492213292b105dc63 Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Thu, 15 Oct 2026 23:02:19 +0000 Subject: [PATCH 0424/1000] fix(agc): free released lavapipe descriptor sets --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 1 + .../Execution/src/VulkanDevice.cpp | 3 ++ .../Graphics/include/Context.hpp | 1 + .../Graphics/include/ShaderResources.hpp | 1 + .../Graphics/src/ShaderResources.cpp | 4 +- .../prx/libSceAgcDriver/tests/Graphics.cpp | 37 +++++++++++++++++++ 6 files changed, 46 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 251d38d6a1..a7ae94e412 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1669,6 +1669,7 @@ if(BUILD_TESTING) add_test(NAME agc_driver_pm4_failure COMMAND agc_driver_pm4_tests failure) set_tests_properties(agc_driver_pm4_failure PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 TIMEOUT 30) add_test(NAME agc_descriptor_pool_reset_failure COMMAND agc_driver_graphics_tests --descriptor-pool-reset-failure) + add_test(NAME agc_descriptor_cache COMMAND agc_driver_graphics_tests --descriptor-cache) add_test(NAME agc_driver_address_space_teardown COMMAND agc_driver_graphics_tests --address-space-teardown) set_tests_properties(agc_driver_address_space_teardown PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") add_test(NAME agc_driver_pm4_failure_inline COMMAND agc_driver_pm4_tests failure) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 694152052f..a176c38a97 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -266,6 +266,7 @@ struct VulkanDevice::State { VkDeviceSize uploadSize = 0; VkPhysicalDeviceProperties properties{}; std::string driverInfo; + bool individuallyFreeDescriptorSets = false; PFN_vkGetCalibratedTimestampsKHR calibratedTimestamps = nullptr; PFN_vkCmdSetCheckpointNV checkpointMarker = nullptr; bool deviceLostHook = false; @@ -1101,6 +1102,7 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : VkPhysicalDeviceProperties2 driverProperties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, &driver}; state->InstanceFunction("vkGetPhysicalDeviceProperties2")(selected, &driverProperties); state->driverInfo = std::string(driver.driverName) + " " + driver.driverInfo; + state->individuallyFreeDescriptorSets = driver.driverID == VK_DRIVER_ID_MESA_LLVMPIPE; } auto byteFeatures = QueryBdaByteFeatures(selected, state->InstanceFunction("vkGetPhysicalDeviceFeatures2"), availableExtensions); auto bdaFeatures = QueryBdaFeatures(selected, state->InstanceFunction("vkGetPhysicalDeviceFeatures2"), availableExtensions); @@ -3446,6 +3448,7 @@ Graphics::Context VulkanDevice::buildContext() const { state->hostImportAlignment }; context.pipelineCacheSynchronization = state->pipelineCache ? &state->pipelineCache->Synchronization() : nullptr; + context.individuallyFreeDescriptorSets = state->individuallyFreeDescriptorSets; context.dmaBufImport = state->dmaBufImport; context.sparseBinding = state->sparseBinding; context.queueFamily = state->queueFamily; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp index cba5321c23..bf54aaaccb 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp @@ -210,6 +210,7 @@ struct Context { bool variableDescriptorCount = false; std::uint32_t imageTableCapacity = 0; std::uint32_t samplerTableCapacity = 0; + bool individuallyFreeDescriptorSets = false; bool geometryShader = false; bool sampleRateShading = false; bool shaderClipDistance = false; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp index efefda3a7f..435b9f30c0 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp @@ -98,6 +98,7 @@ private: PFN_vkDestroyDescriptorSetLayout destroyLayout; PFN_vkDestroyDescriptorPool destroyPool; PFN_vkResetDescriptorPool resetPool; + PFN_vkFreeDescriptorSets freeDescriptorSets = nullptr; mutable std::mutex mutex; std::map, VkDescriptorSetLayout> layouts; struct Pool { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index e31084e0c4..b9ce86e43b 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -3372,7 +3372,7 @@ ResourceCache& SharedResourceCache() { return *cache; } -DescriptorCache::DescriptorCache(const Context& context) : context(context), destroyLayout(context.Function("vkDestroyDescriptorSetLayout")), destroyPool(context.Function("vkDestroyDescriptorPool")), resetPool(context.Function("vkResetDescriptorPool")) {} +DescriptorCache::DescriptorCache(const Context& context) : context(context), destroyLayout(context.Function("vkDestroyDescriptorSetLayout")), destroyPool(context.Function("vkDestroyDescriptorPool")), resetPool(context.Function("vkResetDescriptorPool")), freeDescriptorSets(context.individuallyFreeDescriptorSets ? context.Function("vkFreeDescriptorSets") : nullptr) {} DescriptorCache::~DescriptorCache() { for (const auto& pool : pools) destroyPool(context.device, pool.handle, nullptr); @@ -3470,6 +3470,7 @@ DescriptorCache::SetAllocation DescriptorCache::Allocate(VkDescriptorSetLayout l } VkDescriptorPoolCreateInfo poolInfo{VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO}; poolInfo.flags = updateAfterBind ? VK_DESCRIPTOR_POOL_CREATE_UPDATE_AFTER_BIND_BIT : 0; + if (context.individuallyFreeDescriptorSets) poolInfo.flags |= VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT; poolInfo.maxSets = ChainPoolSets; poolInfo.poolSizeCount = static_cast(ChainPoolCreateSizes.size()); poolInfo.pPoolSizes = ChainPoolCreateSizes.data(); @@ -3525,6 +3526,7 @@ void DescriptorCache::Free(const SetAllocation& allocation) noexcept { if (allocation.poolIndex >= chain.size()) std::terminate(); auto& pool = chain[allocation.poolIndex]; if (pool.handle != allocation.pool || pool.liveSets == 0) std::terminate(); + if (freeDescriptorSets != nullptr && freeDescriptorSets(context.device, allocation.pool, 1, &allocation.set) != VK_SUCCESS) std::terminate(); if (--pool.liveSets != 0) return; if (resetPool(context.device, pool.handle, 0) != VK_SUCCESS) std::terminate(); pool.exhausted = false; diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 2e1d26e11f..9fd774354b 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -3410,6 +3410,7 @@ struct MockVulkan { std::vector poolSizes; std::uint32_t poolMaxSets = 0; VkDescriptorPoolCreateFlags poolFlags = 0; + std::uint32_t freedDescriptorSets = 0; struct DescriptorPool { VkDescriptorPoolCreateFlags flags; std::uint32_t maxSets; @@ -3572,6 +3573,15 @@ VKAPI_ATTR VkResult VKAPI_CALL mockResetDescriptorPool(VkDevice, VkDescriptorPoo return VK_SUCCESS; } +VKAPI_ATTR VkResult VKAPI_CALL mockFreeDescriptorSets(VkDevice, VkDescriptorPool handle, std::uint32_t count, const VkDescriptorSet*) { + auto& pool = mock.descriptorPools.at(handle); + Require((pool.flags & VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT) != 0, "a descriptor set was freed from a pool without free support"); + Require(count <= pool.allocated, "more descriptor sets were freed than allocated"); + pool.allocated -= count; + mock.freedDescriptorSets += count; + return VK_SUCCESS; +} + VKAPI_ATTR VkResult VKAPI_CALL mockCreateSampler(VkDevice, const VkSamplerCreateInfo* info, const VkAllocationCallbacks*, VkSampler* sampler) { *sampler = makeHandle(); mock.samplerFlags[*sampler] = info->flags; @@ -3741,6 +3751,7 @@ PFN_vkVoidFunction VKAPI_CALL mockProc(VkDevice, const char* name) { {"vkCreateDescriptorPool", reinterpret_cast(mockCreateDescriptorPool)}, {"vkDestroyDescriptorPool", reinterpret_cast(mockDestroyDescriptorPool)}, {"vkAllocateDescriptorSets", reinterpret_cast(mockAllocateDescriptorSets)}, + {"vkFreeDescriptorSets", reinterpret_cast(mockFreeDescriptorSets)}, {"vkResetDescriptorPool", reinterpret_cast(mockResetDescriptorPool)}, {"vkCreateSampler", reinterpret_cast(mockCreateSampler)}, {"vkDestroySampler", reinterpret_cast(mockDestroySampler)}, @@ -4428,6 +4439,28 @@ void descriptorCacheTests() { expectFailure([&] { cache.Allocate(fittingLayout, fitting, false, 3u); }, "descriptor set exceeds the device's per-set descriptor limit"); } Require(mock.live == 0, "the combined descriptor cache leaked a pool or layout"); + mock = MockVulkan{}; + auto lavapipeContext = mockContext(); + lavapipeContext.individuallyFreeDescriptorSets = true; + lavapipeContext.limits.maxDescriptorSetStorageBuffers = 16; + { + AgcDriver::Graphics::DescriptorCache cache(lavapipeContext); + const VkDescriptorSetLayoutBinding binding{0, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, VK_SHADER_STAGE_FRAGMENT_BIT, nullptr}; + const std::array key{0, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, VK_SHADER_STAGE_FRAGMENT_BIT}; + const auto layout = cache.Layout(key, std::span(&binding, 1)); + const std::array sizes{{{VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1}}}; + const auto first = cache.Allocate(layout, sizes); + const auto second = cache.Allocate(layout, sizes); + Require(first.pool == second.pool && mock.poolFlags == VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT, "lavapipe chain pools do not enable individual descriptor-set frees"); + cache.Free(first); + Require(mock.freedDescriptorSets == 1 && mock.descriptorPools.at(first.pool).resets == 0, "freeing one of two live sets reset its chain pool"); + cache.Free(second); + Require(mock.freedDescriptorSets == 2 && mock.descriptorPools.at(second.pool).resets == 1, "freeing the last live set did not free it and reset its chain pool"); + const auto reused = cache.Allocate(layout, sizes); + Require(reused.pool == first.pool && mock.descriptorPools.at(reused.pool).attempts == 3, "an emptied lavapipe chain pool was not reused"); + cache.Free(reused); + } + Require(mock.live == 0, "the lavapipe descriptor cache leaked a pool or layout"); } void imageTableAllocationTests() { @@ -7260,6 +7293,10 @@ int main(int argc, char** argv) { setenv("APS5_HEAP_MIRROR_MIB", "4", 1); #endif try { + if (argc == 2 && std::string_view(argv[1]) == "--descriptor-cache") { + descriptorCacheTests(); + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--unused-stencil-reference") { stencilReferenceUnusedTests(); return 0; From d70e9fb6c81af21d46faa86610fc00686490d575 Mon Sep 17 00:00:00 2001 From: Eli Yarson Date: Thu, 15 Oct 2026 07:41:40 -0300 Subject: [PATCH 0425/1000] test(libc): skip the failed heap commit check when the host overcommits --- core/libs/tests/GuestAllocatorFailures.cpp | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/core/libs/tests/GuestAllocatorFailures.cpp b/core/libs/tests/GuestAllocatorFailures.cpp index 22629e0166..594e03b0d7 100644 --- a/core/libs/tests/GuestAllocatorFailures.cpp +++ b/core/libs/tests/GuestAllocatorFailures.cpp @@ -32,6 +32,17 @@ constexpr std::size_t HalfPlusOne = static_cast(std::numeric_limits int failures = 0; +#ifdef __linux__ +int overcommitMode() { + int mode = -1; + if (std::FILE* file = std::fopen("/proc/sys/vm/overcommit_memory", "r")) { + if (std::fscanf(file, "%d", &mode) != 1) mode = -1; + std::fclose(file); + } + return mode; +} +#endif + void report(const char* what, const char* detail) { std::fprintf(stderr, "%s: %s\n", what, detail); ++failures; @@ -151,7 +162,7 @@ int main() { #endif if (committed) { #ifdef __linux__ - report("malloc(300 GiB)", "succeeded despite RLIMIT_DATA"); + std::fprintf(stderr, "skipped: the host can commit 300 GiB (vm.overcommit_memory=%d), so the failed commit and arena release checks did not run\n", overcommitMode()); #else std::fputs("skipped: the host can commit 300 GiB, so the failed commit and arena release checks did not run\n", stderr); #endif From 74f6a23bbaa9e23267edbf86e0c64df41436f9b4 Mon Sep 17 00:00:00 2001 From: Ishmam Ahmed <89687266+IshmamDC217@users.noreply.github.com> Date: Fri, 16 Oct 2026 00:24:05 +0100 Subject: [PATCH 0426/1000] fix(tools): find Python and CMake comments across multi-line strings The comment check looked at each added .py, .cmake and CMakeLists.txt line on its own and treated any # outside a string closed on that line as a comment. A # inside a Python triple-quoted string or a CMake quoted or bracket argument that spans lines was reported, so adding an #include to the C++ held in PatchSpirvTools.cmake's [=[ ]=] arguments failed the check. An escaped \# in CMake was reported too, while the inner lines of a multi-line #[[ ]] bracket comment were not. Find comments over the whole file instead: Python COMMENT tokens from tokenize, and a CMake scan that skips quoted and bracket arguments and escapes and covers every line of a bracket comment. A Python file that does not tokenize keeps the previous per-line check. --- tools/check_conventions.py | 25 +++++++++- .../tests/test_check_conventions_comments.py | 48 +++++++++++++++++++ 2 files changed, 72 insertions(+), 1 deletion(-) diff --git a/tools/check_conventions.py b/tools/check_conventions.py index 000a1de445..2f682e14de 100644 --- a/tools/check_conventions.py +++ b/tools/check_conventions.py @@ -1,10 +1,12 @@ import argparse +import io import json import os import posixpath import re import subprocess import sys +import tokenize from collections import defaultdict from pathlib import Path, PurePosixPath from urllib.parse import unquote @@ -43,6 +45,7 @@ DEBT_TARGET = re.compile(r"\]\((?:\.\./)+(core/[^)#\s]+)") CONVENTIONAL = re.compile(r"^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(\([^)]+\))?!?: \S") STRINGS = re.compile(r'"(?:\\.|[^"\\])*"|\'(?:\\.|[^\'\\])*\'') ALLOWED_COMMENT = re.compile(r"^\s*(#\s*endif\b|}\s*;?\s*//\s*(end\s+)?namespace\b)") +CMAKE_TOKEN = re.compile(r'\[(=*)\[.*?\]\1\]|"(?:\\.|[^"\\])*"|(?P#\[(?P=*)\[.*?\](?P=level)\]|#[^\n]*)|\\.', re.S) EXPORT = re.compile(r"\bAPS5_VABI\s+(\w+)\s*\(") STUB_BODY = re.compile(r"^(?:\(void\)\s*\w+\s*;|static_cast\(\s*\w+\s*\)\s*;)*" r"(?:return\s*\(?\s*(?:-?(?:0x[0-9a-fA-F]+|\d+)[uUlL]*|nullptr|true|false|\w*_OK)?\s*\)?\s*;)?$") @@ -159,9 +162,12 @@ class Check: self.report("silent-stub", path, number, match.group(1)) self.duplicates(exports) for path, lines in self.files(lambda p: p.suffix in (".py", ".cmake") or p.name == "CMakeLists.txt"): + source = self.show(path) + comments = python_comments(source) if path.endswith(".py") else cmake_comments(source) for number, text in lines: shebang = text.startswith("#!") or (number == 1 and text.startswith("\ufeff#!")) - if "#" in STRINGS.sub('""', text) and not shebang: + comment = number in comments if comments is not None else "#" in STRINGS.sub('""', text) + if comment and not shebang: self.report("comment", path, number) if any(is_debt(path) for path in self.status): self.findings = [finding for finding in self.findings if finding[0] != "comment"] @@ -399,6 +405,23 @@ def allowed_notes(path): return path in ("README.md", "CONTRIBUTING.md") or p.parts[0] == ".github" or p.parts[:2] in (("docs", "dev"), ("docs", "user")) +def python_comments(text): + try: + tokens = list(tokenize.generate_tokens(io.StringIO(text.removeprefix("\ufeff")).readline)) + except (tokenize.TokenError, SyntaxError): + return None + return {token.start[0] for token in tokens if token.type == tokenize.COMMENT} + + +def cmake_comments(text): + lines = set() + for match in CMAKE_TOKEN.finditer(text): + if match.group("comment") is not None: + first = text.count("\n", 0, match.start()) + 1 + lines.update(range(first, first + match.group().count("\n") + 1)) + return lines + + def cpp_text(text, preserve_comments=False): def mask(match): if match.group("code") is not None or preserve_comments and match.group("comment") is not None: diff --git a/tools/tests/test_check_conventions_comments.py b/tools/tests/test_check_conventions_comments.py index b7f46f54f8..9156226179 100644 --- a/tools/tests/test_check_conventions_comments.py +++ b/tools/tests/test_check_conventions_comments.py @@ -157,6 +157,54 @@ class RawLiteralCommentTests(unittest.TestCase): self.assertEqual(self.findings(comment, [1]), [("comment", self.path, 1, "")]) +class HashCommentTests(unittest.TestCase): + def findings(self, path, source, added): + check = Check.__new__(Check) + check.tree = set() + check.status = {} + check.findings = [] + lines = source.split("\n") + check.added = {path: [(number, lines[number - 1]) for number in added]} + with patch.object(check, "show", return_value=source), patch.object(check, "duplicates"): + check.code() + return check.findings + + def test_hash_inside_python_multiline_string_is_not_a_comment(self): + for quote in ('"""', "'''"): + with self.subTest(quote=quote): + source = f'TEMPLATE = {quote}\n#include \n{quote}\nprint("#ok")\n' + self.assertEqual(self.findings("tools/generate.py", source, [1, 2, 3, 4]), []) + + def test_python_comment_after_a_multiline_string_is_reported(self): + source = 'TEMPLATE = """\n#include \n""" # pending\nvalue = 1 # pending\n' + self.assertEqual(self.findings("tools/generate.py", source, [2, 3, 4]), + [("comment", "tools/generate.py", 3, ""), ("comment", "tools/generate.py", 4, "")]) + + def test_python_shebang_is_not_a_comment(self): + for source in ("#!/usr/bin/env python3\nprint(1)\n", "\ufeff#!/usr/bin/env python3\nprint(1)\n"): + with self.subTest(source=source): + self.assertEqual(self.findings("tools/generate.py", source, [1, 2]), []) + + def test_unparsable_python_falls_back_to_the_line_check(self): + source = 'TEMPLATE = """\n#include \n' + self.assertEqual(self.findings("tools/generate.py", source, [2]), [("comment", "tools/generate.py", 2, "")]) + + def test_hash_inside_cmake_quoted_and_bracket_arguments_is_not_a_comment(self): + for source in ('set(content "#ifndef GUARD\n#define GUARD\n#endif\n")\n', + 'set(patch [=[\n#include \n]=])\n', + 'set(patch [[\n#include \n]])\n', + 'message(STATUS "a\\"#b")\n', + 'string(REPLACE \\# x out "${in}")\n'): + with self.subTest(source=source): + added = list(range(1, source.count("\n") + 1)) + self.assertEqual(self.findings("cmake/Generate.cmake", source, added), []) + + def test_cmake_line_and_bracket_comments_are_reported(self): + source = 'set(value 1) # pending\n#[[\npending\n]]\nset(patch [=[\n#include \n]=]) # pending\n' + self.assertEqual(self.findings("CMakeLists.txt", source, [1, 2, 3, 4, 6, 7]), + [("comment", "CMakeLists.txt", number, "") for number in (1, 2, 3, 4, 7)]) + + class SilentStubTests(unittest.TestCase): def test_commented_stub_is_reported_when_debt_change_allows_comments(self): checker = Path(__file__).resolve().parents[1] / "check_conventions.py" From 3ee7ab8ffbed9defd9052abd8ae06f256e6befae Mon Sep 17 00:00:00 2001 From: Ishmam Ahmed <89687266+IshmamDC217@users.noreply.github.com> Date: Fri, 16 Oct 2026 00:25:50 +0100 Subject: [PATCH 0427/1000] fix(tools): read workflow job keys at the indentation the file uses The ci-gate check read a job's needs and if only two spaces deeper than the job name. In a workflow indented by four spaces those keys were never found, so a job gated with needs: eligibility and an if on its outputs was reported as ungated. Read the keys at the indentation of the job's first child instead. --- tools/check_conventions.py | 4 +++- tools/tests/test_check_conventions_ci_gate.py | 7 +++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/tools/check_conventions.py b/tools/check_conventions.py index 000a1de445..202f32bf18 100644 --- a/tools/check_conventions.py +++ b/tools/check_conventions.py @@ -372,7 +372,9 @@ def jobs(lines, source): for index, (number, indent, name, _) in enumerate(entries): if indent != entries[0][1]: continue - fields = {entry[2]: field(source, entry) for entry in block(entries, index) if entry[1] == indent + 2} + children = block(entries, index) + child = min((entry[1] for entry in children), default=indent + 2) + fields = {entry[2]: field(source, entry) for entry in children if entry[1] == child} result.append((number, name, re.findall(r"[A-Za-z0-9_-]+", fields.get("needs", "")), fields.get("if", ""))) return result diff --git a/tools/tests/test_check_conventions_ci_gate.py b/tools/tests/test_check_conventions_ci_gate.py index 33f7d94501..200d32f39b 100644 --- a/tools/tests/test_check_conventions_ci_gate.py +++ b/tools/tests/test_check_conventions_ci_gate.py @@ -92,6 +92,13 @@ class CiGateTests(unittest.TestCase): def test_other_triggers_pass(self): self.assertGated(workflow(" pull_request_target:\n types: [opened]\n push:\n branches: [main]\n", " tests:\n runs-on: ubuntu-latest\n")) + def test_four_space_indentation_is_read(self): + def wide(text): + return "\n".join(" " * (2 * (len(line) - len(line.lstrip(" ")))) + line.lstrip(" ") for line in text.split("\n")) + gated = " tests:\n needs: eligibility\n if: ${{ needs.eligibility.outputs.tests == 'true' }}\n runs-on: ubuntu-latest\n" + self.assertGated(wide(workflow(PULL_REQUEST, gated))) + self.assertUngated(wide(workflow(PULL_REQUEST, " tests:\n needs: eligibility\n runs-on: ubuntu-latest\n")), "tests") + def test_repository_workflows_pass(self): files = {path.name: path.read_text(encoding="utf-8") for path in WORKFLOWS.glob("*.y*ml")} self.assertIn("build.yml", files) From 041dbd240e036f456ed533f57b320685b1974fc5 Mon Sep 17 00:00:00 2001 From: Ishmam Ahmed <89687266+IshmamDC217@users.noreply.github.com> Date: Thu, 15 Oct 2026 14:56:49 +0100 Subject: [PATCH 0428/1000] fix(tools): fail when the trace_digest --summary file does not exist load_summary returned None for a missing file whether the path was the default aps5-summary.jsonl next to the trace or one named with --summary. A mistyped --summary path was therefore silently ignored: the digest printed "no summary line found" and, without the window end, dropped the last frame of the window. Report an explicitly named file that does not exist as a usage error; the default stays optional. --- tools/tests/test_trace_digest.py | 9 +++++++++ tools/trace_digest.py | 2 ++ 2 files changed, 11 insertions(+) diff --git a/tools/tests/test_trace_digest.py b/tools/tests/test_trace_digest.py index d9dd888b29..48dbb7c06d 100644 --- a/tools/tests/test_trace_digest.py +++ b/tools/tests/test_trace_digest.py @@ -318,6 +318,15 @@ class TraceDigestTests(unittest.TestCase): self.assertEqual([(entry[3], entry[4]) for entry in trace.slices[1]], [(0, False), (1, True), (0, False)]) + def test_missing_summary_file_is_an_error(self): + missing = Path(self.directory.name) / "missing.jsonl" + error = io.StringIO() + with contextlib.redirect_stderr(error), contextlib.redirect_stdout(io.StringIO()): + with self.assertRaises(SystemExit) as raised: + trace_digest.main([str(self.trace), "--summary", str(missing)]) + self.assertEqual(raised.exception.code, 2) + self.assertIn("summary file not found", error.getvalue()) + def interval_line(start, end, p99, gpu_ms, **changes): line = dict(SUMMARY, window={"spec": "summary_every 30", "first_present": 0, "ms": (end - start) * 1000.0}, interval_start_s=start, interval_end_s=end, diff --git a/tools/trace_digest.py b/tools/trace_digest.py index 6644bdfbe2..eb15dc0e2b 100644 --- a/tools/trace_digest.py +++ b/tools/trace_digest.py @@ -305,6 +305,8 @@ def main(arguments=None): parser.add_argument("--top", type=int, default=10, help="entries per list (default 10)") parser.add_argument("--json", action="store_true", help="print the digest as JSON") options = parser.parse_args(arguments) + if options.summary and not Path(options.summary).is_file(): + parser.error(f"summary file not found: {options.summary}") if options.trace.endswith(".jsonl"): result = digest_intervals(load_intervals(options.trace)) sys.stdout.write(json.dumps(result, indent=1) + "\n" if options.json else markdown_intervals(result)) From 680095be7ad0073b66788d3033e0f3e965f7508a Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Fri, 16 Oct 2026 00:04:17 +0000 Subject: [PATCH 0429/1000] test(agc): set DLL path for descriptor cache test --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index a7ae94e412..51492be28f 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1675,7 +1675,7 @@ if(BUILD_TESTING) add_test(NAME agc_driver_pm4_failure_inline COMMAND agc_driver_pm4_tests failure) set_tests_properties(agc_driver_pm4_failure_inline PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_PIPELINED_DRAWS=0" SKIP_RETURN_CODE 77) if(WIN32) - set_tests_properties(agc_driver_pm4_failure agc_driver_pm4_failure_inline agc_descriptor_pool_reset_failure agc_driver_address_space_teardown PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(agc_driver_pm4_failure agc_driver_pm4_failure_inline agc_descriptor_pool_reset_failure agc_descriptor_cache agc_driver_address_space_teardown PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() if(NOT WIN32) From af58b9562a871c07782605bff6a718c6e6aad47d Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Thu, 15 Oct 2026 18:02:44 -0700 Subject: [PATCH 0430/1000] fix(agc): stop the pipeline library worker before the device is reset Driver::stop reset the device and then stopped the worker. No job could outlive the device, because ClearPipelineLibraries waits for the device's jobs to finish, but that was only visible inside the wait. The worker now stops first: a running job finishes, queued jobs are dropped, and no job runs once the device's teardown starts. agc_driver_draw_cache_reuse checks that the driver's own shutdown leaves no library worker running. --- .../libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp | 2 +- .../libs/prx/libSceAgcDriver/tests/execution/DrawCacheReuse.cpp | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp index 2d30731154..4b2da77999 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp @@ -116,11 +116,11 @@ void Driver::stop() { } StopWorkerSampler(); CloseRegistrationPreparation(); + Graphics::StopPipelineLibraryWorker(); std::lock_guard gpuLock(GuestMemory::GpuMutex()); device.Reset(); replacedDevices.clear(); Graphics::ShutdownGuestBufferWorkers(); - Graphics::StopPipelineLibraryWorker(); Graphics::StopCompareHelpers(); PerformanceTrace::StopOutput(); StopProfileOutput_nid_no_patch(); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DrawCacheReuse.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DrawCacheReuse.cpp index 516e85a915..ab9818c6ba 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DrawCacheReuse.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DrawCacheReuse.cpp @@ -1,5 +1,6 @@ #include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/PipelineLibrary.hpp" #include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" #include "VulkanTestDevice.hpp" #include @@ -243,6 +244,7 @@ int main() { StageDataHitsRefreshTheirWords(libraries); RetainedPixelCacheTransitions(); AgcDriverShutdown_nid_postfix(); + Require(AgcDriver::Graphics::PipelineLibraryWorkers() == 0, "the driver's shutdown left the pipeline library worker running"); std::cout << "A repeated draw reused the cached draw " << (libraries ? "with" : "without") << " graphics pipeline libraries\n"; return 0; } catch (const std::exception& error) { From 8be2de862b3b5ae3aafcbd869d4c4ac11532c6e7 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Thu, 15 Oct 2026 16:59:38 -0700 Subject: [PATCH 0431/1000] feat(agc): return whole 64 KiB blocks to the Windows write watch GuestMemory::Rewatch takes ranges that left the write watch back into it. Only whole tracker blocks whose pages can be collected return: their write-watch marks are collected and dropped, and every block is stamped as a CPU store with a new generation, so a snapshot or a proof made before the block left the watch reads as changed, and the blocks' untracked hints stop applying. It does nothing outside Windows. --- .../Execution/include/GuestMemory.hpp | 1 + .../Execution/src/GuestMemory.cpp | 25 +++++++++++ .../libSceAgcDriver/tests/WriteTracking.cpp | 44 +++++++++++++++++++ 3 files changed, 70 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp index 2388684997..c5794b1b11 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp @@ -60,6 +60,7 @@ void WriteChanged(std::uint64_t address, std::span current, std // UnchangedSince is always false, so callers fall back to comparing bytes. bool WriteWatched(); void Unwatch(std::uint64_t address, std::size_t bytes); +void Rewatch(std::span> ranges); bool ImportWatched(std::uint64_t address, std::size_t bytes, const std::function& import, bool staysWatched = true); void SetUnwatchHook(void (*hook)(std::uint64_t address, std::size_t bytes)); bool Watched(std::uint64_t address, std::size_t bytes); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp index d0859ab061..8b48b073cd 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp @@ -1322,6 +1322,31 @@ void Unwatch(std::uint64_t address, std::size_t bytes) { unwatchLocked(tracker, address, bytes); } +void Rewatch(std::span> ranges) { +#ifdef _WIN32 + auto& tracker = Tracker(); + const auto lock = lockTracker(tracker); + tracker.initialize(); + if (!tracker.watched) return; + bool restored = false; + for (const auto& [begin, end] : ranges) { + if (end <= begin || begin < tracker.base || end - tracker.base > tracker.size || (begin - tracker.base) % WriteBlockBytes != 0 || (end - tracker.base) % WriteBlockBytes != 0) continue; + const auto bytes = static_cast(end - begin); + if (tracker.coverage.Covers(begin, bytes) || !walkWrites(tracker, begin, end, StampKind::Cpu) || !tracker.coverage.Restore(begin, bytes)) continue; + ++tracker.generation; + tracker.noteCpuStore(begin, end, tracker.generation); + for (auto block = tracker.blockOf(begin); block <= tracker.blockOf(end - 1); ++block) { + tracker.driverPieces.erase(block); + tracker.stamp(block, tracker.generation, StampKind::Cpu); + } + restored = true; + } + if (restored) unwatchSerial.fetch_add(1, std::memory_order_release); +#else + static_cast(ranges); +#endif +} + bool ImportWatched(std::uint64_t address, std::size_t bytes, const std::function& import, bool staysWatched) { if (bytes == 0 || bytes > std::numeric_limits::max() - address) return import(); if (!staysWatched) { diff --git a/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp b/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp index d69035db8c..816b3dba83 100644 --- a/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp @@ -372,6 +372,49 @@ void CheckPrivateMappingReuse() { GuestArena::GuestArenaRelease_nid_postfix(replacement, 3 * Block); } +void CheckRewatch() { + void* memory = GuestArena::GuestArenaAllocate_nid_postfix(4 * Block, Block); + GuestArena::GuestArenaCommit_nid_postfix(memory, 4 * Block, PAGE_READWRITE, 16384); + const auto base = reinterpret_cast(memory); + Require(Watched(base, 4 * Block), "the rewatch test block is not watched"); + auto* bytes = static_cast(memory); + const auto imported = base + Block; + std::memset(memory, 0x11, 4 * Block); + BumpCollectEpoch(); + const auto before = CollectWrites(base, 4 * Block); + Require(before != 0, "the test range was not collected"); + Unwatch(imported, 2 * Block); + const auto unwatched = UnwatchSerial(); + Require(!Watched(imported, Block) && UntrackedHint(imported, Block) != 0, "the unwatched block does not read as untracked"); + bytes[Block + 8] = 0x22; + const std::array, 1> partial{{{imported + 4096, imported + Block}}}; + Rewatch(partial); + Require(!Watched(imported, Block) && UnwatchSerial() == unwatched, "part of a block returned to the write watch"); + const std::array, 1> first{{{imported, imported + Block}}}; + Rewatch(first); + Require(Watched(imported, Block) && !Watched(imported + Block, Block), "rewatching did not return exactly its block to the write watch"); + Require(UnwatchSerial() != unwatched, "rewatching did not invalidate what was cached while the block was unwatched"); + Require(UntrackedHint(imported, Block) == 0, "a rewatched block still reads as untracked"); + Require(!UnchangedSince(imported, Block, before) && StoredOver(imported, Block, before), "a block returned to the write watch reads as unchanged since before it left"); + Require(!UnchangedSinceCollected(imported, Block, before), "a CPU store made while the block was unwatched reads as no CPU store"); + Require(UnchangedSince(base, Block, before) && UnchangedSince(base + 3 * Block, Block, before), "rewatching stamped the blocks around it"); + BumpCollectEpoch(); + const auto after = CollectWrites(imported, Block); + Require(after != 0 && UnchangedSince(imported, Block, after), "a rewatched block is not collected"); + bytes[Block + 16] = 0x33; + CollectWritesUncached(imported, Block); + Require(!UnchangedSince(imported, Block, after), "a CPU store after rewatching was missed"); + const auto serial = UnwatchSerial(); + Rewatch(first); + Require(UnwatchSerial() == serial, "rewatching a watched block changed what it covers"); + const std::array, 1> uncommitted{{{base + 2 * Block, base + 3 * Block}}}; + GuestArena::GuestArenaReset_nid_postfix(static_cast(memory) + 2 * Block, 16384); + Rewatch(uncommitted); + Require(!Watched(base + 2 * Block, Block), "a block with uncommitted pages returned to the write watch"); + GuestArena::GuestArenaReset_nid_postfix(memory, 4 * Block); + GuestArena::GuestArenaRelease_nid_postfix(memory, 4 * Block); +} + void CheckUntrackedHint() { constexpr std::size_t bytes = 4 * Block; auto* memory = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(bytes, Block)); @@ -938,6 +981,7 @@ int main() { #ifdef _WIN32 CheckDeferredSharedImports(); CheckPrivateMappingReuse(); + CheckRewatch(); CheckUntrackedHint(); CheckHostHoleInChild(); #endif From fcaa4328404f744b0c646b17763990589f9377ca Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Thu, 15 Oct 2026 16:59:49 -0700 Subject: [PATCH 0432/1000] perf(agc): settle Windows imports under 1 MiB without waiting Since #6060 a Windows host import of private write-watched memory stays in the write watch only after its settle, a 4-byte GPU read that the importing thread waits for, and only imports of 1 MiB or more are settled: SuperTuxKart imports a 64 KiB buffer around every submission, and the wait cost it about 19% of its frame rate. A smaller import is compared instead, and its range leaves the write watch for good. Resident Evil Village maps its GPU heap in 64 KiB pieces, so every texture over that heap is compared with guest memory. An import under 1 MiB is still compared when it is made, but its read is now queued. The next import lookup that holds the device lock records the reads queued since into one batch on the settle queue and submits it, so the thread that maps and imports neither submits nor waits. Once the batch's fence has signalled, a later lookup returns the imports' blocks to the write watch, except where storage results are pending over them, which waits for a later lookup. An import retired while its read is in flight waits for that batch first. A settled import whose range had already left the write watch, such as a whole range imported over a piece that was compared, returns to it the same way. --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 5 +- .../Graphics/include/GuestBufferMemory.hpp | 7 + .../Graphics/src/GuestBufferMemory.cpp | 347 +++++++++++++++++- .../prx/libSceAgcDriver/tests/Recorder.cpp | 137 ++++++- .../tests/execution/DccKeysFirstWrite.cpp | 4 + 5 files changed, 487 insertions(+), 13 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 251d38d6a1..1ec1546127 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1062,7 +1062,10 @@ if(BUILD_TESTING) if(WIN32) add_test(NAME agc_import_watch_other_family COMMAND agc_driver_recorder_tests --import-watch-other-family-only) set_tests_properties(agc_import_watch_other_family PROPERTIES SKIP_RETURN_CODE 77) - set_tests_properties(agc_import_watch agc_import_watch_other_family PROPERTIES ENVIRONMENT_MODIFICATION + add_test(NAME agc_import_settle_deferred COMMAND agc_driver_recorder_tests --deferred-settle-only) + add_test(NAME agc_import_settle_deferred_other_family COMMAND agc_driver_recorder_tests --deferred-settle-other-family-only) + set_tests_properties(agc_import_settle_deferred agc_import_settle_deferred_other_family PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 60) + set_tests_properties(agc_import_watch agc_import_watch_other_family agc_import_settle_deferred agc_import_settle_deferred_other_family PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() add_test(NAME agc_unchanged_cpu_stamps COMMAND agc_driver_recorder_tests --unchanged-cpu-stamps-only) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp index 7f932aad60..c0a7a63ad2 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp @@ -70,7 +70,14 @@ void SetHostImportWindow(std::uint64_t bytes); // Tests: the window of one large registered range (64 MiB) and the import budget (0: the configured one). void SetHostImportRangeWindow(std::uint64_t bytes); void SetHostImportBudget(std::uint64_t bytes); +void SetHostImportDeferredSettles(bool enabled); std::uint64_t HostImportLiveBytes(); +struct HostImportSettleCounts { + std::uint64_t submitted = 0; + std::uint64_t waited = 0; + std::uint64_t rewatched = 0; +}; +HostImportSettleCounts HostImportSettles(); bool ImportMappedRanges(const Context& context, const GuestAllocations::Mapped& ranges, std::uint64_t generation, bool adoptDevice); // Whether an existing import covers [address, address + bytes), without reconciling the imports // with the registry or making one (HostImportFor may take a registry lease): a hint for choices diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index 3aa4c9b378..914e4e9ae2 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -160,6 +160,26 @@ struct ImportFailure { GuestAllocations::Mapped ranges; }; +#ifdef _WIN32 +constexpr std::size_t settleBatchCount = 4; +constexpr std::uint32_t settleBatchCopies = 256; + +struct SettleBatch { + VkCommandBuffer commands = VK_NULL_HANDLE; + VkFence fence = VK_NULL_HANDLE; + std::vector bases; + std::vector aliases; + bool busy = false; +}; + +enum class SettlePhase : std::uint8_t { Wanted, Submitted, Settled }; + +struct PendingSettle { + SettlePhase phase = SettlePhase::Wanted; + std::size_t batch = 0; +}; +#endif + // Imports persist across draws, keyed by allocation base, and are dropped when their allocation leaves // the registered set; a dropped import is destroyed once the recorded work that may read it completed. struct HostImports { @@ -193,6 +213,17 @@ struct HostImports { PFN_vkDestroyFence destroyFence = nullptr; PFN_vkDestroyBuffer settleDestroyBuffer = nullptr; PFN_vkFreeMemory settleFreeMemory = nullptr; + PFN_vkWaitForFences settleWaitForFences = nullptr; + VkBuffer settleBatchTarget = VK_NULL_HANDLE; + VkDeviceMemory settleBatchMemory = VK_NULL_HANDLE; + std::array settleBatches{}; + std::map settles; + std::vector settleWanted; + std::vector settleReady; + std::chrono::steady_clock::time_point settlePolled{}; + std::uint64_t settlesSubmitted = 0; + std::uint64_t settlesWaited = 0; + std::uint64_t settlesRewatched = 0; #endif std::uint64_t useCounter = 0; std::uint64_t buildFloor = 0; @@ -203,6 +234,39 @@ HostImports& Imports() { return imports; } +#ifdef _WIN32 +void waitSettleBatch(const HostImports& state, const SettleBatch& batch) { + if (batch.busy && state.settleWaitForFences != nullptr) static_cast(state.settleWaitForFences(state.settleDevice, 1, &batch.fence, VK_TRUE, 10'000'000'000ull)); +} + +bool settleBatchRunning(const HostImports& state, const SettleBatch& batch) { + return batch.busy && state.settleWaitForFences != nullptr && state.settleWaitForFences(state.settleDevice, 1, &batch.fence, VK_TRUE, 0) == VK_TIMEOUT; +} + +void forgetSettle(HostImports& state, std::uint64_t base) { + const auto found = state.settles.find(base); + if (found == state.settles.end()) return; + if (const auto& batch = state.settleBatches[found->second.batch]; found->second.phase == SettlePhase::Submitted && settleBatchRunning(state, batch)) { + ++state.settlesWaited; + waitSettleBatch(state, batch); + } + state.settles.erase(found); +} + +void drainSettles(HostImports& state) { + for (auto& batch : state.settleBatches) { + waitSettleBatch(state, batch); + for (const auto alias : batch.aliases) state.settleDestroyBuffer(state.settleDevice, alias, nullptr); + batch.aliases.clear(); + batch.bases.clear(); + batch.busy = false; + } + state.settles.clear(); + state.settleWanted.clear(); + state.settleReady.clear(); +} +#endif + void destroyImport(const Context& context, const HostImport& entry) { context.Function("vkDestroyBuffer")(context.device, entry.buffer, nullptr); context.Function("vkFreeMemory")(context.device, entry.memory, nullptr); @@ -239,6 +303,9 @@ bool runCovers(const GuestAllocations::Lease& lease, std::uint64_t begin, std::u bool joinedCovers(const GuestAllocations::Lease& lease, std::uint64_t begin, std::uint64_t end); void retireImport(VkDevice device, HostImports& state, std::map::iterator it, const GuestAllocations::Lease& lease, bool hold = true) { +#ifdef _WIN32 + forgetSettle(state, it->first); +#endif // Results shadowed for the import reach its (old) buffer first where the memory is still a // readable registered range (the import retires because its registration vanished or changed // size); the holder below outlives the batch that copies them. @@ -650,21 +717,38 @@ ImportWatchRequest importWatchRequest() { } #endif +bool importsStale(const Context& context, const HostImports& state); + #ifdef _WIN32 constexpr std::uint64_t settleFloorBytes = 0x100000; +constexpr std::uint64_t settleBlockBytes = 0x10000; +constexpr auto settlePollPeriod = std::chrono::microseconds(100); +std::atomic deferredSettles{true}; + +void adoptSettleDevice(const Context& context, HostImports& state) { + if (state.settleDevice == context.device) return; + for (auto& batch : state.settleBatches) { + waitSettleBatch(state, batch); + for (const auto alias : batch.aliases) state.settleDestroyBuffer(state.settleDevice, alias, nullptr); + batch = SettleBatch{}; + } + std::erase_if(state.settles, [](const auto& settle) { return settle.second.phase == SettlePhase::Submitted; }); + state.settleDevice = context.device; + state.settleTarget = VK_NULL_HANDLE; + state.settleMemory = VK_NULL_HANDLE; + state.settleCommands = VK_NULL_HANDLE; + state.settleFence = VK_NULL_HANDLE; + state.settleBatchTarget = VK_NULL_HANDLE; + state.settleBatchMemory = VK_NULL_HANDLE; + state.destroyFence = context.Function("vkDestroyFence"); + state.settleDestroyBuffer = context.Function("vkDestroyBuffer"); + state.settleFreeMemory = context.Function("vkFreeMemory"); + state.settleWaitForFences = context.Function("vkWaitForFences"); +} const char* settleImport(const Context& context, HostImports& state, const HostImport& entry, VkResult& result) { std::unique_lock queueLock(*context.settleQueueMutex); - if (state.settleDevice != context.device) { - state.settleDevice = context.device; - state.settleTarget = VK_NULL_HANDLE; - state.settleMemory = VK_NULL_HANDLE; - state.settleCommands = VK_NULL_HANDLE; - state.settleFence = VK_NULL_HANDLE; - state.destroyFence = context.Function("vkDestroyFence"); - state.settleDestroyBuffer = context.Function("vkDestroyBuffer"); - state.settleFreeMemory = context.Function("vkFreeMemory"); - } + adoptSettleDevice(context, state); if (state.settleTarget == VK_NULL_HANDLE) { VkBufferCreateInfo info{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO}; info.size = 256; @@ -725,6 +809,7 @@ const char* settleImport(const Context& context, HostImports& state, const HostI submit.commandBufferCount = 1; submit.pCommandBuffers = &state.settleCommands; if ((result = context.Function("vkQueueSubmit")(context.settleQueue, 1, &submit, state.settleFence)) != VK_SUCCESS) return "vkQueueSubmit (settle)"; + ++state.settlesWaited; if ((result = context.Function("vkWaitForFences")(context.device, 1, &state.settleFence, VK_TRUE, 10'000'000'000ull)) != VK_SUCCESS) return "vkWaitForFences (settle)"; return nullptr; }; @@ -733,6 +818,194 @@ const char* settleImport(const Context& context, HostImports& state, const HostI if (alias != VK_NULL_HANDLE) context.Function("vkDestroyBuffer")(context.device, alias, nullptr); return step; } + +const char* prepareSettleBatch(const Context& context, HostImports& state, SettleBatch& batch, VkResult& result) { + if (state.settleBatchTarget == VK_NULL_HANDLE) { + VkBufferCreateInfo info{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO}; + info.size = VkDeviceSize{settleBatchCount} * settleBatchCopies * 4u; + info.usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT; + if ((result = context.Function("vkCreateBuffer")(context.device, &info, nullptr, &state.settleBatchTarget)) != VK_SUCCESS) { + state.settleBatchTarget = VK_NULL_HANDLE; + return "vkCreateBuffer (settle batch)"; + } + VkMemoryRequirements requirements{}; + context.Function("vkGetBufferMemoryRequirements")(context.device, state.settleBatchTarget, &requirements); + VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; + allocation.allocationSize = requirements.size; + allocation.memoryTypeIndex = static_cast(std::countr_zero(requirements.memoryTypeBits)); + if ((result = context.Function("vkAllocateMemory")(context.device, &allocation, nullptr, &state.settleBatchMemory)) != VK_SUCCESS || (result = context.Function("vkBindBufferMemory")(context.device, state.settleBatchTarget, state.settleBatchMemory, 0)) != VK_SUCCESS) { + context.Function("vkDestroyBuffer")(context.device, state.settleBatchTarget, nullptr); + if (state.settleBatchMemory != VK_NULL_HANDLE) context.Function("vkFreeMemory")(context.device, state.settleBatchMemory, nullptr); + state.settleBatchTarget = VK_NULL_HANDLE; + state.settleBatchMemory = VK_NULL_HANDLE; + return "the settle batch target"; + } + } + if (batch.commands == VK_NULL_HANDLE) { + VkCommandBufferAllocateInfo allocate{VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO}; + allocate.commandPool = context.settlePool; + allocate.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; + allocate.commandBufferCount = 1; + if ((result = context.Function("vkAllocateCommandBuffers")(context.device, &allocate, &batch.commands)) != VK_SUCCESS) { + batch.commands = VK_NULL_HANDLE; + return "vkAllocateCommandBuffers (settle batch)"; + } + } + if (batch.fence == VK_NULL_HANDLE) { + const VkFenceCreateInfo fenceInfo{VK_STRUCTURE_TYPE_FENCE_CREATE_INFO}; + if ((result = context.Function("vkCreateFence")(context.device, &fenceInfo, nullptr, &batch.fence)) != VK_SUCCESS) { + batch.fence = VK_NULL_HANDLE; + return "vkCreateFence (settle batch)"; + } + } + return nullptr; +} + +void dropWantedSettles(HostImports& state) { + for (const auto base : state.settleWanted) { + if (const auto settle = state.settles.find(base); settle != state.settles.end() && settle->second.phase == SettlePhase::Wanted) state.settles.erase(settle); + } + state.settleWanted.clear(); +} + +void submitSettleBatch(const Context& context, HostImports& state) { + if (state.settleWanted.empty()) return; + const auto free = std::find_if(state.settleBatches.begin(), state.settleBatches.end(), [](const SettleBatch& batch) { return !batch.busy; }); + if (free == state.settleBatches.end()) return; + const auto index = static_cast(free - state.settleBatches.begin()); + auto& batch = *free; + std::unique_lock queueLock(*context.settleQueueMutex); + adoptSettleDevice(context, state); + VkResult result = VK_SUCCESS; + const auto destroyBuffer = context.Function("vkDestroyBuffer"); + const auto fail = [&](const char* step) { + for (const auto alias : batch.aliases) destroyBuffer(context.device, alias, nullptr); + batch.aliases.clear(); + for (const auto base : batch.bases) state.settles.erase(base); + batch.bases.clear(); + dropWantedSettles(state); + std::fprintf(stderr, "[gpu] settling host imports under %llu KiB without waiting failed at %s (%d); they stay compared\n", static_cast(settleFloorBytes >> 10u), step, static_cast(result)); + }; + if (const char* step = prepareSettleBatch(context, state, batch, result)) return fail(step); + if ((result = context.Function("vkResetFences")(context.device, 1, &batch.fence)) != VK_SUCCESS) return fail("vkResetFences (settle batch)"); + if ((result = context.Function("vkResetCommandBuffer")(batch.commands, 0)) != VK_SUCCESS) return fail("vkResetCommandBuffer (settle batch)"); + VkCommandBufferBeginInfo begin{VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO}; + begin.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; + if ((result = context.Function("vkBeginCommandBuffer")(batch.commands, &begin)) != VK_SUCCESS) return fail("vkBeginCommandBuffer (settle batch)"); + const auto copy = context.Resolved(&DeviceFunctions::cmdCopyBuffer, "vkCmdCopyBuffer"); + const bool otherFamily = context.settleFamily != context.queueFamily; + const auto createBuffer = otherFamily ? context.Function("vkCreateBuffer") : nullptr; + const auto bindBufferMemory = otherFamily ? context.Function("vkBindBufferMemory") : nullptr; + std::size_t consumed = 0; + for (; consumed < state.settleWanted.size() && batch.bases.size() < settleBatchCopies; ++consumed) { + const auto base = state.settleWanted[consumed]; + const auto settle = state.settles.find(base); + if (settle == state.settles.end() || settle->second.phase != SettlePhase::Wanted) continue; + const auto import = state.imports.find(base); + if (import == state.imports.end() || !import->second.unwatched) { + state.settles.erase(settle); + continue; + } + VkBuffer source = import->second.buffer; + if (otherFamily) { + VkExternalMemoryBufferCreateInfo external{VK_STRUCTURE_TYPE_EXTERNAL_MEMORY_BUFFER_CREATE_INFO}; + external.handleTypes = VK_EXTERNAL_MEMORY_HANDLE_TYPE_HOST_ALLOCATION_BIT_EXT; + VkBufferCreateInfo aliasInfo{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, &external}; + aliasInfo.size = std::min(import->second.bytes, 65536); + aliasInfo.usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT; + VkBuffer alias = VK_NULL_HANDLE; + if (createBuffer(context.device, &aliasInfo, nullptr, &alias) != VK_SUCCESS) { + state.settles.erase(settle); + continue; + } + if (bindBufferMemory(context.device, alias, import->second.memory, 0) != VK_SUCCESS) { + destroyBuffer(context.device, alias, nullptr); + state.settles.erase(settle); + continue; + } + batch.aliases.push_back(alias); + source = alias; + } + const VkBufferCopy region{0, (VkDeviceSize{index} * settleBatchCopies + batch.bases.size()) * 4u, 4}; + copy(batch.commands, source, state.settleBatchTarget, 1, ®ion); + settle->second = PendingSettle{SettlePhase::Submitted, index}; + batch.bases.push_back(base); + } + state.settleWanted.erase(state.settleWanted.begin(), state.settleWanted.begin() + static_cast(consumed)); + if ((result = context.Function("vkEndCommandBuffer")(batch.commands)) != VK_SUCCESS) return fail("vkEndCommandBuffer (settle batch)"); + if (batch.bases.empty()) return; + VkSubmitInfo submit{VK_STRUCTURE_TYPE_SUBMIT_INFO}; + submit.commandBufferCount = 1; + submit.pCommandBuffers = &batch.commands; + if ((result = context.Function("vkQueueSubmit")(context.settleQueue, 1, &submit, batch.fence)) != VK_SUCCESS) return fail("vkQueueSubmit (settle batch)"); + batch.busy = true; + state.settlesSubmitted += batch.bases.size(); +} + +void reapSettleBatches(const Context& context, HostImports& state) { + const auto status = context.Resolved(&DeviceFunctions::getFenceStatus, "vkGetFenceStatus"); + for (std::size_t index = 0; index < state.settleBatches.size(); ++index) { + auto& batch = state.settleBatches[index]; + if (!batch.busy) continue; + const auto result = status(context.device, batch.fence); + if (result == VK_NOT_READY) continue; + for (const auto base : batch.bases) { + const auto settle = state.settles.find(base); + if (settle == state.settles.end() || settle->second.phase != SettlePhase::Submitted || settle->second.batch != index) continue; + if (result != VK_SUCCESS) { + state.settles.erase(settle); + continue; + } + settle->second.phase = SettlePhase::Settled; + state.settleReady.push_back(base); + } + for (const auto alias : batch.aliases) state.settleDestroyBuffer(context.device, alias, nullptr); + batch.aliases.clear(); + batch.bases.clear(); + batch.busy = false; + } +} + +void rewatchSettledImports(HostImports& state) { + if (state.settleReady.empty()) return; + std::vector> ranges; + std::vector deferred; + for (const auto base : state.settleReady) { + const auto settle = state.settles.find(base); + if (settle == state.settles.end() || settle->second.phase != SettlePhase::Settled) continue; + const auto import = state.imports.find(base); + if (import == state.imports.end() || !import->second.unwatched) { + state.settles.erase(settle); + continue; + } + const std::array, 1> span{{{base, base + import->second.bytes}}}; + if (StorageTexture::AnyPendingOverlaps(span)) { + deferred.push_back(base); + continue; + } + ranges.push_back(span.front()); + state.settles.erase(settle); + } + state.settleReady = std::move(deferred); + if (ranges.empty()) return; + GuestMemory::Rewatch(ranges); + for (const auto& [begin, end] : ranges) { + const auto import = state.imports.find(begin); + if (import == state.imports.end() || !GuestMemory::Watched(begin, static_cast(end - begin))) continue; + import->second.unwatched = false; + ++state.settlesRewatched; + } +} + +void settleUnwatchedImports(const Context& context, HostImports& state) { + if (state.settles.empty() || importsStale(context, state) || context.settleQueue == VK_NULL_HANDLE || context.settleQueueMutex == nullptr || !GuestMemory::GpuMutex().HeldByThisThread()) return; + const auto now = std::chrono::steady_clock::now(); + if (now - state.settlePolled < settlePollPeriod) return; + state.settlePolled = now; + if (state.settleDevice == context.device) reapSettleBatches(context, state); + rewatchSettledImports(state); + submitSettleBatch(context, state); +} #endif void decideImportWatch(const Context& context, HostImports& state) { @@ -1001,7 +1274,6 @@ ImportRun windowRun(const HostImports& state, ImportRun run, std::uint64_t begin } const HostImport* findImport(HostImports& state, std::uint64_t begin, std::uint64_t end); -bool importsStale(const Context& context, const HostImports& state); const HostImport* importRunAt(const Context& context, HostImports& state, ImportRun run, bool joined, std::uint64_t address, const GuestAllocations::Lease& lease, std::uint64_t end, bool evict); // The bytes retireLeastRecent could free outside [begin, end). @@ -1014,6 +1286,9 @@ std::uint64_t evictableBytes(const HostImports& state, std::uint64_t begin, std: } const HostImport* importAllocation(const Context& context, HostImports& state, std::uint64_t address, const GuestAllocations::Lease& lease, std::uint64_t end = 0, bool evict = true) { +#ifdef _WIN32 + settleUnwatchedImports(context, state); +#endif const bool fresh = !importsStale(context, state); if (fresh && end != 0) { if (const auto* current = findImport(state, address, end)) return current; @@ -1208,6 +1483,18 @@ const HostImport* importRunAt(const Context& context, HostImports& state, Import if (trace) std::fprintf(stderr, "[gpu] host import of 0x%llx+0x%llx ok (%zu live, %.1f MiB live, %.1f MiB ever)%s\n", static_cast(base), static_cast(bytes), state.imports.size() + 1, state.liveBytes / 1048576.0, importedBytes / 1048576.0, joined ? ", joined at bind" : ""); entry.joined = joined; entry.lastUse = ++state.useCounter; +#ifdef _WIN32 + if (deferredSettles.load(std::memory_order_relaxed) && entry.alias == nullptr && state.watchPrivateImports && writable && base % settleBlockBytes == 0 && bytes % settleBlockBytes == 0) { + if (entry.unwatched) { + state.settles.insert_or_assign(base, PendingSettle{}); + state.settleWanted.push_back(base); + } else if (settled && !GuestMemory::Watched(base, static_cast(bytes))) { + entry.unwatched = true; + state.settles.insert_or_assign(base, PendingSettle{SettlePhase::Settled, 0}); + state.settleReady.push_back(base); + } + } +#endif return &state.imports.emplace(base, entry).first->second; } @@ -1223,6 +1510,9 @@ const GuestAllocations::Range* containingRange(const GuestAllocations::Lease& le // guest addresses. Walks the imports only when the registry changed since the last walk. void refreshImports(const Context& context, HostImports& state, const GuestAllocations::Lease& lease) { if (state.device != context.device) { +#ifdef _WIN32 + drainSettles(state); +#endif while (!state.imports.empty()) { auto node = state.imports.extract(state.imports.begin()); if (state.device != VK_NULL_HANDLE && state.destroyBuffer != nullptr && state.freeMemory != nullptr) { @@ -1993,6 +2283,11 @@ void reportMirrors() { heapBytes = state.heapBytes; } AgcDriver::ProfilePrint_nid_no_patch("[buffers] image mirrors: %zu ranges (%.1f MiB, %zu writable, %zu heap %.1f MiB), %llu address-based builds served, %llu descriptor sub-ranges bound, %llu sparse heaps bound, %llu rebuilds, %llu refreshes: %llu blocks compared, %llu copied, %llu refresh syncs; heap refills %llu, unwatched heaps %llu, %llu heap mirrors released for imports (%.1f MiB)\n", count, bytes / 1048576.0, writable, heaps, heapBytes / 1048576.0, static_cast(state.builds), static_cast(state.subranges), static_cast(state.sparseBinds), static_cast(state.rebuilds), static_cast(state.refreshes), static_cast(state.blocksCompared), static_cast(state.blocksCopied), static_cast(state.syncs), static_cast(state.heapRefills), static_cast(state.heapUnwatched), static_cast(state.heapImported), state.heapImportedBytes / 1048576.0); +#ifdef _WIN32 + auto& imports = Imports(); + std::lock_guard importsLock(imports.mutex); + if (imports.settlesSubmitted != 0 || !imports.settles.empty()) AgcDriver::ProfilePrint_nid_no_patch("[buffers] host imports under %llu KiB: %llu settled without waiting, %llu back in the write watch, %zu pending\n", static_cast(settleFloorBytes >> 10u), static_cast(imports.settlesSubmitted), static_cast(imports.settlesRewatched), imports.settles.size()); +#endif } // Deferred lease release. The lease an address-based build takes (AcquireRegistered) is dropped by its @@ -2296,6 +2591,15 @@ void ClearHostImports(VkDevice device) { state.parked.erase(device); #ifdef _WIN32 if (state.settleDevice == device) { + drainSettles(state); + for (auto& batch : state.settleBatches) { + if (batch.fence != VK_NULL_HANDLE) state.destroyFence(device, batch.fence, nullptr); + batch = SettleBatch{}; + } + if (state.settleBatchTarget != VK_NULL_HANDLE) state.settleDestroyBuffer(device, state.settleBatchTarget, nullptr); + if (state.settleBatchMemory != VK_NULL_HANDLE) state.settleFreeMemory(device, state.settleBatchMemory, nullptr); + state.settleBatchTarget = VK_NULL_HANDLE; + state.settleBatchMemory = VK_NULL_HANDLE; if (state.settleFence != VK_NULL_HANDLE) state.destroyFence(device, state.settleFence, nullptr); if (state.settleTarget != VK_NULL_HANDLE) state.settleDestroyBuffer(device, state.settleTarget, nullptr); if (state.settleMemory != VK_NULL_HANDLE) state.settleFreeMemory(device, state.settleMemory, nullptr); @@ -2551,17 +2855,38 @@ void SetHostImportBudget(std::uint64_t bytes) { budgetOverride.store(bytes, std::memory_order_relaxed); } +void SetHostImportDeferredSettles(bool enabled) { +#ifdef _WIN32 + deferredSettles.store(enabled, std::memory_order_relaxed); +#else + static_cast(enabled); +#endif +} + std::uint64_t HostImportLiveBytes() { auto& state = Imports(); std::lock_guard lock(state.mutex); return state.liveBytes; } +HostImportSettleCounts HostImportSettles() { +#ifdef _WIN32 + auto& state = Imports(); + std::lock_guard lock(state.mutex); + return {state.settlesSubmitted, state.settlesWaited, state.settlesRewatched}; +#else + return {}; +#endif +} + const HostImport* HostImportFor(const Context& context, std::uint64_t address, std::size_t bytes) { if (context.hostImportAlignment == 0 || bytes == 0 || bytes > std::numeric_limits::max() - address) return nullptr; ensurePinWaiter(); auto& state = Imports(); std::lock_guard lock(state.mutex); +#ifdef _WIN32 + settleUnwatchedImports(context, state); +#endif // A hit is only valid while the registry has not changed since the imports were reconciled. if (!importsStale(context, state)) { if (const auto* entry = findImport(state, address, address + bytes)) { diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 0514717755..7b552949dc 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -1534,6 +1534,132 @@ void storeRunTests(const Device& device, Recorder& recorder) { HostImportFor(context, address, bytes); } +#ifdef _WIN32 +bool deferredSettleTests(const Device& device) { + using namespace AgcDriver::GuestMemory; + const auto& context = device.GetContext(); + constexpr std::size_t unit = 65536; + constexpr std::size_t settledBytes = std::size_t{1} << 20; + if (context.hostImportAlignment == 0 || context.hostImportAlignment > unit || context.settleQueue == VK_NULL_HANDLE || !WriteWatched()) { + std::cout << "host imports within 64 KiB, a settle queue or write watching unavailable: deferred settles not tested\n"; + return false; + } + const auto registerRange = [](void* block, std::size_t bytes) { + GuestAllocations::Mutation mutation; + mutation.Add(block, bytes, true, true, true); + }; + const auto unregisterRange = [](void* block) { + GuestAllocations::Mutation mutation; + mutation.Remove(block); + }; + std::unique_lock gpu(GpuMutex()); + PrepareImportWatch(context); + void* large = AllocateWatched(settledBytes, unit); + void* spacer = AllocateWatched(unit, unit); + void* small = AllocateWatched(unit, unit); + struct Release { + void* large; + void* spacer; + void* small; + ~Release() { + ReleaseWatched(large, settledBytes); + ReleaseWatched(spacer, unit); + ReleaseWatched(small, unit); + } + } release{large, spacer, small}; + const auto largeAddress = reinterpret_cast(large); + Require(reinterpret_cast(small) != largeAddress + settledBytes && reinterpret_cast(small) + unit != largeAddress, "(d) the 64 KiB block is next to the 1 MiB block, so their imports would join"); + registerRange(large, settledBytes); + const auto* settled = HostImportFor(context, largeAddress, settledBytes); + Require(settled != nullptr, "(d) the 1 MiB import was refused"); + if (settled->unwatched) { + std::cout << "this device compares every Windows import: deferred settles not tested\n"; + unregisterRange(large); + HostImportFor(context, largeAddress, settledBytes); + return false; + } + const auto address = reinterpret_cast(small); + std::memset(small, 0x11, unit); + registerRange(small, unit); + const auto poll = [&](const std::function& done) { + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5); + while (!done() && std::chrono::steady_clock::now() < deadline) { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + HostImportFor(context, address, unit); + } + return done(); + }; + const auto* import = HostImportFor(context, address, unit); + Require(import != nullptr && import->unwatched && !Watched(address, unit), "(d) a 64 KiB import was not compared at first"); + Require(poll([&] { return Watched(address, unit); }), "(d) a 64 KiB import did not return to the write watch after its settle"); + const auto* back = HostImportFor(context, address, unit); + Require(back == import && !back->unwatched, "(d) the import back in the write watch is still marked compared"); + BumpCollectEpoch(); + const auto collected = CollectWrites(address, unit); + Require(collected != 0, "(d) the import back in the write watch is not collected"); + static_cast(small)[4096] = 0x22; + CollectWritesUncached(address, unit); + Require(!UnchangedSince(address, unit, collected), "(d) a CPU store after the import returned to the write watch was missed"); + gpu.unlock(); + const auto beforeRounds = HostImportSettles(); + for (int round = 0; round < 64; ++round) { + unregisterRange(small); + registerRange(small, unit); + const auto* again = HostImportFor(context, address, unit); + Require(again != nullptr && again->unwatched && !Watched(address, unit), "(d) a 64 KiB buffer mapped again off the device lock was not imported compared"); + } + const auto afterRounds = HostImportSettles(); + Require(afterRounds.submitted == beforeRounds.submitted && afterRounds.waited == beforeRounds.waited, "(d) importing 64 KiB buffers off the device lock submitted or waited for a settle"); + gpu.lock(); + const auto beforeLocked = HostImportSettles(); + for (int round = 0; round < 64; ++round) { + unregisterRange(small); + registerRange(small, unit); + const auto* again = HostImportFor(context, address, unit); + Require(again != nullptr && again->unwatched && !Watched(address, unit), "(d) a 64 KiB buffer mapped again under the device lock was not imported compared"); + } + const auto afterLocked = HostImportSettles(); + Require(afterLocked.submitted == beforeLocked.submitted && afterLocked.waited == beforeLocked.waited, "(d) mapping a 64 KiB buffer again under the device lock, with no lookup between, submitted or waited for a settle"); + Require(poll([&] { return Watched(address, unit); }), "(d) the last buffer mapped again did not return to the write watch"); + unregisterRange(small); + registerRange(small, unit); + Require(HostImportFor(context, address, unit) != nullptr, "(d) a buffer mapped again after it returned to the write watch was not imported"); + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + HostImportFor(context, address, unit); + unregisterRange(small); + registerRange(small, unit); + const auto* replaced = HostImportFor(context, address, unit); + Require(replaced != nullptr && replaced->unwatched, "(d) a buffer mapped again while its settle may be in flight was not imported"); + Require(poll([&] { return Watched(address, unit); }), "(d) the buffer mapped again while its settle was in flight did not return to the write watch"); + unregisterRange(large); + registerRange(large, unit); + const auto* piece = HostImportFor(context, largeAddress, unit); + Require(piece != nullptr && piece->unwatched, "(d) the first 64 KiB of the large block was not imported compared"); + unregisterRange(large); + registerRange(large, settledBytes); + const auto* whole = HostImportFor(context, largeAddress, settledBytes); + Require(whole != nullptr && whole->bytes == settledBytes, "(d) the large block mapped whole again was not imported whole"); + Require(poll([&] { return Watched(largeAddress, settledBytes); }), "(d) a settled import over a piece that had left the write watch did not return to it"); + SetHostImportDeferredSettles(false); + unregisterRange(small); + registerRange(small, unit); + const auto* kept = HostImportFor(context, address, unit); + Require(kept != nullptr && kept->unwatched, "(d) with deferred settles off, a 64 KiB buffer mapped again was not imported compared"); + for (int round = 0; round < 20; ++round) { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + HostImportFor(context, address, unit); + } + const bool stayed = !Watched(address, unit); + SetHostImportDeferredSettles(true); + Require(stayed, "(d) with deferred settles off, a 64 KiB import returned to the write watch"); + unregisterRange(small); + unregisterRange(large); + HostImportFor(context, address, unit); + std::cout << "deferred settle tests passed\n"; + return true; +} +#endif + void remappedImportTests(const Device& device) { const auto& context = device.GetContext(); if (context.hostImportAlignment == 0) { @@ -8009,7 +8135,7 @@ int main(int argc, char** argv) { } std::unique_ptr created; try { - created = std::make_unique(argc == 2 && std::string_view(argv[1]) == "--import-watch-other-family-only"); + created = std::make_unique(argc == 2 && (std::string_view(argv[1]) == "--import-watch-other-family-only" || std::string_view(argv[1]) == "--deferred-settle-other-family-only")); } catch (const std::exception& error) { if (std::getenv("ANYPS5_REQUIRE_VULKAN") != nullptr) throw; std::cout << "skipped, no usable Vulkan device: " << error.what() << '\n'; @@ -8028,6 +8154,15 @@ int main(int argc, char** argv) { importWatchTests(device); return 0; } +#ifdef _WIN32 + if (argc == 2 && (std::string_view(argv[1]) == "--deferred-settle-only" || std::string_view(argv[1]) == "--deferred-settle-other-family-only")) { + if (std::string_view(argv[1]) == "--deferred-settle-other-family-only" && device.GetContext().settleFamily == device.GetContext().queueFamily) { + std::cout << "skipped, no other queue family to settle imports on\n"; + return 77; + } + return deferredSettleTests(device) ? 0 : 77; + } +#endif if (argc == 2 && std::string_view(argv[1]) == "--host-import-retirement-only") { const auto alignment = device.GetContext().hostImportAlignment; if (alignment == 0 || alignment > 65536) { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DccKeysFirstWrite.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DccKeysFirstWrite.cpp index e9668e78dc..9f844dd5cc 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DccKeysFirstWrite.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DccKeysFirstWrite.cpp @@ -680,6 +680,10 @@ void RunImportWindow(AgcDriver::VulkanDevice& device, std::uint8_t* block) { #ifdef _WIN32 void RunHintedImport(AgcDriver::VulkanDevice& device, std::uint8_t* block) { using namespace AgcDriver::GuestMemory; + AgcDriver::Graphics::SetHostImportDeferredSettles(false); + struct Restore { + ~Restore() { AgcDriver::Graphics::SetHostImportDeferredSettles(true); } + } restore; const auto address = AddressOf(block); std::memset(block, 0x55, BlockBytes); auto descriptor = TextureDescriptor(block, nullptr); From bc27ba7d071e361e15594ef003f58c32dffd13a8 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Thu, 15 Oct 2026 17:07:22 -0700 Subject: [PATCH 0433/1000] docs(agc): describe the settle of Windows imports under 1 MiB ARCHITECTURE.md and the debt entry describe how imports under 1 MiB are settled without waiting and return to the write watch, which the debt entry listed as not implemented. --- docs/dev/ARCHITECTURE.md | 2 +- docs/dev/debt/libSceAgcDriver.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/dev/ARCHITECTURE.md b/docs/dev/ARCHITECTURE.md index 54b4476de1..c425f5b66b 100644 --- a/docs/dev/ARCHITECTURE.md +++ b/docs/dev/ARCHITECTURE.md @@ -107,7 +107,7 @@ flowchart LR - A draw whose `PA_SC_AA_CONFIG` selects 2, 4 or 8 samples renders into host-only multisampled images. [`MultisampleTarget.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) keeps one image per device, surface address, format, extent and sample count (`MultisampleTargetView`); the pipeline, render pass and library keys carry `State::samples`, and a multisampled depth target is a multisampled depth surface. The guest sample planes and FMASK are never read or written, so the registry also guards the target's guest range: a CPU read (`GuestMemory::SetReadGuard`), a DMA copy, and a sampled or storage image read of it throw. A `CB_COLOR_CONTROL` resolve mode over a multisampled CB0 runs as a render pass whose resolve attachment is CB1's resident image (`ResolveMultisampleTarget`). A zero fill of the target's CMASK (`NoteColorMetadataFill`, from fill kernels and CP DMA fills) clears the image at its next use. - A texture over a depth surface samples the depth image itself when it views one 2D slice of the plane's own format (`DepthSurfaceTexture` in [`DepthSurface.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp)). A view of several slices (a 2D array or a cube) or an integer view of a D16 plane is served by a `DepthPlaneCopy`, an image the slices are copied into at every use: from the depth surface bound at each slice's address, else from guest memory. Such a texture is never kept in the resource templates (`RefreshedPerUse`). - Guest memory reaches the GPU through host imports (`VK_EXT_external_memory_host`) where the driver accepts them, and through copies otherwise ([`GuestBufferMemory.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp)). A registered range larger than the import window (64 MiB) is imported per window for buffer and fill accesses and whole for an address-based build. Live imports stay under a budget, with the least recently used import retired first (see TechnicalDebt, Host import windows and budget). -- On Windows, a host import of private `MEM_WRITE_WATCH` memory (singly mapped direct memory) stays on `GetWriteWatch` when `ProbeImportWriteProtection` shows the driver reports nothing written after the second GPU read and a GPU write, and a CPU store is collected (`decideImportWatch`). The driver marks every page of such an import written, asynchronously, until its first GPU use completes, so `settleImport` in [`GuestBufferMemory.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) copies 4 bytes out of the new import on a second queue of the graphics family, or on a queue of another copy-capable family when the graphics family has only one (`Context::settleQueue`, own pool and mutex) inside `ImportWatched`'s window and waits for it, only for imports of 1 MiB or more (a smaller import is compared, so a title that maps, imports and unmaps a small buffer around every submission does not block its mapping thread); the main queue can't be used there because `importAllocation` holds the imports lock and `GpuMutex` comes before it in the lock order. Imports through a section alias keep tracking on the guest views, and every other Windows host import is compared. A device without such a queue keeps the comparisons. +- On Windows, a host import of private `MEM_WRITE_WATCH` memory (singly mapped direct memory) stays on `GetWriteWatch` when `ProbeImportWriteProtection` shows the driver reports nothing written after the second GPU read and a GPU write, and a CPU store is collected (`decideImportWatch`). The driver marks every page of such an import written, asynchronously, until its first GPU use completes, so `settleImport` in [`GuestBufferMemory.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) copies 4 bytes out of the new import on a second queue of the graphics family, or on a queue of another copy-capable family when the graphics family has only one (`Context::settleQueue`, own pool and mutex) inside `ImportWatched`'s window and waits for it, for imports of 1 MiB or more; the main queue can't be used there because `importAllocation` holds the imports lock and `GpuMutex` comes before it in the lock order. A smaller import is compared when it is made, so a title that maps, imports and unmaps a small buffer around every submission does not block its mapping thread. Its 4-byte read is queued instead: the next import lookup that holds `GpuMutex` while the imports match the registry (`settleUnwatchedImports`) records the queued reads into one batch on the settle queue, and once the batch's fence has signalled a later lookup returns the import's whole 64 KiB blocks to the write watch (`GuestMemory::Rewatch` drops their marks and stamps every block as a CPU store), except while storage results are pending over them. A settled import whose range had already left the watch returns the same way, and an import retired while its read is in flight waits for that batch. Imports through a section alias keep tracking on the guest views, and every other Windows host import is compared. A device without such a queue keeps the comparisons. ### Runtime image and sampler tables diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 21512697ed..b733145d09 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -72,8 +72,8 @@ - [Draw input snapshots](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver): `PrepareDrawBindings` caps read-only input buffer snapshots at 64 MiB (overridable via `APS5_DRAW_SNAPSHOT_MAX_MIB`). Larger inputs (streaming heaps, bindless pools) or draws exceeding the cumulative budget skip the per-draw snapshot and bind in place, avoiding multi-gigabyte CPU copy stalls (such as Ratchet & Clank: Rift Apart PPSA01474 stalling at frame ~380 copying 36 GB every 5 s). If the title rewrites those large ranges on the CPU before GPU completion, the in-place binding observes the modification, as verified by `drawSnapshotCapTests`. - [Pending storage-image results](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) are stored when something reads their memory, not when the GPU work that wrote them completes. A title that waits for that work, frees the image and reuses its memory can have the new data overwritten by the late store (#1970). A title's own CPU reads are not seen either, so a title that reads the results after waiting for a label reads stale memory. `APS5_STORE_AT_FLIP=1` stores every pending result at each flip and `APS5_STORE_AT_LABEL=1` before each label the GPU writes; both store every pending image, not only the ones the waited-for work wrote, and neither is on by default. - [Pending storage-image results over memory outside the write watch](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) leave to the CPU the 64 KiB blocks whose bytes differ from the ones saved at the upload, the fill, the image copy, the previous store or the moment their range left the watch (`Unwatch`, or a host import that does not stay watched), and the blocks the watch saw the CPU write before that moment. A store made on the GPU saves its bytes through a host-visible copy that becomes the saved bytes when its batch completes; a store or a refresh that needs them earlier waits for that batch. A unit without a write generation saves the guest bytes when results land in it outside the watch. One whose range leaves the watch while its results are pending, and ranges that leave the watch through a failed collect, have no saved bytes; storing such results throws. -- Windows host-pointer imports of the guest mapping itself are compared instead of write-watched, except writable imports of private `MEM_WRITE_WATCH` memory of 1 MiB or more when the import probe keeps the write watch and a settle queue exists (see Pages under an import window): a driver can mark imported pages dirty after the synchronous import window. Shared memory imported through a separate writable alias retains protection-based tracking on the guest views; driver access to the import alias does not disarm those views. [GuestMemory](../../../core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp) excludes every overlapping 64 KiB tracker block while its backing is retained. A full block replaced by fresh private backing regains tracking and gets a new generation; protection changes, partial replacements and shared remappings do not restore it. Excluded blocks keep their write-watch marks as a hint only (`UntrackedHint`): a storage image or a sampled texture snapshot over them compares its bytes again after a mark in its range, a driver store, a mapping change or a recorded GPU write instead of at every collect epoch or lookup, and a mark the driver makes without a CPU store costs one comparison. This preserves CPU edits through byte comparisons but limits snapshot reuse on excluded blocks to draws of one collect epoch and disables unit shadows for them; restoring tracking on import retirement without replacing the backing remains unimplemented. -- [Pages under an import window](../../../core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp) (libSceAgcDriver) whose write protection was resolved while a host import that leaves its range watched was made are compared with the bytes they held before the import and count as stored over only when those differ. A CPU store of the bytes already there within the window cannot be told apart from an import that resolves the write protection without storing, as the NVIDIA driver (610.57.04) does for every imported page, so such a store does not win over pending results. Where host-pointer and dma-buf imports differ in whether they stay watched, a failed host-pointer import and the dma-buf import after it are two windows, and the pages the failed one resolved count as stored over. On Windows, the NVIDIA driver (617.42) marks every page of a host import of private `MEM_WRITE_WATCH` memory written, asynchronously, until the import's first GPU use completes. Such an import stays watched only after a 4-byte read of it on the settle queue has completed inside the window. Each one waits for that read, which keeps its window open for one round trip on the settle queue, so a same-value CPU store from another thread in that time counts as unchanged too. An import made where no settle queue exists, and an import of under 1 MiB, is compared instead: the settle blocks the importing thread, and a title that imports a 64 KiB buffer around every submission lost about 19 % of its frame rate to that wait (Dripl1x, #6060). Settling without blocking the importing thread (submit the read, keep the import compared until its fence signals, then switch it to the write watch) is not implemented. +- Windows host-pointer imports of the guest mapping itself are compared instead of write-watched, except writable imports of private `MEM_WRITE_WATCH` memory when the import probe keeps the write watch and a settle queue exists: those of 1 MiB or more stay watched, and smaller ones return to the watch once their settle has completed (see Pages under an import window): a driver can mark imported pages dirty after the synchronous import window. Shared memory imported through a separate writable alias retains protection-based tracking on the guest views; driver access to the import alias does not disarm those views. [GuestMemory](../../../core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp) excludes every overlapping 64 KiB tracker block while its backing is retained. A full block replaced by fresh private backing regains tracking and gets a new generation; protection changes, partial replacements and shared remappings do not restore it. Excluded blocks keep their write-watch marks as a hint only (`UntrackedHint`): a storage image or a sampled texture snapshot over them compares its bytes again after a mark in its range, a driver store, a mapping change or a recorded GPU write instead of at every collect epoch or lookup, and a mark the driver makes without a CPU store costs one comparison. This preserves CPU edits through byte comparisons but limits snapshot reuse on excluded blocks to draws of one collect epoch and disables unit shadows for them; restoring tracking on import retirement without replacing the backing remains unimplemented. +- [Pages under an import window](../../../core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp) (libSceAgcDriver) whose write protection was resolved while a host import that leaves its range watched was made are compared with the bytes they held before the import and count as stored over only when those differ. A CPU store of the bytes already there within the window cannot be told apart from an import that resolves the write protection without storing, as the NVIDIA driver (610.57.04) does for every imported page, so such a store does not win over pending results. Where host-pointer and dma-buf imports differ in whether they stay watched, a failed host-pointer import and the dma-buf import after it are two windows, and the pages the failed one resolved count as stored over. On Windows, the NVIDIA driver (617.42) marks every page of a host import of private `MEM_WRITE_WATCH` memory written, asynchronously, until the import's first GPU use completes. Such an import stays watched only after a 4-byte read of it on the settle queue has completed inside the window. Each one waits for that read, which keeps its window open for one round trip on the settle queue, so a same-value CPU store from another thread in that time counts as unchanged too. An import made where no settle queue exists is compared instead. An import of under 1 MiB is not settled inside its window, because the settle blocks the importing thread and a title that imports a 64 KiB buffer around every submission lost about 19 % of its frame rate to that wait (Dripl1x, #6060). It is compared until its read, submitted in a batch by a later import lookup under `GpuMutex` while the imports match the registry, has completed; a later lookup then returns its whole blocks to the write watch, each stamped as a CPU store, so whatever was derived from them while they were compared reads as changed once. Its blocks stay compared while storage results are pending over them, and those of an import retired before that stay compared. - [Sparse dma-buf imports](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver): on Linux a shared-backed range larger than one 64 MiB udmabuf buffer is imported as one sparse buffer bound to the 64 MiB buffers, on a queue of its own when the device has a second family that binds sparse memory. The path is decided before anything is imported, and a failure after that throws. A range takes the single udmabuf import instead, which udmabuf's `size_limit_mb` refuses above 64 MiB by default so the range is copied, when its offset in the backing file is not a multiple of the buffer's sparse block size (64 KiB on RADV), when the sparse buffer does not accept every memory type a chunk import can have, when the backing file ends inside its last chunk, or when it is imported at map time on a device with no second family (the bind then needs the GPU mutex); the NVIDIA proprietary driver imports no dma-buf memory - [Unnormalized S# coordinates](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp) (libSceAgcDriver) - `FORCE_UNNORMALIZED` uses a Vulkan `unnormalizedCoordinates` sampler, so the S# LOD range, LOD bias and mip filter are not applied, which is exact only on a one-level view. - An [unnormalized S#](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp) (libSceAgcDriver) maps clamp mode 0 (repeat) on X and Y to clamp-to-edge as required by Vulkan unnormalized coordinates, and assigns the magnification filter when minification and magnification filters differ (since unnormalized coordinates sample at LOD 0). Anisotropy, TRUNC_COORD, MC_COORD_TRUNC and other clamp modes still throw. Secondary LOD bias (LOD_BIAS_SEC, bits [19:14]) is accepted and ignored, matching RDNA2 hardware measurements across explicit and implicit samples. High on Life (PPSA15388, #4736) uses secondary LOD bias and unnormalized coordinates with clamp mode 0 and differing min/mag filters during home base and city rendering. From 966da79fcbb59513dfa009753b4c1104bad1c94d Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Fri, 16 Oct 2026 02:14:46 +0000 Subject: [PATCH 0434/1000] fix(agc): stop and join the draw pipeline's committer at shutdown The committer of pipelined draws, started at the first pipelined submission, was never stopped. The driver's shutdown joined the queue workers and left the committer parked on its queue, and a commit the last submission left queued, such as its deferred completion, could still run after the shutdown had reset the device. A process that exited right after its first pipelined submissions could kill the committer inside its first wait on the queue, which takes winpthreads' global condition-variable lock, as with the preparation workers. The driver's shutdown now stops the committer after joining the queue workers and before taking the GPU lock. The committer runs the commits still queued and leaves, and the shutdown joins it. A commit enqueued after that runs on the caller. --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 5 +-- .../include/Driver/Draw/DrawPipeline.hpp | 3 ++ .../src/Driver/Draw/DrawPipeline.cpp | 33 +++++++++++++++++-- .../Execution/src/Driver/Lifecycle.cpp | 2 ++ .../tests/execution/ShutdownThreads.cpp | 29 ++++++++++++++++ 5 files changed, 68 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index f636f2299b..4381f418f6 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1771,9 +1771,10 @@ if(BUILD_TESTING) endforeach() add_test(NAME agc_driver_shutdown_threads_joined COMMAND agc_driver_shutdown_threads_tests --joined) add_test(NAME agc_driver_shutdown_threads_compare_helpers COMMAND agc_driver_shutdown_threads_tests --compare-helpers) - set_tests_properties(agc_driver_shutdown_threads_joined agc_driver_shutdown_threads_compare_helpers PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" TIMEOUT 30) + add_test(NAME agc_driver_shutdown_threads_draw_committer COMMAND agc_driver_shutdown_threads_tests --draw-committer) + set_tests_properties(agc_driver_shutdown_threads_joined agc_driver_shutdown_threads_compare_helpers agc_driver_shutdown_threads_draw_committer PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" TIMEOUT 30) if(WIN32) - set_tests_properties(agc_driver_shutdown_threads_joined agc_driver_shutdown_threads_compare_helpers PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + set_tests_properties(agc_driver_shutdown_threads_joined agc_driver_shutdown_threads_compare_helpers agc_driver_shutdown_threads_draw_committer PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() add_test(NAME agc_driver_compute_user_data_calls_registered COMMAND agc_driver_compute_user_data_calls_tests --registered) set_tests_properties(agc_driver_compute_user_data_calls_registered PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp index d89415137c..ca18d03a3d 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp @@ -29,9 +29,11 @@ public: static bool& Active(); static std::atomic& EpochToken(); static void FollowEpoch(std::uint64_t token); + static unsigned Committers(); void Enqueue(Commit commit, std::vector writes, std::uint64_t labelAddress = 0, std::vector labelBytes = {}); void Drain(DrainReason reason, std::uint32_t opcode = 0x100); + void Stop(); bool Busy() const { return outstanding.load(std::memory_order_acquire) != 0; } std::size_t Outstanding() const { return outstanding.load(std::memory_order_acquire); } bool Overlaps(std::uint64_t address, std::size_t bytes); @@ -58,6 +60,7 @@ private: std::uint32_t idleWaiters = 0; bool committerWaiting = false; bool stopped = false; + bool closing = false; std::thread thread; std::uint64_t commits = 0; std::uint64_t commitNs = 0; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp index b9ddd2c33f..d3f3553c5a 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp @@ -14,6 +14,8 @@ namespace { constexpr std::array(DrawPipeline::DrainReason::Count)> DrainNames{"packet", "flush", "labels", "capture", "indirect", "submission"}; +std::atomic runningCommitters{0}; + std::uint64_t elapsedNs(std::chrono::steady_clock::time_point since) { return static_cast(std::chrono::duration_cast(std::chrono::steady_clock::now() - since).count()); } @@ -53,6 +55,10 @@ void DrawPipeline::FollowEpoch(std::uint64_t token) { GuestMemory::BumpCollectEpoch(); } +unsigned DrawPipeline::Committers() { + return runningCommitters.load(std::memory_order_acquire); +} + void DrawPipeline::rethrowFailure() { if (failure == nullptr) return; auto error = std::exchange(failure, nullptr); @@ -63,6 +69,11 @@ void DrawPipeline::Enqueue(Commit commit, std::vector writes, std::uint64 static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; std::unique_lock lock(mutex); rethrowFailure(); + if (closing) { + lock.unlock(); + commit(); + return; + } if (items.size() >= Depth()) { if (GuestMemory::GpuMutex().HeldByThisThread()) throw std::runtime_error("draw pipeline full while the enqueuing thread holds the GPU lock"); const auto start = std::chrono::steady_clock::now(); @@ -76,7 +87,10 @@ void DrawPipeline::Enqueue(Commit commit, std::vector writes, std::uint64 ++enqueued; items.push_back({std::move(commit), std::move(writes), labelAddress, std::move(labelBytes)}); outstanding.fetch_add(1, std::memory_order_release); - if (!thread.joinable()) thread = std::thread([this] { run(); }); + if (!thread.joinable()) { + thread = std::thread([this] { run(); }); + runningCommitters.fetch_add(1, std::memory_order_acq_rel); + } if (committerWaiting) wake.notify_one(); if (!profile) return; const auto now = std::chrono::steady_clock::now(); @@ -116,8 +130,9 @@ void DrawPipeline::run() { { std::unique_lock lock(mutex); committerWaiting = true; - wake.wait(lock, [&] { return !items.empty(); }); + wake.wait(lock, [&] { return !items.empty() || closing; }); committerWaiting = false; + if (items.empty()) break; commit = stopped ? nullptr : &items.front().commit; } const auto start = std::chrono::steady_clock::now(); @@ -149,6 +164,20 @@ void DrawPipeline::run() { outstanding.fetch_sub(1, std::memory_order_release); if (idleWaiters != 0) idle.notify_all(); } + runningCommitters.fetch_sub(1, std::memory_order_acq_rel); +} + +void DrawPipeline::Stop() { + std::thread committer; + { + std::lock_guard lock(mutex); + closing = true; + committer = std::move(thread); + } + wake.notify_all(); + if (!committer.joinable()) return; + if (committer.get_id() == std::this_thread::get_id()) committer.detach(); + else committer.join(); } void DrawPipeline::Drain(DrainReason reason, std::uint32_t opcode) { diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp index 4b2da77999..0258a86a93 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp @@ -1,5 +1,6 @@ #include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Execution/include/WorkerSampler.hpp" @@ -114,6 +115,7 @@ void Driver::stop() { for (auto& [queue, worker] : workers) { if (worker.thread.joinable()) worker.thread.join(); } + DrawPipeline::Queue0().Stop(); StopWorkerSampler(); CloseRegistrationPreparation(); Graphics::StopPipelineLibraryWorker(); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ShutdownThreads.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ShutdownThreads.cpp index a29d4e2e3a..729c855c1c 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ShutdownThreads.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ShutdownThreads.cpp @@ -1,3 +1,5 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp" #include "prx/libSceAgcDriver/Graphics/include/ParallelCompare.hpp" #include @@ -63,6 +65,32 @@ void StoppingJoinsCompareHelpers() { Require(CompareHelpersRunning() == 0, "a compare after the helpers stopped started them again"); } +void ShutdownJoinsDrawCommitter() { + using namespace AgcDriver::DriverDetail; + auto& pipeline = DrawPipeline::Queue0(); + const auto caller = std::this_thread::get_id(); + std::promise started; + auto startedOn = started.get_future(); + std::promise gate; + auto opened = gate.get_future().share(); + bool queuedRan = false; + pipeline.Enqueue([&started, opened] { + started.set_value(std::this_thread::get_id()); + opened.wait(); + }, {}); + pipeline.Enqueue([&] { queuedRan = true; }, {}); + Require(startedOn.get() != caller, "a pipelined commit ran on the caller instead of the draw committer"); + Require(DrawPipeline::Committers() == 1, "a pipelined commit started no draw committer"); + gate.set_value(); + AgcDriverShutdown_nid_postfix(); + Require(DrawPipeline::Committers() == 0, "the driver's shutdown left the draw committer running"); + Require(queuedRan, "the driver's shutdown dropped a pipelined commit"); + bool ranHere = false; + pipeline.Enqueue([&] { ranHere = std::this_thread::get_id() == caller; }, {}); + Require(ranHere, "a commit enqueued after the shutdown ran on another thread"); + Require(DrawPipeline::Committers() == 0, "a commit enqueued after the shutdown started a draw committer"); +} + } int main(int argc, char** argv) { @@ -70,6 +98,7 @@ int main(int argc, char** argv) { const std::string_view mode = argc > 1 ? argv[1] : ""; if (mode == "--joined") ClosingJoinsPreparation(); else if (mode == "--compare-helpers") StoppingJoinsCompareHelpers(); + else if (mode == "--draw-committer") ShutdownJoinsDrawCommitter(); else ClosingStartsNoPreparation(); std::cout << "shutdown threads tests passed\n"; return 0; From 6522b92fdeb1b5e793a5aa0a7e23a9e15c139836 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Fri, 16 Oct 2026 02:22:08 +0000 Subject: [PATCH 0435/1000] fix(agc): stop and join the shader disk cache writer at shutdown The shader disk cache's writer, started with the cache at the first shader lookup, was detached and never stopped. It stayed parked in a timed wait on its queue until the process exit killed it, and entries still queued at exit were never written. A process that exited right after its first shaders could kill the writer inside its first wait, which takes winpthreads' global condition-variable lock, as with the driver's own threads. The writer is now joinable. The driver's shutdown stops it after the other driver threads: the writer writes the entries still queued and leaves, and the shutdown joins it. A store after that is written on the caller, and a cache first used after the shutdown starts no writer. ShaderDiskCache::Shutdown joins the names the cache key check has reviewed, since it changes nothing a recompile produces. --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 7 +- .../Execution/src/Driver/Lifecycle.cpp | 2 + .../tests/execution/ShutdownThreads.cpp | 38 +++++++++++ core/shader/recompiler/CMakeLists.txt | 1 + core/shader/recompiler/ShaderDiskCache.cpp | 68 +++++++++++++++++-- core/shader/recompiler/ShaderDiskCache.hpp | 2 + 6 files changed, 111 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 4381f418f6..d5e41f434a 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1451,6 +1451,8 @@ target_include_directories(agc_driver_prepared_exports_tests PRIVATE $) +target_include_directories(agc_driver_shutdown_threads_tests PRIVATE $) + target_include_directories(agc_driver_prepared_publication_tests PRIVATE $) target_include_directories(agc_driver_prepared_graphics_tests PRIVATE $) @@ -1772,9 +1774,10 @@ if(BUILD_TESTING) add_test(NAME agc_driver_shutdown_threads_joined COMMAND agc_driver_shutdown_threads_tests --joined) add_test(NAME agc_driver_shutdown_threads_compare_helpers COMMAND agc_driver_shutdown_threads_tests --compare-helpers) add_test(NAME agc_driver_shutdown_threads_draw_committer COMMAND agc_driver_shutdown_threads_tests --draw-committer) - set_tests_properties(agc_driver_shutdown_threads_joined agc_driver_shutdown_threads_compare_helpers agc_driver_shutdown_threads_draw_committer PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" TIMEOUT 30) + add_test(NAME agc_driver_shutdown_threads_shader_cache_writer COMMAND agc_driver_shutdown_threads_tests --shader-cache-writer) + set_tests_properties(agc_driver_shutdown_threads_joined agc_driver_shutdown_threads_compare_helpers agc_driver_shutdown_threads_draw_committer agc_driver_shutdown_threads_shader_cache_writer PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" TIMEOUT 30) if(WIN32) - set_tests_properties(agc_driver_shutdown_threads_joined agc_driver_shutdown_threads_compare_helpers agc_driver_shutdown_threads_draw_committer PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + set_tests_properties(agc_driver_shutdown_threads_joined agc_driver_shutdown_threads_compare_helpers agc_driver_shutdown_threads_draw_committer agc_driver_shutdown_threads_shader_cache_writer PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() add_test(NAME agc_driver_compute_user_data_calls_registered COMMAND agc_driver_compute_user_data_calls_tests --registered) set_tests_properties(agc_driver_compute_user_data_calls_registered PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp index 0258a86a93..60c05d2fca 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp @@ -10,6 +10,7 @@ #include "prx/libSceAgcDriver/Graphics/include/ParallelCompare.hpp" #include "prx/libSceAgcDriver/Graphics/include/PipelineLibrary.hpp" #include "prx/libc/include/GuestAllocations.hpp" +#include "ShaderDiskCache.hpp" #include #include #include @@ -124,6 +125,7 @@ void Driver::stop() { replacedDevices.clear(); Graphics::ShutdownGuestBufferWorkers(); Graphics::StopCompareHelpers(); + ShaderRecompiler::ShaderDiskCache::Shutdown(); PerformanceTrace::StopOutput(); StopProfileOutput_nid_no_patch(); stopped = true; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ShutdownThreads.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ShutdownThreads.cpp index 729c855c1c..747d01164a 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ShutdownThreads.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ShutdownThreads.cpp @@ -2,13 +2,21 @@ #include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp" #include "prx/libSceAgcDriver/Graphics/include/ParallelCompare.hpp" +#include "CompiledVariant.hpp" +#include "ShaderDiskCache.hpp" #include #include #include +#include +#include #include #include +#include +#include #include +#include #include +#include #include #include @@ -18,6 +26,14 @@ void Require(bool condition, const char* message) { if (!condition) throw std::runtime_error(message); } +void SetEnvironment(const char* name, const std::string& value) { +#ifdef _WIN32 + _putenv_s(name, value.c_str()); +#else + setenv(name, value.c_str(), 1); +#endif +} + void ClosingStartsNoPreparation() { using namespace AgcDriver::DriverDetail; Require(RegistrationPreparationThreads() == 0, "a shader preparation thread ran before anything was prepared"); @@ -91,6 +107,27 @@ void ShutdownJoinsDrawCommitter() { Require(DrawPipeline::Committers() == 0, "a commit enqueued after the shutdown started a draw committer"); } +void ShutdownJoinsShaderCacheWriter() { + namespace Cache = ShaderRecompiler::ShaderDiskCache; + const auto directory = std::filesystem::temp_directory_path() / ("aps5-shutdown-shader-cache-" + std::to_string(std::random_device{}())); + SetEnvironment("ANYPS5_NO_SHADER_CACHE", "0"); + SetEnvironment("ANYPS5_SHADER_CACHE_DIR", directory.string()); + Require(Cache::Enabled(), "the shader cache is not enabled"); + Require(Cache::Writers() == 1, "the shader cache started no writer"); + const auto variant = std::make_shared(); + const std::vector queued{std::byte{1}}; + const std::vector late{std::byte{2}}; + Cache::Store(queued, variant); + AgcDriverShutdown_nid_postfix(); + Require(Cache::Writers() == 0, "the driver's shutdown left the shader cache writer running"); + Require(std::filesystem::exists(Cache::EntryDirectory() / Cache::EntryName(queued)), "the driver's shutdown dropped a queued shader cache write"); + Cache::Store(late, variant); + Require(std::filesystem::exists(Cache::EntryDirectory() / Cache::EntryName(late)), "a shader cache store after the shutdown was not written at once"); + Require(Cache::Writers() == 0, "a shader cache store after the shutdown started a writer"); + std::error_code error; + std::filesystem::remove_all(directory, error); +} + } int main(int argc, char** argv) { @@ -99,6 +136,7 @@ int main(int argc, char** argv) { if (mode == "--joined") ClosingJoinsPreparation(); else if (mode == "--compare-helpers") StoppingJoinsCompareHelpers(); else if (mode == "--draw-committer") ShutdownJoinsDrawCommitter(); + else if (mode == "--shader-cache-writer") ShutdownJoinsShaderCacheWriter(); else ClosingStartsNoPreparation(); std::cout << "shutdown threads tests passed\n"; return 0; diff --git a/core/shader/recompiler/CMakeLists.txt b/core/shader/recompiler/CMakeLists.txt index 18cea782dc..28f1c5b11e 100644 --- a/core/shader/recompiler/CMakeLists.txt +++ b/core/shader/recompiler/CMakeLists.txt @@ -254,6 +254,7 @@ function(add_shader_recompiler target runtimeTarget) SampledTextureNumericClass SetDebugProbeActive ShaderCacheDirectory + ShaderDiskCache::Shutdown ShaderStage SrgbDecodeBit SrgbUnormFormat diff --git a/core/shader/recompiler/ShaderDiskCache.cpp b/core/shader/recompiler/ShaderDiskCache.cpp index 0656a8fb3f..804f3dcd3f 100644 --- a/core/shader/recompiler/ShaderDiskCache.cpp +++ b/core/shader/recompiler/ShaderDiskCache.cpp @@ -1311,15 +1311,20 @@ LoadStatus unwrapEntry(std::span file, std::span runningWriters{0}; + class DiskStore { public: - DiskStore() { + explicit DiskStore(bool stopped) { const auto root = ShaderCacheDirectory(); if (root.empty()) return; this->root = root; directory = root / hex(SourceVersion()); discoverPrebuilt(); - std::thread([this] { run(); }).detach(); + stopping = stopped; + if (stopping) return; + writer = std::thread([this] { run(); }); + runningWriters.fetch_add(1, std::memory_order_acq_rel); } [[nodiscard]] bool Enabled() const { return !directory.empty(); } @@ -1363,6 +1368,19 @@ public: idle.wait(lock, [&] { return jobs.empty() && !writing; }); } + void Shutdown() { + std::thread stopped; + { + std::lock_guard lock(mutex); + stopping = true; + stopped = std::move(writer); + } + wake.notify_all(); + if (!stopped.joinable()) return; + if (stopped.get_id() == std::this_thread::get_id()) stopped.detach(); + else stopped.join(); + } + Counters Totals() const { Counters counters; counters.hits = hits.load(std::memory_order_relaxed); @@ -1520,7 +1538,12 @@ private: void enqueue(Job job) { { - std::lock_guard lock(mutex); + std::unique_lock lock(mutex); + if (stopping) { + lock.unlock(); + write(job); + return; + } jobs.push_back(std::move(job)); } wake.notify_one(); @@ -1531,7 +1554,7 @@ private: housekeeping(); std::unique_lock lock(mutex); while (true) { - wake.wait_for(lock, std::chrono::seconds(10), [&] { return !jobs.empty(); }); + wake.wait_for(lock, std::chrono::seconds(10), [&] { return !jobs.empty() || stopping; }); while (!jobs.empty()) { auto job = std::move(jobs.front()); jobs.pop_front(); @@ -1543,10 +1566,12 @@ private: writing = false; } idle.notify_all(); + if (stopping) break; lock.unlock(); report(false); lock.lock(); } + runningWriters.fetch_sub(1, std::memory_order_acq_rel); } void write(const Job& job) { @@ -1628,6 +1653,8 @@ private: std::condition_variable idle; std::deque jobs; bool writing = false; + bool stopping = false; + std::thread writer; std::atomic hits{0}, misses{0}, writes{0}, loadFailures{0}, writeFailures{0}, bytesRead{0}, bytesWritten{0}, loadNanoseconds{0}; std::atomic planHits{0}, planMisses{0}, planWrites{0}, planLoadFailures{0}; std::atomic prebuiltHits{0}, prebuiltMisses{0}, prebuiltRejected{0}, lookupNanoseconds{0}, readNanoseconds{0}, decodeNanoseconds{0}, maxLoadNanoseconds{0}, prebuiltBytesRead{0}, prebuiltLoadNanoseconds{0}; @@ -1637,8 +1664,24 @@ private: std::uint64_t reportedLoadNs = 0; }; +struct StoreLifecycle { + std::mutex mutex; + DiskStore* created = nullptr; + bool stopped = false; +}; + +StoreLifecycle& lifecycle() { + static auto* state = new StoreLifecycle(); + return *state; +} + DiskStore& store() { - static DiskStore* instance = new DiskStore(); + static DiskStore* instance = [] { + auto& state = lifecycle(); + std::lock_guard lock(state.mutex); + state.created = new DiskStore(state.stopped); + return state.created; + }(); return *instance; } @@ -1795,6 +1838,21 @@ void Flush() { if (instance.Enabled()) instance.Flush(); } +void Shutdown() { + DiskStore* instance = nullptr; + { + auto& state = lifecycle(); + std::lock_guard lock(state.mutex); + state.stopped = true; + instance = state.created; + } + if (instance != nullptr) instance->Shutdown(); +} + +unsigned Writers() { + return runningWriters.load(std::memory_order_acquire); +} + Counters Totals() { return store().Totals(); } diff --git a/core/shader/recompiler/ShaderDiskCache.hpp b/core/shader/recompiler/ShaderDiskCache.hpp index b03d85a809..0f9f3f1997 100644 --- a/core/shader/recompiler/ShaderDiskCache.hpp +++ b/core/shader/recompiler/ShaderDiskCache.hpp @@ -73,6 +73,8 @@ void Store(std::vector key, std::shared_ptr va [[nodiscard]] bool LoadPlan(std::span key, IrResourcePlan& plan); void StorePlan(std::vector key, std::shared_ptr plan); void Flush(); +void Shutdown(); +[[nodiscard]] unsigned Writers(); [[nodiscard]] Counters Totals(); } From 20edd079e47298268484c3a2c32b031b4a46fbc9 Mon Sep 17 00:00:00 2001 From: hypermist Date: Fri, 16 Oct 2026 00:13:15 +1300 Subject: [PATCH 0436/1000] fix(libc): store the sign of gamma in an exported signgam from lgamma Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libc/src/Math.cpp | 5 +++++ core/libs/tests/GuestMath.cpp | 10 ++++++++++ docs/dev/debt/libc.md | 2 +- 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libc/src/Math.cpp b/core/libs/prx/libc/src/Math.cpp index 7e91356869..eb151a1b14 100644 --- a/core/libs/prx/libc/src/Math.cpp +++ b/core/libs/prx/libc/src/Math.cpp @@ -302,7 +302,12 @@ float APS5_VABI log1pf_nid_postfix(float x) { return std::log1p(x); } double APS5_VABI expm1_nid_postfix(double x) { return std::expm1(x); } float APS5_VABI expm1f_nid_postfix(float x) { return std::expm1(x); } float APS5_VABI erff_nid_postfix(float x) { return std::erf(x); } +int signgam_nid_postfix = 0; + double APS5_VABI lgamma_nid_postfix(double x) { + const double whole = std::floor(x); + if (x == 0) signgam_nid_postfix = std::signbit(x) ? -1 : 1; + else signgam_nid_postfix = x < 0 && whole != x && std::fmod(whole, 2.0) != 0 ? -1 : 1; const std::uint64_t magnitude = std::bit_cast(x) & 0x7fffffffffffffffull; if (magnitude != 0 && magnitude < 0x3c70000000000000ull) return -std::log(std::fabs(x)); return std::lgamma(x); diff --git a/core/libs/tests/GuestMath.cpp b/core/libs/tests/GuestMath.cpp index ed422b3b41..3ab7ee3685 100644 --- a/core/libs/tests/GuestMath.cpp +++ b/core/libs/tests/GuestMath.cpp @@ -81,6 +81,7 @@ double APS5_VABI expm1_nid_postfix(double); float APS5_VABI expm1f_nid_postfix(float); float APS5_VABI erff_nid_postfix(float); double APS5_VABI lgamma_nid_postfix(double); +extern int signgam_nid_postfix; float APS5_VABI logbf_nid_postfix(float); double APS5_VABI exp2_nid_postfix(double); float APS5_VABI __powisf2_nid_postfix(float, int); @@ -932,6 +933,15 @@ int main() { Require(std::abs(lgamma_nid_postfix(sign * 0x1p-57) - 39.509389291916882) < 1e-13); Require(std::fetestexcept(FE_OVERFLOW | FE_DIVBYZERO | FE_INVALID) == 0); } + const auto gammaSign = [](double x) { + signgam_nid_postfix = 7; + lgamma_nid_postfix(x); + return signgam_nid_postfix; + }; + Require(gammaSign(0.5) == 1 && gammaSign(10.) == 1 && gammaSign(0.) == 1 && gammaSign(-0.) == -1); + Require(gammaSign(-0.5) == -1 && gammaSign(-1.5) == 1 && gammaSign(-2.5) == -1 && gammaSign(-0x1p-80) == -1); + Require(gammaSign(-1.) == 1 && gammaSign(-2.) == 1 && gammaSign(-0x1p60) == 1 && gammaSign(-4503599627370495.5) == 1); + Require(gammaSign(INFINITY) == 1 && gammaSign(-INFINITY) == 1 && gammaSign(NAN) == 1); Require(llroundl_nid_postfix(2.5L) == 3 && llroundl_nid_postfix(-2.5L) == -3 && llroundl_nid_postfix(0.49999999999999999L) == 0); Require(llroundl_nid_postfix(9007199254740993.0L) == 9007199254740993LL && llroundl_nid_postfix(-1e30L) == INT64_MIN); for (const int mode : {FE_TONEAREST, FE_DOWNWARD, FE_UPWARD, FE_TOWARDZERO}) { diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index eb244ed912..2bdd7818c6 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -46,7 +46,7 @@ - [realpath](../../../core/libs/prx/libc/src/RealPath.cpp) (libc) follows FreeBSD 11's [`realpath.c`](https://github.com/freebsd/freebsd-src/blob/stable/11/lib/libc/stdlib/realpath.c) except for symlinks. The console's guest filesystem has none, so a host symlink under the guest root is followed for existence and type but not expanded, and `..` drops the previous component lexically, as `Resolve` does for every guest path. Through a host link `linkdir -> sub/deep`, `linkdir/../x` gives `x` beside the link, not beside its target. - [strtod, strtof, strtold](../../../core/libs/prx/libc/src/Strings.cpp) (libc) - `NAN(n-char-sequence)` converts to the quiet NaN that `nan` and `nanf` return, negated after a minus sign, and ignores the sequence as they ignore their tag. This is inferred from C17 7.12.11.2p2, which makes `nan("n")` equivalent to `strtod("NAN(n)", NULL)`, and not observed on the console. No console `nanl` was observed, so `strtold` returns the x87 default quiet NaN (exponent `0x7fff`, significand `0xc000000000000000`) - [nan and nanf](../../../core/libs/prx/libc/src/Math.cpp) (libc) return fixed quiet NaN values (`0x7fc00000` and `0x7ff8000000000000`), ignoring the tag argument as observed in the console's `libc.prx` (both exports load a global constant via `mov rax,[rip+..]; vmovsd xmm0,[rax]; ret`); VLC (PPSA85300) stops on nanf at start-up after strsep. -- [lgamma](../../../core/libs/prx/libc/src/Math.cpp) (libc) returns the host's `lgamma` but does not store the sign of Γ(x) in the guest's `signgam`, as FreeBSD 11's [`e_lgamma.c`](https://github.com/freebsd/freebsd-src/blob/stable/11/lib/msun/src/e_lgamma.c) does through `__ieee754_lgamma_r`, because no guest `signgam` is exported. PPSA99004 imports it. Console behaviour is unverified +- [lgamma and signgam](../../../core/libs/prx/libc/src/Math.cpp) (libc) - `lgamma` returns the host's `lgamma` and stores the sign of Γ(x) in the exported `signgam` (NID `5gOOC0kzW0c`, starting at 0 as in FreeBSD 11's [`s_signgam.c`](https://github.com/freebsd/freebsd-src/blob/stable/11/lib/msun/src/s_signgam.c)) by the rules of [`e_lgamma_r.c`](https://github.com/freebsd/freebsd-src/blob/stable/11/lib/msun/src/e_lgamma_r.c): -1 for -0 and for a negative non-integer whose floor is odd, 1 otherwise, including poles, infinities and NaN. Taken from the FreeBSD source, not measured on the console. The NID is in the aerolib symbol list; no title is known to read `signgam`. PPSA99004 imports `lgamma` - [sinhf](../../../core/libs/prx/libc/src/Math.cpp) (libc) follows the `sinhf` of the `libc.prx` bundled with PPSA28319 (disassembly): FreeBSD msun's [`e_sinhf.c`](https://github.com/freebsd/freebsd-src/blob/5aa554c7e5b6fd9042986df07f12ac5687e41392/lib/msun/src/e_sinhf.c) from before `__ldexp_expf`, which squares `expf(|x| / 2)` between `logf(FLT_MAX)` and the overflow threshold, calling msun's [`expm1f`](https://github.com/freebsd/freebsd-src/blob/59bc2f095289a10b8cfa55cbb2b45c3bd6c2d4ae/lib/msun/src/s_expm1f.c) and [`expf`](https://github.com/freebsd/freebsd-src/blob/59bc2f095289a10b8cfa55cbb2b45c3bd6c2d4ae/lib/msun/src/e_expf.c), whose paths for the arguments it passes are inlined here. Past the threshold Sony's error helper sets `errno` to `ERANGE` before returning `x * 1e37`; msun itself sets no `errno`. That code, run on a Windows host, gives the same bits as this implementation for every finite argument up to the threshold, of both signs; it was not run on Linux. That the system `libSceLibcInternal`, which Ruffle (PPSA68091) and RetroArch (PPSA99169) import while bundling a zero-return `libc.prx`, has the same code is assumed - [asinl](../../../core/libs/prx/libc/src/Math.cpp) (libc) follows the `asinl` of the `libc.prx` bundled with PPSA28319 (disassembly), FreeBSD msun's [`e_asinl.c`](https://github.com/freebsd/freebsd-src/blob/9f0432041b8fb69832f10c32da50a67f34feeb61/lib/msun/src/e_asinl.c) with the ld80 [`invtrig.c`](https://github.com/freebsd/freebsd-src/blob/9f0432041b8fb69832f10c32da50a67f34feeb61/lib/msun/ld80/invtrig.c) coefficients, which the binary holds as the same 80-bit constants. For |x| > 1, the infinities and NaN, Sony's error helper raises `FE_INVALID` and sets `errno` to `EDOM` before returning `(x - x) / (x - x)`, so a NaN argument also sets `EDOM`; msun itself sets no `errno`. 20 million random arguments in (-1, 1) give the same bits as that code run on a Windows host. The result needs the x87 precision control at 64 bits, as FreeBSD amd64 starts threads (control word `0x37f`), but guest threads do not set it: on Windows a thread started by `_beginthreadex` runs at 53 bits (`0x27f`), where `asinl(0.75)` returns mantissa `0xd91a98ae3406e000` instead of `0xd91a98ae3406e041`. The system `libSceLibcInternal` that RetroArch (PPSA99169) imports is assumed to have the same code - [atanhf](../../../core/libs/prx/libc/src/Math.cpp) (libc) follows the `atanhf` of the `libc.prx` bundled with PPSA28319 (disassembly): FreeBSD msun's [`e_atanhf.c`](https://github.com/freebsd/freebsd-src/blob/59bc2f095289a10b8cfa55cbb2b45c3bd6c2d4ae/lib/msun/src/e_atanhf.c) calling msun's [`log1pf`](https://github.com/freebsd/freebsd-src/blob/59bc2f095289a10b8cfa55cbb2b45c3bd6c2d4ae/lib/msun/src/s_log1pf.c), whose paths for the positive arguments it passes are inlined here; the exported `log1pf` is still the host's. For |x| > 1, the infinities and NaN, Sony's error helper raises `FE_INVALID` and sets `errno` to `EDOM` before returning `(x - x) / (x - x)`, and for |x| = 1 it raises `FE_DIVBYZERO` and sets `errno` to `ERANGE` before returning `x / 0`; msun itself sets no `errno`. That code, run on a Windows host, gives the same bits, `errno` and floating-point exceptions as this implementation for all 2^32 arguments; it was not run on Linux. That the system `libSceLibcInternal`, which Dusklight (PPSA99305) imports while bundling a zero-return `libc.prx`, has the same code is assumed From 1876314b353c1595ab17713ac886c09a186d2902 Mon Sep 17 00:00:00 2001 From: Nithishvelan M Date: Wed, 14 Oct 2026 23:40:30 +0530 Subject: [PATCH 0437/1000] docs: clarify macOS host support --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index c63b83abe6..57c93abbf4 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ Tool for automatic executables porting to Linux and Windows. +The relinker-only build runs on macOS, including Apple silicon. Game execution on a macOS host is not yet supported. + [**Web converter**](https://boykopovar.github.io/AnyPS5/) - files stay on your device. ![anyps5_collage1](https://github.com/user-attachments/assets/ed2f0b2d-33d3-46d7-ab88-767ca872fc99) From e92484a251231dbe823332297cddaaf8dc78154b Mon Sep 17 00:00:00 2001 From: Nithishvelan M Date: Wed, 14 Oct 2026 23:58:15 +0530 Subject: [PATCH 0438/1000] feat(libSceUserService): implement sceUserServiceGetAccessibilityKeyremapEnable and sceUserServiceGetAccessibilityZoom --- core/libs/prx/libSceUserService/Export.cpp | 8 ++++++++ core/libs/tests/GuestUserService.cpp | 4 ++++ docs/dev/debt/libSceUserService.md | 2 +- 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceUserService/Export.cpp b/core/libs/prx/libSceUserService/Export.cpp index a2c906db17..79c8ec3a95 100644 --- a/core/libs/prx/libSceUserService/Export.cpp +++ b/core/libs/prx/libSceUserService/Export.cpp @@ -26,6 +26,10 @@ int APS5_VABI sceUserServiceGetAccessibilityChatTranscription(int user_id, int32 return ReportAccessibilitySetting(user_id, chat_transcription, 0); } +int APS5_VABI sceUserServiceGetAccessibilityKeyremapEnable(int user_id, int32_t* enable) { + return ReportAccessibilitySetting(user_id, enable, 0); +} + int APS5_VABI sceUserServiceGetAccessibilityPressAndHoldDelay(int user_id, int32_t* press_and_hold_delay) { return ReportAccessibilitySetting(user_id, press_and_hold_delay, 0); } @@ -38,6 +42,10 @@ int APS5_VABI sceUserServiceGetAccessibilityVibration(int user_id, int32_t* vibr return ReportAccessibilitySetting(user_id, vibration, 1); } +int APS5_VABI sceUserServiceGetAccessibilityZoom(int user_id, int32_t* zoom) { + return ReportAccessibilitySetting(user_id, zoom, 0); +} + int APS5_VABI sceUserServiceGetAccessibilityZoomEnabled(int user_id, int32_t* zoom_enabled) { return ReportAccessibilitySetting(user_id, zoom_enabled, 0); } diff --git a/core/libs/tests/GuestUserService.cpp b/core/libs/tests/GuestUserService.cpp index 83d7afc731..7686a67219 100644 --- a/core/libs/tests/GuestUserService.cpp +++ b/core/libs/tests/GuestUserService.cpp @@ -10,9 +10,11 @@ int APS5_VABI sceUserServiceGetUserColor(int user_id, int* color); int APS5_VABI sceUserServiceGetNpAccountId(int user_id, std::uint64_t* account_id); int APS5_VABI sceUserServiceGetAgeLevel(int user_id, std::uint32_t* age_level); int APS5_VABI sceUserServiceGetAccessibilityChatTranscription(int user_id, std::int32_t* value); +int APS5_VABI sceUserServiceGetAccessibilityKeyremapEnable(int user_id, std::int32_t* value); int APS5_VABI sceUserServiceGetAccessibilityPressAndHoldDelay(int user_id, std::int32_t* value); int APS5_VABI sceUserServiceGetAccessibilityTriggerEffect(int user_id, std::int32_t* value); int APS5_VABI sceUserServiceGetAccessibilityVibration(int user_id, std::int32_t* value); +int APS5_VABI sceUserServiceGetAccessibilityZoom(int user_id, std::int32_t* value); int APS5_VABI sceUserServiceGetAccessibilityZoomEnabled(int user_id, std::int32_t* value); int APS5_VABI sceUserServiceGetAccessibilityZoomFollowFocus(int user_id, std::int32_t* value); } @@ -66,9 +68,11 @@ int main() { }; const Setting settings[] = { {sceUserServiceGetAccessibilityChatTranscription, 0}, + {sceUserServiceGetAccessibilityKeyremapEnable, 0}, {sceUserServiceGetAccessibilityPressAndHoldDelay, 0}, {sceUserServiceGetAccessibilityTriggerEffect, 1}, {sceUserServiceGetAccessibilityVibration, 1}, + {sceUserServiceGetAccessibilityZoom, 0}, {sceUserServiceGetAccessibilityZoomEnabled, 0}, {sceUserServiceGetAccessibilityZoomFollowFocus, 1}, }; diff --git a/docs/dev/debt/libSceUserService.md b/docs/dev/debt/libSceUserService.md index 085b02a1a2..5e32b84b70 100644 --- a/docs/dev/debt/libSceUserService.md +++ b/docs/dev/debt/libSceUserService.md @@ -7,4 +7,4 @@ - [sceUserServiceGetUserColor](../../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - the default profile colour is assumed to be blue (0) - [sceUserServiceGetUserName](../../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - `0x80960105`, which a PS5 (firmware 13.60) returns for a user id it does not know, has no known name; it is `USER_SERVICE_ERROR_UNKNOWN_USER` here. On the console a registered user who is not logged in still gets their name; here only the initial user exists - [sceUserServiceGetAgeLevel](../../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - on a PS5 (firmware 13.60) the value is the age set in the user's parental restriction (0, 7, 12 and 15 were measured) and `0xFFFFFFFF` without one; a user who is not logged in returns `0x80960009` with or without an output. Parental restrictions are not modelled, so the initial user is always reported as unrestricted. PPSA12544 (Unity) only stores the value for its user profile -- [sceUserServiceGetAccessibilityChatTranscription, PressAndHoldDelay, TriggerEffect, Vibration, ZoomEnabled, ZoomFollowFocus](../../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - report 0, 0, 1, 1, 0 and 1, what a PS5 (firmware 13.60) reports for a user whose accessibility settings are all at their defaults; the console reports the user's own settings (a changed vibration intensity read 3), which are not modelled. What each number means is unknown. A user who is not logged in returns `0x80960009` with or without an output +- [sceUserServiceGetAccessibilityChatTranscription, KeyremapEnable, PressAndHoldDelay, TriggerEffect, Vibration, Zoom, ZoomEnabled, ZoomFollowFocus](../../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - report 0, 0, 0, 1, 1, 0, 0 and 1, what a PS5 (firmware 13.60) reports for a user whose accessibility settings are all at their defaults; the console reports the user's own settings (a changed vibration intensity read 3), which are not modelled. What each number means is unknown. A user who is not logged in returns `0x80960009` with or without an output From 4912955a34bf80c808f444be5376315164a26b2e Mon Sep 17 00:00:00 2001 From: Nithishvelan M Date: Wed, 14 Oct 2026 20:11:38 +0530 Subject: [PATCH 0439/1000] test(libSceRandom): cover zero-sized null buffer and single-byte read in guest test --- core/libs/tests/GuestRandom.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/libs/tests/GuestRandom.cpp b/core/libs/tests/GuestRandom.cpp index f611927f88..df1d5f83c9 100644 --- a/core/libs/tests/GuestRandom.cpp +++ b/core/libs/tests/GuestRandom.cpp @@ -11,11 +11,13 @@ int main() { constexpr int invalid = static_cast(0x817C0016); unsigned char buffer[80]; Require(sceRandomGetRandomNumber(nullptr, 16) == invalid); + Require(sceRandomGetRandomNumber(nullptr, 0) == invalid); std::memset(buffer, 0xAA, sizeof(buffer)); Require(sceRandomGetRandomNumber(buffer, 65) == invalid); Require(buffer[0] == 0xAA); Require(sceRandomGetRandomNumber(buffer, 0) == 0); Require(buffer[0] == 0xAA); + Require(sceRandomGetRandomNumber(buffer, 1) == 0); Require(sceRandomGetRandomNumber(buffer, 7) == 0); Require(buffer[7] == 0xAA); unsigned char first[64] = {}; From 4dc23f9c14c75124f9a98409691441d87c6755d3 Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Fri, 16 Oct 2026 05:45:41 +0200 Subject: [PATCH 0440/1000] docs(debt): list the unimplemented AJM codec 23 of PPSA21480 Broken Sword - Shadow of the Templars: Reforged stops at AJM decoding codec 23, which is not one of the codec ids known in Ajm.cpp (MP3 0, ATRAC9 1, M4AAC 2, Opus 24). Co-Authored-By: Claude Sonnet 5.5 --- docs/dev/debt/libSceAjm.native.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/dev/debt/libSceAjm.native.md b/docs/dev/debt/libSceAjm.native.md index 0546015bae..66dc262de4 100644 --- a/docs/dev/debt/libSceAjm.native.md +++ b/docs/dev/debt/libSceAjm.native.md @@ -7,6 +7,7 @@ - [sceAjmDecMp3ParseFrame](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - signature, result layout and rate tables taken from the [shadPS4](https://github.com/shadps4-emu/shadPS4) reimplementation of the PS4 library, not confirmed on a PS5 title: the layer field is not checked and MPEG-2.5 stops at 64 kbps; the original file length (`parse_ofl`) layout (Xing/Info + LAME tag, VBRI, Fraunhofer `0xB4` block with its CRC, encoder delay = samples per frame + LAME delay + 529) also comes from shadPS4 only, and the Fraunhofer block has no other public description - [sceAjmBatchJobGetCodecInfo, sceAjmBatchJobGetGaplessDecode](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - the `RUN_GET_CODEC_INFO` flag (0x800), the ATRAC9 codec info layout and its next frame size are taken from the [shadPS4](https://github.com/shadps4-emu/shadPS4) and [RPCSX](https://github.com/RPCSX/rpcsx) reimplementations, not confirmed on a PS5 title; a run job without buffers returns result 0, as in shadPS4. The gapless sideband reports the total and skip counts the title set and does not count them down: RPCSX does the same, shadPS4 counts them down and BryKytyPS5 returns its current counts - [AJM Opus decoder](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native, codec 24) - the initialize parameter layout and little-endian `u16` packet byte count come from The Smurfs Dreams (`2, 48000, 0`) and Lost Soul Aside (`4, 48000, 1`). The third word is interpreted as the mapping family, as in [KytyPS5's initialize structure](https://github.com/KytyPS5/KytyPS5/blob/984cfcebd141b211aa9e0a8cde63b0ebe652d07a/src/libs/ajm/opus_decoder.h#L11-L15). Family 1 uses the conventional stream counts, coupled counts and channel map from [libopus 1.5.2's surround layout](https://github.com/xiph/opus/blob/v1.5.2/src/opus_multistream_encoder.c#L47-L55), passed to FFmpeg through an [RFC 7845 section 5.1](https://datatracker.ietf.org/doc/html/rfc7845#section-5.1) header with zero pre-skip and gain; gapless skipping remains controlled by AJM sideband parameters. FFmpeg returns PCM in its native speaker order. The implicit stream layout, PCM channel order and invalid family-0 channel count result are not verified on a console. NHL 27 (PPSA34063) passes `1, 48000, 255`. Family 255 with one channel is decoded as one mono stream, the same as family 0; this is assumed, since the stream count, coupled stream count and channel mapping that a family-255 Opus header carries ([RFC 7845 section 5.1.1](https://datatracker.ietf.org/doc/html/rfc7845#section-5.1.1)) are not passed. Only 48000 Hz, mapping families 0 and 1, and family 255 with one channel are implemented; other rates and families, and family 255 with more channels, throw +- [AJM decoders other than MP3, ATRAC9, M4AAC and Opus](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - decoding throws for every other codec id. Broken Sword - Shadow of the Templars: Reforged (PPSA21480) stops at codec 23 (`AJM: decoding codec 23 is not implemented`). The ids known here are MP3 0, ATRAC9 1, M4AAC 2 and Opus 24; id 23 is not among them and which codec it is, is unknown. - [sceAjmBatchJobSetResampleParametersEx](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - the `(info, instance, float ratio_start, float ratio_change_per_sample, flags, result)` signature is inferred from the name and from `sceAjmBatchJobSetResampleParameters`; with a zero change per sample it sets the ratio as `sceAjmBatchJobSetResampleParameters` does (Watch Dogs: Legion, PPSA01500, calls it so). How a non-zero change ramps the ratio is not confirmed on a console (no source: KytyPS5 ignores it, prosper stubs it, the PS5 firmware does not export the batch builders to a payload): it is assumed to be added to the ratio after each emitted output frame, clamped at zero, and kept across batches. RIDE 4 (PPSA01599) passes one call with a change of 4.6e-7, so the exact rule matters little there. - [sceAjmBatchJobControl](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - RESET (bit 13), INITIALIZE (bit 14) and the sideband input order (gapless decode, then initialize parameters) come from [shadPS4](https://github.com/shadps4-emu/shadPS4) (PS4); other flags throw - [sceAjmBatchJobControl](../../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - RESET is assumed to clear the context as sceAjmBatchJobClearContext does From 905a0dffb243870b122ae833bac51b209e47e135 Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Fri, 16 Oct 2026 05:45:49 +0200 Subject: [PATCH 0441/1000] docs(debt): list the NGS2 custom module id 0x20 of PPSA35169 Trails in the Sky 2nd Chapter creates a custom rack with module id 0x20, which Custom.cpp rejects; the module and its structure are unknown (#4246). Co-Authored-By: Claude Sonnet 5.5 --- docs/dev/debt/libSceNgs2.native.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/dev/debt/libSceNgs2.native.md b/docs/dev/debt/libSceNgs2.native.md index 25765ec1d8..f535026c2b 100644 --- a/docs/dev/debt/libSceNgs2.native.md +++ b/docs/dev/debt/libSceNgs2.native.md @@ -15,6 +15,7 @@ - [NGS2 sampler voice state flag EMPTY](../../../core/libs/prx/libSceNgs2.native/src/Render.cpp) (libSceNgs2.native) - a playing sampler voice that still accepts blocks (`CONTINUE`) and has played all queued blocks reports `INUSE|PLAYING|EMPTY` (0x20) and keeps playing, so blocks added later play on. PPSA18391 (engine: Clickteam Fusion) reads `state_flags & 0x22 != 2` from `sceNgs2VoiceGetState` as the end of a sound and only then reuses the voice. When the console sets and clears EMPTY, and whether it also reports it for other voice types, is unverified on hardware. - [NGS2 port matrix](../../../core/libs/prx/libSceNgs2.native/src/Render.cpp) (libSceNgs2.native) - a port without a matrix, or with a selected matrix whose levels were never set, mixes through a default channel map: mono reaches both stereo channels at -3 dB and the 5.1 and 7.1 centre, stereo folds to mono at -3 dB per side, and wider sources fold down with 0.5 and -3 dB gains. The map and the equivalence of an unset matrix with no matrix were measured from the PS4 firmware 11.00 `libSceNgs2.sprx`, not yet from the PS5 library. That library only accepts submixer and mastering voices with 1, 2, 6 or 8 channels, so the map into 3, 4, 5 or 7 channels throws. A matrix set with fewer levels than the destination needs still mixes only the rows it has - [NGS2 custom submixer rack](../../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (libSceNgs2.native, rack 0x4002) - structure layouts from [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `ngs2_custom.h`; module id 0x1f for UserFx2 and 0x1d for UserFx, voice parameter ids `0x40001f00 | module index` and `0x40001d00 | module index`, the process flags (1 on the first process call after setup, 2 after a parameter change, 0 otherwise) and the call order (setup per voice at rack creation, process after the inputs are mixed, cleanup at destruction) come only from [KytyPS5](https://github.com/KytyPS5/KytyPS5) and shadPS4. Only UserFx and UserFx2 modules on a single buffer are accepted: other module ids, more buffers, a module control handler, different input and output channel counts, setup flags and default rack options throw. Each module's state is a separate buffer, not the `state_offset` slice of the voice state, and `sceNgs2VoiceGetState` still throws for this rack. As in KytyPS5, UserFx2 process runs only on grains where an input produced samples, so an effect tail stops with the input and a pending flag 2 waits for the next audible grain +- [NGS2 custom module ids](../../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (libSceNgs2.native) - a custom rack with a module id other than 0x13 (sampler rack), 0x1d and 0x1f (submixer rack) throws. Trails in the Sky 2nd Chapter (PPSA35169) creates a rack with module id 0x20 (`NGS2: custom module id 0x20 is not implemented`, #4246); which module it is, and its structure, are unknown. - [NGS2 reverb rack](../../../core/libs/prx/libSceNgs2.native/src/Reverb.cpp) (libSceNgs2.native, rack 0x2001) - follows the PS4 firmware 11.00 `libSceNgs2.sprx`, read and checked against its output at 11.025 to 48 kHz, including parameter changes during playback (differences at float rounding level); not yet compared with the PS5 library. Structure layouts come from [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `ngs2_reverb.h` and the param ids 0x20010000 (setup) and 0x20010001 (I3DL2) from PPSA22520. Only the default `reverb_size` of 1 is handled, with its 14400-sample reflection buffer; other sizes and the library's half-rate mode throw. System sample rates below about 7.5 kHz, where an allpass delay would have no samples, throw. Setup only accepts equal input and output channel counts - [sceNgs2ReportRegisterHandler, sceNgs2ReportUnregisterHandler](../../../core/libs/prx/libSceNgs2.native/src/Unimplemented.cpp) (libSceNgs2.native, NIDs `uBIN24Tv2MI`, `nPzb7Ly-VjE`) are throwing stubs. The signatures (report type, handler, user data, out handle) and (report handle) follow [SharpProspero](https://github.com/SvenGDK/SharpProspero) and [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `ngs2_report.h`. No report is produced yet, so registering a handler cannot be honoured. PPSA04540 imports both and registers a CPU load handler (type 5) - [sceNgs2SystemSetSampleRate, sceNgs2SystemSetUserData, sceNgs2SystemGetUserData, sceNgs2SystemLock, sceNgs2SystemUnlock](../../../core/libs/prx/libSceNgs2.native/src/System.cpp) (libSceNgs2.native) - shadPS4 only checks the system handle (`INVALID_SYSTEM_HANDLE`) and SharpProspero names what each call does; neither gives the other limits. A sample rate of 0 and a null user data pointer throw, any other rate is accepted, and changing the rate while a sampler filter is enabled throws because the filter coefficients were computed for the old rate. Lock and unlock take the same lock as `sceNgs2RackLock` From 2cf75dbbfa93890fdd4e61d7e269ff278cc08a8a Mon Sep 17 00:00:00 2001 From: Agnibha Mukherjee Date: Fri, 16 Oct 2026 09:23:12 +0530 Subject: [PATCH 0442/1000] test(relinker): assert entry mapping for grown fixed-base table --- core/relinker/relinker/tests/test_linux_fixed_base.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/core/relinker/relinker/tests/test_linux_fixed_base.py b/core/relinker/relinker/tests/test_linux_fixed_base.py index e01dfc92f3..b181458c12 100644 --- a/core/relinker/relinker/tests/test_linux_fixed_base.py +++ b/core/relinker/relinker/tests/test_linux_fixed_base.py @@ -118,6 +118,7 @@ def main(): assert result.returncode == 0, (result.stdout, result.stderr) relocated_plain = relocated_plain_path.read_bytes() relocated_fixed = relocated_fixed_path.read_bytes() + assert struct.unpack_from(" struct.unpack_from(" Date: Fri, 16 Oct 2026 05:55:24 +0200 Subject: [PATCH 0443/1000] docs(debt): list the unprepared AGC shader types ShaderRegistry.cpp prepares the registered shader types 0, 1, 2, 4, 6 and 7, skips 5, and logs and leaves every other type unprepared at registration; Hello Neighbor 2, Ad Infinitum and Ghostrunner register type 8. Co-Authored-By: Claude Sonnet 5.5 --- docs/dev/debt/libSceAgcDriver.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 21512697ed..c7e003df24 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -31,6 +31,7 @@ - [Resource registration](../../../core/libs/prx/libSceAgcDriver/Resource/src/Registration.cpp) (libSceAgcDriver) - registration always fails with `RESOURCE_REGISTRATION_UNAVAILABLE`, so the owner and resource queries, `sceAgcDriverSetResourceUserData`, `sceAgcDriverFindResourcesPublic` and `sceAgcDriverUnregisterAllResourcesForOwner` return the same error without reading their arguments. Their parameter lists follow the libSceGnmDriver counterparts and are unverified. `sceAgcDriverGetResourceRegistrationMaxNameLength` writes 0xfc and returns 0, as [prosper](https://github.com/mattias800/prosper) does from a title's registration configuration; the `uint32_t*` parameter matches the SDK wrapper `sce::Agc::ResourceRegistration::getMaxNameLength(unsigned int*)`. sharpemu writes 256 and KytyPS5 returns `RESOURCE_REGISTRATION_UNAVAILABLE`, so the value is unverified; a null output throws - [sceAgcDriverRegisterMultipleResources](../../../core/libs/prx/libSceAgcDriver/Resource/src/Registration.cpp) uses the existing unavailable-registration state and leaves caller memory untouched. Its four arguments (uint32 handle output, uint32 owner, descriptor pointer, uint32 count) follow the original PPSA34502 / 01.010.000 codzm call. The caller builds 40-byte descriptors and explicitly accepts 0x8A6C9018 with an invalid output handle. Enabled registration and console argument-validation precedence are unverified. - [Shader headers](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp) (libSceAgcDriver) - registration and ABI resolution read fixed header fields through aligned copies, so the header pointer itself may have any alignment. PPSA26344 passes headers 4 and 1 bytes past an 8-byte boundary, and PPSA01317 passes 4-byte-aligned headers to ABI resolution; console alignment requirements are unknown +- [Registered shader types](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp) (libSceAgcDriver) - types 0, 1, 2, 4, 6 and 7 are prepared and type 5 is skipped. Any other type, such as type 8, is logged (`unsupported registered shader type 8; it is not prepared at registration`) and left unprepared at registration, and throws when it is used later. Hello Neighbor 2 (PPSA07426), Ad Infinitum (PPSA10600) and Ghostrunner (PPSA03685) register type 8 headers and run on past them; what a type 8 header describes is unknown, #1892 found no context registers in it. - [NGG vertex workgroups](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp) (libSceAgcDriver) - type-2 PRIMGEN programs using DS instructions or `s_barrier` are prepared with the draw's routing and primitive registers on the existing mesh path. The workgroup requirement follows [Mesa's NGG lowering](https://github.com/mihawk-99/PS5_Mesa/blob/b3588f78dd7fd46fbadc7ecd487e5f14d4213e4b/src/amd/common/nir/ac_nir_lower_ngg.c#L1135); the mesh path supplies LDS and two guest lanes per host invocation for wave64 on a 32-lane host subgroup. Console routing has not been measured. Passthrough and tessellation keep their existing paths; unsupported mesh assembly and subgroup budgets throw. - [Shader header copies](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp) (libSceAgcDriver) - a header bound for a draw or dispatch at another address than the registered one is accepted when its fixed fields match the registered header except `user_data`; the registered header's metadata is used. PPSA21567 binds such a copy of a vertex shader header with `user_data` set to null. Whether the console reads the copy's `user_data` is unknown - [HtileFillCovers](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver) - a fill is taken to cover a depth surface's whole HTILE when it spans 32 KiB per 1024x512 pixel block from DB_HTILE_DATA_BASE, the pipe-aligned SW_64KB_Z_X metadata extent AMD PAL's gfx10 addrlib gives for GB_ADDR_CONFIG 0x00100044 (1-8 samples, D16/D32); other metadata layouts are not decoded. A partial fill is ignored (the surface keeps its contents); not measured on the console. From ae5046eeb23381101f1064f5cb86185f9f071451 Mon Sep 17 00:00:00 2001 From: Tamalero <11452991+Tamalero@users.noreply.github.com> Date: Thu, 15 Oct 2026 20:13:56 -0700 Subject: [PATCH 0444/1000] feat(libkernel): add sceCoredumpConfigDumpMode as a throwing stub PPSA16833 imports it from libkernel, and its executable does not load while no library exports the NID. The signature is unknown, so it throws if called, like sceCoredumpWriteUserString. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libkernel/System/src/Coredump.cpp | 5 +++++ docs/dev/debt/libkernel.md | 1 + 2 files changed, 6 insertions(+) diff --git a/core/libs/prx/libkernel/System/src/Coredump.cpp b/core/libs/prx/libkernel/System/src/Coredump.cpp index 1a42cca261..309fc8d56c 100644 --- a/core/libs/prx/libkernel/System/src/Coredump.cpp +++ b/core/libs/prx/libkernel/System/src/Coredump.cpp @@ -104,6 +104,11 @@ int APS5_VABI sceCoredumpWriteUserString() { return 0; } +int APS5_VABI sceCoredumpConfigDumpMode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + int APS5_VABI sceCoredumpAttachUserMemoryFile(uint32_t userValue, const void* mem, size_t size) { (void)userValue; return attachOutsideCoredumpHandler(__func__, mem, size); diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 5396a00c3a..2fa0cb74e3 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -35,6 +35,7 @@ - [AMM PRT ranges](../../../core/libs/prx/libkernel/Apr/src/Apr.cpp) (libSceAmpr) - signatures, checks and their order taken from ampr_emu, where `MapAsPrt` is a map with the PRT flag and protection 0 and `AllocatePaForPrt` a memory type and protection change with mask 1019, both checking the buffer memory after its size; the effects are inferred from the names, not confirmed on a title. Unbacked PRT pages are zero-filled pages with GPU read access, so CPU reads see zeros instead of faulting and GPU writes fault instead of being dropped. `AllocatePaForPrt` backs each unbacked page with a pool page and sets the type and protection of backed ones; `RemapIntoPrt` moves a fully mapped range into a PRT range, its opcode argument (1011 when 0, as in ampr_emu) is recorded and ignored; `UnmapToPrt` returns the pages to the pool and zero-fills them again. A plain map, unmap or remap over a PRT range ends it; `AllocatePaForPrt`, `RemapIntoPrt` or `UnmapToPrt` outside a PRT range throws - [AMM kernel write commands](../../../core/libs/prx/libkernel/Apr/src/Apr.cpp) (libkernel) - signatures of `sceKernelWriteModifyMtypeProtectCommand` and `sceKernelWriteRemapIntoPrtCommand` taken from [ampr_emu's libkernel hook targets](https://github.com/drakmor/ampr_emu/blob/cfa85df379f6eeeb165d7badf9b648e266fe77b7/include/ampr_libkernel_hook_targets.h), which forward to the real kernel; the measure mode (`dst` null reports the size, `outSize` null skips the report) follows ampr_emu's measure-then-record calls, not confirmed on a title. The range, protection and mask checks mirror the `libSceAmpr` siblings, and the recorded bytes match them exactly so the submitted buffer executes on the existing paths - [sceCoredumpWriteUserString](../../../core/libs/prx/libkernel/System/src/Coredump.cpp) - imported by PPSA16064, not called during its start-up and added without an implementation; unknown signature +- [sceCoredumpConfigDumpMode](../../../core/libs/prx/libkernel/System/src/Coredump.cpp) (libkernel, NID `gzLt9Qrauk0`) - imported by PPSA16833, whose executable does not load without it; added without an implementation; unknown signature - [sceKernelInternalMemoryGetModuleSegmentInfo](../../../core/libs/prx/libkernel/Module/src/ModuleInfo.cpp) (libkernel) - the `(InternalSegmentInfo* result)` signature, the 16-byte `{address, size}` result and the choice of the main program's first segment follow [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd43e6fddd1ab033e7980cd356d14c964905/kernel/ps4_libkernel.pas#L343-L355) only, which fills it through its `sceKernelGetLibkernelTextLocation` from the first segment of `eboot.bin`; whether the console reports that segment, the text of libkernel itself or the calling module's is unknown. The segment is the first one `sceKernelGetModuleInfo` reports for the main program, and 0 is returned. A null result throws. 44 audited titles that bundle their own `libc.prx` import it; no call has been observed - [sceKernelGetModuleInfoForUnwind](../../../core/libs/prx/libkernel/Module/src/Module.cpp) (libkernel) - on Linux the tables come from `sceKernelGetModuleInfoFromAddr`, whose `eh_frame_size` includes the zero terminator, as measured on firmware 13.60 (reported in [#2419](https://github.com/boykopovar/AnyPS5/issues/2419), not reproduced here); on Windows it is assumed to stop before the terminator. LLVM libunwind's `CFI_Parser::findFDE` stops at the terminator or the length, whichever comes first, so it finds the same FDEs either way. Only the main executable and `.guest.prx` images report tables; other host images report none, as on Windows - [sceKernelGetModuleInfoForUnwind](../../../core/libs/prx/libkernel/Module/src/Module.cpp) (libkernel) - on Windows the `.ehmeta` tables and the image file name are cached per allocation base and dropped after a loader free (`dlclose`, `sceKernelStopUnloadModule`, the thread atexit release) when the module is no longer loaded. A table changed in place, or an image freed by anything other than the loader, is not noticed. An address inside an image of the Windows loader's module list whose tables are cached is resolved with `RtlPcToFileHeader` and no `VirtualQuery`; other addresses, and images without tables, still use `VirtualQuery`. The `.eh_frame` walk that finds its end is done once per image instead of once per call From edb74cb58ba739f8db4669d2bbf2cf9caed88f5f Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Fri, 16 Oct 2026 07:01:12 +0200 Subject: [PATCH 0445/1000] test(agc): cover the DWORD rounding of guest buffer view ranges Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libSceAgcDriver/tests/Graphics.cpp | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 2e1d26e11f..a5eff422f1 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -3962,6 +3962,18 @@ void oversizedDescriptorTests() { ::operator delete(block, std::align_val_t{blockBytes}); } +void guestBufferViewBytesTests() { + using AgcDriver::Graphics::GuestBufferMemory; + for (const std::uint64_t bytes : {4ull, 16ull, 4096ull}) { + for (const std::uint32_t adjustment : {0u, 4u, 16u, 256u}) { + Require(GuestBufferMemory::ViewBytes(bytes, adjustment) == bytes + adjustment, "a view whose adjustment is a whole number of DWORDs did not add the adjustment"); + } + for (const std::uint32_t adjustment : {1u, 2u, 3u, 5u, 7u}) { + Require(GuestBufferMemory::ViewBytes(bytes, adjustment) == bytes + 4u * ((adjustment + 3u) / 4u), "a view that starts off a DWORD boundary did not cover the DWORDs around it"); + } + } +} + void descriptorLayoutFlagTests() { constexpr VkDescriptorBindingFlags indexed = VK_DESCRIPTOR_BINDING_PARTIALLY_BOUND_BIT | VK_DESCRIPTOR_BINDING_VARIABLE_DESCRIPTOR_COUNT_BIT; mock = MockVulkan{}; @@ -7436,6 +7448,7 @@ int main(int argc, char** argv) { pushConstantTests(); resourceTests(); oversizedDescriptorTests(); + guestBufferViewBytesTests(); descriptorSnapshotTests(); descriptorCacheTests(); bufferBudgetPlacementTests(); From 2aeb06f3695078938b974e8a1f2c902c991a0dfd Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Fri, 16 Oct 2026 07:10:31 +0200 Subject: [PATCH 0446/1000] test(agc): cover the color to depth format mapping of depth compare copies Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libSceAgcDriver/tests/Graphics.cpp | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 2e1d26e11f..49ad465788 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -598,6 +598,15 @@ void hardwareScreenOffsetTests() { expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "missing register"); } +void depthCopyFormatTests() { + using AgcDriver::Graphics::Texture; + Require(Texture::DepthCopyFormat(VK_FORMAT_R32_SFLOAT) == VK_FORMAT_D32_SFLOAT, "a 32-bit float color format did not map to the 32-bit float depth format"); + Require(Texture::DepthCopyFormat(VK_FORMAT_R16_UNORM) == VK_FORMAT_D16_UNORM, "a 16-bit unorm color format did not map to the 16-bit unorm depth format"); + for (const auto format : {VK_FORMAT_UNDEFINED, VK_FORMAT_R8G8B8A8_UNORM, VK_FORMAT_R16_SFLOAT, VK_FORMAT_R16_UINT, VK_FORMAT_R32_UINT, VK_FORMAT_D32_SFLOAT}) { + Require(Texture::DepthCopyFormat(format) == VK_FORMAT_UNDEFINED, "a color format without a depth counterpart mapped to a depth format"); + } +} + void UnwrittenSampleLocationTests() { using AgcDriver::Graphics::DecodeState; using AgcDriver::Graphics::DrawRejection; @@ -7418,6 +7427,7 @@ int main(int argc, char** argv) { clipDistanceTests(); framebufferLayerTests(); UnwrittenSampleLocationTests(); + depthCopyFormatTests(); quadPixelMaskTests(); ShaderStageTests(); NggVertexWorkgroupTests(); From 7476390517943331749cb51f690efed9087d88ef Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Thu, 15 Oct 2026 23:20:30 -0700 Subject: [PATCH 0447/1000] perf(agc): free an import retired during its settle read after the read Retiring a Windows import whose settle read was still running waited for the read's batch under the imports mutex, so lookups on every thread waited with it. The retired import is now kept with that batch and freed once the batch has completed. Only a guest unmap of its memory still waits for the batch, in the pin waiter, which waits for the main queue there anyway. The [buffers] profile line counts these retirements and the unmaps that waited, apart from the imports of 1 MiB or more settled inside their window, which the old wait counter mixed with the retirement waits. agc_import_settle_retire replaces a 64 KiB window import with the import of its whole range, 64 times, right after its settle read was submitted. No replacement may wait, at least one must find the read running, and the last import must return to the write watch. --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 5 +- .../Graphics/include/GuestBufferMemory.hpp | 2 + .../Graphics/src/GuestBufferMemory.cpp | 54 +++++++++-- .../prx/libSceAgcDriver/tests/Recorder.cpp | 95 +++++++++++++++++++ docs/dev/ARCHITECTURE.md | 2 +- 5 files changed, 146 insertions(+), 12 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 1ec1546127..96b785a5c7 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1064,8 +1064,9 @@ if(BUILD_TESTING) set_tests_properties(agc_import_watch_other_family PROPERTIES SKIP_RETURN_CODE 77) add_test(NAME agc_import_settle_deferred COMMAND agc_driver_recorder_tests --deferred-settle-only) add_test(NAME agc_import_settle_deferred_other_family COMMAND agc_driver_recorder_tests --deferred-settle-other-family-only) - set_tests_properties(agc_import_settle_deferred agc_import_settle_deferred_other_family PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 60) - set_tests_properties(agc_import_watch agc_import_watch_other_family agc_import_settle_deferred agc_import_settle_deferred_other_family PROPERTIES ENVIRONMENT_MODIFICATION + add_test(NAME agc_import_settle_retire COMMAND agc_driver_recorder_tests --settle-retire-only) + set_tests_properties(agc_import_settle_deferred agc_import_settle_deferred_other_family agc_import_settle_retire PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 60) + set_tests_properties(agc_import_watch agc_import_watch_other_family agc_import_settle_deferred agc_import_settle_deferred_other_family agc_import_settle_retire PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() add_test(NAME agc_unchanged_cpu_stamps COMMAND agc_driver_recorder_tests --unchanged-cpu-stamps-only) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp index c0a7a63ad2..3d3a2c7f21 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp @@ -76,6 +76,8 @@ struct HostImportSettleCounts { std::uint64_t submitted = 0; std::uint64_t waited = 0; std::uint64_t rewatched = 0; + std::uint64_t parked = 0; + std::uint64_t unmapWaits = 0; }; HostImportSettleCounts HostImportSettles(); bool ImportMappedRanges(const Context& context, const GuestAllocations::Mapped& ranges, std::uint64_t generation, bool adoptDevice); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index 914e4e9ae2..0cf9b51eea 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -169,6 +169,7 @@ struct SettleBatch { VkFence fence = VK_NULL_HANDLE; std::vector bases; std::vector aliases; + std::vector> retired; bool busy = false; }; @@ -224,6 +225,8 @@ struct HostImports { std::uint64_t settlesSubmitted = 0; std::uint64_t settlesWaited = 0; std::uint64_t settlesRewatched = 0; + std::uint64_t settlesParked = 0; + std::uint64_t settleUnmapWaits = 0; #endif std::uint64_t useCounter = 0; std::uint64_t buildFloor = 0; @@ -243,14 +246,13 @@ bool settleBatchRunning(const HostImports& state, const SettleBatch& batch) { return batch.busy && state.settleWaitForFences != nullptr && state.settleWaitForFences(state.settleDevice, 1, &batch.fence, VK_TRUE, 0) == VK_TIMEOUT; } -void forgetSettle(HostImports& state, std::uint64_t base) { +SettleBatch* forgetSettle(HostImports& state, std::uint64_t base) { const auto found = state.settles.find(base); - if (found == state.settles.end()) return; - if (const auto& batch = state.settleBatches[found->second.batch]; found->second.phase == SettlePhase::Submitted && settleBatchRunning(state, batch)) { - ++state.settlesWaited; - waitSettleBatch(state, batch); - } + if (found == state.settles.end()) return nullptr; + auto& batch = state.settleBatches[found->second.batch]; + const bool running = found->second.phase == SettlePhase::Submitted && settleBatchRunning(state, batch); state.settles.erase(found); + return running ? &batch : nullptr; } void drainSettles(HostImports& state) { @@ -258,6 +260,7 @@ void drainSettles(HostImports& state) { waitSettleBatch(state, batch); for (const auto alias : batch.aliases) state.settleDestroyBuffer(state.settleDevice, alias, nullptr); batch.aliases.clear(); + batch.retired.clear(); batch.bases.clear(); batch.busy = false; } @@ -304,7 +307,7 @@ bool joinedCovers(const GuestAllocations::Lease& lease, std::uint64_t begin, std void retireImport(VkDevice device, HostImports& state, std::map::iterator it, const GuestAllocations::Lease& lease, bool hold = true) { #ifdef _WIN32 - forgetSettle(state, it->first); + auto* reading = forgetSettle(state, it->first); #endif // Results shadowed for the import reach its (old) buffer first where the memory is still a // readable registered range (the import retires because its registration vanished or changed @@ -313,6 +316,12 @@ void retireImport(VkDevice device, HostImports& state, std::mapsecond.bytes; auto holder = std::make_shared(state.device, state.destroyBuffer, state.freeMemory, state.imports.extract(it)); +#ifdef _WIN32 + if (reading != nullptr) { + reading->retired.push_back(holder); + ++state.settlesParked; + } +#endif auto* recorder = Recorder::Active(); if (recorder == nullptr) return; if (hold) recorder->KeepThroughHold(std::move(holder)); @@ -339,6 +348,26 @@ bool releaseParkedImports(HostImports& state, std::uint64_t begin, std::uint64_t return !released.empty(); } +#ifdef _WIN32 +bool releaseSettleRetired(HostImports& state, std::uint64_t begin, std::uint64_t end) { + std::vector> released; + { + std::lock_guard lock(state.mutex); + for (auto& batch : state.settleBatches) { + const bool overlaps = std::any_of(batch.retired.begin(), batch.retired.end(), [&](const std::shared_ptr& holder) { return holder->node.mapped().base < end && begin < holder->node.mapped().base + holder->node.mapped().bytes; }); + if (!overlaps) continue; + if (settleBatchRunning(state, batch)) { + ++state.settleUnmapWaits; + waitSettleBatch(state, batch); + } + for (auto& holder : batch.retired) released.push_back(std::move(holder)); + batch.retired.clear(); + } + } + return !released.empty(); +} +#endif + const char* bindImport(const Context& context, HostImport& entry, VkExternalMemoryHandleTypeFlagBits handleType, const void* import, std::uint32_t importTypes, bool& allocated, VkResult& failure) { const auto failed = [&](const char* step, VkResult result) -> const char* { if (entry.buffer) context.Function("vkDestroyBuffer")(context.device, entry.buffer, nullptr); @@ -961,6 +990,7 @@ void reapSettleBatches(const Context& context, HostImports& state) { } for (const auto alias : batch.aliases) state.settleDestroyBuffer(context.device, alias, nullptr); batch.aliases.clear(); + batch.retired.clear(); batch.bases.clear(); batch.busy = false; } @@ -2286,7 +2316,7 @@ void reportMirrors() { #ifdef _WIN32 auto& imports = Imports(); std::lock_guard importsLock(imports.mutex); - if (imports.settlesSubmitted != 0 || !imports.settles.empty()) AgcDriver::ProfilePrint_nid_no_patch("[buffers] host imports under %llu KiB: %llu settled without waiting, %llu back in the write watch, %zu pending\n", static_cast(settleFloorBytes >> 10u), static_cast(imports.settlesSubmitted), static_cast(imports.settlesRewatched), imports.settles.size()); + if (imports.settlesSubmitted != 0 || imports.settlesWaited != 0 || !imports.settles.empty()) AgcDriver::ProfilePrint_nid_no_patch("[buffers] host imports under %llu KiB: %llu settle reads submitted, %llu back in the write watch, %llu retired during their read (%llu unmaps waited for it), %zu pending; %llu larger imports settled inside their window\n", static_cast(settleFloorBytes >> 10u), static_cast(imports.settlesSubmitted), static_cast(imports.settlesRewatched), static_cast(imports.settlesParked), static_cast(imports.settleUnmapWaits), imports.settles.size(), static_cast(imports.settlesWaited)); #endif } @@ -2381,6 +2411,9 @@ bool WaitForLeasesAndImports(std::uintptr_t address, std::size_t bytes) noexcept retiredImport = true; } } +#ifdef _WIN32 + if (releaseSettleRetired(imports, address, end)) retiredImport = true; +#endif if (retiredImport && recorder != nullptr && !recorder->Idle()) { Recorder::CountSync(4); recorder->Sync(); @@ -2458,6 +2491,9 @@ bool WaitForLeasesAndImports(std::uintptr_t address, std::size_t bytes) noexcept drained = true; } } +#ifdef _WIN32 + if (releaseSettleRetired(imports, address, end)) retiredImport = true; +#endif } catch (const std::exception& error) { std::fprintf(stderr, "[gpu] lease wait failed: %s\n", error.what()); } @@ -2873,7 +2909,7 @@ HostImportSettleCounts HostImportSettles() { #ifdef _WIN32 auto& state = Imports(); std::lock_guard lock(state.mutex); - return {state.settlesSubmitted, state.settlesWaited, state.settlesRewatched}; + return {state.settlesSubmitted, state.settlesWaited, state.settlesRewatched, state.settlesParked, state.settleUnmapWaits}; #else return {}; #endif diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 7b552949dc..25405473d4 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -1658,6 +1658,100 @@ bool deferredSettleTests(const Device& device) { std::cout << "deferred settle tests passed\n"; return true; } + +bool settledImportsWatchPrivate(const Context& context) { + using namespace AgcDriver::GuestMemory; + constexpr std::size_t settledBytes = std::size_t{1} << 20; + void* large = AllocateWatched(settledBytes, 65536); + const auto address = reinterpret_cast(large); + { + GuestAllocations::Mutation mutation; + mutation.Add(large, settledBytes, true, true, true); + } + const auto* settled = HostImportFor(context, address, settledBytes); + const bool watched = settled != nullptr && !settled->unwatched; + { + GuestAllocations::Mutation mutation; + mutation.Remove(large); + } + HostImportFor(context, address, settledBytes); + ReleaseWatched(large, settledBytes); + Require(settled != nullptr, "the 1 MiB import was refused"); + return watched; +} + +bool settleRetireTests(const Device& device) { + using namespace AgcDriver::GuestMemory; + const auto& context = device.GetContext(); + constexpr std::size_t unit = 65536; + constexpr int rounds = 64; + if (context.hostImportAlignment == 0 || context.hostImportAlignment > unit || context.settleQueue == VK_NULL_HANDLE || !WriteWatched()) { + std::cout << "host imports within 64 KiB, a settle queue or write watching unavailable: imports retired during their settle read not tested\n"; + return false; + } + std::unique_lock gpu(GpuMutex()); + PrepareImportWatch(context); + if (!settledImportsWatchPrivate(context)) { + std::cout << "this device compares every Windows import: imports retired during their settle read not tested\n"; + return false; + } + void* pair = AllocateWatched(2 * unit, unit); + struct Release { + void* pair; + ~Release() { + SetHostImportRangeWindow(std::uint64_t{64} << 20u); + ReleaseWatched(pair, 2 * unit); + } + } release{pair}; + const auto address = reinterpret_cast(pair); + std::memset(pair, 0x11, 2 * unit); + const auto registerPair = [&] { + GuestAllocations::Mutation mutation; + mutation.Add(pair, 2 * unit, true, true, true); + }; + const auto unregisterPair = [&] { + GuestAllocations::Mutation mutation; + mutation.Remove(pair); + }; + Recorder recorder(context); + recorder.Activate(); + SetHostImportRangeWindow(unit); + registerPair(); + const auto before = HostImportSettles(); + for (int round = 0; round < rounds; ++round) { + const auto* cell = HostImportFor(context, address, 16); + Require(cell != nullptr && cell->base == address && cell->bytes == unit && cell->unwatched, "(r) the first window of the range was not imported compared"); + std::this_thread::sleep_for(std::chrono::microseconds(150)); + HostImportFor(context, address, 16); + const auto* grown = HostImportFor(context, address + unit - 16, 32); + Require(grown != nullptr && grown->base == address && grown->bytes == 2 * unit, "(r) an access across the windows did not replace the first window's import"); + std::this_thread::sleep_for(std::chrono::milliseconds(2)); + HostImportFor(context, address + unit - 16, 32); + recorder.Sync(); + recorder.FlushDeferredReleases(); + unregisterPair(); + registerPair(); + } + const auto after = HostImportSettles(); + const auto* last = HostImportFor(context, address + unit - 16, 32); + Require(last != nullptr && last->bytes == 2 * unit && last->unwatched, "(r) the range was not imported whole and compared after the rounds"); + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5); + while (!Watched(address, 2 * unit) && std::chrono::steady_clock::now() < deadline) { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + HostImportFor(context, address + unit - 16, 32); + } + const bool back = Watched(address, 2 * unit); + std::cout << "(r) " << rounds << " window imports replaced after their settle read was submitted: " << after.parked - before.parked << " while the read ran, " << after.waited - before.waited << " waits, " << after.unmapWaits - before.unmapWaits << " unmaps waited for a read\n"; + Require(after.waited == before.waited, "(r) replacing an import during its settle read waited for the read"); + Require(after.parked != before.parked, "(r) no import was replaced while its settle read ran"); + Require(back, "(r) the import that replaced the windows did not return to the write watch"); + recorder.Sync(); + recorder.FlushDeferredReleases(); + unregisterPair(); + HostImportFor(context, address, 16); + std::cout << "imports retired during their settle read tests passed\n"; + return true; +} #endif void remappedImportTests(const Device& device) { @@ -8162,6 +8256,7 @@ int main(int argc, char** argv) { } return deferredSettleTests(device) ? 0 : 77; } + if (argc == 2 && std::string_view(argv[1]) == "--settle-retire-only") return settleRetireTests(device) ? 0 : 77; #endif if (argc == 2 && std::string_view(argv[1]) == "--host-import-retirement-only") { const auto alignment = device.GetContext().hostImportAlignment; diff --git a/docs/dev/ARCHITECTURE.md b/docs/dev/ARCHITECTURE.md index c425f5b66b..7a13f427c4 100644 --- a/docs/dev/ARCHITECTURE.md +++ b/docs/dev/ARCHITECTURE.md @@ -107,7 +107,7 @@ flowchart LR - A draw whose `PA_SC_AA_CONFIG` selects 2, 4 or 8 samples renders into host-only multisampled images. [`MultisampleTarget.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) keeps one image per device, surface address, format, extent and sample count (`MultisampleTargetView`); the pipeline, render pass and library keys carry `State::samples`, and a multisampled depth target is a multisampled depth surface. The guest sample planes and FMASK are never read or written, so the registry also guards the target's guest range: a CPU read (`GuestMemory::SetReadGuard`), a DMA copy, and a sampled or storage image read of it throw. A `CB_COLOR_CONTROL` resolve mode over a multisampled CB0 runs as a render pass whose resolve attachment is CB1's resident image (`ResolveMultisampleTarget`). A zero fill of the target's CMASK (`NoteColorMetadataFill`, from fill kernels and CP DMA fills) clears the image at its next use. - A texture over a depth surface samples the depth image itself when it views one 2D slice of the plane's own format (`DepthSurfaceTexture` in [`DepthSurface.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp)). A view of several slices (a 2D array or a cube) or an integer view of a D16 plane is served by a `DepthPlaneCopy`, an image the slices are copied into at every use: from the depth surface bound at each slice's address, else from guest memory. Such a texture is never kept in the resource templates (`RefreshedPerUse`). - Guest memory reaches the GPU through host imports (`VK_EXT_external_memory_host`) where the driver accepts them, and through copies otherwise ([`GuestBufferMemory.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp)). A registered range larger than the import window (64 MiB) is imported per window for buffer and fill accesses and whole for an address-based build. Live imports stay under a budget, with the least recently used import retired first (see TechnicalDebt, Host import windows and budget). -- On Windows, a host import of private `MEM_WRITE_WATCH` memory (singly mapped direct memory) stays on `GetWriteWatch` when `ProbeImportWriteProtection` shows the driver reports nothing written after the second GPU read and a GPU write, and a CPU store is collected (`decideImportWatch`). The driver marks every page of such an import written, asynchronously, until its first GPU use completes, so `settleImport` in [`GuestBufferMemory.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) copies 4 bytes out of the new import on a second queue of the graphics family, or on a queue of another copy-capable family when the graphics family has only one (`Context::settleQueue`, own pool and mutex) inside `ImportWatched`'s window and waits for it, for imports of 1 MiB or more; the main queue can't be used there because `importAllocation` holds the imports lock and `GpuMutex` comes before it in the lock order. A smaller import is compared when it is made, so a title that maps, imports and unmaps a small buffer around every submission does not block its mapping thread. Its 4-byte read is queued instead: the next import lookup that holds `GpuMutex` while the imports match the registry (`settleUnwatchedImports`) records the queued reads into one batch on the settle queue, and once the batch's fence has signalled a later lookup returns the import's whole 64 KiB blocks to the write watch (`GuestMemory::Rewatch` drops their marks and stamps every block as a CPU store), except while storage results are pending over them. A settled import whose range had already left the watch returns the same way, and an import retired while its read is in flight waits for that batch. Imports through a section alias keep tracking on the guest views, and every other Windows host import is compared. A device without such a queue keeps the comparisons. +- On Windows, a host import of private `MEM_WRITE_WATCH` memory (singly mapped direct memory) stays on `GetWriteWatch` when `ProbeImportWriteProtection` shows the driver reports nothing written after the second GPU read and a GPU write, and a CPU store is collected (`decideImportWatch`). The driver marks every page of such an import written, asynchronously, until its first GPU use completes, so `settleImport` in [`GuestBufferMemory.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) copies 4 bytes out of the new import on a second queue of the graphics family, or on a queue of another copy-capable family when the graphics family has only one (`Context::settleQueue`, own pool and mutex) inside `ImportWatched`'s window and waits for it, for imports of 1 MiB or more; the main queue can't be used there because `importAllocation` holds the imports lock and `GpuMutex` comes before it in the lock order. A smaller import is compared when it is made, so a title that maps, imports and unmaps a small buffer around every submission does not block its mapping thread. Its 4-byte read is queued instead: the next import lookup that holds `GpuMutex` while the imports match the registry (`settleUnwatchedImports`) records the queued reads into one batch on the settle queue, and once the batch's fence has signalled a later lookup returns the import's whole 64 KiB blocks to the write watch (`GuestMemory::Rewatch` drops their marks and stamps every block as a CPU store), except while storage results are pending over them. A settled import whose range had already left the watch returns the same way, and an import retired while its read is in flight is kept with that batch and freed once the batch has completed, without waiting for it; a guest unmap of its memory waits for the batch instead. Imports through a section alias keep tracking on the guest views, and every other Windows host import is compared. A device without such a queue keeps the comparisons. ### Runtime image and sampler tables From 815b8add6f5cbe28f4cf7f2c8ad021ca31884dcc Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Thu, 15 Oct 2026 23:20:51 -0700 Subject: [PATCH 0448/1000] test(agc): check that a settle read ends the driver's marking of an import A Windows import under 1 MiB returns to the write watch once its settle read has completed, though main-queue work submitted before the read may have used the import first, and an earlier import of the same pages may not have run its first use yet. On the NVIDIA driver (617.42) neither marks the pages once the read has completed, so the rewatch needs no wait for the main queue. The docs said the marking lasts until the import's first GPU use completes; they now say that a completed GPU use ends it, the settle read included. agc_import_settle_first_use submits 4096 copies of a 64 KiB import on the main queue before its settle read is queued. The import must return to the write watch while the copies still run, and the driver must mark none of its pages afterwards. agc_import_settle_earlier_import records 4096 copies of a 64 KiB window import, replaces it with the import of its whole 128 KiB range, and submits the copies only once the range is back in the write watch. The driver must mark none of the range's pages while they run. A driver that keeps marking fails either test. --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 6 +- .../prx/libSceAgcDriver/tests/Recorder.cpp | 139 ++++++++++++++++++ docs/dev/ARCHITECTURE.md | 2 +- docs/dev/debt/libSceAgcDriver.md | 2 +- 4 files changed, 145 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 96b785a5c7..34198c721c 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1064,9 +1064,11 @@ if(BUILD_TESTING) set_tests_properties(agc_import_watch_other_family PROPERTIES SKIP_RETURN_CODE 77) add_test(NAME agc_import_settle_deferred COMMAND agc_driver_recorder_tests --deferred-settle-only) add_test(NAME agc_import_settle_deferred_other_family COMMAND agc_driver_recorder_tests --deferred-settle-other-family-only) + add_test(NAME agc_import_settle_first_use COMMAND agc_driver_recorder_tests --settle-first-use-only) + add_test(NAME agc_import_settle_earlier_import COMMAND agc_driver_recorder_tests --settle-earlier-import-only) add_test(NAME agc_import_settle_retire COMMAND agc_driver_recorder_tests --settle-retire-only) - set_tests_properties(agc_import_settle_deferred agc_import_settle_deferred_other_family agc_import_settle_retire PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 60) - set_tests_properties(agc_import_watch agc_import_watch_other_family agc_import_settle_deferred agc_import_settle_deferred_other_family agc_import_settle_retire PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(agc_import_settle_deferred agc_import_settle_deferred_other_family agc_import_settle_first_use agc_import_settle_earlier_import agc_import_settle_retire PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 60) + set_tests_properties(agc_import_watch agc_import_watch_other_family agc_import_settle_deferred agc_import_settle_deferred_other_family agc_import_settle_first_use agc_import_settle_earlier_import agc_import_settle_retire PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() add_test(NAME agc_unchanged_cpu_stamps COMMAND agc_driver_recorder_tests --unchanged-cpu-stamps-only) diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 25405473d4..a21e03b80c 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -1680,6 +1680,143 @@ bool settledImportsWatchPrivate(const Context& context) { return watched; } +bool settleFirstUseTests(const Device& device) { + using namespace AgcDriver::GuestMemory; + const auto& context = device.GetContext(); + constexpr std::size_t unit = 65536; + constexpr int copies = 4096; + if (context.hostImportAlignment == 0 || context.hostImportAlignment > unit || context.settleQueue == VK_NULL_HANDLE || !WriteWatched()) { + std::cout << "host imports within 64 KiB, a settle queue or write watching unavailable: the first use of a settled import not tested\n"; + return false; + } + std::unique_lock gpu(GpuMutex()); + PrepareImportWatch(context); + if (!settledImportsWatchPrivate(context)) { + std::cout << "this device compares every Windows import: the first use of a settled import not tested\n"; + return false; + } + void* small = AllocateWatched(unit, unit); + struct Release { + void* small; + ~Release() { ReleaseWatched(small, unit); } + } release{small}; + const auto address = reinterpret_cast(small); + std::memset(small, 0x11, unit); + Recorder recorder(context); + recorder.Activate(); + Buffer scratch(context, unit, VK_BUFFER_USAGE_TRANSFER_DST_BIT); + { + GuestAllocations::Mutation mutation; + mutation.Add(small, unit, true, true, true); + } + const auto* import = HostImportFor(context, address, unit); + Require(import != nullptr && import->unwatched && !Watched(address, unit), "(f) a 64 KiB import was not compared at first"); + const auto commands = recorder.Commands(); + for (int copy = 0; copy < copies; ++copy) CopyBuffer(context, commands, import->buffer, 0, scratch.Handle(), 0, unit); + recorder.Submit(); + const auto submittedAt = std::chrono::steady_clock::now(); + const auto deadline = submittedAt + std::chrono::seconds(5); + std::uint64_t collected = 0; + bool returned = false; + auto returnedAt = submittedAt; + while (!returned && std::chrono::steady_clock::now() < deadline) { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + HostImportFor(context, address, unit); + if (Watched(address, unit)) { + returnedAt = std::chrono::steady_clock::now(); + BumpCollectEpoch(); + collected = CollectWrites(address, unit); + returned = true; + } + } + recorder.Reap(); + const bool racing = returned && recorder.InFlightBatches() != 0; + while (recorder.InFlightBatches() != 0 && std::chrono::steady_clock::now() < deadline) { + std::this_thread::sleep_for(std::chrono::microseconds(200)); + recorder.Reap(); + } + const auto reapedAt = std::chrono::steady_clock::now(); + recorder.Sync(); + CollectWritesUncached(address, unit); + const bool unchanged = returned && UnchangedSince(address, unit, collected); + const auto since = [&](std::chrono::steady_clock::time_point at) { return std::chrono::duration(at - submittedAt).count(); }; + std::cout << "(f) settled import back in the write watch " << since(returnedAt) << " ms after main-queue copies of it were submitted, copies reaped by " << since(reapedAt) << " ms; driver marks after its return=" << !unchanged << '\n'; + Require(returned, "(f) a settled import did not return to the write watch"); + Require(racing, "(f) the main-queue copies had finished before the import returned to the write watch, so its read did not race them"); + Require(unchanged, "(f) the driver marked a settled import's pages after its settle read completed while earlier main-queue copies of it ran"); + { + GuestAllocations::Mutation mutation; + mutation.Remove(small); + } + HostImportFor(context, address, unit); + std::cout << "settled import first use tests passed\n"; + return true; +} + +bool settleEarlierImportTests(const Device& device) { + using namespace AgcDriver::GuestMemory; + const auto& context = device.GetContext(); + constexpr std::size_t unit = 65536; + constexpr int copies = 4096; + if (context.hostImportAlignment == 0 || context.hostImportAlignment > unit || context.settleQueue == VK_NULL_HANDLE || !WriteWatched()) { + std::cout << "host imports within 64 KiB, a settle queue or write watching unavailable: an earlier import's first use after a settled import's return not tested\n"; + return false; + } + std::unique_lock gpu(GpuMutex()); + PrepareImportWatch(context); + if (!settledImportsWatchPrivate(context)) { + std::cout << "this device compares every Windows import: an earlier import's first use after a settled import's return not tested\n"; + return false; + } + void* pair = AllocateWatched(2 * unit, unit); + struct Release { + void* pair; + ~Release() { + SetHostImportRangeWindow(std::uint64_t{64} << 20u); + ReleaseWatched(pair, 2 * unit); + } + } release{pair}; + const auto address = reinterpret_cast(pair); + std::memset(pair, 0x11, 2 * unit); + Recorder recorder(context); + recorder.Activate(); + Buffer scratch(context, unit, VK_BUFFER_USAGE_TRANSFER_DST_BIT); + SetHostImportRangeWindow(unit); + { + GuestAllocations::Mutation mutation; + mutation.Add(pair, 2 * unit, true, true, true); + } + const auto* earlier = HostImportFor(context, address, 16); + Require(earlier != nullptr && earlier->base == address && earlier->bytes == unit && earlier->unwatched, "(e) the first window of the range was not imported compared"); + const auto commands = recorder.Commands(); + for (int copy = 0; copy < copies; ++copy) CopyBuffer(context, commands, earlier->buffer, 0, scratch.Handle(), 0, unit); + const auto* whole = HostImportFor(context, address + unit - 16, 32); + Require(whole != nullptr && whole->base == address && whole->bytes == 2 * unit && whole->unwatched, "(e) an access across the windows did not replace the first window's import"); + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5); + while (!Watched(address, 2 * unit) && std::chrono::steady_clock::now() < deadline) { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + HostImportFor(context, address + unit - 16, 32); + } + Require(Watched(address, 2 * unit), "(e) the range's import did not return to the write watch"); + Require(recorder.Recording(), "(e) the earlier import's copies were submitted before the range's import returned to the write watch"); + BumpCollectEpoch(); + const auto collected = CollectWrites(address, 2 * unit); + const auto submittedAt = std::chrono::steady_clock::now(); + recorder.Sync(); + const auto syncedAt = std::chrono::steady_clock::now(); + CollectWritesUncached(address, 2 * unit); + const bool unchanged = UnchangedSince(address, 2 * unit, collected); + std::cout << "(e) the range's import back in the write watch before the replaced window's first use ran (" << std::chrono::duration(syncedAt - submittedAt).count() << " ms of copies after it); driver marks after its return=" << !unchanged << '\n'; + Require(unchanged, "(e) the driver marked the pages of a settled import while a replaced import of them ran its first use"); + { + GuestAllocations::Mutation mutation; + mutation.Remove(pair); + } + HostImportFor(context, address, 16); + std::cout << "earlier import first use tests passed\n"; + return true; +} + bool settleRetireTests(const Device& device) { using namespace AgcDriver::GuestMemory; const auto& context = device.GetContext(); @@ -8256,6 +8393,8 @@ int main(int argc, char** argv) { } return deferredSettleTests(device) ? 0 : 77; } + if (argc == 2 && std::string_view(argv[1]) == "--settle-first-use-only") return settleFirstUseTests(device) ? 0 : 77; + if (argc == 2 && std::string_view(argv[1]) == "--settle-earlier-import-only") return settleEarlierImportTests(device) ? 0 : 77; if (argc == 2 && std::string_view(argv[1]) == "--settle-retire-only") return settleRetireTests(device) ? 0 : 77; #endif if (argc == 2 && std::string_view(argv[1]) == "--host-import-retirement-only") { diff --git a/docs/dev/ARCHITECTURE.md b/docs/dev/ARCHITECTURE.md index 7a13f427c4..7c9dd25c52 100644 --- a/docs/dev/ARCHITECTURE.md +++ b/docs/dev/ARCHITECTURE.md @@ -107,7 +107,7 @@ flowchart LR - A draw whose `PA_SC_AA_CONFIG` selects 2, 4 or 8 samples renders into host-only multisampled images. [`MultisampleTarget.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) keeps one image per device, surface address, format, extent and sample count (`MultisampleTargetView`); the pipeline, render pass and library keys carry `State::samples`, and a multisampled depth target is a multisampled depth surface. The guest sample planes and FMASK are never read or written, so the registry also guards the target's guest range: a CPU read (`GuestMemory::SetReadGuard`), a DMA copy, and a sampled or storage image read of it throw. A `CB_COLOR_CONTROL` resolve mode over a multisampled CB0 runs as a render pass whose resolve attachment is CB1's resident image (`ResolveMultisampleTarget`). A zero fill of the target's CMASK (`NoteColorMetadataFill`, from fill kernels and CP DMA fills) clears the image at its next use. - A texture over a depth surface samples the depth image itself when it views one 2D slice of the plane's own format (`DepthSurfaceTexture` in [`DepthSurface.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp)). A view of several slices (a 2D array or a cube) or an integer view of a D16 plane is served by a `DepthPlaneCopy`, an image the slices are copied into at every use: from the depth surface bound at each slice's address, else from guest memory. Such a texture is never kept in the resource templates (`RefreshedPerUse`). - Guest memory reaches the GPU through host imports (`VK_EXT_external_memory_host`) where the driver accepts them, and through copies otherwise ([`GuestBufferMemory.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp)). A registered range larger than the import window (64 MiB) is imported per window for buffer and fill accesses and whole for an address-based build. Live imports stay under a budget, with the least recently used import retired first (see TechnicalDebt, Host import windows and budget). -- On Windows, a host import of private `MEM_WRITE_WATCH` memory (singly mapped direct memory) stays on `GetWriteWatch` when `ProbeImportWriteProtection` shows the driver reports nothing written after the second GPU read and a GPU write, and a CPU store is collected (`decideImportWatch`). The driver marks every page of such an import written, asynchronously, until its first GPU use completes, so `settleImport` in [`GuestBufferMemory.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) copies 4 bytes out of the new import on a second queue of the graphics family, or on a queue of another copy-capable family when the graphics family has only one (`Context::settleQueue`, own pool and mutex) inside `ImportWatched`'s window and waits for it, for imports of 1 MiB or more; the main queue can't be used there because `importAllocation` holds the imports lock and `GpuMutex` comes before it in the lock order. A smaller import is compared when it is made, so a title that maps, imports and unmaps a small buffer around every submission does not block its mapping thread. Its 4-byte read is queued instead: the next import lookup that holds `GpuMutex` while the imports match the registry (`settleUnwatchedImports`) records the queued reads into one batch on the settle queue, and once the batch's fence has signalled a later lookup returns the import's whole 64 KiB blocks to the write watch (`GuestMemory::Rewatch` drops their marks and stamps every block as a CPU store), except while storage results are pending over them. A settled import whose range had already left the watch returns the same way, and an import retired while its read is in flight is kept with that batch and freed once the batch has completed, without waiting for it; a guest unmap of its memory waits for the batch instead. Imports through a section alias keep tracking on the guest views, and every other Windows host import is compared. A device without such a queue keeps the comparisons. +- On Windows, a host import of private `MEM_WRITE_WATCH` memory (singly mapped direct memory) stays on `GetWriteWatch` when `ProbeImportWriteProtection` shows the driver reports nothing written after the second GPU read and a GPU write, and a CPU store is collected (`decideImportWatch`). The driver marks every page of such an import written, asynchronously, until a GPU use of it completes (a read on the settle queue ends it even while main-queue work that used the import first is still running, and the first use of an earlier import of the same pages after that read marks nothing), so `settleImport` in [`GuestBufferMemory.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) copies 4 bytes out of the new import on a second queue of the graphics family, or on a queue of another copy-capable family when the graphics family has only one (`Context::settleQueue`, own pool and mutex) inside `ImportWatched`'s window and waits for it, for imports of 1 MiB or more; the main queue can't be used there because `importAllocation` holds the imports lock and `GpuMutex` comes before it in the lock order. A smaller import is compared when it is made, so a title that maps, imports and unmaps a small buffer around every submission does not block its mapping thread. Its 4-byte read is queued instead: the next import lookup that holds `GpuMutex` while the imports match the registry (`settleUnwatchedImports`) records the queued reads into one batch on the settle queue, and once the batch's fence has signalled a later lookup returns the import's whole 64 KiB blocks to the write watch (`GuestMemory::Rewatch` drops their marks and stamps every block as a CPU store), except while storage results are pending over them. A settled import whose range had already left the watch returns the same way, and an import retired while its read is in flight is kept with that batch and freed once the batch has completed, without waiting for it; a guest unmap of its memory waits for the batch instead. Imports through a section alias keep tracking on the guest views, and every other Windows host import is compared. A device without such a queue keeps the comparisons. ### Runtime image and sampler tables diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index b733145d09..5762dcca0c 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -73,7 +73,7 @@ - [Pending storage-image results](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) are stored when something reads their memory, not when the GPU work that wrote them completes. A title that waits for that work, frees the image and reuses its memory can have the new data overwritten by the late store (#1970). A title's own CPU reads are not seen either, so a title that reads the results after waiting for a label reads stale memory. `APS5_STORE_AT_FLIP=1` stores every pending result at each flip and `APS5_STORE_AT_LABEL=1` before each label the GPU writes; both store every pending image, not only the ones the waited-for work wrote, and neither is on by default. - [Pending storage-image results over memory outside the write watch](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) leave to the CPU the 64 KiB blocks whose bytes differ from the ones saved at the upload, the fill, the image copy, the previous store or the moment their range left the watch (`Unwatch`, or a host import that does not stay watched), and the blocks the watch saw the CPU write before that moment. A store made on the GPU saves its bytes through a host-visible copy that becomes the saved bytes when its batch completes; a store or a refresh that needs them earlier waits for that batch. A unit without a write generation saves the guest bytes when results land in it outside the watch. One whose range leaves the watch while its results are pending, and ranges that leave the watch through a failed collect, have no saved bytes; storing such results throws. - Windows host-pointer imports of the guest mapping itself are compared instead of write-watched, except writable imports of private `MEM_WRITE_WATCH` memory when the import probe keeps the write watch and a settle queue exists: those of 1 MiB or more stay watched, and smaller ones return to the watch once their settle has completed (see Pages under an import window): a driver can mark imported pages dirty after the synchronous import window. Shared memory imported through a separate writable alias retains protection-based tracking on the guest views; driver access to the import alias does not disarm those views. [GuestMemory](../../../core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp) excludes every overlapping 64 KiB tracker block while its backing is retained. A full block replaced by fresh private backing regains tracking and gets a new generation; protection changes, partial replacements and shared remappings do not restore it. Excluded blocks keep their write-watch marks as a hint only (`UntrackedHint`): a storage image or a sampled texture snapshot over them compares its bytes again after a mark in its range, a driver store, a mapping change or a recorded GPU write instead of at every collect epoch or lookup, and a mark the driver makes without a CPU store costs one comparison. This preserves CPU edits through byte comparisons but limits snapshot reuse on excluded blocks to draws of one collect epoch and disables unit shadows for them; restoring tracking on import retirement without replacing the backing remains unimplemented. -- [Pages under an import window](../../../core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp) (libSceAgcDriver) whose write protection was resolved while a host import that leaves its range watched was made are compared with the bytes they held before the import and count as stored over only when those differ. A CPU store of the bytes already there within the window cannot be told apart from an import that resolves the write protection without storing, as the NVIDIA driver (610.57.04) does for every imported page, so such a store does not win over pending results. Where host-pointer and dma-buf imports differ in whether they stay watched, a failed host-pointer import and the dma-buf import after it are two windows, and the pages the failed one resolved count as stored over. On Windows, the NVIDIA driver (617.42) marks every page of a host import of private `MEM_WRITE_WATCH` memory written, asynchronously, until the import's first GPU use completes. Such an import stays watched only after a 4-byte read of it on the settle queue has completed inside the window. Each one waits for that read, which keeps its window open for one round trip on the settle queue, so a same-value CPU store from another thread in that time counts as unchanged too. An import made where no settle queue exists is compared instead. An import of under 1 MiB is not settled inside its window, because the settle blocks the importing thread and a title that imports a 64 KiB buffer around every submission lost about 19 % of its frame rate to that wait (Dripl1x, #6060). It is compared until its read, submitted in a batch by a later import lookup under `GpuMutex` while the imports match the registry, has completed; a later lookup then returns its whole blocks to the write watch, each stamped as a CPU store, so whatever was derived from them while they were compared reads as changed once. Its blocks stay compared while storage results are pending over them, and those of an import retired before that stay compared. +- [Pages under an import window](../../../core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp) (libSceAgcDriver) whose write protection was resolved while a host import that leaves its range watched was made are compared with the bytes they held before the import and count as stored over only when those differ. A CPU store of the bytes already there within the window cannot be told apart from an import that resolves the write protection without storing, as the NVIDIA driver (610.57.04) does for every imported page, so such a store does not win over pending results. Where host-pointer and dma-buf imports differ in whether they stay watched, a failed host-pointer import and the dma-buf import after it are two windows, and the pages the failed one resolved count as stored over. On Windows, the NVIDIA driver (617.42) marks every page of a host import of private `MEM_WRITE_WATCH` memory written, asynchronously, until a GPU use of the import completes; a 4-byte read of it on the settle queue ends that even while main-queue work that used the import first is still running, and an earlier import of the same pages whose first use runs after that read marks none of them (`agc_import_settle_first_use`, `agc_import_settle_earlier_import`). Such an import stays watched only after a 4-byte read of it on the settle queue has completed inside the window. Each one waits for that read, which keeps its window open for one round trip on the settle queue, so a same-value CPU store from another thread in that time counts as unchanged too. An import made where no settle queue exists is compared instead. An import of under 1 MiB is not settled inside its window, because the settle blocks the importing thread and a title that imports a 64 KiB buffer around every submission lost about 19 % of its frame rate to that wait (Dripl1x, #6060). It is compared until its read, submitted in a batch by a later import lookup under `GpuMutex` while the imports match the registry, has completed; a later lookup then returns its whole blocks to the write watch, each stamped as a CPU store, so whatever was derived from them while they were compared reads as changed once. Its blocks stay compared while storage results are pending over them, and those of an import retired before that stay compared. - [Sparse dma-buf imports](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver): on Linux a shared-backed range larger than one 64 MiB udmabuf buffer is imported as one sparse buffer bound to the 64 MiB buffers, on a queue of its own when the device has a second family that binds sparse memory. The path is decided before anything is imported, and a failure after that throws. A range takes the single udmabuf import instead, which udmabuf's `size_limit_mb` refuses above 64 MiB by default so the range is copied, when its offset in the backing file is not a multiple of the buffer's sparse block size (64 KiB on RADV), when the sparse buffer does not accept every memory type a chunk import can have, when the backing file ends inside its last chunk, or when it is imported at map time on a device with no second family (the bind then needs the GPU mutex); the NVIDIA proprietary driver imports no dma-buf memory - [Unnormalized S# coordinates](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp) (libSceAgcDriver) - `FORCE_UNNORMALIZED` uses a Vulkan `unnormalizedCoordinates` sampler, so the S# LOD range, LOD bias and mip filter are not applied, which is exact only on a one-level view. - An [unnormalized S#](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp) (libSceAgcDriver) maps clamp mode 0 (repeat) on X and Y to clamp-to-edge as required by Vulkan unnormalized coordinates, and assigns the magnification filter when minification and magnification filters differ (since unnormalized coordinates sample at LOD 0). Anisotropy, TRUNC_COORD, MC_COORD_TRUNC and other clamp modes still throw. Secondary LOD bias (LOD_BIAS_SEC, bits [19:14]) is accepted and ignored, matching RDNA2 hardware measurements across explicit and implicit samples. High on Life (PPSA15388, #4736) uses secondary LOD bias and unnormalized coordinates with clamp mode 0 and differing min/mag filters during home base and city rendering. From 9bf98992ff50113a15721be0ba73715b6eb8465f Mon Sep 17 00:00:00 2001 From: simonbronk <275178677+simonbronk@users.noreply.github.com> Date: Wed, 14 Oct 2026 17:10:42 +0200 Subject: [PATCH 0449/1000] feat(agc): rasterize a multisampled draw into one-sample targets at sample 0's locations A draw whose rasterizer has more samples than its color or depth targets threw "a color target's sample count differs from the rasterizer's" or "the depth target's sample count differs from the rasterizer's". When every target has one sample, MSAA_ENABLE is set, and there is no alpha-to-coverage, no PS_ITER_SAMPLE and no sample-dependent pixel input, only sample 0's coverage reaches the targets, so draw it with one sample at sample 0's location in each pixel of the grid. One-sample depth images are created sample-location compatible when the device allows it. Co-Authored-By: Claude Opus 5.5 --- .../Execution/src/VulkanDevice.cpp | 2 +- .../Graphics/src/DepthSurface.cpp | 2 +- .../libSceAgcDriver/Graphics/src/Pipeline.cpp | 2 +- .../libSceAgcDriver/Graphics/src/State.cpp | 22 ++++++++++- .../prx/libSceAgcDriver/tests/Graphics.cpp | 37 +++++++++++++++++++ docs/dev/debt/libSceAgcDriver.md | 2 +- 6 files changed, 62 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 694152052f..ac6415f9b2 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -1266,7 +1266,7 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : state->InstanceFunction("vkGetPhysicalDeviceProperties2")(selected, &properties); if (locations.sampleLocationSubPixelBits >= 4 && locations.sampleLocationCoordinateRange[0] <= 0.0f && locations.sampleLocationCoordinateRange[1] >= 15.0f / 16.0f) { const auto multisampleProperties = state->InstanceFunction("vkGetPhysicalDeviceMultisamplePropertiesEXT"); - for (const auto samples : {VK_SAMPLE_COUNT_2_BIT, VK_SAMPLE_COUNT_4_BIT, VK_SAMPLE_COUNT_8_BIT}) { + for (const auto samples : {VK_SAMPLE_COUNT_1_BIT, VK_SAMPLE_COUNT_2_BIT, VK_SAMPLE_COUNT_4_BIT, VK_SAMPLE_COUNT_8_BIT}) { if ((locations.sampleLocationSampleCounts & samples) == 0) continue; VkMultisamplePropertiesEXT multisample{VK_STRUCTURE_TYPE_MULTISAMPLE_PROPERTIES_EXT}; multisampleProperties(selected, samples, &multisample); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp index b15945ee89..555cacd4e0 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp @@ -1139,7 +1139,7 @@ void RetireUnmappedDepth_nid_no_patch(std::uintptr_t address, std::size_t bytes) } bool SampleLocationsCompatibleDepth(const Context& context, VkFormat format, std::uint32_t samples) { - if (samples <= 1 || (context.sampleLocationSampleCounts & samples) == 0) return false; + if (samples == 0 || (context.sampleLocationSampleCounts & samples) == 0) return false; VkImageFormatProperties properties{}; const auto result = context.imageFormatProperties(context.physical, format, VK_IMAGE_TYPE_2D, VK_IMAGE_TILING_OPTIMAL, VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT, VK_IMAGE_CREATE_SAMPLE_LOCATIONS_COMPATIBLE_DEPTH_BIT_EXT, &properties); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp index 9fd5790a24..b03f09e744 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp @@ -118,7 +118,7 @@ bool ValidateSampleLocations(const Context& context, const State& state) { const bool extensionSupportsSamples = (context.sampleLocationSampleCounts & state.samples) != 0; Require(!state.customSampleLocations || extensionSupportsSamples, "custom sample locations require VK_EXT_sample_locations support for the draw's sample count"); Require(!state.sampleLocationGrid || (context.sampleLocationGridSampleCounts & state.samples) != 0, "sample locations varying between pixels require a 2x2 sample location grid for the draw's sample count"); - const bool enabled = extensionSupportsSamples && (!state.depth || SampleLocationsCompatibleDepth(context, HostDepthFormat(context, state.depth->format), state.depth->samples)); + const bool enabled = extensionSupportsSamples && (state.samples > 1 || state.customSampleLocations) && (!state.depth || SampleLocationsCompatibleDepth(context, HostDepthFormat(context, state.depth->format), state.depth->samples)); Require(!state.customSampleLocations || enabled, "custom sample locations require a compatible depth format and sample count"); return enabled; } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index b767129e68..5de09c73b5 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -415,6 +415,7 @@ std::string singleSampleMsaaRejection(const QueueState& queue) { } constexpr std::uint32_t CoveredCentroidIsCenter = 1u << 29u; +constexpr std::uint32_t SampleDependentInputs = ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PerspectiveSample) | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PerspectiveCentroid) | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::LinearSample) | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::LinearCentroid) | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::Ancillary) | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::SampleCoverage); constexpr std::uint32_t CentroidInputs = ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PerspectiveCentroid) | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::LinearCentroid); bool coveredCentroidSupported(const QueueState& queue) { @@ -489,6 +490,19 @@ void decodeSampleLocations(const Registers& cx, State& state) { } } +bool rasterizesSampleZeroOnly(const Registers& cx, const State& state) { + return state.samples > 1 && !state.alphaToCoverage && !state.perSampleShading && (read(cx, 0x292) & 1u) != 0 && (read(cx, 0x1b3) & read(cx, 0x1b4) & SampleDependentInputs) == 0; +} + +void rasterizeSampleZero(State& state) { + std::array sampleZero{}; + for (std::uint32_t pixel = 0; pixel < 4; ++pixel) sampleZero[pixel * 8u] = state.sampleLocations[pixel * 8u]; + state.samples = 1; + state.sampleLocations = sampleZero; + state.customSampleLocations = std::any_of(sampleZero.begin(), sampleZero.end(), [](std::uint8_t bits) { return bits != 0; }); + state.sampleLocationGrid = sampleZero[8] != sampleZero[0] || sampleZero[16] != sampleZero[0] || sampleZero[24] != sampleZero[0]; +} + bool colorControlSupported(std::uint32_t colorControl, bool hasColorTarget) { colorControl &= ~1u; return colorControl == 0xcc0010u || ((colorControl >> 4u) & 7u) == 0u || (!hasColorTarget && (colorControl & ~0x70u) == 0xcc0000u); @@ -1031,7 +1045,8 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) { Require(ditheredAlphaSamplesSupported(cx), "dithered alpha-to-coverage requires eight matched raster/DB_EQAA samples and one pixel iteration"); Require(!result.customSampleLocations, "dithered alpha-to-coverage with custom sample locations is unsupported"); } - Require(!result.depth || result.depth->samples == result.samples, "the depth target's sample count differs from the rasterizer's"); + const bool depthKeepsSampleZero = result.depth && result.depth->samples == 1 && result.samples > 1; + Require(!result.depth || depthKeepsSampleZero || result.depth->samples == result.samples, "the depth target's sample count differs from the rasterizer's"); zero(cx, 0x292, ScanModeMask & ~1u, "scan conversion mode"); if (result.samples > 1 && (read(cx, 0x292) & 1u) == 0) { const bool filled = result.topology != VK_PRIMITIVE_TOPOLOGY_POINT_LIST && result.topology != VK_PRIMITIVE_TOPOLOGY_LINE_LIST && result.topology != VK_PRIMITIVE_TOPOLOGY_LINE_STRIP; @@ -1046,6 +1061,10 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) { } zero(cx, 0x293, ScanControlMask, "primitive discard or out-of-order rasterization"); result.perSampleShading = result.samples > 1 && (read(cx, 0x293) & PixelShaderIteratesSamples) != 0; + if (depthKeepsSampleZero) { + Require(rasterizesSampleZeroOnly(cx, result), "the depth target's sample count differs from the rasterizer's"); + rasterizeSampleZero(result); + } zero(cx, 0x80, ~0u, "window offset"); zero(cx, 0x8d, ScreenOffsetMask, "reserved PA_SU_HARDWARE_SCREEN_OFFSET bits"); Require(read(cx, 0x83) == 0xffffu, "clip rectangles are unsupported"); @@ -1111,6 +1130,7 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) { result.customSampleLocations = true; result.sampleLocations.fill(0); } + if (color.samples == 1 && result.colors.empty() && !result.depth && rasterizesSampleZeroOnly(cx, result)) rasterizeSampleZero(result); Require(color.samples == result.samples, "a color target's sample count differs from the rasterizer's"); APS5_LOG_OUT_DEBUG("Color %u address=0x%llx extent=%ux%u bytes=%llu VkFormat=%u", slot, static_cast(color.address), color.extent.width, color.extent.height, static_cast(color.bytes), static_cast(color.format)); if (result.colors.empty()) { diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 2e1d26e11f..22326d1c25 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -2645,6 +2645,42 @@ void multisampleTests() { expectFailure([&] { AgcDriver::Graphics::ValidateSampleLocations(gridContext, state); }, "2x2 sample location grid"); gridContext.sampleLocationGridSampleCounts |= VK_SAMPLE_COUNT_2_BIT; Require(AgcDriver::Graphics::ValidateSampleLocations(gridContext, state), "a supported 2x2 sample location grid was refused"); + queue.context[0x31d] = 0; + state = DecodeState(queue); + Require(state.samples == 1 && state.colors.size() == 1 && state.colors[0].samples == 1 && state.customSampleLocations && state.sampleLocationGrid, "2x checkerboard rasterization into a one-sample target (PPSA27836) did not rasterize sample 0 alone"); + const auto sampleZero = AgcDriver::Graphics::SampleLocations(state); + const std::array expectedSampleZero{{{0.75f, 0.5f}, {0.75f, 0.5f}, {0.25f, 0.5f}, {0.25f, 0.5f}}}; + for (std::size_t pixel = 0; pixel < expectedSampleZero.size(); ++pixel) Require(sampleZero[pixel].x == expectedSampleZero[pixel].x && sampleZero[pixel].y == expectedSampleZero[pixel].y, "a one-sample target did not take sample 0's location in each pixel of the grid"); + gridContext.sampleLocationSampleCounts = VK_SAMPLE_COUNT_2_BIT; + gridContext.sampleLocationGridSampleCounts = VK_SAMPLE_COUNT_2_BIT; + expectFailure([&] { AgcDriver::Graphics::ValidateSampleLocations(gridContext, state); }, "VK_EXT_sample_locations"); + gridContext.sampleLocationSampleCounts |= VK_SAMPLE_COUNT_1_BIT; + expectFailure([&] { AgcDriver::Graphics::ValidateSampleLocations(gridContext, state); }, "2x2 sample location grid"); + gridContext.sampleLocationGridSampleCounts |= VK_SAMPLE_COUNT_1_BIT; + Require(AgcDriver::Graphics::ValidateSampleLocations(gridContext, state), "one-sample grid locations were refused on a device that supports them"); + for (std::uint32_t pixel = 0; pixel < 4; ++pixel) queue.context[0x2fe + pixel * 4u] = 0xcc44; + state = DecodeState(queue); + Require(state.samples == 1 && state.customSampleLocations && !state.sampleLocationGrid, "a uniform 2x pattern into a one-sample target did not use sample 0's location"); + auto stenciled = queue; + for (const auto [pixel, word] : {std::pair{0u, 0x0c04u}, std::pair{1u, 0x0c04u}, std::pair{2u, 0x040cu}, std::pair{3u, 0x040cu}}) stenciled.context[0x2fe + pixel * 4u] = word; + for (const auto [offset, word] : {std::pair{0x010u, 0x80000180u}, std::pair{0x011u, 0x20000181u}, std::pair{0x012u, 0x00001000u}, std::pair{0x013u, 0x00002000u}, std::pair{0x015u, 0x00002000u}, std::pair{0x007u, 0x003f003fu}, std::pair{0x000u, 0u}, std::pair{0x002u, 0u}, std::pair{0x00au, 0u}, std::pair{0x00bu, 0x3f800000u}, std::pair{0x10bu, 0u}, std::pair{0x10cu, 0x01ffff00u}, std::pair{0x10du, 0x01ffff00u}, std::pair{0x200u, 0x007007b3u}}) stenciled.context[offset] = word; + state = DecodeState(stenciled); + Require(state.samples == 1 && state.depth && state.depth->samples == 1 && state.stencilTest && state.sampleLocationGrid && state.colors.size() == 1 && state.colors[0].samples == 1, "2x checkerboard rasterization into one-sample stencil and color targets (PPSA27836) did not rasterize sample 0 alone"); + stenciled.context[0x31d] = 0x9000; + expectFailure([&] { DecodeState(stenciled); }, "a color target's sample count differs from the rasterizer's"); + stenciled.context[0x31d] = 0; + stenciled.context[0x1b3] |= ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PerspectiveCentroid); + stenciled.context[0x1b4] |= ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PerspectiveCentroid); + expectFailure([&] { DecodeState(stenciled); }, "the depth target's sample count differs from the rasterizer's"); + for (const auto input : {ShaderRecompiler::PixelInput::PerspectiveCentroid, ShaderRecompiler::PixelInput::PerspectiveSample, ShaderRecompiler::PixelInput::Ancillary, ShaderRecompiler::PixelInput::SampleCoverage}) { + auto sampled = queue; + sampled.context[0x1b3] |= ShaderRecompiler::PixelInputBit(input); + sampled.context[0x1b4] |= ShaderRecompiler::PixelInputBit(input); + expectFailure([&] { DecodeState(sampled); }, "sample count differs from the rasterizer's"); + } + auto singleScan = queue; + singleScan.context[0x292] = 2; + expectFailure([&] { DecodeState(singleScan); }, "without MSAA_ENABLE"); queue = multisampled(); queue.context[0x2f8] = 0; queue.context[0x292] = 2; @@ -2785,6 +2821,7 @@ void multisampleTests() { queue.context[0x31d] = 0x1000; expectFailure([&] { DecodeState(queue); }, "fewer fragments than samples"); queue.context[0x31d] = 0; + queue.context[0x293] = 0x10000u; expectFailure([&] { DecodeState(queue); }, "sample count differs"); queue = multisampled(); queue.context[0x292] = 2; diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 21512697ed..c5a9be50d8 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -114,7 +114,7 @@ - [Multisampled targets in CB passes](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a CB eliminate or DCC pass over a multisampled target throws, since its texels are on the host and only a CB resolve and an FMASK decompress (`CB_COLOR_CONTROL` mode 5) are modeled. The FMASK decompress only applies the pending CMASK fast clear to the host image; it requires a CMASK address, and the target's DCC and FMASK contents are not interpreted. DCC over a multisampled target is accepted only in that pass and throws everywhere else. A CB resolve into a fast-clear destination throws: the resolve writes the destination's texels but not its CMASK, which would still read as cleared. - [Multisampled target guest access](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) (libSceAgcDriver) - the memory a multisampled color target throws on (CPU read, DMA copy, texture or storage image read) uses fragment-aware 64 KiB render-target blocks. [AMD Addrlib's GFX10 thin-block rule](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/core/addrlib2.cpp#L1680-L1712) gives the block width and height from element bytes and fragment count; its [surface-size calculation](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/gfx10/gfx10addrlib.cpp#L4000-L4005) rounds both extents to those blocks and multiplies by element bytes and fragments. Thus a 1920x1080 4x RGBA8 surface occupies 0x1fe0000 bytes, leaving the next allocation at +0x2000000 outside the guard. The 2x and 8x block orientations differ from treating the fragments as a larger element. Console address-layout equivalence has not been measured. Other tile modes retain the conservative `max(CB bytes * samples, width * height * element bytes * samples)` bound, which may still reject a distinct allocation. Targets are kept per device for the device's lifetime, one per surface address, format, extent and sample count, and are not evicted. - [CB resolve sample count](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a draw with CB_COLOR_CONTROL in resolve mode is a CB resolve when either PA_SC_AA_CONFIG or CB_COLOR0_ATTRIB.NUM_SAMPLES is multisampled, and takes its sample count from CB0. Inferred, not from documentation: PPSA36082 resolves its 4x targets with no sample count in PA_SC_AA_CONFIG, and radeonsi sets PA_SC_AA_CONFIG to 0 for a multisampled framebuffer when rasterizer multisampling and line smoothing are off. -- [Custom MSAA sample locations](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) support a uniform 1x1 pattern for 2, 4 or 8 samples through VK_EXT_sample_locations. Patterns varying across the guest's 2x2 pixel grid use a 2x2 sample location grid, anchored at the framebuffer origin like the guest's while the window offset is zero, on devices whose `maxSampleLocationGridSize` for the sample count is a multiple of 2x2, and throw on others. The RTX 5070 reports 2x4 for 2 samples, 2x2 for 4 and 1x2 for 8; an AMD Radeon integrated GPU reports 2x2 for each. Devices without the required sample count, four subpixel bits or the full guest coordinate range refuse custom patterns. Depth images remain in GENERAL between draws, and render passes cannot continue across pattern changes. Guest offsets retain the existing decoder's signed four-bit, sixteenth-pixel interpretation; PS5 coverage and interpolation equivalence have not been measured on hardware. +- [Custom MSAA sample locations](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) support a uniform 1x1 pattern for 2, 4 or 8 samples through VK_EXT_sample_locations. Patterns varying across the guest's 2x2 pixel grid use a 2x2 sample location grid, anchored at the framebuffer origin like the guest's while the window offset is zero, on devices whose `maxSampleLocationGridSize` for the sample count is a multiple of 2x2, and throw on others. The RTX 5070 reports 2x4 for 2 samples, 2x2 for 4 and 1x2 for 8; an AMD Radeon integrated GPU reports 2x2 for each. Devices without the required sample count, four subpixel bits or the full guest coordinate range refuse custom patterns. A multisampled rasterizer drawing into one-sample color targets, and a one-sample depth target if any, with MSAA_ENABLE set, no alpha-to-coverage, no PS_ITER_SAMPLE and no sample, centroid, ancillary or coverage pixel inputs is drawn with one sample at sample 0's location in each pixel of the grid, as the targets keep only sample 0's coverage; one-sample depth images are created with `SAMPLE_LOCATIONS_COMPATIBLE_DEPTH` when the device supports it, as multisampled ones already were; this is how VK_AMD_mixed_attachment_samples defines the case, and it is not measured on PS5 hardware. Other mixed sample counts throw. Depth images remain in GENERAL between draws, and render passes cannot continue across pattern changes. Guest offsets retain the existing decoder's signed four-bit, sixteenth-pixel interpretation; PS5 coverage and interpolation equivalence have not been measured on hardware. - [DCC display buffers](../../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) are presented only when all keys of the console's DCC extent agree on uncompressed or a fast-clear code, padding keys included; other keys, and register-clear keys over a pending image, throw. - [Storage image write-back](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) stores the GPU results over a CPU-stamped range whose guest bytes equal the copy taken when the image's layers got their generation (images up to 1 MiB whose guest bytes were settled at that point, with no unit shadow, pending image results or recorded GPU store over them; other images keep the previous rule: the CPU's bytes win). A CPU store that rewrites the baseline value after the GPU changed it is not told apart from no store, and the GPU result wins there. The 1 MiB limit is a choice, not a console value. - [16-bit float display buffers](../../../core/libs/prx/libSceAgcDriver/Execution/src/DisplayBuffer.cpp) (libSceAgcDriver, libSceVideoOut) - pixel formats `0xC001000622000000` and `0xC001000600000000` are taken as R16G16B16A16 and B16G16R16A16 float, eight bytes per texel, as [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/59183dbfa81ed7efdff6069177fed50de0552725/src/graphics/host_gpu/renderer/image/imageInfo.h) maps them and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/VideoOut/VideoOutTypes.cs) names them (`Rgba16Float`, `Bgra16Float`); PPSA11386 registers 3840x2160 tiled buffers of the first. The tiled layout is assumed to be the 8-byte SW_64KB_R_X equation of `TextureSwizzleEquations.hpp` applied to full coordinates, as addrlib applies it, by analogy with the 32-bit scanout; `ColorTargetLayout` applies the equations to in-block coordinates, which for 8-byte texels differs in every odd 64-row block row (address bit 11 takes y bit 6) and for 16-byte texels also differs, so a float display buffer written through `WriteColorTarget` and presented from guest memory would show those rows misplaced. The values are assumed to be linear light and are encoded with the sRGB curve for the 8-bit output. Bits 32-39 of these formats are `0x06`, as in the 10-bit formats SharpProspero lists without sRGB transfer, while its sRGB formats have `0x00`, its 10-bit PQ formats `0x04` (with `0x07` in bits 40-47) and its float BT.2100 PQ formats `0x07`: they show only that these formats are not the sRGB ones. NaN and negative values give 0, values of 1 and above give 255, alpha is linear. Neither the layout nor the transfer is verified against console or title frames, and HDR output is not modelled. DCC metadata on a float buffer throws. From b766ce4fb5725c8cabd8c7fc5a43d4aa5d8cdca0 Mon Sep 17 00:00:00 2001 From: simonbronk <275178677+simonbronk@users.noreply.github.com> Date: Thu, 15 Oct 2026 16:03:17 +0200 Subject: [PATCH 0450/1000] feat(agc): keep sample, sample-index and per-sample shading on the sample-zero path A multisampled rasterizer drawing into one-sample targets keeps PERSP_SAMPLE, LINEAR_SAMPLE and ANCILLARY inputs, and with PS_ITER_SAMPLE also centroid inputs, since only sample 0's coverage reaches the targets. Coverage inputs, and the sample index with POS_X_FLOAT or POS_Y_FLOAT, still throw. The input rule for the non-iterated case follows fdfawef324's proposal on #5907 (PPSA07411). Co-Authored-By: Claude Opus 5.5 --- .../libSceAgcDriver/Graphics/src/State.cpp | 10 ++++- .../prx/libSceAgcDriver/tests/Graphics.cpp | 37 ++++++++++++++++--- docs/dev/debt/libSceAgcDriver.md | 2 +- 3 files changed, 40 insertions(+), 9 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index 5de09c73b5..ec7a9c798b 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -415,7 +415,8 @@ std::string singleSampleMsaaRejection(const QueueState& queue) { } constexpr std::uint32_t CoveredCentroidIsCenter = 1u << 29u; -constexpr std::uint32_t SampleDependentInputs = ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PerspectiveSample) | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PerspectiveCentroid) | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::LinearSample) | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::LinearCentroid) | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::Ancillary) | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::SampleCoverage); +constexpr std::uint32_t SampleZeroRefusedInputs = ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PerspectiveCentroid) | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::LinearCentroid) | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::SampleCoverage); +constexpr std::uint32_t PixelCenterPositionInputs = ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionX) | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionY); constexpr std::uint32_t CentroidInputs = ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PerspectiveCentroid) | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::LinearCentroid); bool coveredCentroidSupported(const QueueState& queue) { @@ -491,13 +492,18 @@ void decodeSampleLocations(const Registers& cx, State& state) { } bool rasterizesSampleZeroOnly(const Registers& cx, const State& state) { - return state.samples > 1 && !state.alphaToCoverage && !state.perSampleShading && (read(cx, 0x292) & 1u) != 0 && (read(cx, 0x1b3) & read(cx, 0x1b4) & SampleDependentInputs) == 0; + if (state.samples <= 1 || state.alphaToCoverage || (read(cx, 0x292) & 1u) == 0) return false; + const auto inputs = read(cx, 0x1b3) & read(cx, 0x1b4); + const auto refused = state.perSampleShading ? ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::SampleCoverage) : SampleZeroRefusedInputs; + const bool sampleIndexMovesPosition = (inputs & ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::Ancillary)) != 0 && (inputs & PixelCenterPositionInputs) != 0; + return (inputs & refused) == 0 && !sampleIndexMovesPosition; } void rasterizeSampleZero(State& state) { std::array sampleZero{}; for (std::uint32_t pixel = 0; pixel < 4; ++pixel) sampleZero[pixel * 8u] = state.sampleLocations[pixel * 8u]; state.samples = 1; + state.perSampleShading = false; state.sampleLocations = sampleZero; state.customSampleLocations = std::any_of(sampleZero.begin(), sampleZero.end(), [](std::uint8_t bits) { return bits != 0; }); state.sampleLocationGrid = sampleZero[8] != sampleZero[0] || sampleZero[16] != sampleZero[0] || sampleZero[24] != sampleZero[0]; diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 22326d1c25..a5bc1ec085 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -2672,12 +2672,37 @@ void multisampleTests() { stenciled.context[0x1b3] |= ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PerspectiveCentroid); stenciled.context[0x1b4] |= ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PerspectiveCentroid); expectFailure([&] { DecodeState(stenciled); }, "the depth target's sample count differs from the rasterizer's"); - for (const auto input : {ShaderRecompiler::PixelInput::PerspectiveCentroid, ShaderRecompiler::PixelInput::PerspectiveSample, ShaderRecompiler::PixelInput::Ancillary, ShaderRecompiler::PixelInput::SampleCoverage}) { - auto sampled = queue; - sampled.context[0x1b3] |= ShaderRecompiler::PixelInputBit(input); - sampled.context[0x1b4] |= ShaderRecompiler::PixelInputBit(input); - expectFailure([&] { DecodeState(sampled); }, "sample count differs from the rasterizer's"); + const auto withInputs = [](AgcDriver::QueueState base, std::initializer_list inputs) { + for (const auto input : inputs) { + base.context[0x1b3] |= ShaderRecompiler::PixelInputBit(input); + base.context[0x1b4] |= ShaderRecompiler::PixelInputBit(input); + } + return base; + }; + for (const auto input : {ShaderRecompiler::PixelInput::PerspectiveCentroid, ShaderRecompiler::PixelInput::LinearCentroid, ShaderRecompiler::PixelInput::SampleCoverage}) expectFailure([&] { DecodeState(withInputs(queue, {input})); }, "sample count differs from the rasterizer's"); + for (const auto input : {ShaderRecompiler::PixelInput::PerspectiveSample, ShaderRecompiler::PixelInput::LinearSample, ShaderRecompiler::PixelInput::Ancillary}) { + state = DecodeState(withInputs(queue, {input})); + Require(state.samples == 1 && !state.perSampleShading, "a sample or sample-index input into a one-sample target did not rasterize sample 0 alone"); } + for (const auto position : {ShaderRecompiler::PixelInput::PositionX, ShaderRecompiler::PixelInput::PositionY}) expectFailure([&] { DecodeState(withInputs(queue, {ShaderRecompiler::PixelInput::Ancillary, position})); }, "sample count differs from the rasterizer's"); + auto iterated = queue; + iterated.context[0x293] = 0x06030000u; + state = DecodeState(iterated); + Require(state.samples == 1 && !state.perSampleShading, "PS_ITER_SAMPLE into a one-sample target (PPSA27836) did not rasterize sample 0 alone"); + state = DecodeState(withInputs(iterated, {ShaderRecompiler::PixelInput::PerspectiveSample, ShaderRecompiler::PixelInput::PositionX, ShaderRecompiler::PixelInput::PositionY})); + Require(state.samples == 1 && !state.perSampleShading, "PS_ITER_SAMPLE with sample and position inputs (PPSA27836) did not rasterize sample 0 alone"); + state = DecodeState(withInputs(iterated, {ShaderRecompiler::PixelInput::PerspectiveCentroid, ShaderRecompiler::PixelInput::LinearCentroid})); + Require(state.samples == 1, "PS_ITER_SAMPLE with centroid inputs did not rasterize sample 0 alone"); + expectFailure([&] { DecodeState(withInputs(iterated, {ShaderRecompiler::PixelInput::SampleCoverage})); }, "sample count differs from the rasterizer's"); + expectFailure([&] { DecodeState(withInputs(iterated, {ShaderRecompiler::PixelInput::Ancillary, ShaderRecompiler::PixelInput::PositionX})); }, "sample count differs from the rasterizer's"); + auto iteratedStencil = withInputs(stenciled, {}); + iteratedStencil.context[0x1b3] &= ~ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PerspectiveCentroid); + iteratedStencil.context[0x1b4] &= ~ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PerspectiveCentroid); + state = DecodeState(withInputs(iteratedStencil, {ShaderRecompiler::PixelInput::PerspectiveSample, ShaderRecompiler::PixelInput::Ancillary})); + Require(state.samples == 1 && state.depth && state.depth->samples == 1, "sample and sample-index inputs into a one-sample depth target (PPSA07411) did not rasterize sample 0 alone"); + iteratedStencil.context[0x293] = 0x06030000u; + state = DecodeState(iteratedStencil); + Require(state.samples == 1 && state.depth && state.depth->samples == 1 && !state.perSampleShading, "PS_ITER_SAMPLE into one-sample depth and color targets did not rasterize sample 0 alone"); auto singleScan = queue; singleScan.context[0x292] = 2; expectFailure([&] { DecodeState(singleScan); }, "without MSAA_ENABLE"); @@ -2822,7 +2847,7 @@ void multisampleTests() { expectFailure([&] { DecodeState(queue); }, "fewer fragments than samples"); queue.context[0x31d] = 0; queue.context[0x293] = 0x10000u; - expectFailure([&] { DecodeState(queue); }, "sample count differs"); + Require(DecodeState(queue).samples == 1 && !DecodeState(queue).perSampleShading, "PS_ITER_SAMPLE into a one-sample target did not rasterize sample 0 alone"); queue = multisampled(); queue.context[0x292] = 2; state = DecodeState(queue); diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index c5a9be50d8..41500142e1 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -114,7 +114,7 @@ - [Multisampled targets in CB passes](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a CB eliminate or DCC pass over a multisampled target throws, since its texels are on the host and only a CB resolve and an FMASK decompress (`CB_COLOR_CONTROL` mode 5) are modeled. The FMASK decompress only applies the pending CMASK fast clear to the host image; it requires a CMASK address, and the target's DCC and FMASK contents are not interpreted. DCC over a multisampled target is accepted only in that pass and throws everywhere else. A CB resolve into a fast-clear destination throws: the resolve writes the destination's texels but not its CMASK, which would still read as cleared. - [Multisampled target guest access](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) (libSceAgcDriver) - the memory a multisampled color target throws on (CPU read, DMA copy, texture or storage image read) uses fragment-aware 64 KiB render-target blocks. [AMD Addrlib's GFX10 thin-block rule](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/core/addrlib2.cpp#L1680-L1712) gives the block width and height from element bytes and fragment count; its [surface-size calculation](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/gfx10/gfx10addrlib.cpp#L4000-L4005) rounds both extents to those blocks and multiplies by element bytes and fragments. Thus a 1920x1080 4x RGBA8 surface occupies 0x1fe0000 bytes, leaving the next allocation at +0x2000000 outside the guard. The 2x and 8x block orientations differ from treating the fragments as a larger element. Console address-layout equivalence has not been measured. Other tile modes retain the conservative `max(CB bytes * samples, width * height * element bytes * samples)` bound, which may still reject a distinct allocation. Targets are kept per device for the device's lifetime, one per surface address, format, extent and sample count, and are not evicted. - [CB resolve sample count](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a draw with CB_COLOR_CONTROL in resolve mode is a CB resolve when either PA_SC_AA_CONFIG or CB_COLOR0_ATTRIB.NUM_SAMPLES is multisampled, and takes its sample count from CB0. Inferred, not from documentation: PPSA36082 resolves its 4x targets with no sample count in PA_SC_AA_CONFIG, and radeonsi sets PA_SC_AA_CONFIG to 0 for a multisampled framebuffer when rasterizer multisampling and line smoothing are off. -- [Custom MSAA sample locations](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) support a uniform 1x1 pattern for 2, 4 or 8 samples through VK_EXT_sample_locations. Patterns varying across the guest's 2x2 pixel grid use a 2x2 sample location grid, anchored at the framebuffer origin like the guest's while the window offset is zero, on devices whose `maxSampleLocationGridSize` for the sample count is a multiple of 2x2, and throw on others. The RTX 5070 reports 2x4 for 2 samples, 2x2 for 4 and 1x2 for 8; an AMD Radeon integrated GPU reports 2x2 for each. Devices without the required sample count, four subpixel bits or the full guest coordinate range refuse custom patterns. A multisampled rasterizer drawing into one-sample color targets, and a one-sample depth target if any, with MSAA_ENABLE set, no alpha-to-coverage, no PS_ITER_SAMPLE and no sample, centroid, ancillary or coverage pixel inputs is drawn with one sample at sample 0's location in each pixel of the grid, as the targets keep only sample 0's coverage; one-sample depth images are created with `SAMPLE_LOCATIONS_COMPATIBLE_DEPTH` when the device supports it, as multisampled ones already were; this is how VK_AMD_mixed_attachment_samples defines the case, and it is not measured on PS5 hardware. Other mixed sample counts throw. Depth images remain in GENERAL between draws, and render passes cannot continue across pattern changes. Guest offsets retain the existing decoder's signed four-bit, sixteenth-pixel interpretation; PS5 coverage and interpolation equivalence have not been measured on hardware. +- [Custom MSAA sample locations](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) support a uniform 1x1 pattern for 2, 4 or 8 samples through VK_EXT_sample_locations. Patterns varying across the guest's 2x2 pixel grid use a 2x2 sample location grid, anchored at the framebuffer origin like the guest's while the window offset is zero, on devices whose `maxSampleLocationGridSize` for the sample count is a multiple of 2x2, and throw on others. The RTX 5070 reports 2x4 for 2 samples, 2x2 for 4 and 1x2 for 8; an AMD Radeon integrated GPU reports 2x2 for each. Devices without the required sample count, four subpixel bits or the full guest coordinate range refuse custom patterns. A multisampled rasterizer drawing into one-sample color targets, and a one-sample depth target if any, with MSAA_ENABLE set, no alpha-to-coverage and no coverage pixel input is drawn with one sample at sample 0's location in each pixel of the grid, as the targets keep only sample 0's coverage. Without PS_ITER_SAMPLE such a draw also refuses centroid inputs, and keeps PERSP_SAMPLE, LINEAR_SAMPLE and ANCILLARY: a pixel shaded once is taken to get sample 0's barycentrics and sample index 0. With PS_ITER_SAMPLE only sample 0's invocation reaches the targets, so its sample and centroid barycentrics are taken to be sample 0's and the draw is shaded once. Both are inferred, not measured. ANCILLARY with POS_X_FLOAT or POS_Y_FLOAT throws, since reading the sample index makes the host shade per sample, which moves FragCoord from the pixel center to sample 0's location. By the same Vulkan rule, PERSP_CENTER and LINEAR_CENTER inputs on this path are interpolated at sample 0's location on the host, where the console uses the pixel center; one-sample depth images are created with `SAMPLE_LOCATIONS_COMPATIBLE_DEPTH` when the device supports it, as multisampled ones already were; this is how VK_AMD_mixed_attachment_samples defines the case, and it is not measured on PS5 hardware. Other mixed sample counts throw. Depth images remain in GENERAL between draws, and render passes cannot continue across pattern changes. Guest offsets retain the existing decoder's signed four-bit, sixteenth-pixel interpretation; PS5 coverage and interpolation equivalence have not been measured on hardware. - [DCC display buffers](../../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) are presented only when all keys of the console's DCC extent agree on uncompressed or a fast-clear code, padding keys included; other keys, and register-clear keys over a pending image, throw. - [Storage image write-back](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) stores the GPU results over a CPU-stamped range whose guest bytes equal the copy taken when the image's layers got their generation (images up to 1 MiB whose guest bytes were settled at that point, with no unit shadow, pending image results or recorded GPU store over them; other images keep the previous rule: the CPU's bytes win). A CPU store that rewrites the baseline value after the GPU changed it is not told apart from no store, and the GPU result wins there. The 1 MiB limit is a choice, not a console value. - [16-bit float display buffers](../../../core/libs/prx/libSceAgcDriver/Execution/src/DisplayBuffer.cpp) (libSceAgcDriver, libSceVideoOut) - pixel formats `0xC001000622000000` and `0xC001000600000000` are taken as R16G16B16A16 and B16G16R16A16 float, eight bytes per texel, as [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/59183dbfa81ed7efdff6069177fed50de0552725/src/graphics/host_gpu/renderer/image/imageInfo.h) maps them and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/VideoOut/VideoOutTypes.cs) names them (`Rgba16Float`, `Bgra16Float`); PPSA11386 registers 3840x2160 tiled buffers of the first. The tiled layout is assumed to be the 8-byte SW_64KB_R_X equation of `TextureSwizzleEquations.hpp` applied to full coordinates, as addrlib applies it, by analogy with the 32-bit scanout; `ColorTargetLayout` applies the equations to in-block coordinates, which for 8-byte texels differs in every odd 64-row block row (address bit 11 takes y bit 6) and for 16-byte texels also differs, so a float display buffer written through `WriteColorTarget` and presented from guest memory would show those rows misplaced. The values are assumed to be linear light and are encoded with the sRGB curve for the 8-bit output. Bits 32-39 of these formats are `0x06`, as in the 10-bit formats SharpProspero lists without sRGB transfer, while its sRGB formats have `0x00`, its 10-bit PQ formats `0x04` (with `0x07` in bits 40-47) and its float BT.2100 PQ formats `0x07`: they show only that these formats are not the sRGB ones. NaN and negative values give 0, values of 1 and above give 255, alpha is linear. Neither the layout nor the transfer is verified against console or title frames, and HDR output is not modelled. DCC metadata on a float buffer throws. From 2c1b825ae5b09b69b317b8e8b1716b40ad63c692 Mon Sep 17 00:00:00 2001 From: simonbronk <275178677+simonbronk@users.noreply.github.com> Date: Fri, 16 Oct 2026 09:00:29 +0200 Subject: [PATCH 0451/1000] test(agc): expect the color sample-count refusal for a one-sample target under a 2x scan without MSAA_ENABLE #5882 lets a multisampled scan without MSAA_ENABLE through the earlier check, so the draw is now refused where its one-sample color target meets the rasterizer's sample count. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAgcDriver/tests/Graphics.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index a5bc1ec085..a448c31201 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -2705,7 +2705,7 @@ void multisampleTests() { Require(state.samples == 1 && state.depth && state.depth->samples == 1 && !state.perSampleShading, "PS_ITER_SAMPLE into one-sample depth and color targets did not rasterize sample 0 alone"); auto singleScan = queue; singleScan.context[0x292] = 2; - expectFailure([&] { DecodeState(singleScan); }, "without MSAA_ENABLE"); + expectFailure([&] { DecodeState(singleScan); }, "a color target's sample count differs from the rasterizer's"); queue = multisampled(); queue.context[0x2f8] = 0; queue.context[0x292] = 2; From b15060bd9dd5539c4d1ffb9a546c9f50ac2243db Mon Sep 17 00:00:00 2001 From: Romans Pokrovskis Date: Thu, 15 Oct 2026 15:52:36 +0300 Subject: [PATCH 0452/1000] test(agc): wait for the second-store image itself to be pending --- .../libSceAgcDriver/tests/execution/StoreOverUntracked.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/StoreOverUntracked.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/StoreOverUntracked.cpp index 63fbbb4539..a729c509aa 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/StoreOverUntracked.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/StoreOverUntracked.cpp @@ -270,8 +270,8 @@ Outcome RunSecondStore(const char* name) { const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(10); while (true) { hold.lock(); - const std::array, 1> range{{{address, address + SurfaceBytes}}}; - if (StorageTexture::FindLive(address, SurfaceBytes) != nullptr && StorageTexture::AnyPendingOverlaps(range)) return true; + const auto image = StorageTexture::FindLive(address, SurfaceBytes); + if (image != nullptr && StorageTexture::FindPending(address, SurfaceBytes) == image) return true; hold.unlock(); if (Holds(surface.Texels(), result)) return false; Require(std::chrono::steady_clock::now() < deadline, "store over untracked: the result neither became pending nor reached guest memory"); From bf861be946edd839a3ccbe1c6d0d42262ac8f57f Mon Sep 17 00:00:00 2001 From: Ondra97 <47064087+Ondra97@users.noreply.github.com> Date: Fri, 16 Oct 2026 10:50:01 +0200 Subject: [PATCH 0453/1000] test(agc): wait for pending result on the tested surface --- .../prx/libSceAgcDriver/tests/execution/StoreOverUntracked.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/StoreOverUntracked.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/StoreOverUntracked.cpp index 63fbbb4539..d98349ac9e 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/StoreOverUntracked.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/StoreOverUntracked.cpp @@ -220,7 +220,8 @@ bool HoldPending(THold& hold, const Surface& surface, std::uint32_t result) { const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(10); while (true) { hold.lock(); - if (StorageTexture::AnyPending()) return true; + const auto live = StorageTexture::FindLive(surface.Address(), SurfaceBytes); + if (live != nullptr && StorageTexture::FindPending(surface.Address(), SurfaceBytes) == live) return true; hold.unlock(); if (Holds(surface.Texels(), result)) return false; Require(std::chrono::steady_clock::now() < deadline, "store over untracked: the result neither became pending nor reached guest memory"); From e54ea3ea298f988e9708a9388d1cacec317b23ce Mon Sep 17 00:00:00 2001 From: simonbronk Date: Fri, 16 Oct 2026 10:51:42 +0200 Subject: [PATCH 0454/1000] fix(stubgen): reject read-only outputs before replacement --- tools/stubgen.py | 5 +++++ tools/tests/test_stubgen.py | 25 +++++++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/tools/stubgen.py b/tools/stubgen.py index 2d451278cc..ae9f68955b 100644 --- a/tools/stubgen.py +++ b/tools/stubgen.py @@ -2,6 +2,7 @@ import argparse import os import re +import stat import sys import tempfile from pathlib import Path @@ -194,6 +195,10 @@ def write_outputs(outputs): temporary.chmod(path.stat().st_mode & 0o7777) else: temporary.chmod(0o666 & ~umask) + if os.name == "nt": + for _, path in staged: + if path.exists() and path.stat().st_file_attributes & stat.FILE_ATTRIBUTE_READONLY: + raise PermissionError(f"Output is read-only: {path}") for temporary, path in staged: temporary.replace(path) finally: diff --git a/tools/tests/test_stubgen.py b/tools/tests/test_stubgen.py index 3a8e9a8be8..bc4bf37488 100644 --- a/tools/tests/test_stubgen.py +++ b/tools/tests/test_stubgen.py @@ -41,6 +41,31 @@ class StubgenTests(unittest.TestCase): class StubGeneratorTests(unittest.TestCase): + @unittest.skipUnless(os.name == "nt", "Windows denies replacing read-only output files") + def test_readonly_second_output_preserves_both_outputs(self): + import subprocess + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + nids = root / "nids.txt" + code = root / "Export.cpp" + debt = root / "debt.md" + nids.write_text("AC1FtjqMCL0\n", encoding="utf-8") + code.write_bytes(b"original code\n") + debt.write_bytes(b"original debt\n") + debt.chmod(stat.S_IREAD) + try: + result = subprocess.run( + [sys.executable, str(Path(stubgen.__file__)), "--module", "libSceTest", + "--nids", str(nids), "--out", str(code), "--td-out", str(debt)], + capture_output=True, text=True) + self.assertNotEqual(result.returncode, 0) + self.assertNotIn("Traceback", result.stderr) + self.assertEqual(code.read_bytes(), b"original code\n") + self.assertEqual(debt.read_bytes(), b"original debt\n") + self.assertEqual(sorted(path.name for path in root.iterdir()), ["Export.cpp", "debt.md", "nids.txt"]) + finally: + debt.chmod(stat.S_IREAD | stat.S_IWRITE) + def test_default_stdout_contains_only_cpp(self): with tempfile.TemporaryDirectory() as directory: nids = Path(directory) / "nids.txt" From dc2f8fb4610edeeddc49b2049d0e6e07d99326cc Mon Sep 17 00:00:00 2001 From: Neithennn <151066966+Neithennn@users.noreply.github.com> Date: Fri, 16 Oct 2026 10:53:24 +0200 Subject: [PATCH 0455/1000] test(agc): wait for a result pending over the neighbour copy surface The NeighbourCopied wait accepted any pending result once the surface image was live, so a pending result of the source surface could end it. It now waits for a pending result overlapping the surface's range. Co-Authored-By: Claude Opus 5.5 --- .../prx/libSceAgcDriver/tests/execution/StoreOverUntracked.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/StoreOverUntracked.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/StoreOverUntracked.cpp index a4ebf5d925..2c266c814c 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/StoreOverUntracked.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/StoreOverUntracked.cpp @@ -355,10 +355,11 @@ Outcome Run(const Case& test) { std::fill(surface.Texels(), surface.Texels() + SurfaceBytes / 4u, Untouched); Store(surface.Texels()); // Pending results are the surface image's alone once it exists. + const std::array, 1> range{{{address, address + SurfaceBytes}}}; const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(10); while (true) { hold.lock(); - if (StorageTexture::FindLive(address, SurfaceBytes) != nullptr && StorageTexture::AnyPending()) break; + if (StorageTexture::FindLive(address, SurfaceBytes) != nullptr && StorageTexture::AnyPendingOverlaps(range)) break; hold.unlock(); if (Holds(surface.Texels(), Stored)) return Outcome::Missed; Require(std::chrono::steady_clock::now() < deadline, "store over untracked: the surface's result neither became pending nor reached guest memory"); From 13b171cab5cb858b48efc09e6d3fc633777b8729 Mon Sep 17 00:00:00 2001 From: fdfawef324 <287205748+fdfawef324@users.noreply.github.com> Date: Mon, 12 Oct 2026 15:07:59 -0400 Subject: [PATCH 0456/1000] feat(agc): keep the last submissions per queue and dump them on a stall or crash A GPU hang or a crash in a title leaves no record of what the queues were last given: APS5_TRACE_GPU prints every submission (too much to leave on), and the WAIT_REG_MEM timeout message shows only the packet before the wait on the waiting queue. Each queue now keeps a ring of its last 16 submissions (APS5_PM4_RING=, 0 to 4096, 0 disables): the TraceMs time in tenths of a millisecond, serial, size, guest address and a copy of the first 512 dwords after indirect buffers are inlined, recorded under the driver lock next to the serial, REWIND tails included. The ring of a queue and the failure report registration are set up at the top of Submit, before any output is reserved, so recording under the lock does not allocate. Disabled, Submit only tests a static flag; enabled, it costs one bounded copy per submission. DumpSubmissionRings decodes the kept dwords with Pm4::PacketWords and the new Pm4::KnownName, which returns the decoder's names as views of static strings (empty for an unknown opcode, printed as UNKNOWN_0x%02x); Pm4::Name now builds its std::string from it. Every line is formatted with integer and string conversions only into a 512-byte stack buffer and written with one WriteFile / write(2) on the stderr handle, so the dump does not allocate from the C runtime heap, lines from other threads do not interleave and the CRT stream lock is not taken (the rule of #3871). The header gives the number of queues and the ring size, and each queue line the submissions recorded and kept. libc gets a small registry of failure reports (LibcRegisterFailureReport_nid_no_patch, Unregister, Run); Run runs the registered reports the first time it is called and returns at once after that. The Windows crash report runs it after the thread dump of an unhandled exception, after SIGABRT (which also covers the APS5_HANG_DUMP_SECS timed dump, since it aborts) and after an invalid CRT parameter, so an abort after a reported exception does not print the rings twice. The driver registers its dump at the first submission and unregisters at static destruction; the dump there waits a bounded time for each ring lock. What the crash-path dump cannot do (a ring held by the crashed thread, rings corrupted by the crash, Linux) is recorded in technical debt. APS5_STALL_WATCHDOG_SECS= (0 to 86400, 0 disables) makes a waiter that has not progressed for n seconds dump the rings once per wait: a WAIT_REG_MEM (progress as for APS5_GPU_WAIT_TIMEOUT_MS: another packet runs or completes), a REWIND waiting for the title, and a compute submission held behind queue 0. The last one prints under the driver lock, so submissions and the other queues wait for it (USAGE.md says so). Both variables are read at the top of Submit, so an invalid value of either throws at the first submission and never later inside a GPU worker. The order wait behind queue 0 releases its order hold on scope exit, so a throwing dump cannot leave it counted. No packet semantics change; packet sizes and names are those of the existing decoder (PM4 type-3 header layout as in Mesa src/amd/common/sid.h). The queue bound of Submit and of the rings is one constant, SubmissionQueueEnd. Tests: agc_driver_pm4_tests gets ten modes, each its own ctest: agc_driver_pm4_ring (APS5_PM4_RING=4, six submissions on queue 0 and two on 0x20: the dump holds #3..#6 and #1..#2 in order, nothing older), agc_driver_pm4_ring_empty (one line), _ring_disabled (one line), _ring_invalid and agc_driver_pm4_stall_watchdog_invalid (APS5_STALL_WATCHDOG_SECS=1x: the submission throws and is not executed), _ring_report (LibcRunFailureReports_nid_no_patch dumps the rings), _ring_report_heap (Windows: with ucrtbase.dll's HeapAlloc and HeapReAlloc imports counting calls from the reporting thread, the failure report of a submission holding INDEX_BUFFER_SIZE names it and allocates nothing, while Pm4::Name of the same header is seen allocating), and with APS5_STALL_WATCHDOG_SECS=1 a WAIT_REG_MEM (agc_driver_pm4_stall_watchdog), a REWIND (_stall_watchdog_rewind) and a compute submission held behind a queue 0 rendering wait (_stall_watchdog_order), each held for about 2.5 s, dump once and complete when released. The PM4 catalog test checks KnownName against every opcode and custom name. windows_backtrace_failure_report checks that abort() runs the registered reports once and not an unregistered one; windows_backtrace_failure_report_once that abort() does not run them again after an earlier run. With the dump decoding through Pm4::Name, agc_driver_pm4_ring_report_heap fails with "the failure report allocated from the C runtime heap"; with Run resetting its flag, windows_backtrace_failure_report_once prints the report twice. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 14 + .../include/Driver/Queues/SubmissionRing.hpp | 40 ++ .../libSceAgcDriver/Execution/include/Pm4.hpp | 1 + .../src/Driver/Queues/QueueWorker.cpp | 17 +- .../src/Driver/Queues/Submission.cpp | 11 +- .../src/Driver/Queues/SubmissionRing.cpp | 281 ++++++++++++ .../src/Driver/Synchronization/WaitMemory.cpp | 4 + .../prx/libSceAgcDriver/Execution/src/Pm4.cpp | 11 +- core/libs/prx/libSceAgcDriver/tests/Pm4.cpp | 408 +++++++++++++++++- core/libs/prx/libc/CMakeLists.txt | 11 + core/libs/prx/libc/include/FailureReport.hpp | 8 + core/libs/prx/libc/src/FailureReport.cpp | 39 ++ .../src/specifics/windows/CrashReport.cpp | 4 + core/libs/tests/WindowsBacktraceReport.cpp | 26 +- docs/dev/debt/libSceAgcDriver.md | 1 + docs/user/USAGE.md | 6 + 16 files changed, 871 insertions(+), 11 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/Execution/include/Driver/Queues/SubmissionRing.hpp create mode 100644 core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/SubmissionRing.cpp create mode 100644 core/libs/prx/libc/include/FailureReport.hpp create mode 100644 core/libs/prx/libc/src/FailureReport.cpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 251d38d6a1..c090ed8237 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -173,6 +173,7 @@ set(agcDriverSources ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/Driver/Presentation.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/Driver/Queues/QueueWorker.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/Driver/Queues/Submission.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/Driver/Queues/SubmissionRing.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/Driver/Queues/WorkerAffinity.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/Driver/Shaders/ShaderDiagnostics.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/Driver/Shaders/ShaderRegistry.cpp @@ -1668,6 +1669,19 @@ if(BUILD_TESTING) endif() add_test(NAME agc_driver_pm4_failure COMMAND agc_driver_pm4_tests failure) set_tests_properties(agc_driver_pm4_failure PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 TIMEOUT 30) + foreach(pm4RingCase IN ITEMS "ring|APS5_PM4_RING=4" "ring-empty|APS5_PM4_RING=16" "ring-disabled|APS5_PM4_RING=0" "ring-invalid|APS5_PM4_RING=4x" "ring-report|APS5_PM4_RING=2" "ring-report-heap|APS5_PM4_RING=2" "stall-watchdog|APS5_STALL_WATCHDOG_SECS=1" + "stall-watchdog-invalid|APS5_STALL_WATCHDOG_SECS=1x" "stall-watchdog-rewind|APS5_STALL_WATCHDOG_SECS=1" "stall-watchdog-order|APS5_STALL_WATCHDOG_SECS=1") + string(REPLACE "|" ";" pm4RingCase "${pm4RingCase}") + list(GET pm4RingCase 0 pm4RingMode) + list(GET pm4RingCase 1 pm4RingEnvironment) + string(REPLACE "-" "_" pm4RingTest "agc_driver_pm4_${pm4RingMode}") + add_test(NAME ${pm4RingTest} COMMAND agc_driver_pm4_tests ${pm4RingMode}) + set_tests_properties(${pm4RingTest} PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;${pm4RingEnvironment}" SKIP_RETURN_CODE 77 TIMEOUT 30) + if(WIN32) + set_tests_properties(${pm4RingTest} PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + endforeach() add_test(NAME agc_descriptor_pool_reset_failure COMMAND agc_driver_graphics_tests --descriptor-pool-reset-failure) add_test(NAME agc_driver_address_space_teardown COMMAND agc_driver_graphics_tests --address-space-teardown) set_tests_properties(agc_driver_address_space_teardown PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Queues/SubmissionRing.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Queues/SubmissionRing.hpp new file mode 100644 index 0000000000..a29def6cff --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Queues/SubmissionRing.hpp @@ -0,0 +1,40 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_DRIVER_QUEUES_SUBMISSIONRING_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_DRIVER_QUEUES_SUBMISSIONRING_HPP + +#include +#include +#include +#include +#include + +namespace AgcDriver::DriverDetail { + +inline constexpr std::uint32_t SubmissionQueueEnd = 0x58; + +std::size_t SubmissionRingCapacity(); + +std::chrono::seconds StallWatchdogPeriod(); + +bool ReadSubmissionRingSettings(); + +void PrepareSubmissionRing(std::uint32_t queue); + +void RecordSubmission(std::uint32_t queue, std::uint64_t serial, std::span commands, const void* guest, bool rewindTail); + +void DumpSubmissionRings(std::string_view reason); + +class StallWatch { +public: + explicit StallWatch(std::uint32_t queue) : queue(queue) {} + void Progressed() { since = {}; } + void Check(const char* waiter, std::uint64_t value); + +private: + std::uint32_t queue; + std::chrono::steady_clock::time_point since{}; + bool dumped = false; +}; + +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Pm4.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Pm4.hpp index 58fde0469b..f902632e1a 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Pm4.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Pm4.hpp @@ -78,6 +78,7 @@ std::uint32_t ReadDrawCount(const DrawParameters::IndirectDraw& indirect); inline bool IndirectDrawOpcode(std::uint32_t opcode) { return opcode == 0x24 || opcode == 0x25 || opcode == 0x2c || opcode == 0x38; } inline bool DrawOpcode(std::uint32_t opcode) { return opcode == 0x27 || opcode == 0x2d || opcode == 0x35 || IndirectDrawOpcode(opcode); } +std::string_view KnownName(std::uint32_t header); std::string Name(std::uint32_t header); // A PM4 type-2 packet is a one-dword filler (command-buffer padding); type 3 and type 0 carry a // dword count in bits 29:16. Type 1 is undefined. diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/QueueWorker.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/QueueWorker.cpp index e914fdd8bb..bbdc2b32d0 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/QueueWorker.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/QueueWorker.cpp @@ -1,4 +1,5 @@ #include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Queues/SubmissionRing.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Synchronization/DeferredLabels.hpp" @@ -233,10 +234,18 @@ void Driver::run(std::uint32_t id) noexcept { if (APS5_ENABLE_TIMING_LOG) submission.dequeuedAt = std::chrono::steady_clock::now(); if (id == 0) queue0Executing = submission.suspend ? 0 : submission.received; if (submission.waitFree) { - orderHolders.fetch_add(1, std::memory_order_acq_rel); - const auto released = [&] { return failure || stopping || orderReleased(id, submission.received); }; - while (!released()) poll(released); - orderHolders.fetch_sub(1, std::memory_order_acq_rel); + { + struct OrderHold { + std::atomic& holders; + explicit OrderHold(std::atomic& holders) : holders(holders) { holders.fetch_add(1, std::memory_order_acq_rel); } + ~OrderHold() { holders.fetch_sub(1, std::memory_order_acq_rel); } + OrderHold(const OrderHold&) = delete; + OrderHold& operator=(const OrderHold&) = delete; + } hold{orderHolders}; + const auto released = [&] { return failure || stopping || orderReleased(id, submission.received); }; + StallWatch stall(id); + while (!released() && !poll(released)) stall.Check("order behind queue 0 for event", submission.received); + } rethrowFailure(); } if (APS5_ENABLE_TIMING_LOG) submission.orderedAt = std::chrono::steady_clock::now(); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp index b7cfef3d62..b1572bfa26 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp @@ -1,6 +1,7 @@ #include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Queues/Submission.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Queues/SubmissionRing.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Synchronization/SynchronizationStatistics.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" @@ -259,6 +260,8 @@ void Driver::reserveOutputs(Submission& submission) { void Driver::executeRewindTail(const Submission& stalled) { PerformanceTimer timing("Driver.Rewind"); + static const bool ring = ReadSubmissionRingSettings(); + if (ring) PrepareSubmissionRing(stalled.queue); std::atomic_ref control(*const_cast(stalled.rewindTail - 1)); if ((control.load(std::memory_order_acquire) & Pm4::RewindValid) == 0) { noteWaitBlocked(stalled.queue, reinterpret_cast(stalled.rewindTail - 1), true); @@ -267,10 +270,12 @@ void Driver::executeRewindTail(const Submission& stalled) { std::uint32_t queue; ~BlockedWait() { driver.noteWaitBlocked(queue, 0, false); } } blocked{*this, stalled.queue}; + StallWatch stall(stalled.queue); while ((control.load(std::memory_order_acquire) & Pm4::RewindValid) == 0) { CheckFailure(); checkStopping(); noteAwaitingTitle(stalled.queue, reinterpret_cast(stalled.rewindTail - 1)); + stall.Check("REWIND at", reinterpret_cast(stalled.rewindTail - 1)); PollSleep(); } } @@ -303,6 +308,7 @@ void Driver::executeRewindTail(const Submission& stalled) { tail.serial = stalled.serial; tail.received = ++eventSerial; if (capture != nullptr) capture->Take(captured, tail.serial, captureTime(), tail.heldAtSubmit); + if (ring) RecordSubmission(tail.queue, tail.serial, tail.commands, stalled.rewindTail, true); } if (capture != nullptr) writeCaptured(*this, *capture, captured); if (APS5_ENABLE_TIMING_LOG) tail.enqueuedAt = tail.dequeuedAt = tail.orderedAt = std::chrono::steady_clock::now(); @@ -313,7 +319,9 @@ void Driver::executeRewindTail(const Submission& stalled) { void Driver::Submit(const Packet* packet, std::uint32_t queue) { const auto receivedAt = APS5_ENABLE_TIMING_LOG ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; CheckFailure(); - require(queue == 0 || (queue >= 0x20 && queue < 0x58), "unsupported compute queue"); + require(queue == 0 || (queue >= 0x20 && queue < SubmissionQueueEnd), "unsupported compute queue"); + static const bool ring = ReadSubmissionRingSettings(); + if (ring) PrepareSubmissionRing(queue); GuestMemory::CheckRange(packet, sizeof(Packet), alignof(Packet)); const auto descriptor = *packet; require(descriptor.flags == 0, "nonzero submission flags are not implemented"); @@ -355,6 +363,7 @@ void Driver::Submit(const Packet* packet, std::uint32_t queue) { const auto now = std::chrono::steady_clock::now(); submission.enqueuedAt = now; if (capture != nullptr) capture->Take(captured, submission.serial, captureTime(), submission.heldAtSubmit); + if (ring) RecordSubmission(queue, submission.serial, submission.commands, descriptor.addr, false); if (profile) { ++costs.submissions; costs.validateNs += static_cast(std::chrono::duration_cast(validated - copied).count()); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/SubmissionRing.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/SubmissionRing.cpp new file mode 100644 index 0000000000..900cbd946d --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/SubmissionRing.cpp @@ -0,0 +1,281 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver/Queues/SubmissionRing.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp" +#include "prx/libSceAgcDriver/Execution/include/Pm4.hpp" +#include "prx/libc/include/FailureReport.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif + +namespace AgcDriver::DriverDetail { + +namespace { + +constexpr std::size_t KeptWords = 512; +constexpr std::size_t MaximumCapacity = 4096; +constexpr std::size_t MaximumStallSeconds = 86400; +constexpr std::size_t PayloadWordsShown = 8; + +struct Entry { + std::uint64_t tenthsOfMs = 0; + std::uint64_t serial = 0; + std::uint64_t guest = 0; + std::size_t dwords = 0; + std::size_t kept = 0; + bool rewindTail = false; + std::array words{}; +}; + +struct Ring { + std::mutex mutex; + std::once_flag allocated; + std::atomic recorded{0}; + std::unique_ptr entries; +}; + +void reportFailure() noexcept; + +struct Rings { + std::array queues; +}; + +Rings& rings() { + static auto* value = new Rings; + return *value; +} + +struct Registration { + Registration() { LibcRegisterFailureReport_nid_no_patch(reportFailure); } + ~Registration() { LibcUnregisterFailureReport_nid_no_patch(reportFailure); } + Registration(const Registration&) = delete; + Registration& operator=(const Registration&) = delete; +}; + +std::size_t environmentCount(const char* name, std::size_t fallback, std::size_t maximum, const char* reason) { + const char* text = std::getenv(name); + if (text == nullptr) return fallback; + const auto* end = text + std::strlen(text); + std::size_t value = 0; + const auto [last, error] = std::from_chars(text, end, value); + require(error == std::errc{} && last == end && value <= maximum, reason); + return value; +} + +bool writeLine(const char* text, std::size_t length) { +#ifdef _WIN32 + const HANDLE handle = GetStdHandle(STD_ERROR_HANDLE); + if (handle == nullptr || handle == INVALID_HANDLE_VALUE) return false; + DWORD written = 0; + return WriteFile(handle, text, static_cast(length), &written, nullptr) != 0 && written == length; +#else + while (length != 0) { + const auto written = ::write(2, text, length); + if (written < 0 && errno == EINTR) continue; + if (written <= 0) return false; + text += written; + length -= static_cast(written); + } + return true; +#endif +} + +class Line { +public: + void Add(const char* format, ...) { + if (length >= sizeof(text) - 2) return; + va_list arguments; + va_start(arguments, format); + const int added = std::vsnprintf(text + length, sizeof(text) - 1 - length, format, arguments); + va_end(arguments); + if (added > 0) length = std::min(length + static_cast(added), sizeof(text) - 2); + } + + bool Write() { + text[length] = '\n'; + const bool written = writeLine(text, length + 1); + length = 0; + return written; + } + +private: + char text[512]; + std::size_t length = 0; +}; + +void addName(Line& line, std::uint32_t header) { + const auto name = Pm4::KnownName(header); + if (name.empty()) line.Add("UNKNOWN_0x%02x", static_cast((header >> 8u) & 0xffu)); + else line.Add("%.*s", static_cast(name.size()), name.data()); +} + +bool writePackets(const Entry& entry, Line& line) { + bool written = true; + for (std::size_t cursor = 0; cursor < entry.kept;) { + const auto header = entry.words[cursor]; + line.Add("[pm4 ring] +0x%04zx ", cursor); + if (Pm4::FillerPacket(header)) { + std::size_t run = 1; + while (cursor + run < entry.kept && Pm4::FillerPacket(entry.words[cursor + run])) ++run; + addName(line, header); + line.Add(" x%zu", run); + written = line.Write() && written; + cursor += run; + continue; + } + if ((header >> 30u) != 3u) { + line.Add("not a type-3 header:"); + for (std::size_t i = 0; i <= PayloadWordsShown && cursor + i < entry.kept; ++i) line.Add(" %08x", entry.words[cursor + i]); + written = line.Write() && written; + break; + } + const auto words = Pm4::PacketWords(header); + const auto shown = std::min(words, PayloadWordsShown + 1); + addName(line, header); + for (std::size_t i = 1; i < shown && cursor + i < entry.kept; ++i) line.Add(" %08x", entry.words[cursor + i]); + if (cursor + shown > entry.kept) line.Add(" (cut)"); + else if (words > shown) line.Add(" (+%zu dwords)", words - shown); + written = line.Write() && written; + cursor += words; + } + return written; +} + +bool dump(std::string_view reason, bool failing) { + const auto capacity = SubmissionRingCapacity(); + const auto label = static_cast(std::min(reason.size(), 200)); + Line line; + if (capacity == 0) { + line.Add("[pm4 ring] %.*s: disabled (APS5_PM4_RING=0)", label, reason.data()); + return line.Write(); + } + auto& state = rings(); + std::size_t used = 0; + for (const auto& ring : state.queues) used += ring.recorded.load(std::memory_order_acquire) != 0 ? 1 : 0; + if (used == 0) { + line.Add("[pm4 ring] %.*s: no submissions recorded", label, reason.data()); + return line.Write(); + } + line.Add("[pm4 ring] %.*s: %zu queue(s), up to %zu submissions each, oldest first", label, reason.data(), used, capacity); + bool written = line.Write(); + for (std::size_t queue = 0; queue < state.queues.size(); ++queue) { + auto& ring = state.queues[queue]; + if (ring.recorded.load(std::memory_order_acquire) == 0) continue; + std::unique_lock lock(ring.mutex, std::defer_lock); + if (failing) { + for (int attempt = 0; attempt < 100 && !lock.try_lock(); ++attempt) std::this_thread::yield(); + } else { + lock.lock(); + } + const auto recorded = ring.recorded.load(std::memory_order_acquire); + if (!lock.owns_lock()) { + line.Add("[pm4 ring] queue 0x%zx: %llu submissions recorded, ring busy", queue, static_cast(recorded)); + written = line.Write() && written; + continue; + } + const auto first = recorded > capacity ? recorded - capacity : 0; + line.Add("[pm4 ring] queue 0x%zx: %llu submissions recorded, last %llu kept", queue, static_cast(recorded), static_cast(recorded - first)); + written = line.Write() && written; + for (auto ordinal = first; ordinal < recorded; ++ordinal) { + const auto& entry = ring.entries[ordinal % capacity]; + line.Add("[pm4 ring] queue 0x%zx #%llu serial %llu t=%llu.%llu ms: %zu dwords from 0x%llx%s, first %zu kept", queue, static_cast(ordinal + 1), static_cast(entry.serial), + static_cast(entry.tenthsOfMs / 10), static_cast(entry.tenthsOfMs % 10), + entry.dwords, static_cast(entry.guest), entry.rewindTail ? " (rewind tail)" : "", entry.kept); + written = line.Write() && written; + written = writePackets(entry, line) && written; + } + } + return written; +} + +void reportFailure() noexcept { + try { + dump("failure report", true); + } catch (...) { + static constexpr char text[] = "[pm4 ring] failure report: dump failed\n"; + writeLine(text, sizeof(text) - 1); + } +} + +} + +std::size_t SubmissionRingCapacity() { + static const std::size_t value = environmentCount("APS5_PM4_RING", 16, MaximumCapacity, "APS5_PM4_RING must be a number of submissions from 0 to 4096"); + return value; +} + +std::chrono::seconds StallWatchdogPeriod() { + static const std::chrono::seconds value(environmentCount("APS5_STALL_WATCHDOG_SECS", 0, MaximumStallSeconds, "APS5_STALL_WATCHDOG_SECS must be a number of seconds from 0 to 86400")); + return value; +} + +bool ReadSubmissionRingSettings() { + StallWatchdogPeriod(); + return SubmissionRingCapacity() != 0; +} + +void PrepareSubmissionRing(std::uint32_t queue) { + const auto capacity = SubmissionRingCapacity(); + require(capacity != 0, "the PM4 ring is disabled"); + require(queue < SubmissionQueueEnd, "PM4 ring queue out of range"); + static const Registration registration; + auto& ring = rings().queues[queue]; + std::call_once(ring.allocated, [&] { ring.entries = std::make_unique(capacity); }); +} + +void RecordSubmission(std::uint32_t queue, std::uint64_t serial, std::span commands, const void* guest, bool rewindTail) { + const auto capacity = SubmissionRingCapacity(); + require(capacity != 0, "the PM4 ring is disabled"); + require(queue < SubmissionQueueEnd, "PM4 ring queue out of range"); + auto& ring = rings().queues[queue]; + require(ring.entries != nullptr, "PM4 ring recorded before it was prepared"); + const auto tenthsOfMs = static_cast(std::max(TraceMs(), 0.0) * 10.0 + 0.5); + std::lock_guard lock(ring.mutex); + const auto recorded = ring.recorded.load(std::memory_order_relaxed); + auto& entry = ring.entries[recorded % capacity]; + entry.tenthsOfMs = tenthsOfMs; + entry.serial = serial; + entry.guest = reinterpret_cast(guest); + entry.dwords = commands.size(); + entry.kept = std::min(commands.size(), KeptWords); + entry.rewindTail = rewindTail; + std::copy_n(commands.begin(), entry.kept, entry.words.begin()); + ring.recorded.store(recorded + 1, std::memory_order_release); +} + +void DumpSubmissionRings(std::string_view reason) { + require(dump(reason, false), "cannot write the PM4 ring dump to stderr"); +} + +void StallWatch::Check(const char* waiter, std::uint64_t value) { + if (dumped) return; + const auto period = StallWatchdogPeriod(); + if (period.count() == 0) return; + const auto now = std::chrono::steady_clock::now(); + if (since == std::chrono::steady_clock::time_point{}) { + since = now; + return; + } + if (now - since < period) return; + dumped = true; + char reason[192]; + std::snprintf(reason, sizeof(reason), "queue 0x%x %s 0x%llx has not progressed for %lld s (APS5_STALL_WATCHDOG_SECS)", queue, waiter, static_cast(value), static_cast(period.count())); + DumpSubmissionRings(reason); +} + +} diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/WaitMemory.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/WaitMemory.cpp index 3deebcb869..ef7a88bb2f 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/WaitMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/WaitMemory.cpp @@ -1,6 +1,7 @@ #include "prx/libSceAgcDriver/Execution/include/ProfileOutput.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Synchronization/WaitMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Queues/SubmissionRing.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Synchronization/SynchronizationStatistics.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" @@ -330,6 +331,7 @@ void Driver::waitMemory(std::span packet, std::uint32_t que const auto spinStart = std::chrono::steady_clock::now(); bool spinning = pauseSpin; std::uint32_t polls = 0; + StallWatch stall(queue); while (!Pm4::WaitSatisfiedUnchecked(packet)) { if (std::uint32_t writer = queue; !requireMemory && storedSince(packet, awaited, awaitedBytes, received, &writer)) { if (writer != queue) notePendingWait(packet, queue, received); @@ -358,7 +360,9 @@ void Driver::waitMemory(std::span packet, std::uint32_t que if (const auto done = packetsDone.load(); done != lastDone || (!requireMemory && packetsInFlight.load() != 0)) { lastDone = done; start = std::chrono::steady_clock::now(); + stall.Progressed(); } + stall.Check(((packet[0] >> 8u) & 0xffu) == 0x93u ? "WAIT_REG_MEM_64 at" : "WAIT_REG_MEM at", awaited); if (!warned && std::chrono::steady_clock::now() - start > std::chrono::milliseconds(waitTimeoutMs())) { warned = true; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp index bdee46ee16..2a8ef54378 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp @@ -175,7 +175,7 @@ std::vector copyDataSource(std::span packet, con } -std::string Name(std::uint32_t header) { +std::string_view KnownName(std::uint32_t header) { if (FillerPacket(header)) return "PM4_FILLER"; const auto opcode = (header >> 8u) & 0xffu; if (opcode == 0x10 && (header & 0xfcu) != 0) { @@ -194,9 +194,14 @@ std::string Name(std::uint32_t header) { default: return "UNKNOWN_CUSTOM"; } } - for (const auto& entry : Opcodes) if (entry.value == opcode) return std::string(entry.name); + for (const auto& entry : Opcodes) if (entry.value == opcode) return entry.name; + return {}; +} + +std::string Name(std::uint32_t header) { + if (const auto known = KnownName(header); !known.empty()) return std::string(known); char text[24]{}; - std::snprintf(text, sizeof(text), "UNKNOWN_0x%02x", opcode); + std::snprintf(text, sizeof(text), "UNKNOWN_0x%02x", (header >> 8u) & 0xffu); return text; } diff --git a/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp b/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp index 39131eb9ca..00359e947e 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp @@ -3,16 +3,27 @@ #include "prx/libSceAgcDriver/Execution/include/Driver.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawRegisterKey.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Queues/SubmissionRing.hpp" +#include "prx/libSceAgcDriver/Execution/include/VideoOutput.hpp" #include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" #include "prx/libSceAgcDriver/Submit/include/Acb.hpp" +#include "prx/libc/include/FailureReport.hpp" #include "prx/libc/include/Shutdown.hpp" +#include #include #include +#include #include #include +#include +#include #include +#include +#include #include #include +#include +#include #include #include #include @@ -21,6 +32,9 @@ #define NOMINMAX #endif #include +#else +#include +#include #endif extern "C" int APS5_VABI sceKernelAvailableFlexibleMemorySize(size_t* size); @@ -62,18 +76,22 @@ void testCatalog() { for (const auto& opcode : AgcDriver::Pm4::Opcodes) { check(values.insert(opcode.value).second, "duplicate PM4 opcode"); const auto packet = makePacket(opcode.value, {0}); - check(AgcDriver::Pm4::Name(packet[0]) == opcode.name, "opcode name mismatch"); + check(AgcDriver::Pm4::Name(packet[0]) == opcode.name && AgcDriver::Pm4::KnownName(packet[0]) == opcode.name, "opcode name mismatch"); const auto reason = AgcDriver::Pm4::UnsupportedReason(packet[0]); if (!reason.empty()) expectFailure([&] { AgcDriver::Pm4::Validate(packet, 0); }, std::string(reason).c_str()); } check(values.size() == 56, "reference opcode catalog is incomplete"); expectFailure([] { AgcDriver::Pm4::Validate(makePacket(0xff, {0}), 0); }, "not known"); + check(AgcDriver::Pm4::KnownName(makePacket(0xff, {0})[0]).empty() && AgcDriver::Pm4::Name(makePacket(0xff, {0})[0]) == "UNKNOWN_0xff", "an unknown opcode is named"); const std::array, 11> custom{{ {5, "DRAW_RESET"}, {6, "WAIT_FLIP_DONE"}, {9, "DISPATCH_RESET"}, {11, "PUSH_MARKER"}, {12, "POP_MARKER"}, {20, "ACQUIRE_MEM_CUSTOM"}, {21, "WRITE_DATA_CUSTOM"}, {23, "FLIP"}, {24, "RELEASE_MEM_CUSTOM"}, {25, "DMA_DATA_CUSTOM"}, {26, "CONTEXT_STATE"} }}; - for (const auto& [id, name] : custom) check(AgcDriver::Pm4::Name(makePacket(0x10, {0}, id << 2)[0]) == name, "custom opcode name mismatch"); + for (const auto& [id, name] : custom) { + const auto header = makePacket(0x10, {0}, id << 2)[0]; + check(AgcDriver::Pm4::Name(header) == name && AgcDriver::Pm4::KnownName(header) == name, "custom opcode name mismatch"); + } } void testNopPad() { @@ -1237,6 +1255,370 @@ void testAsyncMemoryFailure() { expectFailure([] { LibcRunShutdown_nid_postfix(); }, "guest"); } +class CapturedStderr { +public: + CapturedStderr() { +#ifdef _WIN32 + path = std::filesystem::temp_directory_path() / ("aps5-pm4-ring-" + std::to_string(GetCurrentProcessId()) + ".txt"); + file = CreateFileW(path.c_str(), GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr); + check(file != INVALID_HANDLE_VALUE, "cannot create the stderr capture file"); + previous = GetStdHandle(STD_ERROR_HANDLE); + check(SetStdHandle(STD_ERROR_HANDLE, file) != 0, "cannot redirect stderr"); +#else + path = std::filesystem::temp_directory_path() / ("aps5-pm4-ring-" + std::to_string(::getpid()) + ".txt"); + file = ::open(path.c_str(), O_CREAT | O_TRUNC | O_WRONLY, 0600); + check(file >= 0, "cannot create the stderr capture file"); + std::fflush(stderr); + previous = ::dup(2); + check(previous >= 0 && ::dup2(file, 2) >= 0, "cannot redirect stderr"); +#endif + } + + ~CapturedStderr() { +#ifdef _WIN32 + SetStdHandle(STD_ERROR_HANDLE, previous); + CloseHandle(file); +#else + std::fflush(stderr); + ::dup2(previous, 2); + ::close(previous); + ::close(file); +#endif + std::error_code ignored; + std::filesystem::remove(path, ignored); + } + + CapturedStderr(const CapturedStderr&) = delete; + CapturedStderr& operator=(const CapturedStderr&) = delete; + + std::vector RingLines() const { + std::ifstream input(path, std::ios::binary); + std::vector lines; + for (std::string line; std::getline(input, line);) { + if (!line.empty() && line.back() == '\r') line.pop_back(); + if (line.starts_with("[pm4 ring]")) lines.push_back(line); + } + return lines; + } + +private: + std::filesystem::path path; +#ifdef _WIN32 + HANDLE file = INVALID_HANDLE_VALUE; + HANDLE previous = nullptr; +#else + int file = -1; + int previous = -1; +#endif +}; + +std::vector recordedWrites(const std::vector& lines) { + std::vector writes; + std::string entry; + for (const auto& line : lines) { + if (line.starts_with("[pm4 ring] queue ") && line.find(" #") != std::string::npos) { + entry = line.substr(17, line.find(" serial") - 17); + } else if (line.find("WRITE_DATA") != std::string::npos) { + check(!entry.empty() && line.size() >= 8, "a WRITE_DATA line is not under a submission"); + writes.push_back(entry + " " + line.substr(line.size() - 8)); + entry.clear(); + } + } + return writes; +} + +void submitWrites(std::uint32_t queue, std::uint32_t first, std::uint32_t count) { + alignas(8) static std::uint32_t target = 0; + for (std::uint32_t value = first; value < first + count; ++value) { + auto words = writeWord(target, value); + submitWords(words, queue); + } + AgcDriverWaitIdle_nid_postfix(); +} + +void testSubmissionRing() { + submitWrites(0, 0x5a000001u, 6); + submitWrites(0x20, 0x5b000001u, 2); + std::vector lines; + { + CapturedStderr capture; + AgcDriver::DriverDetail::DumpSubmissionRings("ring test"); + lines = capture.RingLines(); + } + check(!lines.empty() && lines.front() == "[pm4 ring] ring test: 2 queue(s), up to 4 submissions each, oldest first", "the ring dump header is wrong"); + check(std::find(lines.begin(), lines.end(), "[pm4 ring] queue 0x0: 6 submissions recorded, last 4 kept") != lines.end(), "the ring dump does not count every submission of queue 0"); + check(std::find(lines.begin(), lines.end(), "[pm4 ring] queue 0x20: 2 submissions recorded, last 2 kept") != lines.end(), "the ring dump does not count every submission of queue 0x20"); + const std::vector expected{"0x0 #3 5a000003", "0x0 #4 5a000004", "0x0 #5 5a000005", "0x0 #6 5a000006", "0x20 #1 5b000001", "0x20 #2 5b000002"}; + check(recordedWrites(lines) == expected, "the ring dump does not hold exactly the last submissions of each queue in order"); +} + +void testEmptySubmissionRing() { + CapturedStderr capture; + AgcDriver::DriverDetail::DumpSubmissionRings("ring test"); + check(capture.RingLines() == std::vector{"[pm4 ring] ring test: no submissions recorded"}, "the dump of empty rings is not one line"); +} + +void testDisabledSubmissionRing() { + submitWrites(0, 0x5a000001u, 2); + CapturedStderr capture; + AgcDriver::DriverDetail::DumpSubmissionRings("ring test"); + check(capture.RingLines() == std::vector{"[pm4 ring] ring test: disabled (APS5_PM4_RING=0)"}, "the dump of disabled rings is not one line"); +} + +void expectSettingRejected(const char* text) { + alignas(8) static std::uint32_t target = 0; + auto words = writeWord(target, 1); + Packet packet{words.data(), static_cast(words.size()), 0, {}}; + expectFailure([&] { sceAgcDriverSubmitDcb(&packet); }, text); + AgcDriverWaitIdle_nid_postfix(); + check(target == 0, "a submission rejected for its ring or watchdog setting was executed"); +} + +void testInvalidSubmissionRing() { + expectSettingRejected("APS5_PM4_RING must be a number"); +} + +void testInvalidStallWatchdog() { + expectSettingRejected("APS5_STALL_WATCHDOG_SECS must be a number"); +} + +void testSubmissionRingFailureReport() { + submitWrites(0, 0x5a000001u, 3); + std::vector lines; + { + CapturedStderr capture; + LibcRunFailureReports_nid_no_patch(); + lines = capture.RingLines(); + } + check(!lines.empty() && lines.front() == "[pm4 ring] failure report: 1 queue(s), up to 2 submissions each, oldest first", "the failure report does not dump the rings"); + check(recordedWrites(lines) == std::vector{"0x0 #2 5a000002", "0x0 #3 5a000003"}, "the failure report does not hold the last submissions in order"); +} + +#ifdef _WIN32 +std::atomic heapTrapThread{0}; +std::atomic heapTrapAllocations{0}; +decltype(&HeapAlloc) heapAllocOriginal = nullptr; +decltype(&HeapReAlloc) heapReAllocOriginal = nullptr; + +LPVOID WINAPI trappedHeapAlloc(HANDLE heap, DWORD flags, SIZE_T bytes) { + if (GetCurrentThreadId() == heapTrapThread.load()) heapTrapAllocations.fetch_add(1); + return heapAllocOriginal(heap, flags, bytes); +} + +LPVOID WINAPI trappedHeapReAlloc(HANDLE heap, DWORD flags, LPVOID block, SIZE_T bytes) { + if (GetCurrentThreadId() == heapTrapThread.load()) heapTrapAllocations.fetch_add(1); + return heapReAllocOriginal(heap, flags, block, bytes); +} + +void** importSlot(HMODULE module, const char* function) { + auto* base = reinterpret_cast(module); + const auto* headers = reinterpret_cast(base + reinterpret_cast(base)->e_lfanew); + const auto& directory = headers->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT]; + for (auto* import = reinterpret_cast(base + directory.VirtualAddress); import->Name != 0; ++import) { + const auto* names = reinterpret_cast(base + import->OriginalFirstThunk); + auto* slots = reinterpret_cast(base + import->FirstThunk); + for (; names->u1.AddressOfData != 0; ++names, ++slots) { + if (IMAGE_SNAP_BY_ORDINAL(names->u1.Ordinal)) continue; + const auto* byName = reinterpret_cast(base + names->u1.AddressOfData); + if (std::strcmp(reinterpret_cast(byName->Name), function) == 0) return reinterpret_cast(&slots->u1.Function); + } + } + return nullptr; +} + +void replaceSlot(void** slot, void* value) { + DWORD protection = 0; + check(VirtualProtect(slot, sizeof(void*), PAGE_READWRITE, &protection) != 0, "cannot unprotect a ucrtbase.dll import"); + *slot = value; + DWORD ignored = 0; + check(VirtualProtect(slot, sizeof(void*), protection, &ignored) != 0, "cannot protect a ucrtbase.dll import"); +} + +class CrtHeapTrap { +public: + CrtHeapTrap() { + const HMODULE runtime = GetModuleHandleW(L"ucrtbase.dll"); + check(runtime != nullptr, "ucrtbase.dll is not loaded"); + allocSlot = importSlot(runtime, "HeapAlloc"); + reAllocSlot = importSlot(runtime, "HeapReAlloc"); + check(allocSlot != nullptr && reAllocSlot != nullptr, "ucrtbase.dll does not import HeapAlloc and HeapReAlloc"); + heapAllocOriginal = reinterpret_cast(*allocSlot); + heapReAllocOriginal = reinterpret_cast(*reAllocSlot); + replaceSlot(allocSlot, reinterpret_cast(trappedHeapAlloc)); + replaceSlot(reAllocSlot, reinterpret_cast(trappedHeapReAlloc)); + } + + ~CrtHeapTrap() { + replaceSlot(allocSlot, reinterpret_cast(heapAllocOriginal)); + replaceSlot(reAllocSlot, reinterpret_cast(heapReAllocOriginal)); + } + + CrtHeapTrap(const CrtHeapTrap&) = delete; + CrtHeapTrap& operator=(const CrtHeapTrap&) = delete; + + template + std::uint32_t Allocations(TAction action) { + heapTrapAllocations.store(0); + heapTrapThread.store(GetCurrentThreadId()); + action(); + heapTrapThread.store(0); + return heapTrapAllocations.load(); + } + +private: + void** allocSlot = nullptr; + void** reAllocSlot = nullptr; +}; + +void testSubmissionRingFailureReportHeap() { + alignas(8) static std::uint32_t target = 0; + auto words = joinPackets({makePacket(0x13, {0x40}), writeWord(target, 0x5c000001u)}); + submitWords(words); + AgcDriverWaitIdle_nid_postfix(); + std::vector lines; + { + CrtHeapTrap trap; + check(trap.Allocations([&] { check(AgcDriver::Pm4::Name(words[0]) == "INDEX_BUFFER_SIZE", "INDEX_BUFFER_SIZE is misnamed"); }) != 0, "the heap trap does not see the allocation of a packet name"); + CapturedStderr capture; + const auto allocations = trap.Allocations([] { LibcRunFailureReports_nid_no_patch(); }); + lines = capture.RingLines(); + check(allocations == 0, "the failure report allocated from the C runtime heap"); + } + check(std::any_of(lines.begin(), lines.end(), [](const std::string& line) { return line.find("+0x0000 INDEX_BUFFER_SIZE 00000040") != std::string::npos; }), "the failure report does not name INDEX_BUFFER_SIZE"); + check(recordedWrites(lines) == std::vector{"0x0 #1 5c000001"}, "the failure report does not hold the submission"); +} +#endif + +bool hasRingLine(const std::vector& lines, std::string_view prefix, std::string_view suffix) { + return std::any_of(lines.begin(), lines.end(), [&](const std::string& line) { return line.size() >= prefix.size() + suffix.size() && line.starts_with(prefix) && line.ends_with(suffix); }); +} + +void awaitRingLine(const CapturedStderr& capture, std::string_view prefix, std::string_view suffix, const char* reason) { + for (int waited = 0; waited < 1000 && !hasRingLine(capture.RingLines(), prefix, suffix); ++waited) std::this_thread::sleep_for(std::chrono::milliseconds(10)); + check(hasRingLine(capture.RingLines(), prefix, suffix), reason); +} + +void checkOneStallDump(const std::vector& lines, std::uint32_t& done) { + check(std::atomic_ref(done).load() == 1, "the stalled submission did not finish after it was released"); + check(std::count_if(lines.begin(), lines.end(), [](const std::string& line) { return line.find("has not progressed") != std::string::npos; }) == 1, "a stalled waiter dumped the rings more than once"); +} + +std::string stallPrefix(const char* waiter, const void* address) { + char text[160]; + std::snprintf(text, sizeof(text), "[pm4 ring] queue 0x0 %s 0x%llx has not progressed", waiter, static_cast(reinterpret_cast(address))); + return text; +} + +void testStallWatchdog() { + alignas(8) static std::uint32_t gate = 0; + alignas(8) static std::uint32_t done = 0; + std::vector lines; + { + CapturedStderr capture; + auto words = joinPackets({makePacket(0x3c, {0x13, low(&gate), high(&gate), 1, 0xffffffffu, 0x19}), writeWord(done, 1)}); + submitWords(words); + awaitRingLine(capture, stallPrefix("WAIT_REG_MEM at", &gate), " for 1 s (APS5_STALL_WATCHDOG_SECS): 1 queue(s), up to 16 submissions each, oldest first", + "a WAIT_REG_MEM stalled past APS5_STALL_WATCHDOG_SECS did not dump the rings"); + std::this_thread::sleep_for(std::chrono::milliseconds(1500)); + check(std::atomic_ref(done).load() == 0, "the stalled submission finished before its gate was opened"); + std::atomic_ref(gate).store(1); + AgcDriverWaitIdle_nid_postfix(); + lines = capture.RingLines(); + } + checkOneStallDump(lines, done); + check(std::any_of(lines.begin(), lines.end(), [](const std::string& line) { return line.find("+0x0000 WAIT_REG_MEM 00000013") != std::string::npos; }), "the stall dump does not decode the stalled submission"); +} + +void testStallWatchdogRewind() { + alignas(8) static std::uint32_t done = 0; + static std::vector words; + words = joinPackets({makePacket(0x59, {0}), writeWord(done, 1)}); + std::vector lines; + { + CapturedStderr capture; + submitWords(words); + awaitRingLine(capture, stallPrefix("REWIND at", &words[1]), " for 1 s (APS5_STALL_WATCHDOG_SECS): 1 queue(s), up to 16 submissions each, oldest first", + "a REWIND stalled past APS5_STALL_WATCHDOG_SECS did not dump the rings"); + std::this_thread::sleep_for(std::chrono::milliseconds(1500)); + check(std::atomic_ref(done).load() == 0, "the tail of a REWIND ran before the title released it"); + std::atomic_ref(words[1]).store(0x80000000u); + AgcDriverWaitIdle_nid_postfix(); + lines = capture.RingLines(); + } + checkOneStallDump(lines, done); + check(std::any_of(lines.begin(), lines.end(), [](const std::string& line) { return line.find("+0x0000 REWIND 00000000") != std::string::npos; }), "the stall dump does not decode the stalled REWIND"); +} + +struct RenderingGateState { + std::mutex mutex; + std::condition_variable changed; + bool closed = true; + bool waiting = false; + + void Open() { + { + std::lock_guard lock(mutex); + closed = false; + } + changed.notify_all(); + } +}; + +class RenderingGate final : public AgcDriver::IRenderingWait { +public: + explicit RenderingGate(std::shared_ptr state) : state(std::move(state)) {} + + void Wait() override { + std::unique_lock lock(state->mutex); + state->waiting = true; + state->changed.notify_all(); + state->changed.wait(lock, [&] { return !state->closed; }); + } + +private: + std::shared_ptr state; +}; + +class GatedOutput final : public AgcDriver::IVideoOutput { +public: + std::shared_ptr Reserve(const AgcDriver::FlipInfo&) override { throw std::runtime_error("the gated test output takes no flips"); } + std::shared_ptr CaptureRenderingWait(std::uint32_t) override { return std::make_shared(state); } + void Fail(std::exception_ptr) noexcept override {} + + std::shared_ptr state = std::make_shared(); +}; + +void testStallWatchdogOrder() { + alignas(8) static std::uint32_t done = 0; + auto output = std::make_shared(); + AgcDriverRegisterVideoOutput_nid_postfix(7, output); + struct GateRelease { + std::shared_ptr state; + ~GateRelease() { state->Open(); } + } release{output->state}; + std::vector lines; + { + CapturedStderr capture; + std::vector wait{AgcDriver::RenderingWaitPacketHeader, 7, 0, 0}; + submitWords(wait); + { + std::unique_lock lock(output->state->mutex); + check(output->state->changed.wait_for(lock, std::chrono::seconds(5), [&] { return output->state->waiting; }), "queue 0 did not reach the rendering wait"); + } + auto words = writeWord(done, 1); + submitWords(words, 0x20); + awaitRingLine(capture, "[pm4 ring] queue 0x20 order behind queue 0 for event 0x", " has not progressed for 1 s (APS5_STALL_WATCHDOG_SECS): 2 queue(s), up to 16 submissions each, oldest first", + "a compute submission held behind queue 0 past APS5_STALL_WATCHDOG_SECS did not dump the rings"); + std::this_thread::sleep_for(std::chrono::milliseconds(1500)); + check(std::atomic_ref(done).load() == 0, "a compute submission passed queue 0 while it was held"); + output->state->Open(); + AgcDriverWaitIdle_nid_postfix(); + lines = capture.RingLines(); + } + AgcDriverUnregisterVideoOutput_nid_postfix(7, output); + checkOneStallDump(lines, done); +} + void testRefusedWorkerPriority() { alignas(8) static std::uint32_t result = 0; auto words = writeWord(result, 91); @@ -1268,6 +1650,28 @@ int main(int argc, char** argv) { std::puts("PM4 refused queue worker priority propagated to idle and shutdown"); return 0; } + const std::pair ringModes[]{ + {"ring", testSubmissionRing}, {"ring-empty", testEmptySubmissionRing}, {"ring-disabled", testDisabledSubmissionRing}, + {"ring-invalid", testInvalidSubmissionRing}, {"ring-report", testSubmissionRingFailureReport}, {"stall-watchdog", testStallWatchdog}, + {"stall-watchdog-invalid", testInvalidStallWatchdog}, {"stall-watchdog-rewind", testStallWatchdogRewind}, {"stall-watchdog-order", testStallWatchdogOrder}}; + if (argc == 2 && std::string(argv[1]) == "ring-report-heap") { +#ifdef _WIN32 + testSubmissionRingFailureReportHeap(); + LibcRunShutdown_nid_postfix(); + std::puts("PM4 ring-report-heap test passed"); + return 0; +#else + std::puts("the C runtime heap trap needs ucrtbase.dll"); + return 77; +#endif + } + for (const auto& [mode, test] : ringModes) { + if (argc != 2 || std::string(argv[1]) != mode) continue; + test(); + LibcRunShutdown_nid_postfix(); + std::printf("PM4 %s test passed\n", mode); + return 0; + } testCatalog(); testNopPad(); testWriteChangedKeepsUntouchedBytes(); diff --git a/core/libs/prx/libc/CMakeLists.txt b/core/libs/prx/libc/CMakeLists.txt index d4f3bc584a..b290daaf29 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -2,6 +2,7 @@ target_sources(libc PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/Export.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/General.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/CreationMask.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/FailureReport.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/specifics/gcc/StackGuard.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/specifics/windows/CrashReport.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/specifics/windows/CallTrace.cpp @@ -758,5 +759,15 @@ if(MINGW) set_tests_properties(windows_backtrace_report PROPERTIES TIMEOUT 20 ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$" PASS_REGULAR_EXPRESSION "aborting[^~]*SIGABRT. on thread[^~]*stack return address candidates:[^~]*rsp[+]0x") + add_test(NAME windows_backtrace_failure_report COMMAND windows_backtrace_report_tests report) + set_tests_properties(windows_backtrace_failure_report PROPERTIES TIMEOUT 20 + ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$" + PASS_REGULAR_EXPRESSION "aborting[^~]*SIGABRT. on thread[^~]*all threads:[^~]*kept failure report ran" + FAIL_REGULAR_EXPRESSION "dropped failure report ran;kept failure report ran[^~]*kept failure report ran") + add_test(NAME windows_backtrace_failure_report_once COMMAND windows_backtrace_report_tests report-once) + set_tests_properties(windows_backtrace_failure_report_once PROPERTIES TIMEOUT 20 + ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$" + PASS_REGULAR_EXPRESSION "kept failure report ran[^~]*aborting[^~]*SIGABRT. on thread[^~]*all threads:" + FAIL_REGULAR_EXPRESSION "kept failure report ran[^~]*kept failure report ran") endif() endif() diff --git a/core/libs/prx/libc/include/FailureReport.hpp b/core/libs/prx/libc/include/FailureReport.hpp new file mode 100644 index 0000000000..7a05284d6e --- /dev/null +++ b/core/libs/prx/libc/include/FailureReport.hpp @@ -0,0 +1,8 @@ +#ifndef CORE_LIBS_PRX_LIBC_INCLUDE_FAILUREREPORT_HPP +#define CORE_LIBS_PRX_LIBC_INCLUDE_FAILUREREPORT_HPP + +extern "C" void LibcRegisterFailureReport_nid_no_patch(void (*report)() noexcept); +extern "C" void LibcUnregisterFailureReport_nid_no_patch(void (*report)() noexcept) noexcept; +extern "C" void LibcRunFailureReports_nid_no_patch() noexcept; + +#endif diff --git a/core/libs/prx/libc/src/FailureReport.cpp b/core/libs/prx/libc/src/FailureReport.cpp new file mode 100644 index 0000000000..9fcb5473de --- /dev/null +++ b/core/libs/prx/libc/src/FailureReport.cpp @@ -0,0 +1,39 @@ +#include "prx/libc/include/FailureReport.hpp" +#include +#include +#include + +namespace { + +using Report = void (*)() noexcept; + +std::array, 8> reports{}; +std::atomic ran{false}; + +} + +extern "C" void LibcRegisterFailureReport_nid_no_patch(Report report) { + if (report == nullptr) throw std::invalid_argument("libc: null failure report"); + for (auto& slot : reports) { + if (slot.load(std::memory_order_acquire) == report) return; + } + for (auto& slot : reports) { + Report empty = nullptr; + if (slot.compare_exchange_strong(empty, report, std::memory_order_acq_rel)) return; + } + throw std::runtime_error("libc: more than 8 failure reports registered"); +} + +extern "C" void LibcUnregisterFailureReport_nid_no_patch(Report report) noexcept { + for (auto& slot : reports) { + Report expected = report; + slot.compare_exchange_strong(expected, nullptr, std::memory_order_acq_rel); + } +} + +extern "C" void LibcRunFailureReports_nid_no_patch() noexcept { + if (ran.exchange(true, std::memory_order_acq_rel)) return; + for (auto& slot : reports) { + if (const auto report = slot.load(std::memory_order_acquire)) report(); + } +} diff --git a/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp b/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp index 6491da364c..fb117a8e5f 100644 --- a/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp +++ b/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp @@ -2,6 +2,7 @@ #include #include #include "prx/libc/include/CallTrace.hpp" +#include "prx/libc/include/FailureReport.hpp" #include "prx/libc/include/GuestArena.hpp" #include #include @@ -394,6 +395,7 @@ LONG WINAPI ReportCrash(EXCEPTION_POINTERS* info) { ReportCallTrace(); std::fflush(stderr); ReportAllThreads(); + LibcRunFailureReports_nid_no_patch(); std::fflush(stderr); return EXCEPTION_CONTINUE_SEARCH; } @@ -546,10 +548,12 @@ void ReportBacktrace(const char* what, bool reportThreads) { void AbortSignalHandler(int) { ReportBacktrace("abort() (SIGABRT)", true); + LibcRunFailureReports_nid_no_patch(); } void InvalidParameterHandler(const wchar_t*, const wchar_t*, const wchar_t*, unsigned, std::uintptr_t) { ReportBacktrace("UCRT invalid parameter", true); + LibcRunFailureReports_nid_no_patch(); } // Static destruction runs on the thread that ends the process (ExitProcess -> DLL_PROCESS_DETACH), diff --git a/core/libs/tests/WindowsBacktraceReport.cpp b/core/libs/tests/WindowsBacktraceReport.cpp index a5d93edc43..32871b4f63 100644 --- a/core/libs/tests/WindowsBacktraceReport.cpp +++ b/core/libs/tests/WindowsBacktraceReport.cpp @@ -1,11 +1,35 @@ #include #include +#include #include "../prx/libc/include/general/VabiMacros.hpp" +#include "../prx/libc/include/FailureReport.hpp" extern "C" int* APS5_VABI __error_nid_postfix(); -int main() { +namespace { + +void KeptReport() noexcept { + std::fputs("kept failure report ran\n", stderr); +} + +void DroppedReport() noexcept { + std::fputs("dropped failure report ran\n", stderr); +} + +} + +int main(int argc, char** argv) { if (__error_nid_postfix() == nullptr) return 1; + if (argc == 2 && std::strcmp(argv[1], "report") == 0) { + LibcRegisterFailureReport_nid_no_patch(DroppedReport); + LibcRegisterFailureReport_nid_no_patch(KeptReport); + LibcRegisterFailureReport_nid_no_patch(KeptReport); + LibcUnregisterFailureReport_nid_no_patch(DroppedReport); + } + if (argc == 2 && std::strcmp(argv[1], "report-once") == 0) { + LibcRegisterFailureReport_nid_no_patch(KeptReport); + LibcRunFailureReports_nid_no_patch(); + } _set_abort_behavior(0, _WRITE_ABORT_MSG | _CALL_REPORTFAULT); std::fputs("aborting\n", stderr); std::abort(); diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 21512697ed..5033507300 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -79,6 +79,7 @@ - An [unnormalized S#](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp) (libSceAgcDriver) maps clamp mode 0 (repeat) on X and Y to clamp-to-edge as required by Vulkan unnormalized coordinates, and assigns the magnification filter when minification and magnification filters differ (since unnormalized coordinates sample at LOD 0). Anisotropy, TRUNC_COORD, MC_COORD_TRUNC and other clamp modes still throw. Secondary LOD bias (LOD_BIAS_SEC, bits [19:14]) is accepted and ignored, matching RDNA2 hardware measurements across explicit and implicit samples. High on Life (PPSA15388, #4736) uses secondary LOD bias and unnormalized coordinates with clamp mode 0 and differing min/mag filters during home base and city rendering. - [Background link-time-optimized relink of library pipelines](../../../core/libs/prx/libSceAgcDriver/Execution/include/PipelineLinkOptimization.hpp) (libSceAgcDriver) is skipped on every GPU under NVIDIA's proprietary Vulkan driver (`VK_DRIVER_ID_NVIDIA_PROPRIETARY`), so those pipelines keep their fast-linked form. The optimized link loses the device when bound on an RTX 3080 (Ampere, #2866) and an RTX 4060 (Ada, #3434); an RTX 5060 and an RTX 5080 (Blackwell) on the same driver do not, so the scope is wider than the evidence. No standard Vulkan query tells the architecture apart, and the driver version that fixes it is unknown. `APS5_NO_PIPELINE_LTO` disables the relink on every driver. - [8_SRGB and 8_8_SRGB textures](../../../core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp) (libSceAgcDriver) that the device samples as R8_SRGB or R8G8_SRGB use its own decode, which need not match gfx1035: NVIDIA's R8_SRGB is one bf16 step off in 29 of 256 codes. +- [PM4 submission rings](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/SubmissionRing.cpp) (libSceAgcDriver) keep only the first 512 dwords of each submission, so the packets past them are not printed. The [failure report](../../../core/libs/prx/libc/src/FailureReport.cpp) dump does not allocate from the C runtime heap: packet names come from static strings, each line is formatted with integer and string conversions into a 512-byte stack buffer and written with one `WriteFile`. It still runs on whatever state the crash left. It waits about 100 yields for each ring's lock and otherwise prints that queue as busy, so a crash while recording a submission loses that queue's ring. It reads the rings without checking them, so a crash that corrupted them can fault inside the dump; such a fault during the report of an unhandled exception stops in that report's `Sleep(INFINITE)` and the process hangs instead of ending. The reports run at most once per process. Linux has no crash report: there the rings are printed only by `APS5_STALL_WATCHDOG_SECS`. - [WAIT_REG_MEM](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/WaitMemory.cpp) reports a diagnostic after `APS5_GPU_WAIT_TIMEOUT_MS` without treating the timeout as completion. An unsatisfied wait keeps polling and checking driver failures; a missing producer can therefore leave its queue waiting indefinitely. Existing recorded-label shortcuts remain in use. - [COND_EXEC](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp) reads its condition after the earlier packets of its queue complete. Without PFP_SYNC_ME the console's graphics PFP reads it ahead of the ME, so a title that races the read sees a later value here. - A [flip inside a COND_EXEC range](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/CommandValidation.cpp) throws: the flip is reserved with the video output when the buffer is submitted, and how the console withdraws a skipped one is unknown. diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index 57f4ba8cdd..aa8af21f3b 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -202,6 +202,12 @@ Limits are listed in the [libc](../dev/debt/libc.md) and [relinker](../dev/debt/ Set `APS5_CRASH_INDIRECT_MEMORY=1` before launching the executable to include up to 64 bytes from each pointer found in the register memory dump. The report labels each block with its register, pointer offset and target address. Inaccessible, incomplete and executable targets are skipped. This additional dump is disabled when the variable is unset or empty. +### GPU submission rings and stall dumps + +Each GPU queue keeps its last 16 command buffer submissions: when each was submitted, its serial, size and guest address, and a copy of its first 512 dwords after indirect buffers are inlined. `APS5_PM4_RING=` keeps the last `n` instead, from 0 to 4096; 0 keeps none, and any other value stops the title at its first submission. The rings are printed to stderr, one line per submission and one per decoded packet, when the Windows crash report runs: an unhandled exception, `abort()`, an invalid CRT parameter or the timed dump of `APS5_HANG_DUMP_SECS`. + +With `APS5_STALL_WATCHDOG_SECS=`, a queue that has waited `n` seconds without progress in a `WAIT_REG_MEM`, a `REWIND` or behind queue 0 prints the rings once for that wait, and the title keeps running. A `WAIT_REG_MEM` progresses while any other packet runs or completes, as for `APS5_GPU_WAIT_TIMEOUT_MS`. `n` is from 0 to 86400 and 0, the default, disables the watchdog; any other value stops the title at its first submission, as for `APS5_PM4_RING`. While the rings are printed for a queue held behind queue 0, new submissions and the other queues wait. + ## Exit codes `0`: conversion succeeded or help was displayed. `1`: invalid arguments. `2`: conversion failed; the error is printed to stderr. With `--autorun`, successful conversion returns the launched application's exit code. From 4859af02132ea045535c2d4ba94f0862ed7b478c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Fri, 16 Oct 2026 10:56:26 +0200 Subject: [PATCH 0457/1000] test(agc): register the C runtime heap trap test only on Windows The mode returns 77 on Linux with a message that tools/check_skips.py does not accept as a device limitation, so the full Build failed the skip check. --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index c090ed8237..f8f84056d8 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1669,8 +1669,12 @@ if(BUILD_TESTING) endif() add_test(NAME agc_driver_pm4_failure COMMAND agc_driver_pm4_tests failure) set_tests_properties(agc_driver_pm4_failure PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 TIMEOUT 30) - foreach(pm4RingCase IN ITEMS "ring|APS5_PM4_RING=4" "ring-empty|APS5_PM4_RING=16" "ring-disabled|APS5_PM4_RING=0" "ring-invalid|APS5_PM4_RING=4x" "ring-report|APS5_PM4_RING=2" "ring-report-heap|APS5_PM4_RING=2" "stall-watchdog|APS5_STALL_WATCHDOG_SECS=1" + set(pm4RingCases "ring|APS5_PM4_RING=4" "ring-empty|APS5_PM4_RING=16" "ring-disabled|APS5_PM4_RING=0" "ring-invalid|APS5_PM4_RING=4x" "ring-report|APS5_PM4_RING=2" "stall-watchdog|APS5_STALL_WATCHDOG_SECS=1" "stall-watchdog-invalid|APS5_STALL_WATCHDOG_SECS=1x" "stall-watchdog-rewind|APS5_STALL_WATCHDOG_SECS=1" "stall-watchdog-order|APS5_STALL_WATCHDOG_SECS=1") + if(WIN32) + list(APPEND pm4RingCases "ring-report-heap|APS5_PM4_RING=2") + endif() + foreach(pm4RingCase IN LISTS pm4RingCases) string(REPLACE "|" ";" pm4RingCase "${pm4RingCase}") list(GET pm4RingCase 0 pm4RingMode) list(GET pm4RingCase 1 pm4RingEnvironment) From 13a82bdccf8ebbdc4e7430949a1480f9f69df7a9 Mon Sep 17 00:00:00 2001 From: Mouad-Scriptz-0 Date: Mon, 12 Oct 2026 17:37:03 +0000 Subject: [PATCH 0458/1000] perf(libSceAgcDriver): order in-place vertex inputs behind pending GPU writes with a barrier An in-place draw input flushed pending GPU writes over its range on the CPU before the bind, a submit-and-wait of the open batch when a dispatch in it wrote the buffer. The CPU never reads a vertex buffer's bytes, so a vertex input under a pending write is bound in place behind a barrier from shader and transfer writes to vertex attribute reads, recorded before the draw's rendering begins; index inputs keep the flush, since the highest index is read on the CPU. Minecraft generates a vertex buffer with a compute dispatch every frame and drew from it one CPU wait later. Co-Authored-By: Claude Fable 5.1 --- .../libSceAgcDriver/Graphics/include/Draw.hpp | 2 +- .../prx/libSceAgcDriver/Graphics/src/Draw.cpp | 23 +++++++++++++++---- .../prx/libSceAgcDriver/tests/Recorder.cpp | 8 ++++++- 3 files changed, 27 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Draw.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Draw.hpp index d852d67432..0286db6e8e 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Draw.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Draw.hpp @@ -31,7 +31,7 @@ struct DrawInputCopy { DrawInputCopy CopyDrawInput(const Context& context, Recorder* recorder, std::uint64_t address, std::size_t bytes, std::size_t alignment, Recorder::SnapshotUse use); DrawInputCopy CopyZeroPaddedDrawInput(const Context& context, std::uint64_t address, std::size_t bytes, std::size_t validBytes); void KeepDrawInput(Recorder* recorder, std::uint64_t address, const DrawInputCopy& copy, Recorder::SnapshotUse use, std::uint32_t derived); -const HostImport* InPlaceDrawInput(const Context& context, std::uint64_t address, std::size_t bytes, std::size_t alignment); +const HostImport* InPlaceDrawInput(const Context& context, std::uint64_t address, std::size_t bytes, std::size_t alignment, bool* behindGpuWrite = nullptr); struct InPlaceIndices { std::optional highest; bool reused = false; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index 7fb4593c6e..48b8178c0d 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -925,11 +925,17 @@ void KeepDrawInput(Recorder* recorder, std::uint64_t address, const DrawInputCop recorder->KeepDrawSnapshot(address, copy.buffer->Bytes().size(), copy.generation, copy.registryGeneration, copy.buffer, use, derived); } -const HostImport* InPlaceDrawInput(const Context& context, std::uint64_t address, std::size_t bytes, std::size_t alignment) { +const HostImport* InPlaceDrawInput(const Context& context, std::uint64_t address, std::size_t bytes, std::size_t alignment, bool* behindGpuWrite) { auto* recorder = context.recorder; if (recorder == nullptr || bytes == 0) return nullptr; - GuestMemory::FlushGpuWrites(address, bytes); - if (recorder->PendingWriteOverlaps(address, bytes) || recorder->PendingLabelIn(address, bytes) || recorder->QueuedStoreOverlaps(address, bytes)) return nullptr; + if (behindGpuWrite == nullptr) { + GuestMemory::FlushGpuWrites(address, bytes); + if (recorder->PendingWriteOverlaps(address, bytes)) return nullptr; + } else { + StorageTexture::FlushPending(address, bytes, nullptr, "in-place vertex input"); + if (recorder->PendingWriteOverlaps(address, bytes)) *behindGpuWrite = true; + } + if (recorder->PendingLabelIn(address, bytes) || recorder->QueuedStoreOverlaps(address, bytes)) return nullptr; const auto overlaps = [&](const auto& writer) { return writer->WritesOverlap(address, bytes); }; if (context.copiedWriters != nullptr && std::any_of(context.copiedWriters->begin(), context.copiedWriters->end(), overlaps)) return nullptr; const auto drawWriters = DrawCopiedWriters(); @@ -978,6 +984,7 @@ struct DrawInputs { std::vector vertexHandles; std::vector vertexOffsets; std::vector> inPlaceReads; + bool vertexInputBehindGpuWrite = false; std::set fragmentOutputs; VkPipelineStageFlags shaderStages = 0; std::uint32_t meshGroups = 0; @@ -1180,7 +1187,7 @@ DrawInputs prepareDrawInputs(const Context& context, const State& state, const P const auto [begin, end] = plan.copies[range]; const auto bytes = static_cast(end - begin); GuestMemory::CheckRange(reinterpret_cast(begin), bytes, 1); - if (const auto* import = InPlaceDrawInput(context, begin, bytes, plan.alignments[range])) { + if (const auto* import = InPlaceDrawInput(context, begin, bytes, plan.alignments[range], &inputs.vertexInputBehindGpuWrite)) { rangeHandles.push_back(import->buffer); rangeOffsets.push_back(begin - import->base); inputs.inPlaceReads.emplace_back(begin, end); @@ -1793,6 +1800,10 @@ void recordDraw(const Context& context, const State& state, const Pm4::DrawParam countBarrier(1); } APS5_LOG_CHARS_OUT_DEBUG("Upload barrier recorded"); + if (inputs.vertexInputBehindGpuWrite) { + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_VERTEX_INPUT_BIT, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_VERTEX_ATTRIBUTE_READ_BIT); + countBarrier(1); + } if (meshIndirect) { meshArguments = recordMeshArguments(context, commands, recorder, true, state, draw, *record.indirect, countBarrier, inputs.restartTable); argumentBuffer = meshArguments->Handle(); @@ -2347,6 +2358,10 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& countBarrier(); } APS5_LOG_CHARS_OUT_DEBUG("Upload barrier recorded"); + if (inputs.vertexInputBehindGpuWrite) { + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_VERTEX_INPUT_BIT, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_VERTEX_ATTRIBUTE_READ_BIT); + countBarrier(); + } std::shared_ptr meshArguments; if (state.stages.mesh && args != nullptr) { meshArguments = recordMeshArguments(context, commands, recorder, recorded, state, draw, indirect, countBarrier, inputs.restartTable); diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 0514717755..ca6b3dad35 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -3131,10 +3131,16 @@ void drawInputInPlaceTests(const Device& device, Recorder& recorder) { Require(InPlaceDrawInput(context, vertices + 2, 256, 2) != nullptr, "an aligned 16-bit draw input was not bound in place"); recorder.NotePendingWrite(address + 8192, 16); Require(InPlaceDrawInput(context, address + 8192, 256, 4) != nullptr && !recorder.PendingWriteOverlaps(address + 8192, 256), "a draw input under a pending GPU write was bound before the write completed"); + bool behind = false; + Require(InPlaceDrawInput(context, address + 20480, 256, 4, &behind) != nullptr && !behind, "a vertex input free of pending GPU writes was marked as ordered behind one"); + recorder.NotePendingWrite(address + 20480, 16); + Require(InPlaceDrawInput(context, address + 20480, 256, 4, &behind) != nullptr && behind && recorder.PendingWriteOverlaps(address + 20480, 256), "a vertex input under a pending GPU write was not bound in place behind it"); const std::array label{std::byte{1}, std::byte{2}, std::byte{3}, std::byte{4}}; recorder.RecordStore(import->buffer, address + 12288 - import->base, label, address + 12288); Require(recorder.QueuedStoreOverlaps(address + 12288, 4), "the label store was not queued"); Require(InPlaceDrawInput(context, address + 12288, 256, 4) == nullptr, "a draw input under a queued label store was bound in place"); + behind = false; + Require(InPlaceDrawInput(context, address + 12288, 256, 4, &behind) == nullptr, "a vertex input under a queued label store was bound in place"); Require(InPlaceDrawInput(context, address + 16384, 256, 4) != nullptr, "a queued label store refused a disjoint draw input"); alignas(64) static std::array unregistered{}; Require(InPlaceDrawInput(context, reinterpret_cast(unregistered.data()), unregistered.size(), 4) == nullptr, "memory without a host import was bound in place"); @@ -7205,7 +7211,7 @@ bool vertexTargetRangeTests(const Device& device, Recorder& recorder, bool impor Draw(context, consumer, draw, shaders, {}, &recipe); WaitForOptimizedPipelines(context.device); const auto read = recorder.DescribePendingRead(address + unit, 16); - Require(read.has_value() == imported && (!read || read->kind == Recorder::ReadKind::DrawInput), "the vertex regression used the wrong copied/imported path"); + Require(read.has_value() == imported && (!read || read->kind == Recorder::ReadKind::DrawInput || read->kind == Recorder::ReadKind::StorageUpload), "the vertex regression used the wrong copied/imported path"); check(16); Require(recipe != nullptr, "the range regression did not create a draw recipe"); std::vector cleared(unit); From 4339eee2c3478ac6a3c4371866a096c8c7a32f0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Fri, 16 Oct 2026 11:30:20 +0200 Subject: [PATCH 0459/1000] fix(libSceAgcDriver): flag the vertex barrier only for bound inputs, cover attachment writes --- core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index 48b8178c0d..5ad7489d14 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -928,12 +928,13 @@ void KeepDrawInput(Recorder* recorder, std::uint64_t address, const DrawInputCop const HostImport* InPlaceDrawInput(const Context& context, std::uint64_t address, std::size_t bytes, std::size_t alignment, bool* behindGpuWrite) { auto* recorder = context.recorder; if (recorder == nullptr || bytes == 0) return nullptr; + bool behind = false; if (behindGpuWrite == nullptr) { GuestMemory::FlushGpuWrites(address, bytes); if (recorder->PendingWriteOverlaps(address, bytes)) return nullptr; } else { StorageTexture::FlushPending(address, bytes, nullptr, "in-place vertex input"); - if (recorder->PendingWriteOverlaps(address, bytes)) *behindGpuWrite = true; + behind = recorder->PendingWriteOverlaps(address, bytes); } if (recorder->PendingLabelIn(address, bytes) || recorder->QueuedStoreOverlaps(address, bytes)) return nullptr; const auto overlaps = [&](const auto& writer) { return writer->WritesOverlap(address, bytes); }; @@ -943,6 +944,7 @@ const HostImport* InPlaceDrawInput(const Context& context, std::uint64_t address const auto* import = HostImportFor(context, address, bytes); if (import == nullptr || (address - import->base) % alignment != 0) return nullptr; recorder->NotePendingRead(address, bytes, Recorder::ReadKind::DrawInput); + if (behindGpuWrite != nullptr && behind) *behindGpuWrite = true; return import; } @@ -1801,7 +1803,7 @@ void recordDraw(const Context& context, const State& state, const Pm4::DrawParam } APS5_LOG_CHARS_OUT_DEBUG("Upload barrier recorded"); if (inputs.vertexInputBehindGpuWrite) { - RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_VERTEX_INPUT_BIT, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_VERTEX_ATTRIBUTE_READ_BIT); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_VERTEX_INPUT_BIT, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT, VK_ACCESS_VERTEX_ATTRIBUTE_READ_BIT); countBarrier(1); } if (meshIndirect) { @@ -2359,7 +2361,7 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& } APS5_LOG_CHARS_OUT_DEBUG("Upload barrier recorded"); if (inputs.vertexInputBehindGpuWrite) { - RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_VERTEX_INPUT_BIT, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_VERTEX_ATTRIBUTE_READ_BIT); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_VERTEX_INPUT_BIT, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT, VK_ACCESS_VERTEX_ATTRIBUTE_READ_BIT); countBarrier(); } std::shared_ptr meshArguments; From 10e5b42ea95ab093b5185bb1c5a3625bf45443fd Mon Sep 17 00:00:00 2001 From: LeandroLP23 Date: Fri, 16 Oct 2026 11:33:28 +0200 Subject: [PATCH 0460/1000] docs(debt): record that guest thread priorities are not applied to host threads The sched_get_priority_max/min entry was rewritten by each fix that matched a PS5 measurement, until it described only behaviour that now matches the console. What remains is the gap no entry named: the priorities are recorded and reported but never reach the host scheduler. Co-Authored-By: Claude Opus 5.5 --- docs/dev/debt/libkernel.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 5396a00c3a..978664fb1e 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -97,7 +97,6 @@ - [uDGXwUKqi8s](../../../core/libs/prx/libkernel/Unimplemented.cpp) (libkernel) - unknown name, signature - [sceKernelGetCpuTemperature, sceKernelGetSocSensorTemperature](../../../core/libs/prx/libkernel/System/src/ThermalSensors.cpp) (libkernel) - read the host's thermal sensors: on Linux from `/sys/class/hwmon` and `/sys/class/thermal`, on Windows from the ACPI thermal zones (`MSAcpi_ThermalZoneTemperature` over WMI) and the storage temperature property of the first physical drive. Sensor 0 is the CPU/package value, 1 the first other value and 2 an NVMe value; a host that exposes none reports every sensor as unsupported (`EINVAL`). The console's SoC channel count and meanings are hardware-dependent and differ from the host's, the host sensors are not the console's, the ACPI reading is unavailable on many machines, and the storage temperature property is numbered 52 on Windows 10 2004 and later and 22 before it, so the mapping, the values and the unsupported-input error are unverified; the storage channel was checked against the drive's own SMART composite temperature on one NVMe host (`StorageDeviceTemperatureProperty`, 36 °C on a WD_BLACK SN7100). `sceKernelGetCpuTemperature` reports sensor 0. PPSA99058 and PPSA99420 call both - [LinuxProtFromSce](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (libkernel) - protection bits 0x100 and 0x200 are accepted and assumed not to affect host CPU access; host protection is derived only from the read, write and execute bits -- [sched_get_priority_max, sched_get_priority_min](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - measured on a PS5 (firmware 13.60): `SCHED_FIFO` (1) and `SCHED_RR` (3) report a maximum of 767 and a minimum of 256, and every other policy, `SCHED_OTHER` (2) included, returns -1 with `EINVAL`. `pthread_setschedparam` and `scePthreadSetschedparam` accept those two policies with a priority of 256 to 767 or 896 to 959 and keep the policy, as measured; a created thread takes the policy and priority of explicit scheduling attributes, or its creator's when it inherits, as it does with a null attribute - [inet_pton, inet_ntop](../../../core/libs/prx/libkernel/Socket/src/AddressConversion.cpp) (libkernel) - the text forms are assumed to be those of FreeBSD 11.4 and are unverified on a console: `inet_pton` accepts only four decimal parts without leading zeros for `AF_INET` and the RFC 4291 forms for `AF_INET6` (one `::` standing for at least one group, a dotted-quad tail only as the last 32 bits, no zone id), and `inet_ntop` prints lowercase hex with the first longest run of two or more zero groups compressed, and a dotted tail only for `::a.b.c.d` and `::ffff:a.b.c.d`. They no longer depend on the host resolver library - [sceKernelIsThreadSanitizerEnabled](../../../core/libs/prx/libkernel/System/src/Sanitizer.cpp) (libkernel, NID `LzoM-wVLJDE`) - no source gives the signature; `int (void)` is assumed from `sceKernelIsAddressSanitizerEnabled` and from PPSA23226's bundled `libSceJobManager_nosubmission.prx`, which passes no arguments and tests only the 32-bit result - [getdtablesize](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - throws: FreeBSD returns the smaller of the `RLIMIT_NOFILE` soft limit and `maxfilesperproc` ([`sys_getdtablesize`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L338-L355)), and neither value is known for the console, where `getrlimit(RLIMIT_NOFILE)` throws too. VLC (PPSA85300) imports it for a `closefrom` loop in the child of its spawn helper ([libc_stubs.c](https://github.com/ZiZc3/VLC/blob/d46be6a68f4244466b041c2dc1b640915ac1a983/ps5/compat/libc_stubs.c#L66-L72)), which never runs because its `fork` fails with `ENOSYS` @@ -105,6 +104,7 @@ ### Functional +- [Guest thread scheduling](../../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) - the policies and priorities that `sched_get_priority_max`, `sched_get_priority_min`, `pthread_setschedparam` and `scePthreadSetschedparam` accept, and those a created thread takes, follow a PS5 (firmware 13.60), but they are only recorded and reported: they are not applied to host threads, so a title's priority order has no effect on host scheduling - [sceKernelMprotect, sceKernelMtypeprotect](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (libkernel) over a range with a gap that is not registered in the allocation tracker protect the registered parts and skip the gap, and return 0, as FreeBSD 11.4's [`vm_map_protect`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/vm/vm_map.c) does (it has no check for holes); a range whose start is unmapped (also when later pages are mapped) returns `SCE_KERNEL_ERROR_EINVAL` (`mprotect`: `EINVAL`) because an existing test pins that for an unmapped start; FreeBSD answers 0 there too (`vm_map_protect` takes the next entry), so this errno is not taken from the source and was not changed. `sceKernelMprotect` threw for a gap before, which ended the title. Every part is checked for a GPU or image pin before the first one is protected; the parts are then protected one after the other, so a host protection failure in a later part leaves the earlier parts changed (FreeBSD's `vm_map_protect` cannot fail in its second pass). The behaviour on the console is not known. None of this was measured on a console. - [sceKernelMprotect protection bits](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - bits outside `0x3F7` are dropped instead of refused, as FreeBSD 11's [`sys_mprotect`](https://github.com/freebsd/freebsd-src/blob/34bd34750187bae02a0086402c78c031c59dd8b3/sys/vm/vm_mmap.c#L606) does with `VM_PROT_ALL`. Onimusha: Way of the Sword (PPSA27836 01.000.001) maps direct memory with `0xf2` and then protects it with `0xfffffff2`. Not measured on a console; the map calls and `sceKernelMtypeprotect` still refuse undefined bits - [JIT shared memory](../../../core/libs/prx/libkernel/DirectMemory/Jit.cpp) (libkernel) - `sceKernelJitCreateSharedMemory`, `sceKernelJitCreateAliasOfSharedMemory` and `sceKernelJitMapSharedMemory` take the signatures of [Orbital](https://github.com/AlexAltea/orbital)'s `tools/ida/db_types.json` (name, length, maximum protection, descriptor out; descriptor, maximum protection, descriptor out; descriptor, protection, address in and out), which ps4sdk, ps5-jar-loader and the call sites in RetroArch PPSA99169, XPSemu PPSA97358 and VLC PPSA85300 match; SharpProspero declares the alias as returning the descriptor. The object is a host anonymous file shared by its descriptors (a `memfd` on Linux, a delete-on-close temporary file mapped as an executable section on Windows), the alias is a duplicate descriptor, and every map maps the whole object, so writes through one view show through the others; a non-null address is a hint, as in `mmap` without `MAP_FIXED`. Not measured on the console: a length that is zero or not a multiple of 16 KiB and protection bits other than CPU read, write and execute are `EINVAL`; an alias or a map asking for more than the descriptor's maximum protection (write and execute counting as read) is `EACCES`, as FreeBSD 11's `vm_map_protect` treats a protection past `max_protection`; a descriptor that is not a JIT object is `EBADF`. The name is ignored, the objects do not count against the flexible memory budget, `mprotect` of a view is not limited to the maximum protection, and `mmap` of the descriptors is unsupported, as for `shm_open`. On Linux the `memfd` is created without `MFD_EXEC`, which Linux 6.3 added and older kernels reject with `EINVAL`, and relies on the protection of the mapping: the flag only sets the file's execute permission bits, and the direct memory `memfd` is mapped executable the same way; a host with `vm.memfd_noexec` set to 2 refuses the creation and the call throws From 3f95553e80131f1cb308b0114eb1011a8f26ff84 Mon Sep 17 00:00:00 2001 From: Ondra97 <47064087+Ondra97@users.noreply.github.com> Date: Fri, 16 Oct 2026 11:35:14 +0200 Subject: [PATCH 0461/1000] fix(libc): ignore noreplace for read-only streams --- core/libs/prx/libc/src/FileAndHeap.cpp | 3 ++- core/libs/tests/GuestFiopen.cpp | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libc/src/FileAndHeap.cpp b/core/libs/prx/libc/src/FileAndHeap.cpp index 997ad6c57c..eb62f98544 100644 --- a/core/libs/prx/libc/src/FileAndHeap.cpp +++ b/core/libs/prx/libc/src/FileAndHeap.cpp @@ -348,7 +348,8 @@ FileStream* APS5_VABI _ZSt7_FiopenPKcNSt5_IosbIiE9_OpenmodeEi_nid_postfix(const {Out | Noreplace, "wx"}, {Out | Trunc | Noreplace, "wx"}, {In | Out | Trunc | Noreplace, "w+x"}, {Out | Binary | Noreplace, "wbx"}, {Out | Trunc | Binary | Noreplace, "wbx"}, {In | Out | Trunc | Binary | Noreplace, "w+bx"}}; - const int open = (mode & (In | Out | App | Trunc | Binary | Noreplace)) | ((mode & Nocreate) ? In : 0) | ((mode & App) ? Out : 0); + const int open = (mode & (In | Out | App | Trunc | Binary)) | ((mode & Nocreate) ? In : 0) | ((mode & App) ? Out : 0) | + (((mode & Noreplace) && (mode & (Out | App))) ? Noreplace : 0); const char* openMode = nullptr; for (const auto& [flags, text] : modes) if (flags == open) openMode = text; if (!openMode) return nullptr; diff --git a/core/libs/tests/GuestFiopen.cpp b/core/libs/tests/GuestFiopen.cpp index 66ab937910..276459ec07 100644 --- a/core/libs/tests/GuestFiopen.cpp +++ b/core/libs/tests/GuestFiopen.cpp @@ -112,6 +112,9 @@ int main() { stream = Open(file, In | Noreplace); Require(stream != nullptr); fclose_nid_postfix(stream); + stream = Open(file, In | Binary | Noreplace); + Require(stream != nullptr); + fclose_nid_postfix(stream); Require(Open(missing, Out | Nocreate) == nullptr); Require(!Exists(missing)); From ca174bc7e630ad11adda871fc7f3cc9cd73cf44f Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Fri, 16 Oct 2026 02:39:51 -0700 Subject: [PATCH 0462/1000] docs(agc): drop the disk cache's format version from ARCHITECTURE.md The line said FormatVersion 36 while the code is at 37, as main's says 33 against 36. Without the number it no longer goes stale on each bump or conflicts between PRs that bump it. --- docs/dev/ARCHITECTURE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/ARCHITECTURE.md b/docs/dev/ARCHITECTURE.md index 4a54b36e5e..8a1a764275 100644 --- a/docs/dev/ARCHITECTURE.md +++ b/docs/dev/ARCHITECTURE.md @@ -119,7 +119,7 @@ At every invocation, `TableSnapshotter` in [`ResourceMaterializer.cpp`](../../co `ResolveImageTables` turns the snapshot into the map buffer (`DescriptorRole::ImageTableMap`, layout in [`ImageTableAbi.hpp`](../../core/shader/recompiler/ImageTableAbi.hpp)): a header, eight words per table (map start, key count, heap base and size, the code for keys past the snapshot, a table fault) and one record per key (flags with the decode mode, the swizzle dword and the element for images; flags and element for samplers). The shader reads `map[key]` and switches over the image's prepared runtime modes. Each distinct T# becomes one element, up to `ImageElementBudget` and the device's capacity. An entry that does not decode, is multisampled or over budget, pairs a reducing sampler with a point-only mode, is an unnormalized or reducing S#, or whose surface has no mapped byte is poisoned: it reads zeros and reports a fault through the BDA fault buffer only when a lane selects it (`APS5_IMAGE_TABLE_STRICT=1` throws at the snapshot instead). The driver's `resolveImageTables` in [`ShaderResources.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) looks up or creates a texture per element and keeps the last four resolved tables per thread for reuse within one collect epoch while their memory is unchanged. Cached objects grow with their tables, so the resource cache keeps at most `ResourceCache::TableElementBudget()` table elements (65,536, `APS5_RESOURCE_CACHE_TABLE_ELEMENTS`) and evicts its least recently used table objects past that, and the dispatch cache evicts its least recently used entries while their captured words exceed `Driver::DispatchCacheBytes()` (256 MiB, `APS5_DISPATCH_CACHE_BYTES`). -The disk cache (`FormatVersion` 36) stores the table and table operation of each image and sampler, the image shapes of the table binding, the invocation's table poison list, read ranges and fault count, and the table column of each descriptor source in the front-end plan, with its key domains and its selection. +The disk cache stores the table and table operation of each image and sampler, the image shapes of the table binding, the invocation's table poison list, read ranges and fault count, and the table column of each descriptor source in the front-end plan, with its key domains and its selection. ### Untracked storage image comparisons From 90b191e5bcaee28464a155ccd0453bd77e179fde Mon Sep 17 00:00:00 2001 From: Romans Pokrovskis Date: Fri, 16 Oct 2026 12:36:33 +0300 Subject: [PATCH 0463/1000] test(agc): cover pending surface identity in the shared wait --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 10 +++++ .../tests/execution/StoreOverUntracked.cpp | 38 ++++++++++++++++++- 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 251d38d6a1..8b7882fe14 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1400,6 +1400,16 @@ target_include_directories(agc_driver_alpha_to_coverage_tests PRIVATE $) +if(BUILD_TESTING) + add_test(NAME agc_driver_store_pending_identity COMMAND agc_driver_store_over_untracked_tests --pending-identity) + set_tests_properties(agc_driver_store_pending_identity PROPERTIES + ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + if(WIN32) + set_tests_properties(agc_driver_store_pending_identity PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() + if(BUILD_TESTING) add_test(NAME agc_driver_undithered_alpha_to_coverage_state COMMAND agc_driver_graphics_tests --undithered-alpha-to-coverage-state) set_tests_properties(agc_driver_undithered_alpha_to_coverage_state PROPERTIES diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/StoreOverUntracked.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/StoreOverUntracked.cpp index d98349ac9e..1286eb40b1 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/StoreOverUntracked.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/StoreOverUntracked.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include #include #include @@ -236,6 +237,34 @@ void Settle(THold& hold, const Surface& surface, std::uint32_t result) { hold.unlock(); } +void PendingIdentity() { + Surface pending; + Surface completed; + std::shared_ptr image; + struct WaitForWork { + ~WaitForWork() { AgcDriverWaitIdle_nid_postfix(); } + } waitForWork; + Store(pending.Texels()); + AgcDriverWaitIdle_nid_postfix(); + std::unique_lock hold(AgcDriver::GuestMemory::GpuMutex()); + image = StorageTexture::FindLive(pending.Address(), SurfaceBytes); + Require(image != nullptr, "store over untracked: the identity test has no live image"); + StorageTexture::FlushAllPending("pending identity fixture"); + image->MarkDirty(); + Require(StorageTexture::FindPending(pending.Address(), SurfaceBytes) == image, "store over untracked: the identity test did not make its image pending"); + Require(StorageTexture::FindLive(completed.Address(), SurfaceBytes) == nullptr && StorageTexture::FindPending(completed.Address(), SurfaceBytes) == nullptr, "store over untracked: the completed surface unexpectedly has an image"); + hold.unlock(); + Require(HoldPending(hold, pending, Stored), "store over untracked: the wait ignored its own pending image"); + Require(hold.owns_lock(), "store over untracked: the pending image was not held under the GPU mutex"); + std::fill(completed.Texels(), completed.Texels() + SurfaceBytes / 4u, Stored); + hold.unlock(); + const bool held = HoldPending(hold, completed, Stored); + if (!hold.owns_lock()) hold.lock(); + StorageTexture::FlushAllPending("pending identity fixture cleanup"); + hold.unlock(); + Require(!held, "store over untracked: the wait accepted another surface's pending image"); +} + enum class Kind { ReadAfter, ReadBefore, Filled, Copied, LeftWatch, WrittenThenLeftWatch, ImportLeftWatch, NeighbourCopied, NeighbourLeftWatch, UntrackedRepeat }; // One 64 KiB block of the surface: the image of its last block, and the source copied into it. @@ -445,8 +474,9 @@ Outcome Run(const Case& test) { } -int main() { +int main(int argc, char** argv) { try { + Require(argc == 1 || (argc == 2 && std::string_view(argv[1]) == "--pending-identity"), "store over untracked: unknown test argument"); { const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; @@ -454,6 +484,12 @@ int main() { struct ShutdownGuard { ~ShutdownGuard() { try { AgcDriverShutdown_nid_postfix(); } catch (...) {} } } shutdown; + if (argc == 2) { + PendingIdentity(); + AgcDriverShutdown_nid_postfix(); + std::puts("pending surface identity tests passed"); + return 0; + } { Surface probe; Require(AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::DecodeTextureResource(TextureDescriptor(probe.Texels()))).guestBytes == SurfaceBytes, "store over untracked: unexpected surface size"); From f7e767add90d0c9884c5733fc84f6fb56d15e756 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Fri, 16 Oct 2026 12:14:07 +0200 Subject: [PATCH 0464/1000] ci(linux): size build and test parallelism to what the shared host has free The self-hosted runners share one 32-thread host and were fixed at one job each, so a build with many ccache misses compiled them one after another (686 misses took 37 min). Pick jobs from the free CPUs and available memory (1 to 8, ctest at most 4) and pass a load limit to ninja so a busy host backs off. Co-Authored-By: Claude --- .github/workflows/build.yml | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 328669a882..ee1d792061 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -300,8 +300,6 @@ jobs: CCACHE_COMPILERCHECK: content MESA_SHADER_CACHE_DIR: /var/cache/anyps5/mesa MESA_SHADER_CACHE_MAX_SIZE: 4G - CMAKE_BUILD_PARALLEL_LEVEL: 1 - CTEST_PARALLEL_LEVEL: 1 steps: - uses: actions/checkout@v7 with: @@ -335,11 +333,23 @@ jobs: ccache --show-config dpkg-query -W mesa-vulkan-drivers grep -m1 '^model name' /proc/cpuinfo + - name: Size parallelism to what the shared host has free + run: | + cpus=$(nproc) + busy=$(awk '{ printf "%d", $1 + 0.5 }' /proc/loadavg) + mem_gb=$(awk '/^MemAvailable:/ { print int($2 / 1048576) }' /proc/meminfo) + jobs=$(( cpus - busy )) + jobs=$(( jobs < mem_gb / 3 ? jobs : mem_gb / 3 )) + jobs=$(( jobs > 8 ? 8 : jobs )) + jobs=$(( jobs < 1 ? 1 : jobs )) + echo "cpus=$cpus load=$busy mem_available_gb=$mem_gb -> build jobs $jobs" + echo "CMAKE_BUILD_PARALLEL_LEVEL=$jobs" >> "$GITHUB_ENV" + echo "CTEST_PARALLEL_LEVEL=$(( jobs > 4 ? 4 : jobs ))" >> "$GITHUB_ENV" - run: cmake -S . -B build $CONFIGURE -DANYPS5_ENABLE_SPIRV_TOOLS=ON - name: Check that every test executable is registered with ctest run: python3 tools/check_test_registration.py build - - run: cmake --build build - - run: cmake --build build --target libs + - run: cmake --build build -- -l "$(nproc)" + - run: cmake --build build --target libs -- -l "$(nproc)" - run: ctest --test-dir build --output-on-failure --timeout 120 --output-log ctest.log --output-junit ctest.xml env: ANYPS5_REQUIRE_VULKAN: 1 From 4aafda8e3b05cca9bebdab0ab647eed4b751ab4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Wed, 14 Oct 2026 16:31:36 +0200 Subject: [PATCH 0465/1000] perf(agc): journal the pipelines of a run and build them ahead on the next one A graphics pipeline the store builds or a compute pipeline a dispatch builds is appended to pipelines.journal next to the shader cache entries, as the materialized result of each stage (the specialized SPIR-V, bindings and push constants) plus the recipe the pipeline was built from: the stages' identities, vertex input layout, descriptor set layout keys and the state fields the Pipeline constructor reads. When a VulkanDevice is created with a window (a headless device waits for its first draw or dispatch, since the window's device usually replaces it first) the journal is loaded and its pipelines are built on a pool of threads while the title loads, so the first draw or dispatch of a configuration seen on an earlier run finds its pipeline resident instead of compiling it on the queue worker. The pipeline stores key on RecompileResult::Identity, a hash of the module words, specialization constants, subgroup width and bindings that is the same on every run (SharedSpirv::ContentHash, computed once per shared module), rather than on the process-lifetime variant ids; a generated stage is keyed like any other. CachedPipeline builds outside the store's mutex and the library parts and link are built outside the library store's mutex, so replay threads and queue workers build in parallel; a concurrent build of the same key keeps the first inserted. A replayed pipeline enters the store at its least recently used end and a full store takes none, and its optimized relink waits for the first draw that binds it, so the title's own pipelines come first in both. The compute pipeline creation moves into VulkanDevice::createComputePipeline, shared by dispatches and the replay. The store key also covers the color component mapping, the depth target's sample count and a depth bias clamp, which the constructor reads. The file names the device it was written for (the properties the driver's pipeline cache file is named by) and starts over for another; a torn tail is cut at a record boundary; two runs sharing the directory take turns on a lock file and keep each other's records; a read failure never starts the file over. A device cancels its replay before it is replaced or destroyed, and a replaced device destroys its pipelines then rather than at the driver's stop. APS5_NO_PIPELINE_JOURNAL=1 disables it; APS5_PIPELINE_JOURNAL_THREADS, APS5_PIPELINE_JOURNAL_ENTRIES (the first recorded), APS5_PIPELINE_STORE_ENTRIES and APS5_PIPELINE_JOURNAL_MAX_MIB bound it; APS5_PROFILE_DRAW prints a [pipeline-journal] summary. APS5_VERIFY_SET_LAYOUTS=1 now also checks a dispatch's set layout against the one described from its shader. The lavapipe test suite, which shares one cache directory, runs with the journal off except for the journal's own tests. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01P6hw3kRAuQdfcU9YCZgaoK --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 14 + .../Execution/include/VulkanDevice.hpp | 14 + .../Execution/src/Driver/Lifecycle.cpp | 2 + .../Execution/src/VulkanDevice.cpp | 223 +++- .../Graphics/include/Pipeline.hpp | 29 +- .../Graphics/include/PipelineJournal.hpp | 104 ++ .../Graphics/include/PipelineLibrary.hpp | 7 + .../libSceAgcDriver/Graphics/src/Pipeline.cpp | 161 ++- .../Graphics/src/PipelineJournal.cpp | 1096 +++++++++++++++++ .../Graphics/src/PipelineLibrary.cpp | 29 +- .../prx/libSceAgcDriver/tests/Graphics.cpp | 257 +++- .../prx/libSceAgcDriver/tests/Recorder.cpp | 13 +- .../tests/execution/PipelineJournal.cpp | 431 +++++++ core/shader/recompiler/CMakeLists.txt | 6 + core/shader/recompiler/Recompiler.hpp | 109 +- core/shader/recompiler/ShaderDiskCache.cpp | 8 +- core/shader/recompiler/ShaderDiskCache.hpp | 2 +- docs/dev/ARCHITECTURE.md | 1 + docs/dev/debt/libSceAgcDriver.md | 1 + docs/user/USAGE.md | 4 + 20 files changed, 2367 insertions(+), 144 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/Graphics/include/PipelineJournal.hpp create mode 100644 core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/PipelineJournal.cpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index ebd508377d..fdaffbc3f8 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -206,6 +206,7 @@ set(agcDriverSources ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/Pipeline.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/PipelineLibrary.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/PipelineCache.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/PipelineJournal.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/Resources.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/PlaceholderImages.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/Recorder.cpp @@ -1463,6 +1464,8 @@ target_include_directories(agc_driver_provoking_vertex_tests PRIVATE ${agcTestSh target_include_directories(agc_driver_prepared_exports_tests PRIVATE $) +target_include_directories(agc_driver_pipeline_journal_tests PRIVATE $) + target_include_directories(agc_driver_buffer_descriptor_runtime_tests PRIVATE $) target_include_directories(agc_driver_prepared_publication_tests PRIVATE $) @@ -1561,6 +1564,11 @@ if(BUILD_TESTING) set_tests_properties(agc_driver_native_depth_clears agc_driver_native_depth_clear_refusals PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() set_tests_properties(agc_sparse_buffer_holes PROPERTIES TIMEOUT 30) + add_test(NAME agc_pipeline_journal COMMAND agc_driver_graphics_tests --pipeline-journal) + set_tests_properties(agc_pipeline_journal PROPERTIES TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + if(WIN32) + set_tests_properties(agc_pipeline_journal PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() set_tests_properties(agc_driver_sparse_buffer_holes PROPERTIES TIMEOUT 30) if(WIN32) set_tests_properties(agc_sparse_buffer_holes PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") @@ -2387,3 +2395,9 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() endif() + +if(BUILD_TESTING) + get_directory_property(agcRegisteredTests TESTS) + list(FILTER agcRegisteredTests EXCLUDE REGEX "^(agc_pipeline_journal|agc_driver_pipeline_journal)$") + set_property(TEST ${agcRegisteredTests} APPEND PROPERTY ENVIRONMENT "APS5_NO_PIPELINE_JOURNAL=1") +endif() diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp index 87800734a3..08de3994f0 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp @@ -30,6 +30,8 @@ struct ComputePipelineObjects; struct RecipeHit; // The record tail shared by an ordinary dispatch and a recipe hit (VulkanDevice::recordDispatch). struct RecordedDispatch; +// The module, layout and pipeline a compute dispatch binds (VulkanDevice::createComputePipeline). +struct ComputePipelineObjects; class VulkanDevice { public: @@ -37,6 +39,14 @@ public: struct CompileOnly {}; explicit VulkanDevice(CompileOnly); ComputePipelineReplayResult ReplayComputePipeline(const ComputePipelineCapture& capture, std::span initialCache = {}); + // Builds the pipeline of a journaled compute result ahead of its first dispatch (the journal + // replay's hook, on its threads): false when the device already holds a pipeline for the module. + bool ReplayComputePipeline(const ShaderRecompiler::RecompileResult& shader, bool pushConstants); + // Starts the journal replay if the device has not (a headless device starts it at its first + // draw or dispatch) and joins it (tests; the teardown joins it anyway). + void WaitForPipelineJournalReplay(); + // Compute pipelines this device built, by dispatches and by the journal replay (tests). + std::uint64_t ComputePipelinesCreated() const; ~VulkanDevice(); VulkanDevice(const VulkanDevice&) = delete; VulkanDevice& operator=(const VulkanDevice&) = delete; @@ -58,6 +68,7 @@ public: // three-step form below so the GPU wait happens without the mutex. void WaitIdle(); void PrepareForReplacement(); + void startJournalReplay(); bool ImportGuestMemory(const GuestAllocations::Mapped& ranges, std::uint64_t generation, bool adoptDevice); // Sends recorded work to the GPU without waiting for it. With `reapFirst` it first retires batches // that already finished, so the in-flight list stays short (APS5_NO_OPPORTUNISTIC_REAP=1 skips @@ -316,6 +327,9 @@ private: // The device's Graphics::Context: a copy of the one built at setup (its instance functions // resolved then, its function table filled then), or with APS5_NO_CONTEXT_CACHE=1 built anew. Graphics::Context graphicsContext() const; + VkPipelineCreateFlags computePipelineFlags() const; + std::uint32_t computeSubgroupSize(const ShaderRecompiler::RecompileResult& shader) const; + std::shared_ptr createComputePipeline(const Graphics::Context& context, const ShaderRecompiler::RecompileResult& shader, VkDescriptorSetLayout setLayout, std::span layoutKey, VkShaderStageFlags pushStages, std::uint64_t programAddress); ShaderRecompiler::SpirvTarget buildTarget() const; Graphics::Context buildContext() const; // Body of Dispatch and DispatchIndirect: `arguments` 0 dispatches x, y, z groups. diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp index 6dc5164943..7aa349ce2d 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp @@ -1,4 +1,5 @@ #include "prx/libSceAgcDriver/Execution/include/AsyncLog.hpp" +#include "prx/libSceAgcDriver/Graphics/include/PipelineJournal.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp" @@ -119,6 +120,7 @@ void Driver::stop() { std::lock_guard gpuLock(GuestMemory::GpuMutex()); device.Reset(); replacedDevices.clear(); + Graphics::FlushPipelineJournal(); Graphics::ShutdownGuestBufferWorkers(); PerformanceTrace::StopOutput(); FlushAsyncLog_nid_no_patch(); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 694152052f..661b90c564 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -6,6 +6,7 @@ #if ANYPS5_ENABLE_SPIRV_TOOLS #include "SpirvBackend/SpirvOptimizer.hpp" #endif +#include "prx/libSceAgcDriver/Graphics/include/PipelineJournal.hpp" #include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver.hpp" #include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp" @@ -215,6 +216,7 @@ struct ComputePipelineObjects { VkShaderModule module = VK_NULL_HANDLE; VkPipelineLayout layout = VK_NULL_HANDLE; VkPipeline pipeline = VK_NULL_HANDLE; + VkDescriptorSetLayout resourcesLayout = VK_NULL_HANDLE; ~ComputePipelineObjects() { if (pipeline != VK_NULL_HANDLE) destroyPipeline(device, pipeline, nullptr); if (layout != VK_NULL_HANDLE) destroyLayout(device, layout, nullptr); @@ -222,6 +224,16 @@ struct ComputePipelineObjects { } }; +// Pipelines are shared by dispatches of one module (the result's run-stable identity) with the +// same push constant use; descriptor set layouts built from the same bindings are compatible, so +// the pipeline layout of the first dispatch, or of the journal replay, serves them all. +struct ComputePipelineKey { + ShaderRecompiler::PipelineIdentity identity; + bool pushConstants = false; + auto operator<=>(const ComputePipelineKey&) const = default; + static ComputePipelineKey Of(const ShaderRecompiler::RecompileResult& shader, bool pushConstants) { return {shader.Identity(), pushConstants}; } +}; + constexpr std::uint64_t WorkgroupMemoryGranule = 1u << 20u; struct WorkgroupMemory { @@ -362,8 +374,12 @@ struct VulkanDevice::State { // dispatch's find-or-insert and the verify switch's lookups touch the map, recipes hold weak // references to its objects. std::mutex computePipelinesMutex; - std::map> computePipelines; - std::map>> computePipelinesBuilding; + std::map> computePipelines; + std::map>> computePipelinesBuilding; + // The journal replay building this device's pipelines on its own threads; drained first at teardown. + std::shared_ptr journalReplay; + std::atomic journalReplayPending{false}; + std::atomic computePipelinesCreated{0}; std::unique_ptr colorTransfer; std::shared_ptr bufferPool; std::unique_ptr emptyBuffer; @@ -671,6 +687,12 @@ struct VulkanDevice::State { } ~State() { + if (journalReplay != nullptr) { + journalReplay->Cancel(); + journalReplay->Wait(); + journalReplay.reset(); + Graphics::ReportPipelineJournal(); + } contextReady = false; if (deviceLostHook && Graphics::DeviceLostOwner() == this) { Graphics::DeviceLostHook() = nullptr; @@ -1928,10 +1950,53 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : state->scaler = std::make_unique(graphicsContext(), VK_FORMAT_B8G8R8A8_UNORM, state->surfaceFormat); state->presentation = std::make_unique(graphicsContext(), state->presentSlots.size()); } + // A device without a window is usually replaced by the window's device before the title draws + // (Driver::AttachWindow): its replay starts at its first draw or dispatch instead, so the + // journal is replayed once, on the device the title draws with. + state->journalReplayPending.store(!compileOnly, std::memory_order_release); + if (window != nullptr) startJournalReplay(); +} + +void VulkanDevice::startJournalReplay() { + if (!state->journalReplayPending.exchange(false, std::memory_order_acq_rel)) return; + state->journalReplay = Graphics::StartPipelineJournalReplay(graphicsContext(), Graphics::PipelineJournalDeviceFingerprint(state->properties), {[this](const ShaderRecompiler::RecompileResult& shader, bool pushConstants) { return ReplayComputePipeline(shader, pushConstants); }}); } VulkanDevice::~VulkanDevice() = default; +VkPipelineCreateFlags VulkanDevice::computePipelineFlags() const { + return (graphicsContext().pipelineExecutableInfo ? VK_PIPELINE_CREATE_CAPTURE_STATISTICS_BIT_KHR : 0) | VK_PIPELINE_CREATE_DISPATCH_BASE_BIT; +} + +std::uint32_t VulkanDevice::computeSubgroupSize(const ShaderRecompiler::RecompileResult& shader) const { + return state->computeWave32 && (shader.hostSubgroupSize == 32u || shader.hostSubgroupSize > state->maxComputeSubgroupSize) ? std::min(shader.hostSubgroupSize, state->maxComputeSubgroupSize) : 0u; +} + +void VulkanDevice::WaitForPipelineJournalReplay() { + startJournalReplay(); + if (state->journalReplay != nullptr) state->journalReplay->Wait(); +} + +std::uint64_t VulkanDevice::ComputePipelinesCreated() const { + return state->computePipelinesCreated.load(std::memory_order_relaxed); +} + +bool VulkanDevice::ReplayComputePipeline(const ShaderRecompiler::RecompileResult& shader, bool pushConstants) { + ShaderRecompiler::RuntimeAbi::RequireVersion(shader.runtimeAbiVersion); + const auto key = ComputePipelineKey::Of(shader, pushConstants); + if (key.identity.Empty()) return false; + { + std::lock_guard pipelines(state->computePipelinesMutex); + if (state->computePipelines.contains(key) || state->computePipelinesBuilding.contains(key)) return false; + } + const auto context = graphicsContext(); + const std::array shaders{{{ShaderRecompiler::ShaderStage::Compute, &shader, 0}}}; + const auto layouts = Graphics::DescribeSetLayouts(context, shaders); + auto objects = createComputePipeline(context, shader, layouts.layouts.main, layouts.mainKey, pushConstants ? VK_SHADER_STAGE_COMPUTE_BIT : 0u, 0); + std::lock_guard pipelines(state->computePipelinesMutex); + return state->computePipelines.try_emplace(key, std::move(objects)).second; +} + bool VulkanDevice::ImportGuestMemory(const GuestAllocations::Mapped& ranges, std::uint64_t generation, bool adoptDevice) { if (adoptDevice) GuestMemory::AssertGpuLockHeld("VulkanDevice::ImportGuestMemory"); return Graphics::ImportMappedRanges(graphicsContext(), ranges, generation, adoptDevice); @@ -1940,10 +2005,21 @@ bool VulkanDevice::ImportGuestMemory(const GuestAllocations::Mapped& ranges, std void VulkanDevice::PrepareForReplacement() { GuestMemory::AssertGpuLockHeld("VulkanDevice::PrepareForReplacement"); require(state->recorder != nullptr && Graphics::Recorder::Active() == state->recorder.get(), "device replacement requires its active recorder"); + state->journalReplayPending.store(false, std::memory_order_release); + if (state->journalReplay != nullptr) { + state->journalReplay->Cancel(); + state->journalReplay->Wait(); + state->journalReplay.reset(); + } Graphics::FlushCachedTextures(state->device); Graphics::PublishAllShadows(state->context, Graphics::PublishReason::Teardown); WaitIdle(); Graphics::DestroyShadows(state->device); + // The replacement's draws never find this device's pipelines (the store keys on the device), and + // the device lives on until the driver stops: destroy them now rather than then. + Graphics::ClearCachedPipelines(state->device); + std::lock_guard pipelines(state->computePipelinesMutex); + state->computePipelines.clear(); } void VulkanDevice::WaitIdle() { @@ -3558,6 +3634,7 @@ bool VulkanDevice::PrebuildGraphicsLibraries(const Graphics::ShaderStages& stage } void VulkanDevice::Draw(const Graphics::State& graphics, const Pm4::DrawParameters& draw, std::span shaders, std::span snapshots, std::shared_ptr* recipe) { + startJournalReplay(); for (const auto& shader : shaders) { require(shader.program != nullptr, "missing compiled shader"); ShaderRecompiler::RuntimeAbi::RequireVersion(shader.program->runtimeAbiVersion); @@ -4417,11 +4494,74 @@ void VulkanDevice::recordDispatch(RecordedDispatch& record) { recordStep(PhaseRecordCompletion); } -std::shared_ptr VulkanDevice::computePipeline(const Graphics::Context& context, const ShaderRecompiler::RecompileResult& shader, VkDescriptorSetLayout setLayout, std::span layoutKey, VkShaderStageFlags pushStages, std::uint64_t programAddress) { +// The module, pipeline layout and pipeline of a compute result over `setLayout` (the set layout +// its bindings describe): what a dispatch or its preparation builds at the first use of a module, +// and what the journal replay builds on its own threads before that use. +std::shared_ptr VulkanDevice::createComputePipeline(const Graphics::Context& context, const ShaderRecompiler::RecompileResult& shader, VkDescriptorSetLayout setLayout, std::span layoutKey, VkShaderStageFlags pushStages, std::uint64_t programAddress) { static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; - const std::uint64_t pipelineKey = shader.PipelineVariantId() != 0 ? (shader.PipelineVariantId() << 1u) | (pushStages != 0 ? 1u : 0u) : 0u; + const auto start = std::chrono::steady_clock::now(); + auto objects = std::make_shared(); + objects->device = state->device; + objects->destroyModule = state->DeviceFunction("vkDestroyShaderModule"); + objects->destroyLayout = state->DeviceFunction("vkDestroyPipelineLayout"); + objects->destroyPipeline = state->DeviceFunction("vkDestroyPipeline"); + VkShaderModuleCreateInfo moduleInfo{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; + moduleInfo.codeSize = shader.spirv.size() * sizeof(std::uint32_t); + moduleInfo.pCode = shader.spirv.data(); + check(state->DeviceFunction("vkCreateShaderModule")(state->device, &moduleInfo, nullptr, &objects->module), "vkCreateShaderModule"); + objects->resourcesLayout = setLayout; + const std::array setLayouts{setLayout, shader.workgroupMemoryDwords != 0 ? state->workgroupMemoryLayout : VK_NULL_HANDLE}; + const VkPushConstantRange push{VK_SHADER_STAGE_COMPUTE_BIT, 0, Graphics::ComputePushConstantBytes}; + VkPipelineLayoutCreateInfo layoutInfo{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO}; + layoutInfo.setLayoutCount = shader.workgroupMemoryDwords != 0 ? ShaderRecompiler::WorkgroupMemoryDescriptorSet + 1 : 1; + layoutInfo.pSetLayouts = setLayouts.data(); + layoutInfo.pushConstantRangeCount = pushStages != 0 ? 1 : 0; + layoutInfo.pPushConstantRanges = pushStages != 0 ? &push : nullptr; + check(state->DeviceFunction("vkCreatePipelineLayout")(state->device, &layoutInfo, nullptr, &objects->layout), "vkCreatePipelineLayout"); + const VkPipelineCreateFlags pipelineFlags = computePipelineFlags(); + const Graphics::PipelineSpecialization specialization(shader); + const auto subgroupSize = computeSubgroupSize(shader); + if (const char* directory = std::getenv("APS5_DUMP_COMPUTE_PIPELINES"); directory != nullptr && *directory != '\0' && CaptureComputePipeline(shader.spirv.size())) { + ComputePipelineCapture capture; + capture.spirv = shader.spirv.Copy(); + ComputeDescriptorSetLayout resourcesLayout; + const bool updateAfterBind = layoutKey.size() % 4u == 1u; + resourcesLayout.flags = updateAfterBind ? VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT : 0; + for (std::size_t index = 0; index + 4u <= layoutKey.size(); index += 4u) { + const auto type = static_cast(layoutKey[index + 1u]); + resourcesLayout.bindings.push_back({layoutKey[index], type, layoutKey[index + 2u], layoutKey[index + 3u], updateAfterBind && type == VK_DESCRIPTOR_TYPE_STORAGE_BUFFER ? VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT : 0u}); + } + capture.sets.push_back(std::move(resourcesLayout)); + if (shader.workgroupMemoryDwords != 0) capture.sets.push_back({0, {{0, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, VK_SHADER_STAGE_COMPUTE_BIT, 0}}}); + if (pushStages != 0) capture.pushConstants.push_back(push); + for (const auto& value : shader.specialization) capture.specialization.push_back({value.id, value.value}); + capture.requiredSubgroupSize = subgroupSize; + capture.flags = pipelineFlags; + capture.deviceName = state->properties.deviceName; + capture.vendorId = state->properties.vendorID; + capture.deviceId = state->properties.deviceID; + capture.driverVersion = state->properties.driverVersion; + capture.apiVersion = state->properties.apiVersion; + std::copy_n(state->properties.pipelineCacheUUID, VK_UUID_SIZE, capture.pipelineCacheUuid.begin()); + const auto path = capture.Write(directory); + std::fprintf(stderr, "[compute-capture] %s\n", path.string().c_str()); + } + if (profile && shader.spirv.size() > 100000) std::fprintf(stderr, "[dispatch] creating a pipeline for %zu SPIR-V words (program 0x%llx)\n", shader.spirv.size(), static_cast(programAddress)); + CreateComputePipeline(state->device, state->DeviceFunction("vkCreateComputePipelines"), context.pipelineCache, objects->module, objects->layout, pipelineFlags, 0, specialization.Info(), subgroupSize, &objects->pipeline, context.pipelineCacheSynchronization); + objects->destroyModule(objects->device, objects->module, nullptr); + objects->module = VK_NULL_HANDLE; + computePipelinesCreated.fetch_add(1, std::memory_order_relaxed); + state->computePipelinesCreated.fetch_add(1, std::memory_order_relaxed); + if (GuestMemory::GpuMutex().HeldByThisThread()) computePipelinesCreatedLocked.fetch_add(1, std::memory_order_relaxed); + Graphics::LogPipelineStatistics_nid_no_patch(context, objects->pipeline); + if (profile && shader.spirv.size() > 100000) std::fprintf(stderr, "[dispatch] created a pipeline for %zu SPIR-V words in %.0f ms (program 0x%llx)\n", shader.spirv.size(), std::chrono::duration(std::chrono::steady_clock::now() - start).count(), static_cast(programAddress)); + return objects; +} + +std::shared_ptr VulkanDevice::computePipeline(const Graphics::Context& context, const ShaderRecompiler::RecompileResult& shader, VkDescriptorSetLayout setLayout, std::span layoutKey, VkShaderStageFlags pushStages, std::uint64_t programAddress) { + const auto pipelineKey = ComputePipelineKey::Of(shader, pushStages != 0); std::promise> building; - if (pipelineKey != 0) { + if (!pipelineKey.identity.Empty()) { std::shared_future> other; { std::lock_guard pipelines(state->computePipelinesMutex); @@ -4432,7 +4572,7 @@ std::shared_ptr VulkanDevice::computePipeline(const Grap if (other.valid()) return other.get(); } const auto finish = [&](std::shared_ptr built, std::exception_ptr error) { - if (pipelineKey == 0) return built; + if (pipelineKey.identity.Empty()) return built; { std::lock_guard pipelines(state->computePipelinesMutex); if (built != nullptr) built = state->computePipelines.try_emplace(pipelineKey, built).first->second; @@ -4444,60 +4584,8 @@ std::shared_ptr VulkanDevice::computePipeline(const Grap }; std::shared_ptr objects; try { - const auto start = std::chrono::steady_clock::now(); - objects = std::make_shared(); - objects->device = state->device; - objects->destroyModule = state->DeviceFunction("vkDestroyShaderModule"); - objects->destroyLayout = state->DeviceFunction("vkDestroyPipelineLayout"); - objects->destroyPipeline = state->DeviceFunction("vkDestroyPipeline"); - VkShaderModuleCreateInfo moduleInfo{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; - moduleInfo.codeSize = shader.spirv.size() * sizeof(std::uint32_t); - moduleInfo.pCode = shader.spirv.data(); - check(state->DeviceFunction("vkCreateShaderModule")(state->device, &moduleInfo, nullptr, &objects->module), "vkCreateShaderModule"); - const std::array setLayouts{setLayout, shader.workgroupMemoryDwords != 0 ? state->workgroupMemoryLayout : VK_NULL_HANDLE}; - const VkPushConstantRange push{VK_SHADER_STAGE_COMPUTE_BIT, 0, Graphics::ComputePushConstantBytes}; - VkPipelineLayoutCreateInfo layoutInfo{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO}; - layoutInfo.setLayoutCount = shader.workgroupMemoryDwords != 0 ? ShaderRecompiler::WorkgroupMemoryDescriptorSet + 1 : 1; - layoutInfo.pSetLayouts = setLayouts.data(); - layoutInfo.pushConstantRangeCount = pushStages != 0 ? 1 : 0; - layoutInfo.pPushConstantRanges = pushStages != 0 ? &push : nullptr; - check(state->DeviceFunction("vkCreatePipelineLayout")(state->device, &layoutInfo, nullptr, &objects->layout), "vkCreatePipelineLayout"); - const VkPipelineCreateFlags pipelineFlags = (context.pipelineExecutableInfo ? VK_PIPELINE_CREATE_CAPTURE_STATISTICS_BIT_KHR : 0) | VK_PIPELINE_CREATE_DISPATCH_BASE_BIT; - const Graphics::PipelineSpecialization specialization(shader); - const auto subgroupSize = state->computeWave32 && (shader.hostSubgroupSize == 32u || shader.hostSubgroupSize > state->maxComputeSubgroupSize) ? std::min(shader.hostSubgroupSize, state->maxComputeSubgroupSize) : 0u; - if (const char* directory = std::getenv("APS5_DUMP_COMPUTE_PIPELINES"); directory != nullptr && *directory != '\0' && CaptureComputePipeline(shader.spirv.size())) { - ComputePipelineCapture capture; - capture.spirv = shader.spirv.Copy(); - ComputeDescriptorSetLayout resourcesLayout; - const bool updateAfterBind = layoutKey.size() % 4u == 1u; - resourcesLayout.flags = updateAfterBind ? VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT : 0; - for (std::size_t index = 0; index + 4u <= layoutKey.size(); index += 4u) { - const auto type = static_cast(layoutKey[index + 1u]); - resourcesLayout.bindings.push_back({layoutKey[index], type, layoutKey[index + 2u], layoutKey[index + 3u], updateAfterBind && type == VK_DESCRIPTOR_TYPE_STORAGE_BUFFER ? VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT : 0u}); - } - capture.sets.push_back(std::move(resourcesLayout)); - if (shader.workgroupMemoryDwords != 0) capture.sets.push_back({0, {{0, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, VK_SHADER_STAGE_COMPUTE_BIT, 0}}}); - if (pushStages != 0) capture.pushConstants.push_back(push); - for (const auto& value : shader.specialization) capture.specialization.push_back({value.id, value.value}); - capture.requiredSubgroupSize = subgroupSize; - capture.flags = pipelineFlags; - capture.deviceName = state->properties.deviceName; - capture.vendorId = state->properties.vendorID; - capture.deviceId = state->properties.deviceID; - capture.driverVersion = state->properties.driverVersion; - capture.apiVersion = state->properties.apiVersion; - std::copy_n(state->properties.pipelineCacheUUID, VK_UUID_SIZE, capture.pipelineCacheUuid.begin()); - const auto path = capture.Write(directory); - std::fprintf(stderr, "[compute-capture] %s\n", path.string().c_str()); - } - if (profile && shader.spirv.size() > 100000) std::fprintf(stderr, "[dispatch] creating a pipeline for %zu SPIR-V words (program 0x%llx)\n", shader.spirv.size(), static_cast(programAddress)); - CreateComputePipeline(state->device, state->DeviceFunction("vkCreateComputePipelines"), context.pipelineCache, objects->module, objects->layout, pipelineFlags, 0, specialization.Info(), subgroupSize, &objects->pipeline, context.pipelineCacheSynchronization); - objects->destroyModule(objects->device, objects->module, nullptr); - objects->module = VK_NULL_HANDLE; - computePipelinesCreated.fetch_add(1, std::memory_order_relaxed); - if (GuestMemory::GpuMutex().HeldByThisThread()) computePipelinesCreatedLocked.fetch_add(1, std::memory_order_relaxed); - Graphics::LogPipelineStatistics_nid_no_patch(context, objects->pipeline); - if (profile && shader.spirv.size() > 100000) std::fprintf(stderr, "[dispatch] created a pipeline for %zu SPIR-V words in %.0f ms (program 0x%llx)\n", shader.spirv.size(), std::chrono::duration(std::chrono::steady_clock::now() - start).count(), static_cast(programAddress)); + objects = createComputePipeline(context, shader, setLayout, layoutKey, pushStages, programAddress); + if (Graphics::PipelineJournalEnabled()) Graphics::RecordComputePipeline(shader, pushStages != 0); } catch (...) { finish(nullptr, std::current_exception()); throw; @@ -4506,6 +4594,7 @@ std::shared_ptr VulkanDevice::computePipeline(const Grap } VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::RecompileResult& shader, std::uint32_t x, std::uint32_t y, std::uint32_t z, std::uint64_t arguments, std::span snapshots, std::uint64_t programAddress, std::shared_ptr prepared, std::shared_ptr* recipeOut) { + startJournalReplay(); state->recorder->BoundKeptBytes(); ShaderRecompiler::RuntimeAbi::RequireVersion(shader.runtimeAbiVersion); PerformanceTimer timing("Vulkan.Dispatch"); @@ -4525,11 +4614,9 @@ VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::Rec } auto pushBytes = Graphics::AssemblePushConstants(shaders); const auto context = graphicsContext(); - // Pipelines are shared by dispatches of one compiled variant; descriptor set layouts built from the - // same bindings are compatible, so the pipeline layout of the first dispatch serves them all. - const std::uint64_t pipelineKey = shader.PipelineVariantId() != 0 ? (shader.PipelineVariantId() << 1u) | (pushStages != 0 ? 1u : 0u) : 0u; + const auto pipelineKey = ComputePipelineKey::Of(shader, pushStages != 0); std::shared_ptr objects = prepared != nullptr ? prepared->objects : nullptr; - if (objects == nullptr && pipelineKey != 0) { + if (objects == nullptr && !pipelineKey.identity.Empty()) { std::lock_guard pipelines(state->computePipelinesMutex); if (const auto found = state->computePipelines.find(pipelineKey); found != state->computePipelines.end()) objects = found->second; } @@ -4672,7 +4759,13 @@ VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::Rec } timing.Mark("shader_resources"); timer.phase(PhaseResources); + static const bool verifySetLayouts = std::getenv("APS5_VERIFY_SET_LAYOUTS") != nullptr; + if (verifySetLayouts && objects != nullptr) require(objects->resourcesLayout == resources->Layout(), "the resident compute pipeline's set layout differs from the dispatch's"); if (objects == nullptr) { + if (verifySetLayouts && context.descriptorCache != nullptr) { + const auto described = Graphics::DescribeSetLayouts(context, shaders); + require(described.layouts.main == resources->Layout() && !described.layouts.split, "the set layout described from the compute shader differs from the dispatch's"); + } const auto layouts = resources->SetLayouts(); objects = computePipeline(context, shader, layouts.main, layouts.mainKey, pushStages, programAddress); timing.Mark("pipeline_create"); @@ -4806,7 +4899,7 @@ RecipeOutcome VulkanDevice::DispatchRecipe(const ShaderRecompiler::RecompileResu return RecipeOutcome::Rebuild; } std::shared_ptr mapped; - const auto pipelineKey = (shader.PipelineVariantId() << 1u) | (recipe.pushes ? 1u : 0u); + const auto pipelineKey = ComputePipelineKey::Of(shader, recipe.pushes); { std::lock_guard pipelines(state->computePipelinesMutex); if (const auto found = state->computePipelines.find(pipelineKey); found != state->computePipelines.end()) mapped = found->second; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp index a195283b06..55c7ba06c2 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp @@ -94,14 +94,29 @@ private: std::vector framebuffers; }; -// The pipeline for this configuration, shared across draws and frames: keyed by the stages' variant -// ids (the rect-list tessellation pair by the vertex and fragment ids that generate it), the vertex -// input layout, the descriptor set layout key, push constant stages and every pipeline state field. -// A configuration whose stage has no variant id gets a private pipeline. Entries are LRU-bounded and -// only evicted once no recorded draw holds them. Debug aid: APS5_NO_PIPELINE_CACHE=1 builds one per -// draw as before. +// The pipeline for this configuration, shared across draws and frames: keyed by the stages' +// identities (the rect-list tessellation pair and the geometry stage by the vertex and fragment +// results that generate them), the vertex input layout, the descriptor set layout key, push constant +// stages and every pipeline state field. A configuration whose stage has no module gets a private +// pipeline. A miss builds the pipeline outside the store's mutex (a concurrent build of the same +// key keeps the first inserted) and records its recipe in the pipeline journal. Entries are +// LRU-bounded (APS5_PIPELINE_STORE_ENTRIES, 1024) and only evicted once no recorded draw holds +// them. Debug aid: APS5_NO_PIPELINE_CACHE=1 builds one per draw as before. std::shared_ptr CachedPipeline(const Context& context, const State& state, const VertexInputLayout& vertexInput, const ShaderResources& resources, std::span shaders, VkImageLayout attachmentLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL); -std::shared_ptr CachedPipeline(const Context& context, const State& state, const VertexInputLayout& vertexInput, const PipelineSetLayouts& resources, std::span shaders, VkImageLayout attachmentLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL); +// `created` reports whether this call built the pipeline (false: served from the store or private). +std::shared_ptr CachedPipeline(const Context& context, const State& state, const VertexInputLayout& vertexInput, const PipelineSetLayouts& resources, std::span shaders, VkImageLayout attachmentLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, bool* created = nullptr); +// Cumulative pipeline store outcomes since start-up (the [pipecache] line reports 10 s windows). +struct PipelineStoreCounters { + std::uint64_t hits = 0; + std::uint64_t misses = 0; + std::uint64_t uncached = 0; + std::uint64_t evicted = 0; + std::uint64_t cached = 0; + std::uint64_t bound = 0; +}; +PipelineStoreCounters PipelineStoreTotals(); +// The pipeline store's key of a configuration (empty when the configuration gets a private pipeline). +void BuildPipelineStoreKey(std::vector& key, const Context& context, const State& state, const VertexInputLayout& vertexInput, const PipelineSetLayouts& resources, std::span shaders, VkImageLayout attachmentLayout); struct DescribedSetLayouts { PipelineSetLayouts layouts; std::vector mainKey; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/PipelineJournal.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/PipelineJournal.hpp new file mode 100644 index 0000000000..c2cf90476e --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/PipelineJournal.hpp @@ -0,0 +1,104 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_PIPELINEJOURNAL_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_PIPELINEJOURNAL_HPP + +#include "prx/libSceAgcDriver/Graphics/include/Context.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Shaders.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp" +#include "prx/libSceAgcDriver/Graphics/include/State.hpp" +#include "prx/libSceAgcDriver/Graphics/include/VertexInput.hpp" +#include "Recompiler.hpp" +#include +#include +#include +#include +#include +#include +#include + +namespace AgcDriver::Graphics { + +// What a graphics pipeline is built from besides the device: the stage results (named by their +// run-stable identity), the vertex input layout, the descriptor set layout keys and the state +// fields the Pipeline constructor reads, with every other State field cleared +// (CanonicalPipelineState). The pipeline store keys on exactly this, and the journal records it so +// the next run builds the same VkPipeline before the title asks for it. A generated stage (the +// rect-list control and evaluation pair, the barycentric geometry stage) is built from the vertex +// and fragment results and the device's limits; its identity is in the key like any other stage's, +// so a replayed pipeline built from another device's generated module is missed, not used. +struct GraphicsPipelineRecipe { + struct Stage { + ShaderRecompiler::ShaderStage stage; + ShaderRecompiler::PipelineIdentity identity; + std::uint32_t pushConstantOffset; + bool generated; + }; + VkImageLayout attachmentLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; + std::vector stages; + VertexInputLayout vertexInput; + std::vector mainLayoutKey; + std::vector fragmentLayoutKey; + bool splitSets = false; + State state; +}; + +[[nodiscard]] State CanonicalPipelineState(const State& state); +[[nodiscard]] bool GeneratedPipelineStage(const State& state, ShaderRecompiler::ShaderStage stage); +[[nodiscard]] GraphicsPipelineRecipe MakeGraphicsPipelineRecipe(const State& state, const VertexInputLayout& vertexInput, const PipelineSetLayouts& resources, std::span shaders, VkImageLayout attachmentLayout); +[[nodiscard]] std::vector EncodeGraphicsPipelineRecipe(const GraphicsPipelineRecipe& recipe); +[[nodiscard]] bool DecodeGraphicsPipelineRecipe(std::span bytes, GraphicsPipelineRecipe& recipe); + +struct PipelineJournalCounters { + std::uint64_t loadedRecipes = 0; + std::uint64_t loadedModules = 0; + std::uint64_t loadedBytes = 0; + std::uint64_t droppedRecords = 0; + double loadMs = 0; + std::uint64_t replayedGraphics = 0; + std::uint64_t replayedCompute = 0; + std::uint64_t replayPresent = 0; + std::uint64_t replaySkipped = 0; + std::uint64_t replayRejected = 0; + std::uint64_t replayFailed = 0; + double replayMs = 0; + std::uint32_t replayThreads = 0; + std::uint64_t recordedGraphics = 0; + std::uint64_t recordedCompute = 0; + std::uint64_t recordedModules = 0; + std::uint64_t writtenBytes = 0; + std::uint64_t writeFailures = 0; +}; + +// A decimal switch of the pipeline store or journal, or its default when unset; anything else throws. +[[nodiscard]] std::uint64_t PipelineSwitch(const char* name, std::uint64_t fallback); +[[nodiscard]] bool PipelineJournalEnabled(); +// True on a replay thread while it builds an entry: what it builds is already in the file. +[[nodiscard]] bool PipelineJournalReplaying(); +[[nodiscard]] std::filesystem::path PipelineJournalPath(); +[[nodiscard]] PipelineJournalCounters PipelineJournalTotals(); +void RecordGraphicsPipeline(const GraphicsPipelineRecipe& recipe, std::span shaders); +void RecordComputePipeline(const ShaderRecompiler::RecompileResult& shader, bool pushConstants); +void FlushPipelineJournal(); +void ReportPipelineJournal(); + +// Cancel stops the threads taking further entries (the pipeline each is building completes); +// Wait joins them. A device cancels its replay before it is replaced or destroyed. +class PipelineJournalReplay { +public: + virtual ~PipelineJournalReplay() = default; + virtual void Cancel() = 0; + virtual void Wait() = 0; + [[nodiscard]] virtual bool Done() const = 0; +}; + +struct PipelineJournalHooks { + std::function compute; +}; + +// The journal belongs to the device that wrote it, named by the same properties as the driver's +// pipeline cache file; a replay for another device starts the file over. +[[nodiscard]] std::uint64_t PipelineJournalDeviceFingerprint(const VkPhysicalDeviceProperties& properties); +[[nodiscard]] std::shared_ptr StartPipelineJournalReplay(const Context& context, std::uint64_t deviceFingerprint, PipelineJournalHooks hooks); + +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/PipelineLibrary.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/PipelineLibrary.hpp index b2ca74e468..a40cf15479 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/PipelineLibrary.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/PipelineLibrary.hpp @@ -4,6 +4,7 @@ #include "prx/libSceAgcDriver/Graphics/include/Context.hpp" #include #include +#include #include #include #include @@ -32,7 +33,13 @@ struct OptimizedPipeline { PFN_vkDestroyPipeline destroy = nullptr; std::atomic handle{VK_NULL_HANDLE}; std::atomic released{false}; + // The relink of a pipeline the journal replay built waits for the first draw that binds it + // (Schedule), so the relink thread serves the title's own pipelines first and a replayed + // pipeline the title never draws is never relinked. + std::atomic deferred{false}; + std::function relink; void Release() noexcept; + void Schedule(); }; VkPipeline LinkPipelineFromLibraries(const Context& context, const VkGraphicsPipelineCreateInfo& info, const VkPipelineRenderingCreateInfoKHR& rendering, const VkPipelineLayoutCreateInfo& layout, const PipelineLibraryKeys& keys, std::shared_ptr* optimized = nullptr, bool partsOnly = false, std::span modules = {}); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp index 9fd5790a24..ec0251bcac 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp @@ -2,6 +2,7 @@ #include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp" #include "prx/libSceAgcDriver/Graphics/include/Pipeline.hpp" #include "prx/libSceAgcDriver/Graphics/include/PipelineLibrary.hpp" +#include "prx/libSceAgcDriver/Graphics/include/PipelineJournal.hpp" #include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" #include "prx/libSceAgcDriver/Graphics/include/PipelineSpecialization.hpp" #include "prx/libSceAgcDriver/Graphics/include/VertexInput.hpp" @@ -193,10 +194,12 @@ bool libraryKeys(PipelineLibraryKeys& keys, const Context& context, const State& if (!context.graphicsPipelineLibrary || context.pipelineExecutableInfo || state.depthBiasPerFace || state.rectList || SplitsAlternateAlpha(state)) return false; keys = {}; for (const auto& shader : shaders) { - if (shader.stage == Stage::Geometry || shader.program->PipelineVariantId() == 0) return false; + const auto identity = shader.program->Identity(); + if (shader.stage == Stage::Geometry || identity.Empty()) return false; auto& key = shader.stage == Stage::Fragment ? keys.fragmentShader : keys.preRasterization; appendKey(key, shader.stage); - appendKey(key, shader.program->PipelineVariantId()); + appendKey(key, identity.low); + appendKey(key, identity.high); appendKey(key, shader.pushConstantOffset); if (resources.split) { auto& sets = shader.stage == Stage::Fragment ? keys.fragmentSets : keys.preRasterizationSets; @@ -711,6 +714,7 @@ void Pipeline::Begin(VkCommandBuffer commands, const Framebuffer& framebuffer, V } void Pipeline::Continue(VkCommandBuffer commands, const State& state) const { + if (optimized != nullptr) optimized->Schedule(); const auto best = optimized != nullptr ? optimized->handle.load() : VK_NULL_HANDLE; context.Resolved(&DeviceFunctions::cmdBindPipeline, "vkCmdBindPipeline")(commands, VK_PIPELINE_BIND_POINT_GRAPHICS, best != VK_NULL_HANDLE ? best : pipeline); context.Resolved(&DeviceFunctions::cmdSetViewport, "vkCmdSetViewport")(commands, 0, 1, &state.viewport); @@ -779,25 +783,27 @@ void append(std::vector& key, const TValue& value) { key.insert(key.end(), bytes.begin(), bytes.end()); } -// Everything the Pipeline objects are built from, or empty when a stage's result has no variant id -// (the recompiler could not identify it, so nothing else may share its pipeline). The rect-list -// control and evaluation stages are generated from the vertex and fragment results, which the key -// already names, so they carry no id of their own. +// Everything the Pipeline objects are built from, or empty when a stage's result has no module +// (nothing else may share its pipeline). The stage identities and the state fields are the same on +// every run (CanonicalPipelineState keeps exactly the fields read here), which lets the journal's +// replay insert an entry a later draw finds; a generated stage (rect-list pair, geometry) is keyed +// by its identity like the others, so a replayed pipeline built from another device's generated +// module is missed rather than used. void pipelineKey(std::vector& key, const Context& context, const State& state, const VertexInputLayout& input, const PipelineSetLayouts& resources, std::span shaders, VkImageLayout attachmentLayout) { - using Stage = ShaderRecompiler::ShaderStage; key.clear(); append(key, context.device); append(key, attachmentLayout); append(key, shaders.size()); for (const auto& shader : shaders) { Require(shader.program != nullptr, "missing compiled shader"); - const bool generated = (state.rectList && (shader.stage == Stage::TessellationControl || shader.stage == Stage::TessellationEvaluation)) || shader.stage == Stage::Geometry; - if (!generated && shader.program->PipelineVariantId() == 0) { + const auto identity = shader.program->Identity(); + if (identity.Empty()) { key.clear(); return; } append(key, shader.stage); - append(key, generated ? std::uint64_t{0} : shader.program->PipelineVariantId()); + append(key, identity.low); + append(key, identity.high); // Where the stage's push constants sit in the block (AssemblePushConstants). append(key, shader.pushConstantOffset); } @@ -840,18 +846,23 @@ void pipelineKey(std::vector& key, const Context& context, const Stat append(key, state.sampleLocations); } append(key, state.colors.size()); - for (const auto& color : state.colors) append(key, color.format); + for (const auto& color : state.colors) { + append(key, color.format); + append(key, color.componentMapping); + } if (state.blends.size() != state.colors.size()) { for (const auto& color : state.colors) append(key, color.exportIndex); } append(key, state.depth.has_value()); if (state.depth) { append(key, state.depth->format); + append(key, state.depth->samples); append(key, state.depthTest); append(key, state.depthWrite); append(key, state.depthCompare); append(key, state.depthBoundsTest); append(key, state.depthBias); + append(key, state.depthBias && state.depthBiasClamp != 0.0f); append(key, state.depthBiasPerFace); append(key, state.stencilTest); auto front = state.stencilFront; @@ -903,7 +914,6 @@ struct PipelineStore { std::shared_ptr pipeline; }; std::mutex mutex; - std::vector scratchKey; // Least recently used first. std::list entries; std::unordered_map::iterator> index; @@ -911,6 +921,7 @@ struct PipelineStore { std::uint64_t misses = 0; std::uint64_t uncached = 0; std::uint64_t evicted = 0; + PipelineStoreCounters totals; std::chrono::steady_clock::time_point lastReport = std::chrono::steady_clock::now(); }; @@ -936,6 +947,11 @@ std::list::iterator abandon(PipelineStore& store, std::lis return store.entries.erase(it); } +std::size_t storeBound() { + static const auto bound = static_cast(std::max(1u, PipelineSwitch("APS5_PIPELINE_STORE_ENTRIES", 1024u))); + return bound; +} + void reportPipelines(PipelineStore& store) { static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; if (!profile) return; @@ -953,57 +969,116 @@ std::shared_ptr CachedPipeline(const Context& context, const State& st return CachedPipeline(context, state, vertexInput, resources.SetLayouts(), shaders, attachmentLayout); } -std::shared_ptr CachedPipeline(const Context& context, const State& state, const VertexInputLayout& vertexInput, const PipelineSetLayouts& resources, std::span shaders, VkImageLayout attachmentLayout) { +std::shared_ptr CachedPipeline(const Context& context, const State& state, const VertexInputLayout& vertexInput, const PipelineSetLayouts& resources, std::span shaders, VkImageLayout attachmentLayout, bool* created) { static const bool disabled = std::getenv("APS5_NO_PIPELINE_CACHE") != nullptr; + if (created != nullptr) *created = false; if (disabled) return std::make_shared(context, state, vertexInput, resources, shaders, attachmentLayout); auto& store = Pipelines(); - std::lock_guard lock(store.mutex); - reportPipelines(store); - auto& key = store.scratchKey; + thread_local std::vector key; pipelineKey(key, context, state, vertexInput, resources, shaders, attachmentLayout); if (key.empty()) { + std::lock_guard lock(store.mutex); ++store.uncached; + ++store.totals.uncached; return std::make_shared(context, state, vertexInput, resources, shaders, attachmentLayout); } const auto hash = hashKey(key); - if (const auto found = store.index.find(hash); found != store.index.end()) { + // The store's entry of the key, or nullptr with `found` telling a miss (the key is new) from a + // collision (another configuration owns the hash, so this one stays private). + const auto lookup = [&](bool& collision) -> std::shared_ptr { + collision = false; + const auto found = store.index.find(hash); + if (found == store.index.end()) return nullptr; const auto it = found->second; - if (it->key == key) { - // The key names the device handle, which the loader may reuse for a device created after - // this one was destroyed without ClearCachedPipelines: such an entry is a miss. - if (alive(*it, context)) { - ++store.hits; - store.entries.splice(store.entries.end(), store.entries, it); - return it->pipeline; - } - abandon(store, it); - } else { + if (it->key != key) { + collision = true; + return nullptr; + } + // The key names the device handle, which the loader may reuse for a device created after + // this one was destroyed without ClearCachedPipelines: such an entry is a miss. + if (alive(*it, context)) { + store.entries.splice(store.entries.end(), store.entries, it); + return it->pipeline; + } + abandon(store, it); + return nullptr; + }; + { + std::lock_guard lock(store.mutex); + reportPipelines(store); + bool collision = false; + if (auto pipeline = lookup(collision)) { + ++store.hits; + ++store.totals.hits; + return pipeline; + } + if (collision) { // A different configuration with the same hash keeps the resident entry; this one stays private. ++store.uncached; + ++store.totals.uncached; return std::make_shared(context, state, vertexInput, resources, shaders, attachmentLayout); } + ++store.misses; + ++store.totals.misses; } - ++store.misses; - // Entries of another device belong to one the driver replaced (it does so under the GpuMutex - // before any draw reaches the new device), whose objects went with it: forget them. - for (auto it = store.entries.begin(); it != store.entries.end();) { - it = alive(*it, context) ? std::next(it) : abandon(store, it); - } + // Built outside the mutex: a draw that finds its pipeline resident is not held behind another + // thread's build (the journal replay builds here too), and a device's driver compiles in + // parallel when several threads build at once. auto pipeline = std::make_shared(context, state, vertexInput, resources, shaders, attachmentLayout); - store.entries.push_back({context.device, context.bufferPool, hash, key, pipeline}); - store.index[hash] = std::prev(store.entries.end()); - constexpr std::size_t bound = 1024; - while (store.entries.size() > bound) { - // Only an entry no recorded draw still holds may go (Kept keeps its shared_ptr until the fence). - const auto victim = std::find_if(store.entries.begin(), store.entries.end(), [](const PipelineStore::Entry& entry) { return entry.pipeline.use_count() == 1; }); - if (victim == store.entries.end()) break; - store.index.erase(victim->hash); - store.entries.erase(victim); - ++store.evicted; + const auto bound = storeBound(); + const bool replaying = PipelineJournalReplaying(); + { + std::lock_guard lock(store.mutex); + bool collision = false; + if (auto raced = lookup(collision)) return raced; + if (collision) { + ++store.uncached; + ++store.totals.uncached; + return pipeline; + } + // Entries of another device belong to one the driver replaced (it does so under the GpuMutex + // before any draw reaches the new device), whose objects went with it: forget them. + for (auto it = store.entries.begin(); it != store.entries.end();) { + it = alive(*it, context) ? std::next(it) : abandon(store, it); + } + // A replayed pipeline enters at the least recently used end and never displaces a draw's: + // the title's own pipelines evict it first, and a full store takes none. + if (replaying && store.entries.size() >= bound) return pipeline; + if (replaying) { + store.entries.push_front({context.device, context.bufferPool, hash, std::vector(key), pipeline}); + store.index[hash] = store.entries.begin(); + } else { + store.entries.push_back({context.device, context.bufferPool, hash, std::vector(key), pipeline}); + store.index[hash] = std::prev(store.entries.end()); + } + while (store.entries.size() > bound) { + // Only an entry no recorded draw still holds may go (Kept keeps its shared_ptr until the fence). + const auto victim = std::find_if(store.entries.begin(), store.entries.end(), [](const PipelineStore::Entry& entry) { return entry.pipeline.use_count() == 1; }); + if (victim == store.entries.end()) break; + store.index.erase(victim->hash); + store.entries.erase(victim); + ++store.evicted; + ++store.totals.evicted; + } } + if (created != nullptr) *created = true; + if (PipelineJournalEnabled() && !PipelineJournalReplaying()) RecordGraphicsPipeline(MakeGraphicsPipelineRecipe(state, vertexInput, resources, shaders, attachmentLayout), shaders); return pipeline; } +void BuildPipelineStoreKey(std::vector& key, const Context& context, const State& state, const VertexInputLayout& vertexInput, const PipelineSetLayouts& resources, std::span shaders, VkImageLayout attachmentLayout) { + pipelineKey(key, context, state, vertexInput, resources, shaders, attachmentLayout); +} + +PipelineStoreCounters PipelineStoreTotals() { + auto& store = Pipelines(); + std::lock_guard lock(store.mutex); + auto totals = store.totals; + totals.cached = store.entries.size(); + totals.bound = storeBound(); + return totals; +} + void ClearCachedPipelines(VkDevice device) { auto& store = Pipelines(); std::lock_guard lock(store.mutex); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp new file mode 100644 index 0000000000..940ff93847 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp @@ -0,0 +1,1096 @@ +#include "prx/libSceAgcDriver/Graphics/include/PipelineJournal.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Pipeline.hpp" +#include "prx/libSceAgcDriver/Execution/include/ProfileOutput.hpp" +#include "ShaderCacheDirectory.hpp" +#include "ShaderDiskCache.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef _WIN32 +#include +#else +#include +#include +#include +#endif + +namespace AgcDriver::Graphics { + +namespace { + +static_assert(sizeof(VkPipelineColorBlendAttachmentState) == 32, "VkPipelineColorBlendAttachmentState changed: update the recipe codec"); +static_assert(sizeof(VkStencilOpState) == 28, "VkStencilOpState changed: update the recipe codec"); +static_assert(sizeof(VkVertexInputBindingDescription) == 12, "VkVertexInputBindingDescription changed: update the recipe codec"); +static_assert(sizeof(VkVertexInputAttributeDescription) == 16, "VkVertexInputAttributeDescription changed: update the recipe codec"); +#if defined(__linux__) && defined(__x86_64__) && defined(__GLIBCXX__) +static_assert(sizeof(State) == 664, "State changed: update CanonicalPipelineState and the recipe codec"); +static_assert(sizeof(ShaderStages) == 100, "ShaderStages changed: update the recipe codec"); +static_assert(sizeof(ShaderRecompiler::MeshConfiguration) == 36, "MeshConfiguration changed: update the recipe codec"); +static_assert(sizeof(ShaderRecompiler::TessellationConfiguration) == 32, "TessellationConfiguration changed: update the recipe codec"); +#endif + +constexpr std::uint32_t FileMagic = 0x4c4a5041u; +// Bumped when the recipe codec, CanonicalPipelineState or the store key change in a way the +// layout guards above do not catch (a field renumbered or reinterpreted at the same size). +constexpr std::uint32_t FileFormat = 1u; +constexpr std::uint32_t ModuleRecord = 1u; +constexpr std::uint32_t GraphicsRecord = 2u; +constexpr std::uint32_t ComputeRecord = 3u; +constexpr std::uint64_t RecordAlignment = 8u; + +struct FileHeader { + std::uint32_t magic; + std::uint32_t format; + std::uint32_t runtimeAbi; + std::uint32_t reserved; + std::uint64_t sourceVersion; + std::uint64_t device; +}; +static_assert(sizeof(FileHeader) == 32 && std::is_trivially_copyable_v); + +struct RecordHeader { + std::uint32_t kind; + std::uint32_t reserved; + std::uint64_t payloadBytes; + std::uint64_t payloadHash; +}; +static_assert(sizeof(RecordHeader) == 24 && std::is_trivially_copyable_v); + +class Writer { +public: + explicit Writer(std::vector& out) : out(out) {} + + template + void Raw(const TValue& value) requires std::is_trivially_copyable_v { + const auto offset = out.size(); + out.resize(offset + sizeof(value)); + std::memcpy(out.data() + offset, &value, sizeof(value)); + } + + void Bool(bool value) { Raw(value ? 1u : 0u); } + void U32(std::uint32_t value) { Raw(value); } + void U64(std::uint64_t value) { Raw(value); } + + template + void Raws(std::span values) requires std::is_trivially_copyable_v { + U64(values.size()); + for (const auto& value : values) Raw(value); + } + +private: + std::vector& out; +}; + +class Reader { +public: + explicit Reader(std::span bytes) : bytes(bytes) {} + + [[nodiscard]] bool Ok() const { return ok; } + [[nodiscard]] bool Done() const { return ok && position == bytes.size(); } + void Fail() { ok = false; } + + template + [[nodiscard]] TValue Raw() requires std::is_trivially_copyable_v { + TValue value{}; + if (!ok || bytes.size() - position < sizeof(value)) { + ok = false; + return value; + } + std::memcpy(&value, bytes.data() + position, sizeof(value)); + position += sizeof(value); + return value; + } + + [[nodiscard]] bool Bool() { + const auto value = Raw(); + if (value > 1u) ok = false; + return ok && value == 1u; + } + + [[nodiscard]] std::uint32_t U32() { return Raw(); } + [[nodiscard]] std::uint64_t U64() { return Raw(); } + + template + void Raws(std::vector& values, std::size_t limit) requires std::is_trivially_copyable_v { + const auto count = U64(); + if (!ok || count > limit || count > (bytes.size() - position) / sizeof(TValue)) { + ok = false; + values.clear(); + return; + } + values.resize(static_cast(count)); + for (auto& value : values) value = Raw(); + } + +private: + std::span bytes; + std::size_t position = 0; + bool ok = true; +}; + +template +void encodeOptional(Writer& writer, const std::optional& value, void (*encode)(Writer&, const TValue&)) { + writer.Bool(value.has_value()); + if (value) encode(writer, *value); +} + +void encodeMesh(Writer& writer, const ShaderRecompiler::MeshConfiguration& mesh) { + for (const auto value : {mesh.inputPrimitive, mesh.primitivesPerGroup, mesh.verticesPerGroup, mesh.maxVertices, mesh.maxPrimitives, mesh.threadsPerGroup, mesh.ldsSizeDwords, mesh.provokingVertex, mesh.esgsItemSize}) writer.U32(value); +} + +void decodeMesh(Reader& reader, ShaderRecompiler::MeshConfiguration& mesh) { + for (auto* value : {&mesh.inputPrimitive, &mesh.primitivesPerGroup, &mesh.verticesPerGroup, &mesh.maxVertices, &mesh.maxPrimitives, &mesh.threadsPerGroup, &mesh.ldsSizeDwords, &mesh.provokingVertex, &mesh.esgsItemSize}) *value = reader.U32(); +} + +void encodeTessellation(Writer& writer, const ShaderRecompiler::TessellationConfiguration& tessellation) { + for (const auto value : {tessellation.inputControlPoints, tessellation.outputControlPoints, tessellation.domain, tessellation.partitioning, tessellation.outputTopology, tessellation.patchesPerGroup, tessellation.primitiveGroupSize}) writer.U32(value); + writer.Bool(tessellation.breakAtInstance); +} + +void decodeTessellation(Reader& reader, ShaderRecompiler::TessellationConfiguration& tessellation) { + for (auto* value : {&tessellation.inputControlPoints, &tessellation.outputControlPoints, &tessellation.domain, &tessellation.partitioning, &tessellation.outputTopology, &tessellation.patchesPerGroup, &tessellation.primitiveGroupSize}) *value = reader.U32(); + tessellation.breakAtInstance = reader.Bool(); +} + +void encodeState(Writer& writer, const State& state) { + writer.U32(static_cast(state.stages.path)); + writer.U32(state.stages.vertexWaveSize); + writer.U32(state.stages.fragmentWaveSize); + encodeOptional(writer, state.stages.mesh, encodeMesh); + encodeOptional(writer, state.stages.tessellation, encodeTessellation); + writer.Bool(state.depth.has_value()); + if (state.depth) { + writer.U32(static_cast(state.depth->format)); + writer.U32(state.depth->samples); + } + writer.Bool(state.depthTest); + writer.Bool(state.depthWrite); + writer.U32(static_cast(state.depthCompare)); + writer.Bool(state.depthBoundsTest); + writer.Bool(state.depthBias); + writer.Raw(state.depthBiasClamp); + writer.Bool(state.depthBiasPerFace); + writer.Bool(state.stencilTest); + writer.Raw(state.stencilFront); + writer.Raw(state.stencilBack); + writer.U64(state.colors.size()); + for (const auto& color : state.colors) { + writer.U32(static_cast(color.format)); + writer.U32(color.exportIndex); + writer.Raw(color.componentMapping); + } + writer.Raws(std::span(state.blends)); + for (const auto value : state.blendConstants) writer.Raw(value); + writer.Bool(state.hasColorTarget); + writer.Bool(state.rectList); + writer.Bool(state.dualSourceBlend); + writer.U32(static_cast(state.topology)); + writer.Bool(state.primitiveRestart); + writer.Bool(state.negativeOneToOne); + writer.Bool(state.depthClamp); + writer.U32(static_cast(state.conservativeRasterization)); + writer.U32(state.cullMode); + writer.U32(static_cast(state.frontFace)); + writer.U32(static_cast(state.provokingVertexMode)); + writer.U32(state.samples); + for (const auto value : state.sampleLocations) writer.Raw(value); + writer.Bool(state.customSampleLocations); +} + +void decodeState(Reader& reader, State& state) { + state = State{}; + state.stages.path = static_cast(reader.U32()); + state.stages.vertexWaveSize = reader.U32(); + state.stages.fragmentWaveSize = reader.U32(); + if (reader.Bool()) decodeMesh(reader, state.stages.mesh.emplace()); + if (reader.Bool()) decodeTessellation(reader, state.stages.tessellation.emplace()); + if (reader.Bool()) { + auto& depth = state.depth.emplace(); + depth.format = static_cast(reader.U32()); + depth.samples = reader.U32(); + } + state.depthTest = reader.Bool(); + state.depthWrite = reader.Bool(); + state.depthCompare = static_cast(reader.U32()); + state.depthBoundsTest = reader.Bool(); + state.depthBias = reader.Bool(); + state.depthBiasClamp = reader.Raw(); + state.depthBiasPerFace = reader.Bool(); + state.stencilTest = reader.Bool(); + state.stencilFront = reader.Raw(); + state.stencilBack = reader.Raw(); + const auto colors = reader.U64(); + if (!reader.Ok() || colors > 8u) { + reader.Fail(); + state = State{}; + return; + } + for (std::uint64_t index = 0; index < colors; ++index) { + ColorTarget color{}; + color.format = static_cast(reader.U32()); + color.exportIndex = reader.U32(); + color.componentMapping = reader.Raw(); + state.colors.push_back(color); + } + reader.Raws(state.blends, 8u); + for (auto& value : state.blendConstants) value = reader.Raw(); + state.hasColorTarget = reader.Bool(); + state.rectList = reader.Bool(); + state.dualSourceBlend = reader.Bool(); + state.topology = static_cast(reader.U32()); + state.primitiveRestart = reader.Bool(); + state.negativeOneToOne = reader.Bool(); + state.depthClamp = reader.Bool(); + state.conservativeRasterization = static_cast(reader.U32()); + state.cullMode = reader.U32(); + state.frontFace = static_cast(reader.U32()); + state.provokingVertexMode = static_cast(reader.U32()); + state.samples = reader.U32(); + for (auto& value : state.sampleLocations) value = reader.Raw(); + state.customSampleLocations = reader.Bool(); +} + +void encodeRecipe(Writer& writer, const GraphicsPipelineRecipe& recipe) { + writer.U32(static_cast(recipe.attachmentLayout)); + writer.U64(recipe.stages.size()); + for (const auto& stage : recipe.stages) { + writer.U32(static_cast(stage.stage)); + writer.U64(stage.identity.low); + writer.U64(stage.identity.high); + writer.U32(stage.pushConstantOffset); + writer.Bool(stage.generated); + } + writer.Raws(std::span(recipe.vertexInput.bindings)); + writer.Raws(std::span(recipe.vertexInput.attributes)); + writer.Raws(std::span(recipe.mainLayoutKey)); + writer.Raws(std::span(recipe.fragmentLayoutKey)); + writer.Bool(recipe.splitSets); + encodeState(writer, recipe.state); +} + +bool decodeRecipe(Reader& reader, GraphicsPipelineRecipe& recipe) { + recipe = GraphicsPipelineRecipe{}; + recipe.attachmentLayout = static_cast(reader.U32()); + const auto stages = reader.U64(); + if (!reader.Ok() || stages > 8u) return false; + for (std::uint64_t index = 0; index < stages; ++index) { + GraphicsPipelineRecipe::Stage stage{}; + stage.stage = static_cast(reader.U32()); + stage.identity.low = reader.U64(); + stage.identity.high = reader.U64(); + stage.pushConstantOffset = reader.U32(); + stage.generated = reader.Bool(); + recipe.stages.push_back(stage); + } + reader.Raws(recipe.vertexInput.bindings, 64u); + reader.Raws(recipe.vertexInput.attributes, 64u); + reader.Raws(recipe.mainLayoutKey, 4096u); + reader.Raws(recipe.fragmentLayoutKey, 4096u); + recipe.splitSets = reader.Bool(); + decodeState(reader, recipe.state); + return reader.Done(); +} + +std::uint64_t padded(std::uint64_t bytes) { + return (bytes + RecordAlignment - 1u) / RecordAlignment * RecordAlignment; +} + +bool profiling() { + static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; + return profile; +} + +thread_local bool replayingHere = false; + +std::uint64_t configured(const char* name, std::uint64_t fallback) { + const char* text = std::getenv(name); + if (text == nullptr || *text == '\0') return fallback; + char* end = nullptr; + const auto value = std::strtoull(text, &end, 10); + if (end == text || *end != '\0') throw std::runtime_error(std::string("AGC graphics: ") + name + " must be a decimal integer"); + return value; +} + +// Held while the journal is read, cut or appended: another process sharing the directory (a +// second run of the same install) takes the same lock file, so its appends and this run's never +// interleave and neither cuts a record the other is writing. Taken before the journal's mutex, +// never while holding it. +class FileLock { +public: + explicit FileLock(const std::filesystem::path& path) { +#ifdef _WIN32 + handle = CreateFileW(path.c_str(), GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr); + if (handle == INVALID_HANDLE_VALUE) return; + OVERLAPPED overlapped{}; + if (!LockFileEx(handle, LOCKFILE_EXCLUSIVE_LOCK, 0, 1, 0, &overlapped)) { + CloseHandle(handle); + handle = INVALID_HANDLE_VALUE; + } +#else + descriptor = ::open(path.c_str(), O_RDWR | O_CREAT | O_CLOEXEC, 0644); + if (descriptor >= 0 && ::flock(descriptor, LOCK_EX) != 0) { + ::close(descriptor); + descriptor = -1; + } +#endif + } + + ~FileLock() { +#ifdef _WIN32 + if (handle == INVALID_HANDLE_VALUE) return; + OVERLAPPED overlapped{}; + UnlockFileEx(handle, 0, 1, 0, &overlapped); + CloseHandle(handle); +#else + if (descriptor < 0) return; + ::flock(descriptor, LOCK_UN); + ::close(descriptor); +#endif + } + + FileLock(const FileLock&) = delete; + FileLock& operator=(const FileLock&) = delete; + +private: +#ifdef _WIN32 + HANDLE handle = INVALID_HANDLE_VALUE; +#else + int descriptor = -1; +#endif +}; + +// The end of the last whole record in [from, size) of the file: records another process appended +// after this one loaded the file are kept, the torn tail of a record a crashed run left is cut. +std::uint64_t validTail(const std::filesystem::path& path, std::uint64_t from, std::uint64_t size); + +struct LoadedModule { + ShaderRecompiler::ShaderStage stage; + std::shared_ptr result; +}; + +struct LoadedEntry { + std::uint32_t kind; + GraphicsPipelineRecipe graphics; + ShaderRecompiler::PipelineIdentity compute; + bool pushConstants = false; +}; + +struct LoadedJournal { + std::map modules; + std::vector entries; + std::set indexModules; + std::set indexRecords; + std::uint64_t validBytes = 0; + std::uint64_t bytes = 0; + std::uint64_t dropped = 0; + bool headerValid = false; +}; + +class Journal { +public: + static Journal& Instance() { + static auto* journal = new Journal(); + return *journal; + } + + [[nodiscard]] bool Enabled() const { return !path.empty() && !mismatched.load(std::memory_order_relaxed); } + [[nodiscard]] const std::filesystem::path& Path() const { return path; } + + // The journal belongs to one device per process: the first device to replay binds its + // fingerprint, the file header names it and a file written by another device starts over. A + // later device with another fingerprint (the window's device is the headless device's GPU, so a + // run does not see one) turns the journal off for the rest of the run. + bool Bind(std::uint64_t fingerprint) { + std::lock_guard lock(mutex); + if (!bound.load(std::memory_order_relaxed)) { + device.store(fingerprint, std::memory_order_relaxed); + bound.store(true, std::memory_order_release); + return true; + } + if (device.load(std::memory_order_relaxed) == fingerprint) return true; + if (!mismatched.exchange(true)) std::fprintf(stderr, "[pipeline-journal] %s belongs to another device; the journal is off for this run\n", path.string().c_str()); + return false; + } + + // Reads the whole file (modules keep views into the buffer, which lives as long as they do), + // fills the dedupe index, and remembers how many bytes parsed so a torn tail is cut before the + // next append. + LoadedJournal Load() { + LoadedJournal loaded; + if (!Enabled() || !bound.load(std::memory_order_acquire)) return loaded; + const auto started = std::chrono::steady_clock::now(); + auto file = std::make_shared>(); + bool read = false; + { + FileLock fileLock(lockPath); + read = ShaderRecompiler::ReadWholeFile(path, *file); + } + if (read) parse(*file, file, loaded, true); + const auto ms = std::chrono::duration(std::chrono::steady_clock::now() - started).count(); + { + std::lock_guard lock(mutex); + std::error_code error; + if (!indexed && (read || !std::filesystem::exists(path, error))) { + indexed = true; + validBytes = loaded.validBytes; + headerValid = loaded.headerValid; + writtenModules = loaded.indexModules; + writtenRecords = loaded.indexRecords; + } + counters.loadedRecipes.store(loaded.entries.size(), std::memory_order_relaxed); + counters.loadedModules.store(loaded.modules.size(), std::memory_order_relaxed); + counters.loadedBytes.store(loaded.bytes, std::memory_order_relaxed); + counters.droppedRecords.store(loaded.dropped, std::memory_order_relaxed); + loadMs.store(ms, std::memory_order_relaxed); + } + if (loaded.bytes != 0 || loaded.dropped != 0) std::fprintf(stderr, "[pipeline-journal] loaded %zu pipelines and %zu modules (%.1f MiB) from %s in %.0f ms%s\n", loaded.entries.size(), loaded.modules.size(), static_cast(loaded.bytes) / (1024.0 * 1024.0), path.string().c_str(), ms, !loaded.headerValid ? "; written by another build or device, starting over" : loaded.dropped != 0 ? "; the file's tail was unreadable and is dropped" : ""); + return loaded; + } + + void RecordGraphics(const GraphicsPipelineRecipe& recipe, std::span shaders) { + if (!Enabled() || !bound.load(std::memory_order_acquire)) return; + Pending pending; + pending.kind = GraphicsRecord; + for (std::size_t index = 0; index < shaders.size() && index < recipe.stages.size(); ++index) { + Require(shaders[index].program != nullptr, "a recorded pipeline stage has no program"); + pending.modules.push_back({recipe.stages[index].identity, recipe.stages[index].stage, std::make_shared(*shaders[index].program)}); + } + Writer writer(pending.payload); + encodeRecipe(writer, recipe); + enqueue(std::move(pending)); + } + + void RecordCompute(const ShaderRecompiler::RecompileResult& shader, bool pushConstants) { + if (!Enabled() || !bound.load(std::memory_order_acquire)) return; + const auto identity = shader.Identity(); + if (identity.Empty()) return; + Pending pending; + pending.kind = ComputeRecord; + pending.modules.push_back({identity, ShaderRecompiler::ShaderStage::Compute, std::make_shared(shader)}); + Writer writer(pending.payload); + writer.U64(identity.low); + writer.U64(identity.high); + writer.Bool(pushConstants); + enqueue(std::move(pending)); + } + + void Flush() { + std::unique_lock lock(mutex); + idle.wait(lock, [&] { return queue.empty() && !writing; }); + } + + PipelineJournalCounters Totals() const { + PipelineJournalCounters totals; + totals.loadedRecipes = counters.loadedRecipes.load(std::memory_order_relaxed); + totals.loadedModules = counters.loadedModules.load(std::memory_order_relaxed); + totals.loadedBytes = counters.loadedBytes.load(std::memory_order_relaxed); + totals.droppedRecords = counters.droppedRecords.load(std::memory_order_relaxed); + totals.loadMs = loadMs.load(std::memory_order_relaxed); + totals.recordedGraphics = counters.recordedGraphics.load(std::memory_order_relaxed); + totals.recordedCompute = counters.recordedCompute.load(std::memory_order_relaxed); + totals.recordedModules = counters.recordedModules.load(std::memory_order_relaxed); + totals.writtenBytes = counters.writtenBytes.load(std::memory_order_relaxed); + totals.writeFailures = counters.writeFailures.load(std::memory_order_relaxed); + return totals; + } + + struct Counters { + std::atomic loadedRecipes{0}, loadedModules{0}, loadedBytes{0}, droppedRecords{0}; + std::atomic recordedGraphics{0}, recordedCompute{0}, recordedModules{0}, writtenBytes{0}, writeFailures{0}; + }; + +private: + struct PendingModule { + ShaderRecompiler::PipelineIdentity identity; + ShaderRecompiler::ShaderStage stage; + std::shared_ptr result; + }; + + struct Pending { + std::uint32_t kind = 0; + std::vector modules; + std::vector payload; + }; + + Journal() { + const char* off = std::getenv("APS5_NO_PIPELINE_JOURNAL"); + if ((off != nullptr && std::strcmp(off, "0") != 0) || !ShaderRecompiler::ShaderDiskCache::Enabled()) return; + path = ShaderRecompiler::ShaderDiskCache::EntryDirectory() / "pipelines.journal"; + lockPath = ShaderRecompiler::ShaderDiskCache::EntryDirectory() / "pipelines.journal.lock"; + maxBytes = configured("APS5_PIPELINE_JOURNAL_MAX_MIB", 1024u) << 20u; + std::thread([this] { run(); }).detach(); + } + + // Parses the file: with `loaded` set every record is decoded, otherwise only the index (the + // module identities and record hashes) is built. Stops at the first record that does not + // parse; the bytes before it stay valid. + void parse(const std::vector& file, const std::shared_ptr>& owner, LoadedJournal& loaded, bool decode) { + std::uint64_t valid = 0; + bool header = false; + std::uint64_t dropped = 0; + const auto bytes = std::span(file); + if (bytes.size() >= sizeof(FileHeader)) { + FileHeader fileHeader; + std::memcpy(&fileHeader, bytes.data(), sizeof(fileHeader)); + header = fileHeader.magic == FileMagic && fileHeader.format == FileFormat && fileHeader.runtimeAbi == ShaderRecompiler::RuntimeAbi::Version && fileHeader.sourceVersion == ShaderRecompiler::ShaderDiskCache::SourceVersion() && fileHeader.device == device.load(std::memory_order_relaxed); + } + if (header) { + valid = padded(sizeof(FileHeader)); + std::uint64_t cursor = valid; + while (cursor + sizeof(RecordHeader) <= bytes.size()) { + RecordHeader record; + std::memcpy(&record, bytes.data() + cursor, sizeof(record)); + const auto payloadOffset = cursor + sizeof(RecordHeader); + if (record.payloadBytes > bytes.size() - payloadOffset) break; + const auto end = padded(payloadOffset + record.payloadBytes); + if (end > bytes.size()) break; + const auto payload = bytes.subspan(static_cast(payloadOffset), static_cast(record.payloadBytes)); + if (ShaderRecompiler::HashBytes(payload) != record.payloadHash) break; + if (record.kind == ModuleRecord) { + if (payload.size() < 20u) break; + ShaderRecompiler::PipelineIdentity identity; + std::memcpy(&identity.low, payload.data(), sizeof(identity.low)); + std::memcpy(&identity.high, payload.data() + 8, sizeof(identity.high)); + std::uint32_t stage = 0; + std::memcpy(&stage, payload.data() + 16, sizeof(stage)); + bool decoded = !decode; + if (decode && !loaded.indexModules.contains(identity)) { + auto result = std::make_shared(); + decoded = ShaderRecompiler::ShaderDiskCache::DecodeResult(payload.subspan(20), *result, owner) && result->Identity() == identity; + if (decoded) loaded.modules.emplace(identity, LoadedModule{static_cast(stage), std::move(result)}); + else ++dropped; + } + if (decoded) loaded.indexModules.insert(identity); + } else if (record.kind == GraphicsRecord || record.kind == ComputeRecord) { + if (decode && !loaded.indexRecords.contains(record.payloadHash)) { + LoadedEntry entry; + entry.kind = record.kind; + bool decoded = false; + if (record.kind == GraphicsRecord) { + Reader reader(payload); + decoded = decodeRecipe(reader, entry.graphics); + } else { + Reader reader(payload); + entry.compute.low = reader.U64(); + entry.compute.high = reader.U64(); + entry.pushConstants = reader.Bool(); + decoded = reader.Done(); + } + if (decoded) loaded.entries.push_back(std::move(entry)); + else ++dropped; + } + loaded.indexRecords.insert(record.payloadHash); + } else { + break; + } + valid = end; + cursor = end; + } + } + if (valid < bytes.size()) ++dropped; + loaded.validBytes = valid; + loaded.bytes = bytes.size(); + loaded.dropped = dropped; + loaded.headerValid = header; + } + + // The index the writer dedupes against, replaced from the file: what it holds and where it + // ends. Under the file lock. False when the file exists but could not be read, so nothing is + // written until it can be (the next record tries again) and the file is never started over for + // a read failure. + bool indexLocked() { + std::vector file; + LoadedJournal index; + std::error_code error; + const bool exists = std::filesystem::exists(path, error); + if (exists && !ShaderRecompiler::ReadWholeFile(path, file)) return false; + if (exists) parse(file, nullptr, index, false); + std::lock_guard lock(mutex); + indexed = true; + validBytes = index.validBytes; + headerValid = index.headerValid; + writtenModules = std::move(index.indexModules); + writtenRecords = std::move(index.indexRecords); + return true; + } + + bool ensureIndexed() { + { + std::lock_guard lock(mutex); + if (indexed) return true; + } + FileLock fileLock(lockPath); + { + std::lock_guard lock(mutex); + if (indexed) return true; + } + return indexLocked(); + } + + void enqueue(Pending pending) { + { + std::lock_guard lock(mutex); + queue.push_back(std::move(pending)); + } + wake.notify_one(); + } + + void run() { + for (;;) { + Pending pending; + { + std::unique_lock lock(mutex); + wake.wait(lock, [&] { return !queue.empty(); }); + pending = std::move(queue.front()); + queue.pop_front(); + writing = true; + } + try { + write(pending); + } catch (const std::exception& error) { + counters.writeFailures.fetch_add(1, std::memory_order_relaxed); + static std::atomic reported{0}; + if (reported.fetch_add(1) < 4) std::fprintf(stderr, "[pipeline-journal] cannot write %s: %s\n", path.string().c_str(), error.what()); + } + { + std::lock_guard lock(mutex); + writing = false; + if (queue.empty()) idle.notify_all(); + } + } + } + + // One record per module the file lacks, then the pipeline record; a record whose payload the + // file already holds is skipped. The file is opened for appending per batch, after the torn + // tail of a crashed run (bytes past the last parsed record) was cut. + void write(const Pending& pending) { + if (!ensureIndexed()) throw std::runtime_error("the journal could not be read"); + for (unsigned attempt = 0; attempt < 2; ++attempt) { + if (append(pending)) return; + } + throw std::runtime_error("the journal keeps changing under this run"); + } + + // One attempt: false when the file was cut or replaced by someone else since it was indexed + // (it is indexed again, and the batch is rebuilt against that index). + bool append(const Pending& pending) { + const auto recordHash = ShaderRecompiler::HashBytes(pending.payload); + std::vector batch; + std::vector newModules; + { + std::lock_guard lock(mutex); + if (writtenRecords.contains(recordHash)) return true; + for (const auto& module : pending.modules) { + if (!writtenModules.contains(module.identity) && std::ranges::find(newModules, module.identity) == newModules.end()) newModules.push_back(module.identity); + } + } + const auto appendRecord = [&](std::uint32_t kind, std::span payload) { + const RecordHeader header{kind, 0u, payload.size(), ShaderRecompiler::HashBytes(payload)}; + Writer writer(batch); + writer.Raw(header); + batch.insert(batch.end(), payload.begin(), payload.end()); + batch.resize(static_cast(padded(batch.size()))); + }; + for (const auto& identity : newModules) { + const auto module = std::ranges::find_if(pending.modules, [&](const PendingModule& candidate) { return candidate.identity == identity; }); + std::vector payload; + Writer writer(payload); + writer.U64(identity.low); + writer.U64(identity.high); + writer.U32(static_cast(module->stage)); + ShaderRecompiler::ShaderDiskCache::EncodeResult(*module->result, payload); + appendRecord(ModuleRecord, payload); + } + appendRecord(pending.kind, pending.payload); + std::error_code error; + std::filesystem::create_directories(path.parent_path(), error); + FileLock fileLock(lockPath); + bool fresh = false; + { + std::lock_guard lock(mutex); + fresh = !headerValid; + if (!fresh) { + const auto size = std::filesystem::file_size(path, error); + if (error) fresh = true; + else if (size > validBytes) { + const auto tail = validTail(path, validBytes, size); + if (tail < size) std::filesystem::resize_file(path, tail, error); + if (error) fresh = true; + else validBytes = tail; + } + } + if (!fresh && validBytes + batch.size() > maxBytes) { + static std::atomic reported{false}; + if (!reported.exchange(true)) std::fprintf(stderr, "[pipeline-journal] %s reached APS5_PIPELINE_JOURNAL_MAX_MIB; later pipelines are not recorded\n", path.string().c_str()); + counters.writeFailures.fetch_add(1, std::memory_order_relaxed); + return true; + } + } + if (!fresh) { + const auto size = std::filesystem::file_size(path, error); + if (!error && size < validBytes) { + if (!indexLocked()) throw std::runtime_error("the journal could not be read"); + return false; + } + } + std::vector prefix; + if (fresh) { + const FileHeader header{FileMagic, FileFormat, ShaderRecompiler::RuntimeAbi::Version, 0u, ShaderRecompiler::ShaderDiskCache::SourceVersion(), device.load(std::memory_order_relaxed)}; + Writer writer(prefix); + writer.Raw(header); + prefix.resize(static_cast(padded(prefix.size()))); + } + std::ofstream file(path, fresh ? std::ios::binary | std::ios::trunc : std::ios::binary | std::ios::app); + if (!file) throw std::runtime_error("cannot open the journal for writing"); + if (!prefix.empty()) file.write(reinterpret_cast(prefix.data()), static_cast(prefix.size())); + file.write(reinterpret_cast(batch.data()), static_cast(batch.size())); + file.flush(); + if (file.fail()) throw std::runtime_error("the journal write failed"); + std::lock_guard lock(mutex); + headerValid = true; + validBytes = (fresh ? padded(sizeof(FileHeader)) : validBytes) + batch.size(); + for (const auto& identity : newModules) writtenModules.insert(identity); + writtenRecords.insert(recordHash); + counters.recordedModules.fetch_add(newModules.size(), std::memory_order_relaxed); + if (pending.kind == GraphicsRecord) counters.recordedGraphics.fetch_add(1, std::memory_order_relaxed); + else counters.recordedCompute.fetch_add(1, std::memory_order_relaxed); + counters.writtenBytes.fetch_add(prefix.size() + batch.size(), std::memory_order_relaxed); + return true; + } + + std::filesystem::path path; + std::filesystem::path lockPath; + std::uint64_t maxBytes = 0; + std::atomic device{0}; + std::atomic bound{false}; + std::atomic mismatched{false}; + std::mutex mutex; + std::condition_variable wake; + std::condition_variable idle; + std::deque queue; + bool writing = false; + bool indexed = false; + bool headerValid = false; + std::uint64_t validBytes = 0; + std::set writtenModules; + std::set writtenRecords; + Counters counters; + std::atomic loadMs{0}; +}; + +std::uint64_t validTail(const std::filesystem::path& path, std::uint64_t from, std::uint64_t size) { + if (size <= from) return from; + std::ifstream file(path, std::ios::binary); + if (!file) return from; + file.seekg(static_cast(from)); + std::vector bytes(static_cast(size - from)); + file.read(reinterpret_cast(bytes.data()), static_cast(bytes.size())); + if (static_cast(file.gcount()) != bytes.size()) return from; + const auto span = std::span(bytes); + std::uint64_t cursor = 0; + std::uint64_t valid = 0; + while (cursor + sizeof(RecordHeader) <= span.size()) { + RecordHeader record; + std::memcpy(&record, span.data() + cursor, sizeof(record)); + const auto payloadOffset = cursor + sizeof(RecordHeader); + if (record.kind == 0 || record.kind > ComputeRecord || record.payloadBytes > span.size() - payloadOffset) break; + const auto end = padded(payloadOffset + record.payloadBytes); + if (end > span.size()) break; + if (ShaderRecompiler::HashBytes(span.subspan(static_cast(payloadOffset), static_cast(record.payloadBytes))) != record.payloadHash) break; + valid = end; + cursor = end; + } + return from + valid; +} + +struct ReplayCounters { + std::atomic graphics{0}, compute{0}, present{0}, skipped{0}, rejected{0}, failed{0}; + std::atomic threads{0}; + std::atomic ms{0}; +}; + +ReplayCounters& replayCounters() { + static ReplayCounters counters; + return counters; +} + +// Builds the journal's pipelines for one device on a pool: the first thread loads the file, every +// thread then takes the next entry. A pipeline the store already holds counts as present; an entry +// whose modules are missing or whose set layouts differ from what its modules describe is rejected. +class Replay : public PipelineJournalReplay { +public: + // The context keeps the device's buffer pool: a store entry is alive for a draw only while its + // pool is the draw's (the pool tells device instances apart), so the entries built here must + // name it. The pool is released with the replay, before the device's teardown resets it. + Replay(Context context, PipelineJournalHooks hooks, unsigned threads) : context(std::move(context)), hooks(std::move(hooks)), active(threads) { + started = std::chrono::steady_clock::now(); + replayCounters().threads.store(threads, std::memory_order_relaxed); + workers.reserve(threads); + for (unsigned index = 0; index < threads; ++index) workers.emplace_back([this] { run(); }); + } + + ~Replay() override { + Cancel(); + Wait(); + } + + void Cancel() override { cancelled.store(true, std::memory_order_relaxed); } + + void Wait() override { + for (auto& worker : workers) { + if (worker.joinable()) worker.join(); + } + workers.clear(); + } + + [[nodiscard]] bool Done() const override { return finished.load(std::memory_order_acquire); } + +private: + void run() { + std::call_once(loadOnce, [&] { + try { + auto journal = Journal::Instance().Load(); + const auto limit = configured("APS5_PIPELINE_JOURNAL_ENTRIES", 1024u); + if (journal.entries.size() > limit) journal.entries.resize(static_cast(limit)); + loaded = std::move(journal); + } catch (const std::exception& error) { + std::fprintf(stderr, "[pipeline-journal] cannot load %s: %s\n", Journal::Instance().Path().string().c_str(), error.what()); + } + }); + for (;;) { + if (cancelled.load(std::memory_order_relaxed)) break; + const auto index = cursor.fetch_add(1, std::memory_order_relaxed); + if (index >= loaded.entries.size()) break; + try { + replay(loaded.entries[index]); + } catch (const std::exception& error) { + replayCounters().failed.fetch_add(1, std::memory_order_relaxed); + static std::atomic reported{0}; + if (reported.fetch_add(1) < 8) std::fprintf(stderr, "[pipeline-journal] replay failed: %s\n", error.what()); + } + } + if (active.fetch_sub(1, std::memory_order_acq_rel) == 1) { + loaded = LoadedJournal{}; + const auto ms = std::chrono::duration(std::chrono::steady_clock::now() - started).count(); + replayCounters().ms.store(ms, std::memory_order_relaxed); + finished.store(true, std::memory_order_release); + ReportPipelineJournal(); + } + } + + void replay(const LoadedEntry& entry) { + struct Replaying { + Replaying() { replayingHere = true; } + ~Replaying() { replayingHere = false; } + } replaying; + auto& counters = replayCounters(); + if (entry.kind == ComputeRecord) { + const auto module = loaded.modules.find(entry.compute); + if (module == loaded.modules.end() || module->second.stage != ShaderRecompiler::ShaderStage::Compute || !hooks.compute) { + counters.rejected.fetch_add(1, std::memory_order_relaxed); + return; + } + (hooks.compute(*module->second.result, entry.pushConstants) ? counters.compute : counters.present).fetch_add(1, std::memory_order_relaxed); + return; + } + std::vector shaders; + for (const auto& stage : entry.graphics.stages) { + const auto module = loaded.modules.find(stage.identity); + if (module == loaded.modules.end() || module->second.stage != stage.stage) { + counters.rejected.fetch_add(1, std::memory_order_relaxed); + return; + } + shaders.push_back({stage.stage, module->second.result.get(), stage.pushConstantOffset}); + } + if (shaders.empty() || context.descriptorCache == nullptr) { + counters.rejected.fetch_add(1, std::memory_order_relaxed); + return; + } + const auto store = PipelineStoreTotals(); + if (store.cached >= store.bound) { + counters.skipped.fetch_add(1, std::memory_order_relaxed); + return; + } + const auto layouts = DescribeSetLayouts(context, shaders); + if (!std::ranges::equal(layouts.mainKey, entry.graphics.mainLayoutKey) || !std::ranges::equal(layouts.fragmentKey, entry.graphics.fragmentLayoutKey) || layouts.layouts.split != entry.graphics.splitSets) { + counters.rejected.fetch_add(1, std::memory_order_relaxed); + return; + } + bool created = false; + static_cast(CachedPipeline(context, entry.graphics.state, entry.graphics.vertexInput, layouts.View(), shaders, entry.graphics.attachmentLayout, &created)); + (created ? counters.graphics : counters.present).fetch_add(1, std::memory_order_relaxed); + } + + Context context; + PipelineJournalHooks hooks; + std::vector workers; + std::once_flag loadOnce; + LoadedJournal loaded; + std::atomic cursor{0}; + std::atomic active{0}; + std::atomic finished{false}; + std::atomic cancelled{false}; + std::chrono::steady_clock::time_point started; +}; + +} + +State CanonicalPipelineState(const State& state) { + State canonical{}; + canonical.stages.path = state.stages.path; + canonical.stages.vertexWaveSize = state.stages.vertexWaveSize; + canonical.stages.fragmentWaveSize = state.stages.fragmentWaveSize; + canonical.stages.mesh = state.stages.mesh; + canonical.stages.tessellation = state.stages.tessellation; + if (state.depth) { + DepthTarget depth{}; + depth.format = state.depth->format; + depth.samples = state.depth->samples; + canonical.depth = depth; + } + canonical.depthTest = state.depthTest; + canonical.depthWrite = state.depthWrite; + canonical.depthCompare = state.depthCompare; + canonical.depthBoundsTest = state.depthBoundsTest; + canonical.depthBias = state.depthBias; + canonical.depthBiasClamp = state.depthBiasClamp; + canonical.depthBiasPerFace = state.depthBiasPerFace; + canonical.stencilTest = state.stencilTest; + canonical.stencilFront = state.stencilFront; + canonical.stencilBack = state.stencilBack; + for (const auto& color : state.colors) { + ColorTarget canonicalColor{}; + canonicalColor.format = color.format; + canonicalColor.exportIndex = color.exportIndex; + canonicalColor.componentMapping = color.componentMapping; + canonical.colors.push_back(canonicalColor); + } + canonical.blends = state.blends; + canonical.blendConstants = state.blendConstants; + canonical.hasColorTarget = state.hasColorTarget; + canonical.rectList = state.rectList; + canonical.dualSourceBlend = state.dualSourceBlend; + canonical.topology = state.topology; + canonical.primitiveRestart = state.primitiveRestart; + canonical.negativeOneToOne = state.negativeOneToOne; + canonical.depthClamp = state.depthClamp; + canonical.conservativeRasterization = state.conservativeRasterization; + canonical.cullMode = state.cullMode; + canonical.frontFace = state.frontFace; + canonical.provokingVertexMode = state.provokingVertexMode; + canonical.samples = state.samples; + canonical.sampleLocations = state.sampleLocations; + canonical.customSampleLocations = state.customSampleLocations; + return canonical; +} + +bool GeneratedPipelineStage(const State& state, ShaderRecompiler::ShaderStage stage) { + using Stage = ShaderRecompiler::ShaderStage; + return (state.rectList && (stage == Stage::TessellationControl || stage == Stage::TessellationEvaluation)) || stage == Stage::Geometry; +} + +GraphicsPipelineRecipe MakeGraphicsPipelineRecipe(const State& state, const VertexInputLayout& vertexInput, const PipelineSetLayouts& resources, std::span shaders, VkImageLayout attachmentLayout) { + GraphicsPipelineRecipe recipe; + recipe.attachmentLayout = attachmentLayout; + for (const auto& shader : shaders) { + Require(shader.program != nullptr, "missing compiled shader"); + recipe.stages.push_back({shader.stage, shader.program->Identity(), shader.pushConstantOffset, GeneratedPipelineStage(state, shader.stage)}); + } + recipe.vertexInput = vertexInput; + recipe.mainLayoutKey.assign(resources.mainKey.begin(), resources.mainKey.end()); + recipe.fragmentLayoutKey.assign(resources.fragmentKey.begin(), resources.fragmentKey.end()); + recipe.splitSets = resources.split; + recipe.state = CanonicalPipelineState(state); + return recipe; +} + +std::vector EncodeGraphicsPipelineRecipe(const GraphicsPipelineRecipe& recipe) { + std::vector bytes; + Writer writer(bytes); + encodeRecipe(writer, recipe); + return bytes; +} + +bool DecodeGraphicsPipelineRecipe(std::span bytes, GraphicsPipelineRecipe& recipe) { + Reader reader(bytes); + return decodeRecipe(reader, recipe); +} + +std::uint64_t PipelineSwitch(const char* name, std::uint64_t fallback) { + return configured(name, fallback); +} + +bool PipelineJournalEnabled() { + return Journal::Instance().Enabled(); +} + +bool PipelineJournalReplaying() { + return replayingHere; +} + +std::filesystem::path PipelineJournalPath() { + return Journal::Instance().Path(); +} + +PipelineJournalCounters PipelineJournalTotals() { + auto totals = Journal::Instance().Totals(); + const auto& replay = replayCounters(); + totals.replayedGraphics = replay.graphics.load(std::memory_order_relaxed); + totals.replayedCompute = replay.compute.load(std::memory_order_relaxed); + totals.replayPresent = replay.present.load(std::memory_order_relaxed); + totals.replaySkipped = replay.skipped.load(std::memory_order_relaxed); + totals.replayRejected = replay.rejected.load(std::memory_order_relaxed); + totals.replayFailed = replay.failed.load(std::memory_order_relaxed); + totals.replayMs = replay.ms.load(std::memory_order_relaxed); + totals.replayThreads = replay.threads.load(std::memory_order_relaxed); + return totals; +} + +void RecordGraphicsPipeline(const GraphicsPipelineRecipe& recipe, std::span shaders) { + Journal::Instance().RecordGraphics(recipe, shaders); +} + +void RecordComputePipeline(const ShaderRecompiler::RecompileResult& shader, bool pushConstants) { + Journal::Instance().RecordCompute(shader, pushConstants); +} + +void FlushPipelineJournal() { + if (Journal::Instance().Enabled()) Journal::Instance().Flush(); +} + +void ReportPipelineJournal() { + if (!profiling() || !Journal::Instance().Enabled()) return; + const auto totals = PipelineJournalTotals(); + AgcDriver::ProfilePrint_nid_no_patch("[pipeline-journal] loaded %llu pipelines, %llu modules (%.1f MiB, %.0f ms); replay on %u threads: %llu graphics and %llu compute built, %llu present, %llu skipped (store full), %llu rejected, %llu failed in %.0f ms; recorded this run: %llu graphics, %llu compute, %llu modules (%.1f MiB), %llu write failures\n", + static_cast(totals.loadedRecipes), static_cast(totals.loadedModules), static_cast(totals.loadedBytes) / (1024.0 * 1024.0), totals.loadMs, totals.replayThreads, + static_cast(totals.replayedGraphics), static_cast(totals.replayedCompute), static_cast(totals.replayPresent), static_cast(totals.replaySkipped), static_cast(totals.replayRejected), static_cast(totals.replayFailed), totals.replayMs, + static_cast(totals.recordedGraphics), static_cast(totals.recordedCompute), static_cast(totals.recordedModules), static_cast(totals.writtenBytes) / (1024.0 * 1024.0), static_cast(totals.writeFailures)); +} + +std::uint64_t PipelineJournalDeviceFingerprint(const VkPhysicalDeviceProperties& properties) { + std::array words{properties.vendorID, properties.deviceID, properties.driverVersion, 0u, 0u, 0u, 0u}; + static_assert(VK_UUID_SIZE == 16); + std::memcpy(words.data() + 3, properties.pipelineCacheUUID, VK_UUID_SIZE); + return ShaderRecompiler::HashSpirvWords(words, 0x6a6f75726e616c01ull); +} + +std::shared_ptr StartPipelineJournalReplay(const Context& context, std::uint64_t deviceFingerprint, PipelineJournalHooks hooks) { + if (!Journal::Instance().Enabled() || !Journal::Instance().Bind(deviceFingerprint)) return nullptr; + const auto threads = static_cast(configured("APS5_PIPELINE_JOURNAL_THREADS", std::max(1u, std::thread::hardware_concurrency() / 2u))); + if (threads == 0) return nullptr; + return std::make_shared(context, std::move(hooks), threads); +} + +} diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp index bd2a12ed32..7e8fec28b6 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp @@ -1,4 +1,5 @@ #include "prx/libSceAgcDriver/Graphics/include/PipelineLibrary.hpp" +#include "prx/libSceAgcDriver/Graphics/include/PipelineJournal.hpp" #include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp" #include #include @@ -204,13 +205,16 @@ VkPipeline LinkPipelineFromLibraries(const Context& context, const VkGraphicsPip break; } VkPipeline handle = VK_NULL_HANDLE; - if (partsOnly) lock.unlock(); + // Built without the store's mutex: a draw whose parts are resident is not held behind + // another thread's build, and builds from several threads (the registration prebuild, the + // journal replay) run at once; a part another thread built meanwhile is kept, this one dropped. + lock.unlock(); VkResult created; { auto pipelineCacheLock = context.LockPipelineCacheForUse(); created = context.Function("vkCreateGraphicsPipelines")(context.device, context.pipelineCache, 1, &create, nullptr, &handle); } - if (partsOnly) lock.lock(); + lock.lock(); Check(created, "vkCreateGraphicsPipelines library"); if (const auto raced = cache.find(key); raced != cache.end()) { device.destroyPipeline(context.device, handle, nullptr); @@ -238,10 +242,12 @@ VkPipeline LinkPipelineFromLibraries(const Context& context, const VkGraphicsPip VkGraphicsPipelineCreateInfo create{VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO, &linked}; create.layout = info.layout; VkPipeline pipeline = VK_NULL_HANDLE; + lock.unlock(); { auto pipelineCacheLock = context.LockPipelineCacheForUse(); Check(context.Function("vkCreateGraphicsPipelines")(context.device, context.pipelineCache, 1, &create, nullptr, &pipeline), "vkCreateGraphicsPipelines link"); } + lock.lock(); ++device.counters.linked; if (optimize && optimized != nullptr) { auto slot = std::make_shared(); @@ -252,7 +258,7 @@ VkPipeline LinkPipelineFromLibraries(const Context& context, const VkGraphicsPip const auto cache = context.pipelineCache; const auto cacheSynchronization = context.pipelineCacheSynchronization; const auto libraryLayout = fullLayout; - enqueue(store, context.device, [slot, createPipelines, cache, cacheSynchronization, libraryLayout, libraries, &store, devicePointer = &device] { + std::function relink = [slot, createPipelines, cache, cacheSynchronization, libraryLayout, libraries, &store, devicePointer = &device] { if (slot->released.load()) return; VkPipelineLibraryCreateInfoKHR linked{VK_STRUCTURE_TYPE_PIPELINE_LIBRARY_CREATE_INFO_KHR}; linked.libraryCount = static_cast(libraries.size()); @@ -272,7 +278,13 @@ VkPipeline LinkPipelineFromLibraries(const Context& context, const VkGraphicsPip } std::lock_guard lock(store.mutex); ++devicePointer->counters.optimized; - }); + }; + if (PipelineJournalReplaying()) { + slot->relink = std::move(relink); + slot->deferred.store(true, std::memory_order_release); + } else { + enqueue(store, context.device, std::move(relink)); + } } return pipeline; } @@ -282,6 +294,15 @@ void OptimizedPipeline::Release() noexcept { if (const auto pipeline = handle.exchange(VK_NULL_HANDLE)) destroy(device, pipeline, nullptr); } +void OptimizedPipeline::Schedule() { + if (!deferred.load(std::memory_order_acquire)) return; + auto& store = Libraries(); + std::lock_guard lock(store.mutex); + if (!deferred.exchange(false, std::memory_order_acq_rel)) return; + if (relink && !released.load()) enqueue(store, device, std::move(relink)); + relink = nullptr; +} + void WaitForOptimizedPipelines(VkDevice device) { auto& store = Libraries(); std::unique_lock lock(store.mutex); diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 2e1d26e11f..6a67b64204 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -3,6 +3,7 @@ #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" #include "prx/libSceAgcDriver/Graphics/include/Pipeline.hpp" +#include "prx/libSceAgcDriver/Graphics/include/PipelineJournal.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureDetiler.hpp" #include "prx/libSceAgcDriver/Graphics/include/VertexInput.hpp" #include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" @@ -34,6 +35,8 @@ #include #include #include +#include +#include #include #include #include @@ -3626,7 +3629,15 @@ VKAPI_ATTR VkResult VKAPI_CALL mockCreatePipelineLayout(VkDevice, const VkPipeli } VKAPI_ATTR VkResult VKAPI_CALL mockCreateGraphicsPipelines(VkDevice, VkPipelineCache, std::uint32_t count, const VkGraphicsPipelineCreateInfo* infos, const VkAllocationCallbacks*, VkPipeline* pipelines) { - Require(count == 1 && (infos[0].flags & VK_PIPELINE_CREATE_LIBRARY_BIT_KHR) != 0, "mock expects a graphics pipeline library"); + Require(count == 1, "mock expects exactly one graphics pipeline per call"); + if ((infos[0].flags & VK_PIPELINE_CREATE_LIBRARY_BIT_KHR) == 0) { + const auto* linked = static_cast(infos[0].pNext); + Require(linked != nullptr && linked->sType == VK_STRUCTURE_TYPE_PIPELINE_LIBRARY_CREATE_INFO_KHR && linked->libraryCount != 0, "mock expects a graphics pipeline library or a pipeline linked from libraries"); + *pipelines = makeHandle(); + ++mock.pipelineCreateCount; + ++mock.live; + return VK_SUCCESS; + } const auto* library = static_cast(infos[0].pNext); Require(library != nullptr && library->sType == VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_LIBRARY_CREATE_INFO_EXT, "missing graphics pipeline library description"); mock.libraryLayouts[library->flags] = infos[0].layout; @@ -7251,6 +7262,245 @@ void ResourceContentKeyTests(bool benchmark) { } } +// The pipeline journal without a GPU: the recipe codec and the store key agree with each other and +// with the canonical state, identities follow the module and the specialization, the journal +// records each pipeline once, and a replay into the recording context (the lavapipe test replays +// into a second device) rebuilds the recorded graphics pipeline so the same configuration hits the +// store, hands the compute result to its hook, recovers a tail torn inside a record's padding and +// stops when cancelled. +void pipelineJournalTests() { + using namespace AgcDriver::Graphics; + using Stage = ShaderRecompiler::ShaderStage; + const auto directory = std::filesystem::temp_directory_path() / ("aps5-pipeline-journal-mock-" + std::to_string(std::random_device{}())); + std::filesystem::create_directories(directory); + struct Cleanup { + std::filesystem::path directory; + ~Cleanup() { + std::error_code error; + std::filesystem::remove_all(directory, error); + } + } cleanup{directory}; +#ifdef _WIN32 + _putenv_s("ANYPS5_NO_SHADER_CACHE", "0"); + _putenv_s("ANYPS5_SHADER_CACHE_DIR", directory.string().c_str()); + _putenv_s("APS5_PIPELINE_JOURNAL_THREADS", "2"); +#else + setenv("ANYPS5_NO_SHADER_CACHE", "0", 1); + setenv("ANYPS5_SHADER_CACHE_DIR", directory.string().c_str(), 1); + setenv("APS5_PIPELINE_JOURNAL_THREADS", "2", 1); +#endif + Require(PipelineJournalEnabled(), "the journal is disabled with a shader cache directory set"); + const auto module = [](std::uint32_t salt) { + ShaderRecompiler::RecompileResult result; + result.spirv = std::vector{0x07230203u, 0x00010000u, 0u, 8u, 0u, 0x00020011u, 1u, 0x00020011u, 2u + salt}; + result.runtimeAbiVersion = ShaderRecompiler::RuntimeAbi::Version; + return result; + }; + { + auto first = module(1); + const auto& constFirst = first; + auto copy = first; + const auto& constCopy = copy; + Require(!constFirst.Identity().Empty() && constFirst.Identity() == constCopy.Identity() && constFirst.spirv.data() == constCopy.spirv.data(), "a copied result did not share its module and identity"); + copy.spirv[8] = 99; + Require(!(constCopy.Identity() == constFirst.Identity()) && constCopy.spirv.data() != constFirst.spirv.data() && constFirst.Identity() == module(1).Identity(), "writing a copied module did not give it an identity of its own"); + auto specialized = module(1); + specialized.specialization.push_back({3, 4}); + Require(!(specialized.Identity() == constFirst.Identity()), "specialization constants did not enter the identity"); + auto subgroup = module(1); + subgroup.hostSubgroupSize = 64; + Require(!(subgroup.Identity() == constFirst.Identity()), "the host subgroup size did not enter the identity"); + Require(ShaderRecompiler::RecompileResult{}.Identity().Empty(), "a result without a module has an identity"); + } + mock = MockVulkan{}; + auto context = mockContext(); + context.device = makeHandle(); + context.graphicsPipelineLibrary = true; + context.tessellationShader = true; + context.limits.maxTessellationPatchSize = 32; + context.limits.maxColorAttachments = 8; + context.limits.maxPushConstantsSize = 256; + context.limits.maxVertexInputBindings = 16; + context.limits.maxVertexInputAttributes = 16; + context.limits.maxFramebufferWidth = 4096; + context.limits.maxFramebufferHeight = 4096; + context.limits.maxFramebufferLayers = 1; + std::uint64_t replayedComputeIdentityLow = 0; + { + DescriptorCache cache(context); + context.descriptorCache = &cache; + auto vertex = module(1); + vertex.bindings.push_back(makeBinding(Role::GuestBuffers, 0, 1, std::vector(4))); + auto fragment = module(2); + fragment.pushConstants.resize(8); + fragment.bindings.push_back(makeBinding(Role::ShaderData, 0, 1, std::vector(4))); + fragment.bindings.back().descriptorSet = ShaderRecompiler::RuntimeAbi::FragmentDescriptorSet; + const std::array shaders{{{Stage::Vertex, &vertex, 0}, {Stage::Fragment, &fragment, PipelinePushSlotBytes}}}; + State state{}; + state.stages = {ShaderPath::Vertex, 7u, 64u, 32u, std::nullopt, std::nullopt}; + state.stages.tessellation = ShaderRecompiler::TessellationConfiguration{3, 3, 2, 1, 4, 128, 16, true}; + ColorTarget color{}; + color.address = 0x1000; + color.extent = {64, 32}; + color.format = VK_FORMAT_R8G8B8A8_UNORM; + color.bytes = 64 * 32 * 4; + color.componentMapping = 0xec; + state.colors = {color, color}; + state.colors[1].format = VK_FORMAT_B8G8R8A8_UNORM; + state.colors[1].exportIndex = 1; + state.color = color; + state.hasColorTarget = true; + VkPipelineColorBlendAttachmentState blend{}; + blend.srcColorBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA; + blend.dstColorBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA; + blend.colorWriteMask = 0xf; + state.blends = {blend, blend}; + state.blend = blend; + state.blendConstants = {0.25f, 0.5f, 0.75f, 1.0f}; + DepthTarget depth{}; + depth.address = 0x2000; + depth.format = VK_FORMAT_D32_SFLOAT; + depth.samples = 1; + state.depth = depth; + state.depthTest = true; + state.depthWrite = true; + state.depthCompare = VK_COMPARE_OP_LESS_OR_EQUAL; + state.stencilTest = true; + state.stencilFront = {VK_STENCIL_OP_KEEP, VK_STENCIL_OP_REPLACE, VK_STENCIL_OP_KEEP, VK_COMPARE_OP_ALWAYS, 0xff, 0xff, 1}; + state.stencilBack = state.stencilFront; + state.renderExtent = {64, 32}; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP; + state.viewport = {0, 32, 64, -32, 0, 1}; + state.scissor = {{0, 0}, {64, 32}}; + state.cullMode = VK_CULL_MODE_BACK_BIT; + state.frontFace = VK_FRONT_FACE_CLOCKWISE; + state.samples = 1; + const VertexInputLayout input{{{0, 16, VK_VERTEX_INPUT_RATE_VERTEX}}, {{0, 0, VK_FORMAT_R32G32B32A32_SFLOAT, 0}}}; + const auto layouts = DescribeSetLayouts(context, shaders); + Require(layouts.layouts.split, "a fragment stage with the fragment set did not split the set layouts"); + const auto recipe = MakeGraphicsPipelineRecipe(state, input, layouts.View(), shaders, VK_IMAGE_LAYOUT_GENERAL); + Require(recipe.stages.size() == 2 && recipe.stages[0].identity == vertex.Identity() && recipe.stages[1].identity == fragment.Identity() && !recipe.stages[0].generated && recipe.stages[1].pushConstantOffset == PipelinePushSlotBytes && recipe.splitSets, "the recipe does not name its stages"); + Require(recipe.state.colors.size() == 2 && recipe.state.colors[0].address == 0 && recipe.state.colors[0].componentMapping == 0xec && recipe.state.depth && recipe.state.depth->address == 0 && recipe.state.viewport.width == 0 && recipe.state.stages.registerValue == 0 && recipe.state.stages.tessellation && recipe.state.stages.tessellation->breakAtInstance, "the canonical state kept or dropped the wrong fields"); + const auto bytes = EncodeGraphicsPipelineRecipe(recipe); + GraphicsPipelineRecipe decoded; + Require(DecodeGraphicsPipelineRecipe(bytes, decoded), "the recipe does not decode"); + Require(EncodeGraphicsPipelineRecipe(decoded) == bytes, "the recipe does not round-trip byte for byte"); + for (std::size_t length = 0; length < bytes.size(); length += 7) { + GraphicsPipelineRecipe scratch; + Require(!DecodeGraphicsPipelineRecipe(std::span(bytes).first(length), scratch), "a truncated recipe decoded"); + } + std::vector original, canonical, replayed, moved, mapped; + BuildPipelineStoreKey(original, context, state, input, layouts.View(), shaders, VK_IMAGE_LAYOUT_GENERAL); + BuildPipelineStoreKey(canonical, context, recipe.state, recipe.vertexInput, layouts.View(), shaders, VK_IMAGE_LAYOUT_GENERAL); + const PipelineSetLayouts decodedLayouts{layouts.layouts.main, layouts.layouts.fragment, decoded.splitSets, decoded.mainLayoutKey, decoded.fragmentLayoutKey}; + BuildPipelineStoreKey(replayed, context, decoded.state, decoded.vertexInput, decodedLayouts, shaders, decoded.attachmentLayout); + Require(!original.empty() && original == canonical && original == replayed, "the canonical or the decoded state keys another pipeline"); + auto movedState = state; + movedState.viewport.x = 5; + movedState.colors[0].address = 0x9000; + movedState.renderLayers = 4; + BuildPipelineStoreKey(moved, context, movedState, input, layouts.View(), shaders, VK_IMAGE_LAYOUT_GENERAL); + Require(moved == original, "a field the pipeline does not read changed the key"); + auto mappedState = state; + mappedState.colors[0].componentMapping = 0x24; + BuildPipelineStoreKey(mapped, context, mappedState, input, layouts.View(), shaders, VK_IMAGE_LAYOUT_GENERAL); + Require(mapped != original, "the component mapping did not enter the key"); + std::vector unclamped, clamped, biased; + auto clampedState = state; + clampedState.depthBiasClamp = 1.0f; + BuildPipelineStoreKey(clamped, context, clampedState, input, layouts.View(), shaders, VK_IMAGE_LAYOUT_GENERAL); + Require(clamped == original, "a depth bias clamp without depth bias changed the key"); + clampedState.depthBias = true; + BuildPipelineStoreKey(clamped, context, clampedState, input, layouts.View(), shaders, VK_IMAGE_LAYOUT_GENERAL); + clampedState.depthBiasClamp = 0.0f; + BuildPipelineStoreKey(unclamped, context, clampedState, input, layouts.View(), shaders, VK_IMAGE_LAYOUT_GENERAL); + Require(unclamped != original && clamped != unclamped && CanonicalPipelineState(clampedState).depthBias && CanonicalPipelineState(mappedState).depthBiasClamp == 0.0f, "the depth bias clamp did not enter the key"); + clampedState.depthBiasClamp = 2.0f; + BuildPipelineStoreKey(biased, context, clampedState, input, layouts.View(), shaders, VK_IMAGE_LAYOUT_GENERAL); + Require(biased == clamped, "the depth bias clamp's value entered the key"); + auto compute = module(3); + compute.bindings.push_back(makeBinding(Role::GuestBuffers, 0, 2, std::vector(8))); + compute.pushConstants.resize(16); + compute.hostSubgroupSize = 32; + constexpr std::uint64_t fingerprint = 0x5eed'0000'0000'0001ull; + { + auto bound = StartPipelineJournalReplay(context, fingerprint, {}); + Require(bound != nullptr, "the first replay did not start"); + bound->Wait(); + Require(bound->Done() && PipelineJournalTotals().loadedRecipes == 0, "an absent journal loaded pipelines"); + } + const auto before = PipelineJournalTotals(); + RecordGraphicsPipeline(recipe, shaders); + RecordComputePipeline(compute, true); + FlushPipelineJournal(); + const auto recorded = PipelineJournalTotals(); + Require(recorded.recordedGraphics == before.recordedGraphics + 1 && recorded.recordedCompute == before.recordedCompute + 1 && recorded.recordedModules == before.recordedModules + 3 && recorded.writeFailures == 0, "the journal did not record one graphics and one compute pipeline with three modules"); + RecordGraphicsPipeline(recipe, shaders); + RecordComputePipeline(compute, true); + FlushPipelineJournal(); + const auto repeated = PipelineJournalTotals(); + Require(repeated.recordedGraphics == recorded.recordedGraphics && repeated.recordedCompute == recorded.recordedCompute && repeated.recordedModules == recorded.recordedModules, "a repeated pipeline was recorded again"); + Require(std::filesystem::file_size(PipelineJournalPath()) > 0, "the journal file is empty"); + const auto storeBefore = PipelineStoreTotals(); + std::vector receivedWords; + ShaderRecompiler::RecompileResult received; + bool receivedPush = false; + std::uint32_t computeCalls = 0; + PipelineJournalHooks hooks{[&](const ShaderRecompiler::RecompileResult& shader, bool pushConstants) { + ++computeCalls; + received = shader; + receivedPush = pushConstants; + return true; + }}; + auto replay = StartPipelineJournalReplay(context, fingerprint, hooks); + Require(replay != nullptr, "the replay did not start"); + replay->Wait(); + Require(replay->Done(), "the replay did not finish"); + const auto totals = PipelineJournalTotals(); + Require(totals.loadedRecipes == 2 && totals.loadedModules == 3 && totals.droppedRecords == 0, "the replay did not load the two recorded pipelines and three modules"); + Require(totals.replayedGraphics == 1 && totals.replayedCompute == 1 && totals.replayRejected == 0 && totals.replayFailed == 0, "the replay did not build one graphics and one compute pipeline"); + Require(computeCalls == 1 && receivedPush && received.Identity() == compute.Identity() && received.spirv == compute.spirv && received.bindings.size() == 1 && received.bindings[0].count == 2 && received.pushConstants.size() == 16 && received.hostSubgroupSize == 32, "the compute hook did not receive the recorded result"); + replayedComputeIdentityLow = received.Identity().low; + const auto storeReplayed = PipelineStoreTotals(); + Require(storeReplayed.misses == storeBefore.misses + 1 && storeReplayed.cached == storeBefore.cached + 1, "the replayed graphics pipeline did not enter the store"); + const auto created = mock.pipelineCreateCount; + bool built = true; + const auto pipeline = CachedPipeline(context, state, input, layouts.View(), shaders, VK_IMAGE_LAYOUT_GENERAL, &built); + const auto storeHit = PipelineStoreTotals(); + Require(pipeline != nullptr && !built && mock.pipelineCreateCount == created && storeHit.hits == storeReplayed.hits + 1 && storeHit.misses == storeReplayed.misses, "the draw's configuration did not find the replayed pipeline"); + { + const auto journalPath = PipelineJournalPath(); + const auto whole = std::filesystem::file_size(journalPath); + Require(whole % 8 == 0 && whole > 3, "the journal file is not record aligned"); + std::filesystem::resize_file(journalPath, whole - 3); + auto later = module(4); + later.bindings.push_back(makeBinding(Role::GuestBuffers, 0, 1, std::vector(4))); + RecordComputePipeline(later, false); + FlushPipelineJournal(); + Require(std::filesystem::file_size(journalPath) % 8 == 0 && PipelineJournalTotals().writeFailures == 0, "an append after a tail torn inside a record's padding left the file unaligned"); + auto torn = StartPipelineJournalReplay(context, fingerprint, hooks); + Require(torn != nullptr, "the replay after the torn tail did not start"); + torn->Wait(); + const auto reloaded = PipelineJournalTotals(); + Require(reloaded.droppedRecords == 0 && reloaded.loadedModules == 4 && reloaded.loadedRecipes == 2 && reloaded.replayFailed == 0, "the journal did not recover from a tail torn inside a record's padding"); + RecordComputePipeline(compute, true); + FlushPipelineJournal(); + Require(PipelineJournalTotals().recordedCompute == reloaded.recordedCompute + 1 && PipelineJournalTotals().recordedModules == reloaded.recordedModules, "the pipeline whose record was torn was not recorded again, or its module was"); + } + { + auto cancelled = StartPipelineJournalReplay(context, fingerprint, hooks); + Require(cancelled != nullptr, "the third replay did not start"); + cancelled->Cancel(); + cancelled->Wait(); + const auto after = PipelineJournalTotals(); + Require(cancelled->Done() && after.replayFailed == 0 && after.replayRejected == 0 && after.replayedGraphics + after.replayPresent <= totals.replayedGraphics + totals.replayPresent + 1, "a cancelled replay did not stop cleanly"); + } + ClearCachedPipelines(context.device); + } + Require(replayedComputeIdentityLow != 0, "the replayed compute identity is empty"); + Require(mock.live == 0, "the pipeline journal test leaked Vulkan objects"); +} + int main(int argc, char** argv) { #ifdef _WIN32 _putenv_s("APS5_PIN_WAIT_MS", "200"); @@ -7264,6 +7514,11 @@ int main(int argc, char** argv) { stencilReferenceUnusedTests(); return 0; } + if (argc == 2 && std::string_view(argv[1]) == "--pipeline-journal") { + pipelineJournalTests(); + std::cout << "pipeline journal recipe, record and replay tests passed\n"; + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--sparse-buffer-holes") { bool passed = false; { diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index ca6b3dad35..e132d8db4e 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -7314,10 +7314,13 @@ void pipelineCacheTests(const Device& device, bool benchmark) { input = {}; Require(lookup(input) == first, "pipeline cache retained bytes from a larger key"); vertex.variantId = 0; - const auto unidentified = lookup(input); - Require(lookup(input) != unidentified, "pipeline cache retained an unidentified shader"); + Require(lookup(input) == first, "pipeline cache keyed on the variant id instead of the module"); + vertex.spirv[2] ^= 1u; + const auto other = lookup(input); + Require(other != first && lookup(input) == other, "pipeline cache ignored another module"); + vertex.spirv[2] ^= 1u; vertex.variantId = 1; - Require(lookup(input) == first, "an unidentified shader disturbed the retained pipeline"); + Require(lookup(input) == first, "another module disturbed the retained pipeline"); ClearCachedPipelines(context.device); Require(lookup(input) != first, "clearing the pipeline cache retained its old entry"); std::cout << "Pipeline cache key and retained lifetime tests passed\n"; @@ -7789,13 +7792,13 @@ void pipelineLibraryTests(const Device& device) { Require(StagePushOffset(88, ShaderRecompiler::ShaderStage::Fragment, true) == PipelinePushSlotBytes && StagePushOffset(88, ShaderRecompiler::ShaderStage::Fragment, false) == 88 && StagePushOffset(40, ShaderRecompiler::ShaderStage::TessellationEvaluation, true) == 40, "a stage got the wrong push offset"); const auto beforeSlots = counters().built; ShaderRecompiler::RecompileResult otherVertex = vertex; - otherVertex.variantId = 14; + otherVertex.spirv[2] ^= 1u; std::array slotted{CompiledShader{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, CompiledShader{ShaderRecompiler::ShaderStage::Fragment, &fragment, PipelinePushSlotBytes}}; static_cast(CachedPipeline(context, state, input, resources, slotted, VK_IMAGE_LAYOUT_GENERAL)); slotted.front().program = &otherVertex; static_cast(CachedPipeline(context, state, input, resources, slotted, VK_IMAGE_LAYOUT_GENERAL)); Require(counters().built[2] == beforeSlots[2] + 1u && counters().built[1] == beforeSlots[1] + 1u, "a pixel shader in its fixed slot built its library again for another vertex shader"); - fragment.variantId = 13; + fragment.spirv[2] ^= 1u; static_cast(lookup()); Require(counters().built == std::array{2, 2, 3, 2}, "a new pixel shader rebuilt more than the fragment shader library"); state.colors.front().format = VK_FORMAT_R16G16B16A16_SFLOAT; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PipelineJournal.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PipelineJournal.cpp new file mode 100644 index 0000000000..16d3742761 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PipelineJournal.cpp @@ -0,0 +1,431 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Pipeline.hpp" +#include "prx/libSceAgcDriver/Graphics/include/PipelineJournal.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp" +#include "prx/libSceAgcDriver/Graphics/include/State.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" +#include "../NarrowConstantStoreFixture.hpp" +#include "Recompiler.hpp" +#include "Optimization/ResourceProgram.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::ColorTargetLayout; +using AgcDriver::Graphics::ColorTileMode; +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Width = 64; +constexpr std::uint32_t Height = 32; +constexpr std::size_t BlockBytes = 65536; +constexpr std::uint32_t Color1555Info = 0x8044u; +constexpr std::uint32_t LinearAttrib3 = 0x09000000u; +constexpr std::uint32_t Float32Export = 9; + +alignas(256) constexpr std::array VertexCode{ + 0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, +}; + +constexpr std::array, 3> Triangle{{ + {-1.0f, -1.0f, 0.5f, 1.0f}, {3.0f, -1.0f, 0.5f, 1.0f}, {-1.0f, 3.0f, 0.5f, 1.0f} +}}; + +alignas(256) std::array PixelProgram{}; +std::size_t pixelProgramWords = 0; + +std::span Program(const std::array& color) { + std::size_t at = 0; + for (std::uint32_t channel = 0; channel < 4; ++channel) { + PixelProgram[at++] = 0x7e0002ffu | ((4u + channel) << 17u); + PixelProgram[at++] = std::bit_cast(color[channel]); + } + PixelProgram[at++] = 0xf800180fu; + PixelProgram[at++] = 0x07060504u; + PixelProgram[at++] = 0xbf810000u; + pixelProgramWords = at; + return std::span(PixelProgram.data(), at); +} + +class Block { +public: + explicit Block(std::size_t bytes) : bytes(bytes), watched(AgcDriver::GuestMemory::WriteWatched()) { +#ifdef _WIN32 + if (watched) { + data = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(bytes, BlockBytes)); + GuestArena::GuestArenaCommit_nid_postfix(data, bytes, PAGE_READWRITE, bytes); + } else { + data = static_cast(VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); + } +#else + if (watched) { + void* raw = mmap(nullptr, bytes + BlockBytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + Require(raw != MAP_FAILED, "cannot map the guest block"); + const auto begin = reinterpret_cast(raw); + const auto aligned = (begin + BlockBytes - 1) & ~(static_cast(BlockBytes) - 1); + if (aligned != begin) munmap(raw, aligned - begin); + if (aligned + bytes != begin + bytes + BlockBytes) munmap(reinterpret_cast(aligned + bytes), begin + BlockBytes - aligned); + data = reinterpret_cast(aligned); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(data, bytes); + } else { + data = static_cast(std::aligned_alloc(BlockBytes, bytes)); + } +#endif + Require(data != nullptr, "cannot allocate the guest block"); + GuestAllocations::Mutation mutation; + mutation.Add(data, bytes, true, true, true); + } + + ~Block() { + AgcDriver::Graphics::StorageTexture::FlushPending(Address(), bytes, nullptr, "test release"); + { + GuestAllocations::Mutation mutation; + mutation.Remove(data); + } +#ifdef _WIN32 + if (watched) { + GuestArena::GuestArenaReset_nid_postfix(data, bytes); + GuestArena::GuestArenaRelease_nid_postfix(data, bytes); + } else { + VirtualFree(data, 0, MEM_RELEASE); + } +#else + if (watched) { + munmap(data, bytes); + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(data, bytes); + } else { + std::free(data); + } +#endif + } + + Block(const Block&) = delete; + Block& operator=(const Block&) = delete; + std::uint64_t Address() const { return reinterpret_cast(data); } + std::size_t bytes; + bool watched; + std::uint8_t* data = nullptr; +}; + +AgcDriver::Graphics::ColorTarget DecodeTarget(const Block& block) { + AgcDriver::Registers cx; + cx[0x318] = static_cast(block.Address() >> 8u); + cx[0x31b] = 0; + cx[0x31c] = Color1555Info; + cx[0x31d] = 0; + cx[0x3b0] = ((Width - 1u) << 14u) | (Height - 1u); + cx[0x3b8] = LinearAttrib3; + cx[0x390] = static_cast(block.Address() >> 40u); + auto color = AgcDriver::Graphics::DecodeColorBuffer(cx, 0); + Require(color.format == VK_FORMAT_A1R5G5B5_UNORM_PACK16 && color.bytes == block.bytes, "the test target did not decode as an A1R5G5B5 surface"); + return color; +} + +void Draw(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::ColorTarget& color, std::span pixelCode) { + const auto target = device.Target(); + constexpr std::uint32_t waveSize = 64; + std::vector vertexUserData(4, 0u); + const auto address = reinterpret_cast(Triangle.data()); + const std::array vertexBuffer{static_cast(address), static_cast((address >> 32u) & 0xffffu) | (16u << 16u), static_cast(Triangle.size()), 0x01016facu}; + std::copy(vertexBuffer.begin(), vertexBuffer.end(), vertexUserData.begin()); + const std::array vertexMemory{{{reinterpret_cast(VertexCode.data()), std::as_bytes(std::span(VertexCode))}}}; + ShaderRecompiler::RecompileRequest vertex{ + {ShaderStage::Vertex, reinterpret_cast(VertexCode.data()), VertexCode, 0, {}}, + {waveSize, 0, vertexUserData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, vertexMemory}, + target, + {0, 0, 0, 64} + }; + vertex.useCache = false; + const auto vertexResult = ShaderRecompiler::Recompile(vertex); + const auto vertexPush = static_cast(vertexResult.pushConstants.size()); + + AgcDriver::Graphics::State state{}; + state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, waveSize, waveSize, std::nullopt, std::nullopt}; + state.colors = {color}; + state.color = color; + state.hasColorTarget = true; + state.renderExtent = {Width, Height}; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.viewport = {0, static_cast(Height), static_cast(Width), -static_cast(Height), 0, 1}; + state.negativeOneToOne = false; + state.scissor = {{0, 0}, {Width, Height}}; + state.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + VkPipelineColorBlendAttachmentState blend{}; + blend.colorWriteMask = 0xf; + state.blends = {blend}; + state.blend = blend; + state.blendConstants = {}; + + const auto codeAddress = reinterpret_cast(pixelCode.data()); + const AgcDriver::Registers context{{0x1b6, 0x8000}, {0x1b3, 0}, {0x1b4, 0}, {0x203, 0}, {0x1c5, Float32Export}}; + const std::array pixelMemory{{{codeAddress, std::as_bytes(pixelCode)}}}; + ShaderRecompiler::RecompileRequest fragment{ + {ShaderStage::Fragment, codeAddress, pixelCode, 0, {}}, + {waveSize, 0, {}, std::nullopt, AgcDriver::Graphics::DecodePixelStageInfo(context, {}), std::nullopt, pixelMemory}, + target, + PixelPushLayout(vertexPush, target) + }; + const auto handle = ShaderRecompiler::PrepareShader(fragment); + AgcDriver::DriverDetail::ShaderSnapshot snapshot{codeAddress, 0, 1, {pixelCode.begin(), pixelCode.end()}, {}}; + snapshot.prepared->entries.push_back({0, handle}); + fragment.shader.code = snapshot.code; + fragment.context.pixel = AgcDriver::Graphics::DecodePixelStageInfo(context, AgcDriver::Graphics::ExportMappings(state)); + const ShaderRecompiler::SrtRuntime runtime{}; + const auto invocation = AgcDriver::DriverDetail::InvocationFor(snapshot, 0, fragment); + const auto pixelResult = invocation.Materialize(*invocation.Capture(runtime)); + const std::array shaders{{ + {ShaderStage::Vertex, &vertexResult, 0}, + {ShaderStage::Fragment, pixelResult.get(), PixelPushOffset(vertexPush, target)} + }}; + const AgcDriver::Pm4::DrawParameters draw{0, static_cast(Triangle.size()), 0, 1, 0, false}; + device.Draw(state, draw, shaders); +} + +void ExpectTexel(AgcDriver::VulkanDevice& device, const Block& block, std::uint16_t texel, const char* what) { + AgcDriver::Graphics::StorageTexture::FlushPending(block.Address(), block.bytes, nullptr, "test read-back"); + device.WaitIdle(); + std::vector stored(block.bytes / 2u); + AgcDriver::GuestMemory::Read(block.Address(), std::as_writable_bytes(std::span(stored)), 1); + const ColorTargetLayout layout(Width, Height, ColorTileMode::Linear, 2); + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + const auto index = layout.Offset(x, y) / 2u; + if (stored[index] == texel) continue; + char detail[160]; + std::snprintf(detail, sizeof(detail), ": pixel (%u, %u) holds 0x%04x, expected 0x%04x", x, y, stored[index], texel); + throw std::runtime_error(std::string(what) + detail); + } + } +} + +// The constant-store compute program with its two stored literals replaced: `salt` 0 is the +// fixture's program, every other value a program of its own with its own module. +std::vector ComputeCode(std::uint32_t salt) { + std::vector code(NarrowConstantStoreFixture::Code.begin(), NarrowConstantStoreFixture::Code.end()); + if (salt != 0) { + code[3] = 0x00a10200u ^ (salt * 0x01010101u); + code[5] = 0x0000fec3u ^ (salt * 0x00010001u); + } + return code; +} + +ShaderRecompiler::RecompileResult CompileCompute(AgcDriver::VulkanDevice& device, const std::vector& code, std::span userData, std::uint32_t waveSize) { + using namespace ShaderRecompiler; + const std::span words(code); + const std::array memory{{{reinterpret_cast(words.data()), std::as_bytes(words)}}}; + const ShaderComputeStageInfo compute{{NarrowConstantStoreFixture::Threads, 1, 1}, 0, {false, false, false}, false, 1, {}}; + RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(words.data()), words, 0, {}}, + {waveSize, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), {0, 0, 0, 128}, std::nullopt, false, + }; + return Recompile(request); +} + +void ExpectConstantStore(const Block& block, const char* what) { + using namespace NarrowConstantStoreFixture; + for (std::size_t offset = 0; offset < Threads * LaneBytes; ++offset) { + const auto expected = ExpectedLane[offset % LaneBytes]; + Require(block.data[offset] == expected, std::string(what) + ": byte " + std::to_string(offset) + " is " + std::to_string(block.data[offset]) + ", expected " + std::to_string(expected)); + } +} + +std::array AddressWords(const Block& block) { + const auto address = block.Address(); + return {static_cast(address), static_cast(address >> 32u)}; +} + +void SetEnvironment(const char* name, const std::string& value) { +#ifdef _WIN32 + _putenv_s(name, value.c_str()); +#else + setenv(name, value.c_str(), 1); +#endif +} + +struct Totals { + AgcDriver::Graphics::PipelineJournalCounters journal; + AgcDriver::Graphics::PipelineStoreCounters store; +}; + +Totals Snapshot() { + return {AgcDriver::Graphics::PipelineJournalTotals(), AgcDriver::Graphics::PipelineStoreTotals()}; +} + +void JournalTests() { + using namespace NarrowConstantStoreFixture; + Require(AgcDriver::Graphics::PipelineJournalEnabled(), "the pipeline journal is disabled although the shader cache directory is set"); + const auto journalPath = AgcDriver::Graphics::PipelineJournalPath(); + Block compute(BlockBytes); + Block target(ColorTargetLayout(Width, Height, ColorTileMode::Linear, 2).Bytes()); + const auto computeCode = ComputeCode(0); + const std::array red{1.0f, 0.0f, 0.0f, 1.0f}; + constexpr std::uint16_t redTexel = 0x801f; + { + const auto device = OpenVulkanTestDevice(); + Require(device != nullptr, "the first device is required once the second opened"); + device->WaitForPipelineJournalReplay(); + const auto before = Snapshot(); + Require(before.journal.loadedRecipes == 0 && before.journal.replayedGraphics == 0 && before.journal.replayedCompute == 0, "an empty journal replayed pipelines"); + std::memset(compute.data, Fill, compute.bytes); + const auto words = AddressWords(compute); + const auto result = CompileCompute(*device, computeCode, words, 32); + device->Dispatch(result, 1, 1, 1, {}, reinterpret_cast(computeCode.data())); + device->WaitIdle(); + ExpectConstantStore(compute, "first run: the constant store dispatch"); + std::memset(target.data, 0, target.bytes); + Draw(*device, DecodeTarget(target), Program(red)); + ExpectTexel(*device, target, redTexel, "first run: the draw"); + Require(device->ComputePipelinesCreated() == 1, "the first run did not build exactly one compute pipeline"); + AgcDriver::Graphics::FlushPipelineJournal(); + const auto after = Snapshot(); + Require(after.store.misses == before.store.misses + 1 && after.store.hits == before.store.hits, "the first draw did not miss the pipeline store exactly once"); + Require(after.journal.recordedGraphics == 1 && after.journal.recordedCompute == 1 && after.journal.recordedModules == 3 && after.journal.writeFailures == 0, "the first run did not record one graphics and one compute pipeline with three modules"); + Draw(*device, DecodeTarget(target), Program(red)); + device->WaitIdle(); + AgcDriver::Graphics::FlushPipelineJournal(); + const auto repeated = Snapshot(); + Require(repeated.store.hits == after.store.hits + 1 && repeated.store.misses == after.store.misses, "a repeated draw did not hit the pipeline store"); + Require(repeated.journal.recordedGraphics == 1 && repeated.journal.recordedModules == 3, "a repeated draw recorded its pipeline again"); + } + Require(std::filesystem::exists(journalPath) && std::filesystem::file_size(journalPath) > 0, "the journal file was not written"); + { + const auto device = OpenVulkanTestDevice(); + Require(device != nullptr, "the second device could not be opened"); + device->WaitForPipelineJournalReplay(); + const auto replayed = Snapshot(); + Require(replayed.journal.loadedRecipes == 2 && replayed.journal.loadedModules == 3 && replayed.journal.droppedRecords == 0, "the second run did not load the two recorded pipelines and their three modules"); + Require(replayed.journal.replayedGraphics == 1 && replayed.journal.replayedCompute == 1 && replayed.journal.replayRejected == 0 && replayed.journal.replayFailed == 0 && replayed.journal.replayPresent == 0, "the replay did not build one graphics and one compute pipeline"); + Require(device->ComputePipelinesCreated() == 1, "the replay did not build the compute pipeline on the new device"); + std::memset(compute.data, Fill, compute.bytes); + const auto words = AddressWords(compute); + const auto result = CompileCompute(*device, computeCode, words, 32); + device->Dispatch(result, 1, 1, 1, {}, reinterpret_cast(computeCode.data())); + device->WaitIdle(); + ExpectConstantStore(compute, "second run: the constant store dispatch"); + Require(device->ComputePipelinesCreated() == 1, "the second run's dispatch built a compute pipeline the replay had built"); + std::memset(target.data, 0, target.bytes); + Draw(*device, DecodeTarget(target), Program(red)); + ExpectTexel(*device, target, redTexel, "second run: the draw through the replayed pipeline"); + const auto drawn = Snapshot(); + Require(drawn.store.hits == replayed.store.hits + 1 && drawn.store.misses == replayed.store.misses, "the second run's draw did not find the replayed pipeline"); + AgcDriver::Graphics::FlushPipelineJournal(); + const auto final = Snapshot(); + Require(final.journal.recordedGraphics == 1 && final.journal.recordedCompute == 1 && final.journal.recordedModules == 3, "the second run recorded pipelines the journal already held"); + } +} + +// --benchmark [count]: `count` distinct compute programs, dispatched for the first time on three +// devices in turn. The first device starts without the driver's pipeline cache file (a driver with +// an on-disk shader cache of its own, lavapipe's Mesa cache here, still serves from it unless that +// is disabled too, MESA_SHADER_CACHE_DISABLE=true, where the driver's compile dominates); it leaves +// the driver's cache and the journal warm. The second replays the journal before its dispatches. +// The third dispatches with the driver's cache warm but the journal file removed: what a warm run +// pays at each first use without the journal. Wall times only; no pass threshold. +void Benchmark(std::uint32_t count) { + using namespace NarrowConstantStoreFixture; + Require(AgcDriver::Graphics::PipelineJournalEnabled(), "the pipeline journal is disabled"); + Block compute(BlockBytes); + const auto words = AddressWords(compute); + std::vector> codes; + for (std::uint32_t salt = 1; salt <= count; ++salt) codes.push_back(ComputeCode(salt)); + struct Run { + double openMs = 0; + double dispatchMs = 0; + std::uint64_t created = 0; + AgcDriver::Graphics::PipelineJournalCounters journal; + }; + const auto run = [&](const char* what) { + Run measured; + const auto openedAt = std::chrono::steady_clock::now(); + const auto device = OpenVulkanTestDevice(); + Require(device != nullptr, std::string("no device for the benchmark's ") + what); + device->WaitForPipelineJournalReplay(); + measured.openMs = std::chrono::duration(std::chrono::steady_clock::now() - openedAt).count(); + measured.journal = AgcDriver::Graphics::PipelineJournalTotals(); + std::vector results; + for (const auto& code : codes) results.push_back(CompileCompute(*device, code, words, 32)); + const auto createdBefore = device->ComputePipelinesCreated(); + const auto started = std::chrono::steady_clock::now(); + for (std::size_t index = 0; index < results.size(); ++index) device->Dispatch(results[index], 1, 1, 1, {}, reinterpret_cast(codes[index].data())); + device->WaitIdle(); + measured.dispatchMs = std::chrono::duration(std::chrono::steady_clock::now() - started).count(); + measured.created = device->ComputePipelinesCreated() - createdBefore; + AgcDriver::Graphics::FlushPipelineJournal(); + return measured; + }; + const auto cold = run("cold run"); + const auto replayed = run("replayed run"); + std::error_code error; + std::filesystem::remove(AgcDriver::Graphics::PipelineJournalPath(), error); + Require(!error, "cannot remove the journal for the benchmark's last run"); + const auto warm = run("warm run without the journal"); + std::printf("pipeline journal benchmark: %u compute programs, %u replay threads\n", count, replayed.journal.replayThreads); + std::printf(" cold driver cache, no journal: %llu pipelines built at their dispatches, %u dispatches in %.1f ms (%.2f ms each)\n", static_cast(cold.created), count, cold.dispatchMs, cold.dispatchMs / count); + std::printf(" warm driver cache, no journal: %llu pipelines built at their dispatches, %u dispatches in %.1f ms (%.2f ms each)\n", static_cast(warm.created), count, warm.dispatchMs, warm.dispatchMs / count); + std::printf(" warm driver cache, journal: %llu pipelines built by the replay in %.1f ms of replay work (%.1f ms from the device opening), then %llu built at their dispatches, %u dispatches in %.1f ms (%.2f ms each)\n", static_cast(replayed.journal.replayedCompute), replayed.journal.replayMs, replayed.openMs, static_cast(replayed.created), count, replayed.dispatchMs, replayed.dispatchMs / count); +} + +} + +int main(int argc, char** argv) { + try { + const auto directory = std::filesystem::temp_directory_path() / ("aps5-pipeline-journal-test-" + std::to_string(std::random_device{}())); + std::filesystem::create_directories(directory); + SetEnvironment("ANYPS5_NO_SHADER_CACHE", "0"); + SetEnvironment("ANYPS5_SHADER_CACHE_DIR", directory.string()); + SetEnvironment("APS5_NO_PIPELINE_JOURNAL", "0"); + SetEnvironment("APS5_VERIFY_SET_LAYOUTS", "1"); + SetEnvironment("APS5_PIPELINE_JOURNAL_THREADS", "2"); + struct Cleanup { + std::filesystem::path directory; + ~Cleanup() { + std::error_code error; + std::filesystem::remove_all(directory, error); + } + } cleanup{directory}; + { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + } + if (argc >= 2 && std::string(argv[1]) == "--benchmark") { + Benchmark(argc >= 3 ? static_cast(std::strtoul(argv[2], nullptr, 10)) : 24u); + return 0; + } + JournalTests(); + std::puts("pipeline journal record and replay passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/shader/recompiler/CMakeLists.txt b/core/shader/recompiler/CMakeLists.txt index 18cea782dc..a14e40ce48 100644 --- a/core/shader/recompiler/CMakeLists.txt +++ b/core/shader/recompiler/CMakeLists.txt @@ -255,6 +255,12 @@ function(add_shader_recompiler target runtimeTarget) SetDebugProbeActive ShaderCacheDirectory ShaderStage + HashSpirvWords + ShaderDiskCache::DecodeResult + ShaderDiskCache::Enabled + ShaderDiskCache::EncodeResult + ShaderDiskCache::EntryDirectory + ShaderDiskCache::SourceVersion SrgbDecodeBit SrgbUnormFormat FindUserDataTail diff --git a/core/shader/recompiler/Recompiler.hpp b/core/shader/recompiler/Recompiler.hpp index ff93352beb..b66f1cd695 100644 --- a/core/shader/recompiler/Recompiler.hpp +++ b/core/shader/recompiler/Recompiler.hpp @@ -5,6 +5,7 @@ #include "PipelineSpecialization.hpp" #include #include +#include #include #include #include @@ -468,25 +469,64 @@ struct BarycentricEmulationLayout { [[nodiscard]] BarycentricEmulationLayout LayoutBarycentricEmulation(std::span parameters, const BarycentricEmulation& emulation); +// A 128-bit hash of a module's words (SharedSpirv::ContentHash), zero for an empty module. The +// mixing is pure integer arithmetic over the words in order, so the value is the same on every run, +// platform and compiler: a pipeline journal names modules by it across runs. +struct SpirvContentHash { + std::uint64_t low = 0; + std::uint64_t high = 0; + bool operator==(const SpirvContentHash&) const = default; +}; + +inline std::uint64_t HashSpirvWords(std::span words, std::uint64_t seed) { + constexpr std::uint64_t multiplier = 0x9e3779b97f4a7c15ull; + std::uint64_t hash = seed ^ (static_cast(words.size()) * multiplier); + const auto mix = [&](std::uint64_t chunk) { + chunk *= 0xff51afd7ed558ccdull; + chunk ^= chunk >> 32u; + hash = (hash ^ chunk) * multiplier; + hash = (hash << 27u) | (hash >> 37u); + }; + std::size_t index = 0; + for (; index + 2 <= words.size(); index += 2) mix(words[index] | (static_cast(words[index + 1]) << 32u)); + if (index < words.size()) mix(words[index] ^ 0x8000000000000000ull); + hash ^= hash >> 33u; + hash *= 0xff51afd7ed558ccdull; + hash ^= hash >> 33u; + hash *= 0xc4ceb9fe1a85ec53ull; + hash ^= hash >> 33u; + return hash; +} + // Compiled SPIR-V shared between a cached variant and every result materialized from it: results // are copied per dispatch and draw, so the words are reference counted and only duplicated when a // holder writes to them (tests and tools patch modules in place). Reads look like a vector. // The non-const data(), operator[], begin() and end() count as writes: on a shared holder (every // cached result) they clone the module, so a consumer that only reads takes the result by const // reference, or the per-dispatch copy this class removes comes back without a compiler hint. +// ContentHash is computed once per shared holder, on first use, and dropped by a write. class SharedSpirv { public: SharedSpirv() = default; - SharedSpirv(std::vector words) : words(std::make_shared>(std::move(words))) {} - SharedSpirv(std::shared_ptr owner, std::span view) : owner(std::move(owner)), view(view) {} + SharedSpirv(std::vector words) : holder(std::make_shared(std::move(words), nullptr, std::span{})) {} + SharedSpirv(std::shared_ptr owner, std::span view) : holder(std::make_shared(std::vector{}, std::move(owner), view)) {} SharedSpirv& operator=(std::vector other) { - words = std::make_shared>(std::move(other)); - owner.reset(); - view = {}; + holder = std::make_shared(std::move(other), nullptr, std::span{}); return *this; } - [[nodiscard]] std::span Words() const { return words ? std::span(*words) : view; } + [[nodiscard]] std::span Words() const { return holder ? holder->Words() : std::span{}; } + [[nodiscard]] SpirvContentHash ContentHash() const { + if (holder == nullptr) return {}; + if (holder->hashed.load(std::memory_order_acquire)) return {holder->hashLow.load(std::memory_order_relaxed), holder->hashHigh.load(std::memory_order_relaxed)}; + const auto words = holder->Words(); + if (words.empty()) return {}; + const SpirvContentHash hash{HashSpirvWords(words, 0x5eed5a1d00000001ull), HashSpirvWords(words, 0x5eed5a1d00000002ull)}; + holder->hashLow.store(hash.low, std::memory_order_relaxed); + holder->hashHigh.store(hash.high, std::memory_order_relaxed); + holder->hashed.store(true, std::memory_order_release); + return hash; + } [[nodiscard]] std::vector Copy() const { return {Words().begin(), Words().end()}; } [[nodiscard]] std::size_t size() const { return Words().size(); } [[nodiscard]] bool empty() const { return Words().empty(); } @@ -503,18 +543,27 @@ public: friend bool operator==(const SharedSpirv& left, const SharedSpirv& right) { return std::ranges::equal(left.Words(), right.Words()); } private: + struct Holder { + Holder(std::vector words, std::shared_ptr owner, std::span view) : words(std::move(words)), owner(std::move(owner)), view(view) {} + [[nodiscard]] std::span Words() const { return owner ? view : std::span(words); } + std::vector words; + std::shared_ptr owner; + std::span view; + mutable std::atomic hashLow{0}; + mutable std::atomic hashHigh{0}; + mutable std::atomic hashed{false}; + }; + std::vector& Mutable() { - if (words == nullptr || words.use_count() != 1) { - words = std::make_shared>(Copy()); - owner.reset(); - view = {}; + if (holder == nullptr || holder.use_count() != 1 || holder->owner != nullptr) { + holder = std::make_shared(Copy(), nullptr, std::span{}); + } else { + holder->hashed.store(false, std::memory_order_relaxed); } - return *words; + return holder->words; } - std::shared_ptr> words; - std::shared_ptr owner; - std::span view; + std::shared_ptr holder; }; struct VertexInput { @@ -576,11 +625,43 @@ struct ShaderInvocation { std::uint32_t imageTableFaults = 0; }; +// What a Vulkan pipeline built from a result depends on besides the fixed-function state and the +// descriptor set layouts: the module words, the specialization constants handed to the driver, the +// subgroup width the module was laid out for, the workgroup memory slice and the bindings' shape. +// Two results with equal identities build equal pipelines, in this run or another: unlike the +// process-lifetime variant ids, the identity is the same on every run, so the pipeline stores key on +// it and a pipeline journal can name a stage across runs. Empty for a result without a module. +struct PipelineIdentity { + std::uint64_t low = 0; + std::uint64_t high = 0; + [[nodiscard]] bool Empty() const { return low == 0 && high == 0; } + bool operator==(const PipelineIdentity&) const = default; + auto operator<=>(const PipelineIdentity&) const = default; +}; + struct RecompileResult : CompiledShaderArtifact, ShaderInvocation { bool cacheHit = false; std::uint32_t workgroupMemoryDwords = 0; std::vector shaderDataUserWords; [[nodiscard]] std::uint64_t PipelineVariantId() const { return specializationId != 0 ? specializationId : variantId; } + [[nodiscard]] PipelineIdentity Identity() const { + const auto hash = spirv.ContentHash(); + if (hash == SpirvContentHash{}) return {}; + PipelineIdentity identity{hash.low, hash.high}; + const auto mix = [&](std::uint64_t value) { + identity.low = (identity.low ^ value) * 0x9e3779b97f4a7c15ull; + identity.low ^= identity.low >> 29u; + identity.high = (identity.high ^ (value + 0x7f4a7c159e3779b9ull)) * 0xc2b2ae3d27d4eb4full; + identity.high ^= identity.high >> 31u; + }; + mix(specialization.size()); + for (const auto& constant : specialization) mix(constant.id | (static_cast(constant.value) << 32u)); + mix(hostSubgroupSize | (static_cast(workgroupMemoryDwords) << 32u)); + mix(bindings.size()); + for (const auto& binding : bindings) mix(binding.descriptorSet | (static_cast(binding.binding) << 16u) | (static_cast(binding.kind) << 40u) | (static_cast(binding.count) << 48u)); + if (identity.Empty()) identity.low = 1; + return identity; + } }; [[nodiscard]] RecompileResult Recompile(const RecompileRequest& request); diff --git a/core/shader/recompiler/ShaderDiskCache.cpp b/core/shader/recompiler/ShaderDiskCache.cpp index de5f1e4f0a..3c288e94b6 100644 --- a/core/shader/recompiler/ShaderDiskCache.cpp +++ b/core/shader/recompiler/ShaderDiskCache.cpp @@ -84,9 +84,9 @@ std::filesystem::path ShaderCacheDirectory() { namespace ShaderRecompiler::ShaderDiskCache { #if defined(__linux__) && defined(__x86_64__) && defined(__GLIBCXX__) -static_assert(sizeof(CompiledShaderArtifact) == 264, "CompiledShaderArtifact changed: update the artifact encoder"); +static_assert(sizeof(CompiledShaderArtifact) == 232, "CompiledShaderArtifact changed: update the artifact encoder"); static_assert(sizeof(ShaderInvocation) == 200, "ShaderInvocation changed: update the invocation encoder"); -static_assert(sizeof(RecompileResult) == 496, "RecompileResult changed: update EncodeResult and DecodeResult"); +static_assert(sizeof(RecompileResult) == 464, "RecompileResult changed: update EncodeResult and DecodeResult"); static_assert(sizeof(DescriptorBinding) == 592, "DescriptorBinding changed: update the binding encoder"); static_assert(sizeof(VertexAttribute) == 32, "VertexAttribute changed: update the attribute encoder"); static_assert(sizeof(VertexInput) == 16, "VertexInput changed: update the vertex input encoder"); @@ -1697,8 +1697,8 @@ void EncodeResult(const RecompileResult& result, std::vector& out) { writer.Values(std::span(result.shaderDataUserWords)); } -bool DecodeResult(std::span bytes, RecompileResult& result) { - Reader reader(bytes); +bool DecodeResult(std::span bytes, RecompileResult& result, std::shared_ptr owner) { + Reader reader(bytes, std::move(owner)); decodeArtifact(reader, result); decodeInvocation(reader, result); reader.Value(result.workgroupMemoryDwords); diff --git a/core/shader/recompiler/ShaderDiskCache.hpp b/core/shader/recompiler/ShaderDiskCache.hpp index b03d85a809..7322efa496 100644 --- a/core/shader/recompiler/ShaderDiskCache.hpp +++ b/core/shader/recompiler/ShaderDiskCache.hpp @@ -53,7 +53,7 @@ void BuildKey(const RecompileRequest& request, std::uint32_t hostSubgroupSize, s [[nodiscard]] std::string EntryName(std::span key); void EncodeResult(const RecompileResult& result, std::vector& out); -[[nodiscard]] bool DecodeResult(std::span bytes, RecompileResult& result); +[[nodiscard]] bool DecodeResult(std::span bytes, RecompileResult& result, std::shared_ptr owner = nullptr); [[nodiscard]] std::vector EncodeEntry(std::span key, const CompiledVariant& variant); [[nodiscard]] LoadStatus DecodeEntry(std::span file, std::span key, CompiledVariant& variant, std::shared_ptr owner = nullptr); diff --git a/docs/dev/ARCHITECTURE.md b/docs/dev/ARCHITECTURE.md index 0a9a669545..d3f5deabad 100644 --- a/docs/dev/ARCHITECTURE.md +++ b/docs/dev/ARCHITECTURE.md @@ -95,6 +95,7 @@ flowchart LR - [`Recompiler.cpp`](../../core/shader/recompiler/Recompiler.cpp) runs the stages in this order. With `ANYPS5_ENABLE_SPIRV_TOOLS`, the SPIR-V is also validated and optimized with SPIRV-Tools. - `ShaderRecompiler::Recompile` keeps compiled variants in memory, and `ShaderDiskCache` stores them on disk so later runs reuse them. Before the writable cache, a miss is also looked up in read-only prebuilt packs and loose entry folders under `shader_cache/prebuilt`; pack entries are ordinary encoded cache entries behind a sorted key-hash index, validated by magic, compiler source version and index checksum when mapped, and a miss or rejected entry falls through to the writable cache and the compiler. Cache format 35 also stores the finite stencil-reference set, its replay-safety proof and reserved push-data word in the artifact and binding layouts. Entries from formats 31 to 34 are rejected. +- The Vulkan pipelines are journaled as well ([`PipelineJournal.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp)): a graphics pipeline the store builds (`CachedPipeline`) or a compute pipeline a dispatch builds is appended to `pipelines.journal` next to the shader cache entries, as the materialized result of each stage (the specialized SPIR-V, bindings and push constants, through `ShaderDiskCache::EncodeResult`) plus the recipe the pipeline was built from (`GraphicsPipelineRecipe`: the stages' identities, vertex input layout, descriptor set layout keys and the state fields the `Pipeline` constructor reads, `CanonicalPipelineState`). When a `VulkanDevice` is created, the journal is loaded and its pipelines are built on a pool of `hardware_concurrency() / 2` threads (`APS5_PIPELINE_JOURNAL_THREADS`) while the title loads, so the first draw or dispatch of a configuration seen on an earlier run finds its pipeline resident. The pipeline stores key on `RecompileResult::Identity`, a hash of the module words, specialization constants, subgroup width and bindings that is the same on every run (`SharedSpirv::ContentHash`, computed once per shared module), rather than on the process-lifetime variant ids, and a graphics pipeline is built outside the store's mutex, so the replay threads and the queue workers build in parallel. A draw that arrives before its replayed entry builds the pipeline itself; the first inserted wins. A replayed entry enters the store at its least recently used end and a full store takes none, so the title's own pipelines are never evicted for a replayed one, and the optimized relink of a replayed pipeline (`OptimizedPipeline::Schedule`) waits for the first draw that binds it, so the relink thread serves the title's pipelines first. A device created without a window starts its replay at its first draw or dispatch rather than at creation, since `Driver::AttachWindow` usually replaces it before the title draws. The journal records a pipeline once (modules and recipes are deduplicated by content), keeps at most `APS5_PIPELINE_JOURNAL_MAX_MIB` (1024), replays the first `APS5_PIPELINE_JOURNAL_ENTRIES` recorded (1024, the pipeline store's bound, `APS5_PIPELINE_STORE_ENTRIES`; a run reaches those first), and is disabled with `APS5_NO_PIPELINE_JOURNAL=1` or without a shader cache directory. The file header names the device (`PipelineJournalDeviceFingerprint`, the properties the driver's pipeline cache file is named by), so a journal written for another GPU or driver is started over rather than replayed; appends and reads hold `pipelines.journal.lock` against another process sharing the directory, and a device cancels its replay before it is replaced (`PrepareForReplacement`) or destroyed, so a replaced headless device never inserts entries the window's device then sweeps. `APS5_PROFILE_DRAW=1` prints a `[pipeline-journal]` summary when the replay finishes and at teardown. - A compute shader whose LDS plus its lock dword exceeds the host's `maxComputeSharedMemorySize` is not given a `Workgroup` array. `Recompile` sets `ShaderInfo::sharedMemoryBytes` for it and the SPIR-V backend addresses the LDS in a coherent storage buffer at descriptor set 1 (`WorkgroupMemoryDescriptorSet`), with a slice of `RecompileResult::workgroupMemoryDwords` per workgroup, indexed by `WorkgroupID` and `NumWorkGroups`. `VulkanDevice` keeps one such buffer, sized from the dispatch dimensions, and binds it after the resource set; programs that fit keep using shared memory. - `Driver::RegisterShader` splits shader preparation in two. `PlanRegistered` runs on the registering guest thread: it validates the registers and entry point, decodes the code and builds the recompile request, so invalid registrations still fail there. The RDNA to IR translation and resource plan (`PrepareShader`) run on a background pool of `hardware_concurrency() / 2` detached threads (at least 2, 64 MiB stacks). - Queue 0's packet worker decodes, captures and prepares a draw and hands its recording (`Graphics::Draw` under the GPU lock) to a committer thread in [`DrawPipeline.cpp`](../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp), so the next packet is decoded while the previous draw is recorded. Each queued item carries the guest ranges it will write (`DrawWriteRanges`: written buffers and storage images, color targets with their DCC and CMASK, depth and stencil). A later packet that reads or writes one of those ranges, or that the pipeline can't order, drains it first; labels, submission ends, buffer fills and (with `APS5_PIPELINE_DMA=1`) DMA packets are queued as items instead. An exception thrown by a commit is rethrown on the worker at its next enqueue or drain, as an inline draw error would be. On by default; `APS5_PIPELINED_DRAWS=0` records every draw inline, and `APS5_EXACT_DRAW_WRITES=0` widens the color and depth ranges to estimates. diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 7e4ffe410b..d5c0f17655 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -64,6 +64,7 @@ - [Covered-centroid raster state](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) accepts PA_SC_AA_CONFIG.COVERED_CENTROID_IS_CENTER only when the pixel program is skipped or SPI_PS_INPUT_ENA and SPI_PS_INPUT_ADDR have no active perspective or linear centroid input. Bit 29 follows [PS5_Mesa GFX10.3 registers](https://github.com/mihawk-99/PS5_Mesa/blob/0b2d6d1a61d9bbf89cf8beb88a696144f67c61f8/src/amd/registers/gfx103.json); [RADV](https://github.com/mihawk-99/PS5_Mesa/blob/0b2d6d1a61d9bbf89cf8beb88a696144f67c61f8/src/amd/vulkan/radv_cmd_buffer.c) sets it on GFX10.3 and later. Active centroid interpolation and unspecified input registers still throw. Console covered-pixel centroid results were not measured. - [Compute LDS beyond the device's shared-memory limit](../../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) (libSceAgcDriver) lives in a coherent storage buffer with one slice of `LDS bytes / 4 + 1` dwords per workgroup (the last dword is the atomic lock), shared by all such dispatches and grown in 1 MiB steps up to `maxStorageBufferRange`; a dispatch whose slices do not fit throws. It is slower than shared memory and its cost was not measured. An indirect dispatch of such a program always reads its group counts on the CPU. Its results are pinned against the in-limit semantics in `agc_driver_lds_beyond_device_limit`, not measured on hardware. - [AGC buffer allocations](../../../core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp) retry host/device out-of-memory once after reclaiming idle pool buffers and fully unused slab blocks. Live buffers and blocks with live slots are retained. Live images and other retained resources have no global memory bound; allocation failure still throws when reclaim cannot satisfy the request. +- [Pipeline journal](../../../core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp) (libSceAgcDriver): the journal is append-only and never compacted, so a module a later run no longer builds pipelines from stays in the file until it is deleted, and the whole file is read into memory while the replay runs (its modules reference that buffer until the last replayed pipeline was built). Only the first `APS5_PIPELINE_JOURNAL_ENTRIES` pipelines recorded are replayed (a record is never rewritten, so a pipeline past the bound stays past it), and the pipeline store evicts the least recently used entry past `APS5_PIPELINE_STORE_ENTRIES` (1024 each): a title with more configurations rebuilds the rest at the draw as before. A compute pipeline is replayed over the set layout `DescribeSetLayouts` derives from the result's bindings, which a dispatch that finds it resident checks against its `ShaderResources` layout only under `APS5_VERIFY_SET_LAYOUTS=1`. Recording and replay are best effort: a record whose write fails is lost, an entry whose build fails is skipped, and both are counted in the `[pipeline-journal]` line rather than stopping the run. A replayed pipeline that the title never asks for again costs its build time and its driver memory (its optimized relink is deferred to its first draw, so a run whose first frames bind many replayed pipelines queues their relinks then); nothing ages recipes out of the file. Two runs sharing the file take turns under its lock file, but each only knows the modules it loaded or wrote itself, so a module the other run appended meanwhile may be appended again (the loader keeps the first copy). Records a run appends after another run's load are replayed by that run only after its next start. The file header's format number is the only guard against a change of the recipe codec, `CanonicalPipelineState` or the store key that keeps the structures' sizes (the recompiler's source version does not cover the driver's sources). - [Graphics pipeline libraries](../../../core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp) (libSceAgcDriver): a pipeline is fast-linked at the draw and relinked with `VK_PIPELINE_CREATE_LINK_TIME_OPTIMIZATION_BIT_EXT` on a background thread, and binds the optimized one from the next draw on. Draws recorded before that use the fast link, whose vertex stage keeps the outputs the fragment stage does not read: on RADV (Radeon 660M, Mesa 26.2.4) 346 of 483 Astro's Playroom vertex shaders paired with a fragment stage that reads none of them used more VGPRs (median 8 more, up to 2.5 times), and on an RTX 4090 13 of 15 PPSA36082 vertex shaders up to 68 instead of 38 (#2242). The optimized link compiles to the same code as a whole pipeline - [Prebuilt pipeline libraries](../../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) (libSceAgcDriver): each registered vertex or pixel shader in registration form gets its pre-rasterization or fragment-shader library built on one background thread, with a fixed state the draw may not use: triangle list, counter-clockwise front face, no depth clamp, depth bias or conservative rasterization, first provoking vertex, one sample. Libraries are kept until the device is torn down; on Astro's Playroom 1948 of them added 0.44 GB of peak memory on RADV. - [`APS5_THREAD_PRIORITY=rt`](../../../core/libs/prx/libSceAgcDriver/Execution/src/ThreadPriority.cpp) (libSceAgcDriver) lowers the process's hard `RLIMIT_RTTIME` when it is above `APS5_THREAD_RTTIME_US` or rtkit's `RTTimeUSecMax`, because [rtkit](https://github.com/heftig/rtkit/blob/c295fa849f52b487be6433e69e08b46251950399/rtkit-daemon.c#L592-L637) (`verify_process_rttime`) reads the hard limit from `/proc//limits` and refuses real-time scheduling when it is higher. An unprivileged process cannot raise the hard limit again, so it stays lowered for the process and its threads even when rtkit then refuses the request (burst limit, user or start-time checks) and the thread throws. rtkit's `MaxRealtimePriority` and `RTTimeUSecMax` are read first, so an unreachable rtkit or a priority above its maximum leaves the limit untouched. The lowered limit only matters for threads that later run under a real-time policy diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index aa8af21f3b..88f4b7e449 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -179,6 +179,10 @@ HDR output is available on Windows only. Titles see it when the monitor showing Recompiled shaders and the Vulkan pipeline cache are kept in a `shader_cache/` directory beside the output executable, so later runs skip recompilation. Set `ANYPS5_SHADER_CACHE_DIR` to use another directory, or set `ANYPS5_NO_SHADER_CACHE` to any non-empty value other than `0` to disable both caches. Shaders are stored in a subdirectory named after the recompiler's source version; subdirectories of other versions unused for 14 days are removed the first time a run looks up a shader. The pipeline cache is one `vk-pipelines-*.bin` file per GPU and driver version. Deleting the directory is safe: it is rebuilt on the next run. +### Pipeline journal + +Besides the recompiled shaders, the shader cache directory holds `pipelines.journal`: every Vulkan graphics pipeline the pipeline store kept and every compute pipeline a run built, with the specialized shader modules they were built from. The next run of the title builds those pipelines on background threads while it loads, so a draw or dispatch the earlier run already made does not compile its pipeline when it is first reached. The journal belongs to the GPU and driver that wrote it, like the driver's pipeline cache file next to it: a run on another GPU or driver version starts it over, and the driver's own pipeline cache decides how fast each pipeline is rebuilt. Two runs sharing the directory take turns on `pipelines.journal.lock`. `APS5_NO_PIPELINE_JOURNAL=1` disables recording and replay; `APS5_PIPELINE_JOURNAL_THREADS` sets the replay threads (half the CPU threads by default; `0` records without replaying), `APS5_PIPELINE_JOURNAL_ENTRIES` how many pipelines are replayed (the first recorded, which a run reaches first) and `APS5_PIPELINE_STORE_ENTRIES` how many graphics pipelines stay resident (1024 each), and `APS5_PIPELINE_JOURNAL_MAX_MIB` (1024) the file size at which recording stops. Deleting the file loses nothing but the head start. With `APS5_PROFILE_DRAW=1` a `[pipeline-journal]` line reports what was loaded, replayed and recorded. + ### Storing GPU results at each flip GPU results in storage images and render targets stay on the GPU until something reads their memory. A title that frees such an image after waiting for its GPU work and reuses the memory can have its new data overwritten when the results are stored later ([TechnicalDebt](../dev/TechnicalDebt.md)). `APS5_STORE_AT_FLIP=1` stores every pending result when a display buffer is flipped, at the cost of one write-back per pending image and frame. Any other value stops the title at its first flip. From addcdc7c41ba0f8c0ce3339c766a840a7add249d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Wed, 14 Oct 2026 17:56:32 +0200 Subject: [PATCH 0466/1000] fix(agc): journal the sample location grid and per-sample shading of a pipeline #5861 added both to the pipeline state and its store key; the recipe dropped them, so a replayed pipeline keyed another configuration. Co-Authored-By: Claude --- .../Graphics/src/PipelineJournal.cpp | 8 +++++++- core/libs/prx/libSceAgcDriver/tests/Graphics.cpp | 13 +++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp index 940ff93847..c0475c6044 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp @@ -35,7 +35,7 @@ static_assert(sizeof(VkStencilOpState) == 28, "VkStencilOpState changed: update static_assert(sizeof(VkVertexInputBindingDescription) == 12, "VkVertexInputBindingDescription changed: update the recipe codec"); static_assert(sizeof(VkVertexInputAttributeDescription) == 16, "VkVertexInputAttributeDescription changed: update the recipe codec"); #if defined(__linux__) && defined(__x86_64__) && defined(__GLIBCXX__) -static_assert(sizeof(State) == 664, "State changed: update CanonicalPipelineState and the recipe codec"); +static_assert(sizeof(State) == 688, "State changed: update CanonicalPipelineState and the recipe codec"); static_assert(sizeof(ShaderStages) == 100, "ShaderStages changed: update the recipe codec"); static_assert(sizeof(ShaderRecompiler::MeshConfiguration) == 36, "MeshConfiguration changed: update the recipe codec"); static_assert(sizeof(ShaderRecompiler::TessellationConfiguration) == 32, "TessellationConfiguration changed: update the recipe codec"); @@ -207,6 +207,8 @@ void encodeState(Writer& writer, const State& state) { writer.U32(state.samples); for (const auto value : state.sampleLocations) writer.Raw(value); writer.Bool(state.customSampleLocations); + writer.Bool(state.sampleLocationGrid); + writer.Bool(state.perSampleShading); } void decodeState(Reader& reader, State& state) { @@ -260,6 +262,8 @@ void decodeState(Reader& reader, State& state) { state.samples = reader.U32(); for (auto& value : state.sampleLocations) value = reader.Raw(); state.customSampleLocations = reader.Bool(); + state.sampleLocationGrid = reader.Bool(); + state.perSampleShading = reader.Bool(); } void encodeRecipe(Writer& writer, const GraphicsPipelineRecipe& recipe) { @@ -993,6 +997,8 @@ State CanonicalPipelineState(const State& state) { canonical.samples = state.samples; canonical.sampleLocations = state.sampleLocations; canonical.customSampleLocations = state.customSampleLocations; + canonical.sampleLocationGrid = state.sampleLocationGrid; + canonical.perSampleShading = state.perSampleShading; return canonical; } diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 6a67b64204..da6f687548 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -7395,6 +7395,19 @@ void pipelineJournalTests() { const PipelineSetLayouts decodedLayouts{layouts.layouts.main, layouts.layouts.fragment, decoded.splitSets, decoded.mainLayoutKey, decoded.fragmentLayoutKey}; BuildPipelineStoreKey(replayed, context, decoded.state, decoded.vertexInput, decodedLayouts, shaders, decoded.attachmentLayout); Require(!original.empty() && original == canonical && original == replayed, "the canonical or the decoded state keys another pipeline"); + auto gridState = state; + gridState.samples = 2; + gridState.customSampleLocations = true; + gridState.sampleLocationGrid = true; + gridState.perSampleShading = true; + gridState.sampleLocations[31] = 0x5a; + GraphicsPipelineRecipe gridDecoded; + Require(DecodeGraphicsPipelineRecipe(EncodeGraphicsPipelineRecipe(MakeGraphicsPipelineRecipe(gridState, input, layouts.View(), shaders, VK_IMAGE_LAYOUT_GENERAL)), gridDecoded), "the sample grid recipe does not decode"); + Require(gridDecoded.state.sampleLocationGrid && gridDecoded.state.perSampleShading && gridDecoded.state.sampleLocations[31] == 0x5a, "the recipe dropped the sample grid, per-sample shading or the last sample location"); + std::vector grid, gridReplayed; + BuildPipelineStoreKey(grid, context, gridState, input, layouts.View(), shaders, VK_IMAGE_LAYOUT_GENERAL); + BuildPipelineStoreKey(gridReplayed, context, gridDecoded.state, gridDecoded.vertexInput, layouts.View(), shaders, gridDecoded.attachmentLayout); + Require(grid != original && grid == gridReplayed, "the decoded sample grid state keys another pipeline"); auto movedState = state; movedState.viewport.x = 5; movedState.colors[0].address = 0x9000; From 67f5350e251d1d42f93d1219ac108574725f0de3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Thu, 15 Oct 2026 16:03:50 +0200 Subject: [PATCH 0467/1000] fix(agc): keep the pipeline key buffer in a host thread-local slot MinGW's emulated TLS runs thread_local destructors from the module's TLS callback, which corrupts the heap when a replay thread or queue worker that ran CachedPipeline exits (0xC0000374, prx_thread_local_destructors). Co-Authored-By: Claude --- core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp index ec0251bcac..e79504441a 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp @@ -6,6 +6,7 @@ #include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" #include "prx/libSceAgcDriver/Graphics/include/PipelineSpecialization.hpp" #include "prx/libSceAgcDriver/Graphics/include/VertexInput.hpp" +#include "prx/libc/include/HostThreadLocal.hpp" #include #include #include @@ -974,7 +975,8 @@ std::shared_ptr CachedPipeline(const Context& context, const State& st if (created != nullptr) *created = false; if (disabled) return std::make_shared(context, state, vertexInput, resources, shaders, attachmentLayout); auto& store = Pipelines(); - thread_local std::vector key; + struct PipelineKeyStorage {}; + auto& key = HostThreadLocal, PipelineKeyStorage>(); pipelineKey(key, context, state, vertexInput, resources, shaders, attachmentLayout); if (key.empty()) { std::lock_guard lock(store.mutex); From 57ff6a63b22463fc7419fd1ecd0ad4ee4962b4ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Thu, 15 Oct 2026 16:03:50 +0200 Subject: [PATCH 0468/1000] fix(agc): record nothing in the pipeline journal without its file lock Co-Authored-By: Claude --- .../libSceAgcDriver/Graphics/src/PipelineJournal.cpp | 12 ++++++++++++ docs/user/USAGE.md | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp index c0475c6044..9882003802 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp @@ -367,6 +367,12 @@ public: FileLock(const FileLock&) = delete; FileLock& operator=(const FileLock&) = delete; +#ifdef _WIN32 + bool Held() const { return handle != INVALID_HANDLE_VALUE; } +#else + bool Held() const { return descriptor >= 0; } +#endif + private: #ifdef _WIN32 HANDLE handle = INVALID_HANDLE_VALUE; @@ -721,6 +727,12 @@ private: std::error_code error; std::filesystem::create_directories(path.parent_path(), error); FileLock fileLock(lockPath); + if (!fileLock.Held()) { + static std::atomic reported{false}; + if (!reported.exchange(true)) std::fprintf(stderr, "[pipeline-journal] cannot lock %s; pipelines are not recorded while it cannot be locked\n", lockPath.string().c_str()); + counters.writeFailures.fetch_add(1, std::memory_order_relaxed); + return true; + } bool fresh = false; { std::lock_guard lock(mutex); diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index 88f4b7e449..72772bf384 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -181,7 +181,7 @@ Recompiled shaders and the Vulkan pipeline cache are kept in a `shader_cache/` d ### Pipeline journal -Besides the recompiled shaders, the shader cache directory holds `pipelines.journal`: every Vulkan graphics pipeline the pipeline store kept and every compute pipeline a run built, with the specialized shader modules they were built from. The next run of the title builds those pipelines on background threads while it loads, so a draw or dispatch the earlier run already made does not compile its pipeline when it is first reached. The journal belongs to the GPU and driver that wrote it, like the driver's pipeline cache file next to it: a run on another GPU or driver version starts it over, and the driver's own pipeline cache decides how fast each pipeline is rebuilt. Two runs sharing the directory take turns on `pipelines.journal.lock`. `APS5_NO_PIPELINE_JOURNAL=1` disables recording and replay; `APS5_PIPELINE_JOURNAL_THREADS` sets the replay threads (half the CPU threads by default; `0` records without replaying), `APS5_PIPELINE_JOURNAL_ENTRIES` how many pipelines are replayed (the first recorded, which a run reaches first) and `APS5_PIPELINE_STORE_ENTRIES` how many graphics pipelines stay resident (1024 each), and `APS5_PIPELINE_JOURNAL_MAX_MIB` (1024) the file size at which recording stops. Deleting the file loses nothing but the head start. With `APS5_PROFILE_DRAW=1` a `[pipeline-journal]` line reports what was loaded, replayed and recorded. +Besides the recompiled shaders, the shader cache directory holds `pipelines.journal`: every Vulkan graphics pipeline the pipeline store kept and every compute pipeline a run built, with the specialized shader modules they were built from. The next run of the title builds those pipelines on background threads while it loads, so a draw or dispatch the earlier run already made does not compile its pipeline when it is first reached. The journal belongs to the GPU and driver that wrote it, like the driver's pipeline cache file next to it: a run on another GPU or driver version starts it over, and the driver's own pipeline cache decides how fast each pipeline is rebuilt. Two runs sharing the directory take turns on `pipelines.journal.lock`; a run that cannot lock it records nothing. `APS5_NO_PIPELINE_JOURNAL=1` disables recording and replay; `APS5_PIPELINE_JOURNAL_THREADS` sets the replay threads (half the CPU threads by default; `0` records without replaying), `APS5_PIPELINE_JOURNAL_ENTRIES` how many pipelines are replayed (the first recorded, which a run reaches first) and `APS5_PIPELINE_STORE_ENTRIES` how many graphics pipelines stay resident (1024 each), and `APS5_PIPELINE_JOURNAL_MAX_MIB` (1024) the file size at which recording stops. Deleting the file loses nothing but the head start. With `APS5_PROFILE_DRAW=1` a `[pipeline-journal]` line reports what was loaded, replayed and recorded. ### Storing GPU results at each flip From 52d8d6ce28543a8118d19281bf20bcae04967318 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Thu, 15 Oct 2026 16:09:17 +0200 Subject: [PATCH 0469/1000] fix(agc): journal the pipeline state fields main added The replayed stencil reference, the reversed blend, the quad sample masks and the tessellation instance patches and draw parameters change the pipeline a draw creates, so the recipe carries them; the file format is bumped and the layout guards follow the new sizes. Co-Authored-By: Claude --- .../Graphics/src/PipelineJournal.cpp | 21 +++++++++++++++---- .../prx/libSceAgcDriver/tests/Graphics.cpp | 14 +++++++++++++ 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp index 9882003802..7bffdeffdd 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp @@ -35,16 +35,16 @@ static_assert(sizeof(VkStencilOpState) == 28, "VkStencilOpState changed: update static_assert(sizeof(VkVertexInputBindingDescription) == 12, "VkVertexInputBindingDescription changed: update the recipe codec"); static_assert(sizeof(VkVertexInputAttributeDescription) == 16, "VkVertexInputAttributeDescription changed: update the recipe codec"); #if defined(__linux__) && defined(__x86_64__) && defined(__GLIBCXX__) -static_assert(sizeof(State) == 688, "State changed: update CanonicalPipelineState and the recipe codec"); -static_assert(sizeof(ShaderStages) == 100, "ShaderStages changed: update the recipe codec"); +static_assert(sizeof(State) == 752, "State changed: update CanonicalPipelineState and the recipe codec"); +static_assert(sizeof(ShaderStages) == 108, "ShaderStages changed: update the recipe codec"); static_assert(sizeof(ShaderRecompiler::MeshConfiguration) == 36, "MeshConfiguration changed: update the recipe codec"); -static_assert(sizeof(ShaderRecompiler::TessellationConfiguration) == 32, "TessellationConfiguration changed: update the recipe codec"); +static_assert(sizeof(ShaderRecompiler::TessellationConfiguration) == 40, "TessellationConfiguration changed: update the recipe codec"); #endif constexpr std::uint32_t FileMagic = 0x4c4a5041u; // Bumped when the recipe codec, CanonicalPipelineState or the store key change in a way the // layout guards above do not catch (a field renumbered or reinterpreted at the same size). -constexpr std::uint32_t FileFormat = 1u; +constexpr std::uint32_t FileFormat = 2u; constexpr std::uint32_t ModuleRecord = 1u; constexpr std::uint32_t GraphicsRecord = 2u; constexpr std::uint32_t ComputeRecord = 3u; @@ -157,11 +157,15 @@ void decodeMesh(Reader& reader, ShaderRecompiler::MeshConfiguration& mesh) { void encodeTessellation(Writer& writer, const ShaderRecompiler::TessellationConfiguration& tessellation) { for (const auto value : {tessellation.inputControlPoints, tessellation.outputControlPoints, tessellation.domain, tessellation.partitioning, tessellation.outputTopology, tessellation.patchesPerGroup, tessellation.primitiveGroupSize}) writer.U32(value); writer.Bool(tessellation.breakAtInstance); + writer.U32(tessellation.instancePatches); + writer.Bool(tessellation.drawParameters); } void decodeTessellation(Reader& reader, ShaderRecompiler::TessellationConfiguration& tessellation) { for (auto* value : {&tessellation.inputControlPoints, &tessellation.outputControlPoints, &tessellation.domain, &tessellation.partitioning, &tessellation.outputTopology, &tessellation.patchesPerGroup, &tessellation.primitiveGroupSize}) *value = reader.U32(); tessellation.breakAtInstance = reader.Bool(); + tessellation.instancePatches = reader.U32(); + tessellation.drawParameters = reader.Bool(); } void encodeState(Writer& writer, const State& state) { @@ -185,6 +189,7 @@ void encodeState(Writer& writer, const State& state) { writer.Bool(state.stencilTest); writer.Raw(state.stencilFront); writer.Raw(state.stencilBack); + writer.Bool(state.stencilReplayReference.has_value()); writer.U64(state.colors.size()); for (const auto& color : state.colors) { writer.U32(static_cast(color.format)); @@ -196,6 +201,7 @@ void encodeState(Writer& writer, const State& state) { writer.Bool(state.hasColorTarget); writer.Bool(state.rectList); writer.Bool(state.dualSourceBlend); + writer.U32(state.reversedBlend); writer.U32(static_cast(state.topology)); writer.Bool(state.primitiveRestart); writer.Bool(state.negativeOneToOne); @@ -209,6 +215,7 @@ void encodeState(Writer& writer, const State& state) { writer.Bool(state.customSampleLocations); writer.Bool(state.sampleLocationGrid); writer.Bool(state.perSampleShading); + for (const auto mask : state.quadSampleMask) writer.Raw(mask); } void decodeState(Reader& reader, State& state) { @@ -233,6 +240,7 @@ void decodeState(Reader& reader, State& state) { state.stencilTest = reader.Bool(); state.stencilFront = reader.Raw(); state.stencilBack = reader.Raw(); + if (reader.Bool()) state.stencilReplayReference = 0u; const auto colors = reader.U64(); if (!reader.Ok() || colors > 8u) { reader.Fail(); @@ -251,6 +259,7 @@ void decodeState(Reader& reader, State& state) { state.hasColorTarget = reader.Bool(); state.rectList = reader.Bool(); state.dualSourceBlend = reader.Bool(); + state.reversedBlend = reader.U32(); state.topology = static_cast(reader.U32()); state.primitiveRestart = reader.Bool(); state.negativeOneToOne = reader.Bool(); @@ -264,6 +273,7 @@ void decodeState(Reader& reader, State& state) { state.customSampleLocations = reader.Bool(); state.sampleLocationGrid = reader.Bool(); state.perSampleShading = reader.Bool(); + for (auto& mask : state.quadSampleMask) mask = reader.Raw(); } void encodeRecipe(Writer& writer, const GraphicsPipelineRecipe& recipe) { @@ -986,6 +996,7 @@ State CanonicalPipelineState(const State& state) { canonical.stencilTest = state.stencilTest; canonical.stencilFront = state.stencilFront; canonical.stencilBack = state.stencilBack; + if (state.stencilReplayReference) canonical.stencilReplayReference = 0u; for (const auto& color : state.colors) { ColorTarget canonicalColor{}; canonicalColor.format = color.format; @@ -998,6 +1009,7 @@ State CanonicalPipelineState(const State& state) { canonical.hasColorTarget = state.hasColorTarget; canonical.rectList = state.rectList; canonical.dualSourceBlend = state.dualSourceBlend; + canonical.reversedBlend = state.reversedBlend; canonical.topology = state.topology; canonical.primitiveRestart = state.primitiveRestart; canonical.negativeOneToOne = state.negativeOneToOne; @@ -1011,6 +1023,7 @@ State CanonicalPipelineState(const State& state) { canonical.customSampleLocations = state.customSampleLocations; canonical.sampleLocationGrid = state.sampleLocationGrid; canonical.perSampleShading = state.perSampleShading; + canonical.quadSampleMask = state.quadSampleMask; return canonical; } diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index da6f687548..139c5247d3 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -7408,6 +7408,20 @@ void pipelineJournalTests() { BuildPipelineStoreKey(grid, context, gridState, input, layouts.View(), shaders, VK_IMAGE_LAYOUT_GENERAL); BuildPipelineStoreKey(gridReplayed, context, gridDecoded.state, gridDecoded.vertexInput, layouts.View(), shaders, gridDecoded.attachmentLayout); Require(grid != original && grid == gridReplayed, "the decoded sample grid state keys another pipeline"); + auto fourSampleState = state; + fourSampleState.samples = 4; + auto maskState = fourSampleState; + maskState.quadSampleMask = {0x3u, 0x3u, 0x3u, 0x3u}; + maskState.stencilReplayReference = 7u; + maskState.reversedBlend = 1u; + GraphicsPipelineRecipe maskDecoded; + Require(DecodeGraphicsPipelineRecipe(EncodeGraphicsPipelineRecipe(MakeGraphicsPipelineRecipe(maskState, input, layouts.View(), shaders, VK_IMAGE_LAYOUT_GENERAL)), maskDecoded), "the sample mask recipe does not decode"); + Require(maskDecoded.state.quadSampleMask == maskState.quadSampleMask && maskDecoded.state.stencilReplayReference.has_value() && maskDecoded.state.reversedBlend == 1u, "the recipe dropped the quad sample masks, the replayed stencil reference or the reversed blend"); + std::vector fourSamples, masked, maskReplayed; + BuildPipelineStoreKey(fourSamples, context, fourSampleState, input, layouts.View(), shaders, VK_IMAGE_LAYOUT_GENERAL); + BuildPipelineStoreKey(masked, context, maskState, input, layouts.View(), shaders, VK_IMAGE_LAYOUT_GENERAL); + BuildPipelineStoreKey(maskReplayed, context, maskDecoded.state, maskDecoded.vertexInput, layouts.View(), shaders, maskDecoded.attachmentLayout); + Require(masked != fourSamples && masked == maskReplayed, "the decoded sample mask state keys another pipeline"); auto movedState = state; movedState.viewport.x = 5; movedState.colors[0].address = 0x9000; From 1ad590fc054db4c767c411c56ce8566e40b8b1bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Thu, 15 Oct 2026 16:27:15 +0200 Subject: [PATCH 0470/1000] test(agc): give the new program of compute_pipeline_outside_gpu_mutex its own module Pipelines are now shared by every program that recompiles to the same module, and a leading s_nop recompiles to the same one, so the second dispatch reused the first pipeline. The new program also stores to the second word. Co-Authored-By: Claude --- .../tests/execution/ComputePipelineOutsideGpuMutex.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ComputePipelineOutsideGpuMutex.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ComputePipelineOutsideGpuMutex.cpp index 95513fdc4b..dd464ab921 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ComputePipelineOutsideGpuMutex.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ComputePipelineOutsideGpuMutex.cpp @@ -21,7 +21,7 @@ void Require(bool condition, const std::string& message) { } alignas(256) std::array ReaderCode{0xf4040100u, 0xfa000000u, 0xf4080201u, 0xfa000000u, 0xbf8cc07fu, 0xf4000182u, 0xfa000000u, 0xbf8cc07fu, 0x7e000206u, 0xe0700000u, 0x80020000u, 0xbf810000u}; -alignas(256) std::array NewReaderCode{0xbf800000u, 0xf4040100u, 0xfa000000u, 0xf4080201u, 0xfa000000u, 0xbf8cc07fu, 0xf4000182u, 0xfa000000u, 0xbf8cc07fu, 0x7e000206u, 0xe0700000u, 0x80020000u, 0xbf810000u}; +alignas(256) std::array NewReaderCode{0xf4040100u, 0xfa000000u, 0xf4080201u, 0xfa000000u, 0xbf8cc07fu, 0xf4000182u, 0xfa000000u, 0xbf8cc07fu, 0x7e000206u, 0xe0700000u, 0x80020000u, 0xe0700004u, 0x80020000u, 0xbf810000u}; struct Slot { alignas(256) std::array value{}; @@ -104,7 +104,7 @@ int main() { Require(Slots[1].done == 0, "the dispatch finished while another thread held the GPU mutex"); } WaitDone(Slots[1], "the dispatch of the new program did not finish"); - Require(Slots[1].output[0] == 0x5678u, "the dispatch of the new program stored " + std::to_string(Slots[1].output[0])); + Require(Slots[1].output[0] == 0x5678u && Slots[1].output[1] == 0x5678u, "the dispatch of the new program stored " + std::to_string(Slots[1].output[0]) + " and " + std::to_string(Slots[1].output[1])); const auto after = VulkanDevice::ComputePipelineCounts(); Require(after.underGpuMutex == before.underGpuMutex, "a compute pipeline was created under the GPU mutex"); std::cout << "compute pipeline outside GPU mutex test passed\n"; From fa30a9d5d296855f39678a251d79e0f7e2156a60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Fri, 16 Oct 2026 11:19:37 +0200 Subject: [PATCH 0471/1000] fix(agc): do not create the pipeline journal at driver shutdown Flushing at shutdown built the singleton and its writer thread on a run that never recorded anything; on Windows that throws std::system_error while the process is exiting. --- .../Graphics/src/PipelineJournal.cpp | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp index 7bffdeffdd..094b1ed9bd 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp @@ -421,10 +421,12 @@ struct LoadedJournal { class Journal { public: static Journal& Instance() { - static auto* journal = new Journal(); + static auto* journal = Create(); return *journal; } + static Journal* Existing() { return created().load(std::memory_order_acquire); } + [[nodiscard]] bool Enabled() const { return !path.empty() && !mismatched.load(std::memory_order_relaxed); } [[nodiscard]] const std::filesystem::path& Path() const { return path; } @@ -544,6 +546,17 @@ private: std::vector payload; }; + static std::atomic& created() { + static std::atomic pointer{nullptr}; + return pointer; + } + + static Journal* Create() { + auto* journal = new Journal(); + created().store(journal, std::memory_order_release); + return journal; + } + Journal() { const char* off = std::getenv("APS5_NO_PIPELINE_JOURNAL"); if ((off != nullptr && std::strcmp(off, "0") != 0) || !ShaderRecompiler::ShaderDiskCache::Enabled()) return; @@ -1098,7 +1111,8 @@ void RecordComputePipeline(const ShaderRecompiler::RecompileResult& shader, bool } void FlushPipelineJournal() { - if (Journal::Instance().Enabled()) Journal::Instance().Flush(); + auto* journal = Journal::Existing(); + if (journal != nullptr && journal->Enabled()) journal->Flush(); } void ReportPipelineJournal() { From ccfee8e260de0bc8d12c8994367ce3c2fabba4ed Mon Sep 17 00:00:00 2001 From: Michon <231849@buas.nl> Date: Thu, 8 Oct 2026 20:22:29 +0200 Subject: [PATCH 0472/1000] perf(agc): dispatch-cache recipe hits as ordered commit items (APS5_PIPELINE_DISPATCH) With pipelined draws, a DISPATCH_DIRECT on queue 0 whose dispatch-cache entry hits with a recipe runs as an ordered commit item instead of draining the pipeline first. Misses, reads that overlap writes still pending in the pipeline, verified data hits and writes that cannot be ranged still drain. A dispatch that is resolved ahead (or adopted from a group resolved ahead) and copy kernels keep the drained path; with the switch on, queue 0 does not resolve dispatch groups ahead, since that drains the pipeline at every group. Off by default. agc_driver_pipelined_dispatch counts drains per opcode (each dispatch drains with the switch off); agc_driver_pipelined_dispatch_item checks that repeated dispatches do not drain, and that a dispatch whose descriptor table a pending fill retargets drains and reads the new buffer. Co-Authored-By: Claude Opus 5.5 (1M context) --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 6 + .../include/Driver/Draw/DrawPipeline.hpp | 3 + .../Execution/include/Driver/Driver.hpp | 1 + .../include/Driver/Memory/DrawWriteRanges.hpp | 1 + .../src/Driver/Dispatch/Dispatch.cpp | 138 ++++++++- .../src/Driver/Draw/DrawPipeline.cpp | 11 + .../src/Driver/Memory/WrittenBuffers.cpp | 25 ++ .../src/Driver/Packets/PacketExecution.cpp | 6 +- .../tests/execution/PipelinedDispatch.cpp | 268 ++++++++++++++++++ 9 files changed, 456 insertions(+), 3 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/PipelinedDispatch.cpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index ebd508377d..060c682edd 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1741,6 +1741,12 @@ if(BUILD_TESTING) set_tests_properties(agc_driver_stencil_replay_no_gpl PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test(NAME agc_driver_pipelined_dispatch_item COMMAND agc_driver_pipelined_dispatch_tests) + set_tests_properties(agc_driver_pipelined_dispatch_item PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_PIPELINE_DISPATCH=1" SKIP_RETURN_CODE 77) + if(WIN32) + set_tests_properties(agc_driver_pipelined_dispatch_item PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() foreach(agcTest IN ITEMS agc_spirv_narrow_constant_store_tests agc_spirv13_image_tests agc_spirv_optimizer_tests agc_driver_constant_store_tests) if(NOT TARGET ${agcTest}) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp index d89415137c..dc8e155378 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp @@ -26,12 +26,14 @@ public: static DrawPipeline& Queue0(); static std::size_t Depth(); + static std::size_t QueuedItems(); static bool& Active(); static std::atomic& EpochToken(); static void FollowEpoch(std::uint64_t token); void Enqueue(Commit commit, std::vector writes, std::uint64_t labelAddress = 0, std::vector labelBytes = {}); void Drain(DrainReason reason, std::uint32_t opcode = 0x100); + static std::uint64_t DrainRequestsByOpcode(std::uint32_t opcode); bool Busy() const { return outstanding.load(std::memory_order_acquire) != 0; } std::size_t Outstanding() const { return outstanding.load(std::memory_order_acquire); } bool Overlaps(std::uint64_t address, std::size_t bytes); @@ -67,6 +69,7 @@ private: std::array(DrainReason::Count)> drains{}; std::array(DrainReason::Count)> drainWaitNs{}; std::array drainOpcodes{}; + std::array, 257> drainRequests{}; std::uint64_t depthSum = 0; std::chrono::steady_clock::time_point lastReport = std::chrono::steady_clock::now(); }; diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp index ed44e7e7be..21df19ebff 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp @@ -111,6 +111,7 @@ private: static bool stampValidate(); static bool dataHits(); static bool verifyDataHits(); + static bool pipelineDispatch(); static std::size_t dispatchVariants(); static bool insertCompare(); static std::size_t dispatchCacheEntries(); diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Memory/DrawWriteRanges.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Memory/DrawWriteRanges.hpp index b5e1b03310..85cc30100c 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Memory/DrawWriteRanges.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Memory/DrawWriteRanges.hpp @@ -11,6 +11,7 @@ namespace AgcDriver::DriverDetail { std::vector> DrawWriteRanges(const Graphics::State& graphics, std::span stages, bool exact); +std::vector> DispatchWriteRanges(const ShaderRecompiler::RecompileResult& compiled, bool& bounded); bool ExactDrawWrites(); } diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp index 045daf3d24..b506cff5c1 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp @@ -2,9 +2,11 @@ #include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Memory/DrawWriteRanges.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Memory/BufferCopy.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Synchronization/DeferredLabels.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Execution/include/Pm4.hpp" #include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" @@ -15,6 +17,47 @@ namespace AgcDriver::DriverDetail { +namespace { + +bool DispatchItemsEnabled() { + static const bool enabled = [] { + const char* text = std::getenv("APS5_PIPELINE_DISPATCH"); + return text != nullptr && text[0] != '\0' && text[0] != '0'; + }(); + return enabled; +} + +enum class ItemFallback : std::size_t { Miss, Reads, NoRecipe, Writes, Verify, Count }; +constexpr std::array(ItemFallback::Count)> ItemFallbackNames{"miss", "reads", "no recipe", "writes", "verify"}; +std::array, static_cast(ItemFallback::Count)> itemFallbacks{}; +std::atomic itemCommits{0}; +std::atomic itemRebuilds{0}; + +void NoteItemFallback(ItemFallback reason) { + itemFallbacks[static_cast(reason)].fetch_add(1, std::memory_order_relaxed); +} + +void ReportDispatchItems(bool profile) { + static auto lastReport = std::chrono::steady_clock::now(); + if (!profile) return; + const auto now = std::chrono::steady_clock::now(); + if (now - lastReport < std::chrono::seconds(10)) return; + lastReport = now; + std::string drained; + for (std::size_t i = 0; i < ItemFallbackNames.size(); ++i) { + char item[48]; + std::snprintf(item, sizeof(item), " %s %llu", ItemFallbackNames[i], static_cast(itemFallbacks[i].load(std::memory_order_relaxed))); + drained += item; + } + std::fprintf(stderr, "[dispatch-item] %llu commit items (%llu recipes rebuilt on the committer); drained instead, cumulative:%s\n", static_cast(itemCommits.load(std::memory_order_relaxed)), static_cast(itemRebuilds.load(std::memory_order_relaxed)), drained.c_str()); +} + +} + +bool Driver::pipelineDispatch() { + return DispatchItemsEnabled(); +} + void Driver::dispatch(QueueState& queue, std::span packet, const Submission& submission, std::uint64_t indirectArguments) { PerformanceTimer timing("Driver.Dispatch"); const auto address = (static_cast(readRegister(queue.shader, 0x20c)) << 8u) | (static_cast(readRegister(queue.shader, 0x20d) & 0xffu) << 40u); @@ -72,6 +115,7 @@ void Driver::dispatch(QueueState& queue, std::span packet, if (indirectArguments == 0 && (packet[4] & 0x2u) != 0) { partialGroups = Pm4::PartialGroupThreads({packet[1], packet[2], packet[3]}, {readRegister(queue.shader, 0x207), readRegister(queue.shader, 0x208), readRegister(queue.shader, 0x209)}); } + const bool pipelineCandidate = !resolving && !fromAhead && DispatchItemsEnabled() && DrawPipeline::Active() && indirectArguments == 0 && deferredLabels().labels.empty() && !matchesCopyKernel(std::span(snapshot.code).subspan(codeOffset), userData, compute) && !MatchesStreamCopyKernel(std::span(snapshot.code).subspan(codeOffset), userData, compute); const bool partialLastGroup = partialGroups != std::array{}; if (resolving) { if (!partialLastGroup && matchesFillKernel(std::span(snapshot.code).subspan(codeOffset), userData, compute)) return; @@ -84,7 +128,7 @@ void Driver::dispatch(QueueState& queue, std::span packet, pendingDispatchPhases().outcome = DispatchOutcome::FillHle; return; } - if (DrawPipeline::Active()) DrawPipeline::Queue0().Drain(DrawPipeline::DrainReason::Packet, 0x15); + if (DrawPipeline::Active() && !pipelineCandidate) DrawPipeline::Queue0().Drain(DrawPipeline::DrainReason::Packet, 0x15); if (const auto htile = Graphics::HtileDepthClearAddress(std::span(snapshot.code).subspan(codeOffset), userData, compute.numThreads); htile != 0) { const auto threads = (packet[4] & 0x20u) != 0 ? static_cast(packet[1]) : static_cast(packet[1]) * compute.numThreads[0]; const auto nativeBytes = Graphics::HtileDepthClearBytes_nid_no_patch(std::span(snapshot.code).subspan(codeOffset), userData, compute, packet, indirectArguments); @@ -223,6 +267,27 @@ void Driver::dispatch(QueueState& queue, std::span packet, } phaseTiming.Phase(PhaseKey); timing.Mark("decode_key_device_setup"); + bool readsClear = false; + if (pipelineCandidate) { + std::vector reads; + bool entryFound = false; + { + std::lock_guard cacheLock(dispatchCacheMutex); + if (const auto found = dispatchCache.find(key); found != dispatchCache.end()) { + entryFound = true; + for (const auto& variant : found->second->variants) appendEntryRegions(*variant, reads); + } + } + if (!entryFound || noDispatchCache || stampValidate()) { + NoteItemFallback(ItemFallback::Miss); + } else if (verifyDataHits()) { + NoteItemFallback(ItemFallback::Verify); + } else { + readsClear = std::none_of(reads.begin(), reads.end(), [](const auto& region) { return DrawPipeline::Queue0().Overlaps(region.guestAddress, region.bytes.size()); }); + if (!readsClear) NoteItemFallback(ItemFallback::Reads); + } + if (!readsClear && DrawPipeline::Queue0().Busy()) DrawPipeline::Queue0().Drain(DrawPipeline::DrainReason::Packet, 0x15); + } if (fromAhead) { compiledResult = std::move(ahead.compiledResult); keepVariant = std::move(ahead.keepVariant); @@ -244,6 +309,19 @@ void Driver::dispatch(QueueState& queue, std::span packet, lookupDispatch(address, submission, key, noDispatchCache, traceCache, profile, memory, phaseTiming, phaseMs, compiledResult, keepVariant, captured, liveWords, dataHit, cached, validated, missedEntry, missedDiffering); } timing.Mark("lookup_cache"); + std::vector itemWrites; + bool pipelined = false; + if (readsClear) { + if (cached && keepVariant != nullptr && keepVariant->recipe.load(std::memory_order_acquire) != nullptr) { + bool bounded = false; + itemWrites = DispatchWriteRanges(*compiledResult, bounded); + pipelined = bounded; + if (!bounded) NoteItemFallback(ItemFallback::Writes); + } else { + NoteItemFallback(ItemFallback::NoRecipe); + } + if (!pipelined && DrawPipeline::Queue0().Busy()) DrawPipeline::Queue0().Drain(DrawPipeline::DrainReason::Packet, 0x15); + } if (cached) { require(keepVariant != nullptr && keepVariant->shader == registeredShader, "dispatch cache belongs to another registered shader"); captureMs += phaseTiming.Elapsed(); @@ -361,6 +439,64 @@ void Driver::dispatch(QueueState& queue, std::span packet, }; phaseTiming.Phase(PhaseSnapshots); + if (pipelined) { + const auto recipe = keepVariant->recipe.load(std::memory_order_acquire); + const auto epoch = DrawPipeline::EpochToken().load(std::memory_order_relaxed); + const auto queueId = submission.queue; + itemCommits.fetch_add(1, std::memory_order_relaxed); + DrawPipeline::Queue0().Enqueue([this, localDevice = std::move(localDevice), result = compiledResult, snapshots = std::move(snapshots), groups, address, queueId, recipe, keepVariant, dataHit, epoch] { + DrawPipeline::FollowEpoch(epoch); + GuestMemory::SetCurrentPacket(0x15, queueId); + const auto& compiledCommit = *result; + const bool noteWrites = writeEvidenceEnabled() || traceCapSync(); + try { + std::shared_ptr recipeHit = localDevice->PrepareRecipe(recipe, false); + std::shared_ptr builtRecipe; + bool writersNoted = false; + for (;;) { + std::shared_ptr prepared; + if (recipeHit == nullptr || VulkanDevice::VerifyRecipes()) prepared = localDevice->PrepareDispatch(compiledCommit, snapshots); + GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Dispatch); + std::lock_guard gpuLock(GuestMemory::GpuMutex()); + recordLabelsForPacket(localDevice.get(), queueId); + if (noteWrites && writerKeyedEvidence() && !writersNoted) { + noteWrittenBuffers(address, queueId, compiledCommit); + writersNoted = true; + } + if (recipeHit != nullptr) { + VulkanDevice::IndirectOutcome outcome{0, 0}; + const auto recipeResult = localDevice->DispatchRecipe(compiledCommit, groups[0], groups[1], groups[2], 0, address, recipeHit, outcome, VulkanDevice::VerifyRecipes() ? prepared : nullptr, dataHit); + if (recipeResult == RecipeOutcome::Rebuild) { + recipeHit = nullptr; + VulkanDevice::NoteRecipe(VulkanDevice::RecipeEvent::Restart, false); + itemRebuilds.fetch_add(1, std::memory_order_relaxed); + continue; + } + } else { + localDevice->Dispatch(compiledCommit, groups[0], groups[1], groups[2], snapshots, address, std::move(prepared), &builtRecipe); + } + break; + } + if (noteWrites && !writerKeyedEvidence()) noteWrittenBuffers(address, queueId, compiledCommit); + if (builtRecipe != nullptr) { + if (dataHit) { + auto own = std::make_shared(*builtRecipe); + own->dataWordsHash = Graphics::ShaderResources::DataWordsHash({ShaderRecompiler::ShaderStage::Compute, keepVariant->compiled.get(), 0}); + builtRecipe = std::move(own); + } + keepVariant->recipe.store(std::move(builtRecipe), std::memory_order_release); + VulkanDevice::NoteRecipe(VulkanDevice::RecipeEvent::Attach, false); + } + } catch (const std::exception& error) { + char where[64]; + std::snprintf(where, sizeof(where), "compute shader 0x%llx: ", static_cast(address)); + throw std::runtime_error(where + std::string(error.what())); + } + }, std::move(itemWrites)); + ReportDispatchItems(profile); + return; + } + std::shared_ptr recipeHit; if (cached && keepVariant != nullptr && !stampValidate()) { recipeHit = localDevice->PrepareRecipe(keepVariant->recipe.load(std::memory_order_acquire), indirectArguments != 0); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp index b9ddd2c33f..bd4c9d9601 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp @@ -36,6 +36,12 @@ std::size_t DrawPipeline::Depth() { return depth; } +std::size_t DrawPipeline::QueuedItems() { + auto& pipeline = Queue0(); + std::lock_guard lock(pipeline.mutex); + return pipeline.items.size(); +} + bool& DrawPipeline::Active() { static thread_local bool active = false; return active; @@ -152,6 +158,7 @@ void DrawPipeline::run() { } void DrawPipeline::Drain(DrainReason reason, std::uint32_t opcode) { + drainRequests[std::min(opcode, 0x100)].fetch_add(1, std::memory_order_relaxed); if (!Busy()) return; if (GuestMemory::GpuMutex().HeldByThisThread()) throw std::runtime_error("draw pipeline drain under the GPU lock (reason " + std::to_string(static_cast(reason)) + ", opcode " + std::to_string(opcode) + ")"); const auto start = std::chrono::steady_clock::now(); @@ -166,6 +173,10 @@ void DrawPipeline::Drain(DrainReason reason, std::uint32_t opcode) { rethrowFailure(); } +std::uint64_t DrawPipeline::DrainRequestsByOpcode(std::uint32_t opcode) { + return Queue0().drainRequests[std::min(opcode, 0x100)].load(std::memory_order_relaxed); +} + std::optional DrawPipeline::PendingLabel(std::uint64_t address, std::size_t bytes) { if (bytes == 0 || bytes > 8) return std::nullopt; const auto end = address + bytes; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/WrittenBuffers.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/WrittenBuffers.cpp index 34b051bb28..47b88455e4 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/WrittenBuffers.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/WrittenBuffers.cpp @@ -50,6 +50,31 @@ void Driver::noteWrittenBuffers(std::uint64_t program, std::uint32_t queue, cons while (writtenBuffers.size() > WrittenBufferRing) writtenBuffers.pop_front(); } +std::vector> DispatchWriteRanges(const ShaderRecompiler::RecompileResult& compiled, bool& bounded) { + bounded = compiled.bdaAbiVersion == 0; + std::vector> ranges; + constexpr std::uint64_t UnknownImageBytes = 64ull * 1024 * 1024; + for (const auto& binding : compiled.bindings) { + if (binding.role == ShaderRecompiler::DescriptorRole::Gds || binding.role == ShaderRecompiler::DescriptorRole::BdaPagetable || binding.role == ShaderRecompiler::DescriptorRole::FaultBuffer) bounded = false; + if (binding.role != ShaderRecompiler::DescriptorRole::GuestImages || binding.kind != ShaderRecompiler::DescriptorKind::StorageImage) continue; + for (std::uint32_t element = 0; element < binding.count; ++element) { + const bool written = element >= binding.imageWritten.size() || binding.imageWritten[element]; + if (!written) continue; + if (binding.guestDescriptor.size() < (static_cast(element) + 1) * 8) { + bounded = false; + continue; + } + const auto resource = Graphics::DecodeTextureResource(std::span(binding.guestDescriptor).subspan(static_cast(element) * 8, 8)); + if (resource.baseAddress == 0) continue; + const auto surfaceBytes = Graphics::DescribeSurface(resource).guestBytes; + ranges.emplace_back(resource.baseAddress, resource.baseAddress + (surfaceBytes != 0 ? surfaceBytes : UnknownImageBytes)); + if (resource.dccAddress != 0) ranges.emplace_back(resource.dccAddress, resource.dccAddress + (surfaceBytes != 0 ? surfaceBytes / 256 + 1 : UnknownImageBytes)); + } + } + visitWrittenBuffers(compiled, [&](std::uint32_t, std::uint64_t begin, std::uint64_t end, bool) { ranges.emplace_back(begin, end); }); + return ranges; +} + void Driver::noteForeignWriter(std::uint64_t begin, std::uint64_t end, std::uint32_t queue) { if (!foreignWriters() || !(writeEvidenceEnabled() || traceCapSync()) || end <= begin) return; std::lock_guard lock(writtenBuffersMutex); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp index 4bd279a45f..02458b6484 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp @@ -314,8 +314,10 @@ void Driver::execute(const Submission& submission) { currentPacketOffset() = cursor; if (gateOpened()) { gateOpened() = false; - if (pipelined && DrawPipeline::Queue0().Busy()) DrawPipeline::Queue0().Drain(DrawPipeline::DrainReason::Packet, opcode); - timed(&WorkerProfile::dispatchMs, [&] { resolveGroupAhead(submission, queue, cursor); }); + if (!pipelineDispatch() || submission.queue != 0) { + if (pipelined && DrawPipeline::Queue0().Busy()) DrawPipeline::Queue0().Drain(DrawPipeline::DrainReason::Packet, opcode); + timed(&WorkerProfile::dispatchMs, [&] { resolveGroupAhead(submission, queue, cursor); }); + } } if (opcode == 0x15) timed(&WorkerProfile::dispatchMs, [&] { dispatch(queue, packet, submission); }); else timed(&WorkerProfile::dispatchMs, [&] { dispatchIndirect(queue, packet, submission); }); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PipelinedDispatch.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PipelinedDispatch.cpp new file mode 100644 index 0000000000..6d61aaf018 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PipelinedDispatch.cpp @@ -0,0 +1,268 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/Shutdown.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::DriverDetail::DrawPipeline; + +constexpr std::uint32_t Lanes = 32; +constexpr std::uint32_t BufferBytes = Lanes * 4u; +constexpr std::uint32_t DispatchOpcode = 0x15; + +constexpr std::size_t BlockBytes = 65536; + +std::uint32_t* NewBlock() { +#ifdef _WIN32 + auto* block = static_cast(VirtualAlloc(nullptr, BlockBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); +#else + auto* block = static_cast(std::aligned_alloc(BlockBytes, BlockBytes)); +#endif + if (block == nullptr) throw std::runtime_error("pipelined dispatch: cannot allocate a guest block"); + std::memset(block, 0, BlockBytes); + GuestAllocations::Mutation().Add(block, BlockBytes, true, true, false); + return block; +} + +std::uint32_t* Input = nullptr; +std::uint32_t* Output = nullptr; +std::uint32_t* Scratch = nullptr; +std::uint32_t* Table = nullptr; +std::uint32_t* TargetA = nullptr; +std::uint32_t* TargetB = nullptr; + +alignas(256) constexpr std::array Code{0x34020082u, 0xe0301000u, 0x80000201u, 0xbf8c3f70u, 0x4a060481u, 0xe0701000u, 0x80010301u, 0xbf810000u}; + +alignas(256) constexpr std::array ScalarCode{0xf4000200u, 0xfa000000u, 0xbf8cc07fu, 0x34020082u, 0x7e040208u, 0x4a040481u, 0xe0701000u, 0x80010201u, 0xbf810000u}; + +alignas(256) constexpr std::array TableCode{0xf4080200u, 0xfa000000u, 0xbf8cc07fu, 0x34020082u, 0xe0301000u, 0x80020201u, 0xbf8c3f70u, 0x4a060481u, 0xe0701000u, 0x80010301u, 0xbf810000u}; + +void check(bool condition, const std::string& what) { + if (!condition) throw std::runtime_error("pipelined dispatch: " + what); +} + +template +void WaitUntil(Ready ready, const std::string& what) { + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(10); + while (!ready()) { + if (std::chrono::steady_clock::now() >= deadline) throw std::runtime_error("pipelined dispatch: timed out waiting for " + what); + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } +} + +std::array FillPacket(std::uint32_t value, const void* destination, std::uint32_t bytes) { + const auto address = reinterpret_cast(destination); + return {0xc0055000u, 2u << 29u, value, 0u, static_cast(address), static_cast(address >> 32u), bytes}; +} + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +std::vector Round(const void* destination, std::uint32_t value, const void* code) { + const auto fill = FillPacket(value, destination, BufferBytes); + std::vector words(fill.begin(), fill.end()); + const auto codeAddress = reinterpret_cast(code); + const auto input = BufferDescriptor(Input, BufferBytes); + const auto output = BufferDescriptor(Output, BufferBytes); + const std::vector commands{ + 0xc0027600u, 0x20cu, static_cast(codeAddress >> 8u), static_cast(codeAddress >> 40u), + 0xc0037600u, 0x207u, Lanes, 1u, 1u, + 0xc0017600u, 0x213u, 8u << 1u, + 0xc0087600u, 0x240u, input[0], input[1], input[2], input[3], output[0], output[1], output[2], output[3], + 0xc0031500u, 1u, 1u, 1u, 0x8041u, + }; + words.insert(words.end(), commands.begin(), commands.end()); + return words; +} + +std::vector TableFills(const void* target, std::uint32_t value) { + const auto targetFill = FillPacket(value, target, BufferBytes); + const auto tableFill = FillPacket(static_cast(reinterpret_cast(target)), Table, 4u); + std::vector words(targetFill.begin(), targetFill.end()); + words.insert(words.end(), tableFill.begin(), tableFill.end()); + return words; +} + +std::vector TableDispatch(const void* code) { + const auto codeAddress = reinterpret_cast(code); + const auto tableAddress = reinterpret_cast(Table); + const auto output = BufferDescriptor(Output, BufferBytes); + const std::vector commands{ + 0xc0027600u, 0x20cu, static_cast(codeAddress >> 8u), static_cast(codeAddress >> 40u), + 0xc0037600u, 0x207u, Lanes, 1u, 1u, + 0xc0017600u, 0x213u, 8u << 1u, + 0xc0087600u, 0x240u, static_cast(tableAddress), static_cast(tableAddress >> 32u), 0u, 0u, output[0], output[1], output[2], output[3], + 0xc0031500u, 1u, 1u, 1u, 0x8041u, + }; + return commands; +} + +void Submit(std::vector& words) { + Packet packet{words.data(), static_cast(words.size()), 0, {}}; + check(sceAgcDriverSubmitDcb(&packet) == 0, "submission failed"); +} + +} + +int main() { +#ifdef _WIN32 + _putenv_s("APS5_PIPELINED_DRAWS", "1"); + _putenv_s("APS5_PIPELINE_DMA", "1"); +#else + setenv("APS5_PIPELINED_DRAWS", "1", 1); + setenv("APS5_PIPELINE_DMA", "1", 1); +#endif + try { + { + const auto probe = OpenVulkanTestDevice(); + if (!probe) return VulkanTestSkipped; + } + const char* dispatchSwitch = std::getenv("APS5_PIPELINE_DISPATCH"); + const bool pipelinedDispatch = dispatchSwitch != nullptr && dispatchSwitch[0] != '\0' && dispatchSwitch[0] != '0'; + + Shader shader{}; + shader.file_header = 0x34333231; + shader.version = 0x18; + shader.header_size = sizeof(Shader); + shader.shader_size = sizeof(Code); + shader.code = Code.data(); + AgcDriverRegisterShader_nid_postfix(&shader); + Shader scalarShader{}; + scalarShader.file_header = 0x34333231; + scalarShader.version = 0x18; + scalarShader.header_size = sizeof(Shader); + scalarShader.shader_size = sizeof(ScalarCode); + scalarShader.code = ScalarCode.data(); + AgcDriverRegisterShader_nid_postfix(&scalarShader); + Shader tableShader{}; + tableShader.file_header = 0x34333231; + tableShader.version = 0x18; + tableShader.header_size = sizeof(Shader); + tableShader.shader_size = sizeof(TableCode); + tableShader.code = TableCode.data(); + AgcDriverRegisterShader_nid_postfix(&tableShader); + + Input = NewBlock(); + Output = NewBlock(); + Scratch = NewBlock(); + Table = NewBlock(); + TargetA = NewBlock(); + TargetB = NewBlock(); + check((reinterpret_cast(TargetA) >> 32u) == (reinterpret_cast(TargetB) >> 32u), "the two targets do not share their high address dword"); + const auto initialDescriptor = BufferDescriptor(TargetA, BufferBytes); + std::copy(initialDescriptor.begin(), initialDescriptor.end(), Table); + const std::array values{0x0a0b0c0du, 0x11112222u, 0x33334444u, 0x55556666u, 0x77778888u}; + for (std::size_t round = 0; round < values.size(); ++round) { + const auto value = values[round]; + const auto drainsBefore = DrawPipeline::DrainRequestsByOpcode(DispatchOpcode); + auto words = Round(Input, value, Code.data()); + Submit(words); + AgcDriverWaitIdle_nid_postfix(); + AgcDriver::GuestMemory::FlushGpuWrites(reinterpret_cast(Output), BufferBytes); + const auto drains = DrawPipeline::DrainRequestsByOpcode(DispatchOpcode) - drainsBefore; + for (std::uint32_t lane = 0; lane < Lanes; ++lane) { + check(Input[lane] == value, "the immediate fill did not land in lane " + std::to_string(lane)); + check(Output[lane] == value + 1u, "round " + std::to_string(round) + ": lane " + std::to_string(lane) + " of the output is " + std::to_string(Output[lane]) + ", expected " + std::to_string(value + 1u) + " (the dispatch did not see the fill before it)"); + } + std::printf("pipelined dispatch: input fill round %zu, fill 0x%08x, dispatch drains %llu\n", round, value, static_cast(drains)); + if (!pipelinedDispatch) check(drains >= 1, "a dispatch did not drain the pipeline, and APS5_PIPELINE_DISPATCH is off"); + if (pipelinedDispatch) check(drains <= 1, "a dispatch drained the pipeline more than once (" + std::to_string(drains) + ")"); + } + for (std::size_t round = 0; round < 3; ++round) { + const auto value = 0xc0de0000u + static_cast(round); + const auto drainsBefore = DrawPipeline::DrainRequestsByOpcode(DispatchOpcode); + auto words = Round(Scratch, value, Code.data()); + Submit(words); + AgcDriverWaitIdle_nid_postfix(); + AgcDriver::GuestMemory::FlushGpuWrites(reinterpret_cast(Scratch), BufferBytes); + AgcDriver::GuestMemory::FlushGpuWrites(reinterpret_cast(Output), BufferBytes); + const auto drains = DrawPipeline::DrainRequestsByOpcode(DispatchOpcode) - drainsBefore; + for (std::uint32_t lane = 0; lane < Lanes; ++lane) { + check(Scratch[lane] == value, "the scratch fill did not land in lane " + std::to_string(lane)); + check(Output[lane] == Input[lane] + 1u, "scratch round " + std::to_string(round) + ": lane " + std::to_string(lane) + " of the output is " + std::to_string(Output[lane]) + ", expected " + std::to_string(Input[lane] + 1u)); + } + std::printf("pipelined dispatch: scratch fill round %zu, fill 0x%08x, dispatch drains %llu\n", round, value, static_cast(drains)); + if (!pipelinedDispatch) check(drains >= 1, "a dispatch did not drain the pipeline, and APS5_PIPELINE_DISPATCH is off"); + if (pipelinedDispatch) check(drains == 0, "a dispatch that reads nothing pending drained the pipeline (" + std::to_string(drains) + ")"); + } + const std::array tableValues{0x6a6a0001u, 0x6b6b0002u, 0x6c6c0003u, 0x6d6d0004u}; + for (std::size_t round = 0; round < tableValues.size(); ++round) { + auto* target = round % 2 == 0 ? TargetA : TargetB; + const auto value = tableValues[round]; + std::atomic released{false}; + struct Release { + std::atomic& flag; + ~Release() { flag.store(true, std::memory_order_release); } + } release{released}; + check(DrawPipeline::QueuedItems() == 0, "the pipeline had items before the descriptor round " + std::to_string(round)); + const auto drainsBefore = DrawPipeline::DrainRequestsByOpcode(DispatchOpcode); + DrawPipeline::Queue0().Enqueue([&released] { + while (!released.load(std::memory_order_acquire)) std::this_thread::sleep_for(std::chrono::milliseconds(1)); + }, {}); + auto fills = TableFills(target, value); + Submit(fills); + WaitUntil([] { return DrawPipeline::QueuedItems() >= 4; }, "the target and table fills queued behind the gate"); + auto dispatch = TableDispatch(TableCode.data()); + Submit(dispatch); + WaitUntil([&drainsBefore] { return DrawPipeline::QueuedItems() >= 5 || DrawPipeline::DrainRequestsByOpcode(DispatchOpcode) > drainsBefore; }, "the dispatch handled while the table fill is pending"); + released.store(true, std::memory_order_release); + AgcDriverWaitIdle_nid_postfix(); + AgcDriver::GuestMemory::FlushGpuWrites(reinterpret_cast(Output), BufferBytes); + const auto drains = DrawPipeline::DrainRequestsByOpcode(DispatchOpcode) - drainsBefore; + for (std::uint32_t lane = 0; lane < Lanes; ++lane) { + check(Output[lane] == value + 1u, "descriptor round " + std::to_string(round) + ": lane " + std::to_string(lane) + " of the output is " + std::to_string(Output[lane]) + ", expected " + std::to_string(value + 1u) + " (the dispatch read the buffer the table named before the pending fill of the table)"); + } + std::printf("pipelined dispatch: descriptor round %zu, fill 0x%08x, dispatch drains %llu\n", round, value, static_cast(drains)); + if (!pipelinedDispatch) check(drains >= 1, "a dispatch did not drain the pipeline, and APS5_PIPELINE_DISPATCH is off"); + if (pipelinedDispatch) check(drains == 1, "a dispatch whose table entry is pending a write drained " + std::to_string(drains) + " times, expected once"); + } + const std::array scalarValues{0x13572468u, 0x2468ace0u, 0x0f1e2d3cu, 0x9abcdef0u, 0x31415926u}; + for (std::size_t round = 0; round < scalarValues.size(); ++round) { + const auto value = scalarValues[round]; + const auto drainsBefore = DrawPipeline::DrainRequestsByOpcode(DispatchOpcode); + auto words = Round(Input, value, ScalarCode.data()); + Submit(words); + AgcDriverWaitIdle_nid_postfix(); + AgcDriver::GuestMemory::FlushGpuWrites(reinterpret_cast(Output), BufferBytes); + const auto drains = DrawPipeline::DrainRequestsByOpcode(DispatchOpcode) - drainsBefore; + for (std::uint32_t lane = 0; lane < Lanes; ++lane) { + check(Output[lane] == value + 1u, "scalar round " + std::to_string(round) + ": lane " + std::to_string(lane) + " of the output is " + std::to_string(Output[lane]) + ", expected " + std::to_string(value + 1u) + " (a stale input)"); + } + std::printf("pipelined dispatch: scalar read round %zu, fill 0x%08x, dispatch drains %llu\n", round, value, static_cast(drains)); + if (!pipelinedDispatch) check(drains >= 1, "a dispatch did not drain the pipeline, and APS5_PIPELINE_DISPATCH is off"); + if (pipelinedDispatch) check(drains <= 1, "a dispatch drained the pipeline more than once (" + std::to_string(drains) + ")"); + } + LibcRunShutdown_nid_postfix(); + std::puts("pipelined dispatch tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + try { LibcRunShutdown_nid_postfix(); } catch (...) {} + return 1; + } +} From ebc2342d1e2e0a75435b45d35b27aa6d1f5dcb8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Fri, 16 Oct 2026 11:42:19 +0200 Subject: [PATCH 0473/1000] docs(architecture): describe dispatches queued as pipeline items --- docs/dev/ARCHITECTURE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/ARCHITECTURE.md b/docs/dev/ARCHITECTURE.md index 0a9a669545..6327ea7c71 100644 --- a/docs/dev/ARCHITECTURE.md +++ b/docs/dev/ARCHITECTURE.md @@ -97,7 +97,7 @@ flowchart LR - `ShaderRecompiler::Recompile` keeps compiled variants in memory, and `ShaderDiskCache` stores them on disk so later runs reuse them. Before the writable cache, a miss is also looked up in read-only prebuilt packs and loose entry folders under `shader_cache/prebuilt`; pack entries are ordinary encoded cache entries behind a sorted key-hash index, validated by magic, compiler source version and index checksum when mapped, and a miss or rejected entry falls through to the writable cache and the compiler. Cache format 35 also stores the finite stencil-reference set, its replay-safety proof and reserved push-data word in the artifact and binding layouts. Entries from formats 31 to 34 are rejected. - A compute shader whose LDS plus its lock dword exceeds the host's `maxComputeSharedMemorySize` is not given a `Workgroup` array. `Recompile` sets `ShaderInfo::sharedMemoryBytes` for it and the SPIR-V backend addresses the LDS in a coherent storage buffer at descriptor set 1 (`WorkgroupMemoryDescriptorSet`), with a slice of `RecompileResult::workgroupMemoryDwords` per workgroup, indexed by `WorkgroupID` and `NumWorkGroups`. `VulkanDevice` keeps one such buffer, sized from the dispatch dimensions, and binds it after the resource set; programs that fit keep using shared memory. - `Driver::RegisterShader` splits shader preparation in two. `PlanRegistered` runs on the registering guest thread: it validates the registers and entry point, decodes the code and builds the recompile request, so invalid registrations still fail there. The RDNA to IR translation and resource plan (`PrepareShader`) run on a background pool of `hardware_concurrency() / 2` detached threads (at least 2, 64 MiB stacks). -- Queue 0's packet worker decodes, captures and prepares a draw and hands its recording (`Graphics::Draw` under the GPU lock) to a committer thread in [`DrawPipeline.cpp`](../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp), so the next packet is decoded while the previous draw is recorded. Each queued item carries the guest ranges it will write (`DrawWriteRanges`: written buffers and storage images, color targets with their DCC and CMASK, depth and stencil). A later packet that reads or writes one of those ranges, or that the pipeline can't order, drains it first; labels, submission ends, buffer fills and (with `APS5_PIPELINE_DMA=1`) DMA packets are queued as items instead. An exception thrown by a commit is rethrown on the worker at its next enqueue or drain, as an inline draw error would be. On by default; `APS5_PIPELINED_DRAWS=0` records every draw inline, and `APS5_EXACT_DRAW_WRITES=0` widens the color and depth ranges to estimates. +- Queue 0's packet worker decodes, captures and prepares a draw and hands its recording (`Graphics::Draw` under the GPU lock) to a committer thread in [`DrawPipeline.cpp`](../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp), so the next packet is decoded while the previous draw is recorded. Each queued item carries the guest ranges it will write (`DrawWriteRanges`: written buffers and storage images, color targets with their DCC and CMASK, depth and stencil). A later packet that reads or writes one of those ranges, or that the pipeline can't order, drains it first; labels, submission ends, buffer fills and (with `APS5_PIPELINE_DMA=1`) DMA packets are queued as items instead. With `APS5_PIPELINE_DISPATCH=1` (off by default), a `DISPATCH_DIRECT` whose dispatch-cache entry hits with a recipe is queued as an item too, carrying the dispatch's written buffers and storage images (`DispatchWriteRanges`); the capture and the cache lookup stay on the worker and the recipe is recorded on the committer. A dispatch still drains when its lookup misses, when what it reads overlaps a write still pending, when the hit is verified against data or when its writes cannot be ranged (bindless access, GDS, fault buffer). An exception thrown by a commit is rethrown on the worker at its next enqueue or drain, as an inline draw error would be. On by default; `APS5_PIPELINED_DRAWS=0` records every draw inline, and `APS5_EXACT_DRAW_WRITES=0` widens the color and depth ranges to estimates. - The shader registry maps a code address to every header registered for that code, in registration order. Static, graphics and rectangle ABI resolution pick the header at the bound address (or one equal to it, `SameHeader`, which also takes a hull header fused by `sceAgcFuseShaderHalves` (type 3) as the registered back half (type 7) it copies, the fused registers aside; graphics ABI resolution then uses the fused header's shader registers); draws and dispatches pick the program by the stage's binary type (`RegisteredProgram`), and a front entry matching both type 2 and type 4 throws. Within a type the last registration wins; re-registering an identical earlier header makes its snapshot current again without preparing it again, and changed code drops the older headers. - The snapshot is published as pending. `SourceHandleFor`, `InvocationFor` and `ShaderPreparationTransaction::Edit` / `Read` wait for the pending job before reading `PreparedShaders`, and rethrow its failure. `APS5_SYNC_SHADER_PREPARE=1` prepares on the registering thread instead. - On devices with graphics pipeline libraries (`SpirvTarget::registrationModules`), a vertex or pixel artifact is emitted in registration form: the buffer V# words, image modes, swizzles and mip ranges, depth-compare state, export mappings and vertex component selectors it used to specialize on per draw are read at run time (image `ResourceMetadata` and a few state dwords in the shader data), descriptor heaps keep every element in place (an unused one gets a null or placeholder descriptor), and the pixel stage's bindings live in descriptor set 1 so each stage's library depends only on its own set layout (`INDEPENDENT_SETS`). An artifact above `registrationWordLimit` SPIR-V words is re-emitted specialized, and `CompiledShaderArtifact::registrationModule` records which form it has. For artifacts in registration form, `RegistrationResult` materializes the module once, and `PrebuildStageLibrary` builds its pre-rasterization or fragment-shader library on a separate one-thread pool (`APS5_PREBUILD_THREADS`) with a fixed default state, pairing it with a stub stage. A later draw with that state finds both libraries in the store and only links. From 89333e056f62752de58badbab2da0fdb5c35384e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Fri, 16 Oct 2026 11:52:36 +0200 Subject: [PATCH 0474/1000] test(agc): wait for the pipeline to empty between descriptor rounds --- .../prx/libSceAgcDriver/tests/execution/PipelinedDispatch.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PipelinedDispatch.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PipelinedDispatch.cpp index 6d61aaf018..8cc143e3c5 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PipelinedDispatch.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PipelinedDispatch.cpp @@ -219,7 +219,7 @@ int main() { std::atomic& flag; ~Release() { flag.store(true, std::memory_order_release); } } release{released}; - check(DrawPipeline::QueuedItems() == 0, "the pipeline had items before the descriptor round " + std::to_string(round)); + WaitUntil([] { return DrawPipeline::QueuedItems() == 0; }, "the pipeline to empty before descriptor round " + std::to_string(round)); const auto drainsBefore = DrawPipeline::DrainRequestsByOpcode(DispatchOpcode); DrawPipeline::Queue0().Enqueue([&released] { while (!released.load(std::memory_order_acquire)) std::this_thread::sleep_for(std::chrono::milliseconds(1)); From 7e383a894341dd26148b417da7fb5ff40e463e56 Mon Sep 17 00:00:00 2001 From: MrMha <114929652+MrMha@users.noreply.github.com> Date: Fri, 16 Oct 2026 15:02:22 +0400 Subject: [PATCH 0475/1000] fix: key oracle builds by the compiler selected through PATH --- tools/hw-oracle/hw_oracle.py | 4 +++- tools/hw-oracle/test_hw_oracle.py | 24 ++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/tools/hw-oracle/hw_oracle.py b/tools/hw-oracle/hw_oracle.py index 8424ab09bf..427f1ed006 100644 --- a/tools/hw-oracle/hw_oracle.py +++ b/tools/hw-oracle/hw_oracle.py @@ -44,7 +44,9 @@ def oracle(): root = rocm_root() if root is not None: root = root.resolve() - configuration = repr((compiler, str(root))).encode("utf-8") + executable = shutil.which(compiler[0]) + selected = str(Path(executable).resolve()) if executable is not None else None + configuration = repr((compiler, selected, str(root))).encode("utf-8") key = hashlib.sha256(source.read_bytes() + configuration).hexdigest() directory = CACHE / key binary = directory / "oracle" diff --git a/tools/hw-oracle/test_hw_oracle.py b/tools/hw-oracle/test_hw_oracle.py index 31fa5240e0..5cdc8c9e47 100644 --- a/tools/hw-oracle/test_hw_oracle.py +++ b/tools/hw-oracle/test_hw_oracle.py @@ -299,6 +299,30 @@ class TargetOverrideTests(unittest.TestCase): class OracleConfigurationTests(unittest.TestCase): + @unittest.skipIf(os.name == "nt", "POSIX executable compiler fixture") + def test_path_selecting_another_compiler_invalidates_the_cache(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + for name in ("first", "second"): + compiler = root / name / "fixture-cc" + compiler.parent.mkdir() + compiler.write_text( + "#!" + sys.executable + "\nimport sys\nfrom pathlib import Path\n" + "Path(sys.argv[sys.argv.index('-o') + 1]).write_bytes(" + repr(name.encode()) + ")\n", + encoding="utf-8") + compiler.chmod(0o755) + with patch.object(hw_oracle, "CACHE", root / "cache"), \ + patch.object(hw_oracle, "rocm_root", return_value=None): + with patch.dict(os.environ, {"CC": "fixture-cc", "PATH": str(root / "first")}): + first = hw_oracle.oracle() + self.assertEqual(first.read_bytes(), b"first") + self.assertEqual(hw_oracle.oracle(), first) + with patch.dict(os.environ, {"CC": "fixture-cc", "PATH": str(root / "second")}): + second = hw_oracle.oracle() + self.assertEqual(second.read_bytes(), b"second") + self.assertNotEqual(first, second) + + @unittest.skipUnless(sys.platform.startswith("linux"), "Linux compiler and shared-library fixture") def test_cache_and_compiler_command_with_host_toolchain(self): compiler = shutil.which("cc") From 7041ba8d9a9fdf8b30895fe1da2aec3cd68d0718 Mon Sep 17 00:00:00 2001 From: abnergtz <63434566+abnergtz@users.noreply.github.com> Date: Sat, 10 Oct 2026 23:52:43 -0300 Subject: [PATCH 0476/1000] fix(recompiler): terminate pixel invocation on BDA fault when supported Pixel invocations stopped by a BDA fault now end with OpTerminateInvocation when the target offers SPV_KHR_terminate_invocation, and keep OpKill otherwise. OpKill is not enough on AMD driver branch 21045, where a guest atomic after the kill still lands once per faulting pixel. Based on the approach in poonasor/AnyPS5@73969879. Refs #2009 --- .../prx/libSceAgcDriver/tests/Graphics.cpp | 23 +++++++++++++++---- .../SpirvBackend/src/SpirvBdaFault.cpp | 10 +++++++- 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 2e1d26e11f..f90bcdee3b 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -5592,7 +5592,7 @@ std::string interlockShape(std::span words) { return shape; } -std::string interlockExits(std::span words, std::size_t& kills) { +std::string interlockExits(std::span words, std::size_t& kills, std::size_t& terminates) { struct Block { std::vector ops; std::vector next; @@ -5610,7 +5610,7 @@ std::string interlockExits(std::span words, std::size_t& ki if (op == spv::OpLabel) label = words[at + 1]; if (op == spv::OpLabel && function == entry && first == 0) first = label; if (function != entry) { - if (op == spv::OpBeginInvocationInterlockEXT || op == spv::OpEndInvocationInterlockEXT || op == spv::OpKill) return "an interlock instruction or OpKill outside the entry point"; + if (op == spv::OpBeginInvocationInterlockEXT || op == spv::OpEndInvocationInterlockEXT || op == spv::OpKill || op == spv::OpTerminateInvocation) return "an interlock instruction, OpKill or OpTerminateInvocation outside the entry point"; continue; } if (label == 0) continue; @@ -5634,6 +5634,7 @@ std::string interlockExits(std::span words, std::size_t& ki if (op == spv::OpEndInvocationInterlockEXT && stage++ != 1) return "block " + std::to_string(current) + " ends the interlock outside it"; if ((op == spv::OpKill || op == spv::OpReturn || op == spv::OpReturnValue || op == spv::OpUnreachable || op == spv::OpTerminateInvocation) && stage != 2) return "an exit in block " + std::to_string(current) + " skips OpEndInvocationInterlockEXT"; if (op == spv::OpKill) ++kills; + if (op == spv::OpTerminateInvocation) ++terminates; } for (const auto next : blocks[current].next) { const auto [known, added] = state.emplace(next, stage); @@ -5682,9 +5683,21 @@ void orderedPixelShaderTests() { auto faulting = orderedPixelRequest(0x30600u, bdaInterlock, OrderedBdaPixelCode); faulting.target.supportedExtensions = bdaExtensions; faulting.target.bdaAbiVersion = ShaderRecompiler::BdaAbi::Version; - std::size_t kills = 0; - const auto exits = interlockExits(ShaderRecompiler::Recompile(faulting).spirv.Words(), kills); - Require(exits.empty() && kills == 6u, "a primitive-ordered pixel shader with BDA reads: " + (exits.empty() ? std::to_string(kills) + " OpKill, not the valid-mask kill and 5 BDA fault kills" : exits)); + std::size_t kills = 0, terminates = 0; + const auto exits = interlockExits(ShaderRecompiler::Recompile(faulting).spirv.Words(), kills, terminates); + Require(exits.empty() && kills == 6u && terminates == 0u, "a primitive-ordered pixel shader with BDA reads: " + (exits.empty() ? std::to_string(kills) + " OpKill and " + std::to_string(terminates) + " OpTerminateInvocation, not the valid-mask kill and 5 BDA fault kills" : exits)); + const std::array terminateExtensions{"SPV_KHR_physical_storage_buffer", "SPV_KHR_8bit_storage", "SPV_KHR_terminate_invocation"}; + auto terminating = faulting; + terminating.target.supportedExtensions = terminateExtensions; + const auto terminated = ShaderRecompiler::Recompile(terminating).spirv.Words(); + std::size_t terminatedKills = 0, terminatedStops = 0; + const auto terminatedExits = interlockExits(terminated, terminatedKills, terminatedStops); + Require(terminatedExits.empty() && terminatedKills == 1u && terminatedStops == 5u, "a primitive-ordered pixel shader with BDA reads and SPV_KHR_terminate_invocation: " + (terminatedExits.empty() ? std::to_string(terminatedKills) + " OpKill and " + std::to_string(terminatedStops) + " OpTerminateInvocation, not the valid-mask kill and 5 BDA fault terminations" : terminatedExits)); + std::size_t terminateDeclarations = 0; + for (std::size_t at = 5; at < terminated.size() && (terminated[at] >> 16u) != 0; at += terminated[at] >> 16u) { + if ((terminated[at] & 0xffffu) == spv::OpExtension && std::string_view(reinterpret_cast(&terminated[at + 1])) == "SPV_KHR_terminate_invocation") ++terminateDeclarations; + } + Require(terminateDeclarations == 1u, "the module declared SPV_KHR_terminate_invocation " + std::to_string(terminateDeclarations) + " times"); expectFailure([&] { static_cast(ShaderRecompiler::Recompile(orderedPixelRequest(0x30600u, plain))); }, "fragmentShaderPixelInterlock"); } diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvBdaFault.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvBdaFault.cpp index 4b0d60d921..3ae3e3744e 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvBdaFault.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvBdaFault.cpp @@ -1,6 +1,7 @@ #include "SpirvBackend/SpirvBda.hpp" #include "SpirvBackend/SpirvMemory/SpirvTypes.hpp" #include "SpirvBackend/SpirvMemory/SpirvConstants.hpp" +#include #include namespace ShaderRecompiler { @@ -115,7 +116,14 @@ void StopBdaInvocationIf(SpirvEmitterState& state, std::uint32_t condition) { if (OrderedPixelShader(state)) { state.module.AddFunction(spv::OpEndInvocationInterlockEXT); } - state.module.AddFunction(state.program.Resources().stage == IrShaderStage::Pixel ? spv::OpKill : spv::OpReturn); + if (state.program.Resources().stage != IrShaderStage::Pixel) { + state.module.AddFunction(spv::OpReturn); + } else if (std::ranges::find(state.supportedExtensions, "SPV_KHR_terminate_invocation") != state.supportedExtensions.end()) { + state.module.EmitExtension("SPV_KHR_terminate_invocation"); + state.module.AddFunction(spv::OpTerminateInvocation); + } else { + state.module.AddFunction(spv::OpKill); + } } EmitLabel(state, next); } From 42cd3f2ec4f28e99c2ac40766a34e82c7b181ebe Mon Sep 17 00:00:00 2001 From: abnergtz <63434566+abnergtz@users.noreply.github.com> Date: Sat, 10 Oct 2026 23:53:56 -0300 Subject: [PATCH 0477/1000] feat(agc): gate SPV_KHR_terminate_invocation on shaderTerminateInvocation Shader validation accepts the extension only in fragment shaders and only when the device enabled shaderTerminateInvocation. The flag is part of the validation memo key. Refs #2009 --- .../Graphics/include/Context.hpp | 1 + .../Graphics/include/Pipeline.hpp | 2 +- .../prx/libSceAgcDriver/Graphics/src/Draw.cpp | 3 +- .../Graphics/src/ShaderValidation.cpp | 31 +++++++++---------- .../prx/libSceAgcDriver/tests/Graphics.cpp | 11 ++++++- 5 files changed, 28 insertions(+), 20 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp index cba5321c23..886e67680c 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp @@ -232,6 +232,7 @@ struct Context { VkSampleCountFlags sampleLocationSampleCounts = 0; VkSampleCountFlags sampleLocationGridSampleCounts = 0; PipelineCacheSynchronization* pipelineCacheSynchronization = nullptr; + bool shaderTerminateInvocation = false; PipelineCacheSynchronization::PipelineUseLock LockPipelineCacheForUse() const { return pipelineCacheSynchronization != nullptr ? pipelineCacheSynchronization->LockForPipelineUse() : PipelineCacheSynchronization::PipelineUseLock{}; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp index a195283b06..e588f58b67 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp @@ -126,7 +126,7 @@ bool ShadersWriteMemory(std::span shaders); bool DeclaresEarlyFragmentTests(const ShaderRecompiler::RecompileResult& program); void ValidateSampleMask(const Context& context, const State& state, std::span shaders); // Returns the color attachment locations the pixel shader writes. -std::set ValidateShaders(std::span shaders, const State& state, const VkPhysicalDeviceSubgroupProperties& subgroup, bool fragmentShaderBarycentric, bool descriptorIndexing = false, bool imageInt64Atomics = false, bool geometryShader = false, bool sampleRateShading = false, bool bufferInt64Atomics = false, bool shaderClipDistance = false, bool shaderCullDistance = false, bool viewportIndexLayer = false, bool runtimeDescriptorArray = false, bool demoteToHelperInvocation = false); +std::set ValidateShaders(std::span shaders, const State& state, const VkPhysicalDeviceSubgroupProperties& subgroup, bool fragmentShaderBarycentric, bool descriptorIndexing = false, bool imageInt64Atomics = false, bool geometryShader = false, bool sampleRateShading = false, bool bufferInt64Atomics = false, bool shaderClipDistance = false, bool shaderCullDistance = false, bool viewportIndexLayer = false, bool runtimeDescriptorArray = false, bool demoteToHelperInvocation = false, bool shaderTerminateInvocation = false); } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index 5ad7489d14..7eb39c07a6 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -646,6 +646,7 @@ bool ValidationKey(const Context& context, std::span shade add(context.shaderClipDistance); add(context.shaderCullDistance); add(context.viewportIndexLayer); + add(context.shaderTerminateInvocation); add(state.paClVsOutCntl); add(state.stages.mesh.has_value()); if (state.stages.mesh) { @@ -701,7 +702,7 @@ std::set CachedFragmentOutputs(const Context& context, std::span< } std::set outputs; try { - outputs = ValidateShaders(shaders, state, context.subgroup, context.fragmentShaderBarycentric, context.descriptorIndexing, context.imageInt64Atomics, context.geometryShader, context.sampleRateShading, context.bufferInt64Atomics, context.shaderClipDistance, context.shaderCullDistance, context.viewportIndexLayer, context.runtimeDescriptorArray, context.demoteToHelperInvocation); + outputs = ValidateShaders(shaders, state, context.subgroup, context.fragmentShaderBarycentric, context.descriptorIndexing, context.imageInt64Atomics, context.geometryShader, context.sampleRateShading, context.bufferInt64Atomics, context.shaderClipDistance, context.shaderCullDistance, context.viewportIndexLayer, context.runtimeDescriptorArray, context.demoteToHelperInvocation, context.shaderTerminateInvocation); } catch (const std::exception& error) { if (keyed) { std::lock_guard lock(validationMutex()); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp index 9c7dcb033e..0862785621 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp @@ -151,7 +151,7 @@ struct Module { } }; -Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysicalDeviceSubgroupProperties& subgroup, bool fragmentShaderBarycentric, bool descriptorIndexing, bool imageInt64Atomics, bool geometryShader, bool sampleRateShading, bool bufferInt64Atomics, bool shaderClipDistance, bool shaderCullDistance, bool viewportIndexLayer, bool runtimeDescriptorArray, bool demoteToHelperInvocation) { +Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysicalDeviceSubgroupProperties& subgroup, bool fragmentShaderBarycentric, bool descriptorIndexing, bool imageInt64Atomics, bool geometryShader, bool sampleRateShading, bool bufferInt64Atomics, bool shaderClipDistance, bool shaderCullDistance, bool viewportIndexLayer, bool runtimeDescriptorArray, bool demoteToHelperInvocation, bool shaderTerminateInvocation) { using Stage = ShaderRecompiler::ShaderStage; Require(compiled.program != nullptr, "missing compiled shader"); const auto& shader = *compiled.program; @@ -185,6 +185,7 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi std::uint32_t entryPoint = 0; std::set executionModeTargets; bool upperLeft = false; + bool terminateExtension = false; for (std::size_t cursor = 5; cursor < words.size();) { const auto count = words[cursor] >> 16u; Require(count != 0 && count <= words.size() - cursor, "truncated SPIR-V instruction"); @@ -331,9 +332,19 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi Require(fragment && fragmentShaderBarycentric, "SPV_KHR_fragment_shader_barycentric requires enabled fragmentShaderBarycentric in a fragment shader"); break; } + if (extension == "SPV_KHR_terminate_invocation") { + Require(fragment && shaderTerminateInvocation, "SPV_KHR_terminate_invocation requires enabled shaderTerminateInvocation in a fragment shader"); + terminateExtension = true; + break; + } Require(extension == "SPV_KHR_float_controls" || (imageInt64Atomics && extension == "SPV_EXT_shader_image_int64") || (viewportIndexLayer && extension == "SPV_EXT_shader_viewport_index_layer") || (fragment && demoteToHelperInvocation && extension == "SPV_EXT_demote_to_helper_invocation") || (mesh && (extension == "SPV_EXT_mesh_shader" || extension == "SPV_KHR_physical_storage_buffer")) || ((descriptorIndexing || runtimeDescriptorArray) && extension == "SPV_EXT_descriptor_indexing") || (shader.bdaAbiVersion == ShaderRecompiler::BdaAbi::Version && (extension == "SPV_KHR_physical_storage_buffer" || extension == "SPV_KHR_8bit_storage")), "unsupported SPIR-V extension"); break; } + case spv::OpTerminateInvocation: + Require(count == 1, "invalid OpTerminateInvocation instruction"); + Require(fragment && shaderTerminateInvocation, "OpTerminateInvocation requires enabled shaderTerminateInvocation in a fragment shader"); + Require(terminateExtension, "OpTerminateInvocation requires SPV_KHR_terminate_invocation"); + break; case spv::OpDecorateId: case spv::OpDecorationGroup: case spv::OpGroupDecorate: @@ -601,21 +612,7 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi } -bool DeclaresEarlyFragmentTests(const ShaderRecompiler::RecompileResult& program) { - const auto& words = program.spirv.Words(); - Require(words.size() >= 5 && words[0] == spv::MagicNumber, "the fragment shader is not a SPIR-V module"); - for (std::size_t cursor = 5; cursor < words.size();) { - const auto count = words[cursor] >> 16u; - Require(count != 0 && count <= words.size() - cursor, "truncated SPIR-V instruction"); - const auto op = static_cast(words[cursor] & 0xffffu); - if (op == spv::OpFunction) return false; - if (op == spv::OpExecutionMode && count >= 3 && words[cursor + 2] == spv::ExecutionModeEarlyFragmentTests) return true; - cursor += count; - } - return false; -} - -std::set ValidateShaders(std::span shaders, const State& state, const VkPhysicalDeviceSubgroupProperties& subgroup, bool fragmentShaderBarycentric, bool descriptorIndexing, bool imageInt64Atomics, bool geometryShader, bool sampleRateShading, bool bufferInt64Atomics, bool shaderClipDistance, bool shaderCullDistance, bool viewportIndexLayer, bool runtimeDescriptorArray, bool demoteToHelperInvocation) { +std::set ValidateShaders(std::span shaders, const State& state, const VkPhysicalDeviceSubgroupProperties& subgroup, bool fragmentShaderBarycentric, bool descriptorIndexing, bool imageInt64Atomics, bool geometryShader, bool sampleRateShading, bool bufferInt64Atomics, bool shaderClipDistance, bool shaderCullDistance, bool viewportIndexLayer, bool runtimeDescriptorArray, bool demoteToHelperInvocation, bool shaderTerminateInvocation) { using Stage = ShaderRecompiler::ShaderStage; const bool tessellation = state.stages.path == ShaderPath::Tessellation; const bool mesh = state.stages.path == ShaderPath::Geometry; @@ -636,7 +633,7 @@ std::set ValidateShaders(std::span shaders, Require(shaders[i].program != nullptr, "missing compiled shader"); Require(shaders[i].stage == expected, "graphics stage order disagrees"); for (const auto& binding : shaders[i].program->bindings) Require(binding.descriptorSet == ShaderRecompiler::RuntimeAbi::DescriptorSet || (shaders[i].stage == ShaderRecompiler::ShaderStage::Fragment && binding.descriptorSet == ShaderRecompiler::RuntimeAbi::FragmentDescriptorSet), "graphics resource uses a descriptor set other than its stage's"); - const auto current = Inspect(shaders[i], state, subgroup, fragmentShaderBarycentric, descriptorIndexing, imageInt64Atomics, geometryShader, sampleRateShading, bufferInt64Atomics, shaderClipDistance, shaderCullDistance, viewportIndexLayer, runtimeDescriptorArray, demoteToHelperInvocation); + const auto current = Inspect(shaders[i], state, subgroup, fragmentShaderBarycentric, descriptorIndexing, imageInt64Atomics, geometryShader, sampleRateShading, bufferInt64Atomics, shaderClipDistance, shaderCullDistance, viewportIndexLayer, runtimeDescriptorArray, demoteToHelperInvocation, shaderTerminateInvocation); if (i != 0) { for (const auto& [location, signature] : current.inputs) { const auto output = previous.outputs.find(location); diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index f90bcdee3b..9452795be1 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -4783,6 +4783,7 @@ struct ModuleShape { std::uint32_t clipDistanceLength = 0; std::uint32_t cullDistanceLength = 0; bool layerOutput = false; + bool terminateInvocation = false; }; void emit(std::vector& out, spv::Op op, std::initializer_list operands) { @@ -4977,7 +4978,7 @@ std::vector makeModule(const ModuleShape& shape) { } emit(function, spv::OpFunction, {voidType, main, 0, functionType}); emit(function, spv::OpLabel, {label}); - emit(function, spv::OpReturn, {}); + emit(function, shape.terminateInvocation ? spv::OpTerminateInvocation : spv::OpReturn, {}); emit(function, spv::OpFunctionEnd, {}); std::vector words{spv::MagicNumber, 0x10300, 0, next, 0}; emit(words, spv::OpCapability, {spv::CapabilityShader}); @@ -5003,6 +5004,14 @@ std::vector makeModule(const ModuleShape& shape) { words.resize(start + count, 0); for (std::size_t i = 0; i < extension.size(); ++i) words[start + i / 4] |= static_cast(static_cast(extension[i])) << ((i % 4) * 8); } + if (shape.terminateInvocation) { + const std::string extension = "SPV_KHR_terminate_invocation"; + const auto count = (extension.size() + 4) / 4; + words.push_back((static_cast(count + 1) << 16u) | spv::OpExtension); + const auto start = words.size(); + words.resize(start + count, 0); + for (std::size_t i = 0; i < extension.size(); ++i) words[start + i / 4] |= static_cast(static_cast(extension[i])) << ((i % 4) * 8); + } emit(words, spv::OpMemoryModel, {spv::AddressingModelLogical, spv::MemoryModelGLSL450}); const auto entryPointOffset = words.size(); if (shape.vertexInput) emit(words, spv::OpEntryPoint, {spv::ExecutionModelVertex, main, 0x6e69616du, 0, output, input}); From 0e8ce286e1830a07320a57ea7f03372d1425db54 Mon Sep 17 00:00:00 2001 From: abnergtz <63434566+abnergtz@users.noreply.github.com> Date: Sun, 11 Oct 2026 00:27:35 -0300 Subject: [PATCH 0478/1000] fix(agc): validate OpTerminateInvocation itself, not only its extension The validator only checked SPV_KHR_terminate_invocation when it saw the OpExtension, so a module using OpTerminateInvocation without declaring the extension, or in a non-fragment stage, was accepted. Require the fragment stage, the enabled shaderTerminateInvocation feature and the declared extension at the instruction itself. Refs #2009 --- .../prx/libSceAgcDriver/tests/Graphics.cpp | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 9452795be1..5faea9186b 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -4784,6 +4784,7 @@ struct ModuleShape { std::uint32_t cullDistanceLength = 0; bool layerOutput = false; bool terminateInvocation = false; + bool omitTerminateExtension = false; }; void emit(std::vector& out, spv::Op op, std::initializer_list operands) { @@ -5004,7 +5005,7 @@ std::vector makeModule(const ModuleShape& shape) { words.resize(start + count, 0); for (std::size_t i = 0; i < extension.size(); ++i) words[start + i / 4] |= static_cast(static_cast(extension[i])) << ((i % 4) * 8); } - if (shape.terminateInvocation) { + if (shape.terminateInvocation && !shape.omitTerminateExtension) { const std::string extension = "SPV_KHR_terminate_invocation"; const auto count = (extension.size() + 4) / 4; words.push_back((static_cast(count + 1) << 16u) | spv::OpExtension); @@ -6699,6 +6700,23 @@ void validationTests() { vertex.spirv = makeModule({.barycentric = true}); pixel.spirv = makeModule({.fragment = true}); expectFailure([&] { AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, true); }, "requires a fragment shader"); + const auto validateTerminate = [&](bool enabled) { + return AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false, false, false, false, false, false, false, false, false, enabled); + }; + vertex.spirv = makeModule({}); + pixel.spirv = makeModule({.fragment = true, .terminateInvocation = true}); + validateTerminate(true); + expectFailure([&] { validateTerminate(false); }, "shaderTerminateInvocation"); + vertex.spirv = makeModule({.terminateInvocation = true}); + pixel.spirv = makeModule({.fragment = true}); + expectFailure([&] { validateTerminate(true); }, "fragment shader"); + vertex.spirv = makeModule({}); + pixel.spirv = makeModule({.fragment = true, .terminateInvocation = true, .omitTerminateExtension = true}); + expectFailure([&] { validateTerminate(true); }, "OpTerminateInvocation requires SPV_KHR_terminate_invocation"); + expectFailure([&] { validateTerminate(false); }, "OpTerminateInvocation requires enabled shaderTerminateInvocation"); + vertex.spirv = makeModule({.terminateInvocation = true, .omitTerminateExtension = true}); + pixel.spirv = makeModule({.fragment = true}); + expectFailure([&] { validateTerminate(true); }, "OpTerminateInvocation requires enabled shaderTerminateInvocation in a fragment shader"); } { ShaderRecompiler::RecompileResult vertex; From 0fe8527a0a2e0e56bbf1aaa2483fdcdbff3f9683 Mon Sep 17 00:00:00 2001 From: abnergtz <63434566+abnergtz@users.noreply.github.com> Date: Wed, 14 Oct 2026 11:45:21 +0000 Subject: [PATCH 0479/1000] test(agc): cover terminate invocation specialization, stage boundaries and disabled feature --- .../prx/libSceAgcDriver/tests/Graphics.cpp | 47 +++++++++++++++++++ .../tests/ShaderDeviceProfile.cpp | 11 +++++ 2 files changed, 58 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 5faea9186b..df69286a9c 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -25,6 +25,7 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include "prx/libc/include/GuestHeap.hpp" #include "ImageTableAbi.hpp" +#include "SpirvBackend/SpirvSpecialization.hpp" #include #include #include @@ -5708,6 +5709,41 @@ void orderedPixelShaderTests() { if ((terminated[at] & 0xffffu) == spv::OpExtension && std::string_view(reinterpret_cast(&terminated[at + 1])) == "SPV_KHR_terminate_invocation") ++terminateDeclarations; } Require(terminateDeclarations == 1u, "the module declared SPV_KHR_terminate_invocation " + std::to_string(terminateDeclarations) + " times"); + const auto countOps = [](std::span words) { + std::map counts; + for (std::size_t at = 5; at < words.size() && (words[at] >> 16u) != 0; at += words[at] >> 16u) ++counts[static_cast(words[at] & 0xffffu)]; + return counts; + }; + const std::array bdaCapabilities{spv::CapabilityShader, spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess}; + auto unordered = orderedPixelRequest(0x20600u, bdaCapabilities, OrderedBdaPixelCode); + unordered.target.bdaAbiVersion = ShaderRecompiler::BdaAbi::Version; + unordered.target.supportedExtensions = terminateExtensions; + const auto unorderedOps = countOps(ShaderRecompiler::Recompile(unordered).spirv.Words()); + Require(!unorderedOps.contains(spv::OpBeginInvocationInterlockEXT), "a pixel shader without PRIMITIVE_ORDERED_PIXEL_SHADER began the interlock"); + Require(unorderedOps.at(spv::OpTerminateInvocation) == 5u && unorderedOps.at(spv::OpKill) == 1u, "a pixel shader with BDA reads and SPV_KHR_terminate_invocation did not terminate on a fault"); + unordered.target.supportedExtensions = bdaExtensions; + const auto unorderedKills = countOps(ShaderRecompiler::Recompile(unordered).spirv.Words()); + Require(!unorderedKills.contains(spv::OpTerminateInvocation) && unorderedKills.at(spv::OpKill) == 6u, "a pixel shader with BDA reads and no SPV_KHR_terminate_invocation did not kill on a fault"); + constexpr std::array vertexBdaCode{0x7e040f00u, 0x7e060280u, 0xdc208001u, 0x047d0002u, 0xdc308008u, 0x057d0002u, 0xbf8c3f70u, 0x4a080b04u, 0x7e0e0d04u, 0xf80008cfu, 0x07070707u, 0xbf810000u}; + const auto vertexWords = [&](std::span extensions) { + ShaderRecompiler::RecompileRequest request{}; + request.shader = {ShaderRecompiler::ShaderStage::Vertex, 0x10000u, vertexBdaCode, 0, {}}; + request.context.waveSize = 64; + request.context.vertex = ShaderRecompiler::ShaderVertexStageInfo{}; + request.target.vulkanVersion = 0x00401000u; + request.target.spirvVersion = 0x00010300u; + request.target.subgroupSize = 64; + request.target.supportedCapabilities = bdaCapabilities; + request.target.supportedExtensions = extensions; + request.target.bdaAbiVersion = ShaderRecompiler::BdaAbi::Version; + request.layout.pushConstantSizeBytes = 128; + request.useCache = false; + return ShaderRecompiler::Recompile(request).spirv.Words(); + }; + const auto vertexStops = vertexWords(bdaExtensions); + const auto vertexTerminate = vertexWords(terminateExtensions); + const auto vertexOps = countOps(vertexTerminate); + Require(vertexStops == vertexTerminate && !vertexOps.contains(spv::OpTerminateInvocation) && !vertexOps.contains(spv::OpKill) && vertexOps.at(spv::OpReturn) > 1u, "a vertex shader with BDA reads changed its fault exit when SPV_KHR_terminate_invocation was advertised"); expectFailure([&] { static_cast(ShaderRecompiler::Recompile(orderedPixelRequest(0x30600u, plain))); }, "fragmentShaderPixelInterlock"); } @@ -6717,6 +6753,17 @@ void validationTests() { vertex.spirv = makeModule({.terminateInvocation = true, .omitTerminateExtension = true}); pixel.spirv = makeModule({.fragment = true}); expectFailure([&] { validateTerminate(true); }, "OpTerminateInvocation requires enabled shaderTerminateInvocation in a fragment shader"); + vertex.spirv = makeModule({}); + pixel.spirv = ShaderRecompiler::SpecializeSpirv(makeModule({.fragment = true, .terminateInvocation = true})); + std::size_t terminates = 0; + std::size_t declarations = 0; + for (std::size_t at = 5; at < pixel.spirv.size(); at += pixel.spirv[at] >> 16u) { + const auto op = pixel.spirv[at] & 0xffffu; + if (op == spv::OpTerminateInvocation) ++terminates; + if (op == spv::OpExtension && std::string_view(reinterpret_cast(&pixel.spirv[at + 1])) == "SPV_KHR_terminate_invocation") ++declarations; + } + Require(terminates == 1u && declarations == 1u, "specialization dropped OpTerminateInvocation or its extension declaration"); + validateTerminate(true); } { ShaderRecompiler::RecompileResult vertex; diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderDeviceProfile.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderDeviceProfile.cpp index 1143e50b0d..ca31e4ab4d 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderDeviceProfile.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderDeviceProfile.cpp @@ -106,6 +106,17 @@ void CheckProfile() { enabled.indexing.shaderStorageImageArrayNonUniformIndexing = VK_TRUE; const AgcDriver::ShaderDeviceProfile accepted(enabled.Target(), enabled.device, enabled.limits); Require(accepted.Target().supportedCapabilities.size() == 6u, "enabled descriptor indexing was rejected"); + ProfileInput terminating; + VkPhysicalDeviceShaderTerminateInvocationFeaturesKHR terminate{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_TERMINATE_INVOCATION_FEATURES_KHR, &terminating.bda, VK_TRUE}; + terminating.device.pNext = &terminate; + terminating.extensions.push_back("SPV_KHR_terminate_invocation"); + const AgcDriver::ShaderDeviceProfile terminated(terminating.Target(), terminating.device, terminating.limits); + Require(std::ranges::find(terminated.Target().supportedExtensions, "SPV_KHR_terminate_invocation") != terminated.Target().supportedExtensions.end(), "enabled shaderTerminateInvocation was rejected"); + ProfileInput disabled; + VkPhysicalDeviceShaderTerminateInvocationFeaturesKHR disabledTerminate{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_TERMINATE_INVOCATION_FEATURES_KHR, &disabled.bda, VK_FALSE}; + disabled.device.pNext = &disabledTerminate; + disabled.extensions.push_back("SPV_KHR_terminate_invocation"); + Reject([&] { AgcDriver::ShaderDeviceProfile rejected(disabled.Target(), disabled.device, disabled.limits); }, "shaderTerminateInvocation"); } void CheckAbi() { From 54e95dfa76e23b9cf464f1cd6d32feae09e3275e Mon Sep 17 00:00:00 2001 From: abnergtz <63434566+abnergtz@users.noreply.github.com> Date: Sat, 10 Oct 2026 23:55:14 -0300 Subject: [PATCH 0480/1000] feat(agc): enable VK_KHR_shader_terminate_invocation on the device The device queries and enables shaderTerminateInvocation when available, advertises SPV_KHR_terminate_invocation to the recompiler and validation, and the shader device profile rejects a target that advertises the extension without the enabled feature. Based on the approach in poonasor/AnyPS5@73969879. Refs #2009 (cherry picked from commit 1866a3f70e7ac1c34c8cd87fc79a973bd9ba6947) --- .../Execution/src/ShaderDeviceProfile.cpp | 4 ++-- .../Execution/src/VulkanDevice.cpp | 17 +++++++++++++++++ .../tests/ShaderDeviceProfile.cpp | 1 + 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/ShaderDeviceProfile.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/ShaderDeviceProfile.cpp index 7dc7a4640c..45bbf6c978 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/ShaderDeviceProfile.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/ShaderDeviceProfile.cpp @@ -30,7 +30,7 @@ ShaderDeviceProfile::ShaderDeviceProfile(const ShaderRecompiler::SpirvTarget& ta const auto* barycentric = findFeatures(deviceInfo.pNext, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FRAGMENT_SHADER_BARYCENTRIC_FEATURES_KHR); const auto* mesh = findFeatures(deviceInfo.pNext, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MESH_SHADER_FEATURES_EXT); const auto* clock = findFeatures(deviceInfo.pNext, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_CLOCK_FEATURES_KHR); - const auto* drawParameters = findFeatures(deviceInfo.pNext, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_DRAW_PARAMETERS_FEATURES); + const auto* terminate = findFeatures(deviceInfo.pNext, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_TERMINATE_INVOCATION_FEATURES_KHR); const auto* maintenance8 = findFeatures(deviceInfo.pNext, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MAINTENANCE_8_FEATURES_KHR); Require(target.bdaAbiVersion == ShaderRecompiler::BdaAbi::Version, "shader device profile has an incompatible BDA ABI"); Require(bda != nullptr && bda->bufferDeviceAddress == VK_TRUE && core.shaderInt64 == VK_TRUE, "shader runtime requires enabled bufferDeviceAddress and shaderInt64"); @@ -65,7 +65,6 @@ ShaderDeviceProfile::ShaderDeviceProfile(const ShaderRecompiler::SpirvTarget& ta const bool meshEnabled = mesh != nullptr && mesh->meshShader == VK_TRUE; checkFeature(spv::CapabilityFragmentBarycentricKHR, barycentricEnabled, "fragmentShaderBarycentric"); checkFeature(spv::CapabilityMeshShadingEXT, meshEnabled, "meshShader"); - checkFeature(spv::CapabilityDrawParameters, drawParameters != nullptr && drawParameters->shaderDrawParameters == VK_TRUE, "shaderDrawParameters"); checkFeature(spv::CapabilityShaderClockKHR, clock != nullptr && clock->shaderSubgroupClock == VK_TRUE && clock->shaderDeviceClock == VK_TRUE, "shader clock"); Require(!target.fragmentShaderBarycentricEnabled || barycentricEnabled, "shader device profile exposes disabled barycentrics"); Require(!target.mesh || meshEnabled, "shader device profile exposes disabled mesh shaders"); @@ -76,6 +75,7 @@ ShaderDeviceProfile::ShaderDeviceProfile(const ShaderRecompiler::SpirvTarget& ta extensions.erase(std::unique(extensions.begin(), extensions.end()), extensions.end()); for (const auto& extension : extensions) extensionViews.emplace_back(extension); Require(std::ranges::find(extensionViews, "SPV_KHR_physical_storage_buffer") != extensionViews.end() && std::ranges::find(extensionViews, "SPV_KHR_8bit_storage") != extensionViews.end(), "shader device profile is missing runtime extensions"); + Require(std::ranges::find(extensionViews, "SPV_KHR_terminate_invocation") == extensionViews.end() || (terminate != nullptr && terminate->shaderTerminateInvocation == VK_TRUE), "shader device profile advertises a disabled feature: shaderTerminateInvocation"); this->target.supportedCapabilities = capabilities; this->target.supportedExtensions = extensionViews; } diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 694152052f..3174261ad5 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -277,6 +277,7 @@ struct VulkanDevice::State { bool tessellationShader = false; bool meshShader = false; bool fragmentShaderBarycentric = false; + bool shaderTerminateInvocation = false; bool geometryShader = false; bool sampleRateShading = false; bool shaderClipDistance = false; @@ -1133,6 +1134,12 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : state->fragmentShaderBarycentric = FragmentShaderBarycentricUsable(driver.driverID, state->fragmentShaderBarycentric); } } + VkPhysicalDeviceShaderTerminateInvocationFeaturesKHR terminateFeatures{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_TERMINATE_INVOCATION_FEATURES_KHR}; + if (hasExtension(VK_KHR_SHADER_TERMINATE_INVOCATION_EXTENSION_NAME)) { + VkPhysicalDeviceFeatures2 features{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2, &terminateFeatures}; + state->InstanceFunction("vkGetPhysicalDeviceFeatures2")(selected, &features); + state->shaderTerminateInvocation = terminateFeatures.shaderTerminateInvocation == VK_TRUE; + } VkPhysicalDeviceFragmentShaderInterlockFeaturesEXT interlockFeatures{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FRAGMENT_SHADER_INTERLOCK_FEATURES_EXT}; if (hasExtension(VK_EXT_FRAGMENT_SHADER_INTERLOCK_EXTENSION_NAME)) { VkPhysicalDeviceFeatures2 features{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2, &interlockFeatures}; @@ -1179,6 +1186,10 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : state->capabilities.push_back(spv::CapabilityFragmentBarycentricKHR); state->spirvExtensions.push_back("SPV_KHR_fragment_shader_barycentric"); } + if (state->shaderTerminateInvocation) { + deviceExtensions.push_back(VK_KHR_SHADER_TERMINATE_INVOCATION_EXTENSION_NAME); + state->spirvExtensions.push_back("SPV_KHR_terminate_invocation"); + } if (state->fragmentShaderPixelInterlock) { deviceExtensions.push_back(VK_EXT_FRAGMENT_SHADER_INTERLOCK_EXTENSION_NAME); state->capabilities.push_back(spv::CapabilityFragmentShaderPixelInterlockEXT); @@ -1651,6 +1662,11 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : barycentricFeatures.pNext = byteFeatures.pNext; byteFeatures.pNext = &barycentricFeatures; } + if (state->shaderTerminateInvocation) { + terminateFeatures.shaderTerminateInvocation = VK_TRUE; + terminateFeatures.pNext = byteFeatures.pNext; + byteFeatures.pNext = &terminateFeatures; + } if (state->fragmentShaderPixelInterlock) { interlockFeatures.pNext = byteFeatures.pNext; byteFeatures.pNext = &interlockFeatures; @@ -3490,6 +3506,7 @@ Graphics::Context VulkanDevice::buildContext() const { context.sampleRateShading = state->sampleRateShading; context.shaderClipDistance = state->shaderClipDistance; context.shaderCullDistance = state->shaderCullDistance; + context.shaderTerminateInvocation = state->shaderTerminateInvocation; context.nullDescriptors = state->shaderProfile != nullptr && state->shaderProfile->NullDescriptors(); context.bufferInt64Atomics = state->bufferInt64Atomics; context.placeholderImages = state->placeholderImages.get(); diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderDeviceProfile.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderDeviceProfile.cpp index ca31e4ab4d..470a8074e2 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderDeviceProfile.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderDeviceProfile.cpp @@ -99,6 +99,7 @@ void CheckProfile() { invalid([](auto& value) { value.capabilities.push_back(spv::CapabilityStorageImageMultisample); }, "shaderStorageImageMultisample"); invalid([](auto& value) { value.capabilities.push_back(spv::CapabilityFragmentBarycentricKHR); }, "fragmentShaderBarycentric"); invalid([](auto& value) { value.capabilities.push_back(spv::CapabilityMeshShadingEXT); }, "meshShader"); + invalid([](auto& value) { value.extensions.push_back("SPV_KHR_terminate_invocation"); }, "shaderTerminateInvocation"); ProfileInput enabled; enabled.capabilities.push_back(spv::CapabilitySampledImageArrayDynamicIndexing); enabled.capabilities.push_back(spv::CapabilityStorageImageArrayNonUniformIndexing); From 502a972f2d5c2390c66b4f3b68ecf49611462dd3 Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Thu, 15 Oct 2026 14:51:11 +0000 Subject: [PATCH 0481/1000] test(agc): update terminate checks for current APIs --- .../Graphics/src/ShaderValidation.cpp | 14 ++++++++++++++ core/libs/prx/libSceAgcDriver/tests/Graphics.cpp | 4 ++-- .../libSceAgcDriver/tests/ShaderDeviceProfile.cpp | 7 ++++--- 3 files changed, 20 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp index 0862785621..1463b56251 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp @@ -612,6 +612,20 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi } +bool DeclaresEarlyFragmentTests(const ShaderRecompiler::RecompileResult& program) { + const auto words = program.spirv.Words(); + Require(words.size() >= 5 && words[0] == spv::MagicNumber, "the fragment shader is not a SPIR-V module"); + for (std::size_t cursor = 5; cursor < words.size();) { + const auto count = words[cursor] >> 16u; + Require(count != 0 && count <= words.size() - cursor, "truncated SPIR-V instruction"); + const auto op = static_cast(words[cursor] & 0xffffu); + if (op == spv::OpFunction) return false; + if (op == spv::OpExecutionMode && count >= 3 && words[cursor + 2] == spv::ExecutionModeEarlyFragmentTests) return true; + cursor += count; + } + return false; +} + std::set ValidateShaders(std::span shaders, const State& state, const VkPhysicalDeviceSubgroupProperties& subgroup, bool fragmentShaderBarycentric, bool descriptorIndexing, bool imageInt64Atomics, bool geometryShader, bool sampleRateShading, bool bufferInt64Atomics, bool shaderClipDistance, bool shaderCullDistance, bool viewportIndexLayer, bool runtimeDescriptorArray, bool demoteToHelperInvocation, bool shaderTerminateInvocation) { using Stage = ShaderRecompiler::ShaderStage; const bool tessellation = state.stages.path == ShaderPath::Tessellation; diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index df69286a9c..5131ceaf2a 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -5743,7 +5743,7 @@ void orderedPixelShaderTests() { const auto vertexStops = vertexWords(bdaExtensions); const auto vertexTerminate = vertexWords(terminateExtensions); const auto vertexOps = countOps(vertexTerminate); - Require(vertexStops == vertexTerminate && !vertexOps.contains(spv::OpTerminateInvocation) && !vertexOps.contains(spv::OpKill) && vertexOps.at(spv::OpReturn) > 1u, "a vertex shader with BDA reads changed its fault exit when SPV_KHR_terminate_invocation was advertised"); + Require(std::ranges::equal(vertexStops, vertexTerminate) && !vertexOps.contains(spv::OpTerminateInvocation) && !vertexOps.contains(spv::OpKill) && vertexOps.at(spv::OpReturn) > 1u, "a vertex shader with BDA reads changed its fault exit when SPV_KHR_terminate_invocation was advertised"); expectFailure([&] { static_cast(ShaderRecompiler::Recompile(orderedPixelRequest(0x30600u, plain))); }, "fragmentShaderPixelInterlock"); } @@ -6737,7 +6737,7 @@ void validationTests() { pixel.spirv = makeModule({.fragment = true}); expectFailure([&] { AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, true); }, "requires a fragment shader"); const auto validateTerminate = [&](bool enabled) { - return AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false, false, false, false, false, false, false, false, false, enabled); + return AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false, false, false, false, false, false, false, false, false, false, enabled); }; vertex.spirv = makeModule({}); pixel.spirv = makeModule({.fragment = true, .terminateInvocation = true}); diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderDeviceProfile.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderDeviceProfile.cpp index 470a8074e2..53f43075d8 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderDeviceProfile.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderDeviceProfile.cpp @@ -9,6 +9,7 @@ #include #include #include +#include namespace { @@ -16,8 +17,8 @@ using AgcDriver::Graphics::Require; struct ProfileInput { std::vector capabilities{spv::CapabilityShader, spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess}; - std::array extensionStrings{"SPV_KHR_physical_storage_buffer", "SPV_KHR_8bit_storage"}; - std::array extensions{extensionStrings[0], extensionStrings[1]}; + std::vector extensionStrings{"SPV_KHR_physical_storage_buffer", "SPV_KHR_8bit_storage"}; + std::vector extensions{extensionStrings[0], extensionStrings[1]}; VkPhysicalDeviceRobustness2FeaturesEXT robustness{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ROBUSTNESS_2_FEATURES_EXT, nullptr, VK_FALSE, VK_FALSE, VK_TRUE}; VkPhysicalDeviceDescriptorIndexingFeatures indexing{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DESCRIPTOR_INDEXING_FEATURES, &robustness}; VkPhysicalDevice8BitStorageFeatures bytes{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_8BIT_STORAGE_FEATURES, &indexing, VK_TRUE}; @@ -54,7 +55,7 @@ void Reject(TAction action, const char* expected) { Require(std::string(error.what()).find(expected) != std::string::npos, "unexpected profile validation error"); return; } - throw std::runtime_error("invalid shader device profile was accepted"); + throw std::runtime_error(std::string("invalid shader device profile was accepted: ") + expected); } void CheckProfile() { From 8a82811d9b02fc18cb40a281bc9fe2b3e07ca659 Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Fri, 16 Oct 2026 05:40:22 +0000 Subject: [PATCH 0482/1000] fix(agc): preserve draw parameter profile validation Restore the shaderDrawParameters feature guard while adding shaderTerminateInvocation support. Cover enabled and disabled DrawParameters profiles, and pass the termination feature to the correct shader validation argument. --- .../libSceAgcDriver/Execution/src/ShaderDeviceProfile.cpp | 2 ++ core/libs/prx/libSceAgcDriver/tests/Graphics.cpp | 2 +- .../libs/prx/libSceAgcDriver/tests/ShaderDeviceProfile.cpp | 7 +++++++ 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/ShaderDeviceProfile.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/ShaderDeviceProfile.cpp index 45bbf6c978..df5ad7ec42 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/ShaderDeviceProfile.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/ShaderDeviceProfile.cpp @@ -30,6 +30,7 @@ ShaderDeviceProfile::ShaderDeviceProfile(const ShaderRecompiler::SpirvTarget& ta const auto* barycentric = findFeatures(deviceInfo.pNext, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FRAGMENT_SHADER_BARYCENTRIC_FEATURES_KHR); const auto* mesh = findFeatures(deviceInfo.pNext, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MESH_SHADER_FEATURES_EXT); const auto* clock = findFeatures(deviceInfo.pNext, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_CLOCK_FEATURES_KHR); + const auto* drawParameters = findFeatures(deviceInfo.pNext, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_DRAW_PARAMETERS_FEATURES); const auto* terminate = findFeatures(deviceInfo.pNext, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_TERMINATE_INVOCATION_FEATURES_KHR); const auto* maintenance8 = findFeatures(deviceInfo.pNext, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MAINTENANCE_8_FEATURES_KHR); Require(target.bdaAbiVersion == ShaderRecompiler::BdaAbi::Version, "shader device profile has an incompatible BDA ABI"); @@ -65,6 +66,7 @@ ShaderDeviceProfile::ShaderDeviceProfile(const ShaderRecompiler::SpirvTarget& ta const bool meshEnabled = mesh != nullptr && mesh->meshShader == VK_TRUE; checkFeature(spv::CapabilityFragmentBarycentricKHR, barycentricEnabled, "fragmentShaderBarycentric"); checkFeature(spv::CapabilityMeshShadingEXT, meshEnabled, "meshShader"); + checkFeature(spv::CapabilityDrawParameters, drawParameters != nullptr && drawParameters->shaderDrawParameters == VK_TRUE, "shaderDrawParameters"); checkFeature(spv::CapabilityShaderClockKHR, clock != nullptr && clock->shaderSubgroupClock == VK_TRUE && clock->shaderDeviceClock == VK_TRUE, "shader clock"); Require(!target.fragmentShaderBarycentricEnabled || barycentricEnabled, "shader device profile exposes disabled barycentrics"); Require(!target.mesh || meshEnabled, "shader device profile exposes disabled mesh shaders"); diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 5131ceaf2a..30078a407a 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -6737,7 +6737,7 @@ void validationTests() { pixel.spirv = makeModule({.fragment = true}); expectFailure([&] { AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, true); }, "requires a fragment shader"); const auto validateTerminate = [&](bool enabled) { - return AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false, false, false, false, false, false, false, false, false, false, enabled); + return AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false, false, false, false, false, false, false, false, false, false, false, enabled); }; vertex.spirv = makeModule({}); pixel.spirv = makeModule({.fragment = true, .terminateInvocation = true}); diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderDeviceProfile.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderDeviceProfile.cpp index 53f43075d8..f5014a3cb9 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderDeviceProfile.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderDeviceProfile.cpp @@ -100,6 +100,7 @@ void CheckProfile() { invalid([](auto& value) { value.capabilities.push_back(spv::CapabilityStorageImageMultisample); }, "shaderStorageImageMultisample"); invalid([](auto& value) { value.capabilities.push_back(spv::CapabilityFragmentBarycentricKHR); }, "fragmentShaderBarycentric"); invalid([](auto& value) { value.capabilities.push_back(spv::CapabilityMeshShadingEXT); }, "meshShader"); + invalid([](auto& value) { value.capabilities.push_back(spv::CapabilityDrawParameters); }, "shaderDrawParameters"); invalid([](auto& value) { value.extensions.push_back("SPV_KHR_terminate_invocation"); }, "shaderTerminateInvocation"); ProfileInput enabled; enabled.capabilities.push_back(spv::CapabilitySampledImageArrayDynamicIndexing); @@ -108,6 +109,12 @@ void CheckProfile() { enabled.indexing.shaderStorageImageArrayNonUniformIndexing = VK_TRUE; const AgcDriver::ShaderDeviceProfile accepted(enabled.Target(), enabled.device, enabled.limits); Require(accepted.Target().supportedCapabilities.size() == 6u, "enabled descriptor indexing was rejected"); + ProfileInput drawParameters; + drawParameters.capabilities.push_back(spv::CapabilityDrawParameters); + VkPhysicalDeviceShaderDrawParametersFeatures drawFeatures{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_DRAW_PARAMETERS_FEATURES, &drawParameters.bda, VK_TRUE}; + drawParameters.device.pNext = &drawFeatures; + const AgcDriver::ShaderDeviceProfile drawParametersAccepted(drawParameters.Target(), drawParameters.device, drawParameters.limits); + Require(std::ranges::find(drawParametersAccepted.Target().supportedCapabilities, spv::CapabilityDrawParameters) != drawParametersAccepted.Target().supportedCapabilities.end(), "enabled shaderDrawParameters was rejected"); ProfileInput terminating; VkPhysicalDeviceShaderTerminateInvocationFeaturesKHR terminate{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_TERMINATE_INVOCATION_FEATURES_KHR, &terminating.bda, VK_TRUE}; terminating.device.pNext = &terminate; From 00fdf5b4c4c6513416e1a7e119d89fb0c7dafdaa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Fri, 16 Oct 2026 11:47:50 +0200 Subject: [PATCH 0483/1000] test(agc): give the vertex terminate test the ClipDistance capability --- core/libs/prx/libSceAgcDriver/tests/Graphics.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 30078a407a..210b55680d 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -5725,6 +5725,7 @@ void orderedPixelShaderTests() { const auto unorderedKills = countOps(ShaderRecompiler::Recompile(unordered).spirv.Words()); Require(!unorderedKills.contains(spv::OpTerminateInvocation) && unorderedKills.at(spv::OpKill) == 6u, "a pixel shader with BDA reads and no SPV_KHR_terminate_invocation did not kill on a fault"); constexpr std::array vertexBdaCode{0x7e040f00u, 0x7e060280u, 0xdc208001u, 0x047d0002u, 0xdc308008u, 0x057d0002u, 0xbf8c3f70u, 0x4a080b04u, 0x7e0e0d04u, 0xf80008cfu, 0x07070707u, 0xbf810000u}; + const std::array vertexCapabilities{spv::CapabilityShader, spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess, spv::CapabilityClipDistance}; const auto vertexWords = [&](std::span extensions) { ShaderRecompiler::RecompileRequest request{}; request.shader = {ShaderRecompiler::ShaderStage::Vertex, 0x10000u, vertexBdaCode, 0, {}}; @@ -5733,7 +5734,7 @@ void orderedPixelShaderTests() { request.target.vulkanVersion = 0x00401000u; request.target.spirvVersion = 0x00010300u; request.target.subgroupSize = 64; - request.target.supportedCapabilities = bdaCapabilities; + request.target.supportedCapabilities = vertexCapabilities; request.target.supportedExtensions = extensions; request.target.bdaAbiVersion = ShaderRecompiler::BdaAbi::Version; request.layout.pushConstantSizeBytes = 128; From b85146dbf596eae40cfbec116b380938ee4bf0a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Fri, 16 Oct 2026 11:54:41 +0200 Subject: [PATCH 0484/1000] test(agc): keep the recompiled words alive in the terminate tests --- core/libs/prx/libSceAgcDriver/tests/Graphics.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 210b55680d..abc9744ce7 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -5700,7 +5700,7 @@ void orderedPixelShaderTests() { const std::array terminateExtensions{"SPV_KHR_physical_storage_buffer", "SPV_KHR_8bit_storage", "SPV_KHR_terminate_invocation"}; auto terminating = faulting; terminating.target.supportedExtensions = terminateExtensions; - const auto terminated = ShaderRecompiler::Recompile(terminating).spirv.Words(); + const auto terminated = ShaderRecompiler::Recompile(terminating).spirv.Copy(); std::size_t terminatedKills = 0, terminatedStops = 0; const auto terminatedExits = interlockExits(terminated, terminatedKills, terminatedStops); Require(terminatedExits.empty() && terminatedKills == 1u && terminatedStops == 5u, "a primitive-ordered pixel shader with BDA reads and SPV_KHR_terminate_invocation: " + (terminatedExits.empty() ? std::to_string(terminatedKills) + " OpKill and " + std::to_string(terminatedStops) + " OpTerminateInvocation, not the valid-mask kill and 5 BDA fault terminations" : terminatedExits)); @@ -5739,7 +5739,7 @@ void orderedPixelShaderTests() { request.target.bdaAbiVersion = ShaderRecompiler::BdaAbi::Version; request.layout.pushConstantSizeBytes = 128; request.useCache = false; - return ShaderRecompiler::Recompile(request).spirv.Words(); + return ShaderRecompiler::Recompile(request).spirv.Copy(); }; const auto vertexStops = vertexWords(bdaExtensions); const auto vertexTerminate = vertexWords(terminateExtensions); From a18d6e9a6b87bab0fe9a4007ede97f05ef1f25bf Mon Sep 17 00:00:00 2001 From: ps54all Date: Wed, 14 Oct 2026 16:22:42 +0200 Subject: [PATCH 0485/1000] feat(shader): store D16 data to SINT storage images image_store with D16 data to a storage image of a SINT format threw "stores 16-bit data to an image of a SINT format", both when the descriptor is decoded and in the SPIR-V image emitter, and the runtime image modes left out the SINT conversions for such images. God of War Ragnarok (PPSA08330) stops on it in compute shader 0x1052a17900 right after its MFSR setup. Each 16-bit half is now converted like the D16 format buffer stores (EmitD16StoreComponent): sign-extended, then saturated to the component width as for 32-bit data. The buffer form was measured on RDNA2 in #331; the image form is not measured (TechnicalDebt). agc_driver_image_store_sint now stores D16 data with DMASK 0xf and 0x5 to all eight SINT formats and after an image_atomic_add on 32_SINT, instead of expecting the refusal. Co-Authored-By: Claude Opus 5.5 --- .../tests/execution/ImageStoreSint.cpp | 35 ++++++++++++++----- .../Optimization/src/ResourceMaterializer.cpp | 3 +- .../SpirvBackend/src/SpirvImageEmitter.cpp | 5 +-- docs/dev/debt/shader.md | 2 +- 4 files changed, 29 insertions(+), 16 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageStoreSint.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageStoreSint.cpp index a178c8c162..8daddabd90 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ImageStoreSint.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageStoreSint.cpp @@ -55,9 +55,15 @@ alignas(256) constexpr std::array LoadCode{ 0xbf810000, }; -alignas(256) constexpr std::array D16Code{0x7e3c0300, 0x7e3e0280, 0xf0201108, 0x8001141e, 0xbf810000}; +alignas(256) constexpr std::array D16Code{ + 0x34020084, 0xe0381000, 0x80001401, 0xbf8c3f70, 0x7e3c0300, 0x7e3e0280, 0xf0201f08, 0x8001141e, + 0x7e3e0281, 0xf0201508, 0x8001141e, 0xbf810000, +}; alignas(256) constexpr std::array PackedCode{0x7e3c0300, 0x7e3e0280, 0xf0281108, 0x0001141e, 0xbf810000}; -alignas(256) constexpr std::array AtomicD16Code{0x7e3c0300, 0x7e3e0280, 0xf0441108, 0x0001141e, 0xf0201108, 0x8001141e, 0xbf810000}; +alignas(256) constexpr std::array AtomicD16Code{ + 0x34020084, 0xe0381000, 0x80001401, 0xbf8c3f70, 0x7e3c0300, 0x7e3e0280, 0xf0441108, 0x0001141e, + 0xf0201108, 0x8001141e, 0xbf810000, +}; alignas(256) constexpr std::array PackedWideCode{0x34020084, 0xe0381000, 0x80001401, 0xbf8c3f70, 0x7e3c0300, 0x7e3e0280, 0xf0281308, 0x0001141e, 0xbf810000}; alignas(256) constexpr std::array AtomicCode{0x7e3c0300, 0x7e3e0280, 0xf0441108, 0x0001141e, 0xbf810000}; @@ -181,13 +187,21 @@ std::uint32_t StoredComponent(const Format& format, std::uint32_t tid, std::uint return Saturate(Value(tid, index), format.bits); } -std::vector Expected(const Format& format, std::span writes) { +std::uint32_t StoredD16Component(const Format& format, std::uint32_t tid, std::uint32_t dmask, std::uint32_t component) { + if (((dmask >> component) & 1u) == 0u) return 0u; + std::uint32_t index = 0; + for (std::uint32_t lower = 0; lower < component; ++lower) index += (dmask >> lower) & 1u; + const auto half = (Value(tid, index / 2u) >> (16u * (index & 1u))) & 0xffffu; + return Saturate(SignExtend(half, 16u), format.bits); +} + +std::vector Expected(const Format& format, std::span writes, bool d16 = false) { auto bytes = Initial(); for (std::uint32_t tid = 0; tid < Threads; ++tid) { for (const auto& write : writes) { auto* texel = bytes.data() + TexelOffset(format, tid, write.row); for (std::uint32_t component = 0; component < format.components; ++component) { - const auto value = StoredComponent(format, tid, write.dmask, component); + const auto value = d16 ? StoredD16Component(format, tid, write.dmask, component) : StoredComponent(format, tid, write.dmask, component); std::memcpy(texel + component * format.bits / 8u, &value, format.bits / 8u); } } @@ -244,8 +258,8 @@ void Store(AgcDriver::VulkanDevice& device, std::uint8_t* texels, std::span(Input.size() * 4u))); } -void CheckTexels(const std::uint8_t* texels, const Format& format, std::span writes, const std::string& what) { - const auto expected = Expected(format, writes); +void CheckTexels(const std::uint8_t* texels, const Format& format, std::span writes, const std::string& what, bool d16 = false) { + const auto expected = Expected(format, writes, d16); for (std::uint32_t y = 0; y < Height; ++y) { for (std::uint32_t x = 0; x < Width; ++x) { const auto offset = TexelOffset(format, x, y); @@ -297,8 +311,10 @@ int main() { } Store(*device, texels, TitleCode, Formats[0].format, TitleSwizzle); CheckTexels(texels, Formats[0], TitleWrites, "image_store dmask:0x1 glc with DST_SEL X,1,1,1"); - RequireRefused(*device, texels, D16Code, 12u, "stores 16-bit data to an image of a SINT format", "image_store d16 of 16_SINT"); - RequireRefused(*device, texels, D16Code, 21u, "stores 16-bit data to an image of a SINT format", "image_store d16 of 32_SINT"); + for (const auto& format : Formats) { + Store(*device, texels, D16Code, format.format, SwizzleOf(format)); + CheckTexels(texels, format, StoreWrites, "image_store d16", true); + } RequireRefused(*device, texels, PackedCode, 12u, "storage image descriptor uses an unsupported format", "image_store_pck of 16_SINT"); RequireRefused(*device, texels, AtomicCode, 12u, "atomic image descriptor uses an unsupported format 12", "image_atomic_add of 16_SINT"); RequireRefused(*device, texels, StoreCode, 73u, "storage image descriptor uses an unsupported format", "image_store of 32_32_32_SINT"); @@ -306,7 +322,8 @@ int main() { Store(*device, texels, PackedWideCode, Formats[index].format, SwizzleOf(Formats[index])); CheckTexels(texels, Formats[index], PackedWideWrites, "image_store_pck dmask:0x3"); } - RequireRefused(*device, texels, AtomicD16Code, 21u, "stores 16-bit data to an image of a SINT format", "image_store d16 of an atomically updated 32_SINT image"); + Store(*device, texels, AtomicD16Code, Formats[2].format, IdentitySwizzle); + CheckTexels(texels, Formats[2], TitleWrites, "image_store d16 after image_atomic_add", true); std::puts("image store sint tests passed"); return 0; } catch (const std::exception& error) { diff --git a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp index 43aad3402d..ff8686cb2f 100644 --- a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp @@ -260,7 +260,6 @@ ImageDecode inspectImageDescriptor(const DescriptorValue& descriptor, const Imag decoded.numericClass = IrTextureNumericClass::Uint; } if ((rawSintStorage || (base.atomic && format == IrBufferFormat::Format32SInt)) && !base.packed) { - if (!base.depthBitsCompatible) return rejectImage(PoisonReason::InvalidFormat, "stores 16-bit data to an image of a SINT format"); decoded.conversionFormat = format; } } else if (decoded.numericClass == IrTextureNumericClass::Unsupported || (base.depthCompare && decoded.numericClass != IrTextureNumericClass::Float)) { @@ -1318,7 +1317,7 @@ std::vector ResourceMaterializer::RuntimeImageModes(const ImageRe } } } - if (storage && image.depthBitsCompatible && !image.packed && !image.atomic64 && ((image.written && !image.read) || image.atomic)) { + if (storage && !image.packed && !image.atomic64 && ((image.written && !image.read) || image.atomic)) { constexpr std::array formats{IrBufferFormat::Format32SInt, IrBufferFormat::Format32_32SInt, IrBufferFormat::Format32_32_32_32SInt, IrBufferFormat::Format16SInt, IrBufferFormat::Format8_8SInt, IrBufferFormat::Format16_16SInt, IrBufferFormat::Format8_8_8_8SInt, IrBufferFormat::Format16_16_16_16SInt}; for (const auto format : formats) { if (image.atomic && format != IrBufferFormat::Format32SInt) continue; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp index db0ca5d84b..d2ce577837 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp @@ -741,9 +741,6 @@ std::uint32_t StoreTexel(SpirvValueEmitContext& ctx, const ImageEmitAccess& acce auto& state = ctx.state; const auto& mem = access.mem; const auto sint = SintStorageFormat(access.image); - if (sint.format != IrBufferFormat::Invalid && mem.dataBits == 16u) { - ctx.Fail(access.inst, "stores 16-bit data to an image of a SINT format"); - } std::uint32_t values[4] = {}; const auto dmask = EffectiveDmask(mem); for (std::uint32_t component = 0; component < 4u; component++) { @@ -766,7 +763,7 @@ std::uint32_t StoreTexel(SpirvValueEmitContext& ctx, const ImageEmitAccess& acce } values[component] = integer ? raw : mem.dataBits == 16u ? EmitF16BitsToF32(state, raw) : Unary(state, spv::OpBitcast, TypeF32(state), raw); if (component < sint.componentCount) { - values[component] = EmitFormatStoreComponent(state, sint, component, values[component]); + values[component] = mem.dataBits == 16u ? EmitD16StoreComponent(state, sint, component, values[component]) : EmitFormatStoreComponent(state, sint, component, values[component]); } } const auto texel = state.module.AllocateId(); diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index 9a5d65d1aa..ac5340dcb2 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -86,7 +86,7 @@ - [v_interp_p1ll_f16, v_interp_p1lv_f16 and v_interp_p2_f16](../../../core/shader/recompiler/Translation/src/AttributeInstructions.cpp) read the 16-bit halves of each vertex's parameter through `VK_KHR_fragment_shader_barycentric` and throw without it, as fixed-function interpolation cannot split a 32-bit attribute. P10 and P20 are the vertex differences rounded to f16 to nearest even, which is not confirmed on hardware. v_interp_p1ll_f16 and v_interp_p1lv_f16 round the exact `P10 * I + P0` once to f32 and v_interp_p2_f16 rounds the exact `P20 * J + S2` once to f16, as measured on RDNA2 (#697); the exact sums are computed in f64, so these instructions need the Float64 capability. They follow the shader's float mode, measured on RDNA2 (gfx1035) in all eight combinations of `IEEE_MODE` and kept/flushed f32 and f16 denormals over 768 special and 1,024 random rows. With IEEE=1 a NaN operand is returned quieted; with IEEE=0 it is returned unquieted, except that v_interp_p2_f16's f16 result is always quiet. Flushed f32 denormals flush I/J, v_interp_p2_f16's f32 S2 and the f32 result; flushed f16 denormals flush P0, P10/P20, v_interp_p1lv_f16's f16 S2 and the f16 result. Denormal modes 1 and 2 (flush only inputs or only outputs) and rounding other than to nearest even are not measured and throw. An unknown mode follows IEEE=0 with denormals kept. Lavapipe has no barycentrics, so the draw test is skipped in CI; `agc_driver_interpolation_modes` checks the arithmetic in every mode against the hardware rows on any device with Float64. - Without `VK_KHR_fragment_shader_barycentric`, a pixel shader whose I/J VGPRs [only feed `v_interp_p1_f32`/`v_interp_p2_f32` of their own pair](../../../core/shader/recompiler/Optimization/src/HostInterpolationChecker.cpp) reads the attribute Vulkan interpolated. Any other read of them or of a raw vertex parameter takes the weights and the three vertex values from a [generated geometry stage](../../../core/shader/recompiler/SpirvBackend/src/BarycentricGeometryShader.cpp), which needs `geometryShader` and a vertex shader before it: tessellation, mesh and rect-list pipelines, primitives other than triangles, more than 32 fragment input locations and interfaces beyond the device's geometry limits throw. - [v_fma_f64 and v_div_fmas_f64](../../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterFloat64.cpp) with a subnormal operand or result round once, as IEEE 754 fusedMultiplyAdd does with f64 denormals kept, measured on RDNA2 (gfx1035) at IEEE=0 and IEEE=1, `v_div_fmas_f64` with VCC clear and set. The f16/f64 denormal field of `FLOAT_MODE` (bits 7:6) is not read: when it flushes, the hardware flushes the operands and the result of `v_fma_f64` (tininess before rounding) and only the result of `v_div_fmas_f64`, while these keep them. Shaders with f64 instructions declare `DenormPreserve` for 64-bit floats only when `shaderDenormPreserveFloat64` is true and Vulkan reports `denormBehaviorIndependence` as `ALL`; other independence modes are not handled by the current backend because they require coupled execution modes. See [VkShaderFloatControlsIndependence](https://docs.vulkan.org/refpages/latest/refpages/source/VkShaderFloatControlsIndependence.html). Elsewhere (including lavapipe), the driver may flush f64 subnormals, and `agc_driver_f64_fma_subnormals` is skipped. -- [SINT storage images](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) of 8- and 16-bit components saturate each stored 32-bit value to the component range, as adriadam10 measured for image stores on RDNA2 (gfx1035) in #944 (448 rows match); the PS5's gfx1013 is not measured. +- [SINT storage images](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) of 8- and 16-bit components saturate each stored 32-bit value to the component range, as adriadam10 measured for image stores on RDNA2 (gfx1035) in #944 (448 rows match); the PS5's gfx1013 is not measured. A D16 store sign-extends each 16-bit half before that saturation, as the D16 format buffer stores measured in #331 do; D16 image stores to SINT surfaces themselves are not measured. - [`StoreTexel`](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) writes channel 1 of a two-channel format from the data slot whose DST_SEL is Y, for SINT and UINT alike. With DST_SEL X,Y,Z,W, gfx1035 writes it from the W slot, as adriadam10 measured in #944. - [v_ldexp_f16](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) follows the `FLOAT_MODE` f16 denormal field: bit 0 preserves the half input and bit 1 the half result. Otherwise a denormal input is flushed to its signed zero, and a result whose exact scaled value is below the minimum normal (2^-14) is flushed to its signed zero before rounding, so `v_ldexp_f16 0x07ff, -1` gives +0 instead of 0x0400. Measured on RDNA2 (gfx1036) with round to nearest, IEEE 0 and 1 and denorm16 0..3, over every half magnitude 0x0000..0x0fff of both signs scaled by 2^-1 and 2^0 (16384 rows per mode): no mismatch. NaN and infinity handling and destination high-half preservation are unchanged. Shaders without a known float mode keep the previous lowering. - [v_mullit_f32](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) follows the hardware measured with the MODE register at IEEE=1 and denormals kept (a signaling NaN src0 is quieted only with IEEE=1, as measured at IEEE=0 too; a positive denormal src2 counts as positive); with f32 denormals flushed it follows the f32 denormals entry below, and denormal modes 1 and 2 throw. From d8c2b71d12ccd54b5676f028563055b452cbe156 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Fri, 16 Oct 2026 13:18:43 +0200 Subject: [PATCH 0486/1000] docs(debt): D16 SINT image stores measured on RDNA2 --- docs/dev/debt/shader.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index ac5340dcb2..dea88ed54f 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -86,7 +86,7 @@ - [v_interp_p1ll_f16, v_interp_p1lv_f16 and v_interp_p2_f16](../../../core/shader/recompiler/Translation/src/AttributeInstructions.cpp) read the 16-bit halves of each vertex's parameter through `VK_KHR_fragment_shader_barycentric` and throw without it, as fixed-function interpolation cannot split a 32-bit attribute. P10 and P20 are the vertex differences rounded to f16 to nearest even, which is not confirmed on hardware. v_interp_p1ll_f16 and v_interp_p1lv_f16 round the exact `P10 * I + P0` once to f32 and v_interp_p2_f16 rounds the exact `P20 * J + S2` once to f16, as measured on RDNA2 (#697); the exact sums are computed in f64, so these instructions need the Float64 capability. They follow the shader's float mode, measured on RDNA2 (gfx1035) in all eight combinations of `IEEE_MODE` and kept/flushed f32 and f16 denormals over 768 special and 1,024 random rows. With IEEE=1 a NaN operand is returned quieted; with IEEE=0 it is returned unquieted, except that v_interp_p2_f16's f16 result is always quiet. Flushed f32 denormals flush I/J, v_interp_p2_f16's f32 S2 and the f32 result; flushed f16 denormals flush P0, P10/P20, v_interp_p1lv_f16's f16 S2 and the f16 result. Denormal modes 1 and 2 (flush only inputs or only outputs) and rounding other than to nearest even are not measured and throw. An unknown mode follows IEEE=0 with denormals kept. Lavapipe has no barycentrics, so the draw test is skipped in CI; `agc_driver_interpolation_modes` checks the arithmetic in every mode against the hardware rows on any device with Float64. - Without `VK_KHR_fragment_shader_barycentric`, a pixel shader whose I/J VGPRs [only feed `v_interp_p1_f32`/`v_interp_p2_f32` of their own pair](../../../core/shader/recompiler/Optimization/src/HostInterpolationChecker.cpp) reads the attribute Vulkan interpolated. Any other read of them or of a raw vertex parameter takes the weights and the three vertex values from a [generated geometry stage](../../../core/shader/recompiler/SpirvBackend/src/BarycentricGeometryShader.cpp), which needs `geometryShader` and a vertex shader before it: tessellation, mesh and rect-list pipelines, primitives other than triangles, more than 32 fragment input locations and interfaces beyond the device's geometry limits throw. - [v_fma_f64 and v_div_fmas_f64](../../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterFloat64.cpp) with a subnormal operand or result round once, as IEEE 754 fusedMultiplyAdd does with f64 denormals kept, measured on RDNA2 (gfx1035) at IEEE=0 and IEEE=1, `v_div_fmas_f64` with VCC clear and set. The f16/f64 denormal field of `FLOAT_MODE` (bits 7:6) is not read: when it flushes, the hardware flushes the operands and the result of `v_fma_f64` (tininess before rounding) and only the result of `v_div_fmas_f64`, while these keep them. Shaders with f64 instructions declare `DenormPreserve` for 64-bit floats only when `shaderDenormPreserveFloat64` is true and Vulkan reports `denormBehaviorIndependence` as `ALL`; other independence modes are not handled by the current backend because they require coupled execution modes. See [VkShaderFloatControlsIndependence](https://docs.vulkan.org/refpages/latest/refpages/source/VkShaderFloatControlsIndependence.html). Elsewhere (including lavapipe), the driver may flush f64 subnormals, and `agc_driver_f64_fma_subnormals` is skipped. -- [SINT storage images](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) of 8- and 16-bit components saturate each stored 32-bit value to the component range, as adriadam10 measured for image stores on RDNA2 (gfx1035) in #944 (448 rows match); the PS5's gfx1013 is not measured. A D16 store sign-extends each 16-bit half before that saturation, as the D16 format buffer stores measured in #331 do; D16 image stores to SINT surfaces themselves are not measured. +- [SINT storage images](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) of 8- and 16-bit components saturate each stored 32-bit value to the component range, as adriadam10 measured for image stores on RDNA2 (gfx1035) in #944 (448 rows match); the PS5's gfx1013 is not measured. A D16 store sign-extends each 16-bit half before that saturation, as measured on RDNA2 (gfx1035) for `image_store d16` to all eight SINT formats with DMASK 0xf, 0x7, 0x5, 0x3, 0x2, 0xa and 0x1 (1792 rows match). - [`StoreTexel`](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) writes channel 1 of a two-channel format from the data slot whose DST_SEL is Y, for SINT and UINT alike. With DST_SEL X,Y,Z,W, gfx1035 writes it from the W slot, as adriadam10 measured in #944. - [v_ldexp_f16](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) follows the `FLOAT_MODE` f16 denormal field: bit 0 preserves the half input and bit 1 the half result. Otherwise a denormal input is flushed to its signed zero, and a result whose exact scaled value is below the minimum normal (2^-14) is flushed to its signed zero before rounding, so `v_ldexp_f16 0x07ff, -1` gives +0 instead of 0x0400. Measured on RDNA2 (gfx1036) with round to nearest, IEEE 0 and 1 and denorm16 0..3, over every half magnitude 0x0000..0x0fff of both signs scaled by 2^-1 and 2^0 (16384 rows per mode): no mismatch. NaN and infinity handling and destination high-half preservation are unchanged. Shaders without a known float mode keep the previous lowering. - [v_mullit_f32](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) follows the hardware measured with the MODE register at IEEE=1 and denormals kept (a signaling NaN src0 is quieted only with IEEE=1, as measured at IEEE=0 too; a positive denormal src2 counts as positive); with f32 denormals flushed it follows the f32 denormals entry below, and denormal modes 1 and 2 throw. From d40347def7858c64924c878060e90d37e0197d3f Mon Sep 17 00:00:00 2001 From: MrMha <114929652+MrMha@users.noreply.github.com> Date: Fri, 16 Oct 2026 15:27:44 +0400 Subject: [PATCH 0487/1000] fix: preserve quoted Git filenames in overlap detection --- tools/pr_overlap.py | 21 ++++++++----- tools/tests/test_pr_overlap.py | 39 +++++++++++++++++++++++++ tools/tests/test_pr_overlap_encoding.py | 4 +-- 3 files changed, 54 insertions(+), 10 deletions(-) diff --git a/tools/pr_overlap.py b/tools/pr_overlap.py index 6a558f828d..0351d6a685 100644 --- a/tools/pr_overlap.py +++ b/tools/pr_overlap.py @@ -17,7 +17,11 @@ FETCH_REFS = 100 def git(*args): - return subprocess.run(["git", "-c", "core.quotePath=false", *args], capture_output=True, text=True, encoding="utf-8", errors="replace", check=True).stdout + result = subprocess.run(["git", "-c", "core.quotePath=false", *args], capture_output=True) + result.stdout = result.stdout.decode("utf-8", errors="replace") + result.stderr = result.stderr.decode("utf-8", errors="replace") + result.check_returncode() + return result.stdout def own(prs, sha): @@ -79,11 +83,12 @@ def fetch(base, prs): def merge(*args): - result = subprocess.run(["git", "-c", "core.quotePath=false", "merge-tree", "--write-tree", "--name-only", "--no-messages", *args], - capture_output=True, text=True, encoding="utf-8", errors="replace") - lines = result.stdout.splitlines() - if result.returncode not in (0, 1) or not lines: - raise RuntimeError(f"git merge-tree {' '.join(args)} failed: {result.stderr.strip()}") + result = subprocess.run(["git", "-c", "core.quotePath=false", "merge-tree", "--write-tree", "--name-only", "--no-messages", "-z", *args], + capture_output=True) + lines = result.stdout.decode("utf-8", errors="replace").rstrip("\0").split("\0") + if result.returncode not in (0, 1) or not lines[0]: + error = result.stderr.decode("utf-8", errors="replace").strip() + raise RuntimeError(f"git merge-tree {' '.join(args)} failed: {error}") return lines[0], set(lines[1:]) if result.returncode else set() @@ -115,8 +120,8 @@ def scan(pr): pr["tree"] = None if conflicted else commit(tree) old, new = (git("merge-base", "refs/pr/base", pr["ref"]).strip(), pr["ref"]) if conflicted else ("refs/pr/base", pr["tree"]) pr["files"], pr["added"] = set(), set() - for line in git("diff", "--name-status", "--no-renames", old, new).splitlines(): - status, path = line.split("\t", 1) + changes = git("diff", "--name-status", "--no-renames", "-z", old, new).split("\0") + for status, path in zip(changes[::2], changes[1::2]): pr["files"].add(path) if status == "A": pr["added"].add(path) diff --git a/tools/tests/test_pr_overlap.py b/tools/tests/test_pr_overlap.py index dad871a5e2..3127a1b00d 100644 --- a/tools/tests/test_pr_overlap.py +++ b/tools/tests/test_pr_overlap.py @@ -227,6 +227,45 @@ class ConflictTests(unittest.TestCase): class UnicodePathTests(unittest.TestCase): + @unittest.skipIf(os.name == "nt", "POSIX filenames containing control characters") + def test_control_characters_keep_exact_paths_in_scans_and_conflicts(self): + previous = os.getcwd() + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + def git(*args): + return subprocess.run( + ["git", "-c", "user.name=Overlap Test", "-c", "user.email=test@example.com", + "-c", "commit.gpgsign=false", *args], cwd=root, + capture_output=True, text=True, encoding="utf-8", check=True).stdout.strip() + filenames = {"core/libs/prx/libExample/return\r.cpp", "core/libs/prx/libExample/tab\t.cpp", "core/libs/prx/libExample/line\n.cpp", + "core/libs/prx/libExample/quote\".cpp"} + for filename in filenames: + source = root / filename + source.parent.mkdir(parents=True, exist_ok=True) + source.write_text("int value = 0;\n", encoding="utf-8") + git("init", "-q") + git("add", ".") + git("commit", "-qm", "test: create base") + base = git("rev-parse", "HEAD") + git("update-ref", "refs/pr/base", base) + for branch, value in (("left", 1), ("right", 2)): + git("checkout", "-qb", branch, base) + for filename in filenames: + (root / filename).write_text(f"int value = {value};\n", encoding="utf-8") + git("add", ".") + git("commit", "-qm", "fix: change value") + try: + os.chdir(root) + left, right = {"ref": "left"}, {"ref": "right"} + pr_overlap.scan(left) + pr_overlap.scan(right) + self.assertEqual(left["files"], filenames) + self.assertEqual(right["files"], filenames) + self.assertEqual(pr_overlap.conflicts(left, right), {name: [(1, 1)] for name in filenames}) + finally: + os.chdir(previous) + + def test_scans_and_reports_conflicts_with_git_path_quoting_enabled(self): previous = os.getcwd() with tempfile.TemporaryDirectory() as directory: diff --git a/tools/tests/test_pr_overlap_encoding.py b/tools/tests/test_pr_overlap_encoding.py index c8c04aff4d..1dad26c746 100644 --- a/tools/tests/test_pr_overlap_encoding.py +++ b/tools/tests/test_pr_overlap_encoding.py @@ -81,11 +81,11 @@ class EncodingTests(unittest.TestCase): def test_merge_conflict_preserves_unicode_paths(self): path = UNICODE + ".txt" - with command_output(TREE + "\n" + path + "\n", returncode=1): + with command_output(TREE + "\0" + path + "\0", returncode=1): self.assertEqual(pr_overlap.merge("main", "topic"), (TREE, {path})) def test_merge_success(self): - with command_output(TREE + "\n"): + with command_output(TREE + "\0"): self.assertEqual(pr_overlap.merge("main", "topic"), (TREE, set())) def test_merge_error_preserves_unicode(self): From 9bd554a64e43d4f1a3ab825659aa7c8c166ad58f Mon Sep 17 00:00:00 2001 From: hugodearaujo Date: Fri, 16 Oct 2026 08:28:23 -0300 Subject: [PATCH 0488/1000] fix(agc): note where a map-time settle blocks, and expect settles only for dma-buf imports An import made at map time settles inside the guest's map call, so the mapping thread waits while the kernel maps the pages. A device that imports shared memory through host pointers settles nothing, so the test expects a settle only for a dma-buf import. --- .../tests/execution/SparseDmaBufImport.cpp | 13 +++++++++++-- docs/dev/debt/libSceAgcDriver.md | 2 +- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SparseDmaBufImport.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SparseDmaBufImport.cpp index 33e26c12d6..72da9158b6 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/SparseDmaBufImport.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SparseDmaBufImport.cpp @@ -64,6 +64,12 @@ bool Imported(VkDevice device, std::uint64_t address, std::size_t bytes) { return AgcDriver::Graphics::HostImportCovers(probe, address, bytes); } +bool DmaBufImport(const AgcDriver::VulkanDevice& device, std::uint64_t address, std::size_t bytes) { + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + const auto* import = AgcDriver::Graphics::HostImportFor(AgcDriver::VulkanDeviceTestAccess::ContextOf(device), address, bytes); + return import != nullptr && import->dmaBuf; +} + } int main() { @@ -118,14 +124,17 @@ int main() { const bool settles = AgcDriver::VulkanDeviceTestAccess::ContextOf(*device).settleQueue != VK_NULL_HANDLE; const auto settledBefore = AgcDriver::Graphics::HostImportsSettled(); device->ImportGuestMemory(Register(small, SmallBytes), GuestAllocations::GuestAllocationsGeneration_nid_postfix(), false); + const auto settledAtMap = AgcDriver::Graphics::HostImportsSettled(); const bool smallImported = Imported(device->Device(), reinterpret_cast(small), static_cast(SmallBytes)); + const bool settlesShared = settles && smallImported && DmaBufImport(*device, reinterpret_cast(small), static_cast(SmallBytes)); GuestAllocations::Mutation().Remove(small); if (!smallImported) { std::printf("skipped, the device does not import dma-buf memory\n"); return VulkanTestSkipped; } if (!settles) std::printf("note: the device has no settle queue, so dma-buf imports are not settled\n"); - Require(!settles || AgcDriver::Graphics::HostImportsSettled() == settledBefore + 1, "sparse import: the 1 MiB dma-buf import made at map time was not settled on the settle queue"); + else if (!settlesShared) std::printf("note: the device imports shared memory through host pointers, which are not settled\n"); + Require(!settlesShared || settledAtMap == settledBefore + 1, "sparse import: the 1 MiB dma-buf import made at map time was not settled on the settle queue"); const auto boundary = ChunkBytes; std::memset(mapping + boundary - 65536, 0x11, 131072); @@ -135,7 +144,7 @@ int main() { const bool atMap = device->ImportGuestMemory(ranges, GuestAllocations::GuestAllocationsGeneration_nid_postfix(), false); Require(Imported(device->Device(), address, static_cast(bytes)), "sparse import: a range larger than udmabuf's size limit was not imported"); Require(atMap, "sparse import: the import made without the GPU lock was refused"); - Require(!settles || AgcDriver::Graphics::HostImportsSettled() == settledSmall + 1, "sparse import: the sparse dma-buf import made at map time was not settled on the settle queue"); + Require(!settlesShared || AgcDriver::Graphics::HostImportsSettled() == settledSmall + 1, "sparse import: the sparse dma-buf import made at map time was not settled on the settle queue"); bool filled = false; { diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 9bc8012517..b5f95e29d2 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -66,7 +66,7 @@ - [Pending storage-image results over memory outside the write watch](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) leave to the CPU the 64 KiB blocks whose bytes differ from the ones saved at the upload, the fill, the image copy, the previous store or the moment their range left the watch (`Unwatch`, or a host import that does not stay watched), and the blocks the watch saw the CPU write before that moment. A store made on the GPU saves its bytes through a host-visible copy that becomes the saved bytes when its batch completes; a store or a refresh that needs them earlier waits for that batch. A unit without a write generation saves the guest bytes when results land in it outside the watch. One whose range leaves the watch while its results are pending, and ranges that leave the watch through a failed collect, have no saved bytes; storing such results throws. - Windows host-pointer imports of the guest mapping itself are compared instead of write-watched, except writable imports of private `MEM_WRITE_WATCH` memory of 1 MiB or more when the import probe keeps the write watch and a settle queue exists (see Pages under an import window): a driver can mark imported pages dirty after the synchronous import window. Shared memory imported through a separate writable alias retains protection-based tracking on the guest views; driver access to the import alias does not disarm those views. [GuestMemory](../../../core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp) excludes every overlapping 64 KiB tracker block while its backing is retained. A full block replaced by fresh private backing regains tracking and gets a new generation; protection changes, partial replacements and shared remappings do not restore it. Excluded blocks keep their write-watch marks as a hint only (`UntrackedHint`): a storage image or a sampled texture snapshot over them compares its bytes again after a mark in its range, a driver store, a mapping change or a recorded GPU write instead of at every collect epoch or lookup, and a mark the driver makes without a CPU store costs one comparison. This preserves CPU edits through byte comparisons but limits snapshot reuse on excluded blocks to draws of one collect epoch and disables unit shadows for them; restoring tracking on import retirement without replacing the backing remains unimplemented. - [Pages under an import window](../../../core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp) (libSceAgcDriver) whose write protection was resolved while a host import that leaves its range watched was made are compared with the bytes they held before the import and count as stored over only when those differ. A CPU store of the bytes already there within the window cannot be told apart from an import that resolves the write protection without storing, as the NVIDIA driver (610.57.04) does for every imported page, so such a store does not win over pending results. Where host-pointer and dma-buf imports differ in whether they stay watched, a failed host-pointer import and the dma-buf import after it are two windows, and the pages the failed one resolved count as stored over. On Windows, the NVIDIA driver (617.42) marks every page of a host import of private `MEM_WRITE_WATCH` memory written, asynchronously, until the import's first GPU use completes. Such an import stays watched only after a 4-byte read of it on the settle queue has completed inside the window. Each one waits for that read, which keeps its window open for one round trip on the settle queue, so a same-value CPU store from another thread in that time counts as unchanged too. An import made where no settle queue exists, and an import of under 1 MiB, is compared instead: the settle blocks the importing thread, and a title that imports a 64 KiB buffer around every submission lost about 19 % of its frame rate to that wait (Dripl1x, #6060). Settling without blocking the importing thread (submit the read, keep the import compared until its fence signals, then switch it to the write watch) is not implemented. -- [Settled dma-buf imports](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver): on Linux a dma-buf import of 1 MiB or more is read by a 4-byte copy on the settle queue right after it is made (`settleImport`), so the kernel maps its pages for the GPU there and not in the first submission that uses it (about 2 s for a 3 GiB import on RADV). The import's buffer is shared with the settle queue's family. An import made from a submission settles under the GPU mutex, so its frame still pays for it; a smaller import, and every import on a device with no settle queue (one without a copy-capable queue besides the main one), pays in its first submission +- [Settled dma-buf imports](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver): on Linux a dma-buf import of 1 MiB or more is read by a 4-byte copy on the settle queue right after it is made (`settleImport`), so the kernel maps its pages for the GPU there and not in the first submission that uses it (about 2 s for a 3 GiB import on RADV). The import's buffer is shared with the settle queue's family. An import made at map time settles on the guest thread that maps the range, inside its map call, so that thread waits while the kernel maps the pages: 0.47 to 2.2 s for the 3 GiB range of Hades (PPSA03355) and 0.49 to 4.3 s for the 4.5 GiB range of ASTRO BOT (PPSA21567) on RADV. Both titles map these ranges at start-up; a title that maps large ranges during gameplay stalls its mapping thread for as long. An import made from a submission settles under the GPU mutex, so its frame still pays for it; a smaller import, and every import on a device with no settle queue (one without a copy-capable queue besides the main one), pays in its first submission. A range imported through a host pointer, as lavapipe imports shared memory, is not settled - [Sparse dma-buf imports](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver): on Linux a shared-backed range larger than one 64 MiB udmabuf buffer is imported as one sparse buffer bound to the 64 MiB buffers, on a queue of its own when the device has a second family that binds sparse memory. The path is decided before anything is imported, and a failure after that throws. A range takes the single udmabuf import instead, which udmabuf's `size_limit_mb` refuses above 64 MiB by default so the range is copied, when its offset in the backing file is not a multiple of the buffer's sparse block size (64 KiB on RADV), when the sparse buffer does not accept every memory type a chunk import can have, when the backing file ends inside its last chunk, or when it is imported at map time on a device with no second family (the bind then needs the GPU mutex); the NVIDIA proprietary driver imports no dma-buf memory - [Unnormalized S# coordinates](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp) (libSceAgcDriver) - `FORCE_UNNORMALIZED` uses a Vulkan `unnormalizedCoordinates` sampler, so the S# LOD range, LOD bias and mip filter are not applied, which is exact only on a one-level view. - An [unnormalized S#](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp) (libSceAgcDriver) maps clamp mode 0 (repeat) on X and Y to clamp-to-edge as required by Vulkan unnormalized coordinates, and assigns the magnification filter when minification and magnification filters differ (since unnormalized coordinates sample at LOD 0). Anisotropy, TRUNC_COORD, MC_COORD_TRUNC and other clamp modes still throw. Secondary LOD bias (LOD_BIAS_SEC, bits [19:14]) is accepted and ignored, matching RDNA2 hardware measurements across explicit and implicit samples. High on Life (PPSA15388, #4736) uses secondary LOD bias and unnormalized coordinates with clamp mode 0 and differing min/mag filters during home base and city rendering. From 7a09d070c5ad964e45529f4df603d2a3cde56f2a Mon Sep 17 00:00:00 2001 From: MrMha <114929652+MrMha@users.noreply.github.com> Date: Fri, 16 Oct 2026 15:29:00 +0400 Subject: [PATCH 0489/1000] fix: classify every explicit export alias in import audits --- tools/import_audit.py | 11 +++++------ tools/tests/test_import_audit.py | 13 +++++++++++++ 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/tools/import_audit.py b/tools/import_audit.py index 037dba8fe3..100c912ce0 100644 --- a/tools/import_audit.py +++ b/tools/import_audit.py @@ -360,10 +360,10 @@ def read_modules(directories): return imports, {name: sorted(files) for name, files in exports.items()} -def nid_of(name, aliases): +def nids_of(name, aliases): if name in aliases: return aliases[name] - return nid_names.compute_nid(name.removesuffix(NID_POSTFIX)) + return {nid_names.compute_nid(name.removesuffix(NID_POSTFIX))} def source_stubs(source): @@ -375,14 +375,13 @@ def source_stubs(source): aliases = {} for file in library.rglob("*.cpp"): for nid, name in nid_names.source_aliases(file.read_text(errors="ignore")): - aliases[name] = nid + aliases.setdefault(name, set()).add(nid) group = progress.scan_library(library) if "shared_sources" in group: shared[library.name] = group["shared_sources"] - done = {nid_of(name, aliases) for name in group["done_names"]} + done = {nid for name in group["done_names"] for nid in nids_of(name, aliases)} for name in group["todo_names"]: - nid = nid_of(name, aliases) - if nid not in done: + for nid in nids_of(name, aliases) - done: stubs.setdefault((library.name + ".prx", nid), name) for library, owner in shared.items(): chain = [library] diff --git a/tools/tests/test_import_audit.py b/tools/tests/test_import_audit.py index 9a0359e654..ef860de913 100644 --- a/tools/tests/test_import_audit.py +++ b/tools/tests/test_import_audit.py @@ -907,6 +907,19 @@ class ImportAuditTests(unittest.TestCase): with self.assertRaisesRegex(import_audit.AuditError, "shared source cycle.*libA.*libB"): import_audit.source_stubs(source) + def test_multiple_aliases_of_one_function_keep_the_same_classification(self): + aliases = 'APS5_EXPORT("AAAAAAAAAAA", sceExample);\nAPS5_EXPORT("BBBBBBBBBBB", sceExample);\n' + for body, classification in ((STUB_SOURCE, "stub"), (DONE_SOURCE, "implemented")): + with self.subTest(classification=classification): + write_source(self.work.source, "libSceExample", aliases + body.format(name="sceExample")) + stubs = import_audit.source_stubs(self.work.source) + records, _ = import_audit.audit( + [(nid, "libSceExample.prx", False) for nid in ("AAAAAAAAAAA", "BBBBBBBBBBB")], + {nid: ["libSceExample.prx"] for nid in ("AAAAAAAAAAA", "BBBBBBBBBBB")}, stubs, + {"libSceExample.prx"}, set()) + self.assertEqual([record["class"] for record in records], [classification, classification]) + + def test_formatted_export_aliases_remain_stubs(self): work = self.work nid = "AAAAAAAAAAA" From b1e88d5995795b56b00df928280bdec345bd722c Mon Sep 17 00:00:00 2001 From: milescoviello Date: Fri, 16 Oct 2026 11:36:24 +0000 Subject: [PATCH 0490/1000] fix(agc): journal the mesh output primitive size #5884 added MeshConfiguration::outputPrimitiveSize, and #5875's pipeline journal guards the layouts it encodes. The two merged separately, so on Linux with libstdc++ PipelineJournal.cpp no longer compiles: MeshConfiguration is 40 bytes, ShaderStages (which holds an optional one) 112. The guards now name those sizes, and the recipe codec writes and reads the new field, so a replayed line-strip geometry pipeline keeps its two-vertex primitives. Co-Authored-By: Claude Opus 5.5 --- .../prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp | 8 ++++---- core/libs/prx/libSceAgcDriver/tests/Graphics.cpp | 5 +++++ 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp index 094b1ed9bd..8729ac4d9d 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp @@ -36,8 +36,8 @@ static_assert(sizeof(VkVertexInputBindingDescription) == 12, "VkVertexInputBindi static_assert(sizeof(VkVertexInputAttributeDescription) == 16, "VkVertexInputAttributeDescription changed: update the recipe codec"); #if defined(__linux__) && defined(__x86_64__) && defined(__GLIBCXX__) static_assert(sizeof(State) == 752, "State changed: update CanonicalPipelineState and the recipe codec"); -static_assert(sizeof(ShaderStages) == 108, "ShaderStages changed: update the recipe codec"); -static_assert(sizeof(ShaderRecompiler::MeshConfiguration) == 36, "MeshConfiguration changed: update the recipe codec"); +static_assert(sizeof(ShaderStages) == 112, "ShaderStages changed: update the recipe codec"); +static_assert(sizeof(ShaderRecompiler::MeshConfiguration) == 40, "MeshConfiguration changed: update the recipe codec"); static_assert(sizeof(ShaderRecompiler::TessellationConfiguration) == 40, "TessellationConfiguration changed: update the recipe codec"); #endif @@ -147,11 +147,11 @@ void encodeOptional(Writer& writer, const std::optional& value, void (*e } void encodeMesh(Writer& writer, const ShaderRecompiler::MeshConfiguration& mesh) { - for (const auto value : {mesh.inputPrimitive, mesh.primitivesPerGroup, mesh.verticesPerGroup, mesh.maxVertices, mesh.maxPrimitives, mesh.threadsPerGroup, mesh.ldsSizeDwords, mesh.provokingVertex, mesh.esgsItemSize}) writer.U32(value); + for (const auto value : {mesh.inputPrimitive, mesh.primitivesPerGroup, mesh.verticesPerGroup, mesh.maxVertices, mesh.maxPrimitives, mesh.threadsPerGroup, mesh.ldsSizeDwords, mesh.provokingVertex, mesh.esgsItemSize, mesh.outputPrimitiveSize}) writer.U32(value); } void decodeMesh(Reader& reader, ShaderRecompiler::MeshConfiguration& mesh) { - for (auto* value : {&mesh.inputPrimitive, &mesh.primitivesPerGroup, &mesh.verticesPerGroup, &mesh.maxVertices, &mesh.maxPrimitives, &mesh.threadsPerGroup, &mesh.ldsSizeDwords, &mesh.provokingVertex, &mesh.esgsItemSize}) *value = reader.U32(); + for (auto* value : {&mesh.inputPrimitive, &mesh.primitivesPerGroup, &mesh.verticesPerGroup, &mesh.maxVertices, &mesh.maxPrimitives, &mesh.threadsPerGroup, &mesh.ldsSizeDwords, &mesh.provokingVertex, &mesh.esgsItemSize, &mesh.outputPrimitiveSize}) *value = reader.U32(); } void encodeTessellation(Writer& writer, const ShaderRecompiler::TessellationConfiguration& tessellation) { diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 5acb0bb165..dcc4ea5ae9 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -7537,6 +7537,11 @@ void pipelineJournalTests() { BuildPipelineStoreKey(grid, context, gridState, input, layouts.View(), shaders, VK_IMAGE_LAYOUT_GENERAL); BuildPipelineStoreKey(gridReplayed, context, gridDecoded.state, gridDecoded.vertexInput, layouts.View(), shaders, gridDecoded.attachmentLayout); Require(grid != original && grid == gridReplayed, "the decoded sample grid state keys another pipeline"); + auto lineState = state; + lineState.stages.mesh = ShaderRecompiler::MeshConfiguration{1, 2, 2, 4, 4, 32, 64, 1, 4, 2}; + GraphicsPipelineRecipe lineDecoded; + Require(DecodeGraphicsPipelineRecipe(EncodeGraphicsPipelineRecipe(MakeGraphicsPipelineRecipe(lineState, input, layouts.View(), shaders, VK_IMAGE_LAYOUT_GENERAL)), lineDecoded), "the line strip mesh recipe does not decode"); + Require(lineDecoded.state.stages.mesh && lineDecoded.state.stages.mesh->outputPrimitiveSize == 2u && lineDecoded.state.stages.mesh->esgsItemSize == 4u, "the recipe dropped the mesh output primitive size"); auto fourSampleState = state; fourSampleState.samples = 4; auto maskState = fourSampleState; From e37137241dd6ae4d8421c66414afc316e14d9e49 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 16 Oct 2026 11:37:32 +0000 Subject: [PATCH 0491/1000] feat(ssl): run TLS 1.2 client connections over Mbed TLS sceSslConnect performs the client handshake on the host socket that the guest number maps to in the libkernel or libSceNet socket table, and sceSslRead and sceSslWrite carry application data. Session state comes from the context's memory pool, and the server is verified against the CAs loaded with sceSslLoadCert; a loaded client certificate is sent when the server asks for one. libkernel exports KernelLookupSocketNative_nid_no_patch, which checks its own socket table before the one libSceNet registers. Pools take a lock so connections can allocate outside the library lock. TLS 1.3 is compiled out. A missing CA, a verify callback, would-block sockets and every failure whose console code is unknown throw, as recorded in the libSceSsl debt entry. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RoU3ZF76A6uR8sCA7NMLch --- core/libs/prx/libSceSsl/CMakeLists.txt | 18 +- core/libs/prx/libSceSsl/Export.cpp | 248 +++++++++++++++-- core/libs/prx/libSceSsl/MbedConfig.hpp | 1 + .../libkernel/Socket/include/SocketPoll.hpp | 1 + core/libs/prx/libkernel/Socket/src/Udp.cpp | 9 + core/libs/tests/GuestSslTransport.cpp | 249 ++++++++++++++++++ core/libs/tests/SslCertificateFixture.hpp | 20 ++ docs/dev/debt/libSceSsl.md | 4 +- 8 files changed, 516 insertions(+), 34 deletions(-) create mode 100644 core/libs/tests/GuestSslTransport.cpp diff --git a/core/libs/prx/libSceSsl/CMakeLists.txt b/core/libs/prx/libSceSsl/CMakeLists.txt index 8142808e06..99095ec97a 100644 --- a/core/libs/prx/libSceSsl/CMakeLists.txt +++ b/core/libs/prx/libSceSsl/CMakeLists.txt @@ -46,8 +46,9 @@ function(configure_ssl_backend) endfunction() configure_ssl_backend() -set_target_properties(mbedcrypto mbedx509 p256m PROPERTIES POSITION_INDEPENDENT_CODE ON) +set_target_properties(mbedtls mbedcrypto mbedx509 p256m PROPERTIES POSITION_INDEPENDENT_CODE ON) if(MINGW) + target_compile_options(mbedtls PRIVATE -fno-asynchronous-unwind-tables) target_compile_options(mbedcrypto PRIVATE -fno-asynchronous-unwind-tables) target_compile_options(mbedx509 PRIVATE -fno-asynchronous-unwind-tables) target_compile_options(p256m PRIVATE -fno-asynchronous-unwind-tables) @@ -55,7 +56,7 @@ endif() add_library(libSceSsl SHARED EXCLUDE_FROM_ALL Export.cpp) target_include_directories(libSceSsl PRIVATE ${LIBS_INCLUDE_DIR}) -target_link_libraries(libSceSsl PRIVATE mbedx509 mbedcrypto) +target_link_libraries(libSceSsl PRIVATE mbedtls mbedx509 mbedcrypto) set_target_properties(libSceSsl PROPERTIES VISIBILITY_INLINES_HIDDEN ON) if(MINGW OR CMAKE_SYSTEM_NAME STREQUAL "Linux") target_link_options(libSceSsl PRIVATE LINKER:--exclude-libs,ALL) @@ -76,8 +77,17 @@ if(BUILD_TESTING) target_link_libraries(guest_ssl_load_tests PRIVATE libSceSsl libc) configure_windows_unwind(guest_ssl_load_tests) add_test(NAME guest_ssl_load COMMAND guest_ssl_load_tests) + add_test_executable(guest_ssl_transport_tests ${LIBS_INCLUDE_DIR}/tests/GuestSslTransport.cpp) + target_include_directories(guest_ssl_transport_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_ssl_transport_tests PRIVATE libSceSsl libSceNet libkernel libc mbedtls mbedx509 mbedcrypto) + configure_windows_unwind(guest_ssl_transport_tests) + foreach(table IN ITEMS kernel net) + add_test(NAME guest_ssl_transport_${table} COMMAND guest_ssl_transport_tests ${table}) + set_tests_properties(guest_ssl_transport_${table} PROPERTIES TIMEOUT 30) + endforeach() if(WIN32) - set_tests_properties(guest_ssl guest_ssl_load PROPERTIES ENVIRONMENT_MODIFICATION - "PATH=path_list_prepend:$") + target_link_libraries(guest_ssl_transport_tests PRIVATE bcrypt ws2_32) + set_tests_properties(guest_ssl guest_ssl_load guest_ssl_transport_kernel guest_ssl_transport_net PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$;PATH=path_list_prepend:$") endif() endif() diff --git a/core/libs/prx/libSceSsl/Export.cpp b/core/libs/prx/libSceSsl/Export.cpp index b12d6bb913..e51e6120bc 100644 --- a/core/libs/prx/libSceSsl/Export.cpp +++ b/core/libs/prx/libSceSsl/Export.cpp @@ -1,15 +1,27 @@ +#ifdef _WIN32 +#include +#else +#include +#include +#endif + #include "SslTypes.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libkernel/Socket/include/SocketPoll.hpp" #include #include #include +#include +#include #include #include #include +#include #include #include +#include #include #include #include @@ -26,10 +38,11 @@ namespace { constexpr int ErrorNotFound = static_cast(0x8095f004); constexpr int ErrorInvalidArg = static_cast(0x8095177a); constexpr int ErrorInvalidId = static_cast(0x8095f006); +constexpr int ReadPartial = 0x1; struct Pool { std::size_t size, current = 0, peak = 0; - bool exhausted = false; + std::mutex mutex; }; struct alignas(std::max_align_t) Allocation { @@ -38,21 +51,23 @@ struct alignas(std::max_align_t) Allocation { }; thread_local Pool* currentPool = nullptr; +thread_local bool currentPoolExhausted = false; void* PoolCalloc(std::size_t count, std::size_t size) noexcept { if (!currentPool) return nullptr; if (size && count > std::numeric_limits::max() / size) { - currentPool->exhausted = true; + currentPoolExhausted = true; return nullptr; } const auto bytes = count * size; + std::lock_guard lock(currentPool->mutex); if (bytes > currentPool->size - currentPool->current || bytes > std::numeric_limits::max() - sizeof(Allocation)) { - currentPool->exhausted = true; + currentPoolExhausted = true; return nullptr; } auto* allocation = static_cast(std::calloc(1, sizeof(Allocation) + bytes)); if (!allocation) { - currentPool->exhausted = true; + currentPoolExhausted = true; return nullptr; } allocation->pool = currentPool; @@ -65,15 +80,23 @@ void* PoolCalloc(std::size_t count, std::size_t size) noexcept { void PoolFree(void* pointer) noexcept { if (!pointer) return; auto* allocation = static_cast(pointer) - 1; - allocation->pool->current -= allocation->size; + { + std::lock_guard lock(allocation->pool->mutex); + allocation->pool->current -= allocation->size; + } mbedtls_platform_zeroize(pointer, allocation->size); std::free(allocation); } struct PoolScope { Pool* previous; - explicit PoolScope(Pool& pool) : previous(std::exchange(currentPool, &pool)) { pool.exhausted = false; } - ~PoolScope() { currentPool = previous; } + bool previousExhausted; + explicit PoolScope(Pool& pool) + : previous(std::exchange(currentPool, &pool)), previousExhausted(std::exchange(currentPoolExhausted, false)) {} + ~PoolScope() { + currentPool = previous; + currentPoolExhausted = previousExhausted; + } }; struct PoolDeleter { @@ -95,7 +118,7 @@ std::size_t Add(std::size_t left, std::size_t right) { void CheckBackend(int error, const char* message) { if (error == 0) return; - if (currentPool && currentPool->exhausted) throw std::bad_alloc(); + if (currentPool && currentPoolExhausted) throw std::bad_alloc(); throw std::invalid_argument(message); } @@ -156,6 +179,125 @@ struct Context { explicit Context(std::size_t size) : pool{size} {} }; +int HostError() { +#ifdef _WIN32 + return WSAGetLastError(); +#else + return errno; +#endif +} + +bool HostWouldBlock(int error) { +#ifdef _WIN32 + return error == WSAEWOULDBLOCK; +#else + return error == EAGAIN || error == EWOULDBLOCK; +#endif +} + +bool HostInterrupted(int error) { +#ifdef _WIN32 + return error == WSAEINTR; +#else + return error == EINTR; +#endif +} + +struct Session { + std::shared_ptr context; + std::shared_ptr identity; + std::shared_ptr socketOwner; + std::uintptr_t socket; + std::mutex mutex; + int hostError = 0; + Random random; + mbedtls_x509_crt authorities; + mbedtls_ssl_config config; + mbedtls_ssl_context ssl; + + Session(std::shared_ptr owner, std::shared_ptr socketHolder, std::uintptr_t native) + : context(std::move(owner)), identity(context->identity), socketOwner(std::move(socketHolder)), socket(native) { + mbedtls_x509_crt_init(&authorities); + mbedtls_ssl_config_init(&config); + mbedtls_ssl_init(&ssl); + } + + ~Session() { + PoolScope scope(context->pool); + mbedtls_ssl_free(&ssl); + mbedtls_ssl_config_free(&config); + mbedtls_x509_crt_free(&authorities); + } + + void Configure(const std::string& host) { + random.Seed(); + for (const auto& chain : context->authorities) + for (const auto* entry = &chain->root; entry; entry = entry->next) + CheckBackend(mbedtls_x509_crt_parse_der(&authorities, entry->raw.p, entry->raw.len), "SSL CA certificate copy failed"); + CheckBackend(mbedtls_ssl_config_defaults(&config, MBEDTLS_SSL_IS_CLIENT, MBEDTLS_SSL_TRANSPORT_STREAM, MBEDTLS_SSL_PRESET_DEFAULT), + "SSL client configuration failed"); + mbedtls_ssl_conf_authmode(&config, MBEDTLS_SSL_VERIFY_REQUIRED); + mbedtls_ssl_conf_ca_chain(&config, &authorities, nullptr); + mbedtls_ssl_conf_rng(&config, mbedtls_ctr_drbg_random, &random.drbg); + if (identity) + CheckBackend(mbedtls_ssl_conf_own_cert(&config, &identity->chain->root, &identity->key.key), "SSL client certificate setup failed"); + CheckBackend(mbedtls_ssl_setup(&ssl, &config), "SSL connection setup failed"); + CheckBackend(mbedtls_ssl_set_hostname(&ssl, host.c_str()), "SSL host name setup failed"); + mbedtls_ssl_set_bio(&ssl, this, Send, Receive, nullptr); + } + + static int Send(void* argument, const unsigned char* buffer, std::size_t length) { + auto& session = *static_cast(argument); + const auto chunk = static_cast(std::min(length, INT_MAX)); + for (;;) { +#ifdef _WIN32 + const auto sent = ::send(static_cast(session.socket), reinterpret_cast(buffer), chunk, 0); +#else + const auto sent = ::send(static_cast(session.socket), buffer, static_cast(chunk), MSG_NOSIGNAL); +#endif + if (sent >= 0) return static_cast(sent); + const int error = HostError(); + if (HostInterrupted(error)) continue; + if (HostWouldBlock(error)) return MBEDTLS_ERR_SSL_WANT_WRITE; + session.hostError = error; + return MBEDTLS_ERR_NET_SEND_FAILED; + } + } + + static int Receive(void* argument, unsigned char* buffer, std::size_t length) { + auto& session = *static_cast(argument); + const auto chunk = static_cast(std::min(length, INT_MAX)); + for (;;) { +#ifdef _WIN32 + const auto received = ::recv(static_cast(session.socket), reinterpret_cast(buffer), chunk, 0); +#else + const auto received = ::recv(static_cast(session.socket), buffer, static_cast(chunk), 0); +#endif + if (received >= 0) return static_cast(received); + const int error = HostError(); + if (HostInterrupted(error)) continue; + if (HostWouldBlock(error)) return MBEDTLS_ERR_SSL_WANT_READ; + session.hostError = error; + return MBEDTLS_ERR_NET_RECV_FAILED; + } + } + + [[noreturn]] void Fail(const char* function, int error) const { + if (currentPoolExhausted || error == MBEDTLS_ERR_SSL_ALLOC_FAILED) throw std::bad_alloc(); + if (error == MBEDTLS_ERR_SSL_WANT_READ || error == MBEDTLS_ERR_SSL_WANT_WRITE) + throw std::runtime_error(std::string(function) + ": the socket would block or its timeout expired; the console's error code is unknown"); + char text[160]; + mbedtls_strerror(error, text, sizeof(text)); + auto message = std::string(function) + ": " + text; + if (hostError) message += ", host socket error " + std::to_string(hostError); + if (error == MBEDTLS_ERR_X509_CERT_VERIFY_FAILED) { + char detail[512]; + if (mbedtls_x509_crt_verify_info(detail, sizeof(detail), "", mbedtls_ssl_get_verify_result(&ssl)) > 0) message += ", " + std::string(detail); + } + throw std::runtime_error(message + "; the console's error code is unknown"); + } +}; + using SslVerifyCallback = int (APS5_VABI *)(int, unsigned int, void**, int, void*); struct SslConnection { @@ -164,11 +306,12 @@ struct SslConnection { std::string host; SslVerifyCallback verifyCallback; void* verifyArgument; + std::shared_ptr session; }; struct Runtime { std::mutex mutex; - std::unordered_map> contexts; + std::unordered_map> contexts; std::unordered_map connections; unsigned nextHandle = 1; Runtime() { @@ -197,6 +340,13 @@ SslConnection& RequireConnection(const char* function, int sslConnectionId) { return connection->second; } +std::shared_ptr RequireSession(const char* function, int sslConnectionId) { + std::lock_guard lock(State().mutex); + auto& connection = RequireConnection(function, sslConnectionId); + if (!connection.session) throw std::logic_error(std::string(function) + ": the connection is not connected"); + return connection.session; +} + struct Input { Block bytes; std::size_t size; @@ -245,7 +395,7 @@ int APS5_VABI sceSslInit_nid_postfix(std::uint64_t poolSize) { std::lock_guard lock(state.mutex); if (state.nextHandle > static_cast(std::numeric_limits::max())) throw std::overflow_error("SSL context handles exhausted"); const auto handle = static_cast(state.nextHandle); - state.contexts.emplace(handle, std::make_unique(poolSize)); + state.contexts.emplace(handle, std::make_shared(poolSize)); ++state.nextHandle; return handle; } @@ -266,7 +416,8 @@ int APS5_VABI sceSslGetMemoryPoolStats(int sslCtxId, SceSsl::MemoryPoolStats* st std::memset(stats, 0, sizeof(*stats)); return static_cast(0x8095F006); } - const auto& pool = found->second->pool; + auto& pool = found->second->pool; + std::lock_guard poolLock(pool.mutex); *stats = {pool.size, pool.peak, pool.current, 0}; return 0; } @@ -415,14 +566,18 @@ int APS5_VABI sceSslCreateConnection(int sslCtxId, int sockId, const char* host) auto& state = State(); if (state.nextHandle > static_cast(std::numeric_limits::max())) throw std::overflow_error("SSL connection handles exhausted"); const auto id = static_cast(state.nextHandle); - state.connections.emplace(id, SslConnection{sslCtxId, sockId, host, nullptr, nullptr}); + state.connections.emplace(id, SslConnection{sslCtxId, sockId, host, nullptr, nullptr, {}}); ++state.nextHandle; return id; } int APS5_VABI sceSslDeleteConnection(int sslConnectionId) { + std::shared_ptr session; std::lock_guard lock(State().mutex); - if (!State().connections.erase(sslConnectionId)) throw std::invalid_argument("sceSslDeleteConnection: unknown connection"); + const auto found = State().connections.find(sslConnectionId); + if (found == State().connections.end()) throw std::invalid_argument("sceSslDeleteConnection: unknown connection"); + session = std::move(found->second.session); + State().connections.erase(found); return 0; } @@ -435,27 +590,64 @@ int APS5_VABI sceSslSetVerifyCallback(int sslConnectionId, SslVerifyCallback cbf } int APS5_VABI sceSslConnect(int sslConnectionId) { - (void)sslConnectionId; - NotImplemented_nid_no_patch(__func__); - return 0; + std::shared_ptr session; + { + auto& state = State(); + std::lock_guard lock(state.mutex); + auto& connection = RequireConnection(__func__, sslConnectionId); + if (connection.session) throw std::logic_error("sceSslConnect: the connection is already connected"); + if (connection.verifyCallback) throw std::invalid_argument("sceSslConnect: how the console invokes the verify callback is unknown"); + auto context = state.contexts.at(connection.context); + if (context->authorities.empty()) + throw std::runtime_error("sceSslConnect: no CA certificate is loaded, and the console's root certificates are not shipped"); + std::shared_ptr owner; + std::uintptr_t native = 0; + if (!KernelLookupSocketNative_nid_no_patch(connection.socket, &owner, &native)) + throw std::invalid_argument("sceSslConnect: descriptor " + std::to_string(connection.socket) + " is not a socket"); + PoolScope scope(context->pool); + session = std::make_shared(std::move(context), std::move(owner), native); + session->Configure(connection.host); + connection.session = session; + } + std::unique_lock sessionLock(session->mutex); + PoolScope scope(session->context->pool); + const int result = mbedtls_ssl_handshake(&session->ssl); + if (result == 0) return 0; + { + std::lock_guard lock(State().mutex); + const auto found = State().connections.find(sslConnectionId); + if (found != State().connections.end() && found->second.session == session) found->second.session.reset(); + } + session->Fail(__func__, result); } int APS5_VABI sceSslRead(int sslConnectionId, void* buf, size_t len, int flags) { - (void)sslConnectionId; - (void)buf; - (void)len; - (void)flags; - NotImplemented_nid_no_patch(__func__); - return 0; + if (flags != 0 && flags != ReadPartial) throw std::invalid_argument("sceSslRead: unsupported flags " + std::to_string(flags)); + if (!buf && len) throw std::invalid_argument("sceSslRead: null buffer"); + const auto session = RequireSession(__func__, sslConnectionId); + std::lock_guard sessionLock(session->mutex); + PoolScope scope(session->context->pool); + const int result = mbedtls_ssl_read(&session->ssl, static_cast(buf), std::min(len, INT_MAX)); + if (result >= 0) return result; + if (result == MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY) return 0; + session->Fail(__func__, result); } int APS5_VABI sceSslWrite(int sslConnectionId, void* buf, size_t len, int flags) { - (void)sslConnectionId; - (void)buf; - (void)len; - (void)flags; - NotImplemented_nid_no_patch(__func__); - return 0; + if (flags != 0) throw std::invalid_argument("sceSslWrite: unsupported flags " + std::to_string(flags)); + if (!buf && len) throw std::invalid_argument("sceSslWrite: null buffer"); + const auto session = RequireSession(__func__, sslConnectionId); + std::lock_guard sessionLock(session->mutex); + PoolScope scope(session->context->pool); + const auto* bytes = static_cast(buf); + const auto total = std::min(len, INT_MAX); + std::size_t written = 0; + while (written < total) { + const int result = mbedtls_ssl_write(&session->ssl, bytes + written, total - written); + if (result < 0) session->Fail(__func__, result); + written += static_cast(result); + } + return static_cast(written); } int APS5_VABI sceSslClose() { diff --git a/core/libs/prx/libSceSsl/MbedConfig.hpp b/core/libs/prx/libSceSsl/MbedConfig.hpp index ab4361c8a7..5555fc58d7 100644 --- a/core/libs/prx/libSceSsl/MbedConfig.hpp +++ b/core/libs/prx/libSceSsl/MbedConfig.hpp @@ -2,3 +2,4 @@ #define MBEDTLS_PLATFORM_MEMORY #undef MBEDTLS_ECDH_VARIANT_EVEREST_ENABLED +#undef MBEDTLS_SSL_PROTO_TLS1_3 diff --git a/core/libs/prx/libkernel/Socket/include/SocketPoll.hpp b/core/libs/prx/libkernel/Socket/include/SocketPoll.hpp index b21208982d..9bf3b90265 100644 --- a/core/libs/prx/libkernel/Socket/include/SocketPoll.hpp +++ b/core/libs/prx/libkernel/Socket/include/SocketPoll.hpp @@ -32,5 +32,6 @@ extern "C" void KernelSetSocketStatusControl_nid_no_patch(KernelSocketPoll::Stat extern "C" bool KernelControlSocketStatus_nid_no_patch(int descriptor, int command, int flags, int* result, int* error); extern "C" void KernelSetSocketNativeLookup_nid_no_patch(KernelSocketPoll::NativeLookup lookup); extern "C" bool KernelFindSocketNative_nid_no_patch(int descriptor, std::shared_ptr* owner, std::uintptr_t* native); +extern "C" bool KernelLookupSocketNative_nid_no_patch(int descriptor, std::shared_ptr* owner, std::uintptr_t* native); extern "C" int KernelReserveSocketNumber_nid_no_patch(std::uintptr_t native); extern "C" void KernelReleaseSocketNumber_nid_no_patch(int number); diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index 7588023bb7..3bcc051d4a 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -775,6 +775,15 @@ int GuestSockets::Poll(KernelSocketPoll::Entry* entries, int count, int timeoutM return ready; } +extern "C" bool KernelLookupSocketNative_nid_no_patch(int descriptor, std::shared_ptr* owner, std::uintptr_t* native) { + if (auto socket = Find(descriptor)) { + *native = static_cast(socket->value); + *owner = std::move(socket); + return true; + } + return KernelFindSocketNative_nid_no_patch(descriptor, owner, native); +} + extern "C" { int APS5_VABI fcntl_nid_postfix(int descriptor, int command, ...) { if (!GuestSockets::IsOpen(descriptor)) { diff --git a/core/libs/tests/GuestSslTransport.cpp b/core/libs/tests/GuestSslTransport.cpp new file mode 100644 index 0000000000..39dd8b497b --- /dev/null +++ b/core/libs/tests/GuestSslTransport.cpp @@ -0,0 +1,249 @@ +#ifdef _WIN32 +#include +#else +#include +#include +#include +#endif + +#include "prx/libSceSsl/SslTypes.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include "SslCertificateFixture.hpp" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceSslInit_nid_postfix(std::uint64_t); +int APS5_VABI sceSslTerm_nid_postfix(int); +int APS5_VABI sceSslGetMemoryPoolStats(int, SceSsl::MemoryPoolStats*); +int APS5_VABI sceSslLoadCert(int, int, SceSsl::Data**, SceSsl::Data*, SceSsl::Data*); +int APS5_VABI sceSslCreateConnection(int, int, const char*); +int APS5_VABI sceSslDeleteConnection(int); +int APS5_VABI sceSslSetVerifyCallback(int, int (APS5_VABI *)(int, unsigned int, void**, int, void*), void*); +int APS5_VABI sceSslConnect(int); +int APS5_VABI sceSslRead(int, void*, std::size_t, int); +int APS5_VABI sceSslWrite(int, void*, std::size_t, int); +int APS5_VABI socket_nid_postfix(int, int, int); +int APS5_VABI connect_nid_postfix(int, const void*, std::uint32_t); +int APS5_VABI close_nid_postfix(int); +int APS5_VABI sceNetInit_nid_postfix(); +int APS5_VABI sceNetSocket(const char*, int, int, int); +int APS5_VABI sceNetConnect(int, const void*, std::uint32_t); +int APS5_VABI sceNetSocketClose(int); +} + +namespace { + +void Require(bool condition, const char* message) { + if (condition) return; + std::fprintf(stderr, "%s\n", message); + std::exit(1); +} + +template +bool Throws(TCall call) { + try { + call(); + } catch (const std::exception&) { + return true; + } + return false; +} + +int APS5_VABI VerifyCallback(int, unsigned int, void**, int, void*) { return 0; } + +struct Server { + mbedtls_net_context listener; + mbedtls_entropy_context entropy; + mbedtls_ctr_drbg_context drbg; + mbedtls_x509_crt certificate; + mbedtls_x509_crt authority; + mbedtls_pk_context key; + mbedtls_ssl_config config; + std::uint16_t port = 0; + std::string failure; + + Server() { + mbedtls_net_init(&listener); + mbedtls_entropy_init(&entropy); + mbedtls_ctr_drbg_init(&drbg); + mbedtls_x509_crt_init(&certificate); + mbedtls_x509_crt_init(&authority); + mbedtls_pk_init(&key); + mbedtls_ssl_config_init(&config); + Require(mbedtls_ctr_drbg_seed(&drbg, mbedtls_entropy_func, &entropy, nullptr, 0) == 0, "seed server random generator"); + Require(mbedtls_x509_crt_parse(&certificate, reinterpret_cast(ServerPem), sizeof(ServerPem)) == 0, + "parse server certificate"); + Require(mbedtls_x509_crt_parse(&authority, reinterpret_cast(RootPem), sizeof(RootPem)) == 0, + "parse client authority"); + Require(mbedtls_pk_parse_key(&key, reinterpret_cast(ServerKey), sizeof(ServerKey), nullptr, 0, + mbedtls_ctr_drbg_random, &drbg) == 0, "parse server key"); + Require(mbedtls_ssl_config_defaults(&config, MBEDTLS_SSL_IS_SERVER, MBEDTLS_SSL_TRANSPORT_STREAM, MBEDTLS_SSL_PRESET_DEFAULT) == 0, + "configure server"); + mbedtls_ssl_conf_rng(&config, mbedtls_ctr_drbg_random, &drbg); + Require(mbedtls_ssl_conf_own_cert(&config, &certificate, &key) == 0, "set server certificate"); + mbedtls_ssl_conf_authmode(&config, MBEDTLS_SSL_VERIFY_REQUIRED); + mbedtls_ssl_conf_ca_chain(&config, &authority, nullptr); + Require(mbedtls_net_bind(&listener, "127.0.0.1", "0", MBEDTLS_NET_PROTO_TCP) == 0, "bind loopback listener"); + sockaddr_in address{}; +#ifdef _WIN32 + int size = sizeof(address); +#else + socklen_t size = sizeof(address); +#endif + Require(getsockname(listener.fd, reinterpret_cast(&address), &size) == 0, "read listener port"); + port = ntohs(address.sin_port); + } + + ~Server() { + mbedtls_ssl_config_free(&config); + mbedtls_pk_free(&key); + mbedtls_x509_crt_free(&authority); + mbedtls_x509_crt_free(&certificate); + mbedtls_ctr_drbg_free(&drbg); + mbedtls_entropy_free(&entropy); + mbedtls_net_free(&listener); + } + + void Serve(bool handshakeSucceeds) { + mbedtls_net_context client; + mbedtls_net_init(&client); + mbedtls_ssl_context ssl; + mbedtls_ssl_init(&ssl); + if (mbedtls_net_accept(&listener, &client, nullptr, 0, nullptr) != 0 || mbedtls_ssl_setup(&ssl, &config) != 0) { + failure = "server accept or setup failed"; + } else { + mbedtls_ssl_set_bio(&ssl, &client, mbedtls_net_send, mbedtls_net_recv, nullptr); + const int handshake = mbedtls_ssl_handshake(&ssl); + if ((handshake == 0) != handshakeSucceeds) failure = "server handshake result differs, error " + std::to_string(handshake); + else if (handshake == 0) Exchange(ssl); + } + mbedtls_ssl_free(&ssl); + mbedtls_net_free(&client); + } + + void Exchange(mbedtls_ssl_context& ssl) { + std::array request{}; + std::size_t received = 0; + while (received < request.size()) { + const int result = mbedtls_ssl_read(&ssl, request.data() + received, request.size() - received); + if (result <= 0) { + failure = "server read failed, error " + std::to_string(result); + return; + } + received += static_cast(result); + } + if (std::memcmp(request.data(), "ping", 4) != 0) { + failure = "server received the wrong request"; + return; + } + if (mbedtls_ssl_write(&ssl, reinterpret_cast("pong"), 4) != 4) { + failure = "server write failed"; + return; + } + if (mbedtls_ssl_close_notify(&ssl) != 0) failure = "server close notify failed"; + } +}; + +struct Sockets { + bool net; + + int Open(std::uint16_t port) const { + const int descriptor = net ? sceNetSocket("ssl", 2, 1, 6) : socket_nid_postfix(2, 1, 6); + Require(descriptor >= 0, "create guest socket"); + std::array address{16, 2, static_cast(port >> 8), static_cast(port), 127, 0, 0, 1}; + Require((net ? sceNetConnect(descriptor, address.data(), 16) : connect_nid_postfix(descriptor, address.data(), 16)) == 0, + "connect guest socket"); + return descriptor; + } + + int Close(int descriptor) const { return net ? sceNetSocketClose(descriptor) : close_nid_postfix(descriptor); } +}; + +std::size_t InUse(int context) { + SceSsl::MemoryPoolStats stats{}; + Require(sceSslGetMemoryPoolStats(context, &stats) == 0, "read pool statistics"); + return stats.currentInuseSize; +} + +} + +int main(int argc, char** argv) { + Require(argc == 2 && (std::strcmp(argv[1], "kernel") == 0 || std::strcmp(argv[1], "net") == 0), "usage: kernel|net"); +#ifndef _WIN32 + std::signal(SIGPIPE, SIG_IGN); +#endif + const Sockets sockets{std::strcmp(argv[1], "net") == 0}; + if (sockets.net) Require(sceNetInit_nid_postfix() == 0, "initialize libSceNet"); + Server server; + std::thread serving([&] { + server.Serve(false); + server.Serve(true); + }); + + const int empty = sceSslInit_nid_postfix(256 * 1024); + const int withoutAuthority = sceSslCreateConnection(empty, -1, "localhost"); + Require(Throws([&] { sceSslConnect(withoutAuthority); }), "a handshake without a loaded CA was attempted"); + Require(sceSslDeleteConnection(withoutAuthority) == 0 && sceSslTerm_nid_postfix(empty) == 0, "release the context without CA"); + + const int context = sceSslInit_nid_postfix(256 * 1024); + std::string root = RootPem; + SceSsl::Data authority{reinterpret_cast(root.data()), root.size()}; + auto* authorities = &authority; + std::string clientCertificate = ClientPem, clientKey = ClientKey; + SceSsl::Data certificate{reinterpret_cast(clientCertificate.data()), clientCertificate.size()}; + SceSsl::Data key{reinterpret_cast(clientKey.data()), clientKey.size()}; + Require(sceSslLoadCert(context, 1, &authorities, &certificate, &key) == 0, "load the fixture root and client certificate"); + const auto loaded = InUse(context); + + const int callback = sceSslCreateConnection(context, -1, "localhost"); + Require(sceSslSetVerifyCallback(callback, VerifyCallback, nullptr) == 0, "set verify callback"); + Require(Throws([&] { sceSslConnect(callback); }), "a verify callback was accepted"); + Require(sceSslDeleteConnection(callback) == 0, "delete callback connection"); + + const int closed = sceSslCreateConnection(context, 1000, "localhost"); + Require(Throws([&] { sceSslConnect(closed); }), "a handshake over a closed descriptor was attempted"); + Require(sceSslDeleteConnection(closed) == 0, "delete connection over a closed descriptor"); + + const int mismatchSocket = sockets.Open(server.port); + const int mismatch = sceSslCreateConnection(context, mismatchSocket, "example.com"); + Require(Throws([&] { sceSslConnect(mismatch); }), "a certificate for another host was accepted"); + Require(Throws([&] { sceSslRead(mismatch, root.data(), 1, 0); }), "a failed handshake left the connection usable"); + Require(sceSslDeleteConnection(mismatch) == 0 && sockets.Close(mismatchSocket) == 0, "release the mismatched connection"); + + const int descriptor = sockets.Open(server.port); + const int connection = sceSslCreateConnection(context, descriptor, "localhost"); + std::array buffer{}; + Require(Throws([&] { sceSslRead(connection, buffer.data(), buffer.size(), 0); }), "read before the handshake"); + Require(Throws([&] { sceSslWrite(connection, buffer.data(), buffer.size(), 0); }), "write before the handshake"); + Require(sceSslConnect(connection) == 0, "handshake with the loopback server"); + Require(InUse(context) > loaded, "the connection is not charged to the context pool"); + Require(Throws([&] { sceSslConnect(connection); }), "a second handshake was accepted"); + Require(Throws([&] { sceSslWrite(connection, buffer.data(), 1, 1); }), "unknown write flags were accepted"); + Require(Throws([&] { sceSslRead(connection, buffer.data(), 1, 3); }), "peek was accepted"); + char request[] = "ping"; + Require(sceSslWrite(connection, request, 4, 0) == 4, "write the request"); + Require(sceSslRead(connection, buffer.data(), buffer.size(), 0) == 4 && std::memcmp(buffer.data(), "pong", 4) == 0, "read the reply"); + Require(sceSslRead(connection, buffer.data(), buffer.size(), 1) == 0, "close notify did not end the stream"); + Require(sceSslDeleteConnection(connection) == 0, "delete the connection"); + Require(InUse(context) == loaded, "the connection's pool memory was not returned"); + Require(sockets.Close(descriptor) == 0, "the socket did not stay open after the connection was deleted"); + + serving.join(); + Require(server.failure.empty(), server.failure.c_str()); + Require(sceSslTerm_nid_postfix(context) == 0, "terminate the context"); +} diff --git a/core/libs/tests/SslCertificateFixture.hpp b/core/libs/tests/SslCertificateFixture.hpp index 82d0ad962b..bc5637db37 100644 --- a/core/libs/tests/SslCertificateFixture.hpp +++ b/core/libs/tests/SslCertificateFixture.hpp @@ -71,3 +71,23 @@ inline constexpr std::array RootDer{ 0x78, 0xd6, 0xd3, 0xf7, 0x39, 0x5b, 0x74, 0x8f, 0xae, 0xa3, 0x9d, 0x2e, 0xb4, 0xcf, 0x6b, 0x0d, 0x9f, 0x24, 0x48, 0x1c, 0x79, 0x46, 0xcb, 0x80, 0x56, 0x3f, 0x0c, 0xdf, }; +inline constexpr char ServerPem[] = R"FIXTURE(-----BEGIN CERTIFICATE----- +MIIB+TCCAZ6gAwIBAgIDXhfhMAoGCCqGSM49BAMCMEIxCzAJBgNVBAYTAkdCMRUw +EwYDVQQKDAxBbnlQUzUgVGVzdHMxHDAaBgNVBAMME0FueVBTNSBmaXh0dXJlIHJv +b3QwHhcNMjYxMDE2MTEyOTUzWhcNMzYxMDEzMTEyOTUzWjA4MQswCQYDVQQGEwJH +QjEVMBMGA1UECgwMQW55UFM1IFRlc3RzMRIwEAYDVQQDDAlsb2NhbGhvc3QwWTAT +BgcqhkjOPQIBBggqhkjOPQMBBwNCAASOWLxgY2XcCm5WcZKzafvQHUuJX1KtbMsN +IbMUyke+Sp9+LrTGU52se7eyb3nq5AVuGiBdmoW5QkBMFx00hkyBo4GMMIGJMBQG +A1UdEQQNMAuCCWxvY2FsaG9zdDAMBgNVHRMBAf8EAjAAMA4GA1UdDwEB/wQEAwIH +gDATBgNVHSUEDDAKBggrBgEFBQcDATAdBgNVHQ4EFgQUuuGJo54FNP2gUW5R7mLC +BAz9xkMwHwYDVR0jBBgwFoAUVmiVtNwaphUy1PoEPfuD3KpoDNwwCgYIKoZIzj0E +AwIDSQAwRgIhAM3tCykI/0NBGJMs+4wsjT835nfuzudPExaxh7XrfjyjAiEAn++S +D83KL8LSeGSsh10kVT7dM8acB2f3gQ9c0m+h3+Q= +-----END CERTIFICATE----- +)FIXTURE"; +inline constexpr char ServerKey[] = R"FIXTURE(-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQggjpMtOf9m8ZADfb/ +g+KCUQ46CTPPM2bFyypaJZ3ZJq2hRANCAASOWLxgY2XcCm5WcZKzafvQHUuJX1Kt +bMsNIbMUyke+Sp9+LrTGU52se7eyb3nq5AVuGiBdmoW5QkBMFx00hkyB +-----END PRIVATE KEY----- +)FIXTURE"; diff --git a/docs/dev/debt/libSceSsl.md b/docs/dev/debt/libSceSsl.md index f5b9ed5354..2fef86f55f 100644 --- a/docs/dev/debt/libSceSsl.md +++ b/docs/dev/debt/libSceSsl.md @@ -2,10 +2,10 @@ ### Unknown function info -- [sceSslCreateConnection, sceSslDeleteConnection, sceSslSetVerifyCallback, sceSslConnect, sceSslRead, sceSslWrite](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - imported by `ffmpeg.prx`. The repository has no TLS client, so a connection only records its context, socket, host and verification callback, and `sceSslConnect`, `sceSslRead` and `sceSslWrite` throw `NotImplemented_nid_no_patch(__func__)` until there is a transport. Signatures follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Net/Http.cs#L195-L252), which also says the socket stays open after `sceSslDeleteConnection`. An unknown context returns `SSL_ERROR_INVALID_ID` (0x8095F006), as `sceSslGetMemoryPoolStats` does on the console and as [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/4865dd92ca6b6bb907748f125883093af038f53b/src/libs/network.cpp#L2688-L2720) and [sharpemu](https://github.com/sharpemu/sharpemu/blob/29021b5a71bbc80f262d887e6ba18b3a72963f49/src/SharpEmu.Libs/Network/SslExports.cs#L41-L54) return for other context calls; for an unknown connection, a connection whose context was terminated and a null host the console's codes are unknown, so these throw. The socket is not checked when the connection is created, `sceSslTerm` leaves the context's connections to `sceSslDeleteConnection`, and `sceSslSetVerifyCallback` accepts any callback, null included, and returns 0; none of this is verified on the console. `SSL_ERROR_BEFORE_INIT` (0x8095F001) is never returned, as the library keeps no state apart from its contexts +- [sceSslCreateConnection, sceSslDeleteConnection, sceSslSetVerifyCallback, sceSslConnect, sceSslRead, sceSslWrite](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - imported by `ffmpeg.prx`. Signatures follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Net/Http.cs#L195-L252), which also says the socket stays open after `sceSslDeleteConnection` and that `sceSslRead` returns 0 at the end of the stream. An unknown context returns `SSL_ERROR_INVALID_ID` (0x8095F006), as `sceSslGetMemoryPoolStats` does on the console and as [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/4865dd92ca6b6bb907748f125883093af038f53b/src/libs/network.cpp#L2688-L2720) and [sharpemu](https://github.com/sharpemu/sharpemu/blob/29021b5a71bbc80f262d887e6ba18b3a72963f49/src/SharpEmu.Libs/Network/SslExports.cs#L41-L54) return for other context calls; for an unknown connection, a connection whose context was terminated and a null host the console's codes are unknown, so these throw. `sceSslConnect` runs a TLS 1.2 client handshake with Mbed TLS 3.6.7 over the host socket that the guest number maps to in the libkernel or libSceNet socket table, and `sceSslRead` and `sceSslWrite` carry application data over it. TLS 1.3 is compiled out: SharpProspero's `SceSslVersion` ends at TLS 1.2, and Mbed TLS 1.3 needs PSA crypto state that is process-wide rather than allocated from a context's pool. Connection state is allocated from the context's pool and returned to it by `sceSslDeleteConnection`; a connection keeps the pool alive after `sceSslTerm`. The server is verified against the CAs loaded with `sceSslLoadCert`: chain, validity dates and the host name the connection was created with, which is also sent as SNI. A client certificate loaded with `sceSslLoadCert` is sent when the server asks for one. With no CA loaded `sceSslConnect` throws, because no root bundle is shipped and the console's built-in roots are unknown. With a verify callback set it throws: SharpProspero describes the arguments as an id, bits saying what failed, the chain, its length and the user argument, with a negative return failing the handshake with that value, but whether the id is the context or the connection, the bit values, the chain element type and when the callback runs are unknown. Handshake, verification and socket failures, a second `sceSslConnect`, a read or write before the handshake, a descriptor that is not a socket, a null buffer, write flags other than 0 and read flags other than 0 and `MsgFlagPartialRead` (1) throw, as their console codes are unknown; `MsgFlagPeek` (3) is not implemented. A non-blocking socket that would block throws, and so does a libSceNet socket whose `SO_RCVTIMEO` or `SO_SNDTIMEO` expires, as libSceNet sets them on the host socket; libkernel keeps them above the host socket, so they do not bound SSL reads and writes on a libkernel socket. `sceSslRead` returns what one Mbed TLS read delivers, up to `len`, with flags 0 and with `MsgFlagPartialRead` alike; SharpProspero describes `sceSslRead` as reading up to `len` but `MsgFlagPartialRead` as returning before the buffer is full, so whether flags 0 waits to fill the buffer is unknown. It returns 0 after the server's close_notify and also when the transport ends without one. `sceSslWrite` sends all of `len`, capped at `INT_MAX`, and returns the count. A connection serializes its reads and writes, so a read blocked in one thread holds back a write on the same connection from another. `sceSslDeleteConnection` sends no close_notify. The socket is not checked when the connection is created, `sceSslTerm` leaves the context's connections to `sceSslDeleteConnection`, and `sceSslSetVerifyCallback` accepts any callback, null included, and returns 0. None of this is verified on the console. `SSL_ERROR_BEFORE_INIT` (0x8095F001) is never returned, as the library keeps no state apart from its contexts - [sceSslClose](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - unknown signature - [sceSslGetSerialNumber](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - unknown signature -- [sceSslLoadCert, sceSslGetCaList, sceSslFreeCaList, sceSslGetCaCerts, sceSslFreeCaCerts, sceSslUnloadCert](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - signatures and layouts follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Http.cs). PPSA15508/01.002.000 calls `sceSslLoadCert` with one 1,408-byte PEM CA and null client certificate/key arguments; this confirms that call shape, not console error behavior. Mbed TLS 3.6.7 parses DER/PEM chains and checks an optional client certificate/key pair. Imports are retained per context and committed together; a failed import preserves existing credentials. CA queries return independently owned snapshots; unload clears active credentials while outstanding snapshots remain valid. Appending repeated CA imports, snapshot lifetime across unload, memory overhead and error precedence have not been measured on a PS5. Invalid loads, malformed material, exhausted pools and unsupported keys throw rather than inventing console error codes. No root bundle is shipped; an empty context still returns `NOT_FOUND`. The existing null-list error, unknown-context behavior and legacy clearing of foreign raw CA lists remain unchanged. Certificate readers remain unimplemented. Loading credentials neither starts a TLS connection nor authenticates a peer +- [sceSslLoadCert, sceSslGetCaList, sceSslFreeCaList, sceSslGetCaCerts, sceSslFreeCaCerts, sceSslUnloadCert](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - signatures and layouts follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Http.cs). PPSA15508/01.002.000 calls `sceSslLoadCert` with one 1,408-byte PEM CA and null client certificate/key arguments; this confirms that call shape, not console error behavior. Mbed TLS 3.6.7 parses DER/PEM chains and checks an optional client certificate/key pair. Imports are retained per context and committed together; a failed import preserves existing credentials. CA queries return independently owned snapshots; unload clears active credentials while outstanding snapshots remain valid. Appending repeated CA imports, snapshot lifetime across unload, memory overhead and error precedence have not been measured on a PS5. Invalid loads, malformed material, exhausted pools and unsupported keys throw rather than inventing console error codes. No root bundle is shipped; an empty context still returns `NOT_FOUND`. The existing null-list error, unknown-context behavior and legacy clearing of foreign raw CA lists remain unchanged. Certificate readers remain unimplemented. Loading credentials does not start a TLS connection; `sceSslConnect` verifies the server against the loaded CAs and offers the loaded client certificate - PPSA01317 imports declared without parameters, signatures unknown: [sceSslDisableVerifyOption, sceSslGetAlpnSelected, sceSslSetAlpn, sceSslSetMinSslVersion](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl). Connection storage exists, but their signatures and option behavior remain unknown, so they still throw ### Functional From 80e3d90ed6098e1b35fb4d27eac63b3e3a784918 Mon Sep 17 00:00:00 2001 From: Alex Collini Date: Tue, 13 Oct 2026 21:02:07 +0200 Subject: [PATCH 0492/1000] feat(recompiler): run single-wave wave64 compute programs at one lane per invocation on 32-lane hosts Co-Authored-By: Claude Opus 5.5 --- core/shader/recompiler/Recompiler.cpp | 11 ++- .../include/SpirvBackend/SpirvEmitter.hpp | 1 + .../SpirvBackend/SpirvEmitterState.hpp | 4 + .../SpirvMemory/SpirvSubgroup.hpp | 1 + .../src/SpirvAlu/SpirvAluEmitterMath.cpp | 17 ++-- .../SpirvBackend/src/SpirvEmitter.cpp | 18 +++- .../src/SpirvMemory/SpirvEmitterHelpers.cpp | 6 +- .../src/SpirvMemory/SpirvInputOutput.cpp | 7 ++ .../src/SpirvMemory/SpirvModuleSetup.cpp | 5 ++ .../src/SpirvMemory/SpirvSubgroup.cpp | 84 +++++++++++++++++++ .../SpirvBackend/src/SpirvModuleEmitter.cpp | 2 +- 11 files changed, 142 insertions(+), 14 deletions(-) diff --git a/core/shader/recompiler/Recompiler.cpp b/core/shader/recompiler/Recompiler.cpp index 7a67b3e3b1..1c74173ebe 100644 --- a/core/shader/recompiler/Recompiler.cpp +++ b/core/shader/recompiler/Recompiler.cpp @@ -524,11 +524,20 @@ CompiledVariant emitVariant(const RecompileRequest& request, IrProgram& program) targetOptions.registrationModules = registration; targetOptions.fragmentDescriptorSet = request.target.registrationModules; targetOptions.float16DenormPreserve = request.target.float16DenormPreserve; + static const bool spannedWaves = std::getenv("APS5_SPANNED_WAVE") != nullptr; + targetOptions.spannedWaves = spannedWaves; constexpr SpirvEmitter spirvEmitter; CompiledShaderArtifact result; result.variantId = nextVariantId(); - result.spirv = spirvEmitter.Emit(program, inputInfo, bindings, targetOptions); + try { + result.spirv = spirvEmitter.Emit(program, inputInfo, bindings, targetOptions); + } catch (const std::exception& error) { + if (!targetOptions.spannedWaves) throw; + std::fprintf(stderr, "[spanned-wave] 0x%llx falls back to two lanes per invocation: %s\n", static_cast(request.shader.codeAddress), error.what()); + targetOptions.spannedWaves = false; + result.spirv = spirvEmitter.Emit(program, inputInfo, bindings, targetOptions); + } if (registration && result.spirv.size() > request.target.registrationWordLimit) { registration = false; program.Metadata().bindingLayoutComplete = false; diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitter.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitter.hpp index 9d22da5885..3736835859 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitter.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitter.hpp @@ -24,6 +24,7 @@ struct SpirvTargetOptions { bool registrationModules = false; bool fragmentDescriptorSet = false; bool float16DenormPreserve = false; + bool spannedWaves = false; }; class SpirvEmitter { diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp index 2ac59b0477..5c89545900 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp @@ -100,6 +100,8 @@ struct SpirvEmitterState { std::uint32_t laneCount = 1; std::uint32_t hostSubgroupSize = 0; bool splitSubgroup = false; + bool spannedWave = false; + std::uint32_t spannedExchangeVariable = 0; bool narrowSubgroupClock = false; bool float64DenormPreserve = false; bool registrationModules = false; @@ -185,6 +187,7 @@ struct SpirvEmitterState { std::uint32_t sampleMaskInputVariable = 0; std::uint32_t quadSampleCoverage = 0; std::uint32_t subgroupLocalInvocationIdVariable = 0; + std::uint32_t subgroupIdVariable = 0; std::uint32_t perVertexVariable = 0; std::uint32_t pointSizeVariable = 0; std::uint32_t clipDistanceVariable = 0; @@ -211,6 +214,7 @@ struct SpirvValueEmitContext { std::uint32_t Ballot(const IrValue* predicate); std::uint32_t FirstLane(std::uint32_t ballot); std::uint32_t Shuffle(const IrValue& inst, std::size_t index, std::uint32_t lane); + std::uint32_t ShuffleInHalf(const IrValue& inst, std::size_t index, std::uint32_t lane); std::uint32_t Result(const IrValue& inst); std::uint32_t Define(const IrValue& inst, std::uint32_t value); std::uint32_t ResourceIndex(const IrValue* value, IrOpcode opcode); diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvMemory/SpirvSubgroup.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvMemory/SpirvSubgroup.hpp index b38bd34c1e..2e4a4adb8c 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvMemory/SpirvSubgroup.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvMemory/SpirvSubgroup.hpp @@ -10,6 +10,7 @@ bool HostSubgroupSingleLane(const SpirvEmitterState& state); void EmitSubgroupBallot(SpirvEmitterState& state, std::uint32_t result, std::uint32_t predicate); void EmitSubgroupBallotFindLsb(SpirvEmitterState& state, std::uint32_t result, std::uint32_t ballot); void EmitSubgroupShuffle(SpirvEmitterState& state, std::uint32_t type, std::uint32_t result, std::uint32_t value, std::uint32_t lane); +void EmitSubgroupShuffleInHalf(SpirvEmitterState& state, std::uint32_t type, std::uint32_t result, std::uint32_t value, std::uint32_t lane); void EmitSubgroupElect(SpirvEmitterState& state, std::uint32_t result); std::uint32_t EmitSubgroupLocalInvocationId(SpirvEmitterState& state); std::uint32_t EmitHostSubgroupLane(SpirvEmitterState& state, std::uint32_t lane); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp index 757a979fa5..dc224a7a2e 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp @@ -186,11 +186,10 @@ std::uint32_t EmitDsMaskedLaneRead(SpirvEmitterState& state, std::uint32_t sourc if (state.laneCount == 2) { lane = Binary(state, spv::OpBitwiseAnd, TypeU32(state), lane, ConstantU32(state, 31u)); } - const auto physicalLane = EmitHostSubgroupLane(state, lane); const auto shuffled = state.module.AllocateId(); - EmitSubgroupShuffle(state, TypeU32(state), shuffled, source, physicalLane); + EmitSubgroupShuffleInHalf(state, TypeU32(state), shuffled, source, lane); const auto sourceExec = state.module.AllocateId(); - EmitSubgroupShuffle(state, TypeBool(state), sourceExec, exec, physicalLane); + EmitSubgroupShuffleInHalf(state, TypeBool(state), sourceExec, exec, lane); const auto sourceActive = Binary(state, spv::OpLogicalAnd, TypeBool(state), sourceExec, EmitSubgroupLaneActiveBool(state, lane)); return Select(state, TypeU32(state), sourceActive, shuffled, ConstantU32(state, 0u)); } @@ -566,7 +565,7 @@ std::optional MatchHalfWaveScan(const IrValue* value, IrShaderStag std::optional EmitHalfWaveReduction(SpirvValueEmitContext& ctx, const IrValue& inst, std::uint32_t lane) { auto& state = ctx.state; - if (state.program.WaveSize() != 64u || (lane & 31u) != 31u) return std::nullopt; + if (state.program.WaveSize() != 64u || (lane & 31u) != 31u || state.spannedWave) return std::nullopt; const auto stage = state.program.Resources().stage; auto scan = MatchHalfWaveScan(inst.Argument(0), stage, false); const bool narrow = !scan && HostSubgroupNarrowerThanWave(state) && state.hostSubgroupSize == 32u; @@ -969,7 +968,7 @@ std::uint32_t EmitDppMoveU32(SpirvValueEmitContext& ctx, const IrValue& inst) { auto& state = ctx.state; const auto flags = inst.Flags(); const auto target = EmitDppTargetLane(state, flags.control); - const auto shuffled = ctx.Shuffle(inst, 0, target.lane); + const auto shuffled = ctx.ShuffleInHalf(inst, 0, target.lane); if (flags.fetchInactive) { return EmitNative(state, target.valid, shuffled, ConstantU32(state, 0u)); } @@ -1079,7 +1078,7 @@ std::uint32_t EmitPermlane16U32(SpirvValueEmitContext& ctx, const IrValue& inst) state.module.AddFunction(spv::OpShiftRightLogical, TypeU32(state), shifted, selected, shift); state.module.AddFunction(spv::OpBitwiseAnd, TypeU32(state), index, shifted, ConstantU32(state, 15u)); state.module.AddFunction(spv::OpBitwiseOr, TypeU32(state), target, rowValue, index); - const auto shuffled = ctx.Shuffle(inst, 0, target); + const auto shuffled = ctx.ShuffleInHalf(inst, 0, target); if (flags.fetchInactive) { return shuffled; } @@ -1107,9 +1106,9 @@ std::uint32_t EmitPermuteU32(SpirvValueEmitContext& ctx, const IrValue& inst) { auto result = ConstantU32(state, 0u); for (std::uint32_t source = 0; source < 32u; ++source) { const auto from = Binary(state, spv::OpBitwiseOr, TypeU32(state), base, ConstantU32(state, source)); - const auto value = ctx.Shuffle(inst, 0, from); - const auto address = ctx.Shuffle(inst, 1, from); - const auto active = ctx.Shuffle(inst, 2, from); + const auto value = ctx.ShuffleInHalf(inst, 0, from); + const auto address = ctx.ShuffleInHalf(inst, 1, from); + const auto active = ctx.ShuffleInHalf(inst, 2, from); const auto target = Binary(state, spv::OpBitwiseAnd, TypeU32(state), Binary(state, spv::OpShiftRightLogical, TypeU32(state), address, ConstantU32(state, 2u)), ConstantU32(state, 31u)); const auto hit = Binary(state, spv::OpLogicalAnd, TypeBool(state), active, Binary(state, spv::OpIEqual, TypeBool(state), target, own)); result = Select(state, TypeU32(state), hit, value, result); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp index 2bd877635a..fa211ba426 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp @@ -173,6 +173,13 @@ std::uint32_t SpirvValueEmitContext::Shuffle(const IrValue& inst, std::size_t in return value; } +std::uint32_t SpirvValueEmitContext::ShuffleInHalf(const IrValue& inst, std::size_t index, std::uint32_t lane) { + if (!state.spannedWave) return Shuffle(inst, index, lane); + const auto result = state.module.AllocateId(); + EmitSubgroupShuffleInHalf(state, TypeId(state, inst.Argument(index)->Type()), result, Arg(inst, index), lane); + return result; +} + std::uint32_t SpirvValueEmitContext::Result(const IrValue& inst) { if (const auto found = definitions.find(&inst); found != definitions.end()) { return found->second; @@ -262,14 +269,21 @@ std::vector SpirvEmitter::Emit(const IrProgram& program, const Sh state.splitSubgroup = program.WaveSize() == 32u && target.subgroupSize > 32u; if (state.splitSubgroup && (state.requirements.subgroupBallot || state.requirements.subgroupShuffle)) state.requirements.subgroupLocalInvocationId = true; const auto* workgroup = ShaderWorkgroupInputFor(state); - state.laneCount = workgroup != nullptr && program.WaveSize() == 64u && workgroup->hostSubgroupSize == 32u ? 2u : 1u; + const bool halfHost = workgroup != nullptr && program.WaveSize() == 64u && workgroup->hostSubgroupSize == 32u; + state.spannedWave = halfHost && target.spannedWaves && program.Resources().stage == IrShaderStage::Compute && !state.requirements.computeDerivatives && std::max(workgroup->threadsNum[0], 1u) * std::max(workgroup->threadsNum[1], 1u) * std::max(workgroup->threadsNum[2], 1u) == 64u; + state.laneCount = halfHost && !state.spannedWave ? 2u : 1u; + if (state.spannedWave) { + state.requirements.subgroupLocalInvocationId = true; + state.requirements.subgroupBallot = true; + state.requirements.subgroupShuffle = true; + } if (state.laneCount == 2u) state.sharedLaneValues = WaveUniformValues(program); if (program.Resources().stage == IrShaderStage::Compute && workgroup != nullptr) { // The key comes from a subgroup ballot (ReadFirstLane), so the slot is uniform over the // workgroup only when the workgroup is one wave held by one host subgroup; a wave64 program // kept at one lane per invocation spans two subgroups (see WaveLdsScope). const auto threads = std::max(workgroup->threadsNum[0], 1u) * std::max(workgroup->threadsNum[1], 1u) * std::max(workgroup->threadsNum[2], 1u); - const bool oneSubgroup = state.laneCount == 2u || (program.WaveSize() == workgroup->hostSubgroupSize && workgroup->hostSubgroupSize <= 32u); + const bool oneSubgroup = state.laneCount == 2u || state.spannedWave || (program.WaveSize() == workgroup->hostSubgroupSize && workgroup->hostSubgroupSize <= 32u); state.tableIndexNonUniform = threads > program.WaveSize() || !oneSubgroup; } state.waveLdsScope = WaveLdsScope(program, workgroup, state.laneCount); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp index 168803014c..9b36762390 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp @@ -165,7 +165,7 @@ void DefineInputs(SpirvEmitterState& state) { for (const auto& input : state.program.Info().inputs) { state.inputs.push_back(SpirvInputBinding {input}); } - if (state.laneCount == 2u) { + if (state.laneCount == 2u || state.spannedWave) { const auto addBuiltin = [&](StageInputKind kind, std::uint32_t components, const char* name) { if (std::none_of(state.inputs.begin(), state.inputs.end(), [kind](const SpirvInputBinding& input) { return input.kind == kind; @@ -293,6 +293,10 @@ void DefineInputs(SpirvEmitterState& state) { if (state.requirements.subgroupLocalInvocationId) { const auto variable = DefineInterfaceVariable(state, TypeU32(state), spv::StorageClassInput, "gl_SubgroupInvocationID"); state.subgroupLocalInvocationIdVariable = variable; + if (state.spannedWave) { + state.subgroupIdVariable = DefineInterfaceVariable(state, TypeU32(state), spv::StorageClassInput, "gl_SubgroupID"); + state.module.AddAnnotation(spv::OpDecorate, state.subgroupIdVariable, spv::DecorationBuiltIn, spv::BuiltInSubgroupId); + } state.module.AddAnnotation(spv::OpDecorate, variable, spv::DecorationBuiltIn, spv::BuiltInSubgroupLocalInvocationId); if (state.program.Resources().stage == IrShaderStage::Pixel) { state.module.AddAnnotation(spv::OpDecorate, variable, spv::DecorationFlat); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvInputOutput.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvInputOutput.cpp index 849e954bbe..4c05ffe094 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvInputOutput.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvInputOutput.cpp @@ -196,6 +196,13 @@ namespace ShaderRecompiler } std::uint32_t EmitLocalInvocationIndex(SpirvEmitterState& state) { + if (state.spannedWave) { + const auto subgroup = state.module.AllocateId(); + const auto invocation = state.module.AllocateId(); + state.module.AddFunction(spv::OpLoad, TypeU32(state), subgroup, state.subgroupIdVariable); + state.module.AddFunction(spv::OpLoad, TypeU32(state), invocation, state.subgroupLocalInvocationIdVariable); + return EmitAddU32(state, EmitBinaryU32(state, spv::OpShiftLeftLogical, subgroup, ConstantU32(state, 5u)), invocation); + } const auto variable = InputVariableForKind(state, StageInputKind::LocalInvocationIndex); if (variable == 0) { FailEmit("LocalInvocationIndex was not declared"); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp index e6efb53722..763f1a1fa8 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp @@ -343,6 +343,11 @@ void DefineModule(SpirvEmitterState& state) { std::uint32_t localX = workgroup->threadsNum[0] != 0u ? workgroup->threadsNum[0] : derivativeDefault; std::uint32_t localY = workgroup->threadsNum[1] != 0u ? workgroup->threadsNum[1] : derivativeDefault; std::uint32_t localZ = workgroup->threadsNum[2] != 0u ? workgroup->threadsNum[2] : 1u; + if (state.spannedWave) { + localX = 64u; + localY = 1u; + localZ = 1u; + } if (state.laneCount == 2) { localX = ((localX * localY * localZ + 63u) / 64u) * 32u; localY = 1u; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvSubgroup.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvSubgroup.cpp index 6efac49c44..08efc6a8b6 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvSubgroup.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvSubgroup.cpp @@ -26,6 +26,59 @@ namespace ShaderRecompiler return value; } + std::uint32_t SpannedLane(SpirvEmitterState& state) { + return EmitBinaryU32(state, spv::OpBitwiseAnd, EmitLocalInvocationIndex(state), ConstantU32(state, 63u)); + } + + std::uint32_t SpannedSlot(SpirvEmitterState& state, std::uint32_t index) { + if (state.spannedExchangeVariable == 0u) { + state.spannedExchangeVariable = state.module.DefineGlobalVariable(TypeU32ArrayPointer(state, spv::StorageClassWorkgroup, 64u), spv::StorageClassWorkgroup); + state.module.AddName(state.spannedExchangeVariable, "wave_exchange"); + } + const auto pointer = state.module.AllocateId(); + state.module.AddFunction(spv::OpAccessChain, TypeU32ElementPointer(state, spv::StorageClassWorkgroup), pointer, state.spannedExchangeVariable, index); + return pointer; + } + + void SpannedBarrier(SpirvEmitterState& state) { + state.module.AddFunction(spv::OpControlBarrier, ConstantU32(state, spv::ScopeWorkgroup), ConstantU32(state, spv::ScopeWorkgroup), ConstantU32(state, spv::MemorySemanticsAcquireReleaseMask | spv::MemorySemanticsWorkgroupMemoryMask)); + } + + std::uint32_t SpannedLoad(SpirvEmitterState& state, std::uint32_t index) { + const auto value = state.module.AllocateId(); + state.module.AddFunction(spv::OpLoad, TypeU32(state), value, SpannedSlot(state, index)); + return value; + } + + void SpannedBallot(SpirvEmitterState& state, std::uint32_t result, std::uint32_t predicate) { + const auto lane = SpannedLane(state); + SpannedBarrier(state); + state.module.AddFunction(spv::OpAtomicStore, SpannedSlot(state, EmitBinaryU32(state, spv::OpBitwiseAnd, lane, ConstantU32(state, 1u))), ConstantU32(state, spv::ScopeWorkgroup), ConstantU32(state, spv::MemorySemanticsMaskNone), ConstantU32(state, 0u)); + SpannedBarrier(state); + const auto bit = Binary(state, spv::OpShiftLeftLogical, TypeU32(state), ConstantU32(state, 1u), EmitBinaryU32(state, spv::OpBitwiseAnd, lane, ConstantU32(state, 31u))); + const auto previous = state.module.AllocateId(); + state.module.AddFunction(spv::OpAtomicOr, TypeU32(state), previous, SpannedSlot(state, EmitBinaryU32(state, spv::OpShiftRightLogical, lane, ConstantU32(state, 5u))), ConstantU32(state, spv::ScopeWorkgroup), ConstantU32(state, spv::MemorySemanticsMaskNone), Select(state, TypeU32(state), predicate, bit, ConstantU32(state, 0u))); + SpannedBarrier(state); + const auto low = SpannedLoad(state, ConstantU32(state, 0u)); + const auto high = SpannedLoad(state, ConstantU32(state, 1u)); + state.module.AddFunction(spv::OpCompositeConstruct, TypeU32Vector(state, 4u), result, low, high, ConstantU32(state, 0u), ConstantU32(state, 0u)); + } + + void SpannedShuffle(SpirvEmitterState& state, std::uint32_t type, std::uint32_t result, std::uint32_t value, std::uint32_t lane) { + const bool boolean = type == TypeBool(state); + if (!boolean && type != TypeU32(state) && type != TypeF32(state)) FailEmit("a spanned wave shuffles only 32-bit and boolean values"); + std::uint32_t word = value; + if (boolean) word = Select(state, TypeU32(state), value, ConstantU32(state, 1u), ConstantU32(state, 0u)); + else if (type != TypeU32(state)) word = Unary(state, spv::OpBitcast, TypeU32(state), value); + SpannedBarrier(state); + state.module.AddFunction(spv::OpStore, SpannedSlot(state, SpannedLane(state)), word); + SpannedBarrier(state); + const auto read = SpannedLoad(state, EmitBinaryU32(state, spv::OpBitwiseAnd, lane, ConstantU32(state, 63u))); + if (boolean) state.module.AddFunction(spv::OpINotEqual, type, result, read, ConstantU32(state, 0u)); + else if (type != TypeU32(state)) state.module.AddFunction(spv::OpBitcast, type, result, read); + else state.module.AddFunction(spv::OpCopyObject, type, result, read); + } + } bool HostSubgroupSingleLane(const SpirvEmitterState& state) { @@ -33,6 +86,10 @@ namespace ShaderRecompiler } void EmitSubgroupBallot(SpirvEmitterState& state, std::uint32_t result, std::uint32_t predicate) { + if (state.spannedWave) { + SpannedBallot(state, result, predicate); + return; + } if (HostSubgroupSingleLane(state)) { const auto zero = ConstantU32(state, 0u); const auto own = Select(state, TypeU32(state), predicate, ConstantU32(state, 1u), zero); @@ -43,6 +100,19 @@ namespace ShaderRecompiler } void EmitSubgroupBallotFindLsb(SpirvEmitterState& state, std::uint32_t result, std::uint32_t ballot) { + if (state.spannedWave) { + const auto low = state.module.AllocateId(); + const auto high = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeExtract, TypeU32(state), low, ballot, 0u); + state.module.AddFunction(spv::OpCompositeExtract, TypeU32(state), high, ballot, 1u); + const auto lowFirst = state.module.AllocateId(); + const auto highFirst = state.module.AllocateId(); + state.module.AddFunction(spv::OpExtInst, TypeU32(state), lowFirst, GlslStd450(state), GLSLstd450FindILsb, low); + state.module.AddFunction(spv::OpExtInst, TypeU32(state), highFirst, GlslStd450(state), GLSLstd450FindILsb, high); + const auto lowActive = Binary(state, spv::OpINotEqual, TypeBool(state), low, ConstantU32(state, 0u)); + state.module.AddFunction(spv::OpSelect, TypeU32(state), result, lowActive, lowFirst, EmitAddU32(state, highFirst, ConstantU32(state, 32u))); + return; + } if (HostSubgroupSingleLane(state)) { const auto own = state.module.AllocateId(); state.module.AddFunction(spv::OpCompositeExtract, TypeU32(state), own, ballot, 0u); @@ -53,6 +123,10 @@ namespace ShaderRecompiler } void EmitSubgroupShuffle(SpirvEmitterState& state, std::uint32_t type, std::uint32_t result, std::uint32_t value, std::uint32_t lane) { + if (state.spannedWave) { + SpannedShuffle(state, type, result, value, lane); + return; + } if (HostSubgroupSingleLane(state)) { state.module.AddFunction(spv::OpCopyObject, type, result, value); return; @@ -60,6 +134,14 @@ namespace ShaderRecompiler state.module.AddFunction(spv::OpGroupNonUniformShuffle, type, result, ConstantU32(state, spv::ScopeSubgroup), value, lane); } + void EmitSubgroupShuffleInHalf(SpirvEmitterState& state, std::uint32_t type, std::uint32_t result, std::uint32_t value, std::uint32_t lane) { + if (!state.spannedWave) { + EmitSubgroupShuffle(state, type, result, value, EmitHostSubgroupLane(state, lane)); + return; + } + state.module.AddFunction(spv::OpGroupNonUniformShuffle, type, result, ConstantU32(state, spv::ScopeSubgroup), value, EmitBinaryU32(state, spv::OpBitwiseAnd, lane, ConstantU32(state, 31u))); + } + void EmitSubgroupElect(SpirvEmitterState& state, std::uint32_t result) { if (HostSubgroupSingleLane(state)) { state.module.AddFunction(spv::OpCopyObject, TypeBool(state), result, ConstantBool(state, true)); @@ -69,12 +151,14 @@ namespace ShaderRecompiler } std::uint32_t EmitSubgroupLocalInvocationId(SpirvEmitterState& state) { + if (state.spannedWave) return SpannedLane(state); const auto value = HostInvocationId(state); if (state.splitSubgroup) return EmitBinaryU32(state, spv::OpBitwiseAnd, value, ConstantU32(state, 31u)); return state.laneHalf == 0 ? value : EmitAddU32(state, value, ConstantU32(state, 32)); } std::uint32_t EmitHostSubgroupLane(SpirvEmitterState& state, std::uint32_t lane) { + if (state.spannedWave) return lane; if (!state.splitSubgroup) return lane; const auto base = EmitBinaryU32(state, spv::OpBitwiseAnd, HostInvocationId(state), ConstantU32(state, ~31u)); return EmitAddU32(state, base, lane); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp index 2c92bf42a0..1f76ed7314 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp @@ -51,7 +51,7 @@ std::uint32_t EmitBuiltinU32(SpirvEmitterState& state, StageInputKind kind, std: if (kind == StageInputKind::LayerExported) { return state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::LayerExported, 0u); } - if (state.laneCount == 2 && (kind == StageInputKind::LocalInvocationId || kind == StageInputKind::GlobalInvocationId)) { + if ((state.laneCount == 2 || state.spannedWave) && (kind == StageInputKind::LocalInvocationId || kind == StageInputKind::GlobalInvocationId)) { const auto* workgroup = ShaderWorkgroupInputFor(state); if (workgroup == nullptr) { throw std::runtime_error("workgroup input info is missing for a local/global invocation builtin"); From bd6ed97f6d6059c922f8f95b90cb2949a541dcb1 Mon Sep 17 00:00:00 2001 From: Alex Collini Date: Thu, 15 Oct 2026 15:21:29 +0200 Subject: [PATCH 0493/1000] fix(recompiler): keep spanned wave invocations running after a BDA fault A spanned wave64 program exchanges ballots and shuffles through workgroup-scope barriers. When a BDA fault stopped one invocation early, the other 63 would wait at the next barrier forever. Spanned programs now keep every invocation running and read zeros after a fault, as programs with wave LDS barriers already do. Co-Authored-By: Claude Opus 5.5 --- core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp index fa211ba426..53273b6efd 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp @@ -289,7 +289,7 @@ std::vector SpirvEmitter::Emit(const IrProgram& program, const Sh state.waveLdsScope = WaveLdsScope(program, workgroup, state.laneCount); if (const char* guard = std::getenv("APS5_LOOP_GUARD")) state.loopGuardLimit = static_cast(std::strtoul(guard, nullptr, 0)); // Stopped invocations would leave the wave LDS barriers incomplete. - state.bdaStopsInvocations = state.waveLdsScope == 0 && BdaInvocationsMayStop(program) && program.Resources().stage != IrShaderStage::Mesh; + state.bdaStopsInvocations = state.waveLdsScope == 0 && !state.spannedWave && BdaInvocationsMayStop(program) && program.Resources().stage != IrShaderStage::Mesh; EmitModuleHeader(state, bindings); EmitProgram(state); state.module.EmitEntryPoint(ExecutionModelForStage(state.program.Resources().stage), state.mainFunc, "main", state.interfaceVariables); From 4adfa5fc72f9024c98ec6dab72951c5ae3c4aee7 Mon Sep 17 00:00:00 2001 From: Alex Collini Date: Fri, 16 Oct 2026 08:46:53 +0200 Subject: [PATCH 0494/1000] docs(debt): record the Yotei device loss with APS5_SPANNED_WAVE=1 Co-Authored-By: Claude Opus 5.5 --- docs/dev/debt/shader.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index 9a5d65d1aa..59cf46ad55 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -146,3 +146,4 @@ - [SDWA source selects on `v_cvt_f32_ubyte1`, `v_cvt_f32_ubyte2` and `v_cvt_f32_ubyte3`](../../../core/shader/recompiler/RdnaDecoder/src/RdnaVectorOpDecoder.cpp) (shader recompiler) - byte N is taken from source 0 after `SRC0_SEL` and `SRC0_SEXT` select and extend it, since SDWA supplies source 0 (RDNA 2 ISA guide 6.8 and 13.3.7) and the opcode reads `S0.u[8N+7:8N]` (12.8). Not measured on hardware. - [Flag structurization](../../../core/shader/recompiler/ControlFlow/src/Structurizer.cpp) (shader recompiler) - a graph that every structurizer attempt refuses, irreducible and non-terminating graphs included, is rebuilt with one reach flag per block and one again flag per loop (`structurizeWithFlags`): each block keeps one copy and runs behind a guard on its flag, and each strongly connected component becomes a loop whose single latch repeats it while a back edge set its flag. The output has about 8 times the input's blocks and one route variable per block, and ends in an empty Return that never runs. Only blocks that start the top-level order unconditionally run unguarded, so resource planning also sees the static paths that skip a guarded block and can plan extra descriptor candidates for them. The attempts still run first. Shared merge splitting refuses an attempt once the graph has grown past 16 times its block count at the start of splitting (at least 64 blocks); succeeding attempts grew at most 10.6 times on random graphs of up to 128 blocks, and an attempt that would succeed only beyond the budget leaves the graph to a later attempt or to this construction. Splitting that adds one block per round stays within that budget until its 4 rounds per block run out, so the attempts can still take more than 10 s (9 of 2,000 random graphs of up to 128 blocks, 63 s at most). `APS5_FORCE_STRUCTURIZER_FALLBACK=1` builds every graph this way, for tests - [Scalar buffer descriptors](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) use only the base address, stride and record count for resources accessed exclusively by scalar loads; their fourth word is normalized to zero, following AMD RDNA2 ISA section 7.2.2. Vector and mixed accesses retain descriptor validation. Native Vulkan readbacks cover ignored fourth-word fields on Windows/NVIDIA; their console behavior has not been measured. +- [Spanned wave64 compute](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvSubgroup.cpp) (shader recompiler) - `APS5_SPANNED_WAVE=1`, off by default, runs a single-wave wave64 compute program at one lane per invocation on a 32-lane host subgroup and exchanges ballots, shuffles and reductions between the two host subgroups through workgroup memory and barriers. On Ghost of Yōtei (PPSA26344, RTX 4090, Windows, with #6402's image-table stores, which the title needs to get that far), the first dispatch of the second variant of the compute program at 0x80001e5f00 (about 830k SPIR-V words, 125-145 storage images through an image table) lost the device (NVIDIA TDR, `vkQueueSubmit` returns `VK_ERROR_DEVICE_LOST`) in five runs, with warm and cold shader caches; without the variable the same dispatch runs. The device-fault report gives no instruction address the module maps to, so the cause is not attributed. Keeping a faulting invocation running in its spanned wave (`bdaStopsInvocations` off for spanned programs) and #6416 did not change it. Stray (PPSA02101) on Intel ANV and a Demon's Souls (PPSA01342) request replay showed no fault From 6006d4566aea31250419853d744f0d31013e46b7 Mon Sep 17 00:00:00 2001 From: Alex Collini Date: Fri, 16 Oct 2026 10:09:24 +0200 Subject: [PATCH 0495/1000] test(agc): run an LDS bitmask waterfall under partial exec masks Co-Authored-By: Claude Opus 5.5 --- .../tests/execution/LdsMaskWaterfall.cpp | 108 ++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/LdsMaskWaterfall.cpp diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/LdsMaskWaterfall.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/LdsMaskWaterfall.cpp new file mode 100644 index 0000000000..734561ebf1 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/LdsMaskWaterfall.cpp @@ -0,0 +1,108 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Lanes = 64; +constexpr std::uint32_t Slots = 16; +constexpr std::uint32_t Stride = 4; +constexpr std::uint32_t Guard = 4096; +alignas(256) std::array Output{}; + +alignas(256) constexpr std::array WaterfallCode{ + 0x34240083, 0x7e040280, 0x7e060280, 0xd9340000, 0x00000212, 0xbf8cc07f, 0x16080085, 0x3608088f, + 0x34080883, 0xd6ff0002, 0x00010300, 0xd9280000, 0x00000204, 0xbf8cc07f, 0xd9d80000, 0x06000012, + 0xbf8cc07f, 0xd4e50002, 0x00020c80, 0x7e140280, 0x7e160280, 0xbe8a0380, 0xbefe0400, 0xbf138002, + 0xbf840016, 0xbe8b1402, 0xbe821c0b, 0x8f0c830b, 0x7e18020c, 0xd9d80000, 0x0e00000c, 0xbf8cc07f, + 0x7dca1c80, 0xbf86fff5, 0x7e1c050e, 0x7e1e050f, 0xbe90140e, 0x8f921081, 0x3a1c1c12, 0x3a1e1e13, + 0x4a141481, 0x4a161610, 0x800a810a, 0xbf08ff0a, 0x00001000, 0xbf850001, 0xbf82fff1, 0xbefe04c1, + 0x7e1a020a, 0xe0702000, 0x80010a00, 0xe0702004, 0x80010b00, 0xe0702008, 0x80010d00, 0xe070200c, + 0x80010600, 0xbf810000, +}; + +std::array BufferDescriptor(const void* data, std::uint32_t records) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | ((Stride * 4u) << 16u), records, 0x11016facu}; +} + +void Run(AgcDriver::VulkanDevice& device, std::uint64_t exec) { + Output.fill(0xdeadbeefu); + std::vector userData(8, 0u); + userData[0] = static_cast(exec); + userData[1] = static_cast(exec >> 32u); + const auto output = BufferDescriptor(Output.data(), Lanes); + std::copy(output.begin(), output.end(), userData.begin() + 4); + const std::span code(WaterfallCode); + const auto address = reinterpret_cast(code.data()); + const std::array memory{{{address, std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Lanes, 1, 1}, Lanes * 2u,{false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, address, code, 0, {}}, + {Lanes, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, address); + device.WaitIdle(); +} + +void Check(std::uint64_t exec) { + std::array slots{}; + for (std::uint32_t lane = 0; lane < Lanes; ++lane) slots[(lane * 5u) & (Slots - 1u)] |= std::uint64_t{1} << lane; + std::uint32_t iterations = 0; + std::uint32_t indexSum = 0; + for (std::uint32_t slot = 0; slot < Slots; ++slot) { + for (auto mask = slots[slot]; mask != 0; mask &= mask - 1) { + ++iterations; + indexSum += static_cast(std::countr_zero(mask)); + } + } + char name[64]; + std::snprintf(name, sizeof(name), "exec 0x%016llx", static_cast(exec)); + for (std::uint32_t lane = 0; lane < Lanes; ++lane) { + const bool active = ((exec >> lane) & 1u) != 0; + const std::array expected{active ? iterations : 0u, active ? indexSum : 0u, iterations, static_cast(slots[lane])}; + const std::array fields{"inner iterations", "bit index sum", "wave iterations", "own slot low word"}; + for (std::uint32_t field = 0; field < Stride; ++field) { + const auto actual = Output[lane * Stride + field]; + Require(actual != Guard + 1u || field != 2u, std::string(name) + ": the waterfall ran past " + std::to_string(Guard) + " iterations"); + Require(actual == expected[field], std::string(name) + ": lane " + std::to_string(lane) + " " + fields[field] + " is " + std::to_string(actual) + ", expected " + std::to_string(expected[field])); + } + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + if (device->Target().subgroupSize < 32) { + std::printf("skipped, subgroup size %u cannot hold a wave32\n", device->Target().subgroupSize); + return VulkanTestSkipped; + } + for (const std::uint64_t exec : {~std::uint64_t{0}, std::uint64_t{0xaaaaaaaaaaaaaaaa}, std::uint64_t{0xffff000000000000}, std::uint64_t{0x000000000000ffff}, std::uint64_t{1} << 63u, std::uint64_t{1}, std::uint64_t{0x0000000100000000}}) { + Run(*device, exec); + Check(exec); + } + std::puts("LDS mask waterfall tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} From a015284ee9421fd0689386f39848a2b405bc0d5e Mon Sep 17 00:00:00 2001 From: Alex Collini Date: Fri, 16 Oct 2026 10:32:13 +0200 Subject: [PATCH 0496/1000] fix(recompiler): ballot a spanned wave's LDS lock loop within its host subgroup The lock loop that emulates 64-bit LDS atomics runs only on the lanes the guest's exec mask keeps, so a spanned wave's barrier-based ballot there was not reached by both host subgroups. When one subgroup had no active lane the other one paired its barriers with unrelated ones and left the loop with lanes still pending, or the GPU hung (Ghost of Yotei, program 0x80003af600). The elect and the lock are already per host subgroup, so each subgroup now loops until its own lanes are done. Co-Authored-By: Claude Opus 5.5 --- .../tests/execution/LdsMaskWaterfall.cpp | 33 ++++++++++++++++--- .../SpirvMemory/SpirvSubgroup.hpp | 1 + .../SpirvMemory/SpirvMemoryInstructions.cpp | 2 +- .../src/SpirvMemory/SpirvSubgroup.cpp | 4 +++ 4 files changed, 35 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/LdsMaskWaterfall.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/LdsMaskWaterfall.cpp index 734561ebf1..15135e5e90 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/LdsMaskWaterfall.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/LdsMaskWaterfall.cpp @@ -32,22 +32,27 @@ alignas(256) constexpr std::array WaterfallCode{ 0x80010600, 0xbf810000, }; +alignas(256) constexpr std::array MaskedOrCode{ + 0x34240083, 0x7e040280, 0x7e060280, 0xd9340000, 0x00000212, 0xbf8cc07f, 0x3608008f, 0x34080883, + 0xd6ff0002, 0x00010300, 0xbefe0400, 0xd9280000, 0x00000204, 0xbf8cc07f, 0xbefe04c1, 0xd9d80000, + 0x06000012, 0xbf8cc07f, 0xe0702000, 0x80010600, 0xe0702004, 0x80010700, 0xbf810000, +}; + std::array BufferDescriptor(const void* data, std::uint32_t records) { const auto address = reinterpret_cast(data); return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | ((Stride * 4u) << 16u), records, 0x11016facu}; } -void Run(AgcDriver::VulkanDevice& device, std::uint64_t exec) { +void Run(AgcDriver::VulkanDevice& device, std::span code, std::uint64_t exec) { Output.fill(0xdeadbeefu); std::vector userData(8, 0u); userData[0] = static_cast(exec); userData[1] = static_cast(exec >> 32u); const auto output = BufferDescriptor(Output.data(), Lanes); std::copy(output.begin(), output.end(), userData.begin() + 4); - const std::span code(WaterfallCode); const auto address = reinterpret_cast(code.data()); const std::array memory{{{address, std::as_bytes(code)}}}; - const ShaderRecompiler::ShaderComputeStageInfo compute{{Lanes, 1, 1}, Lanes * 2u,{false, false, false}, false, 1}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Lanes, 1, 1}, Lanes * 2u, {false, false, false}, false, 1}; ShaderRecompiler::RecompileRequest request{ {ShaderStage::Compute, address, code, 0, {}}, {Lanes, 0, userData, compute, std::nullopt, std::nullopt, memory}, @@ -85,6 +90,22 @@ void Check(std::uint64_t exec) { } } +void CheckMaskedOr(std::uint64_t exec) { + std::array slots{}; + for (std::uint32_t lane = 0; lane < Lanes; ++lane) { + if (((exec >> lane) & 1u) != 0) slots[lane & (Slots - 1u)] |= std::uint64_t{1} << lane; + } + char name[64]; + std::snprintf(name, sizeof(name), "masked ds_or_b64, exec 0x%016llx", static_cast(exec)); + for (std::uint32_t lane = 0; lane < Lanes; ++lane) { + const std::array expected{static_cast(slots[lane]), static_cast(slots[lane] >> 32u)}; + for (std::uint32_t word = 0; word < expected.size(); ++word) { + const auto actual = Output[lane * Stride + word]; + Require(actual == expected[word], std::string(name) + ": lane " + std::to_string(lane) + " slot word " + std::to_string(word) + " is " + std::to_string(actual) + ", expected " + std::to_string(expected[word])); + } + } +} + } int main() { @@ -96,9 +117,13 @@ int main() { return VulkanTestSkipped; } for (const std::uint64_t exec : {~std::uint64_t{0}, std::uint64_t{0xaaaaaaaaaaaaaaaa}, std::uint64_t{0xffff000000000000}, std::uint64_t{0x000000000000ffff}, std::uint64_t{1} << 63u, std::uint64_t{1}, std::uint64_t{0x0000000100000000}}) { - Run(*device, exec); + Run(*device, WaterfallCode, exec); Check(exec); } + for (const std::uint64_t exec : {~std::uint64_t{0}, std::uint64_t{0x00000000ffffffff}, std::uint64_t{0xffffffff00000000}, std::uint64_t{1} << 40u, std::uint64_t{0x8000000000000001}}) { + Run(*device, MaskedOrCode, exec); + CheckMaskedOr(exec); + } std::puts("LDS mask waterfall tests passed"); return 0; } catch (const std::exception& error) { diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvMemory/SpirvSubgroup.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvMemory/SpirvSubgroup.hpp index 2e4a4adb8c..a24284d37c 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvMemory/SpirvSubgroup.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvMemory/SpirvSubgroup.hpp @@ -8,6 +8,7 @@ namespace ShaderRecompiler { bool HostSubgroupSingleLane(const SpirvEmitterState& state); void EmitSubgroupBallot(SpirvEmitterState& state, std::uint32_t result, std::uint32_t predicate); +void EmitHostSubgroupBallot(SpirvEmitterState& state, std::uint32_t result, std::uint32_t predicate); void EmitSubgroupBallotFindLsb(SpirvEmitterState& state, std::uint32_t result, std::uint32_t ballot); void EmitSubgroupShuffle(SpirvEmitterState& state, std::uint32_t type, std::uint32_t result, std::uint32_t value, std::uint32_t lane); void EmitSubgroupShuffleInHalf(SpirvEmitterState& state, std::uint32_t type, std::uint32_t result, std::uint32_t value, std::uint32_t lane); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp index b7ee109d7f..e152d36092 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp @@ -1365,7 +1365,7 @@ std::uint32_t LockedLdsUpdate(SpirvEmitterState& state, std::uint32_t type, std: state.module.AddFunction(spv::OpPhi, TypeBool(state), doneNext, served, servedMerge, done, body); state.module.AddFunction(spv::OpPhi, type, resultNext, servedResult, servedMerge, result, body); const auto ballot = state.module.AllocateId(); - EmitSubgroupBallot(state, ballot, Unary(state, spv::OpLogicalNot, TypeBool(state), doneNext)); + EmitHostSubgroupBallot(state, ballot, Unary(state, spv::OpLogicalNot, TypeBool(state), doneNext)); std::uint32_t remaining = ConstantU32(state, 0u); for (std::uint32_t component = 0; component < 4u; ++component) { const auto word = state.module.AllocateId(); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvSubgroup.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvSubgroup.cpp index 08efc6a8b6..f65a284557 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvSubgroup.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvSubgroup.cpp @@ -90,6 +90,10 @@ namespace ShaderRecompiler SpannedBallot(state, result, predicate); return; } + EmitHostSubgroupBallot(state, result, predicate); + } + + void EmitHostSubgroupBallot(SpirvEmitterState& state, std::uint32_t result, std::uint32_t predicate) { if (HostSubgroupSingleLane(state)) { const auto zero = ConstantU32(state, 0u); const auto own = Select(state, TypeU32(state), predicate, ConstantU32(state, 1u), zero); From adf516f6f1180fc3f65fb7bcf765024b3cb842b5 Mon Sep 17 00:00:00 2001 From: Alex Collini Date: Fri, 16 Oct 2026 12:07:49 +0200 Subject: [PATCH 0497/1000] docs(debt): record why a spanned wave's LDS lock loop ballots per host subgroup Co-Authored-By: Claude Opus 5.5 --- docs/dev/debt/shader.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index 59cf46ad55..2aead77580 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -146,4 +146,4 @@ - [SDWA source selects on `v_cvt_f32_ubyte1`, `v_cvt_f32_ubyte2` and `v_cvt_f32_ubyte3`](../../../core/shader/recompiler/RdnaDecoder/src/RdnaVectorOpDecoder.cpp) (shader recompiler) - byte N is taken from source 0 after `SRC0_SEL` and `SRC0_SEXT` select and extend it, since SDWA supplies source 0 (RDNA 2 ISA guide 6.8 and 13.3.7) and the opcode reads `S0.u[8N+7:8N]` (12.8). Not measured on hardware. - [Flag structurization](../../../core/shader/recompiler/ControlFlow/src/Structurizer.cpp) (shader recompiler) - a graph that every structurizer attempt refuses, irreducible and non-terminating graphs included, is rebuilt with one reach flag per block and one again flag per loop (`structurizeWithFlags`): each block keeps one copy and runs behind a guard on its flag, and each strongly connected component becomes a loop whose single latch repeats it while a back edge set its flag. The output has about 8 times the input's blocks and one route variable per block, and ends in an empty Return that never runs. Only blocks that start the top-level order unconditionally run unguarded, so resource planning also sees the static paths that skip a guarded block and can plan extra descriptor candidates for them. The attempts still run first. Shared merge splitting refuses an attempt once the graph has grown past 16 times its block count at the start of splitting (at least 64 blocks); succeeding attempts grew at most 10.6 times on random graphs of up to 128 blocks, and an attempt that would succeed only beyond the budget leaves the graph to a later attempt or to this construction. Splitting that adds one block per round stays within that budget until its 4 rounds per block run out, so the attempts can still take more than 10 s (9 of 2,000 random graphs of up to 128 blocks, 63 s at most). `APS5_FORCE_STRUCTURIZER_FALLBACK=1` builds every graph this way, for tests - [Scalar buffer descriptors](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) use only the base address, stride and record count for resources accessed exclusively by scalar loads; their fourth word is normalized to zero, following AMD RDNA2 ISA section 7.2.2. Vector and mixed accesses retain descriptor validation. Native Vulkan readbacks cover ignored fourth-word fields on Windows/NVIDIA; their console behavior has not been measured. -- [Spanned wave64 compute](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvSubgroup.cpp) (shader recompiler) - `APS5_SPANNED_WAVE=1`, off by default, runs a single-wave wave64 compute program at one lane per invocation on a 32-lane host subgroup and exchanges ballots, shuffles and reductions between the two host subgroups through workgroup memory and barriers. On Ghost of Yōtei (PPSA26344, RTX 4090, Windows, with #6402's image-table stores, which the title needs to get that far), the first dispatch of the second variant of the compute program at 0x80001e5f00 (about 830k SPIR-V words, 125-145 storage images through an image table) lost the device (NVIDIA TDR, `vkQueueSubmit` returns `VK_ERROR_DEVICE_LOST`) in five runs, with warm and cold shader caches; without the variable the same dispatch runs. The device-fault report gives no instruction address the module maps to, so the cause is not attributed. Keeping a faulting invocation running in its spanned wave (`bdaStopsInvocations` off for spanned programs) and #6416 did not change it. Stray (PPSA02101) on Intel ANV and a Demon's Souls (PPSA01342) request replay showed no fault +- [Spanned wave64 compute](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvSubgroup.cpp) (shader recompiler) - `APS5_SPANNED_WAVE=1`, off by default, runs a single-wave wave64 compute program at one lane per invocation on a 32-lane host subgroup and exchanges ballots, shuffles and reductions between the two host subgroups through workgroup memory and barriers. Those barriers are correct only where every invocation of the workgroup reaches them, which holds for the guest's wave-uniform control flow. The emulated 64-bit LDS atomics ([LockedLdsUpdate](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp)) run only on the lanes the exec mask keeps, so their lock loop ballots within its own host subgroup, as its elect already does. Any other cross-lane exchange emitted inside per-lane control flow would hang a spanned wave in the same way. In Ghost of Yōtei's program 0x80003af600 (PPSA26344), the lock loop was the only place where barriers sat inside per-invocation conditionals (3 of 111, found by scanning the SPIR-V). Other programs and titles have not been scanned. Measured on an RTX 4090 (Windows): an offline replay of that program and two Yōtei runs that previously lost the device (NVIDIA TDR) now run past it, and [LdsMaskWaterfall.cpp](../../../core/libs/prx/libSceAgcDriver/tests/execution/LdsMaskWaterfall.cpp) covers a half-inactive wave. Stray (PPSA02101) on Intel ANV and a Demon's Souls (PPSA01342) request replay showed no fault, before this fix From ef3c83d6becbcd6c282d2d5e33d38385898212f5 Mon Sep 17 00:00:00 2001 From: ArjenS-LA Date: Fri, 16 Oct 2026 04:44:34 -0700 Subject: [PATCH 0498/1000] fix(tools): resolve named audit imports through NIDs --- docs/user/USAGE.md | 2 +- tools/import_audit.py | 10 +++++-- tools/tests/test_import_audit.py | 47 ++++++++++++++++++++++++++++++++ 3 files changed, 56 insertions(+), 3 deletions(-) diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index c38899b148..4be6d5398d 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -237,7 +237,7 @@ The report also lists needed libraries that have no file in `--libs` or `--modul | `--names ` | NID and name per line, as in `aerolib.csv` from [`tools/nid_names.py`](../../tools/nid_names.py); names the absent imports. Never downloaded; a name that does not hash to its NID is marked. | | `--json ` | Full result, stamped with the repository's commit and whether its tree was modified. | -Registry `nid` fields may contain either encoded NIDs or ordinary ELF symbol names such as `strcmp`. Ordinary names are matched against literal exports, without hashing them into NIDs. +Registry `nid` fields may contain either encoded NIDs or ordinary ELF symbol names such as `strcmp`. Imports are matched against literal exports first, then against their computed NIDs when the literal lookup misses. Reports retain the original import name. Export availability does not establish that every platform loader supports that lookup path. Exit codes: `0` nothing blocks loading, `1` there are `absent` imports or needed libraries without a file, `2` an input could not be read; the message names the file and the value. diff --git a/tools/import_audit.py b/tools/import_audit.py index 4059b91a3a..528e20e45e 100644 --- a/tools/import_audit.py +++ b/tools/import_audit.py @@ -459,11 +459,17 @@ def audit(imports, exports, stubs, libraries, modules, module_imports=None, modu providers = exports.get(nid, []) if library is None: providers = sorted(set(providers) | set((module_exports or {}).get(nid, []))) - kind = classify(nid, library, providers, stubs, modules) + resolved_nid = nid + if not providers: + resolved_nid = nid_names.compute_nid(nid) + providers = exports.get(resolved_nid, []) + if library is None: + providers = sorted(set(providers) | set((module_exports or {}).get(resolved_nid, []))) + kind = classify(resolved_nid, library, providers, stubs, modules) record["class"] = kind record["providers"] = providers record["library_mismatch"] = kind in ("implemented", "stub") and bool(library) and library not in providers and "libc.prx" not in providers - record["name"] = next((stubs[(provider, nid)] for provider in providers if (provider, nid) in stubs), None) if kind == "stub" else None + record["name"] = next((stubs[(provider, resolved_nid)] for provider in providers if (provider, resolved_nid) in stubs), None) if kind == "stub" else None ordered = sorted(records.values(), key=lambda record: (record["class"], record["library"] or "", record.get("module") or "", record["nid"])) check_conservation(len(imports) + sum(len(symbols) for symbols, _ in (module_imports or {}).values()), ordered) missing = defaultdict(int) diff --git a/tools/tests/test_import_audit.py b/tools/tests/test_import_audit.py index 993cf3a107..9ad3683144 100644 --- a/tools/tests/test_import_audit.py +++ b/tools/tests/test_import_audit.py @@ -195,6 +195,53 @@ class ImportAuditTests(unittest.TestCase): self.assertTrue(records["missing_function"]["weak"]) self.assertIsNone(data["stops"][0]["nid"]) + def test_named_imports_fall_back_to_nid_exports(self): + work = self.work + (work.libs / "libc.prx").write_bytes(exporting("j4ViWNHEgww")) + registry, report = work.root / "homebrew.registry.json", work.root / "audit.json" + write_registry(registry, [("strlen", ""), ("strlen", ""), + ("missing_function", "", True)]) + code, _, err = work.run(registry, "--json", str(report)) + self.assertEqual((code, err), (1, "")) + data = json.loads(report.read_text(encoding="utf-8")) + records = {record["nid"]: record for record in data["imports"]} + self.assertEqual(data["references"], 3) + self.assertEqual(records["strlen"]["references"], 2) + self.assertEqual(records["strlen"]["class"], "implemented") + self.assertEqual(records["strlen"]["providers"], ["libc.prx"]) + self.assertEqual(records["missing_function"]["class"], "absent") + self.assertTrue(records["missing_function"]["weak"]) + self.assertIsNone(data["stops"][0]["nid"]) + + def test_named_nid_fallback_preserves_stub_and_library_classification(self): + nid = nid_names.compute_nid(MESSAGE_NAME) + imports = [(MESSAGE_NAME, "libSceOther.prx", False)] + records, missing = import_audit.audit(imports, {nid: ["libSceMsgDialog.prx"]}, + {("libSceMsgDialog.prx", nid): MESSAGE_NAME}, + {"libSceMsgDialog.prx", "libSceOther.prx"}, set()) + self.assertEqual(missing, {}) + self.assertEqual(records[0]["class"], "stub") + self.assertEqual(records[0]["nid"], MESSAGE_NAME) + self.assertEqual(records[0]["name"], MESSAGE_NAME) + self.assertTrue(records[0]["library_mismatch"]) + + def test_literal_exports_take_precedence_over_nid_fallback(self): + imports = [("strlen", "", False)] + stubs = {("literal.prx", "strlen"): "strlen"} + exports = {"strlen": ["literal.prx"], "j4ViWNHEgww": ["libc.prx"]} + records, _ = import_audit.audit(imports, exports, stubs, set(exports), set()) + self.assertEqual(records[0]["class"], "stub") + self.assertEqual(records[0]["providers"], ["literal.prx"]) + module_imports = {"guest.so": (["strlen"], [])} + records, _ = import_audit.audit([], {"j4ViWNHEgww": ["libc.prx"]}, {}, + {"libc.prx"}, set(module_imports), module_imports, + {"strlen": ["other.so"]}) + self.assertEqual(records[0]["providers"], ["other.so"]) + records, _ = import_audit.audit([], {"j4ViWNHEgww": ["libc.prx"]}, {}, + {"libc.prx"}, set(module_imports), module_imports) + self.assertEqual(records[0]["class"], "implemented") + self.assertEqual(records[0]["providers"], ["libc.prx"]) + def test_empty_and_control_character_imports_remain_invalid(self): registry = self.work.root / "homebrew.registry.json" for name in ("", "#A#B", "bad\x00name", "bad\nname", "bad\x7fname", "bad name"): From 2d5f761cb59239996a905915c3f20cce8f1a30ac Mon Sep 17 00:00:00 2001 From: milescoviello Date: Fri, 16 Oct 2026 11:45:11 +0000 Subject: [PATCH 0499/1000] fix(agc): discard pipeline journals written before the mesh field The layout guards only build on Linux with libstdc++, so a Windows or macOS build of main after #5884 wrote journals whose mesh records lack outputPrimitiveSize under the new shader-cache source version. The journal format is now 3, so such a file is discarded instead of being read with the new codec. MarcZX100 pointed this out. Co-Authored-By: Claude Opus 5.5 --- .../Graphics/src/PipelineJournal.cpp | 2 +- core/libs/prx/libSceAgcDriver/tests/Graphics.cpp | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp index 8729ac4d9d..77d4566132 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp @@ -44,7 +44,7 @@ static_assert(sizeof(ShaderRecompiler::TessellationConfiguration) == 40, "Tessel constexpr std::uint32_t FileMagic = 0x4c4a5041u; // Bumped when the recipe codec, CanonicalPipelineState or the store key change in a way the // layout guards above do not catch (a field renumbered or reinterpreted at the same size). -constexpr std::uint32_t FileFormat = 2u; +constexpr std::uint32_t FileFormat = 3u; constexpr std::uint32_t ModuleRecord = 1u; constexpr std::uint32_t GraphicsRecord = 2u; constexpr std::uint32_t ComputeRecord = 3u; diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index dcc4ea5ae9..d6c6af2d83 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -37,6 +37,7 @@ #include #include #include +#include #include #include #include @@ -7656,6 +7657,19 @@ void pipelineJournalTests() { const auto after = PipelineJournalTotals(); Require(cancelled->Done() && after.replayFailed == 0 && after.replayRejected == 0 && after.replayedGraphics + after.replayPresent <= totals.replayedGraphics + totals.replayPresent + 1, "a cancelled replay did not stop cleanly"); } + { + const std::uint32_t meshWithoutOutputSize = 2u; + std::fstream file(PipelineJournalPath(), std::ios::in | std::ios::out | std::ios::binary); + file.seekp(4); + file.write(reinterpret_cast(&meshWithoutOutputSize), sizeof(meshWithoutOutputSize)); + file.close(); + Require(file.good(), "the journal header could not be rewritten"); + auto stale = StartPipelineJournalReplay(context, fingerprint, hooks); + Require(stale != nullptr, "the replay of a journal in the previous format did not start"); + stale->Wait(); + const auto discarded = PipelineJournalTotals(); + Require(discarded.loadedRecipes == 0 && discarded.loadedModules == 0, "a journal in format 2, whose mesh records lack the output primitive size, was loaded"); + } ClearCachedPipelines(context.device); } Require(replayedComputeIdentityLow != 0, "the replayed compute identity is empty"); From 5c42e97f47c05f7715cc1310de4de226354f2246 Mon Sep 17 00:00:00 2001 From: codingreapermaps <233954859+codingreapermaps@users.noreply.github.com> Date: Wed, 14 Oct 2026 14:50:03 +0100 Subject: [PATCH 0500/1000] feat(agc): write a multisampled color target through a storage image A program that stores to a 2D multisampled texture writes the samples of its host multisampled color target through a storage view. Targets are created without storage usage; the first storage binding that resolves to a target moves it to an image with storage usage, copying its samples once, and keeps the previous image until the target is released. Storage usage needs the device's shaderStorageImageMultisample feature and its format at the target's sample count. A surface no draw rendered to is created from its uniform color memory, as for sampling; other color memory still throws. Co-Authored-By: Claude Opus 5.5 --- .../Execution/src/VulkanDevice.cpp | 3 + .../Graphics/include/Context.hpp | 1 + .../Graphics/include/MultisampleTarget.hpp | 2 + .../Graphics/include/ShaderResources.hpp | 1 + .../Graphics/include/Texture.hpp | 1 + .../Graphics/src/MultisampleTarget.cpp | 226 ++++++++++++++---- .../Graphics/src/ShaderResources.cpp | 22 +- .../libSceAgcDriver/Graphics/src/Texture.cpp | 4 + .../prx/libSceAgcDriver/tests/Recorder.cpp | 16 +- .../execution/SampleMultisampleTarget.cpp | 87 ++++++- 10 files changed, 314 insertions(+), 49 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index c2ebf9a5ac..eb0e3aff8b 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -292,6 +292,7 @@ struct VulkanDevice::State { bool shaderTerminateInvocation = false; bool geometryShader = false; bool sampleRateShading = false; + bool storageImageMultisample = false; bool shaderClipDistance = false; bool shaderCullDistance = false; bool shaderClock = false; @@ -1574,6 +1575,7 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : state->occlusionQueryPrecise = enabled.occlusionQueryPrecise == VK_TRUE; // Recompiled storage-image access declares no format (the guest descriptor decides it). enabled.shaderStorageImageMultisample = available.shaderStorageImageMultisample; + state->storageImageMultisample = enabled.shaderStorageImageMultisample == VK_TRUE; if (enabled.shaderStorageImageMultisample) state->capabilities.push_back(spv::CapabilityStorageImageMultisample); state->capabilities.push_back(spv::CapabilityImageMSArray); enabled.shaderStorageImageWriteWithoutFormat = available.shaderStorageImageWriteWithoutFormat; @@ -3580,6 +3582,7 @@ Graphics::Context VulkanDevice::buildContext() const { context.samplerTableCapacity = SamplerTableCapacity(context); context.geometryShader = state->geometryShader; context.sampleRateShading = state->sampleRateShading; + context.storageImageMultisample = state->storageImageMultisample; context.shaderClipDistance = state->shaderClipDistance; context.shaderCullDistance = state->shaderCullDistance; context.shaderTerminateInvocation = state->shaderTerminateInvocation; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp index 886e67680c..995eee4957 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp @@ -212,6 +212,7 @@ struct Context { std::uint32_t samplerTableCapacity = 0; bool geometryShader = false; bool sampleRateShading = false; + bool storageImageMultisample = false; bool shaderClipDistance = false; bool shaderCullDistance = false; bool nullDescriptors = false; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp index 237fd10ad0..c7b8e44aba 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp @@ -54,6 +54,8 @@ bool TakeMultisampledClear(MultisampledCmask& cmask, MultisampledColorFill& colo VkImageView MultisampleTargetView(const Context& context, const ColorTarget& target); std::shared_ptr MultisampleTargetTexture(const Context& context, const GuestTextureResource& resource, VkComponentMapping components); void MaterializeMultisampleTargetClear(const Context& context, const ColorTarget& target); +VkImageView MultisampleTargetStorageView(const Context& context, const GuestTextureResource& resource); +bool MultisampleTargetStorable(const Context& context, const ColorTarget& target); void ResolveMultisampleTarget(const Context& context, const ColorTarget& source, StorageTexture& destination, VkImageView destinationView); bool MultisampledColorFillAbsorbs(std::uint64_t address, std::size_t bytes, std::uint32_t pattern); void NoteColorMetadataFill(std::uint64_t address, std::size_t bytes, std::uint32_t pattern); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp index efefda3a7f..8e25d227c9 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp @@ -530,6 +530,7 @@ private: std::vector textureBaseLevel; std::vector textureSrgb; std::vector> storageTextures; + std::vector multisampleStorageViews; std::vector storageMips; std::vector storageKeys; std::vector storageFirstLayer; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp index bc6efaf720..dfe074f738 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp @@ -34,6 +34,7 @@ struct HostImport; VkFormat StorageFormatForGuest(const Context& context, std::uint32_t guestFormat); // Whether StorageFormatForGuest would succeed, without throwing (unknown guest formats included). bool StorageFormatAvailable(const Context& context, std::uint32_t guestFormat); +VkFormat HostStorageFormat(const Context& context, VkFormat format); bool StorageClearAvailable(const Context& context, std::uint32_t guestFormat, DccKeys keys); std::uint64_t SampledTextureMemory(); bool ClearKeepsDenormals(const Context& context, VkFormat format); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp index 15da911cfc..65e55692aa 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp @@ -58,50 +58,29 @@ public: VkFormatProperties properties{}; context.formatProperties(context.physical, target.format, &properties); Require((properties.optimalTilingFeatures & VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT) != 0, "multisampled render-target format cannot be a color attachment"); - VkImageUsageFlags usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT; + usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT; VkImageFormatProperties supported{}; sampleable = (properties.optimalTilingFeatures & VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT) != 0 && context.imageFormatProperties(context.physical, target.format, VK_IMAGE_TYPE_2D, VK_IMAGE_TILING_OPTIMAL, usage | VK_IMAGE_USAGE_SAMPLED_BIT, 0, &supported) == VK_SUCCESS && (supported.sampleCounts & samples) != 0; if (sampleable) usage |= VK_IMAGE_USAGE_SAMPLED_BIT; Check(context.imageFormatProperties(context.physical, target.format, VK_IMAGE_TYPE_2D, VK_IMAGE_TILING_OPTIMAL, usage, 0, &supported), "vkGetPhysicalDeviceImageFormatProperties multisampled target"); Require((supported.sampleCounts & samples) != 0 && target.extent.width <= supported.maxExtent.width && target.extent.height <= supported.maxExtent.height, "multisampled render target exceeds device image limits"); + storageFormat = HostStorageFormat(context, target.format); + storageFlags = storageFormat != target.format ? VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT | VK_IMAGE_CREATE_EXTENDED_USAGE_BIT : 0u; + const auto storageSupports = [&](VkFormat format, VkImageUsageFlags formatUsage, VkImageCreateFlags flags) { + VkImageFormatProperties properties{}; + return context.imageFormatProperties(context.physical, format, VK_IMAGE_TYPE_2D, VK_IMAGE_TILING_OPTIMAL, formatUsage, flags, &properties) == VK_SUCCESS && (properties.sampleCounts & samples) != 0; + }; + if (storageFormat != VK_FORMAT_UNDEFINED && (!context.storageImageMultisample || !storageSupports(storageFormat, VK_IMAGE_USAGE_STORAGE_BIT, 0u) || !storageSupports(target.format, usage | VK_IMAGE_USAGE_STORAGE_BIT, storageFlags))) storageFormat = VK_FORMAT_UNDEFINED; try { - VkImageCreateInfo info{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO}; - info.imageType = VK_IMAGE_TYPE_2D; - info.format = target.format; - info.extent = {target.extent.width, target.extent.height, 1}; - info.mipLevels = 1; - info.arrayLayers = 1; - info.samples = samples; - info.tiling = VK_IMAGE_TILING_OPTIMAL; - info.usage = usage; - info.sharingMode = VK_SHARING_MODE_EXCLUSIVE; - info.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; - Check(context.Function("vkCreateImage")(context.device, &info, nullptr, &image), "vkCreateImage multisampled target"); - VkMemoryRequirements requirements{}; - context.Function("vkGetImageMemoryRequirements")(context.device, image, &requirements); - VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; - allocation.allocationSize = requirements.size; - allocation.memoryTypeIndex = context.MemoryType(requirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); - Check(context.Function("vkAllocateMemory")(context.device, &allocation, nullptr, &memory), "vkAllocateMemory multisampled target"); - Check(context.Function("vkBindImageMemory")(context.device, image, memory, 0), "vkBindImageMemory multisampled target"); - VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; - viewInfo.image = image; - viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D; - viewInfo.format = target.format; - viewInfo.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; - Check(context.Function("vkCreateImageView")(context.device, &viewInfo, nullptr, &view), "vkCreateImageView multisampled target"); + const auto created = allocate(usage, 0u); + image = created.image; + memory = created.memory; + view = created.view; recordCommands(context, [&](VkCommandBuffer commands, Recorder* recorder) { - VkImageMemoryBarrier toGeneral{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; - toGeneral.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; - toGeneral.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; - toGeneral.newLayout = VK_IMAGE_LAYOUT_GENERAL; - toGeneral.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; - toGeneral.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; - toGeneral.image = image; - toGeneral.subresourceRange = viewInfo.subresourceRange; - context.Resolved(&DeviceFunctions::cmdPipelineBarrier, "vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &toGeneral); + recordToGeneral(commands, image); const VkClearColorValue zero{}; - context.Resolved(&DeviceFunctions::cmdClearColorImage, "vkCmdClearColorImage")(commands, image, VK_IMAGE_LAYOUT_GENERAL, &zero, 1, &toGeneral.subresourceRange); + const VkImageSubresourceRange range{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + context.Resolved(&DeviceFunctions::cmdClearColorImage, "vkCmdClearColorImage")(commands, image, VK_IMAGE_LAYOUT_GENERAL, &zero, 1, &range); RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT); if (recorder != nullptr) Recorder::CountBarriers(Recorder::CommandClass::Draw, 2); }); @@ -114,6 +93,88 @@ public: MultisampleTarget(const MultisampleTarget&) = delete; MultisampleTarget& operator=(const MultisampleTarget&) = delete; + struct Allocation { + VkImage image = VK_NULL_HANDLE; + VkDeviceMemory memory = VK_NULL_HANDLE; + VkImageView view = VK_NULL_HANDLE; + }; + + Allocation allocate(VkImageUsageFlags imageUsage, VkImageCreateFlags flags) { + Allocation created; + try { + VkImageCreateInfo info{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO}; + info.flags = flags; + info.imageType = VK_IMAGE_TYPE_2D; + info.format = target.format; + info.extent = {target.extent.width, target.extent.height, 1}; + info.mipLevels = 1; + info.arrayLayers = 1; + info.samples = static_cast(target.samples); + info.tiling = VK_IMAGE_TILING_OPTIMAL; + info.usage = imageUsage; + info.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + info.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + Check(context.Function("vkCreateImage")(context.device, &info, nullptr, &created.image), "vkCreateImage multisampled target"); + VkMemoryRequirements requirements{}; + context.Function("vkGetImageMemoryRequirements")(context.device, created.image, &requirements); + VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; + allocation.allocationSize = requirements.size; + allocation.memoryTypeIndex = context.MemoryType(requirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); + Check(context.Function("vkAllocateMemory")(context.device, &allocation, nullptr, &created.memory), "vkAllocateMemory multisampled target"); + Check(context.Function("vkBindImageMemory")(context.device, created.image, created.memory, 0), "vkBindImageMemory multisampled target"); + VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; + viewInfo.image = created.image; + viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D; + viewInfo.format = target.format; + viewInfo.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + Check(context.Function("vkCreateImageView")(context.device, &viewInfo, nullptr, &created.view), "vkCreateImageView multisampled target"); + } catch (...) { + destroy(created); + throw; + } + return created; + } + + void destroy(const Allocation& allocation) noexcept { + if (allocation.view) context.Function("vkDestroyImageView")(context.device, allocation.view, nullptr); + if (allocation.image) context.Function("vkDestroyImage")(context.device, allocation.image, nullptr); + if (allocation.memory) context.Function("vkFreeMemory")(context.device, allocation.memory, nullptr); + } + + void recordToGeneral(VkCommandBuffer commands, VkImage destination) { + VkImageMemoryBarrier toGeneral{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; + toGeneral.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + toGeneral.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; + toGeneral.newLayout = VK_IMAGE_LAYOUT_GENERAL; + toGeneral.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + toGeneral.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + toGeneral.image = destination; + toGeneral.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + context.Resolved(&DeviceFunctions::cmdPipelineBarrier, "vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &toGeneral); + } + + void makeStorable(Recorder& recorder) { + if ((usage & VK_IMAGE_USAGE_STORAGE_BIT) != 0) return; + const auto storable = allocate(usage | VK_IMAGE_USAGE_STORAGE_BIT, storageFlags); + const auto source = image; + recordToGeneral(recorder.Commands(), storable.image); + RecordMemoryBarrier(context, recorder.Commands(), VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT); + VkImageCopy region{}; + region.srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; + region.dstSubresource = region.srcSubresource; + region.extent = {target.extent.width, target.extent.height, 1}; + context.Function("vkCmdCopyImage")(recorder.Commands(), source, VK_IMAGE_LAYOUT_GENERAL, storable.image, VK_IMAGE_LAYOUT_GENERAL, 1, ®ion); + RecordMemoryBarrier(context, recorder.Commands(), VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT); + Recorder::CountBarriers(Recorder::CommandClass::Draw, 3); + for (const auto& [key, texture] : textures) recorder.Keep(texture); + textures.clear(); + retired.push_back({image, memory, view}); + image = storable.image; + memory = storable.memory; + view = storable.view; + usage |= VK_IMAGE_USAGE_STORAGE_BIT; + } + void ApplyFastClear(const ColorTarget& use) { VkClearColorValue clear{}; if (!TakeMultisampledClear(cmask, colorFill, use, clear)) return; @@ -126,11 +187,50 @@ public: }); } + void SettleStorageWrites() { + if (!storageWritten) return; + storageWritten = false; + recordCommands(context, [&](VkCommandBuffer commands, Recorder* recorder) { + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT); + if (recorder != nullptr) Recorder::CountBarriers(Recorder::CommandClass::Draw, 1); + }); + } + + VkImageView Storage(VkFormat format) { + Require(storageFormat != VK_FORMAT_UNDEFINED, "the device cannot write the multisampled color target's format as a storage image at its sample count"); + Require(format == storageFormat, "a storage image access to a multisampled color target in another format than the target's storage form is not implemented"); + Require(cmask.Refusal().empty(), cmask.Refusal()); + Require(cmask.Keys() != DccKeys::Clear0000, "a storage image access to a multisampled color target whose CMASK fast clear no draw has applied is not implemented"); + auto* recording = Recorder::Active(); + Require(recording != nullptr, "a multisampled color target is only written by recorded dispatches"); + makeStorable(*recording); + ApplyFastClear(target); + if (storageView == VK_NULL_HANDLE) { + VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; + viewInfo.image = image; + viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D; + viewInfo.format = storageFormat; + viewInfo.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + Check(context.Function("vkCreateImageView")(context.device, &viewInfo, nullptr, &storageView), "vkCreateImageView multisampled target storage"); + } + recordCommands(context, [&](VkCommandBuffer commands, Recorder* recorder) { + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT); + if (recorder != nullptr) Recorder::CountBarriers(Recorder::CommandClass::Draw, 1); + }); + storageWritten = true; + return storageView; + } + + bool Storable() const { + return (usage & VK_IMAGE_USAGE_STORAGE_BIT) != 0; + } + std::shared_ptr Sampled(VkComponentMapping components) { Require(sampleable, "the device cannot sample the multisampled color target's format at its sample count"); Require(cmask.Refusal().empty(), cmask.Refusal()); Require(cmask.Keys() != DccKeys::Clear0000, "sampling a multisampled color target whose CMASK fast clear no draw has applied is not implemented"); Require(Recorder::Active() != nullptr, "a multisampled color target is only sampled by recorded draws and dispatches"); + SettleStorageWrites(); ApplyFastClear(target); recordCommands(context, [&](VkCommandBuffer commands, Recorder*) { RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT); @@ -185,17 +285,24 @@ public: VkRenderPass resolvePass = VK_NULL_HANDLE; MultisampledCmask cmask; MultisampledColorFill colorFill; + bool fromTexture = false; private: bool sampleable = false; + VkImageUsageFlags usage = 0; + VkFormat storageFormat = VK_FORMAT_UNDEFINED; + VkImageCreateFlags storageFlags = 0; + std::vector retired; + VkImageView storageView = VK_NULL_HANDLE; + bool storageWritten = false; std::map, std::shared_ptr> textures; void release() noexcept { textures.clear(); if (resolvePass) context.Function("vkDestroyRenderPass")(context.device, resolvePass, nullptr); - if (view) context.Function("vkDestroyImageView")(context.device, view, nullptr); - if (image) context.Function("vkDestroyImage")(context.device, image, nullptr); - if (memory) context.Function("vkFreeMemory")(context.device, memory, nullptr); + if (storageView) context.Function("vkDestroyImageView")(context.device, storageView, nullptr); + destroy({image, memory, view}); + for (const auto& allocation : retired) destroy(allocation); } }; @@ -276,7 +383,12 @@ MultisampleTarget& acquire(const Context& context, const ColorTarget& use) { for (const auto& target : targets()) { if (!target->active->load(std::memory_order_acquire)) continue; if (target->context.device != context.device || !sameTarget(target->target, use)) continue; + if (target->fromTexture && use.cmaskAddress != 0) { + target->fromTexture = false; + target->cmask = MultisampledCmask(use.cmaskAddress, use.cmaskBytes); + } target->cmask.CheckAddress(use.cmaskAddress); + target->SettleStorageWrites(); target->ApplyFastClear(use); return *target; } @@ -288,10 +400,10 @@ MultisampleTarget& acquire(const Context& context, const ColorTarget& use) { return created; } -MultisampleTarget& createFilled(const Context& context, const GuestTextureResource& resource, VkFormat format) { +MultisampleTarget& createFilled(const Context& context, const GuestTextureResource& resource, VkFormat format, const char* access) { const auto refuse = [&](const std::string& reason) { - char text[160]; - std::snprintf(text, sizeof(text), "AGC graphics: sampling the %ux%u multisampled texture 0x%llx of %u samples, which no draw rendered to and ", resource.width, resource.height, static_cast(resource.baseAddress), resource.samples); + char text[192]; + std::snprintf(text, sizeof(text), "AGC graphics: %s the %ux%u multisampled texture 0x%llx of %u samples, which no draw rendered to and ", access, resource.width, resource.height, static_cast(resource.baseAddress), resource.samples); throw std::runtime_error(text + reason + ", is not implemented"); }; ColorTarget use{}; @@ -334,6 +446,7 @@ MultisampleTarget& createFilled(const Context& context, const GuestTextureResour auto& created = *targets().back(); guard(context, use, created.active); created.colorFill.NoteFill(use.address, static_cast(bytes), value); + created.fromTexture = true; return created; } @@ -478,7 +591,7 @@ std::shared_ptr MultisampleTargetTexture(const Context& context, const } if (other == nullptr) { Require(Recorder::Active() != nullptr, "a multisampled texture is only sampled by recorded draws and dispatches"); - return createFilled(context, resource, format).Sampled(components); + return createFilled(context, resource, format, "sampling").Sampled(components); } char text[320]; std::snprintf(text, sizeof(text), "AGC graphics: sampling multisampled color target 0x%llx (%ux%u, %u samples, vk format %d) as a %ux%u texture of %u samples and guest format %u (vk %d) is not implemented", static_cast(resource.baseAddress), other->target.extent.width, other->target.extent.height, other->target.samples, static_cast(other->target.format), resource.width, resource.height, resource.samples, resource.format, static_cast(format)); @@ -490,6 +603,33 @@ void MaterializeMultisampleTargetClear(const Context& context, const ColorTarget acquire(context, target); } +VkImageView MultisampleTargetStorageView(const Context& context, const GuestTextureResource& resource) { + std::lock_guard lock(targetsMutex()); + const auto format = ResolveTextureFormat(resource.format); + const MultisampleTarget* other = nullptr; + for (const auto& target : targets()) { + if (!target->active->load(std::memory_order_acquire)) continue; + if (target->context.device != context.device || target->target.surfaceAddress != resource.baseAddress) continue; + const auto& rendered = target->target; + if (rendered.format == format && rendered.extent.width == resource.width && rendered.extent.height == resource.height && rendered.samples == resource.samples && rendered.mip == 0) return target->Storage(StorageFormatForGuest(context, resource.format)); + other = target.get(); + } + if (other == nullptr) { + Require(Recorder::Active() != nullptr, "a multisampled color target is only written by recorded dispatches"); + return createFilled(context, resource, format, "writing").Storage(StorageFormatForGuest(context, resource.format)); + } + char text[320]; + std::snprintf(text, sizeof(text), "AGC graphics: storage image access to multisampled color target 0x%llx (%ux%u, %u samples, vk format %d) as a %ux%u texture of %u samples and guest format %u (vk %d) is not implemented", static_cast(resource.baseAddress), other->target.extent.width, other->target.extent.height, other->target.samples, static_cast(other->target.format), resource.width, resource.height, resource.samples, resource.format, static_cast(format)); + throw std::runtime_error(text); +} + +bool MultisampleTargetStorable(const Context& context, const ColorTarget& target) { + std::lock_guard lock(targetsMutex()); + return std::any_of(targets().begin(), targets().end(), [&](const auto& candidate) { + return candidate->active->load(std::memory_order_acquire) && candidate->context.device == context.device && sameTarget(candidate->target, target) && candidate->Storable(); + }); +} + void ResolveMultisampleTarget(const Context& context, const ColorTarget& source, StorageTexture& destination, VkImageView destinationView) { std::lock_guard lock(targetsMutex()); auto& target = acquire(context, source); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index e31084e0c4..d9c0124e45 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -1989,6 +1989,7 @@ void ShaderResources::buildComplete() { write.pImageInfo = images.data() + images.size(); for (const auto index : binding.imageAllocations) { if (index == std::numeric_limits::max()) images.push_back(absentImage(binding.layout.binding)); + else if (storageTextures[index] == nullptr) images.push_back({VK_NULL_HANDLE, multisampleStorageViews[index], VK_IMAGE_LAYOUT_GENERAL}); else images.push_back({VK_NULL_HANDLE, storageAtomic64[index] ? storageTextures[index]->Atomic64View(storageMips[index], storageFirstLayer[index]) : storageAtomic[index] ? storageTextures[index]->AtomicView(storageMips[index], storageFirstLayer[index]) : storageTextures[index]->StorageView(storageMips[index], storageFirstLayer[index]), VK_IMAGE_LAYOUT_GENERAL}); } Require(binding.imageAllocations.size() == binding.layout.descriptorCount, "descriptor allocations disagree with compact binding"); @@ -4051,6 +4052,21 @@ void ShaderResources::resolveImageBinding(const ShaderRecompiler::DescriptorBind const auto resource = record != nullptr && record->decoded ? record->resource : DecodeTextureResource(words); const bool firstLayer = binding.imageShape == ShaderRecompiler::DescriptorImageShape::Image2D && resource.dimension == TextureDimension::k2DArray; if (binding.imageShape.has_value() && !firstLayer && !MatchesGuestDimension(*binding.imageShape, resource.dimension)) throw std::runtime_error("AGC graphics: guest storage texture dimension disagrees with the shader's declared image shape (shape " + std::to_string(static_cast(*binding.imageShape)) + ", dimension " + std::to_string(static_cast(resource.dimension)) + ")"); + if (resource.dimension == TextureDimension::k2DMsaa) { + Require(!(element < binding.imageAtomic.size() && binding.imageAtomic[element]) && !(element < binding.imageAtomic64.size() && binding.imageAtomic64[element]), "storage image atomics on a multisampled texture are not implemented"); + storageTextures.push_back(nullptr); + multisampleStorageViews.resize(storageTextures.size(), VK_NULL_HANDLE); + multisampleStorageViews.back() = MultisampleTargetStorageView(context, resource); + multisampleSources.push_back(resource.baseAddress); + storageMips.push_back(0); + storageKeys.push_back(0); + storageFirstLayer.push_back(false); + storageWritten.push_back(false); + storageAtomic.push_back(false); + storageAtomic64.push_back(false); + item.imageAllocations.push_back(storageTextures.size() - 1); + continue; + } const auto mip = std::min(resource.baseLevel + mipOffset, resource.mipCount - 1u); Require(resource.minLod <= mip * 256u, "guest storage texture descriptor clamps its minimum LOD above the level it addresses, which is not implemented"); const auto guestBytes = record != nullptr && record->decoded ? record->guestBytes : DescribeSurface(resource).guestBytes; @@ -4364,7 +4380,7 @@ std::vector> ShaderResources::PresyncSur } } for (std::size_t index = 0; index < storageTextures.size(); ++index) { - if (index != 0 && storageTextures[index] == storageTextures[index - 1]) continue; + if (storageTextures[index] == nullptr || (index != 0 && storageTextures[index] == storageTextures[index - 1])) continue; const auto& image = *storageTextures[index]; consider(image.Descriptor().format, image.Descriptor().baseAddress, image.GuestBytes(), false); } @@ -4649,7 +4665,7 @@ void ShaderResources::WriteBack() { WriteBackBuffers(); if (SkipWriteBack()) return; for (std::size_t index = 0; index < storageTextures.size(); ++index) { - if (!storageWritten[index]) continue; + if (!storageWritten[index] || storageTextures[index] == nullptr) continue; storageTextures[index]->MarkDirty(); static_cast(DepthSurfaceStoreBack(context, *storageTextures[index])); } @@ -4668,7 +4684,7 @@ void ShaderResources::MarkGpuWrites(Recorder& recorder) { } if (SkipWriteBack()) return; for (std::size_t index = 0; index < storageTextures.size(); ++index) { - if (!storageWritten[index]) continue; + if (!storageWritten[index] || storageTextures[index] == nullptr) continue; storageTextures[index]->MarkDirty(); static_cast(DepthSurfaceStoreBack(context, *storageTextures[index])); } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index 2523a53bbf..ed284ea658 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -978,6 +978,10 @@ bool StorageFormatAvailable(const Context& context, std::uint32_t guestFormat) { } } +VkFormat HostStorageFormat(const Context& context, VkFormat format) { + return StorageFormatOrUndefined(context, format); +} + bool IntegerFormat(VkFormat format) { switch (format) { case VK_FORMAT_R8_UINT: case VK_FORMAT_R8_SINT: case VK_FORMAT_R8G8_UINT: case VK_FORMAT_R8G8_SINT: diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index e132d8db4e..2c4073c7a8 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -6234,6 +6234,7 @@ void multisampleGuestAccessTests(const Device& device, Recorder& recorder) { { std::lock_guard gpu(GpuMutex()); Require(MultisampleTargetView(context, target) != VK_NULL_HANDLE, "the multisampled target has no view"); + Require(!MultisampleTargetStorable(context, target), "a multisampled target nothing stored to was created with storage usage"); recorder.Submit(); device.WaitQueue(); recorder.Sync(); @@ -6319,7 +6320,20 @@ void multisampleGuestAccessTests(const Device& device, Recorder& recorder) { refusedSample(ShaderRecompiler::DescriptorKind::SampledImage, multisampledWords(side, 3u, address), "as a 64x64 texture of 8 samples", "sampling a 4x target as an 8x texture"); refusedSample(ShaderRecompiler::DescriptorKind::SampledImage, multisampledWords(side / 2u, 2u, address), "as a 32x64 texture of 4 samples", "sampling a 4x target as a narrower texture"); refusedSample(ShaderRecompiler::DescriptorKind::SampledImage, multisampledWords(side, 2u, address + 4u * 65536u), "which no draw rendered to and whose tile mode 0 has no modeled multisampled color footprint", "sampling a linear multisampled texture no draw rendered to"); - refusedSample(ShaderRecompiler::DescriptorKind::StorageImage, multisampledWords(side, 2u, address), "storage image access to a multisampled texture", "storage access to a multisampled texture"); + if (context.storageImageMultisample) { + const auto stored = sampledProgram(ShaderRecompiler::DescriptorKind::StorageImage, multisampledWords(side, 2u, address)); + const CompiledShader storedCompute{ShaderRecompiler::ShaderStage::Compute, &stored, 0}; + std::lock_guard gpu(GpuMutex()); + const auto rendered = MultisampleTargetView(context, target); + Require(!MultisampleTargetStorable(context, target), "sampling the multisampled target gave it storage usage"); + ShaderResources resources(context, storedCompute); + Require(MultisampleTargetStorable(context, target) && MultisampleTargetView(context, target) != rendered, "a storage binding did not move the multisampled target to an image with storage usage"); + recorder.Submit(); + device.WaitQueue(); + recorder.Sync(); + } else { + refusedSample(ShaderRecompiler::DescriptorKind::StorageImage, multisampledWords(side, 2u, address), "as a storage image at its sample count", "storage access to a multisampled target without shaderStorageImageMultisample"); + } refused([&] { AgcDriver::GuestMemory::Read(address + 3u * 65536u + 256u, texels); }, "a CPU read", "a CPU read in the padding of a later sample plane"); AgcDriver::GuestMemory::Read(address + 4u * 65536u, texels); clearTargets(); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp index f3d7e2bf44..16e51a1967 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp @@ -54,6 +54,8 @@ constexpr std::array Drawn{0.25f, 0.5f, 0.125f, 1.0f}; constexpr double EdgeMargin = 1.0 / 32.0; constexpr std::uint32_t FillOnes = 0x3c003c00u; constexpr std::array Ones{1.0f, 1.0f, 1.0f, 1.0f}; +constexpr std::array Written{0.75f, 0.25f, 0.5f, 1.0f}; +constexpr std::uint32_t StoredSample = 1; alignas(256) constexpr std::array VertexCode{ 0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, @@ -70,6 +72,18 @@ alignas(256) constexpr std::array LoadCode{ 0x80001301, 0xbf810000, }; +alignas(256) constexpr std::array StoreCode{ + 0x34020087, 0xe030100c, 0x80000501, 0xe0301010, 0x80000601, 0xe0301014, 0x80000701, 0xe0301018, + 0x80001001, 0xe030101c, 0x80001101, 0xe0301020, 0x80001201, 0xe0301024, 0x80001301, 0xbf8c3f70, + 0xf0201f30, 0x00011005, 0xbf810000, +}; + +struct SampleWrite { + std::uint32_t x; + std::uint32_t y; + std::uint32_t sample; +}; + constexpr std::array, 3> Triangle{{ {-1.0f, -1.0f, 0.5f, 1.0f}, {0.83f, -1.0f, 0.5f, 1.0f}, {-1.0f, 0.71f, 0.5f, 1.0f} }}; @@ -199,13 +213,12 @@ std::array TargetDescriptor(const Block& block, std::uint32_t }; } -ShaderRecompiler::RecompileResult CompileLoad(AgcDriver::VulkanDevice& device, const std::array& texture, std::vector& userData) { +ShaderRecompiler::RecompileResult CompileProgram(AgcDriver::VulkanDevice& device, std::span code, const std::array& texture, std::vector& userData) { const auto address = reinterpret_cast(Buffer.data()); const std::array buffer{static_cast(address), static_cast((address >> 32u) & 0xffffu), static_cast(Buffer.size() * 4u), 0x31016facu}; userData.assign(16, 0u); std::copy(buffer.begin(), buffer.end(), userData.begin()); std::copy(texture.begin(), texture.end(), userData.begin() + 4); - const std::span code(LoadCode); const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; ShaderRecompiler::RecompileRequest request{ @@ -218,6 +231,63 @@ ShaderRecompiler::RecompileResult CompileLoad(AgcDriver::VulkanDevice& device, c return ShaderRecompiler::Recompile(request); } +ShaderRecompiler::RecompileResult CompileLoad(AgcDriver::VulkanDevice& device, const std::array& texture, std::vector& userData) { + return CompileProgram(device, LoadCode, texture, userData); +} + +void StoreSamples(AgcDriver::VulkanDevice& device, const std::array& texture, const std::vector& writes) { + std::vector userData; + const auto program = CompileProgram(device, StoreCode, texture, userData); + for (std::size_t first = 0; first < writes.size(); first += Threads) { + Buffer.fill(0u); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto& write = writes[std::min(first + tid, writes.size() - 1)]; + const std::array words{0u, 0u, 0u, write.x, write.y, write.sample, std::bit_cast(Written[0]), std::bit_cast(Written[1]), std::bit_cast(Written[2]), std::bit_cast(Written[3])}; + std::copy(words.begin(), words.end(), &Buffer[tid * Words]); + } + device.Dispatch(program, 1, 1, 1, {}, reinterpret_cast(StoreCode.data())); + device.WaitIdle(); + } +} + +std::vector SampleWrites(bool (*selected)(std::uint32_t x, std::uint32_t y, std::uint32_t sample)) { + std::vector writes; + for (std::uint32_t y = 0; y < Side; ++y) { + for (std::uint32_t x = 0; x < Side; ++x) { + for (std::uint32_t sample = 0; sample < Samples; ++sample) { + if (selected(x, y, sample)) writes.push_back({x, y, sample}); + } + } + } + return writes; +} + +void CheckWritten(const std::vector>& before, const std::vector>& after, bool (*selected)(std::uint32_t x, std::uint32_t y, std::uint32_t sample), const std::string& what) { + std::uint32_t written = 0; + for (std::uint32_t index = 0; index < after.size(); ++index) { + const auto pixel = index / Samples; + const auto x = pixel % Side; + const auto y = pixel / Side; + const auto sample = index % Samples; + const bool chosen = selected(x, y, sample); + char where[96]; + std::snprintf(where, sizeof(where), "sample %u of pixel (%u, %u)", sample, x, y); + Require(after[index] == (chosen ? Written : before[index]), std::string(where) + (chosen ? " does not hold the stored color " : " changed although it was not stored to ") + what); + written += chosen ? 1u : 0u; + } + Require(written != 0 && written != after.size(), "the stores covered every sample or none " + what); +} + +void RequireStoreRefused(AgcDriver::VulkanDevice& device, const std::array& texture, const std::string& expected, const std::string& what) { + std::string refusal; + try { + StoreSamples(device, texture, {{0u, 0u, 0u}}); + } catch (const std::exception& error) { + refusal = error.what(); + } + Require(refusal.find(expected) != std::string::npos, what + " was not refused: " + refusal); +} + std::vector> LoadSamples(AgcDriver::VulkanDevice& device, const Block& block) { std::vector userData; const auto program = CompileLoad(device, TargetDescriptor(block, SampleShift), userData); @@ -341,6 +411,19 @@ int main() { const std::uint32_t odd = 0x3c000000u; std::memcpy(mixed.data + colorBytes - sizeof(odd), &odd, sizeof(odd)); RequireRefused(*device, TargetDescriptor(mixed, SampleShift), "does not hold one repeated 32-bit value", "loading a multisampled texture no draw rendered to whose color memory is not uniform"); + const auto oneSample = [](std::uint32_t, std::uint32_t, std::uint32_t sample) { return sample == StoredSample; }; + const auto drawnBefore = LoadSamples(*device, block); + StoreSamples(*device, TargetDescriptor(block, SampleShift), SampleWrites(oneSample)); + CheckWritten(drawnBefore, LoadSamples(*device, block), oneSample, "into one sample of each pixel of the drawn target"); + const auto leftHalf = [](std::uint32_t x, std::uint32_t, std::uint32_t sample) { return x < Side / 2u && sample != 0u; }; + Block undrawn(BlockBytes); + StoreSamples(*device, TargetDescriptor(undrawn, SampleShift), SampleWrites(leftHalf)); + CheckWritten(std::vector>(Side * Side * Samples), LoadSamples(*device, undrawn), leftHalf, "into zeroed color memory no draw rendered to"); + Block mixedStore(BlockBytes); + StoreWords(mixedStore, colorBytes, FillOnes); + std::memcpy(mixedStore.data + colorBytes - sizeof(odd), &odd, sizeof(odd)); + RequireStoreRefused(*device, TargetDescriptor(mixedStore, SampleShift), "writing the 16x16 multisampled texture", "storing to a multisampled texture no draw rendered to whose color memory is not uniform"); + std::puts("stores to one sample of each pixel of a drawn 4x target and to samples of zeroed color memory no draw rendered to reach exactly those samples, and a store to non-uniform color memory no draw rendered to is refused"); std::puts("every sample of a 4x color target loaded through its multisampled texture descriptor matches the triangle drawn into it, then the fill of its color memory; a surface no draw rendered to loads the value its color memory repeats and keeps it under a later draw"); return 0; } catch (const std::exception& error) { From 6c67548618d8a1e696ac50a1f48126f6b7fc8491 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Fri, 16 Oct 2026 14:08:45 +0200 Subject: [PATCH 0501/1000] test(agc): enable shaderStorageImageMultisample on the recorder test device Co-Authored-By: Claude --- core/libs/prx/libSceAgcDriver/tests/Recorder.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 2c4073c7a8..6d9e99fff4 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -210,6 +210,8 @@ public: context.occlusionQueryPrecise = supported.occlusionQueryPrecise == VK_TRUE; enabled.shaderStorageImageReadWithoutFormat = supported.shaderStorageImageReadWithoutFormat; enabled.shaderStorageImageWriteWithoutFormat = supported.shaderStorageImageWriteWithoutFormat; + enabled.shaderStorageImageMultisample = supported.shaderStorageImageMultisample; + context.storageImageMultisample = supported.shaderStorageImageMultisample == VK_TRUE; context.singlePassStorage = supported.shaderStorageImageReadWithoutFormat == VK_TRUE && supported.shaderStorageImageWriteWithoutFormat == VK_TRUE; address.pNext = &bytes; std::vector extensionsEnabled{VK_KHR_BUFFER_DEVICE_ADDRESS_EXTENSION_NAME, VK_KHR_8BIT_STORAGE_EXTENSION_NAME}; From 7a6cc7178637c7d4e33758352a0f55e37c34b755 Mon Sep 17 00:00:00 2001 From: Alex Collini Date: Fri, 16 Oct 2026 14:12:47 +0200 Subject: [PATCH 0502/1000] feat(agc): carry spanned waves in the target and require full subgroups APS5_SPANNED_WAVE is now read once by the driver, which sets SpirvTarget::spannedWaves only on a device with 32-lane compute subgroup control and computeFullSubgroups. The recompiler no longer reads the environment. The field is part of the cache key and of the request serialization (version 32), so a cached or replayed module keeps the mode it was built in. A spanned module numbers its lanes SubgroupId * 32 + SubgroupLocalInvocationId, which needs two full 32-lane subgroups. The artifact records this as fullSubgroups (disk cache and pipeline identity included), and the driver creates those pipelines with REQUIRE_FULL_SUBGROUPS. Compute pipeline captures accept the flag with a required subgroup size. The two-lane fallback now catches only SpannedWaveUnsupported, thrown for a shuffle wider than 32 bits; other emit errors surface as before. Co-Authored-By: Claude Opus 5.5 --- .../Execution/include/VulkanDevice.hpp | 2 ++ .../Execution/src/ComputePipelineCapture.cpp | 2 +- .../Execution/src/VulkanDevice.cpp | 24 ++++++++++++++- .../tests/ComputePipelineCapture.cpp | 10 ++++++- .../libSceAgcDriver/tests/ShaderDiskCache.cpp | 4 ++- .../libSceAgcDriver/tests/ShaderMemory.cpp | 10 +++++-- core/shader/recompiler/CacheKey.hpp | 1 + .../ControlFlow/src/RequestSerializer.cpp | 6 ++-- core/shader/recompiler/Recompiler.cpp | 11 +++---- core/shader/recompiler/Recompiler.hpp | 4 ++- core/shader/recompiler/ShaderDiskCache.cpp | 6 ++-- .../include/SpirvBackend/SpirvEmitter.hpp | 11 ++++++- .../SpirvBackend/src/SpirvEmitter.cpp | 3 +- .../src/SpirvMemory/SpirvSubgroup.cpp | 2 +- .../tests/RequestSerializerTests.cpp | 29 +++++++++++-------- 15 files changed, 94 insertions(+), 31 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp index 08de3994f0..4aa2fa07f2 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp @@ -58,6 +58,7 @@ public: const std::uint32_t* WorkGroupCountLimit() const; std::uint32_t VendorId() const; ShaderRecompiler::SpirvTarget ComputeTarget(std::uint32_t waveSize) const; + bool ComputeFullSubgroups() const; // Distinguishes this device from every earlier one in the process (a memo keyed by the device // cannot rely on the pointer, which a replacement may reuse). std::uint64_t Serial() const { return serial; } @@ -329,6 +330,7 @@ private: Graphics::Context graphicsContext() const; VkPipelineCreateFlags computePipelineFlags() const; std::uint32_t computeSubgroupSize(const ShaderRecompiler::RecompileResult& shader) const; + VkPipelineShaderStageCreateFlags computeStageFlags(const ShaderRecompiler::RecompileResult& shader) const; std::shared_ptr createComputePipeline(const Graphics::Context& context, const ShaderRecompiler::RecompileResult& shader, VkDescriptorSetLayout setLayout, std::span layoutKey, VkShaderStageFlags pushStages, std::uint64_t programAddress); ShaderRecompiler::SpirvTarget buildTarget() const; Graphics::Context buildContext() const; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/ComputePipelineCapture.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/ComputePipelineCapture.cpp index 6137a82dcc..4bdf335e98 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/ComputePipelineCapture.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/ComputePipelineCapture.cpp @@ -94,7 +94,7 @@ void ComputePipelineCapture::Validate() const { RequireCapture(mainEntry, "compute main entry point missing"); RequireCapture(deviceName.size() < VK_MAX_PHYSICAL_DEVICE_NAME_SIZE && deviceName.find('\0') == std::string::npos, "invalid device name"); RequireCapture((flags & ~(VK_PIPELINE_CREATE_DISPATCH_BASE_BIT | VK_PIPELINE_CREATE_CAPTURE_STATISTICS_BIT_KHR)) == 0, "unsupported pipeline flags"); - RequireCapture(stageFlags == 0, "unsupported shader stage flags"); + RequireCapture(stageFlags == 0 || (stageFlags == VK_PIPELINE_SHADER_STAGE_CREATE_REQUIRE_FULL_SUBGROUPS_BIT && requiredSubgroupSize != 0), "unsupported shader stage flags"); RequireCapture(requiredSubgroupSize == 0 || (requiredSubgroupSize <= 128 && (requiredSubgroupSize & (requiredSubgroupSize - 1u)) == 0), "invalid required subgroup size"); RequireCapture(sets.size() <= 8, "too many descriptor sets"); std::size_t totalBindings = 0; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index c2ebf9a5ac..1df4453950 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -353,6 +353,8 @@ struct VulkanDevice::State { bool computeWave32 = false; bool meshWave32 = false; std::uint32_t maxComputeSubgroupSize = 0; + bool computeFullSubgroups = false; + bool spannedWaves = false; // Indirect draw features enabled (see Graphics::Context). bool drawIndirectFirstInstance = false; bool multiDrawIndirect = false; @@ -821,6 +823,7 @@ ComputePipelineReplayResult VulkanDevice::ReplayComputePipeline(const ComputePip VkPhysicalDeviceSubgroupSizeControlPropertiesEXT subgroup{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_SIZE_CONTROL_PROPERTIES_EXT}; VkPhysicalDeviceProperties2 properties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, &subgroup}; state->InstanceFunction("vkGetPhysicalDeviceProperties2")(state->physical, &properties); + require(capture.stageFlags == 0 || state->computeFullSubgroups, "captured full compute subgroups are unsupported"); require(state->computeWave32 && capture.requiredSubgroupSize >= subgroup.minSubgroupSize && capture.requiredSubgroupSize <= subgroup.maxSubgroupSize && (subgroup.requiredSubgroupSizeStages & VK_SHADER_STAGE_COMPUTE_BIT) != 0, "captured required subgroup size is unsupported"); } const auto target = Target(); @@ -1747,10 +1750,12 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : state->meshWave32 = state->meshShader && subgroupSizeFeatures.subgroupSizeControl == VK_TRUE && subgroupSize.minSubgroupSize <= 32u && subgroupSize.maxSubgroupSize >= 32u && (subgroupSize.requiredSubgroupSizeStages & VK_SHADER_STAGE_MESH_BIT_EXT) != 0; state->maxComputeSubgroupSize = subgroupSize.maxSubgroupSize; + state->computeFullSubgroups = state->computeWave32 && subgroupSizeFeatures.computeFullSubgroups == VK_TRUE; } if (state->computeWave32 || state->meshWave32) { subgroupSizeFeatures = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_SIZE_CONTROL_FEATURES_EXT}; subgroupSizeFeatures.subgroupSizeControl = VK_TRUE; + subgroupSizeFeatures.computeFullSubgroups = state->computeFullSubgroups ? VK_TRUE : VK_FALSE; deviceExtensions.push_back(VK_EXT_SUBGROUP_SIZE_CONTROL_EXTENSION_NAME); deviceInfo.enabledExtensionCount = static_cast(deviceExtensions.size()); deviceInfo.ppEnabledExtensionNames = deviceExtensions.data(); @@ -1758,6 +1763,10 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : byteFeatures.pNext = &subgroupSizeFeatures; APS5_LOG_OUT("Wave32 programs run on subgroups of %u: compute %d, mesh %d", 32u, state->computeWave32, state->meshWave32); } + if (std::getenv("APS5_SPANNED_WAVE") != nullptr) { + state->spannedWaves = state->computeFullSubgroups; + APS5_LOG_OUT("APS5_SPANNED_WAVE: single-wave wave64 compute spans two full 32-lane subgroups: %d", state->spannedWaves); + } timelineFeatures = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_TIMELINE_SEMAPHORE_FEATURES_KHR}; timelineFeatures.timelineSemaphore = VK_TRUE; if (state->timelineSemaphores) { @@ -1988,6 +1997,16 @@ std::uint32_t VulkanDevice::computeSubgroupSize(const ShaderRecompiler::Recompil return state->computeWave32 && (shader.hostSubgroupSize == 32u || shader.hostSubgroupSize > state->maxComputeSubgroupSize) ? std::min(shader.hostSubgroupSize, state->maxComputeSubgroupSize) : 0u; } +VkPipelineShaderStageCreateFlags VulkanDevice::computeStageFlags(const ShaderRecompiler::RecompileResult& shader) const { + if (!shader.fullSubgroups) return 0; + require(state->computeFullSubgroups && computeSubgroupSize(shader) == 32u, "a spanned wave64 program needs full 32-lane compute subgroups"); + return VK_PIPELINE_SHADER_STAGE_CREATE_REQUIRE_FULL_SUBGROUPS_BIT; +} + +bool VulkanDevice::ComputeFullSubgroups() const { + return state->computeFullSubgroups; +} + void VulkanDevice::WaitForPipelineJournalReplay() { startJournalReplay(); if (state->journalReplay != nullptr) state->journalReplay->Wait(); @@ -3456,6 +3475,7 @@ ShaderRecompiler::SpirvTarget VulkanDevice::buildTarget() const { target.narrowSubgroupClock = state->narrowSubgroupClock; target.float64DenormPreserve = state->float64DenormPreserve; target.float16DenormPreserve = state->float16DenormPreserve; + target.spannedWaves = state->spannedWaves; target.fixedPushSlots = state->graphicsPipelineLibrary; target.registrationModules = state->graphicsPipelineLibrary; target.srgbDecodeFormats = state->srgbDecodeFormats; @@ -4538,6 +4558,7 @@ std::shared_ptr VulkanDevice::createComputePipeline(cons const VkPipelineCreateFlags pipelineFlags = computePipelineFlags(); const Graphics::PipelineSpecialization specialization(shader); const auto subgroupSize = computeSubgroupSize(shader); + const auto stageFlags = computeStageFlags(shader); if (const char* directory = std::getenv("APS5_DUMP_COMPUTE_PIPELINES"); directory != nullptr && *directory != '\0' && CaptureComputePipeline(shader.spirv.size())) { ComputePipelineCapture capture; capture.spirv = shader.spirv.Copy(); @@ -4553,6 +4574,7 @@ std::shared_ptr VulkanDevice::createComputePipeline(cons if (pushStages != 0) capture.pushConstants.push_back(push); for (const auto& value : shader.specialization) capture.specialization.push_back({value.id, value.value}); capture.requiredSubgroupSize = subgroupSize; + capture.stageFlags = stageFlags; capture.flags = pipelineFlags; capture.deviceName = state->properties.deviceName; capture.vendorId = state->properties.vendorID; @@ -4564,7 +4586,7 @@ std::shared_ptr VulkanDevice::createComputePipeline(cons std::fprintf(stderr, "[compute-capture] %s\n", path.string().c_str()); } if (profile && shader.spirv.size() > 100000) std::fprintf(stderr, "[dispatch] creating a pipeline for %zu SPIR-V words (program 0x%llx)\n", shader.spirv.size(), static_cast(programAddress)); - CreateComputePipeline(state->device, state->DeviceFunction("vkCreateComputePipelines"), context.pipelineCache, objects->module, objects->layout, pipelineFlags, 0, specialization.Info(), subgroupSize, &objects->pipeline, context.pipelineCacheSynchronization); + CreateComputePipeline(state->device, state->DeviceFunction("vkCreateComputePipelines"), context.pipelineCache, objects->module, objects->layout, pipelineFlags, stageFlags, specialization.Info(), subgroupSize, &objects->pipeline, context.pipelineCacheSynchronization); objects->destroyModule(objects->device, objects->module, nullptr); objects->module = VK_NULL_HANDLE; computePipelinesCreated.fetch_add(1, std::memory_order_relaxed); diff --git a/core/libs/prx/libSceAgcDriver/tests/ComputePipelineCapture.cpp b/core/libs/prx/libSceAgcDriver/tests/ComputePipelineCapture.cpp index 5324f2e432..51d13f59c5 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ComputePipelineCapture.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ComputePipelineCapture.cpp @@ -62,6 +62,10 @@ void CheckRoundTrip() { changed = decoded; changed.requiredSubgroupSize = 64; Require(changed.Identity() != capture.Identity(), "subgroup size did not affect capture identity"); + changed = decoded; + changed.stageFlags = VK_PIPELINE_SHADER_STAGE_CREATE_REQUIRE_FULL_SUBGROUPS_BIT; + Require(AgcDriver::ComputePipelineCapture::Decode(changed.Encode()).stageFlags == changed.stageFlags, "full subgroups were lost in the capture"); + Require(changed.Identity() != capture.Identity(), "full subgroups did not affect capture identity"); Directory directory; const auto file = capture.Write(directory.path); Require(AgcDriver::ComputePipelineCapture::Read(file).Encode() == encoded, "capture file changed pipeline inputs"); @@ -144,7 +148,11 @@ void CheckMalformedRecords() { invalid([](auto& copy) { copy.spirv[11] = 4; }); invalid([](auto& copy) { copy.spirv[13] = 0; }); invalid([](auto& copy) { copy.flags = VK_PIPELINE_CREATE_DERIVATIVE_BIT; }); - invalid([](auto& copy) { copy.stageFlags = VK_PIPELINE_SHADER_STAGE_CREATE_REQUIRE_FULL_SUBGROUPS_BIT; }); + invalid([](auto& copy) { copy.stageFlags = VK_PIPELINE_SHADER_STAGE_CREATE_ALLOW_VARYING_SUBGROUP_SIZE_BIT; }); + invalid([](auto& copy) { + copy.stageFlags = VK_PIPELINE_SHADER_STAGE_CREATE_REQUIRE_FULL_SUBGROUPS_BIT; + copy.requiredSubgroupSize = 0; + }); invalid([](auto& copy) { copy.requiredSubgroupSize = 3; }); invalid([](auto& copy) { copy.specialization.push_back(copy.specialization[0]); }); invalid([](auto& copy) { copy.pushConstants = {{VK_SHADER_STAGE_VERTEX_BIT, 0, 4}}; }); diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp index 66fe7adaeb..2ac7728dfd 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp @@ -75,7 +75,7 @@ void requireSameArtifact(const CompiledShaderArtifact& left, const CompiledShade require(left.runtimeAbiVersion == right.runtimeAbiVersion, prefix + "runtime ABI version differs"); require(left.memoryOffsetDword == right.memoryOffsetDword, prefix + "memory offset differs"); require(left.shaderDataDwords == right.shaderDataDwords && left.imageMetadataDword == right.imageMetadataDword && left.runtimeImageCount == right.runtimeImageCount && left.runtimeImageResources == right.runtimeImageResources, prefix + "compact data layout differs"); - require(left.hostSubgroupSize == right.hostSubgroupSize, prefix + "host subgroup size differs"); + require(left.hostSubgroupSize == right.hostSubgroupSize && left.fullSubgroups == right.fullSubgroups, prefix + "host subgroup size differs"); require(left.vertexInputs == right.vertexInputs, prefix + "vertex inputs differ"); require(left.vertexInputPatches == right.vertexInputPatches, prefix + "vertex type patches differ"); require(left.registrationModule == right.registrationModule, prefix + "registration module flags differ"); @@ -170,6 +170,7 @@ RecompileResult sampleResult() { result.bdaAbiVersion = 3; result.memoryOffsetDword = 7; result.hostSubgroupSize = 32; + result.fullSubgroups = true; result.workgroupMemoryDwords = 32769u; result.vertexInputs = {{1, 4, 2}, {5, 2, 0}}; result.vertexInputPatches = {{1, 20, {7, 8, 9}}, {5, 40, {10, 11, 12}}}; @@ -620,6 +621,7 @@ void verifyKeySensitivity() { changes("the narrow subgroup clock", [](SampleRequest& sample) { sample.request.target.narrowSubgroupClock = true; }); changes("f64 denormal preservation", [](SampleRequest& sample) { sample.request.target.float64DenormPreserve = true; }); changes("f16 denormal preservation", [](SampleRequest& sample) { sample.request.target.float16DenormPreserve = true; }); + changes("spanned waves", [](SampleRequest& sample) { sample.request.target.spannedWaves = true; }); changes("the workgroup size limit", [](SampleRequest& sample) { sample.request.target.maxWorkgroupSize[2] = 128; }); changes("the invocation limit", [](SampleRequest& sample) { sample.request.target.maxWorkgroupInvocations = 512; }); changes("the shared memory limit", [](SampleRequest& sample) { sample.request.target.maxWorkgroupSharedMemoryBytes = 32768; }); diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 86f21811f1..6ad15f3495 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -3067,7 +3067,7 @@ void verifyPixelRequestSerialization() { minimal.context.pixel->layerExported = true; minimal.context.pixel->ditheredAlphaToCoverage = true; const auto encoded = serializer.Serialize(minimal); - require(requestPrefix(encoded, 8u) == "NVNQQR8AAAA=", "new requests did not use serialization version 31"); + require(requestPrefix(encoded, 8u) == "NVNQQSAAAAA=", "new requests did not use serialization version 32"); const std::array fallbacks{0x40u, 0x1ac4u}; auto withFallbacks = minimal; withFallbacks.context.linkedCallFallbacks = fallbacks; @@ -3185,7 +3185,7 @@ void verifyPixelRequestSerialization() { for (std::size_t bytes = 0; bytes < 45u; ++bytes) { expectFailure([&] { static_cast(serializer.Deserialize(requestPrefix(encoded, mappingOffset + bytes))); }, "truncated data", "a truncated pixel mapping, packing, dual-source, alpha-to-coverage, stencil reference export flag, primitive class, layer export, dither or front-face flag, quad pixel mask or quad sample mask was accepted"); } - for (const auto unsupported : {"NVNQQQAAAAA=", "NVNQQSAAAAA="}) { + for (const auto unsupported : {"NVNQQQAAAAA=", "NVNQQSEAAAA="}) { expectFailure([&] { static_cast(serializer.Deserialize(unsupported)); }, "serialization version", "an unsupported request version was accepted"); } static constexpr std::string_view version18Request{ @@ -3276,6 +3276,7 @@ void verifyVertexInfoVersion19() { require(offset + 4u <= plain.size() && flagged[offset] == 0xefu && flagged[offset + 1u] == 0xcdu && flagged[offset + 2u] == 0xabu && flagged[offset + 3u] == 0x89u, "PA_CL_VS_OUT_CNTL is not the four bytes the version 20 vertex info adds"); require(std::equal(plain.begin() + offset + 4u, plain.end(), flagged.begin() + offset + 4u), "PA_CL_VS_OUT_CNTL moved the fields after it"); auto version20Vertex = flagged; + version20Vertex.pop_back(); version20Vertex[4] = 20u; const auto replay20Vertex = serializer.Deserialize(encodeBase64(version20Vertex)); require(replay20Vertex.request.context.vertex.has_value() && replay20Vertex.request.context.vertex->paClVsOutCntl == 0x89abcdefu, "a main version20 request lost PA_CL_VS_OUT_CNTL"); @@ -3284,6 +3285,7 @@ void verifyVertexInfoVersion19() { const auto layered = serializer.Deserialize(serializer.Serialize(vertex)); require(layered.request.context.vertex.has_value() && layered.request.context.vertex->paClVsOutCntl == vertex.context.vertex->paClVsOutCntl, "a layered vertex info lost its clip distances or render target index"); auto legacy = plain; + legacy.pop_back(); legacy.erase(legacy.begin() + offset, legacy.begin() + offset + 4u); for (const auto version : {16u, 17u, 18u, 19u}) { legacy[4] = static_cast(version); @@ -3331,21 +3333,25 @@ void verifyVertexInfoVersion19() { const auto sampledBytes = decodeBase64(serializer.Serialize(sampledPixel)); require(std::equal(maskedBytes.begin(), maskedBytes.begin() + static_cast(maskOffset + 1u), sampledBytes.begin()) && sampledBytes[maskOffset + 1u] == 0x1u && sampledBytes[maskOffset + 5u] == 0x2u && sampledBytes[maskOffset + 9u] == 0x4u && sampledBytes[maskOffset + 13u] == 0x8u, "the quad sample masks are not the four words after the quad pixel mask"); auto version29 = sampledBytes; + version29.pop_back(); version29.erase(version29.begin() + static_cast(maskOffset + 1u + sampleMaskBytes), version29.begin() + static_cast(maskOffset + 1u + sampleMaskBytes + 4u)); version29[4] = 29u; const auto replay29 = serializer.Deserialize(encodeBase64(version29)); require(replay29.request.context.pixel.has_value() && replay29.request.context.pixel->quadSampleMask == std::array{0x1u, 0x2u, 0x4u, 0x8u} && replay29.request.context.pixel->reversedBlend == 0u && decodeBase64(serializer.Serialize(replay29.request)) == sampledBytes, "a version 29 request read a reversed-order blend code or misread the fields after the quad sample masks"); auto version28 = sampledBytes; + version28.pop_back(); version28.erase(version28.begin() + static_cast(maskOffset + 1u), version28.begin() + static_cast(maskOffset + 1u + sampleMaskBytes + 4u)); version28[4] = 28u; const auto replay28 = serializer.Deserialize(encodeBase64(version28)); require(replay28.request.context.pixel.has_value() && replay28.request.context.pixel->quadPixelMask == 0x2u && replay28.request.context.pixel->quadSampleMask == std::array{0xffffu, 0xffffu, 0xffffu, 0xffffu} && replay28.request.context.pixel->reversedBlend == 0u && decodeBase64(serializer.Serialize(replay28.request)) == maskedBytes, "a version 28 request read quad sample masks or a reversed-order blend code, or misread the fields after them"); auto version27 = maskedBytes; + version27.pop_back(); version27.erase(version27.begin() + static_cast(maskOffset), version27.begin() + static_cast(maskOffset + 1u + sampleMaskBytes + 4u)); version27[4] = 27u; const auto replay27 = serializer.Deserialize(encodeBase64(version27)); require(replay27.request.context.pixel.has_value() && replay27.request.context.pixel->quadPixelMask == 0xfu && replay27.request.context.pixel->reversedBlend == 0u && serializer.Serialize(replay27.request) == encodedPixel, "a version 27 request read a quad pixel mask or reversed-order blend code, or misread the fields after it"); auto legacyPixel = plainPixel; + legacyPixel.pop_back(); legacyPixel.erase(legacyPixel.begin() + static_cast(allBitsOffset), legacyPixel.begin() + static_cast(allBitsOffset + 2u + sampleMaskBytes + 4u)); legacyPixel[4] = 26u; const auto replay26 = serializer.Deserialize(encodeBase64(legacyPixel)); diff --git a/core/shader/recompiler/CacheKey.hpp b/core/shader/recompiler/CacheKey.hpp index 033a416692..753a7afbdc 100644 --- a/core/shader/recompiler/CacheKey.hpp +++ b/core/shader/recompiler/CacheKey.hpp @@ -261,6 +261,7 @@ private: append(key, value.registrationWordLimit); append(key, value.vertexSubgroups); append(key, value.float16DenormPreserve); + append(key, value.spannedWaves); } }; diff --git a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp index 3bee798ecb..b43d75c3c9 100644 --- a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp +++ b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp @@ -778,7 +778,7 @@ std::string RequestSerializer::Serialize(const RecompileRequest& request) const std::string buffer; Writer writer(buffer); writer.WriteU32(0x41505335u); - writer.WriteU32(31u); + writer.WriteU32(32u); writeShaderBinary(writer, request.shader); writeGuestContext(writer, request.context); writeSpirvTarget(writer, request.target); @@ -803,6 +803,7 @@ std::string RequestSerializer::Serialize(const RecompileRequest& request) const writer.WriteBool(request.target.vertexSubgroups); writer.WriteBool(request.target.float16DenormPreserve); writer.WriteU32Span(request.context.linkedCallFallbacks); + writer.WriteBool(request.target.spannedWaves); return base64Encode(buffer); } @@ -811,7 +812,7 @@ DeserializedRequest RequestSerializer::Deserialize(std::string_view text) const Reader reader(decoded); if (reader.ReadU32() != 0x41505335u) throw std::runtime_error("invalid recompile request signature"); const auto version = reader.ReadU32(); - if (version < 1u || version > 31u) throw std::runtime_error("unsupported recompile request serialization version"); + if (version < 1u || version > 32u) throw std::runtime_error("unsupported recompile request serialization version"); DeserializedRequest result{}; result.request.shader = readShaderBinary(reader, result.shaderCode, result.shaderHeader); result.request.context = readGuestContext(reader, result, version); @@ -847,6 +848,7 @@ DeserializedRequest RequestSerializer::Deserialize(std::string_view text) const result.linkedCallFallbacks = reader.ReadU32Vector(); result.request.context.linkedCallFallbacks = result.linkedCallFallbacks; } + if (version >= 32u) result.request.target.spannedWaves = reader.ReadBool(); if (version >= 21u && reader.Remaining() != 0u) throw std::runtime_error("RequestSerializer: unexpected trailing data"); return result; } diff --git a/core/shader/recompiler/Recompiler.cpp b/core/shader/recompiler/Recompiler.cpp index 1c74173ebe..e5293b5683 100644 --- a/core/shader/recompiler/Recompiler.cpp +++ b/core/shader/recompiler/Recompiler.cpp @@ -524,16 +524,15 @@ CompiledVariant emitVariant(const RecompileRequest& request, IrProgram& program) targetOptions.registrationModules = registration; targetOptions.fragmentDescriptorSet = request.target.registrationModules; targetOptions.float16DenormPreserve = request.target.float16DenormPreserve; - static const bool spannedWaves = std::getenv("APS5_SPANNED_WAVE") != nullptr; - targetOptions.spannedWaves = spannedWaves; + targetOptions.spannedWaves = request.target.spannedWaves; constexpr SpirvEmitter spirvEmitter; CompiledShaderArtifact result; result.variantId = nextVariantId(); + SpirvEmitInfo emitted; try { - result.spirv = spirvEmitter.Emit(program, inputInfo, bindings, targetOptions); - } catch (const std::exception& error) { - if (!targetOptions.spannedWaves) throw; + result.spirv = spirvEmitter.Emit(program, inputInfo, bindings, targetOptions, &emitted); + } catch (const SpannedWaveUnsupported& error) { std::fprintf(stderr, "[spanned-wave] 0x%llx falls back to two lanes per invocation: %s\n", static_cast(request.shader.codeAddress), error.what()); targetOptions.spannedWaves = false; result.spirv = spirvEmitter.Emit(program, inputInfo, bindings, targetOptions); @@ -558,6 +557,7 @@ CompiledVariant emitVariant(const RecompileRequest& request, IrProgram& program) result.imageMetadataDword = bindings.layout.ImageMetadataDword(); result.runtimeImageCount = bindings.layout.runtimeImageCount; result.hostSubgroupSize = HostSubgroupSize(request); + result.fullSubgroups = emitted.fullSubgroups; result.vertexOffsetSgpr = program.Info().vertexOffsetSgpr; result.instanceOffsetSgpr = program.Info().instanceOffsetSgpr; result.vertexOffsetShared = program.Info().vertexOffsetShared; @@ -874,6 +874,7 @@ RecompileResult materializeResult(const CompiledVariant& variant, const Recompil .vertexOffsetConflict = artifact.vertexOffsetConflict, .instanceOffsetConflict = artifact.instanceOffsetConflict, .hostSubgroupSize = artifact.hostSubgroupSize, + .fullSubgroups = artifact.fullSubgroups, .parameterExports = artifact.parameterExports, .fragmentParameters = artifact.fragmentParameters, .barycentricEmulation = artifact.barycentricEmulation, diff --git a/core/shader/recompiler/Recompiler.hpp b/core/shader/recompiler/Recompiler.hpp index a750763184..123c488f40 100644 --- a/core/shader/recompiler/Recompiler.hpp +++ b/core/shader/recompiler/Recompiler.hpp @@ -267,6 +267,7 @@ struct SpirvTarget { std::uint32_t registrationWordLimit = 200000u; bool vertexSubgroups = true; bool float16DenormPreserve = false; + bool spannedWaves = false; }; struct BindingLayout { @@ -603,6 +604,7 @@ struct CompiledShaderArtifact { bool vertexOffsetConflict = false; bool instanceOffsetConflict = false; std::uint32_t hostSubgroupSize = 0; + bool fullSubgroups = false; std::vector parameterExports; std::vector fragmentParameters; BarycentricEmulation barycentricEmulation; @@ -657,7 +659,7 @@ struct RecompileResult : CompiledShaderArtifact, ShaderInvocation { }; mix(specialization.size()); for (const auto& constant : specialization) mix(constant.id | (static_cast(constant.value) << 32u)); - mix(hostSubgroupSize | (static_cast(workgroupMemoryDwords) << 32u)); + mix(hostSubgroupSize | (fullSubgroups ? 0x10000u : 0u) | (static_cast(workgroupMemoryDwords) << 32u)); mix(bindings.size()); for (const auto& binding : bindings) mix(binding.descriptorSet | (static_cast(binding.binding) << 16u) | (static_cast(binding.kind) << 40u) | (static_cast(binding.count) << 48u)); if (identity.Empty()) identity.low = 1; diff --git a/core/shader/recompiler/ShaderDiskCache.cpp b/core/shader/recompiler/ShaderDiskCache.cpp index 3c288e94b6..2e1cdbbfcf 100644 --- a/core/shader/recompiler/ShaderDiskCache.cpp +++ b/core/shader/recompiler/ShaderDiskCache.cpp @@ -84,9 +84,9 @@ std::filesystem::path ShaderCacheDirectory() { namespace ShaderRecompiler::ShaderDiskCache { #if defined(__linux__) && defined(__x86_64__) && defined(__GLIBCXX__) -static_assert(sizeof(CompiledShaderArtifact) == 232, "CompiledShaderArtifact changed: update the artifact encoder"); +static_assert(sizeof(CompiledShaderArtifact) == 240, "CompiledShaderArtifact changed: update the artifact encoder"); static_assert(sizeof(ShaderInvocation) == 200, "ShaderInvocation changed: update the invocation encoder"); -static_assert(sizeof(RecompileResult) == 464, "RecompileResult changed: update EncodeResult and DecodeResult"); +static_assert(sizeof(RecompileResult) == 472, "RecompileResult changed: update EncodeResult and DecodeResult"); static_assert(sizeof(DescriptorBinding) == 592, "DescriptorBinding changed: update the binding encoder"); static_assert(sizeof(VertexAttribute) == 32, "VertexAttribute changed: update the attribute encoder"); static_assert(sizeof(VertexInput) == 16, "VertexInput changed: update the vertex input encoder"); @@ -486,6 +486,7 @@ void encodeArtifact(Writer& writer, const CompiledShaderArtifact& result) { writer.Value(result.vertexOffsetConflict); writer.Value(result.instanceOffsetConflict); writer.Value(result.hostSubgroupSize); + writer.Value(result.fullSubgroups); writer.Values(std::span(result.parameterExports)); writer.List(result.fragmentParameters, [](Writer& out, const FragmentParameter& parameter) { out.Value(parameter.location); @@ -532,6 +533,7 @@ void decodeArtifact(Reader& reader, CompiledShaderArtifact& result) { reader.Value(result.vertexOffsetConflict); reader.Value(result.instanceOffsetConflict); reader.Value(result.hostSubgroupSize); + reader.Value(result.fullSubgroups); reader.Values(result.parameterExports); reader.List(result.fragmentParameters, 10, [](Reader& in, FragmentParameter& parameter) { in.Value(parameter.location); diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitter.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitter.hpp index 3736835859..e4a861594f 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitter.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitter.hpp @@ -5,6 +5,7 @@ #include "Optimization/BindingAllocator.hpp" #include "Optimization/ShaderStageInputInfo.hpp" #include +#include #include #include #include @@ -27,10 +28,18 @@ struct SpirvTargetOptions { bool spannedWaves = false; }; +struct SpirvEmitInfo { + bool fullSubgroups = false; +}; + +struct SpannedWaveUnsupported : std::runtime_error { + using std::runtime_error::runtime_error; +}; + class SpirvEmitter { public: [[nodiscard]] std::vector Emit(const IrProgram& program, const CompiledBindingLayout& bindings, const SpirvTargetOptions& target) const; - [[nodiscard]] std::vector Emit(const IrProgram& program, const ShaderStageInputInfo& inputInfo, const CompiledBindingLayout& bindings, const SpirvTargetOptions& target) const; + [[nodiscard]] std::vector Emit(const IrProgram& program, const ShaderStageInputInfo& inputInfo, const CompiledBindingLayout& bindings, const SpirvTargetOptions& target, SpirvEmitInfo* info = nullptr) const; }; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp index 53273b6efd..0d629adaca 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp @@ -240,7 +240,7 @@ std::vector SpirvEmitter::Emit(const IrProgram& program, const Co return Emit(program, ShaderStageInputInfo {}, bindings, target); } -std::vector SpirvEmitter::Emit(const IrProgram& program, const ShaderStageInputInfo& inputInfo, const CompiledBindingLayout& bindings, const SpirvTargetOptions& target) const { +std::vector SpirvEmitter::Emit(const IrProgram& program, const ShaderStageInputInfo& inputInfo, const CompiledBindingLayout& bindings, const SpirvTargetOptions& target, SpirvEmitInfo* info) const { if (program.Resources().stage != IrShaderStage::Compute && program.Resources().stage != IrShaderStage::Vertex && program.Resources().stage != IrShaderStage::Pixel && program.Resources().stage != IrShaderStage::Mesh && program.Resources().stage != IrShaderStage::Local && program.Resources().stage != IrShaderStage::TessellationControl && program.Resources().stage != IrShaderStage::TessellationEvaluation) { FailProgram(program, "binary SPIR-V emitter received an unsupported shader stage"); } @@ -293,6 +293,7 @@ std::vector SpirvEmitter::Emit(const IrProgram& program, const Sh EmitModuleHeader(state, bindings); EmitProgram(state); state.module.EmitEntryPoint(ExecutionModelForStage(state.program.Resources().stage), state.mainFunc, "main", state.interfaceVariables); + if (info != nullptr) info->fullSubgroups = state.spannedWave; return state.module.Finalize(); } diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvSubgroup.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvSubgroup.cpp index f65a284557..055881c70e 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvSubgroup.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvSubgroup.cpp @@ -66,7 +66,7 @@ namespace ShaderRecompiler void SpannedShuffle(SpirvEmitterState& state, std::uint32_t type, std::uint32_t result, std::uint32_t value, std::uint32_t lane) { const bool boolean = type == TypeBool(state); - if (!boolean && type != TypeU32(state) && type != TypeF32(state)) FailEmit("a spanned wave shuffles only 32-bit and boolean values"); + if (!boolean && type != TypeU32(state) && type != TypeF32(state)) throw SpannedWaveUnsupported("a spanned wave shuffles only 32-bit and boolean values"); std::uint32_t word = value; if (boolean) word = Select(state, TypeU32(state), value, ConstantU32(state, 1u), ConstantU32(state, 0u)); else if (type != TypeU32(state)) word = Unary(state, spv::OpBitcast, TypeU32(state), value); diff --git a/core/shader/recompiler/tests/RequestSerializerTests.cpp b/core/shader/recompiler/tests/RequestSerializerTests.cpp index 54b249f255..f30aae1501 100644 --- a/core/shader/recompiler/tests/RequestSerializerTests.cpp +++ b/core/shader/recompiler/tests/RequestSerializerTests.cpp @@ -206,11 +206,11 @@ int main() { Require(request.graphics->draw.indexAddress == 0xabcdef3400u && request.graphics->draw.indexCount == 57u && request.graphics->draw.indexElementBytes == 4u && request.graphics->draw.instanceCount == 9u); Require(request.target.float16DenormPreserve == preserve); auto upgraded = bytes; - upgraded[4] = 31u; + upgraded[4] = 32u; if (fixture.pixel) upgraded.insert(upgraded.begin() + 258u, {0u, 0u, 0u}); if (fixture.pixel) InsertPixelDefaultsAfterFrontFace(upgraded); if (fixture.tessellation) upgraded.insert(upgraded.end() - 35u, 5u, 0u); - upgraded.insert(upgraded.end(), sizeof(std::uint64_t), 0u); + upgraded.insert(upgraded.end(), sizeof(std::uint64_t) + 1u, 0u); Require(serializer.Serialize(request) == EncodeBase64(upgraded)); auto truncated = bytes; truncated.pop_back(); @@ -245,11 +245,11 @@ int main() { layerRejected = std::string_view(error.what()) == "RequestSerializer: truncated data"; } Require(layerRejected); - layered[4] = 31u; + layered[4] = 32u; layered.insert(layered.begin() + 259u, {0u, 0u}); InsertPixelDefaultsAfterFrontFace(layered); if (fixture.tessellation) layered.insert(layered.end() - 35u, 5u, 0u); - layered.insert(layered.end(), sizeof(std::uint64_t), 0u); + layered.insert(layered.end(), sizeof(std::uint64_t) + 1u, 0u); Require(serializer.Serialize(layerReplay.request) == EncodeBase64(layered)); } } @@ -270,10 +270,10 @@ int main() { Require(replay.request.context.pixel->rasterPrimitive == ShaderRecompiler::RasterPrimitive::Rectangle); Require(replay.request.context.vertex->paClVsOutCntl == 0x00050a05u); Require(replay.request.target.float16DenormPreserve == preserve); - bytes[4] = 31u; + bytes[4] = 32u; bytes.insert(bytes.begin() + 259u, {0u, 0u}); InsertPixelDefaultsAfterFrontFace(bytes); - bytes.insert(bytes.end(), sizeof(std::uint64_t), 0u); + bytes.insert(bytes.end(), sizeof(std::uint64_t) + 1u, 0u); Require(serializer.Serialize(replay.request) == EncodeBase64(bytes)); auto truncated23 = DecodeBase64(encoded23); @@ -394,10 +394,10 @@ int main() { Require(request.graphics->draw.indexAddress == 0xabcdef3400u && request.graphics->draw.indexCount == 57u && request.graphics->draw.indexElementBytes == 4u && request.graphics->draw.instanceCount == 9u); Require(request.target.float16DenormPreserve == preserve); auto upgraded = bytes; - upgraded[4] = 31u; + upgraded[4] = 32u; if (fixture.pixel) upgraded.insert(upgraded.begin() + 259u, {0u, 0u}); if (fixture.pixel) InsertPixelDefaultsAfterFrontFace(upgraded); - upgraded.insert(upgraded.end(), sizeof(std::uint64_t), 0u); + upgraded.insert(upgraded.end(), sizeof(std::uint64_t) + 1u, 0u); Require(serializer.Serialize(request) == EncodeBase64(upgraded)); for (const bool dither : {false, true}) { if (!fixture.pixel && dither) continue; @@ -407,12 +407,12 @@ int main() { const auto replay25 = serializer.Deserialize(EncodeBase64(main25)); Require(!fixture.pixel || (replay25.request.context.pixel->layerExported == layer && replay25.request.context.pixel->ditheredAlphaToCoverage == dither && replay25.request.context.pixel->reversedBlend == 0u)); Require(replay25.request.target.float16DenormPreserve == preserve && (!fixture.tessellation || replay25.request.graphics->tessellation->primitiveGroupSize == 37u)); - main25[4] = 31u; + main25[4] = 32u; if (fixture.pixel) { main25.insert(main25.begin() + 260u, 0u); InsertPixelDefaultsAfterFrontFace(main25); } - main25.insert(main25.end(), sizeof(std::uint64_t), 0u); + main25.insert(main25.end(), sizeof(std::uint64_t) + 1u, 0u); Require(serializer.Serialize(replay25.request) == EncodeBase64(main25)); } } @@ -426,9 +426,9 @@ int main() { const auto replay = serializer.Deserialize(encoded); Require(replay.request.target.float16DenormPreserve == preserve); auto bytes = DecodeBase64(encoded); - Require(bytes.size() >= 8u && bytes[4] == 31u && bytes[5] == 0u && bytes[6] == 0u && bytes[7] == 0u); + Require(bytes.size() >= 8u && bytes[4] == 32u && bytes[5] == 0u && bytes[6] == 0u && bytes[7] == 0u); auto future = bytes; - future[4] = 32u; + future[4] = 33u; bool futureRejected = false; try { static_cast(serializer.Deserialize(EncodeBase64(future))); @@ -446,5 +446,10 @@ int main() { Require(rejected); } + for (const bool spanned : {false, true}) { + request.target.spannedWaves = spanned; + Require(serializer.Deserialize(serializer.Serialize(request)).request.target.spannedWaves == spanned); + } + return 0; } From 641e53b5f3f9d4cf37bf34cbd6e0d56d53c17bf4 Mon Sep 17 00:00:00 2001 From: Alex Collini Date: Fri, 16 Oct 2026 14:12:47 +0200 Subject: [PATCH 0503/1000] test(agc): run the LDS mask waterfall in both wave modes The test set neither mode, so ctest ran only the two-lane form. It now recompiles each program two-lane and then spanned through SpirvTarget::spannedWaves, checks that the spanned module asks for full subgroups, and skips the spanned pass on a device without full 32-lane compute subgroups. Co-Authored-By: Claude Opus 5.5 --- .../tests/execution/LdsMaskWaterfall.cpp | 33 +++++++++++-------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/LdsMaskWaterfall.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/LdsMaskWaterfall.cpp index 15135e5e90..eb4f2499b6 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/LdsMaskWaterfall.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/LdsMaskWaterfall.cpp @@ -43,7 +43,7 @@ std::array BufferDescriptor(const void* data, std::uint32_t re return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | ((Stride * 4u) << 16u), records, 0x11016facu}; } -void Run(AgcDriver::VulkanDevice& device, std::span code, std::uint64_t exec) { +void Run(AgcDriver::VulkanDevice& device, std::span code, std::uint64_t exec, bool spanned) { Output.fill(0xdeadbeefu); std::vector userData(8, 0u); userData[0] = static_cast(exec); @@ -59,13 +59,15 @@ void Run(AgcDriver::VulkanDevice& device, std::span code, s device.Target(), {0, 0, 0, 128} }; + request.target.spannedWaves = spanned; request.useCache = false; const auto result = ShaderRecompiler::Recompile(request); + Require(result.fullSubgroups == spanned, spanned ? "the program did not span two host subgroups" : "the program spans host subgroups without APS5_SPANNED_WAVE"); device.Dispatch(result, 1, 1, 1, {}, address); device.WaitIdle(); } -void Check(std::uint64_t exec) { +void Check(std::uint64_t exec, bool spanned) { std::array slots{}; for (std::uint32_t lane = 0; lane < Lanes; ++lane) slots[(lane * 5u) & (Slots - 1u)] |= std::uint64_t{1} << lane; std::uint32_t iterations = 0; @@ -77,7 +79,7 @@ void Check(std::uint64_t exec) { } } char name[64]; - std::snprintf(name, sizeof(name), "exec 0x%016llx", static_cast(exec)); + std::snprintf(name, sizeof(name), "%s, exec 0x%016llx", spanned ? "spanned" : "two-lane", static_cast(exec)); for (std::uint32_t lane = 0; lane < Lanes; ++lane) { const bool active = ((exec >> lane) & 1u) != 0; const std::array expected{active ? iterations : 0u, active ? indexSum : 0u, iterations, static_cast(slots[lane])}; @@ -90,13 +92,13 @@ void Check(std::uint64_t exec) { } } -void CheckMaskedOr(std::uint64_t exec) { +void CheckMaskedOr(std::uint64_t exec, bool spanned) { std::array slots{}; for (std::uint32_t lane = 0; lane < Lanes; ++lane) { if (((exec >> lane) & 1u) != 0) slots[lane & (Slots - 1u)] |= std::uint64_t{1} << lane; } char name[64]; - std::snprintf(name, sizeof(name), "masked ds_or_b64, exec 0x%016llx", static_cast(exec)); + std::snprintf(name, sizeof(name), "%s masked ds_or_b64, exec 0x%016llx", spanned ? "spanned" : "two-lane", static_cast(exec)); for (std::uint32_t lane = 0; lane < Lanes; ++lane) { const std::array expected{static_cast(slots[lane]), static_cast(slots[lane] >> 32u)}; for (std::uint32_t word = 0; word < expected.size(); ++word) { @@ -116,15 +118,20 @@ int main() { std::printf("skipped, subgroup size %u cannot hold a wave32\n", device->Target().subgroupSize); return VulkanTestSkipped; } - for (const std::uint64_t exec : {~std::uint64_t{0}, std::uint64_t{0xaaaaaaaaaaaaaaaa}, std::uint64_t{0xffff000000000000}, std::uint64_t{0x000000000000ffff}, std::uint64_t{1} << 63u, std::uint64_t{1}, std::uint64_t{0x0000000100000000}}) { - Run(*device, WaterfallCode, exec); - Check(exec); + const bool spannable = device->Target().subgroupSize == 32u && device->ComputeFullSubgroups(); + if (!spannable) std::puts("spanned mode skipped: the device has no full 32-lane compute subgroups"); + for (const bool spanned : {false, true}) { + if (spanned && !spannable) continue; + for (const std::uint64_t exec : {~std::uint64_t{0}, std::uint64_t{0xaaaaaaaaaaaaaaaa}, std::uint64_t{0xffff000000000000}, std::uint64_t{0x000000000000ffff}, std::uint64_t{1} << 63u, std::uint64_t{1}, std::uint64_t{0x0000000100000000}}) { + Run(*device, WaterfallCode, exec, spanned); + Check(exec, spanned); + } + for (const std::uint64_t exec : {~std::uint64_t{0}, std::uint64_t{0x00000000ffffffff}, std::uint64_t{0xffffffff00000000}, std::uint64_t{1} << 40u, std::uint64_t{0x8000000000000001}}) { + Run(*device, MaskedOrCode, exec, spanned); + CheckMaskedOr(exec, spanned); + } } - for (const std::uint64_t exec : {~std::uint64_t{0}, std::uint64_t{0x00000000ffffffff}, std::uint64_t{0xffffffff00000000}, std::uint64_t{1} << 40u, std::uint64_t{0x8000000000000001}}) { - Run(*device, MaskedOrCode, exec); - CheckMaskedOr(exec); - } - std::puts("LDS mask waterfall tests passed"); + std::printf("LDS mask waterfall tests passed (two-lane%s)\n", spannable ? " and spanned" : ""); return 0; } catch (const std::exception& error) { std::cerr << error.what() << '\n'; From 5fb23f78deb12c1363266ffabfcf1c5186316ec1 Mon Sep 17 00:00:00 2001 From: Alex Collini Date: Fri, 16 Oct 2026 14:12:47 +0200 Subject: [PATCH 0504/1000] docs(debt): record how spanned barriers are checked and the full-subgroup rule Co-Authored-By: Claude Opus 5.5 --- docs/dev/debt/shader.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index 2aead77580..eae1416dbb 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -146,4 +146,4 @@ - [SDWA source selects on `v_cvt_f32_ubyte1`, `v_cvt_f32_ubyte2` and `v_cvt_f32_ubyte3`](../../../core/shader/recompiler/RdnaDecoder/src/RdnaVectorOpDecoder.cpp) (shader recompiler) - byte N is taken from source 0 after `SRC0_SEL` and `SRC0_SEXT` select and extend it, since SDWA supplies source 0 (RDNA 2 ISA guide 6.8 and 13.3.7) and the opcode reads `S0.u[8N+7:8N]` (12.8). Not measured on hardware. - [Flag structurization](../../../core/shader/recompiler/ControlFlow/src/Structurizer.cpp) (shader recompiler) - a graph that every structurizer attempt refuses, irreducible and non-terminating graphs included, is rebuilt with one reach flag per block and one again flag per loop (`structurizeWithFlags`): each block keeps one copy and runs behind a guard on its flag, and each strongly connected component becomes a loop whose single latch repeats it while a back edge set its flag. The output has about 8 times the input's blocks and one route variable per block, and ends in an empty Return that never runs. Only blocks that start the top-level order unconditionally run unguarded, so resource planning also sees the static paths that skip a guarded block and can plan extra descriptor candidates for them. The attempts still run first. Shared merge splitting refuses an attempt once the graph has grown past 16 times its block count at the start of splitting (at least 64 blocks); succeeding attempts grew at most 10.6 times on random graphs of up to 128 blocks, and an attempt that would succeed only beyond the budget leaves the graph to a later attempt or to this construction. Splitting that adds one block per round stays within that budget until its 4 rounds per block run out, so the attempts can still take more than 10 s (9 of 2,000 random graphs of up to 128 blocks, 63 s at most). `APS5_FORCE_STRUCTURIZER_FALLBACK=1` builds every graph this way, for tests - [Scalar buffer descriptors](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) use only the base address, stride and record count for resources accessed exclusively by scalar loads; their fourth word is normalized to zero, following AMD RDNA2 ISA section 7.2.2. Vector and mixed accesses retain descriptor validation. Native Vulkan readbacks cover ignored fourth-word fields on Windows/NVIDIA; their console behavior has not been measured. -- [Spanned wave64 compute](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvSubgroup.cpp) (shader recompiler) - `APS5_SPANNED_WAVE=1`, off by default, runs a single-wave wave64 compute program at one lane per invocation on a 32-lane host subgroup and exchanges ballots, shuffles and reductions between the two host subgroups through workgroup memory and barriers. Those barriers are correct only where every invocation of the workgroup reaches them, which holds for the guest's wave-uniform control flow. The emulated 64-bit LDS atomics ([LockedLdsUpdate](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp)) run only on the lanes the exec mask keeps, so their lock loop ballots within its own host subgroup, as its elect already does. Any other cross-lane exchange emitted inside per-lane control flow would hang a spanned wave in the same way. In Ghost of Yōtei's program 0x80003af600 (PPSA26344), the lock loop was the only place where barriers sat inside per-invocation conditionals (3 of 111, found by scanning the SPIR-V). Other programs and titles have not been scanned. Measured on an RTX 4090 (Windows): an offline replay of that program and two Yōtei runs that previously lost the device (NVIDIA TDR) now run past it, and [LdsMaskWaterfall.cpp](../../../core/libs/prx/libSceAgcDriver/tests/execution/LdsMaskWaterfall.cpp) covers a half-inactive wave. Stray (PPSA02101) on Intel ANV and a Demon's Souls (PPSA01342) request replay showed no fault, before this fix +- [Spanned wave64 compute](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvSubgroup.cpp) (shader recompiler) - `APS5_SPANNED_WAVE=1`, off by default, runs a single-wave wave64 compute program at one lane per invocation on a 32-lane host subgroup and exchanges ballots, shuffles and reductions between the two host subgroups through workgroup memory and barriers. The driver turns it on only where compute subgroups can be pinned at 32 full lanes (`subgroupSizeControl` and `computeFullSubgroups`), passes it as `SpirvTarget::spannedWaves` (part of the cache key and the serialized request), and creates the pipelines of spanned programs with `REQUIRE_FULL_SUBGROUPS`, since the lane number `SubgroupId * 32 + SubgroupLocalInvocationId` needs two full subgroups. Those barriers are correct only where every invocation of the workgroup reaches them, which holds for the guest's wave-uniform control flow. The emulated 64-bit LDS atomics ([LockedLdsUpdate](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp)) run only on the lanes the exec mask keeps, so their lock loop ballots within its own host subgroup, as its elect already does. Any other cross-lane exchange emitted inside per-lane control flow would hang a spanned wave in the same way, and the emitter does not reject one: there is no uniformity check. The only check so far is manual: disassemble the module (`spirv-dis`), list the selection and loop constructs that enclose each `OpControlBarrier`, and read the condition of each enclosing selection. In Ghost of Yōtei's program 0x80003af600 (PPSA26344), 108 of 111 barriers sat only in loops on wave-uniform conditions and 3 inside per-invocation conditionals, all in the lock loop. Other programs and titles have not been scanned. An earlier, cheaper ballot (one barrier instead of three) also lost the device for a reason that is not understood, which may be another barrier placement problem. Measured on an RTX 4090 (Windows): an offline replay of that program and two Yōtei runs that previously lost the device (NVIDIA TDR) now run past it, and [LdsMaskWaterfall.cpp](../../../core/libs/prx/libSceAgcDriver/tests/execution/LdsMaskWaterfall.cpp) runs both modes and covers a half-inactive wave. Stray (PPSA02101) on Intel ANV and a Demon's Souls (PPSA01342) request replay showed no fault, before this fix From 2a15be09c91d95ba69186abe2b05b943122e7c9d Mon Sep 17 00:00:00 2001 From: AlxcBTW <187763517+AlxcBTW@users.noreply.github.com> Date: Fri, 16 Oct 2026 00:32:20 +0200 Subject: [PATCH 0505/1000] perf(agc): pre-sync the DCC key writers of a dispatch's images before the GPU mutex The stage-A pre-sync of a dispatch listed only the surfaces that stage B reads on the CPU. The DCC keys of an image that a recorded kernel writes were not in the list, so stage B waited for that kernel in WaitForKeyWriters with the GPU mutex held and every other worker queued behind the wait. At the Minecraft Dungeons II title the pre-sync fired once in 296,000 dispatch checks while WaitForKeyWriters synchronized 400 times per 10 s. PresyncSurfaces now also returns the key range of every image binding (PresyncSurface::dccKeys). presync waits for a key range only while recorded work writes it, the key-store memo does not already give its value (DccKeyWritersPending) and, under the mutex it takes for a moment anyway, the pending stores do not give the keys either (DccKeyWriterNeedsSync, the test WaitForKeyWriters makes before it synchronizes). Which keys the dispatch reads, and when, is unchanged: the locked lookup runs as before and finds nothing pending. APS5_NO_DCC_PRESYNC=1 leaves the key ranges out. The [presync] line counts key ranges checked, pre-syncs that only the keys required and the outcomes of WaitForKeyWriters. The test agc_driver_dcc_key_presync covers a kernel that writes the keys, a recorded fill of the keys and nothing pending, and skips with the reasons check_skips.py expects when the device has no timeline semaphores or no host import. --- .../Execution/include/Recipe.hpp | 2 +- .../Execution/include/VulkanDevice.hpp | 6 +- .../Execution/src/VulkanDevice.cpp | 29 +- .../Graphics/include/DccMetadata.hpp | 10 + .../Graphics/include/ShaderResources.hpp | 8 +- .../Graphics/src/DccMetadata.cpp | 53 +++- .../Graphics/src/ShaderResources.cpp | 25 +- .../tests/execution/DccKeyPresync.cpp | 283 ++++++++++++++++++ docs/dev/ARCHITECTURE.md | 1 + 9 files changed, 401 insertions(+), 16 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/DccKeyPresync.cpp diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Recipe.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Recipe.hpp index 9a4dc16db6..9ba265842a 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Recipe.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Recipe.hpp @@ -47,7 +47,7 @@ struct Recipe { std::uint64_t dataWordsHash = 0; // The template's PresyncSurfaces() as of the attach and the host-import identity they were // computed under (a surface is CPU-read only while its memory has no import). - std::vector> presyncSurfaces; + std::vector presyncSurfaces; Graphics::HostImportsProof presyncProof; // Both false for every recipe (only reusable objects get one); kept for the asserts. bool needsCompletion = false; diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp index 08de3994f0..10258859ac 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp @@ -243,6 +243,10 @@ public: double gpuBusyMs, gpuGapMs, lastSubmitAfterFlipMs, blitSubmitAfterFlipMs; }; static PresentStatistics PresentCounts(); + struct PresyncStatistics { + std::uint64_t checked, presyncs, keyChecks, keyWaits; + }; + static PresyncStatistics PresyncCounts(); // Stage A of a dispatch's resource build, run WITHOUT GuestMemory::GpuMutex before Dispatch or // DispatchIndirect: the binding plan, data buffers, imports already serving the guest buffers, // read-only copies and the descriptor set (see ShaderResources). The dispatch completes it @@ -335,7 +339,7 @@ private: // Body of Dispatch and DispatchIndirect: `arguments` 0 dispatches x, y, z groups. IndirectOutcome dispatch(const ShaderRecompiler::RecompileResult& shader, std::uint32_t x, std::uint32_t y, std::uint32_t z, std::uint64_t arguments, std::span snapshots, std::uint64_t programAddress, std::shared_ptr prepared, std::shared_ptr* recipe); // The stage-A pre-sync over `surfaces` (see PrepareDispatch): the serial waited for, 0 none. - std::uint64_t presync(std::span> surfaces); + std::uint64_t presync(std::span surfaces); std::shared_ptr computePipeline(const Graphics::Context& context, const ShaderRecompiler::RecompileResult& shader, VkDescriptorSetLayout setLayout, std::span layoutKey, VkShaderStageFlags pushStages, std::uint64_t programAddress); // A DISPATCH_INDIRECT's argument path (see DispatchIndirect): GPU-side from the host import, or // the counts read on the CPU (the record then continues as a direct dispatch). diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index c2ebf9a5ac..0bfbc459da 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -3751,6 +3751,8 @@ struct PresyncCounters { std::atomic checked{0}; std::atomic presyncs{0}; std::atomic waitedUs{0}; + std::atomic keyChecks{0}; + std::atomic keyWaits{0}; std::atomic lastReport{0}; }; @@ -4133,23 +4135,31 @@ RecipeOutcome VulkanDevice::DrawFromRecipe(const Graphics::State& graphics, cons return RecipeOutcome::Recorded; } -std::uint64_t VulkanDevice::presync(std::span> surfaces) { +std::uint64_t VulkanDevice::presync(std::span surfaces) { static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; auto& counters = Presyncs(); bool overlaps = false; - for (const auto& [address, bytes] : surfaces) overlaps = overlaps || Graphics::Recorder::SnapshotWriteOverlaps(address, static_cast(bytes)); + for (const auto& surface : surfaces) { + const auto bytes = static_cast(surface.bytes); + if (surface.dccKeys) counters.keyChecks.fetch_add(1, std::memory_order_relaxed); + overlaps = overlaps || (surface.dccKeys ? Graphics::DccKeyWritersPending(surface.address, bytes) : Graphics::Recorder::SnapshotWriteOverlaps(surface.address, bytes)); + } std::uint64_t serial = 0; + bool texelsPending = false; if (overlaps) { // No reap here (SubmitAndEpoch's would run completions, which a compute worker must not do // on queue 0's behalf, see SubmitRecorded): the dispatch reaps after the wait. std::lock_guard lock(GuestMemory::GpuMutex()); if (state->recorder) { bool open = false; - for (const auto& [address, bytes] : surfaces) { - const auto info = state->recorder->DescribePendingWrite(address, static_cast(bytes)); + for (const auto& surface : surfaces) { + const auto bytes = static_cast(surface.bytes); + if (surface.dccKeys && !Graphics::DccKeyWriterNeedsSync(surface.address, bytes)) continue; + const auto info = state->recorder->DescribePendingWrite(surface.address, bytes); if (!info.has_value()) continue; open = open || info->open; serial = std::max(serial, info->serial); + texelsPending = texelsPending || !surface.dccKeys; } if (open) serial = state->recorder->SubmitAndEpoch(); } @@ -4158,16 +4168,25 @@ std::uint64_t VulkanDevice::presync(std::span(std::chrono::duration_cast(std::chrono::steady_clock::now() - waitStart).count()), std::memory_order_relaxed); } if (profile) { const auto nowMs = std::chrono::duration_cast(std::chrono::steady_clock::now().time_since_epoch()).count(); auto last = counters.lastReport.load(); - if (nowMs - last >= 10000 && counters.lastReport.compare_exchange_strong(last, nowMs)) AgcDriver::ProfilePrint_nid_no_patch("[presync] %llu dispatches checked, %llu pre-syncs waited %.0f ms (cumulative)\n", static_cast(counters.checked.load()), static_cast(counters.presyncs.load()), counters.waitedUs.load() / 1000.0); + if (nowMs - last >= 10000 && counters.lastReport.compare_exchange_strong(last, nowMs)) { + const auto keyWaitCounts = Graphics::KeyWaitCounts(); + AgcDriver::ProfilePrint_nid_no_patch("[presync] %llu dispatches checked, %llu pre-syncs waited %.0f ms (cumulative); key ranges %llu checked, %llu pre-syncs that only the keys required; WaitForKeyWriters %llu calls: %llu without a pending writer, %llu known keys, %llu fills leaving uncompressed, %llu synced\n", static_cast(counters.checked.load()), static_cast(counters.presyncs.load()), counters.waitedUs.load() / 1000.0, static_cast(counters.keyChecks.load()), static_cast(counters.keyWaits.load()), static_cast(keyWaitCounts.calls), static_cast(keyWaitCounts.noPending), static_cast(keyWaitCounts.known), static_cast(keyWaitCounts.fillsUncompressed), static_cast(keyWaitCounts.synced)); + } } return serial; } +VulkanDevice::PresyncStatistics VulkanDevice::PresyncCounts() { + const auto& counters = Presyncs(); + return {counters.checked.load(std::memory_order_relaxed), counters.presyncs.load(std::memory_order_relaxed), counters.keyChecks.load(std::memory_order_relaxed), counters.keyWaits.load(std::memory_order_relaxed)}; +} + std::shared_ptr VulkanDevice::PrepareDispatch(const ShaderRecompiler::RecompileResult& shader, std::span snapshots, std::uint64_t dispatchThreads, std::uint64_t programAddress) { ShaderRecompiler::RuntimeAbi::RequireVersion(shader.runtimeAbiVersion); // APS5_LOCKED_BUILD=1: the whole build under the mutex, as before the split. diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp index d2e9dff3a6..765d2acde7 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp @@ -99,6 +99,16 @@ DccKeys ProvedCurrentDccKeys(std::uint64_t metaAddress, std::uint64_t surfaceByt void ReadTextureSurface(const GuestTextureResource& resource, DccKeys keys, std::span bytes); void NoteKeysFillOnGpu(std::uint64_t begin, std::size_t count, DccKeys keys); std::optional WaitForKeyWriters(const GuestTextureResource& resource, std::uint64_t guestBytes); +bool DccKeyWritersPending(std::uint64_t metaAddress, std::size_t count); +bool DccKeyWriterNeedsSync(std::uint64_t metaAddress, std::size_t count); +struct DccKeyWaitCounts { + std::uint64_t calls; + std::uint64_t noPending; + std::uint64_t known; + std::uint64_t fillsUncompressed; + std::uint64_t synced; +}; +DccKeyWaitCounts KeyWaitCounts(); } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp index efefda3a7f..305feabe99 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp @@ -122,6 +122,12 @@ struct ImageTableReuseCounts { }; ImageTableReuseCounts ImageTableReuses(); +struct PresyncSurface { + std::uint64_t address; + std::uint64_t bytes; + bool dccKeys; +}; + class ShaderResources { public: ShaderResources(const Context& context, const ShaderRecompiler::RecompileResult& vertex, const ShaderRecompiler::RecompileResult& fragment, const ColorTarget& target, std::uint64_t indexAddress, std::size_t indexBytes); @@ -280,7 +286,7 @@ public: // Usable right after the deferred constructor, for address-based builds too (it decodes the // compiled shader's bindings itself when stage A has not run), and on a completed object from // the resource cache (its Revalidate repeats the same lookups), without the device lock. - std::vector> PresyncSurfaces() const; + std::vector PresyncSurfaces() const; private: friend class AgcDriver::VulkanDevice; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp index f8f4d371ec..98449f5291 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp @@ -761,13 +761,62 @@ bool PendingFillsLeaveUncompressed(const Recorder& recorder, const GuestTextureR } +namespace { + +struct KeyWaitTotals { + std::atomic calls{0}; + std::atomic noPending{0}; + std::atomic known{0}; + std::atomic fillsUncompressed{0}; + std::atomic synced{0}; +}; + +KeyWaitTotals& KeyWaits() { + static KeyWaitTotals totals; + return totals; +} + +} + +DccKeyWaitCounts KeyWaitCounts() { + const auto& totals = KeyWaits(); + return {totals.calls.load(std::memory_order_relaxed), totals.noPending.load(std::memory_order_relaxed), totals.known.load(std::memory_order_relaxed), totals.fillsUncompressed.load(std::memory_order_relaxed), totals.synced.load(std::memory_order_relaxed)}; +} + +bool DccKeyWritersPending(std::uint64_t metaAddress, std::size_t count) { + if (metaAddress == 0 || count == 0 || !Recorder::SnapshotWriteOverlaps(metaAddress, count)) return false; + auto& memo = Memo(); + std::lock_guard lock(memo.mutex); + MemoRecorder(memo); + return std::none_of(memo.entries.begin(), memo.entries.end(), [&](const GpuKeyStore& store) { return store.note != 0 && store.begin <= metaAddress && metaAddress + count <= store.end; }); +} + +bool DccKeyWriterNeedsSync(std::uint64_t metaAddress, std::size_t count) { + if (metaAddress == 0 || count == 0 || !GuestMemory::GpuMutex().HeldByThisThread()) return false; + auto* recorder = Recorder::Active(); + return recorder != nullptr && recorder->PendingWriteOverlaps(metaAddress, count) && !PendingStoreKeys(*recorder, metaAddress, count).has_value(); +} + std::optional WaitForKeyWriters(const GuestTextureResource& resource, std::uint64_t guestBytes) { const auto count = static_cast(guestBytes / KeyBytes); const auto metaAddress = resource.dccAddress; if (metaAddress == 0 || count == 0 || !GuestMemory::GpuMutex().HeldByThisThread()) return std::nullopt; + auto& waits = KeyWaits(); + waits.calls.fetch_add(1, std::memory_order_relaxed); auto* recorder = Recorder::Active(); - if (recorder == nullptr || !recorder->PendingWriteOverlaps(metaAddress, count) || PendingStoreKeys(*recorder, metaAddress, count).has_value()) return std::nullopt; - if (PendingFillsLeaveUncompressed(*recorder, resource, count)) return DccKeys::Uncompressed; + if (recorder == nullptr || !recorder->PendingWriteOverlaps(metaAddress, count)) { + waits.noPending.fetch_add(1, std::memory_order_relaxed); + return std::nullopt; + } + if (PendingStoreKeys(*recorder, metaAddress, count).has_value()) { + waits.known.fetch_add(1, std::memory_order_relaxed); + return std::nullopt; + } + if (PendingFillsLeaveUncompressed(*recorder, resource, count)) { + waits.fillsUncompressed.fetch_add(1, std::memory_order_relaxed); + return DccKeys::Uncompressed; + } + waits.synced.fetch_add(1, std::memory_order_relaxed); Recorder::CountSync(2); recorder->SyncThrough(metaAddress, count); return std::nullopt; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index e31084e0c4..a2cba7b942 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -4337,15 +4337,20 @@ void ShaderResources::finishImageTables() { bda->SetImageTables(std::move(faults), std::move(ranges)); } -std::vector> ShaderResources::PresyncSurfaces() const { - std::vector> surfaces; +std::vector ShaderResources::PresyncSurfaces() const { + std::vector surfaces; // A surface's lookup reads guest memory on the CPU unless it is served GPU-direct from a host // import: a storage image of imported memory uploads and refreshes from the import, and a // sampled texture of one views that image (SampledFromStorageEligible), so neither waits for // the producer. const auto consider = [&](std::uint32_t format, std::uint64_t address, std::uint64_t guestBytes, bool sampled) { const bool gpuDirect = sampled ? SampledFromStorageEligible(context, format, address, guestBytes) : HostImportCovers(context, address, static_cast(guestBytes)); - if (!gpuDirect) surfaces.emplace_back(address, guestBytes); + if (!gpuDirect) surfaces.push_back({address, guestBytes, false}); + }; + static const bool keyRanges = std::getenv("APS5_NO_DCC_PRESYNC") == nullptr; + const auto considerKeys = [&](std::uint64_t dccAddress, std::uint64_t guestBytes) { + const auto keyBytes = DccKeyBytes(guestBytes); + if (keyRanges && dccAddress != 0 && keyBytes != 0) surfaces.push_back({dccAddress, keyBytes, true}); }; if (completed) { // A cached object (the resource cache): Revalidate repeats the lookups of the objects the @@ -4361,18 +4366,22 @@ std::vector> ShaderResources::PresyncSur const auto& texture = textures[textureIndex++]; if (texture->ViewsStorageImage()) consider(range.format, range.address, range.bytes, false); else consider(range.format, range.address, range.bytes, true); + considerKeys(range.dccAddress, range.bytes); } } for (std::size_t index = 0; index < storageTextures.size(); ++index) { if (index != 0 && storageTextures[index] == storageTextures[index - 1]) continue; const auto& image = *storageTextures[index]; consider(image.Descriptor().format, image.Descriptor().baseAddress, image.GuestBytes(), false); + considerKeys(image.Descriptor().dccAddress, image.GuestBytes()); } return surfaces; } if (!imageRecords.empty()) { for (const auto& record : imageRecords) { - if (record.decoded) consider(record.resource.format, record.resource.baseAddress, record.guestBytes, record.sampled); + if (!record.decoded) continue; + consider(record.resource.format, record.resource.baseAddress, record.guestBytes, record.sampled); + considerKeys(record.resource.dccAddress, record.guestBytes); } return surfaces; } @@ -4381,14 +4390,18 @@ std::vector> ShaderResources::PresyncSur forEachImageElement(*deferredCompute.program, [&](const ShaderRecompiler::DescriptorBinding& binding, std::uint32_t, std::span words) { if (IsNullTextureResource(words)) return; const auto resource = DecodeTextureResource(words); - consider(resource.format, resource.baseAddress, DescribeSurface(resource).guestBytes, binding.kind == ShaderRecompiler::DescriptorKind::SampledImage); + const auto guestBytes = DescribeSurface(resource).guestBytes; + consider(resource.format, resource.baseAddress, guestBytes, binding.kind == ShaderRecompiler::DescriptorKind::SampledImage); + considerKeys(resource.dccAddress, guestBytes); }); for (const auto& binding : deferredCompute.program->bindings) { if (binding.role != ShaderRecompiler::DescriptorRole::ImageTable) continue; for (std::size_t element = 0; element < binding.guestDescriptor.size() / 8u; ++element) { try { const auto resource = DecodeTextureResource(std::span(binding.guestDescriptor).subspan(element * 8u, 8u)); - consider(resource.format, resource.baseAddress, DescribeSurface(resource).guestBytes, true); + const auto guestBytes = DescribeSurface(resource).guestBytes; + consider(resource.format, resource.baseAddress, guestBytes, true); + considerKeys(resource.dccAddress, guestBytes); } catch (const std::runtime_error&) { } } diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DccKeyPresync.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DccKeyPresync.cpp new file mode 100644 index 0000000000..bed49aed8f --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DccKeyPresync.cpp @@ -0,0 +1,283 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 32; +constexpr std::uint32_t Side = 128; +constexpr std::uint32_t Format8888Unorm = 56; +constexpr std::uint32_t TileRenderTarget64KB = 0x1b; +constexpr std::uint32_t Type2D = 9; +constexpr std::size_t SurfaceBytes = 65536; +constexpr std::size_t KeyBytes = SurfaceBytes / 256; +constexpr std::size_t KeysOffset = SurfaceBytes; +constexpr std::size_t SourceKeysOffset = SurfaceBytes + 4096; +constexpr std::size_t BlockBytes = 2 * 65536; + +alignas(256) constexpr std::array CopyKeysCode{ + 0x34020083, 0xe0341000, 0x80000201, 0xbf8c3f70, 0xe0741000, 0x80010201, 0xbf810000, +}; + +alignas(256) constexpr std::array SampleSecondTexelCode{ + 0x34060084, 0x7e2802ff, 0x3c400000, 0x7e2a02ff, 0x3b800000, 0xf09c0f08, 0x00610a14, 0xbf8c3f70, + 0xe0781000, 0x80000a03, 0xbf810000, +}; + +constexpr std::array PointSampler{0x00000092u, 0x00fff000u, 0u, 0u}; + +std::uint64_t AddressOf(const void* data) { + return static_cast(reinterpret_cast(data)); +} + +class GuestBlock { +public: + GuestBlock() { +#ifdef _WIN32 + block = static_cast(VirtualAlloc(nullptr, BlockBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); +#else + block = static_cast(std::aligned_alloc(65536, BlockBytes)); +#endif + Require(block != nullptr, "DCC key pre-sync: cannot allocate the guest block"); + GuestAllocations::Mutation().Add(block, BlockBytes, true, true, true); + } + + ~GuestBlock() { + GuestAllocations::Mutation().Remove(block); +#ifdef _WIN32 + VirtualFree(block, 0, MEM_RELEASE); +#else + std::free(block); +#endif + } + + GuestBlock(const GuestBlock&) = delete; + GuestBlock& operator=(const GuestBlock&) = delete; + + std::uint8_t* Data() { return block; } + +private: + std::uint8_t* block = nullptr; +}; + +std::array TextureDescriptor(const std::uint8_t* texels, const std::uint8_t* keys) { + const auto address = AddressOf(texels); + const auto meta = AddressOf(keys) >> 8u; + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (Format8888Unorm << 20u) | (((Side - 1u) & 3u) << 30u), + ((Side - 1u) >> 2u) | ((Side - 1u) << 14u), + 0xfacu | (TileRenderTarget64KB << 20u) | (Type2D << 28u), + 0u, + 0u, + (1u << 20u) | (1u << 21u) | (static_cast(meta & 0xffu) << 24u), + static_cast(meta >> 8u), + }; +} + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = AddressOf(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +ShaderRecompiler::RecompileResult Compile(AgcDriver::VulkanDevice& device, std::span code, const std::vector& userData) { + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + return ShaderRecompiler::Recompile(request); +} + +struct Scenario { + std::uint8_t* texels; + std::uint8_t* keys; + std::uint8_t* source; + std::uint32_t* output; + std::vector copyData; + std::vector sampleData; +}; + +Scenario Prepare(std::uint8_t* block, std::uint8_t* outputBlock) { + Scenario scenario{block, block + KeysOffset, block + SourceKeysOffset, reinterpret_cast(outputBlock), std::vector(16, 0u), std::vector(24, 0u)}; + const auto source = BufferDescriptor(scenario.source, KeyBytes); + const auto destination = BufferDescriptor(scenario.keys, KeyBytes); + std::copy(source.begin(), source.end(), scenario.copyData.begin()); + std::copy(destination.begin(), destination.end(), scenario.copyData.begin() + 4); + const auto results = BufferDescriptor(scenario.output, Threads * 16u); + const auto texture = TextureDescriptor(scenario.texels, scenario.keys); + std::copy(results.begin(), results.end(), scenario.sampleData.begin()); + std::copy(texture.begin(), texture.end(), scenario.sampleData.begin() + 4); + std::copy(PointSampler.begin(), PointSampler.end(), scenario.sampleData.begin() + 12); + return scenario; +} + +void Reset(const Scenario& scenario) { + std::memset(scenario.texels, 0x55, SurfaceBytes); + std::memset(scenario.keys, 0xff, KeyBytes); + std::memset(scenario.source, 0x00, KeyBytes); + std::fill(scenario.output, scenario.output + Threads * 4u, 0xdeadbeefu); + AgcDriver::GuestMemory::BumpCollectEpoch(); +} + +void RecordKeyCopy(AgcDriver::VulkanDevice& device, const Scenario& scenario) { + const auto copy = Compile(device, CopyKeysCode, scenario.copyData); + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + device.Dispatch(copy, 1, 1, 1, {}, reinterpret_cast(CopyKeysCode.data())); + Require(AgcDriver::Graphics::DccKeyWritersPending(AddressOf(scenario.keys), KeyBytes), "a recorded kernel that copies data over the keys is not seen as a pending key writer"); +} + +void RequireSampledClear(const Scenario& scenario, const char* what) { + for (std::uint32_t index = 0; index < Threads * 4u; ++index) { + char message[256]; + std::snprintf(message, sizeof(message), "%s: component %u of thread %u is 0x%08x, expected 0 (keys stored 0000 by the kernel)", what, index % 4u, index / 4u, scenario.output[index]); + Require(scenario.output[index] == 0u, message); + } +} + +void RunLockedSync(AgcDriver::VulkanDevice& device, const Scenario& scenario) { + Reset(scenario); + RecordKeyCopy(device, scenario); + const auto sample = Compile(device, SampleSecondTexelCode, scenario.sampleData); + const auto waits = AgcDriver::Graphics::KeyWaitCounts(); + { + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + device.Dispatch(sample, 1, 1, 1, {}, reinterpret_cast(SampleSecondTexelCode.data())); + device.WaitIdle(); + } + const auto after = AgcDriver::Graphics::KeyWaitCounts(); + Require(after.synced > waits.synced, "a dispatch built under the GPU mutex did not wait for the kernel that copies data over the keys"); + RequireSampledClear(scenario, "a dispatch built under the GPU mutex"); +} + +void RunPresync(AgcDriver::VulkanDevice& device, const Scenario& scenario) { + Reset(scenario); + RecordKeyCopy(device, scenario); + const auto sample = Compile(device, SampleSecondTexelCode, scenario.sampleData); + const auto waits = AgcDriver::Graphics::KeyWaitCounts(); + const auto presyncs = AgcDriver::VulkanDevice::PresyncCounts(); + auto prepared = device.PrepareDispatch(sample, {}, Threads, reinterpret_cast(SampleSecondTexelCode.data())); + const auto stageA = AgcDriver::VulkanDevice::PresyncCounts(); + Require(stageA.keyChecks > presyncs.keyChecks, "stage A did not list the DCC keys of the sampled texture for the pre-sync"); + Require(stageA.keyWaits == presyncs.keyWaits + 1 && stageA.presyncs == presyncs.presyncs + 1, "stage A did not wait for the kernel that copies data over the keys before the GPU mutex"); + { + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + device.Dispatch(sample, 1, 1, 1, {}, reinterpret_cast(SampleSecondTexelCode.data()), std::move(prepared)); + device.WaitIdle(); + } + const auto after = AgcDriver::Graphics::KeyWaitCounts(); + Require(after.synced == waits.synced, "the dispatch waited for the key writer under the GPU mutex after the pre-sync waited for it"); + RequireSampledClear(scenario, "a dispatch whose keys were pre-synced"); +} + +bool CanFillKeys(AgcDriver::VulkanDevice& device, const Scenario& scenario) { + const std::array pattern{0xffffffffu, 0xffffffffu, 0xffffffffu, 0xffffffffu}; + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + const bool filled = device.FillBuffer(AddressOf(scenario.keys), KeyBytes, pattern); + device.WaitIdle(); + return filled; +} + +void RunKnownKeys(AgcDriver::VulkanDevice& device, const Scenario& scenario) { + Reset(scenario); + const std::array pattern{0u, 0u, 0u, 0u}; + { + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + Require(device.FillBuffer(AddressOf(scenario.keys), KeyBytes, pattern), "the fill of the keys was not recorded"); + Require(AgcDriver::Graphics::DccKeyWritersPending(AddressOf(scenario.keys), KeyBytes), "a recorded fill of the keys is not seen as a pending key writer"); + Require(!AgcDriver::Graphics::DccKeyWriterNeedsSync(AddressOf(scenario.keys), KeyBytes), "the keys of a recorded constant fill need a synchronization"); + } + const auto sample = Compile(device, SampleSecondTexelCode, scenario.sampleData); + const auto waits = AgcDriver::Graphics::KeyWaitCounts(); + const auto before = AgcDriver::VulkanDevice::PresyncCounts(); + auto prepared = device.PrepareDispatch(sample, {}, Threads, reinterpret_cast(SampleSecondTexelCode.data())); + const auto after = AgcDriver::VulkanDevice::PresyncCounts(); + Require(after.keyChecks > before.keyChecks, "stage A did not list the DCC keys of the sampled texture for the pre-sync"); + Require(after.presyncs == before.presyncs, "stage A waited for a fill whose keys the locked lookup derives from the pending stores"); + { + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + device.Dispatch(sample, 1, 1, 1, {}, reinterpret_cast(SampleSecondTexelCode.data()), std::move(prepared)); + device.WaitIdle(); + } + Require(AgcDriver::Graphics::KeyWaitCounts().synced == waits.synced, "the dispatch synchronized with the GPU although the pending fill gives the keys"); + RequireSampledClear(scenario, "a dispatch behind a pending fill of the keys"); +} + +void RunNoWriter(AgcDriver::VulkanDevice& device, const Scenario& scenario) { + const auto sample = Compile(device, SampleSecondTexelCode, scenario.sampleData); + const auto before = AgcDriver::VulkanDevice::PresyncCounts(); + Require(!AgcDriver::Graphics::DccKeyWritersPending(AddressOf(scenario.keys), KeyBytes), "the idle device still reports a pending key writer"); + auto prepared = device.PrepareDispatch(sample, {}, Threads, reinterpret_cast(SampleSecondTexelCode.data())); + const auto after = AgcDriver::VulkanDevice::PresyncCounts(); + Require(after.keyChecks > before.keyChecks, "stage A did not list the DCC keys of the sampled texture for the pre-sync"); + Require(after.keyWaits == before.keyWaits && after.presyncs == before.presyncs, "stage A waited although no recorded work writes the keys"); + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + device.Dispatch(sample, 1, 1, 1, {}, reinterpret_cast(SampleSecondTexelCode.data()), std::move(prepared)); + device.WaitIdle(); +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + if (!device->CanWaitUnlocked()) { + std::puts("skipped, the device has no timeline semaphores: the pre-sync is not tested"); + return VulkanTestSkipped; + } + GuestBlock locked; + GuestBlock lockedOutput; + GuestBlock presynced; + GuestBlock presyncedOutput; + GuestBlock known; + GuestBlock knownOutput; + const auto control = Prepare(locked.Data(), lockedOutput.Data()); + const auto changed = Prepare(presynced.Data(), presyncedOutput.Data()); + const auto fills = Prepare(known.Data(), knownOutput.Data()); + if (!CanFillKeys(*device, control)) { + std::puts("skipped, host imports are unavailable: the key writers of the test are copied writers"); + return VulkanTestSkipped; + } + RunLockedSync(*device, control); + RunPresync(*device, changed); + RunNoWriter(*device, changed); + RunKnownKeys(*device, fills); + std::puts("DCC key pre-sync tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/docs/dev/ARCHITECTURE.md b/docs/dev/ARCHITECTURE.md index c51b0407c7..81124cc96e 100644 --- a/docs/dev/ARCHITECTURE.md +++ b/docs/dev/ARCHITECTURE.md @@ -99,6 +99,7 @@ flowchart LR - A compute shader whose LDS plus its lock dword exceeds the host's `maxComputeSharedMemorySize` is not given a `Workgroup` array. `Recompile` sets `ShaderInfo::sharedMemoryBytes` for it and the SPIR-V backend addresses the LDS in a coherent storage buffer at descriptor set 1 (`WorkgroupMemoryDescriptorSet`), with a slice of `RecompileResult::workgroupMemoryDwords` per workgroup, indexed by `WorkgroupID` and `NumWorkGroups`. `VulkanDevice` keeps one such buffer, sized from the dispatch dimensions, and binds it after the resource set; programs that fit keep using shared memory. - `Driver::RegisterShader` splits shader preparation in two. `PlanRegistered` runs on the registering guest thread: it validates the registers and entry point, decodes the code and builds the recompile request, so invalid registrations still fail there. The RDNA to IR translation and resource plan (`PrepareShader`) run on a background pool of `hardware_concurrency() / 2` detached threads (at least 2, 64 MiB stacks). - Queue 0's packet worker decodes, captures and prepares a draw and hands its recording (`Graphics::Draw` under the GPU lock) to a committer thread in [`DrawPipeline.cpp`](../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp), so the next packet is decoded while the previous draw is recorded. Each queued item carries the guest ranges it will write (`DrawWriteRanges`: written buffers and storage images, color targets with their DCC and CMASK, depth and stencil). A later packet that reads or writes one of those ranges, or that the pipeline can't order, drains it first; labels, submission ends, buffer fills and (with `APS5_PIPELINE_DMA=1`) DMA packets are queued as items instead. With `APS5_PIPELINE_DISPATCH=1` (off by default), a `DISPATCH_DIRECT` whose dispatch-cache entry hits with a recipe is queued as an item too, carrying the dispatch's written buffers and storage images (`DispatchWriteRanges`); the capture and the cache lookup stay on the worker and the recipe is recorded on the committer. A dispatch still drains when its lookup misses, when what it reads overlaps a write still pending, when the hit is verified against data or when its writes cannot be ranged (bindless access, GDS, fault buffer). An exception thrown by a commit is rethrown on the worker at its next enqueue or drain, as an inline draw error would be. On by default; `APS5_PIPELINED_DRAWS=0` records every draw inline, and `APS5_EXACT_DRAW_WRITES=0` widens the color and depth ranges to estimates. +- A dispatch's resource build runs stage A (binding plan, buffers, descriptor set) without the GPU lock and stage B (image lookups) under it. Between them `VulkanDevice::presync` waits, still without the lock, for recorded work that writes a surface stage B would read on the CPU and for recorded work that writes the DCC keys of an image the dispatch binds (`ShaderResources::PresyncSurfaces`), so the locked `WaitForKeyWriters` finds nothing pending to synchronize with. A key range is waited for only when recorded work writes it, the key-store memo does not give its value (`DccKeyWritersPending`, without the mutex) and the pending stores do not give it either (`DccKeyWriterNeedsSync`, under the mutex, the same test `WaitForKeyWriters` makes before it synchronizes). Stage A's own key scan runs before that wait, and stage B compares it with the keys it reads under the lock as before. `APS5_NO_DCC_PRESYNC=1` leaves the key ranges out; `APS5_NO_PRESYNC=1` disables the whole pre-sync. - The shader registry maps a code address to every header registered for that code, in registration order. Static, graphics and rectangle ABI resolution pick the header at the bound address (or one equal to it, `SameHeader`, which also takes a hull header fused by `sceAgcFuseShaderHalves` (type 3) as the registered back half (type 7) it copies, the fused registers aside; graphics ABI resolution then uses the fused header's shader registers); draws and dispatches pick the program by the stage's binary type (`RegisteredProgram`), and a front entry matching both type 2 and type 4 throws. Within a type the last registration wins; re-registering an identical earlier header makes its snapshot current again without preparing it again, and changed code drops the older headers. - The snapshot is published as pending. `SourceHandleFor`, `InvocationFor` and `ShaderPreparationTransaction::Edit` / `Read` wait for the pending job before reading `PreparedShaders`, and rethrow its failure. `APS5_SYNC_SHADER_PREPARE=1` prepares on the registering thread instead. - `APS5_LAZY_SHADER_PREPARE=1` retains registration validation and decoding but discards the temporary plan without queueing preparation. The snapshot is deferred, not pending; draws and dispatches prepare and cache it on first use. Explicit ABI resolution still prepares its requested stages. This opt-in mode takes precedence over synchronous registration preparation and trades background CPU/memory use for possible first-use stalls; disk caches and shader output are unchanged. From e74484e0fb645efccc9f3cda915cb5b484ced799 Mon Sep 17 00:00:00 2001 From: AlxcBTW <187763517+AlxcBTW@users.noreply.github.com> Date: Fri, 16 Oct 2026 00:32:26 +0200 Subject: [PATCH 0506/1000] perf(agc): spin before blocking on the GPU mutex and the draw committer hand-offs At the Minecraft Dungeons II title the queue-0 worker, the draw committer and the compute worker hand work to each other 49,000 times per 10 s through the GPU mutex, the committer's condition variable and DrawPipeline::Drain, and each hand-off that has to wait costs a kernel wake-up. The three workers are busy 21-34 % of the time: the GPU idles because the chain of hand-offs is slow, not because a thread runs out of CPU. GpuMutexType::lock retries try_lock for a window before it blocks, the committer polls its item count for the window before it waits on its condition variable, and Drain polls for twice the window before it waits for the committer. A hand-off that completes within the window costs no wake-up. The window is 150 us on machines with at least 8 logical processors and 0 below that; APS5_HANDOFF_SPIN_US sets it (0 turns it off). Ordering and the lock's semantics are unchanged. The test agc_handoff_spin covers the window policy, the spin helpers and contended locks, and docs/dev/ARCHITECTURE.md describes the spin. --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 11 ++ .../Execution/include/HandoffSpin.hpp | 54 ++++++++ .../src/Driver/Draw/DrawPipeline.cpp | 3 + .../Execution/src/GuestMemory.cpp | 7 +- .../prx/libSceAgcDriver/tests/HandoffSpin.cpp | 125 ++++++++++++++++++ docs/dev/ARCHITECTURE.md | 1 + 6 files changed, 198 insertions(+), 3 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/Execution/include/HandoffSpin.hpp create mode 100644 core/libs/prx/libSceAgcDriver/tests/HandoffSpin.cpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index f112af6ade..d901191190 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -384,6 +384,17 @@ if(BUILD_TESTING) set_tests_properties(agc_performance_trace PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + + add_test_executable(agc_handoff_spin_tests tests/HandoffSpin.cpp) + target_include_directories(agc_handoff_spin_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(agc_handoff_spin_tests PRIVATE libc) + configure_windows_unwind(agc_handoff_spin_tests) + add_test(NAME agc_handoff_spin COMMAND agc_handoff_spin_tests) + set_tests_properties(agc_handoff_spin PROPERTIES TIMEOUT 30) + if(WIN32) + set_tests_properties(agc_handoff_spin PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() endif() if(WIN32) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/HandoffSpin.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/HandoffSpin.hpp new file mode 100644 index 0000000000..d21785cc3c --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Execution/include/HandoffSpin.hpp @@ -0,0 +1,54 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_HANDOFFSPIN_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_HANDOFFSPIN_HPP + +#include +#include +#include +#include +#include +#if defined(__x86_64__) || defined(_M_X64) +#include +#endif + +namespace AgcDriver { + +inline void SpinPause() { +#if defined(__x86_64__) || defined(_M_X64) + _mm_pause(); +#endif +} + +inline std::chrono::nanoseconds HandoffSpinWindowFor(const char* configured, unsigned logicalProcessors) { + constexpr std::chrono::microseconds automaticWindow{150}; + constexpr unsigned automaticProcessors = 8; + constexpr long long longestWindowUs = 10000; + if (configured != nullptr) return std::chrono::microseconds(std::clamp(std::strtoll(configured, nullptr, 10), 0, longestWindowUs)); + return logicalProcessors >= automaticProcessors ? std::chrono::nanoseconds(automaticWindow) : std::chrono::nanoseconds::zero(); +} + +inline std::chrono::nanoseconds HandoffSpinWindow() { + static const std::chrono::nanoseconds window = HandoffSpinWindowFor(std::getenv("APS5_HANDOFF_SPIN_US"), std::thread::hardware_concurrency()); + return window; +} + +template +bool SpinUntil(std::chrono::nanoseconds window, TReady&& ready) { + if (ready()) return true; + if (window.count() <= 0) return false; + const auto until = std::chrono::steady_clock::now() + window; + do { + SpinPause(); + if (ready()) return true; + } while (std::chrono::steady_clock::now() < until); + return false; +} + +template +void LockSpinning(TMutex& mutex, std::chrono::nanoseconds window) { + if (window.count() > 0 && SpinUntil(window, [&] { return mutex.try_lock(); })) return; + mutex.lock(); +} + +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp index bd4c9d9601..30491e8279 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp @@ -1,6 +1,7 @@ #include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/HandoffSpin.hpp" #include #include #include @@ -119,6 +120,7 @@ void DrawPipeline::report(std::chrono::steady_clock::time_point now) { void DrawPipeline::run() { for (;;) { Commit* commit = nullptr; + SpinUntil(HandoffSpinWindow(), [&] { return outstanding.load(std::memory_order_acquire) != 0; }); { std::unique_lock lock(mutex); committerWaiting = true; @@ -162,6 +164,7 @@ void DrawPipeline::Drain(DrainReason reason, std::uint32_t opcode) { if (!Busy()) return; if (GuestMemory::GpuMutex().HeldByThisThread()) throw std::runtime_error("draw pipeline drain under the GPU lock (reason " + std::to_string(static_cast(reason)) + ", opcode " + std::to_string(opcode) + ")"); const auto start = std::chrono::steady_clock::now(); + SpinUntil(HandoffSpinWindow() * 2, [&] { return !Busy(); }); std::unique_lock lock(mutex); ++idleWaiters; idle.wait(lock, [&] { return items.empty(); }); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp index d0859ab061..7c7379b6f1 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp @@ -1,5 +1,6 @@ #include "prx/libSceAgcDriver/Execution/include/ProfileOutput.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/HandoffSpin.hpp" #include "prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp" #include "prx/libSceAgcDriver/Execution/include/WriteWatchCoverage.hpp" #include "ThreadOwned.hpp" @@ -1931,12 +1932,12 @@ void GpuMutexType::lock() { if (!GpuLockProfiled()) { auto* trace = PerformanceTrace::Recording() ? PerformanceTrace::Thread() : nullptr; if (trace == nullptr) { - mutex.lock(); + LockSpinning(mutex, HandoffSpinWindow()); } else { const auto site = static_cast(std::exchange(LockStats().nextSite, GpuLockSite::Other)); if (!mutex.try_lock()) { const auto start = std::chrono::steady_clock::now(); - mutex.lock(); + LockSpinning(mutex, HandoffSpinWindow()); trace->Add("GpuMutex.wait", GpuLockSiteNames[site], start, std::chrono::steady_clock::now()); } } @@ -1954,7 +1955,7 @@ void GpuMutexType::lock() { HolderMatrixValues before{}; if (holders) Holders().Snapshot(before); const auto start = std::chrono::steady_clock::now(); - mutex.lock(); + LockSpinning(mutex, HandoffSpinWindow()); acquired(); const auto now = std::chrono::steady_clock::now(); if (auto* trace = PerformanceTrace::Recording() ? PerformanceTrace::Thread() : nullptr) trace->Add("GpuMutex.wait", GpuLockSiteNames[site], start, now); diff --git a/core/libs/prx/libSceAgcDriver/tests/HandoffSpin.cpp b/core/libs/prx/libSceAgcDriver/tests/HandoffSpin.cpp new file mode 100644 index 0000000000..22b8bbc8e5 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/HandoffSpin.cpp @@ -0,0 +1,125 @@ +#include "prx/libSceAgcDriver/Execution/include/HandoffSpin.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using namespace std::chrono_literals; +using AgcDriver::HandoffSpinWindowFor; +using AgcDriver::LockSpinning; +using AgcDriver::SpinUntil; + +void Require(bool condition, const std::string& message) { + if (!condition) throw std::runtime_error(message); +} + +void CheckWindowPolicy() { + Require(HandoffSpinWindowFor(nullptr, 20) == 150us, "20 logical processors do not spin for 150 us"); + Require(HandoffSpinWindowFor(nullptr, 8) == 150us, "8 logical processors do not spin for 150 us"); + Require(HandoffSpinWindowFor(nullptr, 7) == 0ns, "7 logical processors spin"); + Require(HandoffSpinWindowFor(nullptr, 0) == 0ns, "an unknown processor count spins"); + Require(HandoffSpinWindowFor("0", 20) == 0ns, "APS5_HANDOFF_SPIN_US=0 still spins"); + Require(HandoffSpinWindowFor("400", 2) == 400us, "APS5_HANDOFF_SPIN_US=400 does not set 400 us on 2 logical processors"); + Require(HandoffSpinWindowFor("-5", 20) == 0ns, "a negative window spins"); + Require(HandoffSpinWindowFor("text", 20) == 0ns, "a window that is no number spins"); + Require(HandoffSpinWindowFor("99999999999", 20) == 10000us, "a huge window is not limited to 10000 us"); +} + +void CheckSpinUntil() { + int calls = 0; + Require(SpinUntil(0ns, [&] { ++calls; return true; }) && calls == 1, "a ready condition was evaluated more than once"); + calls = 0; + Require(!SpinUntil(0ns, [&] { ++calls; return false; }) && calls == 1, "a zero window evaluated the condition more than once"); + calls = 0; + Require(SpinUntil(1s, [&] { return ++calls == 5; }) && calls == 5, "a condition that holds on its fifth evaluation was not seen"); + const auto start = std::chrono::steady_clock::now(); + Require(!SpinUntil(3ms, [] { return false; }), "a condition that never holds was reported ready"); + Require(std::chrono::steady_clock::now() - start >= 3ms, "the spin returned before its window ended"); + std::atomic flag{false}; + std::thread setter([&] { + std::this_thread::sleep_for(500us); + flag.store(true, std::memory_order_release); + }); + const bool seen = SpinUntil(5s, [&] { return flag.load(std::memory_order_acquire); }); + setter.join(); + Require(seen, "a flag set by another thread was not seen within the window"); +} + +template +void Contend(std::chrono::nanoseconds window, const char* what) { + constexpr int Workers = 4; + constexpr int Rounds = 20000; + TMutex mutex; + std::atomic inside{0}; + std::atomic overlapped{false}; + std::uint64_t counter = 0; + std::vector workers; + for (int worker = 0; worker < Workers; ++worker) { + workers.emplace_back([&] { + for (int round = 0; round < Rounds; ++round) { + LockSpinning(mutex, window); + if (inside.fetch_add(1) != 0) overlapped.store(true); + ++counter; + inside.fetch_sub(1); + mutex.unlock(); + } + }); + } + for (auto& worker : workers) worker.join(); + Require(!overlapped.load(), std::string(what) + ": two threads held the mutex at once"); + Require(counter == static_cast(Workers) * Rounds, std::string(what) + ": increments were lost"); +} + +void CheckBlockingFallback() { + std::mutex mutex; + std::atomic held{false}; + std::atomic released{false}; + std::thread holder([&] { + mutex.lock(); + held.store(true); + std::this_thread::sleep_for(20ms); + released.store(true); + mutex.unlock(); + }); + while (!held.load()) std::this_thread::yield(); + LockSpinning(mutex, 100us); + const bool afterRelease = released.load(); + mutex.unlock(); + holder.join(); + Require(afterRelease, "a spin that outlasted its window did not wait for the holder"); +} + +void CheckRecursion() { + std::recursive_mutex mutex; + LockSpinning(mutex, 100us); + LockSpinning(mutex, 100us); + mutex.unlock(); + mutex.unlock(); +} + +} + +int main() { + try { + CheckWindowPolicy(); + CheckSpinUntil(); + Contend(0ns, "plain lock"); + Contend(50us, "spin 50 us"); + Contend(1ms, "spin 1 ms"); + Contend(150us, "recursive mutex"); + CheckBlockingFallback(); + CheckRecursion(); + std::cout << "handoff spin tests passed\n"; + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/docs/dev/ARCHITECTURE.md b/docs/dev/ARCHITECTURE.md index c51b0407c7..4e5872b5aa 100644 --- a/docs/dev/ARCHITECTURE.md +++ b/docs/dev/ARCHITECTURE.md @@ -98,6 +98,7 @@ flowchart LR - The Vulkan pipelines are journaled as well ([`PipelineJournal.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp)): a graphics pipeline the store builds (`CachedPipeline`) or a compute pipeline a dispatch builds is appended to `pipelines.journal` next to the shader cache entries, as the materialized result of each stage (the specialized SPIR-V, bindings and push constants, through `ShaderDiskCache::EncodeResult`) plus the recipe the pipeline was built from (`GraphicsPipelineRecipe`: the stages' identities, vertex input layout, descriptor set layout keys and the state fields the `Pipeline` constructor reads, `CanonicalPipelineState`). When a `VulkanDevice` is created, the journal is loaded and its pipelines are built on a pool of `hardware_concurrency() / 2` threads (`APS5_PIPELINE_JOURNAL_THREADS`) while the title loads, so the first draw or dispatch of a configuration seen on an earlier run finds its pipeline resident. The pipeline stores key on `RecompileResult::Identity`, a hash of the module words, specialization constants, subgroup width and bindings that is the same on every run (`SharedSpirv::ContentHash`, computed once per shared module), rather than on the process-lifetime variant ids, and a graphics pipeline is built outside the store's mutex, so the replay threads and the queue workers build in parallel. A draw that arrives before its replayed entry builds the pipeline itself; the first inserted wins. A replayed entry enters the store at its least recently used end and a full store takes none, so the title's own pipelines are never evicted for a replayed one, and the optimized relink of a replayed pipeline (`OptimizedPipeline::Schedule`) waits for the first draw that binds it, so the relink thread serves the title's pipelines first. A device created without a window starts its replay at its first draw or dispatch rather than at creation, since `Driver::AttachWindow` usually replaces it before the title draws. The journal records a pipeline once (modules and recipes are deduplicated by content), keeps at most `APS5_PIPELINE_JOURNAL_MAX_MIB` (1024), replays the first `APS5_PIPELINE_JOURNAL_ENTRIES` recorded (1024, the pipeline store's bound, `APS5_PIPELINE_STORE_ENTRIES`; a run reaches those first), and is disabled with `APS5_NO_PIPELINE_JOURNAL=1` or without a shader cache directory. The file header names the device (`PipelineJournalDeviceFingerprint`, the properties the driver's pipeline cache file is named by), so a journal written for another GPU or driver is started over rather than replayed; appends and reads hold `pipelines.journal.lock` against another process sharing the directory, and a device cancels its replay before it is replaced (`PrepareForReplacement`) or destroyed, so a replaced headless device never inserts entries the window's device then sweeps. `APS5_PROFILE_DRAW=1` prints a `[pipeline-journal]` summary when the replay finishes and at teardown. - A compute shader whose LDS plus its lock dword exceeds the host's `maxComputeSharedMemorySize` is not given a `Workgroup` array. `Recompile` sets `ShaderInfo::sharedMemoryBytes` for it and the SPIR-V backend addresses the LDS in a coherent storage buffer at descriptor set 1 (`WorkgroupMemoryDescriptorSet`), with a slice of `RecompileResult::workgroupMemoryDwords` per workgroup, indexed by `WorkgroupID` and `NumWorkGroups`. `VulkanDevice` keeps one such buffer, sized from the dispatch dimensions, and binds it after the resource set; programs that fit keep using shared memory. - `Driver::RegisterShader` splits shader preparation in two. `PlanRegistered` runs on the registering guest thread: it validates the registers and entry point, decodes the code and builds the recompile request, so invalid registrations still fail there. The RDNA to IR translation and resource plan (`PrepareShader`) run on a background pool of `hardware_concurrency() / 2` detached threads (at least 2, 64 MiB stacks). +- A thread that needs the GPU mutex, the committer's next item or the end of the committer's queue spins before it blocks (`HandoffSpin.hpp`): `GpuMutexType::lock` retries `try_lock` for the window, the committer polls its item count, and `DrawPipeline::Drain` polls it for twice the window, so a hand-off that completes within the window costs no kernel wake-up. The window is 150 us on machines with at least 8 logical processors and 0 below that, where the spinning threads would compete with the holder; `APS5_HANDOFF_SPIN_US=` sets it (0 turns it off, at most 10000). Ordering and the lock's semantics are unchanged. - Queue 0's packet worker decodes, captures and prepares a draw and hands its recording (`Graphics::Draw` under the GPU lock) to a committer thread in [`DrawPipeline.cpp`](../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp), so the next packet is decoded while the previous draw is recorded. Each queued item carries the guest ranges it will write (`DrawWriteRanges`: written buffers and storage images, color targets with their DCC and CMASK, depth and stencil). A later packet that reads or writes one of those ranges, or that the pipeline can't order, drains it first; labels, submission ends, buffer fills and (with `APS5_PIPELINE_DMA=1`) DMA packets are queued as items instead. With `APS5_PIPELINE_DISPATCH=1` (off by default), a `DISPATCH_DIRECT` whose dispatch-cache entry hits with a recipe is queued as an item too, carrying the dispatch's written buffers and storage images (`DispatchWriteRanges`); the capture and the cache lookup stay on the worker and the recipe is recorded on the committer. A dispatch still drains when its lookup misses, when what it reads overlaps a write still pending, when the hit is verified against data or when its writes cannot be ranged (bindless access, GDS, fault buffer). An exception thrown by a commit is rethrown on the worker at its next enqueue or drain, as an inline draw error would be. On by default; `APS5_PIPELINED_DRAWS=0` records every draw inline, and `APS5_EXACT_DRAW_WRITES=0` widens the color and depth ranges to estimates. - The shader registry maps a code address to every header registered for that code, in registration order. Static, graphics and rectangle ABI resolution pick the header at the bound address (or one equal to it, `SameHeader`, which also takes a hull header fused by `sceAgcFuseShaderHalves` (type 3) as the registered back half (type 7) it copies, the fused registers aside; graphics ABI resolution then uses the fused header's shader registers); draws and dispatches pick the program by the stage's binary type (`RegisteredProgram`), and a front entry matching both type 2 and type 4 throws. Within a type the last registration wins; re-registering an identical earlier header makes its snapshot current again without preparing it again, and changed code drops the older headers. - The snapshot is published as pending. `SourceHandleFor`, `InvocationFor` and `ShaderPreparationTransaction::Edit` / `Read` wait for the pending job before reading `PreparedShaders`, and rethrow its failure. `APS5_SYNC_SHADER_PREPARE=1` prepares on the registering thread instead. From 37db6a80b74eadc38cb6945b0f9bcda2dc3fd5ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Fri, 16 Oct 2026 14:30:15 +0200 Subject: [PATCH 0507/1000] fix(agc): bound the async log's exit flush On Windows the atexit handlers of a DLL run after the process has ended its other threads, so the logger thread can be gone with lines still queued and the unbounded Flush waits forever (guest_shader_link timed out on 4 of the last 7 completed main builds since #6371). Run the bounded FlushAtEnd there instead. Co-Authored-By: Claude --- core/libs/prx/libSceAgcDriver/Execution/include/AsyncLog.hpp | 2 +- docs/dev/ARCHITECTURE.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/AsyncLog.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/AsyncLog.hpp index 383e554f61..8af0c00d39 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/AsyncLog.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/AsyncLog.hpp @@ -192,7 +192,7 @@ private: lastStats = Clock::now(); if (!asynchronous) return; LogSink().store(&AsyncLog::Sink, std::memory_order_release); - std::atexit([] { Get().Flush(); }); + std::atexit(&AsyncLog::FlushAtEnd); std::thread([this] { Run(); }).detach(); LibcSetEndFlush_nid_no_patch(&AsyncLog::FlushAtEnd); } diff --git a/docs/dev/ARCHITECTURE.md b/docs/dev/ARCHITECTURE.md index c51b0407c7..6876b67f68 100644 --- a/docs/dev/ARCHITECTURE.md +++ b/docs/dev/ARCHITECTURE.md @@ -125,7 +125,7 @@ flowchart LR - **Queue.** One logger thread drains a queue that holds at most 16 MiB of waiting lines; the batch the logger is writing comes on top. A caller that finds the queue full blocks until the logger has taken the pending batch, so no line is dropped. The logger writes entries in queue order and joins consecutive entries of one stream; it writes them with one `fwrite` and flush when the text reaches 64 KiB, the stream changes or the batch ends. `AGC_LOG_DEFERRED` queues the format string with numeric, enum and `LogLiteral` arguments and formats them on the logger thread; `APS5_LOG_TIMESTAMPS` stamps a line when it is queued. - **Order.** Lines that pass through the sink, on stdout and stderr alike, appear in call order. A direct `fprintf(stderr, ...)` or `printf` does not pass through it and is written at once, so it can appear before older queued lines; the coalesced `deferred write-back failed` notes are of this kind. `APS5_LOG_SYNC=1` keeps one order for all of them. -- **Flush.** `Flush` waits without a limit until every line queued before the call has been written, including while another thread holds the stream. It runs when the driver fails, in `Driver::stop` and at exit. `AsyncLog` also registers a bounded flush (`FlushFor`, 2 s) with `LibcSetEndFlush_nid_no_patch`; libc runs it where the process ends without those: `abort()` and `std::terminate` (a `SIGABRT` handler that the first registration chains in front of the previous one, so `raise(SIGABRT)` still ends the process), guest `abort()`, the Windows crash report of a fatal exception and guest `_Exit`. A title that installs its own `SIGABRT` disposition replaces that handler. The bounded flush polls two counters and takes no lock, so a thread that dies holding the queue lock or a stream cannot stall it. After 2 s it gives up, later calls return at once, and the lines still queued are lost. +- **Flush.** `Flush` waits without a limit until every line queued before the call has been written, including while another thread holds the stream. It runs when the driver fails and in `Driver::stop`. At exit `AsyncLog` runs a bounded flush (`FlushFor`, 2 s) instead: on Windows the `atexit` handlers of a DLL run after the process has ended its other threads, so the logger thread may be gone with lines still queued, and a `Flush` there would wait forever. It also registers that bounded flush with `LibcSetEndFlush_nid_no_patch`; libc runs it where the process ends without `atexit`: `abort()` and `std::terminate` (a `SIGABRT` handler that the first registration chains in front of the previous one, so `raise(SIGABRT)` still ends the process), guest `abort()`, the Windows crash report of a fatal exception and guest `_Exit`. A title that installs its own `SIGABRT` disposition replaces that handler. The bounded flush polls two counters and takes no lock, so a thread that dies holding the queue lock or a stream cannot stall it. After 2 s it gives up, later calls return at once, and the lines still queued are lost. - **Statistics.** A `[log]` line every 10 s in which lines were logged reports lines, bytes, the time spent by the callers and by the logger, and the queue peak; `APS5_LOG_STATS=0` hides it. Identical `deferred write-back failed` notes print three times per 10 s and are then counted in one `repeated N more times` line. `agc_async_log` checks the order of deferred and written lines, long lines, the repeat limit, a `Flush` that waits for a held stream, a bounded flush that gives up, and a full queue that blocks its caller. `agc_async_log_fatal` starts the test executable as a child that queues lines behind a held stream and then calls `abort()`, calls `std::terminate()`, raises an access violation (Windows) or `SIGABRT` (elsewhere), or aborts without ever releasing the stream; the first four must leave every line in the file in order, the last must still end after the bound. From 81e3db853c00453a04668e2c62e8b969f24d324b Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Fri, 16 Oct 2026 13:02:11 +0000 Subject: [PATCH 0508/1000] fix(agc): drain library builds before device replacement --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 2 + .../include/Driver/Shaders/ShaderRegistry.hpp | 1 + .../src/Driver/Shaders/ShaderRegistry.cpp | 4 ++ .../Execution/src/VulkanDevice.cpp | 2 + .../Graphics/src/PipelineLibrary.cpp | 13 +++- .../prx/libSceAgcDriver/tests/Graphics.cpp | 69 +++++++++++++++++++ 6 files changed, 90 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index f112af6ade..e019cd85c0 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1565,7 +1565,9 @@ if(BUILD_TESTING) endif() set_tests_properties(agc_sparse_buffer_holes PROPERTIES TIMEOUT 30) add_test(NAME agc_pipeline_journal COMMAND agc_driver_graphics_tests --pipeline-journal) + add_test(NAME agc_pipeline_library_clear_waits_for_build COMMAND agc_driver_graphics_tests --pipeline-library-clear-waits-for-build) set_tests_properties(agc_pipeline_journal PROPERTIES TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + set_tests_properties(agc_pipeline_library_clear_waits_for_build PROPERTIES TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") if(WIN32) set_tests_properties(agc_pipeline_journal PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp index 60a9fee237..9f3c41f9c4 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp @@ -117,6 +117,7 @@ void SubmitRegistrationPreparation(std::function task, const PreparedSha void PrioritizeRegistrationPreparation(PreparedShaders& prepared); void AwaitRegistrationPreparation(); void SubmitLibraryPrebuild(std::function task); +void AwaitLibraryPrebuilds(); void CloseRegistrationPreparation(); void ResolvePreparedGraphics(const ShaderSnapshot& front, const std::shared_ptr& fragment, std::uint32_t primitiveType, const ShaderRecompiler::SpirvTarget& target); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp index ab8df700c0..5c70f1e74e 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp @@ -1119,6 +1119,10 @@ void SubmitLibraryPrebuild(std::function task) { RegistrationPreparePool::Prebuild().Submit(std::move(task)); } +void AwaitLibraryPrebuilds() { + if (auto* pool = RegistrationPreparePool::ExistingPrebuild()) pool->AwaitSubmitted(); +} + void CloseRegistrationPreparation() { RegistrationPreparePool::Instance().Close(); if (auto* prebuild = RegistrationPreparePool::ExistingPrebuild()) prebuild->Close(); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index c2ebf9a5ac..af0b5a2c2e 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -9,6 +9,7 @@ #include "prx/libSceAgcDriver/Graphics/include/PipelineJournal.hpp" #include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp" #include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp" #include "prx/libSceAgcDriver/Execution/include/BdaFeatures.hpp" #include "prx/libSceAgcDriver/Execution/include/DeviceFault.hpp" @@ -2027,6 +2028,7 @@ void VulkanDevice::PrepareForReplacement() { state->journalReplay->Wait(); state->journalReplay.reset(); } + DriverDetail::AwaitLibraryPrebuilds(); Graphics::FlushCachedTextures(state->device); Graphics::PublishAllShadows(state->context, Graphics::PublishReason::Teardown); WaitIdle(); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp index 7e8fec28b6..611e4a34e6 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp @@ -53,6 +53,7 @@ struct LibraryStore { std::condition_variable work; std::deque>> jobs; std::map pending; + std::map activeBuilds; bool started = false; }; @@ -109,6 +110,16 @@ VkPipeline LinkPipelineFromLibraries(const Context& context, const VkGraphicsPip Require(!split || (layout.setLayoutCount == 2u && layout.pSetLayouts != nullptr && ((keys.preRasterizationSets | keys.fragmentSets) & ~3u) == 0u), "split pipeline library layout requires two descriptor set slots and a valid mask"); auto& store = Libraries(); std::unique_lock lock(store.mutex); + ++store.activeBuilds[context.device]; + struct ActiveBuild { + LibraryStore& store; + VkDevice device; + std::unique_lock& lock; + ~ActiveBuild() { + if (!lock.owns_lock()) lock.lock(); + if (--store.activeBuilds[device] == 0) store.idle.notify_all(); + } + } activeBuild{store, context.device, lock}; auto& device = store.devices[context.device]; if (device.destroyPipeline == nullptr) { device.destroyPipeline = context.Function("vkDestroyPipeline"); @@ -312,7 +323,7 @@ void WaitForOptimizedPipelines(VkDevice device) { void ClearPipelineLibraries(VkDevice device) { auto& store = Libraries(); std::unique_lock lock(store.mutex); - store.idle.wait(lock, [&] { return store.pending[device] == 0; }); + store.idle.wait(lock, [&] { return store.pending[device] == 0 && store.activeBuilds[device] == 0; }); const auto found = store.devices.find(device); if (found == store.devices.end()) return; auto& libraries = found->second; diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index d6c6af2d83..c9cd4c0459 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -32,6 +32,7 @@ #include #include #include +#include #include #include #include @@ -3488,6 +3489,11 @@ struct MockVulkan { }; MockVulkan mock; +std::mutex pipelineCreateMutex; +std::condition_variable pipelineCreateCondition; +bool blockLibraryCreate = false; +bool libraryCreateEntered = false; +bool releaseLibraryCreate = false; template THandle makeHandle() { @@ -3672,6 +3678,14 @@ VKAPI_ATTR VkResult VKAPI_CALL mockCreatePipelineLayout(VkDevice, const VkPipeli } VKAPI_ATTR VkResult VKAPI_CALL mockCreateGraphicsPipelines(VkDevice, VkPipelineCache, std::uint32_t count, const VkGraphicsPipelineCreateInfo* infos, const VkAllocationCallbacks*, VkPipeline* pipelines) { + { + std::unique_lock lock(pipelineCreateMutex); + if (blockLibraryCreate) { + libraryCreateEntered = true; + pipelineCreateCondition.notify_all(); + pipelineCreateCondition.wait(lock, [] { return releaseLibraryCreate; }); + } + } Require(count == 1, "mock expects exactly one graphics pipeline per call"); if ((infos[0].flags & VK_PIPELINE_CREATE_LIBRARY_BIT_KHR) == 0) { const auto* linked = static_cast(infos[0].pNext); @@ -5140,6 +5154,55 @@ void pipelineLibrarySetTests() { } } +void pipelineLibraryClearWaitsForBuildTests() { + using namespace AgcDriver::Graphics; + mock = MockVulkan{}; + auto context = mockContext(); + context.graphicsPipelineLibrary = true; + context.limits.framebufferNoAttachmentsSampleCounts = VK_SAMPLE_COUNT_1_BIT; + State state{}; + ShaderRecompiler::RecompileResult vertex, fragment; + vertex.spirv = makeModule({}); + fragment.spirv = makeModule({.fragment = true}); + vertex.variantId = 101; + fragment.variantId = 102; + const std::array shaders{{{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, {ShaderRecompiler::ShaderStage::Fragment, &fragment, PipelinePushSlotBytes}}}; + std::array mainKey{1}, fragmentKey{2}; + PipelineSetLayouts resources{}; + resources.main = makeHandle(); + resources.fragment = makeHandle(); + resources.mainKey = mainKey; + resources.fragmentKey = fragmentKey; + { + std::lock_guard lock(pipelineCreateMutex); + blockLibraryCreate = true; + libraryCreateEntered = false; + releaseLibraryCreate = false; + } + std::thread build([&] { Require(Pipeline::Prebuild(context, state, VertexInputLayout{}, resources, shaders), "graphics pipeline library prebuild was skipped"); }); + { + std::unique_lock lock(pipelineCreateMutex); + pipelineCreateCondition.wait(lock, [] { return libraryCreateEntered; }); + } + std::atomic cleared{false}; + std::thread clear([&] { + ClearCachedPipelines(context.device); + cleared.store(true, std::memory_order_release); + }); + std::this_thread::sleep_for(std::chrono::milliseconds(20)); + const bool waited = !cleared.load(std::memory_order_acquire); + { + std::lock_guard lock(pipelineCreateMutex); + releaseLibraryCreate = true; + } + pipelineCreateCondition.notify_all(); + build.join(); + clear.join(); + Require(waited, "clearing pipeline libraries did not wait for an active part build"); + Require(cleared.load(std::memory_order_acquire), "clearing pipeline libraries did not complete after the part build"); + Require(mock.live == 0, "clearing during a library build leaked Vulkan objects"); +} + void barycentricEmulationTests() { using namespace ShaderRecompiler; using namespace AgcDriver::Graphics; @@ -7694,6 +7757,11 @@ int main(int argc, char** argv) { std::cout << "pipeline journal recipe, record and replay tests passed\n"; return 0; } + if (argc == 2 && std::string_view(argv[1]) == "--pipeline-library-clear-waits-for-build") { + pipelineLibraryClearWaitsForBuildTests(); + std::cout << "pipeline library clear waits for active part builds\n"; + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--sparse-buffer-holes") { bool passed = false; { @@ -7806,6 +7874,7 @@ int main(int argc, char** argv) { stateTests(); retainedPixelStageTests(); pipelineLibrarySetTests(); + pipelineLibraryClearWaitsForBuildTests(); hdrComponentSwapTests(); depthMaintenanceTests(); hardwareScreenOffsetTests(); From 2132abc6988ab4500a58bc612c977a87038ab394 Mon Sep 17 00:00:00 2001 From: billybobpicklio Date: Fri, 16 Oct 2026 09:17:24 -0400 Subject: [PATCH 0509/1000] test(agc): register the draw descriptor recorder tests with CTest agc_driver_recorder_tests --draw-descriptors-only (drawDescriptorWriteTests and drawSnapshotTableSetTests) had no CTest entry, so CI never ran it. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 051af9c6e5..7c791571a1 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1166,8 +1166,10 @@ if(BUILD_TESTING) set_tests_properties(agc_shadowed_key_stores PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30) add_test(NAME agc_sample_dumps COMMAND agc_driver_recorder_tests --sample-dumps-only) set_tests_properties(agc_sample_dumps PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30) + add_test(NAME agc_draw_descriptors COMMAND agc_driver_recorder_tests --draw-descriptors-only) + set_tests_properties(agc_draw_descriptors PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 60) if(WIN32) - set_tests_properties(agc_pending_key_stores agc_shadowed_key_stores agc_sample_dumps PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(agc_pending_key_stores agc_shadowed_key_stores agc_sample_dumps agc_draw_descriptors PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() endif() From e4ede0e9e1007b35a76bdfcc7e083b0fa4ad580d Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 16 Oct 2026 13:27:13 +0000 Subject: [PATCH 0510/1000] test(ssl): run four connections on one context in parallel Each client thread handshakes, writes, reads the reply and the end of the stream, so session setup and record allocations from several threads contend for the same context pool. The pool memory must return to its loaded size afterwards. Initialize the pool's mutex explicitly, which -Wextra asks for. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RoU3ZF76A6uR8sCA7NMLch --- core/libs/prx/libSceSsl/Export.cpp | 2 +- core/libs/tests/GuestSslTransport.cpp | 21 ++++++++++++++++++++- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceSsl/Export.cpp b/core/libs/prx/libSceSsl/Export.cpp index e51e6120bc..04fa9fe37d 100644 --- a/core/libs/prx/libSceSsl/Export.cpp +++ b/core/libs/prx/libSceSsl/Export.cpp @@ -176,7 +176,7 @@ struct Context { std::vector> certificateHandles; std::unordered_map caLists; std::unordered_map caCerts; - explicit Context(std::size_t size) : pool{size} {} + explicit Context(std::size_t size) : pool{size, 0, 0, {}} {} }; int HostError() { diff --git a/core/libs/tests/GuestSslTransport.cpp b/core/libs/tests/GuestSslTransport.cpp index 39dd8b497b..ddbbd279aa 100644 --- a/core/libs/tests/GuestSslTransport.cpp +++ b/core/libs/tests/GuestSslTransport.cpp @@ -48,6 +48,8 @@ int APS5_VABI sceNetSocketClose(int); namespace { +constexpr int ParallelConnections = 4; + void Require(bool condition, const char* message) { if (condition) return; std::fprintf(stderr, "%s\n", message); @@ -193,6 +195,7 @@ int main(int argc, char** argv) { std::thread serving([&] { server.Serve(false); server.Serve(true); + for (int index = 0; index < ParallelConnections; ++index) server.Serve(true); }); const int empty = sceSslInit_nid_postfix(256 * 1024); @@ -200,7 +203,7 @@ int main(int argc, char** argv) { Require(Throws([&] { sceSslConnect(withoutAuthority); }), "a handshake without a loaded CA was attempted"); Require(sceSslDeleteConnection(withoutAuthority) == 0 && sceSslTerm_nid_postfix(empty) == 0, "release the context without CA"); - const int context = sceSslInit_nid_postfix(256 * 1024); + const int context = sceSslInit_nid_postfix(1024 * 1024); std::string root = RootPem; SceSsl::Data authority{reinterpret_cast(root.data()), root.size()}; auto* authorities = &authority; @@ -243,6 +246,22 @@ int main(int argc, char** argv) { Require(InUse(context) == loaded, "the connection's pool memory was not returned"); Require(sockets.Close(descriptor) == 0, "the socket did not stay open after the connection was deleted"); + std::array clients; + for (auto& client : clients) { + client = std::thread([&] { + const int parallelSocket = sockets.Open(server.port); + const int parallel = sceSslCreateConnection(context, parallelSocket, "localhost"); + std::array reply{}; + char ping[] = "ping"; + Require(sceSslConnect(parallel) == 0 && sceSslWrite(parallel, ping, 4, 0) == 4, "parallel handshake or write"); + Require(sceSslRead(parallel, reply.data(), reply.size(), 0) == 4 && std::memcmp(reply.data(), "pong", 4) == 0, "parallel reply"); + Require(sceSslRead(parallel, reply.data(), reply.size(), 0) == 0, "parallel end of stream"); + Require(sceSslDeleteConnection(parallel) == 0 && sockets.Close(parallelSocket) == 0, "release parallel connection"); + }); + } + for (auto& client : clients) client.join(); + Require(InUse(context) == loaded, "parallel connections did not return their pool memory"); + serving.join(); Require(server.failure.empty(), server.failure.c_str()); Require(sceSslTerm_nid_postfix(context) == 0, "terminate the context"); From 647b5a1c979f533de4360094bf77c11bcb2bb3a9 Mon Sep 17 00:00:00 2001 From: Felipe Arce Date: Fri, 16 Oct 2026 10:44:12 -0300 Subject: [PATCH 0511/1000] refactor(libkernel): share the Windows pipe query between poll and kevent PipeReadiness and PipeReadState both opened the pipe handle and asked NtQueryInformationFile for its access mask and local information. Both now go through QueryPipe, which keeps each caller's error prefix. --- core/libs/prx/libkernel/Socket/src/Udp.cpp | 25 +++++++++------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index b1f1fa4420..74014b66f1 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -472,18 +472,22 @@ constexpr int FileAccessInformationClass = 8; constexpr int FilePipeLocalInformationClass = 24; constexpr ULONG PipeClosingState = 4; constexpr ULONG PipeAtomicBytes = 512; -bool PipeReadiness(int descriptor, short events, short& revents) { +bool QueryPipe(int descriptor, const char* function, ACCESS_MASK& access, PipeLocalInformation& pipe) { const auto handle = reinterpret_cast(::_get_osfhandle(descriptor)); if (handle == INVALID_HANDLE_VALUE || GetFileType(handle) != FILE_TYPE_PIPE) return false; static const auto query = reinterpret_cast( reinterpret_cast(GetProcAddress(GetModuleHandleW(L"ntdll.dll"), "NtQueryInformationFile"))); - if (!query) throw std::runtime_error("poll: NtQueryInformationFile is unavailable"); + if (!query) throw std::runtime_error(std::string(function) + ": NtQueryInformationFile is unavailable"); PipeIoStatus status{}; - ACCESS_MASK access = 0; - PipeLocalInformation pipe{}; if (query(handle, &status, &access, sizeof(access), FileAccessInformationClass) < 0 || query(handle, &status, &pipe, sizeof(pipe), FilePipeLocalInformationClass) < 0) - throw std::runtime_error("poll: querying pipe descriptor " + std::to_string(descriptor) + " failed"); + throw std::runtime_error(std::string(function) + ": querying pipe descriptor " + std::to_string(descriptor) + " failed"); + return true; +} +bool PipeReadiness(int descriptor, short events, short& revents) { + ACCESS_MASK access = 0; + PipeLocalInformation pipe{}; + if (!QueryPipe(descriptor, "poll", access, pipe)) return false; if ((events & GuestPollWritable) && !(access & FILE_WRITE_DATA)) ThrowReadEndWritable(descriptor); const bool closed = pipe.state == PipeClosingState; revents = 0; @@ -745,18 +749,9 @@ bool GuestSockets::Ready(int descriptor, bool write, std::int64_t* data, bool* e bool GuestSockets::PipeReadState(int descriptor, std::int64_t* data, bool* eof) { #ifdef _WIN32 - const auto handle = reinterpret_cast(::_get_osfhandle(descriptor)); - if (handle == INVALID_HANDLE_VALUE || GetFileType(handle) != FILE_TYPE_PIPE) return false; - static const auto query = reinterpret_cast( - reinterpret_cast(GetProcAddress(GetModuleHandleW(L"ntdll.dll"), "NtQueryInformationFile"))); - if (!query) throw std::runtime_error("kevent: NtQueryInformationFile is unavailable"); - PipeIoStatus status{}; ACCESS_MASK access = 0; PipeLocalInformation pipe{}; - if (query(handle, &status, &access, sizeof(access), FileAccessInformationClass) < 0 || - query(handle, &status, &pipe, sizeof(pipe), FilePipeLocalInformationClass) < 0) - throw std::runtime_error("kevent: querying pipe descriptor " + std::to_string(descriptor) + " failed"); - if (!(access & FILE_READ_DATA)) return false; + if (!QueryPipe(descriptor, "kevent", access, pipe) || !(access & FILE_READ_DATA)) return false; *data = pipe.readDataAvailable; *eof = pipe.state == PipeClosingState; #else From 49ea68f2a674c91827850359acac352e61a65347 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Fri, 16 Oct 2026 16:26:42 +0200 Subject: [PATCH 0512/1000] fix(build): link core/libs executables against the shared libstdc++ on Linux Co-Authored-By: Claude --- core/libs/CMakeLists.txt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index 8385138261..f33b4cfbc4 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -1,5 +1,7 @@ if(NOT WIN32) - string(REPLACE "-static " "" CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} ") + foreach(runtimeLinkFlag IN ITEMS -static-libgcc -static-libstdc++ -static) + string(REPLACE "${runtimeLinkFlag}" "" CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS}") + endforeach() string(STRIP "${CMAKE_EXE_LINKER_FLAGS}" CMAKE_EXE_LINKER_FLAGS) endif() From 141d605b9043fc15f922bcb4624e40f7ba087d64 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Fri, 16 Oct 2026 07:32:54 -0700 Subject: [PATCH 0513/1000] test(agc): retry a settled import's first use when its copies finish first The first-use check needs the main-queue copies of an import to be still in flight when the import returns to the write watch. On a fast device they can finish first, and the test then failed although the driver did nothing wrong. Retry with 4 and 16 times as many copies, and report the case as not tested when the copies finish first every time. --- .../prx/libSceAgcDriver/tests/Recorder.cpp | 45 ++++++++++++------- 1 file changed, 28 insertions(+), 17 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index a21e03b80c..4c16ba2c3f 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -1680,21 +1680,9 @@ bool settledImportsWatchPrivate(const Context& context) { return watched; } -bool settleFirstUseTests(const Device& device) { +bool settleFirstUseRaced(const Context& context, int copies) { using namespace AgcDriver::GuestMemory; - const auto& context = device.GetContext(); constexpr std::size_t unit = 65536; - constexpr int copies = 4096; - if (context.hostImportAlignment == 0 || context.hostImportAlignment > unit || context.settleQueue == VK_NULL_HANDLE || !WriteWatched()) { - std::cout << "host imports within 64 KiB, a settle queue or write watching unavailable: the first use of a settled import not tested\n"; - return false; - } - std::unique_lock gpu(GpuMutex()); - PrepareImportWatch(context); - if (!settledImportsWatchPrivate(context)) { - std::cout << "this device compares every Windows import: the first use of a settled import not tested\n"; - return false; - } void* small = AllocateWatched(unit, unit); struct Release { void* small; @@ -1740,17 +1728,40 @@ bool settleFirstUseTests(const Device& device) { CollectWritesUncached(address, unit); const bool unchanged = returned && UnchangedSince(address, unit, collected); const auto since = [&](std::chrono::steady_clock::time_point at) { return std::chrono::duration(at - submittedAt).count(); }; - std::cout << "(f) settled import back in the write watch " << since(returnedAt) << " ms after main-queue copies of it were submitted, copies reaped by " << since(reapedAt) << " ms; driver marks after its return=" << !unchanged << '\n'; + std::cout << "(f) settled import back in the write watch " << since(returnedAt) << " ms after " << copies << " main-queue copies of it were submitted, copies reaped by " << since(reapedAt) << " ms; driver marks after its return=" << !unchanged << '\n'; Require(returned, "(f) a settled import did not return to the write watch"); - Require(racing, "(f) the main-queue copies had finished before the import returned to the write watch, so its read did not race them"); Require(unchanged, "(f) the driver marked a settled import's pages after its settle read completed while earlier main-queue copies of it ran"); { GuestAllocations::Mutation mutation; mutation.Remove(small); } HostImportFor(context, address, unit); - std::cout << "settled import first use tests passed\n"; - return true; + return racing; +} + +bool settleFirstUseTests(const Device& device) { + using namespace AgcDriver::GuestMemory; + const auto& context = device.GetContext(); + constexpr std::size_t unit = 65536; + if (context.hostImportAlignment == 0 || context.hostImportAlignment > unit || context.settleQueue == VK_NULL_HANDLE || !WriteWatched()) { + std::cout << "host imports within 64 KiB, a settle queue or write watching unavailable: the first use of a settled import not tested\n"; + return false; + } + std::unique_lock gpu(GpuMutex()); + PrepareImportWatch(context); + if (!settledImportsWatchPrivate(context)) { + std::cout << "this device compares every Windows import: the first use of a settled import not tested\n"; + return false; + } + for (int copies = 4096; copies <= 65536; copies *= 4) { + if (settleFirstUseRaced(context, copies)) { + std::cout << "settled import first use tests passed\n"; + return true; + } + std::cout << "(f) the main-queue copies had finished before the import returned to the write watch, so its read did not race them\n"; + } + std::cout << "(f) the main-queue copies finished first in every round: the first use of a settled import not tested\n"; + return false; } bool settleEarlierImportTests(const Device& device) { From 94775df2b2e6c0bb7ee490d113c5afdeb7c75648 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Fri, 16 Oct 2026 16:35:37 +0200 Subject: [PATCH 0514/1000] ci(linux): link with wild Co-Authored-By: Claude --- .github/workflows/build.yml | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ee1d792061..89210913d4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -333,6 +333,33 @@ jobs: ccache --show-config dpkg-query -W mesa-vulkan-drivers grep -m1 '^model name' /proc/cpuinfo + - name: Install the wild linker + shell: bash + env: + WILD_VERSION: 0.10.0 + WILD_SHA256: 641265506a7c06cfb03181b8916ab663ec8407855db6d4db7f8450667d105283 + run: | + tools=/var/cache/anyps5/tools + dir=$tools/wild-$WILD_VERSION + tmp= + probe=$(mktemp -d) + trap 'rm -rf "$tmp" "$probe"' EXIT + if [ ! -x "$dir/bin/ld" ]; then + mkdir -p "$tools" + tmp=$(mktemp -d "$tools/.wild.XXXXXX") + name=wild-linker-$WILD_VERSION-x86_64-unknown-linux-gnu + curl -fsSL --retry 3 -o "$tmp/$name.tar.gz" "https://github.com/davidlattimore/wild/releases/download/$WILD_VERSION/$name.tar.gz" + echo "$WILD_SHA256 $tmp/$name.tar.gz" | sha256sum -c - + tar -xzf "$tmp/$name.tar.gz" -C "$tmp" + mkdir -p "$tmp/out/bin" + mv "$tmp/$name/wild" "$tmp/out/bin/wild" + ln -s wild "$tmp/out/bin/ld" + mv -T "$tmp/out" "$dir" 2>/dev/null || test -x "$dir/bin/ld" + fi + echo 'int main(void) { return 0; }' > "$probe/main.c" + gcc "-B$dir/bin" "$probe/main.c" -o "$probe/main" + readelf -p .comment "$probe/main" | grep -F "Linker: Wild $WILD_VERSION" + echo "LINKER_FLAGS=-B$dir/bin" >> "$GITHUB_ENV" - name: Size parallelism to what the shared host has free run: | cpus=$(nproc) @@ -345,7 +372,7 @@ jobs: echo "cpus=$cpus load=$busy mem_available_gb=$mem_gb -> build jobs $jobs" echo "CMAKE_BUILD_PARALLEL_LEVEL=$jobs" >> "$GITHUB_ENV" echo "CTEST_PARALLEL_LEVEL=$(( jobs > 4 ? 4 : jobs ))" >> "$GITHUB_ENV" - - run: cmake -S . -B build $CONFIGURE -DANYPS5_ENABLE_SPIRV_TOOLS=ON + - run: cmake -S . -B build $CONFIGURE -DANYPS5_ENABLE_SPIRV_TOOLS=ON "-DCMAKE_EXE_LINKER_FLAGS=$LINKER_FLAGS" "-DCMAKE_SHARED_LINKER_FLAGS=$LINKER_FLAGS" "-DCMAKE_MODULE_LINKER_FLAGS=$LINKER_FLAGS" - name: Check that every test executable is registered with ctest run: python3 tools/check_test_registration.py build - run: cmake --build build -- -l "$(nproc)" From 83eb3ed04ae2307f494da08010f123dcb165f861 Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Fri, 16 Oct 2026 14:39:57 +0000 Subject: [PATCH 0515/1000] test(agc): set DLL path for pipeline library test --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index e019cd85c0..21ac3fb4cd 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1569,7 +1569,7 @@ if(BUILD_TESTING) set_tests_properties(agc_pipeline_journal PROPERTIES TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") set_tests_properties(agc_pipeline_library_clear_waits_for_build PROPERTIES TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") if(WIN32) - set_tests_properties(agc_pipeline_journal PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + set_tests_properties(agc_pipeline_journal agc_pipeline_library_clear_waits_for_build PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() set_tests_properties(agc_driver_sparse_buffer_holes PROPERTIES TIMEOUT 30) if(WIN32) From 20c11e733423fbd19939af99967161a60e30b1a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Fri, 16 Oct 2026 16:40:52 +0200 Subject: [PATCH 0516/1000] Revert "ci(linux): link with wild" (#6861) The runner cannot create directories under /var/cache/anyps5 (mkdir: Permission denied), so the install step failed and the linux job built nothing. Co-Authored-By: Claude --- .github/workflows/build.yml | 29 +---------------------------- 1 file changed, 1 insertion(+), 28 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 89210913d4..ee1d792061 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -333,33 +333,6 @@ jobs: ccache --show-config dpkg-query -W mesa-vulkan-drivers grep -m1 '^model name' /proc/cpuinfo - - name: Install the wild linker - shell: bash - env: - WILD_VERSION: 0.10.0 - WILD_SHA256: 641265506a7c06cfb03181b8916ab663ec8407855db6d4db7f8450667d105283 - run: | - tools=/var/cache/anyps5/tools - dir=$tools/wild-$WILD_VERSION - tmp= - probe=$(mktemp -d) - trap 'rm -rf "$tmp" "$probe"' EXIT - if [ ! -x "$dir/bin/ld" ]; then - mkdir -p "$tools" - tmp=$(mktemp -d "$tools/.wild.XXXXXX") - name=wild-linker-$WILD_VERSION-x86_64-unknown-linux-gnu - curl -fsSL --retry 3 -o "$tmp/$name.tar.gz" "https://github.com/davidlattimore/wild/releases/download/$WILD_VERSION/$name.tar.gz" - echo "$WILD_SHA256 $tmp/$name.tar.gz" | sha256sum -c - - tar -xzf "$tmp/$name.tar.gz" -C "$tmp" - mkdir -p "$tmp/out/bin" - mv "$tmp/$name/wild" "$tmp/out/bin/wild" - ln -s wild "$tmp/out/bin/ld" - mv -T "$tmp/out" "$dir" 2>/dev/null || test -x "$dir/bin/ld" - fi - echo 'int main(void) { return 0; }' > "$probe/main.c" - gcc "-B$dir/bin" "$probe/main.c" -o "$probe/main" - readelf -p .comment "$probe/main" | grep -F "Linker: Wild $WILD_VERSION" - echo "LINKER_FLAGS=-B$dir/bin" >> "$GITHUB_ENV" - name: Size parallelism to what the shared host has free run: | cpus=$(nproc) @@ -372,7 +345,7 @@ jobs: echo "cpus=$cpus load=$busy mem_available_gb=$mem_gb -> build jobs $jobs" echo "CMAKE_BUILD_PARALLEL_LEVEL=$jobs" >> "$GITHUB_ENV" echo "CTEST_PARALLEL_LEVEL=$(( jobs > 4 ? 4 : jobs ))" >> "$GITHUB_ENV" - - run: cmake -S . -B build $CONFIGURE -DANYPS5_ENABLE_SPIRV_TOOLS=ON "-DCMAKE_EXE_LINKER_FLAGS=$LINKER_FLAGS" "-DCMAKE_SHARED_LINKER_FLAGS=$LINKER_FLAGS" "-DCMAKE_MODULE_LINKER_FLAGS=$LINKER_FLAGS" + - run: cmake -S . -B build $CONFIGURE -DANYPS5_ENABLE_SPIRV_TOOLS=ON - name: Check that every test executable is registered with ctest run: python3 tools/check_test_registration.py build - run: cmake --build build -- -l "$(nproc)" From b066940d4f28d627e0fdf45d512233a55f3f3ae9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Fri, 16 Oct 2026 16:35:37 +0200 Subject: [PATCH 0517/1000] ci(linux): link with wild Co-Authored-By: Claude --- .github/workflows/build.yml | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ee1d792061..430d5e820a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -333,6 +333,33 @@ jobs: ccache --show-config dpkg-query -W mesa-vulkan-drivers grep -m1 '^model name' /proc/cpuinfo + - name: Install the wild linker + shell: bash + env: + WILD_VERSION: 0.10.0 + WILD_SHA256: 641265506a7c06cfb03181b8916ab663ec8407855db6d4db7f8450667d105283 + run: | + tools=$RUNNER_TOOL_CACHE/wild + mkdir -p "$tools" 2>/dev/null && test -w "$tools" || { tools=$RUNNER_TEMP/wild; mkdir -p "$tools"; } + dir=$tools/$WILD_VERSION + tmp= + probe=$(mktemp -d "$RUNNER_TEMP/wild-probe.XXXXXX") + trap 'rm -rf "$tmp" "$probe"' EXIT + if [ ! -x "$dir/bin/ld" ]; then + tmp=$(mktemp -d "$tools/.wild.XXXXXX") + name=wild-linker-$WILD_VERSION-x86_64-unknown-linux-gnu + curl -fsSL --retry 3 -o "$tmp/$name.tar.gz" "https://github.com/davidlattimore/wild/releases/download/$WILD_VERSION/$name.tar.gz" + echo "$WILD_SHA256 $tmp/$name.tar.gz" | sha256sum -c - + tar -xzf "$tmp/$name.tar.gz" -C "$tmp" + mkdir -p "$tmp/out/bin" + mv "$tmp/$name/wild" "$tmp/out/bin/wild" + ln -s wild "$tmp/out/bin/ld" + mv -T "$tmp/out" "$dir" 2>/dev/null || test -x "$dir/bin/ld" + fi + echo 'int main(void) { return 0; }' > "$probe/main.c" + gcc "-B$dir/bin" "$probe/main.c" -o "$probe/main" + readelf -p .comment "$probe/main" | grep -F "Linker: Wild $WILD_VERSION" + echo "LINKER_FLAGS=-B$dir/bin" >> "$GITHUB_ENV" - name: Size parallelism to what the shared host has free run: | cpus=$(nproc) @@ -345,7 +372,7 @@ jobs: echo "cpus=$cpus load=$busy mem_available_gb=$mem_gb -> build jobs $jobs" echo "CMAKE_BUILD_PARALLEL_LEVEL=$jobs" >> "$GITHUB_ENV" echo "CTEST_PARALLEL_LEVEL=$(( jobs > 4 ? 4 : jobs ))" >> "$GITHUB_ENV" - - run: cmake -S . -B build $CONFIGURE -DANYPS5_ENABLE_SPIRV_TOOLS=ON + - run: cmake -S . -B build $CONFIGURE -DANYPS5_ENABLE_SPIRV_TOOLS=ON "-DCMAKE_EXE_LINKER_FLAGS=$LINKER_FLAGS" "-DCMAKE_SHARED_LINKER_FLAGS=$LINKER_FLAGS" "-DCMAKE_MODULE_LINKER_FLAGS=$LINKER_FLAGS" - name: Check that every test executable is registered with ctest run: python3 tools/check_test_registration.py build - run: cmake --build build -- -l "$(nproc)" From 8e671f4b937f20b5b3f5272e58fb88a025f434db Mon Sep 17 00:00:00 2001 From: Daniele Giovane <77807986+yabanana@users.noreply.github.com> Date: Fri, 16 Oct 2026 15:51:26 +0200 Subject: [PATCH 0518/1000] test(agc): cover in-flight preparation and late cache shutdown --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 5 ++-- .../tests/execution/ShutdownThreads.cpp | 28 ++++++++++++++----- 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index e2d9258a4f..befe3b7e0d 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1827,9 +1827,10 @@ if(BUILD_TESTING) add_test(NAME agc_driver_shutdown_threads_compare_helpers COMMAND agc_driver_shutdown_threads_tests --compare-helpers) add_test(NAME agc_driver_shutdown_threads_draw_committer COMMAND agc_driver_shutdown_threads_tests --draw-committer) add_test(NAME agc_driver_shutdown_threads_shader_cache_writer COMMAND agc_driver_shutdown_threads_tests --shader-cache-writer) - set_tests_properties(agc_driver_shutdown_threads_joined agc_driver_shutdown_threads_compare_helpers agc_driver_shutdown_threads_draw_committer agc_driver_shutdown_threads_shader_cache_writer PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" TIMEOUT 30) + add_test(NAME agc_driver_shutdown_threads_late_shader_cache COMMAND agc_driver_shutdown_threads_tests --late-shader-cache) + set_tests_properties(agc_driver_shutdown_threads_joined agc_driver_shutdown_threads_compare_helpers agc_driver_shutdown_threads_draw_committer agc_driver_shutdown_threads_shader_cache_writer agc_driver_shutdown_threads_late_shader_cache PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" TIMEOUT 30) if(WIN32) - set_tests_properties(agc_driver_shutdown_threads_joined agc_driver_shutdown_threads_compare_helpers agc_driver_shutdown_threads_draw_committer agc_driver_shutdown_threads_shader_cache_writer PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + set_tests_properties(agc_driver_shutdown_threads_joined agc_driver_shutdown_threads_compare_helpers agc_driver_shutdown_threads_draw_committer agc_driver_shutdown_threads_shader_cache_writer agc_driver_shutdown_threads_late_shader_cache PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() add_test(NAME agc_driver_compute_user_data_calls_registered COMMAND agc_driver_compute_user_data_calls_tests --registered) set_tests_properties(agc_driver_compute_user_data_calls_registered PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ShutdownThreads.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ShutdownThreads.cpp index 747d01164a..2da24d99d6 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ShutdownThreads.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ShutdownThreads.cpp @@ -5,6 +5,7 @@ #include "CompiledVariant.hpp" #include "ShaderDiskCache.hpp" #include +#include #include #include #include @@ -53,13 +54,24 @@ void ClosingJoinsPreparation() { std::promise prebuilt; auto preparedOn = prepared.get_future(); auto prebuiltOn = prebuilt.get_future(); - SubmitRegistrationPreparation([&] { prepared.set_value(std::this_thread::get_id()); }); - SubmitLibraryPrebuild([&] { prebuilt.set_value(std::this_thread::get_id()); }); + std::promise gate; + auto opened = gate.get_future().share(); + SubmitRegistrationPreparation([&] { prepared.set_value(std::this_thread::get_id()); opened.wait(); }); + SubmitLibraryPrebuild([&] { prebuilt.set_value(std::this_thread::get_id()); opened.wait(); }); const auto caller = std::this_thread::get_id(); - Require(preparedOn.get() != caller && prebuiltOn.get() != caller, "a queued preparation ran on the caller instead of a preparation thread"); - Require(RegistrationPreparationThreads() != 0, "a queued preparation started no preparation thread"); - CloseRegistrationPreparation(); + const bool offCaller = preparedOn.get() != caller && prebuiltOn.get() != caller; + auto closing = std::async(std::launch::async, [] { CloseRegistrationPreparation(); }); + const bool waited = closing.wait_for(std::chrono::milliseconds(20)) == std::future_status::timeout; + gate.set_value(); + closing.get(); + Require(offCaller, "a queued preparation ran on the caller instead of a preparation thread"); + Require(waited, "closing returned while preparation tasks were still running"); Require(RegistrationPreparationThreads() == 0, "closing the shader preparation left its threads running"); + CloseRegistrationPreparation(); + bool ranHere = false; + SubmitRegistrationPreparation([&] { ranHere = std::this_thread::get_id() == caller; }); + SubmitLibraryPrebuild([&] { ranHere = ranHere && std::this_thread::get_id() == caller; }); + Require(ranHere && RegistrationPreparationThreads() == 0, "a closed preparation pool restarted after being joined"); } void StoppingJoinsCompareHelpers() { @@ -107,13 +119,14 @@ void ShutdownJoinsDrawCommitter() { Require(DrawPipeline::Committers() == 0, "a commit enqueued after the shutdown started a draw committer"); } -void ShutdownJoinsShaderCacheWriter() { +void ShutdownJoinsShaderCacheWriter(bool lateInitialization = false) { namespace Cache = ShaderRecompiler::ShaderDiskCache; const auto directory = std::filesystem::temp_directory_path() / ("aps5-shutdown-shader-cache-" + std::to_string(std::random_device{}())); SetEnvironment("ANYPS5_NO_SHADER_CACHE", "0"); SetEnvironment("ANYPS5_SHADER_CACHE_DIR", directory.string()); + if (lateInitialization) AgcDriverShutdown_nid_postfix(); Require(Cache::Enabled(), "the shader cache is not enabled"); - Require(Cache::Writers() == 1, "the shader cache started no writer"); + Require(Cache::Writers() == (lateInitialization ? 0u : 1u), "the shader cache writer did not respect the shutdown state"); const auto variant = std::make_shared(); const std::vector queued{std::byte{1}}; const std::vector late{std::byte{2}}; @@ -137,6 +150,7 @@ int main(int argc, char** argv) { else if (mode == "--compare-helpers") StoppingJoinsCompareHelpers(); else if (mode == "--draw-committer") ShutdownJoinsDrawCommitter(); else if (mode == "--shader-cache-writer") ShutdownJoinsShaderCacheWriter(); + else if (mode == "--late-shader-cache") ShutdownJoinsShaderCacheWriter(true); else ClosingStartsNoPreparation(); std::cout << "shutdown threads tests passed\n"; return 0; From 1f10c8afa5fcc3c5edfb9e48c9ed44102fda1160 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Fri, 16 Oct 2026 07:55:21 -0700 Subject: [PATCH 0519/1000] fix(agc): flush the async log within its exit bound when the driver stops The driver's static is destroyed from its DLL's atexit table at process exit, after ExitProcess has ended the async log's worker. Its stop() then flushed the log through Flush(), which takes the log's lock and waits for every queued line, so it never returned when the worker died holding that lock or with lines queued. #6800's bounded flush runs after it, because the log registers it while the driver is being constructed. stop() now uses that bounded flush, which reads the counters without the lock and gives up after EndFlushLimit, marking the log abandoned. A new shutdown test mode holds the log's lock from another thread and checks that the driver's shutdown still returns. --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 5 +++-- .../Execution/include/AsyncLog.hpp | 22 ++++++++++++++----- .../Execution/src/Driver/Lifecycle.cpp | 10 ++++++++- .../tests/execution/ShutdownThreads.cpp | 22 +++++++++++++++++++ 4 files changed, 50 insertions(+), 9 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index befe3b7e0d..2f6c6ff73e 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1828,9 +1828,10 @@ if(BUILD_TESTING) add_test(NAME agc_driver_shutdown_threads_draw_committer COMMAND agc_driver_shutdown_threads_tests --draw-committer) add_test(NAME agc_driver_shutdown_threads_shader_cache_writer COMMAND agc_driver_shutdown_threads_tests --shader-cache-writer) add_test(NAME agc_driver_shutdown_threads_late_shader_cache COMMAND agc_driver_shutdown_threads_tests --late-shader-cache) - set_tests_properties(agc_driver_shutdown_threads_joined agc_driver_shutdown_threads_compare_helpers agc_driver_shutdown_threads_draw_committer agc_driver_shutdown_threads_shader_cache_writer agc_driver_shutdown_threads_late_shader_cache PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" TIMEOUT 30) + add_test(NAME agc_driver_shutdown_threads_async_log_held COMMAND agc_driver_shutdown_threads_tests --async-log-held) + set_tests_properties(agc_driver_shutdown_threads_joined agc_driver_shutdown_threads_compare_helpers agc_driver_shutdown_threads_draw_committer agc_driver_shutdown_threads_shader_cache_writer agc_driver_shutdown_threads_late_shader_cache agc_driver_shutdown_threads_async_log_held PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" TIMEOUT 30) if(WIN32) - set_tests_properties(agc_driver_shutdown_threads_joined agc_driver_shutdown_threads_compare_helpers agc_driver_shutdown_threads_draw_committer agc_driver_shutdown_threads_shader_cache_writer agc_driver_shutdown_threads_late_shader_cache PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + set_tests_properties(agc_driver_shutdown_threads_joined agc_driver_shutdown_threads_compare_helpers agc_driver_shutdown_threads_draw_committer agc_driver_shutdown_threads_shader_cache_writer agc_driver_shutdown_threads_late_shader_cache agc_driver_shutdown_threads_async_log_held PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() add_test(NAME agc_driver_compute_user_data_calls_registered COMMAND agc_driver_compute_user_data_calls_tests --registered) set_tests_properties(agc_driver_compute_user_data_calls_registered PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/AsyncLog.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/AsyncLog.hpp index 8af0c00d39..06ecc6c52e 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/AsyncLog.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/AsyncLog.hpp @@ -171,6 +171,16 @@ public: return true; } + static void FlushAtEnd() { + auto& log = Get(); + if (log.abandoned.load(std::memory_order_acquire)) return; + if (!log.FlushFor(EndFlushLimit)) log.abandoned.store(true, std::memory_order_release); + } + + std::unique_lock Hold() { + return std::unique_lock(mutex); + } + private: struct Window { Clock::time_point first; @@ -197,12 +207,6 @@ private: LibcSetEndFlush_nid_no_patch(&AsyncLog::FlushAtEnd); } - static void FlushAtEnd() { - auto& log = Get(); - if (log.abandoned.load(std::memory_order_acquire)) return; - if (!log.FlushFor(EndFlushLimit)) log.abandoned.store(true, std::memory_order_release); - } - static int Sink(std::FILE* stream, const char* text, std::size_t length) { return Get().Write(stream, text, length); } @@ -383,6 +387,12 @@ inline void FlushAsyncLog_nid_no_patch() { AsyncLog::Get().Flush(); } +inline void FlushAsyncLogAtEnd_nid_no_patch() { + AsyncLog::FlushAtEnd(); +} + +std::unique_lock HoldAsyncLog(); + template void LogCoalescedStderr_nid_no_patch(const char* format, TArgs... args) { char small[1024]; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp index 96ef486538..91c50d6f3f 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp @@ -131,7 +131,7 @@ void Driver::stop() { Graphics::StopCompareHelpers(); ShaderRecompiler::ShaderDiskCache::Shutdown(); PerformanceTrace::StopOutput(); - FlushAsyncLog_nid_no_patch(); + FlushAsyncLogAtEnd_nid_no_patch(); StopProfileOutput_nid_no_patch(); stopped = true; } @@ -230,3 +230,11 @@ bool& Driver::onWorkerThread() { } } + +namespace AgcDriver { + +std::unique_lock HoldAsyncLog() { + return AsyncLog::Get().Hold(); +} + +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ShutdownThreads.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ShutdownThreads.cpp index 2da24d99d6..5c8ca06e57 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ShutdownThreads.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ShutdownThreads.cpp @@ -1,4 +1,6 @@ +#include "prx/libSceAgcDriver/Execution/include/AsyncLog.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp" #include "prx/libSceAgcDriver/Graphics/include/ParallelCompare.hpp" @@ -141,6 +143,25 @@ void ShutdownJoinsShaderCacheWriter(bool lateInitialization = false) { std::filesystem::remove_all(directory, error); } +void ShutdownPassesHeldAsyncLog() { + static_cast(AgcDriver::DriverDetail::Driver::Get()); + std::promise held; + std::promise release; + auto released = release.get_future(); + std::thread holder([&held, &released] { + const auto lock = AgcDriver::HoldAsyncLog(); + held.set_value(); + released.wait(); + }); + held.get_future().wait(); + auto shutdown = std::async(std::launch::async, [] { AgcDriverShutdown_nid_postfix(); }); + const bool returned = shutdown.wait_for(std::chrono::seconds(10)) == std::future_status::ready; + release.set_value(); + holder.join(); + shutdown.get(); + Require(returned, "the driver's shutdown waited for the async log's lock"); +} + } int main(int argc, char** argv) { @@ -151,6 +172,7 @@ int main(int argc, char** argv) { else if (mode == "--draw-committer") ShutdownJoinsDrawCommitter(); else if (mode == "--shader-cache-writer") ShutdownJoinsShaderCacheWriter(); else if (mode == "--late-shader-cache") ShutdownJoinsShaderCacheWriter(true); + else if (mode == "--async-log-held") ShutdownPassesHeldAsyncLog(); else ClosingStartsNoPreparation(); std::cout << "shutdown threads tests passed\n"; return 0; From 2de10e35ff994d687be2a4eea730b56094d5bcc3 Mon Sep 17 00:00:00 2001 From: pushkarreddyy <212133856+pushkarreddyy@users.noreply.github.com> Date: Wed, 14 Oct 2026 20:37:58 +0530 Subject: [PATCH 0520/1000] feat(libkernel): allow lowering rlim_cur for RLIMIT_NOFILE in setrlimit --- .../libs/prx/libkernel/System/src/Process.cpp | 31 ++++++++++++++----- core/libs/tests/GuestGetrlimit.cpp | 19 ++++++++++++ docs/dev/debt/libkernel.md | 2 +- 3 files changed, 43 insertions(+), 9 deletions(-) diff --git a/core/libs/prx/libkernel/System/src/Process.cpp b/core/libs/prx/libkernel/System/src/Process.cpp index d16c8c6055..bcfac7bc1a 100644 --- a/core/libs/prx/libkernel/System/src/Process.cpp +++ b/core/libs/prx/libkernel/System/src/Process.cpp @@ -31,6 +31,7 @@ #include #include #include +#include #include #include #include @@ -179,6 +180,11 @@ struct GuestResourceLimit { std::int64_t rlim_max; }; +namespace { +std::mutex openFileLimitMutex; +std::optional guestOpenFileLimit; +} + struct GuestResourceUsage { KernelTimeval ru_utime; KernelTimeval ru_stime; @@ -340,6 +346,13 @@ int APS5_VABI setrlimit_nid_postfix(int resource, const GuestResourceLimit* limi GuestResourceLimit current{}; getrlimit_nid_postfix(resource, ¤t); + if (resource == guestRlimitNofile) { + if (requested.rlim_max <= current.rlim_max && requested.rlim_cur <= requested.rlim_max) { + std::lock_guard lock(openFileLimitMutex); + guestOpenFileLimit = requested; + return 0; + } + } if (requested.rlim_cur != current.rlim_cur || requested.rlim_max != current.rlim_max) throw std::runtime_error("setrlimit: changing the limit of resource " + std::to_string(resource) + " is not supported"); return 0; @@ -547,17 +560,19 @@ int APS5_VABI getrlimit_nid_postfix(int resource, GuestResourceLimit* limit) { return 0; } case guestRlimitNofile: { + std::lock_guard lock(openFileLimitMutex); + if (!guestOpenFileLimit) { #ifdef _WIN32 - constexpr std::int64_t crtDescriptors = 8192; - limit->rlim_cur = crtDescriptors; - limit->rlim_max = crtDescriptors; + constexpr std::int64_t crtDescriptors = 8192; + guestOpenFileLimit = GuestResourceLimit{crtDescriptors, crtDescriptors}; #else - rlimit native{}; - if (::getrlimit(RLIMIT_NOFILE, &native) != 0) - throw std::system_error(errno, std::generic_category(), "getrlimit: getrlimit failed"); - limit->rlim_cur = toGuestLimit(native.rlim_cur); - limit->rlim_max = toGuestLimit(native.rlim_max); + rlimit native{}; + if (::getrlimit(RLIMIT_NOFILE, &native) != 0) + throw std::system_error(errno, std::generic_category(), "getrlimit: getrlimit failed"); + guestOpenFileLimit = GuestResourceLimit{toGuestLimit(native.rlim_cur), toGuestLimit(native.rlim_max)}; #endif + } + *limit = *guestOpenFileLimit; return 0; } case guestRlimitCpu: diff --git a/core/libs/tests/GuestGetrlimit.cpp b/core/libs/tests/GuestGetrlimit.cpp index b02f16a287..2f57340267 100644 --- a/core/libs/tests/GuestGetrlimit.cpp +++ b/core/libs/tests/GuestGetrlimit.cpp @@ -75,6 +75,25 @@ static void CheckOpenFileLimit() { Require(host.rlim_cur == RLIM_INFINITY ? limit.rlim_cur == GUEST_RLIM_INFINITY : limit.rlim_cur == static_cast(host.rlim_cur)); #endif Require(setrlimit_nid_postfix(GUEST_RLIMIT_NOFILE, &limit) == 0); + const ResourceLimit lowered{limit.rlim_max > 1 ? limit.rlim_max - 1 : 1, limit.rlim_max}; + Require(setrlimit_nid_postfix(GUEST_RLIMIT_NOFILE, &lowered) == 0); + const ResourceLimit updated = GuestLimit(GUEST_RLIMIT_NOFILE); + Require(updated.rlim_cur == lowered.rlim_cur); + Require(updated.rlim_max == lowered.rlim_max); + Require(setrlimit_nid_postfix(GUEST_RLIMIT_NOFILE, &limit) == 0); + const ResourceLimit restored = GuestLimit(GUEST_RLIMIT_NOFILE); + Require(restored.rlim_cur == limit.rlim_cur); + Require(restored.rlim_max == limit.rlim_max); + if (limit.rlim_max != GUEST_RLIM_INFINITY) { + const ResourceLimit raised{limit.rlim_cur, limit.rlim_max + 1}; + bool raisedRejected = false; + try { + setrlimit_nid_postfix(GUEST_RLIMIT_NOFILE, &raised); + } catch (const std::runtime_error&) { + raisedRejected = true; + } + Require(raisedRejected); + } } static void RequireError(int resource, ResourceLimit* limit, int error) { diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 5396a00c3a..3686135a66 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -158,7 +158,7 @@ - [Host CPU check](../../../core/libs/prx/libkernel/System/src/HostCpu.cpp) (libkernel) - when libkernel loads on a Linux or Windows x86-64 host, it logs which of the PS5 CPU's user-mode extensions CPUID reports and throws, naming the host CPU, if any extension is missing that LLVM selects from plain C/C++ for the PS5's Zen 2 (`-march=znver2`, [feature list](https://github.com/llvm/llvm-project/blob/llvmorg-19.1.7/llvm/lib/Target/X86/X86.td#L1459-L1513)): SSE to SSE4.2, SSSE3, POPCNT, LZCNT, BMI1, BMI2, MOVBE, CMPXCHG16B, LAHF/SAHF, AVX, AVX2, FMA, F16C, and the operating system's AVX state in XCR0 (#3719). SSE4A, CLZERO and SHA, which `--to-intel` converts, and AES, PCLMULQDQ, RDRAND, RDSEED, ADX, CLFLUSHOPT, CLWB, RDPID, XSAVEOPT, XSAVEC and PREFETCHW, which compilers emit only for intrinsics or explicit prefetches, are logged but not required: a title that executes one of them on a host without it still stops with an illegal instruction. macOS hosts are not checked: some Rosetta versions execute extensions without reporting them through CPUID, but support differs by host. ANDN and RDRAND fault on an M1 with macOS 15.3.1; see the [Rosetta conversion limits](relinker.md) - [machdep.tsc_freq](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) reports the host TSC frequency that `sceKernelGetTscFrequency` measures, as a 64-bit value like FreeBSD 11's [`sysctl_machdep_tsc_freq`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/x86/x86/tsc.c#L658-L676). FreeBSD lets root write it to change the TSC timecounter's frequency; a write here returns -1 with `EPERM`. PPSA99203 reads it at start-up and measures the TSC itself when the value is 100 MHz or less - [getrlimit](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - `RLIMIT_CPU`, `RLIMIT_FSIZE`, `RLIMIT_CORE`, `RLIMIT_SBSIZE`, `RLIMIT_VMEM`/`AS`, `RLIMIT_NPTS`, `RLIMIT_SWAP`, `RLIMIT_KQUEUES` and `RLIMIT_UMTXP` report infinity as in FreeBSD's process-0 defaults, and `RLIMIT_DATA` mirrors the host limit. `RLIMIT_NOFILE` reports, as both soft and hard limit, the number of descriptors after which AnyPS5 itself fails with `EMFILE`: on Windows the C runtime's 8192 low-level descriptors ([`_setmaxstdio`](https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/setmaxstdio)), which guest files, pipes and sockets all take, and on Linux the host's `RLIMIT_NOFILE`, whose soft limit libkernel raises to the hard limit at load. The console's limit is not known; XPSemu (PPSA97358) reads it in QEMU's `os_setup_limits` and raises the soft limit to the hard one when they differ. `RLIMIT_STACK`, `RLIMIT_RSS`, `RLIMIT_MEMLOCK` and `RLIMIT_NPROC` throw because no console values are established. -- [setrlimit](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) validates the resource and input in FreeBSD order, maps negative limits to infinity, and lowers a soft limit above the hard limit. A request succeeds only when its normalized values match `getrlimit`; changing limits throws because guest limit state is not stored. +- [setrlimit](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) validates the resource and input in FreeBSD order, maps negative limits to infinity, and lowers a soft limit above the hard limit. A request succeeds when its normalized values match `getrlimit`, or for `RLIMIT_NOFILE` when `rlim_max` does not exceed the current hard limit and `rlim_cur` does not exceed `rlim_max`, storing the updated limits for subsequent `getrlimit` queries; descriptor allocation enforcement of the lowered soft limit is not implemented, and changing other limits throws because guest limit state is not stored. Java 25 LAN / DNS (PPSA99869) sets `rlim_cur = MIN(0x100000, rlim_max)` during VM initialization - [execvp](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) throws: replacing the process image is not supported. PPSA85300 imports it and fails to load without it; its `fork` and `vfork` fail with `ENOSYS` ([libc_missing.c](https://github.com/ZiZc3/VLC/blob/f3253020a64368153233ede29183014c0b6c8979/ps5/compat/libc_missing.c#L290-L292)), so the child process that would call it is never created. - [socketpair](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) - the pair is the host's `AF_UNIX` pair on Linux and, on Windows, a loopback TCP connection for `SOCK_STREAM` or two connected UDP sockets for `SOCK_DGRAM`; `SOCK_SEQPACKET` throws on Windows. A datagram sent to a full receiver gets `ENOBUFS` on the console, waits or gets `EAGAIN` on Linux and is dropped on Windows, and on Windows a datagram `recv` after the peer closed reports `ECONNRESET` instead of the console's `ENOTCONN`, and a stream `send` after the peer closed reports `ECONNABORTED` instead of the console's `EPIPE`. `read` and `write` on a pair end go to the host like on every guest socket. `getpeername` on a pair end succeeds after the peer closed, where the console returns `ENOTCONN`, and `connect`, `bind` and `sendto` with an address on a pair end report an address-family error instead of the console's `EISCONN`. On Windows `poll` on a stream end reports `POLLHUP` without `POLLIN` after the peer's `shutdown(SHUT_WR)` or `close`, where the console reports `POLLIN`, with `POLLHUP` only after `close` - [UDP recv/recvfrom](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) and [sceNetRecv/sceNetRecvfrom](../../../core/libs/prx/libSceNet/Export.cpp) translate Windows WSAEMSGSIZE into the copied byte count for datagram sockets, including MSG_PEEK, and retain the sender address. This follows FreeBSD 11.4 recv(2) and Winsock's documented truncated-datagram behavior; console behavior and existing title impact are unverified. From e1efda12745d4ad95aaa8b273a9c4c5debae82f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Fri, 16 Oct 2026 17:05:56 +0200 Subject: [PATCH 0521/1000] fix(build): link the fixed-base loader test with ld.bfd wild 0.10.0 gives no canonical PLT entry to a DSO function whose address a non-PIE executable takes (wild-linker/wild#2469, fixed after the release by #2480), so the absolute personality pointer in .eh_frame names a local stub and every exception reaching main is unhandled. Co-Authored-By: Claude --- core/libs/prx/libkernel/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 21cf02d628..4593cae303 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -224,7 +224,7 @@ if(BUILD_TESTING) add_dependencies(guest_dynamic_loader_fixed_base_tests guest_module_fixture) set_target_properties(guest_dynamic_loader_fixed_base_tests PROPERTIES ENABLE_EXPORTS ON POSITION_INDEPENDENT_CODE OFF) target_compile_options(guest_dynamic_loader_fixed_base_tests PRIVATE -fno-pie) - target_link_options(guest_dynamic_loader_fixed_base_tests PRIVATE -no-pie) + target_link_options(guest_dynamic_loader_fixed_base_tests PRIVATE -no-pie -fuse-ld=bfd) add_test(NAME guest_dynamic_loader_fixed_base COMMAND guest_dynamic_loader_fixed_base_tests $) set_tests_properties(guest_dynamic_loader_fixed_base PROPERTIES WORKING_DIRECTORY "$") From 264e026c5dbe89ade7d607fb7e7204739f0d89d2 Mon Sep 17 00:00:00 2001 From: Josip Parlov Date: Fri, 16 Oct 2026 17:09:11 +0200 Subject: [PATCH 0522/1000] docs(usage): describe the texture cache budgets and APS5_TEXTURE_CACHE_MIB --- docs/user/USAGE.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index 72772bf384..4f32b7b37a 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -183,6 +183,12 @@ Recompiled shaders and the Vulkan pipeline cache are kept in a `shader_cache/` d Besides the recompiled shaders, the shader cache directory holds `pipelines.journal`: every Vulkan graphics pipeline the pipeline store kept and every compute pipeline a run built, with the specialized shader modules they were built from. The next run of the title builds those pipelines on background threads while it loads, so a draw or dispatch the earlier run already made does not compile its pipeline when it is first reached. The journal belongs to the GPU and driver that wrote it, like the driver's pipeline cache file next to it: a run on another GPU or driver version starts it over, and the driver's own pipeline cache decides how fast each pipeline is rebuilt. Two runs sharing the directory take turns on `pipelines.journal.lock`; a run that cannot lock it records nothing. `APS5_NO_PIPELINE_JOURNAL=1` disables recording and replay; `APS5_PIPELINE_JOURNAL_THREADS` sets the replay threads (half the CPU threads by default; `0` records without replaying), `APS5_PIPELINE_JOURNAL_ENTRIES` how many pipelines are replayed (the first recorded, which a run reaches first) and `APS5_PIPELINE_STORE_ENTRIES` how many graphics pipelines stay resident (1024 each), and `APS5_PIPELINE_JOURNAL_MAX_MIB` (1024) the file size at which recording stops. Deleting the file loses nothing but the head start. With `APS5_PROFILE_DRAW=1` a `[pipeline-journal]` line reports what was loaded, replayed and recorded. +### Texture cache budget + +Sampled textures and storage images stay in device memory in two caches that evict their least recently used entries when they grow past their budgets. The storage image budget is a quarter of the `VK_EXT_memory_budget` budget of the largest device-local heap, capped at a quarter of the heap's size or 2 GiB, whichever is larger, and never below 512 MiB. The sampled texture budget is what the reported budget leaves after the heap's use other than cached textures, that cap and an eighth of the budget, and never below 2 GiB. Both are read again every second; `[gpu] sampled texture cache budget` lines report them at start-up and when they change. `APS5_TEXTURE_CACHE_MIB=` fixes both budgets at `n` MiB, and the lines are then not printed. + +A fixed budget above the automatic one can exceed what the card holds next to the rest of the frame. Allocations then go to host memory (`[gpu] ... placed in host memory` lines), where the GPU reads them over PCIe. PPSA16064 on an RTX 4060 8 GB, whose automatic storage image budget is 1797 MiB, placed 868 and 878 MiB in host memory with `APS5_TEXTURE_CACHE_MIB=3072` and walked through its first level at 4.46 and 4.83 FPS; with the variable unset it placed none and ran the same walk at 6.48 and 6.07 FPS. Leave it unset unless a measurement on the card shows otherwise. + ### Storing GPU results at each flip GPU results in storage images and render targets stay on the GPU until something reads their memory. A title that frees such an image after waiting for its GPU work and reuses the memory can have its new data overwritten when the results are stored later ([TechnicalDebt](../dev/TechnicalDebt.md)). `APS5_STORE_AT_FLIP=1` stores every pending result when a display buffer is flipped, at the cost of one write-back per pending image and frame. Any other value stops the title at its first flip. From 45fcdbaa3c528ab7691edc0a62cd2221276b1c8b Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Fri, 16 Oct 2026 15:13:16 +0000 Subject: [PATCH 0523/1000] test(agc): isolate native index fixture cases --- .../tests/execution/SampleMultisampleTarget.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp index 94101e7a81..8ededa0651 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp @@ -337,6 +337,12 @@ void CheckNativeIndices(AgcDriver::VulkanDevice& device, const Block& source, co const std::array indices{0u, 1u, 2u}; std::memcpy(source.data + colorBytes, indices.data(), sizeof(indices)); } + { + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + AgcDriver::Graphics::NoteColorMetadataFill(destination.Address(), static_cast(AgcDriver::Graphics::MultisampledColorBytes(state.color)), 0u); + AgcDriver::Graphics::MaterializeMultisampleTargetClear(context, state.color); + } + CheckUniform(LoadSamples(device, destination), {}, "before the neighboring indexed draw"); Draw(device, state, neighbor, indexSize); device.WaitIdle(); CheckSamples(state, LoadSamples(device, destination), {}); @@ -355,6 +361,7 @@ void CheckNativeIndices(AgcDriver::VulkanDevice& device, const Block& source, co missed += refused ? 0u : 1u; } } + AgcDriver::Graphics::NoteColorMetadataFill(source.Address(), static_cast(colorBytes), FillOnes); CheckUniform(LoadSamples(device, source), Ones, "after native draw-input alias attempts"); std::puts("native index controls: shared-import=1 source-samples=1024 neighbor-triangle=passed"); Require(missed == 0, "native indices missed " + std::to_string(missed) + " resident-image refusals"); From d67d37bde822b8e2b4a65a987907709788b6c9ec Mon Sep 17 00:00:00 2001 From: Josip Parlov Date: Fri, 16 Oct 2026 17:32:43 +0200 Subject: [PATCH 0524/1000] docs(build): describe the [gputime] line and its command classes --- docs/dev/BUILD.md | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/docs/dev/BUILD.md b/docs/dev/BUILD.md index 2dfcffda8d..5c1d078ae2 100644 --- a/docs/dev/BUILD.md +++ b/docs/dev/BUILD.md @@ -173,6 +173,34 @@ The switch combines with `APS5_TRACE_FRAMES` or `APS5_TRACE_SECONDS`. The interv With `APS5_TRACE_SUMMARY_EVERY` set, the recorder writes a timestamp pair per batch for the whole run and every `PerformanceTimer` scope adds its time to the thread's totals, two clock reads per scope. A thread's totals are read while it runs, so a scope that straddles an interval boundary is counted in one interval or the other. +## GPU time per command class + +Set `APS5_PROFILE_GPU=1` (any value) to time the recorder's GPU work with timestamp queries and print one `[gputime]` line on stderr. `APS5_PROFILE_DRAW` alone does not print it: a batch then gets only its whole-batch timestamp pair. A timed range is a pair of bottom-of-pipe stamps that both wait for all earlier work, so a range is the time its own work adds after everything recorded before it. A batch holds at most 512 ranges, its whole-batch range included. The line is written when a batch's timestamps are read after its fence, at least 10 s after the previous line, and covers every batch read since then. + +The header fields: ` ms of GPU time` is the sum of the program-keyed ranges only (dispatches and buffer copies, see `by program`); draws and the driver's own commands are not in it. `in batches` counts the batches read. `batch ms first to last command` sums the whole-batch ranges, from the stamp written when the batch opens (it waits for the batches before it) to the stamp written at `Submit`. `classes ms` is the plain sum of every class range, including `present-blit`, which runs outside the batches. `all ranges ms` is, per batch, the union of its class and program ranges, with overlapping time counted once; `present-blit` is not in it. `untimed ms` is `batch` minus `all ranges`: batch time that no range covers, including the work of dropped ranges. ` presents` counts the `VulkanDevice::present` calls since the previous line. ` ranges dropped at the 512 cap` counts ranges refused because their batch was full. + +A class range covers a command together with its own barriers. In `VulkanDevice::CopyBuffer` the `copy` class range spans the leading barrier, the transfer and the trailing barrier, and the program-keyed range sits inside it around the transfer alone, so the transfer counts in both `GPU time` and `classes` but once in `all ranges`. A dispatch's leading and trailing barriers are ranges of their own classes, placed before and after its program range, not around it. `GPU time` plus `classes` can therefore exceed `batch`; compare `all ranges` with `batch` instead. + +`by program` lists up to 12 keys, highest time first, as `0x x ms`. A dispatch's key is its guest program address, or the program's `variantId` when the address is 0. A buffer copy's key is the program address of the copy kernel it replaces, or 0 for a PM4 DMA copy (opcode 0x50). `by class` lists every class with at least one range since the previous line as ` x ms MiB (per present x ms MiB)`; the per-present figures are 0 when there was no present. MiB is the byte count each range passes to `EndGpuTiming`. The classes, with the function that opens each range: + +- `dispatch-lead`: a dispatch's leading barrier, when it is recorded rather than merged into the previous trailing barrier (`VulkanDevice::recordDispatch`). 0 MiB. +- `dispatch-trail`: a dispatch's trailing barrier (`recordDispatch`). 0 MiB. +- `indirect-args`: the barrier that makes an indirect dispatch's host-imported group counts visible to the indirect read (`recordDispatch`). 12 bytes per range. +- `label-run`: a run of GPU label stores (`vkCmdUpdateBuffer`, plus the queued occlusion dump folds of a per-batch run) with its barriers (`Recorder::RecordStore` for inline runs, `Recorder::closeStoreRun` for per-batch runs). Stored bytes. +- `fill`: a guest fill of host-imported memory, as `vkCmdFillBuffer` or pattern copies, with its barriers (`VulkanDevice::FillBuffer`). Filled bytes. +- `fill-clear`: a fill done as a `vkCmdClearColorImage` of a storage image, between two layout barriers (`StorageTexture::FillClear`). Filled bytes. +- `copy`: the buffer copy of `VulkanDevice::CopyBuffer`, a whole-surface image copy (`StorageTexture::CopyFrom`) and the depth plane copies to and from storage images through a staging buffer (`copyPlane` and the layered copy in `DepthSurface.cpp`). Copied bytes. +- `staging-in`: a build's copies of guest buffer regions into staging or host-copy buffers, between two barriers (`GuestBufferMemory::recordGpuCopies`). Copied bytes. +- `staging-out`: the copies of shader-written sub-ranges back into the import (`GuestBufferMemory::RecordCopyBacks`). Copied bytes. +- `draw`: for a lean draw, from its first barrier to the end of its render pass, so draws that continue the pass fall inside one range (`recordDraw`, ended by `Recorder::endOpenRenderPass`); for a recorded draw with layout transitions, from its first barrier to its download barrier (`Draw`). 0 MiB. +- `storage-upload`: detiles of guest bytes into a storage image, whole (`StorageTexture::upload`) or by changed runs (`StorageTexture::uploadWindows`), and pending units copied from an alias image (`StorageTexture::borrowUnits`). Uploaded bytes. +- `storage-writeback`: retiles of a storage image back to guest memory, by windows (`StorageTexture::writeBackWindows`) or by layers (`StorageTexture::writeBackLayers`). Stored bytes. +- `dcc-clear`: a `vkCmdClearColorImage` of a storage image whose DCC keys read as a clear, at upload (`StorageTexture::upload`) or for a fill of its key bytes (`StorageTexture::clearByKeysFill`, with `APS5_KEYS_FILL_CLEAR=1`). Surface bytes. +- `dcc-keys`: queued DCC key stores, as `0xff` fills and seed copies between two barriers (`Recorder::recordKeyStores`). Key bytes. +- `present-blit`: the presentation command buffer from its first to its last command, timed in its own two-query pool (`VulkanDevice::present`) and added when its slot retires (`Recorder::AddGpuTiming`). Width x height x 4 bytes of the swapchain extent. +- `shadow-publish`: copies of unit shadow slabs into the import, with their barriers (`recordCopies` in `UnitShadow.cpp`, reached from `PublishShadow`). Copied bytes. +- `template-refresh`: the `vkCmdUpdateBuffer` updates of a cached dispatch's changed data words, as one range from the first update to the last (`ShaderResources::refreshData`, called from `recordDispatch`). Refreshed bytes. + ## Driver thread priority Set `APS5_THREAD_PRIORITY=high` to raise the AGC driver's queue workers and the video output presenter above the title's threads. On Linux the threads get nice `APS5_THREAD_NICE` (default -10) through rtkit; on Windows they get `THREAD_PRIORITY_HIGHEST`. `APS5_THREAD_PRIORITY=rt` asks rtkit for real-time priority `APS5_THREAD_RT` (default 10, at most rtkit's `MaxRealtimePriority`) under rtkit's policy, `SCHED_RR` by default; on Windows it sets `THREAD_PRIORITY_TIME_CRITICAL`. rtkit only grants it when the process's hard `RLIMIT_RTTIME` is at most its `RTTimeUSecMax` (200000 us by default), so a hard limit above `APS5_THREAD_RTTIME_US` (default 200000, capped at `RTTimeUSecMax`) is lowered to it and the soft limit to at most half of it. The hard limit cannot be raised again and stays lowered for the whole process even if rtkit then refuses; see [technical debt](TechnicalDebt.md#functional). There is no fallback between the modes: when rtkit or `SetThreadPriority` refuses, a queue worker reports the refusal as a driver failure, which the next submission, wait or shutdown rethrows, and the presenter's start throws it. Each raised thread logs its priority once as `[thread-priority]`. The setting is `off` by default; a value other than `off`, `high` or `rt` is reported the same way as a refusal. From aa4018fdf25ad0eca44bce77d30e040ea2e2f423 Mon Sep 17 00:00:00 2001 From: Scramblrrrr Date: Fri, 16 Oct 2026 12:33:22 -0300 Subject: [PATCH 0525/1000] fix(shader): poison unsupported biased volume table entries --- .../libSceAgcDriver/tests/ShaderMemory.cpp | 44 +++++++++++++++++++ .../Optimization/src/ResourceMaterializer.cpp | 2 +- 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 86f21811f1..795cf5edfa 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -5122,6 +5122,49 @@ void verifyVolumeArrayLoadMode() { expectFailure([&] { static_cast(ResourceMaterializer::RuntimeImageMode(store, descriptor, ResourceMaterializer::RuntimeImageModes(store))); }, "incompatible with the static runtime image interface", "2D-array image atomic on a 3D T# was accepted"); } +void verifyVolumeBiasTableRejection() { + using namespace ShaderRecompiler; + namespace Abi = ImageTableAbi; + ImageResource image{}; + image.resourceClass = ImageResourceClass::Sampled; + image.dimension = RdnaImageDimension::Dim2D; + image.singleMipPlaneBiasCompatible = true; + const DescriptorValue volume{{0x100u, TableFormat8888UNorm << 20u, 0u, (TableType3D << 28u) | 0xfacu, 0u, 0u, 0u, 0u}, 8u, 0xffu}; + const auto directModes = ResourceMaterializer::RuntimeImageModes(image); + require(directModes[ResourceMaterializer::RuntimeImageMode(image, volume, directModes)].dimension == RdnaImageDimension::Dim3D, "volume bias: a supported direct one-slice volume was rejected"); + image.table = 0u; + image.tableOperation = TableOperation::Supported; + ShaderInfo info{}; + info.images.push_back(image); + info.runtimeImageModes.push_back(ResourceMaterializer::RuntimeImageModes(image)); + ResourceSnapshot snapshot{}; + snapshot.tables.tables.resize(1u); + auto& column = snapshot.tables.tables.front(); + column.records = 2u; + column.keys = 2u; + column.codes = {0u, 1u}; + auto plane = volume; + plane.dwords[3] = (TableType2D << 28u) | 0xfacu; + for (const auto [word, bits] : {std::pair{4u, 1u}, std::pair{3u, 1u << 12u}, std::pair{3u, 1u << 16u}, std::pair{5u, 1u << 4u}}) { + auto invalid = volume; + invalid.dwords[word] |= bits; + snapshot.tables.words = {plane, invalid}; + const auto resolved = ResourceMaterializer::ResolveImageTables(info, snapshot); + const auto mapStart = resolved.map.at(Abi::TableHeader(0u) + Abi::TableMapStart); + const auto good = resolved.map.at(mapStart); + const auto bad = resolved.map.at(mapStart + 1u); + require(resolved.elements.size() == 1u && (resolved.map.at(good + Abi::ImageRecordFlags) & Abi::PoisonFlag) == 0u, "volume bias: an unsupported table neighbour prevented the valid plane binding"); + const auto fault = resolved.map.at(bad + Abi::ImageRecordFlags); + require((fault & Abi::PoisonFlag) != 0u && resolved.faults == 1u, "volume bias: an unsupported volume was not marked to fault on selection"); + const auto& poison = resolved.poison.at(fault & ~Abi::PoisonFlag); + require(poison.reason == Abi::PoisonReason::Dimension && poison.words == invalid.dwords, "volume bias: the table fault lost its reason or descriptor"); + image.table = NoTable; + const auto match = ResourceMaterializer::TryRuntimeImageMode(image, invalid, directModes); + require(!match.mode.has_value() && match.reason == Abi::PoisonReason::Dimension, "volume bias: an unsupported direct descriptor selected a mode"); + expectFailure([&] { static_cast(ResourceMaterializer::RuntimeImageMode(image, invalid, directModes)); }, "one slice and mip level zero only", "volume bias: direct use of an unsupported descriptor was accepted"); + } +} + void verifySrgbDepthCompareModes() { using namespace ShaderRecompiler; for (const auto decodeFormats : {0u, 1u, 2u, 3u}) { @@ -5236,6 +5279,7 @@ int main(int argc, char** argv) { verifyGuardedNullPointers(); verifyCubeDepthCompareMode(); verifyVolumeArrayLoadMode(); + verifyVolumeBiasTableRejection(); verifySrgbDepthCompareModes(); verifyQuadMaskNeighbourReads(); verifyCoherentGds(); diff --git a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp index ff8686cb2f..d1377c2cc6 100644 --- a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp @@ -1423,7 +1423,7 @@ RuntimeImageModeMatch ResourceMaterializer::TryRuntimeImageMode(const ImageResou } } if (image.singleMipPlaneBiasCompatible && image.dimension == RdnaImageDimension::Dim2D && decoded.dimension == RdnaImageDimension::Dim3D) { - if ((descriptor.dwords[4] & 0x1fffu) != 0u || (descriptor.dwords[3] & 0xff000u) != 0u || (descriptor.dwords[5] & 0xf0u) != 0u) throw std::runtime_error("2D biased sampling of a volume requires one slice and mip level zero only"); + if ((descriptor.dwords[4] & 0x1fffu) != 0u || (descriptor.dwords[3] & 0xff000u) != 0u || (descriptor.dwords[5] & 0xf0u) != 0u) return reject(PoisonReason::Dimension, "2D biased sampling of a volume requires one slice and mip level zero only"); } for (std::uint32_t index = 0u; index < modes.size(); ++index) { const auto& mode = modes[index]; From a4485bd8216075064799fd023d6147a326961be3 Mon Sep 17 00:00:00 2001 From: Ondra97 <47064087+Ondra97@users.noreply.github.com> Date: Fri, 16 Oct 2026 17:40:21 +0200 Subject: [PATCH 0526/1000] fix(agc): release unpublished buffer slabs on allocation failure --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 5 + .../Graphics/src/BufferPool.cpp | 11 +- .../tests/AllocationFailure.cpp | 29 ++++ .../tests/AllocationFailure.hpp | 10 ++ .../prx/libSceAgcDriver/tests/BufferPool.cpp | 76 ++++++++- .../tests/execution/PooledBufferRange.cpp | 152 +++++++++++++++++- 6 files changed, 278 insertions(+), 5 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/AllocationFailure.cpp create mode 100644 core/libs/prx/libSceAgcDriver/tests/AllocationFailure.hpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index f112af6ade..6bbb1e6eb8 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -797,6 +797,7 @@ if(BUILD_TESTING) endif() add_test_executable(agc_buffer_pool_tests EXCLUDE_FROM_ALL tests/BufferPool.cpp) +target_sources(agc_buffer_pool_tests PRIVATE Graphics/src/BufferPool.cpp tests/AllocationFailure.cpp) target_include_directories(agc_buffer_pool_tests PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/core/shader/recompiler ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR}) target_link_libraries(agc_buffer_pool_tests PRIVATE libSceAgcDriver libc) set_target_properties(agc_buffer_pool_tests PROPERTIES CXX_EXTENSIONS OFF) @@ -1407,6 +1408,10 @@ foreach(agcExecutionTestName IN LISTS ANYPS5_AGC_EXECUTION_TESTS) endif() endforeach() +if(NOT ANYPS5_AGC_EXECUTION_TESTS OR "PooledBufferRange" IN_LIST ANYPS5_AGC_EXECUTION_TESTS) + target_sources(agc_driver_pooled_buffer_range_tests PRIVATE Graphics/src/BufferPool.cpp tests/AllocationFailure.cpp) +endif() + target_link_libraries(agc_driver_gpu_map_on_fiber_tests PRIVATE libSceFiber) target_link_libraries(agc_driver_preparation_scope_settle_tests PRIVATE libSceAgc) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp index 6ec77770be..ef40002b7f 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp @@ -106,7 +106,11 @@ SlabSlot BufferPool::TakeSlot(const Context& context, std::uint32_t memoryType, if (!slab.available.empty()) return takeFrom(*slab.available.back(), slab); } const auto blockBytes = SlabBlockBytes(capacity); - auto block = std::make_unique(); + const auto release = [this](SlabBlock* block) noexcept { + if (block->memory != VK_NULL_HANDLE) freeBlock(*block); + delete block; + }; + std::unique_ptr block(new SlabBlock, release); block->slotBytes = capacity; block->slab = key; VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; @@ -118,7 +122,6 @@ SlabSlot BufferPool::TakeSlot(const Context& context, std::uint32_t memoryType, if ((context.memory.memoryTypes[memoryType].propertyFlags & VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT) != 0) { void* mapping = nullptr; const auto mapped = mapMemory(device, block->memory, 0, VK_WHOLE_SIZE, 0, &mapping); - if (mapped != VK_SUCCESS) freeMemory(device, block->memory, nullptr); Check(mapped, "vkMapMemory buffer slab"); block->mapping = static_cast(mapping); } @@ -127,8 +130,10 @@ SlabSlot BufferPool::TakeSlot(const Context& context, std::uint32_t memoryType, for (std::uint32_t index = slots; index-- != 0;) block->free.push_back(index); std::lock_guard lock(slabMutex); auto& slab = slabs[key]; + if (slab.available.size() == slab.available.capacity()) slab.available.reserve(std::max(1, slab.available.capacity() * 2)); auto& inserted = *block; - slabBlocks.emplace(inserted.memory, std::move(block)); + const auto owner = slabBlocks.try_emplace(inserted.memory).first; + owner->second.reset(block.release()); slab.available.push_back(&inserted); ++slab.emptyBlocks; return takeFrom(*slab.available.back(), slab); diff --git a/core/libs/prx/libSceAgcDriver/tests/AllocationFailure.cpp b/core/libs/prx/libSceAgcDriver/tests/AllocationFailure.cpp new file mode 100644 index 0000000000..c0b5dbeab4 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/AllocationFailure.cpp @@ -0,0 +1,29 @@ +#include "AllocationFailure.hpp" +#include +#include + +namespace { + +thread_local int allocationCountdown = -1; + +} + +void AgcDriver::Tests::SetAllocationFailureCountdown(int countdown) noexcept { + allocationCountdown = countdown; +} + +void* operator new(std::size_t bytes) { + if (allocationCountdown == 0) { + allocationCountdown = -1; + throw std::bad_alloc(); + } + if (allocationCountdown > 0) --allocationCountdown; + if (auto* allocation = std::malloc(bytes == 0 ? 1 : bytes)) return allocation; + throw std::bad_alloc(); +} + +void* operator new[](std::size_t bytes) { return ::operator new(bytes); } +void operator delete(void* allocation) noexcept { std::free(allocation); } +void operator delete[](void* allocation) noexcept { std::free(allocation); } +void operator delete(void* allocation, std::size_t) noexcept { std::free(allocation); } +void operator delete[](void* allocation, std::size_t) noexcept { std::free(allocation); } diff --git a/core/libs/prx/libSceAgcDriver/tests/AllocationFailure.hpp b/core/libs/prx/libSceAgcDriver/tests/AllocationFailure.hpp new file mode 100644 index 0000000000..947665af55 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/AllocationFailure.hpp @@ -0,0 +1,10 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_TESTS_ALLOCATIONFAILURE_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_TESTS_ALLOCATIONFAILURE_HPP + +namespace AgcDriver::Tests { + +void SetAllocationFailureCountdown(int countdown) noexcept; + +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/tests/BufferPool.cpp b/core/libs/prx/libSceAgcDriver/tests/BufferPool.cpp index f0ac0664ab..5b9e3ce174 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BufferPool.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BufferPool.cpp @@ -1,5 +1,6 @@ #include "prx/libSceAgcDriver/Graphics/include/BufferPool.hpp" #include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" +#include "AllocationFailure.hpp" #include #include #include @@ -7,15 +8,19 @@ #include #include #include +#include #include #include #include #include using namespace AgcDriver::Graphics; +using AgcDriver::Tests::SetAllocationFailureCountdown; namespace { +thread_local int failAfterMap = -1; + int failures = 0; void Expect(bool condition, const std::string& what) { @@ -44,6 +49,9 @@ struct MockDevice { VkDeviceSize limit = 0; VkResult pressureResult = VK_ERROR_OUT_OF_DEVICE_MEMORY; std::uint64_t attempts = 0; + VkResult mapResult = VK_SUCCESS; + unsigned maps = 0; + unsigned unmaps = 0; }; MockDevice mock; @@ -88,11 +96,15 @@ VKAPI_ATTR VkResult VKAPI_CALL mockBindBufferMemory(VkDevice, VkBuffer buffer, V } VKAPI_ATTR VkResult VKAPI_CALL mockMapMemory(VkDevice, VkDeviceMemory memory, VkDeviceSize, VkDeviceSize, VkMemoryMapFlags, void** data) { + if (mock.mapResult != VK_SUCCESS) return mock.mapResult; *data = mock.hostMemory.at(memory).data(); + ++mock.maps; + SetAllocationFailureCountdown(failAfterMap); + failAfterMap = -1; return VK_SUCCESS; } -VKAPI_ATTR void VKAPI_CALL mockUnmapMemory(VkDevice, VkDeviceMemory) {} +VKAPI_ATTR void VKAPI_CALL mockUnmapMemory(VkDevice, VkDeviceMemory) { ++mock.unmaps; } VKAPI_ATTR void VKAPI_CALL mockDestroyBuffer(VkDevice, VkBuffer buffer, const VkAllocationCallbacks*) { Expect(mock.liveBuffers.erase(buffer) == 1, "an unknown buffer was destroyed"); @@ -326,6 +338,66 @@ void SmallDeviceClasses() { Expect(nextArgs.Handle() == tiny && mock.allocations == made, "a 20-byte device request did not reuse the retained 256-byte buffer of its class"); } +void SlabMetadataFailure() { + unsigned rejected = 0; + for (int point = 0; point < 16; ++point) { + mock = MockDevice{}; + bool failed = false; + { + const auto context = mockContext(); + BufferPool pool(context); + SlabSlot slot{}; + failAfterMap = point; + try { + slot = pool.TakeSlot(context, 1, 512, false); + } catch (const std::bad_alloc&) { + failed = true; + ++rejected; + } + SetAllocationFailureCountdown(-1); + failAfterMap = -1; + if (failed) { + Expect(mock.allocations == 1, "metadata failure did not follow a successful Vulkan allocation"); + Expect(mock.frees == 1 && mock.liveBytes == 0, "metadata failure leaked the slab's device memory at allocation " + std::to_string(point)); + Expect(mock.maps == 1 && mock.unmaps == 1, "metadata failure did not unmap its slab"); + Expect(pool.SlabBlocks() == 0, "metadata failure published an unusable slab"); + slot = pool.TakeSlot(context, 1, 512, false); + } + Expect(slot.mapping != nullptr, "slab retry did not return mapped memory"); + pool.PutSlot(slot.memory, slot.offset); + } + Expect(mock.liveBytes == 0 && mock.allocations == mock.frees, "metadata failure retained device memory after pool destruction"); + if (!failed) { + Expect(rejected != 0, "slab metadata allocation failures were not exercised"); + std::printf("slab metadata failures checked: %u\n", rejected); + return; + } + } + Expect(false, "slab allocation never reached the success control"); +} + +void SlabMapFailure() { + mock = MockDevice{}; + { + const auto context = mockContext(); + BufferPool pool(context); + mock.mapResult = VK_ERROR_MEMORY_MAP_FAILED; + bool failed = false; + try { + static_cast(pool.TakeSlot(context, 1, 512, false)); + } catch (const std::runtime_error& error) { + failed = std::strstr(error.what(), "vkMapMemory buffer slab") != nullptr; + } + Expect(failed, "the slab mapping failure was not reported"); + Expect(mock.allocations == 1 && mock.frees == 1 && mock.liveBytes == 0, "a failed slab mapping did not release exactly its allocation"); + Expect(mock.maps == 0 && mock.unmaps == 0 && pool.SlabBlocks() == 0, "a failed mapping published a slab or unmapped invalid memory"); + mock.mapResult = VK_SUCCESS; + const auto slot = pool.TakeSlot(context, 1, 512, false); + pool.PutSlot(slot.memory, slot.offset); + } + Expect(mock.liveBytes == 0 && mock.allocations == mock.frees && mock.maps == mock.unmaps, "a failed mapping made a later slab leak"); +} + void Slabs() { Expect(BufferPool::SlabEligible(512, 256, 512, 64), "a size class with a smaller alignment and atom was refused a slab"); Expect(!BufferPool::SlabEligible(512, 1024, 512, 64), "a size class below its alignment was given a slab"); @@ -538,6 +610,8 @@ int main() { Budget(); AddressAndHostUnchanged(); SmallDeviceClasses(); + SlabMetadataFailure(); + SlabMapFailure(); Slabs(); for (const auto result : {VK_ERROR_OUT_OF_DEVICE_MEMORY, VK_ERROR_OUT_OF_HOST_MEMORY}) { AllocationRecovery(result, false); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PooledBufferRange.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PooledBufferRange.cpp index 42ac67075a..f30cddb737 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PooledBufferRange.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PooledBufferRange.cpp @@ -2,6 +2,7 @@ #include "prx/libSceAgcDriver/Graphics/include/BufferPool.hpp" #include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" #include "VulkanTestDevice.hpp" +#include "../AllocationFailure.hpp" #include #include #include @@ -12,6 +13,7 @@ #include #include #include +#include #include #include @@ -21,6 +23,7 @@ using AgcDriver::Graphics::BufferPool; using AgcDriver::Graphics::Check; using AgcDriver::Graphics::Context; using AgcDriver::Graphics::Require; +using AgcDriver::Tests::SetAllocationFailureCountdown; constexpr std::uint32_t Stale = 0xababababu; constexpr std::uint32_t Fresh = 0x11111111u; @@ -30,6 +33,72 @@ constexpr std::uint32_t PastEnd = 28; PFN_vkGetDeviceProcAddr RealDeviceProc = nullptr; PFN_vkCreateBuffer RealCreateBuffer = nullptr; +PFN_vkAllocateMemory RealAllocateMemory = nullptr; +PFN_vkFreeMemory RealFreeMemory = nullptr; +PFN_vkMapMemory RealMapMemory = nullptr; +PFN_vkUnmapMemory RealUnmapMemory = nullptr; + +struct Allocation { + VkDeviceMemory memory = VK_NULL_HANDLE; + VkDeviceSize bytes = 0; + bool mapped = false; +}; + +std::array Allocations{}; +VkDeviceSize LiveBytes = 0; +unsigned AllocationErrors = 0; +int FailAfterAllocation = -1; +int FailAfterMap = -1; + +VKAPI_ATTR VkResult VKAPI_CALL CountedAllocateMemory(VkDevice device, const VkMemoryAllocateInfo* info, const VkAllocationCallbacks* allocator, VkDeviceMemory* memory) { + const auto result = RealAllocateMemory(device, info, allocator, memory); + if (result == VK_SUCCESS) { + const auto entry = std::find_if(Allocations.begin(), Allocations.end(), [](const Allocation& allocation) { return allocation.memory == VK_NULL_HANDLE; }); + if (entry == Allocations.end()) ++AllocationErrors; + else *entry = {*memory, info->allocationSize, false}; + LiveBytes += info->allocationSize; + if (FailAfterAllocation >= 0) { + SetAllocationFailureCountdown(FailAfterAllocation); + FailAfterAllocation = -1; + } + } + return result; +} + +VKAPI_ATTR void VKAPI_CALL CountedFreeMemory(VkDevice device, VkDeviceMemory memory, const VkAllocationCallbacks* allocator) { + const auto entry = std::find_if(Allocations.begin(), Allocations.end(), [&](const Allocation& allocation) { return allocation.memory == memory; }); + if (entry == Allocations.end() || memory == VK_NULL_HANDLE) { + ++AllocationErrors; + return; + } + LiveBytes -= entry->bytes; + *entry = {}; + RealFreeMemory(device, memory, allocator); +} + +VKAPI_ATTR VkResult VKAPI_CALL CountedMapMemory(VkDevice device, VkDeviceMemory memory, VkDeviceSize offset, VkDeviceSize bytes, VkMemoryMapFlags flags, void** mapping) { + const auto result = RealMapMemory(device, memory, offset, bytes, flags, mapping); + if (result == VK_SUCCESS) { + const auto entry = std::find_if(Allocations.begin(), Allocations.end(), [&](const Allocation& allocation) { return allocation.memory == memory; }); + if (entry == Allocations.end()) ++AllocationErrors; + else entry->mapped = true; + if (FailAfterMap >= 0) { + SetAllocationFailureCountdown(FailAfterMap); + FailAfterMap = -1; + } + } + return result; +} + +VKAPI_ATTR void VKAPI_CALL CountedUnmapMemory(VkDevice device, VkDeviceMemory memory) { + const auto entry = std::find_if(Allocations.begin(), Allocations.end(), [&](const Allocation& allocation) { return allocation.memory == memory; }); + if (entry == Allocations.end() || !entry->mapped) { + ++AllocationErrors; + return; + } + entry->mapped = false; + RealUnmapMemory(device, memory); +} std::vector& CreatedSizes() { static std::vector sizes; @@ -43,6 +112,10 @@ VKAPI_ATTR VkResult VKAPI_CALL CountedCreateBuffer(VkDevice device, const VkBuff VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL CountingDeviceProc(VkDevice device, const char* name) { if (std::strcmp(name, "vkCreateBuffer") == 0) return reinterpret_cast(&CountedCreateBuffer); + if (std::strcmp(name, "vkAllocateMemory") == 0) return reinterpret_cast(&CountedAllocateMemory); + if (std::strcmp(name, "vkFreeMemory") == 0) return reinterpret_cast(&CountedFreeMemory); + if (std::strcmp(name, "vkMapMemory") == 0) return reinterpret_cast(&CountedMapMemory); + if (std::strcmp(name, "vkUnmapMemory") == 0) return reinterpret_cast(&CountedUnmapMemory); return RealDeviceProc(device, name); } @@ -82,7 +155,9 @@ public: VkPhysicalDeviceProperties selected{}; properties(context.physical, &selected); context.limits = selected.limits; - std::printf("device %s\n", selected.deviceName); + std::printf("device %s vendor=0x%x device=0x%x driver=0x%x\n", selected.deviceName, selected.vendorID, selected.deviceID, selected.driverVersion); + const auto* requested = std::getenv("ANYPS5_GPU"); + Require(requested == nullptr || std::strstr(selected.deviceName, requested) != nullptr, "selected device does not match ANYPS5_GPU"); VkPhysicalDeviceFeatures supported{}; function("vkGetPhysicalDeviceFeatures")(context.physical, &supported); Require(supported.robustBufferAccess == VK_TRUE, "the device lacks robustBufferAccess"); @@ -108,6 +183,11 @@ public: RealDeviceProc = function("vkGetDeviceProcAddr"); RealCreateBuffer = reinterpret_cast(RealDeviceProc(context.device, "vkCreateBuffer")); Require(RealCreateBuffer != nullptr, "missing vkCreateBuffer"); + RealAllocateMemory = reinterpret_cast(RealDeviceProc(context.device, "vkAllocateMemory")); + RealFreeMemory = reinterpret_cast(RealDeviceProc(context.device, "vkFreeMemory")); + RealMapMemory = reinterpret_cast(RealDeviceProc(context.device, "vkMapMemory")); + RealUnmapMemory = reinterpret_cast(RealDeviceProc(context.device, "vkUnmapMemory")); + Require(RealAllocateMemory != nullptr && RealFreeMemory != nullptr && RealMapMemory != nullptr && RealUnmapMemory != nullptr, "missing Vulkan memory functions"); context.deviceProc = &CountingDeviceProc; function("vkGetPhysicalDeviceMemoryProperties")(context.physical, &context.memory); context.formatProperties = function("vkGetPhysicalDeviceFormatProperties"); @@ -137,6 +217,13 @@ private: void release() noexcept { if (context.pool != VK_NULL_HANDLE) context.Function("vkDestroyCommandPool")(context.device, context.pool, nullptr); context.bufferPool.reset(); + for (auto& allocation : Allocations) { + if (allocation.memory == VK_NULL_HANDLE) continue; + if (allocation.mapped) RealUnmapMemory(context.device, allocation.memory); + RealFreeMemory(context.device, allocation.memory, nullptr); + LiveBytes -= allocation.bytes; + allocation = {}; + } if (context.device != VK_NULL_HANDLE) function("vkDestroyDevice")(context.device, nullptr); if (instance != VK_NULL_HANDLE) function("vkDestroyInstance")(instance, nullptr); if (library != nullptr) SDL_UnloadObject(library); @@ -148,6 +235,66 @@ private: Context context{}; }; +unsigned SlabMetadataFailure(const Context& context, std::uint32_t memoryType) { + const bool mapped = (context.memory.memoryTypes[memoryType].propertyFlags & VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT) != 0; + const auto initialBytes = LiveBytes; + unsigned rejected = 0; + unsigned failures = 0; + for (int point = 0; point < 16; ++point) { + const auto before = LiveBytes; + bool failed = false; + { + BufferPool pool(context); + AgcDriver::Graphics::SlabSlot slot{}; + (mapped ? FailAfterMap : FailAfterAllocation) = point; + try { + slot = pool.TakeSlot(context, memoryType, 512, false); + } catch (const std::bad_alloc&) { + failed = true; + ++rejected; + } + SetAllocationFailureCountdown(-1); + FailAfterMap = -1; + FailAfterAllocation = -1; + if (failed) { + const auto retained = LiveBytes - before; + std::printf("slab failure type=%u mapped=%u allocation=%d retained=%llu blocks=%zu\n", memoryType, mapped ? 1u : 0u, point, static_cast(retained), pool.SlabBlocks()); + if (retained != 0 || pool.SlabBlocks() != 0) ++failures; + slot = pool.TakeSlot(context, memoryType, 512, false); + } + Require((slot.mapping != nullptr) == mapped, "slab retry returned the wrong mapping state"); + pool.PutSlot(slot.memory, slot.offset); + } + if (LiveBytes != before) ++failures; + if (!failed) { + Require(rejected != 0, "native slab metadata failures were not exercised"); + std::printf("slab metadata type=%u failures=%u leaked=%llu\n", memoryType, rejected, static_cast(LiveBytes - initialBytes)); + return failures; + } + } + throw std::runtime_error("native slab failure injection never reached its success control"); +} + +void SlabMetadataFailures(const Context& context) { + unsigned failures = 0; + bool host = false; + bool device = false; + for (std::uint32_t type = 0; type < context.memory.memoryTypeCount; ++type) { + const auto flags = context.memory.memoryTypes[type].propertyFlags; + if (!host && (flags & HostMemory) == HostMemory) { + failures += SlabMetadataFailure(context, type); + host = true; + } else if (!device && (flags & VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT) != 0 && (flags & VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT) == 0) { + failures += SlabMetadataFailure(context, type); + device = true; + } + } + Require(host, "no coherent host-visible memory type for the slab failure test"); + std::printf("slab metadata total leaked bytes=%llu\n", static_cast(LiveBytes)); + Require(AllocationErrors == 0, "slab cleanup used an invalid memory handle or mapping"); + Require(failures == 0, "slab metadata allocation failure leaked native Vulkan memory or published an unusable block"); +} + void Emit(std::vector& words, std::uint32_t opcode, std::initializer_list operands) { words.push_back(static_cast((operands.size() + 1u) << 16u) | opcode); words.insert(words.end(), operands); @@ -332,8 +479,11 @@ int main() { std::printf("skipped, no usable Vulkan device: %s\n", error.what()); return VulkanTestSkipped; } + SlabMetadataFailures(device->GetContext()); RobustReadPastEnd(device->GetContext()); ExactSizes(device->GetContext()); + device.reset(); + Require(AllocationErrors == 0 && LiveBytes == 0, "native buffer cleanup left an invalid memory state"); std::puts("pooled buffer range tests passed"); return 0; } catch (const std::exception& error) { From e97c9975bc38298df62556be445e0f0dbabd541e Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Fri, 16 Oct 2026 08:40:30 -0700 Subject: [PATCH 0527/1000] fix(agc): initialize the async log's condition variables before its worker starts A condition variable that winpthreads initializes statically is set up on its first wait, under one global spinlock for all such variables. The async log's worker made that first wait itself, right after the driver started it, so a process that exited soon after starting the driver, such as guest_shader_link, could end with ExitProcess killing the worker inside that setup. The spinlock then stayed held, and the next destructor of a never-used condition variable, in ~Driver, spun forever. The log's constructor now waits on each of its condition variables once with no timeout before it starts the worker, so the worker's waits never take that spinlock. A new shutdown test mode exits right after the driver starts. --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 5 +++-- .../prx/libSceAgcDriver/Execution/include/AsyncLog.hpp | 7 +++++++ .../libSceAgcDriver/tests/execution/ShutdownThreads.cpp | 1 + 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 2f6c6ff73e..5ae644f608 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1829,9 +1829,10 @@ if(BUILD_TESTING) add_test(NAME agc_driver_shutdown_threads_shader_cache_writer COMMAND agc_driver_shutdown_threads_tests --shader-cache-writer) add_test(NAME agc_driver_shutdown_threads_late_shader_cache COMMAND agc_driver_shutdown_threads_tests --late-shader-cache) add_test(NAME agc_driver_shutdown_threads_async_log_held COMMAND agc_driver_shutdown_threads_tests --async-log-held) - set_tests_properties(agc_driver_shutdown_threads_joined agc_driver_shutdown_threads_compare_helpers agc_driver_shutdown_threads_draw_committer agc_driver_shutdown_threads_shader_cache_writer agc_driver_shutdown_threads_late_shader_cache agc_driver_shutdown_threads_async_log_held PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" TIMEOUT 30) + add_test(NAME agc_driver_shutdown_threads_exit_after_start COMMAND agc_driver_shutdown_threads_tests --exit-after-start) + set_tests_properties(agc_driver_shutdown_threads_joined agc_driver_shutdown_threads_compare_helpers agc_driver_shutdown_threads_draw_committer agc_driver_shutdown_threads_shader_cache_writer agc_driver_shutdown_threads_late_shader_cache agc_driver_shutdown_threads_async_log_held agc_driver_shutdown_threads_exit_after_start PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" TIMEOUT 30) if(WIN32) - set_tests_properties(agc_driver_shutdown_threads_joined agc_driver_shutdown_threads_compare_helpers agc_driver_shutdown_threads_draw_committer agc_driver_shutdown_threads_shader_cache_writer agc_driver_shutdown_threads_late_shader_cache agc_driver_shutdown_threads_async_log_held PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + set_tests_properties(agc_driver_shutdown_threads_joined agc_driver_shutdown_threads_compare_helpers agc_driver_shutdown_threads_draw_committer agc_driver_shutdown_threads_shader_cache_writer agc_driver_shutdown_threads_late_shader_cache agc_driver_shutdown_threads_async_log_held agc_driver_shutdown_threads_exit_after_start PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() add_test(NAME agc_driver_compute_user_data_calls_registered COMMAND agc_driver_compute_user_data_calls_tests --registered) set_tests_properties(agc_driver_compute_user_data_calls_registered PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/AsyncLog.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/AsyncLog.hpp index 06ecc6c52e..36b218edf9 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/AsyncLog.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/AsyncLog.hpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -203,10 +204,16 @@ private: if (!asynchronous) return; LogSink().store(&AsyncLog::Sink, std::memory_order_release); std::atexit(&AsyncLog::FlushAtEnd); + InitializeWaits(); std::thread([this] { Run(); }).detach(); LibcSetEndFlush_nid_no_patch(&AsyncLog::FlushAtEnd); } + void InitializeWaits() { + std::unique_lock lock(mutex); + for (auto* condition : {&wake, &space, &idle}) static_cast(condition->wait_for(lock, std::chrono::nanoseconds(0))); + } + static int Sink(std::FILE* stream, const char* text, std::size_t length) { return Get().Write(stream, text, length); } diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ShutdownThreads.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ShutdownThreads.cpp index 5c8ca06e57..8f835d52de 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ShutdownThreads.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ShutdownThreads.cpp @@ -173,6 +173,7 @@ int main(int argc, char** argv) { else if (mode == "--shader-cache-writer") ShutdownJoinsShaderCacheWriter(); else if (mode == "--late-shader-cache") ShutdownJoinsShaderCacheWriter(true); else if (mode == "--async-log-held") ShutdownPassesHeldAsyncLog(); + else if (mode == "--exit-after-start") static_cast(AgcDriver::DriverDetail::Driver::Get()); else ClosingStartsNoPreparation(); std::cout << "shutdown threads tests passed\n"; return 0; From 1271998d444c9f05a3330c96fc08d81c7e148bd4 Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Fri, 16 Oct 2026 15:42:42 +0000 Subject: [PATCH 0528/1000] test(agc): check native MSAA source after alias attempts --- .../libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp index 8ededa0651..15903f6577 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp @@ -328,6 +328,7 @@ void CheckNativeIndices(AgcDriver::VulkanDevice& device, const Block& source, co Require(native != nullptr && native == adjacent, "the native indices and their neighbor do not share a host import"); } const auto state = TargetState(DecodeTarget(destination)); + AgcDriver::Graphics::NoteColorMetadataFill(source.Address(), static_cast(colorBytes), FillOnes); std::uint32_t missed = 0; for (const std::uint32_t indexSize : {2u, 4u}) { if (indexSize == 2u) { @@ -361,7 +362,6 @@ void CheckNativeIndices(AgcDriver::VulkanDevice& device, const Block& source, co missed += refused ? 0u : 1u; } } - AgcDriver::Graphics::NoteColorMetadataFill(source.Address(), static_cast(colorBytes), FillOnes); CheckUniform(LoadSamples(device, source), Ones, "after native draw-input alias attempts"); std::puts("native index controls: shared-import=1 source-samples=1024 neighbor-triangle=passed"); Require(missed == 0, "native indices missed " + std::to_string(missed) + " resident-image refusals"); From 3a0b032c59f0ad13693d385547a3aea6cc787dc2 Mon Sep 17 00:00:00 2001 From: Zinebyte <197974009+Zinebyte@users.noreply.github.com> Date: Wed, 14 Oct 2026 18:06:53 -0400 Subject: [PATCH 0529/1000] fix(web): recognize AppleDouble ZIP metadata --- .github/workflows/web-tests.yml | 6 ++ demo/web/archive.mjs | 55 ++++++++++-- demo/web/tests/archive-root.test.mjs | 124 ++++++++++++++++++++++++++- 3 files changed, 176 insertions(+), 9 deletions(-) diff --git a/.github/workflows/web-tests.yml b/.github/workflows/web-tests.yml index e2b5aa9186..41602ab6fe 100644 --- a/.github/workflows/web-tests.yml +++ b/.github/workflows/web-tests.yml @@ -22,6 +22,12 @@ jobs: - uses: actions/checkout@v7 with: persist-credentials: false + - name: Test archive root selection + shell: bash + run: | + zip_url=$(python3 -c 'from pathlib import Path; import re; print(re.search(r"from \"(https://cdn.jsdelivr.net/[^\"]+)\"", Path("demo/web/archive.mjs").read_text(encoding="utf-8")).group(1))') + curl -fsSL "$zip_url" -o "$RUNNER_TEMP/zip.mjs" + ANYPS5_TEST_ZIP_URL="$zip_url" ANYPS5_TEST_ZIP_PATH="$(python3 -c 'from pathlib import Path; import sys; print(Path(sys.argv[1]).resolve().as_uri())' "$RUNNER_TEMP/zip.mjs")" node demo/web/tests/archive-root.test.mjs demo/web - name: Check browser module syntax run: | node --check demo/web/app.mjs diff --git a/demo/web/archive.mjs b/demo/web/archive.mjs index f1a30d2e42..82b00bed6f 100644 --- a/demo/web/archive.mjs +++ b/demo/web/archive.mjs @@ -27,24 +27,45 @@ function safeSize(value) { return value; } -async function headerBytes(entry) { - if (entry.uncompressedSize < 4) return null; +async function headerBytes(entry, size = 4) { + if (entry.uncompressedSize < size) return null; const stop = new AbortController(); - const header = new Uint8Array(4); + const header = new Uint8Array(size); let filled = 0; try { await entry.getData(new WritableStream({ write(chunk) { - const length = Math.min(4 - filled, chunk.length); + const length = Math.min(size - filled, chunk.length); header.set(chunk.subarray(0, length), filled); filled += length; - if (filled === 4) stop.abort(); + if (filled === size) stop.abort(); } }), { signal: stop.signal, checkSignature: false }); } catch (error) { - if (!stop.signal.aborted || filled !== 4) throw error; + if (!stop.signal.aborted || filled !== size) throw error; } - return header; + return filled === size ? header : null; +} + +async function isAppleDouble(entry) { + const header = await headerBytes(entry, 26); + const view = header && new DataView(header.buffer, header.byteOffset, header.byteLength); + if (!view || view.getUint32(0) !== 0x00051607 || view.getUint32(4) !== 0x00020000) return false; + const size = 26 + view.getUint16(24) * 12; + const table = size === 26 ? header : await headerBytes(entry, size); + if (!table) return false; + const descriptors = new DataView(table.buffer, table.byteOffset, table.byteLength); + const identifiers = new Set(); + for (let position = 26; position < size; position += 12) { + const identifier = descriptors.getUint32(position); + const offset = descriptors.getUint32(position + 4); + const length = descriptors.getUint32(position + 8); + if (identifier === 0 || identifier === 1 || identifiers.has(identifier) + || offset > entry.uncompressedSize || length > entry.uncompressedSize - offset + || ((offset !== 0 || length !== 0) && offset < size)) return false; + identifiers.add(identifier); + } + return true; } const SELF_MAGICS = [ @@ -122,12 +143,30 @@ export class Archive { this.entries.push(entry); } if (!this.entries.some(entry => !entry.directory)) throw new Error("No files were selected"); + const directories = new Set(); for (const entry of this.entries) { const parts = entry.filename.split("/"); for (let index = 1; index < parts.length; index++) { - if (paths.get(parts.slice(0, index).join("/").toLowerCase()) === false) throw new Error(`A file is also used as a folder: ${entry.filename}`); + const parent = parts.slice(0, index).join("/").toLowerCase(); + if (paths.get(parent) === false) throw new Error(`A file is also used as a folder: ${entry.filename}`); + if (!entry.filename.toLowerCase().startsWith("__macosx/")) directories.add(parent); } } + const applicationPaths = new Map([...paths].filter(([name]) => name !== "__macosx" && !name.startsWith("__macosx/"))); + const metadata = this.entries.filter(entry => entry.filename.startsWith("__MACOSX/")); + let appleDouble = metadata.some(entry => !entry.directory); + for (const entry of metadata) { + if (entry.directory) { + const companion = entry.filename.slice("__MACOSX/".length).replace(/\/$/u, "").toLowerCase(); + if (companion && applicationPaths.get(companion) !== true && !directories.has(companion)) { appleDouble = false; break; } + continue; + } + const match = /^__MACOSX\/(.*\/)?\._([^/]+)$/u.exec(entry.filename); + const companion = match && ((match[1] ?? "") + match[2]).toLowerCase(); + if (!companion || (!applicationPaths.has(companion) && !directories.has(companion)) || !await isAppleDouble(entry)) { appleDouble = false; break; } + } + if (appleDouble) this.entries = this.entries.filter(entry => !entry.filename.startsWith("__MACOSX/")); + if (!this.entries.some(entry => !entry.directory)) throw new Error("No files were selected"); let prefix = ""; const files = this.entries.filter(entry => !entry.directory); while (files.every(entry => entry.filename.slice(prefix.length).includes("/"))) { diff --git a/demo/web/tests/archive-root.test.mjs b/demo/web/tests/archive-root.test.mjs index deba9ee898..8a77539c39 100644 --- a/demo/web/tests/archive-root.test.mjs +++ b/demo/web/tests/archive-root.test.mjs @@ -10,10 +10,132 @@ const header = new Uint8Array([127, 69, 76, 70]); async function zipped(entries) { const writer = new ZipWriter(new BlobWriter(), { useWebWorkers: false, useCompressionStream: false }); - for (const [name, directory] of entries) await writer.add(name, directory ? undefined : new Uint8ArrayReader(header), { directory }); + for (const [name, directory, bytes = header, options = {}] of entries) await writer.add(name, directory ? undefined : new Uint8ArrayReader(bytes), { directory, ...options }); return writer.close(); } +const metadata = new Uint8Array(86); +const metadataView = new DataView(metadata.buffer); +metadataView.setUint32(0, 0x00051607); +metadataView.setUint32(4, 0x00020000); +metadataView.setUint16(24, 2); +metadataView.setUint32(26, 9); +metadataView.setUint32(30, 50); +metadataView.setUint32(34, 32); +metadataView.setUint32(38, 2); +metadataView.setUint32(42, 82); +metadataView.setUint32(46, 4); +const app = [["Title/main.elf", false], ["Title/sce_module/provider.prx", false]]; +const sidecars = [["__MACOSX/Title/._main.elf", false, metadata], ["__MACOSX/Title/sce_module/._provider.prx", false, metadata]]; +for (const directories of [[], ["Title/", "Title/sce_module/", "__MACOSX/", "__MACOSX/Title/", "__MACOSX/Title/sce_module/"]]) { + const archive = new Archive(await zipped([...directories.map(name => [name, true]), ...app, ...sidecars, ["__MACOSX/._Title", false, metadata]])); + try { + const summary = await archive.inspect(() => {}); + assert.equal(summary.root, "Title/"); + assert.equal(summary.files, 2); + assert.equal(summary.modules, 1); + assert.equal(summary.selectedBytes, 8); + assert.equal(summary.totalBytes, 8 + metadata.length * 3); + assert(!archive.files.some(item => item.name.includes("__MACOSX"))); + for (const target of ["linux", "windows"]) { + const input = await archive.conversionInput(target, false, () => {}); + assert.deepEqual(input.executable, header); + assert.equal(input.modules[0].name, "sce_module/provider.prx"); + assert.deepEqual(input.modules[0].bytes, header); + } + } finally { await archive.close(); } +} + +const wrongMagic = metadata.slice(); +wrongMagic[3] = 0; +const wrongVersion = metadata.slice(); +wrongVersion[5] = 1; +const oversizedTable = metadata.slice(); +new DataView(oversizedTable.buffer).setUint16(24, 100); +const invalidDescriptors = []; +for (const [position, value] of [ + [26, 0], [26, 1], [38, 9], [30, 0], [30, 49], + [30, 0xffffffff], [34, 0xffffffff], [42, 83], [42, 87] +]) { + const invalid = metadata.slice(); + new DataView(invalid.buffer).setUint32(position, value); + invalidDescriptors.push(invalid); +} +const emptyBeforeTable = metadata.slice(); +new DataView(emptyBeforeTable.buffer).setUint32(30, 25); +new DataView(emptyBeforeTable.buffer).setUint32(34, 0); +invalidDescriptors.push(emptyBeforeTable); +const emptyPastEnd = metadata.slice(); +new DataView(emptyPastEnd.buffer).setUint32(42, 87); +new DataView(emptyPastEnd.buffer).setUint32(46, 0); +invalidDescriptors.push(emptyPastEnd); +for (const extra of [ + [["__MACOSX/Title/._main.elf", false, wrongMagic]], + [["__MACOSX/Title/._main.elf", false, wrongVersion]], + [["__MACOSX/Title/._main.elf", false, metadata.subarray(0, 25)]], + [["__MACOSX/Title/._main.elf", false, oversizedTable]], + ...invalidDescriptors.map(bytes => [["__MACOSX/Title/._main.elf", false, bytes]]), + [["__MACOSX/Title/._absent.elf", false, metadata]], + [...sidecars, ["__MACOSX/Title/readme", false, new Uint8Array([1])]], + [...sidecars, ["__MACOSX/unrelated/", true]], + [...sidecars, ["other/resource", false, new Uint8Array([1])]] +]) { + const archive = new Archive(await zipped([...app, ...extra])); + try { await assert.rejects(archive.inspect(() => {}), /exactly one executable ELF/); } + finally { await archive.close(); } +} + +for (const [offset, length] of [[0, 0], [50, 0], [86, 0]]) { + const emptyEntry = metadata.slice(); + new DataView(emptyEntry.buffer).setUint32(30, offset); + new DataView(emptyEntry.buffer).setUint32(34, length); + const archive = new Archive(await zipped([["main.elf", false], ["__MACOSX/._main.elf", false, emptyEntry]])); + try { + assert.equal((await archive.inspect(() => {})).files, 1); + assert.deepEqual(archive.files.map(item => item.name), ["main.elf"]); + } finally { await archive.close(); } +} + +for (const bytes of [wrongMagic, wrongVersion, metadata.subarray(0, 25), oversizedTable, ...invalidDescriptors]) { + const name = "__MACOSX/._main.elf"; + const archive = new Archive(await zipped([["main.elf", false], [name, false, bytes]])); + try { + assert.equal((await archive.inspect(() => {})).files, 2); + assert.deepEqual(archive.files.map(item => item.name), ["main.elf", name]); + } finally { await archive.close(); } +} + +for (const unrelated of [ + ["__MACOSX/readme", false, new Uint8Array([1])], ["__MACOSX/unrelated/", true], + ["__MACOSX/__MACOSX/._._main.elf", false, metadata], ["__MACOSX/__MACOSX/", true] +]) { + const archive = new Archive(await zipped([["main.elf", false], ["__MACOSX/._main.elf", false, metadata], unrelated])); + try { + await archive.inspect(() => {}); + assert.deepEqual(archive.files.map(item => item.name), ["main.elf", "__MACOSX/._main.elf", unrelated[0]]); + } finally { await archive.close(); } +} + +const knownDirectory = new Archive(await zipped([ + ["main.elf", false], ["assets/", true], ["__MACOSX/._main.elf", false, metadata], ["__MACOSX/assets/", true] +])); +try { + await knownDirectory.inspect(() => {}); + assert.deepEqual(knownDirectory.files.map(item => item.name), ["main.elf", "assets/"]); +} finally { await knownDirectory.close(); } + +for (const [entries, message] of [ + [[...app, ["__MACOSX/../._main.elf", false, metadata]], /Unsafe ZIP path/], + [[...app, ...sidecars, ["__MACOSX/title/._MAIN.ELF", false, metadata]], /Duplicate ZIP path/], + [[...app, ["__MACOSX/Title/._main.elf", false, metadata, { password: "metadata-password" }]], /Encrypted ZIP entries/], + [[...app, ["__MACOSX/Title/._main.elf", false, metadata, { versionMadeBy: 0x31e, externalFileAttributes: (0o120777 << 16) >>> 0 }]], /Links and special files/], + [[["Title/", true], ["__MACOSX/._Title", false, metadata]], /No files were selected/] +]) { + const archive = new Archive(await zipped(entries)); + try { await assert.rejects(archive.inspect(() => {}), message); } + finally { await archive.close(); } +} + const parents = ["outer/", "outer/inner/", "outer/inner/title/"]; for (const directories of [[], parents, [...parents].reverse()]) { const archive = new Archive(await zipped([ From 88687e19ebf5ab7cf77df99e31a8cb2d913cbf42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Fri, 16 Oct 2026 17:49:43 +0200 Subject: [PATCH 0530/1000] ci(linux): size build and tests to the host, cap cold compiles by memory Co-Authored-By: Claude --- .github/workflows/build.yml | 24 +++++++++++++++--------- tools/compile_slot.sh | 15 +++++++++++++++ 2 files changed, 30 insertions(+), 9 deletions(-) create mode 100755 tools/compile_slot.sh diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 430d5e820a..c279099b88 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -364,20 +364,26 @@ jobs: run: | cpus=$(nproc) busy=$(awk '{ printf "%d", $1 + 0.5 }' /proc/loadavg) - mem_gb=$(awk '/^MemAvailable:/ { print int($2 / 1048576) }' /proc/meminfo) - jobs=$(( cpus - busy )) - jobs=$(( jobs < mem_gb / 3 ? jobs : mem_gb / 3 )) - jobs=$(( jobs > 8 ? 8 : jobs )) - jobs=$(( jobs < 1 ? 1 : jobs )) - echo "cpus=$cpus load=$busy mem_available_gb=$mem_gb -> build jobs $jobs" - echo "CMAKE_BUILD_PARALLEL_LEVEL=$jobs" >> "$GITHUB_ENV" - echo "CTEST_PARALLEL_LEVEL=$(( jobs > 4 ? 4 : jobs ))" >> "$GITHUB_ENV" + mem_mb=$(awk '/^MemTotal:/ { print int($2 / 1024) }' /proc/meminfo) + slots=$(( (mem_mb - 2048) / 1024 )) + slots=$(( slots < 1 ? 1 : slots )) + tests=$(( cpus - busy )) + tests=$(( tests < 1 ? 1 : tests )) + slot_dir=$CCACHE_DIR/compile-slots + mkdir -p -m 1777 "$slot_dir" + test -w "$slot_dir" + echo "cpus=$cpus load=$busy mem_total_mb=$mem_mb -> build jobs $cpus, cold compiles $slots per host, tests $tests" + echo "CMAKE_BUILD_PARALLEL_LEVEL=$cpus" >> "$GITHUB_ENV" + echo "CTEST_PARALLEL_LEVEL=$tests" >> "$GITHUB_ENV" + echo "CCACHE_PREFIX=$GITHUB_WORKSPACE/tools/compile_slot.sh" >> "$GITHUB_ENV" + echo "ANYPS5_COMPILE_SLOT_DIR=$slot_dir" >> "$GITHUB_ENV" + echo "ANYPS5_COMPILE_SLOTS=$slots" >> "$GITHUB_ENV" - run: cmake -S . -B build $CONFIGURE -DANYPS5_ENABLE_SPIRV_TOOLS=ON "-DCMAKE_EXE_LINKER_FLAGS=$LINKER_FLAGS" "-DCMAKE_SHARED_LINKER_FLAGS=$LINKER_FLAGS" "-DCMAKE_MODULE_LINKER_FLAGS=$LINKER_FLAGS" - name: Check that every test executable is registered with ctest run: python3 tools/check_test_registration.py build - run: cmake --build build -- -l "$(nproc)" - run: cmake --build build --target libs -- -l "$(nproc)" - - run: ctest --test-dir build --output-on-failure --timeout 120 --output-log ctest.log --output-junit ctest.xml + - run: ctest --test-dir build --test-load "$(nproc)" --output-on-failure --timeout 120 --output-log ctest.log --output-junit ctest.xml env: ANYPS5_REQUIRE_VULKAN: 1 GALLIUM_OVERRIDE_CPU_CAPS: avx diff --git a/tools/compile_slot.sh b/tools/compile_slot.sh new file mode 100755 index 0000000000..2f01329de4 --- /dev/null +++ b/tools/compile_slot.sh @@ -0,0 +1,15 @@ +#!/bin/bash +set -u +dir=${ANYPS5_COMPILE_SLOT_DIR:?} +slots=${ANYPS5_COMPILE_SLOTS:?} +while :; do + for ((i = 0; i < slots; i++)); do + [ -e "$dir/slot-$i" ] || : >> "$dir/slot-$i" + exec {fd}<"$dir/slot-$i" + if flock -n "$fd"; then + exec "$@" + fi + exec {fd}>&- + done + sleep 0.1 +done From a65d42755b6619a240f0412efb8d3686c05231de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Fri, 16 Oct 2026 18:13:46 +0200 Subject: [PATCH 0531/1000] test(agc): bound the cancelled journal replay from its own start The cancelled replay's bound was taken from the totals before the torn-tail replay, which already adds one present pipeline; when a worker reached the graphics recipe before the cancel, the test failed (13 of 600 runs with 24 in parallel). Co-Authored-By: Claude --- core/libs/prx/libSceAgcDriver/tests/Graphics.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index d6c6af2d83..9aa7ec64c2 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -7650,12 +7650,13 @@ void pipelineJournalTests() { Require(PipelineJournalTotals().recordedCompute == reloaded.recordedCompute + 1 && PipelineJournalTotals().recordedModules == reloaded.recordedModules, "the pipeline whose record was torn was not recorded again, or its module was"); } { + const auto before = PipelineJournalTotals(); auto cancelled = StartPipelineJournalReplay(context, fingerprint, hooks); Require(cancelled != nullptr, "the third replay did not start"); cancelled->Cancel(); cancelled->Wait(); const auto after = PipelineJournalTotals(); - Require(cancelled->Done() && after.replayFailed == 0 && after.replayRejected == 0 && after.replayedGraphics + after.replayPresent <= totals.replayedGraphics + totals.replayPresent + 1, "a cancelled replay did not stop cleanly"); + Require(cancelled->Done() && after.replayFailed == 0 && after.replayRejected == 0 && after.replayedGraphics + after.replayPresent <= before.replayedGraphics + before.replayPresent + 1, "a cancelled replay did not stop cleanly"); } { const std::uint32_t meshWithoutOutputSize = 2u; From a5a0aa021357b1e9e919333bf3b7c6c60b0d23a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Fri, 16 Oct 2026 18:32:52 +0200 Subject: [PATCH 0532/1000] test(ci): give the eligibility gate's node runs 90 s Co-Authored-By: Claude --- tools/tests/test_build_eligibility.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/tests/test_build_eligibility.py b/tools/tests/test_build_eligibility.py index b39ec9fabe..56e41b71c6 100644 --- a/tools/tests/test_build_eligibility.py +++ b/tools/tests/test_build_eligibility.py @@ -94,7 +94,7 @@ class RelinkerEligibilityTests(unittest.TestCase): encoding="utf-8") environment = dict(os.environ, PR_NUMBER="7", GITHUB_TRIGGERING_ACTOR="maintainer") result = subprocess.run([self.node, "harness.js", "scenario.json", "gate.js"], cwd=self.root, - capture_output=True, text=True, env=environment, timeout=20) + capture_output=True, text=True, env=environment, timeout=90) self.assertEqual(result.returncode, 0, result.stdout + result.stderr) return json.loads(result.stdout) From 11f83f2d4734b58b4d9416a2bc581f8506631d05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Fri, 16 Oct 2026 18:33:34 +0200 Subject: [PATCH 0533/1000] test(agc): keep the vertex BDA shader code static Co-Authored-By: Claude --- core/libs/prx/libSceAgcDriver/tests/Graphics.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 9aa7ec64c2..cd2842756c 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -5777,7 +5777,7 @@ void orderedPixelShaderTests() { unordered.target.supportedExtensions = bdaExtensions; const auto unorderedKills = countOps(ShaderRecompiler::Recompile(unordered).spirv.Words()); Require(!unorderedKills.contains(spv::OpTerminateInvocation) && unorderedKills.at(spv::OpKill) == 6u, "a pixel shader with BDA reads and no SPV_KHR_terminate_invocation did not kill on a fault"); - constexpr std::array vertexBdaCode{0x7e040f00u, 0x7e060280u, 0xdc208001u, 0x047d0002u, 0xdc308008u, 0x057d0002u, 0xbf8c3f70u, 0x4a080b04u, 0x7e0e0d04u, 0xf80008cfu, 0x07070707u, 0xbf810000u}; + static constexpr std::array vertexBdaCode{0x7e040f00u, 0x7e060280u, 0xdc208001u, 0x047d0002u, 0xdc308008u, 0x057d0002u, 0xbf8c3f70u, 0x4a080b04u, 0x7e0e0d04u, 0xf80008cfu, 0x07070707u, 0xbf810000u}; const std::array vertexCapabilities{spv::CapabilityShader, spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess, spv::CapabilityClipDistance}; const auto vertexWords = [&](std::span extensions) { ShaderRecompiler::RecompileRequest request{}; From 931d20929586eb97d3d9f0962e9365f18945cc5d Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Fri, 16 Oct 2026 16:59:51 +0000 Subject: [PATCH 0534/1000] docs(debt): add PPSA26344 call-site details --- docs/dev/debt/libSceAgc.md | 2 +- docs/dev/debt/libScePsml_debug.md | 2 +- docs/dev/debt/libSceSystemService.md | 2 +- docs/dev/debt/libSceVideoOut.md | 4 ++-- docs/dev/debt/libSceVoiceQoS.md | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/dev/debt/libSceAgc.md b/docs/dev/debt/libSceAgc.md index 8a7ce86545..7d871117ed 100644 --- a/docs/dev/debt/libSceAgc.md +++ b/docs/dev/debt/libSceAgc.md @@ -20,7 +20,7 @@ - [sceAgcDcbSet{Cx,Sh,Uc}RegistersIndirectGetSize](../../../core/libs/prx/libSceAgc/DcbState/src/Registers.cpp) (libSceAgc) - the count argument is ignored: PPSA28416 calls them with leftover register contents (0xf5f5039c), so the real functions likely take none; the size is 20 bytes either way - [sceAgcUpdateInterpolantMapping_0100](../../../core/libs/prx/libSceAgc/Misc/src/ShaderFusion.cpp) (libSceAgc) - name from its NID (`k0E7vkgqAuE`); the `(regs, vs, ps)` arguments are the declared ones, behaviour unknown - [sceAgcWriteDataPatchSetDst, sceAgcWriteDataPatchSetCachePolicy and their Async forms](../../../core/libs/prx/libSceAgc/Patch/src/WaitRegMem.cpp) (libSceAgc) - the signatures `(packet, uint8 value)` are inferred from the other patch functions, as no public declaration was found; the value is taken in the encoding of the `dst` and `cachePolicy` arguments of `sceAgcDcbWriteData` (graphics) and `sceAgcAcbWriteData` (Async, compute), and the patched packet is the one those calls would write. `sceAgcAsyncWriteDataPatchSetAddressOrOffset` is assumed to patch the address like `sceAgcWriteDataPatchSetAddressOrOffset`, as the compute WRITE_DATA packet keeps it in the same dwords -- [sceAgcCreateInterpolantMapping_10000](../../../core/libs/prx/libSceAgc/Shader/src/InterpolantMapping.cpp) (libSceAgc) - name from its NID (`dbOlWdppb4o`), not in the aerolib NID list; same arguments as `sceAgcCreateInterpolantMapping_0100`; the `SPI_PS_INPUT_CNTL` bits for `is_f16` 2 inputs follow KytyPS5, not confirmed on hardware +- [sceAgcCreateInterpolantMapping_10000](../../../core/libs/prx/libSceAgc/Shader/src/InterpolantMapping.cpp) (libSceAgc) - name from its NID (`dbOlWdppb4o`); same arguments as `sceAgcCreateInterpolantMapping_0100`; PPSA26344 passes three pointers at each of nine observed call sites (`0x9F914E`, `0x9F91E2`, `0x9F9216`, `0x1604BBD`, `0x16122D7`, `0x1618285`, `0x161838C`, `0x16184EE`, `0x1618607`), consistent with that argument assumption, but does not read the return value there, so its return type is not established. The `SPI_PS_INPUT_CNTL` bits for `is_f16` 2 inputs follow KytyPS5, not confirmed on hardware - [sceAgcGetDataPacketPayloadRange](../../../core/libs/prx/libSceAgc/Misc/src/PacketInfo.cpp) (libSceAgc) - signature, the `{base, size in bytes}` output and the type 0 range (header + 1, one dword longer than the type 1 payload) from KytyPS5 only, matching `sceAgcGetDataPacketPayloadAddress_0090`; prosper returns header + 2 for both types from that address function - [sceAgcAsyncCondExecPatchSetCommandAddress, sceAgcAsyncCondExecPatchSetEnd](../../../core/libs/prx/libSceAgc/Patch/src/CondExec.cpp) (libSceAgc) - no reference has a body for them; they are assumed to patch the COND_EXEC packet like `sceAgcCondExecPatchSetCommandAddress` and `sceAgcCondExecPatchSetEnd`, since `sceAgcAcbCondExec` writes the same packet as `sceAgcDcbCondExec`, as `sceAgcAsyncRewindPatchSetRewindState` does for REWIND - [sceAgcAcbAtomicGds_0900](../../../core/libs/prx/libSceAgc/Acb/src/Control.cpp) (libSceAgc) - name from its NID (`gQkqkLttcpw`), signature unknown diff --git a/docs/dev/debt/libScePsml_debug.md b/docs/dev/debt/libScePsml_debug.md index 3042611621..a24b3c1e9a 100644 --- a/docs/dev/debt/libScePsml_debug.md +++ b/docs/dev/debt/libScePsml_debug.md @@ -3,7 +3,7 @@ ### Unknown function info - [scePsmlMfsr2CreateContext, scePsmlMfsr2CreateSharedResources, scePsmlMfsr2GetContextInitRequirement, scePsmlMfsr2GetDispatchPackets, scePsmlMfsr2GetDispatchPacketsSizeInDwords, scePsmlMfsr2GetSharedResourcesInitRequirement, scePsmlMfsr2Init, scePsmlMfsr2ReleaseContext, scePsmlMfsr2ReleaseSharedResources](../../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml_mfsr2) - unknown signatures, imported by PPSA23566; declared without parameters -- [scePsmlMfsrGetContextBufferRequirement1100, scePsmlMfsrCreateContext1100, scePsmlMfsrGetDispatchMfsrPacket1100](../../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml_debug) - argument layouts unknown; only 0x8A810001 (not initialized) is returned +- [scePsmlMfsrGetContextBufferRequirement1100, scePsmlMfsrCreateContext1100, scePsmlMfsrGetDispatchMfsrPacket1100](../../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml_debug) - argument layouts unknown; PPSA26344 imports but does not call the first two; at `0x87EFA` it passes `(rdi, rsi=&stack, rdx)` as three pointers to `GetDispatchMfsrPacket1100`. Only 0x8A810001 (not initialized) is returned - [scePsmlMfsrEnableSpecularAA, scePsmlMfsrEnableQuality8K](../../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml_specularAA and libScePsml_quality8K) - imported by PPSA01317, which fails to load without them; signatures unknown, so they throw. The names are not in the aerolib NID list; they hash to the imported NIDs `xNZM9Y5lf-M` and `lcZD9tJraAw` (`tools/nid_names.py`) - [scePsmlMfsrGetDispatchMfsrPacket1000](../../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml) - unknown signature, imported by PPSA25344; declared without parameters. libScePsml and libScePsml_debug are built from the same source - [scePsmlMfsrCreateContext1300, scePsmlMfsrGetDispatchMfsrPacket1300](../../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml) - unknown signatures, imported by PPSA28416 next to `scePsmlMfsrGetContextBufferRequirement1100`; declared without parameters. The names are not in the aerolib NID list; they hash to the imported NIDs `kLF7WtQ1-Dg` and `DDVUyPyYHTA` (`tools/nid_names.py`) diff --git a/docs/dev/debt/libSceSystemService.md b/docs/dev/debt/libSceSystemService.md index 3e5bcecac2..a7f51c4858 100644 --- a/docs/dev/debt/libSceSystemService.md +++ b/docs/dev/debt/libSceSystemService.md @@ -11,7 +11,7 @@ - [sceSystemServiceLaunchApp](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - AnyPS5 runs one title and cannot start another, so every call throws without reading its arguments. The (title ID, null-terminated argv, launch parameters) signature follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/SystemService/SystemService.cs#L141-L148), whose `AppLauncher` Prospero Explorer (PPSA99109) and Prospero Multi Tools (PPSA99110) import it through and which passes a 0x20-byte launch context or null; the console's argument checks and errors are unknown - [sceSystemServiceLaunchWebBrowser](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - the console's error for an unavailable web browser is unknown; it returns the kernel's `EOPNOTSUPP` (0x8002002D) for any arguments. The (uri, param) signature follows [RommPS](https://github.com/s0liton/RommPS/blob/754d4d60f25938a5136b9e2efcd074313c4386c5/platform/ps5/app/rommps/examples/rommps/kit/credits.cpp#L86-L95), which passes a URL and a null param and only tests the result for zero; the param layout and the console's argument checks are unknown -- [sceSystemServiceParamGetString](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - the system name (parameter 6) is the fixed `PS5`, not the console's name; other parameters and buffers of 1 to 64 bytes throw +- [sceSystemServiceParamGetString](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - the system name (parameter 6) is the fixed `PS5`, not the console's name; other parameters and buffers of 1 to 64 bytes throw. PPSA26344 at `0xA87935` passes parameter 6 with a 65-byte buffer - [sceSystemServiceParamGetInt](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - console language numbers 0 to 29 follow the PS4 SDK enum in [OpenOrbis](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain/blob/06fcf7fd108fa080035cdd3259ed22eb07e0508c/include/orbis/_types/sys_service.h#L54-L83) (21 Arabic). 30 (Ukrainian) is not in it and follows PPSA99505 ([Jelly5](https://github.com/02dnot/Jelly5/blob/552b866d89b2a58b27f7f7b0484dc743ce235526/app/src/app/i18n.cpp#L29-L60)) and PPSA99002 ([ProsperoLight](https://github.com/blackbearreloaded/ProsperoLight/blob/b708bbde3457bef1073a9ab6959957cb691e8e2f/include/i18n.hpp#L252-L309)), whose numbers agree with the enum. Not confirmed on a console, and neither title starts on `main` yet. Parameter 400 returns the same number as parameter 1: PPSA01325 asks for 400 first, asks for 1 when that fails, and uses either answer as an index into the same 31 languages; in the PS5 10.01 and 12.60 `libSceSystemService` modules parameter 400 reads the same registry value as parameter 1, without parameter 1's mapping of newer language numbers by SDK version, and returns 0x80a10003 when an imported check that is not identified returns 1; the 4.03 module has no parameter 400. Neither the mapping nor that error is modelled, and the parameter's name is not known. Without `ANYPS5_LANGUAGE` the language is English (United States); the date format, time format, time zone, summer time and enter button stay fixed ### Functional diff --git a/docs/dev/debt/libSceVideoOut.md b/docs/dev/debt/libSceVideoOut.md index 6eb7fa25dc..28155782ee 100644 --- a/docs/dev/debt/libSceVideoOut.md +++ b/docs/dev/debt/libSceVideoOut.md @@ -14,9 +14,9 @@ - [sceVideoOutRegisterBuffers2](../../../core/libs/prx/libSceVideoOut/src/Buffer.cpp) (libSceVideoOut) - buffer option 8 (STRICT_COLORIMETRY) is presented as option 0, with the stored values unchanged; what the console changes for it is unknown. Only options 0 and 8 are accepted - [sceVideoOutAddBufferAttributeOption](../../../core/libs/prx/libSceVideoOut/src/Buffer.cpp) (libSceVideoOut, NID `MBlv8YWoyOk`) - the signature `(VideoOutBufferAttribute2*, uint64_t option)` comes from [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/VideoOut/VideoOut.cs#L174-L176) and the call site in PPSA25380, which passes 0x20 and ignores the result. The option is ORed into `attribute->option`; whether the console validates the bits, keeps them elsewhere or fails on an unknown option is unknown. A null attribute throws. Option 0x20 makes `sceVideoOutRegisterBuffers2` throw, as every option other than 0 and 8; what it selects for an overlay buffer is unknown - [sceVideoOutRegisterBuffers2](../../../core/libs/prx/libSceVideoOut/src/Buffer.cpp) (libSceVideoOut) - DCC buffers (category 1): `dcc_control` is taken as the CB_DCC_CONTROL block layout, only bits 0x10026c are accepted; `dcc_cb_register_clear_color` as the register-clear texel, only 32-bit values are presented -- [sceVideoOutIsOutputSupported](../../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - mode 1 is supported; mode 15 is supported when the current host display runs at 120 Hz or above. Configuring mode 15 requires attribute3 bit 0x40; this single-bit mask is inferred from the measured full value 0x80040, not isolated by a console measurement. Vblanks run at 119.88 Hz until mode 1 restores 59.94 Hz. Display modes are read through SDL; a move to a slower display rejects further high-frame-rate presentation. Selecting another host display mode and VRR are not implemented. The console answers per display and per title: with a 120 Hz TV it also answers 1 for mode 15 (119.88 Hz), and [PS5_Vulkan](https://github.com/mihawk-99/PS5_Vulkan/blob/5b5e4fc2d80fdb67a4f61ba9e6a8a424026bb8a5/docs/HARDWARE_FINDINGS.md) measured that `sceVideoOutConfigureOutput` accepts 15 only from a title whose param.json declares high-frame-rate output (attribute3 0x80040). Modes 4, 7, 8, 12-14, 16-17 and 19 return `VIDEO_OUT_ERROR_UNSUPPORTED_OUTPUT_MODE` here, as they did on the measured display. Mode 18 throws, because one console answered 0x80290016 and another 0x8029001E. The name `VIDEO_OUT_ERROR_UNKNOWN_OUTPUT_MODE` for 0x8029001E is ours. Word 3 of the output options is accepted with any value, as on the console, and its meaning is unknown. PPSA12544 probes `0xd000000a`, which the console answers with 0x8029001E, so the title selects 3840 pixels +- [sceVideoOutIsOutputSupported](../../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - mode 1 is supported; mode 15 is supported when the current host display runs at 120 Hz or above. Configuring mode 15 requires attribute3 bit 0x40; this single-bit mask is inferred from the measured full value 0x80040, not isolated by a console measurement. Vblanks run at 119.88 Hz until mode 1 restores 59.94 Hz. Display modes are read through SDL; a move to a slower display rejects further high-frame-rate presentation. Selecting another host display mode and VRR are not implemented. The console answers per display and per title: with a 120 Hz TV it also answers 1 for mode 15 (119.88 Hz), and [PS5_Vulkan](https://github.com/mihawk-99/PS5_Vulkan/blob/5b5e4fc2d80fdb67a4f61ba9e6a8a424026bb8a5/docs/HARDWARE_FINDINGS.md) measured that `sceVideoOutConfigureOutput` accepts 15 only from a title whose param.json declares high-frame-rate output (attribute3 0x80040). Modes 4, 7, 8, 12-14, 16-17 and 19 return `VIDEO_OUT_ERROR_UNSUPPORTED_OUTPUT_MODE` here, as they did on the measured display. Mode 18 throws, because one console answered 0x80290016 and another 0x8029001E. The name `VIDEO_OUT_ERROR_UNKNOWN_OUTPUT_MODE` for 0x8029001E is ours. Word 3 of the output options is accepted with any value, as on the console, and its meaning is unknown. PPSA12544 probes `0xd000000a`, which the console answers with 0x8029001E, so the title selects 3840 pixels; PPSA26344 at `0xCD0946` passes `(handle, 0xf, nullptr, nullptr, 0)` and branches on the result equalling 1 - [sceVideoOutGetResolutionStatus](../../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - the 48-byte layout follows the declarations in ps5-opengl and EVO-PLAYER-PS5 (#1578), which agree; not confirmed on a console. `screenSizeInInch` is 0, the value EDID uses for an undefined size, since the host display's size is not read; `flags` is 0, its bits are unknown. The pane equals the full size because `sceVideoOutSetWindowModeMargins` applies no margins -- [sceVideoOutOpen](../../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - the open param's first word is unknown (PPSA21564 passes 16; it is not the byte size, since the affinity mask is at offset 16); only 16 is accepted. Whether the param continues past offset 24 is also unknown +- [sceVideoOutOpen](../../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - the open param's first word is unknown (PPSA21564 passes 16; it is not the byte size, since the affinity mask is at offset 16); only 16 is accepted. PPSA26344's only call at `0xCCF5EA` passes `(0xFF, 0, 0, nullptr)`. Whether a non-null param continues past offset 24 is also unknown - [sceVideoOutGetBufferLabelAddress](../../../core/libs/prx/libSceVideoOut/src/Buffer.cpp) exposes a port-owned contiguous array of 16 aligned 64-bit labels and returns its count. Astro Bot PPSA21564's bundled AGC driver passes a pointer output and addresses each label at `base + index * 8`. The count, zero initialization on registration, and release of the previous displayed buffer when its replacement completes follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/23c41333822fe8eca4c609b43119180af63ae4ac/src/core/libraries/videoout/video_out.cpp) and its driver; PS5 console equivalence has not been measured. Labels live until the port closes, and reopening creates a new array. Error precedence follows the reference for null output and the existing open-port policy for handles. Exact scanout release timing, repeated same-buffer flips and concurrent guest label stores remain unverified. EOP-flip registration and interrupt tokens remain unimplemented; exposing these labels does not implement that separate API. - [GPU flips](../../../core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp) (libSceVideoOut) - a command-buffer flip of a buffer index with no registered buffer when the GPU reaches it presents black and completes like any flip (flip event, count, `flipPendingNum`). The console's behaviour is unknown; PPSA05684 requires that it completes: its renderer submits a flip of buffer 0 while another thread waits for `sceVideoOutIsFlipPending` to reach 0 before registering the buffers, and `flipPendingNum` counts that flip from its submission as in shadPS4 - [sceVideoOutAddPreVblankStartEvent](../../../core/libs/prx/libSceVideoOut/src/Event.cpp) (libSceVideoOut) - how long before the vblank the console signals pre-vblank start is unknown. The event is triggered on each emulated vblank tick immediately before the vblank event, with its own count as the event data diff --git a/docs/dev/debt/libSceVoiceQoS.md b/docs/dev/debt/libSceVoiceQoS.md index 7b3937b6a9..5700fed07c 100644 --- a/docs/dev/debt/libSceVoiceQoS.md +++ b/docs/dev/debt/libSceVoiceQoS.md @@ -3,7 +3,7 @@ ### Unknown function info - [sceVoiceQoSSetConnectionAttribute](../../../core/libs/prx/libSceVoiceQoS/Export.cpp) (libSceVoiceQoS, NID `KCGOxg8iX7s`) is a throwing stub declared without parameters, like the other endpoint calls of the library: PPSA04159 fails to load without it and its signature is unknown. The name hashes to the imported NID (`tools/nid_names.py`) -- [sceVoiceQoSInit](../../../core/libs/prx/libSceVoiceQoS/Export.cpp) (libSceVoiceQoS) - `0x804E0905` for a second initialization comes from [prosper](https://github.com/mattias800/prosper)'s reading of the module and was not measured: on a PS5 a valid call blocks inside a homebrew payload, so it never returns there. The app type and the block are not used, as no endpoint is implemented +- [sceVoiceQoSInit](../../../core/libs/prx/libSceVoiceQoS/Export.cpp) (libSceVoiceQoS) - `0x804E0905` for a second initialization comes from [prosper](https://github.com/mattias800/prosper)'s reading of the module and was not measured: on a PS5 a valid call blocks inside a homebrew payload, so it never returns there. PPSA26344's two call sites (`0xE307BF`, `0x151C45D`) pass an allocated pointer, a 0x40000-byte block and app type 0x10000000; the app type and block are not used, as no endpoint is implemented ### Functional From 13f6191b753e61cda464c107c21c8460ba30e56a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Fri, 16 Oct 2026 19:10:16 +0200 Subject: [PATCH 0535/1000] ci: build and run only the tests a change reaches on request A new select input of the Build workflow limits the Linux and Windows jobs to the test executables and tests whose build inputs the merged pull requests change. Push builds of main always build and run everything and save the test graph of their tree; dispatched runs with select restore the latest graph and fall back to a full run without one. tools/select_tests.py dump walks the ninja graph of every test executable (order-only dependencies included, so prx libraries loaded at run time are in it) plus the header dependencies of its objects, the CMake files that register each test, the source files its command line names, and its fixtures. select picks the tests whose closure, registration or arguments contain a changed file, and every test that is not a native executable of the tree; files no build reads select the tests under their directory; changes to the root build, cmake/, 3rdparty/, the workflow or the tool run everything. Tests added since the graph was written always run. ctest gets the selection as -I indices through the tool, which keeps the cmd shell on Windows and stays under its command line limit. Co-Authored-By: Claude --- .github/workflows/build.yml | 72 +++++++++- CMakeLists.txt | 2 + tools/select_tests.py | 240 +++++++++++++++++++++++++++++++ tools/tests/test_select_tests.py | 112 +++++++++++++++ 4 files changed, 419 insertions(+), 7 deletions(-) create mode 100644 tools/select_tests.py create mode 100644 tools/tests/test_select_tests.py diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c279099b88..1a0d3a3772 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -22,6 +22,11 @@ on: required: false default: true type: boolean + select: + description: Build and run only the tests whose build inputs the pull requests change (push builds of main always run every test) + required: false + default: false + type: boolean permissions: contents: read @@ -381,13 +386,39 @@ jobs: - run: cmake -S . -B build $CONFIGURE -DANYPS5_ENABLE_SPIRV_TOOLS=ON "-DCMAKE_EXE_LINKER_FLAGS=$LINKER_FLAGS" "-DCMAKE_SHARED_LINKER_FLAGS=$LINKER_FLAGS" "-DCMAKE_MODULE_LINKER_FLAGS=$LINKER_FLAGS" - name: Check that every test executable is registered with ctest run: python3 tools/check_test_registration.py build - - run: cmake --build build -- -l "$(nproc)" - - run: cmake --build build --target libs -- -l "$(nproc)" - - run: ctest --test-dir build --test-load "$(nproc)" --output-on-failure --timeout 120 --output-log ctest.log --output-junit ctest.xml + - if: ${{ inputs.select }} + uses: actions/cache/restore@v6 + with: + path: test-graph.json + key: test-graph-${{ runner.os }}-${{ needs.eligibility.outputs.sha }} + restore-keys: test-graph-${{ runner.os }}- + - name: Select the tests the change reaches + if: ${{ inputs.select }} + env: + BASE: ${{ needs.eligibility.outputs.sha }} + run: | + git diff --name-only "$BASE" HEAD > changed.txt + python3 tools/select_tests.py select build test-graph.json changed.txt + - id: build + run: | + if [ -f build/select.ninja ]; then ninja -C build -f select.ninja -j "$CMAKE_BUILD_PARALLEL_LEVEL" -l "$(nproc)" selected; else cmake --build build -- -l "$(nproc)"; fi + - id: libs + run: cmake --build build --target libs -- -l "$(nproc)" + - run: python3 tools/select_tests.py ctest build --test-load "$(nproc)" --output-on-failure --timeout 120 --output-log ctest.log --output-junit ctest.xml env: ANYPS5_REQUIRE_VULKAN: 1 GALLIUM_OVERRIDE_CPU_CAPS: avx - run: python3 tools/check_skips.py build/ctest.xml + - name: Write the test graph of main + if: ${{ !cancelled() && github.event_name == 'push' && steps.build.outcome == 'success' && steps.libs.outcome == 'success' }} + continue-on-error: true + run: python3 tools/select_tests.py dump build test-graph.json + - if: ${{ !cancelled() && github.event_name == 'push' && hashFiles('test-graph.json') != '' }} + continue-on-error: true + uses: actions/cache/save@v6 + with: + path: test-graph.json + key: test-graph-${{ runner.os }}-${{ github.sha }} - name: Report compiler cache statistics if: ${{ !cancelled() }} run: ccache --show-stats @@ -460,13 +491,40 @@ jobs: - name: Check that every test executable is registered with ctest shell: cmd run: python tools/check_test_registration.py build - - shell: cmd - run: cmake --build build - - shell: cmd + - if: ${{ inputs.select }} + uses: actions/cache/restore@v6 + with: + path: test-graph.json + key: test-graph-${{ runner.os }}-${{ needs.eligibility.outputs.sha }} + restore-keys: test-graph-${{ runner.os }}- + - name: Select the tests the change reaches + if: ${{ inputs.select }} + env: + BASE: ${{ needs.eligibility.outputs.sha }} + shell: cmd + run: | + git diff --name-only %BASE% HEAD > changed.txt + python tools/select_tests.py select build test-graph.json changed.txt + - id: build + shell: cmd + run: if exist build\select.ninja (ninja -C build -f select.ninja selected) else (cmake --build build) + - id: libs + shell: cmd run: cmake --build build --target libs - shell: cmd - run: ctest --test-dir build --output-on-failure --timeout 120 --parallel 4 --output-log ctest.log --output-junit ctest.xml + run: python tools/select_tests.py ctest build --output-on-failure --timeout 120 --parallel 4 --output-log ctest.log --output-junit ctest.xml - run: python tools/check_skips.py build/ctest.xml --job windows + - name: Write the test graph of main + if: ${{ !cancelled() && github.event_name == 'push' && steps.build.outcome == 'success' && steps.libs.outcome == 'success' }} + continue-on-error: true + shell: cmd + run: python tools/select_tests.py dump build test-graph.json + - if: ${{ !cancelled() && github.event_name == 'push' && hashFiles('test-graph.json') != '' }} + continue-on-error: true + uses: actions/cache/save@v6 + with: + path: test-graph.json + key: test-graph-${{ runner.os }}-${{ github.sha }} - name: Report tests slower than 30 s if: ${{ !cancelled() }} shell: bash diff --git a/CMakeLists.txt b/CMakeLists.txt index 23b603d59b..105f952f00 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -135,6 +135,8 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_build_eligibility.py) add_test(NAME check_skips COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_skips.py) + add_test(NAME select_tests COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_select_tests.py) add_test(NAME check_main_sync COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_main_sync.py) add_test(NAME stubgen COMMAND ${Python3_EXECUTABLE} diff --git a/tools/select_tests.py b/tools/select_tests.py new file mode 100644 index 0000000000..0fb850dee8 --- /dev/null +++ b/tools/select_tests.py @@ -0,0 +1,240 @@ +import argparse +import collections +import json +import os +import re +import subprocess +import sys +from pathlib import Path + +FULL = re.compile(r"^CMakeLists\.txt$|^core/CMakeLists\.txt$|^core/libs/CMakeLists\.txt$|^\.github/workflows/build\.yml$" + r"|^3rdparty/|^cmake/|^\.gitmodules$|^tools/select_tests\.py$") +LOCAL = re.compile(r"(^|/)CMakeLists\.txt$|\.cmake$") +BATCH = 20000 + + +class Build: + def __init__(self, path): + self.path = os.path.realpath(path) + cache = {} + for line in (Path(self.path) / "CMakeCache.txt").read_text(encoding="utf-8").splitlines(): + name, _, value = line.partition("=") + cache[name.partition(":")[0]] = value + self.source = os.path.realpath(cache["CMAKE_HOME_DIRECTORY"]) + self.program = cache.get("CMAKE_MAKE_PROGRAM") or "ninja" + self.encoding = "utf-8" + if sys.platform == "win32": + codepage = self.run("-t", "wincodepage").strip() + self.encoding = {"Build file encoding: UTF-8": "utf-8", "Build file encoding: ANSI": "mbcs"}[codepage] + + def run(self, *args): + return subprocess.run([self.program, "-C", self.path, *args], capture_output=True, text=True, + encoding=self.encoding, check=True).stdout + + def absolute(self, path): + return os.path.normcase(os.path.normpath(path if os.path.isabs(path) else os.path.join(self.path, path))) + + def repo(self, path): + absolute = self.absolute(path) + build, source = os.path.normcase(self.path), os.path.normcase(self.source) + if absolute.startswith(build + os.sep) or not absolute.startswith(source + os.sep): + return None + return os.path.relpath(absolute, source).replace(os.sep, "/") + + def query(self, targets): + found = {} + for batch in chunks(targets): + current, section = None, None + for line in self.run("-t", "query", *batch).splitlines(): + if not line.startswith(" "): + current = line.rstrip(":") + found[current] = {"inputs": [], "phony": False} + elif line.startswith(" input: "): + section = "inputs" + found[current]["phony"] = line.strip() == "input: phony" + elif line.startswith(" outputs:"): + section = None + elif section and line.startswith(" "): + found[current]["inputs"].append(line.strip().removeprefix("|| ").removeprefix("| ")) + return found + + def walk(self, roots): + nodes, todo = {}, list(roots) + while todo: + batch = sorted({t for t in todo if t not in nodes}) + if not batch: + break + nodes.update(self.query(batch)) + todo = [i for t in batch for i in nodes.get(t, {"inputs": []})["inputs"]] + return nodes + + def leaves(self): + nodes = self.walk(["all"]) + leaves, todo, seen = set(), ["all"], set() + while todo: + name = todo.pop() + if name in seen or name not in nodes: + continue + seen.add(name) + if nodes[name]["phony"]: + todo.extend(nodes[name]["inputs"]) + else: + leaves.add(name) + return sorted(leaves) + + def tests(self): + listing = json.loads(subprocess.run(["ctest", "--test-dir", self.path, "--show-only=json-v1"], + capture_output=True, text=True, check=True).stdout) + return listing, listing["tests"] + + def output(self, path): + absolute = self.absolute(path) + if not absolute.startswith(os.path.normcase(self.path) + os.sep): + return None + return os.path.relpath(absolute, os.path.normcase(self.path)).replace(os.sep, "/") + + def target(self, test): + command = test.get("command") or [] + return self.output(command[0]) if command else None + + +def chunks(names): + batch, size = [], 0 + for name in names: + if batch and size + len(name) + 1 > BATCH: + yield batch + batch, size = [], 0 + batch.append(name) + size += len(name) + 1 + if batch: + yield batch + + +def dump(build, out): + deps = collections.defaultdict(set) + current = None + for line in build.run("-t", "deps").splitlines(): + if not line.startswith(" ") and ": #deps" in line: + current = build.absolute(line.split(": #deps")[0]) + elif line.strip() and current: + path = build.repo(line.strip()) + if path: + deps[current].add(path) + listing, listed = build.tests() + graph = listing["backtraceGraph"] + targets = {build.target(t) for t in listed} - {None} + nodes = build.walk(sorted(targets)) + closures = {} + for target in sorted(targets): + files, seen, todo = set(), set(), [target] + while todo: + name = todo.pop() + if name in seen: + continue + seen.add(name) + path = build.repo(name) + if path: + files.add(path) + files |= deps.get(build.absolute(name), set()) + todo.extend(nodes.get(name, {"inputs": []})["inputs"]) + closures[target] = sorted(files) + tests = {} + for test in listed: + registered, node = set(), test.get("backtrace") + while node is not None: + registered.add(build.repo(graph["files"][graph["nodes"][node]["file"]])) + node = graph["nodes"][node].get("parent") + props = {p["name"]: p["value"] for p in test.get("properties", [])} + arguments = sorted({build.repo(a) for a in (test.get("command") or [])[1:] if os.path.isabs(a)} - {None}) + tests[test["name"]] = {"registered": sorted(registered - {None}), "arguments": arguments, + "requires": props.get("FIXTURES_REQUIRED", []), "sets": props.get("FIXTURES_SETUP", []), + "depends": props.get("DEPENDS", []), "target": build.target(test)} + with open(out, "w", encoding="utf-8") as file: + json.dump({"tests": tests, "closures": closures}, file) + print(f"{len(tests)} tests, {len(closures)} test executables, {len(deps)} objects with header deps") + + +def select(graph, changed): + full = [f for f in changed if FULL.search(f)] + if full: + return None, full + tests, closures = graph["tests"], {t: set(f) for t, f in graph["closures"].items()} + known = set().union(*closures.values(), *(t["arguments"] for t in tests.values())) + files = set(changed) + dirs = tuple(f.rpartition("/")[0] + "/" for f in changed if "/" in f and (LOCAL.search(f) or f not in known)) + picked = set() + for name, test in tests.items(): + scope = closures.get(test["target"], set()) | set(test["arguments"]) + if (test["target"] is None or files & set(test["registered"]) or files & scope + or any(f.startswith(dirs) for f in scope | set(test["registered"]))): + picked.add(name) + setups = collections.defaultdict(set) + for name, test in tests.items(): + for fixture in test["sets"]: + setups[fixture].add(name) + todo = list(picked) + while todo: + test = tests[todo.pop()] + for extra in set(test["depends"]).union(*(setups[f] for f in test["requires"])): + if extra in tests and extra not in picked: + picked.add(extra) + todo.append(extra) + return picked, [] + + +def plan(build, graph, picked): + _, listed = build.tests() + run = [i for i, t in enumerate(listed, 1) if t["name"] in picked or t["name"] not in graph["tests"]] + names = {listed[i - 1]["name"] for i in run} + keep = {build.output(a) for t in listed if t["name"] in names for a in t.get("command") or [] if os.path.isabs(a)} + skip = {build.target(t) for t in listed if t["name"] not in names} - keep - {None} + return run, [t for t in build.leaves() if t not in skip], len(listed) + + +def escape(name): + return name.replace("$", "$$").replace(":", "$:").replace(" ", "$ ") + + +def main(): + parser = argparse.ArgumentParser(description="Select the tests whose build inputs a change touches") + sub = parser.add_subparsers(dest="command", required=True) + d = sub.add_parser("dump", help="write the test graph of a built tree") + d.add_argument("build") + d.add_argument("graph") + s = sub.add_parser("select", help="write a ninja file and a ctest index list for a configured tree") + s.add_argument("build") + s.add_argument("graph") + s.add_argument("changed", help="file with one changed path per line") + c = sub.add_parser("ctest", help="run ctest on a tree, limited to the selected tests if select wrote any") + c.add_argument("build") + c.add_argument("arguments", nargs=argparse.REMAINDER, help="ctest arguments") + args = parser.parse_args() + if args.command == "ctest": + selected = Path(args.build, "select.tests") + extra = ["-I", selected.read_text(encoding="utf-8").strip()] if selected.exists() else [] + return subprocess.run(["ctest", "--test-dir", args.build, *args.arguments, *extra]).returncode + build = Build(args.build) + if args.command == "dump": + dump(build, args.graph) + return 0 + for name in ("select.ninja", "select.tests"): + Path(build.path, name).unlink(missing_ok=True) + if not os.path.exists(args.graph): + print("full run: no test graph") + return 0 + graph = json.loads(Path(args.graph).read_text(encoding="utf-8")) + changed = [line.strip() for line in Path(args.changed).read_text(encoding="utf-8").splitlines() if line.strip()] + picked, full = select(graph, changed) + if picked is None: + print(f"full run: {', '.join(full[:5])}") + return 0 + run, targets, total = plan(build, graph, picked) + Path(build.path, "select.ninja").write_text("include build.ninja\nbuild selected: phony " + + " ".join(escape(t) for t in targets) + "\n", encoding=build.encoding) + Path(build.path, "select.tests").write_text("0,0,0," + ",".join(map(str, run)) + "\n", encoding="utf-8") + print(f"selected {len(run)}/{total} tests, {len(targets)} build targets") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/tests/test_select_tests.py b/tools/tests/test_select_tests.py new file mode 100644 index 0000000000..87349e729d --- /dev/null +++ b/tools/tests/test_select_tests.py @@ -0,0 +1,112 @@ +import json +import os +import re +import shutil +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path + +TOOL = Path(__file__).resolve().parents[1] / "select_tests.py" + +PROJECT = { + "CMakeLists.txt": "cmake_minimum_required(VERSION 3.20)\nproject(mini C)\nenable_testing()\n" + "add_subdirectory(alpha)\nadd_subdirectory(beta)\n" + "add_test(NAME script COMMAND ${CMAKE_COMMAND} -E true)\n", + "alpha/CMakeLists.txt": "add_executable(alpha_tests main.c)\nadd_test(NAME alpha COMMAND alpha_tests)\n" + "add_test(NAME alpha_data COMMAND alpha_tests ${CMAKE_SOURCE_DIR}/data/input.txt)\n", + "alpha/main.c": "#include \"alpha.h\"\nint main(void) { return ALPHA; }\n", + "alpha/alpha.h": "#define ALPHA 0\n", + "beta/CMakeLists.txt": "add_executable(beta_tests main.c)\nadd_executable(setup_tests setup.c)\n" + "add_test(NAME beta COMMAND beta_tests)\nadd_test(NAME beta_setup COMMAND setup_tests)\n" + "set_tests_properties(beta_setup PROPERTIES FIXTURES_SETUP beta_fixture)\n" + "set_tests_properties(beta PROPERTIES FIXTURES_REQUIRED beta_fixture)\n", + "beta/main.c": "int main(void) { return 0; }\n", + "beta/setup.c": "int main(void) { return 0; }\n", + "data/input.txt": "input\n", +} + + +@unittest.skipUnless(shutil.which("ninja") and (shutil.which("gcc") or shutil.which("cc")), "needs ninja and a C compiler") +class SelectTests(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.directory = tempfile.TemporaryDirectory() + root = Path(cls.directory.name) + cls.source, cls.build, cls.graph = root / "source", root / "build", root / "graph.json" + for name, content in PROJECT.items(): + (cls.source / name).parent.mkdir(parents=True, exist_ok=True) + (cls.source / name).write_text(content, encoding="utf-8") + compiler = shutil.which("gcc") or shutil.which("cc") + subprocess.run(["cmake", "-S", str(cls.source), "-B", str(cls.build), "-G", "Ninja", f"-DCMAKE_C_COMPILER={compiler}"], + check=True, capture_output=True) + subprocess.run(["cmake", "--build", str(cls.build)], check=True, capture_output=True) + cls.tool("dump", cls.build, cls.graph) + + @classmethod + def tearDownClass(cls): + cls.directory.cleanup() + + @staticmethod + def tool(*args): + return subprocess.run([sys.executable, str(TOOL), *map(str, args)], check=True, capture_output=True, text=True).stdout + + def select(self, *changed, graph=None): + listed = self.build / "changed.txt" + listed.write_text("".join(f"{c}\n" for c in changed), encoding="utf-8") + output = self.tool("select", self.build, graph or self.graph, listed) + tests = self.build / "select.tests" + if not tests.exists(): + self.assertFalse((self.build / "select.ninja").exists()) + return output, None + shown = subprocess.run(["ctest", "--test-dir", str(self.build), "-N", "-I", tests.read_text(encoding="utf-8").strip()], + check=True, capture_output=True, text=True).stdout + return output, set(re.findall(r"Test +#\d+: (\S+)", shown)) + + def test_header_selects_the_tests_of_its_executable(self): + self.assertEqual(self.select("alpha/alpha.h")[1], {"alpha", "alpha_data", "script"}) + + def test_argument_file_selects_only_the_test_that_reads_it(self): + self.assertEqual(self.select("data/input.txt")[1], {"alpha_data", "script"}) + + def test_fixture_setup_runs_with_its_test(self): + self.assertEqual(self.select("beta/main.c")[1], {"beta", "beta_setup", "script"}) + self.assertEqual(self.select("beta/setup.c")[1], {"beta_setup", "script"}) + + def test_cmake_file_selects_the_tests_it_registers(self): + self.assertEqual(self.select("beta/CMakeLists.txt")[1], {"beta", "beta_setup", "script"}) + + def test_file_outside_every_build_selects_its_directory(self): + self.assertEqual(self.select("alpha/notes.h")[1], {"alpha", "alpha_data", "script"}) + self.assertEqual(self.select("README.md")[1], {"script"}) + + def test_global_files_run_everything(self): + for changed in ("CMakeLists.txt", "cmake/Toolchain.cmake", ".github/workflows/build.yml", "3rdparty/x/y.c", + "tools/select_tests.py"): + with self.subTest(changed=changed): + output, selected = self.select("alpha/alpha.h", changed) + self.assertIsNone(selected) + self.assertIn(changed, output) + + def test_missing_graph_runs_everything(self): + self.assertIsNone(self.select("alpha/alpha.h", graph=self.build / "missing.json")[1]) + + def test_test_missing_from_the_graph_runs(self): + graph = json.loads(self.graph.read_text(encoding="utf-8")) + del graph["tests"]["beta"] + older = self.build / "older.json" + older.write_text(json.dumps(graph), encoding="utf-8") + self.assertEqual(self.select("alpha/alpha.h", graph=older)[1], {"alpha", "alpha_data", "beta", "beta_setup", "script"}) + + def test_selected_target_skips_unselected_executables(self): + self.select("alpha/main.c") + subprocess.run(["ninja", "-C", str(self.build), "-t", "clean"], check=True, capture_output=True) + subprocess.run(["ninja", "-C", str(self.build), "-f", "select.ninja", "selected"], check=True, capture_output=True) + built = {p.stem for p in self.build.rglob("*_tests*") if p.is_file() and os.access(p, os.X_OK)} + subprocess.run(["cmake", "--build", str(self.build)], check=True, capture_output=True) + self.assertEqual(built, {"alpha_tests"}) + + +if __name__ == "__main__": + unittest.main() From bca62ea696663e5d5d6ae7be2223c84f90069755 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Fri, 16 Oct 2026 19:14:07 +0200 Subject: [PATCH 0536/1000] ci: let a push to main cancel the build of the previous one Push builds of main ran one after another, so a merge waited for the builds of every earlier merge before main got a result for its tree; a fix of a red main sat behind a build of the tree it fixes. The newest push now cancels the build in progress, like pull request and dispatched builds already do. The merge queue's baselines already skip cancelled main builds and use the nearest completed ones. Co-Authored-By: Claude --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c279099b88..8ed8ee86e9 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -28,7 +28,7 @@ permissions: concurrency: group: build-${{ github.event_name }}-${{ github.event.pull_request.number || inputs.pr || github.ref }} - cancel-in-progress: ${{ github.event_name != 'push' }} + cancel-in-progress: true env: CONFIGURE: >- From 3dca09e19477c5b4dd1149abab40cbf7c91ca144 Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Wed, 14 Oct 2026 17:18:47 +0000 Subject: [PATCH 0537/1000] fix(agc): validate geometry shader capability --- .../libSceAgcDriver/Execution/src/ShaderDeviceProfile.cpp | 1 + core/libs/prx/libSceAgcDriver/tests/ShaderDeviceProfile.cpp | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/ShaderDeviceProfile.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/ShaderDeviceProfile.cpp index df5ad7ec42..3ff6cc3d86 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/ShaderDeviceProfile.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/ShaderDeviceProfile.cpp @@ -47,6 +47,7 @@ ShaderDeviceProfile::ShaderDeviceProfile(const ShaderRecompiler::SpirvTarget& ta Require(!hasCapability(capability) || enabled, std::string("shader device profile advertises a disabled feature: ") + name); }; checkFeature(spv::CapabilityFloat64, core.shaderFloat64 == VK_TRUE, "shaderFloat64"); + checkFeature(spv::CapabilityGeometry, core.geometryShader == VK_TRUE, "geometryShader"); checkFeature(spv::CapabilityTessellation, core.tessellationShader == VK_TRUE, "tessellationShader"); checkFeature(spv::CapabilityImageGatherExtended, core.shaderImageGatherExtended == VK_TRUE, "shaderImageGatherExtended"); checkFeature(spv::CapabilityClipDistance, core.shaderClipDistance == VK_TRUE, "shaderClipDistance"); diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderDeviceProfile.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderDeviceProfile.cpp index f5014a3cb9..9b59ac9b2e 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderDeviceProfile.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderDeviceProfile.cpp @@ -81,6 +81,7 @@ void CheckProfile() { invalid([](auto& value) { value.bda.bufferDeviceAddress = VK_FALSE; }, "bufferDeviceAddress"); invalid([](auto& value) { value.device.pNext = nullptr; }, "bufferDeviceAddress"); invalid([](auto& value) { value.core.shaderInt64 = VK_FALSE; }, "shaderInt64"); + invalid([](auto& value) { value.capabilities.push_back(spv::CapabilityGeometry); }, "geometryShader"); invalid([](auto& value) { value.bytes.storageBuffer8BitAccess = VK_FALSE; }, "storageBuffer8BitAccess"); ProfileInput withoutNull; withoutNull.robustness.nullDescriptor = VK_FALSE; @@ -126,6 +127,11 @@ void CheckProfile() { disabled.device.pNext = &disabledTerminate; disabled.extensions.push_back("SPV_KHR_terminate_invocation"); Reject([&] { AgcDriver::ShaderDeviceProfile rejected(disabled.Target(), disabled.device, disabled.limits); }, "shaderTerminateInvocation"); + ProfileInput geometryEnabled; + geometryEnabled.capabilities.push_back(spv::CapabilityGeometry); + geometryEnabled.core.geometryShader = VK_TRUE; + const AgcDriver::ShaderDeviceProfile geometryAccepted(geometryEnabled.Target(), geometryEnabled.device, geometryEnabled.limits); + Require(std::ranges::find(geometryAccepted.Target().supportedCapabilities, spv::CapabilityGeometry) != geometryAccepted.Target().supportedCapabilities.end(), "enabled geometry shaders were rejected"); } void CheckAbi() { From 262de6ab19eb627ef5ab919ad0df26af3dfe4f27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Fri, 16 Oct 2026 19:23:16 +0200 Subject: [PATCH 0538/1000] ci: cache the submodules, FFmpeg and configure checks of the Windows job Co-Authored-By: Claude --- .github/workflows/build.yml | 51 ++++++++++++++++-- CMakeLists.txt | 2 + tools/configure_seed.py | 40 ++++++++++++++ tools/tests/test_configure_seed.py | 85 ++++++++++++++++++++++++++++++ 4 files changed, 175 insertions(+), 3 deletions(-) create mode 100644 tools/configure_seed.py create mode 100644 tools/tests/test_configure_seed.py diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8ed8ee86e9..086cdcf93d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -415,7 +415,6 @@ jobs: with: ref: ${{ needs.eligibility.outputs.sha }} persist-credentials: false - submodules: recursive - name: Merge the pull request into main if: ${{ needs.eligibility.outputs.head != '' }} shell: bash @@ -432,7 +431,28 @@ jobs: if [ "$(git rev-parse "refs/remotes/pull/${numbers[i]}")" != "${heads[i]}" ]; then echo "::error::Pull request #${numbers[i]} has newer commits; start a new build"; exit 1; fi if ! git -c user.name=ci -c user.email=ci@localhost merge --no-ff --no-edit "refs/remotes/pull/${numbers[i]}"; then echo "::error::Pull request #${numbers[i]} does not merge cleanly into main and the pull requests before it; rebase it first"; exit 1; fi done - git submodule update --init --recursive + - id: submodules + name: Key the submodules on their commits + run: | + echo "key=$(git ls-files -s | awk '$1 == "160000" { print $2, $4 }' | cat - .gitmodules | sha256sum | cut -c1-16)" >> "$GITHUB_OUTPUT" + echo "ffmpeg=$(git rev-parse HEAD:3rdparty/ffmpeg-core)" >> "$GITHUB_OUTPUT" + - id: submodule-cache + uses: actions/cache/restore@v6 + with: + path: ${{ runner.temp }}/submodules.tar + key: submodules-${{ runner.os }}-${{ steps.submodules.outputs.key }} + - name: Check out the submodules + run: | + if [ -f "$RUNNER_TEMP/submodules.tar" ]; then tar --force-local -xf "$RUNNER_TEMP/submodules.tar"; fi + git submodule update --init --recursive --depth 1 --jobs 8 + - if: steps.submodule-cache.outputs.cache-hit != 'true' + run: tar --force-local -cf "$RUNNER_TEMP/submodules.tar" .git/modules 3rdparty + - if: steps.submodule-cache.outputs.cache-hit != 'true' + uses: actions/cache/save@v6 + continue-on-error: true + with: + path: ${{ runner.temp }}/submodules.tar + key: submodules-${{ runner.os }}-${{ steps.submodules.outputs.key }} - name: Point the WinLibs CMake at the CA bundle of Git for Windows run: | for prefix in ucrt64 mingw64; do @@ -455,8 +475,33 @@ jobs: key: windows save: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} evict-old-files: job + - id: ffmpeg-cache + uses: actions/cache/restore@v6 + with: + path: build/externals + key: ffmpeg-${{ runner.os }}-${{ steps.submodules.outputs.ffmpeg }} + - id: seed-cache + uses: actions/cache/restore@v6 + with: + path: configure-seed.cmake + key: configure-seed-${{ env.WINLIBS_ZIP }}-${{ steps.submodules.outputs.key }}-${{ hashFiles('CMakeLists.txt', 'cmake/**', 'core/libs/CMakeLists.txt', '.github/workflows/build.yml') }} - shell: cmd - run: cmake -S . -B build %CONFIGURE% "-DANYPS5_AGC_EXECUTION_TESTS=F16ToF32Unpack;F64FmaSubnormals;RegistrationPrepareShutdown" + run: if exist configure-seed.cmake (cmake -C configure-seed.cmake -S . -B build %CONFIGURE% "-DANYPS5_AGC_EXECUTION_TESTS=F16ToF32Unpack;F64FmaSubnormals;RegistrationPrepareShutdown") else (cmake -S . -B build %CONFIGURE% "-DANYPS5_AGC_EXECUTION_TESTS=F16ToF32Unpack;F64FmaSubnormals;RegistrationPrepareShutdown") + - if: steps.seed-cache.outputs.cache-hit != 'true' + shell: cmd + run: python tools/configure_seed.py build/CMakeCache.txt configure-seed.cmake + - if: steps.seed-cache.outputs.cache-hit != 'true' + uses: actions/cache/save@v6 + continue-on-error: true + with: + path: configure-seed.cmake + key: ${{ steps.seed-cache.outputs.cache-primary-key }} + - if: steps.ffmpeg-cache.outputs.cache-hit != 'true' + uses: actions/cache/save@v6 + continue-on-error: true + with: + path: build/externals + key: ${{ steps.ffmpeg-cache.outputs.cache-primary-key }} - name: Check that every test executable is registered with ctest shell: cmd run: python tools/check_test_registration.py build diff --git a/CMakeLists.txt b/CMakeLists.txt index 23b603d59b..3eafb06e8e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -137,6 +137,8 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_skips.py) add_test(NAME check_main_sync COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_main_sync.py) + add_test(NAME configure_seed COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_configure_seed.py) add_test(NAME stubgen COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_stubgen.py) add_test(NAME debt_split COMMAND ${Python3_EXECUTABLE} diff --git a/tools/configure_seed.py b/tools/configure_seed.py new file mode 100644 index 0000000000..31a3eead18 --- /dev/null +++ b/tools/configure_seed.py @@ -0,0 +1,40 @@ +import argparse +import re +import sys +from pathlib import Path + +CHECK = re.compile(r"^//(Have (include|includes|function|symbol|library) |Test |Result of (TRY_COMPILE|TRY_RUN|TEST_BIG_ENDIAN|check_))") +ENTRY = re.compile(r"^([^#/:][^:]*):([A-Z]+)=(.*)$") + + +def checks(cache): + found, doc = {}, "" + for line in Path(cache).read_text(encoding="utf-8").splitlines(): + if line.startswith("//"): + doc = line + continue + match = ENTRY.match(line) + if match and match.group(2) == "INTERNAL" and CHECK.match(doc): + found[match.group(1)] = (match.group(3), doc[2:]) + doc = "" + return found + + +def quote(value): + return '"' + value.replace("\\", "\\\\").replace('"', '\\"').replace("$", "\\$") + '"' + + +def main(): + parser = argparse.ArgumentParser(description="Write the configure check results of a CMake cache as an initial cache file") + parser.add_argument("cache", help="CMakeCache.txt of a configured tree") + parser.add_argument("seed", help="file to pass to cmake -C") + args = parser.parse_args() + found = checks(args.cache) + Path(args.seed).write_text("".join(f"set({name} {quote(value)} CACHE INTERNAL {quote(doc)})\n" + for name, (value, doc) in sorted(found.items())), encoding="utf-8") + print(f"{len(found)} check results") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/tests/test_configure_seed.py b/tools/tests/test_configure_seed.py new file mode 100644 index 0000000000..375ea8c68c --- /dev/null +++ b/tools/tests/test_configure_seed.py @@ -0,0 +1,85 @@ +import shutil +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path + +TOOL = Path(__file__).resolve().parents[1] / "configure_seed.py" +sys.path.insert(0, str(TOOL.parent)) + +import configure_seed + +PROJECT = """cmake_minimum_required(VERSION 3.20) +project(mini C) +include(CheckIncludeFile) +include(CheckSymbolExists) +include(CheckCSourceCompiles) +check_include_file(stdio.h MINI_HAVE_STDIO_H) +check_include_file(mini_missing_header.h MINI_HAVE_MISSING_H) +check_symbol_exists(printf stdio.h MINI_HAVE_PRINTF) +check_c_source_compiles("int main(void) { return 0; }" MINI_COMPILES) +check_c_source_compiles("int main(void) { return undefined_name; }" MINI_FAILS) +set(MINI_OPTION "value" CACHE STRING "An option, not a check result") +""" + + +@unittest.skipUnless(shutil.which("ninja") and (shutil.which("gcc") or shutil.which("cc")), "needs ninja and a C compiler") +class ConfigureSeedTests(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.directory = tempfile.TemporaryDirectory() + cls.root = Path(cls.directory.name) + (cls.root / "source").mkdir() + (cls.root / "source" / "CMakeLists.txt").write_text(PROJECT, encoding="utf-8") + cls.compiler = shutil.which("gcc") or shutil.which("cc") + cls.fresh = cls.configure("fresh") + + @classmethod + def tearDownClass(cls): + cls.directory.cleanup() + + @classmethod + def configure(cls, name, *extra): + return subprocess.run(["cmake", *extra, "-S", str(cls.root / "source"), "-B", str(cls.root / name), "-G", "Ninja", + f"-DCMAKE_C_COMPILER={cls.compiler}"], check=True, capture_output=True, text=True).stdout + + def seed(self, cache, name): + seed = self.root / name + subprocess.run([sys.executable, str(TOOL), str(cache), str(seed)], check=True, capture_output=True) + return seed + + def test_seeded_configure_runs_no_check_and_gets_the_same_results(self): + self.assertRegex(self.fresh, r"Looking for|Performing Test") + output = self.configure("seeded", "-C", str(self.seed(self.root / "fresh" / "CMakeCache.txt", "seed.cmake"))) + self.assertNotRegex(output, r"Looking for|Performing Test") + fresh = configure_seed.checks(self.root / "fresh" / "CMakeCache.txt") + self.assertEqual(configure_seed.checks(self.root / "seeded" / "CMakeCache.txt"), fresh) + self.assertEqual(fresh["MINI_HAVE_STDIO_H"][0], "1") + self.assertEqual(fresh["MINI_FAILS"][0], "") + + def test_only_check_results_are_seeded(self): + found = configure_seed.checks(self.root / "fresh" / "CMakeCache.txt") + self.assertEqual({n for n in found if n.startswith("MINI_")}, + {"MINI_HAVE_STDIO_H", "MINI_HAVE_MISSING_H", "MINI_HAVE_PRINTF", "MINI_COMPILES", "MINI_FAILS"}) + self.assertNotIn("CMAKE_C_COMPILER", found) + + def test_values_with_cmake_syntax_round_trip(self): + cache = self.root / "odd" / "CMakeCache.txt" + cache.parent.mkdir() + value = r'a;b "c" \d ${e} $f' + cache.write_text(f"//Test MINI_ODD\nMINI_ODD:INTERNAL={value}\n", encoding="utf-8") + self.configure("odd-seeded", "-C", str(self.seed(cache, "odd.cmake"))) + seeded = (self.root / "odd-seeded" / "CMakeCache.txt").read_text(encoding="utf-8") + self.assertIn(f"MINI_ODD:INTERNAL={value}\n", seeded) + self.assertEqual(configure_seed.checks(self.root / "odd-seeded" / "CMakeCache.txt")["MINI_ODD"][0], value) + + def test_cache_without_checks_writes_an_empty_seed(self): + cache = self.root / "empty" / "CMakeCache.txt" + cache.parent.mkdir() + cache.write_text("//Path to a program.\nCMAKE_AR:FILEPATH=/usr/bin/ar\n", encoding="utf-8") + self.assertEqual(self.seed(cache, "empty.cmake").read_text(encoding="utf-8"), "") + + +if __name__ == "__main__": + unittest.main() From ad2b2b4c9c3a32f757aa17feaa1ad07c8914ec61 Mon Sep 17 00:00:00 2001 From: AlxcBTW <187763517+AlxcBTW@users.noreply.github.com> Date: Fri, 16 Oct 2026 18:43:08 +0200 Subject: [PATCH 0539/1000] docs: add AGENTS.md with the rules coding agents break --- AGENTS.md | 14 ++++++++++++++ tools/check_conventions.py | 4 ++-- tools/tests/test_check_conventions_agent_files.py | 8 +++++++- 3 files changed, 23 insertions(+), 3 deletions(-) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000000..a1e14a99f6 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,14 @@ +# AGENTS.md + +AnyPS5 relinks PS5 executables into native Linux or Windows programs that run against reimplemented system libraries; it is not an emulator. The rules are [CONTRIBUTING.md](CONTRIBUTING.md) and [CONVENTIONS](docs/dev/CONVENTIONS.md); these are the ones agents break. + +- No comments in code, except `#endif` and namespace ends. +- An export you cannot implement calls `NotImplemented_nid_no_patch(__func__)`; never return a made-up success. +- Implement the function for every input. Never special-case a value, ID or path seen in one title's trace; if the general behaviour is unknown, throw and record it in `docs/dev/debt/.md`. +- New third-party code is a submodule at `3rdparty/`: add its `.gitmodules` entry (`git submodule add 3rdparty/`) and build it from source with CMake. Never `find_package` or copy sources in. +- Known gaps go in `docs/dev/debt/.md`; notes, reports and logs go in the pull request. Add no other files. +- Commit subjects and PR titles are Conventional Commits. +- PR body: copy [the template](.github/pull_request_template.md), keep every line, tick every box, and fill `What`, `Tested`, `Depends on` (#N or none), `Unblocks` (title ID and discussion, or none), `GPUs tested` (AGC, shader recompiler, VideoOut; otherwise n/a) and `AI-assisted` (yes or no). +- Before pushing run `python3 tools/check_conventions.py --base origin/main`; before opening run `python3 tools/pr_overlap.py --branch `. +- Build and test: [BUILD](docs/dev/BUILD.md); a relinker-only change builds with `-DANYPS5_RELINKER_ONLY=ON`. +- A speed claim shows `tools/trace_digest.py` summaries of `main` and of the change from the same machine and scene. diff --git a/tools/check_conventions.py b/tools/check_conventions.py index 000a1de445..f558f4f409 100644 --- a/tools/check_conventions.py +++ b/tools/check_conventions.py @@ -20,7 +20,7 @@ RULES = { "system-dependency": ("error", "CONTRIBUTING.md#code", "Third-party code is a submodule under 3rdparty/ built from source, not found on the system"), "title-specific": ("error", "CONTRIBUTING.md#code", "Implement the general behaviour, not what one title needs; title-specific code belongs in its sce_module"), "extension": ("error", "CONTRIBUTING.md#code", "Avoid non-standard extensions where standard C++ is enough"), - "notes-file": ("error", "CONTRIBUTING.md#branches-and-pull-requests", "Notes, investigation and agent files go in the pull request, not in the repository"), + "notes-file": ("error", "CONTRIBUTING.md#branches-and-pull-requests", "Notes, investigation and agent files go in the pull request, not in the repository; only the root AGENTS.md is allowed"), "binary": ("error", "docs/dev/CONVENTIONS.md", "Only UTF-8 text files in the repository; images go in the gist comments"), "ci-gate": ("error", "CONTRIBUTING.md#build-and-test", "A job that runs for pull requests needs a file filter: paths on the pull_request trigger, or needs eligibility and an if on its outputs"), "doc-link": ("error", "CONTRIBUTING.md#documentation", "Relative link to a file that does not exist"), @@ -393,7 +393,7 @@ def library(path): def allowed_notes(path): p = PurePosixPath(path) if p.name.lower() in AGENT_FILES: - return False + return path == "AGENTS.md" if p.suffix.lower() not in NOTES: return None return path in ("README.md", "CONTRIBUTING.md") or p.parts[0] == ".github" or p.parts[:2] in (("docs", "dev"), ("docs", "user")) diff --git a/tools/tests/test_check_conventions_agent_files.py b/tools/tests/test_check_conventions_agent_files.py index 87704e1226..fcef15671a 100644 --- a/tools/tests/test_check_conventions_agent_files.py +++ b/tools/tests/test_check_conventions_agent_files.py @@ -10,7 +10,7 @@ from check_conventions import allowed_notes class AgentFileTests(unittest.TestCase): def test_agent_files_are_rejected_in_any_case(self): for name in ( - "CLAUDE.md", "claude.md", "Claude.md", "AGENTS.md", "Agents.md", "GEMINI.md", + "CLAUDE.md", "claude.md", "Claude.md", "GEMINI.md", "Copilot-Instructions.md", ".CursorRules", ".WindsurfRules", "windsurf.md", "ChatGPT.md", ".ChatGPTRules", ".aider.conf.yml", ): @@ -18,6 +18,12 @@ class AgentFileTests(unittest.TestCase): self.assertIs(allowed_notes(f"docs/dev/{name}"), False) self.assertIs(allowed_notes(name), False) + def test_only_the_root_agents_file_is_allowed(self): + self.assertIs(allowed_notes("AGENTS.md"), True) + for path in ("agents.md", "Agents.md", "docs/dev/AGENTS.md", "core/AGENTS.md", "tools/agents.md"): + with self.subTest(path=path): + self.assertIs(allowed_notes(path), False) + def test_ordinary_docs_are_still_allowed(self): self.assertTrue(allowed_notes("docs/dev/Claudette.md")) self.assertTrue(allowed_notes("README.md")) From 23a804e39320882d5ab3a4f004f89a289ed15f51 Mon Sep 17 00:00:00 2001 From: AlxcBTW <187763517+AlxcBTW@users.noreply.github.com> Date: Fri, 16 Oct 2026 19:52:11 +0200 Subject: [PATCH 0540/1000] perf(agc): count the processors the process may run on for the hand-off spin guard The automatic window needed 8 logical processors, which a 4-core CPU with SMT passes while the waited-for thread may run on a sibling thread, and ignored the process affinity. It now needs more than 8 processors in the affinity mask (CpuTopology), falling back to hardware_concurrency. --- .../Execution/include/HandoffSpin.hpp | 15 +++++++++++---- .../prx/libSceAgcDriver/tests/HandoffSpin.cpp | 15 ++++++++++++--- docs/dev/ARCHITECTURE.md | 2 +- 3 files changed, 24 insertions(+), 8 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/HandoffSpin.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/HandoffSpin.hpp index d21785cc3c..6c4b8213ea 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/HandoffSpin.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/HandoffSpin.hpp @@ -1,7 +1,9 @@ #ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_HANDOFFSPIN_HPP #define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_HANDOFFSPIN_HPP +#include "prx/libc/include/CpuTopology.hpp" #include +#include #include #include #include @@ -18,16 +20,21 @@ inline void SpinPause() { #endif } -inline std::chrono::nanoseconds HandoffSpinWindowFor(const char* configured, unsigned logicalProcessors) { +inline unsigned UsableProcessors() { + const auto count = static_cast(std::bitset<64>(CpuTopology::Get().process).count()); + return count != 0 ? count : std::thread::hardware_concurrency(); +} + +inline std::chrono::nanoseconds HandoffSpinWindowFor(const char* configured, unsigned usableProcessors) { constexpr std::chrono::microseconds automaticWindow{150}; - constexpr unsigned automaticProcessors = 8; + constexpr unsigned automaticProcessorsAbove = 8; constexpr long long longestWindowUs = 10000; if (configured != nullptr) return std::chrono::microseconds(std::clamp(std::strtoll(configured, nullptr, 10), 0, longestWindowUs)); - return logicalProcessors >= automaticProcessors ? std::chrono::nanoseconds(automaticWindow) : std::chrono::nanoseconds::zero(); + return usableProcessors > automaticProcessorsAbove ? std::chrono::nanoseconds(automaticWindow) : std::chrono::nanoseconds::zero(); } inline std::chrono::nanoseconds HandoffSpinWindow() { - static const std::chrono::nanoseconds window = HandoffSpinWindowFor(std::getenv("APS5_HANDOFF_SPIN_US"), std::thread::hardware_concurrency()); + static const std::chrono::nanoseconds window = HandoffSpinWindowFor(std::getenv("APS5_HANDOFF_SPIN_US"), UsableProcessors()); return window; } diff --git a/core/libs/prx/libSceAgcDriver/tests/HandoffSpin.cpp b/core/libs/prx/libSceAgcDriver/tests/HandoffSpin.cpp index 22b8bbc8e5..3b32c34c00 100644 --- a/core/libs/prx/libSceAgcDriver/tests/HandoffSpin.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/HandoffSpin.cpp @@ -1,4 +1,5 @@ #include "prx/libSceAgcDriver/Execution/include/HandoffSpin.hpp" +#include #include #include #include @@ -21,9 +22,10 @@ void Require(bool condition, const std::string& message) { } void CheckWindowPolicy() { - Require(HandoffSpinWindowFor(nullptr, 20) == 150us, "20 logical processors do not spin for 150 us"); - Require(HandoffSpinWindowFor(nullptr, 8) == 150us, "8 logical processors do not spin for 150 us"); - Require(HandoffSpinWindowFor(nullptr, 7) == 0ns, "7 logical processors spin"); + Require(HandoffSpinWindowFor(nullptr, 20) == 150us, "20 usable processors do not spin for 150 us"); + Require(HandoffSpinWindowFor(nullptr, 9) == 150us, "9 usable processors do not spin for 150 us"); + Require(HandoffSpinWindowFor(nullptr, 8) == 0ns, "8 usable processors spin"); + Require(HandoffSpinWindowFor(nullptr, 4) == 0ns, "4 usable processors spin"); Require(HandoffSpinWindowFor(nullptr, 0) == 0ns, "an unknown processor count spins"); Require(HandoffSpinWindowFor("0", 20) == 0ns, "APS5_HANDOFF_SPIN_US=0 still spins"); Require(HandoffSpinWindowFor("400", 2) == 400us, "APS5_HANDOFF_SPIN_US=400 does not set 400 us on 2 logical processors"); @@ -32,6 +34,12 @@ void CheckWindowPolicy() { Require(HandoffSpinWindowFor("99999999999", 20) == 10000us, "a huge window is not limited to 10000 us"); } +void CheckUsableProcessors() { + const unsigned usable = AgcDriver::UsableProcessors(); + Require(usable >= 1, "no usable processor was counted"); + Require(usable <= (std::max)(std::thread::hardware_concurrency(), 64u), "more usable processors than the machine has"); +} + void CheckSpinUntil() { int calls = 0; Require(SpinUntil(0ns, [&] { ++calls; return true; }) && calls == 1, "a ready condition was evaluated more than once"); @@ -109,6 +117,7 @@ void CheckRecursion() { int main() { try { CheckWindowPolicy(); + CheckUsableProcessors(); CheckSpinUntil(); Contend(0ns, "plain lock"); Contend(50us, "spin 50 us"); diff --git a/docs/dev/ARCHITECTURE.md b/docs/dev/ARCHITECTURE.md index 4e5872b5aa..12dcc29306 100644 --- a/docs/dev/ARCHITECTURE.md +++ b/docs/dev/ARCHITECTURE.md @@ -98,7 +98,7 @@ flowchart LR - The Vulkan pipelines are journaled as well ([`PipelineJournal.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp)): a graphics pipeline the store builds (`CachedPipeline`) or a compute pipeline a dispatch builds is appended to `pipelines.journal` next to the shader cache entries, as the materialized result of each stage (the specialized SPIR-V, bindings and push constants, through `ShaderDiskCache::EncodeResult`) plus the recipe the pipeline was built from (`GraphicsPipelineRecipe`: the stages' identities, vertex input layout, descriptor set layout keys and the state fields the `Pipeline` constructor reads, `CanonicalPipelineState`). When a `VulkanDevice` is created, the journal is loaded and its pipelines are built on a pool of `hardware_concurrency() / 2` threads (`APS5_PIPELINE_JOURNAL_THREADS`) while the title loads, so the first draw or dispatch of a configuration seen on an earlier run finds its pipeline resident. The pipeline stores key on `RecompileResult::Identity`, a hash of the module words, specialization constants, subgroup width and bindings that is the same on every run (`SharedSpirv::ContentHash`, computed once per shared module), rather than on the process-lifetime variant ids, and a graphics pipeline is built outside the store's mutex, so the replay threads and the queue workers build in parallel. A draw that arrives before its replayed entry builds the pipeline itself; the first inserted wins. A replayed entry enters the store at its least recently used end and a full store takes none, so the title's own pipelines are never evicted for a replayed one, and the optimized relink of a replayed pipeline (`OptimizedPipeline::Schedule`) waits for the first draw that binds it, so the relink thread serves the title's pipelines first. A device created without a window starts its replay at its first draw or dispatch rather than at creation, since `Driver::AttachWindow` usually replaces it before the title draws. The journal records a pipeline once (modules and recipes are deduplicated by content), keeps at most `APS5_PIPELINE_JOURNAL_MAX_MIB` (1024), replays the first `APS5_PIPELINE_JOURNAL_ENTRIES` recorded (1024, the pipeline store's bound, `APS5_PIPELINE_STORE_ENTRIES`; a run reaches those first), and is disabled with `APS5_NO_PIPELINE_JOURNAL=1` or without a shader cache directory. The file header names the device (`PipelineJournalDeviceFingerprint`, the properties the driver's pipeline cache file is named by), so a journal written for another GPU or driver is started over rather than replayed; appends and reads hold `pipelines.journal.lock` against another process sharing the directory, and a device cancels its replay before it is replaced (`PrepareForReplacement`) or destroyed, so a replaced headless device never inserts entries the window's device then sweeps. `APS5_PROFILE_DRAW=1` prints a `[pipeline-journal]` summary when the replay finishes and at teardown. - A compute shader whose LDS plus its lock dword exceeds the host's `maxComputeSharedMemorySize` is not given a `Workgroup` array. `Recompile` sets `ShaderInfo::sharedMemoryBytes` for it and the SPIR-V backend addresses the LDS in a coherent storage buffer at descriptor set 1 (`WorkgroupMemoryDescriptorSet`), with a slice of `RecompileResult::workgroupMemoryDwords` per workgroup, indexed by `WorkgroupID` and `NumWorkGroups`. `VulkanDevice` keeps one such buffer, sized from the dispatch dimensions, and binds it after the resource set; programs that fit keep using shared memory. - `Driver::RegisterShader` splits shader preparation in two. `PlanRegistered` runs on the registering guest thread: it validates the registers and entry point, decodes the code and builds the recompile request, so invalid registrations still fail there. The RDNA to IR translation and resource plan (`PrepareShader`) run on a background pool of `hardware_concurrency() / 2` detached threads (at least 2, 64 MiB stacks). -- A thread that needs the GPU mutex, the committer's next item or the end of the committer's queue spins before it blocks (`HandoffSpin.hpp`): `GpuMutexType::lock` retries `try_lock` for the window, the committer polls its item count, and `DrawPipeline::Drain` polls it for twice the window, so a hand-off that completes within the window costs no kernel wake-up. The window is 150 us on machines with at least 8 logical processors and 0 below that, where the spinning threads would compete with the holder; `APS5_HANDOFF_SPIN_US=` sets it (0 turns it off, at most 10000). Ordering and the lock's semantics are unchanged. +- A thread that needs the GPU mutex, the committer's next item or the end of the committer's queue spins before it blocks (`HandoffSpin.hpp`): `GpuMutexType::lock` retries `try_lock` for the window, the committer polls its item count, and `DrawPipeline::Drain` polls it for twice the window, so a hand-off that completes within the window costs no kernel wake-up. The window is 150 us when the process may run on more than 8 logical processors (its affinity mask, via `CpuTopology`) and 0 otherwise, where the spinning threads would compete with the holder; `APS5_HANDOFF_SPIN_US=` sets it (0 turns it off, at most 10000). Ordering and the lock's semantics are unchanged. - Queue 0's packet worker decodes, captures and prepares a draw and hands its recording (`Graphics::Draw` under the GPU lock) to a committer thread in [`DrawPipeline.cpp`](../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp), so the next packet is decoded while the previous draw is recorded. Each queued item carries the guest ranges it will write (`DrawWriteRanges`: written buffers and storage images, color targets with their DCC and CMASK, depth and stencil). A later packet that reads or writes one of those ranges, or that the pipeline can't order, drains it first; labels, submission ends, buffer fills and (with `APS5_PIPELINE_DMA=1`) DMA packets are queued as items instead. With `APS5_PIPELINE_DISPATCH=1` (off by default), a `DISPATCH_DIRECT` whose dispatch-cache entry hits with a recipe is queued as an item too, carrying the dispatch's written buffers and storage images (`DispatchWriteRanges`); the capture and the cache lookup stay on the worker and the recipe is recorded on the committer. A dispatch still drains when its lookup misses, when what it reads overlaps a write still pending, when the hit is verified against data or when its writes cannot be ranged (bindless access, GDS, fault buffer). An exception thrown by a commit is rethrown on the worker at its next enqueue or drain, as an inline draw error would be. On by default; `APS5_PIPELINED_DRAWS=0` records every draw inline, and `APS5_EXACT_DRAW_WRITES=0` widens the color and depth ranges to estimates. - The shader registry maps a code address to every header registered for that code, in registration order. Static, graphics and rectangle ABI resolution pick the header at the bound address (or one equal to it, `SameHeader`, which also takes a hull header fused by `sceAgcFuseShaderHalves` (type 3) as the registered back half (type 7) it copies, the fused registers aside; graphics ABI resolution then uses the fused header's shader registers); draws and dispatches pick the program by the stage's binary type (`RegisteredProgram`), and a front entry matching both type 2 and type 4 throws. Within a type the last registration wins; re-registering an identical earlier header makes its snapshot current again without preparing it again, and changed code drops the older headers. - The snapshot is published as pending. `SourceHandleFor`, `InvocationFor` and `ShaderPreparationTransaction::Edit` / `Read` wait for the pending job before reading `PreparedShaders`, and rethrow its failure. `APS5_SYNC_SHADER_PREPARE=1` prepares on the registering thread instead. From aa8c2f65e240607130e1bdafb1bc4b7b74314b7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexej=20Szpev=C3=A1k?= Date: Wed, 14 Oct 2026 14:56:57 +0200 Subject: [PATCH 0541/1000] build: add zstd and ZArchive submodules Build zstd v1.5.7 and ZArchive from source as static, position-independent libraries that are not part of the default target set. Their symbols stay out of the dynamic export table of the library that links them. --- .gitmodules | 6 ++++++ 3rdparty/ZArchive | 1 + 3rdparty/zstd | 1 + CMakeLists.txt | 2 ++ cmake/ZArchive.cmake | 25 +++++++++++++++++++++++++ 5 files changed, 35 insertions(+) create mode 160000 3rdparty/ZArchive create mode 160000 3rdparty/zstd create mode 100644 cmake/ZArchive.cmake diff --git a/.gitmodules b/.gitmodules index efcc5aa70b..f68f95f507 100644 --- a/.gitmodules +++ b/.gitmodules @@ -40,3 +40,9 @@ [submodule "3rdparty/mbedtls"] path = 3rdparty/mbedtls url = https://github.com/Mbed-TLS/mbedtls.git +[submodule "3rdparty/zstd"] + path = 3rdparty/zstd + url = https://github.com/facebook/zstd.git +[submodule "3rdparty/ZArchive"] + path = 3rdparty/ZArchive + url = https://github.com/Exzap/ZArchive.git diff --git a/3rdparty/ZArchive b/3rdparty/ZArchive new file mode 160000 index 0000000000..965b66c8d6 --- /dev/null +++ b/3rdparty/ZArchive @@ -0,0 +1 @@ +Subproject commit 965b66c8d67b6b7e30fd63b3b75aa91a99ff303b diff --git a/3rdparty/zstd b/3rdparty/zstd new file mode 160000 index 0000000000..f8745da6ff --- /dev/null +++ b/3rdparty/zstd @@ -0,0 +1 @@ +Subproject commit f8745da6ff1ad1e7bab384bd1f9d742439278e99 diff --git a/CMakeLists.txt b/CMakeLists.txt index 23b603d59b..ba9ca8b1d9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -267,6 +267,8 @@ set(LIBUSB_ENABLE_UDEV OFF CACHE BOOL "" FORCE) add_subdirectory(3rdparty/libusb-cmake EXCLUDE_FROM_ALL) set_target_properties(usb-1.0 PROPERTIES POSITION_INDEPENDENT_CODE ON) +include(cmake/ZArchive.cmake) + add_subdirectory(core/shader/recompiler) add_subdirectory(core/Decoder/Jpeg) add_subdirectory(core/Decoder/Png) diff --git a/cmake/ZArchive.cmake b/cmake/ZArchive.cmake new file mode 100644 index 0000000000..c4c7eb2024 --- /dev/null +++ b/cmake/ZArchive.cmake @@ -0,0 +1,25 @@ +enable_language(C) + +set(ZSTD_BUILD_PROGRAMS OFF CACHE BOOL "" FORCE) +set(ZSTD_BUILD_TESTS OFF CACHE BOOL "" FORCE) +set(ZSTD_BUILD_CONTRIB OFF CACHE BOOL "" FORCE) +set(ZSTD_BUILD_SHARED OFF CACHE BOOL "" FORCE) +set(ZSTD_BUILD_STATIC ON CACHE BOOL "" FORCE) +set(ZSTD_LEGACY_SUPPORT OFF CACHE BOOL "" FORCE) +set(ZSTD_MULTITHREAD_SUPPORT OFF CACHE BOOL "" FORCE) +add_subdirectory(${CMAKE_SOURCE_DIR}/3rdparty/zstd/build/cmake ${CMAKE_BINARY_DIR}/3rdparty/zstd EXCLUDE_FROM_ALL) +set_target_properties(libzstd_static PROPERTIES POSITION_INDEPENDENT_CODE ON) + +set(ZARCHIVE_SOURCE_DIR ${CMAKE_SOURCE_DIR}/3rdparty/ZArchive) +add_library(zarchive STATIC EXCLUDE_FROM_ALL + ${ZARCHIVE_SOURCE_DIR}/src/zarchivereader.cpp + ${ZARCHIVE_SOURCE_DIR}/src/zarchivewriter.cpp + ${ZARCHIVE_SOURCE_DIR}/src/sha_256.c +) +target_include_directories(zarchive SYSTEM PUBLIC ${ZARCHIVE_SOURCE_DIR}/include) +target_compile_features(zarchive PUBLIC cxx_std_20) +target_link_libraries(zarchive PRIVATE libzstd_static) +set_target_properties(zarchive PROPERTIES POSITION_INDEPENDENT_CODE ON) +if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + target_link_options(zarchive INTERFACE LINKER:--exclude-libs,libzarchive.a:libzstd.a) +endif() From b71434d9b76eaf7312b7ea4e3c697cc16e0d0b06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexej=20Szpev=C3=A1k?= Date: Wed, 14 Oct 2026 14:56:57 +0200 Subject: [PATCH 0542/1000] feat(libc): read app0.zar beside app0 and refuse archive-only paths elsewhere A ZArchive named app0.zar beside app0/ is a read-only layer under it. libc opens it at the first path resolution, looks paths missing from app0/ up case-insensitively, reads files across compressed blocks and keeps the archived descriptor table with shared positions. Resolving an archive-only path through a call that does not read the archive throws instead of failing with ENOENT, as do opendir of a directory present in both places, fdopen of an archived descriptor and chdir into the archive. A path through .. in an archive-only directory resolves. --- core/libs/prx/libc/CMakeLists.txt | 17 ++ core/libs/prx/libc/include/AppArchive.hpp | 48 ++++ .../libs/prx/libc/include/FilesystemError.hpp | 4 +- core/libs/prx/libc/include/General.hpp | 2 + core/libs/prx/libc/src/AppArchive.cpp | 239 ++++++++++++++++++ core/libs/prx/libc/src/Directory.cpp | 2 + core/libs/prx/libc/src/FileAndHeap.cpp | 2 + core/libs/prx/libc/src/General.cpp | 35 ++- core/libs/tests/AppArchiveFixture.hpp | 53 ++++ core/libs/tests/GuestAppArchive.cpp | 178 +++++++++++++ docs/dev/debt/libc.md | 1 + 11 files changed, 570 insertions(+), 11 deletions(-) create mode 100644 core/libs/prx/libc/include/AppArchive.hpp create mode 100644 core/libs/prx/libc/src/AppArchive.cpp create mode 100644 core/libs/tests/AppArchiveFixture.hpp create mode 100644 core/libs/tests/GuestAppArchive.cpp diff --git a/core/libs/prx/libc/CMakeLists.txt b/core/libs/prx/libc/CMakeLists.txt index b290daaf29..50d0ee43d9 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -1,6 +1,7 @@ target_sources(libc PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/Export.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/General.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/AppArchive.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/CreationMask.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/FailureReport.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/specifics/gcc/StackGuard.cpp @@ -57,6 +58,8 @@ target_include_directories(libc PRIVATE ${LIBS_INCLUDE_DIR} ) +target_link_libraries(libc PRIVATE zarchive) + if(WIN32) target_link_libraries(libc PRIVATE advapi32 ws2_32 iphlpapi) endif() @@ -71,6 +74,20 @@ if(BUILD_TESTING) set_tests_properties(guest_allocating_strings PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + + add_test_executable(guest_app_archive_tests ${LIBS_INCLUDE_DIR}/tests/GuestAppArchive.cpp) + target_include_directories(guest_app_archive_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_app_archive_tests PRIVATE libc zarchive) + configure_windows_unwind(guest_app_archive_tests) + foreach(mode present absent damaged invalid) + add_test(NAME guest_app_archive_${mode} COMMAND guest_app_archive_tests ${mode}) + set_tests_properties(guest_app_archive_${mode} PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_app_archive_${mode} PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + endforeach() + add_test_executable(guest_array_callbacks_tests ${LIBS_INCLUDE_DIR}/tests/GuestArrayCallbacks.cpp) target_include_directories(guest_array_callbacks_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_array_callbacks_tests PRIVATE libc) diff --git a/core/libs/prx/libc/include/AppArchive.hpp b/core/libs/prx/libc/include/AppArchive.hpp new file mode 100644 index 0000000000..13d25f34c3 --- /dev/null +++ b/core/libs/prx/libc/include/AppArchive.hpp @@ -0,0 +1,48 @@ +#ifndef CORE_LIBS_PRX_LIBC_INCLUDE_APPARCHIVE_HPP +#define CORE_LIBS_PRX_LIBC_INCLUDE_APPARCHIVE_HPP + +#include +#include +#include +#include +#include + +struct AppArchiveNode { + std::uint32_t Handle; + bool Directory; + std::uint64_t Size; +}; + +struct AppArchiveBuffer { + void* Base; + std::size_t Length; +}; + +struct AppArchiveDirectoryEntry { + std::string Name; + bool Directory; +}; + +extern "C" { + +bool AppArchiveLookup_nid_no_patch(const std::filesystem::path& hostPath, AppArchiveNode* node); +std::uint64_t AppArchiveRead_nid_no_patch(std::uint32_t handle, std::uint64_t offset, void* buffer, std::uint64_t length); +std::vector AppArchiveList_nid_no_patch(std::uint32_t handle); +void AppArchiveReady_nid_no_patch(); +void AppArchiveRefuseOnly_nid_no_patch(const std::filesystem::path& hostPath, const char* guestPath, const char* operation); +void AppArchiveRefuseMergedListing_nid_no_patch(const std::filesystem::path& hostPath, const char* guestPath); +void AppArchiveRefuseDescriptor_nid_no_patch(int descriptor, const char* operation); +std::filesystem::path AppArchivePath_nid_no_patch(); +bool AppArchiveOnly_nid_no_patch(const std::filesystem::path& hostPath); +bool AppArchiveHasDirectory_nid_no_patch(const std::filesystem::path& hostPath); + +void AppArchiveAttach_nid_no_patch(int descriptor, const AppArchiveNode& node); +void AppArchiveShare_nid_no_patch(int from, int to); +void AppArchiveDetach_nid_no_patch(int descriptor); +bool AppArchiveDescriptor_nid_no_patch(int descriptor, AppArchiveNode* node, std::uint64_t* position); +bool AppArchiveDescriptorRead_nid_no_patch(int descriptor, const AppArchiveBuffer* buffers, int count, const std::int64_t* offset, std::uint64_t* read); +bool AppArchiveDescriptorSeek_nid_no_patch(int descriptor, std::int64_t offset, int whence, std::int64_t* position); + +} + +#endif diff --git a/core/libs/prx/libc/include/FilesystemError.hpp b/core/libs/prx/libc/include/FilesystemError.hpp index c24d52e44e..038b5f04f7 100644 --- a/core/libs/prx/libc/include/FilesystemError.hpp +++ b/core/libs/prx/libc/include/FilesystemError.hpp @@ -25,9 +25,9 @@ inline int FilesystemError(const std::error_code& error) { std::error_code RenameNative_nid_no_patch(const std::filesystem::path& source, const std::filesystem::path& destination, bool replacesFile); -inline int ResolvePathError_nid_no_patch(const char* path, std::filesystem::path& resolved, bool module = false) { +inline int ResolvePathError_nid_no_patch(const char* path, std::filesystem::path& resolved, bool module = false, bool archived = false) { try { - resolved = module ? ResolvePathOrRelinkedModule_nid_no_patch(path) : ResolvePath_nid_no_patch(path); + resolved = archived ? ResolveArchivedPath_nid_no_patch(path, module) : module ? ResolvePathOrRelinkedModule_nid_no_patch(path) : ResolvePath_nid_no_patch(path); return 0; } catch (const std::filesystem::filesystem_error& error) { if (error.code() == std::errc::illegal_byte_sequence) return 2; diff --git a/core/libs/prx/libc/include/General.hpp b/core/libs/prx/libc/include/General.hpp index 37788afcfb..72dd4a7562 100644 --- a/core/libs/prx/libc/include/General.hpp +++ b/core/libs/prx/libc/include/General.hpp @@ -19,6 +19,8 @@ extern "C" int HostStandardError_nid_no_patch(); extern "C" std::filesystem::path ResolvePath_nid_no_patch(const char* path); extern "C" std::filesystem::path ResolvePathOrRelinkedModule_nid_no_patch(const char* path); +extern "C" std::filesystem::path ResolveArchivedPath_nid_no_patch(const char* path, bool module = false); +extern "C" std::filesystem::path ResolveDiskPath_nid_no_patch(const char* path); extern "C" std::filesystem::path RelinkedModulePath_nid_no_patch(const std::filesystem::path& path); extern "C" int SetWorkingDirectory_nid_no_patch(const char* resolved); extern "C" bool FinalPathComponentIsDot_nid_no_patch(const char* path); diff --git a/core/libs/prx/libc/src/AppArchive.cpp b/core/libs/prx/libc/src/AppArchive.cpp new file mode 100644 index 0000000000..be3916a765 --- /dev/null +++ b/core/libs/prx/libc/src/AppArchive.cpp @@ -0,0 +1,239 @@ +#include "prx/libc/include/AppArchive.hpp" +#include "prx/libc/include/General.hpp" + +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace { + +struct Archive { + std::mutex mutex; + bool opened = false; + std::unique_ptr reader; + std::filesystem::path path; + std::filesystem::path app0; +}; + +Archive& State() { + static Archive archive; + return archive; +} + +bool SameComponent(std::string_view left, std::string_view right) { + if (left.size() != right.size()) return false; + const auto fold = [](unsigned char value) { return value >= 'A' && value <= 'Z' ? value + ('a' - 'A') : value; }; + for (std::size_t i = 0; i < left.size(); ++i) if (fold(left[i]) != fold(right[i])) return false; + return true; +} + +Archive& Opened() { + auto& archive = State(); + std::lock_guard lock(archive.mutex); + if (archive.opened) return archive; + const auto app0 = ResolveDiskPath_nid_no_patch("/app0").lexically_normal(); + auto candidate = app0; + candidate += ".zar"; + std::error_code error; + if (std::filesystem::is_regular_file(candidate, error)) { + archive.reader.reset(ZArchiveReader::OpenFromFile(candidate)); + if (!archive.reader) throw std::runtime_error("app0.zar is not a readable ZArchive: " + candidate.string()); + archive.path = candidate; + } + archive.app0 = app0; + archive.opened = true; + return archive; +} + +bool ArchiveRelative(const Archive& archive, const std::filesystem::path& hostPath, std::string& relative) { + const auto normal = hostPath.lexically_normal(); + auto part = normal.begin(); + for (const auto& component : archive.app0) { + if (part == normal.end() || !SameComponent(part->generic_string(), component.generic_string())) return false; + ++part; + } + relative.clear(); + for (; part != normal.end(); ++part) { + const auto name = part->generic_string(); + if (name.empty() || name == ".") continue; + if (name == "..") return false; + if (!relative.empty()) relative += '/'; + relative += name; + } + return true; +} + +struct OpenFile { + AppArchiveNode node; + std::uint64_t position = 0; + std::mutex mutex; +}; + +std::mutex g_descriptorMutex; +std::map> g_descriptors; + +std::shared_ptr Attached(int descriptor) { + std::lock_guard lock(g_descriptorMutex); + const auto found = g_descriptors.find(descriptor); + return found == g_descriptors.end() ? nullptr : found->second; +} + +ZArchiveReader& Reader(std::uint32_t handle) { + auto& archive = Opened(); + if (!archive.reader) throw std::logic_error("app0.zar handle " + std::to_string(handle) + " used without an archive"); + return *archive.reader; +} + +} + +extern "C" { + +bool AppArchiveLookup_nid_no_patch(const std::filesystem::path& hostPath, AppArchiveNode* node) { + if (node == nullptr) throw std::invalid_argument("AppArchiveLookup: null node"); + auto& archive = Opened(); + if (!archive.reader) return false; + std::string relative; + if (!ArchiveRelative(archive, hostPath, relative)) return false; + const auto handle = archive.reader->LookUp(relative); + if (handle == ZARCHIVE_INVALID_NODE) return false; + node->Handle = handle; + node->Directory = archive.reader->IsDirectory(handle); + node->Size = node->Directory ? 0 : archive.reader->GetFileSize(handle); + return true; +} + +std::uint64_t AppArchiveRead_nid_no_patch(std::uint32_t handle, std::uint64_t offset, void* buffer, std::uint64_t length) { + auto& reader = Reader(handle); + if (!reader.IsFile(handle)) throw std::invalid_argument("AppArchiveRead: handle " + std::to_string(handle) + " is not a file"); + const auto size = reader.GetFileSize(handle); + if (length == 0 || offset >= size) return 0; + if (buffer == nullptr) throw std::invalid_argument("AppArchiveRead: null buffer"); + const auto expected = std::min(length, size - offset); + const auto read = reader.ReadFromFile(handle, offset, length, buffer); + if (read != expected) throw std::runtime_error("app0.zar is damaged: a compressed block of handle " + std::to_string(handle) + " cannot be read"); + return read; +} + +std::vector AppArchiveList_nid_no_patch(std::uint32_t handle) { + auto& reader = Reader(handle); + if (!reader.IsDirectory(handle)) throw std::invalid_argument("AppArchiveList: handle " + std::to_string(handle) + " is not a directory"); + std::vector entries; + const auto count = reader.GetDirEntryCount(handle); + entries.reserve(count); + for (std::uint32_t index = 0; index < count; ++index) { + ZArchiveReader::DirEntry entry{}; + if (!reader.GetDirEntry(handle, index, entry)) throw std::runtime_error("app0.zar is damaged: directory entry " + std::to_string(index) + " cannot be read"); + entries.push_back({std::string(entry.name), entry.isDirectory}); + } + return entries; +} + +void AppArchiveReady_nid_no_patch() { + (void)Opened(); +} + +void AppArchiveRefuseOnly_nid_no_patch(const std::filesystem::path& hostPath, const char* guestPath, const char* operation) { + if (AppArchiveOnly_nid_no_patch(hostPath)) NotImplemented_nid_no_patch((std::string(operation) + " " + guestPath + " in app0.zar").c_str()); +} + +void AppArchiveRefuseMergedListing_nid_no_patch(const std::filesystem::path& hostPath, const char* guestPath) { + if (AppArchiveHasDirectory_nid_no_patch(hostPath)) NotImplemented_nid_no_patch((std::string("listing ") + guestPath + " merged with app0.zar").c_str()); +} + +void AppArchiveRefuseDescriptor_nid_no_patch(int descriptor, const char* operation) { + if (AppArchiveDescriptor_nid_no_patch(descriptor, nullptr, nullptr)) NotImplemented_nid_no_patch((std::string(operation) + " of a file read from app0.zar").c_str()); +} + +std::filesystem::path AppArchivePath_nid_no_patch() { + return Opened().path; +} + +bool AppArchiveOnly_nid_no_patch(const std::filesystem::path& hostPath) { + auto& archive = Opened(); + if (!archive.reader) return false; + std::error_code error; + const auto status = std::filesystem::symlink_status(hostPath, error); + if (std::filesystem::exists(status) || (error && error != std::errc::no_such_file_or_directory)) return false; + AppArchiveNode node{}; + return AppArchiveLookup_nid_no_patch(hostPath, &node); +} + +bool AppArchiveHasDirectory_nid_no_patch(const std::filesystem::path& hostPath) { + AppArchiveNode node{}; + return AppArchiveLookup_nid_no_patch(hostPath, &node) && node.Directory; +} + +void AppArchiveAttach_nid_no_patch(int descriptor, const AppArchiveNode& node) { + if (node.Directory) throw std::invalid_argument("AppArchiveAttach: descriptor " + std::to_string(descriptor) + " cannot hold an app0.zar directory"); + auto file = std::make_shared(); + file->node = node; + std::lock_guard lock(g_descriptorMutex); + g_descriptors[descriptor] = std::move(file); +} + +void AppArchiveShare_nid_no_patch(int from, int to) { + std::lock_guard lock(g_descriptorMutex); + const auto found = g_descriptors.find(from); + if (found == g_descriptors.end()) g_descriptors.erase(to); + else g_descriptors[to] = found->second; +} + +void AppArchiveDetach_nid_no_patch(int descriptor) { + std::lock_guard lock(g_descriptorMutex); + g_descriptors.erase(descriptor); +} + +bool AppArchiveDescriptor_nid_no_patch(int descriptor, AppArchiveNode* node, std::uint64_t* position) { + const auto file = Attached(descriptor); + if (!file) return false; + std::lock_guard lock(file->mutex); + if (node != nullptr) *node = file->node; + if (position != nullptr) *position = file->position; + return true; +} + +bool AppArchiveDescriptorRead_nid_no_patch(int descriptor, const AppArchiveBuffer* buffers, int count, const std::int64_t* offset, std::uint64_t* read) { + const auto file = Attached(descriptor); + if (!file) return false; + if (read == nullptr || count < 0 || (buffers == nullptr && count != 0)) throw std::invalid_argument("AppArchiveDescriptorRead: invalid arguments for descriptor " + std::to_string(descriptor)); + if (offset != nullptr && *offset < 0) throw std::invalid_argument("AppArchiveDescriptorRead: negative offset for descriptor " + std::to_string(descriptor)); + std::lock_guard lock(file->mutex); + auto cursor = offset != nullptr ? static_cast(*offset) : file->position; + std::uint64_t total = 0; + for (int i = 0; i < count; ++i) { + const auto done = AppArchiveRead_nid_no_patch(file->node.Handle, cursor, buffers[i].Base, buffers[i].Length); + cursor += done; + total += done; + if (done < buffers[i].Length) break; + } + if (offset == nullptr) file->position = cursor; + *read = total; + return true; +} + +bool AppArchiveDescriptorSeek_nid_no_patch(int descriptor, std::int64_t offset, int whence, std::int64_t* position) { + const auto file = Attached(descriptor); + if (!file) return false; + if (position == nullptr) throw std::invalid_argument("AppArchiveDescriptorSeek: null position"); + std::lock_guard lock(file->mutex); + constexpr auto limit = std::numeric_limits::max(); + std::int64_t base = 0; + if (whence == 1) base = static_cast(file->position); + else if (whence == 2) base = static_cast(file->node.Size); + else if (whence != 0) throw std::invalid_argument("AppArchiveDescriptorSeek: invalid whence " + std::to_string(whence)); + if ((offset > 0 && base > limit - offset) || base + offset < 0) { + *position = -1; + return true; + } + file->position = static_cast(base + offset); + *position = base + offset; + return true; +} + +} diff --git a/core/libs/prx/libc/src/Directory.cpp b/core/libs/prx/libc/src/Directory.cpp index ff24e15a7e..d831ad7976 100644 --- a/core/libs/prx/libc/src/Directory.cpp +++ b/core/libs/prx/libc/src/Directory.cpp @@ -1,6 +1,7 @@ #include "prx/libc/include/GuestDirectory.hpp" #include "prx/libc/include/General.hpp" #include "prx/libc/include/FilesystemError.hpp" +#include "prx/libc/include/AppArchive.hpp" #include #include #include @@ -109,6 +110,7 @@ void* APS5_VABI opendir_nid_postfix(const char* path) { try { auto directory = std::make_unique(); if (const int error = ResolvePathError_nid_no_patch(path, directory->path)) { errno = error; return nullptr; } + AppArchiveRefuseMergedListing_nid_no_patch(directory->path, path); directory->native = OpenNative(directory->path); if (!directory->native) { errno = DirectoryError(errno); return nullptr; } return directory.release(); diff --git a/core/libs/prx/libc/src/FileAndHeap.cpp b/core/libs/prx/libc/src/FileAndHeap.cpp index 16025ecbb9..bf4ae212de 100644 --- a/core/libs/prx/libc/src/FileAndHeap.cpp +++ b/core/libs/prx/libc/src/FileAndHeap.cpp @@ -26,6 +26,7 @@ #include "prx/libc/include/ApplicationHeap.hpp" #include "prx/libc/include/General.hpp" #include "prx/libc/include/FilesystemError.hpp" +#include "prx/libc/include/AppArchive.hpp" #include "prx/libc/include/GuestArena.hpp" #ifdef __linux__ @@ -237,6 +238,7 @@ extern "C" { FileStream* APS5_VABI fdopen_nid_postfix(int descriptor, const char* mode) { if (descriptor < 0) { errno = 9; return nullptr; } if (!mode) { errno = 22; return nullptr; } + AppArchiveRefuseDescriptor_nid_no_patch(descriptor, "fdopen"); const auto parsed = ParseFileMode(mode); if (!parsed) { errno = 22; return nullptr; } try { diff --git a/core/libs/prx/libc/src/General.cpp b/core/libs/prx/libc/src/General.cpp index 2bbfbb1f5e..27caa6d483 100644 --- a/core/libs/prx/libc/src/General.cpp +++ b/core/libs/prx/libc/src/General.cpp @@ -31,6 +31,7 @@ #include #endif #include "prx/libc/include/General.hpp" +#include "prx/libc/include/AppArchive.hpp" #include "prx/libc/include/ApplicationHeap.hpp" namespace { @@ -192,7 +193,7 @@ void RequireHostDirectory(WorkingDirectory& state, const std::filesystem::path& #endif throw std::filesystem::filesystem_error("Guest path component is not a directory", host, error); } -std::filesystem::path Resolve(WorkingDirectory& state, const char* path, bool module = false) { +std::filesystem::path Resolve(WorkingDirectory& state, const char* path, bool module = false, bool archived = false) { std::string text(path); for (auto& character : text) if (character == '\\') character = '/'; std::filesystem::path input(text); @@ -206,7 +207,7 @@ std::filesystem::path Resolve(WorkingDirectory& state, const char* path, bool mo for (auto part = relative.begin(); part != relative.end(); ++part) { if (*part == "..") { const auto parent = guest.lexically_normal(); - if (parent.has_relative_path()) RequireHostDirectory(state, parent); + if (parent.has_relative_path() && !(archived && AppArchiveHasDirectory_nid_no_patch(HostPath(state, parent)))) RequireHostDirectory(state, parent); } guest /= *part; } @@ -424,18 +425,32 @@ extern "C" void BlockPathAlias_nid_no_patch(const char* guestPrefix) { aliases.blocked.push_back(prefix); } -extern "C" std::filesystem::path ResolvePath_nid_no_patch(const char* path) { +static std::filesystem::path ResolveLocked(const char* path, bool module, bool archived) { if (!path) { APS5_INVALID_ARG_EX; } + if (archived) AppArchiveReady_nid_no_patch(); auto& state = Directories(); std::lock_guard lock(state.mutex); - return Resolve(state, path); + return Resolve(state, path, module, archived); +} + +extern "C" std::filesystem::path ResolveDiskPath_nid_no_patch(const char* path) { + return ResolveLocked(path, false, false); +} + +extern "C" std::filesystem::path ResolveArchivedPath_nid_no_patch(const char* path, bool module) { + return ResolveLocked(path, module, true); +} + +extern "C" std::filesystem::path ResolvePath_nid_no_patch(const char* path) { + auto resolved = ResolveLocked(path, false, true); + AppArchiveRefuseOnly_nid_no_patch(resolved, path, "reading"); + return resolved; } extern "C" std::filesystem::path ResolvePathOrRelinkedModule_nid_no_patch(const char* path) { - if (!path) { APS5_INVALID_ARG_EX; } - auto& state = Directories(); - std::lock_guard lock(state.mutex); - return Resolve(state, path, true); + auto resolved = ResolveLocked(path, true, true); + AppArchiveRefuseOnly_nid_no_patch(resolved, path, "reading"); + return resolved; } extern "C" std::filesystem::path RelinkedModulePath_nid_no_patch(const std::filesystem::path& path) { @@ -472,9 +487,11 @@ extern "C" int APS5_VABI chdir_nid_postfix(const char* path) { if (!path) { errno = 14; return -1; } if (!*path) { errno = 2; return -1; } try { + AppArchiveReady_nid_no_patch(); auto& state = Directories(); std::lock_guard lock(state.mutex); - const auto resolved = Resolve(state, path); + const auto resolved = Resolve(state, path, false, true); + AppArchiveRefuseOnly_nid_no_patch(resolved, path, "changing the working directory to"); const int error = SetWorkingDirectory(state, resolved); if (error) { errno = error; return -1; } return 0; diff --git a/core/libs/tests/AppArchiveFixture.hpp b/core/libs/tests/AppArchiveFixture.hpp new file mode 100644 index 0000000000..b6c5e4529c --- /dev/null +++ b/core/libs/tests/AppArchiveFixture.hpp @@ -0,0 +1,53 @@ +#ifndef CORE_LIBS_TESTS_APPARCHIVEFIXTURE_HPP +#define CORE_LIBS_TESTS_APPARCHIVEFIXTURE_HPP + +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace AppArchiveFixture { + +inline std::vector Pattern(std::size_t size) { + std::vector data(size); + for (std::size_t i = 0; i < size; ++i) data[i] = static_cast((i * 7 + i / 4096) & 0xff); + return data; +} + +inline std::filesystem::path EnterFreshRoot(const char* name) { + const auto root = std::filesystem::temp_directory_path() / + (std::string(name) + "-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); + std::filesystem::create_directories(root / "app0" / "sce_sys"); + std::filesystem::current_path(root); + return std::filesystem::canonical(root); +} + +inline void WriteArchive(const std::filesystem::path& path, const std::map>& files, const std::vector& directories = {}) { + std::ofstream output(path, std::ios::binary | std::ios::trunc); + if (!output) std::abort(); + ZArchiveWriter writer( + [](std::int32_t, void*) {}, + [](const void* data, std::size_t length, void* context) { + static_cast(context)->write(static_cast(data), static_cast(length)); + }, + &output); + for (const auto& directory : directories) if (!writer.MakeDir(directory.c_str(), true)) std::abort(); + for (const auto& [name, data] : files) { + const auto slash = name.find_last_of('/'); + if (slash != std::string::npos) writer.MakeDir(name.substr(0, slash).c_str(), true); + if (!writer.StartNewFile(name.c_str())) std::abort(); + if (!data.empty()) writer.AppendData(data.data(), data.size()); + } + writer.Finalize(); + output.close(); + if (!output) std::abort(); +} + +} + +#endif diff --git a/core/libs/tests/GuestAppArchive.cpp b/core/libs/tests/GuestAppArchive.cpp new file mode 100644 index 0000000000..1f865808a5 --- /dev/null +++ b/core/libs/tests/GuestAppArchive.cpp @@ -0,0 +1,178 @@ +#include "AppArchiveFixture.hpp" +#include "prx/libc/include/AppArchive.hpp" +#include "prx/libc/include/General.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +void Require(bool condition, const char* message) { + if (!condition) { + std::cerr << message << '\n'; + std::exit(1); + } +} + +bool Lookup(const char* guestPath, AppArchiveNode& node) { + return AppArchiveLookup_nid_no_patch(ResolveArchivedPath_nid_no_patch(guestPath), &node); +} + +bool GuardRefuses(const char* guestPath) { + try { + (void)ResolvePath_nid_no_patch(guestPath); + } catch (const std::runtime_error& error) { + return std::strstr(error.what(), "not implemented") != nullptr && std::strstr(error.what(), guestPath) != nullptr; + } + return false; +} + +constexpr std::size_t BigSize = 200 * 1024 + 123; + +std::map> Contents() { + return { + {"Data/Big.bin", AppArchiveFixture::Pattern(BigSize)}, + {"Data/Empty.txt", {}}, + {"Data/Sub/Deep.txt", {'d', 'e', 'e', 'p'}}, + {"Readme.TXT", {'r', 'e', 'a', 'd'}}, + }; +} + +void Present(const std::filesystem::path& root) { + AppArchiveFixture::WriteArchive(root / "app0.zar", Contents(), {"Data/Unused"}); + { std::ofstream eboot(root / "app0" / "eboot.bin"); eboot << "elf"; } + Require(AppArchivePath_nid_no_patch() == root / "app0.zar", "the archive beside app0 was not opened"); + + AppArchiveNode node{}; + Require(Lookup("/app0/data/BIG.BIN", node) && !node.Directory && node.Size == BigSize, "a file was not found case-insensitively with its size"); + const auto big = node.Handle; + Require(Lookup("/APP0/Data/Empty.txt", node) && !node.Directory && node.Size == 0, "an empty file was not found"); + Require(Lookup("/app0/Data", node) && node.Directory, "a directory was not found"); + const auto data = node.Handle; + Require(Lookup("/app0/", node) && node.Directory, "the archive root was not app0"); + Require(Lookup("/app0/Data/Sub/../Sub/deep.txt", node) && node.Size == 4, "a normalised path was not found"); + Require(!Lookup("/app0/Data/Missing.bin", node), "a missing file was found"); + Require(!Lookup("/app0/Readme.TXT/x", node), "a path through a file was found"); + Require(!Lookup("/app0x/Readme.TXT", node), "a sibling of app0 was looked up in the archive"); + Require(!Lookup("/Readme.TXT", node), "a path outside app0 was looked up in the archive"); + + Require(GuardRefuses("/app0/Data/Big.bin"), "an archive-only file resolved through an unaware call"); + Require(GuardRefuses("/app0/Data"), "an archive-only directory resolved through an unaware call"); + Require(ResolvePath_nid_no_patch("/app0/eboot.bin") == root / "app0" / "eboot.bin", "a file on disk was refused"); + Require(ResolvePath_nid_no_patch("/app0/Data/Missing.bin") == root / "app0" / "Data" / "Missing.bin", "a path missing everywhere was refused"); + Require(Lookup("/app0/sce_sys", node) == false && ResolvePath_nid_no_patch("/app0/sce_sys") == root / "app0" / "sce_sys", "a directory only on disk was not left to the disk"); + + bool rejected = false; + const auto expected = AppArchiveFixture::Pattern(BigSize); + std::vector buffer(BigSize + 64, 0); + Require(AppArchiveRead_nid_no_patch(big, 0, buffer.data(), BigSize) == BigSize && std::equal(expected.begin(), expected.end(), buffer.begin()), "the whole file did not read back"); + for (const std::uint64_t offset : {std::uint64_t{1}, std::uint64_t{65535}, std::uint64_t{65536}, std::uint64_t{131071}, std::uint64_t{BigSize - 5}}) { + std::fill(buffer.begin(), buffer.end(), 0); + const auto read = AppArchiveRead_nid_no_patch(big, offset, buffer.data(), 70000); + const auto wanted = std::min(70000, BigSize - offset); + Require(read == wanted && std::equal(expected.begin() + static_cast(offset), expected.begin() + static_cast(offset + wanted), buffer.begin()), "a read across block boundaries returned wrong bytes"); + } + Require(AppArchiveRead_nid_no_patch(big, BigSize, buffer.data(), 10) == 0, "a read at the end returned bytes"); + Require(AppArchiveRead_nid_no_patch(big, 10, buffer.data(), 0) == 0, "a zero-length read returned bytes"); + + AppArchiveNode bigNode{}; + Require(Lookup("/app0/Data/Big.bin", bigNode), "the big file vanished"); + constexpr int Descriptor = 1000003; + constexpr int Shared = 1000004; + Require(!AppArchiveDescriptor_nid_no_patch(Descriptor, nullptr, nullptr), "a descriptor was attached before attaching"); + AppArchiveAttach_nid_no_patch(Descriptor, bigNode); + AppArchiveShare_nid_no_patch(Descriptor, Shared); + std::uint64_t done = 0; + std::vector first(65530), second(20); + const AppArchiveBuffer split[] = {{first.data(), first.size()}, {second.data(), second.size()}}; + Require(AppArchiveDescriptorRead_nid_no_patch(Descriptor, split, 2, nullptr, &done) && done == first.size() + second.size(), "a vector read returned the wrong length"); + Require(std::equal(first.begin(), first.end(), expected.begin()) && std::equal(second.begin(), second.end(), expected.begin() + 65530), "a vector read returned wrong bytes"); + std::uint64_t position = 0; + Require(AppArchiveDescriptor_nid_no_patch(Shared, nullptr, &position) && position == 65550, "a shared descriptor did not share the position"); + const std::int64_t at = 131000; + const AppArchiveBuffer single[] = {{second.data(), second.size()}}; + Require(AppArchiveDescriptorRead_nid_no_patch(Shared, single, 1, &at, &done) && done == 20 && std::equal(second.begin(), second.end(), expected.begin() + at), "a positioned read returned wrong bytes"); + Require(AppArchiveDescriptor_nid_no_patch(Descriptor, nullptr, &position) && position == 65550, "a positioned read moved the position"); + std::int64_t sought = 0; + Require(AppArchiveDescriptorSeek_nid_no_patch(Shared, -3, 2, &sought) && sought == static_cast(BigSize - 3), "seeking from the end failed"); + Require(AppArchiveDescriptorRead_nid_no_patch(Descriptor, single, 1, nullptr, &done) && done == 3, "a read at the end was not short"); + Require(AppArchiveDescriptorRead_nid_no_patch(Descriptor, single, 1, nullptr, &done) && done == 0, "a read past the end returned bytes"); + Require(AppArchiveDescriptorSeek_nid_no_patch(Descriptor, -1, 0, &sought) && sought == -1, "a negative seek was accepted"); + Require(AppArchiveDescriptorSeek_nid_no_patch(Descriptor, 10, 1, &sought) && sought == static_cast(BigSize + 10), "seeking past the end failed"); + AppArchiveDetach_nid_no_patch(Descriptor); + Require(!AppArchiveDescriptor_nid_no_patch(Descriptor, nullptr, nullptr) && AppArchiveDescriptor_nid_no_patch(Shared, nullptr, nullptr), "detaching one descriptor detached its duplicate"); + AppArchiveShare_nid_no_patch(Descriptor, Shared); + Require(!AppArchiveDescriptor_nid_no_patch(Shared, nullptr, nullptr), "duplicating a plain descriptor over an archived one kept the archive"); + rejected = false; + try { AppArchiveAttach_nid_no_patch(Descriptor, AppArchiveNode{0, true, 0}); } catch (const std::invalid_argument&) { rejected = true; } + Require(rejected, "a directory was attached to a descriptor"); + + std::set> names; + for (const auto& entry : AppArchiveList_nid_no_patch(data)) names.insert({entry.Name, entry.Directory}); + Require(names == std::set>{{"Big.bin", false}, {"Empty.txt", false}, {"Sub", true}, {"Unused", true}}, "the directory listing is wrong"); + + rejected = false; + try { (void)AppArchiveList_nid_no_patch(big); } catch (const std::invalid_argument&) { rejected = true; } + Require(rejected, "a file was listed as a directory"); + rejected = false; + try { (void)AppArchiveRead_nid_no_patch(data, 0, buffer.data(), 1); } catch (const std::invalid_argument&) { rejected = true; } + Require(rejected, "a directory was read as a file"); +} + +void Absent() { + AppArchiveNode node{}; + Require(AppArchivePath_nid_no_patch().empty(), "an archive was reported without app0.zar"); + Require(!Lookup("/app0/Readme.TXT", node), "a path was found without an archive"); + Require(ResolvePath_nid_no_patch("/app0/Readme.TXT").filename() == "Readme.TXT", "resolution changed without an archive"); +} + +void Damaged(const std::filesystem::path& root) { + AppArchiveFixture::WriteArchive(root / "app0.zar", Contents()); + { + std::fstream archive(root / "app0.zar", std::ios::binary | std::ios::in | std::ios::out); + const char garbage[8] = {0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x00}; + archive.seekp(0); + archive.write(garbage, sizeof(garbage)); + } + AppArchiveNode node{}; + Require(Lookup("/app0/Data/Big.bin", node), "the damaged archive's index was not read"); + std::vector buffer(16); + bool thrown = false; + try { (void)AppArchiveRead_nid_no_patch(node.Handle, 0, buffer.data(), buffer.size()); } catch (const std::runtime_error& error) { thrown = std::strstr(error.what(), "damaged") != nullptr; } + Require(thrown, "a damaged block did not fail the read"); +} + +void Invalid(const std::filesystem::path& root) { + { std::ofstream archive(root / "app0.zar", std::ios::binary); archive << "not an archive"; } + AppArchiveNode node{}; + bool thrown = false; + try { (void)Lookup("/app0/Readme.TXT", node); } catch (const std::runtime_error& error) { thrown = std::strstr(error.what(), "app0.zar") != nullptr; } + Require(thrown, "an invalid app0.zar was not reported"); +} + +} + +int main(int argc, char** argv) { + if (argc != 2) { + std::cerr << "usage: guest_app_archive_tests present|absent|damaged|invalid\n"; + return 2; + } + const std::string mode = argv[1]; + const auto root = AppArchiveFixture::EnterFreshRoot("anyps5-app-archive"); + if (mode == "present") Present(root); + else if (mode == "absent") Absent(); + else if (mode == "damaged") Damaged(root); + else if (mode == "invalid") Invalid(root); + else return 2; + std::error_code error; + std::filesystem::current_path(std::filesystem::temp_directory_path(), error); + std::filesystem::remove_all(root, error); + return 0; +} diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index eb244ed912..37468e999c 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -81,3 +81,4 @@ - [std::_Throw_Cpp_error](../../../core/libs/prx/libc/src/exception/StandardExceptions.cpp) (libSceAmpr) follows Dinkumware's `thread0.cpp` as shipped with [Visual C++ 2013](https://github.com/ganboing/crt.vc12/blob/f1d47126bf13d1c9dccaa6c53b72217fd0474968/src/thread0.cpp) and in [microsoft/STL](https://github.com/microsoft/STL/blob/da52dc0fcadbd83690cfd1ed32408d2847439493/stl/src/thread0.cpp), without console verification. `_Cpp_error` codes 0 to 6 throw a guest `std::system_error` whose `error_code` holds the FreeBSD errno of the `errc` value (EBUSY 16, EINVAL 22, ESRCH 3, ENOMEM 12, EPERM 1, EDEADLK 11, EAGAIN 35) and the generic category, and whose `what()` is the table message, `": "` and the generic category's message for that errno, selected by the first matching errno entry in VC12's [`syserror.cpp`](https://github.com/ganboing/crt.vc12/blob/f1d47126bf13d1c9dccaa6c53b72217fd0474968/src/syserror.cpp); another code throws `std::out_of_range` on the host instead of reading past the table. The guest `system_error` is taken to be [VC12's](https://github.com/ganboing/crt.vc12/blob/f1d47126bf13d1c9dccaa6c53b72217fd0474968/src/system_error): the 16-byte `runtime_error` followed by `error_code {int, const error_category*}`, 32 bytes, so its copy constructor and assignment copy the code too. The generic category is a static libc object with VC12's `error_category` virtual order (destructors, `name`, `message`, `default_error_condition`, the two `equivalent`), not the console's `_Error_objects::_Generic_object`, and the new `generic_category()` export returns that object; a title's inline category object in a bundled libc can still have a different identity. Its `message` and direct message export construct the existing [GuestLocale::String](../../../core/libs/prx/libc/include/GuestLocale.hpp) model: a reserved eight-byte word, 16-byte inline buffer, length and capacity, with guest `operator new` storage above 15 bytes. This 40-byte string layout and virtual return ABI have not been verified against a console library. The deleting destructor still throws. `_Syserror_map` covers the 72 distinct Dinkumware `errc` values present in FreeBSD 11.4 [errno.h](https://github.com/freebsd/freebsd-src/blob/releng/11.4/sys/sys/errno.h), returning null for other values; category messages use `unknown error` for those. First-match aliases are `EAGAIN`/`EWOULDBLOCK` -> `operation would block` and `ENOTSUP`/`EOPNOTSUPP` -> `not supported`. STREAMS-only errno assignments absent from that header (`ENODATA`, `ENOSR`, `ENOSTR`, `ETIME`) are not inferred - [Call tracing](../../../core/libs/prx/libc/src/specifics/windows/CallTrace.cpp) (`--trace-imports`, `APS5_TRACE_CALLS`) exists on Windows only and records the entry of a call, not its arguments or its result. One process-wide ring keeps the last 65536 calls, so a thread that calls often pushes the others out. Calls are also counted per traced target since start-up; an import bound to a target already traced under another name is counted under the first name. The thunk saves the integer argument registers and `xmm0` to `xmm7`; arguments in wider vector registers pass only as long as the recording code stays free of AVX instructions, which is not tested - [_Dtest](../../../core/libs/prx/libc/src/Math.cpp) (libc) follows Dinkumware double classification matching _FDtest: finite values return -1, subnormals -2, zeros 0, infinities 1, and NaNs 2. +- [app0.zar](../../../core/libs/prx/libc/src/AppArchive.cpp) (libc) - a [ZArchive](https://github.com/Exzap/ZArchive) beside `app0/` is a read-only layer under it: a path missing from `app0/` is looked up in the archive, case-insensitively. It is not a console format; titles ship their files in a PFS image instead. Resolving an archive-only path through any call other than the archive-aware libkernel ones throws, so `fopen`, `stat`, `access`, `opendir` and the rest do not read the archive yet, and `opendir` of a directory that exists in both places, `app0/` itself included, and `fdopen` of an archived descriptor throw instead of missing its entries or reading the null device. A damaged compressed block throws instead of returning `EIO` From 9b911ed510835146af72926be23be9753ea45a39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexej=20Szpev=C3=A1k?= Date: Wed, 14 Oct 2026 14:56:57 +0200 Subject: [PATCH 0543/1000] feat(libkernel): serve app0.zar files through the file calls sceKernelOpen and open give an archived file a read-only null-device descriptor. Reads, positioned, vectored and AIO reads, seeks, fstat and FIONREAD are served from the archive; dup, dup2 and F_DUPFD share the position. sceKernelStat and sceKernelCheckReachability see archived paths. Writing opens fail with EROFS, O_CREAT|O_EXCL with EEXIST and O_DIRECTORY on a file with ENOTDIR. Opening an archived directory or one merged with the disk throws until getdents can list it. --- core/libs/prx/libkernel/Aio/src/Aio.cpp | 6 + core/libs/prx/libkernel/CMakeLists.txt | 13 ++ .../libkernel/File/include/ArchivedFile.hpp | 27 +++ .../prx/libkernel/File/src/ArchivedFile.cpp | 117 ++++++++++ .../libkernel/File/src/DescriptorControl.cpp | 16 +- .../prx/libkernel/File/src/NativeStat.cpp | 2 + core/libs/prx/libkernel/File/src/Open.cpp | 20 +- core/libs/prx/libkernel/File/src/Stdio.cpp | 15 +- core/libs/tests/GuestKernelAppArchive.cpp | 218 ++++++++++++++++++ docs/dev/debt/libkernel.md | 1 + 10 files changed, 428 insertions(+), 7 deletions(-) create mode 100644 core/libs/prx/libkernel/File/include/ArchivedFile.hpp create mode 100644 core/libs/prx/libkernel/File/src/ArchivedFile.cpp create mode 100644 core/libs/tests/GuestKernelAppArchive.cpp diff --git a/core/libs/prx/libkernel/Aio/src/Aio.cpp b/core/libs/prx/libkernel/Aio/src/Aio.cpp index c16167088f..8f7367c074 100644 --- a/core/libs/prx/libkernel/Aio/src/Aio.cpp +++ b/core/libs/prx/libkernel/Aio/src/Aio.cpp @@ -10,6 +10,7 @@ #include "prx/libc/include/General.hpp" #include "prx/libc/include/GuestArena.hpp" #include "prx/libc/include/PreciseWait.hpp" +#include "prx/libkernel/File/include/ArchivedFile.hpp" #include "prx/libkernel/File/include/File.hpp" #include "prx/libkernel/File/include/SharedMemory.hpp" #include "prx/libkernel/KernelErrors.hpp" @@ -62,6 +63,11 @@ std::int64_t NativePread(std::int32_t fd, void* buf, std::size_t nbyte, std::int errno = EFAULT; return -1; } + if (const auto archived = File::ReadArchivedFile(fd, buf, nbyte, &offset)) { + if (*archived >= 0) return *archived; + errno = static_cast(-*archived); + return -1; + } #ifdef _WIN32 if (offset < 0) { errno = EINVAL; diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 4593cae303..a7f543196c 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -3,6 +3,7 @@ add_library(libkernel SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/Apr/src/Apr.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Equeue/Equeue.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Equeue/src/EventAccessors.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/File/src/ArchivedFile.cpp ${CMAKE_CURRENT_SOURCE_DIR}/File/src/DescriptorControl.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Equeue/src/FileEvent.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Equeue/src/Kqueue.cpp @@ -1169,6 +1170,18 @@ if(BUILD_TESTING) set_tests_properties(guest_socket_pair PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_kernel_app_archive_tests ${LIBS_INCLUDE_DIR}/tests/GuestKernelAppArchive.cpp) + target_include_directories(guest_kernel_app_archive_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_kernel_app_archive_tests PRIVATE libkernel libc zarchive) + configure_windows_unwind(guest_kernel_app_archive_tests) + foreach(mode archive plain) + add_test(NAME guest_kernel_app_archive_${mode} COMMAND guest_kernel_app_archive_tests ${mode}) + set_tests_properties(guest_kernel_app_archive_${mode} PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_kernel_app_archive_${mode} PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + endforeach() add_test_executable(guest_dup_tests ${LIBS_INCLUDE_DIR}/tests/GuestDup.cpp) target_include_directories(guest_dup_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_dup_tests PRIVATE libkernel libc) diff --git a/core/libs/prx/libkernel/File/include/ArchivedFile.hpp b/core/libs/prx/libkernel/File/include/ArchivedFile.hpp new file mode 100644 index 0000000000..e1640e5b25 --- /dev/null +++ b/core/libs/prx/libkernel/File/include/ArchivedFile.hpp @@ -0,0 +1,27 @@ +#ifndef CORE_LIBS_PRX_LIBKERNEL_FILE_ARCHIVEDFILE_HPP +#define CORE_LIBS_PRX_LIBKERNEL_FILE_ARCHIVEDFILE_HPP + +#include +#include +#include +#include + +#include "SceTypes.hpp" + +namespace File { + +std::optional OpenArchivedFile(const std::filesystem::path& native, int flags); +void RefuseMergedDirectory(const std::filesystem::path& native); +bool IsArchivedFile(int fd); +std::optional ReadArchivedFile(int fd, void* buf, std::size_t nbytes, const std::int64_t* offset); +std::optional ReadArchivedIovecs(int fd, const void* iov, int iovcnt, const std::int64_t* offset); +std::optional SeekArchivedFile(int fd, std::int64_t offset, int whence); +std::optional ArchivedFileRemaining(int fd); +bool FillArchivedStat(const std::filesystem::path& native, FileStat* sb); +bool FillArchivedStatFromDescriptor(int fd, FileStat* sb); +void ShareArchivedFile(int from, int to); +void ForgetArchivedFile(int fd); + +} + +#endif diff --git a/core/libs/prx/libkernel/File/src/ArchivedFile.cpp b/core/libs/prx/libkernel/File/src/ArchivedFile.cpp new file mode 100644 index 0000000000..c477133f3f --- /dev/null +++ b/core/libs/prx/libkernel/File/src/ArchivedFile.cpp @@ -0,0 +1,117 @@ +#include "prx/libkernel/File/include/ArchivedFile.hpp" +#include "prx/libkernel/File/include/FileFlags.hpp" +#include "prx/libkernel/File/include/NativeStat.hpp" +#include "prx/libc/include/AppArchive.hpp" +#include "prx/libc/include/General.hpp" + +#include +#include + +#ifdef _WIN32 +#include +#include +#else +#include +#include +#endif + +namespace { + +constexpr std::uint16_t GuestRegularMode = 0100444; +constexpr std::uint16_t GuestDirectoryMode = 0040555; + +int OpenPlaceholder() { +#ifdef _WIN32 + return ::_open("NUL", _O_RDONLY | _O_BINARY); +#else + return ::open("/dev/null", O_RDONLY | O_CLOEXEC); +#endif +} + +void FillNode(const AppArchiveNode& node, FileStat* sb) { + File::FillFileStat(AppArchivePath_nid_no_patch(), sb); + sb->st_mode = node.Directory ? GuestDirectoryMode : GuestRegularMode; + sb->st_ino = node.Handle + 1; + sb->st_nlink = node.Directory ? 2 : 1; + sb->st_size = static_cast(node.Size); + sb->st_blocks = static_cast((node.Size + 511) / 512); +} + +} + +namespace File { + +std::optional OpenArchivedFile(const std::filesystem::path& native, int flags) { + if (!AppArchiveOnly_nid_no_patch(native)) return std::nullopt; + AppArchiveNode node{}; + if (!AppArchiveLookup_nid_no_patch(native, &node)) throw std::logic_error("OpenArchivedFile: " + native.string() + " left app0.zar while opening"); + if ((flags & (SCE_KERNEL_O_CREAT | SCE_KERNEL_O_EXCL)) == (SCE_KERNEL_O_CREAT | SCE_KERNEL_O_EXCL)) return -EEXIST; + if ((flags & SCE_KERNEL_O_ACCMODE) != SCE_KERNEL_O_RDONLY || (flags & SCE_KERNEL_O_TRUNC) != 0) return -EROFS; + if (node.Directory) NotImplemented_nid_no_patch(("opening the app0.zar directory " + native.string()).c_str()); + if ((flags & SCE_KERNEL_O_DIRECTORY) != 0) return -ENOTDIR; + const int fd = OpenPlaceholder(); + if (fd < 0) return -errno; + AppArchiveAttach_nid_no_patch(fd, node); + return fd; +} + +void RefuseMergedDirectory(const std::filesystem::path& native) { + if (!AppArchiveHasDirectory_nid_no_patch(native)) return; + std::error_code error; + if (std::filesystem::is_directory(native, error)) NotImplemented_nid_no_patch(("opening " + native.string() + " merged with app0.zar").c_str()); +} + +bool IsArchivedFile(int fd) { + return AppArchiveDescriptor_nid_no_patch(fd, nullptr, nullptr); +} + +std::optional ReadArchivedFile(int fd, void* buf, std::size_t nbytes, const std::int64_t* offset) { + const AppArchiveBuffer buffer{buf, nbytes}; + return ReadArchivedIovecs(fd, &buffer, 1, offset); +} + +std::optional ReadArchivedIovecs(int fd, const void* iov, int iovcnt, const std::int64_t* offset) { + if (!IsArchivedFile(fd)) return std::nullopt; + if (offset != nullptr && *offset < 0) return -EINVAL; + std::uint64_t read = 0; + if (!AppArchiveDescriptorRead_nid_no_patch(fd, static_cast(iov), iovcnt, offset, &read)) return std::nullopt; + return static_cast(read); +} + +std::optional SeekArchivedFile(int fd, std::int64_t offset, int whence) { + std::int64_t position = 0; + if (!AppArchiveDescriptorSeek_nid_no_patch(fd, offset, whence, &position)) return std::nullopt; + return position < 0 ? -EINVAL : position; +} + +std::optional ArchivedFileRemaining(int fd) { + AppArchiveNode node{}; + std::uint64_t position = 0; + if (!AppArchiveDescriptor_nid_no_patch(fd, &node, &position)) return std::nullopt; + return position >= node.Size ? 0 : static_cast(node.Size - position); +} + +bool FillArchivedStat(const std::filesystem::path& native, FileStat* sb) { + if (!AppArchiveOnly_nid_no_patch(native)) return false; + AppArchiveNode node{}; + if (!AppArchiveLookup_nid_no_patch(native, &node)) throw std::logic_error("FillArchivedStat: " + native.string() + " left app0.zar while reading its status"); + FillNode(node, sb); + return true; +} + +bool FillArchivedStatFromDescriptor(int fd, FileStat* sb) { + AppArchiveNode node{}; + if (!AppArchiveDescriptor_nid_no_patch(fd, &node, nullptr)) return false; + FillNode(node, sb); + return true; +} + +void ShareArchivedFile(int from, int to) { + AppArchiveShare_nid_no_patch(from, to); +} + +void ForgetArchivedFile(int fd) { + AppArchiveDetach_nid_no_patch(fd); +} + +} diff --git a/core/libs/prx/libkernel/File/src/DescriptorControl.cpp b/core/libs/prx/libkernel/File/src/DescriptorControl.cpp index 3e7f3c0644..43972f05bb 100644 --- a/core/libs/prx/libkernel/File/src/DescriptorControl.cpp +++ b/core/libs/prx/libkernel/File/src/DescriptorControl.cpp @@ -2,10 +2,12 @@ #include "prx/libkernel/File/include/DirectoryDescriptor.hpp" #include "prx/libkernel/File/include/File.hpp" #include "prx/libkernel/File/include/RandomDevice.hpp" +#include "prx/libkernel/File/include/ArchivedFile.hpp" #include "prx/libc/include/GuestArena.hpp" #include "prx/libc/include/FileLock.hpp" #include "prx/libc/include/general/VabiMacros.hpp" +#include #include #include #include @@ -105,7 +107,7 @@ static_assert(sizeof(GuestLock) == 32); static_assert(offsetof(GuestLock, type) == 20); constexpr std::size_t OldGuestLockSize = 24; -enum class Kind { File, Directory, Pipe, Device, RandomDevice }; +enum class Kind { File, Directory, Pipe, Device, RandomDevice, ArchivedFile }; struct Entry { bool closeOnExec = false; @@ -127,6 +129,7 @@ const char* KindName(Kind kind) { case Kind::Pipe: return "pipe"; case Kind::Device: return "character device"; case Kind::RandomDevice: return "random device"; + case Kind::ArchivedFile: return "file read from app0.zar"; } return "descriptor"; } @@ -140,6 +143,7 @@ std::optional Classify(int fd, const char* function) { if (fd < 0 || !DescriptorIsOpen_nid_no_patch(fd)) return std::nullopt; if (File::DirectoryDescriptorPath(fd)) return Kind::Directory; if (File::IsRandomDevice(fd)) return Kind::RandomDevice; + if (File::IsArchivedFile(fd)) return Kind::ArchivedFile; switch (::GetFileType(WindowsDescriptorHandle_nid_no_patch(fd))) { case FILE_TYPE_DISK: return Kind::File; case FILE_TYPE_PIPE: return Kind::Pipe; @@ -259,6 +263,7 @@ std::optional Classify(int fd, const char* function) { throw std::runtime_error(std::string(function) + ": fstat failed, fd=" + std::to_string(fd) + ", errno=" + std::to_string(errno)); } if (File::IsRandomDevice(fd)) return Kind::RandomDevice; + if (File::IsArchivedFile(fd)) return Kind::ArchivedFile; if (S_ISREG(status.st_mode)) return Kind::File; if (S_ISDIR(status.st_mode)) return Kind::Directory; if (S_ISFIFO(status.st_mode)) return Kind::Pipe; @@ -409,7 +414,7 @@ int RecordLock(int fd, int command, std::intptr_t argument) { int ReadAheadControl(int fd, int command, std::intptr_t argument) { const auto kind = Classify(fd, "fcntl"); if (!kind || *kind == Kind::Pipe) return Fail(GuestEbadf); - if (*kind != Kind::File) return Fail(GuestEnotty); + if (*kind != Kind::File && *kind != Kind::ArchivedFile) return Fail(GuestEnotty); std::lock_guard lock(g_mutex); auto& status = *Materialize(fd).status; if (command == DarwinReadAhead || argument >= 0) status |= ReadAheadFlag; @@ -521,8 +526,9 @@ int Ioctl(int fd, std::uint64_t request, void* argument) { } if (command == ReadableRequest) { if (*kind == Kind::RandomDevice) return Fail(GuestEnotty); - if (*kind != Kind::File && *kind != Kind::Pipe) Unsupported("ioctl", "FIONREAD", *kind, fd); - const int available = Readable(fd, *kind, "ioctl"); + if (*kind != Kind::File && *kind != Kind::Pipe && *kind != Kind::ArchivedFile) Unsupported("ioctl", "FIONREAD", *kind, fd); + const auto archived = File::ArchivedFileRemaining(fd); + const int available = archived ? static_cast(std::min(*archived, std::numeric_limits::max())) : Readable(fd, *kind, "ioctl"); if (argument == nullptr) return Fail(GuestEfault); const GuestArena::HostWrite destination(argument, sizeof(available)); if (!destination.Open()) return Fail(GuestEfault); @@ -544,6 +550,7 @@ void RememberDescriptor(int fd, int openFlags) { } void ShareDescriptor(int from, int to) { + File::ShareArchivedFile(from, to); std::lock_guard lock(g_mutex); auto status = Materialize(from).status; g_entries[to] = Entry{false, std::move(status)}; @@ -554,6 +561,7 @@ void ShareDescriptor(int from, int to) { } void ForgetDescriptor(int fd) { + File::ForgetArchivedFile(fd); std::lock_guard lock(g_mutex); g_entries.erase(fd); } diff --git a/core/libs/prx/libkernel/File/src/NativeStat.cpp b/core/libs/prx/libkernel/File/src/NativeStat.cpp index 9d097eec22..b110e980df 100644 --- a/core/libs/prx/libkernel/File/src/NativeStat.cpp +++ b/core/libs/prx/libkernel/File/src/NativeStat.cpp @@ -1,5 +1,6 @@ #include "prx/libkernel/File/include/NativeStat.hpp" #include "prx/libkernel/File/include/DirectoryDescriptor.hpp" +#include "prx/libkernel/File/include/ArchivedFile.hpp" #include "prx/libc/include/General.hpp" #include @@ -156,6 +157,7 @@ void FillFileStat(int nativeDescriptor, FileStat* sb) { } bool FillFileStatFromDescriptor(int fd, FileStat* sb) { + if (FillArchivedStatFromDescriptor(fd, sb)) return true; NativeStat st{}; if (DoFstat(fd, &st) != 0) return false; CopyNativeStat(st, sb); diff --git a/core/libs/prx/libkernel/File/src/Open.cpp b/core/libs/prx/libkernel/File/src/Open.cpp index 9760bbe879..ddc708f82e 100644 --- a/core/libs/prx/libkernel/File/src/Open.cpp +++ b/core/libs/prx/libkernel/File/src/Open.cpp @@ -12,6 +12,7 @@ #include "prx/libkernel/Socket/include/SocketRuntime.hpp" #include "prx/libkernel/File/include/RandomDevice.hpp" #include "prx/libkernel/File/include/HostStreams.hpp" +#include "prx/libkernel/File/include/ArchivedFile.hpp" #include "prx/libkernel/KernelErrors.hpp" #include "prx/libkernel/Equeue/Equeue.hpp" #include "SceTypes.hpp" @@ -174,6 +175,12 @@ static int SceIoErrorFromErrno(int error) { } static int OpenNative(const std::filesystem::path& native, int flags, std::uint16_t mode) { + if (const auto archived = File::OpenArchivedFile(native, flags)) { + if (*archived < 0) return SceErrorFromErrno(-*archived); + File::RememberDescriptor(*archived, flags); + return *archived; + } + File::RefuseMergedDirectory(native); #ifdef _WIN32 if ((flags & (SCE_KERNEL_O_DIRECTORY | SCE_KERNEL_O_CREAT)) == SCE_KERNEL_O_DIRECTORY) { std::error_code error; @@ -222,7 +229,7 @@ int APS5_VABI sceKernelOpen(const char* path, int flags, std::uint16_t mode) { } std::filesystem::path native; const bool module = (flags & (SCE_KERNEL_O_ACCMODE | SCE_KERNEL_O_CREAT)) == SCE_KERNEL_O_RDONLY; - if (const int error = ResolvePathError_nid_no_patch(path, native, module)) return SceKernelError(error); + if (const int error = ResolvePathError_nid_no_patch(path, native, module, true)) return SceKernelError(error); return OpenNative(native, flags, mode); } @@ -262,6 +269,7 @@ std::int64_t APS5_VABI sceKernelRead(int d, void* buf, std::size_t nbytes) { const GuestArena::HostWrite destination(buf, nbytes); if (!destination.Open()) errno = EFAULT; else if (File::ReadRandomDevice(d, buf, nbytes)) return static_cast(nbytes); + else if (const auto archived = File::ReadArchivedFile(d, buf, nbytes, nullptr)) return *archived; auto n = destination.Open() ? NativeRead(d, buf, nbytes) : -1; if (n < 0) return SceIoErrorFromErrno(errno); return static_cast(n); @@ -285,6 +293,7 @@ std::int64_t APS5_VABI sceKernelLseek(int d, std::int64_t offset, int whence) { if (whence < 0 || whence > 2) { throw std::invalid_argument(std::string(__func__) + ": invalid whence=" + std::to_string(whence)); } + if (const auto archived = File::SeekArchivedFile(d, offset, whence)) return *archived < 0 ? SceIoErrorFromErrno(-*archived) : *archived; #ifdef _WIN32 if (const auto directory = File::SeekDirectoryDescriptor(d, offset, whence)) { if (*directory < 0) return SceIoErrorFromErrno(errno); @@ -302,10 +311,17 @@ int APS5_VABI sceKernelStat(const char* path, FileStat* sb) { } if (*path == '\0') return SceErrorFromErrno(2); std::filesystem::path native; - if (const int error = ResolvePathError_nid_no_patch(path, native, true)) return SceKernelError(error); + if (const int error = ResolvePathError_nid_no_patch(path, native, true, true)) return SceKernelError(error); std::error_code error; constexpr int GuestEnotdir = 20; constexpr int GuestEfault = 14; + FileStat archived{}; + if (File::FillArchivedStat(native, &archived)) { + if (std::string_view(path).back() == '/' && (archived.st_mode & 0170000) != 0040000) return SceErrorFromErrno(GuestEnotdir); + if (sb == nullptr) return SceErrorFromErrno(GuestEfault); + *sb = archived; + return 0; + } const auto status = std::filesystem::status(native, error); if (error == std::errc::not_a_directory) return SceErrorFromErrno(GuestEnotdir); if (!std::filesystem::exists(status)) { diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index 5ded00a7ba..a30955ac57 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -17,6 +17,8 @@ #include "prx/libkernel/File/include/FileLock.hpp" #include "prx/libkernel/File/include/RandomDevice.hpp" #include "prx/libkernel/File/include/HostStreams.hpp" +#include "prx/libkernel/File/include/ArchivedFile.hpp" +#include "prx/libc/include/AppArchive.hpp" #include "prx/libkernel/KernelErrors.hpp" #include "prx/libkernel/Equeue/Equeue.hpp" #include "prx/libkernel/Socket/include/SocketRuntime.hpp" @@ -41,6 +43,7 @@ struct KernelIovec { std::size_t length; }; static_assert(sizeof(KernelIovec) == sizeof(GuestSockets::Iovec)); +static_assert(sizeof(KernelIovec) == sizeof(AppArchiveBuffer) && offsetof(KernelIovec, length) == offsetof(AppArchiveBuffer, Length)); static constexpr int KERNEL_IOV_MAX = 1024; @@ -650,6 +653,7 @@ int APS5_VABI dup2_nid_postfix(int from, int to) { if (GuestSockets::DuplicateTo(from, to) < 0) return -1; if (from == to) return to; File::ForgetRandomDevice(to); + File::ForgetArchivedFile(to); #ifdef _WIN32 File::ForgetDirectoryDescriptor(to); File::ForgetFileLock(to); @@ -811,6 +815,7 @@ int64_t APS5_VABI pread_nid_postfix(int d, void* buf, size_t nbytes, int64_t off const GuestArena::HostWrite destination(buf, nbytes); if (!destination.Open()) return PosixFailure(GUEST_EFAULT); if (File::ReadRandomDevice(d, buf, nbytes)) return static_cast(nbytes); + if (const auto archived = File::ReadArchivedFile(d, buf, nbytes, &offset)) return *archived; char empty = 0; const auto result = NativePread(d, buf == nullptr ? &empty : buf, nbytes, offset); return result < 0 ? PosixFailure(SceErrorFromErrno(errno) & 0xffff) : result; @@ -881,7 +886,8 @@ int APS5_VABI sceKernelCheckReachability(const char* path) { if (*path == '\0') return SCE_KERNEL_ERROR_ENOENT; if (std::strlen(path) > 255) return SCE_KERNEL_ERROR_ENAMETOOLONG; std::filesystem::path native; - if (const int error = ResolvePathError_nid_no_patch(path, native, true)) return SceKernelError(error); + if (const int error = ResolvePathError_nid_no_patch(path, native, true, true)) return SceKernelError(error); + if (AppArchiveOnly_nid_no_patch(native)) return 0; std::error_code error; return std::filesystem::exists(native, error) ? 0 : SCE_KERNEL_ERROR_ENOENT; } @@ -1044,6 +1050,7 @@ int64_t APS5_VABI sceKernelPread(int d, void* buf, size_t nbytes, int64_t offset const GuestArena::HostWrite destination(buf, nbytes); if (!destination.Open()) return SceErrorFromErrno(GUEST_EFAULT); if (File::ReadRandomDevice(d, buf, nbytes)) return static_cast(nbytes); + if (const auto archived = File::ReadArchivedFile(d, buf, nbytes, &offset)) return *archived; char empty = 0; const auto result = NativePread(d, nbytes == 0 && buf == nullptr ? &empty : buf, nbytes, offset); return result < 0 ? SceErrorFromErrno(errno) : result; @@ -1079,6 +1086,9 @@ static std::int64_t TransferIovecs(int d, const KernelIovec* iov, int iovcnt, co return result < 0 ? SceKernelError(*__error_nid_postfix()) : result; } if (!write && File::IsRandomDevice(d)) return ReadRandomIovecs(d, iov, iovcnt); + if (!write) { + if (const auto archived = File::ReadArchivedIovecs(d, iov, iovcnt, offset)) return *archived; + } const auto positioned = File::LockPositionedTransfer(d); if (total == 0) { char none = 0; @@ -1127,6 +1137,7 @@ int64_t APS5_VABI sceKernelPread(int d, void* buf, size_t nbytes, int64_t offset const GuestArena::HostWrite destination(buf, nbytes); if (!destination.Open()) return SceErrorFromErrno(GUEST_EFAULT); if (File::ReadRandomDevice(d, buf, nbytes)) return static_cast(nbytes); + if (const auto archived = File::ReadArchivedFile(d, buf, nbytes, &offset)) return *archived; const auto result = NativePread(d, buf, nbytes, offset); return result < 0 ? SceErrorFromErrno(errno) : result; } @@ -1148,6 +1159,7 @@ int64_t APS5_VABI sceKernelReadv(int d, const KernelIovec* iov, int iovcnt) { return result < 0 ? SceKernelError(*__error_nid_postfix()) : result; } if (File::IsRandomDevice(d)) return ReadRandomIovecs(d, iov, iovcnt); + if (const auto archived = File::ReadArchivedIovecs(d, iov, iovcnt, nullptr)) return *archived; const auto result = static_cast(::readv(d, NativeIovecs(iov), iovcnt)); return result < 0 ? SceErrorFromErrno(errno) : result; } @@ -1171,6 +1183,7 @@ int64_t APS5_VABI sceKernelPreadv(int d, const KernelIovec* iov, int iovcnt, int std::deque destinations; if (!OpenIovecs(iov, iovcnt, destinations)) return SceErrorFromErrno(GUEST_EFAULT); if (File::IsRandomDevice(d)) return ReadRandomIovecs(d, iov, iovcnt); + if (const auto archived = File::ReadArchivedIovecs(d, iov, iovcnt, &offset)) return *archived; const auto result = static_cast(::preadv(d, NativeIovecs(iov), iovcnt, static_cast(offset))); return result < 0 ? SceErrorFromErrno(errno) : result; } diff --git a/core/libs/tests/GuestKernelAppArchive.cpp b/core/libs/tests/GuestKernelAppArchive.cpp new file mode 100644 index 0000000000..12679744ee --- /dev/null +++ b/core/libs/tests/GuestKernelAppArchive.cpp @@ -0,0 +1,218 @@ +#include "AppArchiveFixture.hpp" +#include "SceTypes.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libkernel/File/include/FileFlags.hpp" +#include "prx/libkernel/KernelErrors.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +struct KernelIovec { + void* base; + std::size_t length; +}; + +extern "C" { +int APS5_VABI sceKernelOpen(const char* path, int flags, std::uint16_t mode); +int APS5_VABI sceKernelClose(int d); +std::int64_t APS5_VABI sceKernelRead(int d, void* buf, std::size_t nbytes); +std::int64_t APS5_VABI sceKernelWrite(int d, const void* buf, std::size_t nbytes); +std::int64_t APS5_VABI sceKernelPread(int d, void* buf, std::size_t nbytes, std::int64_t offset); +std::int64_t APS5_VABI sceKernelReadv(int d, const KernelIovec* iov, int iovcnt); +std::int64_t APS5_VABI sceKernelPreadv(int d, const KernelIovec* iov, int iovcnt, std::int64_t offset); +std::int64_t APS5_VABI sceKernelLseek(int d, std::int64_t offset, int whence); +int APS5_VABI sceKernelStat(const char* path, FileStat* sb); +int APS5_VABI sceKernelFstat(int d, FileStat* sb); +int APS5_VABI sceKernelCheckReachability(const char* path); +int APS5_VABI dup_nid_postfix(int d); +int APS5_VABI dup2_nid_postfix(int from, int to); +int APS5_VABI close_nid_postfix(int d); +std::int64_t APS5_VABI pread_nid_postfix(int d, void* buf, std::size_t nbytes, std::int64_t offset); +std::int64_t APS5_VABI lseek_nid_postfix(int d, std::int64_t offset, int whence); +int APS5_VABI fcntl_nid_postfix(int d, int command, ...); +int APS5_VABI ioctl_nid_postfix(int d, std::uint64_t request, void* argument); +void* APS5_VABI opendir_nid_postfix(const char* path); +void* APS5_VABI fdopen_nid_postfix(int d, const char* mode); +int APS5_VABI sceKernelAioSubmitReadCommands(KernelAioRwRequest* req, std::int32_t size, std::int32_t prio, std::int32_t* id); +int APS5_VABI sceKernelAioWaitRequest(std::int32_t id, std::int32_t* state, std::uint32_t* usec); +int APS5_VABI sceKernelAioDeleteRequest(std::int32_t id, std::int32_t* ret); +} + +namespace { + +void Require(bool value, int line) { + if (!value) { + std::fprintf(stderr, "app0.zar kernel check failed at line %d\n", line); + std::exit(1); + } +} +#define Check(value) Require((value), __LINE__) + +template bool NotImplemented(TCall call) { + try { + call(); + } catch (const std::runtime_error& error) { + return std::strstr(error.what(), "not implemented") != nullptr; + } + return false; +} + +constexpr std::size_t BigSize = 3 * 65536 + 77; +constexpr std::uint16_t TypeMask = 0170000; +constexpr std::uint16_t RegularType = 0100000; +constexpr std::uint16_t DirectoryType = 0040000; +constexpr int DuplicateFrom = 0; +constexpr std::uint64_t ReadableRequest = 0x4004667f; + +void Archive(const std::filesystem::path& root) { + AppArchiveFixture::WriteArchive(root / "app0.zar", { + {"Data/Big.bin", AppArchiveFixture::Pattern(BigSize)}, + {"Data/Small.txt", {'z', 'a', 'r'}}, + {"sce_sys/Packed.json", {'{', '}'}}, + {"eboot.bin", {'o', 'l', 'd'}}, + }); + std::ofstream eboot(root / "app0" / "eboot.bin", std::ios::binary); + eboot << "disk"; +} + +void Reads() { + const auto expected = AppArchiveFixture::Pattern(BigSize); + const int fd = sceKernelOpen("/app0/data/big.BIN", SCE_KERNEL_O_RDONLY, 0); + Check(fd >= 0); + std::vector buffer(BigSize + 16); + Check(sceKernelRead(fd, buffer.data(), 70000) == 70000); + Check(std::equal(buffer.begin(), buffer.begin() + 70000, expected.begin())); + Check(sceKernelLseek(fd, 0, 1) == 70000); + Check(lseek_nid_postfix(fd, 0, 1) == 70000); + + std::vector left(10), right(65536); + const KernelIovec vector[] = {{left.data(), left.size()}, {right.data(), right.size()}}; + Check(sceKernelReadv(fd, vector, 2) == 65546); + Check(std::equal(left.begin(), left.end(), expected.begin() + 70000) && std::equal(right.begin(), right.end(), expected.begin() + 70010)); + Check(sceKernelPreadv(fd, vector, 2, 5) == 65546); + Check(std::equal(left.begin(), left.end(), expected.begin() + 5) && std::equal(right.begin(), right.end(), expected.begin() + 15)); + Check(sceKernelLseek(fd, 0, 1) == 135546); + + Check(sceKernelPread(fd, buffer.data(), 100, 65500) == 100 && std::equal(buffer.begin(), buffer.begin() + 100, expected.begin() + 65500)); + Check(pread_nid_postfix(fd, buffer.data(), 100, BigSize - 40) == 40 && std::equal(buffer.begin(), buffer.begin() + 40, expected.end() - 40)); + Check(sceKernelPread(fd, buffer.data(), 1, -1) == SCE_KERNEL_ERROR_EINVAL); + Check(sceKernelLseek(fd, 0, 1) == 135546); + + int available = 0; + Check(ioctl_nid_postfix(fd, ReadableRequest, &available) == 0 && available == static_cast(BigSize - 135546)); + + Check(sceKernelLseek(fd, -7, 2) == static_cast(BigSize - 7)); + Check(sceKernelRead(fd, buffer.data(), 100) == 7 && std::equal(buffer.begin(), buffer.begin() + 7, expected.end() - 7)); + Check(sceKernelRead(fd, buffer.data(), 100) == 0); + Check(sceKernelLseek(fd, -1, 0) == SCE_KERNEL_ERROR_EINVAL); + Check(sceKernelLseek(fd, 0, 1) == static_cast(BigSize)); + Check(sceKernelWrite(fd, "x", 1) == SCE_KERNEL_ERROR_EBADF); + + FileStat status{}; + Check(sceKernelFstat(fd, &status) == 0); + Check((status.st_mode & TypeMask) == RegularType && status.st_size == static_cast(BigSize) && status.st_nlink == 1); + + KernelAioResult result{-1, 0}; + KernelAioRwRequest request{65530, 20, buffer.data(), &result, fd}; + std::int32_t id = 0; + std::int32_t state = 0; + Check(sceKernelAioSubmitReadCommands(&request, 1, 0, &id) == 0); + Check(sceKernelAioWaitRequest(id, &state, nullptr) == 0); + Check(result.return_value == 20 && std::equal(buffer.begin(), buffer.begin() + 20, expected.begin() + 65530)); + std::int32_t removed = 0; + Check(sceKernelAioDeleteRequest(id, &removed) == 0); + + Check(sceKernelLseek(fd, 1000, 0) == 1000); + const int duplicate = dup_nid_postfix(fd); + Check(duplicate >= 0 && sceKernelLseek(duplicate, 0, 1) == 1000); + Check(sceKernelRead(duplicate, buffer.data(), 24) == 24 && sceKernelLseek(fd, 0, 1) == 1024); + const int above = fcntl_nid_postfix(fd, DuplicateFrom, 200); + Check(above >= 200 && sceKernelLseek(above, 0, 1) == 1024); + Check(sceKernelClose(fd) == 0); + Check(sceKernelRead(duplicate, buffer.data(), 4) == 4 && std::equal(buffer.begin(), buffer.begin() + 4, expected.begin() + 1024)); + Check(sceKernelLseek(above, 0, 1) == 1028); + Check(close_nid_postfix(above) == 0); + + const int plain = sceKernelOpen("/app0/eboot.bin", SCE_KERNEL_O_RDONLY, 0); + Check(plain >= 0); + Check(dup2_nid_postfix(plain, duplicate) == duplicate); + Check(sceKernelPread(duplicate, buffer.data(), 8, 0) == 4 && std::memcmp(buffer.data(), "disk", 4) == 0); + Check(sceKernelClose(plain) == 0); + Check(sceKernelClose(duplicate) == 0); +} + +void Status() { + FileStat status{}; + Check(sceKernelStat("/app0/Data/Small.txt", &status) == 0); + Check((status.st_mode & TypeMask) == RegularType && (status.st_mode & 0222) == 0 && status.st_size == 3 && status.st_blocks == 1); + Check(sceKernelStat("/app0/data", &status) == 0 && (status.st_mode & TypeMask) == DirectoryType); + Check(sceKernelStat("/app0/Data/", &status) == 0); + Check(sceKernelStat("/app0/Data/Small.txt/", &status) == SCE_KERNEL_ERROR_ENOTDIR); + Check(sceKernelStat("/app0/Data/Missing.txt", &status) == SCE_KERNEL_ERROR_ENOENT); + Check(sceKernelStat("/app0/eboot.bin", &status) == 0 && status.st_size == 4); + Check(sceKernelCheckReachability("/app0/Data/Small.txt") == 0); + Check(sceKernelCheckReachability("/app0/Data/Missing.txt") == SCE_KERNEL_ERROR_ENOENT); +} + +void Refusals() { + Check(sceKernelOpen("/app0/Data/Small.txt", SCE_KERNEL_O_RDWR, 0) == SCE_KERNEL_ERROR_EROFS); + Check(sceKernelOpen("/app0/Data/Small.txt", SCE_KERNEL_O_RDONLY | SCE_KERNEL_O_TRUNC, 0) == SCE_KERNEL_ERROR_EROFS); + Check(sceKernelOpen("/app0/Data/Small.txt", SCE_KERNEL_O_WRONLY | SCE_KERNEL_O_CREAT | SCE_KERNEL_O_EXCL, 0666) == SCE_KERNEL_ERROR_EEXIST); + Check(sceKernelOpen("/app0/Data/Small.txt", SCE_KERNEL_O_RDONLY | SCE_KERNEL_O_DIRECTORY, 0) == SCE_KERNEL_ERROR_ENOTDIR); + Check(sceKernelOpen("/app0/Data/Missing.txt", SCE_KERNEL_O_RDONLY, 0) == SCE_KERNEL_ERROR_ENOENT); + Check(NotImplemented([] { (void)sceKernelOpen("/app0/Data", SCE_KERNEL_O_RDONLY | SCE_KERNEL_O_DIRECTORY, 0); })); + Check(NotImplemented([] { (void)sceKernelOpen("/app0/sce_sys", SCE_KERNEL_O_RDONLY | SCE_KERNEL_O_DIRECTORY, 0); })); + Check(NotImplemented([] { (void)opendir_nid_postfix("/app0/sce_sys"); })); + Check(NotImplemented([] { (void)opendir_nid_postfix("/app0/Data"); })); + const int fd = sceKernelOpen("/app0/Data/Small.txt", SCE_KERNEL_O_RDONLY, 0); + Check(fd >= 0); + Check(NotImplemented([fd] { (void)fdopen_nid_postfix(fd, "r"); })); + Check(sceKernelClose(fd) == 0); +} + +void WithoutArchive() { + { std::ofstream file(std::filesystem::current_path() / "app0" / "Plain.txt", std::ios::binary); file << "plain"; } + const int fd = sceKernelOpen("/app0/Plain.txt", SCE_KERNEL_O_RDONLY, 0); + Check(fd >= 0); + char buffer[8] = {}; + Check(sceKernelRead(fd, buffer, sizeof(buffer)) == 5 && std::memcmp(buffer, "plain", 5) == 0); + Check(sceKernelClose(fd) == 0); + Check(sceKernelOpen("/app0/Data/Small.txt", SCE_KERNEL_O_RDONLY, 0) == SCE_KERNEL_ERROR_ENOENT); + FileStat status{}; + Check(sceKernelStat("/app0/Data/Small.txt", &status) == SCE_KERNEL_ERROR_ENOENT); + const int directory = sceKernelOpen("/app0/sce_sys", SCE_KERNEL_O_RDONLY | SCE_KERNEL_O_DIRECTORY, 0); + Check(directory >= 0 && sceKernelClose(directory) == 0); +} + +} + +int main(int argc, char** argv) { + if (argc != 2) { + std::fprintf(stderr, "usage: guest_kernel_app_archive_tests archive|plain\n"); + return 2; + } + const std::string mode = argv[1]; + const auto root = AppArchiveFixture::EnterFreshRoot("anyps5-kernel-app-archive"); + if (mode == "archive") { + Archive(root); + Reads(); + Status(); + Refusals(); + } else if (mode == "plain") { + WithoutArchive(); + } else { + return 2; + } + std::error_code error; + std::filesystem::current_path(std::filesystem::temp_directory_path(), error); + std::filesystem::remove_all(root, error); + return 0; +} diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 5396a00c3a..a78dcef716 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -200,3 +200,4 @@ - [getpriority](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) follows FreeBSD 11.4's [`sys_getpriority`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_resource.c#L93-L165) for the only guest process, which is taken to run at nice 0: `PRIO_PROCESS` and `PRIO_PGRP` with 0 or its own pid (the guest is taken to lead its own process group, see `kill`) and `PRIO_USER` with 0 or its user id 0 return 0, any other id fails with `ESRCH` and any other `which` with `EINVAL`. As in FreeBSD's libc, where `getpriority` is the bare system call, errno is left unchanged on success. The nice value of a title process on the console is unverified, and the host nice of the process or of the threads `APS5_THREAD_PRIORITY=high` raises is not reported. `setpriority` accepts the same target ids (any other id fails with `ESRCH`, any other `which` with `EINVAL`), clamps the value to FreeBSD's `PRIO_MIN`-`PRIO_MAX` (-20 to 20) and stores it as the guest process's nice value, which `getpriority` returns; the host scheduling class is not changed, and whether a title process may lower the value without privilege is unverified - [rtprio_thread, pthread_setconcurrency, pthread_getconcurrency](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - `rtprio_thread` follows FreeBSD 11.4's `rtprio_thread(2)`. Every guest thread here runs under `SCHED_FIFO` or `SCHED_RR` (priority 256-767), whose rtprio class is real-time; the console's mapping from that priority to the rtprio `prio` range (0-31) is unmeasured, so `RTP_LOOKUP` and `RTP_SET` throw for such a thread rather than report an impossible priority. A thread that is not real-time reports `RTP_PRIO_NORMAL` (3) with 0, and `RTP_SET` accepts only that value. Another function fails with `EINVAL`, a null `rtprio` with `EFAULT`, a thread other than the calling one throws, and an id that is not a live guest thread with `ESRCH`. `pthread_setconcurrency` stores 0 or more and returns `EINVAL` for a negative level (the error itself, as FreeBSD's libthr does, with errno untouched); `pthread_getconcurrency` returns the level, which is a hint and changes nothing about the host's concurrency. Morrowind (`mshivam019/morrowind-ps5`) imports all three at start-up - [sceKernelVirtualQuery](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) returns [VirtualQueryInfo](../../../core/libs/SceTypes.hpp) with a one-byte flag allocation unit so Windows and SysV hosts return the same 72-byte representation, with the name at byte 33 and GPU mask at byte 65. This preserves the existing SysV representation; the console offsets beyond the address, protection and size fields observed in Astro Bot's AGC calls are not hardware-measured. Size and offset assertions plus a raw-byte named-mapping regression guard the host ABI. +- [app0.zar files](../../../core/libs/prx/libkernel/File/src/ArchivedFile.cpp) (libkernel) - `sceKernelOpen`, `open`, `sceKernelStat` and `sceKernelCheckReachability` resolve a path missing from `app0/` in `app0.zar`. An archived file is a read-only host null device whose reads, positioned and vectored reads, AIO reads, seeks, `fstat` and `FIONREAD` are served from the archive, with the position shared by `dup`, `dup2` and `F_DUPFD`; writes fail with `EBADF` as on a read-only descriptor, opening one for writing or truncation fails with `EROFS` and `O_CREAT | O_EXCL` with `EEXIST`. `fstat` and `sceKernelStat` report mode `0444` (`0555` for a directory), the node index as inode and the archive file's device and times. Opening an archived directory or a directory present both on disk and in the archive throws, as `getdents` would miss entries; `mmap`, record locks, `lstat` and the other path calls on archived files are not implemented From 819d5c4a6d3d3bf7cd65385c43a6bd6137c27e0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexej=20Szpev=C3=A1k?= Date: Wed, 14 Oct 2026 14:56:57 +0200 Subject: [PATCH 0544/1000] docs(usage): describe app0.zar --- docs/user/USAGE.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index 72772bf384..73c49d52c6 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -107,6 +107,8 @@ Use `sce_modules/` or `prx/` instead of `sce_module/` if that is the input direc Some applications also need their original `app0/sce_sys/param.json` at runtime. AnyPS5 reads this file for the title ID and display name; it must contain a non-empty `titleId` string and a non-empty `localizedParameters` object with a language entry that has a non-empty `titleName`. It selects the entry named by `defaultLanguage` (inside `localizedParameters`, or at the top level); if that entry is absent, it tries `en-US`, then the first object-valued language entry. Preserve the values from the title's own metadata and place the file under the same `app0/` directory as the other app resources. At startup, its presence in the working directory creates `data/`; a non-zero `downloadDataSize` also creates `download0/`. Without the file, neither directory is created. If it is missing or invalid, metadata calls such as title/window setup can fail with a `param.json` error. +Resources can also be packed into `app0.zar` beside `app0/`, a [ZArchive](https://github.com/Exzap/ZArchive) whose root is the `app0/` directory. A file present in `app0/` is read from there and any other file from the archive; the executable, `sce_module/` and `sce_sys/` stay in `app0/`. The archive is read only through `sceKernelOpen`/`open`, the read, seek and stat calls on its descriptors and `sceKernelStat`; other calls on an archived path, listing a directory that the archive also contains and opening a file in it for writing fail as described in [technical debt](../dev/debt/libkernel.md). An archive that cannot be opened stops the title at its first file access. + Use the generated files printed as `Guest module:` for bundled title modules. `libs/` is for AnyPS5 system libraries, not the original PS5 `.prx` files. Placing an original PS5 module in `libs/` on Windows makes Windows try to load it as a DLL and can fail with error 193 (not a valid Win32 application). On Windows, a self-built `libs/` also needs `libgcc_s_seh-1.dll`, `libstdc++-6.dll` and `libwinpthread-1.dll` from the `mingw64/bin` directory of the toolchain that built the libraries; the release archives already contain them. The libraries are loaded without searching `PATH`, so a copy elsewhere on the system is not used, and a library that needs one of them fails with error 126 (the specified module could not be found) although its `.prx` file is present. From d97d6efb1bc8b14b6d4fef05808eba8f1b86e0a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexej=20Szpev=C3=A1k?= Date: Wed, 14 Oct 2026 16:03:54 +0200 Subject: [PATCH 0545/1000] fix(libkernel): refuse descriptor calls on app0.zar files flock, fstatfs, ftruncate, fsync, fdatasync, getdirentries, fchmod, futimes and mmap on an archived descriptor acted on its null device and succeeded or failed with an unrelated errno. They now throw NotImplemented, each covered by the kernel archive test; mmap is noexcept, so its refusal is checked through the terminate handler. --- core/libs/prx/libkernel/CMakeLists.txt | 2 +- .../libkernel/DirectMemory/PosixMemory.cpp | 2 + .../libkernel/File/include/ArchivedFile.hpp | 1 + .../prx/libkernel/File/src/ArchivedFile.cpp | 4 ++ core/libs/prx/libkernel/File/src/Stdio.cpp | 9 +++++ core/libs/tests/GuestKernelAppArchive.cpp | 39 +++++++++++++++++++ docs/dev/debt/libkernel.md | 2 +- 7 files changed, 57 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index a7f543196c..d4279391a8 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -1174,7 +1174,7 @@ if(BUILD_TESTING) target_include_directories(guest_kernel_app_archive_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_kernel_app_archive_tests PRIVATE libkernel libc zarchive) configure_windows_unwind(guest_kernel_app_archive_tests) - foreach(mode archive plain) + foreach(mode archive plain mmap) add_test(NAME guest_kernel_app_archive_${mode} COMMAND guest_kernel_app_archive_tests ${mode}) set_tests_properties(guest_kernel_app_archive_${mode} PROPERTIES TIMEOUT 20) if(WIN32) diff --git a/core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp b/core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp index 6f2c99760c..250d252b47 100644 --- a/core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp +++ b/core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp @@ -3,6 +3,7 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include "prx/libc/include/General.hpp" #include "prx/libc/include/GuestArena.hpp" +#include "prx/libkernel/File/include/ArchivedFile.hpp" #include "prx/libkernel/File/include/SharedMemory.hpp" #include #include @@ -134,6 +135,7 @@ void* APS5_VABI mmap_nid_postfix(void* address, std::size_t length, int protecti (mappingFlags & ~(GuestShared | GuestPrivate | GuestMapFixed | GuestMapNoOverwrite | GuestMapNoCoalesce)) != 0) return failed(GuestNotSupported); if (descriptor < 0) return failed(GuestBadDescriptor); + File::RefuseArchivedFile(descriptor, "mmap"); if (offset < 0 || (static_cast(offset) & (PS5_PAGE_SIZE - 1)) != 0 || rounded > static_cast(std::numeric_limits::max()) || static_cast(offset) > static_cast(std::numeric_limits::max()) - rounded) diff --git a/core/libs/prx/libkernel/File/include/ArchivedFile.hpp b/core/libs/prx/libkernel/File/include/ArchivedFile.hpp index e1640e5b25..73f84c1cca 100644 --- a/core/libs/prx/libkernel/File/include/ArchivedFile.hpp +++ b/core/libs/prx/libkernel/File/include/ArchivedFile.hpp @@ -13,6 +13,7 @@ namespace File { std::optional OpenArchivedFile(const std::filesystem::path& native, int flags); void RefuseMergedDirectory(const std::filesystem::path& native); bool IsArchivedFile(int fd); +void RefuseArchivedFile(int fd, const char* operation); std::optional ReadArchivedFile(int fd, void* buf, std::size_t nbytes, const std::int64_t* offset); std::optional ReadArchivedIovecs(int fd, const void* iov, int iovcnt, const std::int64_t* offset); std::optional SeekArchivedFile(int fd, std::int64_t offset, int whence); diff --git a/core/libs/prx/libkernel/File/src/ArchivedFile.cpp b/core/libs/prx/libkernel/File/src/ArchivedFile.cpp index c477133f3f..df3a8548b4 100644 --- a/core/libs/prx/libkernel/File/src/ArchivedFile.cpp +++ b/core/libs/prx/libkernel/File/src/ArchivedFile.cpp @@ -65,6 +65,10 @@ bool IsArchivedFile(int fd) { return AppArchiveDescriptor_nid_no_patch(fd, nullptr, nullptr); } +void RefuseArchivedFile(int fd, const char* operation) { + if (IsArchivedFile(fd)) NotImplemented_nid_no_patch((std::string(operation) + " on a file read from app0.zar, fd=" + std::to_string(fd)).c_str()); +} + std::optional ReadArchivedFile(int fd, void* buf, std::size_t nbytes, const std::int64_t* offset) { const AppArchiveBuffer buffer{buf, nbytes}; return ReadArchivedIovecs(fd, &buffer, 1, offset); diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index a30955ac57..9232996b34 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -700,6 +700,7 @@ int APS5_VABI dup2_nid_postfix(int from, int to) { int APS5_VABI flock_nid_postfix(int d, int operation) { if (GuestSockets::IsOpen(d)) return PosixFailure(GUEST_EOPNOTSUPP); + File::RefuseArchivedFile(d, "flock"); const int type = operation & 8 ? 8 : operation & 2 ? 2 : operation & 1 ? 1 : 0; if (type == 0) return PosixFailure(GUEST_EBADF); if (NativeFlock(d, type | (operation & 4)) != 0) { @@ -719,6 +720,7 @@ int APS5_VABI flock_nid_postfix(int d, int operation) { int APS5_VABI _fstatfs_nid_postfix(int d, KernelStatfs* buf) { if (GuestSockets::IsOpen(d)) return PosixFailure(GUEST_EINVAL); if (d < 0) return PosixFailure(GUEST_EBADF); + File::RefuseArchivedFile(d, "fstatfs"); KernelStatfs result{}; result.f_version = KERNEL_STATFS_VERSION; if (const int error = NativeFstatfs(d, &result)) return PosixFailure(error); @@ -745,6 +747,7 @@ int APS5_VABI ftruncate_nid_postfix(int d, int64_t length) { if (length < 0 || GuestSockets::IsOpen(d)) { return PosixFailure(GUEST_EINVAL); } + File::RefuseArchivedFile(d, "ftruncate"); #ifdef _WIN32 int error = NativeFtruncate(d, length); if (error != 0) { @@ -904,6 +907,7 @@ int APS5_VABI sceKernelFstat(int d, FileStat* sb) { int APS5_VABI sceKernelFsync(int fd) { if (GuestSockets::IsOpen(fd)) return SceErrorFromErrno(GUEST_EINVAL); + File::RefuseArchivedFile(fd, "fsync"); #ifdef _WIN32 if (NativeSync(fd) != 0) return SceErrorFromErrno(errno); #else @@ -914,6 +918,7 @@ int APS5_VABI sceKernelFsync(int fd) { int APS5_VABI sceKernelFdatasync(int fd) { if (GuestSockets::IsOpen(fd)) return SceErrorFromErrno(GUEST_EINVAL); + File::RefuseArchivedFile(fd, "fdatasync"); #ifdef _WIN32 if (NativeSync(fd) != 0) return SceErrorFromErrno(errno); #else @@ -935,6 +940,7 @@ int APS5_VABI sceKernelWriteThrottlingStatus(std::uint64_t* status) { int APS5_VABI sceKernelGetdirentries(int fd, char* buf, int nbytes, int64_t* basep) { if (GuestSockets::IsOpen(fd)) return SceErrorFromErrno(GUEST_EINVAL); + File::RefuseArchivedFile(fd, "getdirentries"); if (buf == nullptr) return SceErrorFromErrno(GUEST_EFAULT); if (nbytes <= 0) return SceErrorFromErrno(GUEST_EINVAL); if (basep != nullptr) *basep = 0; @@ -949,6 +955,7 @@ int APS5_VABI sceKernelGetdents(int fd, char* buf, int nbytes) { int APS5_VABI sceKernelGetdirentries(int fd, char* buf, int nbytes, int64_t* basep) { if (GuestSockets::IsOpen(fd)) return SceErrorFromErrno(GUEST_EINVAL); + File::RefuseArchivedFile(fd, "getdirentries"); constexpr std::size_t GuestHeaderBytes = 8; constexpr std::size_t GuestMaxName = 255; if (buf == nullptr) return SceErrorFromErrno(GUEST_EFAULT); @@ -1270,6 +1277,7 @@ int APS5_VABI sceKernelChmod_nid_postfix(const char* path, std::uint16_t mode) { int APS5_VABI sceKernelFchmod(int d, std::uint16_t mode) { if (GuestSockets::IsOpen(d)) return SceErrorFromErrno(GUEST_EINVAL); + File::RefuseArchivedFile(d, "fchmod"); if (NativeFchmod(d, mode & 07777) != 0) return SceErrorFromErrno(errno); return 0; } @@ -1332,6 +1340,7 @@ int APS5_VABI lutimes_nid_postfix(const char* path, const KernelTimeval* times) int APS5_VABI sceKernelFutimes(int d, const KernelTimeval* times) { if (GuestSockets::IsOpen(d)) return SceErrorFromErrno(GUEST_EINVAL); + File::RefuseArchivedFile(d, "futimes"); if (times != nullptr) { for (int i = 0; i < 2; ++i) { if (times[i].tv_usec < 0 || times[i].tv_usec >= 1000000) return SceErrorFromErrno(GUEST_EINVAL); diff --git a/core/libs/tests/GuestKernelAppArchive.cpp b/core/libs/tests/GuestKernelAppArchive.cpp index 12679744ee..2eb8aae01f 100644 --- a/core/libs/tests/GuestKernelAppArchive.cpp +++ b/core/libs/tests/GuestKernelAppArchive.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include struct KernelIovec { @@ -44,6 +45,19 @@ void* APS5_VABI fdopen_nid_postfix(int d, const char* mode); int APS5_VABI sceKernelAioSubmitReadCommands(KernelAioRwRequest* req, std::int32_t size, std::int32_t prio, std::int32_t* id); int APS5_VABI sceKernelAioWaitRequest(std::int32_t id, std::int32_t* state, std::uint32_t* usec); int APS5_VABI sceKernelAioDeleteRequest(std::int32_t id, std::int32_t* ret); +int APS5_VABI flock_nid_postfix(int d, int operation); +int APS5_VABI _fstatfs_nid_postfix(int d, void* buf); +int APS5_VABI ftruncate_nid_postfix(int d, std::int64_t length); +int APS5_VABI sceKernelFsync(int d); +int APS5_VABI sceKernelFdatasync(int d); +int APS5_VABI sceKernelGetdents(int d, char* buf, int nbytes); +int APS5_VABI sceKernelFchmod(int d, std::uint16_t mode); +int APS5_VABI sceKernelFutimes(int d, const void* times); +void* APS5_VABI mmap_nid_postfix(void* address, std::size_t length, int protection, int flags, int descriptor, std::int64_t offset) noexcept; +void* APS5_VABI __cxa_current_primary_exception_nid_postfix(); +std::type_info* APS5_VABI __cxa_current_exception_type_nid_postfix(); +using TerminateHandler = void (*)(); +TerminateHandler APS5_VABI _ZSt13set_terminatePFvvE_nid_postfix(TerminateHandler); } namespace { @@ -56,6 +70,14 @@ void Require(bool value, int line) { } #define Check(value) Require((value), __LINE__) +void OnMmapRefused() { + const auto* type = __cxa_current_exception_type_nid_postfix(); + const auto* error = static_cast(__cxa_current_primary_exception_nid_postfix()); + const bool refused = type != nullptr && error != nullptr && std::strcmp(type->name(), typeid(std::runtime_error).name()) == 0 && + std::strstr(error->what(), "mmap on a file read from app0.zar") != nullptr && std::strstr(error->what(), "not implemented") != nullptr; + std::_Exit(refused ? 0 : 1); +} + template bool NotImplemented(TCall call) { try { call(); @@ -175,6 +197,16 @@ void Refusals() { const int fd = sceKernelOpen("/app0/Data/Small.txt", SCE_KERNEL_O_RDONLY, 0); Check(fd >= 0); Check(NotImplemented([fd] { (void)fdopen_nid_postfix(fd, "r"); })); + Check(NotImplemented([fd] { (void)flock_nid_postfix(fd, 1); })); + Check(NotImplemented([fd] { alignas(8) char statfs[512]; (void)_fstatfs_nid_postfix(fd, statfs); })); + Check(NotImplemented([fd] { (void)ftruncate_nid_postfix(fd, 0); })); + Check(NotImplemented([fd] { (void)sceKernelFsync(fd); })); + Check(NotImplemented([fd] { (void)sceKernelFdatasync(fd); })); + Check(NotImplemented([fd] { char entries[512]; (void)sceKernelGetdents(fd, entries, sizeof(entries)); })); + Check(NotImplemented([fd] { (void)sceKernelFchmod(fd, 0444); })); + Check(NotImplemented([fd] { (void)sceKernelFutimes(fd, nullptr); })); + FileStat status{}; + Check(sceKernelFstat(fd, &status) == 0 && status.st_size == 3); Check(sceKernelClose(fd) == 0); } @@ -206,6 +238,13 @@ int main(int argc, char** argv) { Reads(); Status(); Refusals(); + } else if (mode == "mmap") { + Archive(root); + const int fd = sceKernelOpen("/app0/Data/Small.txt", SCE_KERNEL_O_RDONLY, 0); + Check(fd >= 0); + _ZSt13set_terminatePFvvE_nid_postfix(OnMmapRefused); + (void)mmap_nid_postfix(nullptr, 16384, 1, 2, fd, 0); + return 1; } else if (mode == "plain") { WithoutArchive(); } else { diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index a78dcef716..7d2d75e609 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -200,4 +200,4 @@ - [getpriority](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) follows FreeBSD 11.4's [`sys_getpriority`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_resource.c#L93-L165) for the only guest process, which is taken to run at nice 0: `PRIO_PROCESS` and `PRIO_PGRP` with 0 or its own pid (the guest is taken to lead its own process group, see `kill`) and `PRIO_USER` with 0 or its user id 0 return 0, any other id fails with `ESRCH` and any other `which` with `EINVAL`. As in FreeBSD's libc, where `getpriority` is the bare system call, errno is left unchanged on success. The nice value of a title process on the console is unverified, and the host nice of the process or of the threads `APS5_THREAD_PRIORITY=high` raises is not reported. `setpriority` accepts the same target ids (any other id fails with `ESRCH`, any other `which` with `EINVAL`), clamps the value to FreeBSD's `PRIO_MIN`-`PRIO_MAX` (-20 to 20) and stores it as the guest process's nice value, which `getpriority` returns; the host scheduling class is not changed, and whether a title process may lower the value without privilege is unverified - [rtprio_thread, pthread_setconcurrency, pthread_getconcurrency](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - `rtprio_thread` follows FreeBSD 11.4's `rtprio_thread(2)`. Every guest thread here runs under `SCHED_FIFO` or `SCHED_RR` (priority 256-767), whose rtprio class is real-time; the console's mapping from that priority to the rtprio `prio` range (0-31) is unmeasured, so `RTP_LOOKUP` and `RTP_SET` throw for such a thread rather than report an impossible priority. A thread that is not real-time reports `RTP_PRIO_NORMAL` (3) with 0, and `RTP_SET` accepts only that value. Another function fails with `EINVAL`, a null `rtprio` with `EFAULT`, a thread other than the calling one throws, and an id that is not a live guest thread with `ESRCH`. `pthread_setconcurrency` stores 0 or more and returns `EINVAL` for a negative level (the error itself, as FreeBSD's libthr does, with errno untouched); `pthread_getconcurrency` returns the level, which is a hint and changes nothing about the host's concurrency. Morrowind (`mshivam019/morrowind-ps5`) imports all three at start-up - [sceKernelVirtualQuery](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) returns [VirtualQueryInfo](../../../core/libs/SceTypes.hpp) with a one-byte flag allocation unit so Windows and SysV hosts return the same 72-byte representation, with the name at byte 33 and GPU mask at byte 65. This preserves the existing SysV representation; the console offsets beyond the address, protection and size fields observed in Astro Bot's AGC calls are not hardware-measured. Size and offset assertions plus a raw-byte named-mapping regression guard the host ABI. -- [app0.zar files](../../../core/libs/prx/libkernel/File/src/ArchivedFile.cpp) (libkernel) - `sceKernelOpen`, `open`, `sceKernelStat` and `sceKernelCheckReachability` resolve a path missing from `app0/` in `app0.zar`. An archived file is a read-only host null device whose reads, positioned and vectored reads, AIO reads, seeks, `fstat` and `FIONREAD` are served from the archive, with the position shared by `dup`, `dup2` and `F_DUPFD`; writes fail with `EBADF` as on a read-only descriptor, opening one for writing or truncation fails with `EROFS` and `O_CREAT | O_EXCL` with `EEXIST`. `fstat` and `sceKernelStat` report mode `0444` (`0555` for a directory), the node index as inode and the archive file's device and times. Opening an archived directory or a directory present both on disk and in the archive throws, as `getdents` would miss entries; `mmap`, record locks, `lstat` and the other path calls on archived files are not implemented +- [app0.zar files](../../../core/libs/prx/libkernel/File/src/ArchivedFile.cpp) (libkernel) - `sceKernelOpen`, `open`, `sceKernelStat` and `sceKernelCheckReachability` resolve a path missing from `app0/` in `app0.zar`. An archived file is a read-only host null device whose reads, positioned and vectored reads, AIO reads, seeks, `fstat` and `FIONREAD` are served from the archive, with the position shared by `dup`, `dup2` and `F_DUPFD`; writes fail with `EBADF` as on a read-only descriptor, opening one for writing or truncation fails with `EROFS` and `O_CREAT | O_EXCL` with `EEXIST`. `fstat` and `sceKernelStat` report mode `0444` (`0555` for a directory), the node index as inode and the archive file's device and times. Opening an archived directory or a directory present both on disk and in the archive throws, as `getdents` would miss entries. On an archived descriptor `mmap`, `flock`, record locks, `fstatfs`, `ftruncate`, `fsync`, `fdatasync`, `getdirentries`, `fchmod` and `futimes` throw instead of acting on the null device; `mmap` is `noexcept`, so its refusal terminates the process. `lstat` and the other path calls on archived files are not implemented From 308ad40e46c593a5371a7a3f22f598dacf2ab589 Mon Sep 17 00:00:00 2001 From: xxLnolan <136196109+xxLnolan@users.noreply.github.com> Date: Wed, 14 Oct 2026 14:27:51 +0200 Subject: [PATCH 0546/1000] fix(keycode): map HID non-US backslash key --- core/libs/prx/libSceConvertKeycode/Export.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/core/libs/prx/libSceConvertKeycode/Export.cpp b/core/libs/prx/libSceConvertKeycode/Export.cpp index 0fa5a89b58..9b9e92908f 100644 --- a/core/libs/prx/libSceConvertKeycode/Export.cpp +++ b/core/libs/prx/libSceConvertKeycode/Export.cpp @@ -18,6 +18,7 @@ char16_t UsCharacter(std::uint16_t keycode) { if (keycode == 0x27) return u'0'; if (keycode == 0x2C) return u' '; if (keycode >= 0x2D && keycode <= 0x38) return kPunctuation[keycode - 0x2D]; + if (keycode == 0x64) return u'\\'; if (keycode >= 0x54 && keycode <= 0x63) return kKeypad[keycode - 0x54]; return 0; } From 127e1110488993b0cfdeb3fa98ff4e3915b94ad0 Mon Sep 17 00:00:00 2001 From: xxLnolan <136196109+xxLnolan@users.noreply.github.com> Date: Thu, 15 Oct 2026 19:08:47 +0200 Subject: [PATCH 0547/1000] test(keycode): cover non-US backslash mapping --- core/libs/tests/GuestConvertKeycode.cpp | 4 ++++ docs/dev/debt/libSceConvertKeycode.md | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/core/libs/tests/GuestConvertKeycode.cpp b/core/libs/tests/GuestConvertKeycode.cpp index ec8968222b..7e4b8c1cc4 100644 --- a/core/libs/tests/GuestConvertKeycode.cpp +++ b/core/libs/tests/GuestConvertKeycode.cpp @@ -36,6 +36,10 @@ int main() { Require(character[0] == u'1'); Require(sceConvertKeycodeGetCharacter(0x3A, state, 0, character) == 0); Require(character[0] == 0); + Require(sceConvertKeycodeGetCharacter(0x64, state, 0, character) == 0); + Require(character[0] == u'\\'); + Require(sceConvertKeycodeGetCharacter(0x32, state, 0, character) == 0); + Require(character[0] == 0); } return 0; } diff --git a/docs/dev/debt/libSceConvertKeycode.md b/docs/dev/debt/libSceConvertKeycode.md index 25d15eaa60..458e175688 100644 --- a/docs/dev/debt/libSceConvertKeycode.md +++ b/docs/dev/debt/libSceConvertKeycode.md @@ -2,7 +2,7 @@ ### Unknown function info -- [sceConvertKeycodeGetCharacter](../../../core/libs/prx/libSceConvertKeycode/Export.cpp) (libSceConvertKeycode) - the signature `(uint16_t keycode, uint32_t, uint32_t keyboardType, char16_t* character)` is inferred from PPSA12361, which passes a 16-bit keycode, 0, the type from `sceConvertKeycodeGetImeKeyboardType` and a pointer it reads one UTF-16 unit from when the result is 0. The meaning of the second argument is unknown. PRAGMATA (PPSA02530, #5726) and Onimusha: Way of the Sword (PPSA27836) pass `0x4000000` as a constant: Onimusha calls it for every keycode from 0 to 0x8F to build a table of key names, upper-cases `a` to `z` itself and skips keycodes that fail or return 0, so that bit is ignored and the unshifted character is returned as for 0. Any other bit throws, and so does a keyboard type other than 0. +- [sceConvertKeycodeGetCharacter](../../../core/libs/prx/libSceConvertKeycode/Export.cpp) (libSceConvertKeycode) - the signature `(uint16_t keycode, uint32_t, uint32_t keyboardType, char16_t* character)` is inferred from PPSA12361, which passes a 16-bit keycode, 0, the type from `sceConvertKeycodeGetImeKeyboardType` and a pointer it reads one UTF-16 unit from when the result is 0. The meaning of the second argument is unknown. PRAGMATA (PPSA02530, #5726) and Onimusha: Way of the Sword (PPSA27836) pass `0x4000000` as a constant: Onimusha calls it for every keycode from 0 to 0x8F to build a table of key names, upper-cases `a` to `z` itself and skips keycodes that fail or return 0, so that bit is ignored and the unshifted character is returned as for 0. Any other bit throws, and so does a keyboard type other than 0. The returned character for HID usage `0x64` has not been measured on a console; `\` is an unverified US-layout guess based on the Windows VK mapping, which does not establish the console result. ### Functional From e8c97bf9269850b0784db77022939a1d4d314038 Mon Sep 17 00:00:00 2001 From: Ahmad Date: Wed, 14 Oct 2026 15:31:26 +0300 Subject: [PATCH 0548/1000] fix(libkernel): append O_APPEND writes atomically on Windows On Windows the C runtime appends by seeking to the end and then writing, so concurrent appends through different descriptors overwrite each other and lose data. Write and writev on an O_APPEND descriptor now go through WriteFile at the end-of-file offset, which Windows appends atomically, and writev gathers its buffers into one append as FreeBSD writes the whole vector at the end. --- core/libs/prx/libkernel/CMakeLists.txt | 8 +- .../File/include/DescriptorControl.hpp | 1 + core/libs/prx/libkernel/File/include/File.hpp | 3 + .../libkernel/File/src/DescriptorControl.cpp | 6 + core/libs/prx/libkernel/File/src/Open.cpp | 4 + core/libs/prx/libkernel/File/src/Stdio.cpp | 35 ++++++ core/libs/tests/GuestAppendWrites.cpp | 110 ++++++++++++++++++ 7 files changed, 166 insertions(+), 1 deletion(-) create mode 100644 core/libs/tests/GuestAppendWrites.cpp diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 4593cae303..695ac3da78 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -1399,8 +1399,14 @@ if(BUILD_TESTING) configure_windows_unwind(guest_kernel_fcntl_tests) add_test(NAME guest_kernel_fcntl COMMAND guest_kernel_fcntl_tests) set_tests_properties(guest_kernel_fcntl PROPERTIES TIMEOUT 15) + add_test_executable(guest_append_writes_tests ${LIBS_INCLUDE_DIR}/tests/GuestAppendWrites.cpp) + target_include_directories(guest_append_writes_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_append_writes_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_append_writes_tests) + add_test(NAME guest_append_writes COMMAND guest_append_writes_tests) + set_tests_properties(guest_append_writes PROPERTIES TIMEOUT 20) if(WIN32) - set_tests_properties(guest_kernel_fcntl PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(guest_kernel_fcntl guest_append_writes PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") add_test_executable(guest_file_lock_tests ${LIBS_INCLUDE_DIR}/tests/GuestFileLock.cpp) target_include_directories(guest_file_lock_tests PRIVATE ${LIBS_INCLUDE_DIR}) diff --git a/core/libs/prx/libkernel/File/include/DescriptorControl.hpp b/core/libs/prx/libkernel/File/include/DescriptorControl.hpp index cb327dff54..d7345faec2 100644 --- a/core/libs/prx/libkernel/File/include/DescriptorControl.hpp +++ b/core/libs/prx/libkernel/File/include/DescriptorControl.hpp @@ -14,6 +14,7 @@ int DuplicateFrom(int fd, int start); void RememberDescriptor(int fd, int openFlags); void ShareDescriptor(int from, int to); void ForgetDescriptor(int fd); +bool Appends(int fd); } diff --git a/core/libs/prx/libkernel/File/include/File.hpp b/core/libs/prx/libkernel/File/include/File.hpp index 4a14063e0e..d759d1e3f9 100644 --- a/core/libs/prx/libkernel/File/include/File.hpp +++ b/core/libs/prx/libkernel/File/include/File.hpp @@ -30,6 +30,9 @@ namespace File { int HostStandardError(); [[nodiscard]] std::unique_lock LockPositionedTransfer(int descriptor); void SharePositionedTransfer(int from, int to); +#ifdef _WIN32 +std::int64_t WriteAtEnd(int descriptor, const void* buf, std::size_t nbytes); +#endif } #endif diff --git a/core/libs/prx/libkernel/File/src/DescriptorControl.cpp b/core/libs/prx/libkernel/File/src/DescriptorControl.cpp index 3e7f3c0644..482f9ac521 100644 --- a/core/libs/prx/libkernel/File/src/DescriptorControl.cpp +++ b/core/libs/prx/libkernel/File/src/DescriptorControl.cpp @@ -558,4 +558,10 @@ void ForgetDescriptor(int fd) { g_entries.erase(fd); } +bool Appends(int fd) { + std::lock_guard lock(g_mutex); + const auto found = g_entries.find(fd); + return found != g_entries.end() && found->second.status && (*found->second.status & Append) != 0; +} + } diff --git a/core/libs/prx/libkernel/File/src/Open.cpp b/core/libs/prx/libkernel/File/src/Open.cpp index 9760bbe879..d09dcf908a 100644 --- a/core/libs/prx/libkernel/File/src/Open.cpp +++ b/core/libs/prx/libkernel/File/src/Open.cpp @@ -73,6 +73,10 @@ static int NativeWrite(int fd, const void* buf, std::size_t n) { const auto positioned = File::LockPositionedTransfer(fd); const auto shared = File::WriteSharedMemory_nid_no_patch(fd, buf, n); if (shared != -2) return static_cast(shared); + if (File::Appends(fd)) { + const auto appended = File::WriteAtEnd(fd, buf, n); + if (appended != -2) return static_cast(appended); + } const int result = ::_write(fd, buf, static_cast(n)); if (result < 0 && errno == ENOSPC && _doserrno == 0 && ::GetFileType(reinterpret_cast(::_get_osfhandle(fd))) == FILE_TYPE_PIPE) errno = EAGAIN; return result; diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index 5ded00a7ba..d861251c98 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -237,6 +237,27 @@ std::int64_t NativePositioned_nid_no_patch(int descriptor, void* buf, std::size_ const auto positioned = File::LockPositionedTransfer(descriptor); return NativePositionedTransfer(descriptor, buf, nbytes, offset, write); } +std::int64_t File::WriteAtEnd(int descriptor, const void* buf, std::size_t nbytes) { + if (nbytes > static_cast(std::numeric_limits::max())) { + throw std::runtime_error("WriteAtEnd: nbytes exceeds platform limit"); + } + const auto handle = reinterpret_cast(WithoutParameterHandler([descriptor] { return ::_get_osfhandle(descriptor); })); + if (handle == INVALID_HANDLE_VALUE) { + errno = EBADF; + return -1; + } + if (::GetFileType(handle) != FILE_TYPE_DISK) return -2; + OVERLAPPED end{}; + end.Offset = 0xFFFFFFFF; + end.OffsetHigh = 0xFFFFFFFF; + DWORD done = 0; + if (!::WriteFile(handle, buf, static_cast(nbytes), &done, &end)) { + const DWORD error = ::GetLastError(); + errno = error == ERROR_ACCESS_DENIED ? EBADF : error == ERROR_DISK_FULL || error == ERROR_HANDLE_DISK_FULL ? ENOSPC : EIO; + return -1; + } + return done; +} static std::int64_t NativeTransferBuffer(int descriptor, void* buf, std::size_t nbytes, bool write) { if (nbytes > static_cast(std::numeric_limits::max())) { throw std::runtime_error("NativeTransfer: nbytes exceeds platform limit"); @@ -244,6 +265,10 @@ static std::int64_t NativeTransferBuffer(int descriptor, void* buf, std::size_t if (write) { const auto result = File::WriteSharedMemory_nid_no_patch(descriptor, buf, nbytes); if (result != -2) return result; + if (File::Appends(descriptor)) { + const auto appended = File::WriteAtEnd(descriptor, buf, nbytes); + if (appended != -2) return appended; + } } return write ? ::_write(descriptor, buf, static_cast(nbytes)) : ::_read(descriptor, buf, static_cast(nbytes)); } @@ -1086,6 +1111,16 @@ static std::int64_t TransferIovecs(int d, const KernelIovec* iov, int iovcnt, co return result < 0 ? SceErrorFromErrno(errno) : 0; } if (!write && offset == nullptr && File::DirectoryDescriptorPath(d)) return SceErrorFromErrno(GUEST_EISDIR); + if (write && offset == nullptr && iovcnt > 1 && total <= static_cast(std::numeric_limits::max()) && File::Appends(d)) { + std::vector gathered; + gathered.reserve(total); + for (int i = 0; i < iovcnt; ++i) { + const auto* base = static_cast(iov[i].base); + gathered.insert(gathered.end(), base, base + iov[i].length); + } + const auto result = NativeTransferBuffer(d, gathered.data(), gathered.size(), true); + return result < 0 ? SceErrorFromErrno(errno) : result; + } const bool whole = write || offset != nullptr || NativeIsDisk(d); std::int64_t done = 0; for (int i = 0; i < iovcnt; ++i) { diff --git a/core/libs/tests/GuestAppendWrites.cpp b/core/libs/tests/GuestAppendWrites.cpp new file mode 100644 index 0000000000..0780e303eb --- /dev/null +++ b/core/libs/tests/GuestAppendWrites.cpp @@ -0,0 +1,110 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +struct GuestIovec { + const void* base; + std::size_t length; +}; + +extern "C" { +int APS5_VABI sceKernelOpen(const char*, int, std::uint16_t); +int APS5_VABI sceKernelClose(int); +std::int64_t APS5_VABI sceKernelWrite(int, const void*, std::size_t); +std::int64_t APS5_VABI sceKernelLseek(int, std::int64_t, int); +std::int64_t APS5_VABI write_nid_postfix(int, const char*, std::int64_t); +std::int64_t APS5_VABI writev_nid_postfix(int, const GuestIovec*, int); +int APS5_VABI dup_nid_postfix(int); +} + +static void Check(bool condition, int line) { + if (!condition) { + std::fprintf(stderr, "append check failed at line %d\n", line); + std::abort(); + } +} +#define Require(value) Check((value), __LINE__) + +static constexpr const char* Path = "guest_append_writes.txt"; +static constexpr int CreateWriteAppend = 0x0209; +static constexpr int Truncate = 0x0400; +static constexpr int FromStart = 0; +static constexpr int FromCurrent = 1; +static constexpr int Writers = 4; +static constexpr int LinesPerWriter = 1500; +static constexpr std::size_t LineLength = 32; + +static std::string Line(int writer, int index) { + std::string line = "w" + std::to_string(writer) + " " + std::to_string(index) + " "; + line.resize(LineLength - 1, '.'); + return line + "\n"; +} + +static void WriteLines(int fd, int writer) { + for (int index = 0; index < LinesPerWriter; ++index) { + const std::string line = Line(writer, index); + std::int64_t written = -1; + if (writer == 0 || writer == 2) { + written = sceKernelWrite(fd, line.data(), line.size()); + } else if (writer == 1) { + written = write_nid_postfix(fd, line.data(), static_cast(line.size())); + } else { + const GuestIovec parts[2]{{line.data(), 7}, {line.data() + 7, line.size() - 7}}; + written = writev_nid_postfix(fd, parts, 2); + } + Require(written == static_cast(LineLength)); + } +} + +static std::string Contents() { + std::ifstream file(Path, std::ios::binary); + return std::string(std::istreambuf_iterator(file), std::istreambuf_iterator()); +} + +int main() { + const int first = sceKernelOpen(Path, CreateWriteAppend | Truncate, 0644); + Require(first >= 0); + const int second = sceKernelOpen(Path, CreateWriteAppend, 0644); + const int duplicate = dup_nid_postfix(first); + const int vectored = sceKernelOpen(Path, CreateWriteAppend, 0644); + Require(second >= 0 && duplicate >= 0 && vectored >= 0); + + const int descriptors[Writers]{first, second, duplicate, vectored}; + std::vector threads; + for (int writer = 0; writer < Writers; ++writer) threads.emplace_back(WriteLines, descriptors[writer], writer); + for (auto& thread : threads) thread.join(); + + const std::string contents = Contents(); + Require(contents.size() == static_cast(Writers) * LinesPerWriter * LineLength); + std::set> seen; + for (std::size_t offset = 0; offset < contents.size(); offset += LineLength) { + const std::string line = contents.substr(offset, LineLength); + int writer = -1; + int index = -1; + Require(std::sscanf(line.c_str(), "w%d %d ", &writer, &index) == 2); + Require(writer >= 0 && writer < Writers && index >= 0 && index < LinesPerWriter); + Require(line == Line(writer, index)); + Require(seen.emplace(writer, index).second); + } + Require(seen.size() == static_cast(Writers) * LinesPerWriter); + + const auto size = static_cast(contents.size()); + Require(sceKernelLseek(first, 0, FromStart) == 0); + const std::string tail = Line(9, 0); + Require(sceKernelWrite(first, tail.data(), tail.size()) == static_cast(LineLength)); + Require(sceKernelLseek(first, 0, FromCurrent) == size + static_cast(LineLength)); + Require(Contents().substr(static_cast(size)) == tail); + + for (const int fd : descriptors) Require(sceKernelClose(fd) == 0); + std::remove(Path); +} From f888a236bc47c0f117fd31c90485012dbbacaf9a Mon Sep 17 00:00:00 2001 From: alosta Date: Wed, 14 Oct 2026 15:50:13 +0100 Subject: [PATCH 0549/1000] fix(libSceNgs2): support custom filter type 0 in Ngs2ApplyCustomParam Allow custom voice filter type 0 to clear the destination filter, matching the sampler filter convention. Type 1 continues to configure the biquad frequency, Q and level. Other unsupported filter types still throw. This follows the listener-transform support in fix/libSceNgs2-listener-transform and is needed for Dead Cells (PPSA15552), which reaches this path after the custom sampler rack. Depends on: fix(libSceNgs2): support listener transforms in sceNgs2GeomCalcListener- #5810 Unblocks: PPSA15552 AI assistance: yes --- core/libs/prx/libSceNgs2.native/src/Custom.cpp | 4 +++- docs/dev/debt/libSceNgs2.native.md | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceNgs2.native/src/Custom.cpp b/core/libs/prx/libSceNgs2.native/src/Custom.cpp index 5f163c25ee..d3e586a4d9 100644 --- a/core/libs/prx/libSceNgs2.native/src/Custom.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Custom.cpp @@ -136,7 +136,9 @@ void Ngs2ApplyCustomParam(Ngs2Voice& voice, const Ngs2VoiceParamHeader& param) { filter.location = 1; filter.type = custom.type; filter.channel_mask = custom.channel_mask; - if (custom.type == 1) { + if (custom.type == 0) { + filter = {}; + } else if (custom.type == 1) { filter.frequency = custom.param.fcq.frequency; filter.q = custom.param.fcq.q; filter.level = custom.param.fcq.level; diff --git a/docs/dev/debt/libSceNgs2.native.md b/docs/dev/debt/libSceNgs2.native.md index 25765ec1d8..77db81ff4e 100644 --- a/docs/dev/debt/libSceNgs2.native.md +++ b/docs/dev/debt/libSceNgs2.native.md @@ -26,5 +26,5 @@ - [sceNgs2PanInit / sceNgs2PanGetVolumeMatrix](../../../core/libs/prx/libSceNgs2.native/src/Pan.cpp) (libSceNgs2.native) - follow the PS4 firmware 11.00 `libSceNgs2.sprx`, read and checked against its output for 2, 4, 5 and 7 speakers into 1, 2, 6 and 8 channels; not yet confirmed on the PS5 library. Speakers are taken in the order L R, L R Ls Rs, L R C Ls Rs or L R C Ls Rs Lb Rb and kept in a fixed circular order, not sorted; without angles the defaults are 90/270, 40/110/250/320, 0/40/110/250/320 and 0/40/110/150/210/250/320 degrees. As in that library, defaults converted to radians keep the wrap-around entry in degrees, so angles past the last speaker pan almost fully to it. Error codes are not known for the PS5, so invalid arguments throw, including a speaker angle equal to the unit angle, on which that library loops forever. Angles so large that adding or subtracting a turn leaves them unchanged throw for the same reason - [sceNgs2GeomResetListenerParam, sceNgs2GeomResetSourceParam](../../../core/libs/prx/libSceNgs2.native/src/Geom.cpp) (libSceNgs2.native) - the structures (12, 16, 16, 60, 96, 108, 32 and 308 bytes) come from [KytyPS5](https://github.com/KytyPS5/KytyPS5)'s `ngs2.cpp` and match [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `ngs2_geom.h` field for field, but only KytyPS5 gives the behaviour - shadPS4 logs and returns success without writing the output. The defaults (orient front +Z, orient up +Y, sound speed 343, cone 1/360/1/360, rolloff 0/1000000/1/1, doppler factor, full-band, LFE and maximum level 1, two speakers and matrix format 2) are unverified on a PS5 title; KytyPS5 and iStark/PS5PCEM agree on them. PPSA15552 (Dead Cells) calls `sceNgs2GeomResetSourceParam` after creating its racks. A null output address returns `INVALID_OUT_ADDRESS` as elsewhere in the library rather than throwing as KytyPS5 does. `sceNgs2GeomApply` stays unimplemented - [sceNgs2GeomCalcListener](../../../core/libs/prx/libSceNgs2.native/src/Geom.cpp) builds an orthonormal world-to-listener transform. The row-major matrix, front-axis priority and flag 1 for right-handed input follow [PS5PCEM](https://github.com/iStark/PS5PCEM/blob/d2534bac6325a2c61c0fa88977e5321074304484/src/hle/ngs2_geom.zig); they have not been measured on PS5 hardware. Velocity stays in world coordinates and positive finite sound speed is preserved. Orientation vectors are normalised and up is made perpendicular to front, so non-unit and slightly tilted vectors are accepted; unknown flags, zero or parallel orientation vectors, nonfinite input and an unrepresentable translation throw without changing the output; console error codes are unknown, and a nonpositive sound speed throws where PS5PCEM and [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/df5b9b92be7dfd463b47756b707d6d356a9f0550/src/libs/ngs2.cpp) default it to 343. KytyPS5 always writes the identity matrix, so the transform rests on PS5PCEM alone. Dead Cells (PPSA15552) reaches this call after the custom sampler rack in #4334. This does not implement source attenuation, panning or Doppler in `sceNgs2GeomApply`. -- [NGS2 custom sampler rack](../../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (libSceNgs2.native, rack 0x4001) - implements bounded support for the custom sampler rack and its built-in module 0x13 (Custom Voice Filter), unblocking PPSA15552 (Dead Cells) startup. Structure layouts come from [shadPS4's ngs2_custom.h](https://raw.githubusercontent.com/shadps4-emu/shadPS4/main/src/core/libraries/ngs2/ngs2_custom.h) (OrbisNgs2CustomVoiceFilterParam, 40 bytes) and live trace correlation from [prosper's hle_audio.cpp](https://raw.githubusercontent.com/mattias800/prosper/main/prosper/src/hle/audio/hle_audio.cpp) (0x40001300 command and parameters like 10000.0f and 0.707f). Routes parameter updates via the 0x40001300 command family into the Biquad filter engine, supporting only filter type 1; other filter types and unmodeled configurations throw. +- [NGS2 custom sampler rack](../../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (libSceNgs2.native, rack 0x4001) - implements bounded support for the custom sampler rack and its built-in module 0x13 (Custom Voice Filter), unblocking PPSA15552 (Dead Cells) startup. Structure layouts come from [shadPS4's ngs2_custom.h](https://raw.githubusercontent.com/shadps4-emu/shadPS4/main/src/core/libraries/ngs2/ngs2_custom.h) (OrbisNgs2CustomVoiceFilterParam, 40 bytes) and live trace correlation from [prosper's hle_audio.cpp](https://raw.githubusercontent.com/mattias800/prosper/main/prosper/src/hle/audio/hle_audio.cpp) (0x40001300 command and parameters like 10000.0f and 0.707f). Routes parameter updates via the 0x40001300 command family into the Biquad filter engine. Filter type 0 clears the destination filter; type 1 configures the RBJ low-pass biquad frequency, Q and level. Other filter types and unmodeled configurations throw. - [sceNgs2VoiceGetState custom sampler](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native, rack 0x4001) - only the size and one field come from a title. PPSA15552 (Dead Cells) calls it on custom sampler voices with a 48-byte buffer at two sites (eboot `0x1740f90`, `0x1741009`) and reads only the 64-bit value at offset 16: the first site keeps it and the second subtracts the kept value, so it is filled with the voice's decoded sample count. Other sizes return `INVALID_OUT_SIZE` as for the other racks. The state flags fill the common voice state; the remaining fields stay zero. shadPS4's `OrbisNgs2CustomSamplerVoiceState` is about 80 bytes and does not match the size this title passes, so the field names and the console's value are unverified From b8b42e61ed486084c41f8b9df73a0da5a54a1f85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Fri, 16 Oct 2026 20:32:04 +0200 Subject: [PATCH 0550/1000] test(libSceNgs2): cover custom filter type 0 and unsupported types Co-Authored-By: Claude --- core/libs/prx/libSceNgs2.native/tests/Ngs2Custom.cpp | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Custom.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Custom.cpp index ef5b874c41..379c277110 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2Custom.cpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Custom.cpp @@ -342,6 +342,15 @@ static void TestCustomFilter() { const Ngs2RenderBufferInfo renderInfo{out.data(), out.size() * sizeof(float), SCE_NGS2_WAVEFORM_TYPE_PCM_F32L, 1}; Require(sceNgs2SystemRender(system, &renderInfo, 1) == SCE_NGS2_OK); Require(out[0] > 0.0f && out[0] < 0.2f && out.back() > out[0]); + + Ngs2CustomVoiceFilterParam unsupported{}; + unsupported.type = 2; + Require(ControlThrows(voice, SCE_NGS2_CUSTOM_VOICE_PARAM_FILTER, unsupported)); + Control(voice, SCE_NGS2_CUSTOM_VOICE_PARAM_FILTER, Ngs2CustomVoiceFilterParam{}); + Control(voice, SCE_NGS2_CUSTOM_SAMPLER_VOICE_PARAM_ADD_WAVEFORM_BLOCKS, + Ngs2SamplerVoiceWaveformBlocksParam{{}, pcm.data(), SCE_NGS2_WAVEFORM_BLOCKS_FLAG_RESET, 1, &block}); + Require(sceNgs2SystemRender(system, &renderInfo, 1) == SCE_NGS2_OK); + Require(out[0] > 0.4f); Require(sceNgs2RackDestroy(rack, nullptr) == SCE_NGS2_OK); Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); } From f3fb9df3a03e6dff8ca11d1e56726bec39dd37ed Mon Sep 17 00:00:00 2001 From: fdfawef324 <287205748+fdfawef324@users.noreply.github.com> Date: Wed, 14 Oct 2026 12:55:15 -0400 Subject: [PATCH 0551/1000] feat(agc): replay prepared shader capture and materialization offline Add a CPU replay mode that reaches the runtime descriptor checks and specialization used by prepared shader invocations. Test active and inactive guarded descriptors and continuation after a failed request. Co-Authored-By: Codex --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 19 +++++++ .../tests/ShaderReplayRequests.cpp | 57 +++++++++++++++++++ .../tests/ShaderReplayTool.cmake | 30 ++++++++++ .../libSceAgcDriver/tools/ShaderReplay.cpp | 45 +++++++++++---- 4 files changed, 140 insertions(+), 11 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/ShaderReplayRequests.cpp create mode 100644 core/libs/prx/libSceAgcDriver/tests/ShaderReplayTool.cmake diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index f112af6ade..421a50134a 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1935,6 +1935,25 @@ endif() set_target_properties(agc_shader_replay PROPERTIES CXX_EXTENSIONS OFF) configure_windows_unwind(agc_shader_replay) +if(BUILD_TESTING) + add_test_executable(agc_shader_replay_requests_tests tests/ShaderReplayRequests.cpp) + target_link_libraries(agc_shader_replay_requests_tests PRIVATE shader_recompiler_agc_driver libc) + set_target_properties(agc_shader_replay_requests_tests PROPERTIES CXX_EXTENSIONS OFF) + configure_windows_unwind(agc_shader_replay_requests_tests) + add_dependencies(agc_shader_replay_requests_tests agc_shader_replay) + set(agcShaderReplayCapture "${CMAKE_CURRENT_BINARY_DIR}/shader-replay-capture") + add_test(NAME agc_shader_replay_requests COMMAND agc_shader_replay_requests_tests "${agcShaderReplayCapture}") + set_tests_properties(agc_shader_replay_requests PROPERTIES FIXTURES_SETUP agc_shader_replay_capture TIMEOUT 30) + add_test(NAME agc_shader_replay_tool COMMAND ${CMAKE_COMMAND} -DTOOL=$ "-DCAPTURE=${agcShaderReplayCapture}" + -P ${CMAKE_CURRENT_SOURCE_DIR}/tests/ShaderReplayTool.cmake) + set_tests_properties(agc_shader_replay_tool PROPERTIES FIXTURES_REQUIRED agc_shader_replay_capture TIMEOUT 30) + set_tests_properties(agc_shader_replay_requests agc_shader_replay_tool PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1") + if(WIN32) + set_tests_properties(agc_shader_replay_requests agc_shader_replay_tool PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() + add_executable(agc_pm4_replay EXCLUDE_FROM_ALL tools/Pm4Replay.cpp tools/Pm4Analysis.cpp diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderReplayRequests.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderReplayRequests.cpp new file mode 100644 index 0000000000..ae0b1fb772 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderReplayRequests.cpp @@ -0,0 +1,57 @@ +#include "BdaAbi.hpp" +#include "ControlFlow/RequestSerializer.hpp" +#include "Recompiler.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +int main(int argc, char** argv) try { + if (argc != 2) return 2; + using namespace ShaderRecompiler; + const std::filesystem::path directory = argv[1]; + std::filesystem::create_directories(directory); + const std::vector code{0xf4000400u, 0xfa000050u, 0xf4080500u, 0xfa000020u, 0xf4080600u, 0xfa000040u, 0xbf8cc07fu, 0xbf068010u, 0xbf850006u, 0xf40c0200u, 0xfa000000u, 0xbf8cc07fu, 0xf09c0f08u, 0x00a20000u, 0xbf8c3f70u, 0xe0700000u, 0x80060000u, 0xbf810000u}; + std::array srt{}; + srt[0] = 0x12345678u; + srt[1] = 0xffffffffu; + srt[2] = 0xffffffffu; + srt[3] = 0x20000000u; + srt[16] = 0x100000u; + srt[18] = 16u; + srt[19] = 0xfacu; + const std::array userData{0x200000u, 0u}; + const std::array regions{{{0x200000u, std::as_bytes(std::span(srt))}}}; + const std::array capabilities{spv::CapabilityImageGatherExtended, spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess}; + const std::array extensions{"SPV_KHR_physical_storage_buffer", "SPV_KHR_8bit_storage"}; + RecompileRequest request{}; + request.shader = {ShaderStage::Compute, 0x22000u, code, 0, {}}; + request.context.waveSize = 32u; + request.context.userData = userData; + request.context.memory = regions; + request.context.compute = ShaderComputeStageInfo{{32u, 1u, 1u}, 0u, {false, false, false}, false, 1u}; + request.target.vulkanVersion = 0x00401000u; + request.target.spirvVersion = 0x00010300u; + request.target.subgroupSize = 32u; + request.target.bdaAbiVersion = BdaAbi::Version; + request.target.supportedCapabilities = capabilities; + request.target.supportedExtensions = extensions; + request.layout.pushConstantSizeBytes = 128u; + for (const auto mode : {0u, 1u}) { + srt[20] = mode; + std::ofstream file(directory / (mode == 0u ? "inactive.req" : "invalid.req"), std::ios::binary); + file << RequestSerializer{}.Serialize(request); + if (!file) throw std::runtime_error("cannot write synthetic replay request"); + } + std::puts("synthetic guarded replay requests written"); + return 0; +} catch (const std::exception& error) { + std::fprintf(stderr, "%s\n", error.what()); + return 1; +} diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderReplayTool.cmake b/core/libs/prx/libSceAgcDriver/tests/ShaderReplayTool.cmake new file mode 100644 index 0000000000..9b58c1d485 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderReplayTool.cmake @@ -0,0 +1,30 @@ +foreach(variable TOOL CAPTURE) + if(NOT DEFINED ${variable}) + message(FATAL_ERROR "${variable} is not set") + endif() +endforeach() + +execute_process(COMMAND "${TOOL}" --prepare "${CAPTURE}/invalid.req" RESULT_VARIABLE code OUTPUT_VARIABLE output ERROR_VARIABLE errors) +if(NOT code STREQUAL "0" OR NOT output MATCHES "prepared: [0-9]+ SPIR-V words") + message(FATAL_ERROR "preparation of the synthetic request failed: ${code}\n${output}${errors}") +endif() + +execute_process(COMMAND "${TOOL}" --materialize "${CAPTURE}/inactive.req" RESULT_VARIABLE code OUTPUT_VARIABLE output ERROR_VARIABLE errors) +if(NOT code STREQUAL "0" OR NOT output MATCHES "materialized: [0-9]+ SPIR-V words") + message(FATAL_ERROR "inactive image replay failed: ${code}\n${output}${errors}") +endif() + +execute_process(COMMAND "${TOOL}" --materialize "${CAPTURE}/invalid.req" RESULT_VARIABLE code OUTPUT_VARIABLE output ERROR_VARIABLE errors) +if(NOT code STREQUAL "1" OR NOT output MATCHES "capture failed: runtime image descriptor is invalid") + message(FATAL_ERROR "active invalid image replay did not fail at capture: ${code}\n${output}${errors}") +endif() + +execute_process(COMMAND "${TOOL}" --materialize "${CAPTURE}/invalid.req" "${CAPTURE}/inactive.req" RESULT_VARIABLE code OUTPUT_VARIABLE output ERROR_VARIABLE errors) +if(NOT code STREQUAL "1" OR NOT output MATCHES "capture failed: runtime image descriptor is invalid" OR NOT output MATCHES "materialized: [0-9]+ SPIR-V words") + message(FATAL_ERROR "replay did not continue after a failed request: ${code}\n${output}${errors}") +endif() + +execute_process(COMMAND "${TOOL}" --maintenance8 --materialize "${CAPTURE}/inactive.req" RESULT_VARIABLE code OUTPUT_VARIABLE output ERROR_VARIABLE errors) +if(NOT code STREQUAL "0" OR NOT output MATCHES "materialized: [0-9]+ SPIR-V words") + message(FATAL_ERROR "prepared replay with maintenance8 failed: ${code}\n${output}${errors}") +endif() diff --git a/core/libs/prx/libSceAgcDriver/tools/ShaderReplay.cpp b/core/libs/prx/libSceAgcDriver/tools/ShaderReplay.cpp index 524575eefd..21b36fdfe7 100644 --- a/core/libs/prx/libSceAgcDriver/tools/ShaderReplay.cpp +++ b/core/libs/prx/libSceAgcDriver/tools/ShaderReplay.cpp @@ -5,6 +5,7 @@ #include "ControlFlow/Structurizer.hpp" #include "Optimization/ResourceMaterializer.hpp" #include "Optimization/ResourceProgram.hpp" +#include "Optimization/RequestMemoryView.hpp" #include "Recompiler.hpp" #include "RdnaDecoder/RdnaInstructionDecoder.hpp" @@ -16,6 +17,7 @@ #include #include #include +#include #include namespace { @@ -42,6 +44,7 @@ bool g_assembly = false; bool g_memory = false; bool g_spirv = false; bool g_prepare = false; +bool g_materialize = false; bool g_graph = false; bool g_maintenance8 = false; bool g_code = false; @@ -106,21 +109,37 @@ bool Replay(const char* path) { return false; } } - try { - auto program = ShaderRecompiler::PrepareResourceProgram(request.request); - constexpr ShaderRecompiler::ResourceMaterializer materializer; - static_cast(materializer.ExtractPlan(program)); - } catch (const std::exception& error) { - std::printf(" resource analysis failed: %s\n", FirstLine(error.what()).c_str()); - return false; + if (g_materialize && g_maintenance8) request.request.target.nonConstantImageOffsets = true; + if (!g_materialize) { + try { + auto program = ShaderRecompiler::PrepareResourceProgram(request.request); + constexpr ShaderRecompiler::ResourceMaterializer materializer; + static_cast(materializer.ExtractPlan(program)); + } catch (const std::exception& error) { + std::printf(" resource analysis failed: %s\n", FirstLine(error.what()).c_str()); + return false; + } } - if (g_prepare) { + if (g_prepare || g_materialize) { + const char* phase = "prepare"; try { const auto handle = ShaderRecompiler::PrepareShader(request.request); - std::printf(" prepared: %zu SPIR-V words\n", ShaderRecompiler::GetPreparedArtifact(*handle).spirv.Words().size()); + std::printf(" prepared: %zu SPIR-V words\n", ShaderRecompiler::GetPreparedArtifact(*handle).spirv.size()); + if (g_materialize) { + phase = "prepared invocation"; + const auto invocation = ShaderRecompiler::PreparedShaderInvocation::TryCreate(request.request, handle); + if (!invocation.has_value()) throw std::runtime_error("request does not match its prepared shader"); + ShaderRecompiler::RequestMemoryView memory(request.request.context.memory); + const auto runtime = memory.MakeRuntime(request.request.context.userData, request.request.shader.codeAddress); + phase = "capture"; + const auto capture = invocation->Capture(runtime); + phase = "materialize"; + const auto result = invocation->Materialize(*capture); + std::printf(" materialized: %zu SPIR-V words\n", result->spirv.size()); + } return true; } catch (const std::exception& error) { - std::printf(" prepare failed: %s\n", FirstLine(error.what()).c_str()); + std::printf(" %s failed: %s\n", phase, FirstLine(error.what()).c_str()); return false; } } @@ -163,7 +182,7 @@ bool Replay(const char* path) { int main(int argc, char** argv) { if (argc < 2) { - std::fprintf(stderr, "usage: agc_shader_replay [--dis] [--asm] [--cfg] [--mem] [--spv] [--prepare] [--code] [--maintenance8] ...\n the driver writes shader_
.req files when APS5_DUMP_SHADERS is set\n"); + std::fprintf(stderr, "usage: agc_shader_replay [--dis] [--asm] [--cfg] [--mem] [--spv] [--prepare] [--materialize] [--code] [--maintenance8] ...\n the driver writes shader_
.req files when APS5_DUMP_SHADERS is set\n"); return 2; } int failures = 0; @@ -196,6 +215,10 @@ int main(int argc, char** argv) { g_prepare = true; continue; } + if (std::string(argv[i]) == "--materialize") { + g_materialize = true; + continue; + } if (std::string(argv[i]) == "--spv") { g_spirv = true; continue; From f19050534e94f85b858cf233bef450a603d42415 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Fri, 16 Oct 2026 14:15:43 +0200 Subject: [PATCH 0552/1000] fix(agc): disassemble the shared SPIR-V words in agc_shader_replay --- core/libs/prx/libSceAgcDriver/tools/ShaderReplay.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tools/ShaderReplay.cpp b/core/libs/prx/libSceAgcDriver/tools/ShaderReplay.cpp index 21b36fdfe7..2383684946 100644 --- a/core/libs/prx/libSceAgcDriver/tools/ShaderReplay.cpp +++ b/core/libs/prx/libSceAgcDriver/tools/ShaderReplay.cpp @@ -166,7 +166,7 @@ bool Replay(const char* path) { if (g_disassemble) { spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_2); std::string text; - if (tools.Disassemble(result.spirv, &text, SPV_BINARY_TO_TEXT_OPTION_FRIENDLY_NAMES)) std::printf("%s\n", text.c_str()); + if (tools.Disassemble(result.spirv.data(), result.spirv.size(), &text, SPV_BINARY_TO_TEXT_OPTION_FRIENDLY_NAMES)) std::printf("%s\n", text.c_str()); } #else if (g_disassemble) std::printf(" (--dis needs ANYPS5_ENABLE_SPIRV_TOOLS=ON)\n"); From 7ab3d441c1fa183025c80af56700d86d5be64daf Mon Sep 17 00:00:00 2001 From: ps54all Date: Wed, 14 Oct 2026 13:06:25 +0200 Subject: [PATCH 0553/1000] feat(psml): implement the MFSR shared-resource and 800 context setup scePsmlMfsrInit, the shared-resource calls and the 800M3_2 context calls threw or answered "not initialized", so a title that sets PSSR up at start-up stopped there even when it never uses PSSR on hardware without it (PPSA08330 calls the setup unconditionally and only dispatches when sceKernelIsTrinityMode is set, which AnyPS5 reports as 0). The layouts come from the calls in PPSA08330 and PPSA03016: requirements are {block bytes, alignment, block count}, handles are 8-byte values written through the first argument, and a context parameter carries its block list at 8/16 and the shared-resources handle at 0x38. Init sets the initialized state, the requirements ask for four 2 MiB blocks (what PPSA08330 allocates regardless of the returned count), and create and release keep bookkeeping objects. Invalid arguments throw because their error codes are unknown. The 1100 calls, scePsmlMfsrCreateContext1300 and the 900/1000/1300 dispatch packets keep returning 0x8A810001 before Init and throw after it; packet size, mip bias and capture still throw. The guest test links libc and gets Windows unwinding so it can check the refused calls. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libScePsml_debug/CMakeLists.txt | 3 +- core/libs/prx/libScePsml_debug/Export.cpp | 194 ++++++++++++++---- core/libs/tests/GuestPsml.cpp | 188 +++++++++++++++++ docs/dev/debt/libScePsml_debug.md | 11 +- 4 files changed, 353 insertions(+), 43 deletions(-) diff --git a/core/libs/prx/libScePsml_debug/CMakeLists.txt b/core/libs/prx/libScePsml_debug/CMakeLists.txt index ae4867fdcc..aceb0af750 100644 --- a/core/libs/prx/libScePsml_debug/CMakeLists.txt +++ b/core/libs/prx/libScePsml_debug/CMakeLists.txt @@ -4,7 +4,8 @@ add_sce_psml_library(libScePsml_debug) if(BUILD_TESTING) add_test_executable(guest_psml_tests ${LIBS_INCLUDE_DIR}/tests/GuestPsml.cpp) target_include_directories(guest_psml_tests PRIVATE ${LIBS_INCLUDE_DIR}) - target_link_libraries(guest_psml_tests PRIVATE libScePsml_debug) + target_link_libraries(guest_psml_tests PRIVATE libScePsml_debug libc) + configure_windows_unwind(guest_psml_tests) add_test(NAME guest_psml COMMAND guest_psml_tests) if(WIN32) set_tests_properties(guest_psml PROPERTIES ENVIRONMENT_MODIFICATION diff --git a/core/libs/prx/libScePsml_debug/Export.cpp b/core/libs/prx/libScePsml_debug/Export.cpp index 758ceaffcf..84403c6c44 100644 --- a/core/libs/prx/libScePsml_debug/Export.cpp +++ b/core/libs/prx/libScePsml_debug/Export.cpp @@ -1,11 +1,94 @@ #include #include +#include +#include +#include +#include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" namespace { constexpr std::int32_t SCE_PSML_ERROR_NOT_INITIALIZED = static_cast(0x8A810001); +constexpr std::uint64_t MfsrBlockBytes = 0x200000; +constexpr std::uint64_t MfsrBlockCount = 4; + +struct MfsrRequirement { + std::uint64_t blockBytes; + std::uint64_t alignment; + std::uint64_t blockCount; +}; + +struct MfsrBlock { + std::uint64_t address; + std::uint64_t bytes; +}; + +struct MfsrSharedResourcesParam { + std::uint32_t type; + std::uint32_t reserved; + const MfsrBlock* blocks; + std::uint64_t blockCount; + std::uint64_t reserved2; +}; + +struct MfsrContextParam { + std::uint32_t reserved0[2]; + const MfsrBlock* blocks; + std::uint64_t blockCount; + std::uint32_t reserved1[8]; + void* sharedResources; +}; + +static_assert(sizeof(MfsrRequirement) == 0x18); +static_assert(sizeof(MfsrSharedResourcesParam) == 0x20); +static_assert(offsetof(MfsrContextParam, blocks) == 0x08); +static_assert(offsetof(MfsrContextParam, blockCount) == 0x10); +static_assert(offsetof(MfsrContextParam, sharedResources) == 0x38); + +enum class MfsrObjectKind { SharedResources, Context }; + +struct MfsrObject { + MfsrObjectKind kind; + void* sharedResources; +}; + +std::atomic g_mfsrInitialized{false}; +std::mutex g_mfsrMutex; +std::unordered_map g_mfsrObjects; + +void RequireMfsr(bool condition, const char* function, const char* what) { + if (!condition) throw std::runtime_error(std::string(function) + ": " + what); +} + +void RequireBlocks(const MfsrBlock* blocks, std::uint64_t blockCount, const char* function) { + RequireMfsr(blockCount >= MfsrBlockCount, function, "fewer memory blocks than the requirement"); + RequireMfsr(blocks != nullptr, function, "null memory block list"); + for (std::uint64_t i = 0; i < MfsrBlockCount; ++i) { + RequireMfsr(blocks[i].bytes >= MfsrBlockBytes && blocks[i].address % MfsrBlockBytes == 0, function, "memory block smaller or less aligned than the requirement"); + } +} + +void* CreateMfsrObject(MfsrObjectKind kind, void* sharedResources) { + auto* object = new MfsrObject{kind, sharedResources}; + std::lock_guard lock(g_mfsrMutex); + g_mfsrObjects.emplace(object, object); + return object; +} + +void ReleaseMfsrObject(void* handle, MfsrObjectKind kind, const char* function) { + std::lock_guard lock(g_mfsrMutex); + const auto found = g_mfsrObjects.find(handle); + RequireMfsr(found != g_mfsrObjects.end() && found->second->kind == kind, function, "handle was not created by this library"); + if (kind == MfsrObjectKind::SharedResources) { + for (const auto& [key, object] : g_mfsrObjects) { + RequireMfsr(object->sharedResources != handle, function, "shared resources still used by a context"); + } + } + delete found->second; + g_mfsrObjects.erase(found); +} } @@ -14,12 +97,14 @@ extern "C" { std::int32_t APS5_VABI scePsmlMfsrGetContextBufferRequirement1100(void* requirement, const void* param) { (void)requirement; (void)param; + if (g_mfsrInitialized) NotImplemented_nid_no_patch(__func__); return SCE_PSML_ERROR_NOT_INITIALIZED; } std::int32_t APS5_VABI scePsmlMfsrCreateContext1100(void** context, const void* param) { (void)context; (void)param; + if (g_mfsrInitialized) NotImplemented_nid_no_patch(__func__); return SCE_PSML_ERROR_NOT_INITIALIZED; } @@ -27,39 +112,84 @@ std::int32_t APS5_VABI scePsmlMfsrGetDispatchMfsrPacket1100(void* context, void* (void)context; (void)commandBuffer; (void)param; + if (g_mfsrInitialized) NotImplemented_nid_no_patch(__func__); return SCE_PSML_ERROR_NOT_INITIALIZED; } -int APS5_VABI scePsmlMfsrCreateContext1300() { - NotImplemented_nid_no_patch(__func__); +std::int32_t APS5_VABI scePsmlMfsrInit() { + g_mfsrInitialized = true; return 0; } -int APS5_VABI scePsmlMfsrGetDispatchMfsrPacket1300() { - NotImplemented_nid_no_patch(__func__); +std::int32_t APS5_VABI scePsmlMfsrGetSharedResourcesInitRequirement(MfsrRequirement* requirement, const MfsrSharedResourcesParam* param) { + if (!g_mfsrInitialized) return SCE_PSML_ERROR_NOT_INITIALIZED; + RequireMfsr(requirement != nullptr && param != nullptr, __func__, "null requirement or parameter"); + RequireMfsr(param->type == 0, __func__, ("shared resources type " + std::to_string(param->type) + " is not supported").c_str()); + *requirement = {MfsrBlockBytes, MfsrBlockBytes, MfsrBlockCount}; return 0; } -int APS5_VABI scePsmlMfsrGetSharedResourcesInitRequirement() { - NotImplemented_nid_no_patch(__func__); +std::int32_t APS5_VABI scePsmlMfsrCreateSharedResources(void** sharedResources, const MfsrSharedResourcesParam* param) { + if (!g_mfsrInitialized) return SCE_PSML_ERROR_NOT_INITIALIZED; + RequireMfsr(sharedResources != nullptr && param != nullptr, __func__, "null handle or parameter"); + RequireMfsr(param->type == 0, __func__, ("shared resources type " + std::to_string(param->type) + " is not supported").c_str()); + RequireBlocks(param->blocks, param->blockCount, __func__); + *sharedResources = CreateMfsrObject(MfsrObjectKind::SharedResources, nullptr); return 0; } -int APS5_VABI scePsmlMfsrInit() { - NotImplemented_nid_no_patch(__func__); +std::int32_t APS5_VABI scePsmlMfsrReleaseSharedResources(void* sharedResources) { + if (!g_mfsrInitialized) return SCE_PSML_ERROR_NOT_INITIALIZED; + ReleaseMfsrObject(sharedResources, MfsrObjectKind::SharedResources, __func__); return 0; } +std::int32_t APS5_VABI scePsmlMfsrGetContextBufferRequirement800M3_2(MfsrRequirement* requirement, const MfsrContextParam* param) { + if (!g_mfsrInitialized) return SCE_PSML_ERROR_NOT_INITIALIZED; + RequireMfsr(requirement != nullptr && param != nullptr, __func__, "null requirement or parameter"); + *requirement = {MfsrBlockBytes, MfsrBlockBytes, MfsrBlockCount}; + return 0; +} + +std::int32_t APS5_VABI scePsmlMfsrCreateContext800M3_2(void** context, const MfsrContextParam* param) { + if (!g_mfsrInitialized) return SCE_PSML_ERROR_NOT_INITIALIZED; + RequireMfsr(context != nullptr && param != nullptr, __func__, "null handle or parameter"); + RequireBlocks(param->blocks, param->blockCount, __func__); + { + std::lock_guard lock(g_mfsrMutex); + const auto found = g_mfsrObjects.find(param->sharedResources); + RequireMfsr(found != g_mfsrObjects.end() && found->second->kind == MfsrObjectKind::SharedResources, __func__, "shared resources handle was not created by this library"); + } + *context = CreateMfsrObject(MfsrObjectKind::Context, param->sharedResources); + return 0; +} + +std::int32_t APS5_VABI scePsmlMfsrReleaseContext(void* context) { + if (!g_mfsrInitialized) return SCE_PSML_ERROR_NOT_INITIALIZED; + ReleaseMfsrObject(context, MfsrObjectKind::Context, __func__); + return 0; +} + +std::int32_t APS5_VABI scePsmlMfsrCreateContext1300(void** context, const void* param) { + (void)context; + (void)param; + if (g_mfsrInitialized) NotImplemented_nid_no_patch(__func__); + return SCE_PSML_ERROR_NOT_INITIALIZED; +} + +std::int32_t APS5_VABI scePsmlMfsrGetDispatchMfsrPacket1300(void* context, void* commandBuffer, const void* param) { + (void)context; + (void)commandBuffer; + (void)param; + if (g_mfsrInitialized) NotImplemented_nid_no_patch(__func__); + return SCE_PSML_ERROR_NOT_INITIALIZED; +} + int APS5_VABI scePsmlMfsrGetDispatchMfsrPacketSizeInDwords() { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI scePsmlMfsrGetContextBufferRequirement800M3_2() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI scePsmlMfsrSelectConfig() { NotImplemented_nid_no_patch(__func__); return 0; @@ -75,39 +205,25 @@ int APS5_VABI scePsmlMfsrRequestCapture() { return 0; } -int APS5_VABI scePsmlMfsrReleaseContext() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI scePsmlMfsrIsCaptureInProgress() { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI scePsmlMfsrGetDispatchMfsrPacket1000() { - NotImplemented_nid_no_patch(__func__); - return 0; +std::int32_t APS5_VABI scePsmlMfsrGetDispatchMfsrPacket1000(void* context, void* commandBuffer, const void* param) { + (void)context; + (void)commandBuffer; + (void)param; + if (g_mfsrInitialized) NotImplemented_nid_no_patch(__func__); + return SCE_PSML_ERROR_NOT_INITIALIZED; } -int APS5_VABI scePsmlMfsrGetDispatchMfsrPacket900() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI scePsmlMfsrCreateSharedResources() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI scePsmlMfsrCreateContext800M3_2() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI scePsmlMfsrReleaseSharedResources() { - NotImplemented_nid_no_patch(__func__); - return 0; +std::int32_t APS5_VABI scePsmlMfsrGetDispatchMfsrPacket900(void* context, void* commandBuffer, const void* param) { + (void)context; + (void)commandBuffer; + (void)param; + if (g_mfsrInitialized) NotImplemented_nid_no_patch(__func__); + return SCE_PSML_ERROR_NOT_INITIALIZED; } int APS5_VABI scePsmlMfsr2ReleaseSharedResources() { diff --git a/core/libs/tests/GuestPsml.cpp b/core/libs/tests/GuestPsml.cpp index d0aac1d174..c545232c9b 100644 --- a/core/libs/tests/GuestPsml.cpp +++ b/core/libs/tests/GuestPsml.cpp @@ -2,11 +2,25 @@ #include #include #include +#include +#include +#include extern "C" { std::int32_t APS5_VABI scePsmlMfsrGetContextBufferRequirement1100(void* requirement, const void* param); std::int32_t APS5_VABI scePsmlMfsrCreateContext1100(void** context, const void* param); std::int32_t APS5_VABI scePsmlMfsrGetDispatchMfsrPacket1100(void* context, void* commandBuffer, const void* param); +std::int32_t APS5_VABI scePsmlMfsrGetContextBufferRequirement800M3_2(void* requirement, const void* param); +std::int32_t APS5_VABI scePsmlMfsrCreateContext800M3_2(void** context, const void* param); +std::int32_t APS5_VABI scePsmlMfsrCreateContext1300(void** context, const void* param); +std::int32_t APS5_VABI scePsmlMfsrGetDispatchMfsrPacket900(void* context, void* commandBuffer, const void* param); +std::int32_t APS5_VABI scePsmlMfsrGetDispatchMfsrPacket1000(void* context, void* commandBuffer, const void* param); +std::int32_t APS5_VABI scePsmlMfsrGetDispatchMfsrPacket1300(void* context, void* commandBuffer, const void* param); +std::int32_t APS5_VABI scePsmlMfsrInit(); +std::int32_t APS5_VABI scePsmlMfsrGetSharedResourcesInitRequirement(void* requirement, const void* param); +std::int32_t APS5_VABI scePsmlMfsrCreateSharedResources(void** sharedResources, const void* param); +std::int32_t APS5_VABI scePsmlMfsrReleaseSharedResources(void* sharedResources); +std::int32_t APS5_VABI scePsmlMfsrReleaseContext(void* context); } namespace { @@ -20,6 +34,164 @@ struct CommandBuffer { std::uint32_t sizeInDwords; }; +using CreateContext = std::int32_t (APS5_VABI*)(void**, const void*); +using GetDispatchPacket = std::int32_t (APS5_VABI*)(void*, void*, const void*); + +void CreateContextUntouched(CreateContext create) { + std::array param{}; + std::uint64_t contextStorage = 0; + void* const sentinel = &contextStorage; + void* context = sentinel; + Require(create(&context, param.data()) == kErrNotInitialized); + Require(context == sentinel); + Require(create(nullptr, nullptr) == kErrNotInitialized); +} + +void DispatchPacketUntouched(GetDispatchPacket dispatch) { + std::array param{}; + std::uint64_t contextStorage = 0; + std::array dwords; + dwords.fill(0xa5a5a5a5u); + const auto untouchedDwords = dwords; + CommandBuffer commandBuffer{dwords.data(), static_cast(dwords.size())}; + Require(dispatch(&contextStorage, &commandBuffer, param.data()) == kErrNotInitialized); + Require(dispatch(nullptr, &commandBuffer, nullptr) == kErrNotInitialized); + Require(commandBuffer.cursor == dwords.data()); + Require(commandBuffer.sizeInDwords == dwords.size()); + Require(dwords == untouchedDwords); +} + +template +bool Throws(TCall call, const char* text) { + try { + call(); + } catch (const std::runtime_error& error) { + return std::string(error.what()).find(text) != std::string::npos; + } + return false; +} + +struct Requirement { + std::uint64_t blockBytes; + std::uint64_t alignment; + std::uint64_t blockCount; +}; + +struct Block { + std::uint64_t address; + std::uint64_t bytes; +}; + +struct SharedParam { + std::uint32_t type; + std::uint32_t reserved; + const Block* blocks; + std::uint64_t blockCount; + std::uint64_t reserved2; +}; + +struct ContextParam { + std::array bytes{}; + + void Set(const Block* blocks, std::uint64_t count, void* shared) { + std::memcpy(bytes.data() + 0x08, &blocks, sizeof(blocks)); + std::memcpy(bytes.data() + 0x10, &count, sizeof(count)); + std::memcpy(bytes.data() + 0x38, &shared, sizeof(shared)); + } +}; + +std::array Blocks(const Requirement& requirement, std::uint64_t first) { + std::array blocks{}; + for (std::size_t i = 0; i < blocks.size(); ++i) blocks[i] = {first + i * requirement.blockBytes, requirement.blockBytes}; + return blocks; +} + +void SetupBeforeInit() { + std::uint64_t storage = 0; + void* const sentinel = &storage; + void* handle = sentinel; + Requirement requirement{1, 2, 3}; + SharedParam sharedParam{}; + Require(scePsmlMfsrGetSharedResourcesInitRequirement(&requirement, &sharedParam) == kErrNotInitialized); + Require(requirement.blockBytes == 1 && requirement.alignment == 2 && requirement.blockCount == 3); + Require(scePsmlMfsrCreateSharedResources(&handle, &sharedParam) == kErrNotInitialized); + Require(handle == sentinel); + Require(scePsmlMfsrReleaseContext(sentinel) == kErrNotInitialized); + Require(scePsmlMfsrReleaseSharedResources(sentinel) == kErrNotInitialized); +} + +void SetupAfterInit() { + Require(scePsmlMfsrInit() == 0); + Require(scePsmlMfsrInit() == 0); + + SharedParam sharedParam{}; + Requirement shared{}; + Require(scePsmlMfsrGetSharedResourcesInitRequirement(&shared, &sharedParam) == 0); + Require(shared.blockBytes == 0x200000 && shared.alignment == 0x200000 && shared.blockCount == 4); + + const auto sharedBlocks = Blocks(shared, 0x10000000); + sharedParam.blocks = sharedBlocks.data(); + sharedParam.blockCount = sharedBlocks.size(); + void* sharedResources = nullptr; + Require(scePsmlMfsrCreateSharedResources(&sharedResources, &sharedParam) == 0); + Require(sharedResources != nullptr); + + ContextParam contextParam; + contextParam.Set(nullptr, 0, sharedResources); + Requirement context{}; + Require(scePsmlMfsrGetContextBufferRequirement800M3_2(&context, contextParam.bytes.data()) == 0); + Require(context.blockBytes == 0x200000 && context.alignment == 0x200000 && context.blockCount == 4); + + const auto contextBlocks = Blocks(context, 0x20000000); + contextParam.Set(contextBlocks.data(), contextBlocks.size(), sharedResources); + void* mfsrContext = nullptr; + Require(scePsmlMfsrCreateContext800M3_2(&mfsrContext, contextParam.bytes.data()) == 0); + Require(mfsrContext != nullptr && mfsrContext != sharedResources); + + Require(Throws([&] { scePsmlMfsrReleaseSharedResources(sharedResources); }, "still used by a context")); + Require(Throws([&] { scePsmlMfsrReleaseContext(sharedResources); }, "not created by this library")); + Require(scePsmlMfsrReleaseContext(mfsrContext) == 0); + Require(Throws([&] { scePsmlMfsrReleaseContext(mfsrContext); }, "not created by this library")); + Require(scePsmlMfsrReleaseSharedResources(sharedResources) == 0); +} + +void RefusedAfterInit() { + SharedParam sharedParam{}; + Requirement requirement{}; + sharedParam.type = 1; + Require(Throws([&] { scePsmlMfsrGetSharedResourcesInitRequirement(&requirement, &sharedParam); }, "type 1 is not supported")); + + sharedParam.type = 0; + Require(scePsmlMfsrGetSharedResourcesInitRequirement(&requirement, &sharedParam) == 0); + const auto blocks = Blocks(requirement, 0x10000000); + sharedParam.blocks = blocks.data(); + sharedParam.blockCount = 3; + void* sharedResources = nullptr; + Require(Throws([&] { scePsmlMfsrCreateSharedResources(&sharedResources, &sharedParam); }, "fewer memory blocks")); + auto small = blocks; + small[2].bytes = 0x100000; + sharedParam.blocks = small.data(); + sharedParam.blockCount = small.size(); + Require(Throws([&] { scePsmlMfsrCreateSharedResources(&sharedResources, &sharedParam); }, "smaller or less aligned")); + Require(sharedResources == nullptr); + + std::uint64_t foreign = 0; + ContextParam contextParam; + contextParam.Set(blocks.data(), blocks.size(), &foreign); + void* context = nullptr; + Require(Throws([&] { scePsmlMfsrCreateContext800M3_2(&context, contextParam.bytes.data()); }, "shared resources handle was not created")); + Require(context == nullptr); + + std::array param{}; + std::array dwords{}; + CommandBuffer commandBuffer{dwords.data(), static_cast(dwords.size())}; + Require(Throws([&] { scePsmlMfsrGetContextBufferRequirement1100(&requirement, param.data()); }, "not implemented")); + Require(Throws([&] { scePsmlMfsrCreateContext1300(&context, param.data()); }, "not implemented")); + Require(Throws([&] { scePsmlMfsrGetDispatchMfsrPacket900(&foreign, &commandBuffer, param.data()); }, "not implemented")); + Require(Throws([&] { scePsmlMfsrGetDispatchMfsrPacket1000(&foreign, &commandBuffer, param.data()); }, "not implemented")); + Require(Throws([&] { scePsmlMfsrGetDispatchMfsrPacket1300(&foreign, &commandBuffer, param.data()); }, "not implemented")); +} + } int main() { @@ -50,4 +222,20 @@ int main() { Require(scePsmlMfsrGetDispatchMfsrPacket1100(nullptr, &commandBuffer, param.data()) == kErrNotInitialized); Require(commandBuffer.cursor == dwords.data()); Require(dwords == untouchedDwords); + + std::array olderRequirement; + olderRequirement.fill(0xa5); + const auto untouchedOlderRequirement = olderRequirement; + Require(scePsmlMfsrGetContextBufferRequirement800M3_2(olderRequirement.data(), param.data()) == kErrNotInitialized); + Require(olderRequirement == untouchedOlderRequirement); + Require(scePsmlMfsrGetContextBufferRequirement800M3_2(nullptr, nullptr) == kErrNotInitialized); + + CreateContextUntouched(scePsmlMfsrCreateContext800M3_2); + CreateContextUntouched(scePsmlMfsrCreateContext1300); + DispatchPacketUntouched(scePsmlMfsrGetDispatchMfsrPacket900); + DispatchPacketUntouched(scePsmlMfsrGetDispatchMfsrPacket1000); + DispatchPacketUntouched(scePsmlMfsrGetDispatchMfsrPacket1300); + SetupBeforeInit(); + SetupAfterInit(); + RefusedAfterInit(); } diff --git a/docs/dev/debt/libScePsml_debug.md b/docs/dev/debt/libScePsml_debug.md index a24b3c1e9a..76568190aa 100644 --- a/docs/dev/debt/libScePsml_debug.md +++ b/docs/dev/debt/libScePsml_debug.md @@ -2,8 +2,13 @@ ### Unknown function info +- [scePsmlMfsrGetSharedResourcesInitRequirement, scePsmlMfsrCreateSharedResources, scePsmlMfsrReleaseSharedResources, scePsmlMfsrGetContextBufferRequirement800M3_2, scePsmlMfsrCreateContext800M3_2, scePsmlMfsrReleaseContext](../../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml) - signatures from the calls in PPSA08330 (0x5724b6-0x5727e8, 0xc8697) and PPSA03016 (0x43dcf4d-0x43dd69a): a `{block bytes, alignment, block count}` requirement, a 32-byte shared-resources parameter with the type at 0 and the `{address, bytes}` block list at 8 and 16, a context parameter with the block list at 8 and 16 and the shared-resources handle at 0x38, and 8-byte handles written through the first argument and passed back by value. The other context parameter fields (a 16-bit size pair at 0x18, two descriptor pointers at 0x20 and 0x28) are not read. The requirement of four 2 MiB blocks, 2 MiB aligned, is what PPSA08330 v06.006 allocates regardless of the returned count; Sony's values are unknown ([KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/9e41d8320a2774b303fbad344c2347e8d0292e83/src/libs/libPsml.cpp) derives 52 blocks for type 0). Only type 0 is accepted. The error codes for null pointers, short block lists and foreign handles are unknown, so those calls throw - [scePsmlMfsr2CreateContext, scePsmlMfsr2CreateSharedResources, scePsmlMfsr2GetContextInitRequirement, scePsmlMfsr2GetDispatchPackets, scePsmlMfsr2GetDispatchPacketsSizeInDwords, scePsmlMfsr2GetSharedResourcesInitRequirement, scePsmlMfsr2Init, scePsmlMfsr2ReleaseContext, scePsmlMfsr2ReleaseSharedResources](../../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml_mfsr2) - unknown signatures, imported by PPSA23566; declared without parameters -- [scePsmlMfsrGetContextBufferRequirement1100, scePsmlMfsrCreateContext1100, scePsmlMfsrGetDispatchMfsrPacket1100](../../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml_debug) - argument layouts unknown; PPSA26344 imports but does not call the first two; at `0x87EFA` it passes `(rdi, rsi=&stack, rdx)` as three pointers to `GetDispatchMfsrPacket1100`. Only 0x8A810001 (not initialized) is returned +- [scePsmlMfsrGetContextBufferRequirement1100, scePsmlMfsrCreateContext1100, scePsmlMfsrGetDispatchMfsrPacket1100](../../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml_debug) - argument layouts unknown; PPSA26344 imports but does not call the first two and at `0x87EFA` passes `(rdi, rsi=&stack, rdx)` as three pointers to `GetDispatchMfsrPacket1100`; before `scePsmlMfsrInit` they return 0x8A810001 (not initialized) and after it they throw. `scePsmlMfsrCreateContext1300` and `scePsmlMfsrGetDispatchMfsrPacket900`, `1000` and `1300` take the same shapes (context slot and parameter; context, command buffer and parameter) and behave the same way. PPSA08330 and PPSA03016 pass `GetDispatchMfsrPacket900`/`1000` the context handle, a `{cursor, size in dwords}` command buffer and the dispatch parameters, and advance their cursor by the size `scePsmlMfsrGetDispatchMfsrPacketSizeInDwords` returned. Whether the 1100 and 1300 versions take these arguments, and the console's error, are unverified +- [scePsmlMfsrGetDispatchMfsrPacket1000](../../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml) - imported by PPSA25344. libScePsml and libScePsml_debug are built from the same source +- [scePsmlMfsrCreateContext1300, scePsmlMfsrGetDispatchMfsrPacket1300](../../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml) - imported by PPSA28416 next to `scePsmlMfsrGetContextBufferRequirement1100`. The names are not in the aerolib NID list; they hash to the imported NIDs `kLF7WtQ1-Dg` and `DDVUyPyYHTA` (`tools/nid_names.py`) - [scePsmlMfsrEnableSpecularAA, scePsmlMfsrEnableQuality8K](../../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml_specularAA and libScePsml_quality8K) - imported by PPSA01317, which fails to load without them; signatures unknown, so they throw. The names are not in the aerolib NID list; they hash to the imported NIDs `xNZM9Y5lf-M` and `lcZD9tJraAw` (`tools/nid_names.py`) -- [scePsmlMfsrGetDispatchMfsrPacket1000](../../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml) - unknown signature, imported by PPSA25344; declared without parameters. libScePsml and libScePsml_debug are built from the same source -- [scePsmlMfsrCreateContext1300, scePsmlMfsrGetDispatchMfsrPacket1300](../../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml) - unknown signatures, imported by PPSA28416 next to `scePsmlMfsrGetContextBufferRequirement1100`; declared without parameters. The names are not in the aerolib NID list; they hash to the imported NIDs `kLF7WtQ1-Dg` and `DDVUyPyYHTA` (`tools/nid_names.py`) + +### Functional + +- [scePsmlMfsrInit](../../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml) - succeeds and only sets the initialized state; the shared resources and contexts are bookkeeping objects that use none of the guest blocks. Whether Sony's library fails on hardware without PSSR is unknown. PPSA08330 calls the setup unconditionally and only uses a context when its cached `sceKernelIsTrinityMode` result is set (0x3c643d, 0x567f97; otherwise it stores a -0.25 mip bias itself at 0x567fe2), which AnyPS5 reports as 0, so the dispatch, mip bias and capture calls still throw From 46fdfec8521b6746ada6f08881946410c720eede Mon Sep 17 00:00:00 2001 From: ps54all Date: Thu, 15 Oct 2026 14:21:17 +0200 Subject: [PATCH 0554/1000] fix(psml): check the shared resources and add the context under one lock A concurrent scePsmlMfsrReleaseSharedResources could pass its "still used by a context" check between the handle lookup and the context insertion. The technical debt entry now says how KytyPS5's layout differs from the calls in PPSA08330 and PPSA03016. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libScePsml_debug/Export.cpp | 17 ++++++++--------- docs/dev/debt/libScePsml_debug.md | 2 +- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/core/libs/prx/libScePsml_debug/Export.cpp b/core/libs/prx/libScePsml_debug/Export.cpp index 84403c6c44..e8cc8f8b7a 100644 --- a/core/libs/prx/libScePsml_debug/Export.cpp +++ b/core/libs/prx/libScePsml_debug/Export.cpp @@ -70,9 +70,13 @@ void RequireBlocks(const MfsrBlock* blocks, std::uint64_t blockCount, const char } } -void* CreateMfsrObject(MfsrObjectKind kind, void* sharedResources) { - auto* object = new MfsrObject{kind, sharedResources}; +void* CreateMfsrObject(MfsrObjectKind kind, void* sharedResources, const char* function) { std::lock_guard lock(g_mfsrMutex); + if (kind == MfsrObjectKind::Context) { + const auto found = g_mfsrObjects.find(sharedResources); + RequireMfsr(found != g_mfsrObjects.end() && found->second->kind == MfsrObjectKind::SharedResources, function, "shared resources handle was not created by this library"); + } + auto* object = new MfsrObject{kind, sharedResources}; g_mfsrObjects.emplace(object, object); return object; } @@ -134,7 +138,7 @@ std::int32_t APS5_VABI scePsmlMfsrCreateSharedResources(void** sharedResources, RequireMfsr(sharedResources != nullptr && param != nullptr, __func__, "null handle or parameter"); RequireMfsr(param->type == 0, __func__, ("shared resources type " + std::to_string(param->type) + " is not supported").c_str()); RequireBlocks(param->blocks, param->blockCount, __func__); - *sharedResources = CreateMfsrObject(MfsrObjectKind::SharedResources, nullptr); + *sharedResources = CreateMfsrObject(MfsrObjectKind::SharedResources, nullptr, __func__); return 0; } @@ -155,12 +159,7 @@ std::int32_t APS5_VABI scePsmlMfsrCreateContext800M3_2(void** context, const Mfs if (!g_mfsrInitialized) return SCE_PSML_ERROR_NOT_INITIALIZED; RequireMfsr(context != nullptr && param != nullptr, __func__, "null handle or parameter"); RequireBlocks(param->blocks, param->blockCount, __func__); - { - std::lock_guard lock(g_mfsrMutex); - const auto found = g_mfsrObjects.find(param->sharedResources); - RequireMfsr(found != g_mfsrObjects.end() && found->second->kind == MfsrObjectKind::SharedResources, __func__, "shared resources handle was not created by this library"); - } - *context = CreateMfsrObject(MfsrObjectKind::Context, param->sharedResources); + *context = CreateMfsrObject(MfsrObjectKind::Context, param->sharedResources, __func__); return 0; } diff --git a/docs/dev/debt/libScePsml_debug.md b/docs/dev/debt/libScePsml_debug.md index 76568190aa..d30a44b812 100644 --- a/docs/dev/debt/libScePsml_debug.md +++ b/docs/dev/debt/libScePsml_debug.md @@ -2,7 +2,7 @@ ### Unknown function info -- [scePsmlMfsrGetSharedResourcesInitRequirement, scePsmlMfsrCreateSharedResources, scePsmlMfsrReleaseSharedResources, scePsmlMfsrGetContextBufferRequirement800M3_2, scePsmlMfsrCreateContext800M3_2, scePsmlMfsrReleaseContext](../../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml) - signatures from the calls in PPSA08330 (0x5724b6-0x5727e8, 0xc8697) and PPSA03016 (0x43dcf4d-0x43dd69a): a `{block bytes, alignment, block count}` requirement, a 32-byte shared-resources parameter with the type at 0 and the `{address, bytes}` block list at 8 and 16, a context parameter with the block list at 8 and 16 and the shared-resources handle at 0x38, and 8-byte handles written through the first argument and passed back by value. The other context parameter fields (a 16-bit size pair at 0x18, two descriptor pointers at 0x20 and 0x28) are not read. The requirement of four 2 MiB blocks, 2 MiB aligned, is what PPSA08330 v06.006 allocates regardless of the returned count; Sony's values are unknown ([KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/9e41d8320a2774b303fbad344c2347e8d0292e83/src/libs/libPsml.cpp) derives 52 blocks for type 0). Only type 0 is accepted. The error codes for null pointers, short block lists and foreign handles are unknown, so those calls throw +- [scePsmlMfsrGetSharedResourcesInitRequirement, scePsmlMfsrCreateSharedResources, scePsmlMfsrReleaseSharedResources, scePsmlMfsrGetContextBufferRequirement800M3_2, scePsmlMfsrCreateContext800M3_2, scePsmlMfsrReleaseContext](../../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml) - signatures from the calls in PPSA08330 (0x5724b6-0x5727e8, 0xc8697) and PPSA03016 (0x43dcf4d-0x43dd69a): a `{block bytes, alignment, block count}` requirement, a 32-byte shared-resources parameter with the type at 0 and the `{address, bytes}` block list at 8 and 16, a context parameter with the block list at 8 and 16 and the shared-resources handle at 0x38, and 8-byte handles written through the first argument and passed back by value. The other context parameter fields (a 16-bit size pair at 0x18, two descriptor pointers at 0x20 and 0x28) are not read. The requirement of four 2 MiB blocks, 2 MiB aligned, is what PPSA08330 v06.006 allocates regardless of the returned count; Sony's values are unknown ([KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/9e41d8320a2774b303fbad344c2347e8d0292e83/src/libs/libPsml.cpp) derives 52 blocks for type 0; its layout differs from these calls: it writes a 0x30-byte object through the first argument, where PPSA08330 passes one of two adjacent 8-byte handle globals, and reads the shared resources at context parameter offset 8, where both titles pass the block list). Only type 0 is accepted. The error codes for null pointers, short block lists and foreign handles are unknown, so those calls throw - [scePsmlMfsr2CreateContext, scePsmlMfsr2CreateSharedResources, scePsmlMfsr2GetContextInitRequirement, scePsmlMfsr2GetDispatchPackets, scePsmlMfsr2GetDispatchPacketsSizeInDwords, scePsmlMfsr2GetSharedResourcesInitRequirement, scePsmlMfsr2Init, scePsmlMfsr2ReleaseContext, scePsmlMfsr2ReleaseSharedResources](../../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml_mfsr2) - unknown signatures, imported by PPSA23566; declared without parameters - [scePsmlMfsrGetContextBufferRequirement1100, scePsmlMfsrCreateContext1100, scePsmlMfsrGetDispatchMfsrPacket1100](../../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml_debug) - argument layouts unknown; PPSA26344 imports but does not call the first two and at `0x87EFA` passes `(rdi, rsi=&stack, rdx)` as three pointers to `GetDispatchMfsrPacket1100`; before `scePsmlMfsrInit` they return 0x8A810001 (not initialized) and after it they throw. `scePsmlMfsrCreateContext1300` and `scePsmlMfsrGetDispatchMfsrPacket900`, `1000` and `1300` take the same shapes (context slot and parameter; context, command buffer and parameter) and behave the same way. PPSA08330 and PPSA03016 pass `GetDispatchMfsrPacket900`/`1000` the context handle, a `{cursor, size in dwords}` command buffer and the dispatch parameters, and advance their cursor by the size `scePsmlMfsrGetDispatchMfsrPacketSizeInDwords` returned. Whether the 1100 and 1300 versions take these arguments, and the console's error, are unverified - [scePsmlMfsrGetDispatchMfsrPacket1000](../../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml) - imported by PPSA25344. libScePsml and libScePsml_debug are built from the same source From 88ee391839405a8194c851e1b917116cf7be4225 Mon Sep 17 00:00:00 2001 From: thibaut Date: Fri, 16 Oct 2026 20:42:45 +0200 Subject: [PATCH 0555/1000] perf(agc): linear color targets over imported memory get a resident image A linear color target took a synchronous draw (copy in, draw, copy out, CPU wait) on every draw. When its memory already has a host import, it now gets a resident image like tiled targets, and keepRecordedDraw stores its results back to guest memory right after the draw with a recorded GPU store (FlushPending), so CPU readers see them as before, without the wait. No import is created for it (a lookup only): memory without one keeps the synchronous path. APS5_NO_RESIDENT_LINEAR=1 restores the old path. GTA V Story (RTX 4090, upstream main eb901bcf4 + #6488): 14.64 -> 16.47 FPS. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01X5rdnz9ZTN3ndFVUfizSvj --- .../Graphics/include/GuestBufferMemory.hpp | 1 + .../prx/libSceAgcDriver/Graphics/src/Draw.cpp | 8 +- .../Graphics/src/GuestBufferMemory.cpp | 8 + .../libSceAgcDriver/Graphics/src/State.cpp | 3 +- .../tests/execution/LinearTargetResident.cpp | 268 ++++++++++++++++++ 5 files changed, 286 insertions(+), 2 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/LinearTargetResident.cpp diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp index 7f932aad60..3a4325cfa7 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp @@ -66,6 +66,7 @@ void SetImportWatch(const Context& context, ImportWatch watch); // (alignment and budget permitting), or null. Bytes at `address` are at `address - import->base` in // the import's buffer. const HostImport* HostImportFor(const Context& context, std::uint64_t address, std::size_t bytes); +const HostImport* ExistingHostImportFor(const Context& context, std::uint64_t address, std::size_t bytes); void SetHostImportWindow(std::uint64_t bytes); // Tests: the window of one large registered range (64 MiB) and the import budget (0: the configured one). void SetHostImportRangeWindow(std::uint64_t bytes); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index c3ad0dcbff..4d7634770a 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -1548,6 +1548,10 @@ void keepRecordedDraw(Recorder& recorder, const std::shared_ptr if (index < slices.size()) residents[index]->MarkSliceDirty(slices[index]); else residents[index]->MarkDirty(); } + for (const auto& resident : residents) { + if (resident == nullptr || resident->Descriptor().tileMode != TextureTileMode::kLinear) continue; + StorageTexture::FlushPending(resident->Descriptor().baseAddress, static_cast(resident->GuestBytes()), nullptr, "linear target"); + } } // What recordDraw records from: the recorder, the resources, the pipeline and framebuffer, the @@ -2004,7 +2008,9 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& timer.phase(PhaseSetup); // Debug aid: APS5_NO_RESIDENT_TARGETS=1 copies every target in and out again. static const bool residentTargets = std::getenv("APS5_NO_RESIDENT_TARGETS") == nullptr; - if ((binding.gpuTiling || (color.tileMode != ColorTileMode::Linear && context.detiler != nullptr)) && residentTargets) { + static const bool residentLinear = std::getenv("APS5_NO_RESIDENT_LINEAR") == nullptr; + const bool linearResident = residentLinear && color.tileMode == ColorTileMode::Linear && ExistingHostImportFor(context, color.address, colorLayout.Bytes()) != nullptr; + if ((binding.gpuTiling || ((color.tileMode != ColorTileMode::Linear || linearResident) && context.detiler != nullptr)) && residentTargets) { // The lookup refreshes the image on every draw (StorageTexture::Refresh: FlushPending, // CollectWrites over the target's pages, the DCC key scan of TextureClearKeys, then // UnchangedSince). The page walk is skipped while the worker's collect epoch lasts diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index 3aa4c9b378..9d2b629050 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -2574,6 +2574,14 @@ const HostImport* HostImportFor(const Context& context, std::uint64_t address, s return importAccess(context, state, address, address + bytes, lease); } +const HostImport* ExistingHostImportFor(const Context& context, std::uint64_t address, std::size_t bytes) { + if (context.hostImportAlignment == 0 || bytes == 0 || bytes > std::numeric_limits::max() - address) return nullptr; + auto& state = Imports(); + std::lock_guard lock(state.mutex); + if (importsStale(context, state)) return nullptr; + return findImport(state, address, address + bytes); +} + bool ImportMappedRanges(const Context& context, const GuestAllocations::Mapped& ranges, std::uint64_t generation, bool adoptDevice) { if (context.hostImportAlignment == 0) return true; ensurePinWaiter(); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index cd64ea4aff..5af9c104fe 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -1373,7 +1373,8 @@ std::uint32_t GuestFormatFor(VkFormat format, std::uint32_t elementBytes) { } GuestTextureResource SurfaceForTarget(const ColorTarget& color) { - Require(color.tileMode != ColorTileMode::Linear, "linear color targets are not resident"); + static const bool residentLinear = std::getenv("APS5_NO_RESIDENT_LINEAR") == nullptr; + Require(residentLinear || color.tileMode != ColorTileMode::Linear, "linear color targets are not resident"); const bool chain = color.mipCount > 1; GuestTextureResource surface{}; surface.baseAddress = chain ? color.surfaceAddress : color.address; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/LinearTargetResident.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/LinearTargetResident.cpp new file mode 100644 index 0000000000..993358e4c7 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/LinearTargetResident.cpp @@ -0,0 +1,268 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" +#include "prx/libSceAgcDriver/Graphics/include/State.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace AgcDriver { + +struct VulkanDeviceTestAccess { + static Graphics::Context ContextOf(const VulkanDevice& device) { return device.graphicsContext(); } +}; + +} + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Width = 64; +constexpr std::uint32_t Height = 16; +constexpr std::size_t BlockBytes = 65536; +constexpr std::uint32_t Rgba32FloatInfo = 0x8738u; +constexpr std::uint32_t LinearAttrib3 = 0x09000000u; +constexpr std::uint32_t Float32AbgrExport = 9; +constexpr std::uint32_t NoInput = 0xffffffffu; + +alignas(256) constexpr std::array VertexCode{ + 0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, +}; + +constexpr std::array, 3> Triangle{{ + {-1.0f, -1.0f, 0.5f, 1.0f}, {3.0f, -1.0f, 0.5f, 1.0f}, {-1.0f, 3.0f, 0.5f, 1.0f} +}}; + +alignas(256) std::array, 4> Programs{}; +std::size_t programCount = 0; + +std::array BufferDescriptor(const void* data, std::uint32_t stride, std::uint32_t count) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (stride << 16u), count, 0x01016facu}; +} + +std::span Program(std::uint32_t fixedVgpr, std::uint32_t stippleVgpr) { + Require(programCount < Programs.size(), "too many test programs"); + auto& code = Programs[programCount++]; + std::size_t at = 0; + code[at++] = 0x36000000u | (20u << 17u) | (fixedVgpr << 9u) | 0xffu; + code[at++] = 0x0000ffffu; + code[at++] = 0x2c000000u | (21u << 17u) | (fixedVgpr << 9u) | 0x90u; + code[at++] = 0x7e000c00u | (20u << 17u) | (256u + 20u); + code[at++] = 0x7e000c00u | (21u << 17u) | (256u + 21u); + code[at++] = stippleVgpr == NoInput ? 0x7e000280u | (22u << 17u) : 0x7e000c00u | (22u << 17u) | (256u + stippleVgpr); + code[at++] = 0x7e0002f2u | (23u << 17u); + code[at++] = 0xf800180fu; + code[at++] = 0x17161514u; + code[at++] = 0xbf810000u; + return std::span(code.data(), at); +} + +struct Block { + explicit Block(std::size_t bytes) : bytes(bytes), watched(AgcDriver::GuestMemory::WriteWatched()) { +#ifdef _WIN32 + if (watched) { + data = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(bytes, BlockBytes)); + GuestArena::GuestArenaCommit_nid_postfix(data, bytes, PAGE_READWRITE, bytes); + } else { + data = static_cast(VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); + } +#else + if (watched) { + void* raw = mmap(nullptr, bytes + BlockBytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + Require(raw != MAP_FAILED, "cannot map the color target"); + const auto begin = reinterpret_cast(raw); + const auto aligned = (begin + BlockBytes - 1) & ~(static_cast(BlockBytes) - 1); + if (aligned != begin) munmap(raw, aligned - begin); + if (aligned + bytes != begin + bytes + BlockBytes) munmap(reinterpret_cast(aligned + bytes), begin + BlockBytes - aligned); + data = reinterpret_cast(aligned); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(data, bytes); + } else { + data = static_cast(std::aligned_alloc(BlockBytes, bytes)); + } +#endif + Require(data != nullptr, "cannot allocate the color target"); + Require(!watched || AgcDriver::GuestMemory::Watched(Address(), bytes), "the color target is not write-watched"); + GuestAllocations::Mutation mutation; + mutation.Add(data, bytes, true, true, true); + } + ~Block() { + AgcDriver::Graphics::StorageTexture::FlushPending(Address(), bytes, nullptr, "test release"); + { + GuestAllocations::Mutation mutation; + mutation.Remove(data); + } +#ifdef _WIN32 + if (watched) { + GuestArena::GuestArenaReset_nid_postfix(data, bytes); + GuestArena::GuestArenaRelease_nid_postfix(data, bytes); + } else { + VirtualFree(data, 0, MEM_RELEASE); + } +#else + if (watched) { + munmap(data, bytes); + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(data, bytes); + } else { + std::free(data); + } +#endif + } + Block(const Block&) = delete; + Block& operator=(const Block&) = delete; + std::uint64_t Address() const { return reinterpret_cast(data); } + std::size_t bytes; + bool watched; + std::uint8_t* data = nullptr; +}; + +AgcDriver::Graphics::ColorTarget DecodeTarget(const Block& block, std::uint32_t info) { + AgcDriver::Registers cx; + cx[0x318] = static_cast(block.Address() >> 8u); + cx[0x31b] = 0; + cx[0x31c] = info; + cx[0x31d] = 0; + cx[0x323] = 0; + cx[0x324] = 0; + cx[0x3b0] = ((Width - 1u) << 14u) | (Height - 1u); + cx[0x3b8] = LinearAttrib3; + cx[0x390] = static_cast(block.Address() >> 40u); + auto color = AgcDriver::Graphics::DecodeColorBuffer(cx, 0); + color.exportIndex = 0; + return color; +} + +void Draw(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::ColorTarget& color, std::uint32_t inputAddr, std::span pixelCode) { + const auto target = device.Target(); + constexpr std::uint32_t waveSize = 64; + std::vector vertexUserData(4, 0u); + const auto vertexBuffer = BufferDescriptor(Triangle.data(), 16u, static_cast(Triangle.size())); + std::copy(vertexBuffer.begin(), vertexBuffer.end(), vertexUserData.begin()); + const std::array vertexMemory{{{reinterpret_cast(VertexCode.data()), std::as_bytes(std::span(VertexCode))}}}; + ShaderRecompiler::RecompileRequest vertex{ + {ShaderStage::Vertex, reinterpret_cast(VertexCode.data()), VertexCode, 0, {}}, + {waveSize, 0, vertexUserData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, vertexMemory}, + target, + {0, 0, 0, 64} + }; + vertex.useCache = false; + const auto vertexResult = ShaderRecompiler::Recompile(vertex); + const auto vertexPush = static_cast(vertexResult.pushConstants.size()); + + ShaderRecompiler::ShaderPixelStageInfo pixel{}; + pixel.inputAddr = inputAddr; + pixel.targetOutputMode.at(0) = Float32AbgrExport; + pixel.targetExportMapping.fill(0xe4u); + const std::vector pixelUserData(8, 0u); + const std::array pixelMemory{{{reinterpret_cast(pixelCode.data()), std::as_bytes(pixelCode)}}}; + ShaderRecompiler::RecompileRequest fragment{ + {ShaderStage::Fragment, reinterpret_cast(pixelCode.data()), pixelCode, 0, {}}, + {waveSize, 0, pixelUserData, std::nullopt, pixel, std::nullopt, pixelMemory}, + target, + PixelPushLayout(vertexPush, target) + }; + fragment.useCache = false; + const auto pixelResult = ShaderRecompiler::Recompile(fragment); + const std::array shaders{{ + {ShaderStage::Vertex, &vertexResult, 0}, + {ShaderStage::Fragment, &pixelResult, PixelPushOffset(vertexPush, target)} + }}; + + VkPipelineColorBlendAttachmentState blend{}; + blend.blendEnable = VK_FALSE; + blend.colorWriteMask = 0xf; + AgcDriver::Graphics::State state{}; + state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, waveSize, waveSize, std::nullopt, std::nullopt}; + state.colors = {color}; + state.color = color; + state.hasColorTarget = true; + state.renderExtent = {Width, Height}; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.viewport = {0, static_cast(Height), static_cast(Width), -static_cast(Height), 0, 1}; + state.negativeOneToOne = false; + state.scissor = {{0, 0}, {Width, Height}}; + state.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + state.blends = {blend}; + state.blend = blend; + state.blendConstants = {}; + const AgcDriver::Pm4::DrawParameters draw{0, static_cast(Triangle.size()), 0, 1, 0, false}; + device.Draw(state, draw, shaders); +} + + +void Check(const std::vector& stored, const std::string& what) { + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + std::array texel{}; + std::memcpy(texel.data(), stored.data() + (static_cast(y) * Width + x) * 16u, sizeof(texel)); + const std::array expected{static_cast(x), static_cast(y), 0.0f, 1.0f}; + Require(texel == expected, what + ": pixel (" + std::to_string(x) + ", " + std::to_string(y) + ") holds (" + std::to_string(texel[0]) + ", " + std::to_string(texel[1]) + ", " + std::to_string(texel[2]) + ", " + std::to_string(texel[3]) + ")"); + } + } +} + +bool Expect(AgcDriver::VulkanDevice& device) { + using ShaderRecompiler::PixelInput; + using ShaderRecompiler::PixelInputBit; + constexpr std::size_t bytes = std::size_t{Width} * Height * 16u; + Block block(bytes); + std::fill(block.data, block.data + bytes, std::uint8_t{0xff}); + const auto context = AgcDriver::VulkanDeviceTestAccess::ContextOf(device); + if (AgcDriver::Graphics::HostImportFor(context, block.Address(), bytes) == nullptr) return false; + const auto color = DecodeTarget(block, Rgba32FloatInfo); + Require(color.tileMode == AgcDriver::Graphics::ColorTileMode::Linear && color.bytes == bytes, "the target is not a linear R32G32B32A32 target"); + Draw(device, color, PixelInputBit(PixelInput::PositionFixedPoint), Program(0u, NoInput)); + const auto* recorder = AgcDriver::Graphics::Recorder::Active(); + Require(recorder != nullptr && !recorder->Idle(), "the draw into an imported linear target waited for the GPU instead of being recorded"); + device.WaitIdle(); + std::vector stored(bytes); + std::memcpy(stored.data(), block.data, bytes); + Check(stored, "after the recorded draw, raw guest read"); + return true; +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + if (!Expect(*device)) { + std::puts("linear target resident test skipped: no host import on this device"); + return VulkanTestSkipped; + } + std::puts("linear target resident tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} From f5882503b6892110dcfd7638f7684921b142021f Mon Sep 17 00:00:00 2001 From: pushkarreddyy <212133856+pushkarreddyy@users.noreply.github.com> Date: Wed, 14 Oct 2026 20:41:29 +0530 Subject: [PATCH 0556/1000] feat(libSceWebBrowserDialog): implement sceWebBrowserDialogOpenForPredeterminedContent --- core/libs/prx/libSceWebBrowserDialog/Export.cpp | 8 ++++++-- core/libs/tests/GuestWebBrowserDialog.cpp | 16 ++++++++++++++++ docs/dev/debt/libSceWebBrowserDialog.md | 5 +++++ 3 files changed, 27 insertions(+), 2 deletions(-) create mode 100644 docs/dev/debt/libSceWebBrowserDialog.md diff --git a/core/libs/prx/libSceWebBrowserDialog/Export.cpp b/core/libs/prx/libSceWebBrowserDialog/Export.cpp index cf61a32659..01bf0684d8 100644 --- a/core/libs/prx/libSceWebBrowserDialog/Export.cpp +++ b/core/libs/prx/libSceWebBrowserDialog/Export.cpp @@ -72,8 +72,12 @@ int APS5_VABI sceWebBrowserDialogSetCookie(void) { return 0; } -int APS5_VABI sceWebBrowserDialogOpenForPredeterminedContent() { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceWebBrowserDialogOpenForPredeterminedContent(const void* param) { + const int status = g_status.load(); + if (status == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + if (status == COMMON_DIALOG_STATUS_RUNNING) return COMMON_DIALOG_ERROR_BUSY; + if (param == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; + g_status = COMMON_DIALOG_STATUS_FINISHED; return 0; } diff --git a/core/libs/tests/GuestWebBrowserDialog.cpp b/core/libs/tests/GuestWebBrowserDialog.cpp index b20ec63097..9fc91930ac 100644 --- a/core/libs/tests/GuestWebBrowserDialog.cpp +++ b/core/libs/tests/GuestWebBrowserDialog.cpp @@ -6,6 +6,7 @@ extern "C" { int APS5_VABI sceWebBrowserDialogInitialize(void); int APS5_VABI sceWebBrowserDialogTerminate(void); int APS5_VABI sceWebBrowserDialogOpen(const void* param); +int APS5_VABI sceWebBrowserDialogOpenForPredeterminedContent(const void* param); int APS5_VABI sceWebBrowserDialogClose(void); int APS5_VABI sceWebBrowserDialogGetStatus(void); int APS5_VABI sceWebBrowserDialogUpdateStatus(void); @@ -64,5 +65,20 @@ int main() { Require(sceWebBrowserDialogTerminate() == COMMON_DIALOG_ERROR_NOT_INITIALIZED); Require(sceWebBrowserDialogGetStatus() == COMMON_DIALOG_STATUS_NONE); + Require(sceWebBrowserDialogOpenForPredeterminedContent(param) == COMMON_DIALOG_ERROR_NOT_INITIALIZED); + Require(sceWebBrowserDialogInitialize() == 0); + Require(sceWebBrowserDialogOpenForPredeterminedContent(nullptr) == COMMON_DIALOG_ERROR_ARG_NULL); + Require(sceWebBrowserDialogOpenForPredeterminedContent(param) == 0); + Require(sceWebBrowserDialogGetStatus() == COMMON_DIALOG_STATUS_FINISHED); + Require(sceWebBrowserDialogUpdateStatus() == COMMON_DIALOG_STATUS_FINISHED); + + result = 0x5a5a5a5a; + Require(sceWebBrowserDialogGetResult(&result) == 0); + Require(result == COMMON_DIALOG_RESULT_USER_CANCELED); + + Require(sceWebBrowserDialogClose() == 0); + Require(sceWebBrowserDialogTerminate() == 0); + Require(sceWebBrowserDialogGetStatus() == COMMON_DIALOG_STATUS_NONE); + return 0; } diff --git a/docs/dev/debt/libSceWebBrowserDialog.md b/docs/dev/debt/libSceWebBrowserDialog.md new file mode 100644 index 0000000000..1fd37116d9 --- /dev/null +++ b/docs/dev/debt/libSceWebBrowserDialog.md @@ -0,0 +1,5 @@ +# libSceWebBrowserDialog technical debt + +### Silent stubs + +- [libSceWebBrowserDialog](../../../core/libs/prx/libSceWebBrowserDialog/Export.cpp) - `sceWebBrowserDialogOpen` and `sceWebBrowserDialogOpenForPredeterminedContent` run the common-dialog state machine without displaying an interactive browser, and `sceWebBrowserDialogGetResult` reports that the user cancelled. Minecraft Bedrock (PPSA17221) imports `sceWebBrowserDialogOpenForPredeterminedContent` From 7f03455ba0b06f380b66b9a6d835ac5337c43079 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Fri, 16 Oct 2026 20:47:40 +0200 Subject: [PATCH 0557/1000] docs(debt): finish the OpenForPredeterminedContent line, note the unread allow-list argument Co-Authored-By: Claude --- docs/dev/debt/libSceWebBrowserDialog.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceWebBrowserDialog.md b/docs/dev/debt/libSceWebBrowserDialog.md index 1fd37116d9..bc07026490 100644 --- a/docs/dev/debt/libSceWebBrowserDialog.md +++ b/docs/dev/debt/libSceWebBrowserDialog.md @@ -2,4 +2,4 @@ ### Silent stubs -- [libSceWebBrowserDialog](../../../core/libs/prx/libSceWebBrowserDialog/Export.cpp) - `sceWebBrowserDialogOpen` and `sceWebBrowserDialogOpenForPredeterminedContent` run the common-dialog state machine without displaying an interactive browser, and `sceWebBrowserDialogGetResult` reports that the user cancelled. Minecraft Bedrock (PPSA17221) imports `sceWebBrowserDialogOpenForPredeterminedContent` +- [libSceWebBrowserDialog](../../../core/libs/prx/libSceWebBrowserDialog/Export.cpp) - `sceWebBrowserDialogOpen` and `sceWebBrowserDialogOpenForPredeterminedContent` run the common-dialog state machine without displaying an interactive browser, and `sceWebBrowserDialogGetResult` reports that the user cancelled. Minecraft Bedrock (PPSA17221) imports `sceWebBrowserDialogOpenForPredeterminedContent` (#5744). The real export takes a second argument, the allow-list of up to twenty addresses (SharpProspero `docs/dialogs.md`); it is not read here, so no address filtering is modelled, and the exact validation order and codes with a null second argument are unverified. From f00b2ef833cfc51d1713137cbfd1e809e9945296 Mon Sep 17 00:00:00 2001 From: Kurenaiiiii Date: Wed, 14 Oct 2026 21:41:12 +0530 Subject: [PATCH 0558/1000] feat(libSceNet): return success from ShowIfconfig and ShowNetstat --- core/libs/prx/libSceNet/Export.cpp | 2 -- core/libs/tests/GuestSceNet.cpp | 4 ++++ docs/dev/debt/libSceNet.md | 7 +++++-- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index ac697a4120..9300a84556 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -503,7 +503,6 @@ int* APS5_VABI sceNetErrnoLoc(void) { } int APS5_VABI sceNetShowNetstat(void) { - NotImplemented_nid_no_patch(__func__); return 0; } @@ -1607,7 +1606,6 @@ int APS5_VABI sceNetResolverAbort(int rid, int flags) { } int APS5_VABI sceNetShowIfconfig() { - NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/core/libs/tests/GuestSceNet.cpp b/core/libs/tests/GuestSceNet.cpp index 06e4eb3fe7..e37f109374 100644 --- a/core/libs/tests/GuestSceNet.cpp +++ b/core/libs/tests/GuestSceNet.cpp @@ -30,6 +30,8 @@ extern const std::uint8_t in6addr_any_nid_postfix[16]; extern const std::uint8_t in6addr_loopback_nid_postfix[16]; int APS5_VABI sceNetInit_nid_postfix(void); int APS5_VABI sceNetShowNetstatWithMemory(int); +int APS5_VABI sceNetShowNetstat(void); +int APS5_VABI sceNetShowIfconfig(void); int APS5_VABI sceNetSocket(const char*, int, int, int); int APS5_VABI sceNetBind_nid_postfix(int, const void*, std::uint32_t); int APS5_VABI sceNetListen(int, int); @@ -210,6 +212,8 @@ static std::string CaptureNetstat(int pool) { } static void CheckNetstat() { + Require(sceNetShowNetstat() == 0); + Require(sceNetShowIfconfig() == 0); Require(Failed(sceNetShowNetstatWithMemory(-1), 9)); const int pool = sceNetPoolCreate("netstat", 4096, 0); Require(pool > 0); diff --git a/docs/dev/debt/libSceNet.md b/docs/dev/debt/libSceNet.md index fae79da01e..5d0f051b75 100644 --- a/docs/dev/debt/libSceNet.md +++ b/docs/dev/debt/libSceNet.md @@ -1,10 +1,13 @@ # libSceNet technical debt +### Silent stubs + +- [sceNetShowIfconfig, sceNetShowNetstat](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - return success without printing anything: the `()` signatures and the success return follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/net.cpp), which stubs both the same way. PPSA01530 fails to load without the `sceNetShowIfconfig` export (#3177); PPSA11386 imports `sceNetShowNetstat` but does not call it during start-up. What the console prints and where is unverified + ### Unknown function info -- PPSA01530 and PPSA13356 imports declared without parameters, signatures unknown, added without an implementation because the titles fail to load without them: [sceNetShowIfconfig](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) and [scePadUnknown_n3kSX62fgNo](../../../core/libs/prx/libScePad/Export.cpp) (libScePad), whose real name is unknown. [_sceUltWaitingQueueResourcePoolCreate, _sceUltSemaphoreCreate, _sceUltMutexOptParamInitialize, _sceUltMutexCreate, _sceUltQueueDataResourcePoolCreate and _sceUltQueueCreate](../../../core/libs/prx/libSceUlt/Export.cpp) (libSceUlt) forward to the exports without the underscore; that their argument lists match is inferred from the other underscore Ult exports, not measured. +- PPSA01530 and PPSA13356 imports declared without parameters, signatures unknown, added without an implementation because the titles fail to load without them: [scePadUnknown_n3kSX62fgNo](../../../core/libs/prx/libScePad/Export.cpp) (libScePad), whose real name is unknown. [_sceUltWaitingQueueResourcePoolCreate, _sceUltSemaphoreCreate, _sceUltMutexOptParamInitialize, _sceUltMutexCreate, _sceUltQueueDataResourcePoolCreate and _sceUltQueueCreate](../../../core/libs/prx/libSceUlt/Export.cpp) (libSceUlt) forward to the exports without the underscore; that their argument lists match is inferred from the other underscore Ult exports, not measured. - [sceHttp2GetMemoryPoolStats, sceSslGetMemoryPoolStats, sceNetGetMemoryPoolStats](../../../core/libs/prx/libSceNet/Export.cpp) (libSceHttp2, libSceSsl, libSceNet) - the signatures `(int id, stats*)`, the layouts (pool size, peak and current use, and for Http2 and Ssl a 32-bit reserved field) and the error codes were measured on a PS5 (firmware 13.60). The pool size is the one given at init or pool creation. Http2 and Net allocate nothing from these pools. Ssl counts its certificate backend allocations and returned data against its pool; C++ bookkeeping is outside that budget. All three initially report zero use, where the console already reports 0x15C0 bytes for Ssl, 0x1640 for Http2 and 4 for a Net pool. Ssl's allocation sizes and peak usage do not reproduce the console allocator -- [sceNetShowNetstat](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - imported by PPSA11386 and not called during its start-up; unknown signature, declared without parameters ### Functional From 0e329fc64989bb4dfa02bbf75bbfd775c6dfb8a8 Mon Sep 17 00:00:00 2001 From: Kurenaiiiii Date: Wed, 14 Oct 2026 22:15:50 +0530 Subject: [PATCH 0559/1000] docs: move loader import debt to its library files --- docs/dev/debt/libSceNet.md | 1 - docs/dev/debt/libScePad.md | 1 + docs/dev/debt/libSceUlt.md | 1 + 3 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceNet.md b/docs/dev/debt/libSceNet.md index 5d0f051b75..c96d4e1ad1 100644 --- a/docs/dev/debt/libSceNet.md +++ b/docs/dev/debt/libSceNet.md @@ -6,7 +6,6 @@ ### Unknown function info -- PPSA01530 and PPSA13356 imports declared without parameters, signatures unknown, added without an implementation because the titles fail to load without them: [scePadUnknown_n3kSX62fgNo](../../../core/libs/prx/libScePad/Export.cpp) (libScePad), whose real name is unknown. [_sceUltWaitingQueueResourcePoolCreate, _sceUltSemaphoreCreate, _sceUltMutexOptParamInitialize, _sceUltMutexCreate, _sceUltQueueDataResourcePoolCreate and _sceUltQueueCreate](../../../core/libs/prx/libSceUlt/Export.cpp) (libSceUlt) forward to the exports without the underscore; that their argument lists match is inferred from the other underscore Ult exports, not measured. - [sceHttp2GetMemoryPoolStats, sceSslGetMemoryPoolStats, sceNetGetMemoryPoolStats](../../../core/libs/prx/libSceNet/Export.cpp) (libSceHttp2, libSceSsl, libSceNet) - the signatures `(int id, stats*)`, the layouts (pool size, peak and current use, and for Http2 and Ssl a 32-bit reserved field) and the error codes were measured on a PS5 (firmware 13.60). The pool size is the one given at init or pool creation. Http2 and Net allocate nothing from these pools. Ssl counts its certificate backend allocations and returned data against its pool; C++ bookkeeping is outside that budget. All three initially report zero use, where the console already reports 0x15C0 bytes for Ssl, 0x1640 for Http2 and 4 for a Net pool. Ssl's allocation sizes and peak usage do not reproduce the console allocator ### Functional diff --git a/docs/dev/debt/libScePad.md b/docs/dev/debt/libScePad.md index b828591f2b..9355c1db26 100644 --- a/docs/dev/debt/libScePad.md +++ b/docs/dev/debt/libScePad.md @@ -9,3 +9,4 @@ - [scePadIsRemoteController](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - the name hashes to NID `fCWdlnmB1Ks`; the signature `(int handle, bool* remote)` comes from PPSA16064, which zeroes a byte, passes its address after `scePadGetControllerInformation` and ignores the result. The error codes follow the other pad calls. Controllers are local devices, so the answer is always false - [scePadSetAngularVelocityBiasCorrectionState](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - inferred signature `(int handle, bool enabled)` follows `scePadSetTiltCorrectionState` and `scePadSetAngularVelocityDeadbandState`; an invalid handle returns `PAD_ERROR_INVALID_HANDLE`, disabling is a no-op, and enabling throws because bias correction is not modelled - [scePadUnknown_zBsgF0q8DIM](../../../core/libs/prx/libScePad/Export.cpp) (libScePad, NID `zBsgF0q8DIM`) - PPSA03671 imports it and fails to load without it. The aerolib list has no name for the NID, and none of the pad names in SharpProspero, shadPS4 or OpenOrbis, nor the trigger effect, motion sensor, VR controller, HD and haptic names tried, hashes to it. It is declared without parameters and throws. The title's executable has an import stub for it but no call, jump or address reference to that stub, so the argument count and whether the result is used are unknown +- [scePadUnknown_n3kSX62fgNo](../../../core/libs/prx/libScePad/Export.cpp) (libScePad, NID `n3kSX62fgNo`) - PPSA01530 and PPSA13356 import it and fail to load without it. The real name is unknown; it is declared without parameters and throws diff --git a/docs/dev/debt/libSceUlt.md b/docs/dev/debt/libSceUlt.md index 3ac4a39278..95e0067125 100644 --- a/docs/dev/debt/libSceUlt.md +++ b/docs/dev/debt/libSceUlt.md @@ -5,6 +5,7 @@ - [libSceUlt ulthreads](../../../core/libs/prx/libSceUlt/Export.cpp) - `_sceUltUlthreadRuntimeOptParamInitialize`, `_sceUltUlthreadRuntimeCreate` and `_sceUltUlthreadCreate` are assumed to take the same arguments as the existing exports without the underscore, including the trailing build version. `sceUltUlthreadTryJoin` is assumed to return `ULT_ERROR_BUSY` while the ulthread runs, and `sceUltUlthreadRuntimeDestroy` to return `ULT_ERROR_BUSY` while an ulthread created on the runtime is not joined. Neither error has been confirmed on PS5 hardware - [libSceUlt queues](../../../core/libs/prx/libSceUlt/Export.cpp) - the added `sceUltQueuePop`, `sceUltQueueTryPush`, `sceUltQueueDestroy` and `sceUltQueueDataResourcePoolDestroy` signatures are inferred from the existing push/pop and object APIs. Queue destruction is assumed to reject blocked callers with `ULT_ERROR_BUSY` and otherwise discard queued data; pool exhaustion returns `ULT_ERROR_AGAIN`. These rules, alignment checks and shared slot limits have not been confirmed on PS5 hardware or a title. - [sceUltQueueCreate, sceUltQueueDataResourcePoolCreate](../../../core/libs/prx/libSceUlt/Export.cpp) - a non-null option parameter is accepted and ignored, as the other Ult create calls already do. [OpenOrbis](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain/blob/00d087b2323bf377fd39b7163a256977383cb232/include/orbis/Ult.h) declares `sceUltQueueOptParamInitialize` and `sceUltQueueDataResourcePoolOptParamInitialize` without their structures; the PS Vita libult in [RPCS3](https://github.com/RPCS3/rpcs3/blob/13aa88c3c4d66ea8799e987f7d762d90943ab17b/rpcs3/Emu/PSP2/Modules/sceUlt.h#L25-L78) defines both as a 16-byte reserved header followed by reserved fields, 128 bytes, with no settings, and [fpPS4](https://github.com/red-prig/fpPS4/blob/5ffbdbbd76b4da6e4b8721743ea491f494b28913/src/ps4_libsceult.pas#L142-L172) keeps them as 128 opaque bytes. That the PS5 structures carry no settings either is assumed from these, not verified +- [_sceUltWaitingQueueResourcePoolCreate, _sceUltSemaphoreCreate, _sceUltMutexOptParamInitialize, _sceUltMutexCreate, _sceUltQueueDataResourcePoolCreate and _sceUltQueueCreate](../../../core/libs/prx/libSceUlt/Export.cpp) (libSceUlt) forward to the exports without the underscore; that their argument lists match is inferred from the other underscore Ult exports, not measured ### Functional From 8c6f08bfa74336543220da031eb28447cd88d979 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Fri, 16 Oct 2026 20:51:10 +0200 Subject: [PATCH 0560/1000] test: drop sceNetShowIfconfig from the not-implemented exports It returns success now. --- core/libs/tests/GuestPPSA01530Exports.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/core/libs/tests/GuestPPSA01530Exports.cpp b/core/libs/tests/GuestPPSA01530Exports.cpp index ee9470613e..a903cdca4d 100644 --- a/core/libs/tests/GuestPPSA01530Exports.cpp +++ b/core/libs/tests/GuestPPSA01530Exports.cpp @@ -5,7 +5,6 @@ #include extern "C" { -int APS5_VABI sceNetShowIfconfig(); int APS5_VABI scePadUnknown_n3kSX62fgNo(); } @@ -17,7 +16,6 @@ struct Export { }; const Export exports[] = { - {"sceNetShowIfconfig", sceNetShowIfconfig}, {"scePadUnknown_n3kSX62fgNo", scePadUnknown_n3kSX62fgNo}, }; From f2649c330181603029ec88a7095cd7509accbd36 Mon Sep 17 00:00:00 2001 From: xXPBSXx Date: Wed, 14 Oct 2026 18:21:37 +0200 Subject: [PATCH 0561/1000] fix(libkernel): make close wait for a positioned transfer on the same descriptor Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libkernel/CMakeLists.txt | 10 +++ core/libs/prx/libkernel/File/src/Open.cpp | 3 + core/libs/prx/libkernel/File/src/Stdio.cpp | 4 ++ core/libs/tests/GuestPositionedClose.cpp | 72 ++++++++++++++++++++++ 4 files changed, 89 insertions(+) create mode 100644 core/libs/tests/GuestPositionedClose.cpp diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 4593cae303..ca68eb7ead 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -1373,6 +1373,16 @@ if(BUILD_TESTING) if(MINGW) target_compile_options(guest_directory_close_tests PRIVATE -fasynchronous-unwind-tables) endif() + add_test_executable(guest_positioned_close_tests ${LIBS_INCLUDE_DIR}/tests/GuestPositionedClose.cpp) + target_include_directories(guest_positioned_close_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_positioned_close_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_positioned_close_tests) + add_test(NAME guest_positioned_close COMMAND guest_positioned_close_tests) + set_tests_properties(guest_positioned_close PROPERTIES TIMEOUT 60 ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + if(MINGW) + target_compile_options(guest_positioned_close_tests PRIVATE -fasynchronous-unwind-tables) + endif() endif() add_test_executable(guest_file_vector_tests ${LIBS_INCLUDE_DIR}/tests/GuestFileVector.cpp) target_include_directories(guest_file_vector_tests PRIVATE ${LIBS_INCLUDE_DIR}) diff --git a/core/libs/prx/libkernel/File/src/Open.cpp b/core/libs/prx/libkernel/File/src/Open.cpp index 9760bbe879..83e0b56274 100644 --- a/core/libs/prx/libkernel/File/src/Open.cpp +++ b/core/libs/prx/libkernel/File/src/Open.cpp @@ -236,6 +236,9 @@ int OpenInDirectory_nid_no_patch(int directory, const char* path, int flags, std int APS5_VABI sceKernelClose(int d) { if (File::IsHostStream(d)) return SCE_KERNEL_ERROR_EBADF; if (GuestSockets::IsOpen(d)) return GuestSockets::Close(d) == 0 ? 0 : SceKernelError(*__error_nid_postfix()); +#ifdef _WIN32 + const auto positioned = File::LockPositionedTransfer(d); +#endif EqueueDescriptorClosed(d); File::ForgetRandomDevice(d); File::ForgetDescriptor(d); diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index 5ded00a7ba..6acf506c64 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -206,6 +206,7 @@ static std::int64_t NativePositionedTransfer(int descriptor, void* buf, std::siz const auto result = File::PwriteSharedMemory_nid_no_patch(descriptor, buf, nbytes, offset); if (result != -2) return result; } + const auto positioned = File::LockPositionedTransfer(descriptor); const auto handle = reinterpret_cast(WithoutParameterHandler([descriptor] { return ::_get_osfhandle(descriptor); })); if (handle == INVALID_HANDLE_VALUE) { errno = EBADF; @@ -580,6 +581,9 @@ int APS5_VABI close_nid_postfix(int d) { return -1; } if (GuestSockets::IsOpen(d)) return GuestSockets::Close(d); +#ifdef _WIN32 + const auto positioned = File::LockPositionedTransfer(d); +#endif EqueueDescriptorClosed(d); File::ForgetRandomDevice(d); File::ForgetDescriptor(d); diff --git a/core/libs/tests/GuestPositionedClose.cpp b/core/libs/tests/GuestPositionedClose.cpp new file mode 100644 index 0000000000..d5da32613d --- /dev/null +++ b/core/libs/tests/GuestPositionedClose.cpp @@ -0,0 +1,72 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libkernel/File/include/FileFlags.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceKernelOpen(const char*, int, std::uint16_t); +int APS5_VABI sceKernelClose(int); +std::int64_t APS5_VABI sceKernelPread(int, void*, std::size_t, std::int64_t); +} + +static void Check(bool value, int line) { + if (!value) { + std::fprintf(stderr, "Positioned close check failed at line %d\n", line); + std::abort(); + } +} +#define Require(value) Check((value), __LINE__) + +static constexpr std::int64_t ErrorEbadf = static_cast(0x80020009u); + +int main() { + const auto root = std::filesystem::path("anyps5-positioned-close-test-" + + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); + Require(std::filesystem::create_directory(root)); + const auto path = (root / "data.bin").string(); + constexpr std::size_t fileSize = 48u << 20; + { + std::ofstream stream(path, std::ios::binary); + const std::vector block(1u << 20, 'x'); + for (std::size_t written = 0; written < fileSize; written += block.size()) stream.write(block.data(), static_cast(block.size())); + Require(stream.good()); + } + std::vector buffer(32u << 20); + int overlapped = 0; + for (int round = 0; round < 60; ++round) { + const int descriptor = sceKernelOpen(path.c_str(), SCE_KERNEL_O_RDONLY, 0); + Require(descriptor >= 0); + std::atomic state{0}; + std::atomic threw{false}; + std::int64_t result = 0; + std::thread reader([&] { + state = 1; + try { + result = sceKernelPread(descriptor, buffer.data(), buffer.size(), 0); + } catch (const std::exception&) { + threw = true; + } + state = 2; + }); + while (state.load() == 0) std::this_thread::yield(); + std::this_thread::sleep_for(std::chrono::microseconds(300)); + const int closed = sceKernelClose(descriptor); + reader.join(); + Require(!threw.load()); + Require(closed == 0); + Require(result == static_cast(buffer.size()) || result == ErrorEbadf); + if (result == static_cast(buffer.size())) ++overlapped; + } + Require(overlapped > 0); + std::filesystem::remove_all(root); + return 0; +} From 54845007349e3b12bcb86e04a271c90be939a6d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Fri, 16 Oct 2026 20:52:19 +0200 Subject: [PATCH 0562/1000] fix(libkernel): drop the second positioned lock in NativePositionedTransfer Its callers (NativePositioned_nid_no_patch, the vectored transfer) already hold the non-recursive per-descriptor lock, so locking again deadlocked every positioned transfer. Co-Authored-By: Claude --- core/libs/prx/libkernel/File/src/Stdio.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index 6acf506c64..5d08e0b036 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -206,7 +206,6 @@ static std::int64_t NativePositionedTransfer(int descriptor, void* buf, std::siz const auto result = File::PwriteSharedMemory_nid_no_patch(descriptor, buf, nbytes, offset); if (result != -2) return result; } - const auto positioned = File::LockPositionedTransfer(descriptor); const auto handle = reinterpret_cast(WithoutParameterHandler([descriptor] { return ::_get_osfhandle(descriptor); })); if (handle == INVALID_HANDLE_VALUE) { errno = EBADF; From 76e05544a9361aec2e8f12f80ce104dbf400bcb9 Mon Sep 17 00:00:00 2001 From: Nithishvelan M Date: Wed, 14 Oct 2026 22:11:42 +0530 Subject: [PATCH 0563/1000] feat(libkernel): report hw.pagesize through sysctl and sysctlnametomib --- .../System/src/SystemConfiguration.cpp | 15 +++++++++++- core/libs/tests/GuestSystemConfiguration.cpp | 24 ++++++++++++++++++- docs/dev/debt/libkernel.md | 1 + 3 files changed, 38 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp b/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp index 5803d5300a..3205d75fce 100644 --- a/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp +++ b/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp @@ -44,7 +44,7 @@ int CopyOut(const void* value, std::size_t size, void* oldValue, std::size_t* ol } bool IsLeafMib(const int* name) { - return (name[0] == 6 && (name[1] == 2 || name[1] == 3 || name[1] == 5)) || (name[0] == 1 && name[1] == 37); + return (name[0] == 6 && (name[1] == 2 || name[1] == 3 || name[1] == 5 || name[1] == 7)) || (name[0] == 1 && name[1] == 37); } } @@ -100,6 +100,14 @@ int APS5_VABI sysctl_nid_postfix(const int* name, std::uint32_t nameLength, void *__error_nid_postfix() = 14; return -1; } + if (nameLength == 2 && name[0] == 6 && name[1] == 7) { + if (newValue != nullptr) { + *__error_nid_postfix() = 1; + return -1; + } + constexpr int pagesize = PS5_PAGE_SIZE; + return CopyOut(&pagesize, sizeof(pagesize), oldValue, oldLength); + } if (nameLength == 2 && name[0] == 6 && name[1] == 3) { if (newValue != nullptr) { *__error_nid_postfix() = 1; @@ -219,6 +227,10 @@ int APS5_VABI sysctlbyname_nid_postfix(const char* name, void* oldValue, std::si const std::uint64_t frequency = sceKernelGetTscFrequency(); return CopyOut(&frequency, sizeof(frequency), oldValue, oldLength); } + if (std::strcmp(name, "hw.pagesize") == 0) { + static constexpr int mib[] = {6, 7}; + return sysctl_nid_postfix(mib, 2, oldValue, oldLength, newValue, newLength); + } if (std::strcmp(name, "hw.ncpu") == 0) { static constexpr int mib[] = {6, 3}; return sysctl_nid_postfix(mib, 2, oldValue, oldLength, newValue, newLength); @@ -257,6 +269,7 @@ int APS5_VABI sysctlnametomib_nid_postfix(const char* name, int* mib, std::size_ static constexpr KnownName known[] = { {"hw.model", {6, 2}, 2}, {"hw.ncpu", {6, 3}, 2}, + {"hw.pagesize", {6, 7}, 2}, {"hw.physmem", {6, 5}, 2}, {"hw.realmem", {6, 12}, 2}, {"kern.arandom", {1, 37}, 2}, diff --git a/core/libs/tests/GuestSystemConfiguration.cpp b/core/libs/tests/GuestSystemConfiguration.cpp index 330424b904..5c3f0922f4 100644 --- a/core/libs/tests/GuestSystemConfiguration.cpp +++ b/core/libs/tests/GuestSystemConfiguration.cpp @@ -67,6 +67,26 @@ static void CheckProcessorCountSysctl() { } Require(threw); } +static void CheckPageSizeSysctl() { + const int mib[] = {6, 7}; + *__error_nid_postfix() = 13; + std::size_t length = 0; + Require(sysctl_nid_postfix(mib, 2, nullptr, &length, nullptr, 0) == 0 && length == sizeof(int)); + int value = 0; + length = sizeof(value); + Require(sysctl_nid_postfix(mib, 2, &value, &length, nullptr, 0) == 0 && value == 0x4000 && length == sizeof(int)); + value = 0; + length = 16; + Require(sysctlbyname_nid_postfix("hw.pagesize", &value, &length, nullptr, 0) == 0 && value == 0x4000 && length == sizeof(int)); + Require(*__error_nid_postfix() == 13); + unsigned char bytes[4] = {0xaa, 0xaa, 0xaa, 0xaa}; + length = 2; + Require(sysctlbyname_nid_postfix("hw.pagesize", bytes, &length, nullptr, 0) == -1 && *__error_nid_postfix() == 12); + Require(length == 2 && bytes[2] == 0xaa && bytes[3] == 0xaa); + length = sizeof(value); + Require(sysctl_nid_postfix(mib, 2, &value, &length, &value, sizeof(value)) == -1 && *__error_nid_postfix() == 1); + Require(sysctlbyname_nid_postfix("hw.pagesize", &value, &length, &value, sizeof(value)) == -1 && *__error_nid_postfix() == 1); +} static void CheckPhysicalMemorySysctl(int number, const char* name) { const int mib[] = {6, number}; const std::uint64_t expected = static_cast(sysconf_nid_postfix(121)) * 0x4000; @@ -178,7 +198,7 @@ static void CheckPathnameSysctl() { Require(sysctl_nid_postfix(mibSelf, 4, buffer, &length, buffer, sizeof(buffer)) == -1 && *__error_nid_postfix() == 1); } static void CheckLeafWithTrailingComponent() { - const int leaves[][2] = {{6, 3}, {6, 2}, {6, 5}, {1, 37}}; + const int leaves[][2] = {{6, 3}, {6, 2}, {6, 5}, {6, 7}, {1, 37}}; for (const auto& leaf : leaves) { const int mib[] = {leaf[0], leaf[1], 0}; char buffer[8] = {}; @@ -311,6 +331,7 @@ static void CheckNameToMib() { const Expected expected[] = { {"hw.model", {6, 2}, 2}, {"hw.ncpu", {6, 3}, 2}, + {"hw.pagesize", {6, 7}, 2}, {"hw.physmem", {6, 5}, 2}, {"hw.realmem", {6, 12}, 2}, {"kern.arandom", {1, 37}, 2}, @@ -396,6 +417,7 @@ static void CheckNameToMib() { } int main() { CheckProcessorCountSysctl(); + CheckPageSizeSysctl(); CheckPhysicalMemorySysctl(5, "hw.physmem"); CheckPhysicalMemorySysctl(12, "hw.realmem"); #if defined(__x86_64__) || defined(__i386__) diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 5396a00c3a..e20f277cac 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -154,6 +154,7 @@ - [execve](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) throws like `execvp`: replacing the process image is not supported. The console behavior for invalid arguments and executable path lookups is not modeled. - [kern.proc.pathname](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) returns `/app0/eboot.bin` for pid -1 and the calling process, following FreeBSD's `sysctl_kern_proc_pathname`; the exact pathname on retail consoles remains unverified. - [machdep.cacheline_size](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) returns -1 with `ENOENT`, matching FreeBSD amd64 where this sysctl OID is not defined. +- [hw.pagesize](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) reports the system page size (16384 bytes, PS5_PAGE_SIZE) as an integer through sysctl MIB {6, 7} (CTL_HW, HW_PAGESIZE) and the "hw.pagesize" sysctlbyname lookup, following FreeBSD's sysctl_hw_pagesize. A write returns -1 with EPERM. - [hw.model](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) reports the [host CPU's name](../../../core/libs/prx/libkernel/System/src/HostCpu.cpp), built as FreeBSD 11 builds `hw.model`: the CPUID brand string (leaves 0x80000002 to 0x80000004) without leading spaces ([identcpu.c](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/x86/x86/identcpu.c#L683-L691)). Trailing padding stays, as it does there, so an AMD host's name ends in spaces. The name a PS5 reports is unknown. A write returns -1 with `EPERM`. A host without an x86 CPUID brand string, such as an Apple Silicon Mac, throws. PPSA99203 only prints it (`[console] hw.model: %s`) - [Host CPU check](../../../core/libs/prx/libkernel/System/src/HostCpu.cpp) (libkernel) - when libkernel loads on a Linux or Windows x86-64 host, it logs which of the PS5 CPU's user-mode extensions CPUID reports and throws, naming the host CPU, if any extension is missing that LLVM selects from plain C/C++ for the PS5's Zen 2 (`-march=znver2`, [feature list](https://github.com/llvm/llvm-project/blob/llvmorg-19.1.7/llvm/lib/Target/X86/X86.td#L1459-L1513)): SSE to SSE4.2, SSSE3, POPCNT, LZCNT, BMI1, BMI2, MOVBE, CMPXCHG16B, LAHF/SAHF, AVX, AVX2, FMA, F16C, and the operating system's AVX state in XCR0 (#3719). SSE4A, CLZERO and SHA, which `--to-intel` converts, and AES, PCLMULQDQ, RDRAND, RDSEED, ADX, CLFLUSHOPT, CLWB, RDPID, XSAVEOPT, XSAVEC and PREFETCHW, which compilers emit only for intrinsics or explicit prefetches, are logged but not required: a title that executes one of them on a host without it still stops with an illegal instruction. macOS hosts are not checked: some Rosetta versions execute extensions without reporting them through CPUID, but support differs by host. ANDN and RDRAND fault on an M1 with macOS 15.3.1; see the [Rosetta conversion limits](relinker.md) - [machdep.tsc_freq](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) reports the host TSC frequency that `sceKernelGetTscFrequency` measures, as a 64-bit value like FreeBSD 11's [`sysctl_machdep_tsc_freq`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/x86/x86/tsc.c#L658-L676). FreeBSD lets root write it to change the TSC timecounter's frequency; a write here returns -1 with `EPERM`. PPSA99203 reads it at start-up and measures the TSC itself when the value is 100 MHz or less From 825b7934e76d2ab46d058da7cf10fd7af4c6da55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Fri, 16 Oct 2026 20:55:50 +0200 Subject: [PATCH 0564/1000] docs(debt): cite FreeBSD's hw.pagesize OID, note the unmeasured PS5 value Co-Authored-By: Claude --- docs/dev/debt/libkernel.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index e20f277cac..dc5d17a0be 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -154,7 +154,7 @@ - [execve](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) throws like `execvp`: replacing the process image is not supported. The console behavior for invalid arguments and executable path lookups is not modeled. - [kern.proc.pathname](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) returns `/app0/eboot.bin` for pid -1 and the calling process, following FreeBSD's `sysctl_kern_proc_pathname`; the exact pathname on retail consoles remains unverified. - [machdep.cacheline_size](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) returns -1 with `ENOENT`, matching FreeBSD amd64 where this sysctl OID is not defined. -- [hw.pagesize](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) reports the system page size (16384 bytes, PS5_PAGE_SIZE) as an integer through sysctl MIB {6, 7} (CTL_HW, HW_PAGESIZE) and the "hw.pagesize" sysctlbyname lookup, following FreeBSD's sysctl_hw_pagesize. A write returns -1 with EPERM. +- [hw.pagesize](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) reports the system page size (16384 bytes, PS5_PAGE_SIZE) as an integer through sysctl MIB {6, 7} (CTL_HW, HW_PAGESIZE) and the "hw.pagesize" sysctlbyname lookup, following FreeBSD's [`hw.pagesize`](https://github.com/freebsd/freebsd-src/blob/main/sys/kern/kern_mib.c#L177), a read-only `SYSCTL_INT` of `PAGE_SIZE`. The value on a PS5 follows the 16 KiB page size the rest of libkernel uses and is unmeasured. A write returns -1 with EPERM. - [hw.model](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) reports the [host CPU's name](../../../core/libs/prx/libkernel/System/src/HostCpu.cpp), built as FreeBSD 11 builds `hw.model`: the CPUID brand string (leaves 0x80000002 to 0x80000004) without leading spaces ([identcpu.c](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/x86/x86/identcpu.c#L683-L691)). Trailing padding stays, as it does there, so an AMD host's name ends in spaces. The name a PS5 reports is unknown. A write returns -1 with `EPERM`. A host without an x86 CPUID brand string, such as an Apple Silicon Mac, throws. PPSA99203 only prints it (`[console] hw.model: %s`) - [Host CPU check](../../../core/libs/prx/libkernel/System/src/HostCpu.cpp) (libkernel) - when libkernel loads on a Linux or Windows x86-64 host, it logs which of the PS5 CPU's user-mode extensions CPUID reports and throws, naming the host CPU, if any extension is missing that LLVM selects from plain C/C++ for the PS5's Zen 2 (`-march=znver2`, [feature list](https://github.com/llvm/llvm-project/blob/llvmorg-19.1.7/llvm/lib/Target/X86/X86.td#L1459-L1513)): SSE to SSE4.2, SSSE3, POPCNT, LZCNT, BMI1, BMI2, MOVBE, CMPXCHG16B, LAHF/SAHF, AVX, AVX2, FMA, F16C, and the operating system's AVX state in XCR0 (#3719). SSE4A, CLZERO and SHA, which `--to-intel` converts, and AES, PCLMULQDQ, RDRAND, RDSEED, ADX, CLFLUSHOPT, CLWB, RDPID, XSAVEOPT, XSAVEC and PREFETCHW, which compilers emit only for intrinsics or explicit prefetches, are logged but not required: a title that executes one of them on a host without it still stops with an illegal instruction. macOS hosts are not checked: some Rosetta versions execute extensions without reporting them through CPUID, but support differs by host. ANDN and RDRAND fault on an M1 with macOS 15.3.1; see the [Rosetta conversion limits](relinker.md) - [machdep.tsc_freq](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) reports the host TSC frequency that `sceKernelGetTscFrequency` measures, as a 64-bit value like FreeBSD 11's [`sysctl_machdep_tsc_freq`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/x86/x86/tsc.c#L658-L676). FreeBSD lets root write it to change the TSC timecounter's frequency; a write here returns -1 with `EPERM`. PPSA99203 reads it at start-up and measures the TSC itself when the value is 100 MHz or less From 050f10053cadc315115350e647aa388bb67a8da1 Mon Sep 17 00:00:00 2001 From: LeandroLP23 Date: Fri, 16 Oct 2026 20:56:41 +0200 Subject: [PATCH 0565/1000] test(agc): shut libc down before agc_shader_helpers returns On Windows, agc_shader_helpers returned from main with the driver's threads still running. ExitProcess ends them first, then the DLLs' static destructors run and wait on what those threads left locked: the async log's queue mutex in Driver::stop, DeviceUseGate's condition variable in ~Driver, and condition variables in libkernel's statics. The test hung after printing its result in about one ctest run of three on a Radeon 860M (#6791). Like the other driver tests, it now runs LibcRunShutdown_nid_postfix before returning, which stops the driver while its threads are alive. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAgc/tests/ShaderHelpers.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/core/libs/prx/libSceAgc/tests/ShaderHelpers.cpp b/core/libs/prx/libSceAgc/tests/ShaderHelpers.cpp index ad88c7a410..46f7c3c8e8 100644 --- a/core/libs/prx/libSceAgc/tests/ShaderHelpers.cpp +++ b/core/libs/prx/libSceAgc/tests/ShaderHelpers.cpp @@ -2,6 +2,7 @@ #include "prx/libSceAgc/Shader/include/PrimState.hpp" #include "prx/libSceAgc/Shader/include/InterpolantMapping.hpp" #include "prx/libSceAgc/Shader/include/ShaderConstants.hpp" +#include "prx/libc/include/Shutdown.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include #include @@ -194,9 +195,11 @@ int main() { Require(preparations == 0 && mappings == 0 && links == 0); OptionalGeometryOutputRegister(special, vertex, hull); HandBuiltUserVgprRegister(); + LibcRunShutdown_nid_postfix(); std::cout << "shader helper preparation tests passed\n"; } catch (const std::exception& error) { std::cerr << error.what() << '\n'; + try { LibcRunShutdown_nid_postfix(); } catch (...) {} return 1; } } From 8fe5916fd0253da81939026de7606140286f72b7 Mon Sep 17 00:00:00 2001 From: Alexandros Pournaras Date: Wed, 14 Oct 2026 19:48:07 +0300 Subject: [PATCH 0566/1000] feat(libSceVoice): implement sceVoiceGetPortAttr --- core/libs/CMakeLists.txt | 10 +++++ core/libs/prx/libSceVoice/Export.cpp | 9 +++- core/libs/tests/GuestVoicePortAttr.cpp | 59 ++++++++++++++++++++++++++ docs/dev/debt/libSceVoice.md | 4 ++ 4 files changed, 80 insertions(+), 2 deletions(-) create mode 100644 core/libs/tests/GuestVoicePortAttr.cpp diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index f33b4cfbc4..c4c30756b4 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -279,6 +279,16 @@ if(BUILD_TESTING) PASS_REGULAR_EXPRESSION "C\\+\\+ exception St9bad_alloc raised on thread [0-9]+ cannot be delivered: 0x[0-9a-f]+ undeliverable_host_exception_tests\\.exe\\+0x[0-9a-f]+ has no unwind data") endif() + add_test_executable(guest_voice_port_attr_tests tests/GuestVoicePortAttr.cpp) + target_include_directories(guest_voice_port_attr_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(guest_voice_port_attr_tests PRIVATE libSceVoice libc) + configure_windows_unwind(guest_voice_port_attr_tests) + add_test(NAME guest_voice_port_attr COMMAND guest_voice_port_attr_tests) + if(WIN32) + set_tests_properties(guest_voice_port_attr PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_offline_imports_tests tests/GuestOfflineImports.cpp) target_include_directories(guest_offline_imports_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(guest_offline_imports_tests PRIVATE libSceHttp libSceNpEntitlementAccess libSceRudp libc) diff --git a/core/libs/prx/libSceVoice/Export.cpp b/core/libs/prx/libSceVoice/Export.cpp index f3234417f7..6210925cdb 100644 --- a/core/libs/prx/libSceVoice/Export.cpp +++ b/core/libs/prx/libSceVoice/Export.cpp @@ -1,5 +1,6 @@ #include #include +#include #include #include #include @@ -19,6 +20,7 @@ constexpr std::int32_t PortOutVoice = 4; constexpr std::int32_t PortOutDevice = 5; constexpr std::int32_t PortStateReady = 1; constexpr std::uint32_t VoiceFrameMs = 20; +constexpr std::int32_t PortAttrVoiceActivity = 1001; struct Port { std::int32_t type; @@ -179,11 +181,14 @@ int APS5_VABI sceVoiceGetPortInfo(uint32_t port_id, VoicePortInfo* info) { } int APS5_VABI sceVoiceGetPortAttr(uint32_t port_id, int32_t attr, void* value, int32_t size) { - (void)value; - (void)size; auto& voice = State(); std::lock_guard lock(voice.mutex); RequirePort(voice, port_id, __func__); + if (attr == PortAttrVoiceActivity) { + if (value == nullptr || size != 1) APS5_INVALID_ARG_EX; + std::memset(value, 0, 1); + return 0; + } Fail(__func__, "attribute " + std::to_string(attr) + " not implemented"); } diff --git a/core/libs/tests/GuestVoicePortAttr.cpp b/core/libs/tests/GuestVoicePortAttr.cpp new file mode 100644 index 0000000000..3264ec11c4 --- /dev/null +++ b/core/libs/tests/GuestVoicePortAttr.cpp @@ -0,0 +1,59 @@ +#include "SceTypes.hpp" + +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceVoiceInit(VoiceInitParam*, int32_t); +int APS5_VABI sceVoiceEnd_nid_postfix(void); +int APS5_VABI sceVoiceCreatePort(uint32_t*, const VoicePortParam*); +int APS5_VABI sceVoiceDeletePort(uint32_t); +int APS5_VABI sceVoiceGetPortAttr(uint32_t, int32_t, void*, int32_t); +} + +static void Require(bool value) { if (!value) std::abort(); } + +template +static bool Throws(Call call, std::string_view message) { + try { + call(); + } catch (const std::logic_error& error) { + return std::string_view(error.what()).find(message) != std::string_view::npos; + } + return false; +} + +int main() { + constexpr std::int32_t voiceActivity = 1001; + + Require(Throws([] { sceVoiceGetPortAttr(0, voiceActivity, nullptr, 1); }, "library not initialized")); + + VoiceInitParam init{}; + Require(sceVoiceInit(&init, 0) == 0); + + VoicePortParam param{}; + param.port_type = 2; + param.volume = 1.0f; + param.voice.bitrate = 16000; + std::uint32_t port = 0; + Require(sceVoiceCreatePort(&port, ¶m) == 0); + + std::array buffer{0xa5, 0xa5, 0xa5}; + Require(sceVoiceGetPortAttr(port, voiceActivity, &buffer[1], 1) == 0); + Require(buffer[0] == 0xa5 && buffer[1] == 0 && buffer[2] == 0xa5); + + Require(Throws([port, &buffer] { sceVoiceGetPortAttr(port, 1, &buffer[1], 1); }, "attribute 1 not implemented")); + + Require(Throws([&buffer] { sceVoiceGetPortAttr(0x1234, voiceActivity, &buffer[1], 1); }, "unknown port")); + + Require(sceVoiceDeletePort(port) == 0); + Require(Throws([port, &buffer] { sceVoiceGetPortAttr(port, voiceActivity, &buffer[1], 1); }, "unknown port")); + + Require(sceVoiceEnd_nid_postfix() == 0); + return 0; +} diff --git a/docs/dev/debt/libSceVoice.md b/docs/dev/debt/libSceVoice.md index e86f72ff0f..0a16024b49 100644 --- a/docs/dev/debt/libSceVoice.md +++ b/docs/dev/debt/libSceVoice.md @@ -1,5 +1,9 @@ # libSceVoice technical debt +### Silent stubs + +- [sceVoiceGetPortAttr](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - attribute 1001 writes one zero byte and returns 0 for an existing port; the backend processes no voice audio. PPSA34501 requests size 1 and reads one byte on success, and this fallback avoids its startup exception. The name follows attribute 1001 (`CELLVOICE_ATTR_VAD`) (Voice Activity Detection) in [RPCS3](https://github.com/RPCS3/rpcs3/blob/master/rpcs3/Emu/Cell/Modules/cellVoice.h), not confirmed for the PS5. The correct result, eligible port types and behaviour are unknown; + ### Unknown function info - [sceVoiceResetPort](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - the `(uint32_t portId)` signature is the PS Vita one in [VitaSDK](https://github.com/vitasdk/vita-headers/blob/ae5808f3105c42fa3373ba4ab64ab54e2e3ca848/include/psp2/voice.h#L503-L514) and the PS3 `cellVoiceResetPort` in [RPCS3](https://github.com/RPCS3/rpcs3/blob/be69c054eeac57e2e671697a463473d37fe5a7b2/rpcs3/Emu/Cell/Modules/cellVoice.cpp#L493-L510); that the PS5 library takes the same argument is assumed. A reset clears a port's buffered data, and ports here buffer nothing (`sceVoiceReadFromOPort` returns 0 bytes and `sceVoiceWriteToIPort` throws), so it only checks that the library is initialized and the port exists, as RPCS3 does, and keeps the port's connections, volume and mute flag. An uninitialized library or an unknown port throws like the other port calls; the console's error codes are not measured. PPSA23566 imports it From 32b714c80dc208444130a1f6123c00a48482e47c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Fri, 16 Oct 2026 21:01:35 +0200 Subject: [PATCH 0567/1000] docs(debt): end the libSceVoice attribute 1001 line with a period --- docs/dev/debt/libSceVoice.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceVoice.md b/docs/dev/debt/libSceVoice.md index 0a16024b49..bd90c772f6 100644 --- a/docs/dev/debt/libSceVoice.md +++ b/docs/dev/debt/libSceVoice.md @@ -2,7 +2,7 @@ ### Silent stubs -- [sceVoiceGetPortAttr](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - attribute 1001 writes one zero byte and returns 0 for an existing port; the backend processes no voice audio. PPSA34501 requests size 1 and reads one byte on success, and this fallback avoids its startup exception. The name follows attribute 1001 (`CELLVOICE_ATTR_VAD`) (Voice Activity Detection) in [RPCS3](https://github.com/RPCS3/rpcs3/blob/master/rpcs3/Emu/Cell/Modules/cellVoice.h), not confirmed for the PS5. The correct result, eligible port types and behaviour are unknown; +- [sceVoiceGetPortAttr](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - attribute 1001 writes one zero byte and returns 0 for an existing port; the backend processes no voice audio. PPSA34501 requests size 1 and reads one byte on success, and this fallback avoids its startup exception. The name follows attribute 1001 (`CELLVOICE_ATTR_VAD`) (Voice Activity Detection) in [RPCS3](https://github.com/RPCS3/rpcs3/blob/master/rpcs3/Emu/Cell/Modules/cellVoice.h), not confirmed for the PS5. The correct result, eligible port types and behaviour are unknown. ### Unknown function info From 2b056a4795e3f6834f0822f409cd3485faf880e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Fri, 16 Oct 2026 21:02:18 +0200 Subject: [PATCH 0568/1000] ci: keep the build paths CMake recorded in select_tests.py Co-Authored-By: Claude --- tools/select_tests.py | 6 +++--- tools/tests/test_select_tests.py | 4 +++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/tools/select_tests.py b/tools/select_tests.py index 0fb850dee8..ef0e2bddef 100644 --- a/tools/select_tests.py +++ b/tools/select_tests.py @@ -15,12 +15,12 @@ BATCH = 20000 class Build: def __init__(self, path): - self.path = os.path.realpath(path) cache = {} - for line in (Path(self.path) / "CMakeCache.txt").read_text(encoding="utf-8").splitlines(): + for line in (Path(path) / "CMakeCache.txt").read_text(encoding="utf-8").splitlines(): name, _, value = line.partition("=") cache[name.partition(":")[0]] = value - self.source = os.path.realpath(cache["CMAKE_HOME_DIRECTORY"]) + self.path = os.path.normpath(cache["CMAKE_CACHEFILE_DIR"]) + self.source = os.path.normpath(cache["CMAKE_HOME_DIRECTORY"]) self.program = cache.get("CMAKE_MAKE_PROGRAM") or "ninja" self.encoding = "utf-8" if sys.platform == "win32": diff --git a/tools/tests/test_select_tests.py b/tools/tests/test_select_tests.py index 87349e729d..384e77742a 100644 --- a/tools/tests/test_select_tests.py +++ b/tools/tests/test_select_tests.py @@ -33,7 +33,9 @@ class SelectTests(unittest.TestCase): @classmethod def setUpClass(cls): cls.directory = tempfile.TemporaryDirectory() - root = Path(cls.directory.name) + (Path(cls.directory.name) / "real").mkdir() + (Path(cls.directory.name) / "link").symlink_to("real", target_is_directory=True) + root = Path(cls.directory.name) / "link" cls.source, cls.build, cls.graph = root / "source", root / "build", root / "graph.json" for name, content in PROJECT.items(): (cls.source / name).parent.mkdir(parents=True, exist_ok=True) From d707066f656c58a3fed7d0e82aca4f1c52eb00d9 Mon Sep 17 00:00:00 2001 From: fdfawef324 <287205748+fdfawef324@users.noreply.github.com> Date: Fri, 16 Oct 2026 15:07:46 -0400 Subject: [PATCH 0569/1000] test(agc): make the null-image fixture module and layout agree The stencil null feedback fixture splices a storage image array into the fragment module by hand. spirv-val rejects the result with "Decorate is in an invalid layout section": the decorations were inserted in front of the first OpTypeVoid, which the recompiler emits after its Input and Output variables. The binding also used set 1 with the vertex-group number of a different heap kind (the 1D array one, not the 2D image the module declares), so the set layout the driver builds from the binding metadata (binding / 65 picks the set) put it in set 0 while the module declared set 1. Insert the decorations after the existing annotations, declare the image array type the recompiler emits for a float 2D storage heap, list the variable in the SPIR-V 1.4 entry point interface like the recompiler does, and number the binding as the recompiler does for the fragment stage, in the descriptor set the target uses. When built with ANYPS5_ENABLE_SPIRV_TOOLS the fixture validates its module against the Vulkan 1.1 environment. Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 4 + .../tests/execution/StencilNullFeedback.cpp | 119 ++++++++++++------ 2 files changed, 87 insertions(+), 36 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 6bdee1b0e9..0e16fc0c90 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1379,6 +1379,10 @@ foreach(agcExecutionTestName IN LISTS ANYPS5_AGC_EXECUTION_TESTS) message(FATAL_ERROR "ANYPS5_AGC_EXECUTION_TESTS names ${agcExecutionTestName}, which has no source in tests/execution") endif() endforeach() +if(ANYPS5_ENABLE_SPIRV_TOOLS) + target_compile_definitions(agc_driver_stencil_null_feedback_tests PRIVATE ANYPS5_ENABLE_SPIRV_TOOLS=1) + target_link_libraries(agc_driver_stencil_null_feedback_tests PRIVATE SPIRV-Tools-static) +endif() target_link_libraries(agc_driver_gpu_map_on_fiber_tests PRIVATE libSceFiber) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/StencilNullFeedback.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/StencilNullFeedback.cpp index befa0444a8..389c56e3f4 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/StencilNullFeedback.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/StencilNullFeedback.cpp @@ -13,6 +13,9 @@ #include #include #include +#if ANYPS5_ENABLE_SPIRV_TOOLS +#include +#endif namespace AgcDriver { @@ -89,13 +92,71 @@ ShaderRecompiler::ShaderPixelStageInfo PixelInfo(std::uint32_t shaderControl) { return AgcDriver::Graphics::DecodePixelStageInfo(context, {0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u}); } -void AttachNullImages(ShaderRecompiler::RecompileResult& result, bool depthAlias = false) { +constexpr std::uint32_t ImageElements = 2u; +constexpr std::uint32_t StorageFloat2DHeap = ShaderRecompiler::RuntimeAbi::FirstStorageImageBinding + 2u; + +void Emit(std::vector& output, spv::Op opcode, std::initializer_list operands) { + output.push_back((static_cast(operands.size() + 1u) << spv::WordCountShift) | static_cast(opcode)); + output.insert(output.end(), operands.begin(), operands.end()); +} + +bool StartsDeclarations(spv::Op opcode) { + return opcode == spv::OpUndef || opcode == spv::OpVariable || (opcode >= spv::OpTypeVoid && opcode <= spv::OpTypeForwardPointer) || (opcode >= spv::OpConstantTrue && opcode <= spv::OpSpecConstantOp); +} + +void DeclareImageHeap(std::vector& module, std::uint32_t set, std::uint32_t binding, std::uint32_t elements) { + Require(module.size() >= 5u && module[0] == spv::MagicNumber, "the null-image fixture module is not SPIR-V"); + std::uint32_t scalar = 0u, floating = 0u; + std::size_t entry = 0u, declarations = 0u, functions = 0u; + for (std::size_t offset = 5u; offset < module.size();) { + const auto count = module[offset] >> spv::WordCountShift; + const auto opcode = static_cast(module[offset] & spv::OpCodeMask); + Require(count != 0u && count <= module.size() - offset, "the null-image fixture module has a truncated instruction"); + if (opcode == spv::OpFunction) { + functions = offset; + break; + } + if (opcode == spv::OpEntryPoint) { + Require(entry == 0u, "the null-image fixture module has several entry points"); + entry = offset; + } + if (declarations == 0u && StartsDeclarations(opcode)) declarations = offset; + if (opcode == spv::OpTypeInt && module[offset + 2u] == 32u && module[offset + 3u] == 0u) scalar = module[offset + 1u]; + if (opcode == spv::OpTypeFloat && module[offset + 2u] == 32u) floating = module[offset + 1u]; + offset += count; + } + Require(entry != 0u && declarations != 0u && functions != 0u && floating != 0u, "the null-image fixture module has no entry point, declarations or scalar shader types"); + auto id = module[3]; + const bool declareScalar = scalar == 0u; + if (declareScalar) scalar = id++; + const auto length = id++, image = id++, array = id++, pointer = id++, variable = id++; + std::vector annotations; + Emit(annotations, spv::OpDecorate, {variable, spv::DecorationDescriptorSet, set}); + Emit(annotations, spv::OpDecorate, {variable, spv::DecorationBinding, binding}); + std::vector types; + if (declareScalar) Emit(types, spv::OpTypeInt, {scalar, 32u, 0u}); + Emit(types, spv::OpConstant, {scalar, length, elements}); + Emit(types, spv::OpTypeImage, {image, floating, spv::Dim2D, 0u, 0u, 0u, 2u, spv::ImageFormatUnknown}); + Emit(types, spv::OpTypeArray, {array, image, length}); + Emit(types, spv::OpTypePointer, {pointer, spv::StorageClassUniformConstant, array}); + Emit(types, spv::OpVariable, {pointer, variable, spv::StorageClassUniformConstant}); + module.insert(module.begin() + static_cast(functions), types.begin(), types.end()); + module.insert(module.begin() + static_cast(declarations), annotations.begin(), annotations.end()); + if (module[1] >= 0x00010400u) { + module.insert(module.begin() + static_cast(entry + (module[entry] >> spv::WordCountShift)), variable); + module[entry] += 1u << spv::WordCountShift; + } + module[3] = id; +} + +void AttachNullImages(ShaderRecompiler::RecompileResult& result, const ShaderRecompiler::SpirvTarget& target, bool depthAlias = false) { namespace Runtime = ShaderRecompiler::RuntimeAbi; const auto address = static_cast(reinterpret_cast(depthAlias ? DepthMemory.data() : NullBindingTexture.data())); const auto format = depthAlias ? 22u : 56u; const std::array words{static_cast(address >> 8u), static_cast(address >> 40u) | (format << 20u) | (((Width - 1u) & 3u) << 30u), ((Width - 1u) >> 2u) | ((Height - 1u) << 14u), 0xfacu | (9u << 28u), 0u, 0u, 0u, 0u}; - constexpr auto binding = Runtime::FirstStorageImageBinding + 1u; - ShaderRecompiler::DescriptorBinding images{ShaderRecompiler::DescriptorKind::StorageImage, ShaderRecompiler::DescriptorRole::GuestImages, Runtime::FragmentDescriptorSet, binding, 2u, std::vector(16u)}; + const auto set = target.registrationModules ? Runtime::FragmentDescriptorSet : Runtime::DescriptorSet; + const auto binding = Runtime::BindingNumber(Runtime::Stage::Fragment, static_cast(StorageFloat2DHeap)); + ShaderRecompiler::DescriptorBinding images{ShaderRecompiler::DescriptorKind::StorageImage, ShaderRecompiler::DescriptorRole::GuestImages, set, binding, ImageElements, std::vector(ImageElements * words.size())}; std::copy(words.begin(), words.end(), images.guestDescriptor.begin()); images.imageShape = ShaderRecompiler::DescriptorImageShape::Image2D; images.imageWritten = {false, false}; @@ -103,41 +164,22 @@ void AttachNullImages(ShaderRecompiler::RecompileResult& result, bool depthAlias images.imageAtomic64 = {false, false}; result.bindings.push_back(std::move(images)); auto module = result.spirv.Copy(); - auto id = module[3]; - std::uint32_t scalar = 0u, floating = 0u; - std::size_t types = 0u, functions = 0u; - for (std::size_t offset = 5u; offset < module.size(); offset += module[offset] >> 16u) { - const auto opcode = module[offset] & 0xffffu; - if (types == 0u && opcode == spv::OpTypeVoid) types = offset; - if (opcode == spv::OpTypeInt && module[offset + 2u] == 32u && module[offset + 3u] == 0u) scalar = module[offset + 1u]; - if (opcode == spv::OpTypeFloat && module[offset + 2u] == 32u) floating = module[offset + 1u]; - if (opcode == spv::OpFunction) { functions = offset; break; } - } - Require(types != 0u && functions != 0u && floating != 0u, "the null-image fixture has no scalar shader types"); - const bool declareScalar = scalar == 0u; - if (declareScalar) scalar = id++; - const auto count = id++, image = id++, array = id++, pointer = id++, variable = id++; - const auto emit = [](std::vector& output, spv::Op opcode, std::initializer_list operands) { - output.push_back((static_cast(operands.size() + 1u) << 16u) | static_cast(opcode)); - output.insert(output.end(), operands.begin(), operands.end()); - }; - std::vector annotations; - emit(annotations, spv::OpDecorate, {variable, spv::DecorationDescriptorSet, Runtime::FragmentDescriptorSet}); - emit(annotations, spv::OpDecorate, {variable, spv::DecorationBinding, binding}); - emit(annotations, spv::OpDecorate, {variable, spv::DecorationNonWritable}); - std::vector declarations; - if (declareScalar) emit(declarations, spv::OpTypeInt, {scalar, 32u, 0u}); - emit(declarations, spv::OpConstant, {scalar, count, 2u}); - emit(declarations, spv::OpTypeImage, {image, floating, spv::Dim2D, 0u, 0u, 0u, 2u, depthAlias ? spv::ImageFormatR32f : spv::ImageFormatRgba8}); - emit(declarations, spv::OpTypeArray, {array, image, count}); - emit(declarations, spv::OpTypePointer, {pointer, spv::StorageClassUniformConstant, array}); - emit(declarations, spv::OpVariable, {pointer, variable, spv::StorageClassUniformConstant}); - module.insert(module.begin() + functions, declarations.begin(), declarations.end()); - module.insert(module.begin() + types, annotations.begin(), annotations.end()); - module[3] = id; + DeclareImageHeap(module, set, binding, ImageElements); result.spirv = std::move(module); } +#if ANYPS5_ENABLE_SPIRV_TOOLS +void RequireValidModule(const ShaderRecompiler::RecompileResult& result, const ShaderRecompiler::SpirvTarget& target) { + std::string diagnostics; + spvtools::SpirvTools validator(target.spirvVersion >= 0x00010400u ? SPV_ENV_VULKAN_1_1_SPIRV_1_4 : SPV_ENV_VULKAN_1_1); + validator.SetMessageConsumer([&](spv_message_level_t, const char*, const spv_position_t&, const char* message) { + if (message != nullptr) diagnostics += std::string(message) + '\n'; + }); + const auto& words = result.spirv.Words(); + const bool valid = validator.Validate(words.data(), words.size()); + Require(valid, "the null-image fixture emitted invalid SPIR-V:\n" + diagnostics); +} +#endif void Draw(AgcDriver::VulkanDevice& device, std::span pixelCode, std::uint32_t shaderControl, const VkStencilOpState& stencil, std::uint32_t vertices = 3u, std::uint32_t instances = 1u, bool depthFail = false, const std::function& alter = {}, const std::function& alterDraw = {}, std::span> vertexData = Vertices, std::span vertexCode = VertexCode, std::span pixelData = {}, std::span pixelSrt = {}, bool nullImages = false, bool depthAlias = false) { Pixels.fill(Kept); @@ -171,7 +213,12 @@ void Draw(AgcDriver::VulkanDevice& device, std::span pixelC }; fragment.useCache = false; auto pixelResult = ShaderRecompiler::Recompile(fragment); - if (nullImages) AttachNullImages(pixelResult, depthAlias); + if (nullImages) { + AttachNullImages(pixelResult, target, depthAlias); +#if ANYPS5_ENABLE_SPIRV_TOOLS + RequireValidModule(pixelResult, target); +#endif + } if (!pixelResult.stencilReferences.empty()) { Require(pixelResult.stencilReferenceDword * sizeof(std::uint32_t) + sizeof(std::uint32_t) == pixelResult.pushConstants.size(), "dynamic reference is not reserved after the stage payload"); for (std::size_t cursor = 5; cursor < pixelResult.spirv.size();) { From a0c51d5778647e3f9b3eb8b028b2d9e257a18ca3 Mon Sep 17 00:00:00 2001 From: Romans Pokrovskis Date: Wed, 14 Oct 2026 22:21:32 +0300 Subject: [PATCH 0570/1000] feat(agc): import uniform multisampled DCC fast clears --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 14 +- .../Graphics/include/MultisampleTarget.hpp | 3 + .../Graphics/include/State.hpp | 1 + .../Graphics/src/MultisampleTarget.cpp | 112 +++++++++- .../libSceAgcDriver/Graphics/src/State.cpp | 6 +- .../prx/libSceAgcDriver/tests/Graphics.cpp | 60 +++++- .../tests/execution/AlphaToCoverage.cpp | 200 ++++++++++++++++++ docs/dev/debt/libSceAgcDriver.md | 2 + 8 files changed, 384 insertions(+), 14 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index f112af6ade..ac0751020c 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1417,18 +1417,28 @@ target_include_directories(agc_driver_sample_multisample_target_tests PRIVATE $< if(BUILD_TESTING) add_test(NAME agc_driver_undithered_alpha_to_coverage_state COMMAND agc_driver_graphics_tests --undithered-alpha-to-coverage-state) + add_test(NAME agc_driver_msaa_dcc_state COMMAND agc_driver_graphics_tests --multisample-dcc-state) set_tests_properties(agc_driver_undithered_alpha_to_coverage_state PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") add_test(NAME agc_driver_alpha_to_coverage_state COMMAND agc_driver_graphics_tests --alpha-to-coverage-state) set_tests_properties(agc_driver_alpha_to_coverage_state PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") add_test(NAME agc_driver_dithered_alpha_to_coverage COMMAND agc_driver_alpha_to_coverage_tests --dithered8) + set(multisampledDccClearTests) + foreach(samples IN ITEMS 2 4 8) + set(testName "agc_driver_msaa_dcc_clear_${samples}") + add_test(NAME ${testName} COMMAND agc_driver_alpha_to_coverage_tests --dcc-fast-clear ${samples}) + list(APPEND multisampledDccClearTests ${testName}) + endforeach() + set_tests_properties(agc_driver_msaa_dcc_state ${multisampledDccClearTests} PROPERTIES + ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + set_tests_properties(${multisampledDccClearTests} PROPERTIES SKIP_RETURN_CODE 77) set_tests_properties(agc_driver_dithered_alpha_to_coverage PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") if(WIN32) - set_tests_properties(agc_driver_alpha_to_coverage_state agc_driver_undithered_alpha_to_coverage_state PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(agc_driver_alpha_to_coverage_state agc_driver_undithered_alpha_to_coverage_state agc_driver_msaa_dcc_state PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") - set_tests_properties(agc_driver_dithered_alpha_to_coverage PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(agc_driver_dithered_alpha_to_coverage ${multisampledDccClearTests} PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() endif() diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp index c7b8e44aba..98d65e8c03 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp @@ -8,6 +8,7 @@ #include #include #include +#include #include namespace AgcDriver::Graphics { @@ -49,6 +50,8 @@ private: }; std::uint64_t MultisampledColorBytes(const ColorTarget& target); +std::size_t MultisampledDccBytes(const ColorTarget& target); +VkClearColorValue MultisampledDccClear(const ColorTarget& target, std::span dcc, std::span cmask); bool MultisampledFillClearColor(const ColorTarget& target, std::uint32_t fill, VkClearColorValue& clear); bool TakeMultisampledClear(MultisampledCmask& cmask, MultisampledColorFill& colorFill, const ColorTarget& use, VkClearColorValue& clear); VkImageView MultisampleTargetView(const Context& context, const ColorTarget& target); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp index b9844764d0..c2a2c0510b 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp @@ -67,6 +67,7 @@ struct ColorTarget { bool uintExport = false; std::uint32_t pipeBankXor = 0; std::uint32_t samples = 1; + std::uint32_t dccControl = 0; }; struct DepthTarget { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp index 65e55692aa..5fc7544a28 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp @@ -53,6 +53,8 @@ class MultisampleTarget { public: MultisampleTarget(const Context& context, const ColorTarget& target) : context(context), target(target), cmask(target.cmaskAddress, target.cmaskBytes), colorFill(target) { this->context.bufferPool.reset(); + if (target.dccAddress != 0) metadataValid = std::make_shared>(true); + const auto initial = initialDccClear(target); const auto samples = static_cast(target.samples); Require((context.limits.framebufferColorSampleCounts & samples) != 0, "the device cannot render color with the target's sample count"); VkFormatProperties properties{}; @@ -78,9 +80,8 @@ public: view = created.view; recordCommands(context, [&](VkCommandBuffer commands, Recorder* recorder) { recordToGeneral(commands, image); - const VkClearColorValue zero{}; const VkImageSubresourceRange range{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; - context.Resolved(&DeviceFunctions::cmdClearColorImage, "vkCmdClearColorImage")(commands, image, VK_IMAGE_LAYOUT_GENERAL, &zero, 1, &range); + context.Resolved(&DeviceFunctions::cmdClearColorImage, "vkCmdClearColorImage")(commands, image, VK_IMAGE_LAYOUT_GENERAL, &initial, 1, &range); RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT); if (recorder != nullptr) Recorder::CountBarriers(Recorder::CommandClass::Draw, 2); }); @@ -176,6 +177,7 @@ public: } void ApplyFastClear(const ColorTarget& use) { + checkDccCurrent(use); VkClearColorValue clear{}; if (!TakeMultisampledClear(cmask, colorFill, use, clear)) return; recordCommands(context, [&](VkCommandBuffer commands, Recorder* recorder) { @@ -278,6 +280,7 @@ public: Context context; std::shared_ptr> active = std::make_shared>(true); + std::shared_ptr> metadataValid; ColorTarget target; VkImage image = VK_NULL_HANDLE; VkDeviceMemory memory = VK_NULL_HANDLE; @@ -296,6 +299,55 @@ private: VkImageView storageView = VK_NULL_HANDLE; bool storageWritten = false; std::map, std::shared_ptr> textures; + std::vector dccRanges; + + VkClearColorValue initialDccClear(const ColorTarget& use) { + if (use.dccAddress == 0) return {}; + const auto dccBytes = MultisampledDccBytes(use); + Require((use.dccAddress & 4095u) == 0, "multisampled DCC metadata base alignment is not implemented"); + Require(use.cmaskAddress == 0 ? use.cmaskBytes == 0 : use.cmaskBytes == CmaskBytes(use.extent.width, use.extent.height), "multisampled DCC CMASK footprint is invalid"); + const auto colorBytes = MultisampledColorBytes(use); + Require(colorBytes != 0, "multisampled DCC color footprint is not implemented"); + dccRanges.push_back({use.address, static_cast(colorBytes), 0}); + dccRanges.push_back({use.dccAddress, dccBytes, 0}); + if (use.cmaskAddress != 0) dccRanges.push_back({use.cmaskAddress, use.cmaskBytes, 0}); + for (auto& range : dccRanges) { + GuestMemory::CheckRange(reinterpret_cast(range.address), range.bytes, 256); + Require(GuestMemory::Watched(range.address, range.bytes), "initial multisampled DCC contents require watched guest memory"); + range.generation = GuestMemory::CollectWritesUncached(range.address, range.bytes); + Require(range.generation != 0, "initial multisampled DCC contents have no write generation"); + } + for (std::size_t first = 0; first < dccRanges.size(); ++first) { + for (std::size_t second = first + 1; second < dccRanges.size(); ++second) { + const auto& a = dccRanges[first]; + const auto& b = dccRanges[second]; + Require(a.address >= b.address + b.bytes || b.address >= a.address + a.bytes, "overlapping multisampled DCC color or metadata ranges are not implemented"); + } + } + std::vector dcc(dccRanges[1].bytes); + GuestMemory::Read(use.dccAddress, dcc); + std::vector cmask(use.cmaskBytes); + if (!cmask.empty()) GuestMemory::Read(use.cmaskAddress, cmask); + const auto clear = MultisampledDccClear(use, dcc, cmask); + Require(GuestMemory::UnchangedSinceAll(dccRanges), "initial multisampled DCC contents changed during import"); + return clear; + } + + void checkDccCurrent(const ColorTarget& use) const { + if (dccRanges.empty()) { + Require(use.dccAddress == 0, "adding DCC to a resident multisampled color target is not implemented"); + return; + } + Require(metadataValid->load(std::memory_order_acquire), "multisampled DCC metadata was released while its color target remains resident"); + static_cast(MultisampledDccBytes(use)); + Require(use.address == target.address && use.dccAddress == target.dccAddress && use.dccControl == target.dccControl && use.dccAlphaOnMsb == target.dccAlphaOnMsb && use.dccPipeAligned == target.dccPipeAligned && use.cmaskAddress == target.cmaskAddress && use.cmaskBytes == target.cmaskBytes && use.tileMode == target.tileMode && use.elementBytes == target.elementBytes && use.pipeBankXor == target.pipeBankXor, "multisampled DCC target layout or metadata changed"); + for (const auto& range : dccRanges) { + Require(GuestMemory::Watched(range.address, range.bytes), "multisampled DCC target reuse requires watched guest memory"); + GuestMemory::CollectWritesUncached(range.address, range.bytes); + Require(GuestMemory::UnchangedSince(range.address, range.bytes, range.generation), "writes to a resident multisampled DCC target or its metadata are not implemented"); + if (auto* recorder = Recorder::Active(); recorder != nullptr) Require(!recorder->PendingWriteOverlaps(range.address, range.bytes), "pending writes to a resident multisampled DCC target or its metadata are not implemented"); + } + } void release() noexcept { textures.clear(); @@ -312,6 +364,7 @@ struct GuardedRange { std::uint64_t bytes; std::uint32_t samples; std::shared_ptr> active; + std::shared_ptr> metadataValid; }; class PrimedSharedMutex : public std::shared_mutex { @@ -345,14 +398,16 @@ void retireUnmapped(std::uintptr_t address, std::size_t bytes) { RetireUnmappedDepth_nid_no_patch(address, bytes); std::unique_lock lock(guardedMutex()); std::erase_if(guarded(), [&](const auto& range) { + if (range.metadataValid && address < range.address + range.bytes && range.address < address + bytes) range.metadataValid->store(false, std::memory_order_release); if (range.address < address || range.address - address > bytes || range.bytes > bytes - (range.address - address)) return false; - range.active->store(false, std::memory_order_release); + if (!range.metadataValid) range.active->store(false, std::memory_order_release); return true; }); + std::erase_if(guarded(), [](const auto& range) { return range.metadataValid && !range.active->load(std::memory_order_acquire); }); anyGuarded().store(!guarded().empty(), std::memory_order_release); } -void guard(const Context& context, const ColorTarget& target, const std::shared_ptr>& active) { +void guard(const Context& context, const ColorTarget& target, const std::shared_ptr>& active, const std::shared_ptr>& metadataValid) { auto bytes = std::max(target.bytes * target.samples, static_cast(target.extent.width) * target.extent.height * target.elementBytes * target.samples); if (target.tileMode == ColorTileMode::RenderTarget && std::has_single_bit(target.elementBytes) && std::has_single_bit(target.samples)) { const auto log2Samples = std::countr_zero(target.samples); @@ -365,6 +420,10 @@ void guard(const Context& context, const ColorTarget& target, const std::shared_ } std::unique_lock lock(guardedMutex()); guarded().push_back({context.device, target.address, bytes, target.samples, active}); + if (target.dccAddress != 0) { + guarded().push_back({context.device, target.dccAddress, MultisampledDccBytes(target), target.samples, active, metadataValid}); + if (target.cmaskAddress != 0) guarded().push_back({context.device, target.cmaskAddress, target.cmaskBytes, target.samples, active, metadataValid}); + } anyGuarded().store(true, std::memory_order_release); InstallResidentImageReadGuard_nid_no_patch(); } @@ -394,8 +453,8 @@ MultisampleTarget& acquire(const Context& context, const ColorTarget& use) { } targets().push_back(std::make_unique(context, use)); auto& created = *targets().back(); - guard(context, use, created.active); - if (use.cmaskFastClear) created.cmask.SeedKeys(CurrentDccKeys(use.cmaskAddress, static_cast(use.cmaskBytes) * 256u)); + guard(context, use, created.active, created.metadataValid); + if (use.cmaskFastClear && use.dccAddress == 0) created.cmask.SeedKeys(CurrentDccKeys(use.cmaskAddress, static_cast(use.cmaskBytes) * 256u)); created.ApplyFastClear(use); return created; } @@ -444,7 +503,7 @@ MultisampleTarget& createFilled(const Context& context, const GuestTextureResour if (!MultisampledFillClearColor(use, value, clear)) refuse(std::string("whose color memory ") + range + " holds " + held + ", which has no exact clear value in its format"); targets().push_back(std::make_unique(context, use)); auto& created = *targets().back(); - guard(context, use, created.active); + guard(context, use, created.active, created.metadataValid); created.colorFill.NoteFill(use.address, static_cast(bytes), value); created.fromTexture = true; return created; @@ -519,6 +578,45 @@ std::uint64_t MultisampledColorBytes(const ColorTarget& target) { return ((target.extent.width + blockWidth - 1) / blockWidth) * ((target.extent.height + blockHeight - 1) / blockHeight) << blockLog2; } +std::size_t MultisampledDccBytes(const ColorTarget& target) { + Require(target.samples == 2 || target.samples == 4 || target.samples == 8, "unsupported multisampled DCC fragment count"); + Require(target.tileMode == ColorTileMode::RenderTarget && target.dccPipeAligned, "multisampled DCC requires pipe-aligned SW_64KB_R_X"); + Require(std::has_single_bit(target.elementBytes) && target.elementBytes <= 16, "unsupported multisampled DCC element size"); + Require(target.extent.width != 0 && target.extent.width <= 16384 && target.extent.height != 0 && target.extent.height <= 16384, "invalid multisampled DCC extent"); + Require(target.mipCount == 1 && target.mip == 0 && !target.mipTail && target.depth == 1 && target.depthSlice == 0 && target.arrayLastSlice == 0 && target.arraySlice == 0 && target.layers == 1, "multisampled DCC array, volume or mip contents are not implemented"); + const auto bits = 19u - static_cast(std::countr_zero(target.elementBytes)); + const std::uint64_t width = std::uint64_t{1} << ((bits + 1u) / 2u); + const std::uint64_t height = std::uint64_t{1} << (bits / 2u); + return static_cast(((target.extent.width + width - 1) / width) * ((target.extent.height + height - 1) / height) * 4096u); +} + +VkClearColorValue MultisampledDccClear(const ColorTarget& target, std::span dcc, std::span cmask) { + Require(target.dccAddress != 0 && (target.dccControl & 0x3ff80u) == 0 && (target.pipeBankXor & ~0xcf00u) == 0, "multisampled DCC address, color transform, lossy mode or XOR is not implemented"); + Require(dcc.size() == MultisampledDccBytes(target), "multisampled DCC metadata snapshot size mismatch"); + Require(std::ranges::all_of(dcc, [&](std::byte key) { return key == dcc.front(); }), "mixed multisampled DCC metadata is not implemented"); + Require(target.cmaskAddress == 0 ? cmask.empty() && target.cmaskBytes == 0 : cmask.size() == target.cmaskBytes && target.cmaskBytes == CmaskBytes(target.extent.width, target.extent.height), "multisampled DCC CMASK snapshot size mismatch"); + if (!cmask.empty()) { + const auto code = std::to_integer(cmask.front()); + const auto expanded = target.samples == 2 ? 0xddu : target.samples == 4 ? 0xeeu : 0xffu; + Require(std::ranges::all_of(cmask, [&](std::byte key) { return key == cmask.front(); }) && (code == 0xccu || code == expanded), "multisampled DCC CMASK compression or register clear is not implemented"); + } + DccKeys keys; + switch (std::to_integer(dcc.front())) { + case 0x00u: keys = DccKeys::Clear0000; break; + case 0x40u: keys = DccKeys::Clear0001; break; + case 0x80u: keys = DccKeys::Clear1110; break; + case 0xc0u: keys = DccKeys::Clear1111; break; + default: throw std::runtime_error("initial multisampled DCC contents are not a supported uniform fast clear"); + } + std::array texels{}; + Require(FillDccClear(target.format, keys, target.dccAlphaOnMsb, texels), "multisampled DCC clear format is not implemented"); + std::array pattern{}; + std::memcpy(pattern.data(), texels.data(), texels.size()); + VkClearColorValue clear{}; + Require(ClearColorForTexel(target.format, target.elementBytes, pattern, clear), "multisampled DCC fast clear has no exact host value"); + return clear; +} + bool MultisampledFillClearColor(const ColorTarget& target, std::uint32_t fill, VkClearColorValue& clear) { const std::array pattern{fill, fill, fill, fill}; VkFormat unorm = VK_FORMAT_UNDEFINED; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index a3c166d395..e6bc55ce8f 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -1440,7 +1440,6 @@ ColorTarget DecodeColorBuffer(const Registers& cx, std::uint32_t slot, bool allo Require(color.samples <= 8u, "color targets with more than eight samples are unsupported"); Require((1u << ((attrib >> 15u) & 3u)) == color.samples, "color targets with fewer fragments than samples (EQAA) are unsupported"); const auto multisampleInfo = color.samples > 1 ? 0x0c006000u : 0u; - Require(allowMultisampleDcc || color.samples == 1 || (info & 0x10000000u) == 0, "DCC over a multisampled color target is unsupported"); // ROUND_MODE (bit 18) only affects unorm rounding. With DCC_ENABLE (bit 28) the target is written if ((info & ~(0x00039f7cu | 0x00040000u | 0x10000000u | 0x00002000u | multisampleInfo)) != 0) throw std::runtime_error("AGC graphics: color compression, DCC, endian conversion, nonstandard rounding or color optimization is unsupported (CB_COLOR_INFO 0x" + [&] { char text[16]; std::snprintf(text, sizeof(text), "%08x", info); return std::string(text); }() + ")"); Require((info & 0x8000u) != 0 || number == 7 || number == 4 || number == 5, "unclamped normalized color is unsupported"); @@ -1540,6 +1539,11 @@ ColorTarget DecodeColorBuffer(const Registers& cx, std::uint32_t slot, bool allo color.dccAddress = ((dccHigh == cx.end() ? 0ull : static_cast(dccHigh->second & 0xffu)) << 40u) | (static_cast(read(cx, 0x325 + stride)) << 8u); color.dccAlphaOnMsb = DccAlphaOnMsb(color.format, swap); color.dccPipeAligned = ((attrib3 >> 30u) & 1u) != 0; + if (color.samples > 1) { + if (!allowMultisampleDcc) Require(color.tileMode == ColorTileMode::RenderTarget && color.dccAddress != 0 && color.dccPipeAligned && !volume && color.arrayLastSlice == 0 && slice == 0, "multisampled DCC targets require pipe-aligned single-layer SW_64KB_R_X"); + color.dccControl = read(cx, 0x31e + stride); + if (!allowMultisampleDcc) Require((color.dccControl & 0x3ff80u) == 0, "multisampled DCC color transforms or lossy modes are not implemented"); + } } else { static bool reported = false; if (!reported) { diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index d157121472..e2f1c24796 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -2938,8 +2938,9 @@ void multisampleTests() { expectFailure([&] { DecodeState(queue); }, "without a CMASK address"); queue = multisampled(); queue.context[0x31c] |= 0x10000000u; - expectFailure([&] { DecodeState(queue); }, "DCC over a multisampled color target is unsupported"); - expectFailure([&] { DecodeColorBuffer(queue.context, 0); }, "DCC over a multisampled color target is unsupported"); + queue.context[0x325] = 0; + expectFailure([&] { DecodeState(queue); }, "multisampled DCC targets require pipe-aligned single-layer SW_64KB_R_X"); + expectFailure([&] { DecodeColorBuffer(queue.context, 0); }, "multisampled DCC targets require pipe-aligned single-layer SW_64KB_R_X"); queue = multisampled(); queue.context[0x31c] |= 0x2000; setCmaskAddress(queue); @@ -2984,7 +2985,8 @@ void multisampleTests() { expectFailure([&] { DecodeColorMetadataPass(queue); }, "different formats or extents"); queue = rasterizedResolve; queue.context[0x31c] |= 0x10000000u; - expectFailure([&] { DecodeColorMetadataPass(queue); }, "DCC over a multisampled color target is unsupported"); + queue.context[0x325] = 0; + expectFailure([&] { DecodeColorMetadataPass(queue); }, "multisampled DCC targets require pipe-aligned single-layer SW_64KB_R_X"); queue = multisampled(); queue.context[0x0] = 0; queue.context[0x202] = 0xcc0030; @@ -3001,13 +3003,15 @@ void multisampleTests() { queue.context[0x202] = 0xcc0020; expectFailure([&] { DecodeColorMetadataPass(queue); }, "multisampled color target"); queue.context[0x31c] |= 0x10000000u; - expectFailure([&] { DecodeColorMetadataPass(queue); }, "DCC over a multisampled color target is unsupported"); + queue.context[0x325] = 0; + expectFailure([&] { DecodeColorMetadataPass(queue); }, "multisampled DCC targets require pipe-aligned single-layer SW_64KB_R_X"); queue = multisampled(); queue.context[0x2f8] = 0; queue.context[0x0] = 0; queue.context[0x202] = 0x00cc0050; queue.context[0x31c] = 0x1002e028; queue.context[0x31d] = 0x00012000; + queue.context[0x31e] = 0x00100038u; queue.context[0x325] = 0x2000; queue.context[0x323] = 0x3f800000; queue.context[0x324] = 0x3f800000; @@ -3015,6 +3019,9 @@ void multisampleTests() { auto fmaskPass = DecodeColorMetadataPass(queue); Require(fmaskPass.has_value() && fmaskPass->mode == ColorMetadataPass::Mode::FmaskDecompress && fmaskPass->targets.size() == 1 && fmaskPass->targets[0].samples == 4 && fmaskPass->targets[0].cmaskAddress == cmaskAddress && fmaskPass->targets[0].dccAddress != 0, "an FMASK decompress of a 4x multisampled target with CMASK and DCC did not decode"); Require(fmaskPass->targets[0].clearWords[0] == 0x3f800000 && fmaskPass->targets[0].clearWords[1] == 0x3f800000, "fast clear words changed"); + Require(fmaskPass->targets[0].dccControl == 0x00100038u, "FMASK decompress lost its DCC control"); + queue.context[0x31e] |= 0x80u; + Require(DecodeColorMetadataPass(queue)->targets[0].dccControl == 0x001000b8u, "FMASK decompress hid an unsupported DCC control from the importer"); queue = makeState(); queue.context[0x0] = 0; queue.context[0x202] = 0x00cc0050; @@ -3126,6 +3133,46 @@ void multisampleTests() { Require(TakeMultisampledClear(clearedCmask, pending, halfFloat, clear) && clear.float32[0] == 1.0f && !TakeMultisampledClear(clearedCmask, pending, halfFloat, clear), "a pending CMASK clear and a whole fill did not clear once to the CMASK clear color"); } +void multisampleDccStateTests() { + static std::array color{}; + const auto address = (reinterpret_cast(color.data()) + 65535u) & ~std::uintptr_t{65535u}; + for (const auto log2 : {1u, 2u, 3u}) { + auto queue = makeState(); + queue.context[0x318] = static_cast(address >> 8u); + queue.context[0x390] = static_cast(address >> 40u); + queue.context[0x31c] |= 0x10000000u; + queue.context[0x31d] = (log2 << 12u) | (log2 << 15u); + queue.context[0x31e] = 0x00100038u; + queue.context[0x325] = 0x1234u; + queue.context[0x3b8] = 0x4dc6c000u; + queue.context[0x2f8] = (log2 << 20u) | log2; + for (std::uint32_t pixel = 0; pixel < 4; ++pixel) { + queue.context[0x2fe + pixel * 4u] = 0; + queue.context[0x2ff + pixel * 4u] = 0; + } + queue.context[0x292] = 3; + queue.context[0x1b3] = queue.context[0x1b4] = 2; + const auto state = AgcDriver::Graphics::DecodeState(queue); + Require(state.colors.size() == 1 && state.colors.front().samples == (1u << log2) && state.colors.front().dccAddress == 0x123400u && state.colors.front().dccPipeAligned, "normal-render multisampled DCC state did not decode"); + Require(AgcDriver::Graphics::DrawRejection(queue, false).empty(), "normal-render multisampled DCC state failed the draw precheck"); + auto changed = queue; + changed.context[0x31e] |= 0x80u; + expectFailure([&] { AgcDriver::Graphics::DecodeState(changed); }, "color transforms or lossy"); + changed = queue; + changed.context[0x3b8] &= ~0x40000000u; + expectFailure([&] { AgcDriver::Graphics::DecodeState(changed); }, "pipe-aligned single-layer"); + changed = queue; + changed.context[0x325] = 0; + expectFailure([&] { AgcDriver::Graphics::DecodeState(changed); }, "pipe-aligned single-layer"); + changed = queue; + changed.context[0x3b8] |= 1u; + expectFailure([&] { AgcDriver::Graphics::DecodeState(changed); }, "pipe-aligned single-layer"); + changed = queue; + changed.context[0x3b0] |= 1u << 28u; + expectFailure([&] { AgcDriver::Graphics::DecodeState(changed); }, "multisampled color mips"); + } +} + void clipDistanceTests() { auto queue = makeState(); queue.context[0x207] = 0x0040000f; @@ -7839,6 +7886,11 @@ int main(int argc, char** argv) { std::cout << "Covered-centroid and multisample state checks passed\n"; return 0; } + if (argc == 2 && std::string_view(argv[1]) == "--multisample-dcc-state") { + multisampleDccStateTests(); + std::cout << "Multisampled DCC state checks passed\n"; + return 0; + } { const AgcDriver::Graphics::Context context{}; const AgcDriver::Graphics::State state{}; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/AlphaToCoverage.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/AlphaToCoverage.cpp index 81807e14b2..d2c7fbdcc1 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/AlphaToCoverage.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/AlphaToCoverage.cpp @@ -447,12 +447,212 @@ void DitheredCoverage(AgcDriver::VulkanDevice& device) { AgcDriver::Graphics::ClearMultisampleTargets(device.Device()); } +void DccFastClearTests(AgcDriver::VulkanDevice& device, std::uint32_t selectedSamples) { + const ColorTargetLayout layout(Width, Height, ColorTileMode::RenderTarget, 8); + std::vector> blocks; + struct ClearTargets { + AgcDriver::VulkanDevice& device; + ~ClearTargets() { + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + device.SubmitRecorded(true); + device.WaitIdle(); + AgcDriver::Graphics::ClearMultisampleTargets(device.Device()); + AgcDriver::Graphics::ClearCachedTextures(device.Device()); + } + } clearTargets{device}; + for (const auto samples : {2u, 4u, 8u}) { + if (selectedSamples != 0 && samples != selectedSamples) continue; + for (const auto code : {0x00u, 0x40u, 0x80u, 0xc0u}) { + for (const auto alphaOnMsb : {false, true}) { + for (const auto cmaskMode : {0u, 0xccu, 0xffu}) { + blocks.push_back(std::make_unique(layout.Bytes() * samples)); + auto& source = *blocks.back(); + std::memset(source.data, 0, source.bytes); + blocks.push_back(std::make_unique(BlockBytes)); + auto& metadata = *blocks.back(); + std::memset(metadata.data, code, BlockBytes); + auto target = DecodeTarget(source, samples); + target.dccAddress = metadata.Address(); + target.dccPipeAligned = true; + target.dccAlphaOnMsb = alphaOnMsb; + if (cmaskMode != 0) { + blocks.push_back(std::make_unique(BlockBytes)); + auto& cmask = *blocks.back(); + const auto expanded = samples == 2 ? 0xddu : samples == 4 ? 0xeeu : 0xffu; + std::memset(cmask.data, cmaskMode == 0xccu ? cmaskMode : expanded, BlockBytes); + target.cmaskAddress = cmask.Address(); + target.cmaskBytes = AgcDriver::Graphics::CmaskBytes(Width, Height); + target.cmaskFastClear = true; + } + const bool colorOne = code == 0x80u || code == 0xc0u; + const bool alphaOne = code == 0x40u || code == 0xc0u; + Color initial{}; + for (std::uint32_t channel = 0; channel < 4; ++channel) initial[channel] = channel == (alphaOnMsb ? 3u : 0u) ? static_cast(alphaOne) : static_cast(colorOne); + const Color written{0.25f, 0.25f, 0.25f, 0.25f}; + if (cmaskMode != 0) device.ColorMetadataPass({AgcDriver::Graphics::ColorMetadataPass::Mode::FmaskDecompress, {target}}); + Draw(device, target, false, Float32Export, Program(written, Float32Export, 1u), false, true); + if (cmaskMode != 0) { + auto recolored = target; + recolored.clearWords = {0x3c003c00u, 0x3c003c00u}; + device.ColorMetadataPass({AgcDriver::Graphics::ColorMetadataPass::Mode::FmaskDecompress, {recolored}}); + } + blocks.push_back(std::make_unique(layout.Bytes())); + auto& resolved = *blocks.back(); + std::memset(resolved.data, 0, resolved.bytes); + AgcDriver::Graphics::ColorMetadataPass pass{}; + pass.mode = AgcDriver::Graphics::ColorMetadataPass::Mode::Resolve; + pass.source = target; + pass.targets = {DecodeTarget(resolved)}; + device.ColorMetadataPass(pass); + const auto stored = ReadBack(device, resolved); + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + for (std::uint32_t channel = 0; channel < 4; ++channel) { + const auto expected = x < ScissorWidth ? initial[channel] + (written[channel] - initial[channel]) / static_cast(samples) : initial[channel]; + const auto actual = FromHalf(stored[layout.Offset(x, y) / 2u + channel]); + Require(actual == expected, "multisampled DCC clear did not preserve undrawn samples, channels or scissor pixels"); + } + } + } + if (code == 0 && alphaOnMsb && cmaskMode == 0) { + static_cast(source.data)[0] = 0; + bool refused = false; + try { + device.ColorMetadataPass(pass); + } catch (const std::runtime_error& error) { + refused = std::string_view(error.what()).find("writes to a resident multisampled DCC") != std::string_view::npos; + } + Require(refused, "a same-value CPU color store did not invalidate multisampled DCC residency"); + } + if (code == 0x40u && alphaOnMsb && cmaskMode == 0) { + static_cast(metadata.data)[0] = 0x40u; + bool refused = false; + try { + device.ColorMetadataPass(pass); + } catch (const std::runtime_error& error) { + refused = std::string_view(error.what()).find("writes to a resident multisampled DCC") != std::string_view::npos; + } + Require(refused, "a same-value CPU DCC store did not invalidate multisampled DCC residency"); + } + if (code == 0xc0u && alphaOnMsb && cmaskMode == 0) { + AgcDriver::GuestMemory::Unwatch(metadata.Address(), 4096); + bool refused = false; + try { + device.ColorMetadataPass(pass); + } catch (const std::runtime_error& error) { + refused = std::string_view(error.what()).find("reuse requires watched guest memory") != std::string_view::npos; + } + Require(refused, "an unwatched metadata range remained eligible for multisampled DCC reuse"); + } + if (code == 0x80u && alphaOnMsb && cmaskMode == 0) { + { + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + device.SubmitRecorded(true); + device.WaitIdle(); + } + { + GuestAllocations::Mutation mutation; + mutation.Remove(metadata.data); + } + { + GuestAllocations::Mutation mutation; + mutation.Add(metadata.data, metadata.bytes, true, true, true); + } + bool readRefused = false; + try { + AgcDriver::Graphics::RequireNoMultisampleTarget(source.Address(), source.bytes, "a CPU read"); + } catch (const std::runtime_error&) { + readRefused = true; + } + Require(readRefused, "releasing DCC metadata removed the unpublished color read guard"); + bool reuseRefused = false; + try { + device.ColorMetadataPass(pass); + } catch (const std::runtime_error& error) { + reuseRefused = std::string_view(error.what()).find("metadata was released") != std::string_view::npos; + } + Require(reuseRefused, "recreated DCC metadata reused a resident color target"); + } + programCount = 0; + } + } + } + } + for (const auto samples : {2u, 4u, 8u}) { + if (selectedSamples != 0 && samples != selectedSamples) continue; + for (const auto mode : {0u, 1u, 2u, 3u, 4u, 5u, 6u, 7u}) { + blocks.push_back(std::make_unique(layout.Bytes() * samples)); + auto& source = *blocks.back(); + std::memset(source.data, 0, source.bytes); + blocks.push_back(std::make_unique(BlockBytes)); + auto& metadata = *blocks.back(); + std::memset(metadata.data, mode == 0 ? 0xff : 0, metadata.bytes); + auto target = DecodeTarget(source, samples); + target.dccAddress = metadata.Address(); + target.dccPipeAligned = true; + std::string_view expected = "not a supported uniform fast clear"; + if (mode == 1) { + metadata.data[0] = 0x40u; + expected = "mixed multisampled DCC metadata"; + } + if (mode == 2 || mode == 3 || mode == 7) { + blocks.push_back(std::make_unique(BlockBytes)); + auto& cmask = *blocks.back(); + std::memset(cmask.data, mode == 2 ? 0 : 0xcc, cmask.bytes); + if (mode == 3) cmask.data[0] = 0xdd; + target.cmaskAddress = cmask.Address(); + target.cmaskBytes = AgcDriver::Graphics::CmaskBytes(Width, Height); + target.cmaskFastClear = true; + expected = "CMASK compression or register clear"; + } + if (mode == 4) { + AgcDriver::GuestMemory::Unwatch(metadata.Address(), 4096); + expected = "initial multisampled DCC contents require watched"; + } + if (mode == 5) { + target.dccAddress = source.Address(); + expected = "overlapping multisampled DCC"; + } + if (mode == 6) { + target.dccAddress += 256u; + expected = "metadata base alignment"; + } + if (mode == 7) { + target.dccControl = 0x80u; + expected = "color transform, lossy mode"; + } + bool refused = false; + try { + if (mode == 7) device.ColorMetadataPass({AgcDriver::Graphics::ColorMetadataPass::Mode::FmaskDecompress, {target}}); + else Draw(device, target, false, Float32Export, Program({0.25f, 0.25f, 0.25f, 0.25f}, Float32Export)); + } catch (const std::runtime_error& error) { + refused = std::string_view(error.what()).find(expected) != std::string_view::npos; + } + Require(refused, "unsupported initial multisampled DCC contents were not refused explicitly"); + programCount = 0; + } + } +} + } int main(int argc, char** argv) { try { const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; + if ((argc == 2 || argc == 3) && std::string_view(argv[1]) == "--dcc-fast-clear") { + const auto samples = argc == 2 ? 0u : std::string_view(argv[2]) == "2" ? 2u : std::string_view(argv[2]) == "4" ? 4u : std::string_view(argv[2]) == "8" ? 8u : 0u; + Require(argc == 2 || samples != 0, "unknown DCC test sample count"); + try { + DccFastClearTests(*device, samples); + } catch (const std::runtime_error& error) { + if (std::string_view(error.what()) != "AGC graphics: the device cannot render color with the target's sample count") throw; + std::printf("skipped, the device lacks the requested multisample color attachments: %s\n", error.what()); + return VulkanTestSkipped; + } + std::cout << "Multisampled DCC fast clear tests passed\n"; + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--dithered8") { try { DitheredCoverage(*device); diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index ee8be5c195..b36be4ab30 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -43,6 +43,8 @@ ### Functional +- [Native multisampled DCC fast clears](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) import uniform 0000/0001/1110/1111 keys into a single-layer, level-zero SW_64KB_R_X color image with 2, 4 or 8 fragments. Color and DCC/CMASK ranges must stay write-watched and unchanged; expanded or mixed initial DCC, register CMASK clears, compressed payloads, color transforms, lossy modes, metadata changes, guest publication and writes over a live owner throw. Driver-mediated reads of the color and metadata stay guarded. The metadata extent follows [AMD Addrlib](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/gfx10/gfx10addrlib.cpp#L347-L454) with the [PS5 Mesa model](https://github.com/mihawk-99/PS5_Mesa/blob/0b2d6d1a61d9bbf89cf8beb88a696144f67c61f8/src/amd/vulkan/winsys/ps5/radv_ps5_winsys.c): GFX10.1, 16 pipes, 256-byte interleave, two maximum compressed fragments. Console geometry, concurrent guest mutation and transitions outside this subset are unverified. + - [Initial stencil state](../../../core/libs/prx/libSceAgcDriver/Execution/include/QueueState.hpp) uses zero for `DB_STENCIL_CONTROL`, `DB_STENCILREFMASK` and `DB_STENCILREFMASK_BF` (context `0x10b`-`0x10d`), including after `CLEAR_STATE`. The public V0, V4, V5 and V7-V11 [register-default tables](../../../core/libs/prx/libSceAgc/Command/include/RegisterDefaultsData.hpp) supply these values. Linux's [GFX10 clear-state table](https://github.com/torvalds/linux/blob/master/drivers/gpu/drm/amd/amdgpu/clearstate_gfx10.h) agrees on the control register but uses `0x01000000` for the reference masks; the console reset values have not been measured. Planet Coaster (PPSA01736 v01.017) enables stencil tests without writing the control register. - [Type-0 and type-1 texture descriptors](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp) (`TYPE` = 0 or 1 in dword 3) with a nonzero base are bound as null images, like a zero base. TYPE 0 comes from a gfx1036 (RDNA2) measurement in issue #4139 (image sample, load and resinfo return zero, no fault). TYPE 1 comes from a gfx1010 (RDNA1) measurement in #4139 and #5828 (`image_sample_lz`, `image_sample_b`, `image_load` and `image_get_resinfo` return zero, no fault) with format 22 only; TYPE 1 is not measured on RDNA2, with other formats or with depth-compare reads (`image_sample_c*`). Neither type was measured on the console's gfx1013. - [Multisampled color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) lose their guest-read guards and cannot be reused after a successful allocation removal or unmap covers their full guarded range. Retired Vulkan images remain allocated until device teardown to preserve outstanding references. Partial unmaps keep the guard; partial allocation reuse and guest access to live multisampled data remain unsupported. From e5b31e07285a0bf18576abe54b11b7001039cde5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Fri, 16 Oct 2026 21:31:16 +0200 Subject: [PATCH 0571/1000] ci(windows): cache only the submodule git dirs The cached tar held the 3rdparty work trees too, and its extraction fails on the dangling symlink 3rdparty/zstd/tests/cli-tests/bin/unzstd, which Git Bash tar can't create. Cache .git/modules alone and let git submodule update check the work trees out from it, as a clean checkout does. Co-Authored-By: Claude --- .github/workflows/build.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 08279beaa2..e5d2a9a22d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -470,20 +470,20 @@ jobs: - id: submodule-cache uses: actions/cache/restore@v6 with: - path: ${{ runner.temp }}/submodules.tar - key: submodules-${{ runner.os }}-${{ steps.submodules.outputs.key }} + path: ${{ runner.temp }}/submodule-gitdirs.tar + key: submodule-gitdirs-${{ runner.os }}-${{ steps.submodules.outputs.key }} - name: Check out the submodules run: | - if [ -f "$RUNNER_TEMP/submodules.tar" ]; then tar --force-local -xf "$RUNNER_TEMP/submodules.tar"; fi + if [ -f "$RUNNER_TEMP/submodule-gitdirs.tar" ]; then tar --force-local -xf "$RUNNER_TEMP/submodule-gitdirs.tar"; fi git submodule update --init --recursive --depth 1 --jobs 8 - if: steps.submodule-cache.outputs.cache-hit != 'true' - run: tar --force-local -cf "$RUNNER_TEMP/submodules.tar" .git/modules 3rdparty + run: tar --force-local -cf "$RUNNER_TEMP/submodule-gitdirs.tar" .git/modules - if: steps.submodule-cache.outputs.cache-hit != 'true' uses: actions/cache/save@v6 continue-on-error: true with: - path: ${{ runner.temp }}/submodules.tar - key: submodules-${{ runner.os }}-${{ steps.submodules.outputs.key }} + path: ${{ runner.temp }}/submodule-gitdirs.tar + key: submodule-gitdirs-${{ runner.os }}-${{ steps.submodules.outputs.key }} - name: Point the WinLibs CMake at the CA bundle of Git for Windows run: | for prefix in ucrt64 mingw64; do From 5f822a50086fd880d6b4346709b70f8901431f43 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Fri, 16 Oct 2026 12:38:16 -0700 Subject: [PATCH 0572/1000] feat(agc): keep Windows imports under 1 MiB compared with APS5_NO_DEFERRED_SETTLES The deferred settle of small Windows imports could only be turned off through the test hook. The variable turns it off for a run, so a title can be compared with and without it on one build; unset, nothing changes. --- .../libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp | 2 +- docs/dev/ARCHITECTURE.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index ddc156eff0..ddb43b7af4 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -776,7 +776,7 @@ constexpr std::uint64_t settleFloorBytes = 0x100000; #ifdef _WIN32 constexpr std::uint64_t settleBlockBytes = 0x10000; constexpr auto settlePollPeriod = std::chrono::microseconds(100); -std::atomic deferredSettles{true}; +std::atomic deferredSettles{std::getenv("APS5_NO_DEFERRED_SETTLES") == nullptr}; #endif void adoptSettleDevice(const Context& context, HostImports& state) { diff --git a/docs/dev/ARCHITECTURE.md b/docs/dev/ARCHITECTURE.md index 661128c3f9..7dc9d4ecdb 100644 --- a/docs/dev/ARCHITECTURE.md +++ b/docs/dev/ARCHITECTURE.md @@ -109,7 +109,7 @@ flowchart LR - A draw whose `PA_SC_AA_CONFIG` selects 2, 4 or 8 samples renders into host-only multisampled images. [`MultisampleTarget.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) keeps one image per device, surface address, format, extent and sample count (`MultisampleTargetView`); the pipeline, render pass and library keys carry `State::samples`, and a multisampled depth target is a multisampled depth surface. The guest sample planes and FMASK are never read or written, so the registry also guards the target's guest range: a CPU read (`GuestMemory::SetReadGuard`), a DMA copy, and a sampled or storage image read of it throw. A `CB_COLOR_CONTROL` resolve mode over a multisampled CB0 runs as a render pass whose resolve attachment is CB1's resident image (`ResolveMultisampleTarget`). A zero fill of the target's CMASK (`NoteColorMetadataFill`, from fill kernels and CP DMA fills) clears the image at its next use. - A texture over a depth surface samples the depth image itself when it views one 2D slice of the plane's own format (`DepthSurfaceTexture` in [`DepthSurface.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp)). A view of several slices (a 2D array or a cube) or an integer view of a D16 plane is served by a `DepthPlaneCopy`, an image the slices are copied into at every use: from the depth surface bound at each slice's address, else from guest memory. Such a texture is never kept in the resource templates (`RefreshedPerUse`). - Guest memory reaches the GPU through host imports (`VK_EXT_external_memory_host`) where the driver accepts them, and through copies otherwise ([`GuestBufferMemory.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp)). A registered range larger than the import window (64 MiB) is imported per window for buffer and fill accesses and whole for an address-based build. Live imports stay under a budget, with the least recently used import retired first (see TechnicalDebt, Host import windows and budget). -- On Windows, a host import of private `MEM_WRITE_WATCH` memory (singly mapped direct memory) stays on `GetWriteWatch` when `ProbeImportWriteProtection` shows the driver reports nothing written after the second GPU read and a GPU write, and a CPU store is collected (`decideImportWatch`). The driver marks every page of such an import written, asynchronously, until a GPU use of it completes (a read on the settle queue ends it even while main-queue work that used the import first is still running, and the first use of an earlier import of the same pages after that read marks nothing), so `settleImport` in [`GuestBufferMemory.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) copies 4 bytes out of the new import on a second queue of the graphics family, or on a queue of another copy-capable family when the graphics family has only one (`Context::settleQueue`, own pool and mutex) inside `ImportWatched`'s window and waits for it, for imports of 1 MiB or more; the main queue can't be used there because `importAllocation` holds the imports lock and `GpuMutex` comes before it in the lock order. A smaller import is compared when it is made, so a title that maps, imports and unmaps a small buffer around every submission does not block its mapping thread. Its 4-byte read is queued instead: the next import lookup that holds `GpuMutex` while the imports match the registry (`settleUnwatchedImports`) records the queued reads into one batch on the settle queue, and once the batch's fence has signalled a later lookup returns the import's whole 64 KiB blocks to the write watch (`GuestMemory::Rewatch` drops their marks and stamps every block as a CPU store), except while storage results are pending over them. A settled import whose range had already left the watch returns the same way, and an import retired while its read is in flight is kept with that batch and freed once the batch has completed, without waiting for it; a guest unmap of its memory waits for the batch instead. Imports through a section alias keep tracking on the guest views, and every other Windows host import is compared. A device without such a queue keeps the comparisons. On Linux the settle queue is made when the device imports dma-buf memory, and every dma-buf import of 1 MiB or more is settled right after it is made, for another reason: the kernel maps an import's pages for the GPU at the first submission that uses it, which took about 2 s for a 3 GiB import on RADV and held up the title's frame. The import's buffer is shared with the settle queue's family instead of aliased, since a sparse import is bound to many memory objects. +- On Windows, a host import of private `MEM_WRITE_WATCH` memory (singly mapped direct memory) stays on `GetWriteWatch` when `ProbeImportWriteProtection` shows the driver reports nothing written after the second GPU read and a GPU write, and a CPU store is collected (`decideImportWatch`). The driver marks every page of such an import written, asynchronously, until a GPU use of it completes (a read on the settle queue ends it even while main-queue work that used the import first is still running, and the first use of an earlier import of the same pages after that read marks nothing), so `settleImport` in [`GuestBufferMemory.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) copies 4 bytes out of the new import on a second queue of the graphics family, or on a queue of another copy-capable family when the graphics family has only one (`Context::settleQueue`, own pool and mutex) inside `ImportWatched`'s window and waits for it, for imports of 1 MiB or more; the main queue can't be used there because `importAllocation` holds the imports lock and `GpuMutex` comes before it in the lock order. A smaller import is compared when it is made, so a title that maps, imports and unmaps a small buffer around every submission does not block its mapping thread. Its 4-byte read is queued instead: the next import lookup that holds `GpuMutex` while the imports match the registry (`settleUnwatchedImports`) records the queued reads into one batch on the settle queue, and once the batch's fence has signalled a later lookup returns the import's whole 64 KiB blocks to the write watch (`GuestMemory::Rewatch` drops their marks and stamps every block as a CPU store), except while storage results are pending over them. A settled import whose range had already left the watch returns the same way, and an import retired while its read is in flight is kept with that batch and freed once the batch has completed, without waiting for it; a guest unmap of its memory waits for the batch instead. `APS5_NO_DEFERRED_SETTLES` (any value) keeps every import under 1 MiB compared. Imports through a section alias keep tracking on the guest views, and every other Windows host import is compared. A device without such a queue keeps the comparisons. On Linux the settle queue is made when the device imports dma-buf memory, and every dma-buf import of 1 MiB or more is settled right after it is made, for another reason: the kernel maps an import's pages for the GPU at the first submission that uses it, which took about 2 s for a 3 GiB import on RADV and held up the title's frame. The import's buffer is shared with the settle queue's family instead of aliased, since a sparse import is bound to many memory objects. ### Log output From e910caed27725240254b64dfa81cd796dd123d53 Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Wed, 14 Oct 2026 17:27:56 +0000 Subject: [PATCH 0573/1000] feat(libSceAudio3d): report supported port attributes --- core/libs/prx/libSceAudio3d/Export.cpp | 21 +++++++++++++++++++++ core/libs/tests/GuestAudio3dPort.cpp | 19 +++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/core/libs/prx/libSceAudio3d/Export.cpp b/core/libs/prx/libSceAudio3d/Export.cpp index b366da3067..47489f9d5e 100644 --- a/core/libs/prx/libSceAudio3d/Export.cpp +++ b/core/libs/prx/libSceAudio3d/Export.cpp @@ -1,8 +1,10 @@ +#include #include #include #include #include #include +#include #include #include #include @@ -113,6 +115,25 @@ int APS5_VABI sceAudio3dPortGetQueueLevel(uint32_t port_id, uint32_t* queue_leve return 0; } +int APS5_VABI sceAudio3dPortGetAttributesSupported(uint32_t port_id, uint32_t* attributes, uint32_t* num_attributes) { + std::lock_guard lock(g_mutex); + if (num_attributes == nullptr) return AUDIO3D_ERROR_INVALID_PARAMETER; + if (!PortIsOpen(port_id)) return AUDIO3D_ERROR_INVALID_PORT; + constexpr std::uint32_t supported[]{ + AUDIO3D_ATTRIBUTE_LATE_REVERB_LEVEL, + AUDIO3D_ATTRIBUTE_DOWNMIX_SPREAD_RADIUS, + AUDIO3D_ATTRIBUTE_DOWNMIX_SPREAD_HEIGHT_AWARE, + }; + if (attributes == nullptr) { + *num_attributes = std::size(supported); + return 0; + } + const std::uint32_t count = std::min(*num_attributes, std::size(supported)); + std::copy_n(std::begin(supported), count, attributes); + *num_attributes = count; + return 0; +} + int APS5_VABI sceAudio3dPortOpen(int user_id, const Audio3dOpenParameters* parameters, uint32_t* id) { std::lock_guard lock(g_mutex); if (!g_initialized) return AUDIO3D_ERROR_NOT_READY; diff --git a/core/libs/tests/GuestAudio3dPort.cpp b/core/libs/tests/GuestAudio3dPort.cpp index fa5d8f2431..6c754ac77a 100644 --- a/core/libs/tests/GuestAudio3dPort.cpp +++ b/core/libs/tests/GuestAudio3dPort.cpp @@ -1,9 +1,11 @@ #include "SceTypes.hpp" +#include #include #include #include #include #include +#include #include extern "C" { @@ -13,6 +15,7 @@ int APS5_VABI sceAudio3dTerminate(); int APS5_VABI sceAudio3dPortOpen(int user_id, const Audio3dOpenParameters* parameters, std::uint32_t* id); int APS5_VABI sceAudio3dPortClose(std::uint32_t port_id); int APS5_VABI sceAudio3dPortSetAttribute(std::uint32_t port_id, std::uint32_t attribute_id, const void* attribute, std::size_t attribute_size); +int APS5_VABI sceAudio3dPortGetAttributesSupported(std::uint32_t port_id, std::uint32_t* attributes, std::uint32_t* num_attributes); int APS5_VABI sceAudio3dPortGetQueueLevel(std::uint32_t port_id, std::uint32_t* queue_level, std::uint32_t* queue_available); int APS5_VABI sceAudio3dPortAdvance(std::uint32_t port_id); int APS5_VABI sceAudio3dPortPush(std::uint32_t port_id, std::uint32_t blocking); @@ -76,6 +79,9 @@ void CheckBeforeInitialize() { Require(sceAudio3dPortOpen(1, nullptr, nullptr) == NOT_READY, "NOT_READY is checked before the arguments"); std::uint32_t level = 0; Require(sceAudio3dPortGetQueueLevel(0, &level, nullptr) == INVALID_PORT, "queue level needs an open port"); + std::uint32_t num_attributes = 3; + std::uint32_t attributes[3]{}; + Require(sceAudio3dPortGetAttributesSupported(0, attributes, &num_attributes) == INVALID_PORT, "supported attributes need an open port"); Require(sceAudio3dPortClose(0) == INVALID_PORT, "close needs an open port"); Require(sceAudio3dPortAdvance(0) == INVALID_PORT, "advance needs an open port"); Require(sceAudio3dPortPush(0, 0) == INVALID_PORT, "push needs an open port"); @@ -137,6 +143,19 @@ void CheckOpenClose() { void CheckAttributes() { float value = 0.5f; int flag = 1; + constexpr std::uint32_t supported[]{0x10001, 0x10002, 0x10003}; + std::uint32_t num_attributes = 99; + Require(sceAudio3dPortGetAttributesSupported(0, nullptr, &num_attributes) == 0 && num_attributes == 3, "null output reports the supported attribute count"); + Require(sceAudio3dPortGetAttributesSupported(1, nullptr, &num_attributes) == INVALID_PORT, "supported attributes reject an invalid port"); + Require(sceAudio3dPortGetAttributesSupported(0, nullptr, nullptr) == INVALID_PARAMETER, "supported attributes require a count pointer"); + num_attributes = 2; + std::uint32_t partial[3]{0, 0, 99}; + Require(sceAudio3dPortGetAttributesSupported(0, partial, &num_attributes) == 0 && num_attributes == 2, "supported attributes respect the caller capacity"); + Require(partial[0] == supported[0] && partial[1] == supported[1] && partial[2] == 99, "supported attributes copy only the requested entries"); + num_attributes = 3; + std::uint32_t all[3]{}; + Require(sceAudio3dPortGetAttributesSupported(0, all, &num_attributes) == 0 && num_attributes == 3, "all supported attributes are returned"); + Require(std::equal(std::begin(all), std::end(all), std::begin(supported)), "the reported attributes match the port setter"); Require(sceAudio3dPortSetAttribute(0, 0x10001, nullptr, 4) == INVALID_PARAMETER, "null attribute"); Require(sceAudio3dPortSetAttribute(1, 0x10001, &value, 4) == INVALID_PORT, "attribute on port 1"); Require(sceAudio3dPortSetAttribute(0, 0x10001, &value, 4) == 0, "late reverb level"); From aa00274346026e3e827ec9b24d2f5edcb9bcbcb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Fri, 16 Oct 2026 21:31:21 +0200 Subject: [PATCH 0574/1000] docs(debt): record inferred sceAudio3dPortGetAttributesSupported list Co-Authored-By: Claude --- docs/dev/debt/libSceAudio3d.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/dev/debt/libSceAudio3d.md b/docs/dev/debt/libSceAudio3d.md index 90d7a053d8..09f5bce9c1 100644 --- a/docs/dev/debt/libSceAudio3d.md +++ b/docs/dev/debt/libSceAudio3d.md @@ -7,4 +7,5 @@ ### Unknown function info - [sceAudio3dPortOpen](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - buffer modes 0 and 1 (also selected by the 0x10 and 0x18 parameter sizes) and 3 beds depend on an SDK version check in the module ([prosper](https://github.com/mattias800/prosper)) and throw. A repeated `sceAudio3dInitialize` (`NOT_READY` in shadPS4, 0 in prosper) and `sceAudio3dPortAdvance` on a full queue (`NOT_READY` in shadPS4, unhandled in KytyPS5) throw too +- [sceAudio3dPortGetAttributesSupported](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - the supported list (late reverb level, downmix spread radius, downmix spread height aware) is inferred from the attributes `sceAudio3dPortSetAttribute` accepts; its order and the count written when the caller capacity is smaller are unverified. [shadPS4](https://github.com/shadps4-emu/shadPS4) returns object attributes (PCM, gain...) instead, prosper and KytyPS5 have no body - [sceAudio3dObjectReserve](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - object ids, the `max_objects` limit and the checks of `sceAudio3dObjectReserve` and `sceAudio3dObjectUnreserve` follow [shadPS4](https://github.com/shadps4-emu/shadPS4) (PS4), the only reference with a body: ids count up from 1 per port, skipping 0, 0xFFFFFFFF and ids in use, and a failed reserve writes 0xFFFFFFFF From db3b406dd221f7c56c9674e6b9a0f35562bbb49c Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Wed, 14 Oct 2026 17:58:52 +0000 Subject: [PATCH 0575/1000] fix(libc): run guest finalizers before driver shutdown --- .../prx/libSceSystemService/CMakeLists.txt | 5 ++- core/libs/prx/libc/src/Process.cpp | 3 +- core/libs/tests/GuestSystemExit.cpp | 33 ++++++++++++++++++- 3 files changed, 38 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceSystemService/CMakeLists.txt b/core/libs/prx/libSceSystemService/CMakeLists.txt index 68b75598dd..85f6b2ce80 100644 --- a/core/libs/prx/libSceSystemService/CMakeLists.txt +++ b/core/libs/prx/libSceSystemService/CMakeLists.txt @@ -40,6 +40,7 @@ if(BUILD_TESTING) add_test(NAME guest_return_from_main COMMAND guest_system_exit_tests --return-from-main) add_test(NAME guest_system_relaunch COMMAND guest_system_exit_tests --relaunch-driver) add_test(NAME guest_nested_exit COMMAND guest_system_exit_tests --nested-exit-driver) + add_test(NAME guest_exit_finalizers_before_shutdown COMMAND guest_system_exit_tests --exit-order-driver) set_tests_properties(guest_system_exit guest_requested_exit PROPERTIES PASS_REGULAR_EXPRESSION "Guest shutdown completed" FAIL_REGULAR_EXPRESSION "Guest exit handler ran|Guest thread resumed during exit") @@ -50,6 +51,8 @@ if(BUILD_TESTING) PASS_REGULAR_EXPRESSION "Guest relaunch completed" TIMEOUT 30) set_tests_properties(guest_nested_exit PROPERTIES PASS_REGULAR_EXPRESSION "Guest nested exit completed" TIMEOUT 30) + set_tests_properties(guest_exit_finalizers_before_shutdown PROPERTIES + PASS_REGULAR_EXPRESSION "Guest finalizers ran before shutdown" TIMEOUT 30) if(CMAKE_SYSTEM_NAME STREQUAL "Linux") add_test_executable(guest_system_load_exec_standard_error_tests ${LIBS_INCLUDE_DIR}/tests/GuestSystemServiceLoadExecStandardError.cpp) target_include_directories(guest_system_load_exec_standard_error_tests PRIVATE ${LIBS_INCLUDE_DIR}) @@ -58,7 +61,7 @@ if(BUILD_TESTING) set_tests_properties(guest_system_load_exec_standard_error PROPERTIES TIMEOUT 20) endif() if(WIN32) - set_tests_properties(guest_system_exit guest_requested_exit guest_system_exit_parameters guest_immediate_exit guest_return_from_main guest_quick_exit guest_system_relaunch guest_nested_exit PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(guest_system_exit guest_requested_exit guest_system_exit_parameters guest_immediate_exit guest_return_from_main guest_quick_exit guest_system_relaunch guest_nested_exit guest_exit_finalizers_before_shutdown PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() add_test_executable(guest_system_service_tests ${LIBS_INCLUDE_DIR}/tests/GuestSystemService.cpp) diff --git a/core/libs/prx/libc/src/Process.cpp b/core/libs/prx/libc/src/Process.cpp index 08aab26c27..d47d252bc2 100644 --- a/core/libs/prx/libc/src/Process.cpp +++ b/core/libs/prx/libc/src/Process.cpp @@ -137,7 +137,6 @@ void StartExit() { if (!runningExit) LibcAwaitExit_nid_postfix(); } else { runningExit = true; - LibcRunShutdown_nid_postfix(); } } @@ -165,11 +164,13 @@ extern "C" { [[noreturn]] void LibcExit_nid_no_patch(int code) { StartExit(); CxaFinalize_nid_no_patch(nullptr); + LibcRunShutdown_nid_postfix(); std::exit(code); } [[noreturn]] void LibcExitWithoutHandlers_nid_no_patch(int code) { StartExit(); + LibcRunShutdown_nid_postfix(); std::fflush(nullptr); LibcTerminate_nid_no_patch(code); } diff --git a/core/libs/tests/GuestSystemExit.cpp b/core/libs/tests/GuestSystemExit.cpp index 73c20d67c0..bcfe623a5b 100644 --- a/core/libs/tests/GuestSystemExit.cpp +++ b/core/libs/tests/GuestSystemExit.cpp @@ -92,6 +92,11 @@ void MainShutdown() { AppendMarker("shutdown"); } void SecondaryShutdown() { AppendMarker("secondary shutdown"); } void APS5_VABI RemainingHandler(void*) { AppendMarker("remaining handler"); } void APS5_VABI ExitingHandler(void*) { exit_nid_postfix(7); } +void GuestExitShutdown() { AppendMarker("shutdown"); } +void APS5_VABI CheckShutdownStillRunning(void*) { + if (LibcShutdownToken_nid_postfix().stop_requested()) std::_Exit(8); + AppendMarker("guest finalizer"); +} int RunChild(const std::filesystem::path& self, const char* mode) { #ifdef _WIN32 std::wstring commandLine = L"\"" + self.wstring() + L"\" " + std::wstring(mode, mode + std::strlen(mode)); @@ -134,6 +139,23 @@ int DriveNestedExit() { std::puts("Guest nested exit completed"); return 0; } +int DriveExitOrder() { + const auto self = OwnExecutable(); + const auto marker = std::filesystem::temp_directory_path() / ("anyps5-exit-order-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()) + ".txt"); + SetVariable(RelaunchMarker, marker.string().c_str()); + const int code = RunChild(self, "--exit-order"); + std::string text; + { + std::ifstream file(marker); + text.assign(std::istreambuf_iterator(file), std::istreambuf_iterator()); + } + std::error_code ignored; + std::filesystem::remove(marker, ignored); + Require(code == 0); + Require(text == "guest finalizer\nshutdown\n"); + std::puts("Guest finalizers ran before shutdown"); + return 0; +} int DriveRelaunch() { const auto self = OwnExecutable(); const auto secondary = self.parent_path() / ("picker.picker.bin" + self.extension().string()); @@ -192,7 +214,16 @@ int main(int argc, char** argv) { exit_nid_postfix(0); return 2; } - if (argc > firstArgument && std::strcmp(argv[firstArgument], "--relaunch-driver") == 0) { + if (argc > 1 && std::strcmp(argv[1], "--exit-order-driver") == 0) { + return DriveExitOrder(); + } + if (argc > 1 && std::strcmp(argv[1], "--exit-order") == 0) { + LibcRegisterShutdown_nid_postfix(GuestExitShutdown); + Require(__cxa_atexit_nid_postfix(CheckShutdownStillRunning, nullptr, nullptr) == 0); + exit_nid_postfix(0); + return 2; + } + if (argc > 1 && std::strcmp(argv[1], "--relaunch-driver") == 0) { return DriveRelaunch(); } if (argc > firstArgument && std::strcmp(argv[firstArgument], "--relaunch") == 0) { From 505dc9e9f7f91ce2beb9a8b8f0341c39f403c24c Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Thu, 15 Oct 2026 21:05:20 +0000 Subject: [PATCH 0576/1000] test(libc): verify nested exit shutdown ordering --- core/libs/tests/GuestSystemExit.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/core/libs/tests/GuestSystemExit.cpp b/core/libs/tests/GuestSystemExit.cpp index bcfe623a5b..6c354d0913 100644 --- a/core/libs/tests/GuestSystemExit.cpp +++ b/core/libs/tests/GuestSystemExit.cpp @@ -135,7 +135,7 @@ int DriveNestedExit() { std::error_code ignored; std::filesystem::remove(marker, ignored); Require(code == 7); - Require(text == "remaining handler\n"); + Require(text == "guest finalizer\nremaining handler\nshutdown\n"); std::puts("Guest nested exit completed"); return 0; } @@ -209,7 +209,9 @@ int main(int argc, char** argv) { return DriveNestedExit(); } if (argc > firstArgument && std::strcmp(argv[firstArgument], "--nested-exit") == 0) { + LibcRegisterShutdown_nid_postfix(GuestExitShutdown); Require(__cxa_atexit_nid_postfix(RemainingHandler, nullptr, nullptr) == 0); + Require(__cxa_atexit_nid_postfix(CheckShutdownStillRunning, nullptr, nullptr) == 0); Require(__cxa_atexit_nid_postfix(ExitingHandler, nullptr, nullptr) == 0); exit_nid_postfix(0); return 2; From 160ba25518f69b9902631d068934a63a6825fd70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Fri, 16 Oct 2026 21:53:08 +0200 Subject: [PATCH 0577/1000] test(libc): keep firstArgument parsing for the exit-order and relaunch modes Co-Authored-By: Claude --- core/libs/tests/GuestSystemExit.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/libs/tests/GuestSystemExit.cpp b/core/libs/tests/GuestSystemExit.cpp index 6c354d0913..3c8fb41326 100644 --- a/core/libs/tests/GuestSystemExit.cpp +++ b/core/libs/tests/GuestSystemExit.cpp @@ -216,16 +216,16 @@ int main(int argc, char** argv) { exit_nid_postfix(0); return 2; } - if (argc > 1 && std::strcmp(argv[1], "--exit-order-driver") == 0) { + if (argc > firstArgument && std::strcmp(argv[firstArgument], "--exit-order-driver") == 0) { return DriveExitOrder(); } - if (argc > 1 && std::strcmp(argv[1], "--exit-order") == 0) { + if (argc > firstArgument && std::strcmp(argv[firstArgument], "--exit-order") == 0) { LibcRegisterShutdown_nid_postfix(GuestExitShutdown); Require(__cxa_atexit_nid_postfix(CheckShutdownStillRunning, nullptr, nullptr) == 0); exit_nid_postfix(0); return 2; } - if (argc > 1 && std::strcmp(argv[1], "--relaunch-driver") == 0) { + if (argc > firstArgument && std::strcmp(argv[firstArgument], "--relaunch-driver") == 0) { return DriveRelaunch(); } if (argc > firstArgument && std::strcmp(argv[firstArgument], "--relaunch") == 0) { From b630e7897b49a2ec6bac9ff2a1859fbf375c0f47 Mon Sep 17 00:00:00 2001 From: alosta Date: Wed, 14 Oct 2026 18:42:55 +0100 Subject: [PATCH 0578/1000] fix(libSceNgs2): implement a minimal sceNgs2GeomApply pass-through Implement sceNgs2GeomApply, which was previously an unimplemented throwing stub and was blocking Dead Cells (PPSA15552). The implementation validates null handles/output addresses and writes a minimal Ngs2GeomAttribute: pitch_ratio 1.0, source position, source volume/level maxima, and diagonal channel levels for the source's channel count. It does not implement source attenuation, distance rolloff, panning or Doppler. The export lives in Geom.cpp's internal Ngs2GeomApply and is exposed through the existing default-visibility wrapper in Voice.cpp. Unimplemented.cpp no longer defines the stub. A passing test was added to Ngs2Geom.cpp. Unblocks: PPSA15552 # Depends on: none AI assistance: yes --- core/libs/prx/libSceNgs2.native/src/Geom.cpp | 18 +++++++++ .../libSceNgs2.native/src/Ngs2Internal.hpp | 1 + .../libSceNgs2.native/src/Unimplemented.cpp | 9 ----- core/libs/prx/libSceNgs2.native/src/Voice.cpp | 4 ++ .../prx/libSceNgs2.native/tests/Ngs2Geom.cpp | 37 +++++++++++++++++++ .../prx/libSceNgs2.native/tests/Ngs2Test.hpp | 1 + docs/dev/debt/libSceNgs2.native.md | 2 +- 7 files changed, 62 insertions(+), 10 deletions(-) diff --git a/core/libs/prx/libSceNgs2.native/src/Geom.cpp b/core/libs/prx/libSceNgs2.native/src/Geom.cpp index f6049c0968..f7571f6d8e 100644 --- a/core/libs/prx/libSceNgs2.native/src/Geom.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Geom.cpp @@ -1,6 +1,8 @@ +#include #include #include #include +#include #include #include "prx/libc/include/General.hpp" @@ -86,3 +88,19 @@ int Ngs2GeomResetSourceParam(Ngs2GeomSourceParam* out_source_param) { out_source_param->matrix_format = 2; return SCE_NGS2_OK; } + +int Ngs2GeomApply(const Ngs2GeomListenerWork* listener, const Ngs2GeomSourceParam* source, Ngs2GeomAttribute* out_attrib, std::uint32_t flags) { + if (listener == nullptr || source == nullptr) APS5_INVALID_ARG_EX; + if (out_attrib == nullptr) return SCE_NGS2_ERROR_INVALID_OUT_ADDRESS; + (void)flags; + std::memset(out_attrib, 0, sizeof(*out_attrib)); + out_attrib->pitch_ratio = 1.0f; + out_attrib->a3d_attrib.position = source->position; + out_attrib->a3d_attrib.volume = std::max(source->min_level, source->max_level); + const std::uint32_t channels = std::min(source->matrix_format == 0u ? 2u : source->matrix_format, 8u); + const float level = source->max_level > 0.0f ? source->max_level : 1.0f; + for (std::uint32_t channel = 0; channel < channels; channel++) { + out_attrib->level[static_cast(channel) * 8 + channel] = level; + } + return SCE_NGS2_OK; +} diff --git a/core/libs/prx/libSceNgs2.native/src/Ngs2Internal.hpp b/core/libs/prx/libSceNgs2.native/src/Ngs2Internal.hpp index b219666620..9100c4ee6e 100644 --- a/core/libs/prx/libSceNgs2.native/src/Ngs2Internal.hpp +++ b/core/libs/prx/libSceNgs2.native/src/Ngs2Internal.hpp @@ -214,6 +214,7 @@ int Ngs2PanInit(Ngs2PanWork* work, const float* speaker_angles, float unit_angle int Ngs2PanGetVolumeMatrix(Ngs2PanWork* work, const Ngs2PanParam* params, uint32_t num_params, uint32_t matrix_format, float* out_volume_matrix); int Ngs2GeomResetListenerParam(Ngs2GeomListenerParam* out_listener_param); int Ngs2GeomCalcListener(const Ngs2GeomListenerParam* param, Ngs2GeomListenerWork* outWork, std::uint32_t flags); +int Ngs2GeomApply(const Ngs2GeomListenerWork* listener, const Ngs2GeomSourceParam* source, Ngs2GeomAttribute* outAttrib, std::uint32_t flags); int Ngs2GeomResetSourceParam(Ngs2GeomSourceParam* out_source_param); #endif diff --git a/core/libs/prx/libSceNgs2.native/src/Unimplemented.cpp b/core/libs/prx/libSceNgs2.native/src/Unimplemented.cpp index 1541b64394..0cd8a3dd82 100644 --- a/core/libs/prx/libSceNgs2.native/src/Unimplemented.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Unimplemented.cpp @@ -8,15 +8,6 @@ extern "C" { -int APS5_VABI sceNgs2GeomApply(const Ngs2GeomListenerWork* listener, const Ngs2GeomSourceParam* source, Ngs2GeomAttribute* out_attrib, uint32_t flags) { - (void)listener; - (void)source; - (void)out_attrib; - (void)flags; - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceNgs2ReportRegisterHandler(uint32_t report_type, void* handler, uintptr_t user_data, Ngs2Handle* out_handle) { (void)report_type; (void)handler; diff --git a/core/libs/prx/libSceNgs2.native/src/Voice.cpp b/core/libs/prx/libSceNgs2.native/src/Voice.cpp index 1ae32f78af..a19ad7b442 100644 --- a/core/libs/prx/libSceNgs2.native/src/Voice.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Voice.cpp @@ -514,6 +514,10 @@ int APS5_VABI sceNgs2PanGetVolumeMatrix(Ngs2PanWork* work, const Ngs2PanParam* p return Ngs2PanGetVolumeMatrix(work, params, num_params, matrix_format, out_volume_matrix); } +int APS5_VABI sceNgs2GeomApply(const Ngs2GeomListenerWork* listener, const Ngs2GeomSourceParam* source, Ngs2GeomAttribute* out_attrib, std::uint32_t flags) { + return Ngs2GeomApply(listener, source, out_attrib, flags); +} + int APS5_VABI sceNgs2GeomResetListenerParam(Ngs2GeomListenerParam* out_listener_param) { return Ngs2GeomResetListenerParam(out_listener_param); } diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Geom.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Geom.cpp index 67d472244e..9f1937abb9 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2Geom.cpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Geom.cpp @@ -142,11 +142,48 @@ static void TestInvalidListener() { RejectListener(¶m, 0); } +static void TestGeomApply() { + Ngs2GeomListenerWork listener{}; + for (std::size_t row = 0; row < 4; row++) { + for (std::size_t column = 0; column < 4; column++) listener.matrix[row][column] = row == column ? 1.0f : 0.0f; + } + + Ngs2GeomSourceParam source; + Require(sceNgs2GeomResetSourceParam(&source) == SCE_NGS2_OK); + source.position = {1.0f, 2.0f, 3.0f}; + source.min_level = -0.25f; + source.max_level = 0.5f; + source.matrix_format = 3; + + Ngs2GeomAttribute attribute; + std::memset(&attribute, 0x5a, sizeof(attribute)); + Require(sceNgs2GeomApply(&listener, &source, &attribute, 0) == SCE_NGS2_OK); + Require(attribute.pitch_ratio == 1.0f); + Require(attribute.a3d_attrib.position.x == 1.0f && attribute.a3d_attrib.position.y == 2.0f && attribute.a3d_attrib.position.z == 3.0f); + Require(attribute.a3d_attrib.volume == 0.5f); + Require(attribute.level[0] == 0.5f); + Require(attribute.level[1] == 0.0f); + Require(attribute.level[9] == 0.5f); + Require(attribute.level[18] == 0.5f); + Require(attribute.level[24] == 0.0f); + Require(attribute.level[63] == 0.0f); + + Require(sceNgs2GeomApply(&listener, &source, nullptr, 0) == SCE_NGS2_ERROR_INVALID_OUT_ADDRESS); + + source.matrix_format = 0; + std::memset(&attribute, 0x5a, sizeof(attribute)); + Require(sceNgs2GeomApply(&listener, &source, &attribute, 0) == SCE_NGS2_OK); + Require(attribute.level[0] == 0.5f); + Require(attribute.level[9] == 0.5f); + Require(attribute.level[16] == 0.0f); +} + int main() { TestLayouts(); TestResetListenerParam(); TestResetSourceParam(); TestListenerTransform(); TestInvalidListener(); + TestGeomApply(); return 0; } diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Test.hpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Test.hpp index fdc379e560..4cc96707d1 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2Test.hpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Test.hpp @@ -42,6 +42,7 @@ int APS5_VABI sceNgs2VoiceQueryInfo(uintptr_t, uint32_t, void*, size_t); int APS5_VABI sceNgs2GeomResetListenerParam(Ngs2GeomListenerParam*); int APS5_VABI sceNgs2GeomCalcListener(const Ngs2GeomListenerParam*, Ngs2GeomListenerWork*, std::uint32_t); int APS5_VABI sceNgs2GeomResetSourceParam(Ngs2GeomSourceParam*); +int APS5_VABI sceNgs2GeomApply(const Ngs2GeomListenerWork*, const Ngs2GeomSourceParam*, Ngs2GeomAttribute*, std::uint32_t); } inline void Check(bool value, int line) { diff --git a/docs/dev/debt/libSceNgs2.native.md b/docs/dev/debt/libSceNgs2.native.md index 3cf8ecbe57..43842d9e4a 100644 --- a/docs/dev/debt/libSceNgs2.native.md +++ b/docs/dev/debt/libSceNgs2.native.md @@ -24,7 +24,7 @@ - [sceNgs2ParseWaveformFile](../../../core/libs/prx/libSceNgs2.native/src/Atrac9.cpp) (libSceNgs2.native) - the signature `(path, offset, info)` is taken from the call site of PPSA21402, which sets only three argument registers; the file is read from `offset` to its end, the data and block offsets are taken to be relative to the start of the file, not of `offset`, and an unreadable file throws. The audio unit fields of a 16-bit PCM waveform are assumed to be one sample frame per unit and per frame; `INVALID_WAVEFORM_DATA` for a `data` chunk that runs past the buffer or ends inside a frame is assumed, and looped PCM and other PCM formats throw - [sceNgs2VoiceControl sampler filter](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - param `0x1000000a`, its layout and meaning follow KytyPS5 only: location 1 type 1 is an RBJ low-pass biquad at the system sample rate after resampling, scaled by `level`, a set `channel_mask` bit bypasses that channel, type 0 turns the filter off. Other locations and types throw. The voice keeps playing a filter tail after its last block until the history is zero, with outputs below 1e-20 flushed to zero (a KytyPS5 choice); the console's tail length is unknown - [sceNgs2PanInit / sceNgs2PanGetVolumeMatrix](../../../core/libs/prx/libSceNgs2.native/src/Pan.cpp) (libSceNgs2.native) - follow the PS4 firmware 11.00 `libSceNgs2.sprx`, read and checked against its output for 2, 4, 5 and 7 speakers into 1, 2, 6 and 8 channels; not yet confirmed on the PS5 library. Speakers are taken in the order L R, L R Ls Rs, L R C Ls Rs or L R C Ls Rs Lb Rb and kept in a fixed circular order, not sorted; without angles the defaults are 90/270, 40/110/250/320, 0/40/110/250/320 and 0/40/110/150/210/250/320 degrees. As in that library, defaults converted to radians keep the wrap-around entry in degrees, so angles past the last speaker pan almost fully to it. Error codes are not known for the PS5, so invalid arguments throw, including a speaker angle equal to the unit angle, on which that library loops forever. Angles so large that adding or subtracting a turn leaves them unchanged throw for the same reason -- [sceNgs2GeomResetListenerParam, sceNgs2GeomResetSourceParam](../../../core/libs/prx/libSceNgs2.native/src/Geom.cpp) (libSceNgs2.native) - the structures (12, 16, 16, 60, 96, 108, 32 and 308 bytes) come from [KytyPS5](https://github.com/KytyPS5/KytyPS5)'s `ngs2.cpp` and match [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `ngs2_geom.h` field for field, but only KytyPS5 gives the behaviour - shadPS4 logs and returns success without writing the output. The defaults (orient front +Z, orient up +Y, sound speed 343, cone 1/360/1/360, rolloff 0/1000000/1/1, doppler factor, full-band, LFE and maximum level 1, two speakers and matrix format 2) are unverified on a PS5 title; KytyPS5 and iStark/PS5PCEM agree on them. PPSA15552 (Dead Cells) calls `sceNgs2GeomResetSourceParam` after creating its racks. A null output address returns `INVALID_OUT_ADDRESS` as elsewhere in the library rather than throwing as KytyPS5 does. `sceNgs2GeomApply` stays unimplemented +- [sceNgs2GeomResetListenerParam, sceNgs2GeomResetSourceParam](../../../core/libs/prx/libSceNgs2.native/src/Geom.cpp) (libSceNgs2.native) - the structures (12, 16, 16, 60, 96, 108, 32 and 308 bytes) come from [KytyPS5](https://github.com/KytyPS5/KytyPS5)'s `ngs2.cpp` and match [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `ngs2_geom.h` field for field, but only KytyPS5 gives the behaviour - shadPS4 logs and returns success without writing the output. The defaults (orient front +Z, orient up +Y, sound speed 343, cone 1/360/1/360, rolloff 0/1000000/1/1, doppler factor, full-band, LFE and maximum level 1, two speakers and matrix format 2) are unverified on a PS5 title; KytyPS5 and iStark/PS5PCEM agree on them. PPSA15552 (Dead Cells) calls `sceNgs2GeomResetSourceParam` after creating its racks. A null output address returns `INVALID_OUT_ADDRESS` as elsewhere in the library rather than throwing as KytyPS5 does. `sceNgs2GeomApply` is now a minimal pass-through: pitch 1, source position, source volume/level maximum and per-source-channel levels. Source attenuation, distance rolloff, panning and Doppler stay unimplemented - [sceNgs2GeomCalcListener](../../../core/libs/prx/libSceNgs2.native/src/Geom.cpp) builds an orthonormal world-to-listener transform. The row-major matrix, front-axis priority and flag 1 for right-handed input follow [PS5PCEM](https://github.com/iStark/PS5PCEM/blob/d2534bac6325a2c61c0fa88977e5321074304484/src/hle/ngs2_geom.zig); they have not been measured on PS5 hardware. Velocity stays in world coordinates and positive finite sound speed is preserved. Orientation vectors are normalised and up is made perpendicular to front, so non-unit and slightly tilted vectors are accepted; unknown flags, zero or parallel orientation vectors, nonfinite input and an unrepresentable translation throw without changing the output; console error codes are unknown, and a nonpositive sound speed throws where PS5PCEM and [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/df5b9b92be7dfd463b47756b707d6d356a9f0550/src/libs/ngs2.cpp) default it to 343. KytyPS5 always writes the identity matrix, so the transform rests on PS5PCEM alone. Dead Cells (PPSA15552) reaches this call after the custom sampler rack in #4334. This does not implement source attenuation, panning or Doppler in `sceNgs2GeomApply`. - [NGS2 custom sampler rack](../../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (libSceNgs2.native, rack 0x4001) - implements bounded support for the custom sampler rack and its built-in module 0x13 (Custom Voice Filter), unblocking PPSA15552 (Dead Cells) startup. Structure layouts come from [shadPS4's ngs2_custom.h](https://raw.githubusercontent.com/shadps4-emu/shadPS4/main/src/core/libraries/ngs2/ngs2_custom.h) (OrbisNgs2CustomVoiceFilterParam, 40 bytes) and live trace correlation from [prosper's hle_audio.cpp](https://raw.githubusercontent.com/mattias800/prosper/main/prosper/src/hle/audio/hle_audio.cpp) (0x40001300 command and parameters like 10000.0f and 0.707f). Routes parameter updates via the 0x40001300 command family into the Biquad filter engine. Filter type 0 clears the destination filter; type 1 configures the RBJ low-pass biquad frequency, Q and level. Other filter types and unmodeled configurations throw. - [sceNgs2VoiceGetState custom sampler](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native, rack 0x4001) - only the size and one field come from a title. PPSA15552 (Dead Cells) calls it on custom sampler voices with a 48-byte buffer at two sites (eboot `0x1740f90`, `0x1741009`) and reads only the 64-bit value at offset 16: the first site keeps it and the second subtracts the kept value, so it is filled with the voice's decoded sample count. Other sizes return `INVALID_OUT_SIZE` as for the other racks. The state flags fill the common voice state; the remaining fields stay zero. shadPS4's `OrbisNgs2CustomSamplerVoiceState` is about 80 bytes and does not match the size this title passes, so the field names and the console's value are unverified From 0129876e52fbf43fc26a5fbb99fd0766477de7ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Fri, 16 Oct 2026 21:38:16 +0200 Subject: [PATCH 0579/1000] docs(debt): name the KytyPS5 source and limits of sceNgs2GeomApply Co-Authored-By: Claude --- docs/dev/debt/libSceNgs2.native.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceNgs2.native.md b/docs/dev/debt/libSceNgs2.native.md index 43842d9e4a..0c3fff5002 100644 --- a/docs/dev/debt/libSceNgs2.native.md +++ b/docs/dev/debt/libSceNgs2.native.md @@ -24,7 +24,7 @@ - [sceNgs2ParseWaveformFile](../../../core/libs/prx/libSceNgs2.native/src/Atrac9.cpp) (libSceNgs2.native) - the signature `(path, offset, info)` is taken from the call site of PPSA21402, which sets only three argument registers; the file is read from `offset` to its end, the data and block offsets are taken to be relative to the start of the file, not of `offset`, and an unreadable file throws. The audio unit fields of a 16-bit PCM waveform are assumed to be one sample frame per unit and per frame; `INVALID_WAVEFORM_DATA` for a `data` chunk that runs past the buffer or ends inside a frame is assumed, and looped PCM and other PCM formats throw - [sceNgs2VoiceControl sampler filter](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - param `0x1000000a`, its layout and meaning follow KytyPS5 only: location 1 type 1 is an RBJ low-pass biquad at the system sample rate after resampling, scaled by `level`, a set `channel_mask` bit bypasses that channel, type 0 turns the filter off. Other locations and types throw. The voice keeps playing a filter tail after its last block until the history is zero, with outputs below 1e-20 flushed to zero (a KytyPS5 choice); the console's tail length is unknown - [sceNgs2PanInit / sceNgs2PanGetVolumeMatrix](../../../core/libs/prx/libSceNgs2.native/src/Pan.cpp) (libSceNgs2.native) - follow the PS4 firmware 11.00 `libSceNgs2.sprx`, read and checked against its output for 2, 4, 5 and 7 speakers into 1, 2, 6 and 8 channels; not yet confirmed on the PS5 library. Speakers are taken in the order L R, L R Ls Rs, L R C Ls Rs or L R C Ls Rs Lb Rb and kept in a fixed circular order, not sorted; without angles the defaults are 90/270, 40/110/250/320, 0/40/110/250/320 and 0/40/110/150/210/250/320 degrees. As in that library, defaults converted to radians keep the wrap-around entry in degrees, so angles past the last speaker pan almost fully to it. Error codes are not known for the PS5, so invalid arguments throw, including a speaker angle equal to the unit angle, on which that library loops forever. Angles so large that adding or subtracting a turn leaves them unchanged throw for the same reason -- [sceNgs2GeomResetListenerParam, sceNgs2GeomResetSourceParam](../../../core/libs/prx/libSceNgs2.native/src/Geom.cpp) (libSceNgs2.native) - the structures (12, 16, 16, 60, 96, 108, 32 and 308 bytes) come from [KytyPS5](https://github.com/KytyPS5/KytyPS5)'s `ngs2.cpp` and match [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `ngs2_geom.h` field for field, but only KytyPS5 gives the behaviour - shadPS4 logs and returns success without writing the output. The defaults (orient front +Z, orient up +Y, sound speed 343, cone 1/360/1/360, rolloff 0/1000000/1/1, doppler factor, full-band, LFE and maximum level 1, two speakers and matrix format 2) are unverified on a PS5 title; KytyPS5 and iStark/PS5PCEM agree on them. PPSA15552 (Dead Cells) calls `sceNgs2GeomResetSourceParam` after creating its racks. A null output address returns `INVALID_OUT_ADDRESS` as elsewhere in the library rather than throwing as KytyPS5 does. `sceNgs2GeomApply` is now a minimal pass-through: pitch 1, source position, source volume/level maximum and per-source-channel levels. Source attenuation, distance rolloff, panning and Doppler stay unimplemented +- [sceNgs2GeomResetListenerParam, sceNgs2GeomResetSourceParam](../../../core/libs/prx/libSceNgs2.native/src/Geom.cpp) (libSceNgs2.native) - the structures (12, 16, 16, 60, 96, 108, 32 and 308 bytes) come from [KytyPS5](https://github.com/KytyPS5/KytyPS5)'s `ngs2.cpp` and match [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `ngs2_geom.h` field for field, but only KytyPS5 gives the behaviour - shadPS4 logs and returns success without writing the output. The defaults (orient front +Z, orient up +Y, sound speed 343, cone 1/360/1/360, rolloff 0/1000000/1/1, doppler factor, full-band, LFE and maximum level 1, two speakers and matrix format 2) are unverified on a PS5 title; KytyPS5 and iStark/PS5PCEM agree on them. PPSA15552 (Dead Cells) calls `sceNgs2GeomResetSourceParam` after creating its racks. A null output address returns `INVALID_OUT_ADDRESS` as elsewhere in the library rather than throwing as KytyPS5 does. `sceNgs2GeomApply` is now a minimal pass-through that follows [KytyPS5](https://github.com/KytyPS5/KytyPS5)'s `Ngs2GeomApply` (shadPS4 only stubs it): pitch 1, source position, volume the larger of the source minimum and maximum level, and a diagonal of the source maximum level (1 when it is not positive) over `matrix_format` channels (0 is two, at most 8). The listener and `flags` are ignored, so source attenuation, distance rolloff, panning and Doppler stay unimplemented; neither KytyPS5 nor this has been measured on a PS5, and a null output address returns `INVALID_OUT_ADDRESS` where KytyPS5 exits - [sceNgs2GeomCalcListener](../../../core/libs/prx/libSceNgs2.native/src/Geom.cpp) builds an orthonormal world-to-listener transform. The row-major matrix, front-axis priority and flag 1 for right-handed input follow [PS5PCEM](https://github.com/iStark/PS5PCEM/blob/d2534bac6325a2c61c0fa88977e5321074304484/src/hle/ngs2_geom.zig); they have not been measured on PS5 hardware. Velocity stays in world coordinates and positive finite sound speed is preserved. Orientation vectors are normalised and up is made perpendicular to front, so non-unit and slightly tilted vectors are accepted; unknown flags, zero or parallel orientation vectors, nonfinite input and an unrepresentable translation throw without changing the output; console error codes are unknown, and a nonpositive sound speed throws where PS5PCEM and [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/df5b9b92be7dfd463b47756b707d6d356a9f0550/src/libs/ngs2.cpp) default it to 343. KytyPS5 always writes the identity matrix, so the transform rests on PS5PCEM alone. Dead Cells (PPSA15552) reaches this call after the custom sampler rack in #4334. This does not implement source attenuation, panning or Doppler in `sceNgs2GeomApply`. - [NGS2 custom sampler rack](../../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (libSceNgs2.native, rack 0x4001) - implements bounded support for the custom sampler rack and its built-in module 0x13 (Custom Voice Filter), unblocking PPSA15552 (Dead Cells) startup. Structure layouts come from [shadPS4's ngs2_custom.h](https://raw.githubusercontent.com/shadps4-emu/shadPS4/main/src/core/libraries/ngs2/ngs2_custom.h) (OrbisNgs2CustomVoiceFilterParam, 40 bytes) and live trace correlation from [prosper's hle_audio.cpp](https://raw.githubusercontent.com/mattias800/prosper/main/prosper/src/hle/audio/hle_audio.cpp) (0x40001300 command and parameters like 10000.0f and 0.707f). Routes parameter updates via the 0x40001300 command family into the Biquad filter engine. Filter type 0 clears the destination filter; type 1 configures the RBJ low-pass biquad frequency, Q and level. Other filter types and unmodeled configurations throw. - [sceNgs2VoiceGetState custom sampler](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native, rack 0x4001) - only the size and one field come from a title. PPSA15552 (Dead Cells) calls it on custom sampler voices with a 48-byte buffer at two sites (eboot `0x1740f90`, `0x1741009`) and reads only the 64-bit value at offset 16: the first site keeps it and the second subtracts the kept value, so it is filled with the voice's decoded sample count. Other sizes return `INVALID_OUT_SIZE` as for the other racks. The state flags fill the common voice state; the remaining fields stay zero. shadPS4's `OrbisNgs2CustomSamplerVoiceState` is about 80 bytes and does not match the size this title passes, so the field names and the console's value are unverified From ff1de9150dc7f1290e7adea9feaaad03bb015c5a Mon Sep 17 00:00:00 2001 From: Rick <99503318+0xRickx@users.noreply.github.com> Date: Tue, 13 Oct 2026 13:09:03 +0200 Subject: [PATCH 0580/1000] fix(libSceSystemService): report the host time zone and summer time sceSystemServiceParamGetInt answered 0 for the time zone (parameter 4) and summer time (parameter 5), so the console looked set to UTC while libSceRtc and libc convert local time with the host's zone. Titles add the time zone in minutes and 60 more minutes when summer time is 1 to UTC to get the local time (Apollo, GoldHEN's frame logger). Both now follow the host's local time zone: summer time is whether it is in effect, and the time zone is the current offset east of UTC less the summer time shift while it is. On Windows that shift is the C runtime's own summer time bias (_get_dstbias), the one localtime_s applied, not a fixed 60 minutes. The UCRT reads the system zone when libc.prx loads and keeps that zone's bias after TZ is set later, so on a host whose system zone has no summer time (the CI runner's UTC) a TZ of CET-1CEST shifted by 0 and the time zone came out as 0. The cause and the change are screamm's, from the review. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../prx/libSceSystemService/CMakeLists.txt | 9 +++ core/libs/prx/libSceSystemService/Export.cpp | 34 ++++++++++- .../libs/tests/GuestSystemServiceTimeZone.cpp | 57 +++++++++++++++++++ docs/dev/debt/libSceSystemService.md | 3 +- 4 files changed, 100 insertions(+), 3 deletions(-) create mode 100644 core/libs/tests/GuestSystemServiceTimeZone.cpp diff --git a/core/libs/prx/libSceSystemService/CMakeLists.txt b/core/libs/prx/libSceSystemService/CMakeLists.txt index 68b75598dd..1cdb7c9b16 100644 --- a/core/libs/prx/libSceSystemService/CMakeLists.txt +++ b/core/libs/prx/libSceSystemService/CMakeLists.txt @@ -79,6 +79,15 @@ if(BUILD_TESTING) set_tests_properties(guest_system_service_language PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_system_service_time_zone_tests ${LIBS_INCLUDE_DIR}/tests/GuestSystemServiceTimeZone.cpp) + target_include_directories(guest_system_service_time_zone_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_system_service_time_zone_tests PRIVATE libSceSystemService libc) + configure_windows_unwind(guest_system_service_time_zone_tests) + add_test(NAME guest_system_service_time_zone COMMAND guest_system_service_time_zone_tests) + if(WIN32) + set_tests_properties(guest_system_service_time_zone PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_system_kill_app_tests ${LIBS_INCLUDE_DIR}/tests/GuestSystemKillApp.cpp) target_include_directories(guest_system_kill_app_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_system_kill_app_tests PRIVATE libSceSystemService libc) diff --git a/core/libs/prx/libSceSystemService/Export.cpp b/core/libs/prx/libSceSystemService/Export.cpp index 19bb3e3048..55d0739a3a 100644 --- a/core/libs/prx/libSceSystemService/Export.cpp +++ b/core/libs/prx/libSceSystemService/Export.cpp @@ -1,9 +1,11 @@ #include +#include #include #include #include #include #include +#include #include #include #include @@ -40,6 +42,34 @@ int ConsoleLanguage() { return language; } +struct ConsoleTimeZone { + int minutes; + bool summer; +}; + +ConsoleTimeZone HostTimeZone() { + const std::time_t now = std::time(nullptr); + std::tm local{}; +#if defined(_WIN32) + const bool converted = localtime_s(&local, &now) == 0; +#else + const bool converted = localtime_r(&now, &local) != nullptr; +#endif + if (!converted) throw std::runtime_error("sceSystemServiceParamGetInt: the host local time is unavailable"); + const std::chrono::sys_days date = std::chrono::year{local.tm_year + 1900} / std::chrono::month{static_cast(local.tm_mon + 1)} / std::chrono::day{static_cast(local.tm_mday)}; + const auto localSeconds = date.time_since_epoch() + std::chrono::hours{local.tm_hour} + std::chrono::minutes{local.tm_min} + std::chrono::seconds{local.tm_sec}; + const auto offset = std::chrono::duration_cast(localSeconds - std::chrono::seconds{now}); + const bool summer = local.tm_isdst > 0; +#if defined(_WIN32) + long daylightBias = 0; + _get_dstbias(&daylightBias); + const int summerMinutes = summer ? static_cast(-daylightBias / 60) : 0; +#else + const int summerMinutes = summer ? 60 : 0; +#endif + return {static_cast(offset.count()) - summerMinutes, summer}; +} + constexpr char MainExecutable[] = "/app0/eboot.bin"; constexpr char ApplicationRoot[] = "/app0/"; constexpr char MainExecutableVariable[] = "APS5_EBOOT"; @@ -270,8 +300,8 @@ int APS5_VABI sceSystemServiceParamGetInt(int paramId, int* value) { case SYSTEM_SERVICE_PARAM_ID_400: *value = ConsoleLanguage(); break; case SYSTEM_SERVICE_PARAM_ID_DATE_FORMAT: *value = SYSTEM_SERVICE_PARAM_DATE_FORMAT_DDMMYYYY; break; case SYSTEM_SERVICE_PARAM_ID_TIME_FORMAT: *value = SYSTEM_SERVICE_PARAM_TIME_FORMAT_24HOUR; break; - case SYSTEM_SERVICE_PARAM_ID_TIME_ZONE: *value = 0; break; - case SYSTEM_SERVICE_PARAM_ID_SUMMERTIME: *value = 0; break; + case SYSTEM_SERVICE_PARAM_ID_TIME_ZONE: *value = HostTimeZone().minutes; break; + case SYSTEM_SERVICE_PARAM_ID_SUMMERTIME: *value = HostTimeZone().summer ? 1 : 0; break; case SYSTEM_SERVICE_PARAM_ID_GAME_PARENTAL_LEVEL: *value = SYSTEM_SERVICE_PARAM_GAME_PARENTAL_OFF; break; case SYSTEM_SERVICE_PARAM_ID_ENTER_BUTTON_ASSIGN: *value = SYSTEM_SERVICE_PARAM_ENTER_BUTTON_CROSS; break; default: *value = 0; break; diff --git a/core/libs/tests/GuestSystemServiceTimeZone.cpp b/core/libs/tests/GuestSystemServiceTimeZone.cpp new file mode 100644 index 0000000000..7f61bcf1fe --- /dev/null +++ b/core/libs/tests/GuestSystemServiceTimeZone.cpp @@ -0,0 +1,57 @@ +#include "SceTypes.hpp" +#include "prx/libSceSystemService/SystemService.hpp" +#include +#include + +extern "C" int APS5_VABI sceSystemServiceParamGetInt(int paramId, int* value); + +namespace { + +void Require(bool value) { if (!value) std::abort(); } + +void SetTimeZone(const char* zone) { +#ifdef _WIN32 + _putenv_s("TZ", zone); + _tzset(); +#else + setenv("TZ", zone, 1); + tzset(); +#endif +} + +int ParamInt(int paramId) { + int value = -1; + Require(sceSystemServiceParamGetInt(paramId, &value) == SYSTEM_SERVICE_OK); + return value; +} + +bool HostSummerTime() { + const std::time_t now = std::time(nullptr); + std::tm local{}; +#ifdef _WIN32 + Require(localtime_s(&local, &now) == 0); +#else + Require(localtime_r(&now, &local) != nullptr); +#endif + return local.tm_isdst > 0; +} + +void RequireZone(const char* zone, int minutes, int summer) { + SetTimeZone(zone); + Require(ParamInt(SYSTEM_SERVICE_PARAM_ID_TIME_ZONE) == minutes); + Require(ParamInt(SYSTEM_SERVICE_PARAM_ID_SUMMERTIME) == summer); +} + +} + +int main() { + RequireZone("IST-5:30", 330, 0); + RequireZone("BRT+3", -180, 0); + RequireZone("UTC0", 0, 0); + SetTimeZone("CET-1CEST"); + Require(ParamInt(SYSTEM_SERVICE_PARAM_ID_TIME_ZONE) == 60); + Require(ParamInt(SYSTEM_SERVICE_PARAM_ID_SUMMERTIME) == (HostSummerTime() ? 1 : 0)); +#ifndef _WIN32 + RequireZone("CET-1CEST,0/0,J365/25", 60, 1); +#endif +} diff --git a/docs/dev/debt/libSceSystemService.md b/docs/dev/debt/libSceSystemService.md index 3fa7ec0b3a..409869d29c 100644 --- a/docs/dev/debt/libSceSystemService.md +++ b/docs/dev/debt/libSceSystemService.md @@ -12,7 +12,8 @@ - [sceSystemServiceLaunchApp](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - AnyPS5 runs one title and cannot start another, so every call throws without reading its arguments. The (title ID, null-terminated argv, launch parameters) signature follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/SystemService/SystemService.cs#L141-L148), whose `AppLauncher` Prospero Explorer (PPSA99109) and Prospero Multi Tools (PPSA99110) import it through and which passes a 0x20-byte launch context or null; the console's argument checks and errors are unknown - [sceSystemServiceLaunchWebBrowser](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - the console's error for an unavailable web browser is unknown; it returns the kernel's `EOPNOTSUPP` (0x8002002D) for any arguments. The (uri, param) signature follows [RommPS](https://github.com/s0liton/RommPS/blob/754d4d60f25938a5136b9e2efcd074313c4386c5/platform/ps5/app/rommps/examples/rommps/kit/credits.cpp#L86-L95), which passes a URL and a null param and only tests the result for zero; the param layout and the console's argument checks are unknown - [sceSystemServiceParamGetString](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - the system name (parameter 6) is the fixed `PS5`, not the console's name; other parameters and buffers of 1 to 64 bytes throw. PPSA26344 at `0xA87935` passes parameter 6 with a 65-byte buffer -- [sceSystemServiceParamGetInt](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - console language numbers 0 to 29 follow the PS4 SDK enum in [OpenOrbis](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain/blob/06fcf7fd108fa080035cdd3259ed22eb07e0508c/include/orbis/_types/sys_service.h#L54-L83) (21 Arabic). 30 (Ukrainian) is not in it and follows PPSA99505 ([Jelly5](https://github.com/02dnot/Jelly5/blob/552b866d89b2a58b27f7f7b0484dc743ce235526/app/src/app/i18n.cpp#L29-L60)) and PPSA99002 ([ProsperoLight](https://github.com/blackbearreloaded/ProsperoLight/blob/b708bbde3457bef1073a9ab6959957cb691e8e2f/include/i18n.hpp#L252-L309)), whose numbers agree with the enum. Not confirmed on a console, and neither title starts on `main` yet. Parameter 400 returns the same number as parameter 1: PPSA01325 asks for 400 first, asks for 1 when that fails, and uses either answer as an index into the same 31 languages; in the PS5 10.01 and 12.60 `libSceSystemService` modules parameter 400 reads the same registry value as parameter 1, without parameter 1's mapping of newer language numbers by SDK version, and returns 0x80a10003 when an imported check that is not identified returns 1; the 4.03 module has no parameter 400. Neither the mapping nor that error is modelled, and the parameter's name is not known. Without `ANYPS5_LANGUAGE` the language is English (United States); the date format, time format, time zone, summer time and enter button stay fixed +- [sceSystemServiceParamGetInt](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - console language numbers 0 to 29 follow the PS4 SDK enum in [OpenOrbis](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain/blob/06fcf7fd108fa080035cdd3259ed22eb07e0508c/include/orbis/_types/sys_service.h#L54-L83) (21 Arabic). 30 (Ukrainian) is not in it and follows PPSA99505 ([Jelly5](https://github.com/02dnot/Jelly5/blob/552b866d89b2a58b27f7f7b0484dc743ce235526/app/src/app/i18n.cpp#L29-L60)) and PPSA99002 ([ProsperoLight](https://github.com/blackbearreloaded/ProsperoLight/blob/b708bbde3457bef1073a9ab6959957cb691e8e2f/include/i18n.hpp#L252-L309)), whose numbers agree with the enum. Not confirmed on a console, and neither title starts on `main` yet. Parameter 400 returns the same number as parameter 1: PPSA01325 asks for 400 first, asks for 1 when that fails, and uses either answer as an index into the same 31 languages; in the PS5 10.01 and 12.60 `libSceSystemService` modules parameter 400 reads the same registry value as parameter 1, without parameter 1's mapping of newer language numbers by SDK version, and returns 0x80a10003 when an imported check that is not identified returns 1; the 4.03 module has no parameter 400. Neither the mapping nor that error is modelled, and the parameter's name is not known. Without `ANYPS5_LANGUAGE` the language is English (United States); the date format, time format and enter button stay fixed +- [sceSystemServiceParamGetInt](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - the time zone (parameter 4) and summer time (parameter 5) follow the host's local time zone at the moment of the call. Titles compute the local time as UTC plus the time zone in minutes plus 60 minutes when summer time is 1 ([Apollo](https://github.com/bucanero/apollo-ps4/blob/252a9d5a187c463994263009851a179f4f910c49/source/exec_cmd.c#L70-L92), [GoldHEN's frame logger](https://github.com/GoldHEN/GoldHEN_Plugins_Repository/blob/6c3a06f53d9caebadfb0aeed9bb94119012010a7/plugin_src/frame_logger/source/main.c#L60-L82), and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/413d2e3518fb435f40971d238fe0ecae7ee551da/src/SharpProspero/Platform/SystemParameters.cs#L75-L79) calls them the offset from UTC in minutes and whether summer time is in effect), so the time zone is the host's current offset east of UTC less the summer time shift while summer time is in effect: 60 minutes on Linux, and on Windows the C runtime's own summer time bias (`_get_dstbias`), which is also what shifted its local time. For a host zone whose summer time is not one hour, Linux keeps the sum right and the standard offset wrong, and Windows the opposite; how the console reports such a zone is unknown. libkernel's `sceKernelConvertUtcToLocaltime`, `sceKernelConvertLocaltimeToUtc` and `sceKernelGettimezone` still use UTC ### Functional From 5637c958c4688e210bb1d99f3986f0e615a49f15 Mon Sep 17 00:00:00 2001 From: JJenkx Date: Thu, 15 Oct 2026 08:38:13 -0400 Subject: [PATCH 0581/1000] fix(ngs2): use PS5 system and rack handle errors --- .../libs/prx/libSceNgs2.native/CMakeLists.txt | 2 +- .../libSceNgs2.native/include/Ngs2Types.hpp | 4 +- core/libs/prx/libSceNgs2.native/src/Rack.cpp | 2 +- .../tests/Ngs2HandleErrors.cpp | 48 +++++++++++++++++++ docs/dev/debt/libSceNgs2.native.md | 4 +- 5 files changed, 55 insertions(+), 5 deletions(-) create mode 100644 core/libs/prx/libSceNgs2.native/tests/Ngs2HandleErrors.cpp diff --git a/core/libs/prx/libSceNgs2.native/CMakeLists.txt b/core/libs/prx/libSceNgs2.native/CMakeLists.txt index 0f7fec5756..0fd1c2376a 100644 --- a/core/libs/prx/libSceNgs2.native/CMakeLists.txt +++ b/core/libs/prx/libSceNgs2.native/CMakeLists.txt @@ -2,7 +2,7 @@ include(${CMAKE_CURRENT_SOURCE_DIR}/Ngs2Library.cmake) add_sce_ngs2_library(libSceNgs2.native) if(BUILD_TESTING) - foreach(ngs2Test IN ITEMS Render Atrac9 Filter Custom Parse Stream UserFx Report Geom VoiceIndex) + foreach(ngs2Test IN ITEMS Render Atrac9 Filter Custom Parse Stream UserFx Report Geom VoiceIndex HandleErrors) string(TOLOWER ${ngs2Test} testName) add_test_executable(ngs2_${testName}_tests ${CMAKE_CURRENT_SOURCE_DIR}/tests/Ngs2${ngs2Test}.cpp) target_include_directories(ngs2_${testName}_tests PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/3rdparty/LibAtrac9/C/src) diff --git a/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp b/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp index 7eb02d7205..6f6bb45435 100644 --- a/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp +++ b/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp @@ -9,8 +9,8 @@ static constexpr int SCE_NGS2_OK = 0; static constexpr int SCE_NGS2_ERROR_INVALID_OUT_ADDRESS = static_cast(0x804A8010); static constexpr int SCE_NGS2_ERROR_INVALID_OUT_SIZE = static_cast(0x804A8011); -static constexpr int SCE_NGS2_ERROR_INVALID_SYSTEM_HANDLE = static_cast(0x804A0230); -static constexpr int SCE_NGS2_ERROR_INVALID_RACK_HANDLE = static_cast(0x804A0261); +static constexpr int SCE_NGS2_ERROR_INVALID_SYSTEM_HANDLE = static_cast(0x804A8201); +static constexpr int SCE_NGS2_ERROR_INVALID_RACK_HANDLE = static_cast(0x804A8202); static constexpr int SCE_NGS2_ERROR_INVALID_VOICE_INDEX = static_cast(0x804A8305); static constexpr int SCE_NGS2_ERROR_INVALID_WAVEFORM_DATA = static_cast(0x804A8430); static constexpr int SCE_NGS2_ERROR_INVALID_WAVEFORM_FORMAT = static_cast(0x804A8431); diff --git a/core/libs/prx/libSceNgs2.native/src/Rack.cpp b/core/libs/prx/libSceNgs2.native/src/Rack.cpp index 50f9c9ef2a..8261f9ad42 100644 --- a/core/libs/prx/libSceNgs2.native/src/Rack.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Rack.cpp @@ -200,10 +200,10 @@ int APS5_VABI sceNgs2RackDestroy(uintptr_t rack_handle, Ngs2ContextBufferInfo* b } int APS5_VABI sceNgs2RackGetVoiceHandle(uintptr_t rack_handle, uint32_t voice_id, uintptr_t* handle) { - if (handle == nullptr) return SCE_NGS2_ERROR_INVALID_OUT_ADDRESS; std::lock_guard lock(Ngs2Mutex()); auto* rack = Ngs2FindRack(rack_handle); if (rack == nullptr) return SCE_NGS2_ERROR_INVALID_RACK_HANDLE; + if (handle == nullptr) return SCE_NGS2_ERROR_INVALID_OUT_ADDRESS; if (voice_id >= rack->voices.size()) return SCE_NGS2_ERROR_INVALID_VOICE_INDEX; *handle = reinterpret_cast(&rack->voices[voice_id]); return SCE_NGS2_OK; diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2HandleErrors.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2HandleErrors.cpp new file mode 100644 index 0000000000..56246ca178 --- /dev/null +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2HandleErrors.cpp @@ -0,0 +1,48 @@ +#include "Ngs2Test.hpp" + +#include +#include + +static void Status(int actual, int expected, const char* message) { + if (actual != expected) throw std::runtime_error(message); +} + +int main() { + try { + constexpr int systemError = static_cast(0x804A8201u); + constexpr int rackError = static_cast(0x804A8202u); + constexpr int outputError = static_cast(0x804A8010u); + constexpr uintptr_t sentinel = 0x5a5a; + uintptr_t value = sentinel; + Status(sceNgs2SystemGetUserData(0x1234, &value), systemError, "invalid system handle returned a non-PS5 error"); + Require(value == sentinel); + Status(sceNgs2SystemDestroy(0x1234, nullptr), systemError, "system destroy returned a non-PS5 handle error"); + Status(sceNgs2RackGetVoiceHandle(0x1234, 0, &value), rackError, "invalid rack handle returned a non-PS5 error"); + Require(value == sentinel); + Status(sceNgs2RackGetVoiceHandle(0x1234, 0, nullptr), rackError, "voice lookup checked output before the rack handle"); + Status(sceNgs2RackDestroy(0x1234, nullptr), rackError, "rack destroy returned a non-PS5 handle error"); + const auto system = CreateSystem(); + const auto rack = CreateRack(system, SCE_NGS2_RACK_ID_REVERB); + Status(sceNgs2SystemGetUserData(rack, &value), systemError, "rack handle was accepted as a system"); + Require(value == sentinel); + Status(sceNgs2RackGetVoiceHandle(system, 0, &value), rackError, "system handle was accepted as a rack"); + Require(value == sentinel); + Status(sceNgs2RackGetVoiceHandle(system, 0, nullptr), rackError, "wrong-kind rack did not take precedence over null output"); + Status(sceNgs2RackGetVoiceHandle(rack, 0, nullptr), outputError, "valid rack lost null-output validation"); + Status(sceNgs2RackGetVoiceHandle(rack, 0, &value), SCE_NGS2_OK, "valid voice lookup failed"); + Require(value != 0 && value != sentinel); + Status(sceNgs2RackDestroy(rack, nullptr), SCE_NGS2_OK, "valid rack destroy failed"); + value = sentinel; + Status(sceNgs2RackGetVoiceHandle(rack, 0, &value), rackError, "retired rack returned the wrong error"); + Require(value == sentinel); + Status(sceNgs2RackGetVoiceHandle(rack, 0, nullptr), rackError, "retired rack did not take precedence over null output"); + Status(sceNgs2SystemDestroy(system, nullptr), SCE_NGS2_OK, "valid system destroy failed"); + Status(sceNgs2SystemGetUserData(system, &value), systemError, "retired system returned the wrong error"); + Require(value == sentinel); + std::cout << "NGS2 PS5 handle errors and validation precedence checked\n"; + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/docs/dev/debt/libSceNgs2.native.md b/docs/dev/debt/libSceNgs2.native.md index 3cf8ecbe57..6d9182579c 100644 --- a/docs/dev/debt/libSceNgs2.native.md +++ b/docs/dev/debt/libSceNgs2.native.md @@ -2,6 +2,8 @@ ### Unknown function info +- [NGS2 system and rack handle errors](../../../core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp) use `0x804A8201` and `0x804A8202`, following the native handle-type error helper recorded in [prosper's PS5 module analysis](https://github.com/mattias800/prosper/blob/d61eb2c0ad2de879ef74df9c4aa050c512804a18/prosper/src/hle/audio/hle_audio.cpp). [sceNgs2RackGetVoiceHandle](../../../core/libs/prx/libSceNgs2.native/src/Rack.cpp) resolves the rack before checking the output pointer, matching the export in firmware 1.00 through 12.70 reported in [#5916](https://github.com/boykopovar/AnyPS5/pull/5916#issuecomment-6172832590). Other exports retain their existing validation order; complete invalid-input precedence and console execution remain unverified. + - [NGS2 render buffer capacity](../../../core/libs/prx/libSceNgs2.native/src/Render.cpp) is treated as available storage for one configured grain. PPSA01880 passes 303104 bytes for 256 stereo float frames (2048 bytes); rendering writes or clears only those frames and preserves the remaining capacity. Non-null silent outputs now undergo the same supported-format and minimum-size checks as audible outputs, with channel counts limited to 1 through 8. Unused null outputs remain ignored. The console's handling of surplus capacity and invalid silent output descriptors has not been measured; invalid descriptors throw rather than return guessed SDK errors. - [NGS2 ATRAC9 waveform loops](../../../core/libs/prx/libSceNgs2.native/src/Atrac9.cpp) accepts one integer forward `smpl` loop. Inclusive RIFF endpoints and encoder-delay adjustment follow [vgmstream riff.c](https://github.com/vgmstream/vgmstream/blob/7dc938fa2f210943b37c7b6511852b516ef432ab/src/meta/riff.c). PPSA03747 supplies a 60-byte sampler chunk with one loop, `samplerData` 24, start 256, inclusive end 299224 and delay 256 for 298969 audible samples. The chunk size and loop count bound the records; `samplerData` is not interpreted. Output positions are assumed to use audible samples and an exclusive end. Blocks split into an optional intro, a repeated range and an optional tail; zero play count means infinite repeats, positive play count includes the first pass. ATRAC9 ranges include a preceding codec frame for decoder history. The output block layout, repeat-count mapping and error codes are unverified on hardware. Multiple, reverse, alternating and fractional loops throw. - [NGS2 submixer UserFx](../../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (control `0x20000004`) invokes the title's callback after input mixing on every playing grain, including silence. The 40-byte control and 48-byte process context follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/945dbc3cc3eee80ac3e053b438502ed936fa6bb2/src/core/libraries/ngs2/ngs2_submixer.h). PPSA03747 installs a peak-meter callback; its code confirms the channel pointers, user data 0, channel count, grain size and sample-rate offsets and returns 0. Other user data words are preserved. Flags are assumed to be 1 after registration/restart and 0 thereafter, following UserFx2's first-process convention; this callback does not inspect them. Null handlers disable the effect, setup clears it, and nonzero callback results throw. These lifecycle and error semantics are unverified on console hardware. @@ -31,4 +33,4 @@ ### Functional -- [sceNgs2RackGetVoiceHandle voice index](../../../core/libs/prx/libSceNgs2.native/src/Rack.cpp) (libSceNgs2.native) - an index at or beyond the rack's voice count returns `0x804A8305` and leaves the output handle untouched, after the null-output and rack-handle checks that keep their order and codes. The value is read from the PS5 `libSceNgs2.native.sprx` export `MwmHz8pAdAo` (firmware 10.01 at `0x11f10`, 12.70 at `0x12130`, and every module from 1.00 to 12.70 checked in [#5916](https://github.com/boykopovar/AnyPS5/pull/5916#issuecomment-6172832590)): an index at or past the rack's voice count returns it through the same error helper that answers `0x804A8010` for a null output, and the output handle is written only on success. That export also checks the rack handle before the output pointer and returns `0x804A8202` for a bad one, while this library checks the output first and returns `0x804A0261`, which does not appear in the module; that order and code are unchanged here. Heretic + Hexen (PPSA26525), DOOM + DOOM II (PPSA21444) and STAR WARS: Dark Forces Remaster (PPSA19728) create a one-voice reverb rack, take voice 0 and ask for voices 1 to 3 at start-up, log the failures and go on +- [sceNgs2RackGetVoiceHandle voice index](../../../core/libs/prx/libSceNgs2.native/src/Rack.cpp) (libSceNgs2.native) - an index at or beyond the rack's voice count returns `0x804A8305` and leaves the output handle untouched, after the null-output and rack-handle checks that keep their order and codes. The value is read from the PS5 `libSceNgs2.native.sprx` export `MwmHz8pAdAo` (firmware 10.01 at `0x11f10`, 12.70 at `0x12130`, and every module from 1.00 to 12.70 checked in [#5916](https://github.com/boykopovar/AnyPS5/pull/5916#issuecomment-6172832590)): an index at or past the rack's voice count returns it through the same error helper that answers `0x804A8010` for a null output, and the output handle is written only on success. The rack is checked before the output pointer, and an invalid rack returns `0x804A8202`. Heretic + Hexen (PPSA26525), DOOM + DOOM II (PPSA21444) and STAR WARS: Dark Forces Remaster (PPSA19728) create a one-voice reverb rack, take voice 0 and ask for voices 1 to 3 at start-up, log the failures and go on From b4a93cd91bcb953c96a20820e80a77675596449e Mon Sep 17 00:00:00 2001 From: JJenkx Date: Thu, 15 Oct 2026 08:43:37 -0400 Subject: [PATCH 0582/1000] test(ngs2): expect PS5 handle errors in render checks --- core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp index d65c46548b..9c55e7dece 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp @@ -41,8 +41,8 @@ static void TestErrorsAndInfo() { Require(sceNgs2RackQueryBufferSize(SCE_NGS2_RACK_ID_SAMPLER, nullptr, nullptr) == SCE_NGS2_ERROR_INVALID_OUT_ADDRESS); Ngs2SystemInfo info{}; - Require(sceNgs2SystemGetInfo(0x1234, &info, sizeof(info)) == static_cast(0x804A0230u)); - Require(sceNgs2RackDestroy(0x1234, nullptr) == static_cast(0x804A0261u)); + Require(sceNgs2SystemGetInfo(0x1234, &info, sizeof(info)) == static_cast(0x804A8201u)); + Require(sceNgs2RackDestroy(0x1234, nullptr) == static_cast(0x804A8202u)); const auto system = CreateSystem(); const auto rack = CreateRack(system, SCE_NGS2_RACK_ID_SAMPLER); From c64d2cae0f08972f4e452c525291cbb83d90ae81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Fri, 16 Oct 2026 22:04:06 +0200 Subject: [PATCH 0583/1000] fix(tools): keep path case in select_tests on Windows normcase lowered every path it returned, so on Windows the test graph dump asked ninja for tests/memorypooltest.exe, which it doesn't know as MemoryPoolTest.exe, and failed; repo paths came out lowered too and could never match the changed files git lists. Ignore case only to compare. Co-Authored-By: Claude --- tools/select_tests.py | 26 ++++++++++++++++++-------- tools/tests/test_select_tests.py | 20 ++++++++++++++++++++ 2 files changed, 38 insertions(+), 8 deletions(-) diff --git a/tools/select_tests.py b/tools/select_tests.py index ef0e2bddef..9c77894a0a 100644 --- a/tools/select_tests.py +++ b/tools/select_tests.py @@ -32,14 +32,16 @@ class Build: encoding=self.encoding, check=True).stdout def absolute(self, path): - return os.path.normcase(os.path.normpath(path if os.path.isabs(path) else os.path.join(self.path, path))) + return os.path.normpath(path if os.path.isabs(path) else os.path.join(self.path, path)) + + def key(self, path): + return os.path.normcase(self.absolute(path)) def repo(self, path): absolute = self.absolute(path) - build, source = os.path.normcase(self.path), os.path.normcase(self.source) - if absolute.startswith(build + os.sep) or not absolute.startswith(source + os.sep): + if inside(absolute, self.path) or not inside(absolute, self.source): return None - return os.path.relpath(absolute, source).replace(os.sep, "/") + return relative(absolute, self.source) def query(self, targets): found = {} @@ -89,15 +91,23 @@ class Build: def output(self, path): absolute = self.absolute(path) - if not absolute.startswith(os.path.normcase(self.path) + os.sep): + if not inside(absolute, self.path): return None - return os.path.relpath(absolute, os.path.normcase(self.path)).replace(os.sep, "/") + return relative(absolute, self.path) def target(self, test): command = test.get("command") or [] return self.output(command[0]) if command else None +def inside(path, root): + return os.path.normcase(path).startswith(os.path.normcase(root) + os.sep) + + +def relative(path, root): + return path[len(root) + 1:].replace(os.sep, "/") + + def chunks(names): batch, size = [], 0 for name in names: @@ -115,7 +125,7 @@ def dump(build, out): current = None for line in build.run("-t", "deps").splitlines(): if not line.startswith(" ") and ": #deps" in line: - current = build.absolute(line.split(": #deps")[0]) + current = build.key(line.split(": #deps")[0]) elif line.strip() and current: path = build.repo(line.strip()) if path: @@ -135,7 +145,7 @@ def dump(build, out): path = build.repo(name) if path: files.add(path) - files |= deps.get(build.absolute(name), set()) + files |= deps.get(build.key(name), set()) todo.extend(nodes.get(name, {"inputs": []})["inputs"]) closures[target] = sorted(files) tests = {} diff --git a/tools/tests/test_select_tests.py b/tools/tests/test_select_tests.py index 384e77742a..434a3da749 100644 --- a/tools/tests/test_select_tests.py +++ b/tools/tests/test_select_tests.py @@ -7,8 +7,12 @@ import sys import tempfile import unittest from pathlib import Path +from unittest import mock TOOL = Path(__file__).resolve().parents[1] / "select_tests.py" +sys.path.insert(0, str(TOOL.parent)) + +import select_tests PROJECT = { "CMakeLists.txt": "cmake_minimum_required(VERSION 3.20)\nproject(mini C)\nenable_testing()\n" @@ -110,5 +114,21 @@ class SelectTests(unittest.TestCase): self.assertEqual(built, {"alpha_tests"}) +class CaseInsensitivePathTests(unittest.TestCase): + def build(self): + build = object.__new__(select_tests.Build) + build.path, build.source = os.path.join(os.sep, "Work", "Source", "build"), os.path.join(os.sep, "Work", "Source") + return build + + def test_paths_keep_their_case_when_comparisons_ignore_it(self): + with mock.patch.object(select_tests.os.path, "normcase", str.lower): + build = self.build() + self.assertEqual(build.output(os.path.join(os.sep, "work", "source", "build", "tests", "MemoryPoolTest.exe")), "tests/MemoryPoolTest.exe") + self.assertEqual(build.output(os.path.join("tests", "MemoryPoolTest.exe")), "tests/MemoryPoolTest.exe") + self.assertEqual(build.repo(os.path.join(os.sep, "work", "source", "core", "libSceAgc", "Driver.cpp")), "core/libSceAgc/Driver.cpp") + self.assertIsNone(build.repo(os.path.join(os.sep, "WORK", "SOURCE", "BUILD", "Generated.h"))) + self.assertEqual(build.key(os.path.join("tests", "MemoryPoolTest.exe")), build.key(os.path.join(os.sep, "WORK", "SOURCE", "BUILD", "TESTS", "memorypooltest.exe"))) + + if __name__ == "__main__": unittest.main() From f91f2effb9cb15d9bda320178f939abe463480ad Mon Sep 17 00:00:00 2001 From: jagrat Singh Date: Wed, 14 Oct 2026 22:28:21 +0530 Subject: [PATCH 0584/1000] feat(libSceSsl): implement certificate serial and name entry readers Implement sceSslGetSerialNumber, sceSslGetSubjectName, sceSslGetIssuerName, sceSslGetNameEntryCount, sceSslGetNameEntryInfo, and sceSslFreeSslCertName. Signatures follow VitaSDK. Mbed TLS extracts the certificate's serial number and X.509 subject and issuer distinguished names, extracting OID attribute short names (or numeric strings) and value strings. --- core/libs/prx/libSceSsl/Export.cpp | 114 +++++++++++++++++++++++++---- core/libs/tests/GuestSslLoad.cpp | 86 ++++++++++++++++++++++ docs/dev/debt/libSceSsl.md | 4 +- 3 files changed, 186 insertions(+), 18 deletions(-) diff --git a/core/libs/prx/libSceSsl/Export.cpp b/core/libs/prx/libSceSsl/Export.cpp index f598565ab5..1853e05a3f 100644 --- a/core/libs/prx/libSceSsl/Export.cpp +++ b/core/libs/prx/libSceSsl/Export.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -172,10 +173,20 @@ struct SslConnection { std::string alpn; }; +struct CertNameEntry { + std::string oid; + std::string value; +}; + +struct CertName { + std::vector entries; +}; + struct Runtime { std::mutex mutex; std::unordered_map> contexts; std::unordered_map connections; + std::unordered_map> certNames; unsigned nextHandle = 1; Runtime() { if (mbedtls_platform_set_calloc_free(PoolCalloc, PoolFree) != 0) @@ -242,6 +253,50 @@ std::size_t CertificateCount(const Context& context) { return count; } +const mbedtls_x509_crt* RequireCertificate(const char* function, void* cert) { + if (!cert) throw std::invalid_argument(std::string(function) + ": null certificate"); + auto& state = State(); + for (const auto& [_, ctx] : state.contexts) { + for (const auto& handle : ctx->certificateHandles) { + if (handle.get() == cert) { + if (handle->owner.expired() || !handle->certificate) { + throw std::invalid_argument(std::string(function) + ": expired certificate"); + } + return handle->certificate; + } + } + } + throw std::invalid_argument(std::string(function) + ": unknown certificate"); +} + +void* ExtractCertName(const char* function, void* sslCert, bool subject) { + std::lock_guard lock(State().mutex); + const auto* crt = RequireCertificate(function, sslCert); + const auto* name = subject ? &crt->subject : &crt->issuer; + auto certName = std::make_unique(); + for (const auto* node = name; node; node = node->next) { + if (!node->oid.p || node->oid.len == 0) continue; + std::string oid; + const char* shortName = nullptr; + if (mbedtls_oid_get_attr_short_name(&node->oid, &shortName) == 0 && shortName) { + oid = shortName; + } else { + char numeric[64] = {}; + if (mbedtls_oid_get_numeric_string(numeric, sizeof(numeric), &node->oid) > 0) { + oid = numeric; + } + } + std::string value; + if (node->val.p && node->val.len > 0) { + value.assign(reinterpret_cast(node->val.p), node->val.len); + } + certName->entries.push_back({std::move(oid), std::move(value)}); + } + auto* handle = certName.get(); + State().certNames.emplace(handle, std::move(certName)); + return handle; +} + } extern "C" { @@ -469,13 +524,21 @@ int APS5_VABI sceSslClose() { return 0; } -int APS5_VABI sceSslGetSerialNumber() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceSslGetSerialNumber(void* sslCert, const char** sboData, unsigned int* sboLen) { + if (!sboData || !sboLen) return ErrorInvalidArg; + std::lock_guard lock(State().mutex); + const auto* crt = RequireCertificate(__func__, sslCert); + *sboData = reinterpret_cast(crt->serial.p); + *sboLen = static_cast(crt->serial.len); + return 0; } -int APS5_VABI sceSslFreeSslCertName(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceSslFreeSslCertName(void* certName) { + if (!certName) return ErrorInvalidArg; + std::lock_guard lock(State().mutex); + const auto found = State().certNames.find(certName); + if (found == State().certNames.end()) throw std::invalid_argument("sceSslFreeSslCertName: unknown cert name"); + State().certNames.erase(found); return 0; } @@ -513,18 +576,38 @@ int APS5_VABI sceSslSetMinSslVersion(int sslConnectionId, int version) { return 0; } -int APS5_VABI sceSslGetIssuerName(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +void* APS5_VABI sceSslGetIssuerName(void* sslCert) { + return ExtractCertName(__func__, sslCert, false); } -int APS5_VABI sceSslGetNameEntryCount(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceSslGetNameEntryCount(void* certName) { + if (!certName) return ErrorInvalidArg; + std::lock_guard lock(State().mutex); + const auto found = State().certNames.find(certName); + if (found == State().certNames.end()) throw std::invalid_argument("sceSslGetNameEntryCount: unknown cert name"); + return static_cast(found->second->entries.size()); } -int APS5_VABI sceSslGetNameEntryInfo(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceSslGetNameEntryInfo(void* certName, int entryNum, char* oidname, unsigned int maxOidnameLen, + char* value, unsigned int maxValueLen, unsigned int* valueLen) { + if (!certName) return ErrorInvalidArg; + std::lock_guard lock(State().mutex); + const auto found = State().certNames.find(certName); + if (found == State().certNames.end()) throw std::invalid_argument("sceSslGetNameEntryInfo: unknown cert name"); + const auto& entries = found->second->entries; + if (entryNum < 0 || static_cast(entryNum) >= entries.size()) return ErrorInvalidArg; + const auto& entry = entries[static_cast(entryNum)]; + if (oidname && maxOidnameLen > 0) { + const auto copyLen = std::min(entry.oid.size(), maxOidnameLen - 1); + std::memcpy(oidname, entry.oid.data(), copyLen); + oidname[copyLen] = '\0'; + } + if (value && maxValueLen > 0) { + const auto copyLen = std::min(entry.value.size(), maxValueLen - 1); + std::memcpy(value, entry.value.data(), copyLen); + value[copyLen] = '\0'; + } + if (valueLen) *valueLen = static_cast(entry.value.size()); return 0; } @@ -533,9 +616,8 @@ int APS5_VABI sceSslGetPem(void) { return 0; } -int APS5_VABI sceSslGetSubjectName(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +void* APS5_VABI sceSslGetSubjectName(void* sslCert) { + return ExtractCertName(__func__, sslCert, true); } } diff --git a/core/libs/tests/GuestSslLoad.cpp b/core/libs/tests/GuestSslLoad.cpp index aaa93f2962..82602cf50f 100644 --- a/core/libs/tests/GuestSslLoad.cpp +++ b/core/libs/tests/GuestSslLoad.cpp @@ -23,6 +23,12 @@ int APS5_VABI sceSslFreeCaCerts(int, SceSsl::CaCerts*); int APS5_VABI sceSslCreateConnection(int, int, const char*); int APS5_VABI sceSslDeleteConnection(int); int APS5_VABI sceSslSetVerifyCallback(int, int (APS5_VABI *)(int, unsigned int, void**, int, void*), void*); +int APS5_VABI sceSslGetSerialNumber(void*, const char**, unsigned int*); +void* APS5_VABI sceSslGetSubjectName(void*); +void* APS5_VABI sceSslGetIssuerName(void*); +int APS5_VABI sceSslGetNameEntryCount(void*); +int APS5_VABI sceSslGetNameEntryInfo(void*, int, char*, unsigned int, char*, unsigned int, unsigned int*); +int APS5_VABI sceSslFreeSslCertName(void*); } namespace { @@ -157,6 +163,85 @@ void ClientIdentity() { Check(sceSslTerm_nid_postfix(id) == 0, "identity term failed"); } +void CertificateReaders() { + const int id = sceSslInit_nid_postfix(1024 * 1024); + auto data = Data(RootPem); + auto* pointer = &data; + Check(sceSslLoadCert(id, 1, &pointer, nullptr, nullptr) == 0, "CA load failed"); + SceSsl::CaList handles{}; + Check(sceSslGetCaList(id, &handles) == 0 && handles.num == 1 && handles.certs[0], "CA handle absent"); + void* cert = handles.certs[0]; + const char* serialData = nullptr; + unsigned int serialLen = 0; + Check(sceSslGetSerialNumber(cert, &serialData, &serialLen) == 0, "get serial failed"); + Check(serialLen == 3 && serialData, "serial length mismatch"); + Check(static_cast(serialData[0]) == 0x00 && static_cast(serialData[1]) == 0x80 && static_cast(serialData[2]) == 0xa1, + "serial bytes mismatch"); + Check(sceSslGetSerialNumber(cert, nullptr, &serialLen) == static_cast(0x8095177a), "null serial buffer accepted"); + Check(sceSslGetSerialNumber(cert, &serialData, nullptr) == static_cast(0x8095177a), "null serial length accepted"); + Throws([&] { sceSslGetSerialNumber(nullptr, &serialData, &serialLen); }); + void* fake = &serialLen; + Throws([&] { sceSslGetSerialNumber(fake, &serialData, &serialLen); }); + + void* subject = sceSslGetSubjectName(cert); + Check(subject != nullptr, "get subject failed"); + void* issuer = sceSslGetIssuerName(cert); + Check(issuer != nullptr, "get issuer failed"); + Throws([&] { sceSslGetSubjectName(nullptr); }); + Throws([&] { sceSslGetIssuerName(nullptr); }); + Throws([&] { sceSslGetSubjectName(fake); }); + Throws([&] { sceSslGetIssuerName(fake); }); + + Check(sceSslGetNameEntryCount(subject) == 3, "subject entry count mismatch"); + Check(sceSslGetNameEntryCount(issuer) == 3, "issuer entry count mismatch"); + Check(sceSslGetNameEntryCount(nullptr) == static_cast(0x8095177a), "null cert name count accepted"); + Throws([&] { sceSslGetNameEntryCount(fake); }); + + char oid[32]{}; + char val[64]{}; + unsigned int vlen = 0; + Check(sceSslGetNameEntryInfo(subject, 0, oid, sizeof(oid), val, sizeof(val), &vlen) == 0, "entry 0 info failed"); + Check(std::strcmp(oid, "C") == 0 && std::strcmp(val, "GB") == 0 && vlen == 2, "entry 0 mismatch"); + Check(sceSslGetNameEntryInfo(subject, 1, oid, sizeof(oid), val, sizeof(val), &vlen) == 0, "entry 1 info failed"); + Check(std::strcmp(oid, "O") == 0 && std::strcmp(val, "AnyPS5 Tests") == 0 && vlen == 12, "entry 1 mismatch"); + Check(sceSslGetNameEntryInfo(subject, 2, oid, sizeof(oid), val, sizeof(val), &vlen) == 0, "entry 2 info failed"); + Check(std::strcmp(oid, "CN") == 0 && std::strcmp(val, "AnyPS5 fixture root") == 0 && vlen == 19, "entry 2 mismatch"); + + char smallVal[4]{}; + unsigned int fullLen = 0; + Check(sceSslGetNameEntryInfo(subject, 1, nullptr, 0, smallVal, sizeof(smallVal), &fullLen) == 0, "truncated query failed"); + Check(std::strcmp(smallVal, "Any") == 0 && fullLen == 12, "buffer truncation mismatch"); + + Check(sceSslGetNameEntryInfo(subject, -1, oid, sizeof(oid), val, sizeof(val), &vlen) == static_cast(0x8095177a), "negative index accepted"); + Check(sceSslGetNameEntryInfo(subject, 3, oid, sizeof(oid), val, sizeof(val), &vlen) == static_cast(0x8095177a), "out of bounds index accepted"); + Check(sceSslGetNameEntryInfo(nullptr, 0, oid, sizeof(oid), val, sizeof(val), &vlen) == static_cast(0x8095177a), "null certName accepted"); + Throws([&] { sceSslGetNameEntryInfo(fake, 0, oid, sizeof(oid), val, sizeof(val), &vlen); }); + + Check(sceSslFreeSslCertName(subject) == 0, "free subject name failed"); + Check(sceSslFreeSslCertName(issuer) == 0, "free issuer name failed"); + Check(sceSslFreeSslCertName(nullptr) == static_cast(0x8095177a), "null cert name free accepted"); + Throws([&] { sceSslFreeSslCertName(subject); }); + Throws([&] { sceSslFreeSslCertName(fake); }); + + Check(sceSslFreeCaList(id, &handles) == 0, "free ca list failed"); + Check(sceSslTerm_nid_postfix(id) == 0, "term failed"); + Throws([&] { sceSslGetSerialNumber(cert, &serialData, &serialLen); }); + Throws([&] { sceSslGetSubjectName(cert); }); + Throws([&] { sceSslGetIssuerName(cert); }); + + const int id2 = sceSslInit_nid_postfix(1024 * 1024); + Check(sceSslLoadCert(id2, 1, &pointer, nullptr, nullptr) == 0, "CA load failed"); + SceSsl::CaList handles2{}; + Check(sceSslGetCaList(id2, &handles2) == 0, "get ca list failed"); + void* cert2 = handles2.certs[0]; + Check(sceSslFreeCaList(id2, &handles2) == 0, "free ca list failed"); + Check(sceSslUnloadCert(id2) == 0, "unload failed"); + Throws([&] { sceSslGetSerialNumber(cert2, &serialData, &serialLen); }); + Throws([&] { sceSslGetSubjectName(cert2); }); + Throws([&] { sceSslGetIssuerName(cert2); }); + Check(sceSslTerm_nid_postfix(id2) == 0, "term failed"); +} + } int main() { @@ -165,6 +250,7 @@ int main() { AtomicFailures(); ClientIdentity(); ConnectionLifetime(); + CertificateReaders(); std::vector workers; for (int i = 0; i < 3; ++i) workers.emplace_back([] { for (int n = 0; n < 4; ++n) Lifecycle(); }); for (auto& worker : workers) worker.join(); diff --git a/docs/dev/debt/libSceSsl.md b/docs/dev/debt/libSceSsl.md index d6239318ab..0b06efed16 100644 --- a/docs/dev/debt/libSceSsl.md +++ b/docs/dev/debt/libSceSsl.md @@ -4,6 +4,6 @@ - [sceSslCreateConnection, sceSslDeleteConnection, sceSslSetVerifyCallback, sceSslConnect, sceSslRead, sceSslWrite](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - imported by `ffmpeg.prx`. The repository has no TLS client, so a connection only records its context, socket, host and verification callback, and `sceSslConnect`, `sceSslRead` and `sceSslWrite` throw `NotImplemented_nid_no_patch(__func__)` until there is a transport. Signatures follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Net/Http.cs#L195-L252), which also says the socket stays open after `sceSslDeleteConnection`. An unknown context returns `SSL_ERROR_INVALID_ID` (0x8095F006), as `sceSslGetMemoryPoolStats` does on the console and as [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/4865dd92ca6b6bb907748f125883093af038f53b/src/libs/network.cpp#L2688-L2720) and [sharpemu](https://github.com/sharpemu/sharpemu/blob/29021b5a71bbc80f262d887e6ba18b3a72963f49/src/SharpEmu.Libs/Network/SslExports.cs#L41-L54) return for other context calls; for an unknown connection, a connection whose context was terminated and a null host the console's codes are unknown, so these throw. The socket is not checked when the connection is created, `sceSslTerm` leaves the context's connections to `sceSslDeleteConnection`, and `sceSslSetVerifyCallback` accepts any callback, null included, and returns 0; none of this is verified on the console. `SSL_ERROR_BEFORE_INIT` (0x8095F001) is never returned, as the library keeps no state apart from its contexts - [sceSslClose](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - unknown signature -- [sceSslGetSerialNumber](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - unknown signature -- [sceSslLoadCert, sceSslGetCaList, sceSslFreeCaList, sceSslGetCaCerts, sceSslFreeCaCerts, sceSslUnloadCert](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - signatures and layouts follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Http.cs). PPSA15508/01.002.000 calls `sceSslLoadCert` with one 1,408-byte PEM CA and null client certificate/key arguments; this confirms that call shape, not console error behavior. Mbed TLS 3.6.7 parses DER/PEM chains and checks an optional client certificate/key pair. Imports are retained per context and committed together; a failed import preserves existing credentials. CA queries return independently owned snapshots; unload clears active credentials while outstanding snapshots remain valid. Appending repeated CA imports, snapshot lifetime across unload, memory overhead and error precedence have not been measured on a PS5. Invalid loads, malformed material, exhausted pools and unsupported keys throw rather than inventing console error codes. No root bundle is shipped; an empty context still returns `NOT_FOUND`. The existing null-list error, unknown-context behavior and legacy clearing of foreign raw CA lists remain unchanged. Certificate readers remain unimplemented. Loading credentials neither starts a TLS connection nor authenticates a peer +- [sceSslGetSerialNumber, sceSslGetSubjectName, sceSslGetIssuerName, sceSslGetNameEntryCount, sceSslGetNameEntryInfo, sceSslFreeSslCertName](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - signatures follow [VitaSDK](https://github.com/vitasdk/vita-headers/blob/master/include/psp2/libssl.h). Mbed TLS parses the certificate's serial number and X.509 subject and issuer distinguished names, extracting OID attribute short names (or numeric strings) and value strings. An unknown or expired certificate handle or invalid certificate name allocation throws; null output pointers or out-of-range entry indices return error codes +- [sceSslLoadCert, sceSslGetCaList, sceSslFreeCaList, sceSslGetCaCerts, sceSslFreeCaCerts, sceSslUnloadCert](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - signatures and layouts follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Http.cs). PPSA15508/01.002.000 calls `sceSslLoadCert` with one 1,408-byte PEM CA and null client certificate/key arguments; this confirms that call shape, not console error behavior. Mbed TLS 3.6.7 parses DER/PEM chains and checks an optional client certificate/key pair. Imports are retained per context and committed together; a failed import preserves existing credentials. CA queries return independently owned snapshots; unload clears active credentials while outstanding snapshots remain valid. Appending repeated CA imports, snapshot lifetime across unload, memory overhead and error precedence have not been measured on a PS5. Invalid loads, malformed material, exhausted pools and unsupported keys throw rather than inventing console error codes. No root bundle is shipped; an empty context still returns `NOT_FOUND`. The existing null-list error, unknown-context behavior and legacy clearing of foreign raw CA lists remain unchanged. Certificate reader queries reflect active certificates or outstanding snapshots. Loading credentials neither starts a TLS connection nor authenticates a peer - [sceSslEnableVerifyOption, sceSslDisableVerifyOption, sceSslSetMinSslVersion, sceSslSetAlpn, sceSslGetAlpnSelected](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - imported by PPSA01317 and PPSA07411. Signatures follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Net/Http.cs), the source of the other connection signatures. The verify options, the minimum version and the ALPN protocol list are recorded on the connection; nothing reads them until `sceSslConnect` has a transport, and how the console applies them is unknown. The initial verify options are unknown and taken as 0. `sceSslSetMinSslVersion` accepts the `SceSslVersion` values SharpProspero lists, 0 (none) to 5 (TLS 1.2), and throws for others; whether the console accepts a TLS 1.3 value is unknown. `sceSslSetAlpn` stores the string up to its terminator and throws for a null list; whether the console expects a comma-separated list or the TLS wire format is unknown. `sceSslGetAlpnSelected` returns null, as no handshake completes. An unknown connection and a connection whose context was terminated throw, as for `sceSslSetVerifyCallback` From a637ffc7276d5f4cac484a6d0aa8fc50669cd1f5 Mon Sep 17 00:00:00 2001 From: jagrat Singh Date: Fri, 16 Oct 2026 16:17:02 +0530 Subject: [PATCH 0585/1000] fix(libSceSsl): align certificate and name entry readers with PS5 context ABI --- core/libs/prx/libSceSsl/Export.cpp | 106 +++++++++++++++------------ core/libs/tests/GuestSslLoad.cpp | 111 +++++++++++++++++------------ docs/dev/debt/libSceSsl.md | 2 +- 3 files changed, 125 insertions(+), 94 deletions(-) diff --git a/core/libs/prx/libSceSsl/Export.cpp b/core/libs/prx/libSceSsl/Export.cpp index 1853e05a3f..0f9622a8ac 100644 --- a/core/libs/prx/libSceSsl/Export.cpp +++ b/core/libs/prx/libSceSsl/Export.cpp @@ -147,6 +147,15 @@ struct Snapshot { std::size_t count; }; +struct CertNameEntry { + std::string oid; + std::string value; +}; + +struct CertName { + std::vector entries; +}; + struct Context { Pool pool; std::vector> authorities; @@ -154,6 +163,7 @@ struct Context { std::vector> certificateHandles; std::unordered_map caLists; std::unordered_map caCerts; + std::unordered_map> certNames; explicit Context(std::size_t size) : pool{size} {} }; @@ -173,20 +183,10 @@ struct SslConnection { std::string alpn; }; -struct CertNameEntry { - std::string oid; - std::string value; -}; - -struct CertName { - std::vector entries; -}; - struct Runtime { std::mutex mutex; std::unordered_map> contexts; std::unordered_map connections; - std::unordered_map> certNames; unsigned nextHandle = 1; Runtime() { if (mbedtls_platform_set_calloc_free(PoolCalloc, PoolFree) != 0) @@ -253,25 +253,24 @@ std::size_t CertificateCount(const Context& context) { return count; } -const mbedtls_x509_crt* RequireCertificate(const char* function, void* cert) { +const mbedtls_x509_crt* RequireCertificate(const char* function, int sslCtxId, void* cert) { if (!cert) throw std::invalid_argument(std::string(function) + ": null certificate"); - auto& state = State(); - for (const auto& [_, ctx] : state.contexts) { - for (const auto& handle : ctx->certificateHandles) { - if (handle.get() == cert) { - if (handle->owner.expired() || !handle->certificate) { - throw std::invalid_argument(std::string(function) + ": expired certificate"); - } - return handle->certificate; + auto& context = GetContext(sslCtxId); + for (const auto& handle : context.certificateHandles) { + if (handle.get() == cert) { + if (handle->owner.expired() || !handle->certificate) { + throw std::invalid_argument(std::string(function) + ": expired certificate"); } + return handle->certificate; } } throw std::invalid_argument(std::string(function) + ": unknown certificate"); } -void* ExtractCertName(const char* function, void* sslCert, bool subject) { +void* ExtractCertName(const char* function, int sslCtxId, void* sslCert, bool subject) { std::lock_guard lock(State().mutex); - const auto* crt = RequireCertificate(function, sslCert); + auto& context = GetContext(sslCtxId); + const auto* crt = RequireCertificate(function, sslCtxId, sslCert); const auto* name = subject ? &crt->subject : &crt->issuer; auto certName = std::make_unique(); for (const auto* node = name; node; node = node->next) { @@ -293,7 +292,7 @@ void* ExtractCertName(const char* function, void* sslCert, bool subject) { certName->entries.push_back({std::move(oid), std::move(value)}); } auto* handle = certName.get(); - State().certNames.emplace(handle, std::move(certName)); + context.certNames.emplace(handle, std::move(certName)); return handle; } @@ -524,21 +523,32 @@ int APS5_VABI sceSslClose() { return 0; } -int APS5_VABI sceSslGetSerialNumber(void* sslCert, const char** sboData, unsigned int* sboLen) { - if (!sboData || !sboLen) return ErrorInvalidArg; +int APS5_VABI sceSslGetSerialNumber(int sslCtxId, void* sslCert, unsigned char* sboData, std::size_t* sboLen) { + if (!sboLen) return ErrorInvalidArg; std::lock_guard lock(State().mutex); - const auto* crt = RequireCertificate(__func__, sslCert); - *sboData = reinterpret_cast(crt->serial.p); - *sboLen = static_cast(crt->serial.len); + const auto* crt = RequireCertificate(__func__, sslCtxId, sslCert); + const auto reqLen = crt->serial.len; + if (!sboData || *sboLen == 0) { + *sboLen = reqLen; + return 0; + } + if (*sboLen < reqLen) { + *sboLen = reqLen; + return ErrorInvalidArg; + } + std::memcpy(sboData, crt->serial.p, reqLen); + *sboLen = reqLen; return 0; } -int APS5_VABI sceSslFreeSslCertName(void* certName) { +int APS5_VABI sceSslFreeSslCertName(int sslCtxId, void* certName) { if (!certName) return ErrorInvalidArg; - std::lock_guard lock(State().mutex); - const auto found = State().certNames.find(certName); - if (found == State().certNames.end()) throw std::invalid_argument("sceSslFreeSslCertName: unknown cert name"); - State().certNames.erase(found); + auto& state = State(); + std::lock_guard lock(state.mutex); + auto& context = GetContext(sslCtxId); + const auto found = context.certNames.find(certName); + if (found == context.certNames.end()) throw std::invalid_argument("sceSslFreeSslCertName: unknown cert name"); + context.certNames.erase(found); return 0; } @@ -576,24 +586,28 @@ int APS5_VABI sceSslSetMinSslVersion(int sslConnectionId, int version) { return 0; } -void* APS5_VABI sceSslGetIssuerName(void* sslCert) { - return ExtractCertName(__func__, sslCert, false); +void* APS5_VABI sceSslGetIssuerName(int sslCtxId, void* sslCert) { + return ExtractCertName(__func__, sslCtxId, sslCert, false); } -int APS5_VABI sceSslGetNameEntryCount(void* certName) { +int APS5_VABI sceSslGetNameEntryCount(int sslCtxId, void* certName) { if (!certName) return ErrorInvalidArg; - std::lock_guard lock(State().mutex); - const auto found = State().certNames.find(certName); - if (found == State().certNames.end()) throw std::invalid_argument("sceSslGetNameEntryCount: unknown cert name"); + auto& state = State(); + std::lock_guard lock(state.mutex); + auto& context = GetContext(sslCtxId); + const auto found = context.certNames.find(certName); + if (found == context.certNames.end()) throw std::invalid_argument("sceSslGetNameEntryCount: unknown cert name"); return static_cast(found->second->entries.size()); } -int APS5_VABI sceSslGetNameEntryInfo(void* certName, int entryNum, char* oidname, unsigned int maxOidnameLen, - char* value, unsigned int maxValueLen, unsigned int* valueLen) { +int APS5_VABI sceSslGetNameEntryInfo(int sslCtxId, void* certName, int entryNum, char* oidname, std::size_t maxOidnameLen, + char* value, std::size_t maxValueLen, std::size_t* valueLen) { if (!certName) return ErrorInvalidArg; - std::lock_guard lock(State().mutex); - const auto found = State().certNames.find(certName); - if (found == State().certNames.end()) throw std::invalid_argument("sceSslGetNameEntryInfo: unknown cert name"); + auto& state = State(); + std::lock_guard lock(state.mutex); + auto& context = GetContext(sslCtxId); + const auto found = context.certNames.find(certName); + if (found == context.certNames.end()) throw std::invalid_argument("sceSslGetNameEntryInfo: unknown cert name"); const auto& entries = found->second->entries; if (entryNum < 0 || static_cast(entryNum) >= entries.size()) return ErrorInvalidArg; const auto& entry = entries[static_cast(entryNum)]; @@ -607,7 +621,7 @@ int APS5_VABI sceSslGetNameEntryInfo(void* certName, int entryNum, char* oidname std::memcpy(value, entry.value.data(), copyLen); value[copyLen] = '\0'; } - if (valueLen) *valueLen = static_cast(entry.value.size()); + if (valueLen) *valueLen = entry.value.size(); return 0; } @@ -616,8 +630,8 @@ int APS5_VABI sceSslGetPem(void) { return 0; } -void* APS5_VABI sceSslGetSubjectName(void* sslCert) { - return ExtractCertName(__func__, sslCert, true); +void* APS5_VABI sceSslGetSubjectName(int sslCtxId, void* sslCert) { + return ExtractCertName(__func__, sslCtxId, sslCert, true); } } diff --git a/core/libs/tests/GuestSslLoad.cpp b/core/libs/tests/GuestSslLoad.cpp index 82602cf50f..8c26ab31c7 100644 --- a/core/libs/tests/GuestSslLoad.cpp +++ b/core/libs/tests/GuestSslLoad.cpp @@ -23,12 +23,12 @@ int APS5_VABI sceSslFreeCaCerts(int, SceSsl::CaCerts*); int APS5_VABI sceSslCreateConnection(int, int, const char*); int APS5_VABI sceSslDeleteConnection(int); int APS5_VABI sceSslSetVerifyCallback(int, int (APS5_VABI *)(int, unsigned int, void**, int, void*), void*); -int APS5_VABI sceSslGetSerialNumber(void*, const char**, unsigned int*); -void* APS5_VABI sceSslGetSubjectName(void*); -void* APS5_VABI sceSslGetIssuerName(void*); -int APS5_VABI sceSslGetNameEntryCount(void*); -int APS5_VABI sceSslGetNameEntryInfo(void*, int, char*, unsigned int, char*, unsigned int, unsigned int*); -int APS5_VABI sceSslFreeSslCertName(void*); +int APS5_VABI sceSslGetSerialNumber(int, void*, unsigned char*, std::size_t*); +void* APS5_VABI sceSslGetSubjectName(int, void*); +void* APS5_VABI sceSslGetIssuerName(int, void*); +int APS5_VABI sceSslGetNameEntryCount(int, void*); +int APS5_VABI sceSslGetNameEntryInfo(int, void*, int, char*, std::size_t, char*, std::size_t, std::size_t*); +int APS5_VABI sceSslFreeSslCertName(int, void*); } namespace { @@ -171,75 +171,92 @@ void CertificateReaders() { SceSsl::CaList handles{}; Check(sceSslGetCaList(id, &handles) == 0 && handles.num == 1 && handles.certs[0], "CA handle absent"); void* cert = handles.certs[0]; - const char* serialData = nullptr; - unsigned int serialLen = 0; - Check(sceSslGetSerialNumber(cert, &serialData, &serialLen) == 0, "get serial failed"); - Check(serialLen == 3 && serialData, "serial length mismatch"); - Check(static_cast(serialData[0]) == 0x00 && static_cast(serialData[1]) == 0x80 && static_cast(serialData[2]) == 0xa1, - "serial bytes mismatch"); - Check(sceSslGetSerialNumber(cert, nullptr, &serialLen) == static_cast(0x8095177a), "null serial buffer accepted"); - Check(sceSslGetSerialNumber(cert, &serialData, nullptr) == static_cast(0x8095177a), "null serial length accepted"); - Throws([&] { sceSslGetSerialNumber(nullptr, &serialData, &serialLen); }); + + std::size_t querySize = 0; + Check(sceSslGetSerialNumber(id, cert, nullptr, &querySize) == 0 && querySize == 3, "query serial size failed"); + querySize = 0; + unsigned char shortBuf[2]{}; + Check(sceSslGetSerialNumber(id, cert, shortBuf, &querySize) == 0 && querySize == 3, "zero-len serial query failed"); + querySize = sizeof(shortBuf); + Check(sceSslGetSerialNumber(id, cert, shortBuf, &querySize) == static_cast(0x8095177a) && querySize == 3, + "short serial buffer accepted"); + unsigned char serialData[8]{}; + std::size_t serialLen = sizeof(serialData); + Check(sceSslGetSerialNumber(id, cert, serialData, &serialLen) == 0, "get serial failed"); + Check(serialLen == 3, "serial length mismatch"); + Check(serialData[0] == 0x00 && serialData[1] == 0x80 && serialData[2] == 0xa1, "serial bytes mismatch"); + Check(sceSslGetSerialNumber(id, cert, serialData, nullptr) == static_cast(0x8095177a), "null serial length accepted"); + Throws([&] { sceSslGetSerialNumber(id, nullptr, serialData, &serialLen); }); void* fake = &serialLen; - Throws([&] { sceSslGetSerialNumber(fake, &serialData, &serialLen); }); + Throws([&] { sceSslGetSerialNumber(id, fake, serialData, &serialLen); }); + Throws([&] { sceSslGetSerialNumber(9999, cert, serialData, &serialLen); }); - void* subject = sceSslGetSubjectName(cert); + void* subject = sceSslGetSubjectName(id, cert); Check(subject != nullptr, "get subject failed"); - void* issuer = sceSslGetIssuerName(cert); + void* issuer = sceSslGetIssuerName(id, cert); Check(issuer != nullptr, "get issuer failed"); - Throws([&] { sceSslGetSubjectName(nullptr); }); - Throws([&] { sceSslGetIssuerName(nullptr); }); - Throws([&] { sceSslGetSubjectName(fake); }); - Throws([&] { sceSslGetIssuerName(fake); }); + Throws([&] { sceSslGetSubjectName(id, nullptr); }); + Throws([&] { sceSslGetIssuerName(id, nullptr); }); + Throws([&] { sceSslGetSubjectName(id, fake); }); + Throws([&] { sceSslGetIssuerName(id, fake); }); + Throws([&] { sceSslGetSubjectName(9999, cert); }); + Throws([&] { sceSslGetIssuerName(9999, cert); }); - Check(sceSslGetNameEntryCount(subject) == 3, "subject entry count mismatch"); - Check(sceSslGetNameEntryCount(issuer) == 3, "issuer entry count mismatch"); - Check(sceSslGetNameEntryCount(nullptr) == static_cast(0x8095177a), "null cert name count accepted"); - Throws([&] { sceSslGetNameEntryCount(fake); }); + Check(sceSslGetNameEntryCount(id, subject) == 3, "subject entry count mismatch"); + Check(sceSslGetNameEntryCount(id, issuer) == 3, "issuer entry count mismatch"); + Check(sceSslGetNameEntryCount(id, nullptr) == static_cast(0x8095177a), "null cert name count accepted"); + Throws([&] { sceSslGetNameEntryCount(id, fake); }); + Throws([&] { sceSslGetNameEntryCount(9999, subject); }); char oid[32]{}; char val[64]{}; - unsigned int vlen = 0; - Check(sceSslGetNameEntryInfo(subject, 0, oid, sizeof(oid), val, sizeof(val), &vlen) == 0, "entry 0 info failed"); + std::size_t vlen = 0; + Check(sceSslGetNameEntryInfo(id, subject, 0, oid, sizeof(oid), val, sizeof(val), &vlen) == 0, "entry 0 info failed"); Check(std::strcmp(oid, "C") == 0 && std::strcmp(val, "GB") == 0 && vlen == 2, "entry 0 mismatch"); - Check(sceSslGetNameEntryInfo(subject, 1, oid, sizeof(oid), val, sizeof(val), &vlen) == 0, "entry 1 info failed"); + Check(sceSslGetNameEntryInfo(id, subject, 1, oid, sizeof(oid), val, sizeof(val), &vlen) == 0, "entry 1 info failed"); Check(std::strcmp(oid, "O") == 0 && std::strcmp(val, "AnyPS5 Tests") == 0 && vlen == 12, "entry 1 mismatch"); - Check(sceSslGetNameEntryInfo(subject, 2, oid, sizeof(oid), val, sizeof(val), &vlen) == 0, "entry 2 info failed"); + Check(sceSslGetNameEntryInfo(id, subject, 2, oid, sizeof(oid), val, sizeof(val), &vlen) == 0, "entry 2 info failed"); Check(std::strcmp(oid, "CN") == 0 && std::strcmp(val, "AnyPS5 fixture root") == 0 && vlen == 19, "entry 2 mismatch"); char smallVal[4]{}; - unsigned int fullLen = 0; - Check(sceSslGetNameEntryInfo(subject, 1, nullptr, 0, smallVal, sizeof(smallVal), &fullLen) == 0, "truncated query failed"); + std::size_t fullLen = 0; + Check(sceSslGetNameEntryInfo(id, subject, 1, nullptr, 0, smallVal, sizeof(smallVal), &fullLen) == 0, "truncated query failed"); Check(std::strcmp(smallVal, "Any") == 0 && fullLen == 12, "buffer truncation mismatch"); - Check(sceSslGetNameEntryInfo(subject, -1, oid, sizeof(oid), val, sizeof(val), &vlen) == static_cast(0x8095177a), "negative index accepted"); - Check(sceSslGetNameEntryInfo(subject, 3, oid, sizeof(oid), val, sizeof(val), &vlen) == static_cast(0x8095177a), "out of bounds index accepted"); - Check(sceSslGetNameEntryInfo(nullptr, 0, oid, sizeof(oid), val, sizeof(val), &vlen) == static_cast(0x8095177a), "null certName accepted"); - Throws([&] { sceSslGetNameEntryInfo(fake, 0, oid, sizeof(oid), val, sizeof(val), &vlen); }); + Check(sceSslGetNameEntryInfo(id, subject, -1, oid, sizeof(oid), val, sizeof(val), &vlen) == static_cast(0x8095177a), "negative index accepted"); + Check(sceSslGetNameEntryInfo(id, subject, 3, oid, sizeof(oid), val, sizeof(val), &vlen) == static_cast(0x8095177a), "out of bounds index accepted"); + Check(sceSslGetNameEntryInfo(id, nullptr, 0, oid, sizeof(oid), val, sizeof(val), &vlen) == static_cast(0x8095177a), "null certName accepted"); + Throws([&] { sceSslGetNameEntryInfo(id, fake, 0, oid, sizeof(oid), val, sizeof(val), &vlen); }); + Throws([&] { sceSslGetNameEntryInfo(9999, subject, 0, oid, sizeof(oid), val, sizeof(val), &vlen); }); - Check(sceSslFreeSslCertName(subject) == 0, "free subject name failed"); - Check(sceSslFreeSslCertName(issuer) == 0, "free issuer name failed"); - Check(sceSslFreeSslCertName(nullptr) == static_cast(0x8095177a), "null cert name free accepted"); - Throws([&] { sceSslFreeSslCertName(subject); }); - Throws([&] { sceSslFreeSslCertName(fake); }); + Check(sceSslFreeSslCertName(id, subject) == 0, "free subject name failed"); + Check(sceSslFreeSslCertName(id, issuer) == 0, "free issuer name failed"); + Check(sceSslFreeSslCertName(id, nullptr) == static_cast(0x8095177a), "null cert name free accepted"); + Throws([&] { sceSslFreeSslCertName(id, subject); }); + Throws([&] { sceSslFreeSslCertName(id, fake); }); + Throws([&] { sceSslFreeSslCertName(9999, subject); }); Check(sceSslFreeCaList(id, &handles) == 0, "free ca list failed"); Check(sceSslTerm_nid_postfix(id) == 0, "term failed"); - Throws([&] { sceSslGetSerialNumber(cert, &serialData, &serialLen); }); - Throws([&] { sceSslGetSubjectName(cert); }); - Throws([&] { sceSslGetIssuerName(cert); }); + Throws([&] { sceSslGetSerialNumber(id, cert, serialData, &serialLen); }); + Throws([&] { sceSslGetSubjectName(id, cert); }); + Throws([&] { sceSslGetIssuerName(id, cert); }); const int id2 = sceSslInit_nid_postfix(1024 * 1024); Check(sceSslLoadCert(id2, 1, &pointer, nullptr, nullptr) == 0, "CA load failed"); SceSsl::CaList handles2{}; Check(sceSslGetCaList(id2, &handles2) == 0, "get ca list failed"); void* cert2 = handles2.certs[0]; + void* subject2 = sceSslGetSubjectName(id2, cert2); + Check(subject2 != nullptr, "get subject 2 failed"); Check(sceSslFreeCaList(id2, &handles2) == 0, "free ca list failed"); Check(sceSslUnloadCert(id2) == 0, "unload failed"); - Throws([&] { sceSslGetSerialNumber(cert2, &serialData, &serialLen); }); - Throws([&] { sceSslGetSubjectName(cert2); }); - Throws([&] { sceSslGetIssuerName(cert2); }); + Throws([&] { sceSslGetSerialNumber(id2, cert2, serialData, &serialLen); }); + Throws([&] { sceSslGetSubjectName(id2, cert2); }); + Throws([&] { sceSslGetIssuerName(id2, cert2); }); Check(sceSslTerm_nid_postfix(id2) == 0, "term failed"); + Throws([&] { sceSslGetNameEntryCount(id2, subject2); }); + Throws([&] { sceSslFreeSslCertName(id2, subject2); }); } } diff --git a/docs/dev/debt/libSceSsl.md b/docs/dev/debt/libSceSsl.md index 0b06efed16..1c36abdbbc 100644 --- a/docs/dev/debt/libSceSsl.md +++ b/docs/dev/debt/libSceSsl.md @@ -4,6 +4,6 @@ - [sceSslCreateConnection, sceSslDeleteConnection, sceSslSetVerifyCallback, sceSslConnect, sceSslRead, sceSslWrite](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - imported by `ffmpeg.prx`. The repository has no TLS client, so a connection only records its context, socket, host and verification callback, and `sceSslConnect`, `sceSslRead` and `sceSslWrite` throw `NotImplemented_nid_no_patch(__func__)` until there is a transport. Signatures follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Net/Http.cs#L195-L252), which also says the socket stays open after `sceSslDeleteConnection`. An unknown context returns `SSL_ERROR_INVALID_ID` (0x8095F006), as `sceSslGetMemoryPoolStats` does on the console and as [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/4865dd92ca6b6bb907748f125883093af038f53b/src/libs/network.cpp#L2688-L2720) and [sharpemu](https://github.com/sharpemu/sharpemu/blob/29021b5a71bbc80f262d887e6ba18b3a72963f49/src/SharpEmu.Libs/Network/SslExports.cs#L41-L54) return for other context calls; for an unknown connection, a connection whose context was terminated and a null host the console's codes are unknown, so these throw. The socket is not checked when the connection is created, `sceSslTerm` leaves the context's connections to `sceSslDeleteConnection`, and `sceSslSetVerifyCallback` accepts any callback, null included, and returns 0; none of this is verified on the console. `SSL_ERROR_BEFORE_INIT` (0x8095F001) is never returned, as the library keeps no state apart from its contexts - [sceSslClose](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - unknown signature -- [sceSslGetSerialNumber, sceSslGetSubjectName, sceSslGetIssuerName, sceSslGetNameEntryCount, sceSslGetNameEntryInfo, sceSslFreeSslCertName](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - signatures follow [VitaSDK](https://github.com/vitasdk/vita-headers/blob/master/include/psp2/libssl.h). Mbed TLS parses the certificate's serial number and X.509 subject and issuer distinguished names, extracting OID attribute short names (or numeric strings) and value strings. An unknown or expired certificate handle or invalid certificate name allocation throws; null output pointers or out-of-range entry indices return error codes +- [sceSslGetSerialNumber, sceSslGetSubjectName, sceSslGetIssuerName, sceSslGetNameEntryCount, sceSslGetNameEntryInfo, sceSslFreeSslCertName](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - signatures and context ownership follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Net/Http.cs#L293-L350). Operations take a context handle as their first argument and operate on certificate and name handles scoped to that context. `sceSslGetSerialNumber` copies into a caller-allocated output buffer or queries the required length when the buffer is null or capacity is zero; a buffer smaller than the serial number returns `SSL_ERROR_INVALID_ARG` (0x8095177a). Mbed TLS parses the certificate's serial number and X.509 subject and issuer distinguished names, extracting OID attribute short names (or numeric strings) and value strings. An unknown context, unknown or expired certificate handle or invalid certificate name handle throws; null output pointers or out-of-range entry indices return error codes - [sceSslLoadCert, sceSslGetCaList, sceSslFreeCaList, sceSslGetCaCerts, sceSslFreeCaCerts, sceSslUnloadCert](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - signatures and layouts follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Http.cs). PPSA15508/01.002.000 calls `sceSslLoadCert` with one 1,408-byte PEM CA and null client certificate/key arguments; this confirms that call shape, not console error behavior. Mbed TLS 3.6.7 parses DER/PEM chains and checks an optional client certificate/key pair. Imports are retained per context and committed together; a failed import preserves existing credentials. CA queries return independently owned snapshots; unload clears active credentials while outstanding snapshots remain valid. Appending repeated CA imports, snapshot lifetime across unload, memory overhead and error precedence have not been measured on a PS5. Invalid loads, malformed material, exhausted pools and unsupported keys throw rather than inventing console error codes. No root bundle is shipped; an empty context still returns `NOT_FOUND`. The existing null-list error, unknown-context behavior and legacy clearing of foreign raw CA lists remain unchanged. Certificate reader queries reflect active certificates or outstanding snapshots. Loading credentials neither starts a TLS connection nor authenticates a peer - [sceSslEnableVerifyOption, sceSslDisableVerifyOption, sceSslSetMinSslVersion, sceSslSetAlpn, sceSslGetAlpnSelected](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - imported by PPSA01317 and PPSA07411. Signatures follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Net/Http.cs), the source of the other connection signatures. The verify options, the minimum version and the ALPN protocol list are recorded on the connection; nothing reads them until `sceSslConnect` has a transport, and how the console applies them is unknown. The initial verify options are unknown and taken as 0. `sceSslSetMinSslVersion` accepts the `SceSslVersion` values SharpProspero lists, 0 (none) to 5 (TLS 1.2), and throws for others; whether the console accepts a TLS 1.3 value is unknown. `sceSslSetAlpn` stores the string up to its terminator and throws for a null list; whether the console expects a comma-separated list or the TLS wire format is unknown. `sceSslGetAlpnSelected` returns null, as no handshake completes. An unknown connection and a connection whose context was terminated throw, as for `sceSslSetVerifyCallback` From a315ce8ce40c84d2b81091cf5fdeb74cb6e4983a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Fri, 16 Oct 2026 21:02:00 +0200 Subject: [PATCH 0586/1000] docs(debt): note unmeasured libSceSsl certificate reader behavior --- docs/dev/debt/libSceSsl.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceSsl.md b/docs/dev/debt/libSceSsl.md index 1c36abdbbc..abc8332e1b 100644 --- a/docs/dev/debt/libSceSsl.md +++ b/docs/dev/debt/libSceSsl.md @@ -4,6 +4,6 @@ - [sceSslCreateConnection, sceSslDeleteConnection, sceSslSetVerifyCallback, sceSslConnect, sceSslRead, sceSslWrite](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - imported by `ffmpeg.prx`. The repository has no TLS client, so a connection only records its context, socket, host and verification callback, and `sceSslConnect`, `sceSslRead` and `sceSslWrite` throw `NotImplemented_nid_no_patch(__func__)` until there is a transport. Signatures follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Net/Http.cs#L195-L252), which also says the socket stays open after `sceSslDeleteConnection`. An unknown context returns `SSL_ERROR_INVALID_ID` (0x8095F006), as `sceSslGetMemoryPoolStats` does on the console and as [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/4865dd92ca6b6bb907748f125883093af038f53b/src/libs/network.cpp#L2688-L2720) and [sharpemu](https://github.com/sharpemu/sharpemu/blob/29021b5a71bbc80f262d887e6ba18b3a72963f49/src/SharpEmu.Libs/Network/SslExports.cs#L41-L54) return for other context calls; for an unknown connection, a connection whose context was terminated and a null host the console's codes are unknown, so these throw. The socket is not checked when the connection is created, `sceSslTerm` leaves the context's connections to `sceSslDeleteConnection`, and `sceSslSetVerifyCallback` accepts any callback, null included, and returns 0; none of this is verified on the console. `SSL_ERROR_BEFORE_INIT` (0x8095F001) is never returned, as the library keeps no state apart from its contexts - [sceSslClose](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - unknown signature -- [sceSslGetSerialNumber, sceSslGetSubjectName, sceSslGetIssuerName, sceSslGetNameEntryCount, sceSslGetNameEntryInfo, sceSslFreeSslCertName](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - signatures and context ownership follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Net/Http.cs#L293-L350). Operations take a context handle as their first argument and operate on certificate and name handles scoped to that context. `sceSslGetSerialNumber` copies into a caller-allocated output buffer or queries the required length when the buffer is null or capacity is zero; a buffer smaller than the serial number returns `SSL_ERROR_INVALID_ARG` (0x8095177a). Mbed TLS parses the certificate's serial number and X.509 subject and issuer distinguished names, extracting OID attribute short names (or numeric strings) and value strings. An unknown context, unknown or expired certificate handle or invalid certificate name handle throws; null output pointers or out-of-range entry indices return error codes +- [sceSslGetSerialNumber, sceSslGetSubjectName, sceSslGetIssuerName, sceSslGetNameEntryCount, sceSslGetNameEntryInfo, sceSslFreeSslCertName](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - signatures and context ownership follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Net/Http.cs#L293-L350). Operations take a context handle as their first argument and operate on certificate and name handles scoped to that context. `sceSslGetSerialNumber` copies into a caller-allocated output buffer or queries the required length when the buffer is null or capacity is zero; a buffer smaller than the serial number returns `SSL_ERROR_INVALID_ARG` (0x8095177a). Mbed TLS parses the certificate's serial number and X.509 subject and issuer distinguished names, extracting OID attribute short names (or numeric strings) and value strings. An unknown context, unknown or expired certificate handle or invalid certificate name handle throws; null output pointers or out-of-range entry indices return `SSL_ERROR_INVALID_ARG` (0x8095177a). The error codes, the zero-capacity length query, the NUL-terminated truncation of name entry strings with the full value length in `valueLen`, and the lifetime of name handles (released only by `sceSslFreeSslCertName` or when the context is terminated) have not been measured on a PS5 - [sceSslLoadCert, sceSslGetCaList, sceSslFreeCaList, sceSslGetCaCerts, sceSslFreeCaCerts, sceSslUnloadCert](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - signatures and layouts follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Http.cs). PPSA15508/01.002.000 calls `sceSslLoadCert` with one 1,408-byte PEM CA and null client certificate/key arguments; this confirms that call shape, not console error behavior. Mbed TLS 3.6.7 parses DER/PEM chains and checks an optional client certificate/key pair. Imports are retained per context and committed together; a failed import preserves existing credentials. CA queries return independently owned snapshots; unload clears active credentials while outstanding snapshots remain valid. Appending repeated CA imports, snapshot lifetime across unload, memory overhead and error precedence have not been measured on a PS5. Invalid loads, malformed material, exhausted pools and unsupported keys throw rather than inventing console error codes. No root bundle is shipped; an empty context still returns `NOT_FOUND`. The existing null-list error, unknown-context behavior and legacy clearing of foreign raw CA lists remain unchanged. Certificate reader queries reflect active certificates or outstanding snapshots. Loading credentials neither starts a TLS connection nor authenticates a peer - [sceSslEnableVerifyOption, sceSslDisableVerifyOption, sceSslSetMinSslVersion, sceSslSetAlpn, sceSslGetAlpnSelected](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - imported by PPSA01317 and PPSA07411. Signatures follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Net/Http.cs), the source of the other connection signatures. The verify options, the minimum version and the ALPN protocol list are recorded on the connection; nothing reads them until `sceSslConnect` has a transport, and how the console applies them is unknown. The initial verify options are unknown and taken as 0. `sceSslSetMinSslVersion` accepts the `SceSslVersion` values SharpProspero lists, 0 (none) to 5 (TLS 1.2), and throws for others; whether the console accepts a TLS 1.3 value is unknown. `sceSslSetAlpn` stores the string up to its terminator and throws for a null list; whether the console expects a comma-separated list or the TLS wire format is unknown. `sceSslGetAlpnSelected` returns null, as no handshake completes. An unknown connection and a connection whose context was terminated throw, as for `sceSslSetVerifyCallback` From 6b28b803fce173b4a1e54fe3588025f55529b64d Mon Sep 17 00:00:00 2001 From: Masud Date: Mon, 12 Oct 2026 16:21:35 +0530 Subject: [PATCH 0587/1000] feat(libSceMouse): implement sceMouseResetAbsolutePosition --- core/libs/prx/libSceMouse/Export.cpp | 6 ++---- core/libs/prx/libSceMouse/include/MouseState.hpp | 1 + core/libs/prx/libSceMouse/src/mouse_impl.cpp | 7 +++++++ core/libs/prx/libSceMouse/tests/Mouse.cpp | 8 ++++++++ docs/dev/debt/libSceJson2.md | 2 +- docs/dev/debt/libSceMouse.md | 5 +++++ 6 files changed, 24 insertions(+), 5 deletions(-) create mode 100644 docs/dev/debt/libSceMouse.md diff --git a/core/libs/prx/libSceMouse/Export.cpp b/core/libs/prx/libSceMouse/Export.cpp index 0fd4352a9f..212896b162 100644 --- a/core/libs/prx/libSceMouse/Export.cpp +++ b/core/libs/prx/libSceMouse/Export.cpp @@ -1,5 +1,4 @@ #include "prx/libSceMouse/include/MouseState.hpp" -#include "prx/libc/include/General.hpp" extern "C" { @@ -23,9 +22,8 @@ void MousePublishInput_nid_no_patch(const MouseInputEvent& event) { Mouse::Publish(event); } -int APS5_VABI sceMouseResetAbsolutePosition() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceMouseResetAbsolutePosition(std::int32_t handle) { + return Mouse::ResetAbsolutePosition(handle); } bool MouseIsOpen_nid_no_patch() { diff --git a/core/libs/prx/libSceMouse/include/MouseState.hpp b/core/libs/prx/libSceMouse/include/MouseState.hpp index 5abc3c3e6a..cbb28df4fa 100644 --- a/core/libs/prx/libSceMouse/include/MouseState.hpp +++ b/core/libs/prx/libSceMouse/include/MouseState.hpp @@ -21,6 +21,7 @@ int Initialize(); int Open(int userId, std::int32_t type, std::int32_t index, const MouseOpenParam* param); int Close(std::int32_t handle); int Read(std::int32_t handle, MouseData* data, std::int32_t num); +int ResetAbsolutePosition(std::int32_t handle); void Publish(const MouseInputEvent& event); } diff --git a/core/libs/prx/libSceMouse/src/mouse_impl.cpp b/core/libs/prx/libSceMouse/src/mouse_impl.cpp index c139fff6a2..12140b0f7e 100644 --- a/core/libs/prx/libSceMouse/src/mouse_impl.cpp +++ b/core/libs/prx/libSceMouse/src/mouse_impl.cpp @@ -82,6 +82,13 @@ int Read(std::int32_t handle, MouseData* data, std::int32_t num) { return available; } +int ResetAbsolutePosition(std::int32_t handle) { + std::lock_guard lock(mouseMutex); + if (!initialized) return MOUSE_ERROR_NOT_INITIALIZED; + if (handle != MOUSE_HANDLE || !opened) return MOUSE_ERROR_INVALID_HANDLE; + return MOUSE_OK; +} + void Publish(const MouseInputEvent& event) { std::lock_guard lock(mouseMutex); if (event.connectionChange) { diff --git a/core/libs/prx/libSceMouse/tests/Mouse.cpp b/core/libs/prx/libSceMouse/tests/Mouse.cpp index 2793ec0184..6121a9f1b6 100644 --- a/core/libs/prx/libSceMouse/tests/Mouse.cpp +++ b/core/libs/prx/libSceMouse/tests/Mouse.cpp @@ -11,21 +11,26 @@ int APS5_VABI sceMouseInit(); int APS5_VABI sceMouseOpen(int, std::int32_t, std::int32_t, const void*); int APS5_VABI sceMouseClose(std::int32_t); int APS5_VABI sceMouseRead(std::int32_t, MouseData*, std::int32_t); +int APS5_VABI sceMouseResetAbsolutePosition(std::int32_t); } static void Require(bool value) { if (!value) std::abort(); } int main() { MouseData data[64]{}; + Require(sceMouseResetAbsolutePosition(MOUSE_HANDLE) == MOUSE_ERROR_NOT_INITIALIZED); Require(sceMouseOpen(1, 0, 0, nullptr) == MOUSE_ERROR_NOT_INITIALIZED); Require(sceMouseInit() == MOUSE_OK); Require(sceMouseInit() == MOUSE_OK); + Require(sceMouseResetAbsolutePosition(MOUSE_HANDLE) == MOUSE_ERROR_INVALID_HANDLE); Require(sceMouseOpen(1, 0, 1, nullptr) == MOUSE_ERROR_INVALID_ARG); MouseOpenParam unsupported{}; unsupported.behaviorFlag = 2; Require(sceMouseOpen(1, 0, 0, &unsupported) == MOUSE_ERROR_INVALID_ARG); Require(sceMouseOpen(1, 0, 0, nullptr) == MOUSE_HANDLE); Require(sceMouseOpen(1, 0, 0, nullptr) == MOUSE_ERROR_ALREADY_OPENED); + Require(sceMouseResetAbsolutePosition(42) == MOUSE_ERROR_INVALID_HANDLE); + Require(sceMouseResetAbsolutePosition(MOUSE_HANDLE) == MOUSE_OK); Require(sceMouseRead(42, data, 1) == MOUSE_ERROR_INVALID_HANDLE); Require(sceMouseRead(MOUSE_HANDLE, nullptr, 1) == MOUSE_ERROR_INVALID_ARG); Require(sceMouseRead(MOUSE_HANDLE, data, 65) == MOUSE_ERROR_INVALID_ARG); @@ -99,10 +104,13 @@ int main() { Require(sceMouseRead(MOUSE_HANDLE, data, 1) == 0); Require(sceMouseClose(MOUSE_HANDLE) == MOUSE_OK); Require(sceMouseRead(MOUSE_HANDLE, data, 1) == MOUSE_ERROR_INVALID_HANDLE); + Require(sceMouseResetAbsolutePosition(MOUSE_HANDLE) == MOUSE_ERROR_INVALID_HANDLE); Require(sceMouseClose(MOUSE_HANDLE) == MOUSE_ERROR_INVALID_HANDLE); MouseOpenParam merged{}; merged.behaviorFlag = MOUSE_OPEN_PARAM_MERGED; Require(sceMouseOpen(1, 0, 0, &merged) == MOUSE_HANDLE); + Require(sceMouseResetAbsolutePosition(MOUSE_HANDLE) == MOUSE_OK); Require(sceMouseRead(MOUSE_HANDLE, data, 1) == 1 && data[0].buttons == 0); Require(sceMouseClose(MOUSE_HANDLE) == MOUSE_OK); + Require(sceMouseResetAbsolutePosition(MOUSE_HANDLE) == MOUSE_ERROR_INVALID_HANDLE); } diff --git a/docs/dev/debt/libSceJson2.md b/docs/dev/debt/libSceJson2.md index f3061164b1..f723880a61 100644 --- a/docs/dev/debt/libSceJson2.md +++ b/docs/dev/debt/libSceJson2.md @@ -11,5 +11,5 @@ ### Functional - PPSA07411 imports declared without parameters, signatures unknown, added without an implementation because the title fails to load without them: [four sce::Json::Value members (setElementAccessFailureHandler, setSpecialFloatHandler, setTypeMismatchHandler, operator bool)](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2), [sceConvertKeycodeGetCharacter](../../../core/libs/prx/libSceConvertKeycode/Export.cpp) (libSceConvertKeycode) and [sceNpSessionSignalingGetGroupFromSessionId](../../../core/libs/prx/libSceNpSessionSignaling/Export.cpp) (libSceNpSessionSignaling) -- PPSA30601 imports declared without parameters, signatures unknown, added without an implementation because the title fails to load without them: [sce::Json::Object::insert](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2), whose return type, and so whether a result pointer precedes `this`, is unknown, and [sceMouseResetAbsolutePosition](../../../core/libs/prx/libSceMouse/Export.cpp) (libSceMouse) +- PPSA30601 imports declared without parameters, signatures unknown, added without an implementation because the title fails to load without them: [sce::Json::Object::insert](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2), whose return type, and so whether a result pointer precedes `this`, is unknown - [sce::Json::InitParameter2 base-object constructor](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - forwards to the complete-object constructor and uses its inferred layout and default field values; constructor equivalence and initialization bytes remain unverified on the console diff --git a/docs/dev/debt/libSceMouse.md b/docs/dev/debt/libSceMouse.md new file mode 100644 index 0000000000..bc13697232 --- /dev/null +++ b/docs/dev/debt/libSceMouse.md @@ -0,0 +1,5 @@ +# libSceMouse technical debt + +### Functional + +- [sceMouseResetAbsolutePosition](../../../core/libs/prx/libSceMouse/Export.cpp) (libSceMouse) - the signature and behaviour are not documented on hardware; accepts a device handle matching `sceMouseClose` and `sceMouseRead`, validates initialization and open state, and returns `MOUSE_OK` (0) as the console mouse reports relative motion only From b816673fb531de262f32ac42c0bba66155ba2664 Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Thu, 15 Oct 2026 01:24:27 +1300 Subject: [PATCH 0588/1000] fix(libSceMouse): take the position arguments of sceMouseResetAbsolutePosition A call site passes the handle and then x and y as zero-extended 16-bit values (the centre of the display size), and ignores the result. --- core/libs/prx/libSceMouse/Export.cpp | 4 ++-- core/libs/prx/libSceMouse/include/MouseState.hpp | 2 +- core/libs/prx/libSceMouse/src/mouse_impl.cpp | 2 +- core/libs/prx/libSceMouse/tests/Mouse.cpp | 16 ++++++++-------- docs/dev/debt/libSceMouse.md | 2 +- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/core/libs/prx/libSceMouse/Export.cpp b/core/libs/prx/libSceMouse/Export.cpp index 212896b162..7278fa33eb 100644 --- a/core/libs/prx/libSceMouse/Export.cpp +++ b/core/libs/prx/libSceMouse/Export.cpp @@ -22,8 +22,8 @@ void MousePublishInput_nid_no_patch(const MouseInputEvent& event) { Mouse::Publish(event); } -int APS5_VABI sceMouseResetAbsolutePosition(std::int32_t handle) { - return Mouse::ResetAbsolutePosition(handle); +int APS5_VABI sceMouseResetAbsolutePosition(std::int32_t handle, std::uint16_t x, std::uint16_t y) { + return Mouse::ResetAbsolutePosition(handle, x, y); } bool MouseIsOpen_nid_no_patch() { diff --git a/core/libs/prx/libSceMouse/include/MouseState.hpp b/core/libs/prx/libSceMouse/include/MouseState.hpp index cbb28df4fa..390cf33968 100644 --- a/core/libs/prx/libSceMouse/include/MouseState.hpp +++ b/core/libs/prx/libSceMouse/include/MouseState.hpp @@ -21,7 +21,7 @@ int Initialize(); int Open(int userId, std::int32_t type, std::int32_t index, const MouseOpenParam* param); int Close(std::int32_t handle); int Read(std::int32_t handle, MouseData* data, std::int32_t num); -int ResetAbsolutePosition(std::int32_t handle); +int ResetAbsolutePosition(std::int32_t handle, std::uint16_t x, std::uint16_t y); void Publish(const MouseInputEvent& event); } diff --git a/core/libs/prx/libSceMouse/src/mouse_impl.cpp b/core/libs/prx/libSceMouse/src/mouse_impl.cpp index 12140b0f7e..d73ccb929d 100644 --- a/core/libs/prx/libSceMouse/src/mouse_impl.cpp +++ b/core/libs/prx/libSceMouse/src/mouse_impl.cpp @@ -82,7 +82,7 @@ int Read(std::int32_t handle, MouseData* data, std::int32_t num) { return available; } -int ResetAbsolutePosition(std::int32_t handle) { +int ResetAbsolutePosition(std::int32_t handle, std::uint16_t, std::uint16_t) { std::lock_guard lock(mouseMutex); if (!initialized) return MOUSE_ERROR_NOT_INITIALIZED; if (handle != MOUSE_HANDLE || !opened) return MOUSE_ERROR_INVALID_HANDLE; diff --git a/core/libs/prx/libSceMouse/tests/Mouse.cpp b/core/libs/prx/libSceMouse/tests/Mouse.cpp index 6121a9f1b6..ffada31501 100644 --- a/core/libs/prx/libSceMouse/tests/Mouse.cpp +++ b/core/libs/prx/libSceMouse/tests/Mouse.cpp @@ -11,26 +11,26 @@ int APS5_VABI sceMouseInit(); int APS5_VABI sceMouseOpen(int, std::int32_t, std::int32_t, const void*); int APS5_VABI sceMouseClose(std::int32_t); int APS5_VABI sceMouseRead(std::int32_t, MouseData*, std::int32_t); -int APS5_VABI sceMouseResetAbsolutePosition(std::int32_t); +int APS5_VABI sceMouseResetAbsolutePosition(std::int32_t, std::uint16_t, std::uint16_t); } static void Require(bool value) { if (!value) std::abort(); } int main() { MouseData data[64]{}; - Require(sceMouseResetAbsolutePosition(MOUSE_HANDLE) == MOUSE_ERROR_NOT_INITIALIZED); + Require(sceMouseResetAbsolutePosition(MOUSE_HANDLE, 959, 539) == MOUSE_ERROR_NOT_INITIALIZED); Require(sceMouseOpen(1, 0, 0, nullptr) == MOUSE_ERROR_NOT_INITIALIZED); Require(sceMouseInit() == MOUSE_OK); Require(sceMouseInit() == MOUSE_OK); - Require(sceMouseResetAbsolutePosition(MOUSE_HANDLE) == MOUSE_ERROR_INVALID_HANDLE); + Require(sceMouseResetAbsolutePosition(MOUSE_HANDLE, 959, 539) == MOUSE_ERROR_INVALID_HANDLE); Require(sceMouseOpen(1, 0, 1, nullptr) == MOUSE_ERROR_INVALID_ARG); MouseOpenParam unsupported{}; unsupported.behaviorFlag = 2; Require(sceMouseOpen(1, 0, 0, &unsupported) == MOUSE_ERROR_INVALID_ARG); Require(sceMouseOpen(1, 0, 0, nullptr) == MOUSE_HANDLE); Require(sceMouseOpen(1, 0, 0, nullptr) == MOUSE_ERROR_ALREADY_OPENED); - Require(sceMouseResetAbsolutePosition(42) == MOUSE_ERROR_INVALID_HANDLE); - Require(sceMouseResetAbsolutePosition(MOUSE_HANDLE) == MOUSE_OK); + Require(sceMouseResetAbsolutePosition(42, 959, 539) == MOUSE_ERROR_INVALID_HANDLE); + Require(sceMouseResetAbsolutePosition(MOUSE_HANDLE, 959, 539) == MOUSE_OK); Require(sceMouseRead(42, data, 1) == MOUSE_ERROR_INVALID_HANDLE); Require(sceMouseRead(MOUSE_HANDLE, nullptr, 1) == MOUSE_ERROR_INVALID_ARG); Require(sceMouseRead(MOUSE_HANDLE, data, 65) == MOUSE_ERROR_INVALID_ARG); @@ -104,13 +104,13 @@ int main() { Require(sceMouseRead(MOUSE_HANDLE, data, 1) == 0); Require(sceMouseClose(MOUSE_HANDLE) == MOUSE_OK); Require(sceMouseRead(MOUSE_HANDLE, data, 1) == MOUSE_ERROR_INVALID_HANDLE); - Require(sceMouseResetAbsolutePosition(MOUSE_HANDLE) == MOUSE_ERROR_INVALID_HANDLE); + Require(sceMouseResetAbsolutePosition(MOUSE_HANDLE, 959, 539) == MOUSE_ERROR_INVALID_HANDLE); Require(sceMouseClose(MOUSE_HANDLE) == MOUSE_ERROR_INVALID_HANDLE); MouseOpenParam merged{}; merged.behaviorFlag = MOUSE_OPEN_PARAM_MERGED; Require(sceMouseOpen(1, 0, 0, &merged) == MOUSE_HANDLE); - Require(sceMouseResetAbsolutePosition(MOUSE_HANDLE) == MOUSE_OK); + Require(sceMouseResetAbsolutePosition(MOUSE_HANDLE, 959, 539) == MOUSE_OK); Require(sceMouseRead(MOUSE_HANDLE, data, 1) == 1 && data[0].buttons == 0); Require(sceMouseClose(MOUSE_HANDLE) == MOUSE_OK); - Require(sceMouseResetAbsolutePosition(MOUSE_HANDLE) == MOUSE_ERROR_INVALID_HANDLE); + Require(sceMouseResetAbsolutePosition(MOUSE_HANDLE, 959, 539) == MOUSE_ERROR_INVALID_HANDLE); } diff --git a/docs/dev/debt/libSceMouse.md b/docs/dev/debt/libSceMouse.md index bc13697232..a26decfa8d 100644 --- a/docs/dev/debt/libSceMouse.md +++ b/docs/dev/debt/libSceMouse.md @@ -2,4 +2,4 @@ ### Functional -- [sceMouseResetAbsolutePosition](../../../core/libs/prx/libSceMouse/Export.cpp) (libSceMouse) - the signature and behaviour are not documented on hardware; accepts a device handle matching `sceMouseClose` and `sceMouseRead`, validates initialization and open state, and returns `MOUSE_OK` (0) as the console mouse reports relative motion only +- [sceMouseResetAbsolutePosition](../../../core/libs/prx/libSceMouse/Export.cpp) (libSceMouse) - signature taken from a call site (a handle, then x and y as 16-bit values: PPSA30601 passes the centre of its display size); behaviour not measured on hardware. Validates initialization and the handle like `sceMouseRead` and returns `MOUSE_OK`; the position is not stored because the library reports relative motion only From bd9d10b4ce3989899e2c38005e2819fab4113975 Mon Sep 17 00:00:00 2001 From: Leonardo Baldazzi Date: Wed, 14 Oct 2026 20:21:38 +0200 Subject: [PATCH 0589/1000] fix: correct audio-video playback timing and sync --- core/libs/SceTypes.hpp | 5 +- core/libs/prx/libSceAudioOut/src/AudioOut.cpp | 27 +++++--- .../tests/AudioOutLastOutputTime.cpp | 45 ++++++++++++ core/libs/prx/libSceAvPlayer/Export.cpp | 2 +- .../prx/libSceAvPlayer/include/AvPlayer.hpp | 4 +- core/libs/prx/libSceAvPlayer/src/Player.cpp | 8 ++- core/libs/prx/libSceAvPlayer/src/Source.cpp | 6 +- core/libs/tests/GuestAvPlayer.cpp | 69 +++++++++++++++++++ 8 files changed, 151 insertions(+), 15 deletions(-) diff --git a/core/libs/SceTypes.hpp b/core/libs/SceTypes.hpp index 1338fe31b9..eb37561838 100644 --- a/core/libs/SceTypes.hpp +++ b/core/libs/SceTypes.hpp @@ -680,10 +680,13 @@ struct AvPlayerVideoEx { std::uint8_t luma_bit_depth; std::uint8_t chroma_bit_depth; bool video_full_range_flag; - std::uint8_t reserved1[37]; + std::uint8_t reserved1[5]; + double frame_rate; + std::uint8_t reserved2[24]; }; static_assert(sizeof(AvPlayerVideoEx) == 80); static_assert(offsetof(AvPlayerVideoEx, crop_left_offset) == 20 && offsetof(AvPlayerVideoEx, pitch) == 36); +static_assert(offsetof(AvPlayerVideoEx, frame_rate) == 48); struct AvPlayerTimedTextEx { std::uint8_t language_code[4]; diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut.cpp b/core/libs/prx/libSceAudioOut/src/AudioOut.cpp index dd844adea9..5f6d3a37b4 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut.cpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut.cpp @@ -1,5 +1,3 @@ -#include -#include #include #include #include @@ -110,7 +108,7 @@ struct Port { }; static std::mutex g_mutex; -static std::mutex g_outputMutex; +static std::array g_outputMutexes; static Port g_ports[PORTS_MAX]; static bool g_sdlInitialized = false; @@ -351,7 +349,8 @@ int APS5_VABI sceAudioOutOpen(int userId, int type, int index, std::uint32_t len } int APS5_VABI sceAudioOutClose(int handle) { - std::lock_guard outputLock(g_outputMutex); + if (handle <= 0 || handle > PORTS_MAX) return -2144993277; + std::lock_guard outputLock(g_outputMutexes[handle - 1]); std::lock_guard lock(g_mutex); Port* port = getPort(handle); if (port == nullptr) { @@ -364,7 +363,8 @@ int APS5_VABI sceAudioOutClose(int handle) { int APS5_VABI sceAudioOutOutput(int handle, const void* ptr) try { const AudioOutCall call; - std::lock_guard outputLock(g_outputMutex); + if (handle <= 0 || handle > PORTS_MAX) return -2144993277; + std::lock_guard outputLock(g_outputMutexes[handle - 1]); std::unique_lock lock(g_mutex); Port* port = getPort(handle); if (port == nullptr) { @@ -383,12 +383,20 @@ int APS5_VABI sceAudioOutOutput(int handle, const void* ptr) try { } int APS5_VABI sceAudioOutOutputs(AudioOutOutputParam* param, std::uint32_t num) try { + const AudioOutCall call; if (param == nullptr || num == 0) { return -2144993276; } - const AudioOutCall call; - std::lock_guard outputLock(g_outputMutex); + std::array selected{}; + for (std::uint32_t i = 0; i < num; ++i) { + if (param[i].handle <= 0 || param[i].handle > PORTS_MAX) return -2144993277; + selected[param[i].handle - 1] = true; + } + std::array, PORTS_MAX> outputLocks; + for (int index = 0; index < PORTS_MAX; ++index) { + if (selected[index]) outputLocks[index] = std::unique_lock(g_outputMutexes[index]); + } std::unique_lock lock(g_mutex); for (std::uint32_t i = 0; i < num; i++) { @@ -551,7 +559,10 @@ int APS5_VABI sceAudioOutSetMixLevelPadSpk(int handle, int mixLevel) { } int APS5_VABI sceAudioOutExClose(int handle) { - std::lock_guard outputLock(g_outputMutex); + if (handle <= 0 || handle > PORTS_MAX) { + throw std::runtime_error("sceAudioOutExClose: handle " + std::to_string(handle) + " is not an open Ex port"); + } + std::lock_guard outputLock(g_outputMutexes[handle - 1]); std::lock_guard lock(g_mutex); Port* port = getPort(handle); if (port == nullptr || port->type != PORT_TYPE_EX) { diff --git a/core/libs/prx/libSceAudioOut/tests/AudioOutLastOutputTime.cpp b/core/libs/prx/libSceAudioOut/tests/AudioOutLastOutputTime.cpp index c06cc14d1d..e6cdeed0d7 100644 --- a/core/libs/prx/libSceAudioOut/tests/AudioOutLastOutputTime.cpp +++ b/core/libs/prx/libSceAudioOut/tests/AudioOutLastOutputTime.cpp @@ -156,6 +156,48 @@ void TestErrors() { Require(destination == untouched, "a rejected call must not write the destination"); } +void TestIndependentPortOutputDuringWait(bool batch) { + const std::vector longBlock(frequency, 256); + const std::vector shortBlock(frames, 256); + const int waiting = sceAudioOutOpen(user, portTypeVibration, 0, frequency, frequency, formatS16Mono); + const int independent = Open(portTypeVibration); + Require(waiting > 0, "waiting port must open"); + Require(sceAudioOutOutput(waiting, longBlock.data()) == static_cast(frequency), "initial output failed"); + std::atomic started = false; + std::thread output([&] { + started = true; + AudioOutOutputParam param{waiting, longBlock.data()}; + const int result = batch ? sceAudioOutOutputs(¶m, 1) : sceAudioOutOutput(waiting, longBlock.data()); + Require(result == static_cast(frequency), "paced output failed"); + }); + while (!started) std::this_thread::yield(); + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + const auto before = std::chrono::steady_clock::now(); + Require(sceAudioOutOutput(independent, shortBlock.data()) == static_cast(frames), "independent output failed"); + const auto elapsed = std::chrono::steady_clock::now() - before; + output.join(); + Require(sceAudioOutClose(waiting) == 0 && sceAudioOutClose(independent) == 0, "ports must close"); + std::fprintf(stderr, "Independent port output waited %lld ms\n", static_cast(std::chrono::duration_cast(elapsed).count())); + Require(elapsed < std::chrono::milliseconds(100), "pacing one audio port must not block output on another port"); +} + +void TestOverlappingOutputBatches() { + const std::vector block(frames, 256); + const int first = Open(portTypeVibration); + const int second = Open(portTypeVibration); + AudioOutOutputParam duplicate[2] = {{first, block.data()}, {first, block.data()}}; + Require(sceAudioOutOutputs(duplicate, 2) == static_cast(frames), "duplicate handles in a batch must not deadlock"); + const auto output = [&](bool reverse) { + AudioOutOutputParam params[2] = {{reverse ? second : first, block.data()}, {reverse ? first : second, block.data()}}; + for (int i = 0; i < 12; ++i) Require(sceAudioOutOutputs(params, 2) == static_cast(frames), "overlapping output batch failed"); + }; + std::thread forward(output, false); + std::thread reverse(output, true); + forward.join(); + reverse.join(); + Require(sceAudioOutClose(first) == 0 && sceAudioOutClose(second) == 0, "batch ports must close"); +} + void TestQueriesDuringOutput(bool batch, bool drain) { const std::vector block(frequency, 256); const int handle = sceAudioOutOpen(user, portTypeVibration, 0, frequency, frequency, formatS16Mono); @@ -196,6 +238,9 @@ int main() { TestReopenedPort(); TestErrors(); TestPortWithoutDeviceKeepsRealTime(); + TestIndependentPortOutputDuringWait(false); + TestIndependentPortOutputDuringWait(true); + TestOverlappingOutputBatches(); TestQueriesDuringOutput(false, false); TestQueriesDuringOutput(false, true); TestQueriesDuringOutput(true, false); diff --git a/core/libs/prx/libSceAvPlayer/Export.cpp b/core/libs/prx/libSceAvPlayer/Export.cpp index fc78bf88b5..59ce8e72a8 100644 --- a/core/libs/prx/libSceAvPlayer/Export.cpp +++ b/core/libs/prx/libSceAvPlayer/Export.cpp @@ -112,7 +112,7 @@ int APS5_VABI sceAvPlayerInitEx(const AvPlayerInitDataEx* init_ex, AvPlayerInter init.auto_start = init_ex->auto_start; } init.default_language = init_ex->default_language; - *handle = new Player(init, VideoBufferCount(init.num_output_video_framebuffers)); + *handle = new Player(init, VideoBufferCount(init.num_output_video_framebuffers), init_ex->this_size >= Ps5InitDataExSize); return SCE_OK; } diff --git a/core/libs/prx/libSceAvPlayer/include/AvPlayer.hpp b/core/libs/prx/libSceAvPlayer/include/AvPlayer.hpp index 40ea457572..97d3be2383 100644 --- a/core/libs/prx/libSceAvPlayer/include/AvPlayer.hpp +++ b/core/libs/prx/libSceAvPlayer/include/AvPlayer.hpp @@ -32,6 +32,7 @@ constexpr std::int32_t EventWarningId = 0x20; constexpr std::uint32_t StreamTypeVideo = 1; constexpr std::uint32_t StreamTypeAudio = 0; +constexpr std::uint32_t Ps5StreamTypeAudio = 2; constexpr std::uint32_t StreamTypeTimedText = 2; constexpr std::uint32_t SourceTypeUnknown = 0; @@ -86,7 +87,7 @@ std::uint32_t DetectSourceType(std::string_view path); class Player final : public AvPlayerInternal, private ISourceEvents { public: - Player(const AvPlayerInitData& init, std::uint32_t bufferCount); + Player(const AvPlayerInitData& init, std::uint32_t bufferCount, bool ps5Layout = false); ~Player() override; Player(const Player&) = delete; Player& operator=(const Player&) = delete; @@ -138,6 +139,7 @@ private: AvPlayerMemAllocator memory{}; AvPlayerEventReplacement callback{}; bool autoStartEnabled = false; + bool ps5Layout = false; char language[4]{}; std::uint32_t videoBuffers = 2; diff --git a/core/libs/prx/libSceAvPlayer/src/Player.cpp b/core/libs/prx/libSceAvPlayer/src/Player.cpp index f5c51ce58e..cf28da31f7 100644 --- a/core/libs/prx/libSceAvPlayer/src/Player.cpp +++ b/core/libs/prx/libSceAvPlayer/src/Player.cpp @@ -61,8 +61,8 @@ std::uint32_t DetectSourceType(std::string_view path) { return SourceTypeUnknown; } -Player::Player(const AvPlayerInitData& init, std::uint32_t bufferCount) - : memory(WithGuestHeapFallback(init.memory_replacement)), callback(init.event_replacement), videoBuffers(bufferCount) { +Player::Player(const AvPlayerInitData& init, std::uint32_t bufferCount, bool ps5Layout) + : memory(WithGuestHeapFallback(init.memory_replacement)), callback(init.event_replacement), ps5Layout(ps5Layout), videoBuffers(bufferCount) { if (init.file_replacement.open && init.file_replacement.close && init.file_replacement.read_offset && init.file_replacement.size) { file = init.file_replacement; } @@ -169,7 +169,7 @@ void Player::autoStart() { return; } if (info.type == StreamTypeVideo && (video == -1 || SameLanguage(language, info.details.video.language_code))) video = index; - if (info.type == StreamTypeAudio && (audio == -1 || SameLanguage(language, info.details.audio.language_code))) audio = index; + if (info.type == (ps5Layout ? Ps5StreamTypeAudio : StreamTypeAudio) && (audio == -1 || SameLanguage(language, info.details.audio.language_code))) audio = index; } if (video != -1) EnableStream(static_cast(video)); if (audio != -1) EnableStream(static_cast(audio)); @@ -233,6 +233,7 @@ int Player::StreamCount() { int Player::GetStreamInfo(std::uint32_t index, AvPlayerStreamInfo& info) { std::lock_guard lock(mutex); if (!source || !source->GetStreamInfo(index, info)) return SCE_AVPLAYER_ERROR_OPERATION_FAILED; + if (ps5Layout && info.type == StreamTypeAudio) info.type = Ps5StreamTypeAudio; return SCE_OK; } @@ -240,6 +241,7 @@ int Player::GetStreamInfoEx(std::uint32_t index, AvPlayerStreamInfoEx& info) { std::lock_guard lock(mutex); AvPlayerStreamInfoEx described{}; if (!source || !source->GetStreamInfoEx(index, described)) return SCE_AVPLAYER_ERROR_OPERATION_FAILED; + if (ps5Layout && described.type == StreamTypeAudio) described.type = Ps5StreamTypeAudio; described.this_size = info.this_size; info = described; return SCE_OK; diff --git a/core/libs/prx/libSceAvPlayer/src/Source.cpp b/core/libs/prx/libSceAvPlayer/src/Source.cpp index 1d92faa018..ce33d6b17d 100644 --- a/core/libs/prx/libSceAvPlayer/src/Source.cpp +++ b/core/libs/prx/libSceAvPlayer/src/Source.cpp @@ -516,7 +516,7 @@ private: } if (synced || audioDriving) { const auto now = presentationClock.Now(); - if (synced && static_cast(front.info.timestamp) > now) return false; + if (static_cast(front.info.timestamp) > now) return false; while (frames.size() > 1 && frames[1].epoch == frames.front().epoch && static_cast(frames[1].info.timestamp) <= now) recycleFront(video); } present(video, info); @@ -580,6 +580,8 @@ private: picture.luma_bit_depth = 8; picture.chroma_bit_depth = 8; picture.video_full_range_flag = parameters->color_range == AVCOL_RANGE_JPEG; + const auto rate = av_guess_frame_rate(format, const_cast(&stream), nullptr); + picture.frame_rate = rate.num > 0 && rate.den > 0 ? av_q2d(rate) : 0.0; } else { auto& sound = info.details.audio; sound.channel_count = basic.details.audio.channel_count; @@ -988,6 +990,8 @@ private: info.details.video.luma_bit_depth = 8; info.details.video.chroma_bit_depth = 8; info.details.video.video_full_range_flag = frame.color_range == AVCOL_RANGE_JPEG; + const auto rate = av_guess_frame_rate(format, format->streams[index], nullptr); + info.details.video.frame_rate = rate.num > 0 && rate.den > 0 ? av_q2d(rate) : 0.0; publish(decoder, output); return true; } diff --git a/core/libs/tests/GuestAvPlayer.cpp b/core/libs/tests/GuestAvPlayer.cpp index 72683d0aaf..a8a3ff431d 100644 --- a/core/libs/tests/GuestAvPlayer.cpp +++ b/core/libs/tests/GuestAvPlayer.cpp @@ -1033,8 +1033,22 @@ void TestPs5ExtendedInitLayout() { AvPlayerInternal* player = nullptr; Check(sceAvPlayerInitEx(reinterpret_cast(raw.data()), &player) == 0 && player != nullptr, "PS5 extended init failed"); Check(sceAvPlayerAddSource(player, "/app0/avplayer.mp4") == 0, "add source after the PS5 extended init failed"); + AvPlayerStreamInfo audioInfo{}; + Check(sceAvPlayerGetStreamInfo(player, EnglishAudioStream, &audioInfo) == 0, "PS5 audio stream info failed"); + Check(audioInfo.type == 2, "PS5 audio stream must use type 2 so Unity enables it"); + AvPlayerStreamInfoEx audioInfoEx{}; + audioInfoEx.this_size = sizeof(audioInfoEx); + Check(sceAvPlayerGetStreamInfoEx(player, EnglishAudioStream, &audioInfoEx) == 0 && audioInfoEx.type == 2, "PS5 extended audio stream type is incorrect"); + AvPlayerStreamInfoEx videoInfo{}; + videoInfo.this_size = sizeof(videoInfo); + Check(sceAvPlayerGetStreamInfoEx(player, VideoStream, &videoInfo) == 0, "PS5 video stream info failed"); + double frameRate = 0; + std::memcpy(&frameRate, reinterpret_cast(&videoInfo) + 0x40, sizeof(frameRate)); + Check(std::fabs(frameRate - FrameRate) < 0.01, "PS5 extended video frame rate at 0x40 is missing"); if (autoStart) { Check(WaitFor([&] { return events.Seen(EventPlay); }), "auto start at 0x74 of the PS5 extended init did not start playback"); + AvPlayerFrameInfo audioFrame{}; + Check(WaitFor([&] { return sceAvPlayerGetAudioData(player, &audioFrame) != 0; }), "PS5 auto start did not enable audio"); } else { Check(WaitFor([&] { return events.Seen(EventReady); }), "ready event missing after the PS5 extended init"); std::this_thread::sleep_for(std::chrono::milliseconds(200)); @@ -1043,6 +1057,10 @@ void TestPs5ExtendedInitLayout() { Check(sceAvPlayerStart(player) == 0, "start after the PS5 extended init failed"); } Check(WaitFor([&] { return TextureCount() == Buffers + DecodeAheadBuffers; }), "video frame buffer count at 0x228 of the PS5 extended init not used"); + AvPlayerFrameInfoEx videoFrame{}; + Check(WaitFor([&] { return sceAvPlayerGetVideoDataEx(player, &videoFrame) != 0; }), "PS5 playback did not deliver video"); + std::memcpy(&frameRate, reinterpret_cast(&videoFrame) + 0x48, sizeof(frameRate)); + Check(std::fabs(frameRate - FrameRate) < 0.01, "PS5 video frame rate at 0x48 is missing"); Check(sceAvPlayerClose(player) == 0, "close after the PS5 extended init failed"); }; run(false); @@ -1236,6 +1254,55 @@ void TestLoopWarningSharedByStreams() { Check(sceAvPlayerClose(player) == 0, "shared loop timing close failed"); } +void TestUnsyncedVideoDoesNotRunAheadOfAudio() { + Events events; + AvPlayerInitData init = InitData(&events); + auto* player = sceAvPlayerInit(&init); + Check(player != nullptr, "init failed"); + Check(sceAvPlayerSetAvSyncMode(player, 1) == 0, "sync mode rejected"); + Check(sceAvPlayerAddSource(player, "/app0/avplayer.mp4") == 0, "add source failed"); + Check(sceAvPlayerEnableStream(player, VideoStream) == 0, "enable video failed"); + Check(sceAvPlayerEnableStream(player, EnglishAudioStream) == 0, "enable audio failed"); + Check(sceAvPlayerStart(player) == 0, "start failed"); + AvPlayerFrameInfo sound{}; + Check(WaitFor([&] { return sceAvPlayerGetAudioData(player, &sound) != 0; }), "no audio"); + const auto start = std::chrono::steady_clock::now(); + const auto first = sound.timestamp; + std::uint64_t worst = 0; + int frames = 0; + while (std::chrono::steady_clock::now() - start < std::chrono::milliseconds(700)) { + const auto elapsed = static_cast(std::chrono::duration_cast(std::chrono::steady_clock::now() - start).count()); + while (sound.timestamp <= first + elapsed && sceAvPlayerGetAudioData(player, &sound)) CheckEnglishAudio(sound); + AvPlayerFrameInfoEx frame{}; + if (sceAvPlayerGetVideoDataEx(player, &frame)) { + CheckVideoFrame(frame); + ++frames; + if (frame.timestamp > first + elapsed) worst = std::max(worst, frame.timestamp - first - elapsed); + } + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + Check(frames >= 10, "video stalled while following audio"); + Check(worst <= 67, "video ran " + std::to_string(worst) + " ms ahead of paced audio"); + Check(sceAvPlayerClose(player) == 0, "close failed"); +} + +void TestLoopWarningWaitsForPlayback() { + Events events; + AvPlayerInitData init = InitData(&events); + auto* player = sceAvPlayerInit(&init); + Check(player != nullptr, "loop warning init failed"); + Check(sceAvPlayerAddSource(player, "/app0/avplayer.mp4") == 0, "loop warning source failed"); + Check(sceAvPlayerSetLooping(player, 1) == 0, "loop warning setting failed"); + Check(sceAvPlayerEnableStream(player, VideoStream) == 0, "loop warning video failed"); + Check(sceAvPlayerEnableStream(player, EnglishAudioStream) == 0, "loop warning audio failed"); + Check(sceAvPlayerStart(player) == 0, "loop warning start failed"); + AvPlayerFrameInfoEx frame{}; + Check(WaitFor([&] { return sceAvPlayerGetVideoDataEx(player, &frame) != 0; }), "loop warning first frame missing"); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + Check(!events.Seen(EventWarning, LoopingBack), "loop warning arrived while the first playback still had buffered audio and video"); + Check(sceAvPlayerClose(player) == 0, "loop warning close failed"); +} + void TestEofStopLatency() { constexpr int Rounds = 9; std::array elapsed{}; @@ -1369,6 +1436,8 @@ int main(int argc, char** argv) { TestAutoStartKeepsStartedPlayback(); TestRestartAfterPausedJumpKeepsClockRunning(); TestUnsyncedVideoKeepsUpWithAudio(); + TestUnsyncedVideoDoesNotRunAheadOfAudio(); + TestLoopWarningWaitsForPlayback(); TestEofStopLatency(); std::puts("AvPlayer tests passed"); return 0; From 742aeb4211ec462c300cb7cd696103a9aaf78cb5 Mon Sep 17 00:00:00 2001 From: Leonardo Baldazzi Date: Thu, 15 Oct 2026 23:01:43 +0200 Subject: [PATCH 0590/1000] fix(avplayer): preserve caller-paced delivery in no-sync mode --- core/libs/prx/libSceAudioOut/CMakeLists.txt | 9 ++ .../tests/AudioOutPortLocks.cpp | 135 ++++++++++++++++++ core/libs/prx/libSceAvPlayer/src/Source.cpp | 2 +- core/libs/tests/GuestAvPlayer.cpp | 53 ++++--- 4 files changed, 170 insertions(+), 29 deletions(-) create mode 100644 core/libs/prx/libSceAudioOut/tests/AudioOutPortLocks.cpp diff --git a/core/libs/prx/libSceAudioOut/CMakeLists.txt b/core/libs/prx/libSceAudioOut/CMakeLists.txt index 2e6e7995d9..0da434395f 100644 --- a/core/libs/prx/libSceAudioOut/CMakeLists.txt +++ b/core/libs/prx/libSceAudioOut/CMakeLists.txt @@ -20,6 +20,15 @@ configure_windows_unwind(libSceAudioOut) if(BUILD_TESTING) if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + add_test_executable(audio_out_port_locks_tests tests/AudioOutPortLocks.cpp) + target_include_directories(audio_out_port_locks_tests PRIVATE ${LIBS_INCLUDE_DIR} ${ANYPS5_SDL2_INCLUDE_DIR}) + target_link_libraries(audio_out_port_locks_tests PRIVATE libSceAudioOut libc) + set_target_properties(audio_out_port_locks_tests PROPERTIES ENABLE_EXPORTS ON) + add_test(NAME audio_out_independent_output COMMAND audio_out_port_locks_tests output) + add_test(NAME audio_out_independent_close COMMAND audio_out_port_locks_tests close) + add_test(NAME audio_out_same_port_close COMMAND audio_out_port_locks_tests same-close) + set_tests_properties(audio_out_independent_output audio_out_independent_close audio_out_same_port_close PROPERTIES TIMEOUT 10) + add_test_executable(audio_out_resampling_tests tests/AudioOutResampling.cpp) target_include_directories(audio_out_resampling_tests PRIVATE ${LIBS_INCLUDE_DIR} ${ANYPS5_SDL2_INCLUDE_DIR}) target_link_libraries(audio_out_resampling_tests PRIVATE libSceAudioOut libc) diff --git a/core/libs/prx/libSceAudioOut/tests/AudioOutPortLocks.cpp b/core/libs/prx/libSceAudioOut/tests/AudioOutPortLocks.cpp new file mode 100644 index 0000000000..107a2ef2f5 --- /dev/null +++ b/core/libs/prx/libSceAudioOut/tests/AudioOutPortLocks.cpp @@ -0,0 +1,135 @@ +#include "SDL.h" +#include "prx/libc/include/general/VabiMacros.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceAudioOutOpen(int, int, int, std::uint32_t, std::uint32_t, std::uint32_t); +int APS5_VABI sceAudioOutOutput(int, const void*); +int APS5_VABI sceAudioOutClose(int); +} + +namespace { + +constexpr std::uint32_t Grain = 256; +std::atomic nextDevice{2}; +std::atomic blocked{false}; +std::atomic released{false}; +std::atomic clockQueries{0}; +std::atomic clears{0}; +std::array, 4> queued{}; +std::array, 4> closed{}; +std::mutex stateMutex; +std::condition_variable changed; + +void Require(bool condition, const char* message) { + if (condition) return; + std::fprintf(stderr, "AudioOut port locks: %s\n", message); + std::abort(); +} + +bool WaitFor(const std::atomic& condition, std::chrono::milliseconds timeout) { + std::unique_lock lock(stateMutex); + return changed.wait_for(lock, timeout, [&] { return condition.load(std::memory_order_acquire); }); +} + +} + +extern "C" std::uint64_t APS5_VABI sceKernelGetProcessTime(void) { + clockQueries.fetch_add(1, std::memory_order_relaxed); + return 1; +} + +extern "C" int SDLCALL SDL_InitSubSystem(Uint32) { return 0; } +extern "C" Uint32 SDLCALL SDL_WasInit(Uint32) { return SDL_INIT_AUDIO; } +extern "C" void SDLCALL SDL_PauseAudioDevice(SDL_AudioDeviceID, int) {} + +extern "C" SDL_AudioDeviceID SDLCALL SDL_OpenAudioDevice(const char*, int, const SDL_AudioSpec* desired, SDL_AudioSpec* obtained, int) { + *obtained = *desired; + return nextDevice.fetch_add(1, std::memory_order_relaxed); +} + +extern "C" Uint32 SDLCALL SDL_GetQueuedAudioSize(SDL_AudioDeviceID device) { + Require(device == 2 || device == 3, "unknown device polled"); + if (device == 2 && !released.load(std::memory_order_acquire)) { + blocked.store(true, std::memory_order_release); + changed.notify_all(); + return 1u << 20; + } + return 0; +} + +extern "C" void SDLCALL SDL_ClearQueuedAudio(SDL_AudioDeviceID device) { + if (device == 2 && !released.load(std::memory_order_acquire)) { + clears.fetch_add(1, std::memory_order_relaxed); + released.store(true, std::memory_order_release); + } +} + +extern "C" int SDLCALL SDL_QueueAudio(SDL_AudioDeviceID device, const void* data, Uint32 bytes) { + Require(device == 2 || device == 3, "unknown device queued"); + Require(!closed[device].load(std::memory_order_acquire), "output used a closed device"); + Require(bytes == Grain * sizeof(std::int16_t), "output size changed"); + const auto* samples = static_cast(data); + const auto expected = device == 2 ? 1000 : 2000; + for (std::uint32_t i = 0; i < Grain; ++i) Require(samples[i] == expected, "output samples changed"); + queued[device].fetch_add(1, std::memory_order_relaxed); + return 0; +} + +extern "C" void SDLCALL SDL_CloseAudioDevice(SDL_AudioDeviceID device) { + Require(device == 2 || device == 3, "unknown device closed"); + Require(!closed[device].exchange(true, std::memory_order_acq_rel), "device closed twice"); +} + +int main(int argc, char** argv) { + Require(argc == 2, "expected a test mode"); + const std::string_view mode(argv[1]); + const bool unrelatedOutput = mode == "output"; + const bool unrelatedClose = mode == "close"; + const bool samePortClose = mode == "same-close"; + Require(unrelatedOutput || unrelatedClose || samePortClose, "unknown test mode"); + const int first = sceAudioOutOpen(0, 0, 0, Grain, 48000, 0); + const int second = sceAudioOutOpen(0, 0, 0, Grain, 48000, 0); + Require(first > 0 && second > 0 && first != second, "port creation failed"); + std::array firstSamples; + std::array secondSamples; + firstSamples.fill(1000); + secondSamples.fill(2000); + int firstResult = -1; + int secondResult = -1; + std::atomic secondStarted{false}; + std::atomic secondDone{false}; + std::thread producer([&] { firstResult = sceAudioOutOutput(first, firstSamples.data()); }); + Require(WaitFor(blocked, std::chrono::seconds(2)), "first port did not enter backpressure"); + std::thread other([&] { + secondStarted.store(true, std::memory_order_release); + changed.notify_all(); + secondResult = unrelatedOutput ? sceAudioOutOutput(second, secondSamples.data()) : sceAudioOutClose(samePortClose ? first : second); + secondDone.store(true, std::memory_order_release); + changed.notify_all(); + }); + Require(WaitFor(secondStarted, std::chrono::seconds(2)), "second operation did not start"); + const bool completedWhileBlocked = WaitFor(secondDone, std::chrono::milliseconds(samePortClose ? 150 : 2000)); + released.store(true, std::memory_order_release); + producer.join(); + other.join(); + Require(clockQueries.load() != 0 && clears.load() == 0, "drain timeout invalidated the controlled wait"); + Require(firstResult == static_cast(Grain), "blocked output failed"); + Require(secondResult == (unrelatedOutput ? static_cast(Grain) : 0), "second operation failed"); + Require(completedWhileBlocked != samePortClose, "port output/close lock scope is wrong"); + Require(queued[2].load() == 1 && queued[3].load() == (unrelatedOutput ? 1u : 0u), "unexpected queued output count"); + if (!samePortClose) Require(sceAudioOutClose(first) == 0, "first port close failed"); + if (!unrelatedClose) Require(sceAudioOutClose(second) == 0, "second port close failed"); + Require(closed[2].load() && closed[3].load(), "a device remained open"); + return 0; +} diff --git a/core/libs/prx/libSceAvPlayer/src/Source.cpp b/core/libs/prx/libSceAvPlayer/src/Source.cpp index ce33d6b17d..3844a906b5 100644 --- a/core/libs/prx/libSceAvPlayer/src/Source.cpp +++ b/core/libs/prx/libSceAvPlayer/src/Source.cpp @@ -516,7 +516,7 @@ private: } if (synced || audioDriving) { const auto now = presentationClock.Now(); - if (static_cast(front.info.timestamp) > now) return false; + if (synced && static_cast(front.info.timestamp) > now) return false; while (frames.size() > 1 && frames[1].epoch == frames.front().epoch && static_cast(frames[1].info.timestamp) <= now) recycleFront(video); } present(video, info); diff --git a/core/libs/tests/GuestAvPlayer.cpp b/core/libs/tests/GuestAvPlayer.cpp index a8a3ff431d..7fc12bf694 100644 --- a/core/libs/tests/GuestAvPlayer.cpp +++ b/core/libs/tests/GuestAvPlayer.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include #include #include "SceTypes.hpp" @@ -1254,36 +1255,32 @@ void TestLoopWarningSharedByStreams() { Check(sceAvPlayerClose(player) == 0, "shared loop timing close failed"); } -void TestUnsyncedVideoDoesNotRunAheadOfAudio() { +void TestNoSyncVideoCanLeadAudioClock() { Events events; AvPlayerInitData init = InitData(&events); auto* player = sceAvPlayerInit(&init); - Check(player != nullptr, "init failed"); - Check(sceAvPlayerSetAvSyncMode(player, 1) == 0, "sync mode rejected"); - Check(sceAvPlayerAddSource(player, "/app0/avplayer.mp4") == 0, "add source failed"); - Check(sceAvPlayerEnableStream(player, VideoStream) == 0, "enable video failed"); - Check(sceAvPlayerEnableStream(player, EnglishAudioStream) == 0, "enable audio failed"); - Check(sceAvPlayerStart(player) == 0, "start failed"); - AvPlayerFrameInfo sound{}; - Check(WaitFor([&] { return sceAvPlayerGetAudioData(player, &sound) != 0; }), "no audio"); - const auto start = std::chrono::steady_clock::now(); - const auto first = sound.timestamp; - std::uint64_t worst = 0; - int frames = 0; - while (std::chrono::steady_clock::now() - start < std::chrono::milliseconds(700)) { - const auto elapsed = static_cast(std::chrono::duration_cast(std::chrono::steady_clock::now() - start).count()); - while (sound.timestamp <= first + elapsed && sceAvPlayerGetAudioData(player, &sound)) CheckEnglishAudio(sound); - AvPlayerFrameInfoEx frame{}; - if (sceAvPlayerGetVideoDataEx(player, &frame)) { - CheckVideoFrame(frame); - ++frames; - if (frame.timestamp > first + elapsed) worst = std::max(worst, frame.timestamp - first - elapsed); - } - std::this_thread::sleep_for(std::chrono::milliseconds(1)); - } - Check(frames >= 10, "video stalled while following audio"); - Check(worst <= 67, "video ran " + std::to_string(worst) + " ms ahead of paced audio"); - Check(sceAvPlayerClose(player) == 0, "close failed"); + Check(player != nullptr, "no-sync init failed"); + Check(sceAvPlayerSetAvSyncMode(player, 1) == 0, "no-sync mode rejected"); + Check(sceAvPlayerAddSource(player, "/app0/avplayer.mp4") == 0, "no-sync source failed"); + Check(sceAvPlayerEnableStream(player, VideoStream) == 0, "no-sync video failed"); + Check(sceAvPlayerEnableStream(player, EnglishAudioStream) == 0, "no-sync audio failed"); + Check(sceAvPlayerStart(player) == 0, "no-sync start failed"); + AvPlayerFrameInfo audio{}; + Check(WaitFor([&] { return sceAvPlayerGetAudioData(player, &audio) != 0; }), "no-sync audio unavailable"); + std::uint64_t lead = 0; + const bool available = WaitFor([&] { + AvPlayerFrameInfoEx video{}; + if (!sceAvPlayerGetVideoDataEx(player, &video)) return false; + CheckVideoFrame(video); + const auto clock = sceAvPlayerCurrentTime(player); + if (video.timestamp > clock) lead = std::max(lead, video.timestamp - clock); + return lead >= 100; + }, 500); + Check(sceAvPlayerClose(player) == 0, "no-sync close failed"); + std::printf("No-sync video lead: %llu ms\n", static_cast(lead)); + Check(available, "no-sync video was gated by the audio-backed player clock"); + std::lock_guard lock(allocations.mutex); + Check(allocations.blocks.empty(), "no-sync allocations leaked"); } void TestLoopWarningWaitsForPlayback() { @@ -1436,7 +1433,7 @@ int main(int argc, char** argv) { TestAutoStartKeepsStartedPlayback(); TestRestartAfterPausedJumpKeepsClockRunning(); TestUnsyncedVideoKeepsUpWithAudio(); - TestUnsyncedVideoDoesNotRunAheadOfAudio(); + TestNoSyncVideoCanLeadAudioClock(); TestLoopWarningWaitsForPlayback(); TestEofStopLatency(); std::puts("AvPlayer tests passed"); From b85f017e5edc99b7d4186ef3b84e8b0ba0ffdc69 Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Wed, 14 Oct 2026 06:00:42 +0000 Subject: [PATCH 0591/1000] fix(libScePad): return measured status for unknown Pad export --- core/libs/prx/libScePad/Export.cpp | 11 +++++++++-- core/libs/tests/GuestPad.cpp | 3 +++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libScePad/Export.cpp b/core/libs/prx/libScePad/Export.cpp index c65a5df868..30183072c9 100644 --- a/core/libs/prx/libScePad/Export.cpp +++ b/core/libs/prx/libScePad/Export.cpp @@ -1,5 +1,6 @@ #include #include +#include #include #include #include @@ -14,8 +15,11 @@ namespace { constexpr int PAD_ERROR_DEVICE_NOT_CONNECTED = static_cast(0x80920007); constexpr int PAD_ERROR_DEVICE_NO_HANDLE = static_cast(0x80920008); +constexpr int PAD_UNKNOWN_N3KSX62FGNO_BEFORE_INIT = static_cast(0x80920005); +constexpr int PAD_UNKNOWN_N3KSX62FGNO_AFTER_INIT = static_cast(0x809200ff); bool g_opened = false; +std::atomic_bool g_initialized{false}; bool ValidPort(int userId, int type, int index) { const bool personalPort = type == PAD_PORT_TYPE_STANDARD || type == PAD_PORT_TYPE_SPECIAL; @@ -143,6 +147,7 @@ int APS5_VABI scePadSetParticularMode(bool mode) { int APS5_VABI scePadInit_nid_postfix(void) { Pad::Initialize(); + g_initialized.store(true, std::memory_order_release); return PAD_OK; } @@ -312,8 +317,10 @@ int APS5_VABI scePadSetAngularVelocityBiasCorrectionState(int handle, bool enabl APS5_EXPORT("n3kSX62fgNo", scePadUnknown_n3kSX62fgNo); int APS5_VABI scePadUnknown_n3kSX62fgNo() { - NotImplemented_nid_no_patch(__func__); - return 0; + // Firmware 13.60 returns these status values before and after scePadInit. + // The NID's name and signature remain unknown; the measured call did not + // read or write any argument pointers. + return g_initialized.load(std::memory_order_acquire) ? PAD_UNKNOWN_N3KSX62FGNO_AFTER_INIT : PAD_UNKNOWN_N3KSX62FGNO_BEFORE_INIT; } int APS5_VABI scePadVrControllerGetTriggerEffectStates(void) { diff --git a/core/libs/tests/GuestPad.cpp b/core/libs/tests/GuestPad.cpp index f30bd4d91d..c591e97e66 100644 --- a/core/libs/tests/GuestPad.cpp +++ b/core/libs/tests/GuestPad.cpp @@ -17,6 +17,7 @@ int APS5_VABI scePadGetHandle(int, int, int); int APS5_VABI scePadSetVibrationTriggerEffectWeakWhileEmbeddedMicInUse(bool); int APS5_VABI scePadSetVibrationMode(int, int); int APS5_VABI scePadInit_nid_postfix(void); +int APS5_VABI scePadUnknown_n3kSX62fgNo(); int APS5_VABI scePadRead_nid_postfix(int, PadData*, int); int APS5_VABI scePadReadState(int, PadData*); int APS5_VABI scePadSetTiltCorrectionState(int, bool); @@ -100,6 +101,7 @@ static float SettleOrientationW() { static void CheckTiltCorrection(int handle) { Require(scePadInit_nid_postfix() == 0); + Require(scePadUnknown_n3kSX62fgNo() == static_cast(0x809200ff)); PadInputState tilted; tilted.hasMotion = true; tilted.accel = {9.80665f, 0.0f, 0.0f}; @@ -240,6 +242,7 @@ static void CheckInjectedState(const std::filesystem::path& path) { int main() { CheckInputCommands(); + Require(scePadUnknown_n3kSX62fgNo() == static_cast(0x80920005)); const auto path = std::filesystem::temp_directory_path() / ("anyps5-pad-state-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); struct Cleanup { std::filesystem::path path; From 513694a84848e0f0194e85944b07e3c05a826eb9 Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Wed, 14 Oct 2026 06:35:44 +0000 Subject: [PATCH 0592/1000] docs(libScePad): record unknown NID probe limits --- core/libs/prx/libScePad/Export.cpp | 3 --- docs/dev/debt/libScePad.md | 1 + 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/core/libs/prx/libScePad/Export.cpp b/core/libs/prx/libScePad/Export.cpp index 30183072c9..7bd967400a 100644 --- a/core/libs/prx/libScePad/Export.cpp +++ b/core/libs/prx/libScePad/Export.cpp @@ -317,9 +317,6 @@ int APS5_VABI scePadSetAngularVelocityBiasCorrectionState(int handle, bool enabl APS5_EXPORT("n3kSX62fgNo", scePadUnknown_n3kSX62fgNo); int APS5_VABI scePadUnknown_n3kSX62fgNo() { - // Firmware 13.60 returns these status values before and after scePadInit. - // The NID's name and signature remain unknown; the measured call did not - // read or write any argument pointers. return g_initialized.load(std::memory_order_acquire) ? PAD_UNKNOWN_N3KSX62FGNO_AFTER_INIT : PAD_UNKNOWN_N3KSX62FGNO_BEFORE_INIT; } diff --git a/docs/dev/debt/libScePad.md b/docs/dev/debt/libScePad.md index 9355c1db26..573dc9d373 100644 --- a/docs/dev/debt/libScePad.md +++ b/docs/dev/debt/libScePad.md @@ -2,6 +2,7 @@ ### Unknown function info +- [scePadUnknown_n3kSX62fgNo](../../../core/libs/prx/libScePad/Export.cpp) (libScePad, NID `n3kSX62fgNo`) - firmware 13.60 returned `0x80920005` before `scePadInit` and `0x809200ff` after it in the #3177 probe; the tested call read or wrote no argument pointers. PPSA15645 calls the import after `scePadOpen`, but its return and side effects in that state are unknown. The NID name and signature are unknown, so the statuses are not named by presumed error meanings - [scePadSetAngularVelocityDeadbandState](../../../core/libs/prx/libScePad/src/PadState.cpp) (libScePad) - with the deadband on, each angular velocity axis whose magnitude is below 0.017453292 rad/s (1 degree per second) reads as 0, before the orientation is integrated; it starts off. Threshold, per-axis test and default follow [duaLib](https://github.com/WujekFoliarz/duaLib/blob/03bad1bea2a36561b520846776f2a07ced6773e0/src/source/duaLib.cpp#L44), which reimplements the PC `libScePad.dll`; the console's threshold is not measured, and the deadband is kept per library rather than per handle since one pad handle exists - [scePadOpen and scePadOpenExt](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - special ports (type 2) return `DEVICE_NOT_CONNECTED` (0x80920007) because the host has no special controller, and `scePadGetHandle` has no handle for them; whether the console opens a handle and reports the device as disconnected later is unknown. scePadOpenExt handles only special ports. PPSA01317 passes a 16-byte param that names the device by vendor and product id (`b7 0e 04 0e ...` for 0x0eb7:0x0e04) and skips the user on any negative result. Other port types throw - [scePadVrControllerReadState](../../../core/libs/prx/libScePad/Export.cpp) (libScePad, NID `iA-DdobUen8`) - the name hashes to the imported NID; the `(int handle, void* state)` signature follows [PS5VR](https://github.com/theghostonline/PS5VR/blob/0e408909ce7a39891a2e06eee3438d07a09e11e3/app/src/app_vr.c#L326-L369) (PPSA99177), which passes a PS VR2 Sense controller handle from `scePadOpen` type 3 or 4 and a 0x68-byte state it reads as buttons at 0, stick bytes at 4 and a connected byte at 0x40. The title calls it only after `sceHmd2Initialize` succeeds, which it does not here, so every call throws; the state layout is otherwise unknown From 4ae8caac9a53da13137247ae354ec647e312f6e8 Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Fri, 16 Oct 2026 11:27:32 +0000 Subject: [PATCH 0593/1000] fix(libScePad): refuse unknown NID calls with open controller --- core/libs/prx/libScePad/Export.cpp | 1 + core/libs/tests/GuestPPSA01530Exports.cpp | 40 ----------------------- core/libs/tests/GuestPad.cpp | 17 ++++++++-- 3 files changed, 16 insertions(+), 42 deletions(-) delete mode 100644 core/libs/tests/GuestPPSA01530Exports.cpp diff --git a/core/libs/prx/libScePad/Export.cpp b/core/libs/prx/libScePad/Export.cpp index 7bd967400a..3dd4c0650a 100644 --- a/core/libs/prx/libScePad/Export.cpp +++ b/core/libs/prx/libScePad/Export.cpp @@ -317,6 +317,7 @@ int APS5_VABI scePadSetAngularVelocityBiasCorrectionState(int handle, bool enabl APS5_EXPORT("n3kSX62fgNo", scePadUnknown_n3kSX62fgNo); int APS5_VABI scePadUnknown_n3kSX62fgNo() { + if (g_opened) NotImplemented_nid_no_patch(__func__); return g_initialized.load(std::memory_order_acquire) ? PAD_UNKNOWN_N3KSX62FGNO_AFTER_INIT : PAD_UNKNOWN_N3KSX62FGNO_BEFORE_INIT; } diff --git a/core/libs/tests/GuestPPSA01530Exports.cpp b/core/libs/tests/GuestPPSA01530Exports.cpp deleted file mode 100644 index a903cdca4d..0000000000 --- a/core/libs/tests/GuestPPSA01530Exports.cpp +++ /dev/null @@ -1,40 +0,0 @@ -#include "prx/libc/include/general/VabiMacros.hpp" - -#include -#include -#include - -extern "C" { -int APS5_VABI scePadUnknown_n3kSX62fgNo(); -} - -namespace { - -struct Export { - const char* name; - int (APS5_VABI *function)(); -}; - -const Export exports[] = { - {"scePadUnknown_n3kSX62fgNo", scePadUnknown_n3kSX62fgNo}, -}; - -bool ReportsNotImplemented(const Export& entry) { - try { - entry.function(); - } catch (const std::runtime_error& error) { - if (std::string(error.what()) == std::string(entry.name) + " not implemented") return true; - std::fprintf(stderr, "%s threw: %s\n", entry.name, error.what()); - return false; - } - std::fprintf(stderr, "%s returned instead of reporting that it is not implemented\n", entry.name); - return false; -} - -} - -int main() { - bool correct = true; - for (const auto& entry : exports) correct &= ReportsNotImplemented(entry); - return correct ? 0 : 1; -} diff --git a/core/libs/tests/GuestPad.cpp b/core/libs/tests/GuestPad.cpp index c591e97e66..3ae0932e00 100644 --- a/core/libs/tests/GuestPad.cpp +++ b/core/libs/tests/GuestPad.cpp @@ -7,6 +7,8 @@ #include #include #include +#include +#include #include extern "C" { @@ -29,6 +31,16 @@ int APS5_VABI scePadIsRemoteController(int, bool*); static void Require(bool value) { if (!value) std::abort(); } +static void RequireUnknownPadNidNotImplemented() { + try { + scePadUnknown_n3kSX62fgNo(); + } catch (const std::runtime_error& error) { + Require(std::string(error.what()) == "scePadUnknown_n3kSX62fgNo not implemented"); + return; + } + std::abort(); +} + static void CheckInputCommands() { const auto path = std::filesystem::temp_directory_path() / ("anyps5-pad-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); struct Cleanup { @@ -100,8 +112,7 @@ static float SettleOrientationW() { } static void CheckTiltCorrection(int handle) { - Require(scePadInit_nid_postfix() == 0); - Require(scePadUnknown_n3kSX62fgNo() == static_cast(0x809200ff)); + RequireUnknownPadNidNotImplemented(); PadInputState tilted; tilted.hasMotion = true; tilted.accel = {9.80665f, 0.0f, 0.0f}; @@ -282,6 +293,8 @@ int main() { Require(scePadGetHandle(user, 0, 0) == noHandle); Require(scePadOpen_nid_postfix(user, PAD_PORT_TYPE_SPECIAL, 0, nullptr) == static_cast(0x80920007)); Require(scePadGetHandle(user, 0, 0) == noHandle); + Require(scePadInit_nid_postfix() == 0); + Require(scePadUnknown_n3kSX62fgNo() == static_cast(0x809200ff)); const int handle = scePadOpen_nid_postfix(user, 0, 0, nullptr); Require(handle > 0); Require(scePadGetHandle(user, 0, 0) == handle); From e35c62c84c07c76fa8466c70d3d826ea020d4117 Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Fri, 16 Oct 2026 20:40:43 +0000 Subject: [PATCH 0594/1000] test(libScePad): remove obsolete stub-only export check --- core/libs/CMakeLists.txt | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index 4e218b96f6..16428400e3 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -462,16 +462,6 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() - add_test_executable(guest_ppsa01530_exports_tests tests/GuestPPSA01530Exports.cpp) - target_include_directories(guest_ppsa01530_exports_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) - target_link_libraries(guest_ppsa01530_exports_tests PRIVATE libkernel libSceJson2 libSceNet libScePad libc) - configure_windows_unwind(guest_ppsa01530_exports_tests) - add_test(NAME guest_ppsa01530_exports COMMAND guest_ppsa01530_exports_tests) - if(WIN32) - set_tests_properties(guest_ppsa01530_exports PROPERTIES ENVIRONMENT_MODIFICATION - "PATH=path_list_prepend:$") - endif() - add_test_executable(guest_posix_random_tests tests/GuestPosixRandom.cpp) target_include_directories(guest_posix_random_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(guest_posix_random_tests PRIVATE libScePosixForWebKit) From 80a7ccddf2245d7e52f72524351ce86069133024 Mon Sep 17 00:00:00 2001 From: Hussein Kadhim Date: Wed, 14 Oct 2026 03:02:56 +0300 Subject: [PATCH 0595/1000] fix(libkernel): fail connect on a listening socket with EOPNOTSUPP FreeBSD's soconnectat refuses a socket with SO_ACCEPTCONN set with EOPNOTSUPP (45). The host answers EINVAL on Windows and EISCONN on Linux, so connect() on a listening stream socket returned the host's error. Answer EOPNOTSUPP for a listening stream socket before the address is read. --- core/libs/prx/libkernel/CMakeLists.txt | 10 ++++ core/libs/prx/libkernel/Socket/src/Udp.cpp | 6 +++ .../tests/GuestSocketConnectListening.cpp | 51 +++++++++++++++++++ 3 files changed, 67 insertions(+) create mode 100644 core/libs/tests/GuestSocketConnectListening.cpp diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 788ecf8d5b..eaf0961c7e 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -1235,6 +1235,16 @@ if(BUILD_TESTING) set_tests_properties(guest_socket_events PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_socket_connect_listening_tests ${LIBS_INCLUDE_DIR}/tests/GuestSocketConnectListening.cpp) + target_include_directories(guest_socket_connect_listening_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_socket_connect_listening_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_socket_connect_listening_tests) + add_test(NAME guest_socket_connect_listening COMMAND guest_socket_connect_listening_tests) + set_tests_properties(guest_socket_connect_listening PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_socket_connect_listening PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_elf_phdr_match_tests ${LIBS_INCLUDE_DIR}/tests/GuestElfPhdrMatch.cpp) target_include_directories(guest_elf_phdr_match_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_elf_phdr_match_tests PRIVATE libkernel libc) diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index f45edc6aa5..e9032b07fd 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -244,6 +244,11 @@ bool Address(const void* input, std::uint32_t length, sockaddr_storage& native, std::uint16_t& PortOf(sockaddr_storage& address) { return address.ss_family == AF_INET ? reinterpret_cast(address).sin_port : reinterpret_cast(address).sin6_port; } +bool Listening(const Socket& socket) { + int listening = 0; + socklen_t size = sizeof(listening); + return ::getsockopt(socket.value, SOL_SOCKET, SO_ACCEPTCONN, reinterpret_cast(&listening), &size) == 0 && listening != 0; +} int Disconnect(const Socket& socket) { sockaddr_storage peer{}; socklen_t peerSize = sizeof(peer); @@ -954,6 +959,7 @@ int APS5_VABI connect_nid_postfix(int descriptor, const void* address, std::uint if (socket->family != GuestUnix && socket->type == 2 && address && length >= 2 && static_cast(address)[1] == 0 && Disconnect(*socket) != 0) return -1; + if (socket->type == 1 && Listening(*socket)) return Fail(45); sockaddr_storage native{}; socklen_t size; if (!Address(address, length, native, size)) return -1; diff --git a/core/libs/tests/GuestSocketConnectListening.cpp b/core/libs/tests/GuestSocketConnectListening.cpp new file mode 100644 index 0000000000..d2ba0337b0 --- /dev/null +++ b/core/libs/tests/GuestSocketConnectListening.cpp @@ -0,0 +1,51 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include + +extern "C" { +int APS5_VABI socket_nid_postfix(int, int, int); +int APS5_VABI bind_nid_postfix(int, const void*, std::uint32_t); +int APS5_VABI listen_nid_postfix(int, int); +int APS5_VABI getsockname_nid_postfix(int, void*, std::uint32_t*); +int APS5_VABI connect_nid_postfix(int, const void*, std::uint32_t); +int APS5_VABI accept_nid_postfix(int, void*, std::uint32_t*); +int APS5_VABI close_nid_postfix(int); +int* APS5_VABI __error_nid_postfix(); +} + +namespace { + +constexpr int Inet = 2; +constexpr int Stream = 1; +constexpr int ErrorNotSupported = 45; + +void Require(bool condition, const char* what) { + if (condition) return; + std::fprintf(stderr, "%s (errno %d)\n", what, *__error_nid_postfix()); + std::abort(); +} + +} + +int main() { + const int listener = socket_nid_postfix(Inet, Stream, 0); + Require(listener >= 0, "socket failed"); + std::array address{16, Inet, 0, 0, 127, 0, 0, 1}; + Require(bind_nid_postfix(listener, address.data(), address.size()) == 0, "bind failed"); + std::uint32_t length = address.size(); + Require(getsockname_nid_postfix(listener, address.data(), &length) == 0 && length == address.size(), "getsockname failed"); + Require(listen_nid_postfix(listener, 4) == 0, "listen failed"); + + *__error_nid_postfix() = 0; + Require(connect_nid_postfix(listener, address.data(), address.size()) == -1 && *__error_nid_postfix() == ErrorNotSupported, + "connect on a listening socket must fail with EOPNOTSUPP (45)"); + + const int client = socket_nid_postfix(Inet, Stream, 0); + Require(client >= 0 && connect_nid_postfix(client, address.data(), address.size()) == 0, "a refused connect must leave the listener working"); + const int accepted = accept_nid_postfix(listener, nullptr, nullptr); + Require(accepted >= 0, "accept failed"); + Require(close_nid_postfix(accepted) == 0 && close_nid_postfix(client) == 0 && close_nid_postfix(listener) == 0, "close failed"); + return 0; +} From 0d188ba6bd447143a4897b4e73cba307c1bee056 Mon Sep 17 00:00:00 2001 From: Hussein Kadhim Date: Wed, 14 Oct 2026 03:24:08 +0300 Subject: [PATCH 0596/1000] fix(libkernel): validate the address before refusing connect on a listening socket FreeBSD reads and checks the address (getsockaddr) before soconnect refuses a listening socket with EOPNOTSUPP, so a null or short address on a listener fails with the address error. Move the listener check after the address is read and keep it ahead of the family check. --- core/libs/prx/libkernel/Socket/src/Udp.cpp | 2 +- .../tests/GuestSocketConnectListening.cpp | 21 +++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index e9032b07fd..242e639007 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -959,13 +959,13 @@ int APS5_VABI connect_nid_postfix(int descriptor, const void* address, std::uint if (socket->family != GuestUnix && socket->type == 2 && address && length >= 2 && static_cast(address)[1] == 0 && Disconnect(*socket) != 0) return -1; - if (socket->type == 1 && Listening(*socket)) return Fail(45); sockaddr_storage native{}; socklen_t size; if (!Address(address, length, native, size)) return -1; #ifdef _WIN32 if (socket->type == 1) RefuseClosedLoopbackAtOnce_nid_no_patch(socket->value, reinterpret_cast(&native)); #endif + if (socket->type == 1 && Listening(*socket)) return Fail(45); if (native.ss_family != (socket->family == 2 ? AF_INET : AF_INET6)) return Fail(47); { std::lock_guard lock(socket->modeMutex); diff --git a/core/libs/tests/GuestSocketConnectListening.cpp b/core/libs/tests/GuestSocketConnectListening.cpp index d2ba0337b0..9bfffa3172 100644 --- a/core/libs/tests/GuestSocketConnectListening.cpp +++ b/core/libs/tests/GuestSocketConnectListening.cpp @@ -42,6 +42,27 @@ int main() { Require(connect_nid_postfix(listener, address.data(), address.size()) == -1 && *__error_nid_postfix() == ErrorNotSupported, "connect on a listening socket must fail with EOPNOTSUPP (45)"); + const int plain = socket_nid_postfix(Inet, Stream, 0); + Require(plain >= 0, "socket failed"); + *__error_nid_postfix() = 0; + Require(connect_nid_postfix(plain, nullptr, address.size()) == -1, "connect with a null address must fail"); + const int nullError = *__error_nid_postfix(); + *__error_nid_postfix() = 0; + Require(connect_nid_postfix(listener, nullptr, address.size()) == -1 && *__error_nid_postfix() == nullError, + "connect on a listening socket with a null address must fail as on any socket, not with EOPNOTSUPP"); + *__error_nid_postfix() = 0; + Require(connect_nid_postfix(plain, address.data(), 1) == -1, "connect with a 1-byte address must fail"); + const int shortError = *__error_nid_postfix(); + *__error_nid_postfix() = 0; + Require(connect_nid_postfix(listener, address.data(), 1) == -1 && *__error_nid_postfix() == shortError, + "connect on a listening socket with a 1-byte address must fail as on any socket, not with EOPNOTSUPP"); + Require(nullError != ErrorNotSupported && shortError != ErrorNotSupported, "address errors must not be EOPNOTSUPP"); + Require(close_nid_postfix(plain) == 0, "close failed"); + std::array wrongFamily{28, 28}; + *__error_nid_postfix() = 0; + Require(connect_nid_postfix(listener, wrongFamily.data(), wrongFamily.size()) == -1 && *__error_nid_postfix() == ErrorNotSupported, + "connect on a listening socket with an address of another family must fail with EOPNOTSUPP (45)"); + const int client = socket_nid_postfix(Inet, Stream, 0); Require(client >= 0 && connect_nid_postfix(client, address.data(), address.size()) == 0, "a refused connect must leave the listener working"); const int accepted = accept_nid_postfix(listener, nullptr, nullptr); From 75d29f5b3f94a55b6446cc5667c9266826d10ad8 Mon Sep 17 00:00:00 2001 From: jradic <106374305+jradic@users.noreply.github.com> Date: Fri, 16 Oct 2026 16:03:14 -0500 Subject: [PATCH 0597/1000] fix(libkernel): allocate the Windows thread-local slot on first use WindowsThreadLocal::slot was a template static, so its dynamic initialization was unordered against the timer self-test in Time.cpp. With APS5_TRACE_TIMERS set, the self-test ran first, used slot 0 and aborted every title with "Cannot bind native thread state". The slot is now a function-local static. guest_nanosleep_timer_self_test runs guest_nanosleep_tests with APS5_TRACE_TIMERS=1 on Windows. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01KwsvKtVsZDLefysUM9m2QS --- core/libs/prx/libkernel/CMakeLists.txt | 3 +++ .../libkernel/Pthread/include/WindowsThreadLocal.hpp | 10 +++++++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 788ecf8d5b..d77e3714a7 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -578,6 +578,9 @@ if(BUILD_TESTING) if(WIN32) set_tests_properties(guest_nanosleep PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + add_test(NAME guest_nanosleep_timer_self_test COMMAND guest_nanosleep_tests) + set_tests_properties(guest_nanosleep_timer_self_test PROPERTIES TIMEOUT 30 ENVIRONMENT "APS5_TRACE_TIMERS=1" ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") endif() add_test_executable(guest_pthread_sem_tests ${LIBS_INCLUDE_DIR}/tests/GuestPthreadSem.cpp) target_include_directories(guest_pthread_sem_tests PRIVATE ${LIBS_INCLUDE_DIR}) diff --git a/core/libs/prx/libkernel/Pthread/include/WindowsThreadLocal.hpp b/core/libs/prx/libkernel/Pthread/include/WindowsThreadLocal.hpp index 2a42b292f4..1cd1f7ca70 100644 --- a/core/libs/prx/libkernel/Pthread/include/WindowsThreadLocal.hpp +++ b/core/libs/prx/libkernel/Pthread/include/WindowsThreadLocal.hpp @@ -8,6 +8,7 @@ template class WindowsThreadLocal { public: static T& Get() { + const DWORD slot = slotIndex(); if (slot == FLS_OUT_OF_INDEXES) throw std::runtime_error("Cannot allocate native thread state slot"); if (auto* value = static_cast(FlsGetValue(slot))) return *value; auto value = std::make_unique(); @@ -17,10 +18,13 @@ public: private: static void WINAPI Destroy(void* value) { - FlsSetValue(slot, value); + FlsSetValue(slotIndex(), value); delete static_cast(value); - FlsSetValue(slot, nullptr); + FlsSetValue(slotIndex(), nullptr); + } + static DWORD slotIndex() { + static const DWORD slot = FlsAlloc(Destroy); + return slot; } - inline static const DWORD slot = FlsAlloc(Destroy); }; #endif From c56dbc61660c72b096344ebfd1b9b31a3385d2d3 Mon Sep 17 00:00:00 2001 From: Savvanis Spyros Date: Sat, 17 Oct 2026 00:15:50 +0300 Subject: [PATCH 0598/1000] test(tools): build the select_tests fixture under a junction where Windows refuses symlinks The fixture builds its project under a directory symlink so that the paths CMake records differ from the resolved ones. Creating a symlink on Windows needs SeCreateSymbolicLinkPrivilege, which a user without Developer Mode or elevation lacks, so setUpClass failed with WinError 1314 and select_tests failed on every local Windows run. On that error the link is now a directory junction, a reparse point realpath resolves the same way. Where symlinks can be created nothing changes. Co-Authored-By: Claude Opus 5.5 --- tools/tests/test_select_tests.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tools/tests/test_select_tests.py b/tools/tests/test_select_tests.py index 434a3da749..1c34f5a93d 100644 --- a/tools/tests/test_select_tests.py +++ b/tools/tests/test_select_tests.py @@ -38,8 +38,14 @@ class SelectTests(unittest.TestCase): def setUpClass(cls): cls.directory = tempfile.TemporaryDirectory() (Path(cls.directory.name) / "real").mkdir() - (Path(cls.directory.name) / "link").symlink_to("real", target_is_directory=True) root = Path(cls.directory.name) / "link" + try: + root.symlink_to("real", target_is_directory=True) + except OSError as error: + if getattr(error, "winerror", None) != 1314: + raise + import _winapi + _winapi.CreateJunction(str(Path(cls.directory.name) / "real"), str(root)) cls.source, cls.build, cls.graph = root / "source", root / "build", root / "graph.json" for name, content in PROJECT.items(): (cls.source / name).parent.mkdir(parents=True, exist_ok=True) From ac64ea45242268e9dffd166f49c6162d7a85b57e Mon Sep 17 00:00:00 2001 From: alfalfa-star Date: Fri, 16 Oct 2026 23:20:51 +0200 Subject: [PATCH 0599/1000] test(agc): require the GPU route for the indirect rect-list draws agc_driver_rect_list_indirect compared the indirect draws' output with the direct draws only, so it also passed when the records were read on the CPU. Its records lived in a static array that is not guest memory, so every indirect draw fell back to the CPU path as not imported. The records now sit in a registered 64 KiB guest allocation, the test turns on APS5_PROFILE_DRAW and reads the indirect-draw counters through the new Graphics::IndirectDrawCounts, and it requires all four indirect draws on the GPU path. It fails with APS5_NO_RECT_INDIRECT=1 and with the IndirectDraw.cpp and Draw.cpp changes reverted (4 non-vertex path). Co-Authored-By: Claude Opus 5.5 --- .../libSceAgcDriver/Graphics/include/Draw.hpp | 1 + .../prx/libSceAgcDriver/Graphics/src/Draw.cpp | 6 ++++ .../tests/execution/RectListIndirect.cpp | 36 +++++++++++++++++-- 3 files changed, 40 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Draw.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Draw.hpp index d852d67432..88b940d5f8 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Draw.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Draw.hpp @@ -93,6 +93,7 @@ bool DrawRecipes(); // `rewritten` counts a GPU-side draw whose records were copied and patched with a constant. enum class IndirectDrawPath : std::uint8_t { Gpu, NotFolded, FetchUnknown, NonVertexPath, IndxOffset, DrawIndex, VertexRange, FeatureGap, PendingImage, PendingLabelOrCopy, NotImported, Disabled, Count }; void CountIndirectDraw(IndirectDrawPath path, double readMs, bool rewritten = false); +std::array(IndirectDrawPath::Count)> IndirectDrawCounts(); const char* IndirectDrawPathName(IndirectDrawPath path); // Draw packets that drew nothing, for the [draws] line: Nothing (an empty count, or no color writes diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index d7d4031790..0f6af92578 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -831,6 +831,12 @@ void CountIndirectDraw(IndirectDrawPath path, double readMs, bool rewritten) { stats.indirectReadUs += readMs * 1000.0; } +std::array IndirectDrawCounts() { + auto& stats = Profile(); + std::lock_guard lock(stats.mutex); + return stats.indirect; +} + void CountDrawSkip(DrawSkip kind, double us) { static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; if (!profile) return; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/RectListIndirect.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/RectListIndirect.cpp index aa000b4764..560be5f9d5 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/RectListIndirect.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/RectListIndirect.cpp @@ -1,5 +1,7 @@ #include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" #include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" +#include "prx/libc/include/GuestAllocations.hpp" #include "VulkanTestDevice.hpp" #include #include @@ -7,9 +9,11 @@ #include #include #include +#include #include #include #include +#include #include #include #include @@ -38,7 +42,9 @@ alignas(256) std::array DirectInstancedMemory{}; alignas(256) std::array IndirectInstancedMemory{}; alignas(256) std::array DirectIndexedMemory{}; alignas(256) std::array IndirectIndexedMemory{}; -alignas(256) std::array Records{3, 1, 0, 0, 0, 1, 0, 0, 6, 2, 0, 0, 6, 1, 0, 0, 0, 0, 0, 0}; +constexpr std::array RecordWords{3, 1, 0, 0, 0, 1, 0, 0, 6, 2, 0, 0, 6, 1, 0, 0, 0, 0, 0, 0}; +constexpr std::size_t RecordBytes = 65536; +std::uint32_t* Records = nullptr; alignas(256) std::array Indices{6, 7, 8, 3, 4, 5, 0, 0}; struct Header { @@ -71,7 +77,7 @@ std::vector DrawRectangles() { const auto slotVertex = reinterpret_cast(SlotVertexCode.data()); const auto indices = reinterpret_cast(Indices.data()); const auto pixel = reinterpret_cast(PixelCode.data()); - const auto records = reinterpret_cast(Records.data()); + const auto records = reinterpret_cast(Records); const std::vector> context{ {0x2d5, 0x2000}, {0x1b6, 0}, {0x207, 0x01200000}, {0x200, 0}, {0x203, 0x800}, @@ -158,6 +164,18 @@ void Same(const std::array& indirect, const std::array(path)); + } + Require(cpu.empty(), "indirect rect-list draws read their records on the CPU: " + cpu); + Require(counts[0] == 4, "the GPU drew " + std::to_string(counts[0]) + " indirect rect-list draws from their records, expected 4"); +} + void IndirectRectListsDrawAsDirectOnes() { DirectMemory.fill(Kept); IndirectMemory.fill(Kept); @@ -169,6 +187,12 @@ void IndirectRectListsDrawAsDirectOnes() { Registered(VertexHeader, VertexCode.data(), sizeof(VertexCode), 2); Registered(SlotVertexHeader, SlotVertexCode.data(), sizeof(SlotVertexCode), 2); Registered(PixelHeader, PixelCode.data(), sizeof(PixelCode), 1); + Records = static_cast(::operator new(RecordBytes, std::align_val_t{RecordBytes})); + std::copy(RecordWords.begin(), RecordWords.end(), Records); + { + GuestAllocations::Mutation mutation; + mutation.Add(Records, RecordBytes, true, true, true); + } auto commands = DrawRectangles(); Packet packet{commands.data(), static_cast(commands.size()), 0, {}}; Require(sceAgcDriverSubmitDcb(&packet) == 0, "the draw submission failed"); @@ -182,19 +206,25 @@ void IndirectRectListsDrawAsDirectOnes() { Same(IndirectInstancedMemory, DirectInstancedMemory, "rect-list draw of 6 vertices and 2 instances"); Require(Slots(DirectIndexedMemory) == "0 32 32 0", "the direct indexed rect-list draw covered pixels " + Slots(DirectIndexedMemory) + " of the four rectangles, expected only the second and third"); Same(IndirectIndexedMemory, DirectIndexedMemory, "indexed rect-list draw"); + DrawnOnTheGpu(); } } int main() { try { +#ifdef _WIN32 + _putenv_s("APS5_PROFILE_DRAW", "1"); +#else + setenv("APS5_PROFILE_DRAW", "1", 1); +#endif { const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; } IndirectRectListsDrawAsDirectOnes(); AgcDriverShutdown_nid_postfix(); - std::puts("indirect rect-list draws drew what the direct draw did"); + std::puts("indirect rect-list draws were drawn on the GPU and drew what the direct draw did"); return 0; } catch (const std::exception& error) { std::cerr << error.what() << '\n'; From b263cbbb1bb834593fd95947ff4111aaa72f8cdc Mon Sep 17 00:00:00 2001 From: Niko Date: Wed, 14 Oct 2026 17:54:29 +0200 Subject: [PATCH 0600/1000] feat(libc): print the text that registers point at in Windows crash reports Unreal titles end in their own libc fatal path with the UTF-16 LowLevelFatalError message in a register, and the crash report shows only the first 64 bytes of it as hex words, so the message is lost (#4238: Demon Slayer PPSA03490, Stray PPSA02100). For every general register that points at readable memory starting with at least four printable ASCII characters, as UTF-16LE or as single bytes, the report now prints the text up to a NUL, the first other character, 512 characters or the end of readable memory (each page is checked with VirtualQuery before it is read). Co-Authored-By: Claude Opus 5.5 --- core/libs/CMakeLists.txt | 19 ++++++ core/libs/prx/libc/include/CrashText.hpp | 59 +++++++++++++++++++ .../src/specifics/windows/CrashReport.cpp | 10 ++++ core/libs/tests/CrashText.cpp | 56 ++++++++++++++++++ core/libs/tests/WindowsCrashText.cpp | 45 ++++++++++++++ docs/user/USAGE.md | 2 + 6 files changed, 191 insertions(+) create mode 100644 core/libs/prx/libc/include/CrashText.hpp create mode 100644 core/libs/tests/CrashText.cpp create mode 100644 core/libs/tests/WindowsCrashText.cpp diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index c9f54c0bf0..8de4d5141b 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -281,6 +281,25 @@ if(BUILD_TESTING) PASS_REGULAR_EXPRESSION "C\\+\\+ exception St9bad_alloc raised on thread [0-9]+ cannot be delivered: 0x[0-9a-f]+ undeliverable_host_exception_tests\\.exe\\+0x[0-9a-f]+ has no unwind data") endif() + add_test_executable(crash_text_tests tests/CrashText.cpp) + target_include_directories(crash_text_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + if(MINGW) + target_compile_options(crash_text_tests PRIVATE -fasynchronous-unwind-tables) + endif() + add_test(NAME crash_text COMMAND crash_text_tests) + + if(WIN32) + add_test_executable(windows_crash_text_tests tests/WindowsCrashText.cpp) + add_dependencies(windows_crash_text_tests libc) + if(MINGW) + target_compile_options(windows_crash_text_tests PRIVATE -fasynchronous-unwind-tables) + endif() + add_test(NAME windows_crash_text COMMAND windows_crash_text_tests $) + set_tests_properties(windows_crash_text PROPERTIES TIMEOUT 20 + PASS_REGULAR_EXPRESSION "faulting[^~]*FATAL: unhandled exception 0xc0000005[^~]* \\[r12\\] \"allocation failed in the heap\"[\r\n]+ \\[r13\\] u\"LowLevelFatalError \\[File:Unknown\\] \\[Line: 42\\] Out of memory\"[\r\n]+ \\[r14\\] \"PAGE END\"[.][.][.]" + FAIL_REGULAR_EXPRESSION "\\[rbx\\] u?\"|did not fault") + endif() + add_test_executable(guest_voice_port_attr_tests tests/GuestVoicePortAttr.cpp) target_include_directories(guest_voice_port_attr_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(guest_voice_port_attr_tests PRIVATE libSceVoice libc) diff --git a/core/libs/prx/libc/include/CrashText.hpp b/core/libs/prx/libc/include/CrashText.hpp new file mode 100644 index 0000000000..675cc7c096 --- /dev/null +++ b/core/libs/prx/libc/include/CrashText.hpp @@ -0,0 +1,59 @@ +#ifndef CORE_LIBS_PRX_LIBC_INCLUDE_CRASHTEXT_HPP +#define CORE_LIBS_PRX_LIBC_INCLUDE_CRASHTEXT_HPP + +#include +#include + +namespace crash_text { + +constexpr std::size_t MaximumCharacters = 512; +constexpr std::size_t MinimumCharacters = 4; +constexpr std::size_t MaximumBytes = 2 * (MaximumCharacters + 1); + +inline std::uint16_t Unit(const std::uint8_t* bytes, std::size_t index, std::size_t width) { + if (width == 1) return bytes[index]; + return static_cast(bytes[2 * index] | bytes[2 * index + 1] << 8); +} + +inline bool IsText(std::uint16_t unit) { + return (unit >= 0x20 && unit < 0x7f) || unit == '\t' || unit == '\n' || unit == '\r'; +} + +inline bool StartsWithText(const std::uint8_t* bytes, std::size_t size, std::size_t width) { + if (size < MinimumCharacters * width) return false; + for (std::size_t index = 0; index < MinimumCharacters; ++index) { + if (!IsText(Unit(bytes, index, width))) return false; + } + return true; +} + +inline bool Format(const std::uint8_t* bytes, std::size_t size, char* out, std::size_t capacity) { + const std::size_t width = StartsWithText(bytes, size, 2) ? 2 : StartsWithText(bytes, size, 1) ? 1 : 0; + if (width == 0 || capacity < 8) return false; + std::size_t length = 0; + if (width == 2) out[length++] = 'u'; + out[length++] = '"'; + bool terminated = false; + for (std::size_t index = 0; (index + 1) * width <= size; ++index) { + const std::uint16_t unit = Unit(bytes, index, width); + if (unit == 0) { + terminated = true; + break; + } + if (index == MaximumCharacters || !IsText(unit)) break; + const char escape = unit == '\n' ? 'n' : unit == '\r' ? 'r' : unit == '\t' ? 't' : unit == '"' || unit == '\\' ? static_cast(unit) : '\0'; + if (length + (escape != '\0' ? 2 : 1) + 5 > capacity) break; + if (escape != '\0') out[length++] = '\\'; + out[length++] = escape != '\0' ? escape : static_cast(unit); + } + out[length++] = '"'; + if (!terminated) { + for (int dot = 0; dot < 3; ++dot) out[length++] = '.'; + } + out[length] = '\0'; + return true; +} + +} + +#endif diff --git a/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp b/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp index 19691b7964..47cfaed9fc 100644 --- a/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp +++ b/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp @@ -5,6 +5,7 @@ #include "prx/libc/include/FailureReport.hpp" #include "prx/libc/include/GuestArena.hpp" #include "prx/libc/include/Shutdown.hpp" +#include "prx/libc/include/CrashText.hpp" #include #include #include @@ -355,6 +356,15 @@ LONG WINAPI ReportCrash(EXCEPTION_POINTERS* info) { } } } + { + const std::pair registers[] = {{"rax", context->Rax}, {"rbx", context->Rbx}, {"rcx", context->Rcx}, {"rdx", context->Rdx}, {"rsi", context->Rsi}, {"rdi", context->Rdi}, {"rbp", context->Rbp}, {"r8", context->R8}, {"r9", context->R9}, {"r10", context->R10}, {"r11", context->R11}, {"r12", context->R12}, {"r13", context->R13}, {"r14", context->R14}, {"r15", context->R15}}; + static std::uint8_t bytes[crash_text::MaximumBytes]; + static char text[960]; + for (const auto& [name, value] : registers) { + if (value < 0x10000) continue; + if (crash_text::Format(bytes, ReadCode(value, bytes, sizeof(bytes)), text, sizeof(text))) Report(" [%s] %s\n", name, text); + } + } Report(" frame pointer chain:\n"); std::uint64_t frame = context->Rbp; for (int depth = 0; depth < 32 && frame != 0 && (frame & 7) == 0 && IsReadable(frame) && IsReadable(frame + 8); ++depth) { diff --git a/core/libs/tests/CrashText.cpp b/core/libs/tests/CrashText.cpp new file mode 100644 index 0000000000..e2076b0c84 --- /dev/null +++ b/core/libs/tests/CrashText.cpp @@ -0,0 +1,56 @@ +#include "prx/libc/include/CrashText.hpp" +#include +#include +#include +#include +#include + +namespace { + +int failures = 0; + +std::vector Wide(const std::u16string& text) { + std::vector bytes; + for (const char16_t unit : text) { + bytes.push_back(static_cast(unit & 0xff)); + bytes.push_back(static_cast(unit >> 8)); + } + return bytes; +} + +std::vector Narrow(const std::string& text) { + return std::vector(text.begin(), text.end()); +} + +void Expect(const char* name, const std::vector& bytes, const std::string& expected, std::size_t capacity = 960) { + std::vector out(capacity, '#'); + const bool formatted = crash_text::Format(bytes.data(), bytes.size(), out.data(), capacity); + const std::string actual = formatted ? std::string(out.data()) : std::string(""); + if (actual == expected && actual.size() < capacity) return; + std::fprintf(stderr, "%s: expected %s, got %s\n", name, expected.c_str(), actual.c_str()); + ++failures; +} + +} + +int main() { + using namespace std::string_literals; + Expect("terminated UTF-16", Wide(u"LowLevelFatalError [File:Unknown]\0tail"s), "u\"LowLevelFatalError [File:Unknown]\""); + Expect("terminated ASCII with escapes", Narrow("PS5 Out of Memory:\n\t\"a\\b\"\r\0tail"s), "\"PS5 Out of Memory:\\n\\t\\\"a\\\\b\\\"\\r\""); + Expect("UTF-16 up to the end of readable memory", Wide(u"Ran out of memory"), "u\"Ran out of memory\"..."); + Expect("ASCII up to the end of readable memory", Narrow("PAGE END"), "\"PAGE END\"..."); + Expect("UTF-16 before a non-ASCII unit", Wide(u"Memory\u00e9 more"), "u\"Memory\"..."); + Expect("short ASCII", Narrow("abc\0"s), ""); + Expect("short UTF-16", Wide(u"abc\0"s), ""); + Expect("three readable bytes", Narrow("abc"), ""); + Expect("pointer value", std::vector{0x08, 0xd4, 0x8e, 0x5b, 0xf6, 0x7f, 0x00, 0x00}, ""); + Expect("control byte first", Narrow("\x01text\0"s), ""); + Expect("character limit", Wide(std::u16string(600, u'A') + u'\0'), "u\"" + std::string(crash_text::MaximumCharacters, 'A') + "\"..."); + Expect("NUL right at the character limit", Wide(std::u16string(crash_text::MaximumCharacters, u'B') + u'\0'), "u\"" + std::string(crash_text::MaximumCharacters, 'B') + "\""); + Expect("output capacity", Narrow(std::string(100, 'x')), "\"" + std::string(10, 'x') + "\"...", 16); + Expect("escape at the output capacity", Narrow("abcdefghi\n" + std::string(20, 'z')), "\"abcdefghi\"...", 16); + Expect("too small output", Narrow("long enough text"), "", 7); + if (failures != 0) return 1; + std::puts("crash text: all cases passed"); + return 0; +} diff --git a/core/libs/tests/WindowsCrashText.cpp b/core/libs/tests/WindowsCrashText.cpp new file mode 100644 index 0000000000..677fbcb609 --- /dev/null +++ b/core/libs/tests/WindowsCrashText.cpp @@ -0,0 +1,45 @@ +#include +#include +#include +#include + +int main(int argc, char** argv) { + if (argc != 2) { + std::fprintf(stderr, "usage: %s \n", argv[0]); + return 2; + } + SetErrorMode(SEM_FAILCRITICALERRORS | SEM_NOGPFAULTERRORBOX); + SetUnhandledExceptionFilter([](EXCEPTION_POINTERS*) -> LONG { + TerminateProcess(GetCurrentProcess(), 3); + return EXCEPTION_EXECUTE_HANDLER; + }); + if (LoadLibraryExA(argv[1], nullptr, LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR | LOAD_LIBRARY_SEARCH_DEFAULT_DIRS) == nullptr) { + std::fprintf(stderr, "%s: LoadLibraryEx failed with error %lu\n", argv[1], GetLastError()); + return 1; + } + static const char narrow[] = "allocation failed in the heap"; + static const char16_t wide[] = u"LowLevelFatalError [File:Unknown] [Line: 42] Out of memory"; + static const std::uint8_t binary[] = {0x08, 0xd4, 0x8e, 0x5b, 0xf6, 0x7f, 0x00, 0x00}; + auto* pages = static_cast(VirtualAlloc(nullptr, 0x2000, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); + DWORD previous = 0; + if (pages == nullptr || !VirtualProtect(pages + 0x1000, 0x1000, PAGE_NOACCESS, &previous)) { + std::fprintf(stderr, "cannot prepare the page edge: error %lu\n", GetLastError()); + return 1; + } + char* const edge = pages + 0x1000 - 8; + std::memcpy(edge, "PAGE END", 8); + std::fputs("faulting\n", stderr); + std::fflush(stderr); + const std::uintptr_t unmapped = 8; + asm volatile( + "movq %0, %%r12\n\t" + "movq %1, %%r13\n\t" + "movq %2, %%r14\n\t" + "movq %3, %%rbx\n\t" + "movq (%4), %%rax" + : + : "r"(narrow), "r"(wide), "r"(edge), "r"(binary), "r"(unmapped) + : "rax", "rbx", "r12", "r13", "r14", "memory"); + std::fputs("the read of address 8 did not fault\n", stderr); + return 1; +} diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index 73c49d52c6..63a1e937a3 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -206,6 +206,8 @@ Limits are listed in the [libc](../dev/debt/libc.md) and [relinker](../dev/debt/ ### Windows crash diagnostics +When a register points at readable memory that starts with at least four printable ASCII characters, either as UTF-16LE or as single bytes, the crash report prints that text after the register memory dump, as `[r13] u"..."` for UTF-16LE or `[rdi] "..."` for single bytes. The text ends at a NUL, at the first character that is not printable ASCII, a tab, a line feed or a carriage return, after 512 characters or at the end of readable memory. Tabs, line breaks, quotes and backslashes are escaped, and `...` after the closing quote means that no NUL ended the text within those limits. + Set `APS5_CRASH_INDIRECT_MEMORY=1` before launching the executable to include up to 64 bytes from each pointer found in the register memory dump. The report labels each block with its register, pointer offset and target address. Inaccessible, incomplete and executable targets are skipped. This additional dump is disabled when the variable is unset or empty. ### GPU submission rings and stall dumps From cb6c305038c28c6eff1b3450e1e3de9ade93c46d Mon Sep 17 00:00:00 2001 From: notaxeltv Date: Wed, 14 Oct 2026 23:05:59 +0200 Subject: [PATCH 0601/1000] feat(libSceVdecsw): block on SyncDecodeInput/Output like TrySync Co-authored-by: Cursor --- core/libs/prx/libSceVdecsw/Export.cpp | 24 +++++++++++++++++++++++ core/libs/tests/GuestVdecsw.cpp | 28 +++++++++++++++++++++++++++ docs/dev/debt/libSceVdecsw.md | 6 +++++- 3 files changed, 57 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceVdecsw/Export.cpp b/core/libs/prx/libSceVdecsw/Export.cpp index 26fdb475ef..8d394b9fb3 100644 --- a/core/libs/prx/libSceVdecsw/Export.cpp +++ b/core/libs/prx/libSceVdecsw/Export.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" @@ -311,6 +312,14 @@ int APS5_VABI sceVdecswTrySyncDecodeInput(std::uint64_t handle, InputResult* res return decoder->inputs.empty() ? VdecswErrorInputQueueEmpty : VdecswErrorDecodePending; } +int APS5_VABI sceVdecswSyncDecodeInput(std::uint64_t handle, InputResult* result) { + for (;;) { + const auto status = sceVdecswTrySyncDecodeInput(handle, result); + if (status != VdecswErrorDecodePending) return status; + std::this_thread::yield(); + } +} + int APS5_VABI sceVdecswSetDecodeOutput(std::uint64_t handle, const FrameBuffer* frame) { if (frame == nullptr || frame->thisSize < sizeof(FrameBuffer) || frame->frameBuffer == nullptr) throw std::runtime_error("Vdecsw: invalid decode output"); const auto decoder = findDecoder(handle); @@ -356,6 +365,14 @@ int APS5_VABI sceVdecswTrySyncDecodeOutput(std::uint64_t handle, OutputInfo* out return 0; } +int APS5_VABI sceVdecswSyncDecodeOutput(std::uint64_t handle, OutputInfo* output) { + for (;;) { + const auto status = sceVdecswTrySyncDecodeOutput(handle, output); + if (status != VdecswErrorOutputPending) return status; + std::this_thread::yield(); + } +} + int APS5_VABI sceVdecswGetAvcPictureInfo(const OutputInfo* output, AvcPictureInfo* first, AvcPictureInfo* second) { if (output == nullptr || first == nullptr || first->thisSize < sizeof(std::uint64_t)) throw std::runtime_error("Vdecsw: invalid argument pointer"); if (output->pictureCount > 1) throw std::runtime_error("Vdecsw: second field picture info is not implemented"); @@ -377,6 +394,13 @@ int APS5_VABI sceVdecswGetAvcPictureInfo(const OutputInfo* output, AvcPictureInf throw std::runtime_error("Vdecsw: picture info requested for a frame buffer no decoder returned"); } +int APS5_VABI sceVdecswGetHevcPictureInfo(const OutputInfo* output, void* picture) { + (void)output; + (void)picture; + NotImplemented_nid_no_patch(__func__); + return 0; +} + int APS5_VABI sceVdecswFinalizeDecodeSequence(std::uint64_t handle) { const auto decoder = findDecoder(handle); std::lock_guard lock(decoder->mutex); diff --git a/core/libs/tests/GuestVdecsw.cpp b/core/libs/tests/GuestVdecsw.cpp index 4604a40034..72f76ec270 100644 --- a/core/libs/tests/GuestVdecsw.cpp +++ b/core/libs/tests/GuestVdecsw.cpp @@ -120,9 +120,12 @@ int APS5_VABI sceVdecswCreateDecoder(const DecoderConfigInfo*, const DecoderMemo int APS5_VABI sceVdecswDeleteDecoder(std::uint64_t); int APS5_VABI sceVdecswSetDecodeInput(std::uint64_t, const InputData*); int APS5_VABI sceVdecswTrySyncDecodeInput(std::uint64_t, InputResult*); +int APS5_VABI sceVdecswSyncDecodeInput(std::uint64_t, InputResult*); int APS5_VABI sceVdecswSetDecodeOutput(std::uint64_t, const FrameBuffer*); int APS5_VABI sceVdecswTrySyncDecodeOutput(std::uint64_t, OutputInfo*); +int APS5_VABI sceVdecswSyncDecodeOutput(std::uint64_t, OutputInfo*); int APS5_VABI sceVdecswGetAvcPictureInfo(const OutputInfo*, AvcPictureInfo*, AvcPictureInfo*); +int APS5_VABI sceVdecswGetHevcPictureInfo(const OutputInfo*, void*); int APS5_VABI sceVdecswFinalizeDecodeSequence(std::uint64_t); int APS5_VABI sceVdecswResetDecoder(std::uint64_t); } @@ -233,12 +236,37 @@ void testWithoutOutput() { check(sceVdecswTrySyncDecodeInput(session.decoder, &result) == InputQueueEmpty, "a reset decoder kept its inputs"); } +void testSyncWrappers() { + Session session; + InputResult idle{sizeof(InputResult)}; + check(sceVdecswSyncDecodeInput(session.decoder, &idle) == InputQueueEmpty, "sync input on an idle decoder did not report an empty queue"); + const auto units = accessUnits(false); + const InputData input{sizeof(InputData), units[0].data(), units[0].size(), 1000, 0, 0xa0}; + check(sceVdecswSetDecodeInput(session.decoder, &input) == 0, "decode input was rejected"); + InputResult result{sizeof(InputResult)}; + check(sceVdecswSyncDecodeInput(session.decoder, &result) == 0 && result.decodedAu == units[0].data(), "sync input did not consume the access unit"); + std::vector buffer(session.memory.maxFrameBufferSize); + const FrameBuffer frame{sizeof(FrameBuffer), buffer.data(), buffer.size()}; + check(sceVdecswSetDecodeOutput(session.decoder, &frame) == 0, "output buffer was rejected"); + check(sceVdecswFinalizeDecodeSequence(session.decoder) == 0, "finalize failed"); + OutputInfo output{sizeof(OutputInfo)}; + check(sceVdecswSyncDecodeOutput(session.decoder, &output) == 0 && output.isValid && output.isLastFrame, "sync output did not return the finalized picture"); + bool hevcThrows = false; + try { + sceVdecswGetHevcPictureInfo(&output, &result); + } catch (const std::exception&) { + hevcThrows = true; + } + check(hevcThrows, "GetHevcPictureInfo must still throw"); +} + } int main() { try { testDecode(); testWithoutOutput(); + testSyncWrappers(); std::puts("Vdecsw tests passed"); return 0; } catch (const std::exception& error) { diff --git a/docs/dev/debt/libSceVdecsw.md b/docs/dev/debt/libSceVdecsw.md index 09e1b11228..f9a85dd44e 100644 --- a/docs/dev/debt/libSceVdecsw.md +++ b/docs/dev/debt/libSceVdecsw.md @@ -2,4 +2,8 @@ ### Unknown function info -- [libSceVdecsw](../../../core/libs/prx/libSceVdecsw/Export.cpp) (libSceVdecsw) - structure layouts follow shadPS4's `vdecsw.h` and were checked against PPSA05684's movie decoder (`movie-decoder-vdecsw.cpp`), which asserts `pictureCount == 1` at offset 0xb of `OutputInfo` and reads `InputResult` offset 0x10 as the number of frames an access unit produced. The compute, decoder memory and decode input structures and the first 0x28 bytes of the 0x50-byte decoder config are those of libSceVideodec2, and decoding goes through it. The 0x78-byte AVC picture info is filled with Videodec2's leading fields and the cropping fields only. OUTPUT_PENDING (0x81510115), INPUT_QUEUE_EMPTY (0x81510116) and DECODE_PENDING (0x81510117) follow from the codes each call's assert accepts. At most four decoded pictures are queued and the last one is held until `sceVdecswFinalizeDecodeSequence`; the console's queue depth is unknown +- [libSceVdecsw](../../../core/libs/prx/libSceVdecsw/Export.cpp) (libSceVdecsw) - structure layouts follow shadPS4's `vdecsw.h` and were checked against PPSA05684's movie decoder (`movie-decoder-vdecsw.cpp`), which asserts `pictureCount == 1` at offset 0xb of `OutputInfo` and reads `InputResult` offset 0x10 as the number of frames an access unit produced. The compute, decoder memory and decode input structures and the first 0x28 bytes of the 0x50-byte decoder config are those of libSceVideodec2, and decoding goes through it. The 0x78-byte AVC picture info is filled with Videodec2's leading fields and the cropping fields only. OUTPUT_PENDING (0x81510115), INPUT_QUEUE_EMPTY (0x81510116) and DECODE_PENDING (0x81510117) follow from the codes each call's assert accepts. At most four decoded pictures are queued and the last one is held until `sceVdecswFinalizeDecodeSequence`; the console's queue depth is unknown. `sceVdecswSyncDecodeInput` / `sceVdecswSyncDecodeOutput` retry the TrySync variants until they stop returning DECODE_PENDING / OUTPUT_PENDING; they do not sleep beyond a yield, so another thread must drain outputs when the picture queue is full. `sceVdecswGetHevcPictureInfo` still throws: the 0xb8-byte HEVC picture layout is in shadPS4's `vdecsw.h`, but this wrapper only keeps AVC picture info from Videodec2 + +### Unimplemented paths + +- [sceVdecswGetHevcPictureInfo](../../../core/libs/prx/libSceVdecsw/Export.cpp) (libSceVdecsw) - throws until HEVC picture info is filled from the decoder From ddb135643a3747b32e88e068c58837eb4f53afe9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Fri, 16 Oct 2026 23:38:37 +0200 Subject: [PATCH 0602/1000] docs(debt): list sceVdecswGetHevcPictureInfo under Functional --- docs/dev/debt/libSceVdecsw.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceVdecsw.md b/docs/dev/debt/libSceVdecsw.md index f9a85dd44e..bffe3f23ab 100644 --- a/docs/dev/debt/libSceVdecsw.md +++ b/docs/dev/debt/libSceVdecsw.md @@ -4,6 +4,6 @@ - [libSceVdecsw](../../../core/libs/prx/libSceVdecsw/Export.cpp) (libSceVdecsw) - structure layouts follow shadPS4's `vdecsw.h` and were checked against PPSA05684's movie decoder (`movie-decoder-vdecsw.cpp`), which asserts `pictureCount == 1` at offset 0xb of `OutputInfo` and reads `InputResult` offset 0x10 as the number of frames an access unit produced. The compute, decoder memory and decode input structures and the first 0x28 bytes of the 0x50-byte decoder config are those of libSceVideodec2, and decoding goes through it. The 0x78-byte AVC picture info is filled with Videodec2's leading fields and the cropping fields only. OUTPUT_PENDING (0x81510115), INPUT_QUEUE_EMPTY (0x81510116) and DECODE_PENDING (0x81510117) follow from the codes each call's assert accepts. At most four decoded pictures are queued and the last one is held until `sceVdecswFinalizeDecodeSequence`; the console's queue depth is unknown. `sceVdecswSyncDecodeInput` / `sceVdecswSyncDecodeOutput` retry the TrySync variants until they stop returning DECODE_PENDING / OUTPUT_PENDING; they do not sleep beyond a yield, so another thread must drain outputs when the picture queue is full. `sceVdecswGetHevcPictureInfo` still throws: the 0xb8-byte HEVC picture layout is in shadPS4's `vdecsw.h`, but this wrapper only keeps AVC picture info from Videodec2 -### Unimplemented paths +### Functional - [sceVdecswGetHevcPictureInfo](../../../core/libs/prx/libSceVdecsw/Export.cpp) (libSceVdecsw) - throws until HEVC picture info is filled from the decoder From af9d039d610c07eb3913997d65e156a8b3b7d7c3 Mon Sep 17 00:00:00 2001 From: notaxeltv Date: Wed, 14 Oct 2026 22:57:05 +0200 Subject: [PATCH 0603/1000] feat(libSceNpManager): export two unknown NIDs so PPSA34015 can load Co-authored-by: Cursor --- core/libs/prx/libSceNpManager/CMakeLists.txt | 3 ++- core/libs/prx/libSceNpManager/Export.cpp | 14 ++++++++++++++ core/libs/tests/GuestNpManager.cpp | 19 +++++++++++++++++++ docs/dev/debt/libSceNpManager.md | 4 +++- 4 files changed, 38 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceNpManager/CMakeLists.txt b/core/libs/prx/libSceNpManager/CMakeLists.txt index f0f629d89b..cd99c36b34 100644 --- a/core/libs/prx/libSceNpManager/CMakeLists.txt +++ b/core/libs/prx/libSceNpManager/CMakeLists.txt @@ -20,7 +20,8 @@ if(BUILD_TESTING) endif() add_test_executable(guest_np_manager_tests ${LIBS_INCLUDE_DIR}/tests/GuestNpManager.cpp) target_include_directories(guest_np_manager_tests PRIVATE ${LIBS_INCLUDE_DIR}) - target_link_libraries(guest_np_manager_tests PRIVATE libSceNpManager) + target_link_libraries(guest_np_manager_tests PRIVATE libSceNpManager libc) + configure_windows_unwind(guest_np_manager_tests) add_test(NAME guest_np_manager COMMAND guest_np_manager_tests) if(WIN32) set_tests_properties(guest_np_manager PROPERTIES ENVIRONMENT_MODIFICATION diff --git a/core/libs/prx/libSceNpManager/Export.cpp b/core/libs/prx/libSceNpManager/Export.cpp index f43d909d91..848133acd5 100644 --- a/core/libs/prx/libSceNpManager/Export.cpp +++ b/core/libs/prx/libSceNpManager/Export.cpp @@ -249,4 +249,18 @@ int APS5_VABI sceNpGetSandboxInfo(void* sandbox_info) { return 0; } +APS5_EXPORT("8weiQyIdI3A", sceNpUnknown_8weiQyIdI3A); +int APS5_VABI sceNpUnknown_8weiQyIdI3A(int userId, void* buffer) { + (void)userId; + (void)buffer; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +APS5_EXPORT("WxLcN76q19o", sceNpUnknown_WxLcN76q19o); +int APS5_VABI sceNpUnknown_WxLcN76q19o(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/tests/GuestNpManager.cpp b/core/libs/tests/GuestNpManager.cpp index 7bfefee198..a89ed3ea2e 100644 --- a/core/libs/tests/GuestNpManager.cpp +++ b/core/libs/tests/GuestNpManager.cpp @@ -4,10 +4,14 @@ #include #include #include +#include +#include extern "C" { int APS5_VABI sceNpGetNpId(int user_id, NpId* np_id); int APS5_VABI sceNpGetUserIdByAccountId(std::uint64_t account_id, int* user_id); +int APS5_VABI sceNpUnknown_8weiQyIdI3A(int userId, void* buffer); +int APS5_VABI sceNpUnknown_WxLcN76q19o(void); } extern "C" { @@ -73,5 +77,20 @@ int main() { Require(sceNpGetUserIdByAccountId(0x1234, nullptr) == InvalidArgument, "sceNpGetUserIdByAccountId must reject a null output"); Require(sceNpGetUserIdByAccountId(0x1234, &userId) == SignedOut, "sceNpGetUserIdByAccountId must report NP as signed out"); Require(userId == 0x5a5a, "sceNpGetUserIdByAccountId must leave the user id untouched"); + unsigned char buffer[0x11]{}; + try { + sceNpUnknown_8weiQyIdI3A(0x10000, buffer); + Require(false, "sceNpUnknown_8weiQyIdI3A must report that it is not implemented"); + } catch (const std::runtime_error& error) { + Require(std::string(error.what()) == "sceNpUnknown_8weiQyIdI3A not implemented", + "sceNpUnknown_8weiQyIdI3A threw"); + } + try { + sceNpUnknown_WxLcN76q19o(); + Require(false, "sceNpUnknown_WxLcN76q19o must report that it is not implemented"); + } catch (const std::runtime_error& error) { + Require(std::string(error.what()) == "sceNpUnknown_WxLcN76q19o not implemented", + "sceNpUnknown_WxLcN76q19o threw"); + } return 0; } diff --git a/docs/dev/debt/libSceNpManager.md b/docs/dev/debt/libSceNpManager.md index c7b654e7b0..598d92c09f 100644 --- a/docs/dev/debt/libSceNpManager.md +++ b/docs/dev/debt/libSceNpManager.md @@ -2,7 +2,9 @@ ### Unknown function info -- [sceNpGetSandboxInfo](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager, NID `ZVZ++68nNI8`) - the name hashes to the imported NID (found with nidhunt, confirmed with `tools/nid_names.py`). STAR WARS: Galactic Racer (PPSA28416) imports it; its OnlineSubsystemPS5 calls it with one pointer to a 0x50-byte stack buffer and reads the buffer as a string (discussions/3086). The returned string, the exact parameter struct and console errors are unknown, so every call throws without reading or writing caller memory. A working implementation belongs with that title's bring-up. +- [sceNpGetSandboxInfo](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager, NID `ZVZ++68nNI8`) - the name hashes to the imported NID (found with nidhunt, confirmed with `tools/nid_names.py`). STAR WARS: Galactic Racer (PPSA28416) imports it; its OnlineSubsystemPS5 calls it with one pointer to a 0x50-byte stack buffer and reads the buffer as a string (discussions/3086). The returned string, the exact parameter struct and console errors are unknown, so every call throws without reading or writing caller memory. A working implementation belongs with that title's bring-up +- [sceNpUnknown_8weiQyIdI3A](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager, NID `8weiQyIdI3A`) - the name is not in aerolib. EA SPORTS FC 27 (PPSA34015) fails to load `libSceNpManager.prx` without it (`unresolved ELF import` / `GetLastError: 127`). Its only call is `mov edi, r14d` (a user id) and `lea rsi, [rbp-0x40]` after a 17-byte copy into that buffer (`mov edx, 0x11`); `test eax, eax` follows. The C name, the type of the 17-byte object and the console return are unknown, so every call throws without reading or writing caller memory +- [sceNpUnknown_WxLcN76q19o](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager, NID `WxLcN76q19o`) - the name is not in aerolib. PPSA34015 imports it from `libSceNpManager.prx` as a strong `JUMP_SLOT` with no call found in the eboot; the loader still requires the export. Arguments, result and console errors are unknown, so every call throws. - [sceNpSetTimeout](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager) - the signature follows [orbital](https://github.com/AlexAltea/orbital/blob/b321fcdbd700cc02da1cb1ad25412781d5ec73c6/tools/ida/db_types.json), [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0fe263a4760dfbfa973366890061749b4af0de97/src/core/libraries/np/np_manager.cpp) and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Np/NpAccount.cs); the rejected values (all five 0, negative retries, a resolve timeout under 1 s or another timeout under 10 s, 0 meaning the default) are shadPS4's, not checked on a console. An id that was never created or was deleted returns `REQUEST_NOT_FOUND`, as in shadPS4; there is no network, so the timeouts are not stored - [sceNpGetUserIdByAccountId](../../../core/libs/prx/libSceNpManager/Export.cpp) - the `(account id, user id output)` signature and its errors follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/np/np_manager.cpp): account id 0 or a null output returns `SCE_NP_ERROR_INVALID_ARGUMENT`, and a signed-out NP returns `SCE_NP_ERROR_SIGNED_OUT` without writing the output. NP is always signed out here, so no account id resolves to a user From 53ffc976feca81eac7ec96928df8faf0a7de4f68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Fri, 16 Oct 2026 23:40:45 +0200 Subject: [PATCH 0604/1000] docs(debt): end the libSceNpManager debt lines with a period --- docs/dev/debt/libSceNpManager.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/dev/debt/libSceNpManager.md b/docs/dev/debt/libSceNpManager.md index 598d92c09f..8076229ee3 100644 --- a/docs/dev/debt/libSceNpManager.md +++ b/docs/dev/debt/libSceNpManager.md @@ -2,8 +2,8 @@ ### Unknown function info -- [sceNpGetSandboxInfo](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager, NID `ZVZ++68nNI8`) - the name hashes to the imported NID (found with nidhunt, confirmed with `tools/nid_names.py`). STAR WARS: Galactic Racer (PPSA28416) imports it; its OnlineSubsystemPS5 calls it with one pointer to a 0x50-byte stack buffer and reads the buffer as a string (discussions/3086). The returned string, the exact parameter struct and console errors are unknown, so every call throws without reading or writing caller memory. A working implementation belongs with that title's bring-up -- [sceNpUnknown_8weiQyIdI3A](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager, NID `8weiQyIdI3A`) - the name is not in aerolib. EA SPORTS FC 27 (PPSA34015) fails to load `libSceNpManager.prx` without it (`unresolved ELF import` / `GetLastError: 127`). Its only call is `mov edi, r14d` (a user id) and `lea rsi, [rbp-0x40]` after a 17-byte copy into that buffer (`mov edx, 0x11`); `test eax, eax` follows. The C name, the type of the 17-byte object and the console return are unknown, so every call throws without reading or writing caller memory +- [sceNpGetSandboxInfo](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager, NID `ZVZ++68nNI8`) - the name hashes to the imported NID (found with nidhunt, confirmed with `tools/nid_names.py`). STAR WARS: Galactic Racer (PPSA28416) imports it; its OnlineSubsystemPS5 calls it with one pointer to a 0x50-byte stack buffer and reads the buffer as a string (discussions/3086). The returned string, the exact parameter struct and console errors are unknown, so every call throws without reading or writing caller memory. A working implementation belongs with that title's bring-up. +- [sceNpUnknown_8weiQyIdI3A](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager, NID `8weiQyIdI3A`) - the name is not in aerolib. EA SPORTS FC 27 (PPSA34015) fails to load `libSceNpManager.prx` without it (`unresolved ELF import` / `GetLastError: 127`). Its only call is `mov edi, r14d` (a user id) and `lea rsi, [rbp-0x40]` after a 17-byte copy into that buffer (`mov edx, 0x11`); `test eax, eax` follows. The C name, the type of the 17-byte object and the console return are unknown, so every call throws without reading or writing caller memory. - [sceNpUnknown_WxLcN76q19o](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager, NID `WxLcN76q19o`) - the name is not in aerolib. PPSA34015 imports it from `libSceNpManager.prx` as a strong `JUMP_SLOT` with no call found in the eboot; the loader still requires the export. Arguments, result and console errors are unknown, so every call throws. - [sceNpSetTimeout](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager) - the signature follows [orbital](https://github.com/AlexAltea/orbital/blob/b321fcdbd700cc02da1cb1ad25412781d5ec73c6/tools/ida/db_types.json), [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0fe263a4760dfbfa973366890061749b4af0de97/src/core/libraries/np/np_manager.cpp) and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Np/NpAccount.cs); the rejected values (all five 0, negative retries, a resolve timeout under 1 s or another timeout under 10 s, 0 meaning the default) are shadPS4's, not checked on a console. An id that was never created or was deleted returns `REQUEST_NOT_FOUND`, as in shadPS4; there is no network, so the timeouts are not stored - [sceNpGetUserIdByAccountId](../../../core/libs/prx/libSceNpManager/Export.cpp) - the `(account id, user id output)` signature and its errors follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/np/np_manager.cpp): account id 0 or a null output returns `SCE_NP_ERROR_INVALID_ARGUMENT`, and a signed-out NP returns `SCE_NP_ERROR_SIGNED_OUT` without writing the output. NP is always signed out here, so no account id resolves to a user From 56b43aae5aa6a1ab27493050f3262d5896998979 Mon Sep 17 00:00:00 2001 From: Zinebyte <197974009+Zinebyte@users.noreply.github.com> Date: Wed, 14 Oct 2026 18:15:15 -0400 Subject: [PATCH 0605/1000] fix(relinker): scope host dependencies to required guest modules --- .../relinker/src/guest/GuestModuleBuilder.cpp | 29 ++++--- .../relinker/tests/test_guest_module_args.py | 55 ++++++++++++ .../relinker/tests/test_guest_module_dir.py | 85 ++++++++++++++++++- .../tests/test_guest_module_identity.py | 8 +- 4 files changed, 161 insertions(+), 16 deletions(-) diff --git a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp index 33cc5bdfc2..495ba9038d 100644 --- a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp +++ b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp @@ -292,14 +292,27 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path }; for (std::size_t index = 0; index < images.size(); ++index) visit(index); std::vector hostLibraries; + std::vector> moduleHostLibraries(images.size()); std::set uniqueHosts; - const auto addHost = [&](const std::string& name) { + const auto addHost = [&](const std::string& name, const bool startup) { if (findGuest(name) != guestNames.end()) return; if (name.empty() || name.find_first_of("/\\:$") != std::string::npos) throw Domain::RelinkerException("Invalid host dependency: " + name); - if (uniqueHosts.insert(name).second) hostLibraries.push_back(name); + if (startup && uniqueHosts.insert(name).second) hostLibraries.push_back(name); }; - for (const auto& name : neededNames) addHost(name); - for (const auto& image : images) for (const auto& dependency : image.Dependencies) addHost(dependency); + for (const auto& name : neededNames) addHost(name, true); + for (std::size_t index = 0; index < images.size(); ++index) { + auto& hosts = moduleHostLibraries[index]; + for (const auto& dependency : images[index].Dependencies) { + addHost(dependency, windows || !deferred(index)); + if (findGuest(dependency) == guestNames.end()) hosts.push_back(dependency); + } + if (!windows && !macos && images[index].Init != 0) { + if (std::find(hosts.begin(), hosts.end(), "libkernel.prx") == hosts.end()) hosts.push_back("libkernel.prx"); + if (!deferred(index) && uniqueHosts.insert("libkernel.prx").second) hostLibraries.push_back("libkernel.prx"); + } + if (std::find(hosts.begin(), hosts.end(), "libSceLibcInternal.prx") != hosts.end() && + std::find(hosts.begin(), hosts.end(), "libc.prx") == hosts.end()) hosts.push_back("libc.prx"); + } if (uniqueHosts.contains("libSceLibcInternal.prx") && uniqueHosts.insert("libc.prx").second) hostLibraries.push_back("libc.prx"); dynamic.NeededLibraries = hostLibraries; dynamic.DynamicSegmentData.clear(); @@ -313,12 +326,6 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path if (!windows && runPath != "$ORIGIN" && !runPath.starts_with("$ORIGIN/") && !runPath.starts_with("$ORIGIN:") && !runPath.starts_with('/')) throw Domain::RelinkerException("Guest Linux run path must be absolute or begin with $ORIGIN"); const auto outputDirectory = outputPath.parent_path().lexically_normal(); std::vector artifacts; - std::vector moduleHostLibraries; - std::set uniqueModuleHosts; - for (const auto& image : images) for (const auto& dependency : image.Dependencies) { - if (findGuest(dependency) == guestNames.end() && uniqueModuleHosts.insert(dependency).second) moduleHostLibraries.push_back(dependency); - } - if (uniqueModuleHosts.contains("libSceLibcInternal.prx") && uniqueModuleHosts.insert("libc.prx").second) moduleHostLibraries.push_back("libc.prx"); for (const auto index : order) { auto& image = images[index]; progress.Report({Domain::ProgressStage::Modules, Io::Utf8Path(image.SourcePath), artifacts.size(), order.size()}); @@ -363,7 +370,7 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path const auto dependencyPath = images[dependency].SourcePath.parent_path() / images[dependency].OutputName; needed.push_back("$ORIGIN/" + dependencyPath.lexically_relative(image.SourcePath.parent_path()).generic_string()); } - needed.insert(needed.end(), moduleHostLibraries.begin(), moduleHostLibraries.end()); + needed.insert(needed.end(), moduleHostLibraries[index].begin(), moduleHostLibraries[index].end()); output = macos ? Elfpatcher::GuestModuleWriter().WriteMacOs(image, needed, guestRunPath) : Elfpatcher::GuestModuleWriter().WriteLinux(image, needed, guestRunPath); } if (windows) { diff --git a/core/relinker/relinker/tests/test_guest_module_args.py b/core/relinker/relinker/tests/test_guest_module_args.py index 6c3de5f3a4..5aee388957 100644 --- a/core/relinker/relinker/tests/test_guest_module_args.py +++ b/core/relinker/relinker/tests/test_guest_module_args.py @@ -10,6 +10,9 @@ import tempfile from test_guest_symbol_names import dynamic_tags, imported_symbols, module_symbols from test_macos_output import MachO from test_windows_import_modules import executable +from test_guest_intel_trampolines import main_fixture +from test_guest_module_directories import needed_libraries +from test_guest_needed_modules import module_with_needed ARGS = 0x1122334455667788 ARGP = 0x99AABBCCDDEEFF00 @@ -56,6 +59,55 @@ def no_relocation_module(): return image +def check_lifecycle_host(relinker, host, work, linux, deferred): + case = work / f'lifecycle-host-{deferred}' + modules, plugins = case / 'sce_module', case / 'Media/Plugins' + modules.mkdir(parents=True) + plugins.mkdir(parents=True) + image = no_relocation_module() + image[0x1010:0x1013] = b'\x31\xc0\xc3' + tags = [] + for position in range(0x2000, 0x2200, 16): + tag, value = struct.unpack_from(' Date: Fri, 16 Oct 2026 17:56:47 -0400 Subject: [PATCH 0606/1000] test(relinker): provide kernel fixture for guest finalizers --- core/relinker/relinker/tests/test_guest_module_fini.py | 1 + 1 file changed, 1 insertion(+) diff --git a/core/relinker/relinker/tests/test_guest_module_fini.py b/core/relinker/relinker/tests/test_guest_module_fini.py index aa79a1fc69..afbd17bc93 100644 --- a/core/relinker/relinker/tests/test_guest_module_fini.py +++ b/core/relinker/relinker/tests/test_guest_module_fini.py @@ -66,6 +66,7 @@ def main(): libraries.mkdir() provider = libraries / 'libc.prx' shutil.copyfile(host, provider) + shutil.copyfile(host, libraries / 'libkernel.prx') events = work / 'events.txt' run = subprocess.run([sys.executable, __file__, '--load', str(provider), str(converted)], env={**os.environ, 'ANYPS5_GUEST_MODULE_ARGS': str(events)}, From 890c3218420b9d103fa3f2eaa457788ce6cb3e23 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Fri, 16 Oct 2026 14:18:25 -0700 Subject: [PATCH 0607/1000] fix(agc): give the overlay's view the transfer usage of its pass layouts The overlay's render pass starts in the layout the image was left in and ends in the one its caller needs next, but the attachment view allowed only COLOR_ATTACHMENT usage. A pass that starts in TRANSFER_DST_OPTIMAL (the present path when nothing was drawn, and the tests) or ends in TRANSFER_SRC_OPTIMAL (the tests' readback) then broke VUID-vkCmdBeginRenderPass-initialLayout-00898 and -00899. The view now also allows TRANSFER_DST or TRANSFER_SRC when the pass names that layout. Swapchain images have TRANSFER_DST and their pass ends in PRESENT_SRC, so the present path asks only for that; the test's cost report image, whose pass ends in TRANSFER_SRC_OPTIMAL, is now created with TRANSFER_SRC. --- .../prx/libSceAgcDriver/Execution/src/PerformanceOverlay.cpp | 5 +++++ core/libs/prx/libSceAgcDriver/tests/PerformanceOverlay.cpp | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceOverlay.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceOverlay.cpp index d7b2b9aebf..e21ddf0599 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceOverlay.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceOverlay.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -743,6 +744,10 @@ OverlayRect PerformanceOverlay::Record(VkCommandBuffer commands, std::size_t ind viewInfo.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; VkImageViewUsageCreateInfo usageInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_USAGE_CREATE_INFO}; usageInfo.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT; + for (const auto layout : {initialLayout, finalLayout}) { + if (layout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL) usageInfo.usage |= VK_IMAGE_USAGE_TRANSFER_DST_BIT; + if (layout == VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL) usageInfo.usage |= VK_IMAGE_USAGE_TRANSFER_SRC_BIT; + } viewInfo.pNext = &usageInfo; VkImageView view = VK_NULL_HANDLE; Graphics::Check(context.Function("vkCreateImageView")(context.device, &viewInfo, nullptr, &view), "vkCreateImageView performance overlay"); diff --git a/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlay.cpp b/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlay.cpp index ceb94348e8..860f34e2c3 100644 --- a/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlay.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlay.cpp @@ -575,7 +575,7 @@ void costReport(const Device& device, PerformanceOverlay& overlay) { const auto vramUs = std::chrono::duration(std::chrono::steady_clock::now() - sampled).count() / 100; std::printf("VRAM budget read in %.1f us (%s), once a second while the overlay is shown\n", vramUs, device.MemoryBudget() ? "VK_EXT_memory_budget" : "no VK_EXT_memory_budget"); for (const VkExtent2D extent : {VkExtent2D{1920, 1080}, VkExtent2D{3840, 2160}}) { - Image destination(context, VK_FORMAT_B8G8R8A8_UNORM, extent, VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT); + Image destination(context, VK_FORMAT_B8G8R8A8_UNORM, extent, VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT); const auto content = fullPanel(fixedDetails(extent, extent)); std::vector gpuMs; std::vector cpuMs; From e8614de27194ed8612f2760f1cd65a6d6444364c Mon Sep 17 00:00:00 2001 From: SandstromPL Date: Wed, 14 Oct 2026 20:40:36 +0530 Subject: [PATCH 0608/1000] fix(agc): rasterize a single-sample scan into a multisampled depth target at the pixel center A draw with PA_SC_AA_CONFIG.MSAA_NUM_SAMPLES 0 and MSAA_ENABLE clear into a 4x depth target threw "the depth target's sample count differs from the rasterizer's", which stops METAL GEAR SOLID: Peace Walker (PPSA18216) at its first presentation. Such a draw now rasterizes the depth target's sample count with every sample at the pixel center, as a single-sample scan into a multisampled color target already does. radeonsi programs this MSAA_ENABLE and PA_SC_AA_CONFIG with the depth buffer's own sample count when GL multisampling is disabled, and GL then gives full coverage with every depth and stencil sample set from the pixel center. Triangles and rectangles are accepted. Points, lines, alpha-to-coverage, DB_EQAA Z interpolation or overrasterization, a pixel program reading the sample index and a sample-mask export throw on this path, as does a single-sample color target next to the multisampled depth target. The primitive check is shared with the single-sample MSAA_ENABLE rule, whose behaviour and messages are unchanged. Co-Authored-By: Claude Opus 5.5 --- .../libSceAgcDriver/Graphics/src/State.cpp | 59 ++++++++--- .../prx/libSceAgcDriver/tests/Graphics.cpp | 98 +++++++++++++++++++ docs/dev/debt/libSceAgcDriver.md | 1 + 3 files changed, 143 insertions(+), 15 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index e6bc55ce8f..ad1d61eb3b 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -384,6 +384,29 @@ constexpr std::array, 2> StandardLocations2{{{4, 4}, constexpr std::array, 4> StandardLocations4{{{-2, -6}, {6, -2}, {-6, 2}, {2, 6}}}; constexpr std::array, 8> StandardLocations8{{{1, -3}, {-1, 3}, {5, 1}, {-3, -5}, {-5, 5}, {-7, -1}, {3, 7}, {7, -7}}}; +std::string nonTriangleRejection(const QueueState& queue, const char* message, bool rectangles = false) { + const auto word = [](const Registers& registers, std::uint32_t offset, RegisterBank bank = RegisterBank::Context) -> std::optional { + const auto it = find(registers, offset, bank); + if (it == registers.end()) return std::nullopt; + return it->second; + }; + const auto& cx = queue.context; + const auto stages = word(cx, 0x2d5); + if (!stages) return {}; + if ((*stages & 0x20u) != 0) { + if (const auto output = word(cx, 0x29b); output && ((*output & 0x80000000u) != 0 || (*output & 0x3fu) != 2u)) return zeroMessage(0x29b, *output, message); + } else if ((*stages & 4u) != 0) { + if (const auto parameters = word(cx, 0x2db)) { + const auto domain = *parameters & 3u; + const auto topology = (*parameters >> 5u) & 7u; + if ((domain != 1u && domain != 2u) || (topology != 2u && topology != 3u)) return zeroMessage(0x2db, *parameters, message); + } + } else if (const auto primitive = word(queue.userConfig, 0x242, RegisterBank::UserConfig); primitive && *primitive != 4u && *primitive != 5u && *primitive != 6u && !(rectangles && (*primitive == 7u || *primitive == 17u))) { + return zeroMessage(0x242, *primitive, message); + } + return {}; +} + std::string singleSampleMsaaRejection(const QueueState& queue) { const auto& cx = queue.context; const auto word = [](const Registers& registers, std::uint32_t offset, RegisterBank bank = RegisterBank::Context) -> std::optional { @@ -397,20 +420,19 @@ std::string singleSampleMsaaRejection(const QueueState& queue) { } if (const auto alphaToMask = word(cx, 0x2dc); alphaToMask && (*alphaToMask & 1u) != 0 && !(*alphaToMask == AlphaToCoverageDithered && alphaToCoverageDisabled(cx))) return zeroMessage(0x2dc, *alphaToMask, "MSAA_ENABLE on a single-sample target with alpha-to-coverage"); if (const auto conservative = word(cx, 0x313); conservative && (*conservative & ~0x001f6000u) != 0) return zeroMessage(0x313, *conservative, "MSAA_ENABLE on a single-sample target with conservative rasterization"); - constexpr const char* primitives = "MSAA_ENABLE on a single-sample target for primitives other than triangles"; - const auto stages = word(cx, 0x2d5); - if (!stages) return {}; - if ((*stages & 0x20u) != 0) { - if (const auto output = word(cx, 0x29b); output && ((*output & 0x80000000u) != 0 || (*output & 0x3fu) != 2u)) return zeroMessage(0x29b, *output, primitives); - } else if ((*stages & 4u) != 0) { - if (const auto parameters = word(cx, 0x2db)) { - const auto domain = *parameters & 3u; - const auto topology = (*parameters >> 5u) & 7u; - if ((domain != 1u && domain != 2u) || (topology != 2u && topology != 3u)) return zeroMessage(0x2db, *parameters, primitives); - } - } else if (const auto primitive = word(queue.userConfig, 0x242, RegisterBank::UserConfig); primitive && *primitive != 4u && *primitive != 5u && *primitive != 6u) { - return zeroMessage(0x242, *primitive, primitives); - } + return nonTriangleRejection(queue, "MSAA_ENABLE on a single-sample target for primitives other than triangles"); +} + +std::string singleSampleDepthScanRejection(const QueueState& queue) { + if (auto reason = nonTriangleRejection(queue, "a single-sample scan into a multisampled depth target for primitives other than triangles and rectangles", true); !reason.empty()) return reason; + const auto& cx = queue.context; + if (const auto eqaa = find(cx, 0x201); eqaa != cx.end() && (eqaa->second & 0x0f0c0000u) != 0) return zeroMessage(0x201, eqaa->second, "a single-sample scan into a multisampled depth target with Z interpolation or overrasterization (DB_EQAA)"); + if (PixelProgramSkipped(queue)) return {}; + constexpr std::uint32_t sampleIndex = ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::Ancillary); + const auto ena = find(cx, 0x1b3); + const auto addr = find(cx, 0x1b4); + if (ena != cx.end() && addr != cx.end() && (ena->second & addr->second & sampleIndex) != 0) return zeroMessage(0x1b3, ena->second, "a single-sample scan into a multisampled depth target with a pixel program reading the sample index"); + if (const auto control = find(cx, 0x203); control != cx.end() && (control->second & 0x100u) != 0) return zeroMessage(0x203, control->second, "a single-sample scan into a multisampled depth target with a sample-mask export"); return {}; } @@ -1055,7 +1077,6 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) { Require(!result.customSampleLocations, "dithered alpha-to-coverage with custom sample locations is unsupported"); } const bool depthKeepsSampleZero = result.depth && result.depth->samples == 1 && result.samples > 1; - Require(!result.depth || depthKeepsSampleZero || result.depth->samples == result.samples, "the depth target's sample count differs from the rasterizer's"); zero(cx, 0x292, ScanModeMask & ~1u, "scan conversion mode"); if (result.samples > 1 && (read(cx, 0x292) & 1u) == 0) { const bool filled = result.topology != VK_PRIMITIVE_TOPOLOGY_POINT_LIST && result.topology != VK_PRIMITIVE_TOPOLOGY_LINE_LIST && result.topology != VK_PRIMITIVE_TOPOLOGY_LINE_STRIP; @@ -1074,6 +1095,14 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) { Require(rasterizesSampleZeroOnly(cx, result), "the depth target's sample count differs from the rasterizer's"); rasterizeSampleZero(result); } + if (result.depth && result.depth->samples > 1 && result.samples == 1 && (read(cx, 0x292) & 1u) == 0) { + Require(!result.alphaToCoverage, "a single-sample scan into a multisampled depth target with alpha-to-coverage is unsupported"); + if (auto reason = singleSampleDepthScanRejection(queue); !reason.empty()) throw std::runtime_error(reason); + result.samples = result.depth->samples; + result.customSampleLocations = true; + result.sampleLocations.fill(0); + } + Require(!result.depth || depthKeepsSampleZero || result.depth->samples == result.samples, "the depth target's sample count differs from the rasterizer's"); zero(cx, 0x80, ~0u, "window offset"); zero(cx, 0x8d, ScreenOffsetMask, "reserved PA_SU_HARDWARE_SCREEN_OFFSET bits"); Require(read(cx, 0x83) == 0xffffu, "clip rectangles are unsupported"); diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index e2f1c24796..9377d1e7b0 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -3173,6 +3173,103 @@ void multisampleDccStateTests() { } } +void multisampledDepthTests() { + using AgcDriver::Graphics::DecodeState; + const auto singleSampleScan = [] { + auto queue = makeState(); + queue.context[0x2f8] = 0; + queue.context[0x292] = 2; + queue.context[0x31d] = 0x12000; + queue.context[0x1b3] = 2; + queue.context[0x1b4] = 2; + queue.context[0x000] = 0; + queue.context[0x002] = 0; + queue.context[0x007] = (3u << 16u) | 63u; + queue.context[0x00a] = 0; + queue.context[0x00b] = 0; + queue.context[0x010] = 0x2290098b; + queue.context[0x011] = 0x20000180; + for (const auto offset : {0x012u, 0x014u}) queue.context[offset] = 0x100; + queue.context[0x200] = 0x16; + return queue; + }; + const auto requireCentered = [](const AgcDriver::Graphics::State& state, const std::string& what) { + Require(state.samples == 4 && state.depth && state.depth->samples == 4 && state.customSampleLocations && !state.sampleLocationGrid && !state.perSampleShading, what + " did not rasterize the depth target's samples"); + const auto locations = AgcDriver::Graphics::SampleLocations(state); + for (std::uint32_t sample = 0; sample < state.samples; ++sample) Require(locations[sample].x == 0.5f && locations[sample].y == 0.5f, what + " did not place every sample at the pixel center"); + }; + const auto depthOnly = [&] { + auto queue = singleSampleScan(); + queue.context[0x8e] = 0; + queue.context[0x010] = 0xa290098b; + queue.context[0x200] = 0x777; + queue.context[0x201] = 0; + return queue; + }; + auto queue = depthOnly(); + auto state = DecodeState(queue); + Require(state.colors.empty(), "a depth-only single-sample scan decoded a color target"); + requireCentered(state, "a single-sample scan into a 4x depth target (PPSA18216's registers)"); + Require(AgcDriver::Graphics::DrawRejection(queue, false).empty(), "the precheck refused a single-sample scan into a 4x depth target"); + for (const auto primitive : {5u, 6u, 7u, 17u}) { + queue = depthOnly(); + queue.userConfig[0x242] = primitive; + requireCentered(DecodeState(queue), "a single-sample scan of primitive type " + std::to_string(primitive) + " into a 4x depth target"); + } + queue = depthOnly(); + queue.context[0x000] = 1; + queue.context[0x00b] = std::bit_cast(0.25f); + state = DecodeState(queue); + requireCentered(state, "a depth clear scanned single-sampled into a 4x depth target"); + Require(state.depthWrite && state.depthCompare == VK_COMPARE_OP_ALWAYS, "a depth clear into a 4x depth target did not write every covered sample"); + queue = depthOnly(); + queue.context[0x1b3] = queue.context[0x1b4] = 2u | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::Ancillary); + requireCentered(DecodeState(queue), "a single-sample scan whose pixel program is skipped"); + queue = depthOnly(); + queue.context[0x201] = 0x00040000u; + expectFailure([&] { DecodeState(queue); }, "a single-sample scan into a multisampled depth target with Z interpolation or overrasterization (DB_EQAA)"); + queue = singleSampleScan(); + state = DecodeState(queue); + Require(state.colors.size() == 1 && state.colors[0].samples == 4, "a single-sample scan into 4x depth and color targets lost its color target"); + requireCentered(state, "a single-sample scan into 4x depth and color targets"); + queue.context[0x293] = 0x10000u; + Require(!DecodeState(queue).perSampleShading, "PS_ITER_SAMPLE requested sample shading for a single-sample scan"); + queue = singleSampleScan(); + queue.context[0x31d] = 0; + expectFailure([&] { DecodeState(queue); }, "a color target's sample count differs from the rasterizer's"); + queue = singleSampleScan(); + queue.context[0x010] = 0x22900983; + expectFailure([&] { DecodeState(queue); }, "a color target's sample count differs from the rasterizer's"); + queue = singleSampleScan(); + queue.context[0x2dc] = 0x0000aa01u; + expectFailure([&] { DecodeState(queue); }, "a single-sample scan into a multisampled depth target with alpha-to-coverage"); + for (const auto primitive : {2u, 3u}) { + queue = singleSampleScan(); + queue.userConfig[0x242] = primitive; + expectFailure([&] { DecodeState(queue); }, "a single-sample scan into a multisampled depth target for primitives other than triangles and rectangles"); + } + queue = singleSampleScan(); + queue.context[0x1b3] = queue.context[0x1b4] = 2u | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::Ancillary); + expectFailure([&] { DecodeState(queue); }, "a single-sample scan into a multisampled depth target with a pixel program reading the sample index"); + queue = singleSampleScan(); + queue.context[0x1b3] = queue.context[0x1b4] = 2u | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PerspectiveSample); + requireCentered(DecodeState(queue), "a single-sample scan with a pixel program reading sample-interpolated inputs"); + queue = singleSampleScan(); + queue.context[0x1c4] = 9; + queue.context[0x203] |= 0x100u; + expectFailure([&] { DecodeState(queue); }, "a single-sample scan into a multisampled depth target with a sample-mask export"); + queue = singleSampleScan(); + queue.context[0x292] = 3; + for (std::uint32_t pixel = 0; pixel < 4; ++pixel) queue.context[0x2fe + pixel * 4u] = 0; + expectFailure([&] { DecodeState(queue); }, "the depth target's sample count differs from the rasterizer's"); + queue = singleSampleScan(); + queue.context[0x2f8] = 0x00100001; + queue.context[0x292] = 3; + for (std::uint32_t pixel = 0; pixel < 4; ++pixel) queue.context[0x2fe + pixel * 4u] = 0xcc44; + queue.context[0x31d] = 0x9000; + expectFailure([&] { DecodeState(queue); }, "the depth target's sample count differs from the rasterizer's"); +} + void clipDistanceTests() { auto queue = makeState(); queue.context[0x207] = 0x0040000f; @@ -7965,6 +8062,7 @@ int main(int argc, char** argv) { uint8x4TargetTests(); multisampleTests(); MsaaEnableSingleSampleTests(); + multisampledDepthTests(); clipDistanceTests(); framebufferLayerTests(); UnwrittenSampleLocationTests(); diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index b36be4ab30..12232f9731 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -121,6 +121,7 @@ - [Multisampled target guest access](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) (libSceAgcDriver) - the memory a multisampled color target throws on (CPU read, DMA copy, texture or storage image read) uses fragment-aware 64 KiB render-target blocks. [AMD Addrlib's GFX10 thin-block rule](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/core/addrlib2.cpp#L1680-L1712) gives the block width and height from element bytes and fragment count; its [surface-size calculation](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/gfx10/gfx10addrlib.cpp#L4000-L4005) rounds both extents to those blocks and multiplies by element bytes and fragments. Thus a 1920x1080 4x RGBA8 surface occupies 0x1fe0000 bytes, leaving the next allocation at +0x2000000 outside the guard. The 2x and 8x block orientations differ from treating the fragments as a larger element. Console address-layout equivalence has not been measured. Other tile modes retain the conservative `max(CB bytes * samples, width * height * element bytes * samples)` bound, which may still reject a distinct allocation. Targets are kept per device for the device's lifetime, one per surface address, format, extent and sample count, and are not evicted. - [CB resolve sample count](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a draw with CB_COLOR_CONTROL in resolve mode is a CB resolve when either PA_SC_AA_CONFIG or CB_COLOR0_ATTRIB.NUM_SAMPLES is multisampled, and takes its sample count from CB0. Inferred, not from documentation: PPSA36082 resolves its 4x targets with no sample count in PA_SC_AA_CONFIG, and radeonsi sets PA_SC_AA_CONFIG to 0 for a multisampled framebuffer when rasterizer multisampling and line smoothing are off. - [Custom MSAA sample locations](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) support a uniform 1x1 pattern for 2, 4 or 8 samples through VK_EXT_sample_locations. Patterns varying across the guest's 2x2 pixel grid use a 2x2 sample location grid, anchored at the framebuffer origin like the guest's while the window offset is zero, on devices whose `maxSampleLocationGridSize` for the sample count is a multiple of 2x2, and throw on others. The RTX 5070 reports 2x4 for 2 samples, 2x2 for 4 and 1x2 for 8; an AMD Radeon integrated GPU reports 2x2 for each. Devices without the required sample count, four subpixel bits or the full guest coordinate range refuse custom patterns. A multisampled rasterizer drawing into one-sample color targets, and a one-sample depth target if any, with MSAA_ENABLE set, no alpha-to-coverage and no coverage pixel input is drawn with one sample at sample 0's location in each pixel of the grid, as the targets keep only sample 0's coverage. Without PS_ITER_SAMPLE such a draw also refuses centroid inputs, and keeps PERSP_SAMPLE, LINEAR_SAMPLE and ANCILLARY: a pixel shaded once is taken to get sample 0's barycentrics and sample index 0. With PS_ITER_SAMPLE only sample 0's invocation reaches the targets, so its sample and centroid barycentrics are taken to be sample 0's and the draw is shaded once. Both are inferred, not measured. ANCILLARY with POS_X_FLOAT or POS_Y_FLOAT throws, since reading the sample index makes the host shade per sample, which moves FragCoord from the pixel center to sample 0's location. By the same Vulkan rule, PERSP_CENTER and LINEAR_CENTER inputs on this path are interpolated at sample 0's location on the host, where the console uses the pixel center; one-sample depth images are created with `SAMPLE_LOCATIONS_COMPATIBLE_DEPTH` when the device supports it, as multisampled ones already were; this is how VK_AMD_mixed_attachment_samples defines the case, and it is not measured on PS5 hardware. Other mixed sample counts throw. Depth images remain in GENERAL between draws, and render passes cannot continue across pattern changes. Guest offsets retain the existing decoder's signed four-bit, sixteenth-pixel interpretation; PS5 coverage and interpolation equivalence have not been measured on hardware. +- [Single-sample scan into a multisampled depth target](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a draw with `PA_SC_AA_CONFIG.MSAA_NUM_SAMPLES` = 0 and `PA_SC_MODE_CNTL_0.MSAA_ENABLE` clear into a multisampled depth target rasterizes the depth target's sample count with every sample at the pixel center, as a single-sample scan into a multisampled color target already does. radeonsi programs this `MSAA_ENABLE` and `PA_SC_AA_CONFIG` with the depth buffer's own sample count when GL multisampling is disabled on a multisampled framebuffer ([L1066-L1068](https://github.com/openbsd/xenocara/blob/1b39033df8d928ec247ca64448c4a63c72302db7/lib/mesa/src/gallium/drivers/radeonsi/si_state.c#L1066-L1068), [L3485-L3512](https://github.com/openbsd/xenocara/blob/1b39033df8d928ec247ca64448c4a63c72302db7/lib/mesa/src/gallium/drivers/radeonsi/si_state.c#L3485-L3512)), although its comment lists Z samples as at most the coverage samples ([L3439](https://github.com/openbsd/xenocara/blob/1b39033df8d928ec247ca64448c4a63c72302db7/lib/mesa/src/gallium/drivers/radeonsi/si_state.c#L3439)) and it sets `DB_EQAA.MAX_ANCHOR_SAMPLES` to the Z sample count, where PPSA18216 sets `DB_EQAA` to 0. GL then rasterizes with full coverage and lets every sample take the single-sample depth ([L185-L190](https://github.com/KhronosGroup/OpenGL-Registry/blob/6af574a14089ccfee87efe230ebcdd8742859813/extensions/ARB/ARB_multisample.txt#L185-L190)), or tests only a centermost sample and sets every depth and stencil sample from it ([L422-L434](https://github.com/KhronosGroup/OpenGL-Registry/blob/6af574a14089ccfee87efe230ebcdd8742859813/extensions/ARB/ARB_multisample.txt#L422-L434)), which is what this draws. Whether the console instead evaluates the depth plane at each sample position, and how its occlusion queries count such a draw, is not measured on hardware. Points, lines, alpha-to-coverage, `DB_EQAA` Z interpolation or overrasterization, a pixel program reading the sample index, a sample-mask export and a single-sample color target next to the multisampled depth target throw - [DCC display buffers](../../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) are presented only when all keys of the console's DCC extent agree on uncompressed or a fast-clear code, padding keys included; other keys, and register-clear keys over a pending image, throw. - [Storage image write-back](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) stores the GPU results over a CPU-stamped range whose guest bytes equal the copy taken when the image's layers got their generation (images up to 1 MiB whose guest bytes were settled at that point, with no unit shadow, pending image results or recorded GPU store over them; other images keep the previous rule: the CPU's bytes win). A CPU store that rewrites the baseline value after the GPU changed it is not told apart from no store, and the GPU result wins there. The 1 MiB limit is a choice, not a console value. - [16-bit float display buffers](../../../core/libs/prx/libSceAgcDriver/Execution/src/DisplayBuffer.cpp) (libSceAgcDriver, libSceVideoOut) - pixel formats `0xC001000622000000` and `0xC001000600000000` are taken as R16G16B16A16 and B16G16R16A16 float, eight bytes per texel, as [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/59183dbfa81ed7efdff6069177fed50de0552725/src/graphics/host_gpu/renderer/image/imageInfo.h) maps them and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/VideoOut/VideoOutTypes.cs) names them (`Rgba16Float`, `Bgra16Float`); PPSA11386 registers 3840x2160 tiled buffers of the first. The tiled layout is assumed to be the 8-byte SW_64KB_R_X equation of `TextureSwizzleEquations.hpp` applied to full coordinates, as addrlib applies it, by analogy with the 32-bit scanout; `ColorTargetLayout` applies the equations to in-block coordinates, which for 8-byte texels differs in every odd 64-row block row (address bit 11 takes y bit 6) and for 16-byte texels also differs, so a float display buffer written through `WriteColorTarget` and presented from guest memory would show those rows misplaced. The values are assumed to be linear light and are encoded with the sRGB curve for the 8-bit output. Bits 32-39 of these formats are `0x06`, as in the 10-bit formats SharpProspero lists without sRGB transfer, while its sRGB formats have `0x00`, its 10-bit PQ formats `0x04` (with `0x07` in bits 40-47) and its float BT.2100 PQ formats `0x07`: they show only that these formats are not the sRGB ones. NaN and negative values give 0, values of 1 and above give 255, alpha is linear. Neither the layout nor the transfer is verified against console or title frames, and HDR output is not modelled. DCC metadata on a float buffer throws. From 237fe9c2690d3fbb0d7615593d7890459ccb5260 Mon Sep 17 00:00:00 2001 From: SandstromPL Date: Thu, 15 Oct 2026 00:47:28 +0530 Subject: [PATCH 0609/1000] docs(agc): cite the GL rule the multisampled depth scan follows The debt entry cited ARB_multisample's centermost-sample shortcut as what a single-sample scan into a multisampled depth target draws. The draw tests each depth and stencil sample with the depth at the pixel center, which is the rule above that shortcut (L185-L190 and L422-L425); the shortcut is now named as an unmeasured alternative. Co-Authored-By: Claude Opus 5.5 --- docs/dev/debt/libSceAgcDriver.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 12232f9731..3f7f483afe 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -121,7 +121,7 @@ - [Multisampled target guest access](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) (libSceAgcDriver) - the memory a multisampled color target throws on (CPU read, DMA copy, texture or storage image read) uses fragment-aware 64 KiB render-target blocks. [AMD Addrlib's GFX10 thin-block rule](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/core/addrlib2.cpp#L1680-L1712) gives the block width and height from element bytes and fragment count; its [surface-size calculation](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/gfx10/gfx10addrlib.cpp#L4000-L4005) rounds both extents to those blocks and multiplies by element bytes and fragments. Thus a 1920x1080 4x RGBA8 surface occupies 0x1fe0000 bytes, leaving the next allocation at +0x2000000 outside the guard. The 2x and 8x block orientations differ from treating the fragments as a larger element. Console address-layout equivalence has not been measured. Other tile modes retain the conservative `max(CB bytes * samples, width * height * element bytes * samples)` bound, which may still reject a distinct allocation. Targets are kept per device for the device's lifetime, one per surface address, format, extent and sample count, and are not evicted. - [CB resolve sample count](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a draw with CB_COLOR_CONTROL in resolve mode is a CB resolve when either PA_SC_AA_CONFIG or CB_COLOR0_ATTRIB.NUM_SAMPLES is multisampled, and takes its sample count from CB0. Inferred, not from documentation: PPSA36082 resolves its 4x targets with no sample count in PA_SC_AA_CONFIG, and radeonsi sets PA_SC_AA_CONFIG to 0 for a multisampled framebuffer when rasterizer multisampling and line smoothing are off. - [Custom MSAA sample locations](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) support a uniform 1x1 pattern for 2, 4 or 8 samples through VK_EXT_sample_locations. Patterns varying across the guest's 2x2 pixel grid use a 2x2 sample location grid, anchored at the framebuffer origin like the guest's while the window offset is zero, on devices whose `maxSampleLocationGridSize` for the sample count is a multiple of 2x2, and throw on others. The RTX 5070 reports 2x4 for 2 samples, 2x2 for 4 and 1x2 for 8; an AMD Radeon integrated GPU reports 2x2 for each. Devices without the required sample count, four subpixel bits or the full guest coordinate range refuse custom patterns. A multisampled rasterizer drawing into one-sample color targets, and a one-sample depth target if any, with MSAA_ENABLE set, no alpha-to-coverage and no coverage pixel input is drawn with one sample at sample 0's location in each pixel of the grid, as the targets keep only sample 0's coverage. Without PS_ITER_SAMPLE such a draw also refuses centroid inputs, and keeps PERSP_SAMPLE, LINEAR_SAMPLE and ANCILLARY: a pixel shaded once is taken to get sample 0's barycentrics and sample index 0. With PS_ITER_SAMPLE only sample 0's invocation reaches the targets, so its sample and centroid barycentrics are taken to be sample 0's and the draw is shaded once. Both are inferred, not measured. ANCILLARY with POS_X_FLOAT or POS_Y_FLOAT throws, since reading the sample index makes the host shade per sample, which moves FragCoord from the pixel center to sample 0's location. By the same Vulkan rule, PERSP_CENTER and LINEAR_CENTER inputs on this path are interpolated at sample 0's location on the host, where the console uses the pixel center; one-sample depth images are created with `SAMPLE_LOCATIONS_COMPATIBLE_DEPTH` when the device supports it, as multisampled ones already were; this is how VK_AMD_mixed_attachment_samples defines the case, and it is not measured on PS5 hardware. Other mixed sample counts throw. Depth images remain in GENERAL between draws, and render passes cannot continue across pattern changes. Guest offsets retain the existing decoder's signed four-bit, sixteenth-pixel interpretation; PS5 coverage and interpolation equivalence have not been measured on hardware. -- [Single-sample scan into a multisampled depth target](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a draw with `PA_SC_AA_CONFIG.MSAA_NUM_SAMPLES` = 0 and `PA_SC_MODE_CNTL_0.MSAA_ENABLE` clear into a multisampled depth target rasterizes the depth target's sample count with every sample at the pixel center, as a single-sample scan into a multisampled color target already does. radeonsi programs this `MSAA_ENABLE` and `PA_SC_AA_CONFIG` with the depth buffer's own sample count when GL multisampling is disabled on a multisampled framebuffer ([L1066-L1068](https://github.com/openbsd/xenocara/blob/1b39033df8d928ec247ca64448c4a63c72302db7/lib/mesa/src/gallium/drivers/radeonsi/si_state.c#L1066-L1068), [L3485-L3512](https://github.com/openbsd/xenocara/blob/1b39033df8d928ec247ca64448c4a63c72302db7/lib/mesa/src/gallium/drivers/radeonsi/si_state.c#L3485-L3512)), although its comment lists Z samples as at most the coverage samples ([L3439](https://github.com/openbsd/xenocara/blob/1b39033df8d928ec247ca64448c4a63c72302db7/lib/mesa/src/gallium/drivers/radeonsi/si_state.c#L3439)) and it sets `DB_EQAA.MAX_ANCHOR_SAMPLES` to the Z sample count, where PPSA18216 sets `DB_EQAA` to 0. GL then rasterizes with full coverage and lets every sample take the single-sample depth ([L185-L190](https://github.com/KhronosGroup/OpenGL-Registry/blob/6af574a14089ccfee87efe230ebcdd8742859813/extensions/ARB/ARB_multisample.txt#L185-L190)), or tests only a centermost sample and sets every depth and stencil sample from it ([L422-L434](https://github.com/KhronosGroup/OpenGL-Registry/blob/6af574a14089ccfee87efe230ebcdd8742859813/extensions/ARB/ARB_multisample.txt#L422-L434)), which is what this draws. Whether the console instead evaluates the depth plane at each sample position, and how its occlusion queries count such a draw, is not measured on hardware. Points, lines, alpha-to-coverage, `DB_EQAA` Z interpolation or overrasterization, a pixel program reading the sample index, a sample-mask export and a single-sample color target next to the multisampled depth target throw +- [Single-sample scan into a multisampled depth target](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a draw with `PA_SC_AA_CONFIG.MSAA_NUM_SAMPLES` = 0 and `PA_SC_MODE_CNTL_0.MSAA_ENABLE` clear into a multisampled depth target rasterizes the depth target's sample count with every sample at the pixel center, as a single-sample scan into a multisampled color target already does. radeonsi programs this `MSAA_ENABLE` and `PA_SC_AA_CONFIG` with the depth buffer's own sample count when GL multisampling is disabled on a multisampled framebuffer ([L1066-L1068](https://github.com/openbsd/xenocara/blob/1b39033df8d928ec247ca64448c4a63c72302db7/lib/mesa/src/gallium/drivers/radeonsi/si_state.c#L1066-L1068), [L3485-L3512](https://github.com/openbsd/xenocara/blob/1b39033df8d928ec247ca64448c4a63c72302db7/lib/mesa/src/gallium/drivers/radeonsi/si_state.c#L3485-L3512)), although its comment lists Z samples as at most the coverage samples ([L3439](https://github.com/openbsd/xenocara/blob/1b39033df8d928ec247ca64448c4a63c72302db7/lib/mesa/src/gallium/drivers/radeonsi/si_state.c#L3439)) and it sets `DB_EQAA.MAX_ANCHOR_SAMPLES` to the Z sample count, where PPSA18216 sets `DB_EQAA` to 0. GL then lets every sample take the single-sample depth ([L185-L190](https://github.com/KhronosGroup/OpenGL-Registry/blob/6af574a14089ccfee87efe230ebcdd8742859813/extensions/ARB/ARB_multisample.txt#L185-L190)) and the fragment be treated as in multisampling with full coverage ([L422-L425](https://github.com/KhronosGroup/OpenGL-Registry/blob/6af574a14089ccfee87efe230ebcdd8742859813/extensions/ARB/ARB_multisample.txt#L422-L425)), which is what this draws: each depth and stencil sample is tested and written with the depth at the pixel center. GL also lets an implementation test only a centermost sample and set every sample from it ([L425-L434](https://github.com/KhronosGroup/OpenGL-Registry/blob/6af574a14089ccfee87efe230ebcdd8742859813/extensions/ARB/ARB_multisample.txt#L425-L434)), which differs when a pixel's stored samples differ; which one the console does, whether it instead evaluates the depth plane at each sample position, and how its occlusion queries count such a draw, is not measured on hardware. Points, lines, alpha-to-coverage, `DB_EQAA` Z interpolation or overrasterization, a pixel program reading the sample index, a sample-mask export and a single-sample color target next to the multisampled depth target throw - [DCC display buffers](../../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) are presented only when all keys of the console's DCC extent agree on uncompressed or a fast-clear code, padding keys included; other keys, and register-clear keys over a pending image, throw. - [Storage image write-back](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) stores the GPU results over a CPU-stamped range whose guest bytes equal the copy taken when the image's layers got their generation (images up to 1 MiB whose guest bytes were settled at that point, with no unit shadow, pending image results or recorded GPU store over them; other images keep the previous rule: the CPU's bytes win). A CPU store that rewrites the baseline value after the GPU changed it is not told apart from no store, and the GPU result wins there. The 1 MiB limit is a choice, not a console value. - [16-bit float display buffers](../../../core/libs/prx/libSceAgcDriver/Execution/src/DisplayBuffer.cpp) (libSceAgcDriver, libSceVideoOut) - pixel formats `0xC001000622000000` and `0xC001000600000000` are taken as R16G16B16A16 and B16G16R16A16 float, eight bytes per texel, as [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/59183dbfa81ed7efdff6069177fed50de0552725/src/graphics/host_gpu/renderer/image/imageInfo.h) maps them and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/VideoOut/VideoOutTypes.cs) names them (`Rgba16Float`, `Bgra16Float`); PPSA11386 registers 3840x2160 tiled buffers of the first. The tiled layout is assumed to be the 8-byte SW_64KB_R_X equation of `TextureSwizzleEquations.hpp` applied to full coordinates, as addrlib applies it, by analogy with the 32-bit scanout; `ColorTargetLayout` applies the equations to in-block coordinates, which for 8-byte texels differs in every odd 64-row block row (address bit 11 takes y bit 6) and for 16-byte texels also differs, so a float display buffer written through `WriteColorTarget` and presented from guest memory would show those rows misplaced. The values are assumed to be linear light and are encoded with the sRGB curve for the 8-bit output. Bits 32-39 of these formats are `0x06`, as in the 10-bit formats SharpProspero lists without sRGB transfer, while its sRGB formats have `0x00`, its 10-bit PQ formats `0x04` (with `0x07` in bits 40-47) and its float BT.2100 PQ formats `0x07`: they show only that these formats are not the sRGB ones. NaN and negative values give 0, values of 1 and above give 255, alpha is linear. Neither the layout nor the transfer is verified against console or title frames, and HDR output is not modelled. DCC metadata on a float buffer throws. From cd4b52f05a88f0a258c3f4980874b308339a49cd Mon Sep 17 00:00:00 2001 From: notaxeltv Date: Wed, 14 Oct 2026 23:24:36 +0200 Subject: [PATCH 0610/1000] feat(libc): allocate through operator new with align_val_t Co-authored-by: Cursor --- core/libs/prx/libc/src/StdioExtras.cpp | 22 ++++++++++++++++++++++ core/libs/tests/GuestNewHandler.cpp | 9 +++++++++ 2 files changed, 31 insertions(+) diff --git a/core/libs/prx/libc/src/StdioExtras.cpp b/core/libs/prx/libc/src/StdioExtras.cpp index ab69378356..28b5f7b33b 100644 --- a/core/libs/prx/libc/src/StdioExtras.cpp +++ b/core/libs/prx/libc/src/StdioExtras.cpp @@ -38,6 +38,24 @@ void* Allocate(std::size_t size) { } } +void* AllocateAligned(std::size_t size, std::size_t alignment) { + for (;;) { + GuestNewHandler handler = nullptr; + try { + return ApplicationHeapAlign_nid_no_patch(alignment, size == 0 ? 1 : size); + } catch (const std::bad_alloc&) { + handler = g_newHandler.load(); + if (handler == nullptr) throw; + } catch (const std::length_error&) { + handler = g_newHandler.load(); + if (handler == nullptr) throw std::bad_alloc(); + } catch (const std::invalid_argument&) { + throw std::bad_alloc(); + } + handler(); + } +} + } extern "C" { @@ -89,6 +107,10 @@ void* APS5_VABI _Znam_nid_postfix(std::size_t size) { return Allocate(size); } +void* APS5_VABI _ZnwmSt11align_val_t_nid_postfix(std::size_t size, std::size_t alignment) { + return AllocateAligned(size, alignment); +} + void* APS5_VABI _ZnwmRKSt9nothrow_t_nid_postfix(std::size_t size, const void*) noexcept { try { return Allocate(size); diff --git a/core/libs/tests/GuestNewHandler.cpp b/core/libs/tests/GuestNewHandler.cpp index cb1a0364e1..780f02246d 100644 --- a/core/libs/tests/GuestNewHandler.cpp +++ b/core/libs/tests/GuestNewHandler.cpp @@ -15,9 +15,11 @@ Handler APS5_VABI _ZSt15set_new_handlerPFvvE_nid_postfix(Handler); Handler APS5_VABI _ZSt15get_new_handlerv_nid_postfix(); void* APS5_VABI _Znwm_nid_postfix(std::size_t); void* APS5_VABI _Znam_nid_postfix(std::size_t); +void* APS5_VABI _ZnwmSt11align_val_t_nid_postfix(std::size_t, std::size_t); void* APS5_VABI _ZnwmRKSt9nothrow_t_nid_postfix(std::size_t, const void*) noexcept; void* APS5_VABI _ZnamRKSt9nothrow_t_nid_postfix(std::size_t, const void*) noexcept; void APS5_VABI _ZdlPv_nid_postfix(void*); +void APS5_VABI _ZdlPvSt11align_val_t_nid_postfix(void*, std::size_t); } namespace { @@ -176,6 +178,13 @@ int main(int argc, char** argv) { REQUIRE(_ZSt15set_new_handlerPFvvE_nid_postfix(nullptr) == &allocatingHandler); REQUIRE(_ZSt15get_new_handlerv_nid_postfix() == nullptr); + Reset(0); + void* aligned = _ZnwmSt11align_val_t_nid_postfix(24, 16); + REQUIRE(aligned == storage.data() && lastSize == 24 && allocations == 1); + _ZdlPvSt11align_val_t_nid_postfix(aligned, 16); + Reset(1); + REQUIRE(Throws([] { _ZnwmSt11align_val_t_nid_postfix(8, 32); }) && handlerCalls == 0 && allocations == 1); + std::puts("Guest new handler checks passed"); return 0; } From a6fd136a30fec125de3a65ce5d7250817d3e4a53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Sat, 17 Oct 2026 00:11:10 +0200 Subject: [PATCH 0611/1000] test(libc): pin aligned new alignment argument, zero size and new-handler retry --- core/libs/tests/GuestNewHandler.cpp | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/core/libs/tests/GuestNewHandler.cpp b/core/libs/tests/GuestNewHandler.cpp index 780f02246d..b7209c6df6 100644 --- a/core/libs/tests/GuestNewHandler.cpp +++ b/core/libs/tests/GuestNewHandler.cpp @@ -29,6 +29,7 @@ unsigned failuresLeft = 0; unsigned allocations = 0; unsigned handlerCalls = 0; std::size_t lastSize = 0; +std::size_t lastAlignment = 0; void Require(bool condition, int line) { if (!condition) { @@ -52,7 +53,10 @@ void* APS5_VABI allocate(std::size_t bytes) { void APS5_VABI release(void*) {} void* APS5_VABI allocateZeroed(std::size_t count, std::size_t bytes) { return allocate(count * bytes); } void* APS5_VABI reallocate(void*, std::size_t bytes) { return allocate(bytes); } -void* APS5_VABI align(std::size_t, std::size_t bytes) { return allocate(bytes); } +void* APS5_VABI align(std::size_t alignment, std::size_t bytes) { + lastAlignment = alignment; + return allocate(bytes); +} void* APS5_VABI realign(void*, std::size_t bytes, std::size_t) { return allocate(bytes); } int APS5_VABI posixAlign(void** pointer, std::size_t, std::size_t bytes) { *pointer = allocate(bytes); @@ -180,11 +184,18 @@ int main(int argc, char** argv) { Reset(0); void* aligned = _ZnwmSt11align_val_t_nid_postfix(24, 16); - REQUIRE(aligned == storage.data() && lastSize == 24 && allocations == 1); + REQUIRE(aligned == storage.data() && lastSize == 24 && lastAlignment == 16 && allocations == 1); _ZdlPvSt11align_val_t_nid_postfix(aligned, 16); Reset(1); REQUIRE(Throws([] { _ZnwmSt11align_val_t_nid_postfix(8, 32); }) && handlerCalls == 0 && allocations == 1); + Reset(0); + REQUIRE(_ZnwmSt11align_val_t_nid_postfix(0, 64) == storage.data() && lastSize == 1 && lastAlignment == 64); + Reset(2); + _ZSt15set_new_handlerPFvvE_nid_postfix(&countingHandler); + REQUIRE(_ZnwmSt11align_val_t_nid_postfix(8, 32) == storage.data() && handlerCalls == 2 && allocations == 3); + _ZSt15set_new_handlerPFvvE_nid_postfix(nullptr); + std::puts("Guest new handler checks passed"); return 0; } From 883e4b64f6410149c7bd868c7311943a14a659dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Sat, 17 Oct 2026 00:14:01 +0200 Subject: [PATCH 0612/1000] test(libc): use 16-byte alignment the mock storage satisfies --- core/libs/tests/GuestNewHandler.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/libs/tests/GuestNewHandler.cpp b/core/libs/tests/GuestNewHandler.cpp index b7209c6df6..04a5d9a8a1 100644 --- a/core/libs/tests/GuestNewHandler.cpp +++ b/core/libs/tests/GuestNewHandler.cpp @@ -190,10 +190,10 @@ int main(int argc, char** argv) { REQUIRE(Throws([] { _ZnwmSt11align_val_t_nid_postfix(8, 32); }) && handlerCalls == 0 && allocations == 1); Reset(0); - REQUIRE(_ZnwmSt11align_val_t_nid_postfix(0, 64) == storage.data() && lastSize == 1 && lastAlignment == 64); + REQUIRE(_ZnwmSt11align_val_t_nid_postfix(0, 16) == storage.data() && lastSize == 1 && lastAlignment == 16); Reset(2); _ZSt15set_new_handlerPFvvE_nid_postfix(&countingHandler); - REQUIRE(_ZnwmSt11align_val_t_nid_postfix(8, 32) == storage.data() && handlerCalls == 2 && allocations == 3); + REQUIRE(_ZnwmSt11align_val_t_nid_postfix(8, 16) == storage.data() && handlerCalls == 2 && allocations == 3); _ZSt15set_new_handlerPFvvE_nid_postfix(nullptr); std::puts("Guest new handler checks passed"); From b868190bc217a665551152e95ba661a37398cea0 Mon Sep 17 00:00:00 2001 From: Leonardo Date: Sat, 17 Oct 2026 00:36:18 +0200 Subject: [PATCH 0613/1000] fix(libSceVoiceQoS): take 4-arg local endpoint and relax unverified rejections --- core/libs/prx/libSceVoiceQoS/Export.cpp | 31 +++++++++++-------------- core/libs/tests/GuestVoiceQoS.cpp | 24 ++++++++++++------- docs/dev/debt/libSceVoiceQoS.md | 2 +- 3 files changed, 31 insertions(+), 26 deletions(-) diff --git a/core/libs/prx/libSceVoiceQoS/Export.cpp b/core/libs/prx/libSceVoiceQoS/Export.cpp index 3c856d1a4e..9813b98413 100644 --- a/core/libs/prx/libSceVoiceQoS/Export.cpp +++ b/core/libs/prx/libSceVoiceQoS/Export.cpp @@ -52,20 +52,19 @@ int CreateEndpoint(std::int32_t* id, std::map& endpoints return 0; } -int DeleteEndpoint(std::int32_t id, std::map& endpoints, std::int32_t Connection::*side, const QoS& qos) { - if (!endpoints.contains(id)) return SCE_VOICE_ERROR_LIBVOICEQOS_ARGUMENT_INVALID; - for (const auto& entry : qos.connections) { - if (entry.second.*side == id) return SCE_VOICE_ERROR_LIBVOICEQOS_ARGUMENT_INVALID; - } - endpoints.erase(id); - return 0; +int DeleteEndpoint(std::int32_t id, std::map& endpoints) { + return endpoints.erase(id) != 0 ? 0 : SCE_VOICE_ERROR_LIBVOICEQOS_ARGUMENT_INVALID; } int SetAttribute(std::map& endpoints, std::int32_t id, std::int32_t attribute, const void* value, std::int32_t size) { const auto endpoint = endpoints.find(id); - if (endpoint == endpoints.end() || !value || size <= 0) return SCE_VOICE_ERROR_LIBVOICEQOS_ARGUMENT_INVALID; - const auto* bytes = static_cast(value); - endpoint->second[attribute].assign(bytes, bytes + size); + if (endpoint == endpoints.end() || !value) return SCE_VOICE_ERROR_LIBVOICEQOS_ARGUMENT_INVALID; + if (size > 0) { + const auto* bytes = static_cast(value); + endpoint->second[attribute].assign(bytes, bytes + size); + } else { + endpoint->second[attribute].clear(); + } return 0; } @@ -100,14 +99,15 @@ int APS5_VABI sceVoiceQoSEnd() { }); } -int APS5_VABI sceVoiceQoSCreateLocalEndpoint(int32_t* localId, int32_t userId, int32_t device) { +int APS5_VABI sceVoiceQoSCreateLocalEndpoint(int32_t* localId, int32_t userId, int32_t deviceInId, int32_t deviceOutId) { (void)userId; - (void)device; + (void)deviceInId; + (void)deviceOutId; return Locked([&](QoS& qos) { return CreateEndpoint(localId, qos.locals, qos); }); } int APS5_VABI sceVoiceQoSDeleteLocalEndpoint(int32_t localId) { - return Locked([&](QoS& qos) { return DeleteEndpoint(localId, qos.locals, &Connection::local, qos); }); + return Locked([&](QoS& qos) { return DeleteEndpoint(localId, qos.locals); }); } int APS5_VABI sceVoiceQoSCreateRemoteEndpoint(int32_t* remoteId) { @@ -115,15 +115,12 @@ int APS5_VABI sceVoiceQoSCreateRemoteEndpoint(int32_t* remoteId) { } int APS5_VABI sceVoiceQoSDeleteRemoteEndpoint(int32_t remoteId) { - return Locked([&](QoS& qos) { return DeleteEndpoint(remoteId, qos.remotes, &Connection::remote, qos); }); + return Locked([&](QoS& qos) { return DeleteEndpoint(remoteId, qos.remotes); }); } int APS5_VABI sceVoiceQoSConnect(int32_t* connectionId, int32_t localId, int32_t remoteId) { return Locked([&](QoS& qos) { if (!connectionId || !qos.locals.contains(localId) || !qos.remotes.contains(remoteId)) return SCE_VOICE_ERROR_LIBVOICEQOS_ARGUMENT_INVALID; - for (const auto& entry : qos.connections) { - if (entry.second.local == localId && entry.second.remote == remoteId) return SCE_VOICE_ERROR_LIBVOICEQOS_ARGUMENT_INVALID; - } *connectionId = NewId(qos); qos.connections.emplace(*connectionId, Connection{localId, remoteId}); return 0; diff --git a/core/libs/tests/GuestVoiceQoS.cpp b/core/libs/tests/GuestVoiceQoS.cpp index a1aa646fc0..272b43a1a0 100644 --- a/core/libs/tests/GuestVoiceQoS.cpp +++ b/core/libs/tests/GuestVoiceQoS.cpp @@ -6,7 +6,7 @@ extern "C" { int APS5_VABI sceVoiceQoSInit(void*, std::uint32_t, std::int32_t); int APS5_VABI sceVoiceQoSEnd(); -int APS5_VABI sceVoiceQoSCreateLocalEndpoint(std::int32_t*, std::int32_t, std::int32_t); +int APS5_VABI sceVoiceQoSCreateLocalEndpoint(std::int32_t*, std::int32_t, std::int32_t, std::int32_t); int APS5_VABI sceVoiceQoSDeleteLocalEndpoint(std::int32_t); int APS5_VABI sceVoiceQoSCreateRemoteEndpoint(std::int32_t*); int APS5_VABI sceVoiceQoSDeleteRemoteEndpoint(std::int32_t); @@ -39,20 +39,28 @@ int main(int argc, char** argv) { std::int32_t local = -1; std::int32_t remote = -1; std::int32_t connection = -1; - Require(sceVoiceQoSCreateLocalEndpoint(nullptr, 1, 0) == argumentInvalid); - Require(sceVoiceQoSCreateLocalEndpoint(&local, 1, 0) == 0 && local > 0); + Require(sceVoiceQoSCreateLocalEndpoint(nullptr, 1, 0, 0) == argumentInvalid); + Require(sceVoiceQoSCreateLocalEndpoint(&local, 1, 0, 0) == 0 && local > 0); Require(sceVoiceQoSCreateRemoteEndpoint(&remote) == 0 && remote > 0 && remote != local); Require(sceVoiceQoSConnect(&connection, remote, local) == argumentInvalid); Require(sceVoiceQoSConnect(&connection, local, remote) == 0 && connection > 0); std::int32_t duplicate = -1; - Require(sceVoiceQoSConnect(&duplicate, local, remote) == argumentInvalid); - Require(sceVoiceQoSDeleteLocalEndpoint(local) == argumentInvalid); + Require(sceVoiceQoSConnect(&duplicate, local, remote) == 0 && duplicate > 0 && duplicate != connection); + Require(sceVoiceQoSDisconnect(duplicate) == 0); + + std::int32_t otherLocal = -1; + std::int32_t otherConnection = -1; + Require(sceVoiceQoSCreateLocalEndpoint(&otherLocal, 1, 0, 0) == 0); + Require(sceVoiceQoSConnect(&otherConnection, otherLocal, remote) == 0); + Require(sceVoiceQoSDeleteLocalEndpoint(otherLocal) == 0); + Require(sceVoiceQoSDeleteLocalEndpoint(otherLocal) == argumentInvalid); + Require(sceVoiceQoSDisconnect(otherConnection) == 0); const std::uint32_t level = 0x1234; Require(sceVoiceQoSSetLocalEndpointAttribute(local, 3, &level, sizeof(level)) == 0); Require(sceVoiceQoSSetLocalEndpointAttribute(local, 3, nullptr, sizeof(level)) == argumentInvalid); - Require(sceVoiceQoSSetLocalEndpointAttribute(local, 3, &level, 0) == argumentInvalid); - Require(sceVoiceQoSSetLocalEndpointAttribute(local, 3, &level, -1) == argumentInvalid); + Require(sceVoiceQoSSetLocalEndpointAttribute(local, 3, &level, 0) == 0); + Require(sceVoiceQoSSetLocalEndpointAttribute(local, 3, &level, -1) == 0); Require(sceVoiceQoSSetLocalEndpointAttribute(local + 100, 3, &level, sizeof(level)) == argumentInvalid); Require(sceVoiceQoSDisconnect(connection) == 0); @@ -65,7 +73,7 @@ int main(int argc, char** argv) { Require(sceVoiceQoSEnd() == 0); Require(sceVoiceQoSEnd() == notInitialized); Require(sceVoiceQoSDeleteRemoteEndpoint(remote) == notInitialized); - Require(sceVoiceQoSCreateLocalEndpoint(&local, 1, 0) == notInitialized); + Require(sceVoiceQoSCreateLocalEndpoint(&local, 1, 0, 0) == notInitialized); Require(sceVoiceQoSInit(memory.data(), 0x40000, appType) == 0); Require(sceVoiceQoSDeleteRemoteEndpoint(remote) == argumentInvalid); Require(sceVoiceQoSEnd() == 0); diff --git a/docs/dev/debt/libSceVoiceQoS.md b/docs/dev/debt/libSceVoiceQoS.md index 4dd889ccfa..49bb102e5a 100644 --- a/docs/dev/debt/libSceVoiceQoS.md +++ b/docs/dev/debt/libSceVoiceQoS.md @@ -3,7 +3,7 @@ ### Unknown function info - [sceVoiceQoSSetConnectionAttribute](../../../core/libs/prx/libSceVoiceQoS/Export.cpp) (libSceVoiceQoS, NID `KCGOxg8iX7s`) is a throwing stub declared without parameters, like the other endpoint calls of the library: PPSA04159 fails to load without it and its signature is unknown. The name hashes to the imported NID (`tools/nid_names.py`) -- [sceVoiceQoSInit](../../../core/libs/prx/libSceVoiceQoS/Export.cpp) (libSceVoiceQoS) - `0x804E0905` for a second initialization comes from [prosper](https://github.com/mattias800/prosper)'s reading of the module and was not measured: on a PS5 a valid call blocks inside a homebrew payload, so it never returns there. PPSA26344's two call sites (`0xE307BF`, `0x151C45D`) pass an allocated pointer, a 0x40000-byte block and app type 0x10000000; the app type and block are not used. The endpoint calls assume the PS4 `libSceVoiceQoS` signatures (`CreateLocalEndpoint(int* id, user, device)`, `DeleteLocalEndpoint(id)`, `CreateRemoteEndpoint(int* id)`, `DeleteRemoteEndpoint(id)`, `Connect(int* id, local, remote)`, `Disconnect(connection)`, `SetLocalEndpointAttribute(id, attribute, value, size)`), not confirmed on a PS5 title. Endpoints and connections are tracked (an endpoint in a connection cannot be deleted, a pair connects once); local endpoint attributes are stored as given, as their ids and defaults are unknown; other endpoint and packet calls remain throwing stubs. The not-initialized error 0x804E0901 is assumed from the 0x804E09xx numbering, and unknown ids return `ARGUMENT_INVALID`. Aliens: Fireteam Elite (PPSA03393) uses all seven implemented calls on its first load path, starting with `sceVoiceQoSCreateLocalEndpoint` when it registers the local talker; it stops with `not implemented` when any of them throws +- [sceVoiceQoSInit](../../../core/libs/prx/libSceVoiceQoS/Export.cpp) (libSceVoiceQoS) - `0x804E0905` for a second initialization comes from [prosper](https://github.com/mattias800/prosper)'s reading of the module and was not measured: on a PS5 a valid call blocks inside a homebrew payload, so it never returns there. PPSA26344's two call sites (`0xE307BF`, `0x151C45D`) pass an allocated pointer, a 0x40000-byte block and app type 0x10000000; the app type and block are not used. `sceVoiceQoSCreateLocalEndpoint` takes four arguments (`int* id, userId, deviceInId, deviceOutId`) as declared in fpPS4 (`src/ps4_libscevoice.pas`, `ps4_sceVoiceQoSCreateLocalEndpoint`); no public reference exists for the other six signatures (`DeleteLocalEndpoint(id)`, `CreateRemoteEndpoint(int* id)`, `DeleteRemoteEndpoint(id)`, `Connect(int* id, local, remote)`, `Disconnect(connection)`, `SetLocalEndpointAttribute(id, attribute, value, size)`), which are assumed from function naming and call patterns and not confirmed on a PS5 title. Endpoints and connections are tracked by allocated id; duplicate connections and deleting an endpoint while referenced in a connection are permitted as the console is not known to reject them; local endpoint attributes are stored as given (including empty or non-positive sizes), as their ids and defaults are unknown; other endpoint and packet calls remain throwing stubs. The not-initialized error 0x804E0901 is assumed from the 0x804E09xx numbering, and unknown ids return `ARGUMENT_INVALID`. Aliens: Fireteam Elite (PPSA03393) uses all seven implemented calls on its first load path, starting with `sceVoiceQoSCreateLocalEndpoint` when it registers the local talker; it stops with `not implemented` when any of them throws ### Functional From 84cae0bda0fdd7744ed781af9cee079c74c4b4d6 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Fri, 16 Oct 2026 15:45:52 -0700 Subject: [PATCH 0614/1000] fix(agc): pick the overlay's palette entry with constant indices On RADV the overlay's fragment shader read parameters.palette[paletteIndex] as palette[0] for the text and graph rectangles, so they were drawn in the panel's colour (0,0,0,0.6) and darkened the panel a second time instead of drawing white or green. On an AMD Radeon RX 7600 (NAVI33) with Mesa 26.0.8, agc_performance_overlay failed in its SDR case at "'6' of 'FPS: 60.0' glyph pixel 2,0 is not white text: 0x4280161290": 0xff1e140a is the clear colour (191,128,64) darkened by 0.4 twice, where the panel alone gives 0xff4c331a. An integrated Radeon 660M failed at the full panel's first glyph. The shader was valid SPIR-V: a flat uint input indexing a push-constant array (the palette at offset 0, the extent at 48). The index is per rectangle, and fragments of the panel and of its glyphs share waves, so it varies within a wave; RADV read that divergent index into the push-constant array as 0. The shader now reads each entry at a constant index and selects one by the colour index. Every case passes on that RX 7600 with this change, full panel included, and still passes on llvmpipe from the same Mesa. --- .../libSceAgcDriver/Graphics/shaders/PerformanceOverlay.frag | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/shaders/PerformanceOverlay.frag b/core/libs/prx/libSceAgcDriver/Graphics/shaders/PerformanceOverlay.frag index 33f1f9740d..435fb75d5a 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/shaders/PerformanceOverlay.frag +++ b/core/libs/prx/libSceAgcDriver/Graphics/shaders/PerformanceOverlay.frag @@ -8,5 +8,5 @@ layout(location = 0) flat in uint paletteIndex; layout(location = 0) out vec4 color; void main() { - color = parameters.palette[paletteIndex]; + color = paletteIndex == 1u ? parameters.palette[1] : paletteIndex == 2u ? parameters.palette[2] : parameters.palette[0]; } From c020d73a5b3c83ba72be8c5be22bc2df7be545d3 Mon Sep 17 00:00:00 2001 From: fma-rc <188743521+fma-rc@users.noreply.github.com> Date: Wed, 14 Oct 2026 23:52:04 +0200 Subject: [PATCH 0615/1000] fix(libc): catch exceptions that libstdc++ throws inside the prx libraries on Linux The nid_patcher turns each *_nid_no_patch_cut alias into a second dynamic symbol with the same name as the plain alias exported next to it, seven names in all. With those duplicates, LD_DEBUG=bindings shows libstdc++ keeping its own __cxa_allocate_exception, __cxa_throw, __cxa_begin_catch, __cxa_end_catch, __cxa_rethrow, __gxx_personality_v0 and _Unwind_Resume while taking __cxa_init_primary_exception, _Unwind_RaiseException and std::terminate from libc.prx. An exception thrown inside libstdc++ (std::stoi, a stream with exceptions enabled) got a libc header laid over a libstdc++ allocation, reached the libc personality as a foreign exception, matched no typed handler and ended in "terminate called without an active exception". The plain aliases already export these names, so the duplicates go. libstdc++ also describes std::__ios_failure with __iosfail_type_info, a class that privately derives from __si_class_type_info at the same address. Kind now follows such single same-address bases to the __cxxabiv1 class, so catching std::ios_base::failure or std::exception matches a stream failure. The plain aliases came in a4c519b26 next to the cut aliases from 14f8c16ac. --- core/libs/CMakeLists.txt | 11 ++++ core/libs/prx/libc/src/exception/Exports.cpp | 6 -- core/libs/prx/libc/src/exception/Runtime.cpp | 24 ++++++- core/libs/tests/HostLibstdcxxException.cpp | 15 +++++ .../tests/HostLibstdcxxExceptionModule.cpp | 62 +++++++++++++++++++ 5 files changed, 110 insertions(+), 8 deletions(-) create mode 100644 core/libs/tests/HostLibstdcxxException.cpp create mode 100644 core/libs/tests/HostLibstdcxxExceptionModule.cpp diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index c9f54c0bf0..f25d121bd3 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -242,6 +242,17 @@ if(BUILD_TESTING) target_link_options(patched_libraries_tests PRIVATE -static -static-libgcc -static-libstdc++) endif() add_test(NAME patched_libraries COMMAND patched_libraries_tests ${PATCHED_DIR}) + if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + add_library(host_libstdcxx_exception_module SHARED tests/HostLibstdcxxExceptionModule.cpp) + target_link_libraries(host_libstdcxx_exception_module PRIVATE ${PATCHED_DIR}/libc.prx) + add_dependencies(host_libstdcxx_exception_module libs) + add_test_executable(host_libstdcxx_exception_tests tests/HostLibstdcxxException.cpp) + target_link_libraries(host_libstdcxx_exception_tests PRIVATE host_libstdcxx_exception_module ${PATCHED_DIR}/libc.prx) + add_dependencies(host_libstdcxx_exception_tests libs) + set_target_properties(host_libstdcxx_exception_module host_libstdcxx_exception_tests PROPERTIES BUILD_RPATH ${PATCHED_DIR}) + add_test(NAME host_libstdcxx_exception COMMAND host_libstdcxx_exception_tests) + set_tests_properties(host_libstdcxx_exception PROPERTIES TIMEOUT 20) + endif() if(Python3_Interpreter_FOUND) add_test(NAME nid_elf_layout COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/nid/tests/test_elf_layout.py $) diff --git a/core/libs/prx/libc/src/exception/Exports.cpp b/core/libs/prx/libc/src/exception/Exports.cpp index 4f6575b340..231c5e0679 100644 --- a/core/libs/prx/libc/src/exception/Exports.cpp +++ b/core/libs/prx/libc/src/exception/Exports.cpp @@ -13,12 +13,6 @@ extern "C" [[noreturn]] GCC_HIDDEN_FN void APS5_VABI LibcRuntimeAssertion(const char*, int, const char*, const char*) { std::abort(); } GCC_LOCAL_ALIAS(_ZSt21__glibcxx_assert_failPKciS0_S0_, LibcRuntimeAssertion); -GCC_GLOBAL_ALIAS(__cxa_allocate_exception_nid_no_patch_cut, __cxa_allocate_exception_nid_postfix); -GCC_GLOBAL_ALIAS(__cxa_throw_nid_no_patch_cut, __cxa_throw_nid_postfix); -GCC_GLOBAL_ALIAS(__cxa_begin_catch_nid_no_patch_cut, __cxa_begin_catch_nid_postfix); -GCC_GLOBAL_ALIAS(__cxa_end_catch_nid_no_patch_cut, __cxa_end_catch_nid_postfix); -GCC_GLOBAL_ALIAS(__cxa_rethrow_nid_no_patch_cut, __cxa_rethrow_nid_postfix); -GCC_GLOBAL_ALIAS(_Unwind_Resume_nid_no_patch_cut, _Unwind_Resume_nid_postfix); GCC_GLOBAL_ALIAS(__cxa_free_exception, __cxa_free_exception_nid_postfix); GCC_GLOBAL_ALIAS(_ZSt9terminatev, _ZSt9terminatev_nid_postfix); GCC_GLOBAL_ALIAS(__cxa_call_terminate, __cxa_call_terminate_nid_postfix); diff --git a/core/libs/prx/libc/src/exception/Runtime.cpp b/core/libs/prx/libc/src/exception/Runtime.cpp index 0ccea4f8ad..4f96164637 100644 --- a/core/libs/prx/libc/src/exception/Runtime.cpp +++ b/core/libs/prx/libc/src/exception/Runtime.cpp @@ -131,9 +131,29 @@ bool Equal(const std::type_info* a, const std::type_info* b) { return a == b || (a && b && std::strcmp(a->name(), b->name()) == 0); } -const char* Kind(const std::type_info* type) { +static const std::type_info* TypeOfTypeInfo(const std::type_info* type) { auto table = *reinterpret_cast(type); - return table[-1]->name(); + return table[-1]; +} + +static const std::type_info* BaseAtSameAddress(const std::type_info* type) { + const char* kind = TypeOfTypeInfo(type)->name(); + if (std::strcmp(kind, "N10__cxxabiv120__si_class_type_infoE") == 0) + return static_cast(type)->__base_type; + if (std::strcmp(kind, "N10__cxxabiv121__vmi_class_type_infoE") != 0) return nullptr; + auto* vmi = static_cast(type); + if (vmi->__base_count != 1 || (vmi->__base_info[0].__offset_flags & ~2L) != 0) return nullptr; + return vmi->__base_info[0].__base_type; +} + +const char* Kind(const std::type_info* type) { + const std::type_info* kind = TypeOfTypeInfo(type); + for (unsigned depth = 0; depth < 16 && std::strncmp(kind->name(), "N10__cxxabiv1", 13) != 0; ++depth) { + const std::type_info* base = BaseAtSameAddress(kind); + if (!base) break; + kind = base; + } + return kind->name(); } struct Search { diff --git a/core/libs/tests/HostLibstdcxxException.cpp b/core/libs/tests/HostLibstdcxxException.cpp new file mode 100644 index 0000000000..192682cbe8 --- /dev/null +++ b/core/libs/tests/HostLibstdcxxException.cpp @@ -0,0 +1,15 @@ +#include +#include +#include + +extern "C" const char* HostLibstdcxxExceptionFailure(const char* path); + +int main() { + const std::string path = "/proc/" + std::to_string(getpid()) + "/missing/file"; + if (const char* failure = HostLibstdcxxExceptionFailure(path.c_str())) { + std::fprintf(stderr, "%s\n", failure); + return 1; + } + std::puts("host libstdc++ exceptions caught"); + return 0; +} diff --git a/core/libs/tests/HostLibstdcxxExceptionModule.cpp b/core/libs/tests/HostLibstdcxxExceptionModule.cpp new file mode 100644 index 0000000000..aeb11b5a9d --- /dev/null +++ b/core/libs/tests/HostLibstdcxxExceptionModule.cpp @@ -0,0 +1,62 @@ +#include +#include +#include +#include +#include + +static bool CatchesConversion() { + try { + (void)std::stoi("x"); + } catch (const std::invalid_argument& error) { + return std::string(error.what()) == "stoi"; + } + return false; +} + +static bool CatchesStreamFailure(const char* path) { + try { + std::ofstream file; + file.exceptions(std::ios::failbit); + file.open(path); + } catch (const std::ios_base::failure&) { + return true; + } + return false; +} + +static bool CatchesStreamFailureAsException(const char* path) { + try { + std::ofstream file; + file.exceptions(std::ios::failbit); + file.open(path); + } catch (const std::exception&) { + return true; + } + return false; +} + +static bool RethrowsStoredStreamFailure(const char* path) { + std::exception_ptr failure; + try { + std::ofstream file; + file.exceptions(std::ios::failbit); + file.open(path); + } catch (...) { + failure = std::current_exception(); + } + if (!failure) return false; + try { + std::rethrow_exception(failure); + } catch (const std::ios_base::failure&) { + return true; + } + return false; +} + +extern "C" const char* HostLibstdcxxExceptionFailure(const char* path) { + if (!CatchesConversion()) return "std::stoi failure was not caught as std::invalid_argument"; + if (!CatchesStreamFailure(path)) return "std::ofstream::open failure was not caught as std::ios_base::failure"; + if (!CatchesStreamFailureAsException(path)) return "std::ofstream::open failure was not caught as std::exception"; + if (!RethrowsStoredStreamFailure(path)) return "a stored std::ofstream::open failure was not rethrown"; + return nullptr; +} From 6dbbf4d7d9bb27f6f5cbbed9e24934b5d8ec5452 Mon Sep 17 00:00:00 2001 From: Mouad-Scriptz-0 Date: Sun, 11 Oct 2026 21:41:54 +0000 Subject: [PATCH 0616/1000] perf(recompiler): select guest memory loads instead of branching around them LDS, scratch and direct storage-buffer loads load element 0 when the lane is inactive or the address is out of bounds and select 0 as the result, instead of opening a selection per guard. Stores, atomics, formatted and physical-address accesses keep their branches. A branch guard cost four blocks per lane half, and the host compiler's time grows with the block count: PPSA11142's merged PSP vertex program emitted 16,983 blocks and 1,710 conditional branches, 5,907 and 868 with the selects. Co-Authored-By: Claude Fable 5.1 --- .../SpirvBackend/SpirvEmitterHelpers.hpp | 1 + .../src/SpirvMemory/SpirvBufferAccess.cpp | 4 ++ .../SpirvMemory/SpirvMemoryInstructions.cpp | 55 +++++++++---------- .../SpirvBackend/src/SpirvRuntimeBuffer.cpp | 43 +++++++-------- docs/dev/debt/shader.md | 1 + 5 files changed, 52 insertions(+), 52 deletions(-) diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterHelpers.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterHelpers.hpp index dcd4b95b44..cfd8205030 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterHelpers.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterHelpers.hpp @@ -117,6 +117,7 @@ std::uint32_t EmitMemoryElementIndex(SpirvEmitterState& state, const MemoryResou std::uint32_t EmitMemoryElementInBounds(SpirvEmitterState& state, const MemoryResourceAccess& access, std::uint32_t index); bool InFunctionStorage(const SpirvEmitterState& state, ResourceKind kind); std::uint32_t EmitMemoryElementPointer(SpirvEmitterState& state, const MemoryResourceAccess& access, std::uint32_t index); +std::uint32_t EmitValueOrZero(SpirvEmitterState& state, std::uint32_t condition, std::uint32_t value); std::uint32_t EmitStorageBufferElementPointer(SpirvEmitterState& state, const MemoryResourceAccess& access, std::uint32_t index, std::uint32_t pointerType); std::uint32_t EmitTBufferBitcastU32ToI32(SpirvEmitterState& state, std::uint32_t value); bool IsSignedFormatComponent(SpirvFormatComponentType type); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvBufferAccess.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvBufferAccess.cpp index 9312b24e49..c8716c472a 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvBufferAccess.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvBufferAccess.cpp @@ -173,6 +173,10 @@ bool InFunctionStorage(const SpirvEmitterState& state, ResourceKind kind) { return kind == ResourceKind::Lds && !LdsInDeviceMemory(state) && ShaderWorkgroupInput(state) == nullptr; } +std::uint32_t EmitValueOrZero(SpirvEmitterState& state, std::uint32_t condition, std::uint32_t value) { + return Select(state, TypeU32(state), condition, value, ConstantU32(state, 0u)); +} + std::uint32_t EmitMemoryElementPointer(SpirvEmitterState& state, const MemoryResourceAccess& access, std::uint32_t index) { if (access.kind == ResourceKind::Lds && LdsInDeviceMemory(state)) { return EmitStorageBufferElementPointer(state, access, EmitAddU32(state, state.ldsBufferBase, index), TypeStorageBufferElementPointer(state)); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp index e152d36092..f8bd930b18 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp @@ -134,36 +134,33 @@ std::uint32_t LoadSubwordInBounds(SpirvValueEmitContext& ctx, const MemoryResour return Binary(state, spv::OpShiftRightArithmetic, TypeU32(state), left, ConstantU32(state, 32u - bits)); } -std::uint32_t LoadWordPrepared(SpirvValueEmitContext& ctx, const IrValue& inst, const MemoryInfo& mem, const MemoryResourceAccess& resource) { +std::uint32_t LoadEnable(SpirvEmitterState& state, const MemoryResourceAccess& resource, std::uint32_t index, std::uint32_t active) { + const auto inBounds = EmitMemoryElementInBounds(state, resource, index); + return active == 0u ? inBounds : Binary(state, spv::OpLogicalAnd, TypeBool(state), active, inBounds); +} + +std::uint32_t LoadWordPrepared(SpirvValueEmitContext& ctx, const IrValue& inst, const MemoryInfo& mem, const MemoryResourceAccess& resource, std::uint32_t active) { auto& state = ctx.state; const auto index = EmitMemoryElementIndex(state, resource, DwordIndex(ctx, inst, mem)); - return EmitValueOrZeroIfCondition(state, EmitMemoryElementInBounds(state, resource, index), [&]() { - return LoadWordInBounds(ctx, resource, index); - }); + const auto enable = LoadEnable(state, resource, index, active); + return EmitValueOrZero(state, enable, LoadWordInBounds(ctx, resource, EmitValueOrZero(state, enable, index))); } std::uint32_t LoadWord(SpirvValueEmitContext& ctx, const IrValue& inst, const MemoryInfo& mem) { - return EmitValueOrZeroIfCondition(ctx.state, ActiveArgument(ctx, inst), [&]() { - const auto resource = PrepareMemoryResourceAccess(ctx.state, mem); - return LoadWordPrepared(ctx, inst, mem, resource); - }); + return LoadWordPrepared(ctx, inst, mem, PrepareMemoryResourceAccess(ctx.state, mem), ActiveArgument(ctx, inst)); } -std::uint32_t LoadSubwordPrepared(SpirvValueEmitContext& ctx, const IrValue& inst, const MemoryInfo& mem, const MemoryResourceAccess& resource, std::uint32_t bits, bool signExtend) { +std::uint32_t LoadSubwordPrepared(SpirvValueEmitContext& ctx, const IrValue& inst, const MemoryInfo& mem, const MemoryResourceAccess& resource, std::uint32_t bits, bool signExtend, std::uint32_t active) { auto& state = ctx.state; const auto address = ByteAddress(ctx, inst, mem); const auto rawIndex = Binary(state, spv::OpShiftRightLogical, TypeU32(state), address, ConstantU32(state, 2u)); const auto index = EmitMemoryElementIndex(state, resource, rawIndex); - return EmitValueOrZeroIfCondition(state, EmitMemoryElementInBounds(state, resource, index), [&]() { - return LoadSubwordInBounds(ctx, resource, address, index, bits, signExtend); - }); + const auto enable = LoadEnable(state, resource, index, active); + return EmitValueOrZero(state, enable, LoadSubwordInBounds(ctx, resource, address, EmitValueOrZero(state, enable, index), bits, signExtend)); } std::uint32_t LoadSubword(SpirvValueEmitContext& ctx, const IrValue& inst, const MemoryInfo& mem, std::uint32_t bits) { - return EmitValueOrZeroIfCondition(ctx.state, ActiveArgument(ctx, inst), [&]() { - const auto resource = PrepareMemoryResourceAccess(ctx.state, mem); - return LoadSubwordPrepared(ctx, inst, mem, resource, bits, false); - }); + return LoadSubwordPrepared(ctx, inst, mem, PrepareMemoryResourceAccess(ctx.state, mem), bits, false, ActiveArgument(ctx, inst)); } std::uint32_t ConstantDeviceAddress(SpirvEmitterState& state, std::uint64_t value) { @@ -368,20 +365,18 @@ std::uint32_t ConstructU32Composite(SpirvEmitterState& state, std::uint32_t comp std::uint32_t LoadWideShared(SpirvValueEmitContext& ctx, const IrValue& inst, const MemoryInfo& mem, std::uint32_t components) { auto& state = ctx.state; - return EmitValueOrDefaultIfCondition(state, ActiveArgument(ctx, inst), TypeU32Composite(state, components), ConstantU32CompositeZero(state, components), [&]() { - const auto resource = PrepareMemoryResourceAccess(state, mem); - const auto base = ByteAddress(ctx, inst, mem); - std::array values{}; - for (std::uint32_t component = 0; component < components; component++) { - const auto address = component == 0u ? base : Binary(state, spv::OpIAdd, TypeU32(state), base, ConstantU32(state, component * 4u)); - const auto rawIndex = Binary(state, spv::OpShiftRightLogical, TypeU32(state), address, ConstantU32(state, 2u)); - const auto index = EmitMemoryElementIndex(state, resource, rawIndex); - values.at(component) = EmitValueOrZeroIfCondition(state, EmitMemoryElementInBounds(state, resource, index), [&]() { - return LoadWordInBounds(ctx, resource, index); - }); - } - return ConstructU32Composite(state, components, values); - }); + const auto active = ActiveArgument(ctx, inst); + const auto resource = PrepareMemoryResourceAccess(state, mem); + const auto base = ByteAddress(ctx, inst, mem); + std::array values{}; + for (std::uint32_t component = 0; component < components; component++) { + const auto address = component == 0u ? base : Binary(state, spv::OpIAdd, TypeU32(state), base, ConstantU32(state, component * 4u)); + const auto rawIndex = Binary(state, spv::OpShiftRightLogical, TypeU32(state), address, ConstantU32(state, 2u)); + const auto index = EmitMemoryElementIndex(state, resource, rawIndex); + const auto enable = LoadEnable(state, resource, index, active); + values.at(component) = EmitValueOrZero(state, enable, LoadWordInBounds(ctx, resource, EmitValueOrZero(state, enable, index))); + } + return ConstructU32Composite(state, components, values); } void StoreWideShared(SpirvValueEmitContext& ctx, const IrValue& inst, const MemoryInfo& mem, std::uint32_t components) { diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp index ec559b5340..f31fa225a0 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp @@ -169,26 +169,24 @@ std::uint32_t readBuffer(SpirvValueEmitContext& context, const IrValue& instruct const auto unaligned = nonzero(state, misalignment); const auto next = EmitAddU32(state, index, ConstantU32(state, 1u)); const auto last = Select(state, TypeU32(state), unaligned, next, index); - return EmitValueOrZeroIfCondition(state, EmitMemoryElementInBounds(state, access, last), [&] { - const auto load = [&](std::uint32_t word) { - const auto value = state.module.AllocateId(); - const auto pointer = EmitMemoryElementPointer(state, access, word); - if (access.memoryAccess != 0u) state.module.AddFunction(spv::OpLoad, TypeU32(state), value, pointer, access.memoryAccess); - else state.module.AddFunction(spv::OpLoad, TypeU32(state), value, pointer); - return value; - }; - const auto shift = Binary(state, spv::OpShiftLeftLogical, TypeU32(state), misalignment, ConstantU32(state, 3u)); - const auto low = Binary(state, spv::OpShiftRightLogical, TypeU32(state), load(index), shift); - const auto high = EmitValueOrZeroIfCondition(state, unaligned, [&] { - return Binary(state, spv::OpShiftLeftLogical, TypeU32(state), load(next), Binary(state, spv::OpISub, TypeU32(state), ConstantU32(state, 32u), shift)); - }); - auto value = EmitOrU32(state, low, high); - if (bits != 32u) { - const auto subwordShift = Binary(state, spv::OpShiftLeftLogical, TypeU32(state), EmitAndConstant(state, address, 3u), ConstantU32(state, 3u)); - value = Binary(state, spv::OpShiftRightLogical, TypeU32(state), value, subwordShift); - } - return EmitAndConstant(state, value, bits == 32u ? 0xffffffffu : (1u << bits) - 1u); - }); + const auto inBounds = EmitMemoryElementInBounds(state, access, last); + const auto load = [&](std::uint32_t word) { + const auto value = state.module.AllocateId(); + const auto pointer = EmitMemoryElementPointer(state, access, word); + if (access.memoryAccess != 0u) state.module.AddFunction(spv::OpLoad, TypeU32(state), value, pointer, access.memoryAccess); + else state.module.AddFunction(spv::OpLoad, TypeU32(state), value, pointer); + return value; + }; + const auto shift = Binary(state, spv::OpShiftLeftLogical, TypeU32(state), misalignment, ConstantU32(state, 3u)); + const auto low = Binary(state, spv::OpShiftRightLogical, TypeU32(state), load(EmitValueOrZero(state, inBounds, index)), shift); + const auto highWord = load(EmitValueOrZero(state, both(state, inBounds, unaligned), next)); + const auto high = EmitValueOrZero(state, unaligned, Binary(state, spv::OpShiftLeftLogical, TypeU32(state), highWord, Binary(state, spv::OpISub, TypeU32(state), ConstantU32(state, 32u), shift))); + auto value = EmitOrU32(state, low, high); + if (bits != 32u) { + const auto subwordShift = Binary(state, spv::OpShiftLeftLogical, TypeU32(state), EmitAndConstant(state, address, 3u), ConstantU32(state, 3u)); + value = Binary(state, spv::OpShiftRightLogical, TypeU32(state), value, subwordShift); + } + return EmitValueOrZero(state, inBounds, EmitAndConstant(state, value, bits == 32u ? 0xffffffffu : (1u << bits) - 1u)); } std::uint32_t byteBuffers(SpirvEmitterState& state) { @@ -726,7 +724,8 @@ std::uint32_t EmitRuntimeBufferLoad(SpirvValueEmitContext& context, const IrValu std::array values{}; for (std::uint32_t component = 0; component < components; ++component) { const auto access = address(context, instruction, resource, component * 4u, memory.dataBits / 8u); - values[component] = EmitValueOrZeroIfCondition(state, access.inBounds, [&] { return readBuffer(context, instruction, access.guest, memory.dataBits); }); + values[component] = memory.gpuDescriptor ? EmitValueOrZeroIfCondition(state, access.inBounds, [&] { return readBuffer(context, instruction, access.guest, memory.dataBits); }) + : EmitValueOrZero(state, access.inBounds, readBuffer(context, instruction, access.guest, memory.dataBits)); } return composite(state, components, values); }; @@ -776,7 +775,7 @@ std::uint32_t EmitRuntimeScalarBufferLoad(SpirvValueEmitContext& context, const const auto byte = EmitAndConstant(state, address, ~3u); const auto offset = Unary(state, spv::OpUConvert, u64, byte); const auto inBounds = both(state, noCarry, both(state, resource.valid, Binary(state, spv::OpULessThanEqual, TypeBool(state), Binary(state, spv::OpIAdd, u64, offset, BdaConstant(state, 4u)), size))); - if (!context.Memory(instruction).gpuDescriptor) return EmitValueOrZeroIfCondition(state, inBounds, [&] { return readBuffer(context, instruction, byte, 32u); }); + if (!context.Memory(instruction).gpuDescriptor) return EmitValueOrZero(state, inBounds, readBuffer(context, instruction, byte, 32u)); return EmitValueOrZeroIfCondition(state, inBounds, [&] { return readBuffer(context, instruction, AddBdaAddress(context, instruction, resource.base, offset, false), 32u); }); } diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index ec051f71ed..427301445d 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -147,3 +147,4 @@ - [Flag structurization](../../../core/shader/recompiler/ControlFlow/src/Structurizer.cpp) (shader recompiler) - a graph that every structurizer attempt refuses, irreducible and non-terminating graphs included, is rebuilt with one reach flag per block and one again flag per loop (`structurizeWithFlags`): each block keeps one copy and runs behind a guard on its flag, and each strongly connected component becomes a loop whose single latch repeats it while a back edge set its flag. The output has about 8 times the input's blocks and one route variable per block, and ends in an empty Return that never runs. Only blocks that start the top-level order unconditionally run unguarded, so resource planning also sees the static paths that skip a guarded block and can plan extra descriptor candidates for them. The attempts still run first. Shared merge splitting refuses an attempt once the graph has grown past 16 times its block count at the start of splitting (at least 64 blocks); succeeding attempts grew at most 10.6 times on random graphs of up to 128 blocks, and an attempt that would succeed only beyond the budget leaves the graph to a later attempt or to this construction. Splitting that adds one block per round stays within that budget until its 4 rounds per block run out, so the attempts can still take more than 10 s (9 of 2,000 random graphs of up to 128 blocks, 63 s at most). `APS5_FORCE_STRUCTURIZER_FALLBACK=1` builds every graph this way, for tests - [Scalar buffer descriptors](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) use only the base address, stride and record count for resources accessed exclusively by scalar loads; their fourth word is normalized to zero, following AMD RDNA2 ISA section 7.2.2. Vector and mixed accesses retain descriptor validation. Native Vulkan readbacks cover ignored fourth-word fields on Windows/NVIDIA; their console behavior has not been measured. - [Spanned wave64 compute](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvSubgroup.cpp) (shader recompiler) - `APS5_SPANNED_WAVE=1`, off by default, runs a single-wave wave64 compute program at one lane per invocation on a 32-lane host subgroup and exchanges ballots, shuffles and reductions between the two host subgroups through workgroup memory and barriers. The driver turns it on only where compute subgroups can be pinned at 32 full lanes (`subgroupSizeControl` and `computeFullSubgroups`), passes it as `SpirvTarget::spannedWaves` (part of the cache key and the serialized request), and creates the pipelines of spanned programs with `REQUIRE_FULL_SUBGROUPS`, since the lane number `SubgroupId * 32 + SubgroupLocalInvocationId` needs two full subgroups. Those barriers are correct only where every invocation of the workgroup reaches them, which holds for the guest's wave-uniform control flow. The emulated 64-bit LDS atomics ([LockedLdsUpdate](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp)) run only on the lanes the exec mask keeps, so their lock loop ballots within its own host subgroup, as its elect already does. Any other cross-lane exchange emitted inside per-lane control flow would hang a spanned wave in the same way, and the emitter does not reject one: there is no uniformity check. The only check so far is manual: disassemble the module (`spirv-dis`), list the selection and loop constructs that enclose each `OpControlBarrier`, and read the condition of each enclosing selection. In Ghost of Yōtei's program 0x80003af600 (PPSA26344), 108 of 111 barriers sat only in loops on wave-uniform conditions and 3 inside per-invocation conditionals, all in the lock loop. Other programs and titles have not been scanned. An earlier, cheaper ballot (one barrier instead of three) also lost the device for a reason that is not understood, which may be another barrier placement problem. Measured on an RTX 4090 (Windows): an offline replay of that program and two Yōtei runs that previously lost the device (NVIDIA TDR) now run past it, and [LdsMaskWaterfall.cpp](../../../core/libs/prx/libSceAgcDriver/tests/execution/LdsMaskWaterfall.cpp) runs both modes and covers a half-inactive wave. Stray (PPSA02101) on Intel ANV and a Demon's Souls (PPSA01342) request replay showed no fault, before this fix +- [Guest memory load guards](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (shader recompiler) - LDS, scratch and direct storage-buffer loads (`LoadWord`, `LoadSubword`, `LoadWideShared`, `readBuffer` and `EmitRuntimeScalarBufferLoad` in [SpirvRuntimeBuffer.cpp](../../../core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp)) load element 0 when the lane is inactive or the address is out of bounds and select 0 as the result, instead of branching around the load; stores, atomics, formatted and physical-address (BDA) accesses still branch. A branch guard costs four blocks per lane half and the host compiler's time grows with the block count: PPSA11142's merged PSP vertex program (3176 ISA words, wave64) emitted 16,983 blocks and 1,710 conditional branches, 5,907 and 868 with the selects. The unconditional load depends on element 0 being addressable: SPIR-V function and workgroup arrays have at least one element, and a storage buffer with no elements reads out of bounds, which is defined only with `robustBufferAccess` ([VulkanDevice.cpp](../../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) enables it when the device has it). From ccaeeed416823445dba15212eee397662ac1e304 Mon Sep 17 00:00:00 2001 From: Mouad-Scriptz-0 Date: Sun, 11 Oct 2026 23:11:57 +0000 Subject: [PATCH 0617/1000] fix(recompiler): point selected guest loads at locations nothing writes A load selected instead of branched still accesses memory, so its fallback element must be one no write can race with: scratch keeps element 0, LDS in workgroup or function memory reads a sentinel dword appended after the program's LDS and lock, and storage buffers read element 0xfffffffe, which robustBufferAccess2 turns into a zero load without a memory access. The device enables robustBufferAccess2 when it has it and the target carries the flag; without it, and for LDS in device memory, buffer loads keep their selections. The aligned high word of a buffer read re-reads the low word's element instead of word 0. agc_guarded_memory_loads traces the emitted loads in both device modes. Co-Authored-By: Claude Fable 5.1 --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 14 + .../Execution/src/VulkanDevice.cpp | 9 +- .../tests/GuardedMemoryLoads.cpp | 375 ++++++++++++++++++ .../libSceAgcDriver/tests/ShaderDiskCache.cpp | 1 + .../libSceAgcDriver/tools/ShaderReplay.cpp | 8 +- core/shader/recompiler/CacheKey.hpp | 1 + core/shader/recompiler/Recompiler.cpp | 1 + core/shader/recompiler/Recompiler.hpp | 1 + .../include/SpirvBackend/SpirvEmitter.hpp | 1 + .../SpirvBackend/SpirvEmitterHelpers.hpp | 3 + .../SpirvBackend/SpirvEmitterState.hpp | 1 + .../SpirvBackend/src/SpirvEmitter.cpp | 1 + .../src/SpirvMemory/SpirvBufferAccess.cpp | 17 +- .../SpirvMemory/SpirvMemoryInstructions.cpp | 9 +- .../SpirvBackend/src/SpirvModuleEmitter.cpp | 2 +- .../SpirvBackend/src/SpirvRuntimeBuffer.cpp | 37 +- docs/dev/debt/shader.md | 1 + 17 files changed, 462 insertions(+), 20 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/GuardedMemoryLoads.cpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index ada29c686f..3eed40db9f 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -498,6 +498,20 @@ target_link_libraries(agc_vertex_input_tests PRIVATE shader_recompiler_agc_drive set_target_properties(agc_vertex_input_tests PROPERTIES CXX_EXTENSIONS OFF) configure_windows_unwind(agc_vertex_input_tests) +add_test_executable(agc_guarded_memory_loads_tests EXCLUDE_FROM_ALL tests/GuardedMemoryLoads.cpp) +target_link_libraries(agc_guarded_memory_loads_tests PRIVATE shader_recompiler_agc_driver libc) +set_target_properties(agc_guarded_memory_loads_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_guarded_memory_loads_tests) +if(BUILD_TESTING) + set_target_properties(agc_guarded_memory_loads_tests PROPERTIES EXCLUDE_FROM_ALL OFF) + add_test(NAME agc_guarded_memory_loads COMMAND agc_guarded_memory_loads_tests) + set_tests_properties(agc_guarded_memory_loads PROPERTIES TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + if(WIN32) + set_tests_properties(agc_guarded_memory_loads PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() + add_test_executable(agc_shared_write_tests EXCLUDE_FROM_ALL tests/SharedWrites.cpp) target_include_directories(agc_shared_write_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(agc_shared_write_tests PRIVATE libSceAgcDriver libkernel libc) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 44c1dce86b..95dbfcfced 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -301,6 +301,7 @@ struct VulkanDevice::State { bool float16DenormPreserve = false; bool demoteToHelperInvocation = false; bool earlyFragmentSampleMaskTestBeforeSampleCounting = false; + bool robustBufferAccess2 = false; // VK_EXT_descriptor_indexing with non-uniform image array indexing (bindless image tables in // graphics stages, and compute workgroups wider than a wave). bool descriptorIndexing = false; @@ -1832,9 +1833,12 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : VkPhysicalDeviceFeatures2 queried{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2, &robustness2}; state->InstanceFunction("vkGetPhysicalDeviceFeatures2")(selected, &queried); } - if (robustness2.nullDescriptor == VK_TRUE) { + state->robustBufferAccess2 = robustness2.robustBufferAccess2 == VK_TRUE && enabled.robustBufferAccess == VK_TRUE; + if (robustness2.nullDescriptor == VK_TRUE || state->robustBufferAccess2) { + const bool nullDescriptor = robustness2.nullDescriptor == VK_TRUE; robustness2 = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ROBUSTNESS_2_FEATURES_EXT}; - robustness2.nullDescriptor = VK_TRUE; + robustness2.nullDescriptor = nullDescriptor ? VK_TRUE : VK_FALSE; + robustness2.robustBufferAccess2 = state->robustBufferAccess2 ? VK_TRUE : VK_FALSE; robustness2.pNext = bdaFeatures.pNext; bdaFeatures.pNext = &robustness2; deviceExtensions.push_back(VK_EXT_ROBUSTNESS_2_EXTENSION_NAME); @@ -3489,6 +3493,7 @@ ShaderRecompiler::SpirvTarget VulkanDevice::buildTarget() const { target.spannedWaves = state->spannedWaves; target.fixedPushSlots = state->graphicsPipelineLibrary; target.registrationModules = state->graphicsPipelineLibrary; + target.robustBufferAccess2 = state->robustBufferAccess2; target.srgbDecodeFormats = state->srgbDecodeFormats; target.vertexSubgroups = (state->subgroup.supportedStages & VK_SHADER_STAGE_VERTEX_BIT) != 0; if (state->meshShader) { diff --git a/core/libs/prx/libSceAgcDriver/tests/GuardedMemoryLoads.cpp b/core/libs/prx/libSceAgcDriver/tests/GuardedMemoryLoads.cpp new file mode 100644 index 0000000000..241430786c --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/GuardedMemoryLoads.cpp @@ -0,0 +1,375 @@ +#define SPV_ENABLE_UTILITY_CODE +#include +#undef SPV_ENABLE_UTILITY_CODE +#include "IntermediateRepresentation/IrProgram.hpp" +#include "Optimization/ShaderStageInputInfo.hpp" +#include "SpirvBackend/SpirvEmitter.hpp" +#include "PipelineSpecialization.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using namespace ShaderRecompiler; + +void require(bool condition, const std::string& message) { + if (!condition) throw std::runtime_error(message); +} + +struct LoadCase { + ResourceKind kind; + IrOpcode opcode; + IrType type; + std::uint32_t bits = 32; + std::uint32_t components = 1; +}; + +std::vector emitLoad(const LoadCase& test, bool active, std::uint32_t offset, std::uint32_t records, bool robust) { + IrProgram program; + program.SetWaveSize(32); + program.Resources().stage = IrShaderStage::Compute; + program.Resources().srtPlanComplete = true; + program.Resources().resourceTrackingComplete = true; + program.Metadata().shaderInfoComplete = true; + program.Metadata().bindingLayoutComplete = true; + auto& block = program.CreateBlock(); + program.SetEntryBlock(block); + program.BlockOrder().push_back(&block); + program.Metadata().blockInfo.emplace_back(); + const auto constant = [&](std::uint32_t number) -> IrValue& { + auto& value = program.CreateValue(IrOpcode::Void, IrType::U32); + value.SetImmediateU32(number); + return value; + }; + const auto emit = [&](IrOpcode opcode, IrType type, std::initializer_list arguments) -> IrValue& { + auto& value = program.CreateValue(opcode, type); + for (auto* argument : arguments) value.AddArgument(argument); + block.AppendInstruction(&value); + return value; + }; + auto& enabled = program.CreateValue(IrOpcode::Void, IrType::Bool); + enabled.SetImmediateBool(active); + auto& zero = constant(0); + auto& address = constant(offset); + MemoryInfo memory; + memory.kind = test.kind; + memory.dataBits = test.bits; + memory.dataDwords = test.components; + memory.componentCount = test.components; + program.Resources().memoryInfo.push_back(memory); + if (test.kind == ResourceKind::Scratch) { + program.Info().scratchDwords = 16; + auto& handle = emit(IrOpcode::GetScratchResource, IrType::AddressResource, {}); + emit(test.opcode, test.type, {&handle, &address, &zero, &enabled}); + } else if (test.kind == ResourceKind::Buffer) { + program.Resources().memoryInfo.front().coherent = true; + program.Info().buffers.emplace_back().read = true; + program.Metadata().bindings.descriptors.push_back({DescriptorBindingKind::Buffers, {0}}); + program.Metadata().bindings.pushDataStartDword = 0; + program.Metadata().bindings.memoryOffsetCount = 1; + auto& size = constant(records); + auto& handle = emit(IrOpcode::GetBufferResource, IrType::BufferResource, {&zero, &zero, &size, &zero}); + emit(test.opcode, test.type, {&handle, &zero, &address, &zero, &enabled}); + } else { + if (test.kind == ResourceKind::Gds) program.Metadata().bindings.descriptors.push_back({DescriptorBindingKind::Gds, {0}}); + emit(test.opcode, test.type, {&address, &enabled}); + } + ShaderComputeInputInfo compute; + compute.threadsNum[0] = 32; + compute.threadsNum[1] = 1; + compute.threadsNum[2] = 1; + compute.ldsSizeDwords = 16; + compute.scratchSizeDwords = 16; + compute.hostSubgroupSize = 32; + compute.waveSize = 32; + ShaderStageInputInfo inputs; + inputs.compute = &compute; + CompiledBindingLayout bindings; + bindings.layout = program.Metadata().bindings; + SpirvTargetOptions target{}; + target.vulkanVersion = 0x00401000; + target.spirvVersion = 0x00010300; + target.subgroupSize = 32; + target.robustBufferAccess2 = robust; + return SpirvEmitter().Emit(program, inputs, bindings, target); +} + +class AccessTrace { +public: + AccessTrace(const std::vector& module, std::uint32_t misalignment = 0, bool valid = true) + : words(module), misalignment(misalignment), valid(valid) { + require(words.size() >= 5, "SPIR-V module header is missing"); + for (std::size_t offset = 5; offset < words.size();) { + const auto count = words[offset] >> 16; + require(count != 0 && count <= words.size() - offset, "SPIR-V instruction is truncated"); + const auto instruction = std::span(words).subspan(offset, count); + const auto opcode = static_cast(instruction[0] & 0xffff); + bool result = false; + bool type = false; + spv::HasResultAndType(opcode, &result, &type); + if (result) definitions.emplace(instruction[type ? 2 : 1], instruction); + if (opcode == spv::OpName) names.emplace(instruction[1], std::string(reinterpret_cast(instruction.data() + 2))); + if (opcode == spv::OpDecorate && instruction[2] == spv::DecorationSpecId) specializations.emplace(instruction[1], instruction[3]); + if (opcode == spv::OpLabel) labels.emplace(instruction[1], offset); + if (opcode == spv::OpEntryPoint) entryFunction = instruction[2]; + offset += count; + } + } + + std::vector Run() { + std::size_t cursor = 0; + bool entry = false; + for (std::size_t offset = 5; offset < words.size(); offset += words[offset] >> 16) { + const auto opcode = static_cast(words[offset] & 0xffff); + if (opcode == spv::OpFunction) entry = words[offset + 2] == entryFunction; + if (entry && opcode == spv::OpLabel) { cursor = offset; break; } + } + require(cursor != 0, "SPIR-V entry block is missing"); + std::vector reads; + std::uint32_t previous = 0; + std::uint32_t current = 0; + for (std::size_t steps = 0; steps < 4096; ++steps) { + const auto instruction = std::span(words).subspan(cursor, words[cursor] >> 16); + const auto opcode = static_cast(instruction[0] & 0xffff); + if (opcode == spv::OpLabel) current = instruction[1]; + if (opcode == spv::OpPhi) { + bool found = false; + for (std::size_t index = 3; index + 1 < instruction.size(); index += 2) { + if (instruction[index + 1] == previous) { phiValues[instruction[2]] = instruction[index]; found = true; } + } + require(found, "SPIR-V phi has no incoming edge for the executed path"); + } + if (opcode == spv::OpLoad && isGuestPointer(instruction[3])) { + const auto pointer = definition(instruction[3]); + require((pointer[0] & 0xffff) == spv::OpAccessChain, "guest read is not an indexed access"); + reads.push_back(value(pointer.back())); + if (rootName(instruction[3]) == "scratch_dwords") require(pointerStorage(instruction[3]) == spv::StorageClassFunction, "scratch selection accessed non-private storage"); + if (rootName(instruction[3]).starts_with("buffers")) require(instruction.size() == 5 && (instruction[4] & spv::MemoryAccessVolatileMask), "coherent buffer read lost its volatile access"); + } + if (opcode == spv::OpBranch || opcode == spv::OpBranchConditional) { + const auto destination = opcode == spv::OpBranch ? instruction[1] : instruction[value(instruction[1]) ? 2 : 3]; + previous = current; + cursor = labels.at(destination); + continue; + } + if (opcode == spv::OpReturn) return reads; + require(opcode != spv::OpFunctionCall && opcode != spv::OpSwitch && opcode != spv::OpLoopMerge, "access fixture unexpectedly requires a call, switch or loop"); + cursor += instruction.size(); + require(cursor < words.size(), "SPIR-V execution fell outside the function"); + } + throw std::runtime_error("SPIR-V access trace exceeded its bounded instruction budget"); + } + +private: + std::span definition(std::uint32_t id) const { + const auto found = definitions.find(id); + require(found != definitions.end(), "SPIR-V access condition refers to an undefined value"); + return found->second; + } + + std::string rootName(std::uint32_t id) const { + const auto instruction = definition(id); + if ((instruction[0] & 0xffff) == spv::OpAccessChain) return rootName(instruction[3]); + const auto found = names.find(id); + return found == names.end() ? std::string() : found->second; + } + + std::uint32_t pointerStorage(std::uint32_t id) const { + const auto instruction = definition(id); + if ((instruction[0] & 0xffff) == spv::OpAccessChain) return pointerStorage(instruction[3]); + require((instruction[0] & 0xffff) == spv::OpVariable, "access pointer has no variable root"); + return instruction[3]; + } + + bool isGuestPointer(std::uint32_t id) const { + const auto name = rootName(id); + return name == "lds_dwords" || name == "gds" || name == "scratch_dwords" || name.starts_with("buffers"); + } + + std::uint32_t value(std::uint32_t id) const { + const auto instruction = definition(id); + const auto opcode = static_cast(instruction[0] & 0xffff); + const auto argument = [&](std::size_t index) { return value(instruction[index]); }; + switch (opcode) { + case spv::OpConstantTrue: return 1; + case spv::OpConstantFalse: case spv::OpConstantNull: return 0; + case spv::OpConstant: return instruction[3]; + case spv::OpSpecConstant: { + const auto spec = specializations.at(id); + if (spec == PipelineSpecialization::BufferBase) return 0; + if (spec == PipelineSpecialization::BufferBase + 1) return 0x30001000; + if (spec == PipelineSpecialization::BufferBase + 2) return valid; + if (spec == PipelineSpecialization::BufferBase + 3) return misalignment; + if (spec == PipelineSpecialization::PushDataOffset) return 0; + throw std::runtime_error("unexpected specialization in access fixture"); + } + case spv::OpSelect: return argument(3) ? argument(4) : argument(5); + case spv::OpPhi: return value(phiValues.at(id)); + case spv::OpIAdd: return argument(3) + argument(4); + case spv::OpISub: return argument(3) - argument(4); + case spv::OpIMul: return argument(3) * argument(4); + case spv::OpShiftLeftLogical: + require(argument(4) < 32, "undefined shift in access condition"); + return argument(3) << argument(4); + case spv::OpShiftRightLogical: + require(argument(4) < 32, "undefined shift in access condition"); + return argument(3) >> argument(4); + case spv::OpBitwiseAnd: return argument(3) & argument(4); + case spv::OpBitwiseOr: return argument(3) | argument(4); + case spv::OpBitFieldUExtract: { + const auto count = argument(5); + require(count <= 32 && argument(4) <= 32 - count, "invalid bitfield in access condition"); + return (argument(3) >> argument(4)) & (count == 32 ? 0xffffffffu : (1u << count) - 1); + } + case spv::OpIEqual: return argument(3) == argument(4); + case spv::OpINotEqual: return argument(3) != argument(4); + case spv::OpULessThan: return argument(3) < argument(4); + case spv::OpULessThanEqual: return argument(3) <= argument(4); + case spv::OpUGreaterThanEqual: return argument(3) >= argument(4); + case spv::OpLogicalAnd: return argument(3) && argument(4); + case spv::OpLogicalOr: return argument(3) || argument(4); + case spv::OpLogicalNot: return !argument(3); + case spv::OpArrayLength: + require(isGuestPointer(instruction[3]), "unknown array length in access fixture"); + return 16; + case spv::OpLoad: + require(!isGuestPointer(instruction[3]), "guest data unexpectedly controls a fixture access"); + require(rootName(instruction[3]) == "vsharp" || rootName(instruction[3]).find("subgroup") != std::string::npos, "unknown input to access condition"); + return 0; + default: throw std::runtime_error("unsupported opcode in access condition: " + std::to_string(opcode)); + } + } + + const std::vector& words; + std::uint32_t misalignment; + bool valid; + std::uint32_t entryFunction = 0; + std::map> definitions; + std::map names; + std::map specializations; + std::map labels; + std::map phiValues; +}; + +constexpr std::uint32_t Unreachable = 0xfffffffeu; + +std::uint32_t ldsLength(const std::vector& words) { + std::map> definitions; + std::uint32_t variable = 0; + for (std::size_t offset = 5; offset < words.size(); offset += words[offset] >> 16) { + const auto instruction = std::span(words).subspan(offset, words[offset] >> 16); + const auto opcode = static_cast(instruction[0] & 0xffff); + if (opcode == spv::OpName && std::string(reinterpret_cast(instruction.data() + 2)) == "lds_dwords") variable = instruction[1]; + bool result = false; + bool type = false; + spv::HasResultAndType(opcode, &result, &type); + if (result) definitions.emplace(instruction[type ? 2 : 1], instruction); + } + require(variable != 0, "LDS array is missing"); + const auto pointer = definitions.at(definitions.at(variable)[1]); + const auto array = definitions.at(pointer[3]); + require((array[0] & 0xffff) == spv::OpTypeArray, "LDS variable is not an array"); + return definitions.at(array[3])[3]; +} + +void expectReads(const LoadCase& test, bool robust, bool active, std::uint32_t offset, std::vector expected, std::uint32_t records = 64, std::uint32_t misalignment = 0, bool valid = true) { + const auto module = emitLoad(test, active, offset, records, robust); + const auto actual = AccessTrace(module, misalignment, valid).Run(); + if (actual != expected) { + std::string detail = std::string(robust ? "robust" : "plain") + " guest reads:"; + for (const auto index : actual) detail += " " + std::to_string(index); + detail += "; expected:"; + for (const auto index : expected) detail += " " + std::to_string(index); + throw std::runtime_error(detail); + } +} + +void ldsLoads(IrOpcode opcode, IrType type, std::uint32_t bits, std::uint32_t components) { + const LoadCase test{ResourceKind::Lds, opcode, type, bits, components}; + const auto length = ldsLength(emitLoad(test, true, 8, 64, false)); + const auto sentinel = length - 1u; + require(sentinel >= 16u, "LDS fixture is smaller than its probes"); + for (const bool robust : {false, true}) { + std::vector sentinels(components, sentinel); + expectReads(test, robust, false, 8, sentinels); + expectReads(test, robust, true, (sentinel + 5u) * 4u, sentinels); + std::vector indices; + for (std::uint32_t index = 0; index < components; ++index) indices.push_back(2 + index); + expectReads(test, robust, true, 8, indices); + std::vector edge{15}; + edge.insert(edge.end(), components - 1u, sentinel); + expectReads(test, robust, true, 60, edge); + } +} + +} + +int main() { + unsigned failures = 0; + const auto run = [&](std::string_view name, const auto& test) { + try { + test(); + std::cout << "guarded memory loads: " << name << " passed\n"; + } catch (const std::exception& error) { + ++failures; + std::cout << "guarded memory loads: " << name << ": " << error.what() << '\n'; + } + }; + run("LDS word reads its sentinel", [] { ldsLoads(IrOpcode::LoadSharedU32, IrType::U32, 32, 1); }); + run("LDS subword reads its sentinel", [] { + ldsLoads(IrOpcode::LoadSharedU8, IrType::U8, 8, 1); + ldsLoads(IrOpcode::LoadSharedU16, IrType::U16, 16, 1); + }); + run("LDS wide reads its sentinel", [] { ldsLoads(IrOpcode::LoadSharedU32x4, IrType::U32x4, 32, 4); }); + run("GDS word", [] { + const LoadCase test{ResourceKind::Gds, IrOpcode::LoadSharedU32, IrType::U32, 32, 1}; + expectReads(test, false, false, 8, {}); + expectReads(test, false, true, 64, {}); + expectReads(test, false, true, 8, {2}); + expectReads(test, true, false, 8, {Unreachable}); + expectReads(test, true, true, 64, {Unreachable}); + expectReads(test, true, true, 8, {2}); + expectReads(test, true, true, 60, {15}); + }); + run("coherent buffer bounds", [] { + const LoadCase test{ResourceKind::Buffer, IrOpcode::LoadBufferU32, IrType::U32}; + for (const bool robust : {false, true}) expectReads(test, robust, false, 8, {}); + expectReads(test, false, true, 64, {}, 128); + expectReads(test, false, true, 60, {}, 32); + expectReads(test, false, true, 8, {}, 64, 0, false); + expectReads(test, true, true, 64, {Unreachable, Unreachable}, 128); + expectReads(test, true, true, 60, {Unreachable, Unreachable}, 32); + expectReads(test, true, true, 8, {Unreachable, Unreachable}, 64, 0, false); + }); + run("coherent buffer high word", [] { + const LoadCase test{ResourceKind::Buffer, IrOpcode::LoadBufferU32, IrType::U32}; + expectReads(test, false, true, 8, {2}); + expectReads(test, false, true, 8, {2, 3}, 64, 1); + expectReads(test, false, true, 60, {}, 64, 1); + expectReads(test, true, true, 8, {2, 2}); + expectReads(test, true, true, 8, {2, 3}, 64, 1); + expectReads(test, true, true, 60, {Unreachable, Unreachable}, 64, 1); + }); + run("private scratch selection", [] { + const LoadCase test{ResourceKind::Scratch, IrOpcode::LoadAddressU32, IrType::U32}; + for (const bool robust : {false, true}) { + expectReads(test, robust, false, 8, {0}); + expectReads(test, robust, true, 64, {0}); + expectReads(test, robust, true, 8, {2}); + } + }); + if (failures != 0) { + std::cerr << "guarded memory loads: a guest load reached a location the guarded program could not\n"; + return 1; + } + std::cout << "Guarded memory load tests passed\n"; + return 0; +} diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp index 2ac7728dfd..e1035df27d 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp @@ -622,6 +622,7 @@ void verifyKeySensitivity() { changes("f64 denormal preservation", [](SampleRequest& sample) { sample.request.target.float64DenormPreserve = true; }); changes("f16 denormal preservation", [](SampleRequest& sample) { sample.request.target.float16DenormPreserve = true; }); changes("spanned waves", [](SampleRequest& sample) { sample.request.target.spannedWaves = true; }); + changes("robust buffer access 2", [](SampleRequest& sample) { sample.request.target.robustBufferAccess2 = true; }); changes("the workgroup size limit", [](SampleRequest& sample) { sample.request.target.maxWorkgroupSize[2] = 128; }); changes("the invocation limit", [](SampleRequest& sample) { sample.request.target.maxWorkgroupInvocations = 512; }); changes("the shared memory limit", [](SampleRequest& sample) { sample.request.target.maxWorkgroupSharedMemoryBytes = 32768; }); diff --git a/core/libs/prx/libSceAgcDriver/tools/ShaderReplay.cpp b/core/libs/prx/libSceAgcDriver/tools/ShaderReplay.cpp index 2383684946..2e7d97c327 100644 --- a/core/libs/prx/libSceAgcDriver/tools/ShaderReplay.cpp +++ b/core/libs/prx/libSceAgcDriver/tools/ShaderReplay.cpp @@ -47,6 +47,7 @@ bool g_prepare = false; bool g_materialize = false; bool g_graph = false; bool g_maintenance8 = false; +bool g_robust2 = false; bool g_code = false; bool Replay(const char* path) { @@ -145,6 +146,7 @@ bool Replay(const char* path) { } try { if (g_maintenance8) request.request.target.nonConstantImageOffsets = true; + if (g_robust2) request.request.target.robustBufferAccess2 = true; const auto result = ShaderRecompiler::Recompile(request.request); std::printf(" recompiled: %zu SPIR-V words\n", result.spirv.size()); if (g_spirv) { @@ -182,7 +184,7 @@ bool Replay(const char* path) { int main(int argc, char** argv) { if (argc < 2) { - std::fprintf(stderr, "usage: agc_shader_replay [--dis] [--asm] [--cfg] [--mem] [--spv] [--prepare] [--materialize] [--code] [--maintenance8] ...\n the driver writes shader_
.req files when APS5_DUMP_SHADERS is set\n"); + std::fprintf(stderr, "usage: agc_shader_replay [--dis] [--asm] [--cfg] [--mem] [--spv] [--prepare] [--materialize] [--code] [--maintenance8] [--robust2] ...\n the driver writes shader_
.req files when APS5_DUMP_SHADERS is set\n"); return 2; } int failures = 0; @@ -203,6 +205,10 @@ int main(int argc, char** argv) { g_maintenance8 = true; continue; } + if (std::string(argv[i]) == "--robust2") { + g_robust2 = true; + continue; + } if (std::string(argv[i]) == "--mem") { g_memory = true; continue; diff --git a/core/shader/recompiler/CacheKey.hpp b/core/shader/recompiler/CacheKey.hpp index 753a7afbdc..f501252286 100644 --- a/core/shader/recompiler/CacheKey.hpp +++ b/core/shader/recompiler/CacheKey.hpp @@ -262,6 +262,7 @@ private: append(key, value.vertexSubgroups); append(key, value.float16DenormPreserve); append(key, value.spannedWaves); + append(key, value.robustBufferAccess2); } }; diff --git a/core/shader/recompiler/Recompiler.cpp b/core/shader/recompiler/Recompiler.cpp index 9bf473cf58..d08bc8a148 100644 --- a/core/shader/recompiler/Recompiler.cpp +++ b/core/shader/recompiler/Recompiler.cpp @@ -531,6 +531,7 @@ CompiledVariant emitVariant(const RecompileRequest& request, IrProgram& program) targetOptions.fragmentDescriptorSet = request.target.registrationModules; targetOptions.float16DenormPreserve = request.target.float16DenormPreserve; targetOptions.spannedWaves = request.target.spannedWaves; + targetOptions.robustBufferAccess2 = request.target.robustBufferAccess2; constexpr SpirvEmitter spirvEmitter; CompiledShaderArtifact result; diff --git a/core/shader/recompiler/Recompiler.hpp b/core/shader/recompiler/Recompiler.hpp index 123c488f40..79cf3c339f 100644 --- a/core/shader/recompiler/Recompiler.hpp +++ b/core/shader/recompiler/Recompiler.hpp @@ -268,6 +268,7 @@ struct SpirvTarget { bool vertexSubgroups = true; bool float16DenormPreserve = false; bool spannedWaves = false; + bool robustBufferAccess2 = false; }; struct BindingLayout { diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitter.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitter.hpp index e4a861594f..7f7ccbdec5 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitter.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitter.hpp @@ -26,6 +26,7 @@ struct SpirvTargetOptions { bool fragmentDescriptorSet = false; bool float16DenormPreserve = false; bool spannedWaves = false; + bool robustBufferAccess2 = false; }; struct SpirvEmitInfo { diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterHelpers.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterHelpers.hpp index cfd8205030..d996faf30e 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterHelpers.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterHelpers.hpp @@ -118,6 +118,9 @@ std::uint32_t EmitMemoryElementInBounds(SpirvEmitterState& state, const MemoryRe bool InFunctionStorage(const SpirvEmitterState& state, ResourceKind kind); std::uint32_t EmitMemoryElementPointer(SpirvEmitterState& state, const MemoryResourceAccess& access, std::uint32_t index); std::uint32_t EmitValueOrZero(SpirvEmitterState& state, std::uint32_t condition, std::uint32_t value); +std::uint32_t LdsSentinelIndex(const SpirvEmitterState& state); +bool LoadsSelectable(const SpirvEmitterState& state, const MemoryResourceAccess& access); +std::uint32_t GuardedElementIndex(SpirvEmitterState& state, const MemoryResourceAccess& access, std::uint32_t condition, std::uint32_t index); std::uint32_t EmitStorageBufferElementPointer(SpirvEmitterState& state, const MemoryResourceAccess& access, std::uint32_t index, std::uint32_t pointerType); std::uint32_t EmitTBufferBitcastU32ToI32(SpirvEmitterState& state, std::uint32_t value); bool IsSignedFormatComponent(SpirvFormatComponentType type); diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp index 5c89545900..4c54b41678 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp @@ -107,6 +107,7 @@ struct SpirvEmitterState { bool registrationModules = false; bool fragmentDescriptorSet = false; bool float16DenormPreserve = false; + bool robustBufferAccess2 = false; std::unordered_set sharedLaneValues; std::uint32_t laneHalf = 0; std::uint32_t spirvVersion = 0x00010300u; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp index 0d629adaca..f30656653d 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp @@ -260,6 +260,7 @@ std::vector SpirvEmitter::Emit(const IrProgram& program, const Sh state.registrationModules = target.registrationModules; state.fragmentDescriptorSet = target.fragmentDescriptorSet; state.float16DenormPreserve = target.float16DenormPreserve; + state.robustBufferAccess2 = target.robustBufferAccess2; state.hostSubgroupSize = target.subgroupSize; if (HostSubgroupSingleLane(state)) { state.requirements.subgroupBallot = false; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvBufferAccess.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvBufferAccess.cpp index c8716c472a..5fa98f5610 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvBufferAccess.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvBufferAccess.cpp @@ -20,7 +20,7 @@ namespace { if (ShaderWorkgroupInput(state) == nullptr) { FailEmit("function LDS was not prepared before function emission"); } - state.ldsVariable = state.module.DefineGlobalVariable(TypeU32ArrayPointer(state, spv::StorageClassWorkgroup, LdsDwordCount(state) + (state.requirements.ldsLock ? 1u : 0u)), spv::StorageClassWorkgroup); + state.ldsVariable = state.module.DefineGlobalVariable(TypeU32ArrayPointer(state, spv::StorageClassWorkgroup, LdsSentinelIndex(state) + 1u), spv::StorageClassWorkgroup); state.module.AddName(state.ldsVariable, "lds_dwords"); } @@ -177,6 +177,21 @@ std::uint32_t EmitValueOrZero(SpirvEmitterState& state, std::uint32_t condition, return Select(state, TypeU32(state), condition, value, ConstantU32(state, 0u)); } +std::uint32_t LdsSentinelIndex(const SpirvEmitterState& state) { + return LdsDwordCount(state) + (state.requirements.ldsLock ? 1u : 0u); +} + +bool LoadsSelectable(const SpirvEmitterState& state, const MemoryResourceAccess& access) { + if (access.kind == ResourceKind::Scratch) return true; + if (access.kind == ResourceKind::Lds) return !LdsInDeviceMemory(state); + return state.robustBufferAccess2; +} + +std::uint32_t GuardedElementIndex(SpirvEmitterState& state, const MemoryResourceAccess& access, std::uint32_t condition, std::uint32_t index) { + const auto fallback = access.kind == ResourceKind::Scratch ? 0u : access.kind == ResourceKind::Lds ? LdsSentinelIndex(state) : 0xfffffffeu; + return Select(state, TypeU32(state), condition, index, ConstantU32(state, fallback)); +} + std::uint32_t EmitMemoryElementPointer(SpirvEmitterState& state, const MemoryResourceAccess& access, std::uint32_t index) { if (access.kind == ResourceKind::Lds && LdsInDeviceMemory(state)) { return EmitStorageBufferElementPointer(state, access, EmitAddU32(state, state.ldsBufferBase, index), TypeStorageBufferElementPointer(state)); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp index f8bd930b18..d561dea75c 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp @@ -143,7 +143,8 @@ std::uint32_t LoadWordPrepared(SpirvValueEmitContext& ctx, const IrValue& inst, auto& state = ctx.state; const auto index = EmitMemoryElementIndex(state, resource, DwordIndex(ctx, inst, mem)); const auto enable = LoadEnable(state, resource, index, active); - return EmitValueOrZero(state, enable, LoadWordInBounds(ctx, resource, EmitValueOrZero(state, enable, index))); + if (!LoadsSelectable(state, resource)) return EmitValueOrZeroIfCondition(state, enable, [&] { return LoadWordInBounds(ctx, resource, index); }); + return EmitValueOrZero(state, enable, LoadWordInBounds(ctx, resource, GuardedElementIndex(state, resource, enable, index))); } std::uint32_t LoadWord(SpirvValueEmitContext& ctx, const IrValue& inst, const MemoryInfo& mem) { @@ -156,7 +157,8 @@ std::uint32_t LoadSubwordPrepared(SpirvValueEmitContext& ctx, const IrValue& ins const auto rawIndex = Binary(state, spv::OpShiftRightLogical, TypeU32(state), address, ConstantU32(state, 2u)); const auto index = EmitMemoryElementIndex(state, resource, rawIndex); const auto enable = LoadEnable(state, resource, index, active); - return EmitValueOrZero(state, enable, LoadSubwordInBounds(ctx, resource, address, EmitValueOrZero(state, enable, index), bits, signExtend)); + if (!LoadsSelectable(state, resource)) return EmitValueOrZeroIfCondition(state, enable, [&] { return LoadSubwordInBounds(ctx, resource, address, index, bits, signExtend); }); + return EmitValueOrZero(state, enable, LoadSubwordInBounds(ctx, resource, address, GuardedElementIndex(state, resource, enable, index), bits, signExtend)); } std::uint32_t LoadSubword(SpirvValueEmitContext& ctx, const IrValue& inst, const MemoryInfo& mem, std::uint32_t bits) { @@ -374,7 +376,8 @@ std::uint32_t LoadWideShared(SpirvValueEmitContext& ctx, const IrValue& inst, co const auto rawIndex = Binary(state, spv::OpShiftRightLogical, TypeU32(state), address, ConstantU32(state, 2u)); const auto index = EmitMemoryElementIndex(state, resource, rawIndex); const auto enable = LoadEnable(state, resource, index, active); - values.at(component) = EmitValueOrZero(state, enable, LoadWordInBounds(ctx, resource, EmitValueOrZero(state, enable, index))); + values.at(component) = LoadsSelectable(state, resource) ? EmitValueOrZero(state, enable, LoadWordInBounds(ctx, resource, GuardedElementIndex(state, resource, enable, index))) + : EmitValueOrZeroIfCondition(state, enable, [&] { return LoadWordInBounds(ctx, resource, index); }); } return ConstructU32Composite(state, components, values); } diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp index 1f76ed7314..cd1f8c8ddc 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp @@ -1146,7 +1146,7 @@ void EmitProgram(SpirvEmitterState& state) { state.module.AddFunction(spv::OpFunction, TypeVoid(state), state.meshGuestFunc != 0u ? state.meshGuestFunc : state.mainFunc, spv::FunctionControlMaskNone, TypeFunction(state)); EmitLabel(state, state.entryLabel); if (state.requirements.functionLds) { - state.module.AddFunction(spv::OpVariable, TypeU32ArrayPointer(state, spv::StorageClassFunction, LdsDwordCount(state)), state.ldsVariable, spv::StorageClassFunction); + state.module.AddFunction(spv::OpVariable, TypeU32ArrayPointer(state, spv::StorageClassFunction, LdsSentinelIndex(state) + 1u), state.ldsVariable, spv::StorageClassFunction); } if (state.requirements.functionScratch) { for (std::uint32_t half = 0; half < state.laneCount; half++) { diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp index f31fa225a0..7820a803c3 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp @@ -160,7 +160,7 @@ std::uint32_t directWordIndex(SpirvEmitterState& state, const MemoryResourceAcce return EmitAddU32(state, relative, base); } -std::uint32_t readBuffer(SpirvValueEmitContext& context, const IrValue& instruction, std::uint32_t address, std::uint32_t bits) { +std::uint32_t readBuffer(SpirvValueEmitContext& context, const IrValue& instruction, std::uint32_t address, std::uint32_t bits, std::uint32_t enable = 0u) { if (context.Memory(instruction).gpuDescriptor) return EmitBdaRead(context, instruction, address, bits); auto& state = context.state; const auto access = directAccess(context, instruction, false, !context.Memory(instruction).coherent); @@ -169,7 +169,8 @@ std::uint32_t readBuffer(SpirvValueEmitContext& context, const IrValue& instruct const auto unaligned = nonzero(state, misalignment); const auto next = EmitAddU32(state, index, ConstantU32(state, 1u)); const auto last = Select(state, TypeU32(state), unaligned, next, index); - const auto inBounds = EmitMemoryElementInBounds(state, access, last); + const auto bounds = EmitMemoryElementInBounds(state, access, last); + const auto inBounds = enable != 0u ? both(state, enable, bounds) : bounds; const auto load = [&](std::uint32_t word) { const auto value = state.module.AllocateId(); const auto pointer = EmitMemoryElementPointer(state, access, word); @@ -178,15 +179,27 @@ std::uint32_t readBuffer(SpirvValueEmitContext& context, const IrValue& instruct return value; }; const auto shift = Binary(state, spv::OpShiftLeftLogical, TypeU32(state), misalignment, ConstantU32(state, 3u)); - const auto low = Binary(state, spv::OpShiftRightLogical, TypeU32(state), load(EmitValueOrZero(state, inBounds, index)), shift); - const auto highWord = load(EmitValueOrZero(state, both(state, inBounds, unaligned), next)); - const auto high = EmitValueOrZero(state, unaligned, Binary(state, spv::OpShiftLeftLogical, TypeU32(state), highWord, Binary(state, spv::OpISub, TypeU32(state), ConstantU32(state, 32u), shift))); - auto value = EmitOrU32(state, low, high); - if (bits != 32u) { - const auto subwordShift = Binary(state, spv::OpShiftLeftLogical, TypeU32(state), EmitAndConstant(state, address, 3u), ConstantU32(state, 3u)); - value = Binary(state, spv::OpShiftRightLogical, TypeU32(state), value, subwordShift); + const auto combine = [&](std::uint32_t low, std::uint32_t high) { + auto value = EmitOrU32(state, low, high); + if (bits != 32u) { + const auto subwordShift = Binary(state, spv::OpShiftLeftLogical, TypeU32(state), EmitAndConstant(state, address, 3u), ConstantU32(state, 3u)); + value = Binary(state, spv::OpShiftRightLogical, TypeU32(state), value, subwordShift); + } + return EmitAndConstant(state, value, bits == 32u ? 0xffffffffu : (1u << bits) - 1u); + }; + const auto highShift = Binary(state, spv::OpISub, TypeU32(state), ConstantU32(state, 32u), shift); + if (!LoadsSelectable(state, access)) { + return EmitValueOrZeroIfCondition(state, inBounds, [&] { + const auto low = Binary(state, spv::OpShiftRightLogical, TypeU32(state), load(index), shift); + const auto high = EmitValueOrZeroIfCondition(state, unaligned, [&] { return Binary(state, spv::OpShiftLeftLogical, TypeU32(state), load(next), highShift); }); + return combine(low, high); + }); } - return EmitValueOrZero(state, inBounds, EmitAndConstant(state, value, bits == 32u ? 0xffffffffu : (1u << bits) - 1u)); + const auto lowIndex = GuardedElementIndex(state, access, inBounds, index); + const auto highIndex = Select(state, TypeU32(state), both(state, inBounds, unaligned), next, lowIndex); + const auto low = Binary(state, spv::OpShiftRightLogical, TypeU32(state), load(lowIndex), shift); + const auto high = EmitValueOrZero(state, unaligned, Binary(state, spv::OpShiftLeftLogical, TypeU32(state), load(highIndex), highShift)); + return EmitValueOrZero(state, inBounds, combine(low, high)); } std::uint32_t byteBuffers(SpirvEmitterState& state) { @@ -725,7 +738,7 @@ std::uint32_t EmitRuntimeBufferLoad(SpirvValueEmitContext& context, const IrValu for (std::uint32_t component = 0; component < components; ++component) { const auto access = address(context, instruction, resource, component * 4u, memory.dataBits / 8u); values[component] = memory.gpuDescriptor ? EmitValueOrZeroIfCondition(state, access.inBounds, [&] { return readBuffer(context, instruction, access.guest, memory.dataBits); }) - : EmitValueOrZero(state, access.inBounds, readBuffer(context, instruction, access.guest, memory.dataBits)); + : readBuffer(context, instruction, access.guest, memory.dataBits, access.inBounds); } return composite(state, components, values); }; @@ -775,7 +788,7 @@ std::uint32_t EmitRuntimeScalarBufferLoad(SpirvValueEmitContext& context, const const auto byte = EmitAndConstant(state, address, ~3u); const auto offset = Unary(state, spv::OpUConvert, u64, byte); const auto inBounds = both(state, noCarry, both(state, resource.valid, Binary(state, spv::OpULessThanEqual, TypeBool(state), Binary(state, spv::OpIAdd, u64, offset, BdaConstant(state, 4u)), size))); - if (!context.Memory(instruction).gpuDescriptor) return EmitValueOrZero(state, inBounds, readBuffer(context, instruction, byte, 32u)); + if (!context.Memory(instruction).gpuDescriptor) return readBuffer(context, instruction, byte, 32u, inBounds); return EmitValueOrZeroIfCondition(state, inBounds, [&] { return readBuffer(context, instruction, AddBdaAddress(context, instruction, resource.base, offset, false), 32u); }); } diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index 427301445d..7b7e78f8b8 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -148,3 +148,4 @@ - [Scalar buffer descriptors](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) use only the base address, stride and record count for resources accessed exclusively by scalar loads; their fourth word is normalized to zero, following AMD RDNA2 ISA section 7.2.2. Vector and mixed accesses retain descriptor validation. Native Vulkan readbacks cover ignored fourth-word fields on Windows/NVIDIA; their console behavior has not been measured. - [Spanned wave64 compute](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvSubgroup.cpp) (shader recompiler) - `APS5_SPANNED_WAVE=1`, off by default, runs a single-wave wave64 compute program at one lane per invocation on a 32-lane host subgroup and exchanges ballots, shuffles and reductions between the two host subgroups through workgroup memory and barriers. The driver turns it on only where compute subgroups can be pinned at 32 full lanes (`subgroupSizeControl` and `computeFullSubgroups`), passes it as `SpirvTarget::spannedWaves` (part of the cache key and the serialized request), and creates the pipelines of spanned programs with `REQUIRE_FULL_SUBGROUPS`, since the lane number `SubgroupId * 32 + SubgroupLocalInvocationId` needs two full subgroups. Those barriers are correct only where every invocation of the workgroup reaches them, which holds for the guest's wave-uniform control flow. The emulated 64-bit LDS atomics ([LockedLdsUpdate](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp)) run only on the lanes the exec mask keeps, so their lock loop ballots within its own host subgroup, as its elect already does. Any other cross-lane exchange emitted inside per-lane control flow would hang a spanned wave in the same way, and the emitter does not reject one: there is no uniformity check. The only check so far is manual: disassemble the module (`spirv-dis`), list the selection and loop constructs that enclose each `OpControlBarrier`, and read the condition of each enclosing selection. In Ghost of Yōtei's program 0x80003af600 (PPSA26344), 108 of 111 barriers sat only in loops on wave-uniform conditions and 3 inside per-invocation conditionals, all in the lock loop. Other programs and titles have not been scanned. An earlier, cheaper ballot (one barrier instead of three) also lost the device for a reason that is not understood, which may be another barrier placement problem. Measured on an RTX 4090 (Windows): an offline replay of that program and two Yōtei runs that previously lost the device (NVIDIA TDR) now run past it, and [LdsMaskWaterfall.cpp](../../../core/libs/prx/libSceAgcDriver/tests/execution/LdsMaskWaterfall.cpp) runs both modes and covers a half-inactive wave. Stray (PPSA02101) on Intel ANV and a Demon's Souls (PPSA01342) request replay showed no fault, before this fix - [Guest memory load guards](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (shader recompiler) - LDS, scratch and direct storage-buffer loads (`LoadWord`, `LoadSubword`, `LoadWideShared`, `readBuffer` and `EmitRuntimeScalarBufferLoad` in [SpirvRuntimeBuffer.cpp](../../../core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp)) load element 0 when the lane is inactive or the address is out of bounds and select 0 as the result, instead of branching around the load; stores, atomics, formatted and physical-address (BDA) accesses still branch. A branch guard costs four blocks per lane half and the host compiler's time grows with the block count: PPSA11142's merged PSP vertex program (3176 ISA words, wave64) emitted 16,983 blocks and 1,710 conditional branches, 5,907 and 868 with the selects. The unconditional load depends on element 0 being addressable: SPIR-V function and workgroup arrays have at least one element, and a storage buffer with no elements reads out of bounds, which is defined only with `robustBufferAccess` ([VulkanDevice.cpp](../../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) enables it when the device has it). +- [Guest memory load guards](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (shader recompiler) - a guest load whose lane is inactive or whose address is out of bounds is still performed and its result replaced by 0 with `OpSelect`, instead of a selection per guard, when the load can be pointed at a location nothing writes: scratch reads its own element 0; LDS in workgroup or function memory reads a sentinel dword appended after the program's LDS (and after the lock dword), which no bounds-checked write reaches; storage buffers (direct buffers, GDS) read element 0xfffffffe, which `robustBufferAccess2` turns into a zero load with no memory access, so the extra read cannot race with a write the guarded program would not have raced with ([Vulkan memory model](https://docs.vulkan.org/spec/latest/appendices/memorymodel.html#data-race)). Without `robustBufferAccess2` ([VulkanDevice.cpp](../../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) enables it when the device has it), and for LDS kept in device memory, those loads keep their selections; stores, atomics, formatted and physical-address (BDA) accesses always do. The aligned high word of a misaligned-capable buffer read re-reads the low word's own element. A selection guard costs four blocks per lane half and the host compiler's time grows with the block count: PPSA11142's merged PSP vertex program (3176 ISA words, wave64) emitted 16,983 blocks and 1,710 conditional branches, 5,907 and 868 with the selects. `agc_guarded_memory_loads` traces the emitted loads in both device modes. From d914130cc3d1097a6cb88fd161b5bf15421d2912 Mon Sep 17 00:00:00 2001 From: jweidenh <38249793+jweidenh@users.noreply.github.com> Date: Sun, 11 Oct 2026 20:22:14 -0400 Subject: [PATCH 0618/1000] test(recompiler): cover LDS sentinel budget boundaries --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 14 +++ .../tests/LdsSentinelBudget.cpp | 115 ++++++++++++++++++ 2 files changed, 129 insertions(+) create mode 100644 core/libs/prx/libSceAgcDriver/tests/LdsSentinelBudget.cpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 3eed40db9f..d1545095c4 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -512,6 +512,20 @@ if(BUILD_TESTING) endif() endif() +add_test_executable(agc_lds_sentinel_budget_tests EXCLUDE_FROM_ALL tests/LdsSentinelBudget.cpp) +target_link_libraries(agc_lds_sentinel_budget_tests PRIVATE shader_recompiler_agc_driver libc) +set_target_properties(agc_lds_sentinel_budget_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_lds_sentinel_budget_tests) +if(BUILD_TESTING) + set_target_properties(agc_lds_sentinel_budget_tests PROPERTIES EXCLUDE_FROM_ALL OFF) + add_test(NAME agc_lds_sentinel_budget COMMAND agc_lds_sentinel_budget_tests) + set_tests_properties(agc_lds_sentinel_budget PROPERTIES TIMEOUT 30 ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + if(WIN32) + set_tests_properties(agc_lds_sentinel_budget PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() + add_test_executable(agc_shared_write_tests EXCLUDE_FROM_ALL tests/SharedWrites.cpp) target_include_directories(agc_shared_write_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(agc_shared_write_tests PRIVATE libSceAgcDriver libkernel libc) diff --git a/core/libs/prx/libSceAgcDriver/tests/LdsSentinelBudget.cpp b/core/libs/prx/libSceAgcDriver/tests/LdsSentinelBudget.cpp new file mode 100644 index 0000000000..0e54ad04ce --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/LdsSentinelBudget.cpp @@ -0,0 +1,115 @@ +#include "Recompiler.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using namespace ShaderRecompiler; + +void require(bool condition, const std::string& message) { + if (!condition) throw std::runtime_error(message); +} + +std::uint64_t workgroupBytes(std::span words) { + std::map> types; + std::map constants; + std::vector pointers; + for (std::size_t offset = 5; offset < words.size();) { + const auto count = words[offset] >> 16u; + require(count != 0u && count <= words.size() - offset, "malformed SPIR-V instruction"); + const auto instruction = words.subspan(offset, count); + const auto opcode = static_cast(instruction[0] & 0xffffu); + if (opcode == spv::OpTypeInt || opcode == spv::OpTypeFloat || opcode == spv::OpTypeVector || opcode == spv::OpTypeArray || opcode == spv::OpTypeStruct || opcode == spv::OpTypePointer) types.emplace(instruction[1], instruction); + if (opcode == spv::OpConstant) constants.emplace(instruction[2], instruction[3]); + if (opcode == spv::OpVariable && instruction[3] == spv::StorageClassWorkgroup) pointers.push_back(instruction[1]); + offset += count; + } + const std::function size = [&](std::uint32_t id) -> std::uint64_t { + const auto type = types.at(id); + switch (static_cast(type[0] & 0xffffu)) { + case spv::OpTypeInt: case spv::OpTypeFloat: return type[2] / 8u; + case spv::OpTypeVector: return size(type[2]) * type[3]; + case spv::OpTypeArray: return size(type[2]) * constants.at(type[3]); + case spv::OpTypeStruct: { + std::uint64_t bytes = 0; + for (const auto member : type.subspan(2)) bytes += size(member); + return bytes; + } + default: throw std::runtime_error("unexpected Workgroup variable type"); + } + }; + std::uint64_t bytes = 0; + for (const auto pointer : pointers) { + const auto type = types.at(pointer); + require((type[0] & 0xffffu) == spv::OpTypePointer, "Workgroup variable is not a pointer"); + bytes += size(type[3]); + } + return bytes; +} + +RecompileResult compile(std::uint32_t waveSize, std::uint32_t limit) { + static constexpr std::array code{ + 0x7e020280u, 0x7e0402c1u, 0x7e060280u, 0x7e080281u, + 0xd9000000u, 0x00000201u, 0xbf8cc07fu, 0xbf810000u + }; + static constexpr std::array capabilities{spv::CapabilityShader, spv::CapabilityGroupNonUniform, spv::CapabilityGroupNonUniformBallot, spv::CapabilityGroupNonUniformShuffle, spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess}; + static constexpr std::array extensions{"SPV_KHR_physical_storage_buffer", "SPV_KHR_8bit_storage"}; + static constexpr std::array userData{}; + RecompileRequest request{}; + request.shader = {ShaderStage::Compute, 0x20000u, code, 0, {}}; + request.context.waveSize = waveSize; + request.context.userData = userData; + request.context.compute = ShaderComputeStageInfo{{64u, 1u, 1u}, 16u, {false, false, false}, false, 1u}; + request.target.vulkanVersion = 0x00403000u; + request.target.spirvVersion = 0x00010600u; + request.target.subgroupSize = 32; + request.target.bdaAbiVersion = 1; + request.target.supportedCapabilities = capabilities; + request.target.supportedExtensions = extensions; + request.target.maxWorkgroupSize = {1024u, 1024u, 64u}; + request.target.maxWorkgroupInvocations = 1024; + request.target.maxWorkgroupSharedMemoryBytes = limit; + request.layout = {0, 0, 0, 128}; + request.useCache = false; + return Recompile(request); +} + +void verify(std::uint32_t waveSize, std::uint32_t limit) { + const auto result = compile(waveSize, limit); + require(!result.spirv.empty(), "LDS atomic program did not compile"); + const auto declared = workgroupBytes(result.spirv.Words()); + require(declared <= limit, "declared " + std::to_string(declared) + " Workgroup bytes exceeds limit " + std::to_string(limit)); + require(result.workgroupMemoryDwords == (limit < 72u ? 17u : 0u), "incorrect device-memory LDS stride at limit " + std::to_string(limit)); + if (limit >= 72u) require(declared != 0u, "in-budget LDS unexpectedly lost its Workgroup allocation"); +} + +} + +int main() { + unsigned failures = 0; + for (const auto waveSize : {32u, 64u}) { + for (const auto limit : {64u, 68u, 72u}) { + const auto name = "wave" + std::to_string(waveSize) + " limit" + std::to_string(limit); + try { + verify(waveSize, limit); + std::cout << "LDS sentinel budget: " << name << " passed\n"; + } catch (const std::exception& error) { + ++failures; + const std::string message(error.what()); + std::cerr << "LDS sentinel budget: " << name << ": " << message.substr(0, message.find("RecompileRequest:")) << '\n'; + } + } + } + if (failures != 0) return 1; + std::cout << "LDS sentinel budget tests passed\n"; + return 0; +} From ae56b9558cf17cfcad944c0c7fa62d051f432101 Mon Sep 17 00:00:00 2001 From: jweidenh <38249793+jweidenh@users.noreply.github.com> Date: Sun, 11 Oct 2026 20:22:46 -0400 Subject: [PATCH 0619/1000] test(agc): exercise LDS sentinel boundaries on native devices --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 7 +++ .../tests/execution/LdsMixedWidthAtomics.cpp | 63 ++++++++++++++++++- 2 files changed, 67 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index d1545095c4..8289252e66 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1611,6 +1611,13 @@ if(BUILD_TESTING) if(WIN32) set_tests_properties(agc_sparse_buffer_holes PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test(NAME agc_driver_lds_sentinel_budget COMMAND agc_driver_lds_mixed_width_atomics_tests --sentinel-budget) + set_tests_properties(agc_driver_lds_sentinel_budget PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" TIMEOUT 30) + if(WIN32) + set_tests_properties(agc_driver_lds_sentinel_budget PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test(NAME agc_driver_emulated_color_compare_srgb_decode COMMAND agc_driver_emulated_color_compare_tests) set_tests_properties(agc_driver_emulated_color_compare_srgb_decode PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_SRGB_SHADER_DECODE=1" diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/LdsMixedWidthAtomics.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/LdsMixedWidthAtomics.cpp index 6528e8af9e..3db59e8190 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/LdsMixedWidthAtomics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/LdsMixedWidthAtomics.cpp @@ -6,9 +6,13 @@ #include #include #include +#include #include +#include #include #include +#include +#include #include namespace { @@ -182,14 +186,41 @@ void Check(const char* name, std::uint64_t operations, std::uint32_t run) { } } -void Run(AgcDriver::VulkanDevice& device, std::span code, const char* name, std::uint64_t operations, std::uint32_t repeats) { +std::uint64_t WorkgroupBytes(std::span words) { + std::unordered_map> types; + std::unordered_map constants; + std::vector pointers; + for (std::size_t at = 5; at < words.size();) { + const auto count = words[at] >> 16u; + Require(count != 0u && count <= words.size() - at, "lds sentinel budget: malformed SPIR-V"); + const auto instruction = words.subspan(at, count); + const auto opcode = instruction[0] & 0xffffu; + if (opcode == spv::OpTypeInt || opcode == spv::OpTypeArray || opcode == spv::OpTypePointer) types[instruction[1]] = instruction; + if (opcode == spv::OpConstant) constants[instruction[2]] = instruction[3]; + if (opcode == spv::OpVariable && instruction[3] == spv::StorageClassWorkgroup) pointers.push_back(instruction[1]); + at += count; + } + std::uint64_t total = 0; + for (const auto pointer : pointers) { + const auto definition = types.at(pointer); + Require((definition[0] & 0xffffu) == spv::OpTypePointer, "lds sentinel budget: Workgroup storage has no pointer type"); + const auto array = types.at(definition[3]); + Require((array[0] & 0xffffu) == spv::OpTypeArray, "lds sentinel budget: unexpected Workgroup allocation shape"); + const auto element = types.at(array[2]); + Require((element[0] & 0xffffu) == spv::OpTypeInt && element[2] == 32u, "lds sentinel budget: Workgroup array is not dwords"); + total += static_cast(constants.at(array[3])) * sizeof(std::uint32_t); + } + return total; +} + +void Run(AgcDriver::VulkanDevice& device, std::span code, const char* name, std::uint64_t operations, std::uint32_t repeats, std::uint32_t ldsDwords = LdsDwords, std::optional expectedDeviceDwords = std::nullopt) { std::vector userData(8, 0u); const auto input = BufferDescriptor(Input.data(), static_cast(Input.size() * 4u)); const auto output = BufferDescriptor(Output.data(), static_cast(Output.size() * 4u)); std::copy(input.begin(), input.end(), userData.begin()); std::copy(output.begin(), output.end(), userData.begin() + 4); const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; - const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, LdsDwords, {false, false, false}, false, 1}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, ldsDwords, {false, false, false}, false, 1}; ShaderRecompiler::RecompileRequest request{ {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, @@ -198,20 +229,46 @@ void Run(AgcDriver::VulkanDevice& device, std::span code, c }; request.useCache = false; const auto result = ShaderRecompiler::Recompile(request); + if (expectedDeviceDwords) { + const auto bytes = WorkgroupBytes(result.spirv.Words()); + const auto limit = request.target.maxWorkgroupSharedMemoryBytes; + Require(bytes <= limit, "lds sentinel budget: guest LDS, lock and sentinel exceed the device limit before dispatch"); + Require(result.workgroupMemoryDwords == *expectedDeviceDwords, "lds sentinel budget: unexpected device-backed LDS allocation before dispatch"); + if (*expectedDeviceDwords == 0u) Require(bytes == limit, "lds sentinel budget: exact-fit case did not retain its lock and sentinel"); + std::printf("lds sentinel budget: %s declarations verified; guest=%u; workgroup=%llu; device=%u\n", name, ldsDwords * 4u, static_cast(bytes), result.workgroupMemoryDwords); + } for (std::uint32_t run = 0; run < repeats; ++run) { Output.fill(0xdeadbeefu); device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); device.WaitIdle(); Check(name, operations, run); } + if (expectedDeviceDwords) std::printf("lds sentinel budget: %s GPU readback verified across 256 lanes\n", name); +} + +void SentinelBudget(AgcDriver::VulkanDevice& device) { + const auto target = device.Target(); + const auto limit = target.maxWorkgroupSharedMemoryBytes; + std::printf("lds sentinel budget: robustBufferAccess2=%u; maxWorkgroupSharedMemoryBytes=%u\n", static_cast(target.robustBufferAccess2), limit); + if (std::getenv("ANYPS5_REQUIRE_ROBUST2") != nullptr) Require(target.robustBufferAccess2, "lds sentinel budget: robustBufferAccess2 was not enabled"); + Require(limit >= 32u && limit % 4u == 0u, "lds sentinel budget: unexpected device shared-memory limit"); + const auto operations = static_cast(Threads) * Iterations; + Run(device, UniformCode, "limit-4", operations, 1, (limit - 4u) / 4u, (limit - 4u) / 4u + 1u); + Run(device, UniformCode, "limit-8", operations, 1, (limit - 8u) / 4u, 0u); + std::puts("lds sentinel budget tests passed"); } } -int main() { +int main(int argc, char** argv) { try { const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; + if (argc == 2 && std::string_view(argv[1]) == "--sentinel-budget") { + SentinelBudget(*device); + return 0; + } + Require(argc == 1, "lds mixed width atomics: unexpected argument"); Run(*device, UniformCode, "uniform", static_cast(Threads) * Iterations, 1); Run(*device, DivergentCode, "divergent", static_cast(Threads) / 2u * Iterations, 1); Run(*device, ParityCode, "split lanes", Threads / 2u, Repeats); From 21a3bd5606a627cad2fb9e7883c87fc558ec7829 Mon Sep 17 00:00:00 2001 From: jweidenh <38249793+jweidenh@users.noreply.github.com> Date: Sun, 11 Oct 2026 20:23:44 -0400 Subject: [PATCH 0620/1000] fix(recompiler): reserve LDS sentinel and lock headroom --- core/shader/recompiler/Deserialize.cpp | 2 +- core/shader/recompiler/Recompiler.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/core/shader/recompiler/Deserialize.cpp b/core/shader/recompiler/Deserialize.cpp index 6f1b3a0ca9..61ccd6d5f9 100644 --- a/core/shader/recompiler/Deserialize.cpp +++ b/core/shader/recompiler/Deserialize.cpp @@ -167,7 +167,7 @@ int main(int argc, char** argv) { translateOptions.userDataBaseRegister = request.context.userDataBaseRegister; translateOptions.userDataCount = static_cast(request.context.userData.size()); translateOptions.scratchDwords = request.context.compute.has_value() ? request.context.compute->scratchDwords : 0u; - translateOptions.sharedMemoryBytes = request.context.compute.has_value() && static_cast(request.context.compute->ldsSizeDwords) * 4u + 4u > request.target.maxWorkgroupSharedMemoryBytes ? request.context.compute->ldsSizeDwords * 4u : 0u; + translateOptions.sharedMemoryBytes = request.context.compute.has_value() && static_cast(request.context.compute->ldsSizeDwords) * 4u + 8u > request.target.maxWorkgroupSharedMemoryBytes ? request.context.compute->ldsSizeDwords * 4u : 0u; translateOptions.inputInfo = inputInfo; InstructionTranslator translator; diff --git a/core/shader/recompiler/Recompiler.cpp b/core/shader/recompiler/Recompiler.cpp index d08bc8a148..2481fb8554 100644 --- a/core/shader/recompiler/Recompiler.cpp +++ b/core/shader/recompiler/Recompiler.cpp @@ -123,7 +123,7 @@ ShaderStageInputInfo RequestInputInfo(const RecompileRequest& request) { std::uint32_t DeviceMemoryLdsBytes(const RecompileRequest& request) { if (!request.context.compute.has_value()) return 0u; const auto bytes = static_cast(request.context.compute->ldsSizeDwords) * 4u; - return bytes + 4u > request.target.maxWorkgroupSharedMemoryBytes ? static_cast(bytes) : 0u; + return bytes + 8u > request.target.maxWorkgroupSharedMemoryBytes ? static_cast(bytes) : 0u; } std::atomic frontEndBuilds{0}; From 0a9ee3698aeb7ae3095e624c748f7ff6a34b5bb1 Mon Sep 17 00:00:00 2001 From: Mouad-Scriptz-0 Date: Mon, 12 Oct 2026 08:51:36 +0000 Subject: [PATCH 0621/1000] perf(recompiler): keep LDS loads branched and drop the LDS sentinel NVIDIA's compiler predicates a branched LDS load at no cost, while the selected form costs about 2.5 instructions per dword and splits a ds_read_b128 into four loads (RTX 4060 measurements by @karanvora2599 in loads therefore keep the branch of main, in workgroup, function and device memory alike, and the sentinel dword that only they read goes away: the LDS array is the program's dwords plus the lock again, and the device-memory decision reserves the lock alone. The sentinel budget tests become lock budget tests at that boundary, and the guarded-load test pins the branch. Co-Authored-By: Claude Fable 5.1 --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 22 ++++++------ .../tests/GuardedMemoryLoads.cpp | 19 ++++------ ...dsSentinelBudget.cpp => LdsLockBudget.cpp} | 10 +++--- .../tests/execution/LdsMixedWidthAtomics.cpp | 36 +++++++++---------- core/shader/recompiler/Deserialize.cpp | 2 +- core/shader/recompiler/Recompiler.cpp | 2 +- .../SpirvBackend/SpirvEmitterHelpers.hpp | 2 +- .../src/SpirvMemory/SpirvBufferAccess.cpp | 9 ++--- .../SpirvBackend/src/SpirvModuleEmitter.cpp | 2 +- docs/dev/debt/shader.md | 3 +- 10 files changed, 51 insertions(+), 56 deletions(-) rename core/libs/prx/libSceAgcDriver/tests/{LdsSentinelBudget.cpp => LdsLockBudget.cpp} (92%) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 8289252e66..3702492677 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -512,16 +512,16 @@ if(BUILD_TESTING) endif() endif() -add_test_executable(agc_lds_sentinel_budget_tests EXCLUDE_FROM_ALL tests/LdsSentinelBudget.cpp) -target_link_libraries(agc_lds_sentinel_budget_tests PRIVATE shader_recompiler_agc_driver libc) -set_target_properties(agc_lds_sentinel_budget_tests PROPERTIES CXX_EXTENSIONS OFF) -configure_windows_unwind(agc_lds_sentinel_budget_tests) +add_test_executable(agc_lds_lock_budget_tests EXCLUDE_FROM_ALL tests/LdsLockBudget.cpp) +target_link_libraries(agc_lds_lock_budget_tests PRIVATE shader_recompiler_agc_driver libc) +set_target_properties(agc_lds_lock_budget_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_lds_lock_budget_tests) if(BUILD_TESTING) - set_target_properties(agc_lds_sentinel_budget_tests PROPERTIES EXCLUDE_FROM_ALL OFF) - add_test(NAME agc_lds_sentinel_budget COMMAND agc_lds_sentinel_budget_tests) - set_tests_properties(agc_lds_sentinel_budget PROPERTIES TIMEOUT 30 ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + set_target_properties(agc_lds_lock_budget_tests PROPERTIES EXCLUDE_FROM_ALL OFF) + add_test(NAME agc_lds_lock_budget COMMAND agc_lds_lock_budget_tests) + set_tests_properties(agc_lds_lock_budget PROPERTIES TIMEOUT 30 ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") if(WIN32) - set_tests_properties(agc_lds_sentinel_budget PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(agc_lds_lock_budget PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() endif() @@ -1611,10 +1611,10 @@ if(BUILD_TESTING) if(WIN32) set_tests_properties(agc_sparse_buffer_holes PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() - add_test(NAME agc_driver_lds_sentinel_budget COMMAND agc_driver_lds_mixed_width_atomics_tests --sentinel-budget) - set_tests_properties(agc_driver_lds_sentinel_budget PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" TIMEOUT 30) + add_test(NAME agc_driver_lds_lock_budget COMMAND agc_driver_lds_mixed_width_atomics_tests --lock-budget) + set_tests_properties(agc_driver_lds_lock_budget PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" TIMEOUT 30) if(WIN32) - set_tests_properties(agc_driver_lds_sentinel_budget PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(agc_driver_lds_lock_budget PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() diff --git a/core/libs/prx/libSceAgcDriver/tests/GuardedMemoryLoads.cpp b/core/libs/prx/libSceAgcDriver/tests/GuardedMemoryLoads.cpp index 241430786c..e20b23d8d5 100644 --- a/core/libs/prx/libSceAgcDriver/tests/GuardedMemoryLoads.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/GuardedMemoryLoads.cpp @@ -294,19 +294,14 @@ void expectReads(const LoadCase& test, bool robust, bool active, std::uint32_t o void ldsLoads(IrOpcode opcode, IrType type, std::uint32_t bits, std::uint32_t components) { const LoadCase test{ResourceKind::Lds, opcode, type, bits, components}; - const auto length = ldsLength(emitLoad(test, true, 8, 64, false)); - const auto sentinel = length - 1u; - require(sentinel >= 16u, "LDS fixture is smaller than its probes"); + require(ldsLength(emitLoad(test, true, 8, 64, false)) == 16u, "LDS array declares more than the program's dwords"); for (const bool robust : {false, true}) { - std::vector sentinels(components, sentinel); - expectReads(test, robust, false, 8, sentinels); - expectReads(test, robust, true, (sentinel + 5u) * 4u, sentinels); + expectReads(test, robust, false, 8, {}); + expectReads(test, robust, true, 84, {}); std::vector indices; for (std::uint32_t index = 0; index < components; ++index) indices.push_back(2 + index); expectReads(test, robust, true, 8, indices); - std::vector edge{15}; - edge.insert(edge.end(), components - 1u, sentinel); - expectReads(test, robust, true, 60, edge); + expectReads(test, robust, true, 60, {15}); } } @@ -323,12 +318,12 @@ int main() { std::cout << "guarded memory loads: " << name << ": " << error.what() << '\n'; } }; - run("LDS word reads its sentinel", [] { ldsLoads(IrOpcode::LoadSharedU32, IrType::U32, 32, 1); }); - run("LDS subword reads its sentinel", [] { + run("LDS word keeps its branch", [] { ldsLoads(IrOpcode::LoadSharedU32, IrType::U32, 32, 1); }); + run("LDS subword keeps its branch", [] { ldsLoads(IrOpcode::LoadSharedU8, IrType::U8, 8, 1); ldsLoads(IrOpcode::LoadSharedU16, IrType::U16, 16, 1); }); - run("LDS wide reads its sentinel", [] { ldsLoads(IrOpcode::LoadSharedU32x4, IrType::U32x4, 32, 4); }); + run("LDS wide keeps its branch", [] { ldsLoads(IrOpcode::LoadSharedU32x4, IrType::U32x4, 32, 4); }); run("GDS word", [] { const LoadCase test{ResourceKind::Gds, IrOpcode::LoadSharedU32, IrType::U32, 32, 1}; expectReads(test, false, false, 8, {}); diff --git a/core/libs/prx/libSceAgcDriver/tests/LdsSentinelBudget.cpp b/core/libs/prx/libSceAgcDriver/tests/LdsLockBudget.cpp similarity index 92% rename from core/libs/prx/libSceAgcDriver/tests/LdsSentinelBudget.cpp rename to core/libs/prx/libSceAgcDriver/tests/LdsLockBudget.cpp index 0e54ad04ce..8901e04a35 100644 --- a/core/libs/prx/libSceAgcDriver/tests/LdsSentinelBudget.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/LdsLockBudget.cpp @@ -88,8 +88,8 @@ void verify(std::uint32_t waveSize, std::uint32_t limit) { require(!result.spirv.empty(), "LDS atomic program did not compile"); const auto declared = workgroupBytes(result.spirv.Words()); require(declared <= limit, "declared " + std::to_string(declared) + " Workgroup bytes exceeds limit " + std::to_string(limit)); - require(result.workgroupMemoryDwords == (limit < 72u ? 17u : 0u), "incorrect device-memory LDS stride at limit " + std::to_string(limit)); - if (limit >= 72u) require(declared != 0u, "in-budget LDS unexpectedly lost its Workgroup allocation"); + require(result.workgroupMemoryDwords == (limit < 68u ? 17u : 0u), "incorrect device-memory LDS stride at limit " + std::to_string(limit)); + if (limit >= 68u) require(declared != 0u, "in-budget LDS unexpectedly lost its Workgroup allocation"); } } @@ -101,15 +101,15 @@ int main() { const auto name = "wave" + std::to_string(waveSize) + " limit" + std::to_string(limit); try { verify(waveSize, limit); - std::cout << "LDS sentinel budget: " << name << " passed\n"; + std::cout << "LDS lock budget: " << name << " passed\n"; } catch (const std::exception& error) { ++failures; const std::string message(error.what()); - std::cerr << "LDS sentinel budget: " << name << ": " << message.substr(0, message.find("RecompileRequest:")) << '\n'; + std::cerr << "LDS lock budget: " << name << ": " << message.substr(0, message.find("RecompileRequest:")) << '\n'; } } } if (failures != 0) return 1; - std::cout << "LDS sentinel budget tests passed\n"; + std::cout << "LDS lock budget tests passed\n"; return 0; } diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/LdsMixedWidthAtomics.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/LdsMixedWidthAtomics.cpp index 3db59e8190..7c35530d9f 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/LdsMixedWidthAtomics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/LdsMixedWidthAtomics.cpp @@ -192,7 +192,7 @@ std::uint64_t WorkgroupBytes(std::span words) { std::vector pointers; for (std::size_t at = 5; at < words.size();) { const auto count = words[at] >> 16u; - Require(count != 0u && count <= words.size() - at, "lds sentinel budget: malformed SPIR-V"); + Require(count != 0u && count <= words.size() - at, "lds lock budget: malformed SPIR-V"); const auto instruction = words.subspan(at, count); const auto opcode = instruction[0] & 0xffffu; if (opcode == spv::OpTypeInt || opcode == spv::OpTypeArray || opcode == spv::OpTypePointer) types[instruction[1]] = instruction; @@ -203,11 +203,11 @@ std::uint64_t WorkgroupBytes(std::span words) { std::uint64_t total = 0; for (const auto pointer : pointers) { const auto definition = types.at(pointer); - Require((definition[0] & 0xffffu) == spv::OpTypePointer, "lds sentinel budget: Workgroup storage has no pointer type"); + Require((definition[0] & 0xffffu) == spv::OpTypePointer, "lds lock budget: Workgroup storage has no pointer type"); const auto array = types.at(definition[3]); - Require((array[0] & 0xffffu) == spv::OpTypeArray, "lds sentinel budget: unexpected Workgroup allocation shape"); + Require((array[0] & 0xffffu) == spv::OpTypeArray, "lds lock budget: unexpected Workgroup allocation shape"); const auto element = types.at(array[2]); - Require((element[0] & 0xffffu) == spv::OpTypeInt && element[2] == 32u, "lds sentinel budget: Workgroup array is not dwords"); + Require((element[0] & 0xffffu) == spv::OpTypeInt && element[2] == 32u, "lds lock budget: Workgroup array is not dwords"); total += static_cast(constants.at(array[3])) * sizeof(std::uint32_t); } return total; @@ -232,10 +232,10 @@ void Run(AgcDriver::VulkanDevice& device, std::span code, c if (expectedDeviceDwords) { const auto bytes = WorkgroupBytes(result.spirv.Words()); const auto limit = request.target.maxWorkgroupSharedMemoryBytes; - Require(bytes <= limit, "lds sentinel budget: guest LDS, lock and sentinel exceed the device limit before dispatch"); - Require(result.workgroupMemoryDwords == *expectedDeviceDwords, "lds sentinel budget: unexpected device-backed LDS allocation before dispatch"); - if (*expectedDeviceDwords == 0u) Require(bytes == limit, "lds sentinel budget: exact-fit case did not retain its lock and sentinel"); - std::printf("lds sentinel budget: %s declarations verified; guest=%u; workgroup=%llu; device=%u\n", name, ldsDwords * 4u, static_cast(bytes), result.workgroupMemoryDwords); + Require(bytes <= limit, "lds lock budget: guest LDS and lock exceed the device limit before dispatch"); + Require(result.workgroupMemoryDwords == *expectedDeviceDwords, "lds lock budget: unexpected device-backed LDS allocation before dispatch"); + if (*expectedDeviceDwords == 0u) Require(bytes == limit, "lds lock budget: exact-fit case did not retain its lock"); + std::printf("lds lock budget: %s declarations verified; guest=%u; workgroup=%llu; device=%u\n", name, ldsDwords * 4u, static_cast(bytes), result.workgroupMemoryDwords); } for (std::uint32_t run = 0; run < repeats; ++run) { Output.fill(0xdeadbeefu); @@ -243,19 +243,19 @@ void Run(AgcDriver::VulkanDevice& device, std::span code, c device.WaitIdle(); Check(name, operations, run); } - if (expectedDeviceDwords) std::printf("lds sentinel budget: %s GPU readback verified across 256 lanes\n", name); + if (expectedDeviceDwords) std::printf("lds lock budget: %s GPU readback verified across 256 lanes\n", name); } -void SentinelBudget(AgcDriver::VulkanDevice& device) { +void LockBudget(AgcDriver::VulkanDevice& device) { const auto target = device.Target(); const auto limit = target.maxWorkgroupSharedMemoryBytes; - std::printf("lds sentinel budget: robustBufferAccess2=%u; maxWorkgroupSharedMemoryBytes=%u\n", static_cast(target.robustBufferAccess2), limit); - if (std::getenv("ANYPS5_REQUIRE_ROBUST2") != nullptr) Require(target.robustBufferAccess2, "lds sentinel budget: robustBufferAccess2 was not enabled"); - Require(limit >= 32u && limit % 4u == 0u, "lds sentinel budget: unexpected device shared-memory limit"); + std::printf("lds lock budget: robustBufferAccess2=%u; maxWorkgroupSharedMemoryBytes=%u\n", static_cast(target.robustBufferAccess2), limit); + if (std::getenv("ANYPS5_REQUIRE_ROBUST2") != nullptr) Require(target.robustBufferAccess2, "lds lock budget: robustBufferAccess2 was not enabled"); + Require(limit >= 32u && limit % 4u == 0u, "lds lock budget: unexpected device shared-memory limit"); const auto operations = static_cast(Threads) * Iterations; - Run(device, UniformCode, "limit-4", operations, 1, (limit - 4u) / 4u, (limit - 4u) / 4u + 1u); - Run(device, UniformCode, "limit-8", operations, 1, (limit - 8u) / 4u, 0u); - std::puts("lds sentinel budget tests passed"); + Run(device, UniformCode, "limit", operations, 1, limit / 4u, limit / 4u + 1u); + Run(device, UniformCode, "limit-4", operations, 1, (limit - 4u) / 4u, 0u); + std::puts("lds lock budget tests passed"); } } @@ -264,8 +264,8 @@ int main(int argc, char** argv) { try { const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; - if (argc == 2 && std::string_view(argv[1]) == "--sentinel-budget") { - SentinelBudget(*device); + if (argc == 2 && std::string_view(argv[1]) == "--lock-budget") { + LockBudget(*device); return 0; } Require(argc == 1, "lds mixed width atomics: unexpected argument"); diff --git a/core/shader/recompiler/Deserialize.cpp b/core/shader/recompiler/Deserialize.cpp index 61ccd6d5f9..6f1b3a0ca9 100644 --- a/core/shader/recompiler/Deserialize.cpp +++ b/core/shader/recompiler/Deserialize.cpp @@ -167,7 +167,7 @@ int main(int argc, char** argv) { translateOptions.userDataBaseRegister = request.context.userDataBaseRegister; translateOptions.userDataCount = static_cast(request.context.userData.size()); translateOptions.scratchDwords = request.context.compute.has_value() ? request.context.compute->scratchDwords : 0u; - translateOptions.sharedMemoryBytes = request.context.compute.has_value() && static_cast(request.context.compute->ldsSizeDwords) * 4u + 8u > request.target.maxWorkgroupSharedMemoryBytes ? request.context.compute->ldsSizeDwords * 4u : 0u; + translateOptions.sharedMemoryBytes = request.context.compute.has_value() && static_cast(request.context.compute->ldsSizeDwords) * 4u + 4u > request.target.maxWorkgroupSharedMemoryBytes ? request.context.compute->ldsSizeDwords * 4u : 0u; translateOptions.inputInfo = inputInfo; InstructionTranslator translator; diff --git a/core/shader/recompiler/Recompiler.cpp b/core/shader/recompiler/Recompiler.cpp index 2481fb8554..d08bc8a148 100644 --- a/core/shader/recompiler/Recompiler.cpp +++ b/core/shader/recompiler/Recompiler.cpp @@ -123,7 +123,7 @@ ShaderStageInputInfo RequestInputInfo(const RecompileRequest& request) { std::uint32_t DeviceMemoryLdsBytes(const RecompileRequest& request) { if (!request.context.compute.has_value()) return 0u; const auto bytes = static_cast(request.context.compute->ldsSizeDwords) * 4u; - return bytes + 8u > request.target.maxWorkgroupSharedMemoryBytes ? static_cast(bytes) : 0u; + return bytes + 4u > request.target.maxWorkgroupSharedMemoryBytes ? static_cast(bytes) : 0u; } std::atomic frontEndBuilds{0}; diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterHelpers.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterHelpers.hpp index d996faf30e..836b58a04c 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterHelpers.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterHelpers.hpp @@ -118,7 +118,7 @@ std::uint32_t EmitMemoryElementInBounds(SpirvEmitterState& state, const MemoryRe bool InFunctionStorage(const SpirvEmitterState& state, ResourceKind kind); std::uint32_t EmitMemoryElementPointer(SpirvEmitterState& state, const MemoryResourceAccess& access, std::uint32_t index); std::uint32_t EmitValueOrZero(SpirvEmitterState& state, std::uint32_t condition, std::uint32_t value); -std::uint32_t LdsSentinelIndex(const SpirvEmitterState& state); +std::uint32_t LdsStorageDwords(const SpirvEmitterState& state); bool LoadsSelectable(const SpirvEmitterState& state, const MemoryResourceAccess& access); std::uint32_t GuardedElementIndex(SpirvEmitterState& state, const MemoryResourceAccess& access, std::uint32_t condition, std::uint32_t index); std::uint32_t EmitStorageBufferElementPointer(SpirvEmitterState& state, const MemoryResourceAccess& access, std::uint32_t index, std::uint32_t pointerType); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvBufferAccess.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvBufferAccess.cpp index 5fa98f5610..d208290cac 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvBufferAccess.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvBufferAccess.cpp @@ -20,7 +20,7 @@ namespace { if (ShaderWorkgroupInput(state) == nullptr) { FailEmit("function LDS was not prepared before function emission"); } - state.ldsVariable = state.module.DefineGlobalVariable(TypeU32ArrayPointer(state, spv::StorageClassWorkgroup, LdsSentinelIndex(state) + 1u), spv::StorageClassWorkgroup); + state.ldsVariable = state.module.DefineGlobalVariable(TypeU32ArrayPointer(state, spv::StorageClassWorkgroup, LdsStorageDwords(state)), spv::StorageClassWorkgroup); state.module.AddName(state.ldsVariable, "lds_dwords"); } @@ -177,18 +177,19 @@ std::uint32_t EmitValueOrZero(SpirvEmitterState& state, std::uint32_t condition, return Select(state, TypeU32(state), condition, value, ConstantU32(state, 0u)); } -std::uint32_t LdsSentinelIndex(const SpirvEmitterState& state) { +std::uint32_t LdsStorageDwords(const SpirvEmitterState& state) { return LdsDwordCount(state) + (state.requirements.ldsLock ? 1u : 0u); } bool LoadsSelectable(const SpirvEmitterState& state, const MemoryResourceAccess& access) { if (access.kind == ResourceKind::Scratch) return true; - if (access.kind == ResourceKind::Lds) return !LdsInDeviceMemory(state); + if (access.kind == ResourceKind::Lds) return false; return state.robustBufferAccess2; } std::uint32_t GuardedElementIndex(SpirvEmitterState& state, const MemoryResourceAccess& access, std::uint32_t condition, std::uint32_t index) { - const auto fallback = access.kind == ResourceKind::Scratch ? 0u : access.kind == ResourceKind::Lds ? LdsSentinelIndex(state) : 0xfffffffeu; + if (access.kind == ResourceKind::Lds) FailEmit("LDS loads keep their branch and have no guarded element"); + const auto fallback = access.kind == ResourceKind::Scratch ? 0u : 0xfffffffeu; return Select(state, TypeU32(state), condition, index, ConstantU32(state, fallback)); } diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp index cd1f8c8ddc..33ce878077 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp @@ -1146,7 +1146,7 @@ void EmitProgram(SpirvEmitterState& state) { state.module.AddFunction(spv::OpFunction, TypeVoid(state), state.meshGuestFunc != 0u ? state.meshGuestFunc : state.mainFunc, spv::FunctionControlMaskNone, TypeFunction(state)); EmitLabel(state, state.entryLabel); if (state.requirements.functionLds) { - state.module.AddFunction(spv::OpVariable, TypeU32ArrayPointer(state, spv::StorageClassFunction, LdsSentinelIndex(state) + 1u), state.ldsVariable, spv::StorageClassFunction); + state.module.AddFunction(spv::OpVariable, TypeU32ArrayPointer(state, spv::StorageClassFunction, LdsStorageDwords(state)), state.ldsVariable, spv::StorageClassFunction); } if (state.requirements.functionScratch) { for (std::uint32_t half = 0; half < state.laneCount; half++) { diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index 7b7e78f8b8..5ad66baace 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -147,5 +147,4 @@ - [Flag structurization](../../../core/shader/recompiler/ControlFlow/src/Structurizer.cpp) (shader recompiler) - a graph that every structurizer attempt refuses, irreducible and non-terminating graphs included, is rebuilt with one reach flag per block and one again flag per loop (`structurizeWithFlags`): each block keeps one copy and runs behind a guard on its flag, and each strongly connected component becomes a loop whose single latch repeats it while a back edge set its flag. The output has about 8 times the input's blocks and one route variable per block, and ends in an empty Return that never runs. Only blocks that start the top-level order unconditionally run unguarded, so resource planning also sees the static paths that skip a guarded block and can plan extra descriptor candidates for them. The attempts still run first. Shared merge splitting refuses an attempt once the graph has grown past 16 times its block count at the start of splitting (at least 64 blocks); succeeding attempts grew at most 10.6 times on random graphs of up to 128 blocks, and an attempt that would succeed only beyond the budget leaves the graph to a later attempt or to this construction. Splitting that adds one block per round stays within that budget until its 4 rounds per block run out, so the attempts can still take more than 10 s (9 of 2,000 random graphs of up to 128 blocks, 63 s at most). `APS5_FORCE_STRUCTURIZER_FALLBACK=1` builds every graph this way, for tests - [Scalar buffer descriptors](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) use only the base address, stride and record count for resources accessed exclusively by scalar loads; their fourth word is normalized to zero, following AMD RDNA2 ISA section 7.2.2. Vector and mixed accesses retain descriptor validation. Native Vulkan readbacks cover ignored fourth-word fields on Windows/NVIDIA; their console behavior has not been measured. - [Spanned wave64 compute](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvSubgroup.cpp) (shader recompiler) - `APS5_SPANNED_WAVE=1`, off by default, runs a single-wave wave64 compute program at one lane per invocation on a 32-lane host subgroup and exchanges ballots, shuffles and reductions between the two host subgroups through workgroup memory and barriers. The driver turns it on only where compute subgroups can be pinned at 32 full lanes (`subgroupSizeControl` and `computeFullSubgroups`), passes it as `SpirvTarget::spannedWaves` (part of the cache key and the serialized request), and creates the pipelines of spanned programs with `REQUIRE_FULL_SUBGROUPS`, since the lane number `SubgroupId * 32 + SubgroupLocalInvocationId` needs two full subgroups. Those barriers are correct only where every invocation of the workgroup reaches them, which holds for the guest's wave-uniform control flow. The emulated 64-bit LDS atomics ([LockedLdsUpdate](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp)) run only on the lanes the exec mask keeps, so their lock loop ballots within its own host subgroup, as its elect already does. Any other cross-lane exchange emitted inside per-lane control flow would hang a spanned wave in the same way, and the emitter does not reject one: there is no uniformity check. The only check so far is manual: disassemble the module (`spirv-dis`), list the selection and loop constructs that enclose each `OpControlBarrier`, and read the condition of each enclosing selection. In Ghost of Yōtei's program 0x80003af600 (PPSA26344), 108 of 111 barriers sat only in loops on wave-uniform conditions and 3 inside per-invocation conditionals, all in the lock loop. Other programs and titles have not been scanned. An earlier, cheaper ballot (one barrier instead of three) also lost the device for a reason that is not understood, which may be another barrier placement problem. Measured on an RTX 4090 (Windows): an offline replay of that program and two Yōtei runs that previously lost the device (NVIDIA TDR) now run past it, and [LdsMaskWaterfall.cpp](../../../core/libs/prx/libSceAgcDriver/tests/execution/LdsMaskWaterfall.cpp) runs both modes and covers a half-inactive wave. Stray (PPSA02101) on Intel ANV and a Demon's Souls (PPSA01342) request replay showed no fault, before this fix -- [Guest memory load guards](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (shader recompiler) - LDS, scratch and direct storage-buffer loads (`LoadWord`, `LoadSubword`, `LoadWideShared`, `readBuffer` and `EmitRuntimeScalarBufferLoad` in [SpirvRuntimeBuffer.cpp](../../../core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp)) load element 0 when the lane is inactive or the address is out of bounds and select 0 as the result, instead of branching around the load; stores, atomics, formatted and physical-address (BDA) accesses still branch. A branch guard costs four blocks per lane half and the host compiler's time grows with the block count: PPSA11142's merged PSP vertex program (3176 ISA words, wave64) emitted 16,983 blocks and 1,710 conditional branches, 5,907 and 868 with the selects. The unconditional load depends on element 0 being addressable: SPIR-V function and workgroup arrays have at least one element, and a storage buffer with no elements reads out of bounds, which is defined only with `robustBufferAccess` ([VulkanDevice.cpp](../../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) enables it when the device has it). -- [Guest memory load guards](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (shader recompiler) - a guest load whose lane is inactive or whose address is out of bounds is still performed and its result replaced by 0 with `OpSelect`, instead of a selection per guard, when the load can be pointed at a location nothing writes: scratch reads its own element 0; LDS in workgroup or function memory reads a sentinel dword appended after the program's LDS (and after the lock dword), which no bounds-checked write reaches; storage buffers (direct buffers, GDS) read element 0xfffffffe, which `robustBufferAccess2` turns into a zero load with no memory access, so the extra read cannot race with a write the guarded program would not have raced with ([Vulkan memory model](https://docs.vulkan.org/spec/latest/appendices/memorymodel.html#data-race)). Without `robustBufferAccess2` ([VulkanDevice.cpp](../../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) enables it when the device has it), and for LDS kept in device memory, those loads keep their selections; stores, atomics, formatted and physical-address (BDA) accesses always do. The aligned high word of a misaligned-capable buffer read re-reads the low word's own element. A selection guard costs four blocks per lane half and the host compiler's time grows with the block count: PPSA11142's merged PSP vertex program (3176 ISA words, wave64) emitted 16,983 blocks and 1,710 conditional branches, 5,907 and 868 with the selects. `agc_guarded_memory_loads` traces the emitted loads in both device modes. +- [Guest memory load guards](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (shader recompiler) - a guest load whose lane is inactive or whose address is out of bounds is still performed and its result replaced by 0 with `OpSelect`, instead of a selection per guard, when the load can be pointed at a location nothing writes: scratch reads its own element 0; storage buffers (direct buffers, GDS) read element 0xfffffffe, which `robustBufferAccess2` turns into a zero load with no memory access, so the extra read cannot race with a write the guarded program would not have raced with ([Vulkan memory model](https://docs.vulkan.org/spec/latest/appendices/memorymodel.html#data-race)). Without `robustBufferAccess2` ([VulkanDevice.cpp](../../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) enables it when the device has it) those loads keep their selections; LDS loads, stores, atomics, formatted and physical-address (BDA) accesses always do. LDS keeps the branch because NVIDIA's compiler predicates a branched LDS load at no cost, while the select form costs about 2.5 instructions per dword and splits a `ds_read_b128` into four loads (RTX 4060 measurements by @karanvora2599 in #4048); Intel's static counts favour the select only for wide reads, and run time was not measured on either. The aligned high word of a misaligned-capable buffer read re-reads the low word's own element. A selection guard costs four blocks per lane half and the host compiler's time grows with the block count: PPSA11142's merged PSP vertex program (3176 ISA words, wave64) emitted 16,983 blocks and 1,710 conditional branches, 5,907 and 868 with the selects. `agc_guarded_memory_loads` traces the emitted loads in both device modes. From acee01a4e650c795d744d39d7c12e1887066484a Mon Sep 17 00:00:00 2001 From: Mehran Date: Wed, 14 Oct 2026 21:47:28 +0300 Subject: [PATCH 0622/1000] perf(shader): compile resource plan walks instead of interpreting them --- .../libSceAgcDriver/tests/ShaderMemory.cpp | 131 ++++ core/shader/recompiler/CMakeLists.txt | 1 + .../IrMetadata/ResourcePlan.hpp | 9 + .../Optimization/SrtWalker/SrtTape.hpp | 59 ++ .../src/SrtWalker/SrtDescriptorEvaluation.cpp | 97 ++- .../Optimization/src/SrtWalker/SrtTape.cpp | 621 ++++++++++++++++++ core/shader/recompiler/ShaderDiskCache.cpp | 4 +- 7 files changed, 917 insertions(+), 5 deletions(-) create mode 100644 core/shader/recompiler/Optimization/include/Optimization/SrtWalker/SrtTape.hpp create mode 100644 core/shader/recompiler/Optimization/src/SrtWalker/SrtTape.cpp diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 6ad15f3495..d440b57913 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -10,6 +10,7 @@ #include "Optimization/SrtWalker/SrtEvaluator.hpp" #include "Optimization/SrtWalker/SrtInstructionPredicates.hpp" #include "Optimization/SrtWalker/SrtFlatSlotClasses.hpp" +#include "Optimization/SrtWalker/SrtTape.hpp" #include "Optimization/SrtWalker.hpp" #include "IntermediateRepresentation/IrBuilder.hpp" #include "IntermediateRepresentation/IrProgram.hpp" @@ -954,6 +955,135 @@ void verifyGuardedDescriptorSources() { expectFailure([&] { static_cast(compileStore(0u)); }, "incompatible with the static runtime image interface", "guarded images: a store of the wrong dimension behind a taken branch was accepted"); } +bool readDirect(void*, std::uint64_t address, std::uint32_t* value) { + std::memcpy(value, reinterpret_cast(address), sizeof(*value)); + return true; +} + +void verifySrtTape() { + using namespace ShaderRecompiler; + struct Walk { + bool ok = false; + std::vector results; + std::vector flat; + std::vector active; + std::size_t poisoned = 0; + std::uint32_t nullRoots = 0; + std::vector> leaves; + std::vector reads; + std::uint64_t asBase = 0; + std::uint64_t asValue = 0; + std::vector> userDataReads; + bool operator==(const Walk&) const = default; + }; + const auto walk = [](const IrResourcePlan& plan, SrtRuntime runtime, bool tape) { + Walk out; + SrtReadTrace trace; + SrtUserDataUse use; + runtime.readTrace = &trace; + runtime.userDataUse = &use; + const std::vector sources(plan.materializationSources.begin(), plan.materializationSources.end()); + const std::vector clean(plan.cleanFlatSlots.begin(), plan.cleanFlatSlots.end()); + const auto sourceSpan = tape ? std::span(plan.materializationSources) : std::span(sources); + const auto cleanSpan = tape ? std::span(plan.cleanFlatSlots) : std::span(clean); + std::vector poison; + out.ok = Detail::EvaluateRuntimeSourcesImpl(plan, sourceSpan, runtime, out.results, out.flat, true, cleanSpan, out.active, &poison, &out.nullRoots); + out.poisoned = poison.size(); + out.leaves = trace.leaves; + out.reads = trace.otherReads; + std::sort(out.leaves.begin(), out.leaves.end()); + std::sort(out.reads.begin(), out.reads.end()); + out.reads.erase(std::unique(out.reads.begin(), out.reads.end()), out.reads.end()); + out.asBase = use.asBase; + out.asValue = use.asValue; + out.userDataReads = use.reads; + std::sort(out.userDataReads.begin(), out.userDataReads.end()); + out.userDataReads.erase(std::unique(out.userDataReads.begin(), out.userDataReads.end()), out.userDataReads.end()); + return out; + }; + const auto answers = [](const IrResourcePlan& plan, const SrtRuntime& runtime) { + const auto* tape = Detail::SrtTape::For(plan); + require(tape != nullptr, "SRT tape: a complete plan has no tape"); + std::vector results; + std::vector flat; + std::vector active; + return tape->Run(plan, runtime, results, flat, active); + }; + constexpr std::uint32_t Format8888UNorm = 56; + constexpr std::uint32_t Type2D = 9; + struct alignas(256) Texture { std::array bytes{}; }; + static Texture texture; + const auto base = static_cast(reinterpret_cast(texture.bytes.data())); + const std::array valid{static_cast(base >> 8u), static_cast((base >> 40u) & 0xffu) | (Format8888UNorm << 20u) | (3u << 30u), 3u << 14u, 0xfacu | (Type2D << 28u), 0u, 0u, 0u, 0u}; + const std::array invalid{0x12345678u, 0xffffffffu, 0xffffffffu, 0u, 0u, 0u, 0u, 0u}; + std::array output{}; + const auto outputAddress = static_cast(reinterpret_cast(output.data())); + const std::array outputV{static_cast(outputAddress), static_cast((outputAddress >> 32u) & 0xffffu), 16u, 0xfacu}; + std::array srt{}; + const auto srtAddress = static_cast(reinterpret_cast(srt.data())); + const std::array userData{static_cast(srtAddress), static_cast(srtAddress >> 32u)}; + const std::array nullUserData{0u, 0u}; + const std::array capabilities{29u, spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess}; + const std::array extensions{"SPV_KHR_physical_storage_buffer", "SPV_KHR_8bit_storage"}; + const auto requestFor = [&](std::uint64_t address, const std::vector& code) { + RecompileRequest request{}; + request.shader = {ShaderStage::Compute, address, code, 0, {}}; + request.context.waveSize = 32; + request.context.userDataBaseRegister = 0; + request.context.userData = userData; + request.context.compute = ShaderComputeStageInfo{{32u, 1u, 1u}, 0u, {false, false, false}, false, 1u}; + request.target.vulkanVersion = 0x00401000u; + request.target.spirvVersion = 0x00010300u; + request.target.subgroupSize = 32; + request.target.supportedCapabilities = capabilities; + request.target.supportedExtensions = extensions; + request.target.bdaAbiVersion = BdaAbi::Version; + request.target.fragmentShaderBarycentricEnabled = false; + request.layout.pushConstantSizeBytes = 128; + return request; + }; + const auto runtimeFor = [&](std::span words) { + SrtRuntime runtime{words}; + runtime.readSpecializationMemory = readDirect; + return runtime; + }; + + const std::vector guarded{0xf4000400u, 0xfa000050u, 0xf4080500u, 0xfa000020u, 0xf4080600u, 0xfa000040u, 0xbf8cc07fu, 0xbf068010u, 0xbf850006u, 0xf40c0200u, 0xfa000000u, 0xbf8cc07fu, 0xf09c0f08u, 0x00a20000u, 0xbf8c3f70u, 0xe0700000u, 0x80060000u, 0xbf810000u}; + const auto guardedPlan = GetResourcePlan(requestFor(0x24000u, guarded)); + require(!guardedPlan->controlFlow.empty() && guardedPlan->info.images.size() == 1u, "SRT tape: the guarded program has no branch around its image"); + const auto imageActivity = guardedPlan->descriptorSources.size(); + for (const auto& [image, mode] : std::array*, std::uint32_t>, 4>{{{&invalid, 0u}, {&invalid, 1u}, {&valid, 0u}, {&valid, 1u}}}) { + srt.fill(0u); + std::copy(image->begin(), image->end(), srt.begin()); + std::copy(outputV.begin(), outputV.end(), srt.begin() + 16); + srt[20] = mode; + const auto runtime = runtimeFor(userData); + require(answers(*guardedPlan, runtime), "SRT tape: the tape refused the guarded program's walk"); + const auto tape = walk(*guardedPlan, runtime, true); + require(tape == walk(*guardedPlan, runtime, false), "SRT tape: the tape's walk differs from the Evaluator's on the guarded program"); + require(tape.ok && tape.active.size() == imageActivity + 1u && tape.active[imageActivity] == mode, "SRT tape: the image behind the branch has the wrong activity"); + require(tape.asBase != 0u && tape.asValue != 0u && !tape.userDataReads.empty(), "SRT tape: the guarded program's walk reported no user-data use"); + } + + const auto nullRuntime = runtimeFor(nullUserData); + require(!answers(*guardedPlan, nullRuntime), "SRT tape: the tape answered a walk through a null user-data root"); + const auto nullWalk = walk(*guardedPlan, nullRuntime, true); + require(nullWalk == walk(*guardedPlan, nullRuntime, false) && nullWalk.ok && nullWalk.nullRoots != 0u, "SRT tape: a walk through a null user-data root differs from the Evaluator's"); + + std::vector outOfRange{0xf40c0200u, 0xfa000000u, 0xf4080500u, 0xfa000020u, 0xf4080600u, 0xfa000040u, 0xf4000400u, 0xfa000050u, 0xf4080a00u, 0xfa000080u, 0xbf8cc07fu, 0xf42c0814u, 0xfa000000u, 0xbf8cc07fu, 0xbf068010u, 0xbf850008u}; + for (std::uint32_t dword = 0; dword < 8u; dword++) outOfRange.push_back(0xbe800300u | ((8u + dword) << 16u) | (32u + dword)); + outOfRange.insert(outOfRange.end(), {0xf09c0f08u, 0x00a20000u, 0xbf8c3f70u, 0xe0700000u, 0x80060000u, 0xbf810000u}); + srt.fill(0u); + std::copy(valid.begin(), valid.end(), srt.begin()); + std::copy(outputV.begin(), outputV.end(), srt.begin() + 16); + srt[20] = 1u; + std::copy(valid.begin(), valid.end(), srt.begin() + 24); + const auto outOfRangePlan = GetResourcePlan(requestFor(0x25000u, outOfRange)); + const auto runtime = runtimeFor(userData); + require(answers(*outOfRangePlan, runtime), "SRT tape: the tape refused a constant read past its V#"); + require(walk(*outOfRangePlan, runtime, true) == walk(*outOfRangePlan, runtime, false), "SRT tape: a constant read past its V# differs from the Evaluator's"); +} + struct alignas(256) TableTexture { std::array bytes{}; }; @@ -5202,6 +5332,7 @@ int main(int argc, char** argv) { verifyMayEvaluate(); verifyPureFlatSlots(); verifyGuardedDescriptorSources(); + verifySrtTape(); verifyImageTableKeys(); verifyImageTables(); verifyImageTableRegistration(); diff --git a/core/shader/recompiler/CMakeLists.txt b/core/shader/recompiler/CMakeLists.txt index a14e40ce48..062a33420f 100644 --- a/core/shader/recompiler/CMakeLists.txt +++ b/core/shader/recompiler/CMakeLists.txt @@ -121,6 +121,7 @@ function(add_shader_recompiler target runtimeTarget) ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/Optimization/src/SrtWalker/SrtInstructionPredicates.cpp ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/Optimization/src/SrtWalker/SrtPlanBuilder.cpp ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/Optimization/src/SrtWalker/SrtRuntimeValidator.cpp + ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/Optimization/src/SrtWalker/SrtTape.cpp ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/Optimization/src/SrtWalker/SrtWalker.cpp ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/SpirvBackend/src/SpirvModule.cpp diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ResourcePlan.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ResourcePlan.hpp index 4a7f86847a..bdd3727ea1 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ResourcePlan.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ResourcePlan.hpp @@ -9,8 +9,10 @@ #include "IntermediateRepresentation/IrMetadata/ShaderInfo.hpp" #include "IntermediateRepresentation/IrMetadata/ShaderStage.hpp" #include +#include #include #include +#include #include #include #include @@ -151,6 +153,12 @@ struct CompactResourcePlan { std::vector conditions; }; +struct SrtTapeSlot { + std::once_flag once; + std::atomic ready{false}; + std::shared_ptr tape; +}; + struct IrResourcePlan { IrShaderStage stage = IrShaderStage::Unknown; std::uint64_t shaderHash = 0; @@ -177,6 +185,7 @@ struct IrResourcePlan { bool resourceTrackingComplete = false; ShaderInfo info; UniformFillPlan uniformFill; + std::shared_ptr tapeSlot = std::make_shared(); }; } diff --git a/core/shader/recompiler/Optimization/include/Optimization/SrtWalker/SrtTape.hpp b/core/shader/recompiler/Optimization/include/Optimization/SrtWalker/SrtTape.hpp new file mode 100644 index 0000000000..0dcb1a168d --- /dev/null +++ b/core/shader/recompiler/Optimization/include/Optimization/SrtWalker/SrtTape.hpp @@ -0,0 +1,59 @@ +#ifndef CORE_SHADER_RECOMPILIER_OPTIMIZATION_SRTWALKER_SRTTAPE_HPP +#define CORE_SHADER_RECOMPILIER_OPTIMIZATION_SRTWALKER_SRTTAPE_HPP + +#include "IntermediateRepresentation/IrProgram.hpp" +#include "Optimization/SrtWalker.hpp" + +#include +#include +#include +#include + +namespace ShaderRecompiler::Detail { + +class SrtTape { +public: + static const SrtTape* For(const IrResourcePlan& plan); + static bool Enabled(); + static bool Verified(); + + bool Run(const IrResourcePlan& plan, const SrtRuntime& runtime, std::vector& results, std::vector& flat, std::vector& activeSources) const; + + std::size_t Nodes() const { return nodes.size(); } + +private: + friend class SrtTapeCompiler; + static std::shared_ptr Compile(const IrResourcePlan& plan); + enum class Op : std::uint8_t { + Fail, Const, UserData, ShaderBase, Read, ReadConstBuffer, ExtractHalf, AddCarry, ConstructU64, + ConvertF32U32, ConvertU32F32, FPMul32, FPTrunc32, FPIsNan32, FPOrdLessThanEqual32, FPOrdGreaterThanEqual32, + IAdd32, IAdd64, ISub32, ISub64, IMul32, IMul64, SMin32, UMin32, BitwiseAnd32, BitwiseAnd64, BitwiseOr32, BitwiseXor32, BitwiseNot32, + ShiftLeftLogical32, ShiftLeftLogical64, ShiftRightLogical32, ShiftRightLogical64, ShiftRightArithmetic32, ShiftRightArithmetic64, + BitFieldUExtract, BitFieldSExtract, BitFieldInsert, Select, IEqual32, INotEqual32, SLessThan32, ULessThan32, UGreaterThan32, + LogicalAnd, LogicalOr, LogicalXor, LogicalNot + }; + static constexpr std::uint32_t NoNode = 0xffffffffu; + struct Node { + Op op = Op::Fail; + bool clean = false; + bool loaded = false; + std::uint8_t component = 0; + std::uint8_t argumentCount = 0; + std::int32_t baseWord = -1; + std::array arguments{}; + std::uint64_t immediate = 0; + const IrValue* source = nullptr; + }; + struct Frame; + bool evaluate(Frame& frame, std::uint32_t index, std::uint64_t& result) const; + bool compute(Frame& frame, const Node& node, std::uint64_t& result) const; + + std::vector nodes; + std::vector> sourceDwords; + std::vector flatRoots; + std::vector conditionRoots; +}; + +} + +#endif diff --git a/core/shader/recompiler/Optimization/src/SrtWalker/SrtDescriptorEvaluation.cpp b/core/shader/recompiler/Optimization/src/SrtWalker/SrtDescriptorEvaluation.cpp index 19c4663e86..c83724c373 100644 --- a/core/shader/recompiler/Optimization/src/SrtWalker/SrtDescriptorEvaluation.cpp +++ b/core/shader/recompiler/Optimization/src/SrtWalker/SrtDescriptorEvaluation.cpp @@ -1,10 +1,13 @@ #include "Optimization/SrtWalker/SrtDescriptorEvaluation.hpp" #include "Optimization/SrtWalker/SrtEvaluator.hpp" +#include "Optimization/SrtWalker/SrtTape.hpp" #include "prx/libc/include/HostThreadLocal.hpp" #include #include #include +#include +#include #include #include #include @@ -226,9 +229,7 @@ std::string describeDifference(const std::vector& compactResult return "sizes " + std::to_string(compactResults.size()) + "/" + std::to_string(irResults.size()) + " sources, " + std::to_string(compactFlat.size()) + "/" + std::to_string(irFlat.size()) + " flat words"; } -} - -bool EvaluateRuntimeSourcesImpl(const IrResourcePlan& program, std::span sources, const SrtRuntime& runtime, std::vector& results, std::vector& flat, bool evaluateFlat, std::span cleanFlatSlots, std::vector& activeSources, std::vector* poison, std::uint32_t* nullRootReads) { +bool interpretRuntimeSources(const IrResourcePlan& program, std::span sources, const SrtRuntime& runtime, std::vector& results, std::vector& flat, bool evaluateFlat, std::span cleanFlatSlots, std::vector& activeSources, std::vector* poison, std::uint32_t* nullRootReads) { if (program.compact.values.empty()) return evaluateRuntimeSources(program, sources, runtime, results, flat, evaluateFlat, cleanFlatSlots, activeSources, poison, nullRootReads); prefetchCompactPlan(program.compact); const bool evaluated = evaluateRuntimeSources(program, sources, runtime, results, flat, evaluateFlat, cleanFlatSlots, activeSources, poison, nullRootReads); @@ -250,6 +251,96 @@ bool EvaluateRuntimeSourcesImpl(const IrResourcePlan& program, std::spanbegin(), reads->end()); + reads->erase(std::unique(reads->begin(), reads->end()), reads->end()); + } + return tapeReads == interpretedReads; +} + +void verifyTape(const IrResourcePlan& program, const SrtRuntime& runtime, std::vector& results, std::vector& flat, std::vector& activeSources, std::vector* poison, std::uint32_t* nullRootReads, std::size_t leavesBefore, std::size_t readsBefore, const SrtUserDataUse* useBefore) { + SrtReadTrace expectedTrace; + SrtUserDataUse expectedUse = useBefore != nullptr ? *useBefore : SrtUserDataUse {}; + SrtRuntime interpreted = runtime; + interpreted.readTrace = runtime.readTrace != nullptr ? &expectedTrace : nullptr; + interpreted.userDataUse = runtime.userDataUse != nullptr ? &expectedUse : nullptr; + std::vector expectedResults; + std::vector expectedFlat; + std::vector expectedActive; + std::vector expectedPoison; + std::uint32_t expectedNullRoots = 0; + const bool ok = interpretRuntimeSources(program, program.materializationSources, interpreted, expectedResults, expectedFlat, true, program.cleanFlatSlots, expectedActive, &expectedPoison, &expectedNullRoots); + std::string difference; + if (!ok) difference = "the Evaluator failed: " + failureReason(); + else if (expectedResults != results) difference = "descriptors"; + else if (expectedFlat != flat) difference = "flat slots"; + else if (expectedActive != activeSources) difference = "active sources"; + else if (!expectedPoison.empty() || expectedNullRoots != 0u) difference = "poisoned reads"; + else if (auto* trace = runtime.readTrace; trace != nullptr) { + auto leaves = std::vector(trace->leaves.begin() + static_cast(leavesBefore), trace->leaves.end()); + auto reads = std::vector(trace->otherReads.begin() + static_cast(readsBefore), trace->otherReads.end()); + auto expectedLeaves = expectedTrace.leaves; + auto expectedReads = expectedTrace.otherReads; + std::sort(leaves.begin(), leaves.end()); + std::sort(expectedLeaves.begin(), expectedLeaves.end()); + std::sort(reads.begin(), reads.end()); + reads.erase(std::unique(reads.begin(), reads.end()), reads.end()); + std::sort(expectedReads.begin(), expectedReads.end()); + expectedReads.erase(std::unique(expectedReads.begin(), expectedReads.end()), expectedReads.end()); + if (leaves != expectedLeaves) difference = "traced leaf reads"; + else if (reads != expectedReads) difference = "traced reads"; + if (!difference.empty()) { + trace->leaves.resize(leavesBefore); + trace->otherReads.resize(readsBefore); + trace->leaves.insert(trace->leaves.end(), expectedTrace.leaves.begin(), expectedTrace.leaves.end()); + trace->otherReads.insert(trace->otherReads.end(), expectedTrace.otherReads.begin(), expectedTrace.otherReads.end()); + } + } + if (difference.empty() && runtime.userDataUse != nullptr && !sameUserDataUse(*runtime.userDataUse, expectedUse)) difference = "user-data use"; + static std::atomic walks {0}; + static std::atomic differences {0}; + const auto walked = walks.fetch_add(1, std::memory_order_relaxed) + 1; + if (difference.empty()) { + bool decade = walked >= 10; + for (auto rest = walked; decade && rest >= 10; rest /= 10) decade = rest % 10 == 0; + if (decade) std::fprintf(stderr, "[srt-tape] verify: %llu walks, %llu differed\n", static_cast(walked), static_cast(differences.load(std::memory_order_relaxed))); + return; + } + if (differences.fetch_add(1, std::memory_order_relaxed) < 20) std::fprintf(stderr, "[srt-tape] verify: shader %016llx: the tape's walk differs from the Evaluator's in its %s\n", static_cast(program.shaderHash), difference.c_str()); + if (!ok) return; + if (runtime.userDataUse != nullptr) *runtime.userDataUse = std::move(expectedUse); + results = std::move(expectedResults); + flat = std::move(expectedFlat); + activeSources = std::move(expectedActive); + if (poison != nullptr) *poison = std::move(expectedPoison); + if (nullRootReads != nullptr) *nullRootReads = expectedNullRoots; +} + +} + +bool EvaluateRuntimeSourcesImpl(const IrResourcePlan& program, std::span sources, const SrtRuntime& runtime, std::vector& results, std::vector& flat, bool evaluateFlat, std::span cleanFlatSlots, std::vector& activeSources, std::vector* poison, std::uint32_t* nullRootReads) { + const bool planInputs = evaluateFlat && sources.data() == program.materializationSources.data() && sources.size() == program.materializationSources.size() && cleanFlatSlots.data() == program.cleanFlatSlots.data() && cleanFlatSlots.size() == program.cleanFlatSlots.size(); + if (planInputs) { + if (const auto* tape = SrtTape::For(program); tape != nullptr) { + const auto leavesBefore = runtime.readTrace != nullptr ? runtime.readTrace->leaves.size() : 0; + const auto readsBefore = runtime.readTrace != nullptr ? runtime.readTrace->otherReads.size() : 0; + std::optional useBefore; + if (SrtTape::Verified() && runtime.userDataUse != nullptr) useBefore = *runtime.userDataUse; + if (tape->Run(program, runtime, results, flat, activeSources)) { + if (poison != nullptr) poison->clear(); + if (nullRootReads != nullptr) *nullRootReads = 0; + if (SrtTape::Verified()) verifyTape(program, runtime, results, flat, activeSources, poison, nullRootReads, leavesBefore, readsBefore, useBefore ? &*useBefore : nullptr); + return true; + } + } + } + return interpretRuntimeSources(program, sources, runtime, results, flat, evaluateFlat, cleanFlatSlots, activeSources, poison, nullRootReads); +} + const std::string& RuntimeSourceFailureReason() { return failureReason(); } diff --git a/core/shader/recompiler/Optimization/src/SrtWalker/SrtTape.cpp b/core/shader/recompiler/Optimization/src/SrtWalker/SrtTape.cpp new file mode 100644 index 0000000000..5bb86efe50 --- /dev/null +++ b/core/shader/recompiler/Optimization/src/SrtWalker/SrtTape.cpp @@ -0,0 +1,621 @@ +#include "Optimization/SrtWalker/SrtTape.hpp" +#include "Optimization/SrtWalker/SrtAddressArithmetic.hpp" +#include "Optimization/SrtWalker/SrtInstructionPredicates.hpp" +#include "IntermediateRepresentation/IrBuilder.hpp" +#include "prx/libc/include/HostThreadLocal.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace ShaderRecompiler::Detail { + +namespace { + +float float32(std::uint64_t bits) { return std::bit_cast(static_cast(bits)); } + +std::uint64_t float32Bits(float value) { return std::bit_cast(value); } + +constexpr std::uint32_t MainContext = 0; +constexpr std::uint32_t CleanContext = 1; +constexpr std::uint32_t MaxDepth = 512; + +} + +class SrtTapeCompiler { +public: + SrtTapeCompiler(const IrResourcePlan& plan, SrtTape& tape) : plan(plan), tape(tape) { + contexts.push_back({nullptr, false, true}); + contexts.push_back({nullptr, true, false}); + } + + bool Build() { + tape.sourceDwords.assign(plan.descriptorSources.size(), {}); + for (auto& dwords : tape.sourceDwords) dwords.fill(SrtTape::NoNode); + for (const auto index : plan.materializationSources) { + if (index >= plan.descriptorSources.size()) return false; + const auto& source = plan.descriptorSources[index]; + if (source.dwordCount > source.dwords.size()) return false; + for (std::uint32_t dword = 0; dword < source.dwordCount; ++dword) tape.sourceDwords[index][dword] = compile(source.dwords[dword], MainContext, 0); + } + tape.flatRoots.resize(plan.srtReads.size()); + for (std::size_t slot = 0; slot < plan.srtReads.size(); ++slot) { + const auto& read = plan.srtReads[slot]; + const bool clean = read.flatOffset < plan.cleanFlatSlots.size() && plan.cleanFlatSlots[read.flatOffset] != 0u; + tape.flatRoots[slot] = compile(read.value, clean ? CleanContext : MainContext, 0); + } + tape.conditionRoots.assign(plan.controlFlow.size(), SrtTape::NoNode); + for (std::size_t block = 0; block < plan.controlFlow.size(); ++block) { + if (plan.controlFlow[block].condition != nullptr) tape.conditionRoots[block] = compile(plan.controlFlow[block].condition, CleanContext, 0); + } + return !malformed; + } + +private: + using Op = SrtTape::Op; + using Node = SrtTape::Node; + struct Context { + const IrValue* mask; + bool clean; + bool cleanSlots; + }; + static constexpr std::uint32_t Building = 0xfffffffeu; + + std::uint32_t emit(const Node& node) { + tape.nodes.push_back(node); + return static_cast(tape.nodes.size() - 1); + } + + std::uint32_t fail() { + if (failNode == SrtTape::NoNode) failNode = emit({}); + return failNode; + } + + std::uint32_t malformedFail() { + malformed = true; + return fail(); + } + + std::uint32_t constant(std::uint64_t value) { + Node node; + node.op = Op::Const; + node.immediate = value; + return emit(node); + } + + std::uint32_t compile(IrValue* raw, std::uint32_t context, std::uint32_t depth) { + if (raw == nullptr || depth > MaxDepth) return malformedFail(); + IrValue* value = raw->Resolve(); + if (value->HasImmediate()) { + switch (value->Type()) { + case IrType::Bool: return constant(value->ImmediateBool() ? 1u : 0u); + case IrType::U8: return constant(value->ImmediateU8()); + case IrType::U16: return constant(value->ImmediateU16()); + case IrType::U32: return constant(value->ImmediateU32()); + case IrType::U64: return constant(value->ImmediateU64()); + case IrType::F32: return constant(float32Bits(value->ImmediateF32())); + default: return fail(); + } + } + if (value->Opcode() == IrOpcode::Void) return fail(); + const auto mask = contexts[context].mask; + if (mask != nullptr && IsRuntimeSelect(value->Opcode()) && value->ArgumentCount() == 3 && value->Argument(0)->Resolve() == mask) return compile(value->Argument(1), context, depth + 1); + const auto key = std::make_pair(static_cast(value), context); + if (const auto found = built.find(key); found != built.end()) return found->second == Building ? fail() : found->second; + built.emplace(key, Building); + const auto index = build(value, context, depth); + built[key] = index; + return index; + } + + std::uint32_t operation(Op op, IrValue* value, std::uint32_t context, std::uint32_t depth, std::uint8_t count) { + if (value->ArgumentCount() < count) return malformedFail(); + Node node; + node.op = op; + node.argumentCount = count; + for (std::uint8_t index = 0; index < count; ++index) node.arguments[index] = compile(value->Argument(index), context, depth + 1); + return emit(node); + } + + std::uint32_t build(IrValue* value, std::uint32_t context, std::uint32_t depth) { + const auto current = contexts[context]; + switch (value->Opcode()) { + case IrOpcode::GetUserData: { + if (value->ArgumentCount() < 1 || value->Argument(0) == nullptr) return malformedFail(); + const auto reg = RegIndex(static_cast(value->Argument(0)->Register().index)); + if (reg < plan.userDataBase) return fail(); + Node node; + node.op = Op::UserData; + node.immediate = reg - plan.userDataBase; + return emit(node); + } + case IrOpcode::GetShaderBase: { + Node node; + node.op = Op::ShaderBase; + return emit(node); + } + case IrOpcode::Phi: { + IrValue* resolved = ResolveInvariantPhi(plan, value); + return resolved != nullptr ? compile(resolved, context, depth + 1) : fail(); + } + case IrOpcode::ReadFirstLane: { + if (value->ArgumentCount() < 2) return malformedFail(); + IrValue* laneMask = value->Argument(1); + contexts.push_back({laneMask != nullptr ? laneMask->Resolve() : nullptr, current.clean, current.cleanSlots}); + return compile(value->Argument(0), static_cast(contexts.size() - 1), depth + 1); + } + case IrOpcode::BitCastU32F32: + case IrOpcode::BitCastF32U32: + if (value->ArgumentCount() < 1) return malformedFail(); + return compile(value->Argument(0), context, depth + 1); + case IrOpcode::CompositeExtractU64: + case IrOpcode::CompositeExtractU32x2: { + if (value->ArgumentCount() < 2) return malformedFail(); + IrValue* index = value->Argument(1)->Resolve(); + if (!index->HasImmediate() || index->Type() != IrType::U32) return fail(); + const auto component = index->ImmediateU32(); + if (component >= 2u) return fail(); + if (value->Opcode() == IrOpcode::CompositeExtractU64) { + Node node; + node.op = Op::ExtractHalf; + node.component = static_cast(component); + node.argumentCount = 1; + node.arguments[0] = compile(value->Argument(0), context, depth + 1); + return emit(node); + } + IrValue* source = value->Argument(0)->Resolve(); + if (source->Opcode() == IrOpcode::Void) return fail(); + if (source->Opcode() == IrOpcode::CompositeConstructU32x2) { + if (source->ArgumentCount() <= component) return malformedFail(); + return compile(source->Argument(component), context, depth + 1); + } + if (source->Opcode() == IrOpcode::IAddCarry32) { + if (source->ArgumentCount() < 2) return malformedFail(); + Node node; + node.op = Op::AddCarry; + node.component = static_cast(component); + node.argumentCount = 2; + node.arguments[0] = compile(source->Argument(0), context, depth + 1); + node.arguments[1] = compile(source->Argument(1), context, depth + 1); + return emit(node); + } + return fail(); + } + case IrOpcode::CompositeConstructU64: return operation(Op::ConstructU64, value, context, depth, 2); + case IrOpcode::ReadConst: { + if (value->ArgumentCount() < 2) return malformedFail(); + IrValue* slot = value->Argument(1)->Resolve(); + if (!slot->HasImmediate() || slot->Type() != IrType::U32 || slot->ImmediateU32() >= plan.srtReads.size()) return fail(); + const auto index = slot->ImmediateU32(); + const bool clean = current.cleanSlots && index < plan.cleanFlatSlots.size() && plan.cleanFlatSlots[index] != 0u; + return compile(plan.srtReads[index].value, clean ? CleanContext : context, depth + 1); + } + case IrOpcode::LoadAddressU32: + case IrOpcode::ReadConstBuffer: { + if (!IsRawRead(plan, *value)) return fail(); + if (value->ArgumentCount() < 2) return malformedFail(); + if (value->Flags().index >= plan.memoryInfo.size()) return fail(); + const auto& memory = plan.memoryInfo[value->Flags().index]; + IrValue* handle = value->Argument(0)->Resolve(); + if (handle->Opcode() == IrOpcode::Void) return fail(); + if (handle->ArgumentCount() < 2) return malformedFail(); + Node node; + node.op = value->Opcode() == IrOpcode::ReadConstBuffer ? Op::ReadConstBuffer : Op::Read; + node.clean = current.clean; + node.loaded = LoadedFromMemory(*handle); + node.source = value; + if (IrValue* low = handle->Argument(0); low != nullptr) { + low = low->Resolve(); + if (!low->HasImmediate() && low->Opcode() == IrOpcode::GetUserData && low->ArgumentCount() >= 1 && low->Argument(0) != nullptr) { + const auto reg = RegIndex(static_cast(low->Argument(0)->Register().index)); + if (reg >= plan.userDataBase && reg - plan.userDataBase < SrtUserDataUse::TrackedWords) node.baseWord = static_cast(reg - plan.userDataBase); + } + } + const auto immediate = static_cast(static_cast(memory.offset)); + node.immediate = static_cast(immediate); + node.arguments[0] = compile(handle->Argument(0), context, depth + 1); + node.arguments[1] = compile(handle->Argument(1), context, depth + 1); + node.arguments[2] = compile(value->Argument(1), context, depth + 1); + node.argumentCount = 3; + if (node.op == Op::ReadConstBuffer) { + if (handle->ArgumentCount() != 4u || immediate < 0) return fail(); + node.arguments[3] = compile(handle->Argument(2), context, depth + 1); + node.arguments[4] = compile(handle->Argument(3), context, depth + 1); + node.argumentCount = 5; + } + return emit(node); + } + case IrOpcode::IAdd32: return operation(Op::IAdd32, value, context, depth, 2); + case IrOpcode::IAdd64: return operation(Op::IAdd64, value, context, depth, 2); + case IrOpcode::ISub32: return operation(Op::ISub32, value, context, depth, 2); + case IrOpcode::ISub64: return operation(Op::ISub64, value, context, depth, 2); + case IrOpcode::IMul32: return operation(Op::IMul32, value, context, depth, 2); + case IrOpcode::IMul64: return operation(Op::IMul64, value, context, depth, 2); + case IrOpcode::SMin32: return operation(Op::SMin32, value, context, depth, 2); + case IrOpcode::UMin32: return operation(Op::UMin32, value, context, depth, 2); + case IrOpcode::ConvertF32U32: return operation(Op::ConvertF32U32, value, context, depth, 1); + case IrOpcode::ConvertU32F32: return operation(Op::ConvertU32F32, value, context, depth, 1); + case IrOpcode::FPMul32: return operation(Op::FPMul32, value, context, depth, 2); + case IrOpcode::FPTrunc32: return operation(Op::FPTrunc32, value, context, depth, 1); + case IrOpcode::FPIsNan32: return operation(Op::FPIsNan32, value, context, depth, 1); + case IrOpcode::FPOrdLessThanEqual32: return operation(Op::FPOrdLessThanEqual32, value, context, depth, 2); + case IrOpcode::FPOrdGreaterThanEqual32: return operation(Op::FPOrdGreaterThanEqual32, value, context, depth, 2); + case IrOpcode::BitwiseAnd32: return operation(Op::BitwiseAnd32, value, context, depth, 2); + case IrOpcode::BitwiseAnd64: return operation(Op::BitwiseAnd64, value, context, depth, 2); + case IrOpcode::BitwiseOr32: return operation(Op::BitwiseOr32, value, context, depth, 2); + case IrOpcode::BitwiseXor32: return operation(Op::BitwiseXor32, value, context, depth, 2); + case IrOpcode::BitwiseNot32: return operation(Op::BitwiseNot32, value, context, depth, 1); + case IrOpcode::ShiftLeftLogical32: return operation(Op::ShiftLeftLogical32, value, context, depth, 2); + case IrOpcode::ShiftLeftLogical64: return operation(Op::ShiftLeftLogical64, value, context, depth, 2); + case IrOpcode::ShiftRightLogical32: return operation(Op::ShiftRightLogical32, value, context, depth, 2); + case IrOpcode::ShiftRightLogical64: return operation(Op::ShiftRightLogical64, value, context, depth, 2); + case IrOpcode::ShiftRightArithmetic32: return operation(Op::ShiftRightArithmetic32, value, context, depth, 2); + case IrOpcode::ShiftRightArithmetic64: return operation(Op::ShiftRightArithmetic64, value, context, depth, 2); + case IrOpcode::BitFieldUExtract: return operation(Op::BitFieldUExtract, value, context, depth, 3); + case IrOpcode::BitFieldSExtract: return operation(Op::BitFieldSExtract, value, context, depth, 3); + case IrOpcode::BitFieldInsert: return operation(Op::BitFieldInsert, value, context, depth, 4); + case IrOpcode::SelectU32: + case IrOpcode::SelectU1: + case IrOpcode::SelectF32: return operation(Op::Select, value, context, depth, 3); + case IrOpcode::IEqual32: return operation(Op::IEqual32, value, context, depth, 2); + case IrOpcode::INotEqual32: return operation(Op::INotEqual32, value, context, depth, 2); + case IrOpcode::SLessThan32: return operation(Op::SLessThan32, value, context, depth, 2); + case IrOpcode::ULessThan32: return operation(Op::ULessThan32, value, context, depth, 2); + case IrOpcode::UGreaterThan32: return operation(Op::UGreaterThan32, value, context, depth, 2); + case IrOpcode::LogicalAnd: return operation(Op::LogicalAnd, value, context, depth, 2); + case IrOpcode::LogicalOr: return operation(Op::LogicalOr, value, context, depth, 2); + case IrOpcode::LogicalXor: return operation(Op::LogicalXor, value, context, depth, 2); + case IrOpcode::LogicalNot: return operation(Op::LogicalNot, value, context, depth, 1); + default: return fail(); + } + } + + const IrResourcePlan& plan; + SrtTape& tape; + std::vector contexts; + std::map, std::uint32_t> built; + std::uint32_t failNode = SrtTape::NoNode; + bool malformed = false; +}; + +bool SrtTape::Enabled() { + static const bool enabled = std::getenv("APS5_NO_SRT_TAPE") == nullptr && std::getenv("APS5_SRT_DEBUG") == nullptr; + return enabled; +} + +bool SrtTape::Verified() { + static const bool verified = std::getenv("APS5_VERIFY_SRT_TAPE") != nullptr; + return verified; +} + +std::shared_ptr SrtTape::Compile(const IrResourcePlan& plan) { + if (!plan.srtPlanComplete) return nullptr; + auto tape = std::make_shared(); + SrtTapeCompiler compiler(plan, *tape); + if (!compiler.Build()) return nullptr; + return tape; +} + +const SrtTape* SrtTape::For(const IrResourcePlan& plan) { + auto* slot = plan.tapeSlot.get(); + if (slot == nullptr || !Enabled()) return nullptr; + if (!slot->ready.load(std::memory_order_acquire)) { + std::call_once(slot->once, [&] { + try { + slot->tape = Compile(plan); + } catch (...) { + slot->tape = nullptr; + } + }); + slot->ready.store(true, std::memory_order_release); + } + return static_cast(slot->tape.get()); +} + +struct SrtTape::Frame { + const SrtRuntime& runtime; + std::uint64_t* values; + std::uint8_t* states; + bool nullRoot = false; +}; + +bool SrtTape::evaluate(Frame& frame, std::uint32_t index, std::uint64_t& result) const { + const auto state = frame.states[index]; + if (state == 1) { + result = frame.values[index]; + return true; + } + if (state == 2) return false; + std::uint64_t value = 0; + if (!compute(frame, nodes[index], value)) { + frame.states[index] = 2; + return false; + } + frame.states[index] = 1; + frame.values[index] = value; + result = value; + return true; +} + +bool SrtTape::compute(Frame& frame, const Node& node, std::uint64_t& result) const { + std::array operand{}; + std::int32_t origin = -1; + for (std::uint8_t index = 0; index < node.argumentCount; ++index) { + if (index == 0 && node.baseWord >= 0 && frame.runtime.userDataUse != nullptr && static_cast(node.baseWord) < frame.runtime.userData.size()) { + origin = node.baseWord; + operand[0] = frame.runtime.userData[static_cast(origin)]; + frame.runtime.userDataUse->asBase |= std::uint64_t {1} << origin; + continue; + } + if (!evaluate(frame, node.arguments[index], operand[index])) return false; + } + const auto a = operand[0]; + const auto b = operand[1]; + const auto c = operand[2]; + const auto d = operand[3]; + switch (node.op) { + case Op::Fail: return false; + case Op::Const: result = node.immediate; return true; + case Op::UserData: + if (node.immediate >= frame.runtime.userData.size()) return false; + if (frame.runtime.userDataUse != nullptr) frame.runtime.userDataUse->asValue |= node.immediate < SrtUserDataUse::TrackedWords ? std::uint64_t {1} << node.immediate : ~std::uint64_t {0}; + result = frame.runtime.userData[static_cast(node.immediate)]; + return true; + case Op::ShaderBase: result = frame.runtime.shaderBase; return true; + case Op::Read: + case Op::ReadConstBuffer: { + const auto base = ((b << 32u) | static_cast(a)) & AddressMask; + const auto immediate = static_cast(node.immediate); + std::uint64_t address = 0; + if (node.op == Op::ReadConstBuffer) { + const auto records = d; + const auto byteOffset = static_cast(immediate) + static_cast(c); + const auto aligned = byteOffset & ~std::uint64_t {3}; + const auto stride = (static_cast(b) >> 16u) & 0x3fffu; + const auto size = stride == 0u ? static_cast(static_cast(records)) : static_cast(stride) * static_cast(records); + if (aligned > size || size - aligned < sizeof(std::uint32_t)) { + result = 0; + return true; + } + address = ((base & ~std::uint64_t {3}) + byteOffset) & ~std::uint64_t {3}; + } else { + const auto relative = (immediate & ~std::int64_t {3}) + static_cast(static_cast(c) & ~3u); + if (!AddSignedAddress(base & ~std::uint64_t {3}, relative, address)) return false; + } + if (base == 0u && !node.loaded) { + frame.nullRoot = true; + return false; + } + if (frame.runtime.userDataUse != nullptr) frame.runtime.userDataUse->reads.emplace_back(address, origin); + if (node.loaded && frame.runtime.accessible != nullptr && !frame.runtime.accessible(frame.runtime.userContext, address, sizeof(std::uint32_t))) return false; + if (auto* trace = frame.runtime.readTrace; trace != nullptr) { + if (node.source == trace->leaf) trace->leaves.emplace_back(trace->leafSlot, address); + else trace->otherReads.push_back(address); + } + const auto reader = node.clean ? frame.runtime.readSpecializationMemory : frame.runtime.readMemory; + std::uint32_t word = 0; + if (reader != nullptr) { + if (!reader(frame.runtime.userContext, address, &word)) return false; + } else { + std::memcpy(&word, reinterpret_cast(address), sizeof(word)); + } + result = word; + return true; + } + case Op::ExtractHalf: result = static_cast(a >> (node.component * 32u)); return true; + case Op::AddCarry: { + const auto sum = static_cast(static_cast(a)) + static_cast(b); + result = node.component == 0u ? static_cast(sum) : static_cast(sum >> 32u); + return true; + } + case Op::ConstructU64: result = static_cast(a) | (static_cast(static_cast(b)) << 32u); return true; + case Op::ConvertF32U32: result = float32Bits(static_cast(static_cast(a))); return true; + case Op::ConvertU32F32: { + const auto value = float32(a); + if (!std::isfinite(value) || value < 0.0f || static_cast(value) > 4294967295.0) return false; + result = static_cast(value); + return true; + } + case Op::FPMul32: result = float32Bits(float32(a) * float32(b)); return true; + case Op::FPTrunc32: result = float32Bits(std::trunc(float32(a))); return true; + case Op::FPIsNan32: result = std::isnan(float32(a)) ? 1u : 0u; return true; + case Op::FPOrdLessThanEqual32: result = float32(a) <= float32(b) ? 1u : 0u; return true; + case Op::FPOrdGreaterThanEqual32: result = float32(a) >= float32(b) ? 1u : 0u; return true; + case Op::IAdd32: result = static_cast(a + b); return true; + case Op::IAdd64: result = a + b; return true; + case Op::ISub32: result = static_cast(a - b); return true; + case Op::ISub64: result = a - b; return true; + case Op::IMul32: result = static_cast(a * b); return true; + case Op::IMul64: result = a * b; return true; + case Op::SMin32: result = static_cast(std::min(std::bit_cast(static_cast(a)), std::bit_cast(static_cast(b)))); return true; + case Op::UMin32: result = std::min(static_cast(a), static_cast(b)); return true; + case Op::BitwiseAnd32: result = static_cast(a & b); return true; + case Op::BitwiseAnd64: result = a & b; return true; + case Op::BitwiseOr32: result = static_cast(a | b); return true; + case Op::BitwiseXor32: result = static_cast(a ^ b); return true; + case Op::BitwiseNot32: result = ~static_cast(a); return true; + case Op::ShiftLeftLogical32: result = static_cast(a) << (b & 31u); return true; + case Op::ShiftLeftLogical64: result = a << (b & 63u); return true; + case Op::ShiftRightLogical32: result = static_cast(a) >> (b & 31u); return true; + case Op::ShiftRightLogical64: result = a >> (b & 63u); return true; + case Op::ShiftRightArithmetic32: result = static_cast(std::bit_cast(static_cast(a)) >> (b & 31u)); return true; + case Op::ShiftRightArithmetic64: result = static_cast(std::bit_cast(a) >> (b & 63u)); return true; + case Op::BitFieldUExtract: { + const auto offset = static_cast(b); + const auto width = static_cast(c); + if (offset > 32u || width > 32u - offset) return false; + const auto mask = width == 32u ? 0xffffffffu : width == 0u ? 0u : (std::uint32_t {1} << width) - 1u; + result = width == 0u ? 0u : (static_cast(a) >> offset) & mask; + return true; + } + case Op::BitFieldSExtract: { + const auto offset = static_cast(b); + const auto width = static_cast(c); + if (offset > 32u || width > 32u - offset) return false; + if (width == 0u) { + result = 0; + return true; + } + const auto mask = width == 32u ? 0xffffffffu : (std::uint32_t {1} << width) - 1u; + auto bits = (static_cast(a) >> offset) & mask; + if (width < 32u && (bits & (std::uint32_t {1} << (width - 1u))) != 0u) bits |= ~mask; + result = bits; + return true; + } + case Op::BitFieldInsert: { + const auto offset = static_cast(c); + const auto width = static_cast(d); + if (offset > 32u || width > 32u - offset) return false; + if (width == 0u) { + result = static_cast(a); + return true; + } + const auto mask = width == 32u ? 0xffffffffu : ((std::uint32_t {1} << width) - 1u) << offset; + result = (static_cast(a) & ~mask) | ((static_cast(b) << offset) & mask); + return true; + } + case Op::Select: result = a != 0u ? b : c; return true; + case Op::IEqual32: result = static_cast(a) == static_cast(b) ? 1u : 0u; return true; + case Op::INotEqual32: result = static_cast(a) != static_cast(b) ? 1u : 0u; return true; + case Op::SLessThan32: result = std::bit_cast(static_cast(a)) < std::bit_cast(static_cast(b)) ? 1u : 0u; return true; + case Op::ULessThan32: result = static_cast(a) < static_cast(b) ? 1u : 0u; return true; + case Op::UGreaterThan32: result = static_cast(a) > static_cast(b) ? 1u : 0u; return true; + case Op::LogicalAnd: result = (a != 0u) && (b != 0u) ? 1u : 0u; return true; + case Op::LogicalOr: result = (a != 0u) || (b != 0u) ? 1u : 0u; return true; + case Op::LogicalXor: result = (a != 0u) != (b != 0u) ? 1u : 0u; return true; + case Op::LogicalNot: result = a == 0u ? 1u : 0u; return true; + } + return false; +} + +bool SrtTape::Run(const IrResourcePlan& plan, const SrtRuntime& runtime, std::vector& results, std::vector& flat, std::vector& activeSources) const { + if (!plan.srtPlanComplete) return false; + if (std::any_of(plan.cleanFlatSlots.begin(), plan.cleanFlatSlots.end(), [](std::uint8_t clean) { return clean != 0u; }) && runtime.readSpecializationMemory == nullptr) return false; + auto* trace = runtime.readTrace; + const auto leavesBefore = trace != nullptr ? trace->leaves.size() : 0; + const auto readsBefore = trace != nullptr ? trace->otherReads.size() : 0; + auto* use = runtime.userDataUse; + const auto baseBefore = use != nullptr ? use->asBase : 0; + const auto valueBefore = use != nullptr ? use->asValue : 0; + const auto useReadsBefore = use != nullptr ? use->reads.size() : 0; + const auto refuse = [&] { + if (trace != nullptr) { + trace->leaves.resize(leavesBefore); + trace->otherReads.resize(readsBefore); + trace->leaf = nullptr; + } + if (use != nullptr) { + use->asBase = baseBefore; + use->asValue = valueBefore; + use->reads.resize(useReadsBefore); + } + return false; + }; + constexpr std::size_t InlineNodes = 512; + std::array inlineValues; + std::array inlineStates; + std::vector heapValues; + std::vector heapStates; + std::uint64_t* values = inlineValues.data(); + std::uint8_t* states = inlineStates.data(); + if (nodes.size() > InlineNodes) { + heapValues.resize(nodes.size()); + heapStates.assign(nodes.size(), 0u); + values = heapValues.data(); + states = heapStates.data(); + } else { + std::fill_n(states, nodes.size(), std::uint8_t {0}); + } + Frame frame {runtime, values, states}; + + struct Scratch { + std::vector active; + std::vector visited; + std::vector pending; + std::vector evaluated; + std::vector flattened; + }; + struct ScratchStorage {}; + auto& scratch = HostThreadLocal(); + auto& active = scratch.active; + const auto imageBase = plan.descriptorSources.size(); + active.assign(imageBase + plan.info.images.size(), 1u); + if (!plan.controlFlow.empty()) { + for (const auto& block : plan.controlFlow) { + for (const auto source : block.sources) { + if (source >= active.size()) return refuse(); + active[source] = 0u; + } + for (const auto image : block.images) { + if (image >= plan.info.images.size()) return refuse(); + active[imageBase + image] = 0u; + } + } + auto& visited = scratch.visited; + visited.assign(plan.controlFlow.size(), 0u); + auto& pending = scratch.pending; + pending.assign(1, 0u); + while (!pending.empty()) { + const auto index = pending.back(); + pending.pop_back(); + if (index >= visited.size()) return refuse(); + if (visited[index]) continue; + visited[index] = 1u; + const auto& block = plan.controlFlow[index]; + for (const auto source : block.sources) active[source] = 1u; + for (const auto image : block.images) active[imageBase + image] = 1u; + std::uint64_t condition = 0; + const bool cleanEvaluable = block.condition != nullptr && runtime.readSpecializationMemory != nullptr && conditionRoots[index] != NoNode && evaluate(frame, conditionRoots[index], condition); + if (frame.nullRoot) return refuse(); + if (cleanEvaluable) { + if (block.successors.size() < 2) return refuse(); + pending.push_back(block.successors[static_cast(condition) != 0u ? 0u : 1u]); + } else { + pending.insert(pending.end(), block.successors.begin(), block.successors.end()); + } + } + } + auto& evaluated = scratch.evaluated; + evaluated.clear(); + evaluated.reserve(plan.materializationSources.size()); + for (const auto sourceIndex : plan.materializationSources) { + if (sourceIndex >= plan.descriptorSources.size()) return refuse(); + const auto& source = plan.descriptorSources[sourceIndex]; + DescriptorValue value; + value.dwordCount = source.dwordCount; + if (active[sourceIndex]) { + for (std::uint32_t dword = 0; dword < source.dwordCount; ++dword) { + std::uint64_t word = 0; + if (!evaluate(frame, sourceDwords[sourceIndex][dword], word)) return refuse(); + value.dwords[dword] = static_cast(word); + } + } + evaluated.push_back(value); + } + auto& flattened = scratch.flattened; + flattened.assign(plan.srtReads.size(), 0u); + for (std::size_t slot = 0; slot < plan.srtReads.size(); ++slot) { + const auto& read = plan.srtReads[slot]; + const bool pure = trace != nullptr && read.flatOffset < plan.pureFlatSlots.size() && plan.pureFlatSlots[read.flatOffset] != 0u; + if (pure) { + trace->leaf = read.value->Resolve(); + trace->leafSlot = read.flatOffset; + } + std::uint64_t word = 0; + const bool ok = read.flatOffset < flattened.size() && evaluate(frame, flatRoots[slot], word); + if (pure) trace->leaf = nullptr; + if (!ok) return refuse(); + flattened[read.flatOffset] = static_cast(word); + } + results.swap(evaluated); + activeSources.swap(active); + flat.assign(flattened.begin(), flattened.end()); + return true; +} + +} diff --git a/core/shader/recompiler/ShaderDiskCache.cpp b/core/shader/recompiler/ShaderDiskCache.cpp index 2e1cdbbfcf..7db08dc884 100644 --- a/core/shader/recompiler/ShaderDiskCache.cpp +++ b/core/shader/recompiler/ShaderDiskCache.cpp @@ -104,7 +104,7 @@ static_assert(sizeof(IrDescriptorBinding) == 32, "IrDescriptorBinding changed: u static_assert(sizeof(BindingAllocationResult) == 184, "BindingAllocationResult changed: update the allocation encoder"); static_assert(sizeof(CompiledBindingLayout) == 88, "CompiledBindingLayout changed: update the allocation encoder"); static_assert(sizeof(BindingLayout) == 16, "BindingLayout changed: update BuildKey"); -static_assert(sizeof(IrResourcePlan) == 712, "IrResourcePlan changed: update EncodePlan and DecodePlan"); +static_assert(sizeof(IrResourcePlan) == 728, "IrResourcePlan changed: update EncodePlan and DecodePlan"); static_assert(sizeof(IrValue) == 152, "IrValue changed: update the plan value encoder"); static_assert(sizeof(GuestRegister) == 8, "GuestRegister changed: update the plan value encoder"); static_assert(sizeof(MemoryInfo) == 80, "MemoryInfo changed: update the plan encoder"); @@ -145,7 +145,7 @@ static_assert(fieldCount() == 8, "SamplerResource changed: upda static_assert(fieldCount() == 3, "SampledResourcePair changed: update the info encoder"); static_assert(fieldCount() == 5, "StageInput changed: update the info encoder"); static_assert(fieldCount() == 4, "StageOutput changed: update the info encoder"); -static_assert(fieldCount() == 23, "IrResourcePlan changed: update EncodePlan and DecodePlan"); +static_assert(fieldCount() == 24, "IrResourcePlan changed: update EncodePlan and DecodePlan"); static_assert(fieldCount() == 2, "GuestRegister changed: update the plan value encoder"); static_assert(fieldCount() == 31, "MemoryInfo changed: update the plan encoder"); static_assert(fieldCount() == 3, "DescriptorSource changed: update the plan encoder"); From f4a33b7960a05ea91f0a8a4dab3b37e45ed0c716 Mon Sep 17 00:00:00 2001 From: julienlgy Date: Thu, 8 Oct 2026 18:44:43 +0200 Subject: [PATCH 0623/1000] fix(libSceRazorCpu): add sceRazorCpuNamedSync export Resolve the XCuZoBSVFG8 import required by Killzone Liberation by adding sceRazorCpuNamedSync as a no-op returning success when capture is inactive. Extend the RazorCpu behavior test and document the NID source and unverified console signature and capture behavior. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- core/libs/prx/libSceRazorCpu/Export.cpp | 4 ++++ core/libs/tests/GuestRazorCpu.cpp | 2 ++ docs/dev/debt/libSceRazorCpu.md | 1 + 3 files changed, 7 insertions(+) diff --git a/core/libs/prx/libSceRazorCpu/Export.cpp b/core/libs/prx/libSceRazorCpu/Export.cpp index 23369d1158..fd54971af7 100644 --- a/core/libs/prx/libSceRazorCpu/Export.cpp +++ b/core/libs/prx/libSceRazorCpu/Export.cpp @@ -31,6 +31,10 @@ int APS5_VABI sceRazorCpuPushMarker(const char* label, uint32_t color, uint32_t return 0; } +int APS5_VABI sceRazorCpuNamedSync(void) { + return 0; +} + int APS5_VABI sceRazorCpuPushMarkerStatic(const char* name, uint32_t color, uint32_t flags) { (void)name; (void)color; diff --git a/core/libs/tests/GuestRazorCpu.cpp b/core/libs/tests/GuestRazorCpu.cpp index 85f04438fc..a2e64e544c 100644 --- a/core/libs/tests/GuestRazorCpu.cpp +++ b/core/libs/tests/GuestRazorCpu.cpp @@ -8,6 +8,7 @@ int APS5_VABI sceRazorCpuJobManagerDispatch(const void* args); int APS5_VABI sceRazorCpuJobManagerJob(const void* args); int APS5_VABI sceRazorCpuJobManagerSequence(const void* args); int APS5_VABI sceRazorCpuPushMarker(const char* label, std::uint32_t color, std::uint32_t flags); +int APS5_VABI sceRazorCpuNamedSync(void); int APS5_VABI sceRazorCpuPushMarkerStatic(const char* name, std::uint32_t color, std::uint32_t flags); int APS5_VABI sceRazorCpuPopMarker(void); int APS5_VABI sceRazorCpuFlushOccurred(std::uint64_t* timeSpentInFlush); @@ -27,6 +28,7 @@ int main() { Require(sceRazorCpuJobManagerDispatch(nullptr) == 0); Require(sceRazorCpuJobManagerJob(nullptr) == 0); Require(sceRazorCpuJobManagerSequence(nullptr) == 0); + Require(sceRazorCpuNamedSync() == 0); Require(sceRazorCpuPushMarkerStatic("outer", 0x80ffffffu, 2) == 0); Require(sceRazorCpuPushMarkerStatic("inner", 0x80ffffffu, 2) == 0); Require(sceRazorCpuPopMarker() == 0); diff --git a/docs/dev/debt/libSceRazorCpu.md b/docs/dev/debt/libSceRazorCpu.md index 0c42c424a8..19f0f0b6d7 100644 --- a/docs/dev/debt/libSceRazorCpu.md +++ b/docs/dev/debt/libSceRazorCpu.md @@ -10,3 +10,4 @@ - [sceRazorCpuFlushOccurred](../../../core/libs/prx/libSceRazorCpu/Export.cpp) (libSceRazorCpu) - signature and return values (1 = flush since the last call, 0 = none) from [SharpProspero](https://github.com/SvenGDK/SharpProspero); without a capture nothing is flushed, so it returns 0 and writes 0 cycles to the optional out pointer (whether the real library writes it when nothing was flushed is unknown) - [sceRazorCpuPushMarkerStatic, sceRazorCpuPopMarker](../../../core/libs/prx/libSceRazorCpu/Export.cpp) (libSceRazorCpu) - signatures from PPSA26344's calls; (name, color, flags) assumed for the push; both do nothing without a capture - [sceRazorCpuPlotValue, sceRazorCpuWriteBookmark](../../../core/libs/prx/libSceRazorCpu/Export.cpp) (libSceRazorCpu) - signatures (series, float value) and (label, optional description) from [SharpProspero](https://github.com/SvenGDK/SharpProspero), matching PPSA06320's calls; both do nothing without a capture and return 0 +- [sceRazorCpuNamedSync](../../../core/libs/prx/libSceRazorCpu/Export.cpp) (libSceRazorCpu) - NID `XCuZoBSVFG8` is named by [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0fe263a4760dfbfa973366890061749b4af0de97/src/core/libraries/razor_cpu/razor_cpu.cpp); its stub takes no arguments and returns 0, as here without a capture. The console signature and capture behavior are unverified From b35caffd12723b1a2ef35937b95a2d174531eaf8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Fri, 9 Oct 2026 23:31:41 +0200 Subject: [PATCH 0624/1000] fix(libSceRazorCpu): take the label argument of sceRazorCpuNamedSync --- core/libs/prx/libSceRazorCpu/Export.cpp | 3 ++- core/libs/tests/GuestRazorCpu.cpp | 4 ++-- docs/dev/debt/libSceRazorCpu.md | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceRazorCpu/Export.cpp b/core/libs/prx/libSceRazorCpu/Export.cpp index fd54971af7..529be85908 100644 --- a/core/libs/prx/libSceRazorCpu/Export.cpp +++ b/core/libs/prx/libSceRazorCpu/Export.cpp @@ -31,7 +31,8 @@ int APS5_VABI sceRazorCpuPushMarker(const char* label, uint32_t color, uint32_t return 0; } -int APS5_VABI sceRazorCpuNamedSync(void) { +int APS5_VABI sceRazorCpuNamedSync(const char* label) { + (void)label; return 0; } diff --git a/core/libs/tests/GuestRazorCpu.cpp b/core/libs/tests/GuestRazorCpu.cpp index a2e64e544c..19ac107f19 100644 --- a/core/libs/tests/GuestRazorCpu.cpp +++ b/core/libs/tests/GuestRazorCpu.cpp @@ -8,7 +8,7 @@ int APS5_VABI sceRazorCpuJobManagerDispatch(const void* args); int APS5_VABI sceRazorCpuJobManagerJob(const void* args); int APS5_VABI sceRazorCpuJobManagerSequence(const void* args); int APS5_VABI sceRazorCpuPushMarker(const char* label, std::uint32_t color, std::uint32_t flags); -int APS5_VABI sceRazorCpuNamedSync(void); +int APS5_VABI sceRazorCpuNamedSync(const char* label); int APS5_VABI sceRazorCpuPushMarkerStatic(const char* name, std::uint32_t color, std::uint32_t flags); int APS5_VABI sceRazorCpuPopMarker(void); int APS5_VABI sceRazorCpuFlushOccurred(std::uint64_t* timeSpentInFlush); @@ -28,7 +28,7 @@ int main() { Require(sceRazorCpuJobManagerDispatch(nullptr) == 0); Require(sceRazorCpuJobManagerJob(nullptr) == 0); Require(sceRazorCpuJobManagerSequence(nullptr) == 0); - Require(sceRazorCpuNamedSync() == 0); + Require(sceRazorCpuNamedSync("frame") == 0); Require(sceRazorCpuPushMarkerStatic("outer", 0x80ffffffu, 2) == 0); Require(sceRazorCpuPushMarkerStatic("inner", 0x80ffffffu, 2) == 0); Require(sceRazorCpuPopMarker() == 0); diff --git a/docs/dev/debt/libSceRazorCpu.md b/docs/dev/debt/libSceRazorCpu.md index 19f0f0b6d7..30115eb282 100644 --- a/docs/dev/debt/libSceRazorCpu.md +++ b/docs/dev/debt/libSceRazorCpu.md @@ -10,4 +10,4 @@ - [sceRazorCpuFlushOccurred](../../../core/libs/prx/libSceRazorCpu/Export.cpp) (libSceRazorCpu) - signature and return values (1 = flush since the last call, 0 = none) from [SharpProspero](https://github.com/SvenGDK/SharpProspero); without a capture nothing is flushed, so it returns 0 and writes 0 cycles to the optional out pointer (whether the real library writes it when nothing was flushed is unknown) - [sceRazorCpuPushMarkerStatic, sceRazorCpuPopMarker](../../../core/libs/prx/libSceRazorCpu/Export.cpp) (libSceRazorCpu) - signatures from PPSA26344's calls; (name, color, flags) assumed for the push; both do nothing without a capture - [sceRazorCpuPlotValue, sceRazorCpuWriteBookmark](../../../core/libs/prx/libSceRazorCpu/Export.cpp) (libSceRazorCpu) - signatures (series, float value) and (label, optional description) from [SharpProspero](https://github.com/SvenGDK/SharpProspero), matching PPSA06320's calls; both do nothing without a capture and return 0 -- [sceRazorCpuNamedSync](../../../core/libs/prx/libSceRazorCpu/Export.cpp) (libSceRazorCpu) - NID `XCuZoBSVFG8` is named by [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0fe263a4760dfbfa973366890061749b4af0de97/src/core/libraries/razor_cpu/razor_cpu.cpp); its stub takes no arguments and returns 0, as here without a capture. The console signature and capture behavior are unverified +- [sceRazorCpuNamedSync](../../../core/libs/prx/libSceRazorCpu/Export.cpp) (libSceRazorCpu) - NID `XCuZoBSVFG8` is named by [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0fe263a4760dfbfa973366890061749b4af0de97/src/core/libraries/razor_cpu/razor_cpu.cpp); the NID and a stub returning 0 come from there, but its stub declares no arguments; the `const char* label` parameter (NUL-terminated, up to 16384 bytes) comes from [SharpProspero](https://github.com/SvenGDK/SharpProspero), which documents it as the frame-boundary label. Without a capture it does nothing and returns 0; the label is not read, and the capture behavior and the real return values are unverified From ea33174d7dc18041dbce83e9093d332110cbaa8b Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Sat, 17 Oct 2026 14:38:28 +1300 Subject: [PATCH 0625/1000] fix(tests): end Windows test processes through libc before DLL detach On Windows a test that returns from main ends through exit and ExitProcess, which kills every other thread before the prx static destructors run at DLL_PROCESS_DETACH. Detached host threads (the driver's async log thread, libkernel's file watch thread, ...) die holding winpthreads and queue locks, and a destructor that then locks a mutex, waits for the dead logger or destroys a condition variable never returns: the test prints its last line and hangs until the CTest timeout (#6791). Every test executable now wraps main on MinGW. After main returns the wrapper runs the executable's own exit handlers and static destructors (_cexit), as exit does today, flushes the streams and ends the process with LibcTerminate_nid_no_patch, the path the guest exit already takes. That flushes the driver log while its thread is alive and calls TerminateProcess, so no DLL_PROCESS_DETACH work runs after the threads are gone. The libc shutdown callbacks are not run, as on a plain return from main on Linux. A test that does not load libc.prx returns as before. --- CMakeLists.txt | 5 +++++ cmake/WindowsTestExit.cpp | 31 +++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+) create mode 100644 cmake/WindowsTestExit.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 85d556c961..2f79844c69 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,10 +6,15 @@ endif() project(Relinker CXX) set(ANYPS5_TEST_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/tests") +set(ANYPS5_WINDOWS_TEST_EXIT "${CMAKE_CURRENT_SOURCE_DIR}/cmake/WindowsTestExit.cpp") function(add_test_executable target) add_executable(${target} ${ARGN}) set_target_properties(${target} PROPERTIES RUNTIME_OUTPUT_DIRECTORY "$<1:${ANYPS5_TEST_OUTPUT_DIRECTORY}>") + if(MINGW) + target_sources(${target} PRIVATE "${ANYPS5_WINDOWS_TEST_EXIT}") + target_link_options(${target} PRIVATE LINKER:--wrap=main) + endif() endfunction() option(BUILD_TESTING "Build and register relinker tests" OFF) diff --git a/cmake/WindowsTestExit.cpp b/cmake/WindowsTestExit.cpp new file mode 100644 index 0000000000..abe70fd2c7 --- /dev/null +++ b/cmake/WindowsTestExit.cpp @@ -0,0 +1,31 @@ +#include +#include +#include +#include +#include + +extern "C" int __real_main(int argc, char** argv); + +namespace { + +template +TFunction LibcFunction(const char* name) { + const HMODULE libc = GetModuleHandleW(L"libc.prx"); + if (libc == nullptr) return nullptr; + return reinterpret_cast(reinterpret_cast(GetProcAddress(libc, name))); +} + +} + +extern "C" int __wrap_main(int argc, char** argv) { + const int code = __real_main(argc, argv); + const auto terminate = LibcFunction("LibcTerminate_nid_no_patch"); + if (terminate == nullptr) return code; + _cexit(); + std::cout.flush(); + std::clog.flush(); + std::cerr.flush(); + std::fflush(nullptr); + terminate(code); + return code; +} From 487b9fd4e4885867eca402dc001ffeb1ea4c717c Mon Sep 17 00:00:00 2001 From: Get-fix <100056769+Get-fix@users.noreply.github.com> Date: Sun, 11 Oct 2026 17:46:31 +0300 Subject: [PATCH 0626/1000] feat(libkernel): read param.sfo when param.json is absent PS4 titles ship sce_sys/param.sfo instead of param.json. When param.json is absent, the application metadata now reads the title, title ID, download data size and user defined parameters from param.sfo, and /data and /download0 are mounted for it as for param.json. param.json is still preferred when both files exist. --- .../AppMetadata/include/ParamSfoParser.hpp | 10 ++ .../libkernel/AppMetadata/src/AppMetadata.cpp | 20 ++- .../AppMetadata/src/ParamSfoParser.cpp | 85 +++++++++++ core/libs/prx/libkernel/CMakeLists.txt | 14 +- core/libs/tests/GuestAppInfo.cpp | 47 +++++- core/libs/tests/GuestDownloadDataMount.cpp | 32 +++- core/libs/tests/ParamSfoParser.cpp | 144 ++++++++++++++++++ docs/dev/debt/libkernel.md | 1 + 8 files changed, 334 insertions(+), 19 deletions(-) create mode 100644 core/libs/prx/libkernel/AppMetadata/include/ParamSfoParser.hpp create mode 100644 core/libs/prx/libkernel/AppMetadata/src/ParamSfoParser.cpp create mode 100644 core/libs/tests/ParamSfoParser.cpp diff --git a/core/libs/prx/libkernel/AppMetadata/include/ParamSfoParser.hpp b/core/libs/prx/libkernel/AppMetadata/include/ParamSfoParser.hpp new file mode 100644 index 0000000000..4e828a4970 --- /dev/null +++ b/core/libs/prx/libkernel/AppMetadata/include/ParamSfoParser.hpp @@ -0,0 +1,10 @@ +#ifndef CORE_LIBS_PRX_LIBKERNEL_APPMETADATA_INCLUDE_PARAMSFOPARSER_HPP +#define CORE_LIBS_PRX_LIBKERNEL_APPMETADATA_INCLUDE_PARAMSFOPARSER_HPP + +#include "prx/libkernel/AppMetadata/include/ParamJsonParser.hpp" + +#include + +ParsedParamJson parseParamSfo(const std::filesystem::path& paramSfoPath); + +#endif diff --git a/core/libs/prx/libkernel/AppMetadata/src/AppMetadata.cpp b/core/libs/prx/libkernel/AppMetadata/src/AppMetadata.cpp index c2c9b2d2c6..f078143320 100644 --- a/core/libs/prx/libkernel/AppMetadata/src/AppMetadata.cpp +++ b/core/libs/prx/libkernel/AppMetadata/src/AppMetadata.cpp @@ -1,5 +1,6 @@ #include "prx/libkernel/AppMetadata/include/AppMetadata.hpp" #include "prx/libkernel/AppMetadata/include/ParamJsonParser.hpp" +#include "prx/libkernel/AppMetadata/include/ParamSfoParser.hpp" #include "prx/libc/include/General.hpp" #include @@ -25,6 +26,7 @@ std::int32_t g_userDefinedParams[4] = {}; std::vector g_iconBytes; constexpr const char* AppMetadataParamJsonGuestPath = "/app0/sce_sys/param.json"; +constexpr const char* AppMetadataParamSfoGuestPath = "/app0/sce_sys/param.sfo"; constexpr const char* AppMetadataIconGuestPath = "/app0/sce_sys/icon0.png"; void copyToFixedBuffer(char* destination, std::size_t destinationSize, const std::string& source) { @@ -33,12 +35,16 @@ void copyToFixedBuffer(char* destination, std::size_t destinationSize, const std std::memcpy(destination, source.c_str(), source.size() + 1); } +ParsedParamJson parseParams(const std::filesystem::path& paramJson, const std::filesystem::path& paramSfo) { + if (std::filesystem::exists(paramJson)) return parseParamJson(paramJson); + if (std::filesystem::exists(paramSfo)) return parseParamSfo(paramSfo); + throw std::runtime_error("param.json or param.sfo not found"); +} + void ensureTitleLoaded() { std::lock_guard lock(g_metadataMutex); if (g_titleLoaded) return; - const auto resolvedPath = ResolvePath_nid_no_patch(AppMetadataParamJsonGuestPath); - if (!std::filesystem::exists(resolvedPath)) throw std::runtime_error("param.json not found"); - const auto parsed = parseParamJson(resolvedPath); + const auto parsed = parseParams(ResolvePath_nid_no_patch(AppMetadataParamJsonGuestPath), ResolvePath_nid_no_patch(AppMetadataParamSfoGuestPath)); copyToFixedBuffer(g_title, sizeof(g_title), parsed.title); copyToFixedBuffer(g_titleId, sizeof(g_titleId), parsed.titleId); g_downloadDataSizeMiB = parsed.downloadDataSizeMiB; @@ -50,8 +56,9 @@ void ensureTitleLoaded() { const bool g_downloadDataMounted = [] { try { const auto run = std::filesystem::current_path(); - const auto paramJson = run / "app0" / "sce_sys" / "param.json"; - if (!std::filesystem::exists(paramJson) || parseParamJson(paramJson).downloadDataSizeMiB == 0) return false; + const auto metadata = run / "app0" / "sce_sys"; + if (!std::filesystem::exists(metadata / "param.json") && !std::filesystem::exists(metadata / "param.sfo")) return false; + if (parseParams(metadata / "param.json", metadata / "param.sfo").downloadDataSizeMiB == 0) return false; std::filesystem::create_directories(run / "download0"); return true; } catch (const std::exception&) { @@ -62,7 +69,8 @@ const bool g_downloadDataMounted = [] { const bool g_dataMounted = [] { try { const auto run = std::filesystem::current_path(); - if (!std::filesystem::exists(run / "app0" / "sce_sys" / "param.json")) return false; + const auto metadata = run / "app0" / "sce_sys"; + if (!std::filesystem::exists(metadata / "param.json") && !std::filesystem::exists(metadata / "param.sfo")) return false; std::filesystem::create_directories(run / "data"); return true; } catch (const std::exception&) { diff --git a/core/libs/prx/libkernel/AppMetadata/src/ParamSfoParser.cpp b/core/libs/prx/libkernel/AppMetadata/src/ParamSfoParser.cpp new file mode 100644 index 0000000000..bfaf32d9f8 --- /dev/null +++ b/core/libs/prx/libkernel/AppMetadata/src/ParamSfoParser.cpp @@ -0,0 +1,85 @@ +#include "prx/libkernel/AppMetadata/include/ParamSfoParser.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +constexpr std::uint16_t SfoUtf8 = 0x0204; +constexpr std::uint16_t SfoInteger = 0x0404; +constexpr std::size_t HeaderSize = 20; +constexpr std::size_t EntrySize = 16; + +std::uint16_t readU16(const std::vector& bytes, std::size_t offset) { + std::uint16_t value; + std::memcpy(&value, bytes.data() + offset, sizeof(value)); + return value; +} + +std::uint32_t readU32(const std::vector& bytes, std::size_t offset) { + std::uint32_t value; + std::memcpy(&value, bytes.data() + offset, sizeof(value)); + return value; +} + +} + +ParsedParamJson parseParamSfo(const std::filesystem::path& paramSfoPath) { + std::ifstream file(paramSfoPath, std::ios::binary); + if (!file.is_open()) throw std::runtime_error("failed to open param.sfo"); + const std::vector bytes((std::istreambuf_iterator(file)), std::istreambuf_iterator()); + if (bytes.size() < HeaderSize || std::memcmp(bytes.data(), "\0PSF", 4) != 0) throw std::runtime_error("param.sfo has no PSF header"); + const std::uint64_t keyTable = readU32(bytes, 8); + const std::uint64_t dataTable = readU32(bytes, 12); + const std::uint64_t count = readU32(bytes, 16); + if (count > (bytes.size() - HeaderSize) / EntrySize) throw std::runtime_error("param.sfo tables are out of bounds"); + std::set keys; + std::map strings; + std::map integers; + for (std::uint64_t index = 0; index < count; ++index) { + const auto entry = HeaderSize + index * EntrySize; + const auto keyOffset = keyTable + readU16(bytes, entry); + const auto format = readU16(bytes, entry + 2); + const std::uint64_t length = readU32(bytes, entry + 4); + const auto dataOffset = dataTable + readU32(bytes, entry + 12); + if (keyOffset >= bytes.size() || dataOffset > bytes.size() || length > bytes.size() - dataOffset) throw std::runtime_error("param.sfo entry is out of bounds"); + const auto keyEnd = std::find(bytes.begin() + static_cast(keyOffset), bytes.end(), std::uint8_t{0}); + if (keyEnd == bytes.end()) throw std::runtime_error("param.sfo key is not terminated"); + const std::string key(bytes.begin() + static_cast(keyOffset), keyEnd); + if (!keys.insert(key).second) throw std::runtime_error("param.sfo repeats " + key); + if (format == SfoUtf8) { + const auto begin = bytes.begin() + static_cast(dataOffset); + const auto end = std::find(begin, begin + static_cast(length), std::uint8_t{0}); + if (end == begin + static_cast(length)) throw std::runtime_error("param.sfo " + key + " is not a terminated string"); + strings.emplace(key, std::string(begin, end)); + } else if (format == SfoInteger) { + if (length != 4) throw std::runtime_error("param.sfo " + key + " is not a 32-bit integer"); + integers.emplace(key, static_cast(readU32(bytes, dataOffset))); + } + } + const auto text = [&](const char* key) { + const auto found = strings.find(key); + if (found == strings.end() || found->second.empty()) throw std::runtime_error(std::string("param.sfo is missing ") + key); + return found->second; + }; + ParsedParamJson result; + result.title = text("TITLE"); + result.titleId = text("TITLE_ID"); + if (const auto size = integers.find("DOWNLOAD_DATA_SIZE"); size != integers.end()) { + if (size->second < 0) throw std::runtime_error("param.sfo DOWNLOAD_DATA_SIZE is negative"); + result.downloadDataSizeMiB = static_cast(size->second); + } + for (int index = 0; index < 4; ++index) { + const auto param = integers.find("USER_DEFINED_PARAM_" + std::to_string(index + 1)); + if (param != integers.end()) result.userDefinedParams[index] = param->second; + } + return result; +} diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index b5fee1a726..4f0d237125 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -75,6 +75,7 @@ add_library(libkernel SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/Time/TimedWait.cpp ${CMAKE_CURRENT_SOURCE_DIR}/AppMetadata/src/AppMetadata.cpp ${CMAKE_CURRENT_SOURCE_DIR}/AppMetadata/src/ParamJsonParser.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/AppMetadata/src/ParamSfoParser.cpp ) target_include_directories(libkernel PRIVATE ${LIBS_INCLUDE_DIR}) @@ -103,12 +104,12 @@ if(BUILD_TESTING) target_include_directories(download_data_mount_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(download_data_mount_tests PRIVATE libkernel libc) configure_windows_unwind(download_data_mount_tests) - foreach(downloadDataCase IN ITEMS declared:256 none:0 invalid:256 missing:0) + foreach(downloadDataCase IN ITEMS declared:256 none:0 invalid:256 missing:0 sfo:0) string(REPLACE ":" ";" downloadDataCase ${downloadDataCase}) list(GET downloadDataCase 0 downloadDataName) list(GET downloadDataCase 1 downloadDataSize) set(downloadDataRun ${CMAKE_CURRENT_BINARY_DIR}/download_data_mount_${downloadDataName}) - if(downloadDataName STREQUAL "missing") + if(downloadDataName STREQUAL "missing" OR downloadDataName STREQUAL "sfo") file(MAKE_DIRECTORY ${downloadDataRun}) elseif(downloadDataName STREQUAL "invalid") file(WRITE ${downloadDataRun}/app0/sce_sys/param.json "{\"downloadDataSize\":${downloadDataSize}}") @@ -133,6 +134,15 @@ if(BUILD_TESTING) set_tests_properties(param_json_parser PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(param_sfo_parser_tests ${LIBS_INCLUDE_DIR}/tests/ParamSfoParser.cpp) + target_include_directories(param_sfo_parser_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(param_sfo_parser_tests PRIVATE libkernel libc) + configure_windows_unwind(param_sfo_parser_tests) + add_test(NAME param_sfo_parser COMMAND param_sfo_parser_tests) + if(WIN32) + set_tests_properties(param_sfo_parser PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_posix_sem_tests ${LIBS_INCLUDE_DIR}/tests/GuestPosixSem.cpp) target_include_directories(guest_posix_sem_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_posix_sem_tests PRIVATE libkernel libc) diff --git a/core/libs/tests/GuestAppInfo.cpp b/core/libs/tests/GuestAppInfo.cpp index e65d891a06..5421462aea 100644 --- a/core/libs/tests/GuestAppInfo.cpp +++ b/core/libs/tests/GuestAppInfo.cpp @@ -26,6 +26,29 @@ struct AppInfo { char unknown2[0x3c]; }; +static void WriteParamSfo(const std::filesystem::path& path) { + const char title[] = "Example"; + const char titleId[] = "CUSA00000"; + const char keys[16] = "TITLE\0TITLE_ID"; + unsigned char bytes[86] = {0, 'P', 'S', 'F', 1, 1, 0, 0, 52, 0, 0, 0, 68, 0, 0, 0, 2, 0, 0, 0, + 0, 0, 4, 2, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, + 6, 0, 4, 2, 10, 0, 0, 0, 10, 0, 0, 0, 8, 0, 0, 0}; + std::memcpy(bytes + 52, keys, sizeof(keys)); + std::memcpy(bytes + 68, title, sizeof(title)); + std::memcpy(bytes + 76, titleId, sizeof(titleId)); + std::ofstream(path, std::ios::binary).write(reinterpret_cast(bytes), sizeof(bytes)); +} + +static int RunSelf(const char* program, std::string_view mode, const std::filesystem::path& directory) { + const std::string arguments = "\"" + std::string(program) + "\" " + std::string(mode) + " \"" + directory.string() + "\""; +#ifdef _WIN32 + const std::string command = "\"" + arguments + "\""; +#else + const std::string command = arguments; +#endif + return std::system(command.c_str()); +} + static std::filesystem::path EnterTemporaryRoot(std::string_view name) { const auto root = std::filesystem::temp_directory_path() / (std::string(name) + std::to_string(getpid_nid_postfix())); std::filesystem::remove_all(root); @@ -40,13 +63,21 @@ int main(int argc, char** argv) { AppInfo info{}; try { sceKernelGetAppInfo(getpid_nid_postfix(), &info); } catch (const std::runtime_error& error) { - return std::string_view(error.what()) == "param.json not found" ? 0 : 1; + return std::string_view(error.what()) == "param.json or param.sfo not found" ? 0 : 1; } return 1; } + if (argc == 3 && std::string_view(argv[1]) == "--with-param-sfo") { + std::filesystem::current_path(argv[2]); + AppInfo info{}; + Require(sceKernelGetAppInfo(getpid_nid_postfix(), &info) == 0); + Require(std::strcmp(info.titleId, "CUSA00000") == 0); + return 0; + } const auto previous = std::filesystem::current_path(); const auto root = EnterTemporaryRoot("guest_app_info_"); std::filesystem::create_directories(root / "app0" / "sce_sys"); + WriteParamSfo(root / "app0" / "sce_sys" / "param.sfo"); std::ofstream(root / "app0" / "sce_sys" / "param.json") << "{\n \"contentId\": \"IV0000-PPSA97358_00-XPSEMU0000000000\",\n \"localizedParameters\": {\"defaultLanguage\": \"en-US\", \"en-US\": {\"titleName\": \"XPSemu\"}},\n \"titleId\": \"PPSA97358\"\n}\n"; AppInfo info; std::memset(&info, 0x5a, sizeof(info)); @@ -63,14 +94,14 @@ int main(int argc, char** argv) { const auto empty = std::filesystem::temp_directory_path() / ("guest_app_info_empty_" + std::to_string(self)); std::filesystem::remove_all(empty); std::filesystem::create_directories(empty); - const std::string arguments = "\"" + std::string(argv[0]) + "\" --without-param-json \"" + empty.string() + "\""; -#ifdef _WIN32 - const std::string command = "\"" + arguments + "\""; -#else - const std::string command = arguments; -#endif - Require(std::system(command.c_str()) == 0); + Require(RunSelf(argv[0], "--without-param-json", empty) == 0); std::filesystem::remove_all(empty); + const auto sfo = std::filesystem::temp_directory_path() / ("guest_app_info_sfo_" + std::to_string(self)); + std::filesystem::remove_all(sfo); + std::filesystem::create_directories(sfo / "app0" / "sce_sys"); + WriteParamSfo(sfo / "app0" / "sce_sys" / "param.sfo"); + Require(RunSelf(argv[0], "--with-param-sfo", sfo) == 0); + std::filesystem::remove_all(sfo); Require(sceKernelUuidCreate(nullptr) == SCE_KERNEL_ERROR_EINVAL); std::uint32_t first[4]{}; diff --git a/core/libs/tests/GuestDownloadDataMount.cpp b/core/libs/tests/GuestDownloadDataMount.cpp index a3ebfccef8..1c3abe8c03 100644 --- a/core/libs/tests/GuestDownloadDataMount.cpp +++ b/core/libs/tests/GuestDownloadDataMount.cpp @@ -1,20 +1,46 @@ #include "prx/libkernel/AppMetadata/include/AppMetadata.hpp" +#include #include #include #include +#include #include +#include #include static void Require(bool value) { if (!value) std::abort(); } +static void WriteParamSfo(const std::filesystem::path& path) { + struct Entry { std::uint16_t key, format; std::uint32_t length, maximum, offset; }; + const std::uint32_t header[5] = {0x46535000, 0x0101, 68, 104, 3}; + const Entry entries[3] = {{0, 0x0404, 4, 4, 0}, {19, 0x0204, 8, 8, 4}, {25, 0x0204, 10, 10, 12}}; + std::ofstream file(path, std::ios::binary); + file.write(reinterpret_cast(header), sizeof(header)); + file.write(reinterpret_cast(entries), sizeof(entries)); + file << std::string("DOWNLOAD_DATA_SIZE\0TITLE\0TITLE_ID\0\0\0", 36) << std::string("\0\1\0\0Example\0CUSA00000\0", 22); +} + int main(int argc, char** argv) { Require(argc == 2); const std::string_view mode = argv[1]; - Require(std::filesystem::is_directory("download0") == (mode == "declared")); + Require(std::filesystem::is_directory("download0") == (mode == "declared" || mode == "sfo-child")); const bool data = std::filesystem::is_directory("data"); std::filesystem::remove_all("data"); - Require(data == (mode != "missing")); + Require(data == (mode != "missing" && mode != "sfo")); if (mode == "missing") return 0; + if (mode == "sfo") { + std::filesystem::create_directories("app0/sce_sys"); + WriteParamSfo("app0/sce_sys/param.sfo"); +#ifdef _WIN32 + const std::string command = "\"\"" + std::string(argv[0]) + "\" sfo-child\""; +#else + const std::string command = "\"" + std::string(argv[0]) + "\" sfo-child"; +#endif + const int status = std::system(command.c_str()); + std::filesystem::remove_all("app0"); + Require(status == 0); + return 0; + } if (mode == "invalid") { bool rejected = false; try { @@ -25,6 +51,6 @@ int main(int argc, char** argv) { Require(rejected); return 0; } - Require(std::strcmp(GetAppTitleId_nid_postfix().value, "PPSA00000") == 0); + Require(std::strcmp(GetAppTitleId_nid_postfix().value, mode == "sfo-child" ? "CUSA00000" : "PPSA00000") == 0); std::filesystem::remove_all("download0"); } diff --git a/core/libs/tests/ParamSfoParser.cpp b/core/libs/tests/ParamSfoParser.cpp new file mode 100644 index 0000000000..d4f9ff1c12 --- /dev/null +++ b/core/libs/tests/ParamSfoParser.cpp @@ -0,0 +1,144 @@ +#include "prx/libkernel/AppMetadata/include/ParamSfoParser.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +struct Entry { + std::string key; + std::uint16_t format; + std::vector data; +}; + +void Require(bool condition, const std::string& message) { + if (!condition) throw std::runtime_error(message); +} + +Entry Text(const std::string& key, const std::string& value) { + std::vector data(value.begin(), value.end()); + data.push_back(0); + return {key, 0x0204, data}; +} + +Entry Integer(const std::string& key, std::int32_t value) { + std::vector data(4); + std::memcpy(data.data(), &value, 4); + return {key, 0x0404, data}; +} + +void Put32(std::vector& bytes, std::size_t offset, std::uint32_t value) { + std::memcpy(bytes.data() + offset, &value, 4); +} + +std::vector Build(const std::vector& entries) { + std::vector keys; + std::vector data; + std::vector bytes(20 + entries.size() * 16); + for (std::size_t index = 0; index < entries.size(); ++index) { + const auto entry = 20 + index * 16; + const auto keyOffset = static_cast(keys.size()); + std::memcpy(bytes.data() + entry, &keyOffset, 2); + std::memcpy(bytes.data() + entry + 2, &entries[index].format, 2); + Put32(bytes, entry + 4, static_cast(entries[index].data.size())); + Put32(bytes, entry + 8, static_cast(entries[index].data.size())); + Put32(bytes, entry + 12, static_cast(data.size())); + keys.insert(keys.end(), entries[index].key.begin(), entries[index].key.end()); + keys.push_back(0); + data.insert(data.end(), entries[index].data.begin(), entries[index].data.end()); + } + while (keys.size() % 4 != 0) keys.push_back(0); + std::memcpy(bytes.data(), "\0PSF", 4); + Put32(bytes, 4, 0x0101); + Put32(bytes, 8, static_cast(bytes.size())); + Put32(bytes, 12, static_cast(bytes.size() + keys.size())); + Put32(bytes, 16, static_cast(entries.size())); + bytes.insert(bytes.end(), keys.begin(), keys.end()); + bytes.insert(bytes.end(), data.begin(), data.end()); + return bytes; +} + +ParsedParamJson Parse(const std::filesystem::path& path, const std::vector& bytes) { + { + std::ofstream file(path, std::ios::binary); + file.write(reinterpret_cast(bytes.data()), static_cast(bytes.size())); + Require(static_cast(file), "Cannot write param.sfo fixture"); + } + return parseParamSfo(path); +} + +void Rejects(const std::filesystem::path& path, const std::vector& bytes, const std::string& expected) { + try { + Parse(path, bytes); + } catch (const std::exception& error) { + Require(std::string(error.what()).find(expected) != std::string::npos, "Unexpected error for " + expected + ": " + error.what()); + return; + } + throw std::runtime_error("Accepted invalid param.sfo: " + expected); +} + +std::vector Valid() { + return {Text("CATEGORY", "gd"), Integer("DOWNLOAD_DATA_SIZE", 64), Text("TITLE", "Example"), Text("TITLE_ID", "CUSA00000"), + Integer("USER_DEFINED_PARAM_1", 23566), Integer("USER_DEFINED_PARAM_3", -7), {"PUBTOOLINFO", 0x0004, {1, 2, 3}}}; +} + +} + +int main() { + const auto path = std::filesystem::temp_directory_path() / ("anyps5-param-sfo-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()) + ".sfo"); + int result = 0; + try { + const auto parsed = Parse(path, Build(Valid())); + Require(parsed.title == "Example" && parsed.titleId == "CUSA00000", "Incorrect title metadata"); + Require(parsed.downloadDataSizeMiB == 64, "Incorrect download data size"); + Require(parsed.userDefinedParams[0] == 23566 && parsed.userDefinedParams[1] == 0 && parsed.userDefinedParams[2] == -7 && parsed.userDefinedParams[3] == 0, "Incorrect user defined params"); + + auto entries = Valid(); + entries.erase(entries.begin() + 2); + Rejects(path, Build(entries), "missing TITLE"); + entries = Valid(); + entries.erase(entries.begin() + 3); + Rejects(path, Build(entries), "missing TITLE_ID"); + entries = Valid(); + entries[1] = Integer("DOWNLOAD_DATA_SIZE", -1); + Rejects(path, Build(entries), "DOWNLOAD_DATA_SIZE is negative"); + entries = Valid(); + entries[2].data.back() = 'x'; + Rejects(path, Build(entries), "TITLE is not a terminated string"); + entries = Valid(); + entries[4].data.pop_back(); + Rejects(path, Build(entries), "USER_DEFINED_PARAM_1 is not a 32-bit integer"); + entries = Valid(); + entries.push_back(Text("TITLE", "Again")); + Rejects(path, Build(entries), "repeats TITLE"); + + auto bytes = Build(Valid()); + bytes[1] = 'X'; + Rejects(path, bytes, "no PSF header"); + bytes = Build(Valid()); + Put32(bytes, 16, 1000); + Rejects(path, bytes, "tables are out of bounds"); + bytes = Build(Valid()); + Put32(bytes, 20 + 2 * 16 + 12, 0x10000); + Rejects(path, bytes, "entry is out of bounds"); + bytes = Build(Valid()); + Put32(bytes, 20 + 4, 0x10000); + Rejects(path, bytes, "entry is out of bounds"); + bytes = Build(Valid()); + bytes.resize(bytes.size() - 64); + Rejects(path, bytes, "out of bounds"); + Rejects(path, std::vector(8), "no PSF header"); + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + result = 1; + } + std::filesystem::remove(path); + if (result == 0) std::cout << "param.sfo parser tests passed\n"; + return result; +} diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index c4fe965173..feed86e6c9 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -202,3 +202,4 @@ - [rtprio_thread, pthread_setconcurrency, pthread_getconcurrency](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - `rtprio_thread` follows FreeBSD 11.4's `rtprio_thread(2)`. Every guest thread here runs under `SCHED_FIFO` or `SCHED_RR` (priority 256-767), whose rtprio class is real-time; the console's mapping from that priority to the rtprio `prio` range (0-31) is unmeasured, so `RTP_LOOKUP` and `RTP_SET` throw for such a thread rather than report an impossible priority. A thread that is not real-time reports `RTP_PRIO_NORMAL` (3) with 0, and `RTP_SET` accepts only that value. Another function fails with `EINVAL`, a null `rtprio` with `EFAULT`, a thread other than the calling one throws, and an id that is not a live guest thread with `ESRCH`. `pthread_setconcurrency` stores 0 or more and returns `EINVAL` for a negative level (the error itself, as FreeBSD's libthr does, with errno untouched); `pthread_getconcurrency` returns the level, which is a hint and changes nothing about the host's concurrency. Morrowind (`mshivam019/morrowind-ps5`) imports all three at start-up - [sceKernelVirtualQuery](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) returns [VirtualQueryInfo](../../../core/libs/SceTypes.hpp) with a one-byte flag allocation unit so Windows and SysV hosts return the same 72-byte representation, with the name at byte 33 and GPU mask at byte 65. This preserves the existing SysV representation; the console offsets beyond the address, protection and size fields observed in Astro Bot's AGC calls are not hardware-measured. Size and offset assertions plus a raw-byte named-mapping regression guard the host ABI. - [app0.zar files](../../../core/libs/prx/libkernel/File/src/ArchivedFile.cpp) (libkernel) - `sceKernelOpen`, `open`, `sceKernelStat` and `sceKernelCheckReachability` resolve a path missing from `app0/` in `app0.zar`. An archived file is a read-only host null device whose reads, positioned and vectored reads, AIO reads, seeks, `fstat` and `FIONREAD` are served from the archive, with the position shared by `dup`, `dup2` and `F_DUPFD`; writes fail with `EBADF` as on a read-only descriptor, opening one for writing or truncation fails with `EROFS` and `O_CREAT | O_EXCL` with `EEXIST`. `fstat` and `sceKernelStat` report mode `0444` (`0555` for a directory), the node index as inode and the archive file's device and times. Opening an archived directory or a directory present both on disk and in the archive throws, as `getdents` would miss entries. On an archived descriptor `mmap`, `flock`, record locks, `fstatfs`, `ftruncate`, `fsync`, `fdatasync`, `getdirentries`, `fchmod` and `futimes` throw instead of acting on the null device; `mmap` is `noexcept`, so its refusal terminates the process. `lstat` and the other path calls on archived files are not implemented +- [param.sfo](../../../core/libs/prx/libkernel/AppMetadata/src/ParamSfoParser.cpp) (libkernel) is read when `param.json` is absent, for `TITLE`, `TITLE_ID`, `DOWNLOAD_DATA_SIZE` and `USER_DEFINED_PARAM_1` to `_4`, and mounts `/data` and `/download0` like `param.json`; the localized `TITLE_00` to `TITLE_30` are not selected by the console language. From 871232eede8d9f01c7ee1c70aaf9f66a711dc295 Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Sat, 17 Oct 2026 01:58:31 +0000 Subject: [PATCH 0627/1000] test(agc): wait for preparation workers before linking --- .../tests/execution/PreparationScopeSettle.cpp | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PreparationScopeSettle.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PreparationScopeSettle.cpp index 2233d80ba3..b67059d036 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PreparationScopeSettle.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PreparationScopeSettle.cpp @@ -3,6 +3,7 @@ #include "prx/libSceAgcDriver/Execution/include/Driver.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp" #include "prx/libc/include/general/VabiMacros.hpp" +#include #include #include #include @@ -75,12 +76,23 @@ struct PixelHeader { class HeldPreparationThreads { public: HeldPreparationThreads() { - for (unsigned index = 0; index < std::thread::hardware_concurrency() + 2u; ++index) { + const unsigned hardwareConcurrency = std::thread::hardware_concurrency(); + const unsigned workerCount = std::max(2u, hardwareConcurrency / 2u); + for (unsigned index = 0; index < hardwareConcurrency + 2u; ++index) { AgcDriver::DriverDetail::SubmitRegistrationPreparation([state = state] { std::unique_lock lock(state->mutex); + ++state->active; + state->changed.notify_all(); state->changed.wait(lock, [&] { return state->released; }); }); } + std::unique_lock lock(state->mutex); + const bool workersBusy = state->changed.wait_for(lock, std::chrono::seconds(5), [&] { return state->active >= workerCount; }); + lock.unlock(); + if (!workersBusy) { + Release(); + throw std::runtime_error("registration preparation workers did not become busy"); + } } ~HeldPreparationThreads() { @@ -99,6 +111,7 @@ private: struct State { std::mutex mutex; std::condition_variable changed; + unsigned active = 0; bool released = false; }; std::shared_ptr state = std::make_shared(); From 9f4a8d6ec50de9cbaff7f07f19c46af3c8bb6c51 Mon Sep 17 00:00:00 2001 From: screamm Date: Tue, 13 Oct 2026 10:25:22 +0200 Subject: [PATCH 0628/1000] feat(libkernel): serve the console CA list from the host trust store Titles that bundle OpenSSL load the console's CA file from the sandbox directory and fail every HTTPS request without it. ProsperoRadio (PPSA99001, libcurl 8.18.0 + OpenSSL 3.5.2) on main + #4796 + #4525: [ProsperoRadio][http] https://all.api.radio-browser.info/json/servers: Problem with the SSL CA cert (path? access rights?) (error adding trust anchors from file: /gkbHtXc3Wq/common/cert/CA_LIST.cer) SwanStation (PPSA98510) stops on the same file (#5001). The guest path resolver now accepts exact-path providers, and libkernel registers one for //common/cert/CA_LIST.cer. On first use it writes a PEM bundle once per process to cert_cache/CA_LIST.cer in the run directory, read-only: on Windows the current user's ROOT and CA system stores, elsewhere SSL_CERT_FILE or the first distro bundle that exists. Without a certificate the path keeps resolving under the guest root and does not exist, as before. Every other path under the sandbox word is unchanged. The console file's format and the rest of its common/cert directory are unverified and recorded in docs/dev/debt/libkernel.md. With this change, #4796, #4525 and #5025 on Windows 11, 60 s each: [ProsperoRadio][http] 11846160 bytes in 1458 ms: 1161 passes, 803 waits (679 woken) for 1428 ms [ProsperoRadio][catalog] page offset=0 bytes=11846160 objects=10000 accepted=9549 total=9549 ... [ProsperoRadio][catalog] page offset=60000 bytes=1712035 objects=1494 accepted=1436 total=59371 [SwanStationPS5] update: latest 1.2.5, this 1.1.3, available [SwanStationPS5] gamedb: index written Co-Authored-By: Claude Fable 5.1 --- core/libs/prx/libc/include/General.hpp | 2 + core/libs/prx/libc/src/General.cpp | 37 ++++++ core/libs/prx/libkernel/CMakeLists.txt | 14 ++- .../File/src/SandboxCertificates.cpp | 114 ++++++++++++++++++ core/libs/tests/GuestSandboxCertificates.cpp | 76 ++++++++++++ docs/dev/debt/libkernel.md | 3 +- 6 files changed, 244 insertions(+), 2 deletions(-) create mode 100644 core/libs/prx/libkernel/File/src/SandboxCertificates.cpp create mode 100644 core/libs/tests/GuestSandboxCertificates.cpp diff --git a/core/libs/prx/libc/include/General.hpp b/core/libs/prx/libc/include/General.hpp index 72dd4a7562..df34c7d52b 100644 --- a/core/libs/prx/libc/include/General.hpp +++ b/core/libs/prx/libc/include/General.hpp @@ -29,6 +29,8 @@ extern "C" int RenameGuestPath_nid_no_patch(const char* from, const char* to); // directory instead of the run directory; the prefix matches whole path components only. extern "C" void AddPathAlias_nid_no_patch(const char* guestPrefix, const char* hostPath); extern "C" void RemovePathAlias_nid_no_patch(const char* guestPrefix); +using PathProvider = std::filesystem::path (*)(const std::filesystem::path& root); +extern "C" void AddPathProvider_nid_no_patch(const char* guestPath, PathProvider provide); extern "C" void RecordWrittenPath_nid_no_patch(const std::filesystem::path& path); extern "C" std::vector WrittenPaths_nid_no_patch(); extern "C" void SyncWrittenPaths_nid_no_patch(); diff --git a/core/libs/prx/libc/src/General.cpp b/core/libs/prx/libc/src/General.cpp index 27caa6d483..c014fa324c 100644 --- a/core/libs/prx/libc/src/General.cpp +++ b/core/libs/prx/libc/src/General.cpp @@ -41,6 +41,7 @@ struct PathAliases { std::mutex mutex; std::vector> entries; std::vector blocked; + std::vector> providers; }; PathAliases& Aliases() { @@ -170,10 +171,29 @@ WorkingDirectory& Directories() { static auto* state = new WorkingDirectory; return *state; } +std::optional ProvidedPath(const WorkingDirectory& state, const std::filesystem::path& guest) { + const auto relative = TrimSlashes(guest.relative_path().generic_string().c_str()); + PathProvider provide = nullptr; + { + auto& aliases = Aliases(); + std::lock_guard lock(aliases.mutex); + for (const auto& [path, provider] : aliases.providers) { + if (SameName(relative, path)) provide = provider; + } + } + if (provide == nullptr) return std::nullopt; + auto provided = provide(state.root); + if (provided.empty()) return std::nullopt; + return provided.make_preferred(); +} std::filesystem::path HostPath(WorkingDirectory& state, const std::filesystem::path& guest, bool module = false) { if (auto aliased = ResolveAlias(guest.relative_path().generic_string(), module)) return *aliased; + if (!module) { + if (auto provided = ProvidedPath(state, guest)) return *provided; + } return ResolveHostPath(state.root, guest.relative_path(), module).make_preferred(); } +} void RequireHostDirectory(WorkingDirectory& state, const std::filesystem::path& guest) { const auto host = HostPath(state, guest); std::error_code error; @@ -355,6 +375,23 @@ extern "C" void AddPathAlias_nid_no_patch(const char* guestPrefix, const char* h aliases.entries.emplace_back(prefix, hostPath); } +extern "C" void AddPathProvider_nid_no_patch(const char* guestPath, PathProvider provide) { + if (guestPath == nullptr || provide == nullptr) { + APS5_INVALID_ARG_EX; + } + const auto path = TrimSlashes(guestPath); + if (path.empty()) { APS5_INVALID_ARG_EX; } + auto& aliases = Aliases(); + std::lock_guard lock(aliases.mutex); + for (auto& entry : aliases.providers) { + if (SameName(entry.first, path)) { + entry.second = provide; + return; + } + } + aliases.providers.emplace_back(path, provide); +} + extern "C" void RemovePathAlias_nid_no_patch(const char* guestPrefix) { if (guestPrefix == nullptr) { APS5_INVALID_ARG_EX; diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index b5fee1a726..1203674b39 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -16,6 +16,7 @@ add_library(libkernel SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/File/src/SharedMemory.cpp ${CMAKE_CURRENT_SOURCE_DIR}/File/src/RandomDevice.cpp ${CMAKE_CURRENT_SOURCE_DIR}/File/src/HostStreams.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/File/src/SandboxCertificates.cpp ${CMAKE_CURRENT_SOURCE_DIR}/File/src/Stdio.cpp ${CMAKE_CURRENT_SOURCE_DIR}/../libc/src/Rename.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Semaphore/src/Semaphore.cpp @@ -82,7 +83,7 @@ target_include_directories(libkernel PRIVATE ${LIBS_INCLUDE_DIR}/nid/include) target_link_libraries(libkernel PRIVATE ${CMAKE_DL_LIBS}) target_link_libraries(libkernel PRIVATE libc) if(WIN32) - target_link_libraries(libkernel PRIVATE ws2_32 shell32 advapi32 wbemuuid ole32 oleaut32) + target_link_libraries(libkernel PRIVATE ws2_32 shell32 advapi32 crypt32 wbemuuid ole32 oleaut32) target_link_libraries(libkernel PRIVATE iphlpapi) target_compile_definitions(libkernel PRIVATE _WIN32_WINNT=0x0602) endif() @@ -298,6 +299,17 @@ if(BUILD_TESTING) set_tests_properties(guest_kernel_sync PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_sandbox_certificates_tests ${LIBS_INCLUDE_DIR}/tests/GuestSandboxCertificates.cpp) + target_include_directories(guest_sandbox_certificates_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_sandbox_certificates_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_sandbox_certificates_tests) + set(sandboxCertificatesRun ${CMAKE_CURRENT_BINARY_DIR}/guest_sandbox_certificates) + file(MAKE_DIRECTORY ${sandboxCertificatesRun}) + add_test(NAME guest_sandbox_certificates COMMAND guest_sandbox_certificates_tests WORKING_DIRECTORY ${sandboxCertificatesRun}) + if(WIN32) + set_tests_properties(guest_sandbox_certificates PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_kernel_rename_tests ${LIBS_INCLUDE_DIR}/tests/GuestKernelRename.cpp) target_include_directories(guest_kernel_rename_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_kernel_rename_tests PRIVATE libkernel libc) diff --git a/core/libs/prx/libkernel/File/src/SandboxCertificates.cpp b/core/libs/prx/libkernel/File/src/SandboxCertificates.cpp new file mode 100644 index 0000000000..16e6cd153f --- /dev/null +++ b/core/libs/prx/libkernel/File/src/SandboxCertificates.cpp @@ -0,0 +1,114 @@ +#include "prx/libc/include/General.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#include +#else +#include +#endif + +extern "C" const char* APS5_VABI sceKernelGetFsSandboxRandomWord(); + +namespace { +constexpr char CertificateBegin[] = "-----BEGIN CERTIFICATE-----"; + +std::optional ReadFile(const std::filesystem::path& path) { + std::ifstream file(path, std::ios::binary); + if (!file) return std::nullopt; + std::string text((std::istreambuf_iterator(file)), std::istreambuf_iterator()); + if (file.bad()) throw std::runtime_error("Reading " + path.string() + " failed"); + return text; +} + +#ifdef _WIN32 +void AppendStore(const wchar_t* name, std::string& bundle) { + const std::unique_ptr store(CertOpenSystemStoreW(0, name), [](HCERTSTORE handle) { CertCloseStore(handle, 0); }); + if (store == nullptr) throw std::system_error(static_cast(GetLastError()), std::system_category(), "Opening the host certificate store"); + for (PCCERT_CONTEXT certificate = CertEnumCertificatesInStore(store.get(), nullptr); certificate != nullptr; + certificate = CertEnumCertificatesInStore(store.get(), certificate)) { + DWORD length = 0; + std::wstring text; + bool encoded = CryptBinaryToStringW(certificate->pbCertEncoded, certificate->cbCertEncoded, CRYPT_STRING_BASE64HEADER, nullptr, &length); + if (encoded) { + text.resize(length); + encoded = CryptBinaryToStringW(certificate->pbCertEncoded, certificate->cbCertEncoded, CRYPT_STRING_BASE64HEADER, text.data(), &length); + } + if (!encoded) { + const auto error = GetLastError(); + CertFreeCertificateContext(certificate); + throw std::system_error(static_cast(error), std::system_category(), "Encoding a host certificate"); + } + text.resize(length); + for (const wchar_t character : text) bundle.push_back(static_cast(character)); + } +} + +std::string HostCertificates() { + std::string bundle; + AppendStore(L"ROOT", bundle); + AppendStore(L"CA", bundle); + return bundle; +} + +unsigned long ProcessId() { return GetCurrentProcessId(); } +#else +std::string HostCertificates() { + std::array candidates{std::getenv("SSL_CERT_FILE"), "/etc/ssl/certs/ca-certificates.crt", "/etc/pki/tls/certs/ca-bundle.crt", + "/etc/ssl/ca-bundle.pem"}; + for (const char* candidate : candidates) { + std::error_code error; + if (candidate == nullptr || *candidate == '\0' || !std::filesystem::is_regular_file(candidate, error)) continue; + if (auto text = ReadFile(candidate)) return *text; + } + return {}; +} + +unsigned long ProcessId() { return static_cast(getpid()); } +#endif + +std::filesystem::path WriteCertificateList(const std::filesystem::path& root) { + const auto bundle = HostCertificates(); + if (bundle.find(CertificateBegin) == std::string::npos) return {}; + const auto directory = root / "cert_cache"; + std::filesystem::create_directories(directory); + const auto target = directory / "CA_LIST.cer"; + if (ReadFile(target) == bundle) return target; + auto temporary = target; + temporary += "." + std::to_string(ProcessId()) + ".tmp"; + { + std::ofstream file(temporary, std::ios::binary | std::ios::trunc); + file.write(bundle.data(), static_cast(bundle.size())); + if (!file.flush()) throw std::runtime_error("Writing " + temporary.string() + " failed"); + } + using std::filesystem::perms; + std::filesystem::permissions(temporary, perms::owner_read | perms::group_read | perms::others_read); + std::error_code error; + if (std::filesystem::exists(target, error)) std::filesystem::permissions(target, perms::owner_write, std::filesystem::perm_options::add); + std::filesystem::rename(temporary, target); + return target; +} + +std::filesystem::path CertificateList(const std::filesystem::path& root) { + static const std::filesystem::path list = WriteCertificateList(root); + return list; +} + +const bool g_certificateListProvided = [] { + AddPathProvider_nid_no_patch((std::string("/") + sceKernelGetFsSandboxRandomWord() + "/common/cert/CA_LIST.cer").c_str(), &CertificateList); + return true; +}(); +} diff --git a/core/libs/tests/GuestSandboxCertificates.cpp b/core/libs/tests/GuestSandboxCertificates.cpp new file mode 100644 index 0000000000..4f99b3f312 --- /dev/null +++ b/core/libs/tests/GuestSandboxCertificates.cpp @@ -0,0 +1,76 @@ +#include "prx/libc/include/General.hpp" +#include "prx/libkernel/KernelErrors.hpp" +#include "SceTypes.hpp" +#include +#include +#include +#include +#include +#include +extern "C" { +const char* APS5_VABI sceKernelGetFsSandboxRandomWord(); +int APS5_VABI sceKernelOpen(const char*, int, unsigned short); +int APS5_VABI sceKernelClose(int); +std::int64_t APS5_VABI sceKernelRead(int, void*, std::size_t); +int APS5_VABI sceKernelStat(const char*, FileStat*); +void* APS5_VABI fopen_nid_postfix(const char*, const char*); +std::size_t APS5_VABI fread_nid_postfix(void*, std::size_t, std::size_t, void*); +int APS5_VABI fclose_nid_postfix(void*); +} +static void Check(bool value, int line) { + if (!value) { + std::fprintf(stderr, "Sandbox certificates check failed at line %d\n", line); + std::abort(); + } +} +#define Require(value) Check((value), __LINE__) +static std::string ReadKernel(const std::string& path) { + const int descriptor = sceKernelOpen(path.c_str(), 0, 0); + Require(descriptor >= 0); + std::string text; + char buffer[4096]; + for (std::int64_t count; (count = sceKernelRead(descriptor, buffer, sizeof(buffer))) != 0;) { + Require(count > 0); + text.append(buffer, static_cast(count)); + } + Require(sceKernelClose(descriptor) == 0); + return text; +} +static std::string ReadStream(const std::string& path) { + void* stream = fopen_nid_postfix(path.c_str(), "rb"); + Require(stream != nullptr); + std::string text; + char buffer[4096]; + for (std::size_t count; (count = fread_nid_postfix(buffer, 1, sizeof(buffer), stream)) != 0;) text.append(buffer, count); + Require(fclose_nid_postfix(stream) == 0); + return text; +} +int main() { +#ifndef _WIN32 + const auto bundle = std::filesystem::absolute("host_bundle.pem"); + const std::string expected = "-----BEGIN CERTIFICATE-----\nMIIBAA==\n-----END CERTIFICATE-----\n"; + { std::ofstream file(bundle, std::ios::binary | std::ios::trunc); file << expected; } + Require(setenv("SSL_CERT_FILE", bundle.c_str(), 1) == 0); +#endif + const std::string directory = std::string("/") + sceKernelGetFsSandboxRandomWord() + "/common/cert"; + const std::string path = directory + "/CA_LIST.cer"; + const auto text = ReadKernel(path); + const auto begin = text.find("-----BEGIN CERTIFICATE-----"); + Require(begin != std::string::npos); + Require(text.find("-----END CERTIFICATE-----", begin) != std::string::npos); +#ifndef _WIN32 + Require(text == expected); +#endif + Require(ReadStream(path) == text); + Require(ReadKernel(directory + "/./ca_list.cer") == text); + FileStat status{}; + Require(sceKernelStat(path.c_str(), &status) == 0); + Require(status.st_size == static_cast(text.size())); + const auto cached = std::filesystem::current_path() / "cert_cache" / "CA_LIST.cer"; + Require(std::filesystem::is_regular_file(cached)); + Require((std::filesystem::status(cached).permissions() & std::filesystem::perms::owner_write) == std::filesystem::perms::none); + Require(sceKernelOpen((directory + "/CA_LIST2.cer").c_str(), 0, 0) == SCE_KERNEL_ERROR_ENOENT); + Require(sceKernelOpen((directory + "/CA_LIST.cer.bak").c_str(), 0, 0) == SCE_KERNEL_ERROR_ENOENT); + Require(fopen_nid_postfix((directory + "/CA_LIST2.cer").c_str(), "rb") == nullptr); + return 0; +} diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index c4fe965173..04df587f65 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -130,7 +130,8 @@ - [sceKernelPread, sceKernelPwrite, sceKernelPreadv, sceKernelPwritev](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) on Windows transfer at the given offset with an overlapped `ReadFile`/`WriteFile` and restore the descriptor's file pointer afterwards. Guest scalar transfers, positioned transfers, vectored transfers and seeks share a position lock across duplicated descriptors; a vector holds it for the whole operation. Independently opened descriptors and other processes do not share that lock, so vector transfers over their overlapping file ranges are not coordinated. Handles that are not disk files return `ESPIPE`. A pipe or console `readv` can return a short read when no more input is available; each iovec is transferred separately, so a failure after the first transfer returns the bytes already done - [sceKernelAioSubmitReadCommands, sceKernelAioSubmitWriteCommands and their Multiple variants](../../../core/libs/prx/libkernel/Aio/src/Aio.cpp) (libkernel) on Windows transfer through the same positioned path as `sceKernelPread`/`sceKernelPwrite` and restore the descriptor's file pointer afterwards; positioned transfers, plain transfers and seeks of one descriptor are serialized per descriptor (shared across duplicated descriptors), so a concurrent transfer observes a stable file pointer - [sceKernelAioSetParam](../../../core/libs/prx/libkernel/Aio/src/Aio.cpp) (libkernel) writes the five values into the 20-byte scheduling struct the first argument points to, following the declarations in [Orbital](https://github.com/AlexAltea/orbital/blob/cc408367da6a6ebbf5ac1ac559fefeadc73b2831/tools/ida/db_types.json), [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Kernel/KernelAio.cs) and [ampr_emu](https://github.com/drakmor/ampr_emu/blob/cfa85df379f6eeeb165d7badf9b648e266fe77b7/ps5/include/sce/_fs.h). Whether the console checks the values is unknown, so a null struct, a scheduling window or delayed count limit outside 0..128, a split flag other than 0 or 1 and a split size or chunk size above 0x1000000 (SharpProspero's maximums) throw. The scheduling settings are not used: `sceKernelAioInitializeImpl` ignores the struct and requests run synchronously on submit -- [sceKernelGetFsSandboxRandomWord](../../../core/libs/prx/libkernel/System/src/Sandbox.cpp) (libkernel) returns a fixed directory name, `gkbHtXc3Wq`. On the console the name is the process's randomized path (syscall 602, `randomized_path`), and the sandbox exposes system files under it, such as `common/lib` and `common/cert`. Nothing is mounted under the name here, so `/gkbHtXc3Wq/common/...` resolves under the guest root like any other path and does not exist +- [sceKernelGetFsSandboxRandomWord](../../../core/libs/prx/libkernel/System/src/Sandbox.cpp) (libkernel) returns a fixed directory name, `gkbHtXc3Wq`. On the console the name is the process's randomized path (syscall 602, `randomized_path`), and the sandbox exposes system files under it, such as `common/lib` and `common/cert`. Nothing is mounted under the name here, so `/gkbHtXc3Wq/common/...` resolves under the guest root like any other path and does not exist, except `/gkbHtXc3Wq/common/cert/CA_LIST.cer`, which [SandboxCertificates.cpp](../../../core/libs/prx/libkernel/File/src/SandboxCertificates.cpp) serves from the host trust store +- [The sandbox CA list](../../../core/libs/prx/libkernel/File/src/SandboxCertificates.cpp) (libkernel), `//common/cert/CA_LIST.cer`, is a PEM bundle of the host's trust anchors, written once per process to `cert_cache/CA_LIST.cer` in the run directory with the owner write bit cleared: on Windows the certificates of the current user's `ROOT` and `CA` system stores (Windows fetches missing roots on demand, so a root that was never fetched is absent), elsewhere `SSL_CERT_FILE` or the first of `/etc/ssl/certs/ca-certificates.crt`, `/etc/pki/tls/certs/ca-bundle.crt` and `/etc/ssl/ca-bundle.pem` that exists. Without any certificate the path does not exist. The console file's format is unverified: only that bundled OpenSSL accepts a PEM bundle there is shown (ProsperoRadio PPSA99001 and SwanStation PPSA98510, #5001). The other entries of the console's `common/cert` directory, the directory itself (it does not exist here) and the error a write open gets (`EACCES` from the host here) are unverified - [getcontext and setcontext](../../../core/libs/prx/libkernel/System/src/UserContext.cpp) (libkernel) and [makecontext](../../../core/libs/prx/libSceLibcInternal/UserContext.cpp) (libSceLibcInternal) use FreeBSD 11's amd64 `ucontext_t` (`mcontext` at 0x10, `mc_len` 800, `uc_link` at 0x330, `uc_stack` at 0x338, 0x370 bytes), the layout homebrew built against FreeBSD's headers passes: PPSA99039's OpenSSL `ASYNC` (`crypto/async/arch/async_posix.c`) captures into a 0x370-byte stack buffer. `getcontext` writes the 0x330 bytes before `uc_link`, as FreeBSD's `UC_COPY_SIZE`. The console's own layout for these functions is not verified; the exception handler's context is a different one (see `sceKernelRaiseException`) - [getcontext and setcontext](../../../core/libs/prx/libkernel/System/src/UserContext.cpp) (libkernel) keep the 512-byte FXSAVE image in `mc_fpstate` (x87, MXCSR, XMM0-15), so the upper halves of YMM0-15 are neither saved nor restored, and `getcontext` leaves `mc_xfpustate` and `mc_xfpustate_len` 0. `getcontext` sets `mc_fpformat` and `mc_ownedfp` to FreeBSD's `_MC_FPFMT_XMM` and `_MC_FPOWNED_FPU`, `mc_cs` and `mc_ss` to the host selectors, and `mc_flags`, `mc_fsbase` and `mc_gsbase` to 0. `setcontext` loads `mc_fpstate` whatever `mc_fpformat` and `mc_ownedfp` say, and restores neither the segment selectors, `mc_fsbase` and `mc_gsbase` nor the extended state, which FreeBSD restores when `mc_flags` asks for them - [setcontext](../../../core/libs/prx/libkernel/System/src/UserContext.cpp) (libkernel) and [longjmp](../../../core/libs/prx/libc/src/JumpBuffer.cpp) (libc) on Windows set the thread's stack bounds in the TEB to the stack they switch to: a `makecontext` stack, or else the committed region around the target stack pointer. A switch to any other guest stack, such as a context built by hand, gets the bounds of that region only From eb26fb97f8d1b75e0c7b2ef410c51a89b02a77ee Mon Sep 17 00:00:00 2001 From: Santhosh Kumar Date: Tue, 13 Oct 2026 16:12:28 +0530 Subject: [PATCH 0629/1000] fix(libkernel): filter sandbox TLS roots and preserve path overrides --- core/libs/prx/libc/src/General.cpp | 8 +- core/libs/prx/libkernel/CMakeLists.txt | 9 +++ .../libkernel/File/include/TrustedRoots.hpp | 17 ++++ .../File/src/SandboxCertificates.cpp | 34 +------- .../prx/libkernel/File/src/TrustedRoots.cpp | 78 +++++++++++++++++++ .../tests/GuestSandboxCertificateTrust.cpp | 63 +++++++++++++++ core/libs/tests/GuestSandboxCertificates.cpp | 26 +++++++ docs/dev/debt/libkernel.md | 2 +- 8 files changed, 202 insertions(+), 35 deletions(-) create mode 100644 core/libs/prx/libkernel/File/include/TrustedRoots.hpp create mode 100644 core/libs/prx/libkernel/File/src/TrustedRoots.cpp create mode 100644 core/libs/tests/GuestSandboxCertificateTrust.cpp diff --git a/core/libs/prx/libc/src/General.cpp b/core/libs/prx/libc/src/General.cpp index c014fa324c..0f18419dfd 100644 --- a/core/libs/prx/libc/src/General.cpp +++ b/core/libs/prx/libc/src/General.cpp @@ -182,18 +182,18 @@ std::optional ProvidedPath(const WorkingDirectory& state, } } if (provide == nullptr) return std::nullopt; + const auto supplied = ResolveHostPath(state.root, guest.relative_path()); + std::error_code error; + if (std::filesystem::exists(supplied, error) || error) return supplied; auto provided = provide(state.root); if (provided.empty()) return std::nullopt; return provided.make_preferred(); } std::filesystem::path HostPath(WorkingDirectory& state, const std::filesystem::path& guest, bool module = false) { if (auto aliased = ResolveAlias(guest.relative_path().generic_string(), module)) return *aliased; - if (!module) { - if (auto provided = ProvidedPath(state, guest)) return *provided; - } + if (auto provided = ProvidedPath(state, guest)) return *provided; return ResolveHostPath(state.root, guest.relative_path(), module).make_preferred(); } -} void RequireHostDirectory(WorkingDirectory& state, const std::filesystem::path& guest) { const auto host = HostPath(state, guest); std::error_code error; diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 1203674b39..8684f5b161 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -83,6 +83,7 @@ target_include_directories(libkernel PRIVATE ${LIBS_INCLUDE_DIR}/nid/include) target_link_libraries(libkernel PRIVATE ${CMAKE_DL_LIBS}) target_link_libraries(libkernel PRIVATE libc) if(WIN32) + target_sources(libkernel PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/File/src/TrustedRoots.cpp) target_link_libraries(libkernel PRIVATE ws2_32 shell32 advapi32 crypt32 wbemuuid ole32 oleaut32) target_link_libraries(libkernel PRIVATE iphlpapi) target_compile_definitions(libkernel PRIVATE _WIN32_WINNT=0x0602) @@ -306,7 +307,15 @@ if(BUILD_TESTING) set(sandboxCertificatesRun ${CMAKE_CURRENT_BINARY_DIR}/guest_sandbox_certificates) file(MAKE_DIRECTORY ${sandboxCertificatesRun}) add_test(NAME guest_sandbox_certificates COMMAND guest_sandbox_certificates_tests WORKING_DIRECTORY ${sandboxCertificatesRun}) + set_tests_properties(guest_sandbox_certificates PROPERTIES TIMEOUT 20) if(WIN32) + add_test_executable(guest_sandbox_certificate_trust_tests ${LIBS_INCLUDE_DIR}/tests/GuestSandboxCertificateTrust.cpp) + target_include_directories(guest_sandbox_certificate_trust_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_sandbox_certificate_trust_tests PRIVATE libkernel libc crypt32) + configure_windows_unwind(guest_sandbox_certificate_trust_tests) + add_test(NAME guest_sandbox_certificate_trust COMMAND guest_sandbox_certificate_trust_tests) + set_tests_properties(guest_sandbox_certificate_trust PROPERTIES TIMEOUT 10 ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") set_tests_properties(guest_sandbox_certificates PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() diff --git a/core/libs/prx/libkernel/File/include/TrustedRoots.hpp b/core/libs/prx/libkernel/File/include/TrustedRoots.hpp new file mode 100644 index 0000000000..950d25dcff --- /dev/null +++ b/core/libs/prx/libkernel/File/include/TrustedRoots.hpp @@ -0,0 +1,17 @@ +#ifndef CORE_LIBS_PRX_LIBKERNEL_FILE_TRUSTEDROOTS_HPP +#define CORE_LIBS_PRX_LIBKERNEL_FILE_TRUSTEDROOTS_HPP + +#ifndef NOMINMAX +#define NOMINMAX +#endif +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#include +#include +#include + +extern "C" std::string ExportHostRoots_nid_no_patch(HCERTSTORE roots, HCERTSTORE disallowed); +extern "C" std::string HostTrustedTlsRoots_nid_no_patch(); + +#endif diff --git a/core/libs/prx/libkernel/File/src/SandboxCertificates.cpp b/core/libs/prx/libkernel/File/src/SandboxCertificates.cpp index 16e6cd153f..d41befaec6 100644 --- a/core/libs/prx/libkernel/File/src/SandboxCertificates.cpp +++ b/core/libs/prx/libkernel/File/src/SandboxCertificates.cpp @@ -5,7 +5,6 @@ #include #include #include -#include #include #include #include @@ -16,7 +15,7 @@ #define NOMINMAX #endif #include -#include +#include "prx/libkernel/File/include/TrustedRoots.hpp" #else #include #endif @@ -35,40 +34,15 @@ std::optional ReadFile(const std::filesystem::path& path) { } #ifdef _WIN32 -void AppendStore(const wchar_t* name, std::string& bundle) { - const std::unique_ptr store(CertOpenSystemStoreW(0, name), [](HCERTSTORE handle) { CertCloseStore(handle, 0); }); - if (store == nullptr) throw std::system_error(static_cast(GetLastError()), std::system_category(), "Opening the host certificate store"); - for (PCCERT_CONTEXT certificate = CertEnumCertificatesInStore(store.get(), nullptr); certificate != nullptr; - certificate = CertEnumCertificatesInStore(store.get(), certificate)) { - DWORD length = 0; - std::wstring text; - bool encoded = CryptBinaryToStringW(certificate->pbCertEncoded, certificate->cbCertEncoded, CRYPT_STRING_BASE64HEADER, nullptr, &length); - if (encoded) { - text.resize(length); - encoded = CryptBinaryToStringW(certificate->pbCertEncoded, certificate->cbCertEncoded, CRYPT_STRING_BASE64HEADER, text.data(), &length); - } - if (!encoded) { - const auto error = GetLastError(); - CertFreeCertificateContext(certificate); - throw std::system_error(static_cast(error), std::system_category(), "Encoding a host certificate"); - } - text.resize(length); - for (const wchar_t character : text) bundle.push_back(static_cast(character)); - } -} - std::string HostCertificates() { - std::string bundle; - AppendStore(L"ROOT", bundle); - AppendStore(L"CA", bundle); - return bundle; + return HostTrustedTlsRoots_nid_no_patch(); } unsigned long ProcessId() { return GetCurrentProcessId(); } #else std::string HostCertificates() { - std::array candidates{std::getenv("SSL_CERT_FILE"), "/etc/ssl/certs/ca-certificates.crt", "/etc/pki/tls/certs/ca-bundle.crt", - "/etc/ssl/ca-bundle.pem"}; + std::array candidates{std::getenv("SSL_CERT_FILE"), "/etc/ssl/certs/ca-certificates.crt", "/etc/pki/tls/certs/ca-bundle.crt", + "/etc/ssl/ca-bundle.pem", "/etc/ssl/cert.pem"}; for (const char* candidate : candidates) { std::error_code error; if (candidate == nullptr || *candidate == '\0' || !std::filesystem::is_regular_file(candidate, error)) continue; diff --git a/core/libs/prx/libkernel/File/src/TrustedRoots.cpp b/core/libs/prx/libkernel/File/src/TrustedRoots.cpp new file mode 100644 index 0000000000..e5f1f4f0c4 --- /dev/null +++ b/core/libs/prx/libkernel/File/src/TrustedRoots.cpp @@ -0,0 +1,78 @@ +#include "prx/libkernel/File/include/TrustedRoots.hpp" + +#include +#include +#include +#include +#include + +namespace { + +void RequireCrypto(bool success, const char* operation) { + if (!success) throw std::system_error(static_cast(GetLastError()), std::system_category(), operation); +} + +bool AllowsServerAuthentication(PCCERT_CONTEXT certificate) { + DWORD size = 0; + RequireCrypto(CertGetEnhancedKeyUsage(certificate, 0, nullptr, &size), "Reading root certificate usage size"); + std::vector storage(size); + auto* usage = reinterpret_cast(storage.data()); + SetLastError(ERROR_SUCCESS); + RequireCrypto(CertGetEnhancedKeyUsage(certificate, 0, usage, &size), "Reading root certificate usage"); + if (usage->cUsageIdentifier == 0) return GetLastError() == static_cast(CRYPT_E_NOT_FOUND); + for (DWORD index = 0; index < usage->cUsageIdentifier; ++index) { + const auto* identifier = usage->rgpszUsageIdentifier[index]; + if (std::strcmp(identifier, szOID_PKIX_KP_SERVER_AUTH) == 0 || std::strcmp(identifier, szOID_ANY_ENHANCED_KEY_USAGE) == 0) + return true; + } + return false; +} + +std::string Pem(PCCERT_CONTEXT certificate) { + DWORD size = 0; + RequireCrypto(CryptBinaryToStringA(certificate->pbCertEncoded, certificate->cbCertEncoded, + CRYPT_STRING_BASE64HEADER, nullptr, &size), "Measuring a PEM root certificate"); + std::string pem(size, '\0'); + RequireCrypto(CryptBinaryToStringA(certificate->pbCertEncoded, certificate->cbCertEncoded, + CRYPT_STRING_BASE64HEADER, pem.data(), &size), "Encoding a PEM root certificate"); + pem.resize(size); + return pem; +} + +} + +extern "C" std::string ExportHostRoots_nid_no_patch(HCERTSTORE roots, HCERTSTORE disallowed) { + if (!roots || !disallowed) throw std::invalid_argument("ExportHostRoots: null certificate store"); + std::string bundle; + using Certificate = std::unique_ptr; + Certificate certificate(nullptr, &CertFreeCertificateContext); + for (;;) { + auto* next = CertEnumCertificatesInStore(roots, certificate.release()); + if (!next) { + const auto error = GetLastError(); + if (error != static_cast(CRYPT_E_NOT_FOUND)) + throw std::system_error(static_cast(error), std::system_category(), "Enumerating trusted roots"); + break; + } + certificate.reset(next); + Certificate rejected(CertFindCertificateInStore(disallowed, X509_ASN_ENCODING | PKCS_7_ASN_ENCODING, + 0, CERT_FIND_EXISTING, certificate.get(), nullptr), &CertFreeCertificateContext); + if (rejected) continue; + const auto error = GetLastError(); + if (error != static_cast(CRYPT_E_NOT_FOUND)) + throw std::system_error(static_cast(error), std::system_category(), "Checking distrusted roots"); + if (AllowsServerAuthentication(certificate.get())) bundle += Pem(certificate.get()); + } + return bundle; +} + +extern "C" std::string HostTrustedTlsRoots_nid_no_patch() { + const auto closeStore = [](void* store) { CertCloseStore(store, 0); }; + using Store = std::unique_ptr; + const DWORD flags = CERT_SYSTEM_STORE_CURRENT_USER | CERT_STORE_READONLY_FLAG | CERT_STORE_OPEN_EXISTING_FLAG; + Store roots(CertOpenStore(CERT_STORE_PROV_SYSTEM_W, 0, 0, flags, L"ROOT"), closeStore); + RequireCrypto(roots != nullptr, "Opening the Windows trusted root store"); + Store disallowed(CertOpenStore(CERT_STORE_PROV_SYSTEM_W, 0, 0, flags, L"Disallowed"), closeStore); + RequireCrypto(disallowed != nullptr, "Opening the Windows disallowed certificate store"); + return ExportHostRoots_nid_no_patch(roots.get(), disallowed.get()); +} diff --git a/core/libs/tests/GuestSandboxCertificateTrust.cpp b/core/libs/tests/GuestSandboxCertificateTrust.cpp new file mode 100644 index 0000000000..c2fdb60f40 --- /dev/null +++ b/core/libs/tests/GuestSandboxCertificateTrust.cpp @@ -0,0 +1,63 @@ +#include "prx/libkernel/File/include/TrustedRoots.hpp" + +#include +#include +#include + +static void Check(bool value, int line) { + if (!value) { + std::fprintf(stderr, "Certificate trust check failed at line %d\n", line); + std::abort(); + } +} +#define Require(value) Check((value), __LINE__) + +static constexpr char Certificate[] = R"(-----BEGIN CERTIFICATE----- +MIIBijCCATGgAwIBAgIUFMQYJ1in3pTJaCqjg6JVgSBNQgkwCgYIKoZIzj0EAwIw +GzEZMBcGA1UEAwwQQW55UFM1LVRlc3QtUm9vdDAeFw0yNjEwMTMwOTE2NTRaFw0z +NjEwMTAwOTE2NTRaMBsxGTAXBgNVBAMMEEFueVBTNS1UZXN0LVJvb3QwWTATBgcq +hkjOPQIBBggqhkjOPQMBBwNCAAQiHlYF9b/zaT5lFC26MOBndhdySsdLsuthNut0 +fhQkEHlybL5nUrvOPZrkV1e/vEZ2ykmq7xMkSYeIHnNy4wxMo1MwUTAdBgNVHQ4E +FgQUJKW6nreyN5yBQn0axEJkZnkfZB4wHwYDVR0jBBgwFoAUJKW6nreyN5yBQn0a +xEJkZnkfZB4wDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAgNHADBEAiBfTER0 +UmcQ0u8NzHTODNgVOHZHQFrYjcOyVjzZlSKomQIgDhztVM5obBElAnm22JgWEPk+ +Bk30RAcx5xk3rjar3qs= +-----END CERTIFICATE----- +)"; + +static void CheckWindowsTrust() { + DWORD size = 0; + Require(CryptStringToBinaryA(Certificate, sizeof(Certificate) - 1, CRYPT_STRING_BASE64HEADER, nullptr, &size, nullptr, nullptr)); + std::vector der(size); + Require(CryptStringToBinaryA(Certificate, sizeof(Certificate) - 1, CRYPT_STRING_BASE64HEADER, der.data(), &size, nullptr, nullptr)); + const auto roots = CertOpenStore(CERT_STORE_PROV_MEMORY, 0, 0, 0, nullptr); + const auto disallowed = CertOpenStore(CERT_STORE_PROV_MEMORY, 0, 0, 0, nullptr); + Require(roots && disallowed); + PCCERT_CONTEXT certificate = nullptr; + Require(CertAddEncodedCertificateToStore(roots, X509_ASN_ENCODING, der.data(), size, CERT_STORE_ADD_NEW, &certificate)); + const auto unrestricted = ExportHostRoots_nid_no_patch(roots, disallowed); + Require(!unrestricted.empty() && unrestricted.find('\0') == std::string::npos); + DWORD decodedSize = size; + std::vector decoded(decodedSize); + Require(CryptStringToBinaryA(unrestricted.c_str(), static_cast(unrestricted.size()), CRYPT_STRING_BASE64HEADER, + decoded.data(), &decodedSize, nullptr, nullptr)); + Require(decoded == der); + CERT_ENHKEY_USAGE usage{}; + Require(CertSetEnhancedKeyUsage(certificate, &usage)); + Require(ExportHostRoots_nid_no_patch(roots, disallowed).empty()); + char* email = const_cast(szOID_PKIX_KP_EMAIL_PROTECTION); + usage = {1, &email}; + Require(CertSetEnhancedKeyUsage(certificate, &usage)); + Require(ExportHostRoots_nid_no_patch(roots, disallowed).empty()); + char* server = const_cast(szOID_PKIX_KP_SERVER_AUTH); + usage = {1, &server}; + Require(CertSetEnhancedKeyUsage(certificate, &usage)); + Require(ExportHostRoots_nid_no_patch(roots, disallowed) == unrestricted); + Require(CertAddCertificateContextToStore(disallowed, certificate, CERT_STORE_ADD_NEW, nullptr)); + Require(ExportHostRoots_nid_no_patch(roots, disallowed).empty()); + Require(CertFreeCertificateContext(certificate)); + Require(CertCloseStore(disallowed, CERT_CLOSE_STORE_CHECK_FLAG)); + Require(CertCloseStore(roots, CERT_CLOSE_STORE_CHECK_FLAG)); +} + +int main() { CheckWindowsTrust(); } diff --git a/core/libs/tests/GuestSandboxCertificates.cpp b/core/libs/tests/GuestSandboxCertificates.cpp index 4f99b3f312..c95eded2f8 100644 --- a/core/libs/tests/GuestSandboxCertificates.cpp +++ b/core/libs/tests/GuestSandboxCertificates.cpp @@ -45,6 +45,31 @@ static std::string ReadStream(const std::string& path) { Require(fclose_nid_postfix(stream) == 0); return text; } +static void CheckOverrides(const std::string& directory, const std::string& path) { + const auto root = std::filesystem::current_path(); + const auto supplied = root / std::filesystem::path(path).relative_path(); + std::filesystem::create_directories(supplied.parent_path()); + const std::string expected = "explicit guest certificate bundle\n"; + { std::ofstream file(supplied, std::ios::binary | std::ios::trunc); file << expected; Require(static_cast(file)); } + Require(ReadKernel(path) == expected); + Require(ReadStream(path) == expected); + Require(ResolvePath_nid_no_patch(path.c_str()) == supplied); + Require(std::filesystem::remove(supplied)); + for (auto parent = supplied.parent_path(); parent != root; parent = parent.parent_path()) + Require(std::filesystem::remove(parent)); + const auto alias = root / "sandbox_certificate_override"; + std::filesystem::create_directory(alias); + { std::ofstream file(alias / "CA_LIST.cer", std::ios::binary | std::ios::trunc); file << expected; Require(static_cast(file)); } + AddPathAlias_nid_no_patch(directory.c_str(), alias.string().c_str()); + Require(ReadKernel(path) == expected && ReadStream(path) == expected); + BlockPathAlias_nid_no_patch(directory.c_str()); + Require(sceKernelOpen(path.c_str(), 0, 0) == SCE_KERNEL_ERROR_ENOENT); + Require(fopen_nid_postfix(path.c_str(), "rb") == nullptr); + AddPathAlias_nid_no_patch(directory.c_str(), alias.string().c_str()); + RemovePathAlias_nid_no_patch(directory.c_str()); + Require(std::filesystem::remove(alias / "CA_LIST.cer")); + Require(std::filesystem::remove(alias)); +} int main() { #ifndef _WIN32 const auto bundle = std::filesystem::absolute("host_bundle.pem"); @@ -54,6 +79,7 @@ int main() { #endif const std::string directory = std::string("/") + sceKernelGetFsSandboxRandomWord() + "/common/cert"; const std::string path = directory + "/CA_LIST.cer"; + CheckOverrides(directory, path); const auto text = ReadKernel(path); const auto begin = text.find("-----BEGIN CERTIFICATE-----"); Require(begin != std::string::npos); diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 04df587f65..ceaafa5cbc 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -131,7 +131,7 @@ - [sceKernelAioSubmitReadCommands, sceKernelAioSubmitWriteCommands and their Multiple variants](../../../core/libs/prx/libkernel/Aio/src/Aio.cpp) (libkernel) on Windows transfer through the same positioned path as `sceKernelPread`/`sceKernelPwrite` and restore the descriptor's file pointer afterwards; positioned transfers, plain transfers and seeks of one descriptor are serialized per descriptor (shared across duplicated descriptors), so a concurrent transfer observes a stable file pointer - [sceKernelAioSetParam](../../../core/libs/prx/libkernel/Aio/src/Aio.cpp) (libkernel) writes the five values into the 20-byte scheduling struct the first argument points to, following the declarations in [Orbital](https://github.com/AlexAltea/orbital/blob/cc408367da6a6ebbf5ac1ac559fefeadc73b2831/tools/ida/db_types.json), [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Kernel/KernelAio.cs) and [ampr_emu](https://github.com/drakmor/ampr_emu/blob/cfa85df379f6eeeb165d7badf9b648e266fe77b7/ps5/include/sce/_fs.h). Whether the console checks the values is unknown, so a null struct, a scheduling window or delayed count limit outside 0..128, a split flag other than 0 or 1 and a split size or chunk size above 0x1000000 (SharpProspero's maximums) throw. The scheduling settings are not used: `sceKernelAioInitializeImpl` ignores the struct and requests run synchronously on submit - [sceKernelGetFsSandboxRandomWord](../../../core/libs/prx/libkernel/System/src/Sandbox.cpp) (libkernel) returns a fixed directory name, `gkbHtXc3Wq`. On the console the name is the process's randomized path (syscall 602, `randomized_path`), and the sandbox exposes system files under it, such as `common/lib` and `common/cert`. Nothing is mounted under the name here, so `/gkbHtXc3Wq/common/...` resolves under the guest root like any other path and does not exist, except `/gkbHtXc3Wq/common/cert/CA_LIST.cer`, which [SandboxCertificates.cpp](../../../core/libs/prx/libkernel/File/src/SandboxCertificates.cpp) serves from the host trust store -- [The sandbox CA list](../../../core/libs/prx/libkernel/File/src/SandboxCertificates.cpp) (libkernel), `//common/cert/CA_LIST.cer`, is a PEM bundle of the host's trust anchors, written once per process to `cert_cache/CA_LIST.cer` in the run directory with the owner write bit cleared: on Windows the certificates of the current user's `ROOT` and `CA` system stores (Windows fetches missing roots on demand, so a root that was never fetched is absent), elsewhere `SSL_CERT_FILE` or the first of `/etc/ssl/certs/ca-certificates.crt`, `/etc/pki/tls/certs/ca-bundle.crt` and `/etc/ssl/ca-bundle.pem` that exists. Without any certificate the path does not exist. The console file's format is unverified: only that bundled OpenSSL accepts a PEM bundle there is shown (ProsperoRadio PPSA99001 and SwanStation PPSA98510, #5001). The other entries of the console's `common/cert` directory, the directory itself (it does not exist here) and the error a write open gets (`EACCES` from the host here) are unverified +- [The sandbox CA list](../../../core/libs/prx/libkernel/File/src/SandboxCertificates.cpp) (libkernel), `//common/cert/CA_LIST.cer`, is a PEM bundle of the host's trust anchors, written once per process to `cert_cache/CA_LIST.cer` in the run directory with the owner write bit cleared: on Windows the current user's `ROOT` store, filtered through Disallowed and the combined EKU extension/property for TLS server authentication (Windows fetches missing roots on demand, so a root that was never fetched is absent), elsewhere `SSL_CERT_FILE` or the first of `/etc/ssl/certs/ca-certificates.crt`, `/etc/pki/tls/certs/ca-bundle.crt`, `/etc/ssl/ca-bundle.pem` and `/etc/ssl/cert.pem` that exists. On macOS, `/etc/ssl/cert.pem` is a static snapshot and does not reflect roots added to or distrusted in the Keychain. Existing guest files, explicit aliases and blocked mounts take precedence over the provider. The PEM snapshot does not carry Windows CTL restrictions beyond EKU, native revocation policy or later trust-store changes. Without any certificate the path does not exist. The console file's format is unverified: only that bundled OpenSSL accepts a PEM bundle there is shown (ProsperoRadio PPSA99001 and SwanStation PPSA98510, #5001). The other entries of the console's `common/cert` directory, the directory itself (it does not exist here) and the error a write open gets (`EACCES` from the host here) are unverified - [getcontext and setcontext](../../../core/libs/prx/libkernel/System/src/UserContext.cpp) (libkernel) and [makecontext](../../../core/libs/prx/libSceLibcInternal/UserContext.cpp) (libSceLibcInternal) use FreeBSD 11's amd64 `ucontext_t` (`mcontext` at 0x10, `mc_len` 800, `uc_link` at 0x330, `uc_stack` at 0x338, 0x370 bytes), the layout homebrew built against FreeBSD's headers passes: PPSA99039's OpenSSL `ASYNC` (`crypto/async/arch/async_posix.c`) captures into a 0x370-byte stack buffer. `getcontext` writes the 0x330 bytes before `uc_link`, as FreeBSD's `UC_COPY_SIZE`. The console's own layout for these functions is not verified; the exception handler's context is a different one (see `sceKernelRaiseException`) - [getcontext and setcontext](../../../core/libs/prx/libkernel/System/src/UserContext.cpp) (libkernel) keep the 512-byte FXSAVE image in `mc_fpstate` (x87, MXCSR, XMM0-15), so the upper halves of YMM0-15 are neither saved nor restored, and `getcontext` leaves `mc_xfpustate` and `mc_xfpustate_len` 0. `getcontext` sets `mc_fpformat` and `mc_ownedfp` to FreeBSD's `_MC_FPFMT_XMM` and `_MC_FPOWNED_FPU`, `mc_cs` and `mc_ss` to the host selectors, and `mc_flags`, `mc_fsbase` and `mc_gsbase` to 0. `setcontext` loads `mc_fpstate` whatever `mc_fpformat` and `mc_ownedfp` say, and restores neither the segment selectors, `mc_fsbase` and `mc_gsbase` nor the extended state, which FreeBSD restores when `mc_flags` asks for them - [setcontext](../../../core/libs/prx/libkernel/System/src/UserContext.cpp) (libkernel) and [longjmp](../../../core/libs/prx/libc/src/JumpBuffer.cpp) (libc) on Windows set the thread's stack bounds in the TEB to the stack they switch to: a `makecontext` stack, or else the committed region around the target stack pointer. A switch to any other guest stack, such as a context built by hand, gets the bounds of that region only From d5dbce438adb2e59a5c1280980e8e3227b81004f Mon Sep 17 00:00:00 2001 From: thuanquang <60096168+thuanquang@users.noreply.github.com> Date: Sat, 17 Oct 2026 09:41:59 +0700 Subject: [PATCH 0630/1000] test(agc): map GPU memory on fibers with and without a host stack Since #6003 a fiber with a context of 64 KiB or more runs on the context itself, so the 64 KiB fiber of agc_driver_gpu_map_on_fiber imports its mapping on a host thread on Windows too, and the test's Windows-only expectation of no thread failed. The test now also maps on a 16 KiB fiber, which keeps a host stack and imports on it on Windows, and expects one import thread for the 64 KiB fiber on every platform. Co-Authored-By: Claude Opus 5.5 --- .../tests/execution/GpuMapOnFiber.cpp | 46 ++++++++++++------- 1 file changed, 30 insertions(+), 16 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/GpuMapOnFiber.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/GpuMapOnFiber.cpp index f415d1e5e8..aefdae5293 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/GpuMapOnFiber.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/GpuMapOnFiber.cpp @@ -31,6 +31,7 @@ namespace { using AgcDriver::DriverDetail::Driver; using AgcDriver::Graphics::Require; +constexpr std::size_t HostStackContextBytes = 16 * 1024; constexpr std::size_t ContextBytes = 64 * 1024; constexpr std::size_t MappingBytes = 4 * 65536; constexpr std::size_t FiberStackLimit = 8 * 1024; @@ -84,10 +85,28 @@ void ReleaseMapping() { Mapping = nullptr; } -std::size_t FiberStackUsed() { +std::size_t FiberStackUsed(std::size_t contextBytes) { + const std::size_t words = contextBytes / sizeof(std::uint64_t); std::size_t untouched = 0; - while (untouched < FiberContext.size() && FiberContext[untouched] == Fill) ++untouched; - return (FiberContext.size() - untouched) * sizeof(std::uint64_t); + while (untouched < words && FiberContext[untouched] == Fill) ++untouched; + return (words - untouched) * sizeof(std::uint64_t); +} + +struct FiberMapping { + std::uint64_t threads = 0; + std::size_t used = 0; +}; + +FiberMapping MapOnFiberWithContext(std::size_t contextBytes) { + FiberContext.fill(Fill); + auto* fiber = reinterpret_cast(&FiberObjectStorage); + Require(_sceFiberInitializeImpl_nid_postfix(fiber, "gpu map", MapOnFiber, 0, FiberContext.data(), contextBytes, nullptr, 0) == 0, "GPU mapping on a fiber: the fiber was not initialized"); + const auto before = Driver::GpuMapImportThreads(); + Require(sceFiberRun_nid_postfix(fiber, 0, nullptr) == 0, "GPU mapping on a fiber: the fiber did not run"); + const auto threads = Driver::GpuMapImportThreads() - before; + Require(sceFiberFinalize(fiber) == 0, "GPU mapping on a fiber: the fiber was not finalized"); + GuestAllocations::Mutation().Remove(Mapping); + return {threads, FiberStackUsed(contextBytes)}; } } @@ -103,29 +122,24 @@ int main() { #endif Require(Mapping != nullptr, "GPU mapping on a fiber: cannot allocate the mapped memory"); std::memset(Mapping, 0, MappingBytes); - FiberContext.fill(Fill); - auto* fiber = reinterpret_cast(&FiberObjectStorage); - Require(_sceFiberInitializeImpl_nid_postfix(fiber, "gpu map", MapOnFiber, 0, FiberContext.data(), ContextBytes, nullptr, 0) == 0, "GPU mapping on a fiber: the fiber was not initialized"); - const auto beforeFiber = Driver::GpuMapImportThreads(); - Require(sceFiberRun_nid_postfix(fiber, 0, nullptr) == 0, "GPU mapping on a fiber: the fiber did not run"); - const auto onFiber = Driver::GpuMapImportThreads() - beforeFiber; - Require(sceFiberFinalize(fiber) == 0, "GPU mapping on a fiber: the fiber was not finalized"); - const auto used = FiberStackUsed(); - GuestAllocations::Mutation().Remove(Mapping); + const auto onHostStack = MapOnFiberWithContext(HostStackContextBytes); + const auto onContext = MapOnFiberWithContext(ContextBytes); const auto beforeThread = Driver::GpuMapImportThreads(); MapForGpu(); const auto onThread = Driver::GpuMapImportThreads() - beforeThread; GuestAllocations::Mutation().Remove(Mapping); AgcDriverShutdown_nid_postfix(); ReleaseMapping(); - Require(used <= FiberStackLimit, "GPU mapping on a fiber used " + std::to_string(used) + " bytes of the fiber's stack, more than " + std::to_string(FiberStackLimit) + ": the import of the mapped memory ran on it"); + Require(onHostStack.used <= FiberStackLimit, "GPU mapping on a fiber with a host stack used " + std::to_string(onHostStack.used) + " bytes of the fiber's context, more than " + std::to_string(FiberStackLimit) + ": the import of the mapped memory ran on it"); + Require(onContext.used <= FiberStackLimit, "GPU mapping on a fiber running on its context used " + std::to_string(onContext.used) + " bytes of it, more than " + std::to_string(FiberStackLimit) + ": the import of the mapped memory ran on it"); #ifdef _WIN32 - Require(onFiber == 0, "GPU mapping on a fiber started " + std::to_string(onFiber) + " host threads for its import instead of importing on the fiber's host stack"); + Require(onHostStack.threads == 0, "GPU mapping on a fiber with a host stack started " + std::to_string(onHostStack.threads) + " host threads for its import instead of importing on the fiber's host stack"); #else - Require(onFiber == 1, "GPU mapping on a fiber started " + std::to_string(onFiber) + " host threads for its import instead of one"); + Require(onHostStack.threads == 1, "GPU mapping on a fiber with a host stack started " + std::to_string(onHostStack.threads) + " host threads for its import instead of one"); #endif + Require(onContext.threads == 1, "GPU mapping on a fiber running on its context started " + std::to_string(onContext.threads) + " host threads for its import instead of one"); Require(onThread == 0, "GPU mapping on an ordinary thread started " + std::to_string(onThread) + " host threads for its import instead of importing on its own stack"); - std::cout << "A GPU mapping made on a fiber used " << used << " bytes of its stack\n"; + std::cout << "A GPU mapping made on a fiber used " << onHostStack.used << " bytes of its context with a host stack and " << onContext.used << " running on the context\n"; return 0; } catch (const std::exception& error) { std::cerr << error.what() << '\n'; From f7e6131565a9fcff715794fbf38f95606da3c014 Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Sat, 17 Oct 2026 03:01:05 +0000 Subject: [PATCH 0631/1000] fix(agc): fall back when host-write alias is refused --- core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index 16604afe97..ce2452bd27 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -1131,6 +1131,7 @@ const HostImport* importRunAt(const Context& context, HostImports& state, Import state.unaliasedSpans.insert(base); return importAllocation(context, state, address, lease, end, evict); } + if (entry.alias == nullptr) return nullptr; } #else if (!GuestMemory::Accessible(reinterpret_cast(base), static_cast(bytes), true)) { From d76ea9a6bede961f431dd8072f5ae518b991491e Mon Sep 17 00:00:00 2001 From: Nithishvelan M Date: Sat, 17 Oct 2026 08:38:06 +0530 Subject: [PATCH 0632/1000] docs: drop unverified note on macOS host game execution --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 57c93abbf4..5777022fb4 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Tool for automatic executables porting to Linux and Windows. -The relinker-only build runs on macOS, including Apple silicon. Game execution on a macOS host is not yet supported. +The relinker-only build runs on macOS, including Apple silicon. [**Web converter**](https://boykopovar.github.io/AnyPS5/) - files stay on your device. From 63cfca8466829af1c48681951b7861aff1926f3c Mon Sep 17 00:00:00 2001 From: Nithishvelan M Date: Sat, 17 Oct 2026 08:43:48 +0530 Subject: [PATCH 0633/1000] feat(libSceUserService): declare KeyremapEnable and Zoom as throwing stubs --- core/libs/prx/libSceUserService/Export.cpp | 10 ++++++---- core/libs/tests/GuestUserService.cpp | 22 ++++++++++++++++++---- docs/dev/debt/libSceUserService.md | 3 ++- 3 files changed, 26 insertions(+), 9 deletions(-) diff --git a/core/libs/prx/libSceUserService/Export.cpp b/core/libs/prx/libSceUserService/Export.cpp index 79c8ec3a95..9f258f9aa8 100644 --- a/core/libs/prx/libSceUserService/Export.cpp +++ b/core/libs/prx/libSceUserService/Export.cpp @@ -26,8 +26,9 @@ int APS5_VABI sceUserServiceGetAccessibilityChatTranscription(int user_id, int32 return ReportAccessibilitySetting(user_id, chat_transcription, 0); } -int APS5_VABI sceUserServiceGetAccessibilityKeyremapEnable(int user_id, int32_t* enable) { - return ReportAccessibilitySetting(user_id, enable, 0); +int APS5_VABI sceUserServiceGetAccessibilityKeyremapEnable() { + NotImplemented_nid_no_patch(__func__); + return 0; } int APS5_VABI sceUserServiceGetAccessibilityPressAndHoldDelay(int user_id, int32_t* press_and_hold_delay) { @@ -42,8 +43,9 @@ int APS5_VABI sceUserServiceGetAccessibilityVibration(int user_id, int32_t* vibr return ReportAccessibilitySetting(user_id, vibration, 1); } -int APS5_VABI sceUserServiceGetAccessibilityZoom(int user_id, int32_t* zoom) { - return ReportAccessibilitySetting(user_id, zoom, 0); +int APS5_VABI sceUserServiceGetAccessibilityZoom() { + NotImplemented_nid_no_patch(__func__); + return 0; } int APS5_VABI sceUserServiceGetAccessibilityZoomEnabled(int user_id, int32_t* zoom_enabled) { diff --git a/core/libs/tests/GuestUserService.cpp b/core/libs/tests/GuestUserService.cpp index 7686a67219..4e6e1809a4 100644 --- a/core/libs/tests/GuestUserService.cpp +++ b/core/libs/tests/GuestUserService.cpp @@ -2,6 +2,7 @@ #include "prx/libSceUserService/UserService.hpp" #include #include +#include extern "C" { int APS5_VABI sceUserServiceGetForegroundUser(int* user_id); @@ -10,11 +11,11 @@ int APS5_VABI sceUserServiceGetUserColor(int user_id, int* color); int APS5_VABI sceUserServiceGetNpAccountId(int user_id, std::uint64_t* account_id); int APS5_VABI sceUserServiceGetAgeLevel(int user_id, std::uint32_t* age_level); int APS5_VABI sceUserServiceGetAccessibilityChatTranscription(int user_id, std::int32_t* value); -int APS5_VABI sceUserServiceGetAccessibilityKeyremapEnable(int user_id, std::int32_t* value); +int APS5_VABI sceUserServiceGetAccessibilityKeyremapEnable(); int APS5_VABI sceUserServiceGetAccessibilityPressAndHoldDelay(int user_id, std::int32_t* value); int APS5_VABI sceUserServiceGetAccessibilityTriggerEffect(int user_id, std::int32_t* value); int APS5_VABI sceUserServiceGetAccessibilityVibration(int user_id, std::int32_t* value); -int APS5_VABI sceUserServiceGetAccessibilityZoom(int user_id, std::int32_t* value); +int APS5_VABI sceUserServiceGetAccessibilityZoom(); int APS5_VABI sceUserServiceGetAccessibilityZoomEnabled(int user_id, std::int32_t* value); int APS5_VABI sceUserServiceGetAccessibilityZoomFollowFocus(int user_id, std::int32_t* value); } @@ -68,11 +69,9 @@ int main() { }; const Setting settings[] = { {sceUserServiceGetAccessibilityChatTranscription, 0}, - {sceUserServiceGetAccessibilityKeyremapEnable, 0}, {sceUserServiceGetAccessibilityPressAndHoldDelay, 0}, {sceUserServiceGetAccessibilityTriggerEffect, 1}, {sceUserServiceGetAccessibilityVibration, 1}, - {sceUserServiceGetAccessibilityZoom, 0}, {sceUserServiceGetAccessibilityZoomEnabled, 0}, {sceUserServiceGetAccessibilityZoomFollowFocus, 1}, }; @@ -87,4 +86,19 @@ int main() { Require(setting.get(USER_SERVICE_INITIAL_USER_ID, &value) == USER_SERVICE_OK); Require(value == setting.value); } + bool threwKeyremap = false; + try { + sceUserServiceGetAccessibilityKeyremapEnable(); + } catch (const std::runtime_error&) { + threwKeyremap = true; + } + Require(threwKeyremap); + + bool threwZoom = false; + try { + sceUserServiceGetAccessibilityZoom(); + } catch (const std::runtime_error&) { + threwZoom = true; + } + Require(threwZoom); } diff --git a/docs/dev/debt/libSceUserService.md b/docs/dev/debt/libSceUserService.md index 5e32b84b70..f159b8f893 100644 --- a/docs/dev/debt/libSceUserService.md +++ b/docs/dev/debt/libSceUserService.md @@ -7,4 +7,5 @@ - [sceUserServiceGetUserColor](../../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - the default profile colour is assumed to be blue (0) - [sceUserServiceGetUserName](../../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - `0x80960105`, which a PS5 (firmware 13.60) returns for a user id it does not know, has no known name; it is `USER_SERVICE_ERROR_UNKNOWN_USER` here. On the console a registered user who is not logged in still gets their name; here only the initial user exists - [sceUserServiceGetAgeLevel](../../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - on a PS5 (firmware 13.60) the value is the age set in the user's parental restriction (0, 7, 12 and 15 were measured) and `0xFFFFFFFF` without one; a user who is not logged in returns `0x80960009` with or without an output. Parental restrictions are not modelled, so the initial user is always reported as unrestricted. PPSA12544 (Unity) only stores the value for its user profile -- [sceUserServiceGetAccessibilityChatTranscription, KeyremapEnable, PressAndHoldDelay, TriggerEffect, Vibration, Zoom, ZoomEnabled, ZoomFollowFocus](../../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - report 0, 0, 0, 1, 1, 0, 0 and 1, what a PS5 (firmware 13.60) reports for a user whose accessibility settings are all at their defaults; the console reports the user's own settings (a changed vibration intensity read 3), which are not modelled. What each number means is unknown. A user who is not logged in returns `0x80960009` with or without an output +- [sceUserServiceGetAccessibilityChatTranscription, PressAndHoldDelay, TriggerEffect, Vibration, ZoomEnabled, ZoomFollowFocus](../../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - report 0, 0, 1, 1, 0 and 1, what a PS5 (firmware 13.60) reports for a user whose accessibility settings are all at their defaults; the console reports the user's own settings (a changed vibration intensity read 3), which are not modelled. What each number means is unknown. A user who is not logged in returns `0x80960009` with or without an output +- [sceUserServiceGetAccessibilityKeyremapEnable, sceUserServiceGetAccessibilityZoom](../../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - declared without parameters as throwing stubs calling `NotImplemented_nid_no_patch`. Their signatures and values remain unverified on hardware From ec780d902fd5b9439975a213f4360b18dd0fb8b7 Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Sat, 17 Oct 2026 03:22:06 +0000 Subject: [PATCH 0634/1000] test(shader): generate pixel lane proof regression --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 6 +-- .../libSceAgcDriver/tests/ShaderMemory.cpp | 53 ++++++++++++++----- .../tests/data/nhl26-wave64-pixel-lane63.req | 1 - 3 files changed, 42 insertions(+), 18 deletions(-) delete mode 100644 core/libs/prx/libSceAgcDriver/tests/data/nhl26-wave64-pixel-lane63.req diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 99f035d482..a41c2a9968 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1498,10 +1498,10 @@ if(BUILD_TESTING) add_test(NAME agc_shader_layout_pair_off COMMAND agc_shader_memory_tests --layout-pair) set_tests_properties(agc_shader_layout_pair PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") set_tests_properties(agc_shader_layout_pair_off PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_NO_PERF_LAYOUT_PAIR=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") - add_test(NAME agc_shader_entry_lane_pixel_replay COMMAND agc_shader_memory_tests --entry-lane-5398) - set_tests_properties(agc_shader_entry_lane_pixel_replay PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + add_test(NAME agc_shader_entry_lane_proof_budget COMMAND agc_shader_memory_tests --entry-lane-proof-budget) + set_tests_properties(agc_shader_entry_lane_proof_budget PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") if(WIN32) - set_tests_properties(agc_shader_bindless agc_shader_srt_read_patching agc_shader_type_zero_image_materialization agc_shader_frontend_pair agc_shader_frontend_pair_off agc_shader_layout_pair agc_shader_layout_pair_off agc_shader_entry_lane_pixel_replay PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + set_tests_properties(agc_shader_bindless agc_shader_srt_read_patching agc_shader_type_zero_image_materialization agc_shader_frontend_pair agc_shader_frontend_pair_off agc_shader_layout_pair agc_shader_layout_pair_off agc_shader_entry_lane_proof_budget PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() foreach(agcTest IN ITEMS agc_vertex_input_tests agc_shared_write_tests agc_driver_tests agc_shader_memory_tests agc_structurizer_tests agc_spirv_module_tests agc_dpp_write_tests agc_masked_select_tests agc_ir_reclaim_tests agc_vector_window_tests agc_constant_bit_test_tests agc_integer_narrowing_tests agc_scalar_select_mask_tests agc_vop1_control_decode_tests agc_validate_program_tests agc_pass_hazards_tests agc_shader_disk_cache_tests agc_shader_device_profile_tests agc_placeholder_images_tests agc_shader_control_flow_tests agc_driver_flip_tests agc_driver_pm4_tests agc_driver_graphics_tests agc_driver_bda_device_tests agc_driver_mesh_tests agc_driver_cmask_fast_clear_tests agc_formatted_buffer_once_tests agc_formatted_buffer_byte_stores_tests ${agcExecutionTests} agc_fma_mix_once_tests) if(agcTest STREQUAL "agc_driver_constant_store_tests" OR agcTest STREQUAL "agc_driver_flat_apertures_tests" OR agcTest STREQUAL "agc_driver_pipeline_optimized_readback_tests") diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 81ace209fe..a3fe67ddd4 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -28,13 +28,11 @@ #include #include #include -#include #include #include #include #include #include -#include #include #include #include @@ -2420,16 +2418,43 @@ void verifyHalfWaveReduction() { expectFailure([&] { static_cast(pixel(umin, 8u, true)); }, "v_permlanex16_b32 from lanes 16-31 is outside the 8-lane host subgroup", "half-wave reduction: the scan built on an 8-lane subgroup"); } -void verifyNhl26Wave64PixelLane63Replay() { +void verifyEntryLaneProofBudget() { using namespace ShaderRecompiler; - std::ifstream file("core/libs/prx/libSceAgcDriver/tests/data/nhl26-wave64-pixel-lane63.req", std::ios::binary); - require(file.good(), "NHL 26 wave64 pixel request fixture could not be opened"); - std::ostringstream serialized; - serialized << file.rdbuf(); - const auto request = RequestSerializer{}.Deserialize(serialized.str()); - require(request.request.shader.stage == ShaderStage::Fragment && request.request.context.waveSize == 64u && request.request.target.subgroupSize == 32u, "NHL 26 lane-63 fixture no longer describes a wave64 pixel shader on a 32-lane host"); - const auto handle = PrepareShader(request.request); - require(!GetPreparedArtifact(*handle).spirv.Words().empty(), "NHL 26 wave64 pixel shader did not produce SPIR-V"); + static constexpr std::array capabilities{spv::CapabilityGroupNonUniform, spv::CapabilityGroupNonUniformBallot, spv::CapabilityGroupNonUniformShuffle, spv::CapabilityGroupNonUniformArithmetic, spv::CapabilityClipDistance}; + std::vector code{0xbe98047eu, 0x7e160f00u, 0x8786187eu, 0xbeea287eu, 0xd501000du, 0x001a16c1u}; + for (const auto control : {0xff01110du, 0xff01120du, 0xff01140du, 0xff01180du}) code.insert(code.end(), {0x261a1afau, control}); + code.insert(code.end(), {0xd778100cu, 0x0305830du, 0x261a190du, 0xbefe046au, 0xd7600006u, 0x00013f0du, 0xd7600007u, 0x00017f0du, 0x83880706u, 0x7e020208u, 0xf800180fu, 0x01010101u, 0xbf810000u}); + std::vector savedMaskLoops; + for (std::uint32_t loop = 0; loop < 3u; ++loop) { + std::vector savedMaskLoop{0xbea2047eu, 0xbea40380u}; + savedMaskLoop.insert(savedMaskLoop.end(), {0xbefe0422u, 0x7c080080u, 0x8aa26a7eu}); + const auto branch = 0xbf850000u | static_cast(2 - static_cast(savedMaskLoop.size() + 3u)); + savedMaskLoop.insert(savedMaskLoop.end(), {0x80248124u, 0xbf0a8424u, branch, 0xbefe0422u}); + savedMaskLoops.insert(savedMaskLoops.end(), savedMaskLoop.begin(), savedMaskLoop.end()); + } + code.insert(code.begin(), savedMaskLoops.begin(), savedMaskLoops.end()); + + ShaderPixelStageInfo pixel{}; + pixel.inputAddr = PixelInputBit(PixelInput::PositionX); + pixel.posX = true; + pixel.targetOutputMode[0] = 9u; + pixel.targetExportMapping.fill(0xe4u); + RecompileRequest request{}; + request.shader = {ShaderStage::Fragment, 0x30000u, code, 0, {}}; + request.context.waveSize = 64u; + request.context.pixel = pixel; + request.target.vulkanVersion = 0x00401000u; + request.target.spirvVersion = 0x00010300u; + request.target.subgroupSize = 32u; + request.target.supportedCapabilities = capabilities; + request.layout.pushConstantSizeBytes = 128u; + request.useCache = false; + const auto spirv = Recompile(request).spirv; + std::uint32_t reductions = 0; + for (std::size_t cursor = 5; cursor < spirv.size() && (spirv[cursor] >> 16u) != 0u; cursor += spirv[cursor] >> 16u) { + if ((spirv[cursor] & 0xffffu) == static_cast(spv::OpGroupNonUniformUMin) && (spirv[cursor] >> 16u) == 6u && spirv[cursor + 4u] == spv::GroupOperationReduce) ++reductions; + } + require(reductions == 1u, "a wave64 pixel UMin scan after loop-carried saved EXEC masks did not use a 32-lane subgroup reduction"); } void verifyNarrowVertexWave() { @@ -5144,9 +5169,9 @@ int main(int argc, char** argv) { std::cout << "Both push-constant layouts of a pixel shader compile from one program unchanged\n"; return 0; } - if (argc == 2 && std::string_view(argv[1]) == "--entry-lane-5398") { - verifyNhl26Wave64PixelLane63Replay(); - std::cout << "The NHL 26 wave64 pixel lane-63 shader compiles for a 32-lane host\n"; + if (argc == 2 && std::string_view(argv[1]) == "--entry-lane-proof-budget") { + verifyEntryLaneProofBudget(); + std::cout << "The wave64 pixel entry-lane proof handles loop-carried saved EXEC masks on a 32-lane host\n"; return 0; } if (argc == 2 && std::string_view(argv[1]) == "--bindless") { diff --git a/core/libs/prx/libSceAgcDriver/tests/data/nhl26-wave64-pixel-lane63.req b/core/libs/prx/libSceAgcDriver/tests/data/nhl26-wave64-pixel-lane63.req deleted file mode 100644 index f0ed10c761..0000000000 --- a/core/libs/prx/libSceAgcDriver/tests/data/nhl26-wave64-pixel-lane63.req +++ /dev/null @@ -1 +0,0 @@ -NVNQQRcAAAAFABCucxEAAADkAgAAAAAAAAMAoL8CEQh+AxEKfgABDPQgAAD6AAMM9AAAAPoDDwx+AAYI9KAAAPp/wIy/CAMA8AQAAQAIAQDwBAQDAAwHKPRAAAD6DAUk9FAAAPoMATD0AAAA+v8HJrB/wIy/HwwKShQEBBAVBgYQjBog9EwBAPoCA/y+gQoKNn/AjL+Aage/AwBB1fQGzgN+gKSFAgCiyAUNCn4CAabIgAIQfvICVH7BAlZ+8QoKBoACWH6AAk5+gAJKfoACRn4fCgQ+gAJMfoACSH6AAkR+gAJAfgIAQdX0BM4DgAJCfoACPn6AAjx+gAI6foACOH6AAjZ+gAI0foACMn6AAjB+gAIufoACLH6AAip+gAIofoACJn6AAiR+gAIifoACIH6AAh5+gAIcfoACGn6AAhh+gAIWfoACFH6AAhJ+fgSCvgwKKPRwAQD6fgSgvn4Eor5xP4y/AQBv1wEhAQT/AAw2/w8AAHA/jL8tAEHVBCVMADAAQdUEDRwAMQBB1QQVLAAHAEjVARkxAgYNAn75DIJ9JoaGBi0AH9URBgJABgBB1QQdPAAHDQh+AQBB1f8CzgMBCAA6+Q6CfSaKhgYQBFo+MAAf1QUGAkAFAEHV/wjOAwEIADr5AlwG8waGJjEAH9UJBgJALVUIfgYAH9UNBgJA+QpeBvIGhjb5XAJ8BQA2BgMAAdUuXSpA+VwABgUGNhYEBGA+LgAB1S9fGiAIBGI+BQcGAgwEDD4wAEHVBGFyAAFdAgIxAEHVBGN2ADIAQdUEDXoAAQMEEAMHBD4AAQQ+Al0IfgQDAhAEBwQQBAEKEAMHQdUEBwYE+QoIfICEhgYDAx/VAAkCAANVCH4EAwYQBAUAEAIDQdUEBc4DAQNB1QQDzgOABgZ8+QAGfICGhgb5BAQCAgYGFgEAAdUBAxpAAgAB1QMFEgAAAAHVAAMSAAGAS9XwBMIDAIBL1fAAwgMBAEHV/wLCAwDwf0UAAEHV/wDCAwDwf0UBDwJ+AA8Efv8CADb/DwAAAgBv1wIZBQQADQJ+AgBv14AwCQT5AIJ9JoSGBgEAQdX/As4DAQgAOgIASNUCGTEC+QIABvMGhiYCDQZ+/wSCff8HAAACAEHV/wbOAwEIADr5AAYCAAYGFvkECAbyBoY2+QACfAIANgb5AAAGAgY2FgQAAdUECRIgAgcEAgEJAgIBAwYQAgUGPgABBj4DXQZ+AwNeEAMFWhADAVwQMwAB1YBekgA0AAHVgFqSADUAAdWAXJIAEwGIvyxRjH1+av6KImqiig8BiL8sUwBKAAEI9IAAAPp/wIy/ACAw4AAAAYB+KOq+cD+Mv8EABAL6BAQmAhEB//oEBCYCEgH/+gQEJgIUAf/6BAQmAhgB/wEQeNcCgwUDAgMEJmoE/r4EAGDXAj8BAAUAYNcCfwEABAWcgxwAjH35BG0ChAaGhoJspH3rAIi/+RAGfPKIhgYc/2qTcAAAAAABCPRwAAD6f8CMv8IFIPRMAADUf8CMvxdWgn0IauqHImqiin5q/orcAIi/hAJsfoFYWEoc/x2TcAAAAAADCPRwAAD6f8CMvwYEKPRQAAA6BgEs9AAAADoGBij0IAAAOoYaJPRgAAA6f8CMvwQAQdUTasoEAQBB1RNoxgQAAEHVE2bCBBACBH4FAEHVBggeAAYAQdUKCC4ABwBB1RoIbgADAEHVBQIWBAkCDD4ZAg4+BAAGPgQAQdUIABoEBgBB1RgAHgT5Bgh8gISGBgAAV9XwBsYD+QgIfICGhgb5DAh8gIiGBgEAV9XwDMYDBwAB1WoEEgADAQoIAgBX1fAIxgOGGiD0aAAAOgYDBgj/DgwgbxKDOgQFCAgGVQx+BgsKEBECDH4GAAHVawwaAP8MDCBvEoM6BlUMfgYJCBAFCwwQEgIKfgQJDD5/wIy/BQAB1WoKIgD/CgogbxKDOgVVCn4FBwYQAwcMPvIMKHyLAIi/8hAICAADCPRwAAD6cAAcuPAEBAZ/wIy/BgYk9EAAADiACAggF1aCffACAgbwAAAGBmcKfhcCVn6GBiT0MAAAOAAECPSQAAD6AAEM9EAAAPoqCVQCf8CMvxkMCH6GGiD0bAAAOMYaIPQ4AAA4f8CMv2pUbhDyCAYGhhog9EgAADg3AB/VNwsCQAMFBBAIbxAGBAUGHhsMBH7yBgYgAgcGBn/AjL9qDAR+AwBL1fAGxgPyBAgG/wYGBgAAgD4EAwIQOQBB1SkGwgMCAwQeawwCfikGcBDyBAQgAQUEBhgMAn4CAEvV8ATGA/ICBgb/BAQGAACAPgMBABA6AEHVKgTCAwEBAh4aDAB+KgR2EPICAiAAAwAGAABL1fAAxgP/AAAGAACAPigAeBA9AEHVKADCAxIHnPA8AIEAOToAABIPnPA8BIEAOTsAAHE/jL83ARY+NwUSPjcDFD5wP4y/NwlKPjcPOj43DUI+NwtIPhIPnPA9AIEAOToAABIPnPA8BIEAODsAAHE/jL83ATA+NwcYPjcFID43Ayg+cD+MvzcJTj43Dzw+Nw1APjcLTD4SD5zwPQCBADg6AAASD5zwPASBADg6AABxP4y/NwEyPjcHGj43BSI+NwMqPhIPnPA9AIEAOTsAAHE/jL83CUY+Nw84PjcNPj43C0Q+cD+MvzcBND43Bxw+NwUkPjcDLD4SD5zwPQCBADg7AABwP4y/NwE2PjcHHj43BSY+NwMuPiIE/r6EbKR9AgCIv4ACbH7s/oK/IAT+viAE6r7/EAAgrMUnN/IQUB4AVQB+AFFSECdTThAlU0oQI1NGECZTChAkUwYQIlMCECBTRBAhU0AQH1M+EB5TDBAdUwgQHFMEEBtTNhAaUzQQGVMyEBhTABAXUy4QFlMsEBVTKhAUUw4QE1MmEBJTJBARUyIQEFMgEA9THhAOUxwQDVMaEAxTGBALUxYQClMUEAlTEhDyECh8KgCIv/JQEAgACQj0YAAA+n/AjL8SATD0AAAA+hIFMPRAAAD6Egoo9IAAAPp/wIy/BBBOPgUQSj4GEEY+CBAKPgkQBj4KEAI+DBBEPg0QQD4OED4+EBAMPhEQCD4SEAQ+FBA2PhUQND4WEDI+FxAAPhgQLj4ZECw+GhAqPhsQDj4cECY+HRAkPh4QIj4fECA+IBAePiEQHD4iEBo+IxAYPigQFj4pEBQ+KhASPmoE/r4AAQj0oAAA+i9fOhAnNzwIJTVCCCMzOAgPXxAQHQAf1S5dAkD0NjZWDl8cEPQ0NFYNXxoQ9DIyVh0XPD4dFUI+HRM4Pi03RD4tNUA+LhE8Pi4dQj4uGzg+LTM+PhdfRD4vCzw+LwdCPhZfQD4vAzg+FV8+Pi4NPD4TXUQ+LglCPhJdQD4uBTg+EV0+Pn/AjL/CGiD0WAAA+oIaIPRgAAD6Ils8Pn/AjL/51AQQawaGhiBbQj4fWzg+AA8AXhAZCF4CPQoQAkMGEAI5AhAFBwReAQAv1QEBAQACBP6+DxQA+AIBAAAfDAD4AAQAAAAAgb9zbDAwDgEAACYBAAAAAAAAZQ8gIRAiICRAAABXJRAAAF0pEAAAYSkQAACmAQQQQEBCIEsQTBBNEE4Qb2B5EHoQhRAAAG4AAGQPICEQJEAAAGQPICEQIiAkQAAAWQAARCRAAABBglEAASAAPYCgQ8BBAGoRYEAARaXnYOFB0klYOt1Co8l4Mg+RLmLKTFaXlxlbZ/PwCw0dRQCUAiaWAAABXvmBgAAloAEFW6UBJakBASCsAQggsAEHUAKgAQClARC4AQGQAQCpARC/AQCAARKsARDIAQBwEKwBENEBAGAGrAEQ2QEAQBGwARDhAQAgBbQBEOkBAAAAtAEbtAHxAbABRxsAoAEb+QGAAghEhQIqiwIBhQIBDvhV4gMBYz2Qi6BAIIiEnncAAAAAAAAAAAAAYmFyZWZvb3T700E4AAAAAAEAAAAwCgAAAAAAACYBAAABoyTtCILnDgAAAAAAAAAAABCmUBEAAACYAQAAAAAAADEyMzQYAAAAEBGmUBEAAAAAEK5zEQAAAMgQplARAAAAmBCmUBEAAABgEKZQEQAAAJAQplARAAAAAAAAAAAAAACYAQAAkAsAAAAAAAAOAAAAAQAAAAAAAAAwAAEJBgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA8AQAAAAAAACAAAABCucxEJAAAAAAAAAAYAAABgCwAhBgAAAPvTQTgKAAAATwEsAgsAAAAEAAAAxAEAAAAAAADFAQAARAAAALMBAAACAwAAtAEAAAIDAAC2AQAAAQAAALgBAAAAAAABAwIAABAAAACPAAAA/wAAABADAAAAAAAASBGmUBEAAABgEaZQEQAAAIgRplARAAAAiBGmUBEAAACQEaZQEQAAACwAAAALABMAAAABAAEAAAD/////////////AAD/////////////AAAgAP//////////KAA4gP///////////////////////zyAMABAgAAARIAAAAAAAABIgAAAAAAAAEAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAABAQAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIDAAABAAEBAAAAAAAAAAAAAAEAAAAEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAACAAAAAAAAAAAQrnMRAAAAkAsAAAAAAAADAKC/AhEIfgMRCn4AAQz0IAAA+gADDPQAAAD6Aw8MfgAGCPSgAAD6f8CMvwgDAPAEAAEACAEA8AQEAwAMByj0QAAA+gwFJPRQAAD6DAEw9AAAAPr/Byawf8CMvx8MCkoUBAQQFQYGEIwaIPRMAQD6AgP8voEKCjZ/wIy/gGoHvwMAQdX0Bs4DfoCkhQIAosgFDQp+AgGmyIACEH7yAlR+wQJWfvEKCgaAAlh+gAJOfoACSn6AAkZ+HwoEPoACTH6AAkh+gAJEfoACQH4CAEHV9ATOA4ACQn6AAj5+gAI8foACOn6AAjh+gAI2foACNH6AAjJ+gAIwfoACLn6AAix+gAIqfoACKH6AAiZ+gAIkfoACIn6AAiB+gAIefoACHH6AAhp+gAIYfoACFn6AAhR+gAISfn4Egr4MCij0cAEA+n4EoL5+BKK+cT+MvwEAb9cBIQEE/wAMNv8PAABwP4y/LQBB1QQlTAAwAEHVBA0cADEAQdUEFSwABwBI1QEZMQIGDQJ++QyCfSaGhgYtAB/VEQYCQAYAQdUEHTwABw0IfgEAQdX/As4DAQgAOvkOgn0mioYGEARaPjAAH9UFBgJABQBB1f8IzgMBCAA6+QJcBvMGhiYxAB/VCQYCQC1VCH4GAB/VDQYCQPkKXgbyBoY2+VwCfAUANgYDAAHVLl0qQPlcAAYFBjYWBARgPi4AAdUvXxogCARiPgUHBgIMBAw+MABB1QRhcgABXQICMQBB1QRjdgAyAEHVBA16AAEDBBADBwQ+AAEEPgJdCH4EAwIQBAcEEAQBChADB0HVBAcGBPkKCHyAhIYGAwMf1QAJAgADVQh+BAMGEAQFABACA0HVBAXOAwEDQdUEA84DgAYGfPkABnyAhoYG+QQEAgIGBhYBAAHVAQMaQAIAAdUDBRIAAAAB1QADEgABgEvV8ATCAwCAS9XwAMIDAQBB1f8CwgMA8H9FAABB1f8AwgMA8H9FAQ8CfgAPBH7/AgA2/w8AAAIAb9cCGQUEAA0CfgIAb9eAMAkE+QCCfSaEhgYBAEHV/wLOAwEIADoCAEjVAhkxAvkCAAbzBoYmAg0Gfv8Egn3/BwAAAgBB1f8GzgMBCAA6+QAGAgAGBhb5BAgG8gaGNvkAAnwCADYG+QAABgIGNhYEAAHVBAkSIAIHBAIBCQICAQMGEAIFBj4AAQY+A10GfgMDXhADBVoQAwFcEDMAAdWAXpIANAAB1YBakgA1AAHVgFySABMBiL8sUYx9fmr+iiJqoooPAYi/LFMASgABCPSAAAD6f8CMvwAgMOAAAAGAfijqvnA/jL/BAAQC+gQEJgIRAf/6BAQmAhIB//oEBCYCFAH/+gQEJgIYAf8BEHjXAoMFAwIDBCZqBP6+BABg1wI/AQAFAGDXAn8BAAQFnIMcAIx9+QRtAoQGhoaCbKR96wCIv/kQBnzyiIYGHP9qk3AAAAAAAQj0cAAA+n/AjL/CBSD0TAAA1H/AjL8XVoJ9CGrqhyJqoop+av6K3ACIv4QCbH6BWFhKHP8dk3AAAAAAAwj0cAAA+n/AjL8GBCj0UAAAOgYBLPQAAAA6BgYo9CAAADqGGiT0YAAAOn/AjL8EAEHVE2rKBAEAQdUTaMYEAABB1RNmwgQQAgR+BQBB1QYIHgAGAEHVCgguAAcAQdUaCG4AAwBB1QUCFgQJAgw+GQIOPgQABj4EAEHVCAAaBAYAQdUYAB4E+QYIfICEhgYAAFfV8AbGA/kICHyAhoYG+QwIfICIhgYBAFfV8AzGAwcAAdVqBBIAAwEKCAIAV9XwCMYDhhog9GgAADoGAwYI/w4MIG8SgzoEBQgIBlUMfgYLChARAgx+BgAB1WsMGgD/DAwgbxKDOgZVDH4GCQgQBQsMEBICCn4ECQw+f8CMvwUAAdVqCiIA/woKIG8SgzoFVQp+BQcGEAMHDD7yDCh8iwCIv/IQCAgAAwj0cAAA+nAAHLjwBAQGf8CMvwYGJPRAAAA4gAgIIBdWgn3wAgIG8AAABgZnCn4XAlZ+hgYk9DAAADgABAj0kAAA+gABDPRAAAD6KglUAn/AjL8ZDAh+hhog9GwAADjGGiD0OAAAOH/AjL9qVG4Q8ggGBoYaIPRIAAA4NwAf1TcLAkADBQQQCG8QBgQFBh4bDAR+8gYGIAIHBgZ/wIy/agwEfgMAS9XwBsYD8gQIBv8GBgYAAIA+BAMCEDkAQdUpBsIDAgMEHmsMAn4pBnAQ8gQEIAEFBAYYDAJ+AgBL1fAExgPyAgYG/wQEBgAAgD4DAQAQOgBB1SoEwgMBAQIeGgwAfioEdhDyAgIgAAMABgAAS9XwAMYD/wAABgAAgD4oAHgQPQBB1SgAwgMSB5zwPACBADk6AAASD5zwPASBADk7AABxP4y/NwEWPjcFEj43AxQ+cD+MvzcJSj43Dzo+Nw1CPjcLSD4SD5zwPQCBADk6AAASD5zwPASBADg7AABxP4y/NwEwPjcHGD43BSA+NwMoPnA/jL83CU4+Nw88PjcNQD43C0w+Eg+c8D0AgQA4OgAAEg+c8DwEgQA4OgAAcT+MvzcBMj43Bxo+NwUiPjcDKj4SD5zwPQCBADk7AABxP4y/NwlGPjcPOD43DT4+NwtEPnA/jL83ATQ+NwccPjcFJD43Ayw+Eg+c8D0AgQA4OwAAcD+MvzcBNj43Bx4+NwUmPjcDLj4iBP6+hGykfQIAiL+AAmx+7P6CvyAE/r4gBOq+/xAAIKzFJzfyEFAeAFUAfgBRUhAnU04QJVNKECNTRhAmUwoQJFMGECJTAhAgU0QQIVNAEB9TPhAeUwwQHVMIEBxTBBAbUzYQGlM0EBlTMhAYUwAQF1MuEBZTLBAVUyoQFFMOEBNTJhASUyQQEVMiEBBTIBAPUx4QDlMcEA1TGhAMUxgQC1MWEApTFBAJUxIQ8hAofCoAiL/yUBAIAAkI9GAAAPp/wIy/EgEw9AAAAPoSBTD0QAAA+hIKKPSAAAD6f8CMvwQQTj4FEEo+BhBGPggQCj4JEAY+ChACPgwQRD4NEEA+DhA+PhAQDD4REAg+EhAEPhQQNj4VEDQ+FhAyPhcQAD4YEC4+GRAsPhoQKj4bEA4+HBAmPh0QJD4eECI+HxAgPiAQHj4hEBw+IhAaPiMQGD4oEBY+KRAUPioQEj5qBP6+AAEI9KAAAPovXzoQJzc8CCU1QggjMzgID18QEB0AH9UuXQJA9DY2Vg5fHBD0NDRWDV8aEPQyMlYdFzw+HRVCPh0TOD4tN0Q+LTVAPi4RPD4uHUI+Lhs4Pi0zPj4XX0Q+Lws8Pi8HQj4WX0A+LwM4PhVfPj4uDTw+E11EPi4JQj4SXUA+LgU4PhFdPj5/wIy/whog9FgAAPqCGiD0YAAA+iJbPD5/wIy/+dQEEGsGhoYgW0I+H1s4PgAPAF4QGQheAj0KEAJDBhACOQIQBQcEXgEAL9UBAQEAAgT+vg8UAPgCAQAAHwwA+AAEAAAAAIG/c2wwMA4BAAAmAQAAAAAAAGUPICEQIiAkQAAAVyUQAABdKRAAAGEpEAAApgEEEEBAQiBLEEwQTRBOEG9geRB6EIUQAABuAABkDyAhECRAAABkDyAhECIgJEAAAFkAAEQkQAAAQYJRAAEgAD2AoEPAQQBqEWBAAEWl52DhQdJJWDrdQqPJeDIPkS5iykxWl5cZW2fz8AsNHUUAlAImlgAAAV75gYAAJaABBVulASWpAQEgrAEIILABB1ACoAEApQEQuAEBkAEAqQEQvwEAgAESrAEQyAEAcBCsARDRAQBgBqwBENkBAEARsAEQ4QEAIAW0ARDpAQAAALQBG7QB8QGwAUcbAKABG/kBgAIIRIUCKosCAYUCAQ74VeIDAWM9kIugQCCIhJ53AAAAAAAAAAAAAGJhcmVmb290+9NBOAAAAAABAAAAMAoAAAAAAAAmAQAAAaMk7QiC5w4AAAAAAAAAAAAQplARAAAAmAEAAAAAAAAxMjM0GAAAABARplARAAAAABCucxEAAADIEKZQEQAAAJgQplARAAAAYBCmUBEAAACQEKZQEQAAAAAAAAAAAAAAmAEAAJALAAAAAAAADgAAAAEAAAAAAAAAMAABCQYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPAEAAAAAAAAgAAAAQrnMRCQAAAAAAAAAGAAAAYAsAIQYAAAD700E4CgAAAE8BLAILAAAABAAAAMQBAAAAAAAAxQEAAEQAAACzAQAAAgMAALQBAAACAwAAtgEAAAEAAAC4AQAAAAAAAQMCAAAQAAAAjwAAAP8AAAAQAwAAAAAAAEgRplARAAAAYBGmUBEAAACIEaZQEQAAAIgRplARAAAAkBGmUBEAAAAsAAAACwATAAAAAQABAAAA/////////////wAA/////////////wAAIAD//////////ygAOID///////////////////////88gDAAQIAAAESAAAAAAAAASIAAAAAAAAABwAAAAAEAAAAQQAAABAEAIAAAAAEAAAAjAAAAAAAAAAEAAAACAAAAAwAAAAkAAAAKAAAACwAAAAwAAAAZAAAAGwAAAB0AAAAfAAAAIAAAACEAAAAqAAAAMAAAADcAAAA4AAAAPQAAAD8AAABAAAAAQQAAAGARAABwEQAAchEAAHMRAACYEwAAvxMAAIYUAACjFAAApBQAALUUAAC7FAAAvRQAAOMUAAACFQAACgAAAAAAAAAbAAAAAAAAAFNQVl9FWFRfZGVzY3JpcHRvcl9pbmRleGluZyEAAAAAAAAAU1BWX0VYVF9mcmFnbWVudF9zaGFkZXJfaW50ZXJsb2NrEwAAAAAAAABTUFZfRVhUX21lc2hfc2hhZGVyGgAAAAAAAABTUFZfRVhUX3NoYWRlcl9pbWFnZV9pbnQ2NCMAAAAAAAAAU1BWX0VYVF9zaGFkZXJfdmlld3BvcnRfaW5kZXhfbGF5ZXIUAAAAAAAAAFNQVl9LSFJfOGJpdF9zdG9yYWdlFgAAAAAAAABTUFZfS0hSX2Zsb2F0X2NvbnRyb2xzIwAAAAAAAABTUFZfS0hSX2ZyYWdtZW50X3NoYWRlcl9iYXJ5Y2VudHJpYx8AAAAAAAAAU1BWX0tIUl9waHlzaWNhbF9zdG9yYWdlX2J1ZmZlchQAAAAAAAAAU1BWX0tIUl9zaGFkZXJfY2xvY2sBAAQAAAAEAABAAAAAAAQAAADAAAABgAAAAIAAAACAAAAAgAAAAABwAAAAAQAAAAEAAIAAAAAAgAAAIAAAACAAAAABIAAAAIAAAACAAAAAeAAAAHgQAACAAAAAgAAAAAAAAAAAAAAAgAAAAIAAAAAAAQECAAAAAAABQA0DAAEB \ No newline at end of file From 97a84e3261a330c480e349fa2ecd9eefb0583c16 Mon Sep 17 00:00:00 2001 From: Tamalero <11452991+Tamalero@users.noreply.github.com> Date: Thu, 15 Oct 2026 21:56:38 -0700 Subject: [PATCH 0635/1000] fix(recompiler): hold 32 samplers in the runtime sampler heap A pixel shader of PPSA16833 binds more than 16 samplers outside the sampler tables. Each takes two heap entries, one per descriptor variant, so the 32-entry heap refused it with "shader sampler pairs exceed runtime heap capacity" and the title aborted when a new game started. The heap now holds 64 entries (32 samplers). Its metadata array is part of ShaderData, which grows from 14528 to 16064 bytes, still within the 16 KiB uniform buffer every Vulkan device supports; the runtime ABI version moves to 13 so shaders cached with the old layout are rebuilt. The overflow message names the sampler count. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAgcDriver/tests/ShaderDeviceProfile.cpp | 4 ++++ core/shader/recompiler/Optimization/src/BindingAllocator.cpp | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderDeviceProfile.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderDeviceProfile.cpp index f2a5903c50..20112c62f3 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderDeviceProfile.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderDeviceProfile.cpp @@ -169,6 +169,10 @@ void CheckHeaps() { const auto sixteenSamplers = allocate(image, 1u, 16u); Require(BindingAllocator{}.FindBinding(sixteenSamplers.layout, DescriptorBindingKind::Samplers).resources.size() == 32u, "sixteen logical samplers must fit with both descriptor variants"); Require(BindingAllocator{}.FindBinding(allocate(image, 1u, 17u).layout, DescriptorBindingKind::Samplers).resources.size() == 34u, "seventeen logical samplers must fit with both descriptor variants"); + const auto twentySamplers = allocate(image, 1u, 20u); + Require(BindingAllocator{}.FindBinding(twentySamplers.layout, DescriptorBindingKind::Samplers).resources.size() == 40u, "twenty logical samplers must fit with both descriptor variants"); + const auto thirtyTwoSamplers = allocate(image, 1u, 32u); + Require(BindingAllocator{}.FindBinding(thirtyTwoSamplers.layout, DescriptorBindingKind::Samplers).resources.size() == 64u, "thirty-two logical samplers must fit with both descriptor variants"); const auto full = allocate(image, RuntimeAbi::SampledHeapCapacity, RuntimeAbi::SamplerHeapCapacity / 2u); Require(single.layout.ShaderDataDwords() == full.layout.ShaderDataDwords() && single.layout.memoryOffsetDword == full.layout.memoryOffsetDword && !full.layout.UsesPushData(), "runtime layout depends on resource count"); Require(full.layout.memoryOffsetDword == 0u && full.layout.DispatchThreadLimitDword() == 0u && full.layout.ShaderDataDwords() == 0u, "direct image resources allocated runtime metadata"); diff --git a/core/shader/recompiler/Optimization/src/BindingAllocator.cpp b/core/shader/recompiler/Optimization/src/BindingAllocator.cpp index 1910bb1531..209bf16a9a 100644 --- a/core/shader/recompiler/Optimization/src/BindingAllocator.cpp +++ b/core/shader/recompiler/Optimization/src/BindingAllocator.cpp @@ -160,7 +160,7 @@ BindingAllocationResult BindingAllocator::Allocate(IrProgram& program, const Bin } } if (!samplers.empty()) { - if (samplers.size() > RuntimeAbi::SamplerHeapCapacity) fail("shader sampler pairs exceed runtime heap capacity"); + if (samplers.size() > RuntimeAbi::SamplerHeapCapacity) fail("shader sampler pairs exceed runtime heap capacity: " + std::to_string(samplers.size() / 2u) + " samplers"); addBinding(next, DescriptorBindingKind::Samplers, std::move(samplers)); } if (usesGds(program)) { From 45be3c03312f59063f2747d21a5e22e91d84819e Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Sat, 17 Oct 2026 03:40:37 +0000 Subject: [PATCH 0636/1000] fix(agc): cover unaligned guest buffer views --- .../prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp | 5 ++++- core/libs/prx/libSceAgcDriver/tests/Graphics.cpp | 5 +++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index 030ce59094..359d00f80f 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -4115,7 +4115,10 @@ VkDescriptorBufferInfo GuestBufferMemory::Descriptor(std::uint64_t address, std: } std::uint64_t GuestBufferMemory::ViewBytes(std::uint64_t bytes, std::uint32_t adjustment) { - return adjustment % 4 == 0 ? bytes + adjustment : 4 * (adjustment / 4 + bytes / 4 + 1); + if (adjustment % 4 == 0) return bytes + adjustment; + const auto dwords = adjustment / 4 + bytes / 4 + (adjustment % 4 + bytes % 4 + 3) / 4; + Require(dwords <= std::numeric_limits::max() / 4, "guest buffer view range overflow"); + return 4 * dwords; } ShaderRecompiler::BdaAbi::Range GuestBufferMemory::addressRange(const Region& region) { diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 8052250ac5..a96784d582 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -4132,12 +4132,13 @@ void oversizedDescriptorTests() { void guestBufferViewBytesTests() { using AgcDriver::Graphics::GuestBufferMemory; - for (const std::uint64_t bytes : {4ull, 16ull, 4096ull}) { + for (const std::uint64_t bytes : {2ull, 3ull, 4ull, 6ull, 7ull, 16ull, 4096ull, 4098ull}) { for (const std::uint32_t adjustment : {0u, 4u, 16u, 256u}) { Require(GuestBufferMemory::ViewBytes(bytes, adjustment) == bytes + adjustment, "a view whose adjustment is a whole number of DWORDs did not add the adjustment"); } for (const std::uint32_t adjustment : {1u, 2u, 3u, 5u, 7u}) { - Require(GuestBufferMemory::ViewBytes(bytes, adjustment) == bytes + 4u * ((adjustment + 3u) / 4u), "a view that starts off a DWORD boundary did not cover the DWORDs around it"); + const auto expected = 4 * ((adjustment + bytes + 3) / 4); + Require(GuestBufferMemory::ViewBytes(bytes, adjustment) == expected, "an off-DWORD view did not cover every byte in its range"); } } } From 17973bc52d4ae8a296c0d4d7414997f8021b4011 Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Sat, 17 Oct 2026 03:59:59 +0000 Subject: [PATCH 0637/1000] fix(agc): report sampler device limits --- .../libSceAgcDriver/Graphics/src/ShaderResources.cpp | 11 ++++++++--- core/libs/prx/libSceAgcDriver/tests/Graphics.cpp | 12 ++++++++++++ 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index 926bfc67fa..ab7d9b47ce 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -1497,6 +1497,10 @@ std::string HexText(std::uint64_t value) { return text; } +std::string DescriptorLimitError(std::string_view description, std::uint64_t required, std::uint64_t limit) { + return std::string(description) + ": required " + std::to_string(required) + ", device limit " + std::to_string(limit); +} + void ValidateDescriptorLayout(const Context& context, std::span bindings, bool updateAfterBind) { const auto maxStageResources = updateAfterBind ? context.descriptorIndexingLimits.maxPerStageUpdateAfterBindResources : context.limits.maxPerStageResources; std::map heapTotals; @@ -1512,10 +1516,11 @@ void ValidateDescriptorLayout(const Context& context, std::span context.limits.maxPerStageDescriptorSamplers) throw std::runtime_error(DescriptorLimitError("typed heaps exceed per-stage sampler capacity", count, context.limits.maxPerStageDescriptorSamplers)); } } - Require(heapTotals[VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE] <= context.limits.maxDescriptorSetSampledImages && heapTotals[VK_DESCRIPTOR_TYPE_STORAGE_IMAGE] <= context.limits.maxDescriptorSetStorageImages && heapTotals[VK_DESCRIPTOR_TYPE_SAMPLER] <= context.limits.maxDescriptorSetSamplers, "typed heaps exceed descriptor set capacity"); + Require(heapTotals[VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE] <= context.limits.maxDescriptorSetSampledImages && heapTotals[VK_DESCRIPTOR_TYPE_STORAGE_IMAGE] <= context.limits.maxDescriptorSetStorageImages, "typed heaps exceed descriptor set capacity"); + if (heapTotals[VK_DESCRIPTOR_TYPE_SAMPLER] > context.limits.maxDescriptorSetSamplers) throw std::runtime_error(DescriptorLimitError("typed heaps exceed descriptor set sampler capacity", heapTotals[VK_DESCRIPTOR_TYPE_SAMPLER], context.limits.maxDescriptorSetSamplers)); } void ValidateRuntimeResources(const CompiledShader& shader, std::span words) { @@ -3787,7 +3792,7 @@ void ShaderResources::addImageBinding(const ShaderRecompiler::DescriptorBinding& Require(plannedSampledImages <= context.limits.maxDescriptorSetSampledImages, "pipeline sampled-image descriptors exceed device limits"); } else { Require(elementWords == 4, "guest sampler descriptor must contain 4 dwords"); - Require(capacity <= context.limits.maxPerStageDescriptorSamplers, "shader sampler descriptors exceed per-stage limits"); + if (capacity > context.limits.maxPerStageDescriptorSamplers) throw std::runtime_error(DescriptorLimitError("shader sampler descriptors exceed per-stage limits", capacity, context.limits.maxPerStageDescriptorSamplers)); Require(binding.samplerDepthCompare.size() == binding.count, "guest sampler binding is missing depth comparison metadata"); Require(binding.samplerUnnormalized.empty() || binding.samplerUnnormalized.size() == binding.count, "guest sampler binding has unnormalized coordinate metadata of another size"); for (std::uint32_t element = 0; element < binding.count; ++element) { diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index e2f1c24796..077c8e6c6c 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -4482,6 +4482,18 @@ void resourceTests() { expectStageResources(Role::GuestImages, Kind::StorageImage, 8, 3, "detiler is unavailable"); expectStageResources(Role::GuestSamplers, Kind::Sampler, 4, 2, "shader sampler descriptors exceed per-stage limits"); } + { + mock = MockVulkan{}; + auto limited = mockContext(); + limited.limits.maxPerStageDescriptorSamplers = 16; + ShaderRecompiler::RecompileResult vertex; + ShaderRecompiler::RecompileResult fragment; + auto samplers = makeBinding(Role::GuestSamplers, static_cast(ShaderRecompiler::RuntimeAbi::Binding::Samplers), 17, std::vector(17u * 4u)); + samplers.kind = Kind::Sampler; + vertex.bindings.push_back(std::move(samplers)); + expectFailure([&] { AgcDriver::Graphics::ShaderResources resources(limited, vertex, fragment, state.color, 0, 0); }, "shader sampler descriptors exceed per-stage limits: required 17, device limit 16"); + Require(mock.live == 0, "sampler limit rejection leaked Vulkan objects"); + } { ShaderRecompiler::RecompileResult vertex; ShaderRecompiler::RecompileResult fragment; From 8980d77e059f58312affe0791a1fa105666248ba Mon Sep 17 00:00:00 2001 From: Romans Pokrovskis Date: Sat, 17 Oct 2026 08:05:06 +0300 Subject: [PATCH 0638/1000] test(agc): cover native vertex aliases after index guards --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 7 + .../execution/SampleMultisampleTarget.cpp | 120 +++++++++++++++++- 2 files changed, 126 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index ab672d4463..53832097d4 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1431,6 +1431,13 @@ if(BUILD_TESTING) endif() if(BUILD_TESTING) + add_test(NAME agc_driver_native_msaa_vertex_input COMMAND agc_driver_sample_multisample_target_tests --native-vertex-input) + set_tests_properties(agc_driver_native_msaa_vertex_input PROPERTIES + ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" TIMEOUT 30 SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + if(WIN32) + set_tests_properties(agc_driver_native_msaa_vertex_input PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test(NAME agc_driver_undithered_alpha_to_coverage_state COMMAND agc_driver_graphics_tests --undithered-alpha-to-coverage-state) add_test(NAME agc_driver_msaa_dcc_state COMMAND agc_driver_graphics_tests --multisample-dcc-state) set_tests_properties(agc_driver_undithered_alpha_to_coverage_state PROPERTIES diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp index d0d00efa8f..e165888e8c 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp @@ -29,6 +29,7 @@ #include #include #include +#include #include #include @@ -455,12 +456,129 @@ void RequireRefused(AgcDriver::VulkanDevice& device, const std::array VertexValues{}; + +void DrawVertexInput(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::State& state, std::uint64_t address, std::uint32_t stride) { + alignas(256) static constexpr std::array code{ + 0xbe822100u, 0x34020a84u, 0xe0781000u, 0x80010801u, 0xf80008cfu, 0x0b0a0908u, 0xbf810000u, + }; + VertexValues.fill(0xdeadbeefu); + const auto output = reinterpret_cast(VertexValues.data()); + std::vector userData(12u, 0u); + userData[0] = 0x10000000u; + userData[1] = 1u; + userData[4] = static_cast(output); + userData[5] = static_cast((output >> 32u) & 0xffffu); + userData[6] = static_cast(sizeof(VertexValues)); + userData[7] = 0x31016facu; + ShaderRecompiler::ShaderVertexStageInfo info{}; + info.resourcesNum = 1u; + info.resources[0].fields = {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (stride << 16u), stride == 0u ? 16u : 3u, 0x3104dfacu}; + info.resourcesDst[0] = {8, 4, 0, 0}; + info.fetchAttribReg = 8u; + info.fetchBufferReg = 10u; + info.fetchEmbedded = true; + const std::array vertexMemory{{{reinterpret_cast(code.data()), std::as_bytes(std::span(code))}}}; + ShaderRecompiler::RecompileRequest vertexRequest{ + {ShaderStage::Vertex, reinterpret_cast(code.data()), code, 0, {}}, + {64u, 0, userData, std::nullopt, std::nullopt, info, vertexMemory}, + device.Target(), {0, 0, 0, 64} + }; + vertexRequest.useCache = false; + const auto vertex = ShaderRecompiler::Recompile(vertexRequest); + Require(vertex.vertexAttributes.size() == 1u, "the native vertex probe did not create a fixed-function attribute"); + const auto vertexPush = static_cast(vertex.pushConstants.size()); + const auto pixelAddress = reinterpret_cast(PixelCode.data()); + const std::array pixelMemory{{{pixelAddress, std::as_bytes(std::span(PixelCode))}}}; + const AgcDriver::Registers registers{{0x1b6, 0x8000}, {0x1b3, 0}, {0x1b4, 0}, {0x203, 0}, {0x1c5, Float32Export}}; + ShaderRecompiler::RecompileRequest pixelRequest{ + {ShaderStage::Fragment, pixelAddress, PixelCode, 0, {}}, + {64u, 0, {}, std::nullopt, AgcDriver::Graphics::DecodePixelStageInfo(registers, AgcDriver::Graphics::ExportMappings(state)), std::nullopt, pixelMemory}, + device.Target(), PixelPushLayout(vertexPush, device.Target()) + }; + pixelRequest.useCache = false; + const auto pixel = ShaderRecompiler::Recompile(pixelRequest); + const std::array shaders{{{ShaderStage::Vertex, &vertex, 0u}, {ShaderStage::Fragment, &pixel, PixelPushOffset(vertexPush, device.Target())}}}; + const AgcDriver::Pm4::DrawParameters draw{0u, 3u, 0u, 1u, 0u, false}; + device.Draw(state, draw, shaders); + device.WaitIdle(); } -int main() { +void CheckVertexValues(std::uint64_t address, std::uint32_t stride) { + std::array expected{}; + for (std::size_t vertex = 0; vertex < 3u; ++vertex) { + std::memcpy(expected.data(), reinterpret_cast(address + vertex * stride), sizeof(expected)); + for (std::size_t component = 0; component < expected.size(); ++component) { + Require(VertexValues[vertex * 4u + component] == expected[component], "the fixed-function vertex probe did not observe the original guest bytes"); + } + } +} + +int NativeVertexInput(std::unique_ptr& device) { + Block source(4u * BlockBytes); + Block destination(BlockBytes); + struct ReleaseDevice { + std::unique_ptr& value; + ~ReleaseDevice() { value.reset(); } + } releaseDevice{device}; + const auto context = AgcDriver::VulkanDeviceTestAccess::ContextOf(*device); + if (context.hostImportAlignment == 0u) { + std::puts("native vertex input not tested: host imports unavailable"); + return VulkanTestSkipped; + } + const auto color = DecodeTarget(source); + const auto colorBytes = AgcDriver::Graphics::MultisampledColorBytes(color); + Require(colorBytes >= 48u && colorBytes + 48u <= source.bytes, "the native vertex probe has no neighboring range"); + constexpr std::array element{0.25f, -2.0f, 7.5f, 1.0f}; + for (std::size_t vertex = 0; vertex < 3u; ++vertex) { + std::memcpy(source.data + vertex * sizeof(element), element.data(), sizeof(element)); + std::memcpy(source.data + colorBytes + vertex * sizeof(element), element.data(), sizeof(element)); + } + std::memcpy(source.data + colorBytes - 8u, element.data(), 8u); + Draw(*device, TargetState(color)); + AgcDriver::Graphics::NoteColorMetadataFill(source.Address(), static_cast(colorBytes), FillOnes); + CheckUniform(LoadSamples(*device, source), Ones, "before the fixed-function vertex probe"); + const auto neighbor = source.Address() + colorBytes; + { + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + const auto* native = AgcDriver::Graphics::HostImportFor(context, source.Address(), 48u); + const auto* adjacent = AgcDriver::Graphics::HostImportFor(context, neighbor, 48u); + Require(native != nullptr && native == adjacent, "the native and neighboring vertices do not share an import"); + } + AgcDriver::Graphics::RequireNoMultisampleTarget(neighbor, 48u, "the vertex neighbor control"); + const auto state = TargetState(DecodeTarget(destination)); + std::uint32_t missed = 0u; + for (const auto stride : {0u, 16u}) { + for (const bool edge : {false, true}) { + DrawVertexInput(*device, state, neighbor, stride); + CheckVertexValues(neighbor, stride); + const auto address = edge ? neighbor - 8u : source.Address(); + std::string refusal; + try { + DrawVertexInput(*device, state, address, stride); + CheckVertexValues(address, stride); + } catch (const std::exception& error) { + refusal = error.what(); + } + const bool refused = refusal.find("whose samples are only on the host") != std::string::npos; + Require(refusal.empty() || refused, "the native vertex input failed for another reason: " + refusal); + std::printf("native vertex stride %u %s: refused=%u neighbor=passed original-guest-values=%u detail=%s\n", stride, edge ? "edge" : "base", refused ? 1u : 0u, refused ? 0u : 12u, refusal.c_str()); + missed += refused ? 0u : 1u; + } + } + CheckUniform(LoadSamples(*device, source), Ones, "after fixed-function vertex inputs"); + std::puts("native vertex controls: shared-import=1 source-samples=1024 neighbor-values=passed"); + Require(missed == 0u, "native vertex inputs missed " + std::to_string(missed) + " resident-image refusals"); + return 0; +} + +} + +int main(int argc, char** argv) { try { auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; + if (argc == 2 && std::string_view(argv[1]) == "--native-vertex-input") return NativeVertexInput(device); Block block(4u * BlockBytes); Block other(BlockBytes); const auto color = DecodeTarget(block); From 15eb12009cf68b68bc223939c1ad132f5b3db506 Mon Sep 17 00:00:00 2001 From: Romans Pokrovskis Date: Sat, 17 Oct 2026 08:05:12 +0300 Subject: [PATCH 0639/1000] fix(agc): refuse native MSAA vertex buffer reads --- core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp | 1 + docs/dev/debt/libSceAgcDriver.md | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index 1184567b71..7bcdfb44dc 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -1198,6 +1198,7 @@ DrawInputs prepareDrawInputs(const Context& context, const State& state, const P const auto [begin, end] = plan.copies[range]; const auto bytes = static_cast(end - begin); GuestMemory::CheckRange(reinterpret_cast(begin), bytes, 1); + RequireNoMultisampleTarget(begin, bytes, "a vertex buffer read"); if (const auto* import = InPlaceDrawInput(context, begin, bytes, plan.alignments[range], &inputs.vertexInputBehindGpuWrite)) { rangeHandles.push_back(import->buffer); rangeOffsets.push_back(begin - import->base); diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index e1aef6ab0b..d608be1b9a 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -119,7 +119,7 @@ - [Multisampled color fills](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) (libSceAgcDriver) - a multisampled target's samples are on the host, so a fill of its color memory is applied at its next use: a uniform fill over the whole of it clears the host image to that value, and a pending CMASK fast clear takes precedence over the fill and over a fill that would throw. Its color memory is a single mip and slice of addrlib's block dimensions for its sample count (ComputeThinBlockDimension). A fill over part of it, a fill reaching the start of a target whose footprint is not computed (linear), and a fill value with no exact clear value in the format (sRGB channels other than 0 and 1) throw at the next use. A fill before the target's first use is not seen (the host image starts at 0), nor are copies or shader writes into its color memory. A uniform compute fill over exactly the whole color memory with an exact clear value is not stored into guest memory, since only the host image holds the samples. A target is retired only when its whole guarded range is unmapped or its allocation removed, so if a title reuses that memory for other data while it stays mapped, a read the guard does not check (a buffer read through a V# or a pointer) sees the bytes from before the fill. - [Multisampled texture no draw rendered to](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) (libSceAgcDriver) - sampling a multisampled texture whose surface no draw rendered to creates the host target from the T# and clears it to the color a single 32-bit value repeated over the whole footprint encodes, padding up to whole tile blocks included. Inferred, not measured on the console: that the samples of such a surface are the guest color memory, as a title that clears its canvas on the CPU (ps5-opengl `ps5_clear_msaa4_color`) relies on. Memory that is not one repeated value, tile modes other than Standard4KB, Standard64KB and RenderTarget, unmapped memory and values with no exact clear color throw. CPU writes to the surface after its host target exists are not seen by the host. - [Multisampled targets in CB passes](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a CB eliminate or DCC pass over a multisampled target throws, since its texels are on the host and only a CB resolve and an FMASK decompress (`CB_COLOR_CONTROL` mode 5) are modeled. The FMASK decompress only applies the pending CMASK fast clear to the host image; it requires a CMASK address, and the target's DCC and FMASK contents are not interpreted. DCC over a multisampled target is accepted only in that pass and throws everywhere else. A CB resolve into a fast-clear destination throws: the resolve writes the destination's texels but not its CMASK, which would still read as cleared. -- [Multisampled target guest access](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) (libSceAgcDriver) - the memory a multisampled color target throws on (CPU read, index buffer read, DMA copy, texture or storage image read) uses fragment-aware 64 KiB render-target blocks. [AMD Addrlib's GFX10 thin-block rule](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/core/addrlib2.cpp#L1680-L1712) gives the block width and height from element bytes and fragment count; its [surface-size calculation](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/gfx10/gfx10addrlib.cpp#L4000-L4005) rounds both extents to those blocks and multiplies by element bytes and fragments. Thus a 1920x1080 4x RGBA8 surface occupies 0x1fe0000 bytes, leaving the next allocation at +0x2000000 outside the guard. The 2x and 8x block orientations differ from treating the fragments as a larger element. Console address-layout equivalence has not been measured. Other tile modes retain the conservative `max(CB bytes * samples, width * height * element bytes * samples)` bound, which may still reject a distinct allocation. Targets are kept per device for the device's lifetime, one per surface address, format, extent and sample count, and are not evicted. +- [Multisampled target guest access](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) (libSceAgcDriver) - the memory a multisampled color target throws on (CPU read, index buffer read, fixed-function vertex buffer read, DMA copy, texture or storage image read) uses fragment-aware 64 KiB render-target blocks. [AMD Addrlib's GFX10 thin-block rule](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/core/addrlib2.cpp#L1680-L1712) gives the block width and height from element bytes and fragment count; its [surface-size calculation](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/gfx10/gfx10addrlib.cpp#L4000-L4005) rounds both extents to those blocks and multiplies by element bytes and fragments. Thus a 1920x1080 4x RGBA8 surface occupies 0x1fe0000 bytes, leaving the next allocation at +0x2000000 outside the guard. The 2x and 8x block orientations differ from treating the fragments as a larger element. Console address-layout equivalence has not been measured. Other tile modes retain the conservative `max(CB bytes * samples, width * height * element bytes * samples)` bound, which may still reject a distinct allocation. Targets are kept per device for the device's lifetime, one per surface address, format, extent and sample count, and are not evicted. - [CB resolve sample count](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a draw with CB_COLOR_CONTROL in resolve mode is a CB resolve when either PA_SC_AA_CONFIG or CB_COLOR0_ATTRIB.NUM_SAMPLES is multisampled, and takes its sample count from CB0. Inferred, not from documentation: PPSA36082 resolves its 4x targets with no sample count in PA_SC_AA_CONFIG, and radeonsi sets PA_SC_AA_CONFIG to 0 for a multisampled framebuffer when rasterizer multisampling and line smoothing are off. - [Custom MSAA sample locations](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) support a uniform 1x1 pattern for 2, 4 or 8 samples through VK_EXT_sample_locations. Patterns varying across the guest's 2x2 pixel grid use a 2x2 sample location grid, anchored at the framebuffer origin like the guest's while the window offset is zero, on devices whose `maxSampleLocationGridSize` for the sample count is a multiple of 2x2, and throw on others. The RTX 5070 reports 2x4 for 2 samples, 2x2 for 4 and 1x2 for 8; an AMD Radeon integrated GPU reports 2x2 for each. Devices without the required sample count, four subpixel bits or the full guest coordinate range refuse custom patterns. A multisampled rasterizer drawing into one-sample color targets, and a one-sample depth target if any, with MSAA_ENABLE set, no alpha-to-coverage and no coverage pixel input is drawn with one sample at sample 0's location in each pixel of the grid, as the targets keep only sample 0's coverage. Without PS_ITER_SAMPLE such a draw also refuses centroid inputs, and keeps PERSP_SAMPLE, LINEAR_SAMPLE and ANCILLARY: a pixel shaded once is taken to get sample 0's barycentrics and sample index 0. With PS_ITER_SAMPLE only sample 0's invocation reaches the targets, so its sample and centroid barycentrics are taken to be sample 0's and the draw is shaded once. Both are inferred, not measured. ANCILLARY with POS_X_FLOAT or POS_Y_FLOAT throws, since reading the sample index makes the host shade per sample, which moves FragCoord from the pixel center to sample 0's location. By the same Vulkan rule, PERSP_CENTER and LINEAR_CENTER inputs on this path are interpolated at sample 0's location on the host, where the console uses the pixel center; one-sample depth images are created with `SAMPLE_LOCATIONS_COMPATIBLE_DEPTH` when the device supports it, as multisampled ones already were; this is how VK_AMD_mixed_attachment_samples defines the case, and it is not measured on PS5 hardware. Other mixed sample counts throw. Depth images remain in GENERAL between draws, and render passes cannot continue across pattern changes. Guest offsets retain the existing decoder's signed four-bit, sixteenth-pixel interpretation; PS5 coverage and interpolation equivalence have not been measured on hardware. - [DCC display buffers](../../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) are presented only when all keys of the console's DCC extent agree on uncompressed or a fast-clear code, padding keys included; other keys, and register-clear keys over a pending image, throw. From 0300bb0e79b78fb8b8af74fe70562062a42ef1e2 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Fri, 16 Oct 2026 22:23:26 -0700 Subject: [PATCH 0640/1000] feat(agc): find the programs that store through the BDA table A program writes guest memory through the BDA table only with a store or an atomic through a PhysicalStorageBuffer pointer. SpirvStoresThroughAddresses scans a module for such an instruction: it collects the PhysicalStorageBuffer pointer types, every value of those types, and the stores, copies and atomics whose pointer operand is one of them. A module it can't read to its end counts as storing. An address-based build now records whether its programs only read through the table (ShaderResources::BdaReadOnly). Each module is scanned once; the answer is kept by its content hash. --- .../Graphics/include/ShaderResources.hpp | 3 + .../Graphics/src/ShaderAddressResources.cpp | 103 ++++++++++++++++++ .../libSceAgcDriver/tests/BdaContracts.cpp | 15 +++ 3 files changed, 121 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp index 8e25d227c9..900a34c3bc 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp @@ -42,6 +42,7 @@ std::uint64_t SampledTextureCacheBudget(const Context& context); std::shared_ptr CachedSampledTexture(const Context& context, std::span words); bool SampledTexturesShareEntry(std::span first, std::span second); bool CompletePendingCpuWrites(const Context& context, std::uint64_t address, std::size_t bytes); +bool SpirvStoresThroughAddresses(std::span words); // The cached storage image of a surface (render targets use it as their resident image); brought up // to date with guest memory before it is returned. @@ -182,6 +183,7 @@ public: // Whether a written buffer was copied (its results reach guest memory by the CPU write-back). bool HasCopiedWrites() const { return guestMemory.HasCopiedWrites(); } bool HoldsLease() const { return guestMemory.HoldsLease(); } + bool BdaReadOnly() const { return usesBda && bdaReadOnly; } bool UsesAddressTable() const { return usesBda; } std::size_t CopiedBytes() const { return guestMemory.CopiedBytes(); } bool WritesOverlap(std::uint64_t address, std::size_t bytes) const { return guestMemory.WritesOverlap(address, bytes); } @@ -486,6 +488,7 @@ private: std::size_t maxDrawSnapshotBytes = MaxDrawSnapshotBytes(); std::unique_ptr bda; bool usesBda = false; + bool bdaReadOnly = true; std::vector multisampleSources; bool usesFaultBuffer = false; bool faultChecks = false; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderAddressResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderAddressResources.cpp index cade6d3a3d..03b171e774 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderAddressResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderAddressResources.cpp @@ -1,10 +1,112 @@ #include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp" +#include #include #include #include +#include +#include +#include +#include namespace AgcDriver::Graphics { +namespace { + +std::uint32_t pointerOperandStored(std::uint32_t opcode, std::span instruction) { + switch (static_cast(opcode)) { + case spv::OpStore: + case spv::OpCopyMemory: + case spv::OpCopyMemorySized: + case spv::OpAtomicStore: + case spv::OpAtomicFlagClear: + return instruction.size() > 1 ? instruction[1] : 0u; + case spv::OpAtomicExchange: + case spv::OpAtomicCompareExchange: + case spv::OpAtomicCompareExchangeWeak: + case spv::OpAtomicIIncrement: + case spv::OpAtomicIDecrement: + case spv::OpAtomicIAdd: + case spv::OpAtomicISub: + case spv::OpAtomicSMin: + case spv::OpAtomicUMin: + case spv::OpAtomicSMax: + case spv::OpAtomicUMax: + case spv::OpAtomicAnd: + case spv::OpAtomicOr: + case spv::OpAtomicXor: + case spv::OpAtomicFlagTestAndSet: + case spv::OpAtomicFMinEXT: + case spv::OpAtomicFMaxEXT: + case spv::OpAtomicFAddEXT: + return instruction.size() > 3 ? instruction[3] : 0u; + default: + return 0u; + } +} + +bool namesOrDecorates(std::uint32_t opcode) { + switch (static_cast(opcode)) { + case spv::OpName: + case spv::OpMemberName: + case spv::OpDecorate: + case spv::OpMemberDecorate: + case spv::OpDecorateId: + case spv::OpDecorateString: + case spv::OpMemberDecorateString: + return true; + default: + return false; + } +} + +template +bool forEachInstruction(std::span words, const TVisit& visit) { + for (std::size_t at = 5; at < words.size();) { + const auto count = words[at] >> 16u; + if (count == 0 || at + count > words.size()) return false; + visit(words[at] & 0xffffu, words.subspan(at, count)); + at += count; + } + return true; +} + +bool storesThroughAddressesCached(const ShaderRecompiler::SharedSpirv& spirv) { + static std::mutex mutex; + static std::map, bool> known; + const auto hash = spirv.ContentHash(); + const std::pair key{hash.low, hash.high}; + { + std::lock_guard lock(mutex); + if (const auto found = known.find(key); found != known.end()) return found->second; + } + const bool stores = SpirvStoresThroughAddresses(spirv.Words()); + std::lock_guard lock(mutex); + known.emplace(key, stores); + return stores; +} + +} + +bool SpirvStoresThroughAddresses(std::span words) { + if (words.size() < 5 || words[0] != spv::MagicNumber) return true; + std::unordered_set pointerTypes; + const bool readable = forEachInstruction(words, [&](std::uint32_t opcode, std::span instruction) { + if (opcode == spv::OpTypePointer && instruction.size() > 2 && instruction[2] == spv::StorageClassPhysicalStorageBuffer) pointerTypes.insert(instruction[1]); + }); + if (!readable) return true; + if (pointerTypes.empty()) return false; + std::unordered_set pointers; + forEachInstruction(words, [&](std::uint32_t opcode, std::span instruction) { + if (instruction.size() > 2 && !namesOrDecorates(opcode) && opcode != spv::OpTypePointer && pointerTypes.contains(instruction[1])) pointers.insert(instruction[2]); + }); + bool stores = false; + forEachInstruction(words, [&](std::uint32_t opcode, std::span instruction) { + const auto pointer = pointerOperandStored(opcode, instruction); + stores = stores || (pointer != 0 && pointers.contains(pointer)); + }); + return stores; +} + void ShaderResources::prepareAddressBindings(std::span shaders, std::span snapshots) { static const bool loopGuarded = std::getenv("APS5_LOOP_GUARD") != nullptr; for (const auto& shader : shaders) { @@ -22,6 +124,7 @@ void ShaderResources::prepareAddressBindings(std::span sha Require(shader.program->bdaAbiVersion == (faults == 0 ? 0u : ShaderRecompiler::BdaAbi::Version), "incompatible BDA ABI version"); // Rect-list validation needs a fault buffer, but never accesses guest addresses. usesBda = usesBda || tables != 0; + if (tables != 0 && storesThroughAddressesCached(shader.program->spirv)) bdaReadOnly = false; usesFaultBuffer = usesFaultBuffer || faults != 0; const bool tableFaultsOnly = std::any_of(shader.program->bindings.begin(), shader.program->bindings.end(), [](const ShaderRecompiler::DescriptorBinding& binding) { return binding.role == ShaderRecompiler::DescriptorRole::ImageTableMap; }); faultChecks = faultChecks || tables != 0 || (faults != 0 && (!tableFaultsOnly || shader.program->imageTableFaults != 0 || shader.program->poisonedSrtReads != 0 || loopGuarded)); diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaContracts.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaContracts.cpp index cf54e315dd..0802a3f3fe 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaContracts.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaContracts.cpp @@ -1,4 +1,5 @@ #include "BdaShader.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp" #include "ControlFlow/RequestSerializer.hpp" #include "SpirvBackend/SpirvBda.hpp" #include @@ -19,6 +20,20 @@ void reject(TAction action, const char* expected) { void RunBdaContractTests() { using namespace ShaderRecompiler; + using AgcDriver::Graphics::SpirvStoresThroughAddresses; + AgcDriver::Graphics::Require(!SpirvStoresThroughAddresses(MakeBdaTestShader(0x1000, 32)), "a BDA read was taken for a store through an address"); + AgcDriver::Graphics::Require(!SpirvStoresThroughAddresses(MakeBdaDwordReadTestShader(0x1000, 4, false, false)), "BDA dword reads were taken for a store through an address"); + AgcDriver::Graphics::Require(SpirvStoresThroughAddresses(MakeBdaStoreTestShader(0x1000, 7, 32, false)), "a BDA store was not seen"); + AgcDriver::Graphics::Require(SpirvStoresThroughAddresses(MakeBdaWriteTestShader(0x1000, 7, 32, false)), "a BDA write was not seen"); + const std::array stored{7u, 9u}; + AgcDriver::Graphics::Require(SpirvStoresThroughAddresses(MakeBdaDwordWriteTestShader(0x1000, static_cast(stored.size()), stored.data())), "BDA dword writes were not seen"); + const std::array headerOnly{spv::MagicNumber, 0x00010300u, 0u, 1u, 0u}; + AgcDriver::Graphics::Require(!SpirvStoresThroughAddresses(headerOnly), "a module without physical pointers was taken for one storing through addresses"); + const std::array truncated{spv::MagicNumber, 0u}; + AgcDriver::Graphics::Require(SpirvStoresThroughAddresses(truncated), "an unreadable module was taken for one without stores"); + auto unparsed = MakeBdaStoreTestShader(0x1000, 7, 32, false); + unparsed[5] |= 0xffff0000u; + AgcDriver::Graphics::Require(SpirvStoresThroughAddresses(unparsed), "a module with an instruction past its end was taken for one without stores"); const std::array capabilities{spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess}; const std::array extensions{"SPV_KHR_physical_storage_buffer", "SPV_KHR_8bit_storage"}; SpirvTargetOptions target{0x00401000u, 0x00010300u, 32, BdaAbi::Version, capabilities, extensions}; From a939efe1a054bf8ab60c81e37a7d2a937d143974 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Fri, 16 Oct 2026 22:23:27 -0700 Subject: [PATCH 0641/1000] perf(agc): let a draw that only reads through the BDA table continue its render pass recordDraw passed every address-based draw to the pass hazard tracker as both an unknown reader and an unknown writer of guest memory. An unknown writer conflicts with any earlier access in the pass, so no address-based draw ever continued the previous draw's pass: each one ended the pass and began a new one, which loads and stores the attachments again. At Resident Evil Village's menu about half of all draws are address-based, and about 590 render passes begin for about 850 draws per frame. A build whose programs only read through the BDA table now reports unknown reads only. The tracker already orders unknown reads behind any recorded range write in the pass and keeps later range writes behind them, so such a draw continues a pass whose earlier draws wrote no guest ranges. Builds with stores through the table, copied writes, or an address lease without a table keep both flags. APS5_NO_ADDRESS_READ_PASSES=1 restores the old behaviour. Render targets reach guest memory only by explicit write-backs, so a read through the table sees the same bytes whether or not the pass breaks. Raghav314's #6947 makes the same change inside its GPU descriptor heap, with the read-only fact from #6946's recompiler field; this one keeps it in the driver, without a shader cache format change. The idea also comes from fable-void's 5d57298425 (wip/new-gpu-path), which orders address-based builds by their known access ranges; this takes the read-only half without his run-time pre-pass bounds. --- core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index 34accb1198..73a90b700e 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -1766,7 +1766,10 @@ void recordDraw(const Context& context, const State& state, const Pm4::DrawParam if (target != nullptr) passAttachments.emplace_back(target->Image(), true); } const bool unknownMemory = addressBased || resources.UsesAddressTable() || resources.HasCopiedWrites(); - const PassAccess passAccess{passReads, resources.GpuWrites(), passImages, passAttachments, unknownMemory, unknownMemory}; + static const bool addressReadPasses = std::getenv("APS5_NO_ADDRESS_READ_PASSES") == nullptr; + const bool readOnlyBda = addressReadPasses && resources.BdaReadOnly(); + const bool unknownWrites = ((addressBased || resources.UsesAddressTable()) && !readOnlyBda) || resources.HasCopiedWrites(); + const PassAccess passAccess{passReads, resources.GpuWrites(), passImages, passAttachments, unknownMemory, unknownWrites}; const bool fenced = state.depth.has_value() || !record.proxies.empty(); const auto start = recorder->StartDrawPass(passKey, capture || readsTarget || gpuIndirect || meshIndirect, fenced, passAccess); const bool continued = start.continued; From b437dc5dbabe37c7503676623eb4ba74c90d03be Mon Sep 17 00:00:00 2001 From: milescoviello Date: Sat, 17 Oct 2026 05:49:06 +0000 Subject: [PATCH 0642/1000] fix(libkernel): return EFAULT from sceKernelRead/sceKernelWrite for a null buffer read and write already fail with EFAULT for a null buffer and a non-zero length and treat a zero-length null buffer as empty. The sceKernel entry points threw std::invalid_argument for both. They now do the same as read and write. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libkernel/File/src/Open.cpp | 8 ++++++-- core/libs/tests/GuestPosixScalarIo.cpp | 6 ++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libkernel/File/src/Open.cpp b/core/libs/prx/libkernel/File/src/Open.cpp index f461a59cd8..f3a34a761f 100644 --- a/core/libs/prx/libkernel/File/src/Open.cpp +++ b/core/libs/prx/libkernel/File/src/Open.cpp @@ -271,7 +271,9 @@ std::int64_t APS5_VABI sceKernelRead(int d, void* buf, std::size_t nbytes) { return n < 0 ? SceKernelError(*__error_nid_postfix()) : n; } if (buf == nullptr) { - throw std::invalid_argument(std::string(__func__) + ": buf is null"); + if (nbytes != 0) return SceIoErrorFromErrno(EFAULT); + char empty = 0; + return sceKernelRead(d, &empty, 0); } const GuestArena::HostWrite destination(buf, nbytes); if (!destination.Open()) errno = EFAULT; @@ -288,7 +290,9 @@ std::int64_t APS5_VABI sceKernelWrite(int d, const void* buf, std::size_t nbytes return n < 0 ? SceKernelError(*__error_nid_postfix()) : n; } if (buf == nullptr) { - throw std::invalid_argument(std::string(__func__) + ": buf is null"); + if (nbytes != 0) return SceIoErrorFromErrno(EFAULT); + const char empty = 0; + return sceKernelWrite(d, &empty, 0); } auto n = NativeWrite(d, buf, nbytes); if (n < 0) return SceIoErrorFromErrno(errno); diff --git a/core/libs/tests/GuestPosixScalarIo.cpp b/core/libs/tests/GuestPosixScalarIo.cpp index 177845674a..a4b9a32780 100644 --- a/core/libs/tests/GuestPosixScalarIo.cpp +++ b/core/libs/tests/GuestPosixScalarIo.cpp @@ -135,6 +135,12 @@ int main() { Require(sceKernelRead(-1, direct, 1) == static_cast(0x80020009u)); Require(sceKernelWrite(-1, direct, 1) == static_cast(0x80020009u)); Require(sceKernelLseek(-1, 0, 0) == static_cast(0x80020009u)); + Require(sceKernelRead(descriptor, nullptr, 1) == static_cast(0x8002000eu)); + Require(sceKernelWrite(descriptor, nullptr, 1) == static_cast(0x8002000eu)); + Require(sceKernelRead(descriptor, nullptr, 0) == 0); + Require(sceKernelWrite(descriptor, nullptr, 0) == 0); + Require(sceKernelRead(-1, nullptr, 0) == static_cast(0x80020009u)); + Require(sceKernelWrite(-1, nullptr, 0) == static_cast(0x80020009u)); Require(close_nid_postfix(descriptor) == 0); const int readOnly = open_nid_postfix(fileName.c_str(), SCE_KERNEL_O_RDONLY, 0); Require(readOnly >= 0); From ffd468e148e99e0ce4490343b377bf59fa995eb8 Mon Sep 17 00:00:00 2001 From: thuanquang <60096168+thuanquang@users.noreply.github.com> Date: Sat, 17 Oct 2026 12:16:22 +0700 Subject: [PATCH 0643/1000] fix(shader): read low-half 16-bit multiply factors as 24-bit fields On AMD's Windows driver 32.0.21045.5002 (RX 6700 XT), a product of two values the driver knows fit in 16 bits, reduced to 16 bits and stored with nothing else merged in, keeps all 32 bits of the product. v_mul_lo_u16 and v_pk_mul_lo_u16 extracted 16-bit factors, so into a register whose high half was zero they wrote 0xfffe0001 for 0xffff * 0xffff. A factor taken from the low half of its source is now read as the low 24 bits of the register; the low 16 bits of the product depend only on the low 16 bits of the factors. Co-Authored-By: Claude Opus 5.5 --- .../tests/execution/MulLoU16HighHalf.cpp | 145 ++++++++++++++++++ .../Translation/TranslationContext.hpp | 3 + .../Translation/src/IntegerInstructions.cpp | 30 ++++ .../Translation/src/VectorInstructions.cpp | 4 +- docs/dev/debt/shader.md | 1 + 5 files changed, 181 insertions(+), 2 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/MulLoU16HighHalf.cpp diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/MulLoU16HighHalf.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/MulLoU16HighHalf.cpp new file mode 100644 index 0000000000..f64c0541dd --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/MulLoU16HighHalf.cpp @@ -0,0 +1,145 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 64; +constexpr std::uint32_t Inputs = 4; +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; + +alignas(256) constexpr std::array ZeroHighCode{ + 0x34020082, 0x34060080, 0xe0302000, 0x80000401, 0xe0302004, 0x80000501, 0xe0302008, 0x80000601, + 0xbf8c3f70, 0x36140cff, 0x0000ffff, 0xd705000a, 0x00020b04, 0xe0702000, 0x80010a03, 0xbf810000, +}; + +alignas(256) constexpr std::array ChainCode{ + 0x34020082, 0x34060080, 0xe0302000, 0x80000401, 0xe0302004, 0x80000501, 0xe0302008, 0x80000601, + 0xbf8c3f70, 0x7e140280, 0xd703000a, 0x00020906, 0xd705000a, 0x00020b0a, 0xe0702000, 0x80010a03, + 0xbf810000, +}; + +alignas(256) constexpr std::array KeptHighCode{ + 0x34020082, 0x34060080, 0xe0302000, 0x80000401, 0xe0302004, 0x80000501, 0xe0302008, 0x80000601, + 0xbf8c3f70, 0x7e140306, 0xd705000a, 0x00020b04, 0xe0702000, 0x80010a03, 0xbf810000, +}; + +alignas(256) constexpr std::array MaskedFactorsCode{ + 0x34020082, 0x34060080, 0xe0302000, 0x80000401, 0xe0302004, 0x80000501, 0xe0302008, 0x80000601, + 0xbf8c3f70, 0x360808ff, 0x0000ffff, 0x360a0aff, 0x0000ffff, 0x36140cff, 0x0000ffff, 0xd705000a, + 0x00020b04, 0xe0702000, 0x80010a03, 0xbf810000, +}; + +alignas(256) constexpr std::array PackedLowLaneCode{ + 0x34020082, 0x34060080, 0xe0302000, 0x80000401, 0xe0302004, 0x80000501, 0xe0302008, 0x80000601, + 0xbf8c3f70, 0x360808ff, 0x0000ffff, 0x360a0aff, 0x0000ffff, 0xcc01400a, 0x18020b04, 0xe0702000, + 0x80010a03, 0xbf810000, +}; + +alignas(256) constexpr std::array PackedHighLaneCode{ + 0x34020082, 0x34060080, 0xe0302000, 0x80000401, 0xe0302004, 0x80000501, 0xe0302008, 0x80000601, + 0xbf8c3f70, 0x360808ff, 0xffff0000, 0x360a0aff, 0xffff0000, 0xcc01400a, 0x18020b04, 0xe0702000, + 0x80010a03, 0xbf810000, +}; + +constexpr std::array, 8> Edges{{ + {0xffffffffu, 0xffffffffu, 0x00000000u}, {0x0000ffffu, 0x0000ffffu, 0xabcd1234u}, {0xffff0000u, 0xffff0000u, 0xffffffffu}, + {0x8000ffffu, 0x7fff8000u, 0x12345678u}, {0x12345678u, 0x9abcdef0u, 0x80000000u}, {0x00010001u, 0x00010001u, 0x0000ffffu}, + {0x0000ff00u, 0x00000100u, 0xffff0000u}, {0x00000000u, 0xffffffffu, 0x7fff7fffu}, +}}; + +void FillInput() { + std::uint64_t state = 0x9e3779b97f4a7c15ull; + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + auto* words = &Input[tid * Inputs]; + for (std::uint32_t j = 0; j < 3u; ++j) { + state = state * 6364136223846793005ull + 1442695040888963407ull; + words[j] = tid < Edges.size() ? Edges[tid][j] : static_cast(state >> 32u); + } + } +} + +std::array BufferDescriptor(const void* data, std::uint32_t count) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (4u << 16u), count, 0x11016facu}; +} + +void Run(AgcDriver::VulkanDevice& device, std::span code) { + Output.fill(0xdeadbeefu); + std::vector userData(8, 0u); + const auto input = BufferDescriptor(Input.data(), static_cast(Input.size())); + const auto output = BufferDescriptor(Output.data(), static_cast(Output.size())); + std::copy(input.begin(), input.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); +} + +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + +std::uint32_t Low16Product(std::uint32_t a, std::uint32_t b) { + return ((a & 0xffffu) * (b & 0xffffu)) & 0xffffu; +} + +void Check(const char* name, const std::function& expected) { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto* in = &Input[tid * Inputs]; + const auto want = expected(in[0], in[1], in[2]); + Require(Output[tid] == want, std::string("mul lo u16: ") + name + ": thread " + std::to_string(tid) + " (" + Hex(in[0]) + ", " + Hex(in[1]) + ", " + Hex(in[2]) + ") wrote " + Hex(Output[tid]) + ", expected " + Hex(want)); + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + FillInput(); + Run(*device, ZeroHighCode); + Check("v_mul_lo_u16 into a register whose high half is zero", [](std::uint32_t a, std::uint32_t b, std::uint32_t) { return Low16Product(a, b); }); + Run(*device, ChainCode); + Check("v_add_nc_u16 then v_mul_lo_u16 in a cleared register", [](std::uint32_t a, std::uint32_t b, std::uint32_t c) { return Low16Product(c + a, b); }); + Run(*device, KeptHighCode); + Check("v_mul_lo_u16 into a register holding another value", [](std::uint32_t a, std::uint32_t b, std::uint32_t c) { return (c & 0xffff0000u) | Low16Product(a, b); }); + Run(*device, MaskedFactorsCode); + Check("v_mul_lo_u16 of factors masked to 16 bits into a register whose high half is zero", [](std::uint32_t a, std::uint32_t b, std::uint32_t) { return Low16Product(a, b); }); + Run(*device, PackedLowLaneCode); + Check("v_pk_mul_lo_u16 of factors whose high halves are zero", [](std::uint32_t a, std::uint32_t b, std::uint32_t) { return Low16Product(a, b); }); + Run(*device, PackedHighLaneCode); + Check("v_pk_mul_lo_u16 of factors whose low halves are zero", [](std::uint32_t a, std::uint32_t b, std::uint32_t) { return Low16Product(a >> 16u, b >> 16u) << 16u; }); + std::puts("mul lo u16 high half tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp b/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp index c0e248f919..c2feab9317 100644 --- a/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp +++ b/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp @@ -248,6 +248,9 @@ private: bool floatCube(const RdnaInstruction& inst, std::uint32_t resultKind); bool integer16Shift(const RdnaInstruction& inst, IrOpcode opcode, bool arithmetic); bool integer16Binary(const RdnaInstruction& inst, IrOpcode opcode, bool sign); + IrU32 readU16Factor(const RdnaOperand& operand, bool highHalf, bool negate); + bool vMulLoU16(const RdnaInstruction& inst); + bool vPkMulLoU16(const RdnaInstruction& inst); bool integer16Mad(const RdnaInstruction& inst, bool sign, bool wide); IrU32 saturateU16Result(const RdnaInstruction& inst, const IrU32& value, bool sign); IrU32 wideAdd(const RdnaInstruction& inst, const IrU32& low, const IrU32& high, const IrU32& addend, bool sign); diff --git a/core/shader/recompiler/Translation/src/IntegerInstructions.cpp b/core/shader/recompiler/Translation/src/IntegerInstructions.cpp index d1af902f6c..353a6b2fc3 100644 --- a/core/shader/recompiler/Translation/src/IntegerInstructions.cpp +++ b/core/shader/recompiler/Translation/src/IntegerInstructions.cpp @@ -25,6 +25,36 @@ bool TranslationContext::integer16Binary(const RdnaInstruction& inst, IrOpcode o return true; } +IrU32 TranslationContext::readU16Factor(const RdnaOperand& operand, bool highHalf, bool negate) { + const IrU32 raw = applyBitSourceModifiers(operand, readRawU32(operand)); + const IrU32 field(ir.Emit(IrOpcode::BitFieldUExtract, IrType::U32, {&raw.Value(), &ir.Constant(highHalf ? 16u : 0u), &ir.Constant(highHalf ? 16u : 24u)})); + return negate ? IrU32(ir.BitwiseXor(field.Value(), ir.Constant(0x8000u))) : field; +} + +bool TranslationContext::vMulLoU16(const RdnaInstruction& inst) { + const RdnaOperand& lhs = sourceAt(inst, 0u); + const RdnaOperand& rhs = sourceAt(inst, 1u); + const IrU32 left = readU16Factor(lhs, lhs.opSel, false); + const IrU32 right = readU16Factor(rhs, rhs.opSel, false); + write16Bits(inst.destination, IrU32(ir.BitwiseAnd(ir.IMul(left.Value(), right.Value()), ir.Constant(0xffffu)))); + return true; +} + +bool TranslationContext::vPkMulLoU16(const RdnaInstruction& inst) { + const RdnaOperand& lhs = sourceAt(inst, 0u); + const RdnaOperand& rhs = sourceAt(inst, 1u); + const auto lane = [&](bool highLane) { + const IrU32 left = readU16Factor(lhs, highLane ? lhs.opSelHi : lhs.opSel, highLane ? lhs.negateHi : lhs.negate); + const IrU32 right = readU16Factor(rhs, highLane ? rhs.opSelHi : rhs.opSel, highLane ? rhs.negateHi : rhs.negate); + return IrU32(ir.IMul(left.Value(), right.Value())); + }; + const IrU32 low = lane(false); + const IrU32 high = lane(true); + const IrU32 result = packU16Lanes(low, high); + writeOperand(inst.destination, &result.Value()); + return true; +} + IrU32 TranslationContext::saturateU16Result(const RdnaInstruction& inst, const IrU32& value, bool sign) { if (!inst.destination.clamp) { return value; diff --git a/core/shader/recompiler/Translation/src/VectorInstructions.cpp b/core/shader/recompiler/Translation/src/VectorInstructions.cpp index bba0a9020a..db326fa784 100644 --- a/core/shader/recompiler/Translation/src/VectorInstructions.cpp +++ b/core/shader/recompiler/Translation/src/VectorInstructions.cpp @@ -856,7 +856,7 @@ bool TranslationContext::emitVector(const RdnaInstruction& inst) { case RdnaOpcode::VPkMadU16: return packedInteger16Mad(inst, false); case RdnaOpcode::VPkMulLoU16: - return packedInteger16Binary(inst, IrOpcode::IMul32, false); + return vPkMulLoU16(inst); case RdnaOpcode::VPkAddI16: return packedInteger16Binary(inst, IrOpcode::IAdd32, true); case RdnaOpcode::VPkAddU16: @@ -1141,7 +1141,7 @@ bool TranslationContext::emitVector(const RdnaInstruction& inst) { case RdnaOpcode::VSubNcI32: return vAddSubNcI32(inst, true); case RdnaOpcode::VMulLoU16: - return integer16Binary(inst, IrOpcode::IMul32, false); + return vMulLoU16(inst); case RdnaOpcode::VMadU16: return integer16Mad(inst, false, false); case RdnaOpcode::VMadI16: diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index 5b449de647..657446faf1 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -101,6 +101,7 @@ - [v_ldexp_f32 in denormal modes 1 and 2](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) follows the `FLOAT_MODE` f32 denormal field: mode 1 keeps a subnormal input and flushes a result below 2^-126 to a zero of its sign in every f32 round mode, also one that rounds to 2^-126 at subnormal precision (`0x3fffffff` scaled by 2^-127); mode 2 flushes a subnormal input to a zero of its sign and rounds a subnormal result in the f32 round mode of `FLOAT_MODE`, as mode 3 does. Measured on RDNA2 (gfx1030, Radeon RX 6800, AMD Windows driver 32.0.21045.5002 through HIP) over 10,260 rows (54 inputs of both signs: zeros, subnormals, normals near 2^-126 and 1.0, the largest finite value, infinities and NaNs, each scaled by 190 exponents from -2^31 to 2^31-1) at IEEE=0 and IEEE=1 with each f32 denormal mode 0 to 3 and f32 round mode 0 to 3, DX10_CLAMP=1, f16 denormals kept, f16 round to nearest even and FP16_OVFL=0; with the `clamp` modifier the result is clamped as in the other modes. The PS5's gfx1013 is not measured. - [v_log_f32 and v_log_f16](../../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) keep the driver's log2 unless, for inputs within 1/8 of 1.0, it is more than 4 ULP from an eight-term series in x - 1, which then replaces it. `v_log_f16` goes through the same f32 lowering and is rounded to half. The series is evaluated for every `v_log` and chosen with `OpSelect` rather than a branch, so its seven fused multiply-adds and two multiplies are an unconditional cost. Measured on RDNA2 over all 3,145,728 f32 inputs within 1/8 of 1.0 (gfx1035, IEEE 0, f32 denormals flushed), the hardware is within 1 ULP of the correctly rounded log2 and the series within 2 ULP of it and within 3 ULP of the hardware (2 except at 0x3f752e47, 0x3f7baab0 and 0x3f7bc4fb), so a kept driver value is within 7 ULP of the hardware and a driver that returns the hardware's bits never triggers the gate; RADV (Mesa 26.2.4) returns the hardware's bits on the 9,280 such inputs measured on gfx1036; NVIDIA (615.71.09) was up to 23,947,836 ULP off at 1 - 2^-24 and is within 6 ULP inside the band after the change; outside it its own log2 stays up to 9 ULP off (measured up to 1.375). Over every f16 input, the change moves one `v_log_f16` result on NVIDIA (0x3bed, 1 ULP, onto the hardware's bits) and none on RADV. - [Ordered not-equal, less-than and greater-than compares](../../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) (shader recompiler) - `FPOrdNotEqual32` is emitted as `OpFUnordNotEqual`, and `FPOrdLessThan32` and `FPOrdGreaterThan32` as their ordered forms, each with both operands checked for NaN on their bits; the results are the same for every input. The AMD Windows driver 32.0.13058.8001 (Radeon 860M, RDNA 3.5) returns 1 for a NaN operand when any float form of not-equal feeds a ballot on a 32-wide subgroup (#825), and so does a less-than or greater-than against an infinity the driver sees as a constant (#3536), presumably rewritten into a not-equal by its compiler +- [16-bit integer multiplies](../../../core/shader/recompiler/Translation/src/IntegerInstructions.cpp) (`v_mul_lo_u16`, `v_pk_mul_lo_u16`) (shader recompiler) - a factor taken from the low half of its source is read as the low 24 bits of the register, not the low 16; the low 16 bits of the product are the same for every input. The AMD Windows driver 32.0.21045.5002 (RX 6700 XT, RDNA 2) stores the full 32-bit product when two values it knows fit in 16 bits are multiplied, the product is reduced to 16 bits and nothing else is merged into the stored value: `v_mul_lo_u16` into a register whose high half is zero, also with factors masked to 16 bits by the program, and `v_pk_mul_lo_u16` whose high lanes are zero wrote 0xfffe0001 for 0xffff * 0xffff where the GPU writes 0x00000001. Reducing the product with `v_and_b32`, `v_bfe_u32` or a shift pair loses the same bits; factors read as 24-bit fields do not. Measured against the same instructions run through the [hardware oracle](../HW_ORACLE.md) on that GPU; other drivers are not checked - [f32 compares in denormal modes 1 and 2](../../../core/shader/recompiler/Translation/src/VectorInstructions.cpp) (`v_cmp_X_f32` and `v_cmpx_X_f32` with X = eq, ge, gt, le, lg, lt, neq, nge, ngt, nle, nlg, nlt) flush their f32 denormal inputs by bit 0 of the `FLOAT_MODE` f32 denormal field: mode 1 keeps them as mode 3 does, and mode 2 flushes them as mode 0 does. Measured on RDNA2 (gfx1030, Radeon RX 6800, AMD Windows driver 32.0.21045.5002 through HIP) over 484 operand pairs of zeros, denormals, minimum normals, ones, the largest finite value, infinities and quiet and signaling NaNs of both signs, in VOPC form, at IEEE=0 and IEEE=1 with each f32 denormal mode 0 to 3, f32 round to nearest even, DX10_CLAMP=1, f16 denormals kept, f16 round to nearest even and FP16_OVFL=0. The PS5's gfx1013 is not measured. - [DB_SHADER_CONTROL.PRIMITIVE_ORDERED_PIXEL_SHADER](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) (shader recompiler) - the host pixel interlock spans the whole shader, not only up to `s_sendmsg` ORDERED_PS_DONE. - [POPS collision SGPRs](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) (shader recompiler) - the collision wave ID and intrawave collision mask read 0, so a title skips its POPS wait and runs its per-collision loop once. From 28acd0997730c93f369dd1bf8ef4eb0d4873feac Mon Sep 17 00:00:00 2001 From: milescoviello Date: Sat, 17 Oct 2026 06:01:09 +0000 Subject: [PATCH 0644/1000] fix(libkernel): return EINVAL from read and write for a byte count above SSIZE_MAX FreeBSD's read, write, pread and pwrite reject a byte count above IOSIZE_MAX with EINVAL before they look up the descriptor. write threw std::invalid_argument for a negative count, and the others passed the count on to the host, which reports EFAULT on Linux. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libkernel/File/src/Stdio.cpp | 13 ++++++++++--- core/libs/tests/GuestPosixScalarIo.cpp | 12 ++++++++++++ 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index 929b8eeb33..caec37ef14 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -416,6 +416,10 @@ static int PosixFailure(int error) { return -1; } +static bool ExceedsTransferLimit(std::uint64_t bytes) { + return bytes > static_cast(std::numeric_limits::max()); +} + static int PosixResult(int result) { return result < 0 ? PosixFailure(result & 0xffff) : result; } @@ -841,6 +845,7 @@ int APS5_VABI _open_nid_postfix(const char* path, int flags, ...) { } int64_t APS5_VABI pread_nid_postfix(int d, void* buf, size_t nbytes, int64_t offset) { + if (ExceedsTransferLimit(nbytes)) return PosixFailure(GUEST_EINVAL); if (buf == nullptr && nbytes != 0) return PosixFailure(GUEST_EFAULT); if (offset < 0) return PosixFailure(GUEST_EINVAL); if (FailPosixPositionedGuestSocket(d)) return -1; @@ -854,6 +859,7 @@ int64_t APS5_VABI pread_nid_postfix(int d, void* buf, size_t nbytes, int64_t off } int64_t APS5_VABI pwrite_nid_disambig1_nid_postfix(int d, const void* buf, size_t nbytes, int64_t offset) { + if (ExceedsTransferLimit(nbytes)) return PosixFailure(GUEST_EINVAL); if (buf == nullptr && nbytes != 0) return PosixFailure(GUEST_EFAULT); if (offset < 0) return PosixFailure(GUEST_EINVAL); if (FailPosixPositionedGuestSocket(d)) return -1; @@ -863,6 +869,7 @@ int64_t APS5_VABI pwrite_nid_disambig1_nid_postfix(int d, const void* buf, size_ } int64_t APS5_VABI read_nid_postfix(int d, void* buf, uint64_t nbytes) { + if (ExceedsTransferLimit(nbytes)) return PosixFailure(GUEST_EINVAL); if (GuestSockets::IsOpen(d)) return GuestSockets::Read(d, buf, static_cast(nbytes)); if (buf == nullptr && nbytes != 0) return PosixFailure(GUEST_EFAULT); char emptyBuffer = 0; @@ -870,6 +877,7 @@ int64_t APS5_VABI read_nid_postfix(int d, void* buf, uint64_t nbytes) { } std::int64_t APS5_VABI _read_nid_postfix(int descriptor, void* buffer, std::size_t count) { + if (ExceedsTransferLimit(count)) return PosixFailure(GUEST_EINVAL); if (GuestSockets::IsOpen(descriptor)) return GuestSockets::Read(descriptor, buffer, count); if (buffer == nullptr && count != 0) return PosixFailure(GUEST_EFAULT); char emptyBuffer = 0; @@ -877,9 +885,7 @@ std::int64_t APS5_VABI _read_nid_postfix(int descriptor, void* buffer, std::size } int64_t APS5_VABI write_nid_postfix(int d, const char* str, int64_t size) { - if (size < 0) { - APS5_INVALID_ARG_EX; - } + if (size < 0) return PosixFailure(GUEST_EINVAL); if (GuestSockets::IsOpen(d)) return GuestSockets::Write(d, str, static_cast(size)); if (str == nullptr && size != 0) return PosixFailure(GUEST_EFAULT); const char emptyBuffer = 0; @@ -887,6 +893,7 @@ int64_t APS5_VABI write_nid_postfix(int d, const char* str, int64_t size) { } std::int64_t APS5_VABI _write_nid_postfix(int descriptor, const void* buffer, std::size_t count) { + if (ExceedsTransferLimit(count)) return PosixFailure(GUEST_EINVAL); if (GuestSockets::IsOpen(descriptor)) return GuestSockets::Write(descriptor, buffer, count); if (buffer == nullptr && count != 0) return PosixFailure(GUEST_EFAULT); const char emptyBuffer = 0; diff --git a/core/libs/tests/GuestPosixScalarIo.cpp b/core/libs/tests/GuestPosixScalarIo.cpp index 177845674a..c1c48481ac 100644 --- a/core/libs/tests/GuestPosixScalarIo.cpp +++ b/core/libs/tests/GuestPosixScalarIo.cpp @@ -30,6 +30,8 @@ std::int64_t APS5_VABI write_nid_postfix(int, const char*, std::int64_t); std::int64_t APS5_VABI _read_nid_postfix(int, void*, std::size_t); std::int64_t APS5_VABI _write_nid_postfix(int, const void*, std::size_t); std::int64_t APS5_VABI lseek_nid_postfix(int, std::int64_t, int); +std::int64_t APS5_VABI pread_nid_postfix(int, void*, std::size_t, std::int64_t); +std::int64_t APS5_VABI pwrite_nid_disambig1_nid_postfix(int, const void*, std::size_t, std::int64_t); std::int64_t APS5_VABI sceKernelRead(int, void*, std::size_t); std::int64_t APS5_VABI sceKernelWrite(int, const void*, std::size_t); std::int64_t APS5_VABI sceKernelLseek(int, std::int64_t, int); @@ -121,6 +123,16 @@ int main() { RequirePosixFailure([&] { return _read_nid_postfix(-1, nullptr, 0); }, EBADF); RequirePosixFailure([&] { return write_nid_postfix(-1, nullptr, 0); }, EBADF); RequirePosixFailure([&] { return _write_nid_postfix(-1, nullptr, 0); }, EBADF); + constexpr auto oversized = static_cast(std::numeric_limits::max()) + 1; + RequirePosixFailure([&] { return read_nid_postfix(descriptor, buffer, oversized); }, EINVAL); + RequirePosixFailure([&] { return _read_nid_postfix(descriptor, buffer, oversized); }, EINVAL); + RequirePosixFailure([&] { return write_nid_postfix(descriptor, payload, -1); }, EINVAL); + RequirePosixFailure([&] { return write_nid_postfix(descriptor, payload, std::numeric_limits::min()); }, EINVAL); + RequirePosixFailure([&] { return _write_nid_postfix(descriptor, payload, oversized); }, EINVAL); + RequirePosixFailure([&] { return pread_nid_postfix(descriptor, buffer, oversized, 0); }, EINVAL); + RequirePosixFailure([&] { return pwrite_nid_disambig1_nid_postfix(descriptor, payload, oversized, 0); }, EINVAL); + RequirePosixFailure([&] { return read_nid_postfix(-1, buffer, oversized); }, EINVAL); + RequirePosixFailure([&] { return write_nid_postfix(-1, payload, -1); }, EINVAL); #ifdef _WIN32 errno = EACCES; bool limitDiagnosticPreserved = false; From b693ef234010cefd046c3ff8e23c7c60df86a64b Mon Sep 17 00:00:00 2001 From: milescoviello Date: Sat, 17 Oct 2026 06:05:43 +0000 Subject: [PATCH 0645/1000] fix(libkernel): return ENOTSUP from pthread_attr_setschedparam and setschedpolicy like libthr FreeBSD's libthr rejects a null sched_param and, under SCHED_FIFO or SCHED_RR, a priority outside sched_get_priority_min..max with ENOTSUP, and a policy outside SCHED_FIFO..SCHED_RR with ENOTSUP. The wrappers returned EINVAL for the null parameter and stored any priority or policy. Co-Authored-By: Claude Opus 5.5 --- .../libs/prx/libkernel/Pthread/Posix/Attr.cpp | 13 +++++- core/libs/tests/GuestPthreadAttr.cpp | 40 +++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libkernel/Pthread/Posix/Attr.cpp b/core/libs/prx/libkernel/Pthread/Posix/Attr.cpp index ea6ce0c0b1..1ea13b5de0 100644 --- a/core/libs/prx/libkernel/Pthread/Posix/Attr.cpp +++ b/core/libs/prx/libkernel/Pthread/Posix/Attr.cpp @@ -13,8 +13,13 @@ int APS5_VABI scePthreadAttrDestroy(PthreadAttr* attr); int APS5_VABI scePthreadAttrSetstacksize(PthreadAttr* attr, std::size_t stacksize); int APS5_VABI scePthreadAttrSetstack(PthreadAttr* attr, void* addr, std::size_t size); int APS5_VABI scePthreadAttrSetsolosched(PthreadAttr* attr, int solosched); +int APS5_VABI sched_get_priority_max_nid_postfix(int policy); +int APS5_VABI sched_get_priority_min_nid_postfix(int policy); } +static constexpr int GuestSchedFifo = 1; +static constexpr int GuestSchedRr = 3; + static bool Valid(const PthreadAttr* attr) { return attr && *attr; } @@ -120,13 +125,19 @@ int APS5_VABI pthread_attr_setinheritsched_nid_postfix(PthreadAttr* attr, int in } int APS5_VABI pthread_attr_setschedparam_nid_postfix(PthreadAttr* attr, const KernelSchedParam* param) { - if (!Valid(attr) || !param) return PosixThread::GUEST_EINVAL; + if (!Valid(attr)) return PosixThread::GUEST_EINVAL; + if (!param) return PosixThread::GUEST_ENOTSUP; + const int policy = (*attr)->_schedpolicy; + if ((policy == GuestSchedFifo || policy == GuestSchedRr) && + (param->sched_priority < sched_get_priority_min_nid_postfix(policy) || param->sched_priority > sched_get_priority_max_nid_postfix(policy))) + return PosixThread::GUEST_ENOTSUP; (*attr)->_schedpriority = param->sched_priority; return 0; } int APS5_VABI pthread_attr_setschedpolicy_nid_postfix(PthreadAttr* attr, int policy) { if (!Valid(attr)) return PosixThread::GUEST_EINVAL; + if (policy < GuestSchedFifo || policy > GuestSchedRr) return PosixThread::GUEST_ENOTSUP; (*attr)->_schedpolicy = policy; return 0; } diff --git a/core/libs/tests/GuestPthreadAttr.cpp b/core/libs/tests/GuestPthreadAttr.cpp index 12c5f1b9b1..0b28513f9d 100644 --- a/core/libs/tests/GuestPthreadAttr.cpp +++ b/core/libs/tests/GuestPthreadAttr.cpp @@ -27,6 +27,9 @@ int APS5_VABI pthread_attr_setinheritsched_nid_postfix(PthreadAttr* attr, int in int APS5_VABI pthread_attr_getscope_nid_postfix(const PthreadAttr* attr, int* scope); int APS5_VABI pthread_attr_setscope_nid_postfix(PthreadAttr* attr, int scope); int APS5_VABI pthread_attr_get_np_nid_postfix(Pthread thread, PthreadAttr* attr); +int APS5_VABI pthread_attr_setschedparam_nid_postfix(PthreadAttr* attr, const KernelSchedParam* param); +int APS5_VABI pthread_attr_setschedpolicy_nid_postfix(PthreadAttr* attr, int policy); +int APS5_VABI pthread_attr_getschedpolicy_nid_postfix(const PthreadAttr* attr, int* policy); } static constexpr int SCE_OK = 0; @@ -116,6 +119,42 @@ static void CheckSoloSched() { Require(pthread_attr_setsolosched_np_nid_postfix(&attr, 1) == GUEST_EINVAL); } +static void CheckSchedArguments() { + constexpr int SchedFifo = 1; + constexpr int SchedOther = 2; + constexpr int SchedRr = 3; + PthreadAttr attr = nullptr; + PthreadAttr unset = nullptr; + KernelSchedParam param{CREATION_PRIORITY}; + Require(pthread_attr_setschedparam_nid_postfix(nullptr, ¶m) == GUEST_EINVAL); + Require(pthread_attr_setschedparam_nid_postfix(&unset, ¶m) == GUEST_EINVAL); + Require(pthread_attr_setschedpolicy_nid_postfix(&unset, SchedRr) == GUEST_EINVAL); + Require(scePthreadAttrInit(&attr) == SCE_OK); + Require(pthread_attr_setschedparam_nid_postfix(&attr, nullptr) == GUEST_ENOTSUP); + for (const int policy : {SchedFifo, SchedRr}) { + Require(pthread_attr_setschedpolicy_nid_postfix(&attr, policy) == 0); + Require(pthread_attr_setschedparam_nid_postfix(&attr, ¶m) == 0); + for (const int rejected : {0, 1, CREATION_PRIORITY - 1, UPDATED_PRIORITY + 1, 1000}) { + KernelSchedParam outside{rejected}; + Require(pthread_attr_setschedparam_nid_postfix(&attr, &outside) == GUEST_ENOTSUP); + KernelSchedParam reported{-1}; + Require(scePthreadAttrGetschedparam(&attr, &reported) == SCE_OK && reported.sched_priority == CREATION_PRIORITY); + } + KernelSchedParam highest{UPDATED_PRIORITY}; + Require(pthread_attr_setschedparam_nid_postfix(&attr, &highest) == 0); + Require(pthread_attr_setschedparam_nid_postfix(&attr, ¶m) == 0); + } + Require(pthread_attr_setschedpolicy_nid_postfix(&attr, SchedOther) == 0); + KernelSchedParam other{1000}; + Require(pthread_attr_setschedparam_nid_postfix(&attr, &other) == 0); + int policy = -1; + for (const int rejected : {-1, 0, 4, 100}) { + Require(pthread_attr_setschedpolicy_nid_postfix(&attr, rejected) == GUEST_ENOTSUP); + Require(pthread_attr_getschedpolicy_nid_postfix(&attr, &policy) == 0 && policy == SchedOther); + } + Require(scePthreadAttrDestroy(&attr) == SCE_OK); +} + static void* APS5_VABI Worker(void* arg) { static_cast*>(arg)->get(); return nullptr; @@ -220,6 +259,7 @@ static void CheckThreadScope() { int main() { CheckStackSizeLimit(); CheckSoloSched(); + CheckSchedArguments(); CheckSchedulingAttributes(); CheckThreadScope(); PthreadAttr attr = nullptr; From 01c8cebf8c30119abe3593e7e4bead35f1a5af4b Mon Sep 17 00:00:00 2001 From: milescoviello Date: Sat, 17 Oct 2026 05:39:37 +0000 Subject: [PATCH 0646/1000] fix(libkernel): return EINVAL/EFAULT from getrusage instead of throwing FreeBSD's getrusage rejects an unknown `who` with EINVAL before it copies anything out and returns EFAULT when the result can't be written. The HLE threw std::invalid_argument for both, which ends the title. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libkernel/System/src/Process.cpp | 12 ++++++++---- core/libs/tests/GuestGetrusage.cpp | 16 ++++++++++++++++ 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libkernel/System/src/Process.cpp b/core/libs/prx/libkernel/System/src/Process.cpp index bcfac7bc1a..01c4a2b11d 100644 --- a/core/libs/prx/libkernel/System/src/Process.cpp +++ b/core/libs/prx/libkernel/System/src/Process.cpp @@ -466,14 +466,18 @@ int APS5_VABI sched_get_priority_min_nid_postfix(int policy) { } int APS5_VABI getrusage_nid_postfix(int who, GuestResourceUsage* usage) { - if (usage == nullptr) - throw std::invalid_argument("getrusage: usage is null"); + if (who != -1 && who != 0 && who != 1) { + *__error_nid_postfix() = guestInvalid; + return -1; + } + if (usage == nullptr) { + *__error_nid_postfix() = guestFault; + return -1; + } if (who == -1) { *usage = {}; return 0; } - if (who != 0 && who != 1) - throw std::invalid_argument("getrusage: unsupported who"); #ifdef _WIN32 FILETIME creation{}; FILETIME exitTime{}; diff --git a/core/libs/tests/GuestGetrusage.cpp b/core/libs/tests/GuestGetrusage.cpp index da998f3e4b..7fefbdca94 100644 --- a/core/libs/tests/GuestGetrusage.cpp +++ b/core/libs/tests/GuestGetrusage.cpp @@ -14,6 +14,7 @@ struct ResourceUsage { std::int64_t rest[14]; }; extern "C" int APS5_VABI getrusage_nid_postfix(int who, ResourceUsage* usage); +extern "C" int* APS5_VABI __error_nid_postfix(); static void Require(bool value) { if (!value) std::abort(); } static std::int64_t CpuMicros(int who) { ResourceUsage usage{}; @@ -28,7 +29,22 @@ static void SpinUntil(int who, std::int64_t micros) { Require(std::chrono::steady_clock::now() < deadline); } } +static void RequireFailure(int who, ResourceUsage* usage, int error) { + *__error_nid_postfix() = 0; + Require(getrusage_nid_postfix(who, usage) == -1); + Require(*__error_nid_postfix() == error); +} int main() { + ResourceUsage untouched; + std::memset(&untouched, 0x5a, sizeof(untouched)); + const ResourceUsage pattern = untouched; + RequireFailure(2, &untouched, 22); + RequireFailure(-2, &untouched, 22); + Require(std::memcmp(&untouched, &pattern, sizeof(untouched)) == 0); + RequireFailure(7, nullptr, 22); + RequireFailure(0, nullptr, 14); + RequireFailure(1, nullptr, 14); + RequireFailure(-1, nullptr, 14); ResourceUsage children; std::memset(&children, 0xff, sizeof(children)); Require(getrusage_nid_postfix(-1, &children) == 0); From 4a235b43588ed2dc23f08a0b1cbe0364a908549f Mon Sep 17 00:00:00 2001 From: thuanquang Date: Sat, 17 Oct 2026 13:11:24 +0700 Subject: [PATCH 0647/1000] fix(shader): flush f16 compare inputs by the FLOAT_MODE f16 denormal field v_cmp_X_f16 and v_cmpx_X_f16 compared denormal halves as their exact values in every mode. On RDNA2 (gfx1031) f16 denormal modes 0 and 2 flush both inputs to a zero of the same sign first, so v_cmp_eq_f16 0x0000, 0x0001 is true there. Both inputs now go through flushF16InputValue; modes 1 and 3 and shaders without a known float mode are translated as before. Co-Authored-By: Claude Opus 5.5 --- .../tests/execution/VCmpF16DenormalModes.cpp | 183 ++++++++++++++++++ .../Translation/src/CompareInstructions.cpp | 4 +- docs/dev/debt/shader.md | 1 + 3 files changed, 186 insertions(+), 2 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/VCmpF16DenormalModes.cpp diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/VCmpF16DenormalModes.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/VCmpF16DenormalModes.cpp new file mode 100644 index 0000000000..ebc36d2016 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/VCmpF16DenormalModes.cpp @@ -0,0 +1,183 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t ValueCount = 16; +constexpr std::uint32_t Threads = ValueCount * ValueCount; +constexpr std::uint32_t Groups = Threads / 32; +constexpr std::uint32_t Inputs = 4; +constexpr std::uint32_t Results = 16; +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; + +constexpr std::uint32_t Values[ValueCount] = { + 0x0000u, 0x8000u, 0x0001u, 0x8001u, 0x03ffu, 0x83ffu, 0x0400u, 0x8400u, 0x3c00u, 0xbc00u, 0x7c00u, 0xfc00u, 0x7e00u, 0x7c01u, 0xfe00u, 0x7bffu, +}; + +alignas(256) constexpr std::array CompareCode{ + 0x34020084u, 0x34040086u, 0xe0341000u, 0x80000401u, 0xbf8c3f70u, 0x7d940b04u, 0xd501000au, 0x01a90280u, + 0x7d9c0b04u, 0xd501000bu, 0x01a90280u, 0x7d980b04u, 0xd501000cu, 0x01a90280u, 0x7d960b04u, 0xd501000du, + 0x01a90280u, 0x7d9a0b04u, 0xd501000eu, 0x01a90280u, 0x7d920b04u, 0xd501000fu, 0x01a90280u, 0x7dda0b04u, + 0xd5010010u, 0x01a90280u, 0x7dd20b04u, 0xd5010011u, 0x01a90280u, 0x7dd60b04u, 0xd5010012u, 0x01a90280u, + 0x7dd80b04u, 0xd5010013u, 0x01a90280u, 0x7dd40b04u, 0xd5010014u, 0x01a90280u, 0x7ddc0b04u, 0xd5010015u, + 0x01a90280u, 0xe0781000u, 0x80010a02u, 0xe0781010u, 0x80010e02u, 0xe0781020u, 0x80011202u, 0xbf810000u, +}; + +alignas(256) constexpr std::array CompareExecCode{ + 0x34020084u, 0x34040086u, 0xe0341000u, 0x80000401u, 0xbf8c3f70u, 0xbe94037eu, 0x7db40b04u, 0xbe95037eu, + 0xbefe0314u, 0xd501000au, 0x00550280u, 0xbe94037eu, 0x7dbc0b04u, 0xbe95037eu, 0xbefe0314u, 0xd501000bu, + 0x00550280u, 0xbe94037eu, 0x7db80b04u, 0xbe95037eu, 0xbefe0314u, 0xd501000cu, 0x00550280u, 0xbe94037eu, + 0x7db60b04u, 0xbe95037eu, 0xbefe0314u, 0xd501000du, 0x00550280u, 0xbe94037eu, 0x7dba0b04u, 0xbe95037eu, + 0xbefe0314u, 0xd501000eu, 0x00550280u, 0xbe94037eu, 0x7db20b04u, 0xbe95037eu, 0xbefe0314u, 0xd501000fu, + 0x00550280u, 0xbe94037eu, 0x7dfa0b04u, 0xbe95037eu, 0xbefe0314u, 0xd5010010u, 0x00550280u, 0xbe94037eu, + 0x7df20b04u, 0xbe95037eu, 0xbefe0314u, 0xd5010011u, 0x00550280u, 0xbe94037eu, 0x7df60b04u, 0xbe95037eu, + 0xbefe0314u, 0xd5010012u, 0x00550280u, 0xbe94037eu, 0x7df80b04u, 0xbe95037eu, 0xbefe0314u, 0xd5010013u, + 0x00550280u, 0xbe94037eu, 0x7df40b04u, 0xbe95037eu, 0xbefe0314u, 0xd5010014u, 0x00550280u, 0xbe94037eu, + 0x7dfc0b04u, 0xbe95037eu, 0xbefe0314u, 0xd5010015u, 0x00550280u, 0xe0781000u, 0x80010a02u, 0xe0781010u, + 0x80010e02u, 0xe0781020u, 0x80011202u, 0xbf810000u, +}; + +struct Row { + const char* condition; + std::uint32_t inputsPreserved[Groups]; + std::uint32_t inputsFlushed[Groups]; +}; + +constexpr Row Rows[] = { + {"eq", + {0x00030003u, 0x00080004u, 0x00200010u, 0x00800040u, 0x02000100u, 0x08000400u, 0x00000000u, 0x80000000u}, + {0x003f003fu, 0x003f003fu, 0x003f003fu, 0x00800040u, 0x02000100u, 0x08000400u, 0x00000000u, 0x80000000u}}, + {"ge", + {0x0aab0aabu, 0x0aa80aafu, 0x0aa00abfu, 0x0a800affu, 0x0a000bffu, 0x08008fffu, 0x00000000u, 0x8bff0000u}, + {0x0abf0abfu, 0x0abf0abfu, 0x0abf0abfu, 0x0a800affu, 0x0a000bffu, 0x08008fffu, 0x00000000u, 0x8bff0000u}}, + {"gt", + {0x0aa80aa8u, 0x0aa00aabu, 0x0a800aafu, 0x0a000abfu, 0x08000affu, 0x00008bffu, 0x00000000u, 0x0bff0000u}, + {0x0a800a80u, 0x0a800a80u, 0x0a800a80u, 0x0a000abfu, 0x08000affu, 0x00008bffu, 0x00000000u, 0x0bff0000u}}, + {"le", + {0x85578557u, 0x855f8554u, 0x857f8550u, 0x85ff8540u, 0x87ff8500u, 0x8fff0400u, 0x00000000u, 0x84000000u}, + {0x857f857fu, 0x857f857fu, 0x857f857fu, 0x85ff8540u, 0x87ff8500u, 0x8fff0400u, 0x00000000u, 0x84000000u}}, + {"lg", + {0x8ffc8ffcu, 0x8ff78ffbu, 0x8fdf8fefu, 0x8f7f8fbfu, 0x8dff8effu, 0x87ff8bffu, 0x00000000u, 0x0fff0000u}, + {0x8fc08fc0u, 0x8fc08fc0u, 0x8fc08fc0u, 0x8f7f8fbfu, 0x8dff8effu, 0x87ff8bffu, 0x00000000u, 0x0fff0000u}}, + {"lt", + {0x85548554u, 0x85578550u, 0x855f8540u, 0x857f8500u, 0x85ff8400u, 0x87ff0000u, 0x00000000u, 0x04000000u}, + {0x85408540u, 0x85408540u, 0x85408540u, 0x857f8500u, 0x85ff8400u, 0x87ff0000u, 0x00000000u, 0x04000000u}}, + {"neq", + {0xfffcfffcu, 0xfff7fffbu, 0xffdfffefu, 0xff7fffbfu, 0xfdfffeffu, 0xf7fffbffu, 0xffffffffu, 0x7fffffffu}, + {0xffc0ffc0u, 0xffc0ffc0u, 0xffc0ffc0u, 0xff7fffbfu, 0xfdfffeffu, 0xf7fffbffu, 0xffffffffu, 0x7fffffffu}}, + {"nge", + {0xf554f554u, 0xf557f550u, 0xf55ff540u, 0xf57ff500u, 0xf5fff400u, 0xf7ff7000u, 0xffffffffu, 0x7400ffffu}, + {0xf540f540u, 0xf540f540u, 0xf540f540u, 0xf57ff500u, 0xf5fff400u, 0xf7ff7000u, 0xffffffffu, 0x7400ffffu}}, + {"ngt", + {0xf557f557u, 0xf55ff554u, 0xf57ff550u, 0xf5fff540u, 0xf7fff500u, 0xffff7400u, 0xffffffffu, 0xf400ffffu}, + {0xf57ff57fu, 0xf57ff57fu, 0xf57ff57fu, 0xf5fff540u, 0xf7fff500u, 0xffff7400u, 0xffffffffu, 0xf400ffffu}}, + {"nle", + {0x7aa87aa8u, 0x7aa07aabu, 0x7a807aafu, 0x7a007abfu, 0x78007affu, 0x7000fbffu, 0xffffffffu, 0x7bffffffu}, + {0x7a807a80u, 0x7a807a80u, 0x7a807a80u, 0x7a007abfu, 0x78007affu, 0x7000fbffu, 0xffffffffu, 0x7bffffffu}}, + {"nlg", + {0x70037003u, 0x70087004u, 0x70207010u, 0x70807040u, 0x72007100u, 0x78007400u, 0xffffffffu, 0xf000ffffu}, + {0x703f703fu, 0x703f703fu, 0x703f703fu, 0x70807040u, 0x72007100u, 0x78007400u, 0xffffffffu, 0xf000ffffu}}, + {"nlt", + {0x7aab7aabu, 0x7aa87aafu, 0x7aa07abfu, 0x7a807affu, 0x7a007bffu, 0x7800ffffu, 0xffffffffu, 0xfbffffffu}, + {0x7abf7abfu, 0x7abf7abfu, 0x7abf7abfu, 0x7a807affu, 0x7a007bffu, 0x7800ffffu, 0xffffffffu, 0xfbffffffu}}, +}; + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%04x", value); + return text; +} + +void Run(AgcDriver::VulkanDevice& device, std::span code, ShaderRecompiler::ShaderFloatMode mode) { + for (std::uint32_t pair = 0; pair < Threads; ++pair) { + Input[pair * Inputs] = Values[pair / ValueCount]; + Input[pair * Inputs + 1u] = Values[pair % ValueCount]; + Input[pair * Inputs + 2u] = 0u; + Input[pair * Inputs + 3u] = 0u; + } + Output.fill(0xdeadbeefu); + std::vector userData(8, 0u); + const auto input = BufferDescriptor(Input.data(), static_cast(Input.size() * 4u)); + const auto output = BufferDescriptor(Output.data(), static_cast(Output.size() * 4u)); + std::copy(input.begin(), input.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + request.context.floatMode = mode; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); +} + +void Check(const char* form, const char* modeName, bool flushed) { + for (std::uint32_t index = 0; index < std::size(Rows); ++index) { + const Row& row = Rows[index]; + for (std::uint32_t pair = 0; pair < Threads; ++pair) { + const std::uint32_t actual = Output[pair * Results + index]; + const std::uint32_t expected = ((flushed ? row.inputsFlushed : row.inputsPreserved)[pair / 32u] >> (pair % 32u)) & 1u; + Require(actual == expected, std::string(form) + row.condition + "_f16 " + modeName + " of " + Hex(Values[pair / ValueCount]) + " and " + + Hex(Values[pair % ValueCount]) + " is " + Hex(actual) + ", expected " + std::to_string(expected)); + } + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + struct Mode { + const char* name; + ShaderRecompiler::ShaderFloatMode mode; + bool flushed; + }; + const Mode modes[] = { + {"f16 denormal mode 0, IEEE mode on", ShaderRecompiler::ShaderFloatMode{0x30u, true, true, false}, true}, + {"f16 denormal mode 0, IEEE mode off", ShaderRecompiler::ShaderFloatMode{0x30u, true, false, false}, true}, + {"f16 denormal mode 1, IEEE mode on", ShaderRecompiler::ShaderFloatMode{0x70u, true, true, false}, false}, + {"f16 denormal mode 1, IEEE mode off", ShaderRecompiler::ShaderFloatMode{0x70u, true, false, false}, false}, + {"f16 denormal mode 2, IEEE mode on", ShaderRecompiler::ShaderFloatMode{0xb0u, true, true, false}, true}, + {"f16 denormal mode 2, IEEE mode off", ShaderRecompiler::ShaderFloatMode{0xb0u, true, false, false}, true}, + {"f16 denormal mode 3, IEEE mode on", ShaderRecompiler::ShaderFloatMode{0xf0u, true, true, false}, false}, + {"f16 denormal mode 3, IEEE mode off", ShaderRecompiler::ShaderFloatMode{0xf0u, true, false, false}, false}, + }; + for (const Mode& mode : modes) { + Run(*device, CompareCode, mode.mode); + Check("v_cmp_", mode.name, mode.flushed); + Run(*device, CompareExecCode, mode.mode); + Check("v_cmpx_", mode.name, mode.flushed); + } + std::puts("v_cmp and v_cmpx f16 denormal mode tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/shader/recompiler/Translation/src/CompareInstructions.cpp b/core/shader/recompiler/Translation/src/CompareInstructions.cpp index 50bdf556cc..9520a70caf 100644 --- a/core/shader/recompiler/Translation/src/CompareInstructions.cpp +++ b/core/shader/recompiler/Translation/src/CompareInstructions.cpp @@ -115,8 +115,8 @@ void TranslationContext::emitFloatCompare(const RdnaInstruction& inst, IrOpcode IrValue* lhs = nullptr; IrValue* rhs = nullptr; if (half) { - lhs = &readF16AsF32(sourceAt(inst, 0u)).Value(); - rhs = &readF16AsF32(sourceAt(inst, 1u)).Value(); + lhs = &flushF16InputValue(readF16AsF32(sourceAt(inst, 0u))).Value(); + rhs = &flushF16InputValue(readF16AsF32(sourceAt(inst, 1u))).Value(); } else { IrValue* lhsBits = readOperand(sourceAt(inst, 0u), IrType::U32); IrValue* rhsBits = readOperand(sourceAt(inst, 1u), IrType::U32); diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index 5b449de647..821f466664 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -102,6 +102,7 @@ - [v_log_f32 and v_log_f16](../../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) keep the driver's log2 unless, for inputs within 1/8 of 1.0, it is more than 4 ULP from an eight-term series in x - 1, which then replaces it. `v_log_f16` goes through the same f32 lowering and is rounded to half. The series is evaluated for every `v_log` and chosen with `OpSelect` rather than a branch, so its seven fused multiply-adds and two multiplies are an unconditional cost. Measured on RDNA2 over all 3,145,728 f32 inputs within 1/8 of 1.0 (gfx1035, IEEE 0, f32 denormals flushed), the hardware is within 1 ULP of the correctly rounded log2 and the series within 2 ULP of it and within 3 ULP of the hardware (2 except at 0x3f752e47, 0x3f7baab0 and 0x3f7bc4fb), so a kept driver value is within 7 ULP of the hardware and a driver that returns the hardware's bits never triggers the gate; RADV (Mesa 26.2.4) returns the hardware's bits on the 9,280 such inputs measured on gfx1036; NVIDIA (615.71.09) was up to 23,947,836 ULP off at 1 - 2^-24 and is within 6 ULP inside the band after the change; outside it its own log2 stays up to 9 ULP off (measured up to 1.375). Over every f16 input, the change moves one `v_log_f16` result on NVIDIA (0x3bed, 1 ULP, onto the hardware's bits) and none on RADV. - [Ordered not-equal, less-than and greater-than compares](../../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) (shader recompiler) - `FPOrdNotEqual32` is emitted as `OpFUnordNotEqual`, and `FPOrdLessThan32` and `FPOrdGreaterThan32` as their ordered forms, each with both operands checked for NaN on their bits; the results are the same for every input. The AMD Windows driver 32.0.13058.8001 (Radeon 860M, RDNA 3.5) returns 1 for a NaN operand when any float form of not-equal feeds a ballot on a 32-wide subgroup (#825), and so does a less-than or greater-than against an infinity the driver sees as a constant (#3536), presumably rewritten into a not-equal by its compiler - [f32 compares in denormal modes 1 and 2](../../../core/shader/recompiler/Translation/src/VectorInstructions.cpp) (`v_cmp_X_f32` and `v_cmpx_X_f32` with X = eq, ge, gt, le, lg, lt, neq, nge, ngt, nle, nlg, nlt) flush their f32 denormal inputs by bit 0 of the `FLOAT_MODE` f32 denormal field: mode 1 keeps them as mode 3 does, and mode 2 flushes them as mode 0 does. Measured on RDNA2 (gfx1030, Radeon RX 6800, AMD Windows driver 32.0.21045.5002 through HIP) over 484 operand pairs of zeros, denormals, minimum normals, ones, the largest finite value, infinities and quiet and signaling NaNs of both signs, in VOPC form, at IEEE=0 and IEEE=1 with each f32 denormal mode 0 to 3, f32 round to nearest even, DX10_CLAMP=1, f16 denormals kept, f16 round to nearest even and FP16_OVFL=0. The PS5's gfx1013 is not measured. +- [f16 compares](../../../core/shader/recompiler/Translation/src/CompareInstructions.cpp) (`v_cmp_X_f16` and `v_cmpx_X_f16` with X = eq, ge, gt, le, lg, lt, neq, nge, ngt, nle, nlg, nlt) flush their f16 denormal inputs to a zero of the same sign by bit 0 of the `FLOAT_MODE` f16 denormal field: modes 0 and 2 flush them, 1 and 3 keep them. Measured on RDNA2 (gfx1031, Radeon RX 6700 XT, AMD Windows driver 32.0.21045.5002 through HIP) over 256 operand pairs of zeros, denormals, minimum normals, ones, the largest finite value, infinities and quiet and signaling NaNs of both signs, in VOPC form, at IEEE=0 and IEEE=1 with each f16 denormal mode 0 to 3, f16 round to nearest even, DX10_CLAMP=1, f32 denormals kept and FP16_OVFL=0. Shaders without a known float mode keep the previous lowering. The PS5's gfx1013 is not measured. - [DB_SHADER_CONTROL.PRIMITIVE_ORDERED_PIXEL_SHADER](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) (shader recompiler) - the host pixel interlock spans the whole shader, not only up to `s_sendmsg` ORDERED_PS_DONE. - [POPS collision SGPRs](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) (shader recompiler) - the collision wave ID and intrawave collision mask read 0, so a title skips its POPS wait and runs its per-collision loop once. - [Image loads](../../../core/shader/recompiler/Optimization/src/ResourceTracker.cpp) (shader recompiler) - an image_load reads a sampled binding, also of an image the shader stores to, so in a primitive-ordered pixel shader it misses an earlier overlapping primitive's image_store in the same draw. From 9f704dc1eb8161d33ed622b4102f45ea6a566979 Mon Sep 17 00:00:00 2001 From: milescoviello Date: Sat, 17 Oct 2026 06:17:58 +0000 Subject: [PATCH 0648/1000] fix(libkernel): read ITIMER_VIRTUAL and ITIMER_PROF as disarmed in getitimer setitimer refuses to arm the CPU interval timers and already reads and disarms them as zero, so they are never armed. getitimer threw for them instead of reporting that state, as FreeBSD's kern_getitimer does for a timer that is not set. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libkernel/System/src/Signals.cpp | 5 ++++- core/libs/tests/GuestGetitimer.cpp | 17 +++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/core/libs/prx/libkernel/System/src/Signals.cpp b/core/libs/prx/libkernel/System/src/Signals.cpp index 23f4e7f977..2547ebc59c 100644 --- a/core/libs/prx/libkernel/System/src/Signals.cpp +++ b/core/libs/prx/libkernel/System/src/Signals.cpp @@ -359,11 +359,14 @@ extern "C" int APS5_VABI getitimer_nid_postfix(int which, GuestItimerval* output *__error_nid_postfix() = 22; return -1; } - if (which != 0) throw std::runtime_error("getitimer: CPU interval timers are not implemented"); if (output == nullptr) { *__error_nid_postfix() = 14; return -1; } + if (which != 0) { + *output = {}; + return 0; + } GuestItimerval result{}; auto& alarms = Alarms(); { diff --git a/core/libs/tests/GuestGetitimer.cpp b/core/libs/tests/GuestGetitimer.cpp index 57e484e580..04f037f499 100644 --- a/core/libs/tests/GuestGetitimer.cpp +++ b/core/libs/tests/GuestGetitimer.cpp @@ -8,7 +8,6 @@ #include #include #include -#include #include struct GuestItimerval { @@ -62,16 +61,14 @@ int main() { Require(getitimer_nid_postfix(invalid, nullptr) == -1 && *__error_nid_postfix() == 22, "invalid timer validation order changed"); } Require(getitimer_nid_postfix(0, nullptr) == -1 && *__error_nid_postfix() == 14, "null output did not report EFAULT"); - for (int unsupported : {1, 2}) { + for (int disarmed : {1, 2}) { GuestItimerval output{{11, 22}, {33, 44}}; - const auto saved = output; - bool threw = false; - try { - getitimer_nid_postfix(unsupported, &output); - } catch (const std::runtime_error&) { - threw = true; - } - Require(threw && std::memcmp(&output, &saved, sizeof(output)) == 0, "unsupported CPU timer reported fabricated state"); + *__error_nid_postfix() = 12345; + Require(getitimer_nid_postfix(disarmed, &output) == 0, "CPU timer read failed"); + Require(*__error_nid_postfix() == 12345, "CPU timer read changed errno"); + Require(output.interval.tv_sec == 0 && output.interval.tv_usec == 0 && output.value.tv_sec == 0 && output.value.tv_usec == 0, + "a CPU timer that cannot be armed did not read as disarmed"); + Require(getitimer_nid_postfix(disarmed, nullptr) == -1 && *__error_nid_postfix() == 14, "null CPU timer output did not report EFAULT"); } const auto ignore = reinterpret_cast(std::uintptr_t{1}); From be17961e3210d7856f6bf613166bdcd8dbde15b2 Mon Sep 17 00:00:00 2001 From: milescoviello Date: Sat, 17 Oct 2026 05:42:38 +0000 Subject: [PATCH 0649/1000] fix(libkernel): return EFAULT from fstat for a null stat buffer FreeBSD's fstat looks the descriptor up before it copies the result out, so a bad descriptor still fails with EBADF and an open one fails with EFAULT when sb is null. fstat and sceKernelFstat threw std::invalid_argument instead. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libkernel/CMakeLists.txt | 10 ++++ core/libs/prx/libkernel/File/src/Stdio.cpp | 10 +++- core/libs/tests/GuestFstatFault.cpp | 69 ++++++++++++++++++++++ 3 files changed, 87 insertions(+), 2 deletions(-) create mode 100644 core/libs/tests/GuestFstatFault.cpp diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index ace5ed5a9d..2d33028502 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -1449,6 +1449,16 @@ if(BUILD_TESTING) set_tests_properties(guest_file_vector PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_fstat_fault_tests ${LIBS_INCLUDE_DIR}/tests/GuestFstatFault.cpp) + target_include_directories(guest_fstat_fault_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_fstat_fault_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_fstat_fault_tests) + add_test(NAME guest_fstat_fault COMMAND guest_fstat_fault_tests) + set_tests_properties(guest_fstat_fault PROPERTIES TIMEOUT 15) + if(WIN32) + set_tests_properties(guest_fstat_fault PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_fstatfs_tests ${LIBS_INCLUDE_DIR}/tests/GuestFstatfs.cpp) target_include_directories(guest_fstatfs_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_fstatfs_tests PRIVATE libkernel libc) diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index 929b8eeb33..dc8f149bea 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -762,7 +762,9 @@ int APS5_VABI _fstatfs_nid_postfix(int d, KernelStatfs* buf) { int64_t APS5_VABI fstat_nid_disambig1_nid_postfix(int d, FileStat* sb) { if (sb == nullptr) { - APS5_INVALID_ARG_EX; + FileStat ignored{}; + if (fstat_nid_disambig1_nid_postfix(d, &ignored) != 0) return -1; + return PosixFailure(GUEST_EFAULT); } if (GuestSockets::IsOpen(d)) { if (const int error = GuestSockets::Stat(d, sb)) return PosixFailure(error); @@ -925,7 +927,11 @@ int APS5_VABI sceKernelCheckReachability(const char* path) { } int APS5_VABI sceKernelFstat(int d, FileStat* sb) { - if (sb == nullptr) throw std::invalid_argument("sceKernelFstat: sb is null"); + if (sb == nullptr) { + FileStat ignored{}; + if (const int error = sceKernelFstat(d, &ignored)) return error; + return SceErrorFromErrno(GUEST_EFAULT); + } if (GuestSockets::IsOpen(d)) { const int error = GuestSockets::Stat(d, sb); return error == 0 ? 0 : SceErrorFromErrno(error); diff --git a/core/libs/tests/GuestFstatFault.cpp b/core/libs/tests/GuestFstatFault.cpp new file mode 100644 index 0000000000..4f0aa66bb9 --- /dev/null +++ b/core/libs/tests/GuestFstatFault.cpp @@ -0,0 +1,69 @@ +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" +#include "prx/libkernel/File/include/FileFlags.hpp" + +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +std::int64_t APS5_VABI fstat_nid_disambig1_nid_postfix(int, FileStat*); +int APS5_VABI sceKernelFstat(int, FileStat*); +int APS5_VABI sceKernelOpen(const char*, int, std::uint16_t); +int APS5_VABI close_nid_postfix(int); +int APS5_VABI socketpair_nid_postfix(int, int, int, int*); +int* APS5_VABI __error_nid_postfix(); +} + +static void Check(bool value, int line) { + if (!value) { + std::fprintf(stderr, "fstat check failed at line %d\n", line); + std::abort(); + } +} +#define Require(value) Check((value), __LINE__) + +static constexpr int GuestEbadf = 9; +static constexpr int GuestEfault = 14; + +static void RequireFailure(int descriptor, int error) { + *__error_nid_postfix() = 0; + Require(fstat_nid_disambig1_nid_postfix(descriptor, nullptr) == -1); + Require(*__error_nid_postfix() == error); + Require(sceKernelFstat(descriptor, nullptr) == static_cast(0x80020000u | static_cast(error))); +} + +int main() { + const auto root = std::filesystem::temp_directory_path() / + ("anyps5-fstat-fault-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); + Require(std::filesystem::create_directory(root)); + { + std::ofstream file(root / "file.txt", std::ios::binary); + file << "fstat"; + Require(static_cast(file)); + } + + AddPathAlias_nid_no_patch("/anyps5-fstat-fault-test", root.string().c_str()); + const int file = sceKernelOpen("/anyps5-fstat-fault-test/file.txt", SCE_KERNEL_O_RDONLY, 0); + Require(file >= 0); + FileStat status{}; + Require(fstat_nid_disambig1_nid_postfix(file, &status) == 0 && status.st_size == 5); + RequireFailure(file, GuestEfault); + + int sockets[2]; + Require(socketpair_nid_postfix(1, 1, 0, sockets) == 0); + RequireFailure(sockets[0], GuestEfault); + Require(close_nid_postfix(sockets[0]) == 0); + Require(close_nid_postfix(sockets[1]) == 0); + Require(close_nid_postfix(file) == 0); +#ifndef _WIN32 + RequireFailure(file, GuestEbadf); + RequireFailure(-1, GuestEbadf); +#endif + + std::filesystem::remove_all(root); +} From 552ab7da477794f815f71dbcfde405f052f3a21a Mon Sep 17 00:00:00 2001 From: thuanquang Date: Sat, 17 Oct 2026 13:21:18 +0700 Subject: [PATCH 0650/1000] fix(shader): flush v_div_fixup_f16 src1 and src2 by the f16 denormal field On RDNA2 (gfx1031) f16 denormal modes 0 and 2 flush the denominator and numerator of v_div_fixup_f16 to a zero of the same sign and keep a denormal quotient and result, as v_div_fixup_f32 does with the f32 field. The translation used them unflushed in every mode, so 0x0001 / 0 gave +infinity instead of the default NaN. Co-Authored-By: Claude Opus 5.5 --- .../execution/DivFixupF16DenormalModes.cpp | 138 ++++++++++++++++++ .../Translation/src/FloatInstructions.cpp | 3 + docs/dev/debt/shader.md | 1 + 3 files changed, 142 insertions(+) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/DivFixupF16DenormalModes.cpp diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DivFixupF16DenormalModes.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DivFixupF16DenormalModes.cpp new file mode 100644 index 0000000000..12b3fe84c9 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DivFixupF16DenormalModes.cpp @@ -0,0 +1,138 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t ValueCount = 10; +constexpr std::uint32_t Threads = 1024; +constexpr std::uint32_t Inputs = 4; +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; + +constexpr std::uint32_t Values[ValueCount] = {0x0000u, 0x8000u, 0x0001u, 0x83ffu, 0x0200u, 0x0400u, 0x3c00u, 0xfc00u, 0x7e00u, 0x7d00u}; + +alignas(256) constexpr std::array Code{ + 0x34020084u, 0x34040082u, 0xe03c1000u, 0x80000401u, 0xbf8c3f70u, 0x7e140280u, 0xd75f000au, 0x041a0b04u, + 0xe0701000u, 0x80010a02u, 0xbf810000u, +}; + +struct Mode { + const char* name; + std::optional value; + bool flushed; + bool ieee; +}; + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + +void Fill() { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const std::uint32_t row = tid < ValueCount * ValueCount * ValueCount ? tid : 0u; + Input[tid * Inputs] = Values[row / (ValueCount * ValueCount)]; + Input[tid * Inputs + 1u] = Values[row / ValueCount % ValueCount]; + Input[tid * Inputs + 2u] = Values[row % ValueCount]; + Input[tid * Inputs + 3u] = 0u; + } +} + +void Run(AgcDriver::VulkanDevice& device, const Mode& mode) { + Output.fill(0xdeadbeefu); + std::vector userData(8, 0u); + const auto input = BufferDescriptor(Input.data(), static_cast(Input.size() * 4u)); + const auto output = BufferDescriptor(Output.data(), static_cast(Output.size() * 4u)); + std::copy(input.begin(), input.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4); + const std::span code(Code); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + request.context.floatMode = mode.value; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); +} + +std::uint32_t Fixup(std::uint32_t quotient, std::uint32_t denominator, std::uint32_t numerator, bool flushed, bool ieee) { + const auto magnitude = [](std::uint32_t half) { return half & 0x7fffu; }; + const auto flush = [&](std::uint32_t half) { return flushed && magnitude(half) < 0x0400u ? half & 0x8000u : half; }; + denominator = flush(denominator); + numerator = flush(numerator); + const auto nan = [&](std::uint32_t half) { return magnitude(half) > 0x7c00u; }; + const auto infinite = [&](std::uint32_t half) { return magnitude(half) == 0x7c00u; }; + const auto zero = [&](std::uint32_t half) { return magnitude(half) == 0u; }; + const std::uint32_t sign = (denominator ^ numerator) & 0x8000u; + if (nan(numerator)) return ieee ? numerator | 0x0200u : numerator; + if (nan(denominator)) return ieee ? denominator | 0x0200u : denominator; + if ((zero(denominator) && zero(numerator)) || (infinite(denominator) && infinite(numerator))) return 0xfe00u; + if (zero(denominator) || infinite(numerator)) return sign | 0x7c00u; + if (infinite(denominator) || zero(numerator)) return sign; + if (nan(quotient)) return sign | 0x7c00u; + return sign | magnitude(quotient); +} + +void Check(const Mode& mode) { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const std::uint32_t* in = &Input[tid * Inputs]; + const std::uint32_t expected = Fixup(in[0], in[1], in[2], mode.flushed, mode.ieee); + Require(Output[tid] == expected, std::string("v_div_fixup_f16 ") + mode.name + " of " + Hex(in[0]) + ", " + Hex(in[1]) + ", " + Hex(in[2]) + " is " + Hex(Output[tid]) + ", expected " + Hex(expected)); + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + Fill(); + const Mode modes[] = { + {"without a known float mode", std::nullopt, false, false}, + {"f16 denormal mode 0, IEEE mode on", ShaderRecompiler::ShaderFloatMode{0x30u, true, true, false}, true, true}, + {"f16 denormal mode 0, IEEE mode off", ShaderRecompiler::ShaderFloatMode{0x30u, true, false, false}, true, false}, + {"f16 denormal mode 1, IEEE mode on", ShaderRecompiler::ShaderFloatMode{0x70u, true, true, false}, false, true}, + {"f16 denormal mode 1, IEEE mode off", ShaderRecompiler::ShaderFloatMode{0x70u, true, false, false}, false, false}, + {"f16 denormal mode 2, IEEE mode on", ShaderRecompiler::ShaderFloatMode{0xb0u, true, true, false}, true, true}, + {"f16 denormal mode 2, IEEE mode off", ShaderRecompiler::ShaderFloatMode{0xb0u, true, false, false}, true, false}, + {"f16 denormal mode 3, IEEE mode on", ShaderRecompiler::ShaderFloatMode{0xf0u, true, true, false}, false, true}, + {"f16 denormal mode 3, IEEE mode off", ShaderRecompiler::ShaderFloatMode{0xf0u, true, false, false}, false, false}, + }; + for (const Mode& mode : modes) { + Run(*device, mode); + Check(mode); + } + std::puts("v_div_fixup_f16 denormal mode tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/shader/recompiler/Translation/src/FloatInstructions.cpp b/core/shader/recompiler/Translation/src/FloatInstructions.cpp index d59e81706e..72e3fda09a 100644 --- a/core/shader/recompiler/Translation/src/FloatInstructions.cpp +++ b/core/shader/recompiler/Translation/src/FloatInstructions.cpp @@ -213,6 +213,9 @@ bool TranslationContext::vDivFixupF16(const RdnaInstruction& inst) { if (operand.negate) { bits[index] = IrU32(ir.BitwiseXor(bits[index].Value(), ir.Constant(0x8000u))); } + if (index != 0u) { + bits[index] = flushF16Input(bits[index]); + } magnitude[index] = IrU32(ir.BitwiseAnd(bits[index].Value(), ir.Constant(0x7fffu))); } const auto isNan = [&](std::uint32_t index) { return IrU1(ir.UGreaterThan(magnitude[index].Value(), ir.Constant(0x7c00u))); }; diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index 5b449de647..0a76774fe4 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -95,6 +95,7 @@ - [Signaling NaN inputs](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) of the f32 and f16 unary instructions (`v_sqrt/rcp/rsq/log/exp/sin/cos/fract/floor/ceil/trunc/rndne/rcp_iflag/frexp_mant`), `v_ldexp_f32/f16`, `v_frexp_mant_f16`, `v_div_fixup_f32/f16` and `v_mullit_f32` are returned quieted only with IEEE=1 and unchanged with IEEE=0, measured on RDNA2 (gfx1035) in both modes, and so are those of the f64 forms (`v_sqrt/rcp/rsq/fract/floor/ceil/trunc/rndne/frexp_mant/ldexp/div_fixup_f64`) and `v_cvt_f64_f32`, measured on RDNA2 (gfx1036) in both modes; shaders without a known float mode follow IEEE=0. `v_pack_b32_f16` quiets each signaling NaN half by the same rule, measured on RDNA2 (gfx1036) in both modes. A signaling NaN result of `v_cubesc/cubetc/cubema_f32` is quieted by the same rule and keeps the sign the instruction gives it, measured on RDNA2 (gfx1036) in both modes. `v_add/sub/subrev/mul/fma/fmac/fmamk/fmaak/mul_legacy_f32` and `v_fma_mix_f32` with f32 sources choose their NaN result on the bits, measured over 1000 operand triples in both modes: the first NaN source in operand order after its modifiers (the subtrahend negated, as the hardware adds its negation), quieted only with IEEE=1, or the default NaN 0xffc00000 for an invalid operation; in the fma forms an invalid product (inf * 0) gives the default NaN even when src2 is a NaN, and in the legacy forms a zero factor makes the product 0 even when the other factor is a NaN (a NaN src2 still wins). `v_mad_legacy_f32` and `v_mac_legacy_f32` follow the same rule with their separately rounded product, as measured on gfx1010 (#1281, #1290): a NaN source quieted only with IEEE=1, inf - inf giving 0xffc00000, the zero rule hiding a NaN factor and a NaN src2 winning. The f16 and f64 arithmetic, `v_fma_mix_f32` with f16 sources and the separately rounded mad/mac forms (not measurable on RDNA2) follow the same rule on hardware but their NaN bits are left to the driver. `v_cvt_f16_f32`, `v_cvt_f32_f64`, `v_dot2_f32_f16`, `v_fma_mixlo_f16` and `v_fma_mixhi_f16` always quiet and `v_div_scale_f32` never does: the f32 signaling NaN 0x7f812345 as src0 or src2 gives 0x7e09 from mixlo and mixhi at IEEE=0 and IEEE=1 on RDNA2 (gfx1036), where `v_fma_mix_f32` gives 0x7f812345 at IEEE=0 and 0x7fc12345 at IEEE=1. The payload of an f16 NaN source of mixlo and mixhi is left to the driver's f16 to f32 conversion, which NVIDIA replaces. - [Scalar f16 add, sub, subrev and mul denormals](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) follow the `FLOAT_MODE` f16 denormal field at nearest-even: bit 0 preserves inputs and bit 1 preserves the result; otherwise a denormal is flushed to its signed zero. Measured on RDNA2 (gfx1036) with IEEE=0/1 and FP16_OVFL=0/1, over 32 operand pairs in all four denormal modes, including signed zeros, denormals, minimum normals, infinities and NaNs. Multiply detects tininess after rounding to 11 significant bits with an unbounded exponent, before gradual half packing: `0x3bff * 0x0400` flushes, while `0x3c01 * 0x03ff` rounds to `0x0400` and is kept. Directed rounding and ternary arithmetic keep their previous denormal lowering. Shaders without a known float mode keep the previous lowering. Scalar `v_sub_f16`/`v_subrev_f16` share `float16Binary` with add/mul and were measured on gfx1036 over the same operand set as add (1156 non-NaN-input rows per mode); NaN result bits still depend on the driver as recorded above. - [f16 unary, frexp and conversion denormals](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) follow the `FLOAT_MODE` f16 denormal field: `v_ceil_f16`, `v_floor_f16`, `v_fract_f16`, `v_rcp_f16`, `v_rsq_f16`, `v_sqrt_f16`, `v_log_f16`, `v_exp_f16`, `v_sin_f16`, `v_frexp_mant_f16`, `v_frexp_exp_i16_f16`, `v_cvt_norm_i16_f16`, `v_cvt_norm_u16_f16` and `v_cvt_f32_f16` flush a denormal half input to its signed zero when bit 0 is clear (0 and 2), and the f16-result ones flush a denormal half result to its signed zero when bit 1 is clear (0 and 1), after the host rounds it to half. Measured on RDNA2 (gfx1036) at IEEE=0 and IEEE=1 with denorm16 0..3 over every non-NaN f16 input (63,490 rows per mode and instruction): the only mismatches are the 1-ULP driver differences of `v_exp_f16` (2 inputs) and `v_sin_f16` (14), which are the same with denormals kept. NaN inputs are not compared. `v_ldexp_f16` shares the input rule. `v_cvt_pknorm_i16_f16` and `v_cvt_pknorm_u16_f16` flush both half inputs by the same bit, measured on RDNA2 (gfx1030) over every f16 input (each half also negated in the other source) at IEEE=0 and IEEE=1 with denorm16 0..3 and at f16 rounding 1..3: every row matches. Shaders without a known float mode keep the previous lowering. +- [v_div_fixup_f16](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) flushes its src1 and src2 to a zero of the same sign when bit 0 of the `FLOAT_MODE` f16 denormal field is clear (0 and 2) and keeps a denormal src0 and result in every mode, as `v_div_fixup_f32` does with the f32 field. Measured on RDNA2 (gfx1031, Radeon RX 6700 XT, AMD Windows driver 32.0.21045.5002 through HIP) over 6859 triples of 19 halves (zeros, denormals, minimum normals, ±0.5, ±1, the largest finite value, infinities and quiet and signaling NaNs) at IEEE=0 and IEEE=1 with denorm16 0..3 and FP16_OVFL 0 and 1, also with neg and abs source modifiers and with op_sel high halves. Shaders without a known float mode keep the previous lowering. The PS5's gfx1013 is not measured. - [Packed f16 NaN results](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) (`v_pk_add/mul/fma/min/max_f16`) follow the hardware measured on RDNA2 (gfx1035) at IEEE=0 and IEEE=1 with f16 round-to-nearest-even and f16 denormals kept: with IEEE=1 a signaling NaN input is quieted and min/max return the other operand only for a quiet NaN; with IEEE=0 NaN inputs pass through unquieted and min/max ignore signaling. In `v_pk_fma_f16` an invalid product (inf * 0) gives the default NaN even when src2 is a NaN. A shader without a known float mode is translated as IEEE=0, the mode titles use; a mode with another f16 rounding or that flushes f16 denormals throws. - [f32 denormals](../../../core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp) follow the shader's `FLOAT_MODE` f32 denormal field (bits 5:4) when it flushes them (0, and shaders without a known float mode, as in the 0xc0 most titles use), measured on RDNA2 (gfx1035) over 1444 rows per instruction at IEEE=0 and IEEE=1. `v_add/sub/subrev/mul/mul_legacy/mullit/fma/fmac/fmamk/fmaak/min/max/min3/max3/med3/fract/floor/ceil/frexp_mant/sin/cos_f32` flush their f32 inputs and their result to a zero of the same sign (`v_mullit_f32` measured on RDNA2 (gfx1036) over 6907 rows per mode at IEEE=0 and IEEE=1, so a positive denormal src2 counts as zero); the f32 compares, `v_cvt_i32/u32/rpi_i32/flr_i32_f32`, `v_frexp_exp_i32_f32`, `v_cvt_f64_f32`, `v_cvt_f16_f32`, `v_cvt_pkrtz_f16_f32`, `v_cvt_pknorm_i16/u16_f32` and `v_cvt_pk_u8_f32` flush their f32 inputs. `v_div_scale_f32` flushes all three inputs to signed zeros, measured on RDNA2 (gfx1036) over 2744 operand triples per mode at IEEE=0 and IEEE=1 with f32 denormals flushed and kept; its scale-down behaviour and host denormal preservation remain incomplete. `v_div_fixup_f32` flushes its src1 and src2 and keeps a denormal src0 and result, measured on RDNA2 (gfx1036) over 6859 rows per mode at IEEE=0 and IEEE=1. Tininess is detected after rounding, measured on RDNA2 (gfx1036) for `v_mul_f32` and `v_mullit_f32` at IEEE=0 and IEEE=1: a product is flushed when its value rounded to 24 bits with an unbounded exponent is below 2^-126, so `0x3f7ffffe * 0x00800001` (2^-126 * (1 - 2^-46)) gives `0x00800000` and `0x3f7fffff * 0x00800000` (2^-126 * (1 - 2^-24)) gives zero. The check rounds in the f32 round mode of `FLOAT_MODE` (bits 1:0), measured in all four modes, while the products themselves are still rounded to nearest even. This applies to `v_mul_f32`, `v_mul_legacy_f32`, `v_mullit_f32`, and to `v_fma_f32`, `v_fmac_f32`, `v_fmamk_f32`, `v_fmaak_f32` and `v_fma_mix_f32` when the addend is zero; with a nonzero addend the fused result is kept, so an exact result just below 2^-126 that rounds to it is not flushed. In the three directed modes the check is computed exactly from the significands. To nearest even (0, the mode of shaders without a known float mode) one factor is multiplied by 2^64, which is exact below 2^64, and the product with the other factor is compared with 2^-62. That multiply is a plain `OpFMul` like the product, and no f32 rounding mode is requested in the SPIR-V, so the check relies on the driver rounding f32 to nearest even by default, as the products do, and a product that rounds up to 2^-126 is the emitted product. `v_fma_mix_f32` follows this rule with mixed f16/f32 sources too, measured on RDNA2 (gfx1036) with f16 in either multiplicand slot, low and high half selection, and an f32 or f16 zero addend: f32 `0x00801002` times f16 `0x3bff` gives `0x00800000` to nearest at IEEE=0 and IEEE=1, while the neighbours `0x00801001` and `0x00801003` give zero and `0x00800001`. The tiny-product boundary was also measured for both signs in all four f32 round modes at IEEE=0; f16 inputs retain their previous conversion and denormal handling. `v_fma_mixlo_f16` and `v_fma_mixhi_f16` flush their f32 sources too and keep a product below 2^-126, measured on RDNA2 (gfx1036). `v_mad_legacy_f32` and `v_mac_legacy_f32` flush their inputs, their product and their result in every mode, see its entry; their product is still checked for tininess on its exact value, so one below 2^-126 gives zero even when it rounds to 2^-126, as the console's separately rounded encoding is not measured at this boundary. Denormal modes 1 and 2 throw for the other instructions. Not covered, and not measurable on RDNA2 (no such opcodes) or not measured: `v_mac_f32`, `v_mad_f32`, `v_madak_f32` and `v_madmk_f32` (rounded separately), `v_dot2_f32_f16` and `v_div_fmas_f32` (on hardware `v_div_fmas_f32` flushes only its result), f32 interpolation and `ds_add_f32` keep the previous lowering; `ds_min/max/cmpst_f32` flush both operands by the field, see the LDS float atomics entry. With f32 denormals kept (3) the results depend on the driver's default denormal behaviour, as no `DenormPreserve` execution mode is requested; `v_ldexp_f32` and the f32 compares are the exceptions. `v_cmp_X_f32` and `v_cmpx_X_f32` compare the operands as sign-magnitude integers with the NaN cases handled apart, so a denormal is not read as zero, measured on RDNA2 (gfx1036) over 256 operand pairs per opcode at IEEE=0 and IEEE=1 with f32 denormals kept and flushed. `v_ldexp_f32` preserves subnormal inputs and rounds a subnormal result in the f32 round mode of `FLOAT_MODE`, measured on RDNA2 (gfx1036) over 2163 rows in each of the four round modes at IEEE=0 and IEEE=1. An overflow rounds like any other result (infinity, or the largest finite value when the mode rounds that sign toward zero). With f32 denormals flushed it flushes a subnormal input and result, measured over the same rows in each round mode at IEEE=0 only. A device whose driver splits `Fma` into a multiply and an add flushes a product below 2^-126 before adding a non-zero addend in `v_fma_f32` and `v_fmac_f32`: `0x1fffffff * 0x1fffffff + 0x00800000` gives `0x00800000` instead of the fused `0x00ffffff` on RADV RENOIR (Mesa 26.2.4), whose compiled shader has `v_mad_f32` and no `v_fma_f32`. [F32DenormalFlush](../../../core/libs/prx/libSceAgcDriver/tests/execution/F32DenormalFlush.cpp) accepts that sum on such a device. - [v_rcp, v_rsq, v_sqrt, v_log, v_exp, v_sin and v_cos](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) (f32 and f16) use the driver's approximation for finite results, not the console's: the low bits can differ. Only NaN, invalid, zero and infinite results and the exact sin/cos zeros are chosen on the bits. Measured on RDNA2 (gfx1036) over every f16 input and 32,832 f32 inputs: RADV returns the hardware's bits for every input of rcp, rsq, sqrt and log and all but one of exp (f16 0x11c5, 1 ULP); NVIDIA (615.71.09) stays within 2 ULP except `v_log_f32` near 1.0 (next entry). [v_sin and v_cos](../../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) take the driver's sine or cosine of the radians of the exact cycle, except within 2^-12 of a turn from a zero of the function, where the result is the linear term of the exact distance to the zero: the hardware's constant 6.2831826 (0x40c90fd5) at the integers, which the hardware applies exactly to inputs below 2^-12 of a turn, and the f32 2π at the half and quarter turns, where the hardware is within 7 ULP of it. Outside those bands, a driver result more than 4 ULP from a Taylor series in turns of the exact cycle is replaced by the series, which is within 2 ULP of the true sine and cosine; the hardware is up to 9 ULP from them (gfx1035, 243,092 f32 inputs), so results stay within 13 ULP of the hardware on any driver. RADV returns the hardware's bits for every f16 input and for 32,422 sine and 32,486 cosine results of the 32,832 f32 inputs; the rest are 1 ULP off, except up to 14 ULP on either side of non-zero integers and up to 15 ULP beyond ±256 cycles, where the hardware's own reduction differs from the exact fraction, and up to 6 ULP inside the half- and quarter-turn bands. NVIDIA's own sine is within 1 ULP on 93% of the f32 inputs and up to 1,166 ULP off just outside the bands, its cosine within 163 ULP; the series replaces those results. A denormal f32 input with f32 denormals kept gives the hardware the input times 6.2831826; both hosts flush it. From 19b236a4c508c18521877b0bed7dee549ae3a8c2 Mon Sep 17 00:00:00 2001 From: milescoviello Date: Sat, 17 Oct 2026 05:56:29 +0000 Subject: [PATCH 0651/1000] fix(libkernel): return EINVAL from the scePthreadAttr stack and guard size calls scePthreadAttrGetstack, Getstackaddr, Getguardsize, Setguardsize, Setstack and Setstackaddr threw for a null attribute, a null pointer argument or a stack below the 16 KiB minimum. FreeBSD's libthr returns EINVAL for those, as the pthread_attr_* wrappers here and the other scePthreadAttr calls already do. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libkernel/CMakeLists.txt | 10 +++ core/libs/prx/libkernel/Pthread/src/Attr.cpp | 16 ++--- core/libs/tests/GuestPthreadAttrStack.cpp | 73 ++++++++++++++++++++ 3 files changed, 89 insertions(+), 10 deletions(-) create mode 100644 core/libs/tests/GuestPthreadAttrStack.cpp diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index ace5ed5a9d..1b42f904fe 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -460,6 +460,16 @@ if(BUILD_TESTING) set_tests_properties(guest_pthread_schedparam PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_pthread_attr_stack_tests ${LIBS_INCLUDE_DIR}/tests/GuestPthreadAttrStack.cpp) + target_include_directories(guest_pthread_attr_stack_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_pthread_attr_stack_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_pthread_attr_stack_tests) + add_test(NAME guest_pthread_attr_stack COMMAND guest_pthread_attr_stack_tests) + set_tests_properties(guest_pthread_attr_stack PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_pthread_attr_stack PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_pthread_mutexattr_tests ${LIBS_INCLUDE_DIR}/tests/GuestPthreadMutexattr.cpp) target_include_directories(guest_pthread_mutexattr_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_pthread_mutexattr_tests PRIVATE libkernel libc) diff --git a/core/libs/prx/libkernel/Pthread/src/Attr.cpp b/core/libs/prx/libkernel/Pthread/src/Attr.cpp index 9d2f032743..4102a9e69e 100644 --- a/core/libs/prx/libkernel/Pthread/src/Attr.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Attr.cpp @@ -68,8 +68,7 @@ int APS5_VABI scePthreadAttrSetstacksize(PthreadAttr* attr, std::size_t stacksiz } int APS5_VABI scePthreadAttrGetstack(const PthreadAttr* attr, void** stackaddr, std::size_t* stacksize) { - if (!attr || !*attr || !stackaddr || !stacksize) - throw std::runtime_error(std::string(__func__) + ": null arg"); + if (!attr || !*attr || !stackaddr || !stacksize) return SCE_KERNEL_ERROR_EINVAL; *stackaddr = (*attr)->stackAddress; *stacksize = (*attr)->_stacksize; return SCE_OK; @@ -104,8 +103,7 @@ int APS5_VABI scePthreadAttrGetdetachstate(const PthreadAttr* attr, int* state) } int APS5_VABI scePthreadAttrGetguardsize(const PthreadAttr* attr, size_t* guard_size) { - if (!attr || !*attr) throw std::runtime_error(std::string(__func__) + ": null attr"); - if (!guard_size) throw std::runtime_error(std::string(__func__) + ": null output"); + if (!attr || !*attr || !guard_size) return SCE_KERNEL_ERROR_EINVAL; *guard_size = (*attr)->_guardsize; return SCE_OK; } @@ -124,8 +122,7 @@ int APS5_VABI scePthreadAttrGetsolosched(const PthreadAttr* attr, int* solosched } int APS5_VABI scePthreadAttrGetstackaddr(const PthreadAttr* attr, void** stack_addr) { - if (!attr || !*attr) throw std::runtime_error(std::string(__func__) + ": null attr"); - if (!stack_addr) throw std::runtime_error(std::string(__func__) + ": null output"); + if (!attr || !*attr || !stack_addr) return SCE_KERNEL_ERROR_EINVAL; *stack_addr = (*attr)->stackAddress; return SCE_OK; } @@ -144,7 +141,7 @@ int APS5_VABI scePthreadAttrSetaffinity(PthreadAttr* attr, KernelCpumask mask) { } int APS5_VABI scePthreadAttrSetguardsize(PthreadAttr* attr, size_t guard_size) { - if (!attr || !*attr) throw std::runtime_error(std::string(__func__) + ": null attr"); + if (!attr || !*attr) return SCE_KERNEL_ERROR_EINVAL; (*attr)->_guardsize = guard_size; return SCE_OK; } @@ -176,15 +173,14 @@ int APS5_VABI scePthreadAttrSetsolosched(PthreadAttr* attr, int solosched) { } int APS5_VABI scePthreadAttrSetstack(PthreadAttr* attr, void* addr, size_t size) { - if (!attr || !*attr || !addr) throw std::runtime_error(std::string(__func__) + ": null arg"); - if (size < 16384) throw std::runtime_error(std::string(__func__) + ": too small"); + if (!attr || !*attr || !addr || size < MIN_STACK_SIZE) return SCE_KERNEL_ERROR_EINVAL; (*attr)->stackAddress = addr; (*attr)->_stacksize = size; return SCE_OK; } int APS5_VABI scePthreadAttrSetstackaddr(PthreadAttr* attr, void* addr) { - if (!attr || !*attr || !addr) throw std::runtime_error(std::string(__func__) + ": null arg"); + if (!attr || !*attr || !addr) return SCE_KERNEL_ERROR_EINVAL; (*attr)->stackAddress = addr; return SCE_OK; } diff --git a/core/libs/tests/GuestPthreadAttrStack.cpp b/core/libs/tests/GuestPthreadAttrStack.cpp new file mode 100644 index 0000000000..faf907527a --- /dev/null +++ b/core/libs/tests/GuestPthreadAttrStack.cpp @@ -0,0 +1,73 @@ +#include "SceTypes.hpp" +#include +#include +#include + +extern "C" { +int APS5_VABI scePthreadAttrInit(PthreadAttr* attr); +int APS5_VABI scePthreadAttrDestroy(PthreadAttr* attr); +int APS5_VABI scePthreadAttrGetstack(const PthreadAttr* attr, void** stackAddress, std::size_t* stackSize); +int APS5_VABI scePthreadAttrGetstackaddr(const PthreadAttr* attr, void** stackAddress); +int APS5_VABI scePthreadAttrGetguardsize(const PthreadAttr* attr, std::size_t* guardSize); +int APS5_VABI scePthreadAttrSetguardsize(PthreadAttr* attr, std::size_t guardSize); +int APS5_VABI scePthreadAttrSetstack(PthreadAttr* attr, void* stackAddress, std::size_t stackSize); +int APS5_VABI scePthreadAttrSetstackaddr(PthreadAttr* attr, void* stackAddress); +} + +static constexpr int SCE_OK = 0; +static constexpr int SceEinval = static_cast(0x80020016u); +static constexpr std::size_t DefaultStackSize = 1u << 20; +static constexpr std::size_t MinimumStackSize = 16384; + +static void Require(bool value) { if (!value) std::abort(); } + +alignas(16) static unsigned char stack[65536]; + +static void RequireStack(const PthreadAttr* attr, const void* expectedAddress, std::size_t expectedSize) { + void* address = &stack[1]; + std::size_t size = 1; + Require(scePthreadAttrGetstack(attr, &address, &size) == SCE_OK); + Require(address == expectedAddress && size == expectedSize); +} + +int main() { + PthreadAttr attr = nullptr; + PthreadAttr empty = nullptr; + Require(scePthreadAttrInit(&attr) == SCE_OK); + void* address = nullptr; + std::size_t size = 0; + + Require(scePthreadAttrSetstack(nullptr, stack, sizeof(stack)) == SceEinval); + Require(scePthreadAttrSetstack(&empty, stack, sizeof(stack)) == SceEinval); + Require(scePthreadAttrSetstack(&attr, nullptr, sizeof(stack)) == SceEinval); + Require(scePthreadAttrSetstack(&attr, stack, MinimumStackSize - 1) == SceEinval); + RequireStack(&attr, nullptr, DefaultStackSize); + Require(scePthreadAttrSetstack(&attr, stack, MinimumStackSize) == SCE_OK); + RequireStack(&attr, stack, MinimumStackSize); + + Require(scePthreadAttrGetstack(nullptr, &address, &size) == SceEinval); + Require(scePthreadAttrGetstack(&empty, &address, &size) == SceEinval); + Require(scePthreadAttrGetstack(&attr, nullptr, &size) == SceEinval); + Require(scePthreadAttrGetstack(&attr, &address, nullptr) == SceEinval); + + Require(scePthreadAttrSetstackaddr(nullptr, &stack[16]) == SceEinval); + Require(scePthreadAttrSetstackaddr(&empty, &stack[16]) == SceEinval); + Require(scePthreadAttrSetstackaddr(&attr, nullptr) == SceEinval); + Require(scePthreadAttrGetstackaddr(nullptr, &address) == SceEinval); + Require(scePthreadAttrGetstackaddr(&empty, &address) == SceEinval); + Require(scePthreadAttrGetstackaddr(&attr, nullptr) == SceEinval); + Require(scePthreadAttrGetstackaddr(&attr, &address) == SCE_OK && address == stack); + Require(scePthreadAttrSetstackaddr(&attr, &stack[16]) == SCE_OK); + Require(scePthreadAttrGetstackaddr(&attr, &address) == SCE_OK && address == &stack[16]); + + std::size_t guard = 0; + Require(scePthreadAttrSetguardsize(nullptr, 8192) == SceEinval); + Require(scePthreadAttrSetguardsize(&empty, 8192) == SceEinval); + Require(scePthreadAttrSetguardsize(&attr, 8192) == SCE_OK); + Require(scePthreadAttrGetguardsize(nullptr, &guard) == SceEinval); + Require(scePthreadAttrGetguardsize(&empty, &guard) == SceEinval); + Require(scePthreadAttrGetguardsize(&attr, nullptr) == SceEinval); + Require(scePthreadAttrGetguardsize(&attr, &guard) == SCE_OK && guard == 8192); + + Require(scePthreadAttrDestroy(&attr) == SCE_OK); +} From 3a4d3bb35e68ca8b0bdf2a54163dee3211744373 Mon Sep 17 00:00:00 2001 From: thuanquang Date: Sat, 17 Oct 2026 13:24:16 +0700 Subject: [PATCH 0652/1000] fix(shader): flush v_cvt_pkrtz_f16_f32 and v_pack_b32_f16 f16 denormals Both ignored the FLOAT_MODE f16 denormal field. On RDNA2 (gfx1031) v_cvt_pkrtz_f16_f32 flushes each denormal half result to its signed zero when bit 7 of FLOAT_MODE is clear, and v_pack_b32_f16 flushes each denormal half input when bit 6 is clear. Modes that keep them and shaders without a known float mode are translated as before. Co-Authored-By: Claude Opus 5.5 --- .../tests/execution/F16PackDenormalModes.cpp | 145 ++++++++++++++++++ .../Translation/src/ConvertInstructions.cpp | 9 +- docs/dev/debt/shader.md | 1 + 3 files changed, 153 insertions(+), 2 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/F16PackDenormalModes.cpp diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/F16PackDenormalModes.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/F16PackDenormalModes.cpp new file mode 100644 index 0000000000..44554b31dc --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/F16PackDenormalModes.cpp @@ -0,0 +1,145 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t ValueCount = 16; +constexpr std::uint32_t Threads = ValueCount * ValueCount; +constexpr std::uint32_t Inputs = 4; +constexpr std::uint32_t Results = 2; +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; + +alignas(256) constexpr std::array Code{ + 0x34020084u, 0x34040083u, 0xe0381000u, 0x80000401u, 0xbf8c3f70u, 0x5e140b04u, 0xd711000bu, 0x00020f06u, + 0xe0741000u, 0x80010a02u, 0xbf810000u, +}; + +constexpr std::uint32_t Singles[ValueCount] = { + 0x00000000u, 0x80000000u, 0x33800000u, 0xb3800000u, 0x33c00000u, 0x33000000u, 0x387fc000u, 0xb87fc000u, + 0x387fffffu, 0x38800000u, 0xb8800000u, 0x37730edfu, 0x3f800000u, 0x477ff000u, 0x7f800000u, 0xff800000u, +}; + +constexpr std::uint32_t SinglesTowardZero[ValueCount] = { + 0x0000u, 0x8000u, 0x0001u, 0x8001u, 0x0001u, 0x0000u, 0x03ffu, 0x83ffu, + 0x03ffu, 0x0400u, 0x8400u, 0x00f3u, 0x3c00u, 0x7bffu, 0x7c00u, 0xfc00u, +}; + +constexpr std::uint32_t Halves[ValueCount] = { + 0x0000u, 0x8000u, 0x0001u, 0x8001u, 0x03ffu, 0x83ffu, 0x0200u, 0x0400u, + 0x8400u, 0x3c00u, 0xbc00u, 0x7bffu, 0x7c00u, 0xfc00u, 0x7e00u, 0x7d00u, +}; + +struct Mode { + const char* name; + std::optional value; +}; + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + +void Fill() { + for (std::uint32_t pair = 0; pair < Threads; ++pair) { + Input[pair * Inputs] = Singles[pair / ValueCount]; + Input[pair * Inputs + 1u] = Singles[pair % ValueCount]; + Input[pair * Inputs + 2u] = Halves[pair / ValueCount]; + Input[pair * Inputs + 3u] = Halves[pair % ValueCount]; + } +} + +void Run(AgcDriver::VulkanDevice& device, const Mode& mode) { + Output.fill(0xdeadbeefu); + std::vector userData(8, 0u); + const auto input = BufferDescriptor(Input.data(), static_cast(Input.size() * 4u)); + const auto output = BufferDescriptor(Output.data(), static_cast(Output.size() * 4u)); + std::copy(input.begin(), input.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4); + const std::span code(Code); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + request.context.floatMode = mode.value; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); +} + +std::uint32_t Flush(std::uint32_t half, bool flushed) { + return flushed && (half & 0x7fffu) < 0x0400u ? half & 0x8000u : half; +} + +void Check(const Mode& mode) { + const std::uint32_t field = mode.value.has_value() ? mode.value->floatMode >> 6u : 3u; + const bool ieee = mode.value.has_value() && mode.value->ieeeMode; + for (std::uint32_t pair = 0; pair < Threads; ++pair) { + const std::uint32_t lhs = pair / ValueCount; + const std::uint32_t rhs = pair % ValueCount; + const bool flushResults = (field & 2u) == 0u; + const std::uint32_t packed = Flush(SinglesTowardZero[lhs], flushResults) | (Flush(SinglesTowardZero[rhs], flushResults) << 16u); + Require(Output[pair * Results] == packed, std::string("v_cvt_pkrtz_f16_f32 ") + mode.name + " of " + Hex(Singles[lhs]) + " and " + Hex(Singles[rhs]) + " is " + Hex(Output[pair * Results]) + ", expected " + Hex(packed)); + const auto half = [&](std::uint32_t value) { + const std::uint32_t bits = Flush(value, (field & 1u) == 0u); + return ieee && (bits & 0x7fffu) > 0x7c00u ? bits | 0x0200u : bits; + }; + const std::uint32_t joined = half(Halves[lhs]) | (half(Halves[rhs]) << 16u); + Require(Output[pair * Results + 1u] == joined, std::string("v_pack_b32_f16 ") + mode.name + " of " + Hex(Halves[lhs]) + " and " + Hex(Halves[rhs]) + " is " + Hex(Output[pair * Results + 1u]) + ", expected " + Hex(joined)); + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + Fill(); + const Mode modes[] = { + {"without a known float mode", std::nullopt}, + {"f16 denormal mode 0, IEEE mode on", ShaderRecompiler::ShaderFloatMode{0x30u, true, true, false}}, + {"f16 denormal mode 0, IEEE mode off", ShaderRecompiler::ShaderFloatMode{0x30u, true, false, false}}, + {"f16 denormal mode 1, IEEE mode on", ShaderRecompiler::ShaderFloatMode{0x70u, true, true, false}}, + {"f16 denormal mode 1, IEEE mode off", ShaderRecompiler::ShaderFloatMode{0x70u, true, false, false}}, + {"f16 denormal mode 2, IEEE mode on", ShaderRecompiler::ShaderFloatMode{0xb0u, true, true, false}}, + {"f16 denormal mode 2, IEEE mode off", ShaderRecompiler::ShaderFloatMode{0xb0u, true, false, false}}, + {"f16 denormal mode 3, IEEE mode on", ShaderRecompiler::ShaderFloatMode{0xf0u, true, true, false}}, + {"f16 denormal mode 3, IEEE mode off", ShaderRecompiler::ShaderFloatMode{0xf0u, true, false, false}}, + }; + for (const Mode& mode : modes) { + Run(*device, mode); + Check(mode); + } + std::puts("f16 pack denormal mode tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/shader/recompiler/Translation/src/ConvertInstructions.cpp b/core/shader/recompiler/Translation/src/ConvertInstructions.cpp index 6b76cb8f98..d14b698c77 100644 --- a/core/shader/recompiler/Translation/src/ConvertInstructions.cpp +++ b/core/shader/recompiler/Translation/src/ConvertInstructions.cpp @@ -198,7 +198,12 @@ void TranslationContext::vCvtOffF32I4(const RdnaInstruction& inst) { void TranslationContext::vCvtPkrtzF16F32(const RdnaInstruction& inst) { const IrF32 lhs(*readOperand(sourceAt(inst, 0u), IrType::F32)); const IrF32 rhs(*readOperand(sourceAt(inst, 1u), IrType::F32)); - const IrU32 result(ir.Emit(IrOpcode::PackFloat2x16Rtz, IrType::U32, {&lhs.Value(), &rhs.Value()})); + IrU32 result(ir.Emit(IrOpcode::PackFloat2x16Rtz, IrType::U32, {&lhs.Value(), &rhs.Value()})); + if (floatMode.has_value() && (floatMode->floatMode & 0x80u) == 0u) { + const IrU32 low = flushF16Result(IrU32(ir.BitwiseAnd(result.Value(), ir.Constant(0xffffu)))); + const IrU32 high = flushF16Result(IrU32(ir.ShiftRightLogical(result.Value(), ir.Constant(16u)))); + result = packU16Lanes(low, high); + } writeOperand(inst.destination, &result.Value()); } @@ -229,7 +234,7 @@ void TranslationContext::vCvtPkU8F32(const RdnaInstruction& inst) { void TranslationContext::vPackB32F16(const RdnaInstruction& inst) { const auto half = [&](std::uint32_t index) { - const IrU32 bits = readF16Bits(sourceAt(inst, index)); + const IrU32 bits = flushF16Input(readF16Bits(sourceAt(inst, index))); const IrU1 nan(ir.UGreaterThan(ir.BitwiseAnd(bits.Value(), ir.Constant(0x7fffu)), ir.Constant(0x7c00u))); return IrU32(ir.Select(nan.Value(), quietNan16(bits).Value(), bits.Value())); }; diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index 5b449de647..15bdeb376f 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -119,6 +119,7 @@ - The [clamp modifier](../../../core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp) follows `DX10_CLAMP`, measured on RDNA2 (gfx1035) for f32, f16, packed f16 and f64 results at IEEE=0 and IEEE=1: with `DX10_CLAMP`=1 (and in shaders without a known float mode) a NaN result clamps to +0, including the quieted NaN that `v_pk_min_f16` and `v_pk_max_f16` return for a signaling NaN operand with IEEE=1 (measured on RDNA2 gfx1036 at both `IEEE_MODE` and both `DX10_CLAMP` settings); with `DX10_CLAMP`=0 a NaN result is written unchanged and every other value clamps to [0, 1] as before. f64 results follow the same rule on hardware, but a clamped f64 result with `DX10_CLAMP`=0 throws until it has an execution test. With `DX10_CLAMP`=0 the NaN's bits are those the translation already produces for the instruction without clamp, so where those are left to the driver (f32 and scalar f16 arithmetic) the payload is the driver's, not the hardware's input propagation. `v_dot2_f32_f16` and the integer clamps are unchanged. - [FP16_OVFL](../../../core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp) follows the shader's float mode as measured on RDNA2 (gfx1036) at IEEE=0 and IEEE=1 with f16 denormals kept and flushed: with `FP16_OVFL`=1 an f16 result that rounds to ±infinity is written as ±0x7bff unless a source operand is infinite, so `v_rcp_f16`, `v_rsq_f16` and `v_log_f16` of ±0 and `v_exp_f16` of 16 or more are clamped while an infinite input passes through, and `v_div_fixup_f16` keeps the infinity it writes for a zero denominator or an infinite numerator and clamps any other infinite quotient, a NaN one included. `v_cvt_pkrtz_f16_f32` rounds toward zero and never overflows. The sources are checked as the f32 values the translation computes with; the rule is applied to `v_fma_mix*_f16` with f32 sources and to `v_interp_p2_f16` without a measurement of infinite f32 sources or parameters. Round modes other than nearest even are not modelled, so the overflow threshold is the nearest-even one (65520). - [Directed f16 rounding](../../../core/shader/recompiler/Translation/src/TranslationContext/SubwordPacking.cpp) for `v_cvt_f16_f32`, `v_cvt_f16_i16`, `v_cvt_f16_u16` and `v_ldexp_f16` follows `FLOAT_MODE` bits 3:2 in every f16 denormal mode, measured on RDNA2 (gfx1036 with f16 denormals preserved, gfx1030 in all sixteen combinations of f16 rounding and denormal mode). When bit 7 flushes f16 results, `v_cvt_f16_f32`, `v_cvt_f16_i16` and `v_cvt_f16_u16` write a zero of the result's sign for a value that, rounded in that mode with an unbounded exponent, is below 2^-14, so a value just below 2^-14 that rounds up to it stays 0x0400; `v_ldexp_f16` keeps its rule above. The `clamp` modifier is applied before the rounding. On gfx1030 every row matches: 8,996 f32 inputs around the half denormal, normal and overflow boundaries with and without `clamp`, every 16-bit integer, and 2,470 `v_ldexp_f16` operands. +- [v_cvt_pkrtz_f16_f32 and v_pack_b32_f16 denormals](../../../core/shader/recompiler/Translation/src/ConvertInstructions.cpp) follow the `FLOAT_MODE` f16 denormal field: `v_cvt_pkrtz_f16_f32` flushes each denormal half result to its signed zero when bit 1 is clear (0 and 1), and `v_pack_b32_f16` flushes each denormal half input to its signed zero when bit 0 is clear (0 and 2). Measured on RDNA2 (gfx1031, Radeon RX 6700 XT, AMD Windows driver 32.0.21045.5002 through HIP) at IEEE=0 and IEEE=1 with denorm16 0..3 and f32 denormals kept, over 256 pairs of 16 f32 values around the f16 denormal range for `v_cvt_pkrtz_f16_f32` and 256 pairs of 16 halves (zeros, denormals, minimum normals, ±1, the largest finite value, infinities and quiet and signaling NaNs) for `v_pack_b32_f16`. Shaders without a known float mode keep the previous lowering. The PS5's gfx1013 is not measured. - [Comparison sampling of a color texture](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) (`image_sample_c`/`image_sample_c_lz` on a texture that is not R32 float or R16 unorm) is emulated in the shader, since Vulkan compares only depth formats: the red channel is fetched, compared and, for bilinear filtering, blended (compare, then filter). Only 2D, 2D array and cube views, float/unorm/snorm formats, wrap, clamp-to-edge and clamp-to-border addressing (border red 0 for the black borders, 1 for opaque white) and point or bilinear filtering at one level are implemented, with texel offsets added to the texel indices; LOD biases and clamps are accepted, as they cannot select another level of a single-level sample. Half-border addressing, the border color table, gradients, gathers, other dimensions, anisotropy (except on the 8-bit formats below) and mip filtering throw. Under bilinear filtering, taps past a clamp-to-border edge compare the border value and blend with the texel taps, so results near the edge depend on the filter weights. AMD documentation does not say how a color format's reference is handled: it is clamped to [0, 1] for unorm and [-1, 1] for snorm, as Vulkan does for unorm depth, and not clamped for float. 8-bit sRGB and UNORM (`Format8Srgb`, `Format8_8Srgb`, `Format8_8_8_8Srgb`, `Format8UNorm`, `Format8_8UNorm`, `Format8_8_8_8UNorm`) return 0 for every compare function, measured with `image_sample_c_lz` on gfx1035 over references -0.5..1.5 and texel codes 0..255. A bilinear cube of those 8-bit formats without `DISABLE_CUBE_WRAP`, and an anisotropic S# on them, return 0 for the same reason; float, snorm and the other unorm bilinear cubes without that bit still throw. Bilinear weights are full float, not the hardware's fixed-point subtexel weights - [Negative color-texture comparison wrap indices](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) (libSceAgcDriver) use explicit `OpSRem` normalization to implement modulo; the negative-offset point-wrap regression fails with direct `OpSMod` and passes with normalization on Mesa 22.3.6 llvmpipe (LLVM 15.0.6), but negative-index wrap has not been measured on PS5 hardware. - [Converted unorm images](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) (10_11_11_UNORM, read and written through an R32_UINT view): samples, gathers, LOD queries, 16-bit data and a DST_SEL of the missing W channel throw. From cbf006cb4b1ce82d6f0d065fc607bac0205fe00d Mon Sep 17 00:00:00 2001 From: milescoviello Date: Sat, 17 Oct 2026 06:26:57 +0000 Subject: [PATCH 0653/1000] fix(libkernel): return 0 from sceKernelMprotect and sceKernelMtypeprotect for a zero length mprotect already returns 0 for a zero length. The sceKernel entry points passed it to DoMprotect, which throws for an empty range. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libkernel/DirectMemory/Export.cpp | 2 ++ core/libs/tests/GuestMprotectUnmapped.cpp | 6 ++++++ 2 files changed, 8 insertions(+) diff --git a/core/libs/prx/libkernel/DirectMemory/Export.cpp b/core/libs/prx/libkernel/DirectMemory/Export.cpp index 658129faa1..8533a87c32 100644 --- a/core/libs/prx/libkernel/DirectMemory/Export.cpp +++ b/core/libs/prx/libkernel/DirectMemory/Export.cpp @@ -258,6 +258,7 @@ int32_t APS5_VABI sceKernelMapNamedFlexibleMemoryInternal(void** addr_in_out, si int APS5_VABI sceKernelMprotect(const void* addr, size_t len, int prot) { constexpr int DefinedProtection = 0x3F7; + if (len == 0) return 0; return DoMprotect(addr, len, prot & DefinedProtection); } @@ -382,6 +383,7 @@ int APS5_VABI sceKernelCheckedReleaseDirectMemory(int64_t start, size_t len) { } int APS5_VABI sceKernelMtypeprotect(const void* addr, size_t len, int type, int prot) { + if (len == 0) return 0; return DoMtypeprotect(addr, len, type, prot); } diff --git a/core/libs/tests/GuestMprotectUnmapped.cpp b/core/libs/tests/GuestMprotectUnmapped.cpp index ed26e5bfa3..8a4ed05199 100644 --- a/core/libs/tests/GuestMprotectUnmapped.cpp +++ b/core/libs/tests/GuestMprotectUnmapped.cpp @@ -56,6 +56,12 @@ int main() { unsigned char* memory = Map(3); Require(munmap_nid_postfix(memory + Page, Page) == 0, "munmap of the middle page failed"); + Require(Protect(memory, 0, ProtectRead) == 0, "a zero-length protection of a mapped page must succeed"); + Require(ProtectionAt(memory) == ProtectReadWrite, "a zero-length protection must not change the page"); + Require(Protect(memory + Page, 0, ProtectRead) == 0 && Protect(nullptr, 0, ProtectRead) == 0, "a zero-length protection of unmapped memory must succeed"); + Require(sceKernelMtypeprotect(memory, 0, 0, ProtectRead) == 0 && ProtectionAt(memory) == ProtectReadWrite, "a zero-length sceKernelMtypeprotect must succeed without changing the page"); + Require(mprotect_nid_postfix(memory + Page, 0, ProtectRead) == 0, "a zero-length mprotect of unmapped memory must succeed"); + Require(Protect(memory, Page * 3, ProtectRead) == 0, "a range with a gap in the middle must succeed for its mapped parts"); Require(ProtectionAt(memory) == ProtectRead && ProtectionAt(memory + Page * 2) == ProtectRead, "the mapped parts on both sides of the gap must be protected"); Require(mprotect_nid_postfix(memory, Page * 3, ProtectReadWrite) == 0, "mprotect over a range with a gap must succeed for its mapped parts"); From 62ac8d655f6e14f63da2c5b57f7120de2d91c0eb Mon Sep 17 00:00:00 2001 From: milescoviello Date: Sat, 17 Oct 2026 06:41:55 +0000 Subject: [PATCH 0654/1000] fix(libkernel): return EINVAL from scePthreadCondattrSetclock and Destroy for a null attribute Both threw std::invalid_argument for a null or uninitialized attribute. FreeBSD's libthr returns EINVAL for one, as the other scePthread attribute calls here already do. The clock id is still stored as given. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libkernel/CMakeLists.txt | 10 ++++++ core/libs/prx/libkernel/Pthread/src/Cond.cpp | 6 ++-- core/libs/tests/GuestPthreadCondattr.cpp | 35 ++++++++++++++++++++ 3 files changed, 47 insertions(+), 4 deletions(-) create mode 100644 core/libs/tests/GuestPthreadCondattr.cpp diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index ace5ed5a9d..33f8f21eff 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -520,6 +520,16 @@ if(BUILD_TESTING) set_tests_properties(guest_cond_timedwait PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_pthread_condattr_tests ${LIBS_INCLUDE_DIR}/tests/GuestPthreadCondattr.cpp) + target_include_directories(guest_pthread_condattr_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_pthread_condattr_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_pthread_condattr_tests) + add_test(NAME guest_pthread_condattr COMMAND guest_pthread_condattr_tests) + set_tests_properties(guest_pthread_condattr PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_pthread_condattr PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_cond_recursive_mutex_tests ${LIBS_INCLUDE_DIR}/tests/GuestCondRecursiveMutex.cpp) target_include_directories(guest_cond_recursive_mutex_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_cond_recursive_mutex_tests PRIVATE libkernel libc) diff --git a/core/libs/prx/libkernel/Pthread/src/Cond.cpp b/core/libs/prx/libkernel/Pthread/src/Cond.cpp index d576c64ff0..fe35bca255 100644 --- a/core/libs/prx/libkernel/Pthread/src/Cond.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Cond.cpp @@ -130,16 +130,14 @@ int APS5_VABI scePthreadCondattrInit(PthreadCondattr* attr) { } int APS5_VABI scePthreadCondattrDestroy(PthreadCondattr* attr) { - if (!attr || !*attr) - throw std::invalid_argument("Condition attributes are not initialized"); + if (!attr || !*attr) return SCE_KERNEL_ERROR_EINVAL; delete *attr; *attr = nullptr; return 0; } int APS5_VABI scePthreadCondattrSetclock(PthreadCondattr* attr, KernelClockid clockId) { - if (!attr || !*attr) - throw std::invalid_argument("Condition attributes are not initialized"); + if (!attr || !*attr) return SCE_KERNEL_ERROR_EINVAL; (*attr)->_clockid = static_cast(clockId); return 0; } diff --git a/core/libs/tests/GuestPthreadCondattr.cpp b/core/libs/tests/GuestPthreadCondattr.cpp new file mode 100644 index 0000000000..a605eda3a1 --- /dev/null +++ b/core/libs/tests/GuestPthreadCondattr.cpp @@ -0,0 +1,35 @@ +#include "SceTypes.hpp" +#include +#include + +extern "C" { +int APS5_VABI scePthreadCondattrInit(PthreadCondattr* attr); +int APS5_VABI scePthreadCondattrDestroy(PthreadCondattr* attr); +int APS5_VABI scePthreadCondattrSetclock(PthreadCondattr* attr, KernelClockid clockId); +int APS5_VABI pthread_condattr_setclock_nid_postfix(PthreadCondattr* attr, KernelClockid clockId); +} + +static constexpr int SCE_OK = 0; +static constexpr int SceEinval = static_cast(0x80020016u); +static constexpr int GuestEinval = 22; +static constexpr KernelClockid Monotonic = 4; + +static void Require(bool value) { if (!value) std::abort(); } + +int main() { + PthreadCondattr unset = nullptr; + Require(scePthreadCondattrSetclock(nullptr, Monotonic) == SceEinval); + Require(scePthreadCondattrSetclock(&unset, Monotonic) == SceEinval); + Require(scePthreadCondattrDestroy(nullptr) == SceEinval); + Require(scePthreadCondattrDestroy(&unset) == SceEinval); + Require(pthread_condattr_setclock_nid_postfix(&unset, Monotonic) == GuestEinval); + + PthreadCondattr attr = nullptr; + Require(scePthreadCondattrInit(&attr) == SCE_OK); + for (const KernelClockid clock : {0, 1, 2, Monotonic, 5, 12}) { + Require(scePthreadCondattrSetclock(&attr, clock) == SCE_OK); + Require(pthread_condattr_setclock_nid_postfix(&attr, clock) == 0); + } + Require(scePthreadCondattrDestroy(&attr) == SCE_OK && attr == nullptr); + Require(scePthreadCondattrDestroy(&attr) == SceEinval); +} From 79a8c81b54408910eee401039b05b5c3f19e8c7b Mon Sep 17 00:00:00 2001 From: thuanquang Date: Sat, 17 Oct 2026 13:41:02 +0700 Subject: [PATCH 0655/1000] fix(shader): flush v_fma_f16 family denormals by the f16 denormal field v_fma_f16, v_fmac_f16, v_fmamk_f16 and v_fmaak_f16 ignored the FLOAT_MODE f16 denormal field. On RDNA2 (gfx1031) at f16 round to nearest even, bit 6 clear flushes every denormal input to a zero of its sign, and bit 7 clear flushes the result when the exact product-sum rounded to 11 significant bits with an unbounded exponent is below 2^-14, the rule v_mul_f16 already follows. The f16 FMA path now flushes its inputs and its round-to-odd result in those modes; the magnitude test is shared with float16Binary as flushTinyF16Value. Co-Authored-By: Claude Opus 5.5 --- .../tests/execution/F16FmaDenormalModes.cpp | 246 ++++++++++++++++++ .../Translation/TranslationContext.hpp | 1 + .../Translation/src/FloatInstructions.cpp | 21 +- .../src/TranslationContext/SubwordPacking.cpp | 6 +- docs/dev/debt/shader.md | 2 +- 5 files changed, 265 insertions(+), 11 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/F16FmaDenormalModes.cpp diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/F16FmaDenormalModes.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/F16FmaDenormalModes.cpp new file mode 100644 index 0000000000..1152f71606 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/F16FmaDenormalModes.cpp @@ -0,0 +1,246 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 64; +constexpr std::uint32_t Inputs = 4; +constexpr std::uint32_t Results = 2; +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; + +alignas(256) constexpr std::array Code{ + 0x34020084u, 0x34040083u, 0xe03c1000u, 0x80000401u, 0xbf8c3f70u, 0x7e140280u, 0xd74b000au, 0x041a0b04u, + 0x7e160306u, 0x6c160b04u, 0xe0741000u, 0x80010a02u, 0xbf810000u, +}; + +constexpr std::uint32_t Rows[Threads][3] = { + {0x0001u, 0x0001u, 0x8400u}, + {0x7bffu, 0x7bffu, 0x0000u}, + {0x7c00u, 0x0000u, 0x0000u}, + {0x3bffu, 0x0400u, 0x0000u}, + {0x3c01u, 0x03ffu, 0x0000u}, + {0x0400u, 0x8400u, 0x0400u}, + {0x0400u, 0x0400u, 0x8400u}, + {0x0000u, 0x0000u, 0x0000u}, + {0x0000u, 0x0000u, 0x8000u}, + {0x0000u, 0x0000u, 0x0400u}, + {0x0000u, 0x0001u, 0x0001u}, + {0x0000u, 0x0001u, 0x03ffu}, + {0x0000u, 0x0001u, 0x0200u}, + {0x0000u, 0x7c00u, 0x0001u}, + {0x0000u, 0x7c00u, 0x8001u}, + {0x0000u, 0x7c00u, 0x03ffu}, + {0x0000u, 0x0001u, 0x0000u}, + {0x0000u, 0x0001u, 0x8000u}, + {0x0000u, 0x0001u, 0x0400u}, + {0x0001u, 0x8001u, 0x0000u}, + {0x0001u, 0x83ffu, 0x0000u}, + {0x0001u, 0x8400u, 0x0000u}, + {0x0000u, 0x0000u, 0x0001u}, + {0x0000u, 0x0000u, 0x03ffu}, + {0x0000u, 0x0000u, 0x0200u}, + {0x0001u, 0x3800u, 0x8001u}, + {0x0001u, 0x3c00u, 0x03ffu}, + {0x0001u, 0xbc00u, 0x83ffu}, + {0x0400u, 0x3c00u, 0x0001u}, + {0x0400u, 0x3c00u, 0x03ffu}, + {0x0400u, 0x3c00u, 0x0200u}, + {0x0000u, 0x0001u, 0x8001u}, + {0x0000u, 0x0001u, 0x83ffu}, + {0x0000u, 0x03ffu, 0x8001u}, + {0x0000u, 0x0000u, 0x8001u}, + {0x0000u, 0x0000u, 0x83ffu}, + {0x0000u, 0x0400u, 0x8001u}, + {0x0400u, 0x3800u, 0x0000u}, + {0x0400u, 0x3800u, 0x8000u}, + {0x0400u, 0x3800u, 0x8400u}, + {0x0001u, 0x3c00u, 0x8400u}, + {0x0001u, 0xbc00u, 0x0000u}, + {0x0001u, 0xbc00u, 0x0400u}, + {0x0001u, 0x3c00u, 0x0000u}, + {0x0001u, 0x3c00u, 0x8000u}, + {0x0001u, 0xbc00u, 0x8000u}, + {0x0400u, 0x3800u, 0x0001u}, + {0x0400u, 0x3800u, 0x8001u}, + {0x0400u, 0xb800u, 0x0001u}, + {0x0400u, 0x3800u, 0x03ffu}, + {0x0400u, 0x3800u, 0x0200u}, + {0x0400u, 0xb800u, 0x83ffu}, + {0x0001u, 0xb800u, 0x0001u}, + {0x0001u, 0x3c00u, 0x8001u}, + {0x0001u, 0xbc00u, 0x0001u}, + {0x0400u, 0x3800u, 0x83ffu}, + {0x0400u, 0xb800u, 0x03ffu}, + {0x0400u, 0x1fffu, 0x83ffu}, + {0x0001u, 0x3800u, 0x8400u}, + {0x0001u, 0xb800u, 0x0400u}, + {0x8001u, 0x3800u, 0x0400u}, + {0x0400u, 0x3bffu, 0x83ffu}, + {0x8400u, 0x3bffu, 0x03ffu}, + {0x0401u, 0xb800u, 0x0200u}, +}; +constexpr std::uint32_t Expected[Threads][4] = { + {0x8400u, 0x8400u, 0x8400u, 0x8400u}, + {0x7c00u, 0x7c00u, 0x7c00u, 0x7c00u}, + {0xfe00u, 0xfe00u, 0xfe00u, 0xfe00u}, + {0x0000u, 0x0000u, 0x0400u, 0x0400u}, + {0x0000u, 0x0400u, 0x0000u, 0x0400u}, + {0x0400u, 0x0400u, 0x0400u, 0x0400u}, + {0x8400u, 0x8400u, 0x8400u, 0x8400u}, + {0x0000u, 0x0000u, 0x0000u, 0x0000u}, + {0x0000u, 0x0000u, 0x0000u, 0x0000u}, + {0x0400u, 0x0400u, 0x0400u, 0x0400u}, + {0x0000u, 0x0000u, 0x0000u, 0x0001u}, + {0x0000u, 0x0000u, 0x0000u, 0x03ffu}, + {0x0000u, 0x0000u, 0x0000u, 0x0200u}, + {0xfe00u, 0xfe00u, 0xfe00u, 0xfe00u}, + {0xfe00u, 0xfe00u, 0xfe00u, 0xfe00u}, + {0xfe00u, 0xfe00u, 0xfe00u, 0xfe00u}, + {0x0000u, 0x0000u, 0x0000u, 0x0000u}, + {0x0000u, 0x0000u, 0x0000u, 0x0000u}, + {0x0400u, 0x0400u, 0x0400u, 0x0400u}, + {0x0000u, 0x8000u, 0x0000u, 0x8000u}, + {0x0000u, 0x8000u, 0x0000u, 0x8000u}, + {0x0000u, 0x8000u, 0x0000u, 0x8000u}, + {0x0000u, 0x0000u, 0x0000u, 0x0001u}, + {0x0000u, 0x0000u, 0x0000u, 0x03ffu}, + {0x0000u, 0x0000u, 0x0000u, 0x0200u}, + {0x0000u, 0x8000u, 0x0000u, 0x8000u}, + {0x0000u, 0x0400u, 0x0000u, 0x0400u}, + {0x8000u, 0x8400u, 0x8000u, 0x8400u}, + {0x0400u, 0x0401u, 0x0400u, 0x0401u}, + {0x0400u, 0x07ffu, 0x0400u, 0x07ffu}, + {0x0400u, 0x0600u, 0x0400u, 0x0600u}, + {0x0000u, 0x8000u, 0x0000u, 0x8001u}, + {0x0000u, 0x8000u, 0x0000u, 0x83ffu}, + {0x0000u, 0x8000u, 0x0000u, 0x8001u}, + {0x0000u, 0x8000u, 0x0000u, 0x8001u}, + {0x0000u, 0x8000u, 0x0000u, 0x83ffu}, + {0x0000u, 0x8000u, 0x0000u, 0x8001u}, + {0x0000u, 0x0000u, 0x0200u, 0x0200u}, + {0x0000u, 0x0000u, 0x0200u, 0x0200u}, + {0x8000u, 0x8000u, 0x8200u, 0x8200u}, + {0x8400u, 0x8000u, 0x8400u, 0x83ffu}, + {0x0000u, 0x8000u, 0x0000u, 0x8001u}, + {0x0400u, 0x0000u, 0x0400u, 0x03ffu}, + {0x0000u, 0x0000u, 0x0000u, 0x0001u}, + {0x0000u, 0x0000u, 0x0000u, 0x0001u}, + {0x8000u, 0x8000u, 0x8000u, 0x8001u}, + {0x0000u, 0x0000u, 0x0200u, 0x0201u}, + {0x0000u, 0x0000u, 0x0200u, 0x01ffu}, + {0x8000u, 0x8000u, 0x8200u, 0x81ffu}, + {0x0000u, 0x05ffu, 0x0200u, 0x05ffu}, + {0x0000u, 0x0400u, 0x0200u, 0x0400u}, + {0x8000u, 0x85ffu, 0x8200u, 0x85ffu}, + {0x0000u, 0x0000u, 0x0000u, 0x0000u}, + {0x0000u, 0x0000u, 0x0000u, 0x0000u}, + {0x0000u, 0x0000u, 0x0000u, 0x0000u}, + {0x0000u, 0x8000u, 0x0200u, 0x81ffu}, + {0x8000u, 0x0000u, 0x8200u, 0x01ffu}, + {0x0000u, 0x8000u, 0x0008u, 0x83f7u}, + {0x8400u, 0x8000u, 0x8400u, 0x8400u}, + {0x0400u, 0x0000u, 0x0400u, 0x0400u}, + {0x0400u, 0x0000u, 0x0400u, 0x0400u}, + {0x0000u, 0x0000u, 0x0400u, 0x0000u}, + {0x8000u, 0x8000u, 0x8400u, 0x8000u}, + {0x8000u, 0x8000u, 0x8200u, 0x8000u}, +}; + +struct Mode { + const char* name; + std::optional value; + std::uint32_t column; +}; + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%04x", value); + return text; +} + +void Run(AgcDriver::VulkanDevice& device, const Mode& mode) { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + std::copy(std::begin(Rows[tid]), std::end(Rows[tid]), &Input[tid * Inputs]); + Input[tid * Inputs + 3u] = 0u; + } + Output.fill(0xdeadbeefu); + std::vector userData(8, 0u); + const auto input = BufferDescriptor(Input.data(), static_cast(Input.size() * 4u)); + const auto output = BufferDescriptor(Output.data(), static_cast(Output.size() * 4u)); + std::copy(input.begin(), input.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4); + const std::span code(Code); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + request.context.floatMode = mode.value; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); +} + +void Check(const Mode& mode) { + constexpr const char* Names[Results] = {"v_fma_f16", "v_fmac_f16"}; + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const std::uint32_t expected = Expected[tid][mode.column]; + for (std::uint32_t form = 0; form < Results; ++form) { + const std::uint32_t actual = Output[tid * Results + form]; + Require(actual == expected, std::string(Names[form]) + " " + mode.name + " of " + Hex(Rows[tid][0]) + ", " + Hex(Rows[tid][1]) + ", " + Hex(Rows[tid][2]) + " is " + Hex(actual) + ", expected " + Hex(expected)); + } + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + const Mode modes[] = { + {"without a known float mode", std::nullopt, 3u}, + {"f16 denormal mode 0, IEEE mode on", ShaderRecompiler::ShaderFloatMode{0x30u, true, true, false}, 0u}, + {"f16 denormal mode 0, IEEE mode off", ShaderRecompiler::ShaderFloatMode{0x30u, true, false, false}, 0u}, + {"f16 denormal mode 1, IEEE mode on", ShaderRecompiler::ShaderFloatMode{0x70u, true, true, false}, 1u}, + {"f16 denormal mode 1, IEEE mode off", ShaderRecompiler::ShaderFloatMode{0x70u, true, false, false}, 1u}, + {"f16 denormal mode 2, IEEE mode on", ShaderRecompiler::ShaderFloatMode{0xb0u, true, true, false}, 2u}, + {"f16 denormal mode 2, IEEE mode off", ShaderRecompiler::ShaderFloatMode{0xb0u, true, false, false}, 2u}, + {"f16 denormal mode 3, IEEE mode on", ShaderRecompiler::ShaderFloatMode{0xf0u, true, true, false}, 3u}, + {"f16 denormal mode 3, IEEE mode off", ShaderRecompiler::ShaderFloatMode{0xf0u, true, false, false}, 3u}, + }; + for (const Mode& mode : modes) { + Run(*device, mode); + Check(mode); + } + std::puts("f16 fma denormal mode tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp b/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp index c0e248f919..70da614af8 100644 --- a/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp +++ b/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp @@ -73,6 +73,7 @@ private: IrU32 flushF16Input(IrU32 bits); IrU32 flushF16Result(IrU32 bits); IrF32 flushF16InputValue(IrF32 value); + IrF32 flushTinyF16Value(IrF32 value, std::uint32_t threshold); IrU32 convertF32ToF16Bits(IrF32 value, std::initializer_list sources); IrU32 flushTinyF16Conversion(IrF32 value, IrU32 bits); void writeF16Conversion(const RdnaOperand& operand, IrF32 value, std::initializer_list sources); diff --git a/core/shader/recompiler/Translation/src/FloatInstructions.cpp b/core/shader/recompiler/Translation/src/FloatInstructions.cpp index d59e81706e..d45f9ece83 100644 --- a/core/shader/recompiler/Translation/src/FloatInstructions.cpp +++ b/core/shader/recompiler/Translation/src/FloatInstructions.cpp @@ -239,19 +239,14 @@ bool TranslationContext::vDivFixupF16(const RdnaInstruction& inst) { bool TranslationContext::float16Binary(const RdnaInstruction& inst, IrOpcode opcode, bool reverse) { const bool measured = (opcode == IrOpcode::FPAdd32 || opcode == IrOpcode::FPSub32 || opcode == IrOpcode::FPMul32) && floatMode.has_value() && (floatMode->floatMode & 0x0cu) == 0u; - const auto flush = [&](IrF32 value, std::uint32_t threshold) { - const IrU32 bits(ir.BitCastU32(value.Value())); - const IrU1 tiny(ir.ULessThan(ir.BitwiseAnd(bits.Value(), ir.Constant(0x7fffffffu)), ir.Constant(threshold))); - return IrF32(ir.BitCastF32(ir.Select(tiny.Value(), ir.BitwiseAnd(bits.Value(), ir.Constant(0x80000000u)), bits.Value()))); - }; const auto read = [&](const RdnaOperand& operand) { const IrF32 value = readF16AsF32(operand); - return measured && (floatMode->floatMode & 0x40u) == 0u ? flush(value, 0x38800000u) : value; + return measured ? flushF16InputValue(value) : value; }; const IrF32 lhs = read(sourceAt(inst, reverse ? 1u : 0u)); const IrF32 rhs = read(sourceAt(inst, reverse ? 0u : 1u)); IrF32 result(ir.Emit(opcode, IrType::F32, {&lhs.Value(), &rhs.Value()})); - if (measured && (floatMode->floatMode & 0x80u) == 0u) result = flush(result, opcode == IrOpcode::FPMul32 ? 0x387ff000u : 0x38800000u); + if (measured && (floatMode->floatMode & 0x80u) == 0u) result = flushTinyF16Value(result, opcode == IrOpcode::FPMul32 ? 0x387ff000u : 0x38800000u); writeF16(inst.destination, result, {&lhs.Value(), &rhs.Value()}); return true; } @@ -260,13 +255,21 @@ bool TranslationContext::float16Ternary(const RdnaInstruction& inst, IrOpcode op if (mix && floatMode.has_value() && (floatMode->floatMode & 0xc0u) != 0xc0u) { throw std::runtime_error("v_fma_mixlo/mixhi_f16 at pc " + std::to_string(inst.programCounter) + " in FLOAT_MODE " + std::to_string(floatMode->floatMode) + " (f16 denormals not kept) is not implemented"); } + const bool measured = opcode == IrOpcode::FPFma32 && !mix && floatMode.has_value() && (floatMode->floatMode & 0x0cu) == 0u; std::array args{}; for (std::uint32_t index = 0u; index < args.size(); ++index) { const RdnaOperand& operand = accumulator && index == 2u ? accumulatorOperand(inst) : sourceAt(inst, index); - args[index] = mix ? &readMixF32(operand).Value() : &readF16AsF32(operand).Value(); + if (mix) { + args[index] = &readMixF32(operand).Value(); + continue; + } + const IrF32 value = readF16AsF32(operand); + args[index] = measured ? &flushF16InputValue(value).Value() : &value.Value(); } if (opcode == IrOpcode::FPFma32 && !mix) { - writeF16(inst.destination, fmaF16RoundedToOdd(IrF32(*args[0]), IrF32(*args[1]), IrF32(*args[2])), {args[0], args[1], args[2]}); + IrF32 result = fmaF16RoundedToOdd(IrF32(*args[0]), IrF32(*args[1]), IrF32(*args[2])); + if (measured && (floatMode->floatMode & 0x80u) == 0u) result = flushTinyF16Value(result, 0x387ff000u); + writeF16(inst.destination, result, {args[0], args[1], args[2]}); return true; } if (mix) { diff --git a/core/shader/recompiler/Translation/src/TranslationContext/SubwordPacking.cpp b/core/shader/recompiler/Translation/src/TranslationContext/SubwordPacking.cpp index f64a0c72ee..0f23cc3f0b 100644 --- a/core/shader/recompiler/Translation/src/TranslationContext/SubwordPacking.cpp +++ b/core/shader/recompiler/Translation/src/TranslationContext/SubwordPacking.cpp @@ -184,8 +184,12 @@ IrU32 TranslationContext::flushF16Result(IrU32 bits) { IrF32 TranslationContext::flushF16InputValue(IrF32 value) { if (!floatMode.has_value() || (floatMode->floatMode & 0x40u) != 0u) return value; + return flushTinyF16Value(value, 0x38800000u); +} + +IrF32 TranslationContext::flushTinyF16Value(IrF32 value, std::uint32_t threshold) { const IrU32 bits(ir.BitCastU32(value.Value())); - const IrU1 tiny(ir.ULessThan(ir.BitwiseAnd(bits.Value(), ir.Constant(0x7fffffffu)), ir.Constant(0x38800000u))); + const IrU1 tiny(ir.ULessThan(ir.BitwiseAnd(bits.Value(), ir.Constant(0x7fffffffu)), ir.Constant(threshold))); return IrF32(ir.BitCastF32(ir.Select(tiny.Value(), ir.BitwiseAnd(bits.Value(), ir.Constant(0x80000000u)), bits.Value()))); } diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index 5b449de647..b8003c2e90 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -93,7 +93,7 @@ - [v_mad_legacy_f32 and v_mac_legacy_f32](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) (VOP3 0x140, VOP2 0x06) round the product to f32 before the add, give a +0 product for a zero factor, and flush their f32 denormal sources, their product and their result to a zero of the same sign in every f32 denormal mode, an exception to the `FLOAT_MODE` model in [f32DenormalFlushFor](../../../core/shader/recompiler/Translation/src/VectorInstructions.cpp). Measured on gfx1010 (RX 5700 XT) with `tools/hw-oracle/template.s`, assembled by the AMD Windows driver's `amd_comgr_2.dll` and launched through its `amdhip64_6.dll` instead of `oracle.c` and ROCr, over the 32 rows of the five `FmaLegacy.cpp` and the four `MacLegacy.cpp` encodings: identical in wave32 and wave64 at IEEE 0 and 1 with f32 denormal modes 0 to 3, except that the two signaling NaN rows are quieted only with IEEE=1 (#1281, #1290). A sum of two zeros takes its sign on the bits (+0 unless both are -0), as lavapipe returned -0 for +0 + -0. The PS5's gfx1013 is not measured; LLVM decodes the encoding as `v_mad_legacy_f32` there, as on gfx1010. RDNA2 differs: on gfx1031 (RX 6700 XT, measured by pandather in #1281) the encoding is `v_fma_legacy_f32` and fused, with the same zero rule and denormal-source flush, so an RDNA2 measurement is not evidence for the console. - [v_fma_mixlo_f16 and v_fma_mixhi_f16](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) round the exact product-sum once to f16 with the f64 arithmetic of v_interp_p2_f16, so they need the Float64 capability. On RDNA2 (gfx1036) they follow the `FLOAT_MODE` f16 round and denormal fields, not the f32 round field: -1.5 * 2^-24 + 2^-126 from f32 sources (0xbfc00000, 0x33800000, 0x00800000) gives 0x8001 with f16 denormals kept and round to nearest even, 0x8000 with f16 denormals flushed and 0x8002 with f16 round toward -infinity. With f16 denormals kept, every f16 rounding mode is translated: the exact product-sum is rounded once in the mode's direction. All four modes match RDNA2 (gfx1036) on the 2,233 rows of the #2631 sweep, including `FLOAT_MODE` 0xcc (toward zero, used by ps5-opengl) and the exact cancellations toward -infinity; the PS5's gfx1013 is not measured. A result beyond 65504 becomes infinity or 65504 as IEEE 754 gives for the mode. Flushed f16 denormals still throw, and packed f16 still throws in every mode but round to nearest even with f16 denormals kept. `FP16_OVFL` set is not measured. - [Signaling NaN inputs](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) of the f32 and f16 unary instructions (`v_sqrt/rcp/rsq/log/exp/sin/cos/fract/floor/ceil/trunc/rndne/rcp_iflag/frexp_mant`), `v_ldexp_f32/f16`, `v_frexp_mant_f16`, `v_div_fixup_f32/f16` and `v_mullit_f32` are returned quieted only with IEEE=1 and unchanged with IEEE=0, measured on RDNA2 (gfx1035) in both modes, and so are those of the f64 forms (`v_sqrt/rcp/rsq/fract/floor/ceil/trunc/rndne/frexp_mant/ldexp/div_fixup_f64`) and `v_cvt_f64_f32`, measured on RDNA2 (gfx1036) in both modes; shaders without a known float mode follow IEEE=0. `v_pack_b32_f16` quiets each signaling NaN half by the same rule, measured on RDNA2 (gfx1036) in both modes. A signaling NaN result of `v_cubesc/cubetc/cubema_f32` is quieted by the same rule and keeps the sign the instruction gives it, measured on RDNA2 (gfx1036) in both modes. `v_add/sub/subrev/mul/fma/fmac/fmamk/fmaak/mul_legacy_f32` and `v_fma_mix_f32` with f32 sources choose their NaN result on the bits, measured over 1000 operand triples in both modes: the first NaN source in operand order after its modifiers (the subtrahend negated, as the hardware adds its negation), quieted only with IEEE=1, or the default NaN 0xffc00000 for an invalid operation; in the fma forms an invalid product (inf * 0) gives the default NaN even when src2 is a NaN, and in the legacy forms a zero factor makes the product 0 even when the other factor is a NaN (a NaN src2 still wins). `v_mad_legacy_f32` and `v_mac_legacy_f32` follow the same rule with their separately rounded product, as measured on gfx1010 (#1281, #1290): a NaN source quieted only with IEEE=1, inf - inf giving 0xffc00000, the zero rule hiding a NaN factor and a NaN src2 winning. The f16 and f64 arithmetic, `v_fma_mix_f32` with f16 sources and the separately rounded mad/mac forms (not measurable on RDNA2) follow the same rule on hardware but their NaN bits are left to the driver. `v_cvt_f16_f32`, `v_cvt_f32_f64`, `v_dot2_f32_f16`, `v_fma_mixlo_f16` and `v_fma_mixhi_f16` always quiet and `v_div_scale_f32` never does: the f32 signaling NaN 0x7f812345 as src0 or src2 gives 0x7e09 from mixlo and mixhi at IEEE=0 and IEEE=1 on RDNA2 (gfx1036), where `v_fma_mix_f32` gives 0x7f812345 at IEEE=0 and 0x7fc12345 at IEEE=1. The payload of an f16 NaN source of mixlo and mixhi is left to the driver's f16 to f32 conversion, which NVIDIA replaces. -- [Scalar f16 add, sub, subrev and mul denormals](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) follow the `FLOAT_MODE` f16 denormal field at nearest-even: bit 0 preserves inputs and bit 1 preserves the result; otherwise a denormal is flushed to its signed zero. Measured on RDNA2 (gfx1036) with IEEE=0/1 and FP16_OVFL=0/1, over 32 operand pairs in all four denormal modes, including signed zeros, denormals, minimum normals, infinities and NaNs. Multiply detects tininess after rounding to 11 significant bits with an unbounded exponent, before gradual half packing: `0x3bff * 0x0400` flushes, while `0x3c01 * 0x03ff` rounds to `0x0400` and is kept. Directed rounding and ternary arithmetic keep their previous denormal lowering. Shaders without a known float mode keep the previous lowering. Scalar `v_sub_f16`/`v_subrev_f16` share `float16Binary` with add/mul and were measured on gfx1036 over the same operand set as add (1156 non-NaN-input rows per mode); NaN result bits still depend on the driver as recorded above. +- [Scalar f16 add, sub, subrev and mul denormals](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) follow the `FLOAT_MODE` f16 denormal field at nearest-even: bit 0 preserves inputs and bit 1 preserves the result; otherwise a denormal is flushed to its signed zero. Measured on RDNA2 (gfx1036) with IEEE=0/1 and FP16_OVFL=0/1, over 32 operand pairs in all four denormal modes, including signed zeros, denormals, minimum normals, infinities and NaNs. Multiply detects tininess after rounding to 11 significant bits with an unbounded exponent, before gradual half packing: `0x3bff * 0x0400` flushes, while `0x3c01 * 0x03ff` rounds to `0x0400` and is kept. `v_fma_f16`, `v_fmac_f16`, `v_fmamk_f16` and `v_fmaak_f16` follow the same field at nearest-even: inputs are flushed when bit 0 is clear, and the result is flushed when bit 1 is clear if the exact product-sum rounded to 11 significant bits with an unbounded exponent is below 2^-14, so `0x0400 * 0x0400 + 0x8400` rounds to `0x8400` and is kept. Measured on RDNA2 (gfx1031, Radeon RX 6700 XT, AMD Windows driver 32.0.21045.5002 through HIP) at IEEE=0 and IEEE=1 with denorm16 0..3 over 5075 operand triples of `v_fma_f16` and `v_fmac_f16` (4000 from 20 halves around the denormal range, 75 whose product-sum lies just below 2^-14 and 1000 random), and over the first 4075 of them for `v_fmamk_f16` and `v_fmaak_f16`, FP16_OVFL=1, the clamp, neg and abs modifiers and op_sel. Directed rounding and `v_fma_mixlo/mixhi_f16` keep their previous denormal lowering. Shaders without a known float mode keep the previous lowering. Scalar `v_sub_f16`/`v_subrev_f16` share `float16Binary` with add/mul and were measured on gfx1036 over the same operand set as add (1156 non-NaN-input rows per mode); NaN result bits still depend on the driver as recorded above. - [f16 unary, frexp and conversion denormals](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) follow the `FLOAT_MODE` f16 denormal field: `v_ceil_f16`, `v_floor_f16`, `v_fract_f16`, `v_rcp_f16`, `v_rsq_f16`, `v_sqrt_f16`, `v_log_f16`, `v_exp_f16`, `v_sin_f16`, `v_frexp_mant_f16`, `v_frexp_exp_i16_f16`, `v_cvt_norm_i16_f16`, `v_cvt_norm_u16_f16` and `v_cvt_f32_f16` flush a denormal half input to its signed zero when bit 0 is clear (0 and 2), and the f16-result ones flush a denormal half result to its signed zero when bit 1 is clear (0 and 1), after the host rounds it to half. Measured on RDNA2 (gfx1036) at IEEE=0 and IEEE=1 with denorm16 0..3 over every non-NaN f16 input (63,490 rows per mode and instruction): the only mismatches are the 1-ULP driver differences of `v_exp_f16` (2 inputs) and `v_sin_f16` (14), which are the same with denormals kept. NaN inputs are not compared. `v_ldexp_f16` shares the input rule. `v_cvt_pknorm_i16_f16` and `v_cvt_pknorm_u16_f16` flush both half inputs by the same bit, measured on RDNA2 (gfx1030) over every f16 input (each half also negated in the other source) at IEEE=0 and IEEE=1 with denorm16 0..3 and at f16 rounding 1..3: every row matches. Shaders without a known float mode keep the previous lowering. - [Packed f16 NaN results](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) (`v_pk_add/mul/fma/min/max_f16`) follow the hardware measured on RDNA2 (gfx1035) at IEEE=0 and IEEE=1 with f16 round-to-nearest-even and f16 denormals kept: with IEEE=1 a signaling NaN input is quieted and min/max return the other operand only for a quiet NaN; with IEEE=0 NaN inputs pass through unquieted and min/max ignore signaling. In `v_pk_fma_f16` an invalid product (inf * 0) gives the default NaN even when src2 is a NaN. A shader without a known float mode is translated as IEEE=0, the mode titles use; a mode with another f16 rounding or that flushes f16 denormals throws. - [f32 denormals](../../../core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp) follow the shader's `FLOAT_MODE` f32 denormal field (bits 5:4) when it flushes them (0, and shaders without a known float mode, as in the 0xc0 most titles use), measured on RDNA2 (gfx1035) over 1444 rows per instruction at IEEE=0 and IEEE=1. `v_add/sub/subrev/mul/mul_legacy/mullit/fma/fmac/fmamk/fmaak/min/max/min3/max3/med3/fract/floor/ceil/frexp_mant/sin/cos_f32` flush their f32 inputs and their result to a zero of the same sign (`v_mullit_f32` measured on RDNA2 (gfx1036) over 6907 rows per mode at IEEE=0 and IEEE=1, so a positive denormal src2 counts as zero); the f32 compares, `v_cvt_i32/u32/rpi_i32/flr_i32_f32`, `v_frexp_exp_i32_f32`, `v_cvt_f64_f32`, `v_cvt_f16_f32`, `v_cvt_pkrtz_f16_f32`, `v_cvt_pknorm_i16/u16_f32` and `v_cvt_pk_u8_f32` flush their f32 inputs. `v_div_scale_f32` flushes all three inputs to signed zeros, measured on RDNA2 (gfx1036) over 2744 operand triples per mode at IEEE=0 and IEEE=1 with f32 denormals flushed and kept; its scale-down behaviour and host denormal preservation remain incomplete. `v_div_fixup_f32` flushes its src1 and src2 and keeps a denormal src0 and result, measured on RDNA2 (gfx1036) over 6859 rows per mode at IEEE=0 and IEEE=1. Tininess is detected after rounding, measured on RDNA2 (gfx1036) for `v_mul_f32` and `v_mullit_f32` at IEEE=0 and IEEE=1: a product is flushed when its value rounded to 24 bits with an unbounded exponent is below 2^-126, so `0x3f7ffffe * 0x00800001` (2^-126 * (1 - 2^-46)) gives `0x00800000` and `0x3f7fffff * 0x00800000` (2^-126 * (1 - 2^-24)) gives zero. The check rounds in the f32 round mode of `FLOAT_MODE` (bits 1:0), measured in all four modes, while the products themselves are still rounded to nearest even. This applies to `v_mul_f32`, `v_mul_legacy_f32`, `v_mullit_f32`, and to `v_fma_f32`, `v_fmac_f32`, `v_fmamk_f32`, `v_fmaak_f32` and `v_fma_mix_f32` when the addend is zero; with a nonzero addend the fused result is kept, so an exact result just below 2^-126 that rounds to it is not flushed. In the three directed modes the check is computed exactly from the significands. To nearest even (0, the mode of shaders without a known float mode) one factor is multiplied by 2^64, which is exact below 2^64, and the product with the other factor is compared with 2^-62. That multiply is a plain `OpFMul` like the product, and no f32 rounding mode is requested in the SPIR-V, so the check relies on the driver rounding f32 to nearest even by default, as the products do, and a product that rounds up to 2^-126 is the emitted product. `v_fma_mix_f32` follows this rule with mixed f16/f32 sources too, measured on RDNA2 (gfx1036) with f16 in either multiplicand slot, low and high half selection, and an f32 or f16 zero addend: f32 `0x00801002` times f16 `0x3bff` gives `0x00800000` to nearest at IEEE=0 and IEEE=1, while the neighbours `0x00801001` and `0x00801003` give zero and `0x00800001`. The tiny-product boundary was also measured for both signs in all four f32 round modes at IEEE=0; f16 inputs retain their previous conversion and denormal handling. `v_fma_mixlo_f16` and `v_fma_mixhi_f16` flush their f32 sources too and keep a product below 2^-126, measured on RDNA2 (gfx1036). `v_mad_legacy_f32` and `v_mac_legacy_f32` flush their inputs, their product and their result in every mode, see its entry; their product is still checked for tininess on its exact value, so one below 2^-126 gives zero even when it rounds to 2^-126, as the console's separately rounded encoding is not measured at this boundary. Denormal modes 1 and 2 throw for the other instructions. Not covered, and not measurable on RDNA2 (no such opcodes) or not measured: `v_mac_f32`, `v_mad_f32`, `v_madak_f32` and `v_madmk_f32` (rounded separately), `v_dot2_f32_f16` and `v_div_fmas_f32` (on hardware `v_div_fmas_f32` flushes only its result), f32 interpolation and `ds_add_f32` keep the previous lowering; `ds_min/max/cmpst_f32` flush both operands by the field, see the LDS float atomics entry. With f32 denormals kept (3) the results depend on the driver's default denormal behaviour, as no `DenormPreserve` execution mode is requested; `v_ldexp_f32` and the f32 compares are the exceptions. `v_cmp_X_f32` and `v_cmpx_X_f32` compare the operands as sign-magnitude integers with the NaN cases handled apart, so a denormal is not read as zero, measured on RDNA2 (gfx1036) over 256 operand pairs per opcode at IEEE=0 and IEEE=1 with f32 denormals kept and flushed. `v_ldexp_f32` preserves subnormal inputs and rounds a subnormal result in the f32 round mode of `FLOAT_MODE`, measured on RDNA2 (gfx1036) over 2163 rows in each of the four round modes at IEEE=0 and IEEE=1. An overflow rounds like any other result (infinity, or the largest finite value when the mode rounds that sign toward zero). With f32 denormals flushed it flushes a subnormal input and result, measured over the same rows in each round mode at IEEE=0 only. A device whose driver splits `Fma` into a multiply and an add flushes a product below 2^-126 before adding a non-zero addend in `v_fma_f32` and `v_fmac_f32`: `0x1fffffff * 0x1fffffff + 0x00800000` gives `0x00800000` instead of the fused `0x00ffffff` on RADV RENOIR (Mesa 26.2.4), whose compiled shader has `v_mad_f32` and no `v_fma_f32`. [F32DenormalFlush](../../../core/libs/prx/libSceAgcDriver/tests/execution/F32DenormalFlush.cpp) accepts that sum on such a device. From 603dc01dbfefae12203a334b3da4db1d28f8c3fb Mon Sep 17 00:00:00 2001 From: Alex Collini Date: Thu, 15 Oct 2026 10:04:46 +0200 Subject: [PATCH 0656/1000] feat(agc): store to images selected through image tables An IMAGE_STORE whose T# comes from a runtime-indexed image table used to poison the whole table as an unsupported operation. Written, non-atomic table images now get storage modes, a STORAGE_IMAGE table binding (260, the sampled image table moves to 261 and keeps the variable count), and storage views of tracked storage textures at the same element indices. Every bound entry is marked written, so each dispatch writes back all of the table's storage images. Atomic, packed, BY2/BY4 and dynamic-mip table stores still fault. Tables with written entries are not reused. Co-Authored-By: Claude Opus 5.5 --- .../Graphics/include/Context.hpp | 1 + .../Graphics/include/ShaderResources.hpp | 3 + .../Graphics/src/ShaderResources.cpp | 76 +++++++++++++++++-- .../Graphics/src/ShaderValidation.cpp | 4 +- .../tests/execution/ImageTable.cpp | 37 +++++++-- .../Optimization/src/BindingAllocator.cpp | 3 + .../src/DescriptorBindingBuilder.cpp | 11 ++- .../Optimization/src/ResourceMaterializer.cpp | 3 +- .../Optimization/src/ResourceTracker.cpp | 3 +- core/shader/recompiler/Recompiler.hpp | 3 +- core/shader/recompiler/RuntimeAbi.hpp | 11 ++- .../src/SpirvMemory/SpirvDescriptors.cpp | 23 +++++- .../src/SpirvMemory/SpirvEmitterHelpers.cpp | 5 +- 13 files changed, 155 insertions(+), 28 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp index 995eee4957..e85a7462dd 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp @@ -319,6 +319,7 @@ inline void EndRenderPass(const Context& context, VkCommandBuffer commands) { } std::uint32_t ImageTableCapacity(const Context& context); +std::uint32_t StorageTableCapacity(const Context& context); std::uint32_t SamplerTableCapacity(const Context& context); } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp index 8e25d227c9..89a05894c1 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp @@ -338,12 +338,15 @@ private: std::uint32_t set = 0; std::size_t firstSampler = 0; std::size_t samplerCount = 0; + bool sampled = false; + bool storage = false; std::vector elementTextures; std::vector> samplerRejections; }; std::vector tableStages; std::array tableBindings{std::numeric_limits::max(), std::numeric_limits::max()}; std::array tableElements{}; + std::array storageTableBindings{std::numeric_limits::max(), std::numeric_limits::max()}; TableStage& tableStage(const CompiledShader& shader); static const ShaderRecompiler::DescriptorBinding* tableImages(std::span shaders, ShaderRecompiler::ShaderStage stage); std::uint32_t rejectTableElement(TableStage& table, std::span words, const std::string& message); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index 926bfc67fa..55cf3ffbde 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -1160,6 +1160,7 @@ const char* roleName(ShaderRecompiler::DescriptorRole role) { case ShaderRecompiler::DescriptorRole::ImageTable: return "ImageTable"; case ShaderRecompiler::DescriptorRole::SamplerTable: return "SamplerTable"; case ShaderRecompiler::DescriptorRole::ImageTableMap: return "ImageTableMap"; + case ShaderRecompiler::DescriptorRole::ImageStorageTable: return "ImageStorageTable"; } throw std::runtime_error("AGC graphics: unknown descriptor role"); } @@ -1580,10 +1581,13 @@ void ShaderResources::buildPrepare(std::span shaders, cons }; for (const auto& binding : shader.program->bindings) { Require(binding.descriptorSet == ShaderRecompiler::RuntimeAbi::DescriptorSet || (shader.stage == ShaderRecompiler::ShaderStage::Fragment && binding.descriptorSet == ShaderRecompiler::RuntimeAbi::FragmentDescriptorSet), "unexpected descriptor set: a shader resource must use its stage's descriptor set"); - if (binding.role == ShaderRecompiler::DescriptorRole::ImageTable) { - Require(binding.binding == ShaderRecompiler::RuntimeAbi::ImageTableBinding && binding.kind == ShaderRecompiler::DescriptorKind::SampledImage && binding.guestDescriptor.size() == static_cast(binding.count) * 8u && binding.imageShapes.size() == binding.count && binding.imageDepthCompare.size() == binding.count && binding.imageSamplers.size() == binding.count, "image table binding is malformed"); + if (binding.role == ShaderRecompiler::DescriptorRole::ImageTable || binding.role == ShaderRecompiler::DescriptorRole::ImageStorageTable) { + const bool storage = binding.role == ShaderRecompiler::DescriptorRole::ImageStorageTable; + Require(binding.binding == (storage ? ShaderRecompiler::RuntimeAbi::ImageStorageTableBinding : ShaderRecompiler::RuntimeAbi::ImageTableBinding) && binding.kind == (storage ? ShaderRecompiler::DescriptorKind::StorageImage : ShaderRecompiler::DescriptorKind::SampledImage) && binding.guestDescriptor.size() == static_cast(binding.count) * 8u && binding.imageShapes.size() == binding.count && binding.imageDepthCompare.size() == binding.count && binding.imageSamplers.size() == binding.count && binding.imageWritten.size() == binding.count, "image table binding is malformed"); auto& table = tableStage(shader); - Require(table.images == nullptr, "a stage binds two image tables"); + Require(storage ? !table.storage : !table.sampled, "a stage binds two image tables"); + Require(table.images == nullptr || table.images->count == binding.count, "a stage's image tables disagree on their elements"); + (storage ? table.storage : table.sampled) = true; table.images = &binding; table.firstSampler = firstSampler; continue; @@ -1678,6 +1682,7 @@ void ShaderResources::buildPrepare(std::span shaders, cons } splitSets = SplitsSets(context, shaders); std::array tableFlags{}; + std::array storageTableFlags{}; for (auto& table : tableStages) { Require(table.mapAllocation != std::numeric_limits::max(), "an image table stage has no map"); const auto& map = table.map; @@ -1692,7 +1697,13 @@ void ShaderResources::buildPrepare(std::span shaders, cons table.elementBase = tableElements[table.set]; table.map[ShaderRecompiler::ImageTableAbi::HeaderElementBase] = table.elementBase; tableElements[table.set] += elements; - tableFlags[table.set] |= VulkanStage(table.stage); + if (table.sampled) tableFlags[table.set] |= VulkanStage(table.stage); + if (table.storage) storageTableFlags[table.set] |= VulkanStage(table.stage); + } + for (std::uint32_t set = 0; set < storageTableFlags.size(); ++set) { + if (storageTableFlags[set] == 0) continue; + bindings.push_back({{ShaderRecompiler::RuntimeAbi::ImageStorageTableBinding, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, StorageTableCapacity(context), storageTableFlags[set], nullptr}, {}, {}, VK_DESCRIPTOR_BINDING_PARTIALLY_BOUND_BIT, static_cast(set)}); + storageTableBindings[set] = bindings.size() - 1u; } for (std::uint32_t set = 0; set < tableFlags.size(); ++set) { if (tableFlags[set] == 0) continue; @@ -1843,6 +1854,7 @@ DescribedSetLayouts DescribeSetLayouts(const Context& context, std::span, 2> bindingFlags; std::array tableFlags{}; + std::array storageTableFlags{}; bool updateAfterBind = false; std::uint64_t storageBuffers = 0; for (const auto& shader : shaders) { @@ -1856,6 +1868,10 @@ DescribedSetLayouts DescribeSetLayouts(const Context& context, std::spanHandle(), VK_NULL_HANDLE, VK_IMAGE_LAYOUT_UNDEFINED}); + else if (binding.layout.descriptorType == VK_DESCRIPTOR_TYPE_STORAGE_IMAGE) images.push_back({VK_NULL_HANDLE, storageTextures[index]->StorageView(storageMips[index], storageFirstLayer[index]), VK_IMAGE_LAYOUT_GENERAL}); else images.push_back({VK_NULL_HANDLE, textureFirstLayer[index] ? textures[index]->FirstLayerView() : textures[index]->View(), textures[index]->Layout()}); } writes.push_back(write); @@ -4093,6 +4115,10 @@ void ShaderResources::resolveImageBinding(const ShaderRecompiler::DescriptorBind } } +std::uint32_t StorageTableCapacity(const Context& context) { + return std::min({context.imageTableCapacity, 4096u, context.limits.maxPerStageDescriptorStorageImages / 4u}); +} + std::uint32_t ImageTableCapacity(const Context& context) { if (!context.runtimeDescriptorArray || !context.partiallyBound || !context.variableDescriptorCount || !context.descriptorIndexing) return 0u; constexpr std::uint64_t sampledHeaps = static_cast(ShaderRecompiler::RuntimeAbi::FirstStorageImageBinding - ShaderRecompiler::RuntimeAbi::FirstImageBinding) * ShaderRecompiler::RuntimeAbi::SampledHeapCapacity; @@ -4188,14 +4214,16 @@ void ShaderResources::addSamplerTable(const ShaderRecompiler::DescriptorBinding& } void ShaderResources::resolveImageTables() { - if (tableBindings[0] == std::numeric_limits::max() && tableBindings[1] == std::numeric_limits::max()) return; + const auto none = [](const std::array& set) { return set[0] == std::numeric_limits::max() && set[1] == std::numeric_limits::max(); }; + if (none(tableBindings) && none(storageTableBindings)) return; auto& counters = TextureCounts(); for (std::size_t set = 0; set < tableBindings.size(); ++set) { + if (storageTableBindings[set] != std::numeric_limits::max()) bindings[storageTableBindings[set]].imageAllocations.assign(std::min(tableElements[set], StorageTableCapacity(context)), std::numeric_limits::max()); if (tableBindings[set] != std::numeric_limits::max()) bindings[tableBindings[set]].imageAllocations.assign(std::min(tableElements[set], context.imageTableCapacity), std::numeric_limits::max()); } for (auto& table : tableStages) { if (table.images == nullptr) continue; - auto& item = bindings[tableBindings[table.set]]; + auto* item = tableBindings[table.set] != std::numeric_limits::max() ? &bindings[tableBindings[table.set]] : nullptr; const auto& binding = *table.images; const auto faults = table.map[ShaderRecompiler::ImageTableAbi::HeaderElementFaults]; const auto stageSamplers = std::span>(samplers).subspan(table.firstSampler, table.samplerCount); @@ -4224,11 +4252,12 @@ void ShaderResources::resolveImageTables() { textureFirstLayer.push_back(firstLayer); describedRanges.push_back({"texture", resource.baseAddress, guestBytes, resource.width, resource.height, resource.format, static_cast(resource.tileMode), resource.dccAddress}); table.elementTextures[element] = textures.size() - 1u; - item.imageAllocations[static_cast(table.elementBase) + element] = textures.size() - 1u; + item->imageAllocations[static_cast(table.elementBase) + element] = textures.size() - 1u; }; const auto state = CurrentTableState(); std::vector> cached; - if (auto* reused = FindReusedTable(context.device, binding, state); reused != nullptr && TableCurrent(*reused) && CachedTableTextures(*reused, cached)) { + const bool storageTable = std::find(binding.imageWritten.begin(), binding.imageWritten.end(), true) != binding.imageWritten.end(); + if (auto* reused = storageTable ? nullptr : FindReusedTable(context.device, binding, state); reused != nullptr && TableCurrent(*reused) && CachedTableTextures(*reused, cached)) { auto next = cached.begin(); for (std::uint32_t element = 0; element < binding.count; ++element) { const auto words = std::span(binding.guestDescriptor).subspan(static_cast(element) * 8u, 8u); @@ -4272,6 +4301,37 @@ void ShaderResources::resolveImageTables() { if (reusable) fresh.elements.push_back({{}, {}, {}, 0, false, true, error.what()}); continue; } + if (element < binding.imageWritten.size() && binding.imageWritten[element]) { + reusable = false; + const auto storageBinding = storageTableBindings[table.set]; + if (storageBinding == std::numeric_limits::max() || absolute >= StorageTableCapacity(context)) { + table.map[faults + element] = rejectTableElement(table, words, "the image storage table exceeds its capacity of " + std::to_string(StorageTableCapacity(context)) + " images"); + continue; + } + CompletePendingCpuWrites(context, resource.baseAddress, static_cast(guestBytes)); + if (resource.dccAddress != 0) CompletePendingCpuWrites(context, resource.dccAddress, DccKeyBytes(guestBytes)); + const auto mip = std::min(resource.baseLevel, resource.mipCount - 1u); + try { + storageTextures.push_back(cachedStorageTexture(context, words, resource, mip, guestBytes)); + } catch (const std::runtime_error& error) { + table.map[faults + element] = rejectTableElement(table, words, error.what()); + continue; + } + storageMips.push_back(mip); + storageKeys.push_back(resource.dccAddress); + storageFirstLayer.push_back(firstLayer); + storageWritten.push_back(true); + storageAtomic.push_back(false); + storageAtomic64.push_back(false); + describedRanges.push_back({"storage", resource.baseAddress, guestBytes, resource.width, resource.height, resource.format, static_cast(resource.tileMode), resource.dccAddress}); + bindings[storageBinding].imageAllocations[absolute] = storageTextures.size() - 1u; + continue; + } + if (item == nullptr) { + table.map[faults + element] = rejectTableElement(table, words, "a sampled image table entry has no sampled image table binding"); + reusable = false; + continue; + } CompletePendingCpuWrites(context, resource.baseAddress, static_cast(guestBytes)); if (resource.dccAddress != 0) CompletePendingCpuWrites(context, resource.dccAddress, DccKeyBytes(guestBytes)); std::shared_ptr texture; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp index f2c1970381..f04ec34921 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp @@ -549,10 +549,10 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi Require(decoration.set && decoration.binding, "unbound shader resource"); const auto key = std::make_pair(*decoration.set, *decoration.binding); const auto binding = std::find_if(shader.bindings.begin(), shader.bindings.end(), [&](const auto& item) { return item.descriptorSet == key.first && item.binding == key.second; }); - Require(descriptors.insert(key).second || (binding != shader.bindings.end() && binding->role == ShaderRecompiler::DescriptorRole::ImageTable), "duplicate SPIR-V resource binding"); + Require(descriptors.insert(key).second || (binding != shader.bindings.end() && (binding->role == ShaderRecompiler::DescriptorRole::ImageTable || binding->role == ShaderRecompiler::DescriptorRole::ImageStorageTable)), "duplicate SPIR-V resource binding"); Require(binding != shader.bindings.end(), "SPIR-V resource is absent from recompiler binding metadata"); Require(binding->kind == ShaderRecompiler::DescriptorKind::Sampler || binding->kind == ShaderRecompiler::DescriptorKind::SampledImage || binding->kind == ShaderRecompiler::DescriptorKind::StorageImage, "SPIR-V descriptor type disagrees with recompiler binding metadata"); - if (binding->role == ShaderRecompiler::DescriptorRole::ImageTable || binding->role == ShaderRecompiler::DescriptorRole::SamplerTable) { + if (binding->role == ShaderRecompiler::DescriptorRole::ImageTable || binding->role == ShaderRecompiler::DescriptorRole::ImageStorageTable || binding->role == ShaderRecompiler::DescriptorRole::SamplerTable) { Require(type.size() == 3 && (type[0] & 0xffffu) == spv::OpTypeRuntimeArray, "image table must be declared as a runtime descriptor array"); continue; } diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp index 43ff32af56..cad4de2bbf 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp @@ -184,6 +184,11 @@ alignas(256) constexpr std::array StoreTableCode{ 0xf42c0802u, 0x24000000u, 0x360200bfu, 0x7e040280u, 0xf0208f08u, 0x00090401u, 0xbf810000u, }; +alignas(256) constexpr std::array StoreLaneTableCode{ + 0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0x8f108202u, 0xf4200444u, 0x20000000u, 0x9312b011u, + 0xf42c0802u, 0x24000000u, 0x360200bfu, 0x7e040280u, 0x7e080301u, 0xf0208f08u, 0x00090401u, 0xbf810000u, +}; + alignas(256) constexpr std::array StoreTableCutCode{ 0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0x8f108202u, 0xf4200444u, 0x20000000u, 0x9312b011u, 0xf42c0802u, 0x24000000u, 0x360200bfu, 0x7e040280u, 0x7da400ffu, 0x00001000u, 0xf0208f08u, 0x00090401u, @@ -368,7 +373,7 @@ Outcome Execute(AgcDriver::VulkanDevice& device, std::span outcome.cacheable = AgcDriver::DriverDetail::CacheableResult(result); outcome.imageTableReadRanges = result.imageTableReadRanges; for (const auto& binding : result.bindings) { - if (binding.role == ShaderRecompiler::DescriptorRole::ImageTable) outcome.imageTableCount = binding.count; + if (binding.role == ShaderRecompiler::DescriptorRole::ImageTable || binding.role == ShaderRecompiler::DescriptorRole::ImageStorageTable) outcome.imageTableCount = binding.count; if (binding.role == ShaderRecompiler::DescriptorRole::GuestSamplers) outcome.samplers = SamplerWords(binding.guestDescriptor); if (binding.role == ShaderRecompiler::DescriptorRole::SamplerTable) outcome.samplerTable = SamplerWords(binding.guestDescriptor); } @@ -988,15 +993,37 @@ void RunModeTests(AgcDriver::VulkanDevice& device) { } void RunStoreTests(AgcDriver::VulkanDevice& device) { + struct Registration { + void* data; + Registration(void* data, std::size_t bytes) : data(data) { GuestAllocations::Mutation().Add(data, bytes, true, true, true); } + ~Registration() { GuestAllocations::Mutation().Remove(data); } + } registeredUint(UintTexels.data(), sizeof(UintTexels)), registeredFloat(FloatTexels.data(), sizeof(FloatTexels)), registeredSint(SintTexels.data(), sizeof(SintTexels)); SetClassRecords(); - const auto saved = UintTexels; + const auto savedUint = UintTexels; + const auto savedFloat = FloatTexels; + const auto savedSint = SintTexels; + const auto untouched = [&] { return UintTexels == savedUint && FloatTexels == savedFloat && SintTexels == savedSint; }; Keys = {}; Keys[0] = 1; const auto cut = Run(device, StoreTableCutCode, 1, Lanes, 64); + AgcDriver::Graphics::StorageTexture::FlushAllPending("image table store test"); Require(!Faulted(cut), "image table store: a store with EXEC clear faulted:\n" + cut.log); - const auto stored = Run(device, StoreTableCode, 1, Lanes, 64); - Require(Faulted(stored) && stored.log.find("unsupported operation") != std::string::npos, "image table store: an image_store through a table did not fault as an unsupported operation:\n" + stored.log); - Require(UintTexels == saved, "image table store: an unsupported store reached the image"); + Require(untouched(), "image table store: a store with EXEC clear reached an image"); + const auto stored = Run(device, StoreLaneTableCode, 1, Lanes, 64); + AgcDriver::Graphics::StorageTexture::FlushAllPending("image table store test"); + Require(!Faulted(stored), "image table store: a store through a table faulted:\n" + stored.log); + Require(stored.imageTableCount == 2u, "image table store: the storage table does not hold one element per distinct T# (" + std::to_string(stored.imageTableCount) + ")"); + for (std::uint32_t x = 0; x < Width; ++x) Require(UintTexels[x] == x, "image table store: texel " + std::to_string(x) + " of the selected image is " + Hex(UintTexels[x]) + ", expected " + Hex(x)); + Require(std::equal(UintTexels.begin() + Width, UintTexels.end(), savedUint.begin() + Width), "image table store: a store to row 0 reached another row"); + Require(FloatTexels == savedFloat && SintTexels == savedSint, "image table store: a store through key 1 reached another entry's image"); + UintTexels = savedUint; + const auto restored = Run(device, StoreTableCutCode, 1, Lanes, 64); + AgcDriver::Graphics::StorageTexture::FlushAllPending("image table store test"); + Require(!Faulted(restored) && untouched(), "image table store: restoring the image failed:\n" + restored.log); + Keys[0] = 6; + const auto null = Run(device, StoreLaneTableCode, 1, Lanes, 64); + AgcDriver::Graphics::StorageTexture::FlushAllPending("image table store test"); + Require(untouched(), "image table store: a store through a null entry reached an image:\n" + null.log); } void RunPointerTests(AgcDriver::VulkanDevice& device) { diff --git a/core/shader/recompiler/Optimization/src/BindingAllocator.cpp b/core/shader/recompiler/Optimization/src/BindingAllocator.cpp index 209bf16a9a..1f6c140f8d 100644 --- a/core/shader/recompiler/Optimization/src/BindingAllocator.cpp +++ b/core/shader/recompiler/Optimization/src/BindingAllocator.cpp @@ -187,6 +187,9 @@ BindingAllocationResult BindingAllocator::Allocate(IrProgram& program, const Bin if (imageTables) { addBinding(next, DescriptorBindingKind::ImageTable); } + if (std::ranges::any_of(info.images, [](const ImageResource& image) { return image.table != NoTable && image.resourceClass == ImageResourceClass::Storage && image.tableOperation == TableOperation::Supported; })) { + addBinding(next, DescriptorBindingKind::ImageStorageTable); + } if (next.ShaderDataDwords() != 0u && !next.UsesPushData()) { addBinding(next, DescriptorBindingKind::ShaderData); diff --git a/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp b/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp index fb6e05b76f..15214ec67c 100644 --- a/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp +++ b/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp @@ -41,6 +41,9 @@ DescriptorKind PhysicalKindFor(DescriptorBindingKind kind) { if (kind == DescriptorBindingKind::ImageTable) { return DescriptorKind::SampledImage; } + if (kind == DescriptorBindingKind::ImageStorageTable) { + return DescriptorKind::StorageImage; + } const ImageResourceClass imageClass = ImageBindingResourceClass(kind); if (imageClass == ImageResourceClass::Sampled) { return DescriptorKind::SampledImage; @@ -82,6 +85,9 @@ DescriptorRole RoleFor(DescriptorBindingKind kind) { if (kind == DescriptorBindingKind::ImageTableMap) { return DescriptorRole::ImageTableMap; } + if (kind == DescriptorBindingKind::ImageStorageTable) { + return DescriptorRole::ImageStorageTable; + } if (ImageBindingResourceClass(kind) != ImageResourceClass::None) { return DescriptorRole::GuestImages; } @@ -494,7 +500,7 @@ DescriptorBindingPlan DescriptorBindingBuilder::Prepare(const IrBindingLayout& l } } for (auto& binding : plan.bindings) { - if (binding.descriptor.role == DescriptorRole::ImageTable) binding.tableSamplerMasks = samplerMasks; + if (binding.descriptor.role == DescriptorRole::ImageTable || binding.descriptor.role == DescriptorRole::ImageStorageTable) binding.tableSamplerMasks = samplerMasks; } return plan; } @@ -563,6 +569,7 @@ void DescriptorBindingBuilder::Populate(BindingAllocationResult& allocation, con } break; case DescriptorRole::ImageTable: + case DescriptorRole::ImageStorageTable: if (tables == nullptr) fail("DescriptorBindingBuilder::Populate image table elements were not resolved"); physical.guestDescriptor.reserve(tables->elements.size() * 8u); for (const auto& element : tables->elements) { @@ -571,7 +578,7 @@ void DescriptorBindingBuilder::Populate(BindingAllocationResult& allocation, con shape.dimension = element.dimension; physical.imageShapes.push_back(ImageShapeForResource(shape)); physical.imageDepthCompare.push_back(element.depthCompare); - physical.imageWritten.push_back(false); + physical.imageWritten.push_back(ImageBindingResourceClass(element.kind) == ImageResourceClass::Storage); physical.imageAtomic.push_back(false); std::uint32_t samplers = 0; for (std::uint32_t sampler = 0; sampler < entry.tableSamplerMasks.size(); ++sampler) { diff --git a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp index ff8686cb2f..dadcb6869f 100644 --- a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp @@ -1502,7 +1502,8 @@ void ResourceMaterializer::ApplyStaticInterface(IrProgram& program, bool nativeS if (image.table == NoTable) continue; tables = true; const auto modes = RuntimeImageModes(image); - if (modes.empty() || modes.size() > ImageTableAbi::MaxModes) image.tableOperation = TableOperation::Unsupported; + const bool storage = image.resourceClass == ImageResourceClass::Storage; + if (modes.empty() || modes.size() > ImageTableAbi::MaxModes || (storage && image.mipMode == ImageMipMode::DynamicStorage)) image.tableOperation = TableOperation::Unsupported; } for (const auto& pair : resources.info.sampledPairs) { if (pair.image >= images.size() || pair.sampler >= resources.info.samplers.size()) throw std::runtime_error("static sampled pair is out of range"); diff --git a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp index 3900471a2f..0ca144b9d0 100644 --- a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp @@ -1358,8 +1358,7 @@ private: continue; } image.table = next++; - const bool storage = image.resourceClass == ImageResourceClass::Storage || image.written || image.atomic; - if (storage || image.packed || image.byElements != 0u) { + if (image.atomic || image.packed || image.byElements != 0u) { image.tableOperation = TableOperation::Unsupported; } } diff --git a/core/shader/recompiler/Recompiler.hpp b/core/shader/recompiler/Recompiler.hpp index 123c488f40..628d33d47b 100644 --- a/core/shader/recompiler/Recompiler.hpp +++ b/core/shader/recompiler/Recompiler.hpp @@ -394,7 +394,8 @@ enum class DescriptorRole { ShaderData, ImageTable, SamplerTable, - ImageTableMap + ImageTableMap, + ImageStorageTable }; struct DescriptorBinding { diff --git a/core/shader/recompiler/RuntimeAbi.hpp b/core/shader/recompiler/RuntimeAbi.hpp index ce3489c14f..8fe2cea3f1 100644 --- a/core/shader/recompiler/RuntimeAbi.hpp +++ b/core/shader/recompiler/RuntimeAbi.hpp @@ -9,7 +9,7 @@ namespace ShaderRecompiler::RuntimeAbi { -inline constexpr std::uint32_t Version = 13u; +inline constexpr std::uint32_t Version = 14u; inline constexpr std::uint32_t DescriptorSet = 0u; inline constexpr std::uint32_t FragmentDescriptorSet = 1u; inline constexpr std::uint32_t StageCount = 4u; @@ -30,7 +30,8 @@ enum class Binding : std::uint32_t { SamplerTable = 63u, ImageTableMap = 64u, Count = 65u, - ImageTable = 256u + ImageTable = 256u, + ImageStorageTable = 257u }; enum class Stage : std::uint32_t { Main, Fragment, TessellationControl, TessellationEvaluation }; @@ -96,10 +97,12 @@ inline void RequireVersion(std::uint32_t version) { if (version != Version) throw std::runtime_error("Shader runtime ABI: incompatible version"); } -inline constexpr std::uint32_t ImageTableBinding = StageCount * static_cast(Binding::Count); +inline constexpr std::uint32_t ImageStorageTableBinding = StageCount * static_cast(Binding::Count); +inline constexpr std::uint32_t ImageTableBinding = ImageStorageTableBinding + 1u; inline std::uint32_t BindingNumber(Stage stage, Binding binding) { if (binding == Binding::ImageTable) return ImageTableBinding; + if (binding == Binding::ImageStorageTable) return ImageStorageTableBinding; const auto group = static_cast(stage); const auto index = static_cast(binding); const auto count = static_cast(Binding::Count); @@ -108,7 +111,7 @@ inline std::uint32_t BindingNumber(Stage stage, Binding binding) { } static_assert(FirstImageBinding + ImageBindingCount == static_cast(Binding::Samplers)); -static_assert(static_cast(Binding::ImageTable) > static_cast(Binding::Count) && ImageTableBinding == 260u); +static_assert(static_cast(Binding::ImageTable) > static_cast(Binding::Count) && ImageStorageTableBinding == 260u && ImageTableBinding == 261u); } diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvDescriptors.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvDescriptors.cpp index 10a18de9ff..843f1f1824 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvDescriptors.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvDescriptors.cpp @@ -75,9 +75,19 @@ void DecorateTableNonUniform(SpirvEmitterState& state, std::uint32_t id) { if (!state.tableIndexNonUniform) return; state.module.EmitCapability(spv::CapabilityShaderNonUniform); state.module.EmitCapability(spv::CapabilitySampledImageArrayNonUniformIndexing); + if (std::any_of(state.tableImageVariables.begin(), state.tableImageVariables.end(), [](const SpirvTableImageVariable& variable) { return ImageBindingResourceClass(variable.kind) == ImageResourceClass::Storage; })) state.module.EmitCapability(spv::CapabilityStorageImageArrayNonUniformIndexing); state.module.AddAnnotation(spv::OpDecorate, id, spv::DecorationNonUniform); } +std::uint32_t TableStorageImagePointer(SpirvEmitterState& state, DescriptorBindingKind kind, std::uint32_t resource, std::uint32_t pointerType) { + const auto found = std::find_if(state.tableImageVariables.begin(), state.tableImageVariables.end(), [&](const SpirvTableImageVariable& variable) { return variable.kind == kind; }); + if (found == state.tableImageVariables.end() || !state.tableAccess.active) ExitDescriptorBindingFailure(state, kind, resource, "image storage table array was not emitted for the mode"); + const auto pointer = state.module.AllocateId(); + state.module.AddFunction(spv::OpAccessChain, pointerType, pointer, found->variable, state.tableAccess.element); + DecorateTableNonUniform(state, pointer); + return pointer; +} + bool TableModeEmitted(const ImageResource& mode) { return RdnaImageDimensionInfoFor(mode.dimension).multisampled == 0u && mode.packedFormat != IrBufferFormat::Fmask8_S2_F1; } @@ -216,8 +226,9 @@ std::uint32_t StorageImageDescriptorPointer(SpirvEmitterState& state, std::uint3 FailEmit("storage image descriptor requested for a non-storage image"); } const auto kind = DescriptorBindingForImage(image); - const auto arrayIndex = ResourceForDescriptor(state, kind, resource); const auto pointerType = state.module.Type(spv::OpTypePointer, spv::StorageClassUniformConstant, ImageType(state, image)); + if (image.table != NoTable) return TableStorageImagePointer(state, kind, resource, pointerType); + const auto arrayIndex = ResourceForDescriptor(state, kind, resource); const auto variable = state.imageVariables.at(ImageBindingIndex(kind)); return DescriptorElementPointer(state, pointerType, variable, arrayIndex, kind, resource, "storage image descriptor array was not emitted"); } @@ -231,9 +242,17 @@ void EmitStorageImageWrite(SpirvEmitterState& state, std::uint32_t resource, std state.module.EmitCapability(spv::CapabilityStorageImageWriteWithoutFormat); } const auto kind = DescriptorBindingForImage(image); - const auto arrayIndex = ResourceForDescriptor(state, kind, resource); const auto imageType = ImageType(state, image); const auto pointerType = state.module.Type(spv::OpTypePointer, spv::StorageClassUniformConstant, imageType); + if (image.table != NoTable) { + if (RdnaImageDimensionInfoFor(image.dimension).multisampled != 0u || image.mipMode == ImageMipMode::DynamicStorage) FailEmit("an image table store to a multisampled image or through dynamic mips is not implemented"); + const auto descriptor = state.module.AllocateId(); + state.module.AddFunction(spv::OpLoad, imageType, descriptor, TableStorageImagePointer(state, kind, resource, pointerType)); + DecorateTableNonUniform(state, descriptor); + state.module.AddFunction(spv::OpImageWrite, descriptor, coord, texel); + return; + } + const auto arrayIndex = ResourceForDescriptor(state, kind, resource); const auto variable = state.imageVariables.at(ImageBindingIndex(kind)); const auto loadAt = [&](std::uint32_t index) { const auto pointer = DescriptorElementPointer(state, pointerType, variable, index, kind, resource, "storage image descriptor array was not emitted"); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp index 9b36762390..2f44307dc3 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp @@ -471,17 +471,20 @@ void DefineDescriptors(SpirvEmitterState& state) { state.module.AddAnnotation(spv::OpDecorate, state.gdsVariable, spv::DecorationCoherent); break; case DescriptorBindingKind::ImageTable: + case DescriptorBindingKind::ImageStorageTable: RequireTableIndexing(state); + if (binding.kind == DescriptorBindingKind::ImageStorageTable) state.module.EmitCapability(spv::CapabilityStorageImageArrayDynamicIndexing); for (std::uint32_t resource = 0; resource < state.program.Info().images.size(); ++resource) { const auto& image = state.program.Info().images[resource]; if (image.table == NoTable) continue; for (const auto& mode : state.program.Info().runtimeImageModes.at(resource)) { if (!TableModeEmitted(mode)) continue; const auto kind = DescriptorBindingForImage(mode); + if ((ImageBindingResourceClass(kind) == ImageResourceClass::Storage) != (binding.kind == DescriptorBindingKind::ImageStorageTable)) continue; if (std::any_of(state.tableImageVariables.begin(), state.tableImageVariables.end(), [&](const SpirvTableImageVariable& variable) { return variable.kind == kind; })) continue; const auto name = "table_images_" + std::to_string(static_cast(kind)); state.tableImageVariables.push_back({kind, Define(state.module.Type(spv::OpTypeRuntimeArray, ImageType(state, mode)), name.c_str(), spv::StorageClassUniformConstant)}); - if (mode.dimension == RdnaImageDimension::Dim1D || mode.dimension == RdnaImageDimension::Dim1DArray) state.module.EmitCapability(spv::CapabilitySampled1D); + if (mode.dimension == RdnaImageDimension::Dim1D || mode.dimension == RdnaImageDimension::Dim1DArray) state.module.EmitCapability(binding.kind == DescriptorBindingKind::ImageStorageTable ? spv::CapabilityImage1D : spv::CapabilitySampled1D); } } break; From cb56c64b012149100ce8e42a6161162f1a4608b9 Mon Sep 17 00:00:00 2001 From: Alex Collini Date: Thu, 15 Oct 2026 10:15:50 +0200 Subject: [PATCH 0657/1000] test(agc): expect image_store through a table to bind written storage elements The shader memory registration test pinned the old refusal. It now checks that the store's table is supported, does not fault, and binds every element as written on the storage table binding, and that a store-only table binds no sampled elements. The driver no longer requires a written flag per table element, so bindings that carry none read as unwritten. Co-Authored-By: Claude Opus 5.5 --- .../prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp | 2 +- core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index 55cf3ffbde..7b3ae8e5ec 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -1583,7 +1583,7 @@ void ShaderResources::buildPrepare(std::span shaders, cons Require(binding.descriptorSet == ShaderRecompiler::RuntimeAbi::DescriptorSet || (shader.stage == ShaderRecompiler::ShaderStage::Fragment && binding.descriptorSet == ShaderRecompiler::RuntimeAbi::FragmentDescriptorSet), "unexpected descriptor set: a shader resource must use its stage's descriptor set"); if (binding.role == ShaderRecompiler::DescriptorRole::ImageTable || binding.role == ShaderRecompiler::DescriptorRole::ImageStorageTable) { const bool storage = binding.role == ShaderRecompiler::DescriptorRole::ImageStorageTable; - Require(binding.binding == (storage ? ShaderRecompiler::RuntimeAbi::ImageStorageTableBinding : ShaderRecompiler::RuntimeAbi::ImageTableBinding) && binding.kind == (storage ? ShaderRecompiler::DescriptorKind::StorageImage : ShaderRecompiler::DescriptorKind::SampledImage) && binding.guestDescriptor.size() == static_cast(binding.count) * 8u && binding.imageShapes.size() == binding.count && binding.imageDepthCompare.size() == binding.count && binding.imageSamplers.size() == binding.count && binding.imageWritten.size() == binding.count, "image table binding is malformed"); + Require(binding.binding == (storage ? ShaderRecompiler::RuntimeAbi::ImageStorageTableBinding : ShaderRecompiler::RuntimeAbi::ImageTableBinding) && binding.kind == (storage ? ShaderRecompiler::DescriptorKind::StorageImage : ShaderRecompiler::DescriptorKind::SampledImage) && binding.guestDescriptor.size() == static_cast(binding.count) * 8u && binding.imageShapes.size() == binding.count && binding.imageDepthCompare.size() == binding.count && binding.imageSamplers.size() == binding.count, "image table binding is malformed"); auto& table = tableStage(shader); Require(storage ? !table.storage : !table.sampled, "a stage binds two image tables"); Require(table.images == nullptr || table.images->count == binding.count, "a stage's image tables disagree on their elements"); diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 6ad15f3495..7eefa3fa10 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -1587,11 +1587,12 @@ void verifyImageTableRegistration() { const std::vector storeCode{0xf4080100u, 0xfa000000u, 0xf4080700u, 0xfa000030u, 0x7e200500u, 0x8f108510u, 0xf42c0502u, 0x20000000u, 0xf0200f08u, 0x00050200u, 0xbf810000u}; const auto store = CompileTable(TableRequest(storeCode, userData)); const auto& storePlan = *store.capture->plan; - require(storePlan.info.images.size() == 1u && storePlan.info.images[0].table == 0u && storePlan.info.images[0].tableOperation == TableOperation::Unsupported, "image table registration: a table T# feeding image_store was not an unsupported table operation"); + require(storePlan.info.images.size() == 1u && storePlan.info.images[0].table == 0u && storePlan.info.images[0].tableOperation == TableOperation::Supported, "image table registration: a table T# feeding image_store was not a supported table operation"); const auto& storeMap = TableMap(*store.result); const auto storeFault = storeMap[Abi::TableHeader(0) + Abi::TableFault]; - require((storeFault & Abi::PoisonFlag) != 0u && store.result->imageTablePoison.at(storeFault & ~Abi::PoisonFlag).reason == static_cast(Abi::PoisonReason::UnsupportedOperation) && store.result->imageTableFaults != 0u, "image table registration: the store's table does not fault on use"); - require(TableBinding(*store.result, DescriptorRole::ImageTable) == nullptr || TableBinding(*store.result, DescriptorRole::ImageTable)->count == 0u, "image table registration: an unsupported table bound elements"); + const auto* storeTable = TableBinding(*store.result, DescriptorRole::ImageStorageTable); + require((storeFault & Abi::PoisonFlag) == 0u && storeTable != nullptr && storeTable->count != 0u && std::all_of(storeTable->imageWritten.begin(), storeTable->imageWritten.end(), [](bool written) { return written; }), "image table registration: the store's table was not bound as written storage elements"); + require(TableBinding(*store.result, DescriptorRole::ImageTable) == nullptr || TableBinding(*store.result, DescriptorRole::ImageTable)->count == 0u, "image table registration: a store-only table bound sampled elements"); const std::vector compareCode{0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080700u, 0xfa000030u, 0x7e200500u, 0x8f108510u, 0xf42c0502u, 0x20000000u, 0xf0fc0108u, 0x00450000u, 0xe0700000u, 0x80070000u, 0xbf810000u}; auto compareRequest = TableRequest(compareCode, userData); From 35a12f5c5cf155a10e8a845e837dde01c3360a62 Mon Sep 17 00:00:00 2001 From: Alex Collini Date: Thu, 15 Oct 2026 17:30:12 +0200 Subject: [PATCH 0658/1000] fix(agc): size the pool for the storage table and check table store elements like direct ones The non-split descriptor set's pool now counts the storage table binding's descriptors, as the split path already did. A table storage element whose minimum LOD clamps above the level it addresses is rejected per element, as the direct path refuses it, and the element is looked up as written so a depth surface under it is retired. The registration test checks again that image_store_mip, image_store_pck and image_atomic_add through a table stay unsupported operations that fault on use. Co-Authored-By: Claude Opus 5.5 --- .../libSceAgcDriver/Graphics/src/ShaderResources.cpp | 9 +++++++-- core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp | 12 ++++++++++++ docs/dev/debt/libSceAgcDriver.md | 1 + 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index 7b3ae8e5ec..76eddf4648 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -1804,7 +1804,8 @@ void ShaderResources::buildPrepare(std::span shaders, cons if (storageBuffers != 0) sizes.push_back({VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, static_cast(storageBuffers)}); const auto tableDescriptors = tableBindings[0] != std::numeric_limits::max() ? tableDescriptorCount(0) : 0u; if (plannedSampledImages + tableDescriptors != 0) sizes.push_back({VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, plannedSampledImages + tableDescriptors}); - if (plannedStorageImages != 0) sizes.push_back({VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, plannedStorageImages}); + const auto storageTableDescriptors = storageTableBindings[0] != std::numeric_limits::max() ? bindings[storageTableBindings[0]].layout.descriptorCount : 0u; + if (plannedStorageImages + storageTableDescriptors != 0) sizes.push_back({VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, plannedStorageImages + storageTableDescriptors}); std::uint32_t samplerDescriptors = 0; for (const auto& binding : bindings) { if (binding.layout.descriptorType == VK_DESCRIPTOR_TYPE_SAMPLER) samplerDescriptors += binding.layout.descriptorCount; @@ -4311,8 +4312,12 @@ void ShaderResources::resolveImageTables() { CompletePendingCpuWrites(context, resource.baseAddress, static_cast(guestBytes)); if (resource.dccAddress != 0) CompletePendingCpuWrites(context, resource.dccAddress, DccKeyBytes(guestBytes)); const auto mip = std::min(resource.baseLevel, resource.mipCount - 1u); + if (resource.minLod > mip * 256u) { + table.map[faults + element] = rejectTableElement(table, words, "the table entry's storage T# clamps its minimum LOD above the level it addresses, which is not implemented"); + continue; + } try { - storageTextures.push_back(cachedStorageTexture(context, words, resource, mip, guestBytes)); + storageTextures.push_back(cachedStorageTexture(context, words, resource, mip, guestBytes, true)); } catch (const std::runtime_error& error) { table.map[faults + element] = rejectTableElement(table, words, error.what()); continue; diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 7eefa3fa10..d824397cf6 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -1593,6 +1593,18 @@ void verifyImageTableRegistration() { const auto* storeTable = TableBinding(*store.result, DescriptorRole::ImageStorageTable); require((storeFault & Abi::PoisonFlag) == 0u && storeTable != nullptr && storeTable->count != 0u && std::all_of(storeTable->imageWritten.begin(), storeTable->imageWritten.end(), [](bool written) { return written; }), "image table registration: the store's table was not bound as written storage elements"); require(TableBinding(*store.result, DescriptorRole::ImageTable) == nullptr || TableBinding(*store.result, DescriptorRole::ImageTable)->count == 0u, "image table registration: a store-only table bound sampled elements"); + const std::array, 3> unsupportedStores{{{0xf0240f08u, "image_store_mip"}, {0xf0280f08u, "image_store_pck"}, {0xf0440108u, "image_atomic_add"}}}; + for (const auto& [instruction, name] : unsupportedStores) { + auto code = storeCode; + code[8] = instruction; + const auto unsupported = CompileTable(TableRequest(code, userData)); + const auto& plan = *unsupported.capture->plan; + require(plan.info.images.size() == 1u && plan.info.images[0].table == 0u && (plan.info.images[0].tableOperation == TableOperation::Unsupported || plan.info.images[0].mipMode == ImageMipMode::DynamicStorage), (std::string("image table registration: a table T# feeding ") + name + " was not planned as an unsupported or dynamic-mip table store").c_str()); + const auto fault = TableMap(*unsupported.result)[Abi::TableHeader(0) + Abi::TableFault]; + require((fault & Abi::PoisonFlag) != 0u && unsupported.result->imageTablePoison.at(fault & ~Abi::PoisonFlag).reason == static_cast(Abi::PoisonReason::UnsupportedOperation) && unsupported.result->imageTableFaults != 0u, (std::string("image table registration: the ") + name + " table does not fault on use").c_str()); + const auto* storage = TableBinding(*unsupported.result, DescriptorRole::ImageStorageTable); + require(storage == nullptr || storage->count == 0u, (std::string("image table registration: the unsupported ") + name + " bound storage table elements").c_str()); + } const std::vector compareCode{0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080700u, 0xfa000030u, 0x7e200500u, 0x8f108510u, 0xf42c0502u, 0x20000000u, 0xf0fc0108u, 0x00450000u, 0xe0700000u, 0x80070000u, 0xbf810000u}; auto compareRequest = TableRequest(compareCode, userData); diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index ad0c93ad2b..cb3c84a5a1 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -148,6 +148,7 @@ - [VertexBufferReadSize](../../../core/libs/prx/libSceAgcDriver/Graphics/include/VertexInput.hpp) and the [vertex staging copy](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver) - a vertex fetch whose index is at or past the descriptor's `num_records` no longer throws for `OOB_SELECT` 0 and 1. The copy still spans the whole fetch, but the bytes from `num_records * stride` onward read as zeros, so the out-of-range vertex is `(0, 0, 0, 0)`. Measured with `buffer_load_format_xyzw` (`idxen`) through the [hardware oracle](../HW_ORACLE.md) on RDNA2 gfx1036 for PPSA01325's word 3 `0x0004dfac` (`OOB_SELECT` 0) in #2241: indices 3, 4 and 5 over 3 records each return `00000000 00000000 00000000 00000000`, W included, unlike Vulkan `robustBufferAccess`, which returns `(0, 0, 0, 1)`. The zeros come from the record check, not from memory: stored values placed past the records are ignored, and a `num_records` 6 control returns them. `RESOURCE_LEVEL` 1, `OOB_SELECT` 1 and FORMAT 74 `32_32_32_FLOAT` give the same zeros, in wave32 and wave64; gfx1013 (the console) is not measured. `OOB_SELECT` 2 returns the stored values past the records and `OOB_SELECT` 3 compares `num_records` against the byte offset, both measured on the same setup in #2241; the two keep throwing past `num_records` here, since each needs its own rule. Zero-stride descriptors (`stride` 0) keep their separate byte-count check and are not covered here. Each out-of-range fetch is copied alone and outside the draw-snapshot cache, which is neither read nor written for it: a reused snapshot would carry another draw's bytes into the zero region, and a stored zero-padded copy would hand zeros to a later in-range draw at the same address. The valid prefix is still flushed for GPU writes before it is read. - [Draw snapshot buffer descriptors](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) rewrite each affected array from element zero, preserving unchanged descriptors. MoltenVK 1.4.2 writes auxiliary buffer lengths at index `i` instead of `start + i` for partial array updates ([MVKDescriptorSet.mm, BufferAuxSize](https://github.com/KhronosGroup/MoltenVK/blob/v1.4.2/MoltenVK/MoltenVK/GPUObjects/MVKDescriptorSet.mm#L1051)). Per-element writes therefore let one buffer length replace another even when Vulkan buffer handles and ranges are correct. - The [driver](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) binds at most 262144 table images per pipeline, fewer under lower sampled-image limits; the rest fault when selected. A device without runtime descriptor arrays or variable-count bindings throws. +- [Image storage tables](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - every bound element of a storage table is marked written and stored back after each dispatch, whether or not a lane wrote it. Two elements over the same bytes are therefore both stored back, and the order of their write-backs decides the result: PPSA26344's compute 0x80001e5f00 binds the stencil plane at 0x505f350000 twice, as R8_UINT in its own tile mode and as R8_UNORM in another. Rejecting the second writer of the same bytes per element, and per-element written bits from the GPU, are not implemented. The storage table binding is declared with its full capacity, so a set that holds one does not fit a chain descriptor pool and gets a dedicated pool. That capacity is a choice, not a device limit: the smallest of the sampled table capacity, 4096 and a quarter of `maxPerStageDescriptorStorageImages`. The sampled table capacity is not reduced by it, so a device with tight storage-image limits can fail layout validation. A storage element is rejected when its absolute index in the table reaches that capacity, even when few of the table's elements are storage, and it faults when a lane selects it. Atomic, packed, BY2/BY4 and dynamic-mip stores through a table still fault on use. `PresyncSurfaces` walks only the sampled table, so a store-only table's images are not presynced. Tables that mix sampled and storage elements, a T# that is both read and written through a table, non-uniform storage indexing and non-2D storage modes are not covered by tests. - [Image-table S#s](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) reading opaque black borders fault on selection: no runtime BC-swizzle pair check, even for valid swizzles 0/4 with direct or table T#s. Unselected entries do not fault. Table T#/direct S# keeps the direct check: only opaque-black swizzles moving alpha fault. - [Table S#s](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) that clamp to border fault only when selected: four variants cannot encode dynamic integer/float pairing. This also rejects otherwise-valid float pairs. - [Table T#s](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) paired with direct S#s fault when a paired border class differs from the decoded image class. Depth-bit views use float borders. Matching float pairs and inactive mismatches do not fault. From b7e21ca0316f89b5ba8fccdad27ae6fe5ca3821b Mon Sep 17 00:00:00 2001 From: Alex Collini Date: Fri, 16 Oct 2026 23:21:09 +0200 Subject: [PATCH 0659/1000] fix(agc): tell a set without a storage table from a stored element past its capacity Both cases printed that the image storage table exceeds its capacity. They need different fixes, so each now has its own message with the set, the element's index in the set and the capacity. The debt entry now says that the index counts every table element before it in the set. Co-Authored-By: Claude Opus 5.5 --- .../prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp | 8 ++++++-- docs/dev/debt/libSceAgcDriver.md | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index 76eddf4648..ef19234ea4 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -4305,8 +4305,12 @@ void ShaderResources::resolveImageTables() { if (element < binding.imageWritten.size() && binding.imageWritten[element]) { reusable = false; const auto storageBinding = storageTableBindings[table.set]; - if (storageBinding == std::numeric_limits::max() || absolute >= StorageTableCapacity(context)) { - table.map[faults + element] = rejectTableElement(table, words, "the image storage table exceeds its capacity of " + std::to_string(StorageTableCapacity(context)) + " images"); + if (storageBinding == std::numeric_limits::max()) { + table.map[faults + element] = rejectTableElement(table, words, "set " + std::to_string(table.set) + " has no image storage table, but the shader stores to its table element " + std::to_string(absolute)); + continue; + } + if (absolute >= StorageTableCapacity(context)) { + table.map[faults + element] = rejectTableElement(table, words, "the stored table element " + std::to_string(absolute) + " of set " + std::to_string(table.set) + " is past the image storage table's capacity of " + std::to_string(StorageTableCapacity(context)) + " images"); continue; } CompletePendingCpuWrites(context, resource.baseAddress, static_cast(guestBytes)); diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index cb3c84a5a1..5658bc6937 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -148,7 +148,7 @@ - [VertexBufferReadSize](../../../core/libs/prx/libSceAgcDriver/Graphics/include/VertexInput.hpp) and the [vertex staging copy](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver) - a vertex fetch whose index is at or past the descriptor's `num_records` no longer throws for `OOB_SELECT` 0 and 1. The copy still spans the whole fetch, but the bytes from `num_records * stride` onward read as zeros, so the out-of-range vertex is `(0, 0, 0, 0)`. Measured with `buffer_load_format_xyzw` (`idxen`) through the [hardware oracle](../HW_ORACLE.md) on RDNA2 gfx1036 for PPSA01325's word 3 `0x0004dfac` (`OOB_SELECT` 0) in #2241: indices 3, 4 and 5 over 3 records each return `00000000 00000000 00000000 00000000`, W included, unlike Vulkan `robustBufferAccess`, which returns `(0, 0, 0, 1)`. The zeros come from the record check, not from memory: stored values placed past the records are ignored, and a `num_records` 6 control returns them. `RESOURCE_LEVEL` 1, `OOB_SELECT` 1 and FORMAT 74 `32_32_32_FLOAT` give the same zeros, in wave32 and wave64; gfx1013 (the console) is not measured. `OOB_SELECT` 2 returns the stored values past the records and `OOB_SELECT` 3 compares `num_records` against the byte offset, both measured on the same setup in #2241; the two keep throwing past `num_records` here, since each needs its own rule. Zero-stride descriptors (`stride` 0) keep their separate byte-count check and are not covered here. Each out-of-range fetch is copied alone and outside the draw-snapshot cache, which is neither read nor written for it: a reused snapshot would carry another draw's bytes into the zero region, and a stored zero-padded copy would hand zeros to a later in-range draw at the same address. The valid prefix is still flushed for GPU writes before it is read. - [Draw snapshot buffer descriptors](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) rewrite each affected array from element zero, preserving unchanged descriptors. MoltenVK 1.4.2 writes auxiliary buffer lengths at index `i` instead of `start + i` for partial array updates ([MVKDescriptorSet.mm, BufferAuxSize](https://github.com/KhronosGroup/MoltenVK/blob/v1.4.2/MoltenVK/MoltenVK/GPUObjects/MVKDescriptorSet.mm#L1051)). Per-element writes therefore let one buffer length replace another even when Vulkan buffer handles and ranges are correct. - The [driver](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) binds at most 262144 table images per pipeline, fewer under lower sampled-image limits; the rest fault when selected. A device without runtime descriptor arrays or variable-count bindings throws. -- [Image storage tables](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - every bound element of a storage table is marked written and stored back after each dispatch, whether or not a lane wrote it. Two elements over the same bytes are therefore both stored back, and the order of their write-backs decides the result: PPSA26344's compute 0x80001e5f00 binds the stencil plane at 0x505f350000 twice, as R8_UINT in its own tile mode and as R8_UNORM in another. Rejecting the second writer of the same bytes per element, and per-element written bits from the GPU, are not implemented. The storage table binding is declared with its full capacity, so a set that holds one does not fit a chain descriptor pool and gets a dedicated pool. That capacity is a choice, not a device limit: the smallest of the sampled table capacity, 4096 and a quarter of `maxPerStageDescriptorStorageImages`. The sampled table capacity is not reduced by it, so a device with tight storage-image limits can fail layout validation. A storage element is rejected when its absolute index in the table reaches that capacity, even when few of the table's elements are storage, and it faults when a lane selects it. Atomic, packed, BY2/BY4 and dynamic-mip stores through a table still fault on use. `PresyncSurfaces` walks only the sampled table, so a store-only table's images are not presynced. Tables that mix sampled and storage elements, a T# that is both read and written through a table, non-uniform storage indexing and non-2D storage modes are not covered by tests. +- [Image storage tables](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - every bound element of a storage table is marked written and stored back after each dispatch, whether or not a lane wrote it. Two elements over the same bytes are therefore both stored back, and the order of their write-backs decides the result: PPSA26344's compute 0x80001e5f00 binds the stencil plane at 0x505f350000 twice, as R8_UINT in its own tile mode and as R8_UNORM in another. Rejecting the second writer of the same bytes per element, and per-element written bits from the GPU, are not implemented. The storage table binding is declared with its full capacity, so a set that holds one does not fit a chain descriptor pool and gets a dedicated pool. That capacity is a choice, not a device limit: the smallest of the sampled table capacity, 4096 and a quarter of `maxPerStageDescriptorStorageImages`. The sampled table capacity is not reduced by it, so a device with tight storage-image limits can fail layout validation. A storage element is rejected when its absolute index in the set reaches that capacity, even when few of the set's table elements are storage: the index counts every table element before it in the set, sampled ones included, and the element faults when a lane selects it. Atomic, packed, BY2/BY4 and dynamic-mip stores through a table still fault on use. `PresyncSurfaces` walks only the sampled table, so a store-only table's images are not presynced. Tables that mix sampled and storage elements, a T# that is both read and written through a table, non-uniform storage indexing and non-2D storage modes are not covered by tests. - [Image-table S#s](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) reading opaque black borders fault on selection: no runtime BC-swizzle pair check, even for valid swizzles 0/4 with direct or table T#s. Unselected entries do not fault. Table T#/direct S# keeps the direct check: only opaque-black swizzles moving alpha fault. - [Table S#s](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) that clamp to border fault only when selected: four variants cannot encode dynamic integer/float pairing. This also rejects otherwise-valid float pairs. - [Table T#s](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) paired with direct S#s fault when a paired border class differs from the decoded image class. Depth-bit views use float borders. Matching float pairs and inactive mismatches do not fault. From 676ee71c34cce6114b28a2e66ef973f95637a20e Mon Sep 17 00:00:00 2001 From: milescoviello Date: Sat, 17 Oct 2026 06:39:51 +0000 Subject: [PATCH 0660/1000] fix(libkernel): treat a null deadline as an untimed lock in the timed pthread locks pthread_rwlock_timedrdlock, pthread_rwlock_timedwrlock and pthread_mutex_timedlock threw for a null abstime. FreeBSD's libthr passes a null abstime on to the untimed kernel lock, so these calls then wait like pthread_rwlock_rdlock, pthread_rwlock_wrlock and pthread_mutex_lock. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libkernel/CMakeLists.txt | 10 +++ .../prx/libkernel/Pthread/Posix/Mutex.cpp | 2 +- .../prx/libkernel/Pthread/Posix/Rwlock.cpp | 6 +- core/libs/tests/GuestPthreadRwlock.cpp | 7 +- core/libs/tests/GuestPthreadTimedLocks.cpp | 84 +++++++++++++++++++ 5 files changed, 101 insertions(+), 8 deletions(-) create mode 100644 core/libs/tests/GuestPthreadTimedLocks.cpp diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index ace5ed5a9d..a5319d1b55 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -642,6 +642,16 @@ if(BUILD_TESTING) set_tests_properties(guest_semaphore PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_pthread_timed_locks_tests ${LIBS_INCLUDE_DIR}/tests/GuestPthreadTimedLocks.cpp) + target_include_directories(guest_pthread_timed_locks_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_pthread_timed_locks_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_pthread_timed_locks_tests) + add_test(NAME guest_pthread_timed_locks COMMAND guest_pthread_timed_locks_tests) + set_tests_properties(guest_pthread_timed_locks PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_pthread_timed_locks PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_pthread_rwlock_tests ${LIBS_INCLUDE_DIR}/tests/GuestPthreadRwlock.cpp) target_include_directories(guest_pthread_rwlock_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_pthread_rwlock_tests PRIVATE libkernel libc) diff --git a/core/libs/prx/libkernel/Pthread/Posix/Mutex.cpp b/core/libs/prx/libkernel/Pthread/Posix/Mutex.cpp index fbe5145666..07e2f10d9f 100644 --- a/core/libs/prx/libkernel/Pthread/Posix/Mutex.cpp +++ b/core/libs/prx/libkernel/Pthread/Posix/Mutex.cpp @@ -62,7 +62,7 @@ int APS5_VABI pthread_mutex_lock_nid_postfix(PthreadMutex* mutex) { } int APS5_VABI pthread_mutex_timedlock_nid_postfix(PthreadMutex* mutex, const KernelTimespec* abstime) { - if (!abstime) throw std::runtime_error("pthread_mutex_timedlock: null abstime"); + if (!abstime) return pthread_mutex_lock_nid_postfix(mutex); _initializeStatic(mutex, __func__); if (scePthreadMutexTrylock(mutex) == 0) return 0; KernelUseconds usec = 0; diff --git a/core/libs/prx/libkernel/Pthread/Posix/Rwlock.cpp b/core/libs/prx/libkernel/Pthread/Posix/Rwlock.cpp index b64d1359b1..1faef35844 100644 --- a/core/libs/prx/libkernel/Pthread/Posix/Rwlock.cpp +++ b/core/libs/prx/libkernel/Pthread/Posix/Rwlock.cpp @@ -36,6 +36,8 @@ void initializeStatic(PthreadRwlock* rwlock, const char* funcName) { extern "C" { +int APS5_VABI pthread_rwlock_wrlock_nid_postfix(PthreadRwlock* rwlock); + int APS5_VABI pthread_rwlock_destroy_nid_postfix(PthreadRwlock* rwlock) { if (!rwlock) throw std::runtime_error("pthread_rwlock_destroy: null rwlock"); if (*rwlock == nullptr) return 0; @@ -60,7 +62,7 @@ int APS5_VABI pthread_rwlock_rdlock_nid_postfix(PthreadRwlock* rwlock) { } int APS5_VABI pthread_rwlock_timedrdlock_nid_postfix(PthreadRwlock* rwlock, const KernelTimespec* abstime) { - if (!abstime) throw std::runtime_error("pthread_rwlock_timedrdlock: null abstime"); + if (!abstime) return pthread_rwlock_rdlock_nid_postfix(rwlock); initializeStatic(rwlock, __func__); if (scePthreadRwlockTryrdlock(rwlock) == 0) return 0; KernelUseconds usec = 0; @@ -69,7 +71,7 @@ int APS5_VABI pthread_rwlock_timedrdlock_nid_postfix(PthreadRwlock* rwlock, cons } int APS5_VABI pthread_rwlock_timedwrlock_nid_postfix(PthreadRwlock* rwlock, const KernelTimespec* abstime) { - if (!abstime) throw std::runtime_error("pthread_rwlock_timedwrlock: null abstime"); + if (!abstime) return pthread_rwlock_wrlock_nid_postfix(rwlock); initializeStatic(rwlock, __func__); if (scePthreadRwlockTrywrlock(rwlock) == 0) return 0; KernelUseconds usec = 0; diff --git a/core/libs/tests/GuestPthreadRwlock.cpp b/core/libs/tests/GuestPthreadRwlock.cpp index 4680fca3bf..1274b6be3a 100644 --- a/core/libs/tests/GuestPthreadRwlock.cpp +++ b/core/libs/tests/GuestPthreadRwlock.cpp @@ -3,7 +3,6 @@ #include #include #include -#include #include extern "C" { @@ -379,10 +378,8 @@ int main() { Require(pthread_rwlock_unlock_nid_postfix(&rwlock) == 0); Require(pthread_rwlock_unlock_nid_postfix(&rwlock) == GUEST_EPERM); - bool rejected = false; - try { pthread_rwlock_timedrdlock_nid_postfix(&rwlock, nullptr); } - catch (const std::runtime_error&) { rejected = true; } - Require(rejected); + Require(pthread_rwlock_timedrdlock_nid_postfix(&rwlock, nullptr) == 0); + Require(pthread_rwlock_unlock_nid_postfix(&rwlock) == 0); Require(pthread_rwlock_destroy_nid_postfix(&rwlock) == 0); Require(rwlock == DestroyedRwlock); diff --git a/core/libs/tests/GuestPthreadTimedLocks.cpp b/core/libs/tests/GuestPthreadTimedLocks.cpp new file mode 100644 index 0000000000..9a8de83ee7 --- /dev/null +++ b/core/libs/tests/GuestPthreadTimedLocks.cpp @@ -0,0 +1,84 @@ +#include "SceTypes.hpp" +#include +#include +#include +#include + +extern "C" { +int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, PthreadEntry entry, void* arg, const char* name); +int APS5_VABI scePthreadJoin(Pthread thread, void** retval); +int APS5_VABI pthread_rwlock_wrlock_nid_postfix(PthreadRwlock* rwlock); +int APS5_VABI pthread_rwlock_unlock_nid_postfix(PthreadRwlock* rwlock); +int APS5_VABI pthread_rwlock_destroy_nid_postfix(PthreadRwlock* rwlock); +int APS5_VABI pthread_rwlock_timedrdlock_nid_postfix(PthreadRwlock* rwlock, const KernelTimespec* abstime); +int APS5_VABI pthread_rwlock_timedwrlock_nid_postfix(PthreadRwlock* rwlock, const KernelTimespec* abstime); +int APS5_VABI pthread_mutex_init_nid_postfix(PthreadMutex* mutex, const PthreadMutexattr* attr); +int APS5_VABI pthread_mutex_lock_nid_postfix(PthreadMutex* mutex); +int APS5_VABI pthread_mutex_unlock_nid_postfix(PthreadMutex* mutex); +int APS5_VABI pthread_mutex_destroy_nid_postfix(PthreadMutex* mutex); +int APS5_VABI pthread_mutex_timedlock_nid_postfix(PthreadMutex* mutex, const KernelTimespec* abstime); +} + +static constexpr int SCE_OK = 0; +static constexpr int GUEST_EDEADLK = 11; +static constexpr auto HoldTime = std::chrono::milliseconds(50); + +static void Require(bool value) { if (!value) std::abort(); } + +struct Holder { + void* lock; + bool mutex; + std::atomic held{false}; + Pthread thread = nullptr; +}; + +static void* APS5_VABI Hold(void* arg) { + auto& holder = *static_cast(arg); + auto* mutex = static_cast(holder.lock); + auto* rwlock = static_cast(holder.lock); + Require((holder.mutex ? pthread_mutex_lock_nid_postfix(mutex) : pthread_rwlock_wrlock_nid_postfix(rwlock)) == 0); + holder.held.store(true); + std::this_thread::sleep_for(HoldTime); + Require((holder.mutex ? pthread_mutex_unlock_nid_postfix(mutex) : pthread_rwlock_unlock_nid_postfix(rwlock)) == 0); + return nullptr; +} + +template +static void RequireWaitsForHolder(void* lock, bool mutex, Lock acquire) { + Holder holder{lock, mutex}; + Require(scePthreadCreate(&holder.thread, nullptr, Hold, &holder, nullptr) == SCE_OK); + while (!holder.held.load()) std::this_thread::yield(); + const auto start = std::chrono::steady_clock::now(); + Require(acquire() == 0); + Require(std::chrono::steady_clock::now() - start >= HoldTime / 2); + Require(scePthreadJoin(holder.thread, nullptr) == SCE_OK); +} + +static void CheckRwlock() { + PthreadRwlock rwlock = nullptr; + Require(pthread_rwlock_timedrdlock_nid_postfix(&rwlock, nullptr) == 0 && rwlock != nullptr); + Require(pthread_rwlock_unlock_nid_postfix(&rwlock) == 0); + Require(pthread_rwlock_timedwrlock_nid_postfix(&rwlock, nullptr) == 0); + Require(pthread_rwlock_unlock_nid_postfix(&rwlock) == 0); + RequireWaitsForHolder(&rwlock, false, [&] { return pthread_rwlock_timedrdlock_nid_postfix(&rwlock, nullptr); }); + Require(pthread_rwlock_unlock_nid_postfix(&rwlock) == 0); + RequireWaitsForHolder(&rwlock, false, [&] { return pthread_rwlock_timedwrlock_nid_postfix(&rwlock, nullptr); }); + Require(pthread_rwlock_unlock_nid_postfix(&rwlock) == 0); + Require(pthread_rwlock_destroy_nid_postfix(&rwlock) == 0); +} + +static void CheckMutex() { + PthreadMutex mutex = nullptr; + Require(pthread_mutex_init_nid_postfix(&mutex, nullptr) == 0); + Require(pthread_mutex_timedlock_nid_postfix(&mutex, nullptr) == 0); + Require(pthread_mutex_timedlock_nid_postfix(&mutex, nullptr) == GUEST_EDEADLK); + Require(pthread_mutex_unlock_nid_postfix(&mutex) == 0); + RequireWaitsForHolder(&mutex, true, [&] { return pthread_mutex_timedlock_nid_postfix(&mutex, nullptr); }); + Require(pthread_mutex_unlock_nid_postfix(&mutex) == 0); + Require(pthread_mutex_destroy_nid_postfix(&mutex) == 0); +} + +int main() { + CheckRwlock(); + CheckMutex(); +} From 2d4512e5c893b487c8d8401562d23a19d7eb24f8 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Sat, 17 Oct 2026 06:06:21 +0000 Subject: [PATCH 0661/1000] feat(agc): count the render passes recorded draws begin and continue DrawPassTotals returns, since the process started, how many recorded draws began a render pass of their own or continued the previous draw's, and how many flushed the queued DCC key stores or label stores before themselves, for address-based draws and for the others. A draw is address-based when its resources hold the allocation registry lease, whether they were built for it or found in the draw cache. The totals are kept with the [draws] line's (APS5_PROFILE_DRAW), so a test or a harness can tell whether an address-based draw kept the open pass without reading the 10-second report. --- .../libSceAgcDriver/Graphics/include/Draw.hpp | 9 ++++++ .../prx/libSceAgcDriver/Graphics/src/Draw.cpp | 29 +++++++++++++++++-- 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Draw.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Draw.hpp index 3b504b6d12..0b596093b7 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Draw.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Draw.hpp @@ -94,6 +94,15 @@ bool DrawRecipes(); enum class IndirectDrawPath : std::uint8_t { Gpu, NotFolded, FetchUnknown, NonVertexPath, IndxOffset, DrawIndex, VertexRange, FeatureGap, PendingImage, PendingLabelOrCopy, NotImported, Disabled, Count }; void CountIndirectDraw(IndirectDrawPath path, double readMs, bool rewritten = false); std::array(IndirectDrawPath::Count)> IndirectDrawCounts(); + +struct DrawPassCounts { + std::uint64_t draws = 0; + std::uint64_t passesBegun = 0; + std::uint64_t passesContinued = 0; + std::uint64_t keyStoreFlushes = 0; + std::uint64_t storeFlushes = 0; +}; +std::array DrawPassTotals(); const char* IndirectDrawPathName(IndirectDrawPath path); // Draw packets that drew nothing, for the [draws] line: Nothing (an empty count, or no color writes diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index 73a90b700e..c21d23480a 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -353,6 +353,9 @@ struct DrawOutcome { // A recorded draw began a render pass of its own, or continued the previous draw's. bool passBegun = false; bool passContinued = false; + bool keyStoresFlushed = false; + bool storesFlushed = false; + bool addressDraw = false; SyncReason reason = SyncNone; // Shader validation memo (see CachedFragmentOutputs). bool validateMemoized = false; @@ -425,6 +428,7 @@ struct DrawProfile { // Render passes recorded draws began, and draws that continued the previous draw's pass. std::uint64_t passesBegun = 0; std::uint64_t passesContinued = 0; + std::array passTotals{}; std::array reasons{}; // The CPU inside Graphics::Draw, split by outcome: a synchronous draw costs ten times a recorded // one, so one average would only show the synchronous population. @@ -495,6 +499,14 @@ void reportDraw(const std::array& us, const ShaderResources: if (outcome.completion) ++profile.completion; if (outcome.passBegun) ++profile.passesBegun; if (outcome.passContinued) ++profile.passesContinued; + if (outcome.recorded) { + auto& totals = profile.passTotals[outcome.addressDraw ? 1 : 0]; + ++totals.draws; + if (outcome.passBegun) ++totals.passesBegun; + if (outcome.passContinued) ++totals.passesContinued; + if (outcome.keyStoresFlushed) ++totals.keyStoreFlushes; + if (outcome.storesFlushed) ++totals.storeFlushes; + } ++profile.reasons[outcome.reason]; (outcome.recorded ? (outcome.waited ? profile.waitedUs : profile.recordedUs) : profile.synchronousUs) += drawUs; profile.targetLookups += outcome.targetLookups; @@ -840,6 +852,12 @@ std::array IndirectDrawCounts() { return stats.indirect; } +std::array DrawPassTotals() { + auto& stats = Profile(); + std::lock_guard lock(stats.mutex); + return stats.passTotals; +} + void CountDrawSkip(DrawSkip kind, double us) { static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; if (!profile) return; @@ -1747,13 +1765,20 @@ void recordDraw(const Context& context, const State& state, const Pm4::DrawParam if (!gpuIndirect) return false; return (begin < args->arguments + args->RangeBytes() && args->arguments < end) || (args->countIndirect && begin < args->countAddress + 4 && args->countAddress < end); }; - if (recorder->HasQueuedKeyStores() && (resources.HoldsLease() || recorder->AnyQueuedKeyStore(touches))) recorder->FlushKeyStores(); + if (recorder->HasQueuedKeyStores() && (resources.HoldsLease() || recorder->AnyQueuedKeyStore(touches))) { + recorder->FlushKeyStores(); + outcome.keyStoresFlushed = true; + } // A queued label store over such memory likewise (Recorder::RecordStore). - if (recorder->HasQueuedStores() && (resources.HoldsLease() || recorder->AnyQueuedStore(touches))) recorder->FlushStores(); + if (recorder->HasQueuedStores() && (resources.HoldsLease() || recorder->AnyQueuedStore(touches))) { + recorder->FlushStores(); + outcome.storesFlushed = true; + } const auto drawBindings = resources.PrepareDrawBindings(*recorder, record.moved); const bool capture = CaptureInputsEnabled(); const bool meshIndirect = state.stages.mesh && args != nullptr; const bool addressBased = resources.HoldsLease(); + outcome.addressDraw = addressBased; auto passReads = addressBased ? std::vector>{} : resources.DeviceReads(); if (gpuIndirect) { passReads.emplace_back(args->arguments, args->arguments + args->RangeBytes()); From b3e94a51483aa8f6c029dd64b19e8fbdbdb0a73b Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Sat, 17 Oct 2026 06:48:56 +0000 Subject: [PATCH 0662/1000] test(agc): check that draws reading through the BDA table share a render pass agc_driver_address_passes draws a triangle twice onto a guest block, with pixel shaders that global_load_dword or global_store_dword through s[2:3], and reads DrawPassTotals for the address-based draws: - --readers: two draws reading through the table share one render pass; - --store-after-read: a draw storing through the table after one reading through it begins its own pass; - --read-sees-store: a draw reading through the table what the previous draw stored sees the store, in a pass of its own. Taken from c44c9a670 (perf/agc-bounded-address-passes) without its --store-beside-reads case, which needs that commit's store bounds. --- .../tests/execution/AddressPasses.cpp | 263 ++++++++++++++++++ 1 file changed, 263 insertions(+) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/AddressPasses.cpp diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/AddressPasses.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/AddressPasses.cpp new file mode 100644 index 0000000000..9de59a0343 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/AddressPasses.cpp @@ -0,0 +1,263 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; + +constexpr std::uint32_t VertexBuffers = 3; +constexpr std::size_t VertexTableOffset = 0x300; +constexpr std::size_t VertexDataOffset = 0x800; +constexpr std::size_t ColorOffset = 0; +constexpr std::size_t ColorBytes = 65536; +constexpr std::uint32_t RenderTargetTileMode = 0x1b; +constexpr std::size_t BlockBytes = 65536; + +alignas(256) constexpr std::array VertexCode{ + 0x7e000d05u, 0x080200f4u, 0x10020300u, 0x100202f4u, 0x060202f3u, 0x060400f3u, 0x10040500u, 0x060404f3u, + 0x7e060280u, 0x7e0802f2u, + 0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080300u, 0xfa000020u, + 0xbf8cc07fu, + 0xe00c2000u, 0x80010a05u, 0xe00c2000u, 0x80020e05u, 0xe00c2000u, 0x80031205u, + 0xbf8c3f70u, + 0x0602030au, 0x0604050bu, 0x0606070cu, 0x0608090du, + 0x0602030eu, 0x0604050fu, 0x06060710u, 0x06080911u, + 0x06020312u, 0x06040513u, 0x06060714u, 0x06080915u, + 0xf80008cfu, 0x04030201u, + 0xbf810000u, 0xbf810000u, +}; + +alignas(256) constexpr std::array VertexCodeUser8 = [] { + auto code = VertexCode; + for (const std::size_t load : {10u, 12u, 14u}) code[load] |= 4u; + return code; +}(); + +alignas(256) constexpr std::array PixelRead{0x7e1a0280u, 0xdc308000u, 0x0c02000du, 0xbf8c3f70u, 0x7e10030cu, 0x7e120280u, 0x7e140280u, 0x7e160280u, 0xf800180fu, 0x0b0a0908u, 0xbf810000u}; +alignas(256) constexpr std::array PixelStore{0x7e1a0280u, 0x7e1802f2u, 0xdc708000u, 0x00020c0du, 0x7e1002f2u, 0x7e1202f2u, 0x7e1402f2u, 0x7e1602f2u, 0xf800180fu, 0x0b0a0908u, 0xbf810000u}; + +struct Header { + Shader shader{}; + ShaderUserData userData{}; +}; + +alignas(8) Header VertexHeader{}; +alignas(8) Header ReadHeader{}; +alignas(8) Header StoreHeader{}; + +void Register(Header& header, const std::uint32_t* code, std::size_t bytes, std::uint8_t type) { + header.shader.file_header = 0x34333231u; + header.shader.version = 0x18u; + header.shader.header_size = sizeof(Header); + header.shader.shader_size = static_cast(bytes); + header.shader.code = code; + header.shader.type = type; + header.shader.user_data = &header.userData; + AgcDriverRegisterShader_nid_postfix(&header.shader); +} + +class GuestBlock { +public: + GuestBlock() { +#ifdef _WIN32 + block = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(BlockBytes, 65536)); + if (block != nullptr) GuestArena::GuestArenaCommit_nid_postfix(block, BlockBytes, PAGE_READWRITE, BlockBytes); +#else + constexpr std::uintptr_t alignment = 65536; + void* mapped = mmap(nullptr, BlockBytes + alignment, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (mapped != MAP_FAILED) { + const auto begin = reinterpret_cast(mapped); + const auto aligned = (begin + alignment - 1) & ~(alignment - 1); + if (aligned != begin) munmap(mapped, aligned - begin); + if (aligned + BlockBytes != begin + BlockBytes + alignment) munmap(reinterpret_cast(aligned + BlockBytes), begin + alignment - aligned); + block = reinterpret_cast(aligned); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(block, BlockBytes); + } +#endif + Require(block != nullptr, "address passes: cannot allocate a guest block"); + std::memset(block, 0, BlockBytes); + } + + GuestBlock(const GuestBlock&) = delete; + GuestBlock& operator=(const GuestBlock&) = delete; + + void Map(bool writable) const { GuestAllocations::Mutation().Add(block, BlockBytes, true, writable, true); } + std::uint64_t At(std::size_t offset) const { return static_cast(reinterpret_cast(block + offset)); } + std::uint8_t* Bytes(std::size_t offset) const { return block + offset; } + +private: + std::uint8_t* block = nullptr; +}; + +std::array BufferDescriptor(std::uint64_t address, std::uint32_t stride, std::uint32_t records, std::uint32_t word3) { + return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (stride << 16u), records, word3}; +} + +void FillVertices(const GuestBlock& block) { + for (std::uint32_t buffer = 0; buffer < VertexBuffers; ++buffer) { + const auto vertices = BufferDescriptor(block.At(VertexDataOffset + buffer * 0x100u), 16, 3, 0x0104dfacu); + std::memcpy(block.Bytes(VertexTableOffset + buffer * 16u), vertices.data(), sizeof(vertices)); + } +} + +void Append(std::vector& commands, std::uint32_t opcode, std::initializer_list payload) { + commands.push_back(0xc0000000u | (static_cast(payload.size() - 1) << 16u) | (opcode << 8u)); + commands.insert(commands.end(), payload); +} + +struct Step { + const std::uint32_t* pixel; + std::uint64_t pointer; +}; + +void Submit(const GuestBlock& vertices, const GuestBlock& target, std::initializer_list steps) { + const auto color = target.At(ColorOffset); + const auto vertex = reinterpret_cast(VertexCodeUser8.data()); + const auto table = vertices.At(VertexTableOffset); + const float scale = 32.0f; + const std::vector> context{ + {0x2d5, 0x2000}, + {0x1b6, 0}, {0x207, 0}, {0x200, 0}, {0x203, 0x800}, + {0x2dc, 0xaa00}, {0x2f8, 0}, {0x292, 2}, {0x293, 0}, + {0x80, 0}, {0x8d, 0}, {0x83, 0xffff}, {0x8c, 0xa}, + {0x2f9, 0x2d}, {0x313, 0x6000}, {0x30e, 0xffffffff}, {0x30f, 0xffffffff}, + {0x206, 0x43f}, {0x204, 0x80000}, {0x205, 0x240}, + {0x8e, 0xf}, {0x8f, 0xf}, {0x202, 0xcc0010}, + {0x1c4, 0}, {0x1c5, 9}, {0x1c3, 4}, {0x31c, 0x28028}, + {0x31b, 0}, {0x31d, 0}, {0x3b0, (63u << 14u) | 3u}, + {0x3b8, 0x9000000u | (RenderTargetTileMode << 14u)}, {0x1e0, 0}, + {0xc, 0}, {0xd, 0x40040}, + {0x81, 0x80000000}, {0x82, 0x40040}, + {0x90, 0x80000000}, {0x91, 0x40040}, + {0x94, 0x80000000}, {0x95, 0x40040}, + {0x318, static_cast(color >> 8u)}, {0x390, static_cast(color >> 40u)}, + {0x10f, std::bit_cast(scale)}, {0x110, std::bit_cast(scale)}, + {0x111, std::bit_cast(-2.0f)}, {0x112, std::bit_cast(2.0f)}, + {0x113, std::bit_cast(1.0f)}, {0x114, 0}, + {0xb4, 0}, {0xb5, std::bit_cast(1.0f)}, + {0x2fe, 0}, {0x302, 0}, {0x306, 0}, {0x30a, 0}, + }; + std::vector commands; + Append(commands, 0x79, {0x242, 4}); + for (const auto& [offset, value] : context) Append(commands, 0x69, {offset, value}); + for (const auto& [offset, value] : std::initializer_list>{{0xc8, static_cast(vertex >> 8u)}, {0xc9, static_cast(vertex >> 40u)}, {0x8b, 2u << 1u}, {0x8c, static_cast(table)}, {0x8d, static_cast(table >> 32u)}, {0x00b, 8u << 1u}, {0x00c, 0}, {0x00d, 0}}) Append(commands, 0x76, {offset, value}); + for (const auto& step : steps) { + const auto pixel = reinterpret_cast(step.pixel); + Append(commands, 0x76, {0x008, static_cast(pixel >> 8u), static_cast(pixel >> 40u)}); + Append(commands, 0x76, {0x00e, static_cast(step.pointer), static_cast(step.pointer >> 32u)}); + Append(commands, 0x2d, {3, 2}); + } + Packet packet{commands.data(), static_cast(commands.size()), 0, {}}; + Require(sceAgcDriverSubmitDcb(&packet) == 0, "address passes: the draw submission failed"); + AgcDriverWaitIdle_nid_postfix(); +} + +AgcDriver::Graphics::DrawPassCounts AddressDraws() { + return AgcDriver::Graphics::DrawPassTotals()[1]; +} + +void Expect(bool condition, const std::string& what) { + if (!condition) throw std::runtime_error("address passes: " + what); +} + +std::string Counts(const AgcDriver::Graphics::DrawPassCounts& before, const AgcDriver::Graphics::DrawPassCounts& after) { + return std::to_string(after.draws - before.draws) + " address-based draws, " + std::to_string(after.passesBegun - before.passesBegun) + " began a pass, " + std::to_string(after.passesContinued - before.passesContinued) + " continued one"; +} + +void ReadersShareAPass(const GuestBlock& vertices, const GuestBlock& data, const GuestBlock& target) { + const auto before = AddressDraws(); + Submit(vertices, target, {{PixelRead.data(), data.At(0)}, {PixelRead.data(), data.At(0)}}); + const auto after = AddressDraws(); + std::printf("two draws reading through the table: %s\n", Counts(before, after).c_str()); + Expect(after.draws - before.draws == 2 && after.passesContinued - before.passesContinued == 1, "a draw reading through the table after another did not continue its pass: " + Counts(before, after)); +} + +void StoreAfterTableReadBreaks(const GuestBlock& vertices, const GuestBlock& data, const GuestBlock& target) { + const auto before = AddressDraws(); + Submit(vertices, target, {{PixelRead.data(), data.At(32)}, {PixelStore.data(), data.At(32)}}); + const auto after = AddressDraws(); + std::printf("a draw storing through the table after a draw reading through it: %s\n", Counts(before, after).c_str()); + Expect(after.draws - before.draws == 2 && after.passesBegun - before.passesBegun == 2, "a draw storing through the table continued the pass of a draw that read through it: " + Counts(before, after)); +} + +void ReadSeesStore(const GuestBlock& vertices, const GuestBlock& data, const GuestBlock& target) { + std::memset(target.Bytes(ColorOffset), 0, ColorBytes); + std::memset(data.Bytes(48), 0, 4); + const auto before = AddressDraws(); + Submit(vertices, target, {{PixelStore.data(), data.At(48)}, {PixelRead.data(), data.At(48)}}); + const auto after = AddressDraws(); + AgcDriver::GuestMemory::FlushGpuWrites(static_cast(target.At(ColorOffset)), ColorBytes); + float stored = 0; + std::memcpy(&stored, data.Bytes(48), sizeof(stored)); + unsigned brightest = 0; + for (std::size_t i = 0; i < ColorBytes; ++i) brightest = std::max(brightest, static_cast(target.Bytes(ColorOffset)[i])); + std::printf("a draw reading through the table what the previous draw stored: stored %g, brightest color byte %u, %s\n", static_cast(stored), brightest, Counts(before, after).c_str()); + Expect(stored == 1.0f, "the store through the table did not reach guest memory"); + Expect(brightest == 255u, "the read through the table did not see the previous draw's store"); + Expect(after.passesBegun - before.passesBegun == 2, "a draw reading through the table continued the pass of a draw that stored through it: " + Counts(before, after)); +} + +} + +int main(int argc, char** argv) { + try { +#ifdef _WIN32 + _putenv_s("APS5_PROFILE_DRAW", "1"); +#else + setenv("APS5_PROFILE_DRAW", "1", 1); +#endif + { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + } + Register(VertexHeader, VertexCodeUser8.data(), sizeof(VertexCodeUser8), 2); + Register(ReadHeader, PixelRead.data(), sizeof(PixelRead), 1); + Register(StoreHeader, PixelStore.data(), sizeof(PixelStore), 1); + GuestBlock readOnlyVertices; + GuestBlock data; + GuestBlock target; + FillVertices(readOnlyVertices); + readOnlyVertices.Map(false); + data.Map(true); + target.Map(true); + const std::string only = argc > 1 ? argv[1] : ""; + if (only.empty() || only == "--readers") ReadersShareAPass(readOnlyVertices, data, target); + if (only.empty() || only == "--store-after-read") StoreAfterTableReadBreaks(readOnlyVertices, data, target); + if (only.empty() || only == "--read-sees-store") ReadSeesStore(readOnlyVertices, data, target); + AgcDriverShutdown_nid_postfix(); + std::puts("address passes: draws reading through the BDA table share a render pass, and a store through it still begins one"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + try { AgcDriverShutdown_nid_postfix(); } catch (...) {} + return 1; + } +} From 0194926b8aaf6ba42149b46ad7a5f5f99f6c5d62 Mon Sep 17 00:00:00 2001 From: thuanquang Date: Sat, 17 Oct 2026 14:03:55 +0700 Subject: [PATCH 0663/1000] fix(shader): flush v_fma_mix_f32 f16 sources by the f16 denormal field v_fma_mix_f32 used its f16 sources unflushed in every mode. On RDNA2 (gfx1031) f16 denormal modes 0 and 2 flush each f16 source to a zero of its sign, whatever the f32 denormal field, so v_fma_mix_f32 0, 0, 0x0001 (all f16) is 0 there instead of 2^-24. Co-Authored-By: Claude Opus 5.5 --- .../execution/FmaMixF16DenormalModes.cpp | 216 ++++++++++++++++++ .../src/TranslationContext/SubwordPacking.cpp | 2 +- docs/dev/debt/shader.md | 2 +- 3 files changed, 218 insertions(+), 2 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/FmaMixF16DenormalModes.cpp diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/FmaMixF16DenormalModes.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/FmaMixF16DenormalModes.cpp new file mode 100644 index 0000000000..537b0bb483 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/FmaMixF16DenormalModes.cpp @@ -0,0 +1,216 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 16; +constexpr std::uint32_t Inputs = 4; +constexpr std::uint32_t Results = 2; +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; + +alignas(256) constexpr std::array Code{ + 0x34020084u, 0x34040083u, 0xe03c1000u, 0x80000401u, 0xbf8c3f70u, 0xcc20600au, 0x1c120b04u, 0xcc20600bu, + 0x0c120d04u, 0xe0741000u, 0x80010a02u, 0xbf810000u, +}; +constexpr std::uint32_t Rows[Threads][3] = { + {0x00010000u, 0x00000000u, 0x3f800000u}, + {0x80010000u, 0x00000000u, 0x00800000u}, + {0x00003c00u, 0x00000001u, 0x4b000000u}, + {0x80000001u, 0x00003c00u, 0xbf800000u}, + {0x000003ffu, 0x00003c00u, 0x33800000u}, + {0x000183ffu, 0x00004000u, 0x00000001u}, + {0x00000200u, 0x00000200u, 0x3e800000u}, + {0x00013c00u, 0x00003c00u, 0x7f7fffffu}, + {0x00010400u, 0x00003800u, 0x3f800000u}, + {0x00007c00u, 0x00000001u, 0x3f800000u}, + {0x00010001u, 0x00000001u, 0x40000000u}, + {0x81554400u, 0x00000155u, 0xc0800000u}, + {0x83ff3c00u, 0x00000400u, 0x3f000000u}, + {0x00007bffu, 0x00000001u, 0x2f800000u}, + {0x80000000u, 0x00008000u, 0x80000000u}, + {0x00008001u, 0x00008001u, 0x3f800000u}, +}; +constexpr std::uint32_t Expected[2][2][Threads][2] = { + { + { + {0x00000000u, 0x00000000u}, + {0x00000000u, 0x00000000u}, + {0x00000000u, 0x4b000000u}, + {0x00000000u, 0x80000000u}, + {0x00000000u, 0x00000000u}, + {0x00000000u, 0x00000000u}, + {0x00000000u, 0x00000000u}, + {0x3f800000u, 0x7f7fffffu}, + {0x38000000u, 0x38800000u}, + {0xffc00000u, 0x7f800000u}, + {0x00000000u, 0x00000000u}, + {0x00000000u, 0xc1800000u}, + {0x38800000u, 0x3f000000u}, + {0x00000000u, 0x377fe000u}, + {0x80000000u, 0x80000000u}, + {0x00000000u, 0x00000000u}, + }, + { + {0x33800000u, 0x33800000u}, + {0xb3800000u, 0xb3800000u}, + {0x33800000u, 0x4b000000u}, + {0x33800000u, 0xb3800000u}, + {0x387fc000u, 0x2c7fc000u}, + {0xb8ffa000u, 0x33800000u}, + {0x30800000u, 0x37000000u}, + {0x3f800000u, 0x7f7fffffu}, + {0x38004000u, 0x38802000u}, + {0x7f800000u, 0x7f800000u}, + {0x33800000u, 0x34400000u}, + {0x387fc000u, 0xc180000bu}, + {0x33800000u, 0x3efff802u}, + {0x3b7fe000u, 0x377fe000u}, + {0x80000000u, 0x80000000u}, + {0x27800000u, 0xb3800000u}, + }, + }, + { + { + {0x00000000u, 0x00000000u}, + {0x00000000u, 0x00000000u}, + {0x00000000u, 0x4b000000u}, + {0x00000000u, 0x80000000u}, + {0x00000000u, 0x00000000u}, + {0x00000000u, 0x00000000u}, + {0x00000000u, 0x00000000u}, + {0x3f800000u, 0x7f7fffffu}, + {0x38000000u, 0x38800000u}, + {0xffc00000u, 0x7f800000u}, + {0x00000000u, 0x00000000u}, + {0x00000000u, 0xc1800000u}, + {0x38800000u, 0x3f000000u}, + {0x00000000u, 0x377fe000u}, + {0x80000000u, 0x80000000u}, + {0x00000000u, 0x00000000u}, + }, + { + {0x33800000u, 0x33800000u}, + {0xb3800000u, 0xb3800000u}, + {0x33800000u, 0x4b000000u}, + {0x33800000u, 0xb3800000u}, + {0x387fc000u, 0x2c7fc000u}, + {0xb8ffa000u, 0x33800000u}, + {0x30800000u, 0x37000000u}, + {0x3f800000u, 0x7f7fffffu}, + {0x38004000u, 0x38802000u}, + {0x7f800000u, 0x7f800000u}, + {0x33800000u, 0x34400000u}, + {0x387fc000u, 0xc180000bu}, + {0x33800000u, 0x3efff802u}, + {0x3b7fe000u, 0x377fe000u}, + {0x80000000u, 0x80000000u}, + {0x27800000u, 0xb3800000u}, + }, + }, +}; + +struct Mode { + const char* name; + ShaderRecompiler::ShaderFloatMode value; +}; + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + +void Run(AgcDriver::VulkanDevice& device, const Mode& mode) { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + std::copy(std::begin(Rows[tid]), std::end(Rows[tid]), &Input[tid * Inputs]); + Input[tid * Inputs + 3u] = 0u; + } + Output.fill(0xdeadbeefu); + std::vector userData(8, 0u); + const auto input = BufferDescriptor(Input.data(), static_cast(Input.size() * 4u)); + const auto output = BufferDescriptor(Output.data(), static_cast(Output.size() * 4u)); + std::copy(input.begin(), input.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4); + const std::span code(Code); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + request.context.floatMode = mode.value; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); +} + +void Check(const Mode& mode) { + constexpr const char* Names[Results] = {"v_fma_mix_f32 of three f16 sources", "v_fma_mix_f32 of an f16, an f32 and an f16 source"}; + const std::uint32_t f32Kept = (mode.value.floatMode >> 4u) & 1u; + const std::uint32_t f16InputsKept = (mode.value.floatMode >> 6u) & 1u; + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + for (std::uint32_t form = 0; form < Results; ++form) { + const std::uint32_t actual = Output[tid * Results + form]; + const std::uint32_t expected = Expected[f32Kept][f16InputsKept][tid][form]; + Require(actual == expected, std::string(Names[form]) + " " + mode.name + " of " + Hex(Rows[tid][0]) + ", " + Hex(Rows[tid][1]) + ", " + Hex(Rows[tid][2]) + " is " + Hex(actual) + ", expected " + Hex(expected)); + } + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + std::vector modes; + static const char* const Names[2][4][2] = { + {{"f32 denormals flushed, f16 denormal mode 0, IEEE mode off", "f32 denormals flushed, f16 denormal mode 0, IEEE mode on"}, + {"f32 denormals flushed, f16 denormal mode 1, IEEE mode off", "f32 denormals flushed, f16 denormal mode 1, IEEE mode on"}, + {"f32 denormals flushed, f16 denormal mode 2, IEEE mode off", "f32 denormals flushed, f16 denormal mode 2, IEEE mode on"}, + {"f32 denormals flushed, f16 denormal mode 3, IEEE mode off", "f32 denormals flushed, f16 denormal mode 3, IEEE mode on"}}, + {{"f32 denormals kept, f16 denormal mode 0, IEEE mode off", "f32 denormals kept, f16 denormal mode 0, IEEE mode on"}, + {"f32 denormals kept, f16 denormal mode 1, IEEE mode off", "f32 denormals kept, f16 denormal mode 1, IEEE mode on"}, + {"f32 denormals kept, f16 denormal mode 2, IEEE mode off", "f32 denormals kept, f16 denormal mode 2, IEEE mode on"}, + {"f32 denormals kept, f16 denormal mode 3, IEEE mode off", "f32 denormals kept, f16 denormal mode 3, IEEE mode on"}}, + }; + for (std::uint32_t f32 = 0; f32 < 2u; ++f32) { + for (std::uint32_t f16 = 0; f16 < 4u; ++f16) { + for (std::uint32_t ieee = 0; ieee < 2u; ++ieee) { + modes.push_back({Names[f32][f16][ieee], ShaderRecompiler::ShaderFloatMode{(f16 << 6u) | (f32 != 0u ? 0x30u : 0u), true, ieee != 0u, false}}); + } + } + } + for (const Mode& mode : modes) { + Run(*device, mode); + Check(mode); + } + std::puts("v_fma_mix_f32 f16 denormal mode tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/shader/recompiler/Translation/src/TranslationContext/SubwordPacking.cpp b/core/shader/recompiler/Translation/src/TranslationContext/SubwordPacking.cpp index f64a0c72ee..c48c763787 100644 --- a/core/shader/recompiler/Translation/src/TranslationContext/SubwordPacking.cpp +++ b/core/shader/recompiler/Translation/src/TranslationContext/SubwordPacking.cpp @@ -117,7 +117,7 @@ IrF32 TranslationContext::readMixF32(const RdnaOperand& operand) { if (!operand.opSelHi) { return IrF32(*readOperand(operand, IrType::F32)); } - return readF16LaneAsF32(operand, operand.opSel, false); + return flushF16InputValue(readF16LaneAsF32(operand, operand.opSel, false)); } IrU32 TranslationContext::readF16LaneBits(const RdnaOperand& operand, bool highLane) { diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index 5b449de647..77ffad922e 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -96,7 +96,7 @@ - [Scalar f16 add, sub, subrev and mul denormals](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) follow the `FLOAT_MODE` f16 denormal field at nearest-even: bit 0 preserves inputs and bit 1 preserves the result; otherwise a denormal is flushed to its signed zero. Measured on RDNA2 (gfx1036) with IEEE=0/1 and FP16_OVFL=0/1, over 32 operand pairs in all four denormal modes, including signed zeros, denormals, minimum normals, infinities and NaNs. Multiply detects tininess after rounding to 11 significant bits with an unbounded exponent, before gradual half packing: `0x3bff * 0x0400` flushes, while `0x3c01 * 0x03ff` rounds to `0x0400` and is kept. Directed rounding and ternary arithmetic keep their previous denormal lowering. Shaders without a known float mode keep the previous lowering. Scalar `v_sub_f16`/`v_subrev_f16` share `float16Binary` with add/mul and were measured on gfx1036 over the same operand set as add (1156 non-NaN-input rows per mode); NaN result bits still depend on the driver as recorded above. - [f16 unary, frexp and conversion denormals](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) follow the `FLOAT_MODE` f16 denormal field: `v_ceil_f16`, `v_floor_f16`, `v_fract_f16`, `v_rcp_f16`, `v_rsq_f16`, `v_sqrt_f16`, `v_log_f16`, `v_exp_f16`, `v_sin_f16`, `v_frexp_mant_f16`, `v_frexp_exp_i16_f16`, `v_cvt_norm_i16_f16`, `v_cvt_norm_u16_f16` and `v_cvt_f32_f16` flush a denormal half input to its signed zero when bit 0 is clear (0 and 2), and the f16-result ones flush a denormal half result to its signed zero when bit 1 is clear (0 and 1), after the host rounds it to half. Measured on RDNA2 (gfx1036) at IEEE=0 and IEEE=1 with denorm16 0..3 over every non-NaN f16 input (63,490 rows per mode and instruction): the only mismatches are the 1-ULP driver differences of `v_exp_f16` (2 inputs) and `v_sin_f16` (14), which are the same with denormals kept. NaN inputs are not compared. `v_ldexp_f16` shares the input rule. `v_cvt_pknorm_i16_f16` and `v_cvt_pknorm_u16_f16` flush both half inputs by the same bit, measured on RDNA2 (gfx1030) over every f16 input (each half also negated in the other source) at IEEE=0 and IEEE=1 with denorm16 0..3 and at f16 rounding 1..3: every row matches. Shaders without a known float mode keep the previous lowering. - [Packed f16 NaN results](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) (`v_pk_add/mul/fma/min/max_f16`) follow the hardware measured on RDNA2 (gfx1035) at IEEE=0 and IEEE=1 with f16 round-to-nearest-even and f16 denormals kept: with IEEE=1 a signaling NaN input is quieted and min/max return the other operand only for a quiet NaN; with IEEE=0 NaN inputs pass through unquieted and min/max ignore signaling. In `v_pk_fma_f16` an invalid product (inf * 0) gives the default NaN even when src2 is a NaN. A shader without a known float mode is translated as IEEE=0, the mode titles use; a mode with another f16 rounding or that flushes f16 denormals throws. -- [f32 denormals](../../../core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp) follow the shader's `FLOAT_MODE` f32 denormal field (bits 5:4) when it flushes them (0, and shaders without a known float mode, as in the 0xc0 most titles use), measured on RDNA2 (gfx1035) over 1444 rows per instruction at IEEE=0 and IEEE=1. `v_add/sub/subrev/mul/mul_legacy/mullit/fma/fmac/fmamk/fmaak/min/max/min3/max3/med3/fract/floor/ceil/frexp_mant/sin/cos_f32` flush their f32 inputs and their result to a zero of the same sign (`v_mullit_f32` measured on RDNA2 (gfx1036) over 6907 rows per mode at IEEE=0 and IEEE=1, so a positive denormal src2 counts as zero); the f32 compares, `v_cvt_i32/u32/rpi_i32/flr_i32_f32`, `v_frexp_exp_i32_f32`, `v_cvt_f64_f32`, `v_cvt_f16_f32`, `v_cvt_pkrtz_f16_f32`, `v_cvt_pknorm_i16/u16_f32` and `v_cvt_pk_u8_f32` flush their f32 inputs. `v_div_scale_f32` flushes all three inputs to signed zeros, measured on RDNA2 (gfx1036) over 2744 operand triples per mode at IEEE=0 and IEEE=1 with f32 denormals flushed and kept; its scale-down behaviour and host denormal preservation remain incomplete. `v_div_fixup_f32` flushes its src1 and src2 and keeps a denormal src0 and result, measured on RDNA2 (gfx1036) over 6859 rows per mode at IEEE=0 and IEEE=1. Tininess is detected after rounding, measured on RDNA2 (gfx1036) for `v_mul_f32` and `v_mullit_f32` at IEEE=0 and IEEE=1: a product is flushed when its value rounded to 24 bits with an unbounded exponent is below 2^-126, so `0x3f7ffffe * 0x00800001` (2^-126 * (1 - 2^-46)) gives `0x00800000` and `0x3f7fffff * 0x00800000` (2^-126 * (1 - 2^-24)) gives zero. The check rounds in the f32 round mode of `FLOAT_MODE` (bits 1:0), measured in all four modes, while the products themselves are still rounded to nearest even. This applies to `v_mul_f32`, `v_mul_legacy_f32`, `v_mullit_f32`, and to `v_fma_f32`, `v_fmac_f32`, `v_fmamk_f32`, `v_fmaak_f32` and `v_fma_mix_f32` when the addend is zero; with a nonzero addend the fused result is kept, so an exact result just below 2^-126 that rounds to it is not flushed. In the three directed modes the check is computed exactly from the significands. To nearest even (0, the mode of shaders without a known float mode) one factor is multiplied by 2^64, which is exact below 2^64, and the product with the other factor is compared with 2^-62. That multiply is a plain `OpFMul` like the product, and no f32 rounding mode is requested in the SPIR-V, so the check relies on the driver rounding f32 to nearest even by default, as the products do, and a product that rounds up to 2^-126 is the emitted product. `v_fma_mix_f32` follows this rule with mixed f16/f32 sources too, measured on RDNA2 (gfx1036) with f16 in either multiplicand slot, low and high half selection, and an f32 or f16 zero addend: f32 `0x00801002` times f16 `0x3bff` gives `0x00800000` to nearest at IEEE=0 and IEEE=1, while the neighbours `0x00801001` and `0x00801003` give zero and `0x00800001`. The tiny-product boundary was also measured for both signs in all four f32 round modes at IEEE=0; f16 inputs retain their previous conversion and denormal handling. `v_fma_mixlo_f16` and `v_fma_mixhi_f16` flush their f32 sources too and keep a product below 2^-126, measured on RDNA2 (gfx1036). `v_mad_legacy_f32` and `v_mac_legacy_f32` flush their inputs, their product and their result in every mode, see its entry; their product is still checked for tininess on its exact value, so one below 2^-126 gives zero even when it rounds to 2^-126, as the console's separately rounded encoding is not measured at this boundary. Denormal modes 1 and 2 throw for the other instructions. Not covered, and not measurable on RDNA2 (no such opcodes) or not measured: `v_mac_f32`, `v_mad_f32`, `v_madak_f32` and `v_madmk_f32` (rounded separately), `v_dot2_f32_f16` and `v_div_fmas_f32` (on hardware `v_div_fmas_f32` flushes only its result), f32 interpolation and `ds_add_f32` keep the previous lowering; `ds_min/max/cmpst_f32` flush both operands by the field, see the LDS float atomics entry. With f32 denormals kept (3) the results depend on the driver's default denormal behaviour, as no `DenormPreserve` execution mode is requested; `v_ldexp_f32` and the f32 compares are the exceptions. `v_cmp_X_f32` and `v_cmpx_X_f32` compare the operands as sign-magnitude integers with the NaN cases handled apart, so a denormal is not read as zero, measured on RDNA2 (gfx1036) over 256 operand pairs per opcode at IEEE=0 and IEEE=1 with f32 denormals kept and flushed. `v_ldexp_f32` preserves subnormal inputs and rounds a subnormal result in the f32 round mode of `FLOAT_MODE`, measured on RDNA2 (gfx1036) over 2163 rows in each of the four round modes at IEEE=0 and IEEE=1. An overflow rounds like any other result (infinity, or the largest finite value when the mode rounds that sign toward zero). With f32 denormals flushed it flushes a subnormal input and result, measured over the same rows in each round mode at IEEE=0 only. A device whose driver splits `Fma` into a multiply and an add flushes a product below 2^-126 before adding a non-zero addend in `v_fma_f32` and `v_fmac_f32`: `0x1fffffff * 0x1fffffff + 0x00800000` gives `0x00800000` instead of the fused `0x00ffffff` on RADV RENOIR (Mesa 26.2.4), whose compiled shader has `v_mad_f32` and no `v_fma_f32`. [F32DenormalFlush](../../../core/libs/prx/libSceAgcDriver/tests/execution/F32DenormalFlush.cpp) accepts that sum on such a device. +- [f32 denormals](../../../core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp) follow the shader's `FLOAT_MODE` f32 denormal field (bits 5:4) when it flushes them (0, and shaders without a known float mode, as in the 0xc0 most titles use), measured on RDNA2 (gfx1035) over 1444 rows per instruction at IEEE=0 and IEEE=1. `v_add/sub/subrev/mul/mul_legacy/mullit/fma/fmac/fmamk/fmaak/min/max/min3/max3/med3/fract/floor/ceil/frexp_mant/sin/cos_f32` flush their f32 inputs and their result to a zero of the same sign (`v_mullit_f32` measured on RDNA2 (gfx1036) over 6907 rows per mode at IEEE=0 and IEEE=1, so a positive denormal src2 counts as zero); the f32 compares, `v_cvt_i32/u32/rpi_i32/flr_i32_f32`, `v_frexp_exp_i32_f32`, `v_cvt_f64_f32`, `v_cvt_f16_f32`, `v_cvt_pkrtz_f16_f32`, `v_cvt_pknorm_i16/u16_f32` and `v_cvt_pk_u8_f32` flush their f32 inputs. `v_div_scale_f32` flushes all three inputs to signed zeros, measured on RDNA2 (gfx1036) over 2744 operand triples per mode at IEEE=0 and IEEE=1 with f32 denormals flushed and kept; its scale-down behaviour and host denormal preservation remain incomplete. `v_div_fixup_f32` flushes its src1 and src2 and keeps a denormal src0 and result, measured on RDNA2 (gfx1036) over 6859 rows per mode at IEEE=0 and IEEE=1. Tininess is detected after rounding, measured on RDNA2 (gfx1036) for `v_mul_f32` and `v_mullit_f32` at IEEE=0 and IEEE=1: a product is flushed when its value rounded to 24 bits with an unbounded exponent is below 2^-126, so `0x3f7ffffe * 0x00800001` (2^-126 * (1 - 2^-46)) gives `0x00800000` and `0x3f7fffff * 0x00800000` (2^-126 * (1 - 2^-24)) gives zero. The check rounds in the f32 round mode of `FLOAT_MODE` (bits 1:0), measured in all four modes, while the products themselves are still rounded to nearest even. This applies to `v_mul_f32`, `v_mul_legacy_f32`, `v_mullit_f32`, and to `v_fma_f32`, `v_fmac_f32`, `v_fmamk_f32`, `v_fmaak_f32` and `v_fma_mix_f32` when the addend is zero; with a nonzero addend the fused result is kept, so an exact result just below 2^-126 that rounds to it is not flushed. In the three directed modes the check is computed exactly from the significands. To nearest even (0, the mode of shaders without a known float mode) one factor is multiplied by 2^64, which is exact below 2^64, and the product with the other factor is compared with 2^-62. That multiply is a plain `OpFMul` like the product, and no f32 rounding mode is requested in the SPIR-V, so the check relies on the driver rounding f32 to nearest even by default, as the products do, and a product that rounds up to 2^-126 is the emitted product. `v_fma_mix_f32` follows this rule with mixed f16/f32 sources too, measured on RDNA2 (gfx1036) with f16 in either multiplicand slot, low and high half selection, and an f32 or f16 zero addend: f32 `0x00801002` times f16 `0x3bff` gives `0x00800000` to nearest at IEEE=0 and IEEE=1, while the neighbours `0x00801001` and `0x00801003` give zero and `0x00800001`. The tiny-product boundary was also measured for both signs in all four f32 round modes at IEEE=0; Its f16 sources are flushed to a zero of their sign when bit 0 of the f16/f64 denormal field is clear (0 and 2), whatever the f32 field, measured on RDNA2 (gfx1031, Radeon RX 6700 XT, AMD Windows driver 32.0.21045.5002 through HIP) at IEEE=0 and IEEE=1 with f32 denormal modes 0 and 3 and f16 denormal modes 0 to 3, over 2744 triples of 14 halves with all three sources f16 and 1960 rows with an f32 multiplicand. `v_fma_mixlo_f16` and `v_fma_mixhi_f16` flush their f32 sources too and keep a product below 2^-126, measured on RDNA2 (gfx1036). `v_mad_legacy_f32` and `v_mac_legacy_f32` flush their inputs, their product and their result in every mode, see its entry; their product is still checked for tininess on its exact value, so one below 2^-126 gives zero even when it rounds to 2^-126, as the console's separately rounded encoding is not measured at this boundary. Denormal modes 1 and 2 throw for the other instructions. Not covered, and not measurable on RDNA2 (no such opcodes) or not measured: `v_mac_f32`, `v_mad_f32`, `v_madak_f32` and `v_madmk_f32` (rounded separately), `v_dot2_f32_f16` and `v_div_fmas_f32` (on hardware `v_div_fmas_f32` flushes only its result), f32 interpolation and `ds_add_f32` keep the previous lowering; `ds_min/max/cmpst_f32` flush both operands by the field, see the LDS float atomics entry. With f32 denormals kept (3) the results depend on the driver's default denormal behaviour, as no `DenormPreserve` execution mode is requested; `v_ldexp_f32` and the f32 compares are the exceptions. `v_cmp_X_f32` and `v_cmpx_X_f32` compare the operands as sign-magnitude integers with the NaN cases handled apart, so a denormal is not read as zero, measured on RDNA2 (gfx1036) over 256 operand pairs per opcode at IEEE=0 and IEEE=1 with f32 denormals kept and flushed. `v_ldexp_f32` preserves subnormal inputs and rounds a subnormal result in the f32 round mode of `FLOAT_MODE`, measured on RDNA2 (gfx1036) over 2163 rows in each of the four round modes at IEEE=0 and IEEE=1. An overflow rounds like any other result (infinity, or the largest finite value when the mode rounds that sign toward zero). With f32 denormals flushed it flushes a subnormal input and result, measured over the same rows in each round mode at IEEE=0 only. A device whose driver splits `Fma` into a multiply and an add flushes a product below 2^-126 before adding a non-zero addend in `v_fma_f32` and `v_fmac_f32`: `0x1fffffff * 0x1fffffff + 0x00800000` gives `0x00800000` instead of the fused `0x00ffffff` on RADV RENOIR (Mesa 26.2.4), whose compiled shader has `v_mad_f32` and no `v_fma_f32`. [F32DenormalFlush](../../../core/libs/prx/libSceAgcDriver/tests/execution/F32DenormalFlush.cpp) accepts that sum on such a device. - [v_rcp, v_rsq, v_sqrt, v_log, v_exp, v_sin and v_cos](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) (f32 and f16) use the driver's approximation for finite results, not the console's: the low bits can differ. Only NaN, invalid, zero and infinite results and the exact sin/cos zeros are chosen on the bits. Measured on RDNA2 (gfx1036) over every f16 input and 32,832 f32 inputs: RADV returns the hardware's bits for every input of rcp, rsq, sqrt and log and all but one of exp (f16 0x11c5, 1 ULP); NVIDIA (615.71.09) stays within 2 ULP except `v_log_f32` near 1.0 (next entry). [v_sin and v_cos](../../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) take the driver's sine or cosine of the radians of the exact cycle, except within 2^-12 of a turn from a zero of the function, where the result is the linear term of the exact distance to the zero: the hardware's constant 6.2831826 (0x40c90fd5) at the integers, which the hardware applies exactly to inputs below 2^-12 of a turn, and the f32 2π at the half and quarter turns, where the hardware is within 7 ULP of it. Outside those bands, a driver result more than 4 ULP from a Taylor series in turns of the exact cycle is replaced by the series, which is within 2 ULP of the true sine and cosine; the hardware is up to 9 ULP from them (gfx1035, 243,092 f32 inputs), so results stay within 13 ULP of the hardware on any driver. RADV returns the hardware's bits for every f16 input and for 32,422 sine and 32,486 cosine results of the 32,832 f32 inputs; the rest are 1 ULP off, except up to 14 ULP on either side of non-zero integers and up to 15 ULP beyond ±256 cycles, where the hardware's own reduction differs from the exact fraction, and up to 6 ULP inside the half- and quarter-turn bands. NVIDIA's own sine is within 1 ULP on 93% of the f32 inputs and up to 1,166 ULP off just outside the bands, its cosine within 163 ULP; the series replaces those results. A denormal f32 input with f32 denormals kept gives the hardware the input times 6.2831826; both hosts flush it. - [v_ldexp_f32 in denormal modes 1 and 2](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) follows the `FLOAT_MODE` f32 denormal field: mode 1 keeps a subnormal input and flushes a result below 2^-126 to a zero of its sign in every f32 round mode, also one that rounds to 2^-126 at subnormal precision (`0x3fffffff` scaled by 2^-127); mode 2 flushes a subnormal input to a zero of its sign and rounds a subnormal result in the f32 round mode of `FLOAT_MODE`, as mode 3 does. Measured on RDNA2 (gfx1030, Radeon RX 6800, AMD Windows driver 32.0.21045.5002 through HIP) over 10,260 rows (54 inputs of both signs: zeros, subnormals, normals near 2^-126 and 1.0, the largest finite value, infinities and NaNs, each scaled by 190 exponents from -2^31 to 2^31-1) at IEEE=0 and IEEE=1 with each f32 denormal mode 0 to 3 and f32 round mode 0 to 3, DX10_CLAMP=1, f16 denormals kept, f16 round to nearest even and FP16_OVFL=0; with the `clamp` modifier the result is clamped as in the other modes. The PS5's gfx1013 is not measured. - [v_log_f32 and v_log_f16](../../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) keep the driver's log2 unless, for inputs within 1/8 of 1.0, it is more than 4 ULP from an eight-term series in x - 1, which then replaces it. `v_log_f16` goes through the same f32 lowering and is rounded to half. The series is evaluated for every `v_log` and chosen with `OpSelect` rather than a branch, so its seven fused multiply-adds and two multiplies are an unconditional cost. Measured on RDNA2 over all 3,145,728 f32 inputs within 1/8 of 1.0 (gfx1035, IEEE 0, f32 denormals flushed), the hardware is within 1 ULP of the correctly rounded log2 and the series within 2 ULP of it and within 3 ULP of the hardware (2 except at 0x3f752e47, 0x3f7baab0 and 0x3f7bc4fb), so a kept driver value is within 7 ULP of the hardware and a driver that returns the hardware's bits never triggers the gate; RADV (Mesa 26.2.4) returns the hardware's bits on the 9,280 such inputs measured on gfx1036; NVIDIA (615.71.09) was up to 23,947,836 ULP off at 1 - 2^-24 and is within 6 ULP inside the band after the change; outside it its own log2 stays up to 9 ULP off (measured up to 1.375). Over every f16 input, the change moves one `v_log_f16` result on NVIDIA (0x3bed, 1 ULP, onto the hardware's bits) and none on RADV. From a08adae15fdb64a73f2420445da8888ce6c38750 Mon Sep 17 00:00:00 2001 From: thuanquang Date: Sat, 17 Oct 2026 14:08:08 +0700 Subject: [PATCH 0664/1000] fix(shader): flush v_div_fixup_f64 src1 and src2 by the f64 denormal field On RDNA2 (gfx1031) f16/f64 denormal modes 0 and 2 flush the denominator and numerator of v_div_fixup_f64 to a zero of the same sign and keep a denormal quotient and result, as v_div_fixup_f32 does with the f32 field. The translation used them unflushed, so 0 / 2^-1074 gave 0 instead of the default NaN. Co-Authored-By: Claude Opus 5.5 --- .../execution/DivFixupF64DenormalModes.cpp | 149 ++++++++++++++++++ .../Translation/src/Float64Instructions.cpp | 9 +- docs/dev/debt/shader.md | 1 + 3 files changed, 157 insertions(+), 2 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/DivFixupF64DenormalModes.cpp diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DivFixupF64DenormalModes.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DivFixupF64DenormalModes.cpp new file mode 100644 index 0000000000..b609850c3f --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DivFixupF64DenormalModes.cpp @@ -0,0 +1,149 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t ValueCount = 10; +constexpr std::uint32_t Threads = 1024; +constexpr std::uint32_t Inputs = 8; +constexpr std::uint32_t Results = 2; +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; + +alignas(256) constexpr std::array Code{ + 0x34020085u, 0x34040083u, 0xe0381000u, 0x80000401u, 0xe0341010u, 0x80000801u, 0xbf8c3f70u, 0xd560000au, + 0x04220d04u, 0xe0741000u, 0x80010a02u, 0xbf810000u, +}; + +constexpr std::uint64_t Values[ValueCount] = { + 0x0000000000000000ull, 0x8000000000000000ull, 0x0000000000000001ull, 0x800fffffffffffffull, 0x0010000000000000ull, + 0x3ff0000000000000ull, 0xbff0000000000000ull, 0x7fefffffffffffffull, 0x7ff0000000000000ull, 0x7ff8000000000000ull, +}; + +struct Mode { + const char* name; + std::optional value; + bool flushed; + bool ieee; +}; + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +std::string Hex(std::uint64_t value) { + char text[24]; + std::snprintf(text, sizeof(text), "0x%016llx", static_cast(value)); + return text; +} + +std::array Sources(std::uint32_t tid) { + const std::uint32_t row = tid < ValueCount * ValueCount * ValueCount ? tid : 0u; + return {Values[row / (ValueCount * ValueCount)], Values[row / ValueCount % ValueCount], Values[row % ValueCount]}; +} + +void Run(AgcDriver::VulkanDevice& device, const Mode& mode) { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto sources = Sources(tid); + for (std::uint32_t index = 0; index < sources.size(); ++index) { + Input[tid * Inputs + index * 2u] = static_cast(sources[index]); + Input[tid * Inputs + index * 2u + 1u] = static_cast(sources[index] >> 32u); + } + Input[tid * Inputs + 6u] = 0u; + Input[tid * Inputs + 7u] = 0u; + } + Output.fill(0xdeadbeefu); + std::vector userData(8, 0u); + const auto input = BufferDescriptor(Input.data(), static_cast(Input.size() * 4u)); + const auto output = BufferDescriptor(Output.data(), static_cast(Output.size() * 4u)); + std::copy(input.begin(), input.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4); + const std::span code(Code); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + request.context.floatMode = mode.value; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); +} + +std::uint64_t Fixup(std::uint64_t quotient, std::uint64_t denominator, std::uint64_t numerator, bool flushed, bool ieee) { + constexpr std::uint64_t Magnitude = 0x7fffffffffffffffull; + constexpr std::uint64_t Infinity = 0x7ff0000000000000ull; + const auto flush = [&](std::uint64_t value) { return flushed && (value & Infinity) == 0u ? value & ~Magnitude : value; }; + denominator = flush(denominator); + numerator = flush(numerator); + const auto nan = [&](std::uint64_t value) { return (value & Magnitude) > Infinity; }; + const auto infinite = [&](std::uint64_t value) { return (value & Magnitude) == Infinity; }; + const auto zero = [&](std::uint64_t value) { return (value & Magnitude) == 0u; }; + const auto exponent = [](std::uint64_t value) { return static_cast((value >> 52u) & 0x7ffu); }; + const std::uint64_t sign = (denominator ^ numerator) & ~Magnitude; + if (nan(numerator)) return ieee ? numerator | 0x0008000000000000ull : numerator; + if (nan(denominator)) return ieee ? denominator | 0x0008000000000000ull : denominator; + if ((zero(denominator) && zero(numerator)) || (infinite(denominator) && infinite(numerator))) return 0xfff8000000000000ull; + if (zero(denominator) || infinite(numerator)) return sign | Infinity; + if (infinite(denominator) || zero(numerator)) return sign; + if (exponent(numerator) - exponent(denominator) < -1075) return sign; + if (nan(quotient)) return sign | Infinity; + return sign | (quotient & Magnitude); +} + +void Check(const Mode& mode) { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto sources = Sources(tid); + const std::uint64_t expected = Fixup(sources[0], sources[1], sources[2], mode.flushed, mode.ieee); + const std::uint64_t actual = Output[tid * Results] | (static_cast(Output[tid * Results + 1u]) << 32u); + Require(actual == expected, std::string("v_div_fixup_f64 ") + mode.name + " of " + Hex(sources[0]) + ", " + Hex(sources[1]) + ", " + Hex(sources[2]) + " is " + Hex(actual) + ", expected " + Hex(expected)); + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + const Mode modes[] = { + {"without a known float mode", std::nullopt, false, false}, + {"f64 denormal mode 0, IEEE mode on", ShaderRecompiler::ShaderFloatMode{0x30u, true, true, false}, true, true}, + {"f64 denormal mode 0, IEEE mode off", ShaderRecompiler::ShaderFloatMode{0x30u, true, false, false}, true, false}, + {"f64 denormal mode 1, IEEE mode on", ShaderRecompiler::ShaderFloatMode{0x70u, true, true, false}, false, true}, + {"f64 denormal mode 1, IEEE mode off", ShaderRecompiler::ShaderFloatMode{0x70u, true, false, false}, false, false}, + {"f64 denormal mode 2, IEEE mode on", ShaderRecompiler::ShaderFloatMode{0xb0u, true, true, false}, true, true}, + {"f64 denormal mode 2, IEEE mode off", ShaderRecompiler::ShaderFloatMode{0xb0u, true, false, false}, true, false}, + {"f64 denormal mode 3, IEEE mode on", ShaderRecompiler::ShaderFloatMode{0xf0u, true, true, false}, false, true}, + {"f64 denormal mode 3, IEEE mode off", ShaderRecompiler::ShaderFloatMode{0xf0u, true, false, false}, false, false}, + }; + for (const Mode& mode : modes) { + Run(*device, mode); + Check(mode); + } + std::puts("v_div_fixup_f64 denormal mode tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/shader/recompiler/Translation/src/Float64Instructions.cpp b/core/shader/recompiler/Translation/src/Float64Instructions.cpp index 00a0bac5b3..2f355fe3bc 100644 --- a/core/shader/recompiler/Translation/src/Float64Instructions.cpp +++ b/core/shader/recompiler/Translation/src/Float64Instructions.cpp @@ -149,9 +149,14 @@ bool TranslationContext::vDivFmasF64(const RdnaInstruction& inst) { } bool TranslationContext::vDivFixupF64(const RdnaInstruction& inst) { + const auto flush = [&](const std::array& word) { + if (!floatMode.has_value() || (floatMode->floatMode & 0x40u) != 0u) return word; + const IrU1 tiny(ir.IEqual(ir.BitwiseAnd(word[1].Value(), ir.Constant(0x7ff00000u)), ir.Constant(0u))); + return std::array{IrU32(ir.Select(tiny.Value(), ir.Constant(0u), word[0].Value())), IrU32(ir.Select(tiny.Value(), ir.BitwiseAnd(word[1].Value(), ir.Constant(0x80000000u)), word[1].Value()))}; + }; const auto quotient = readF64Bits(sourceAt(inst, 0u)); - const auto denominator = readF64Bits(sourceAt(inst, 1u)); - const auto numerator = readF64Bits(sourceAt(inst, 2u)); + const auto denominator = flush(readF64Bits(sourceAt(inst, 1u))); + const auto numerator = flush(readF64Bits(sourceAt(inst, 2u))); const auto magnitude = [&](IrU32 high) { return IrU32(ir.BitwiseAnd(high.Value(), ir.Constant(0x7fffffffu))); }; const auto lowNonzero = [&](const std::array& word) { return IrU1(ir.INotEqual(word[0].Value(), ir.Constant(0u))); }; const auto isNan = [&](const std::array& word) { diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index 5b449de647..1fc471152f 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -89,6 +89,7 @@ - [SINT storage images](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) of 8- and 16-bit components saturate each stored 32-bit value to the component range, as adriadam10 measured for image stores on RDNA2 (gfx1035) in #944 (448 rows match); the PS5's gfx1013 is not measured. A D16 store sign-extends each 16-bit half before that saturation, as measured on RDNA2 (gfx1035) for `image_store d16` to all eight SINT formats with DMASK 0xf, 0x7, 0x5, 0x3, 0x2, 0xa and 0x1 (1792 rows match). - [`StoreTexel`](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) writes channel 1 of a two-channel format from the data slot whose DST_SEL is Y, for SINT and UINT alike. With DST_SEL X,Y,Z,W, gfx1035 writes it from the W slot, as adriadam10 measured in #944. - [v_ldexp_f16](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) follows the `FLOAT_MODE` f16 denormal field: bit 0 preserves the half input and bit 1 the half result. Otherwise a denormal input is flushed to its signed zero, and a result whose exact scaled value is below the minimum normal (2^-14) is flushed to its signed zero before rounding, so `v_ldexp_f16 0x07ff, -1` gives +0 instead of 0x0400. Measured on RDNA2 (gfx1036) with round to nearest, IEEE 0 and 1 and denorm16 0..3, over every half magnitude 0x0000..0x0fff of both signs scaled by 2^-1 and 2^0 (16384 rows per mode): no mismatch. NaN and infinity handling and destination high-half preservation are unchanged. Shaders without a known float mode keep the previous lowering. +- [v_div_fixup_f64](../../../core/shader/recompiler/Translation/src/Float64Instructions.cpp) flushes its src1 and src2 to a zero of the same sign when bit 0 of the `FLOAT_MODE` f16/f64 denormal field is clear (0 and 2) and keeps a denormal src0 and result in every mode, as `v_div_fixup_f32` does with the f32 field. Measured on RDNA2 (gfx1031, Radeon RX 6700 XT, AMD Windows driver 32.0.21045.5002 through HIP) over 1728 triples of 12 f64 values (zeros, denormals, the minimum normal, ±1, the largest finite value, infinity and quiet and signaling NaNs) at IEEE=0 and IEEE=1 with denorm16 0..3. Shaders without a known float mode keep the previous lowering. The PS5's gfx1013 is not measured. - [v_mullit_f32](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) follows the hardware measured with the MODE register at IEEE=1 and denormals kept (a signaling NaN src0 is quieted only with IEEE=1, as measured at IEEE=0 too; a positive denormal src2 counts as positive); with f32 denormals flushed it follows the f32 denormals entry below, and denormal modes 1 and 2 throw. - [v_mad_legacy_f32 and v_mac_legacy_f32](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) (VOP3 0x140, VOP2 0x06) round the product to f32 before the add, give a +0 product for a zero factor, and flush their f32 denormal sources, their product and their result to a zero of the same sign in every f32 denormal mode, an exception to the `FLOAT_MODE` model in [f32DenormalFlushFor](../../../core/shader/recompiler/Translation/src/VectorInstructions.cpp). Measured on gfx1010 (RX 5700 XT) with `tools/hw-oracle/template.s`, assembled by the AMD Windows driver's `amd_comgr_2.dll` and launched through its `amdhip64_6.dll` instead of `oracle.c` and ROCr, over the 32 rows of the five `FmaLegacy.cpp` and the four `MacLegacy.cpp` encodings: identical in wave32 and wave64 at IEEE 0 and 1 with f32 denormal modes 0 to 3, except that the two signaling NaN rows are quieted only with IEEE=1 (#1281, #1290). A sum of two zeros takes its sign on the bits (+0 unless both are -0), as lavapipe returned -0 for +0 + -0. The PS5's gfx1013 is not measured; LLVM decodes the encoding as `v_mad_legacy_f32` there, as on gfx1010. RDNA2 differs: on gfx1031 (RX 6700 XT, measured by pandather in #1281) the encoding is `v_fma_legacy_f32` and fused, with the same zero rule and denormal-source flush, so an RDNA2 measurement is not evidence for the console. - [v_fma_mixlo_f16 and v_fma_mixhi_f16](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) round the exact product-sum once to f16 with the f64 arithmetic of v_interp_p2_f16, so they need the Float64 capability. On RDNA2 (gfx1036) they follow the `FLOAT_MODE` f16 round and denormal fields, not the f32 round field: -1.5 * 2^-24 + 2^-126 from f32 sources (0xbfc00000, 0x33800000, 0x00800000) gives 0x8001 with f16 denormals kept and round to nearest even, 0x8000 with f16 denormals flushed and 0x8002 with f16 round toward -infinity. With f16 denormals kept, every f16 rounding mode is translated: the exact product-sum is rounded once in the mode's direction. All four modes match RDNA2 (gfx1036) on the 2,233 rows of the #2631 sweep, including `FLOAT_MODE` 0xcc (toward zero, used by ps5-opengl) and the exact cancellations toward -infinity; the PS5's gfx1013 is not measured. A result beyond 65504 becomes infinity or 65504 as IEEE 754 gives for the mode. Flushed f16 denormals still throw, and packed f16 still throws in every mode but round to nearest even with f16 denormals kept. `FP16_OVFL` set is not measured. From 89d255c5f397f43890ce7e7419dcb5ce6c6a9709 Mon Sep 17 00:00:00 2001 From: AlxcBTW <187763517+AlxcBTW@users.noreply.github.com> Date: Sat, 17 Oct 2026 09:08:43 +0200 Subject: [PATCH 0665/1000] perf(agc): serve compute dispatches whose read-only buffers moved from the cached template The content key of a compute stage leaves the base address and size of its read-only guest buffers out, as a draw's key does. A template hit whose such buffers moved is proved the way a draw's is (ShaderResources::MovedDispatchBuffers: read-only, read in place when built, in reach, nothing pending over the new range) and binds snapshots of the new ranges in a descriptor set of its own, kept with the batch that records the dispatch. A refused proof builds the dispatch fresh, as before. APS5_NO_DISPATCH_MOVE=1 turns the path off, APS5_DISPMOVE_MAX_KIB caps the bytes snapshotted per dispatch. A template whose storage image lies over memory that a depth surface covers is no longer proved from stamps (fastRevalidate): cachedStorageTexture answers such a binding with the plane's colour mirror, copied in from the depth image for every use, and nothing stamps the appearance of a depth surface after the build. The full walk decides, and a stale image can no longer be copied over the depth image. Tests: Graphics.cpp (verdicts of the proof), execution DispatchMove (real device: moved, exact, shortened, stored-over, own-output and written-range refusals) and DispatchMoveDepth (a depth surface appearing over a template's plain storage image, planes inside and outside the write watch, imports watched and unwatched). --- .../Execution/include/VulkanDevice.hpp | 4 + .../Execution/src/VulkanDevice.cpp | 131 ++++- .../Graphics/include/DepthSurface.hpp | 1 + .../Graphics/include/ShaderResources.hpp | 13 +- .../Graphics/src/DepthSurface.cpp | 5 + .../Graphics/src/ShaderResources.cpp | 75 ++- .../prx/libSceAgcDriver/tests/Graphics.cpp | 80 +++ .../tests/execution/DepthFastClearHarness.hpp | 4 +- .../tests/execution/DispatchMove.cpp | 263 ++++++++++ .../tests/execution/DispatchMoveDepth.cpp | 460 ++++++++++++++++++ 10 files changed, 1024 insertions(+), 12 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/DispatchMove.cpp create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/DispatchMoveDepth.cpp diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp index 4aa2fa07f2..ff47f6adbc 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp @@ -185,6 +185,10 @@ public: std::uint64_t created, underGpuMutex; }; static ComputePipelineCreation ComputePipelineCounts(); + struct DispatchMoveCount { + std::uint64_t checked, exact, moved, refused, filtered; + }; + static DispatchMoveCount DispatchMoveCounts(); void ResolveMemory(std::uint64_t address, std::size_t bytes, bool writable); void* Window() const; void Resize(std::uint32_t width, std::uint32_t height); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 44c1dce86b..9bf731eaf1 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -3716,11 +3716,24 @@ bool TemplateDataRefresh() { return enabled; } +bool DispatchMove() { + static const bool enabled = std::getenv("APS5_NO_DISPATCH_MOVE") == nullptr && TemplateDataRefresh(); + return enabled; +} + +std::size_t DispatchMoveBytes() { + static const std::size_t bytes = [] { + const char* text = std::getenv("APS5_DISPMOVE_MAX_KIB"); + return static_cast(text != nullptr ? std::strtoull(text, nullptr, 10) : 1024ull) * 1024u; + }(); + return bytes; +} + // The content key of a compute stage, naming the device: the cache is process-wide and the driver // replaces the headless device with the windowed one while workers may still use the old one, so // the key names the device that built the entry (its descriptor set and pooled buffers belong to it). ResourceCache::Key DispatchContentKey(const Graphics::CompiledShader& shader, VkDevice device) { - auto key = Graphics::ShaderResources::ContentKey(shader, !TemplateDataRefresh()); + auto key = Graphics::ShaderResources::ContentKey(shader, !TemplateDataRefresh(), DispatchMove()); const auto deviceHandle = reinterpret_cast(device); key.push_back(static_cast(deviceHandle)); key.push_back(static_cast(deviceHandle >> 32u)); @@ -3948,6 +3961,59 @@ RecipeCounters& Recipes() { return counters; } +constexpr std::size_t MoveRefusals = static_cast(Graphics::ShaderResources::MoveRefusal::Count); + +struct DispatchMoveCounters { + std::atomic checked{0}, exact{0}, moved{0}, buffers{0}, bytes{0}; + std::array, 5> sizes{}; + std::array, MoveRefusals> refused{}; + std::array, MoveRefusals> filtered{}; + std::atomic lastReport{0}; +}; + +DispatchMoveCounters& DispatchMoves() { + static DispatchMoveCounters counters; + return counters; +} + +void reportDispatchMove() { + auto& moves = DispatchMoves(); + const auto nowMs = std::chrono::duration_cast(std::chrono::steady_clock::now().time_since_epoch()).count(); + auto last = moves.lastReport.load(); + if (nowMs - last < 10000 || !moves.lastReport.compare_exchange_strong(last, nowMs)) return; + const auto take = [](std::atomic& counter) { return static_cast(counter.exchange(0, std::memory_order_relaxed)); }; + const auto list = [&](std::array, MoveRefusals>& counters) { + std::string text; + unsigned long long total = 0; + char item[80]; + for (std::size_t i = 1; i < MoveRefusals; ++i) { + const auto count = take(counters[i]); + total += count; + if (count == 0) continue; + std::snprintf(item, sizeof(item), "%s %s %llu", text.empty() ? "" : ",", Graphics::ShaderResources::MoveRefusalName(static_cast(i)), count); + text += item; + } + std::snprintf(item, sizeof(item), "%llu (", total); + return std::string(item) + text + ")"; + }; + const auto checked = take(moves.checked); + const auto exact = take(moves.exact); + const auto moved = take(moves.moved); + const auto buffers = take(moves.buffers); + const auto bytes = take(moves.bytes); + unsigned long long sizes[5]; + for (std::size_t i = 0; i < 5; ++i) sizes[i] = take(moves.sizes[i]); + const auto refused = list(moves.refused); + const auto filtered = list(moves.filtered); + AgcDriver::ProfilePrint_nid_no_patch("[dispmove] (10 s) %llu template hits checked: %llu with every read-only buffer where it was built, %llu with %llu moved ones (%.2f MiB snapshotted; per dispatch up to 1 KiB %llu, 16 KiB %llu, 256 KiB %llu, 4 MiB %llu, more %llu); refused %s; stage A dropped cached objects: %s\n", checked, exact, moved, buffers, static_cast(bytes) / 1048576.0, sizes[0], sizes[1], sizes[2], sizes[3], sizes[4], refused.c_str(), filtered.c_str()); +} + +void noteDispatchMoveSize(std::size_t bytes) { + auto& moves = DispatchMoves(); + const std::size_t bucket = bytes <= 1024 ? 0 : bytes <= 16384 ? 1 : bytes <= 262144 ? 2 : bytes <= 4194304 ? 3 : 4; + moves.sizes[bucket].fetch_add(1, std::memory_order_relaxed); +} + void reportRecipes() { auto& recipes = Recipes(); const auto nowMs = std::chrono::duration_cast(std::chrono::steady_clock::now().time_since_epoch()).count(); @@ -4098,12 +4164,23 @@ struct RecordedDispatch { // Whether the data refresh recorded anything. bool refreshed = false; bool resourceKeyMatched = false; + std::shared_ptr bindings{}; }; VkDevice VulkanDevice::Device() const { return state->device; } +VulkanDevice::DispatchMoveCount VulkanDevice::DispatchMoveCounts() { + auto& moves = DispatchMoves(); + DispatchMoveCount count{moves.checked.load(), moves.exact.load(), moves.moved.load(), 0, 0}; + for (std::size_t i = 1; i < MoveRefusals; ++i) { + count.refused += moves.refused[i].load(); + count.filtered += moves.filtered[i].load(); + } + return count; +} + bool VulkanDevice::DispatchRecipes() { static const bool noDispatchRecipe = std::getenv("APS5_NO_DISPATCH_RECIPE") != nullptr; return !noDispatchRecipe; @@ -4226,6 +4303,13 @@ std::shared_ptr VulkanDevice::PrepareDispatch(const ShaderReco prepared->key = DispatchContentKey(compute, context.device); phase(PreparedDispatch::PrepareKey); cached = state->resourceCache.Find(prepared->key); + if (cached != nullptr && DispatchMove()) { + Graphics::ShaderResources::MoveProof proof; + if (!cached->MovedDispatchBuffers(compute, nullptr, DispatchMoveBytes(), proof).has_value()) { + DispatchMoves().filtered[static_cast(proof.refusal)].fetch_add(1, std::memory_order_relaxed); + cached = nullptr; + } + } prepared->cached = cached; phase(PreparedDispatch::PrepareFind); } @@ -4423,6 +4507,10 @@ void VulkanDevice::recordDispatch(RecordedDispatch& record) { recorder.Keep(record.objects); recorder.Keep(record.resources); if (record.resources != nullptr) recorder.KeepBytes(record.resources.get(), record.resources->CopiedBytes()); + if (record.bindings != nullptr) { + recorder.Keep(record.bindings); + for (const auto& snapshot : record.bindings->snapshots) recorder.KeepBytes(snapshot.buffer.get(), snapshot.buffer->Bytes().size()); + } recordStep(PhaseRecordKeeps); if (record.dataRefresh != RecordedDispatch::DataRefresh::None) { // The template's data buffers take this dispatch's words: a transfer write the pre-dispatch @@ -4472,7 +4560,8 @@ void VulkanDevice::recordDispatch(RecordedDispatch& record) { ++d.preBarriersRecorded; } context.Resolved(&Graphics::DeviceFunctions::cmdBindPipeline, "vkCmdBindPipeline")(commands, VK_PIPELINE_BIND_POINT_COMPUTE, record.objects->pipeline); - resources.Bind(commands, VK_PIPELINE_BIND_POINT_COMPUTE, record.objects->layout); + if (record.bindings != nullptr && record.bindings->allocation.set != VK_NULL_HANDLE) context.Resolved(&Graphics::DeviceFunctions::cmdBindDescriptorSets, "vkCmdBindDescriptorSets")(commands, VK_PIPELINE_BIND_POINT_COMPUTE, record.objects->layout, 0, 1, &record.bindings->allocation.set, 0, nullptr); + else resources.Bind(commands, VK_PIPELINE_BIND_POINT_COMPUTE, record.objects->layout); if (const auto dwords = record.shader->program->workgroupMemoryDwords; dwords != 0) { auto memory = state->WorkgroupMemoryFor(context, record.x, record.y, record.z, dwords); context.Resolved(&Graphics::DeviceFunctions::cmdBindDescriptorSets, "vkCmdBindDescriptorSets")(commands, VK_PIPELINE_BIND_POINT_COMPUTE, record.objects->layout, ShaderRecompiler::WorkgroupMemoryDescriptorSet, 1, &memory->set, 0, nullptr); @@ -4686,6 +4775,7 @@ VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::Rec // dispatch's words at the record (ShaderResources::RefreshData). bool fromCache = false; double revalidateMs = 0; + std::shared_ptr overlay; // The batches the pre-sync (PrepareDispatch) waited for are reaped first: their completions // (CPU write-backs) land before stage B or a cached object's Revalidate reads, and the flush // hook then finds nothing pending over their surfaces. @@ -4764,9 +4854,36 @@ VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::Rec auto cached = preparedFind && prepared != nullptr && prepared->cached != nullptr ? std::move(prepared->cached) : state->resourceCache.Find(contentKey); if (cached != nullptr) { if (cached->Revalidate(shaders[0])) { - resources = std::move(cached); - fromCache = true; - ++d.cacheHits; + bool usable = true; + if (DispatchMove()) { + auto& moves = DispatchMoves(); + Graphics::ShaderResources::MoveProof proof; + auto moved = cached->MovedDispatchBuffers(shaders[0], state->recorder.get(), DispatchMoveBytes(), proof); + moves.checked.fetch_add(1, std::memory_order_relaxed); + if (moved.has_value() && !moved->empty()) { + overlay = cached->PrepareDrawBindings(*state->recorder, *moved, true); + if (overlay == nullptr) { + proof.refusal = Graphics::ShaderResources::MoveRefusal::Bindings; + moved.reset(); + } + } + if (!moved.has_value()) { + usable = false; + moves.refused[static_cast(proof.refusal)].fetch_add(1, std::memory_order_relaxed); + } else if (moved->empty()) { + moves.exact.fetch_add(1, std::memory_order_relaxed); + } else { + moves.moved.fetch_add(1, std::memory_order_relaxed); + moves.buffers.fetch_add(proof.buffers, std::memory_order_relaxed); + moves.bytes.fetch_add(proof.bytes, std::memory_order_relaxed); + noteDispatchMoveSize(proof.bytes); + } + } + if (usable) { + resources = std::move(cached); + fromCache = true; + ++d.cacheHits; + } } else { state->resourceCache.Remove(contentKey, cached.get()); ++d.cacheInvalidated; @@ -4807,6 +4924,7 @@ VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::Rec AgcDriver::ProfilePrint_nid_no_patch("[rescache] %llu hits, %llu misses, %llu invalidated, %zu entries (dispatch + draw); template hits: %llu refreshed the data buffers (revalidate %.1f ms), %llu had the same words; Find calls %llu, Touch calls %llu\n", static_cast(d.cacheHits), static_cast(d.cacheMisses), static_cast(d.cacheInvalidated), state->resourceCache.Size(), static_cast(d.templateRefreshed), d.templateRevalidateMs, static_cast(d.templateSameWords), static_cast(ResourceCache::Finds()), static_cast(ResourceCache::Touches())); AgcDriver::ProfilePrint_nid_no_patch("[vk] deviceProc lookups inside the device call: %llu (%.2f per dispatch); pre-dispatch barriers recorded %llu, skipped %llu; compute pipelines created %llu (%llu under the GPU mutex)\n", static_cast(d.procLookups), d.profiledDispatches != 0 ? static_cast(d.procLookups) / static_cast(d.profiledDispatches) : 0.0, static_cast(d.preBarriersRecorded), static_cast(d.preBarriersSkipped), static_cast(computePipelinesCreated.load(std::memory_order_relaxed)), static_cast(computePipelinesCreatedLocked.load(std::memory_order_relaxed))); reportRecipes(); + reportDispatchMove(); } timing.Mark("shader_resources"); timer.phase(PhaseResources); @@ -4828,6 +4946,7 @@ VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::Rec IndirectOutcome outcome{0, 0}; RecordedDispatch record{&context, &shaders[0], resources, objects, pushStages, &pushBytes, x, y, z, arguments, nullptr, programAddress, fromCache && TemplateDataRefresh() ? RecordedDispatch::DataRefresh::Words : RecordedDispatch::DataRefresh::None, 0, revalidateMs, &timer}; record.resourceKeyMatched = fromCache; + record.bindings = overlay; if (arguments != 0) decideIndirect(record, outcome, groupsText); // The indirect hold total below still covers the whole call of a CPU-resolved one. const bool indirect = record.arguments != 0 || outcome.cpuReason != 0; @@ -4881,7 +5000,7 @@ VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::Rec // object the resource cache serves under this content key (reusable: no lease, no copied // writes, every direct region import- or mirror-served), so a hit's proof is the template's // Revalidate and nothing needs completion work. - if (recipeOut != nullptr && DispatchRecipes() && cacheable && resources->Reusable() && !resources->NeedsCompletion() && objects != nullptr && !contentKey.empty()) { + if (recipeOut != nullptr && DispatchRecipes() && cacheable && overlay == nullptr && resources->Reusable() && !resources->NeedsCompletion() && objects != nullptr && !contentKey.empty()) { auto recipe = std::make_shared(); recipe->device = state->device; recipe->templateRef = resources; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp index 2f54bee302..40e1076b79 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp @@ -26,6 +26,7 @@ void RequireNoNativeDepthFeedback_nid_no_patch(const Context& context, const Dep void ClearDepthSurfaces(VkDevice device); void RetireDepthSurfaces(VkDevice device, std::uint64_t address, std::uint64_t bytes); bool DepthSurfaceAt(std::uint64_t address); +bool DepthSurfaceOverlaps(VkDevice device, std::uint64_t address, std::uint64_t bytes); bool DepthSurfaceAt(const Context& context, std::span words, const GuestTextureResource& resource); std::uint64_t HtileDepthClearAddress(std::span code, std::span userData, const std::array& numThreads); std::size_t HtileDepthClearBytes_nid_no_patch(std::span code, std::span userData, const ShaderRecompiler::ShaderComputeStageInfo& compute, std::span packet, std::uint64_t indirectArguments); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp index 8e25d227c9..ef87148b53 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp @@ -168,12 +168,18 @@ public: std::size_t size; std::vector words; }; - std::shared_ptr PrepareDrawBindings(Recorder& recorder, std::span moved = {}) const; + std::shared_ptr PrepareDrawBindings(Recorder& recorder, std::span moved = {}, bool movedOnly = false) const; std::optional> MovedReadOnlyBuffers(std::span shaders, Recorder& recorder) const; + enum class MoveRefusal : std::size_t { None, Shape, NotGuest, Written, Limit, Inaccessible, Overlap, Bindings, Count }; + struct MoveProof { + MoveRefusal refusal = MoveRefusal::None; + std::size_t buffers = 0; + std::size_t bytes = 0; + }; + static const char* MoveRefusalName(MoveRefusal refusal); + std::optional> MovedDispatchBuffers(const CompiledShader& shader, Recorder* recorder, std::size_t maxBytes, MoveProof& proof) const; static std::size_t MaxDrawSnapshotBytes(); void WriteBack(); - // Deferred completion: MarkGpuWrites registers the results the recorded work leaves on the GPU - // (storage images stay there; buffer ranges are noted so CPU reads wait); WriteBackBuffers runs // once the work completed. void MarkGpuWrites(Recorder& recorder); void WriteBackBuffers(); @@ -226,6 +232,7 @@ public: bool Reusable() const { return reusable; } const std::vector& MultisampleSources() const { return multisampleSources; } static bool NeverReusable(std::span shaders); + static std::uint64_t DepthGuardRefusals(); const std::vector>& SampledTextures() const { return textures; } bool ChecksFaults() const { return bda != nullptr; } void CheckFaults() const; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp index 4db10e6b76..651c305f19 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp @@ -1519,6 +1519,11 @@ bool DepthSurfaceAt(std::uint64_t address) { return std::any_of(surfaces().begin(), surfaces().end(), [&](const auto& surface) { return surface->active->load(std::memory_order_acquire) && !surface->retired && (surface->target.address == address || surface->target.stencilAddress == address); }); } +bool DepthSurfaceOverlaps(VkDevice device, std::uint64_t address, std::uint64_t bytes) { + std::lock_guard lock(surfacesMutex()); + return std::any_of(surfaces().begin(), surfaces().end(), [&](const auto& surface) { return surface->active->load(std::memory_order_acquire) && !surface->retired && surface->context.device == device && surface->Overlaps(address, bytes); }); +} + void RetireDepthSurfaces(VkDevice device, std::uint64_t address, std::uint64_t bytes) { std::lock_guard lock(surfacesMutex()); for (const auto& surface : surfaces()) { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index 926bfc67fa..f479b74778 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -2067,6 +2067,7 @@ void mixDataWords(std::uint64_t& hash, std::span words) { // ResourceCache::Find and Touch calls (the [rescache] line). std::atomic resourceCacheFinds{0}; std::atomic resourceCacheTouches{0}; +std::atomic depthGuardRefusals{0}; } @@ -2109,6 +2110,10 @@ void ShaderResources::noteReusable() { reusable = true; } +std::uint64_t ShaderResources::DepthGuardRefusals() { + return depthGuardRefusals.load(std::memory_order_relaxed); +} + bool ShaderResources::NeverReusable(std::span shaders) { const bool loopGuard = LoopGuardEnabled(); return std::any_of(shaders.begin(), shaders.end(), [loopGuard](const CompiledShader& shader) { @@ -2579,6 +2584,10 @@ bool ShaderResources::fastRevalidate(std::uint64_t serialBefore, std::spanDescriptor(); const auto address = own.baseAddress; const auto bytes = static_cast(image->GuestBytes()); + if (DepthSurfaceOverlaps(context.device, address, bytes)) { + depthGuardRefusals.fetch_add(1, std::memory_order_relaxed); + return fail(FastFail::Depth); + } if (GuestMemory::CollectWrites(address, bytes) == 0) return fail(FastFail::Collect); if (own.dccAddress != 0) { // Refresh's unchanged branch (its key compare against the keys the content was @@ -2682,6 +2691,10 @@ bool ShaderResources::fastRevalidateEach() { if (own.dccAddress != 0) return false; const auto address = own.baseAddress; const auto bytes = static_cast(image->GuestBytes()); + if (DepthSurfaceOverlaps(context.device, address, bytes)) { + depthGuardRefusals.fetch_add(1, std::memory_order_relaxed); + return false; + } if (GuestMemory::CollectWrites(address, bytes) == 0) return false; if (PendingStorageOverlaps(address, bytes, image)) return false; if (!StorageImageCached(context, image)) return false; @@ -4515,13 +4528,71 @@ std::optional> ShaderResources::MovedR return moved; } +const char* ShaderResources::MoveRefusalName(MoveRefusal refusal) { + switch (refusal) { + case MoveRefusal::None: return "none"; + case MoveRefusal::Shape: return "bindings differ"; + case MoveRefusal::NotGuest: return "not read in place"; + case MoveRefusal::Written: return "written or emptied"; + case MoveRefusal::Limit: return "over the limit"; + case MoveRefusal::Inaccessible: return "unmapped"; + case MoveRefusal::Overlap: return "pending write"; + case MoveRefusal::Bindings: return "no bindings made"; + case MoveRefusal::Count: break; + } + return "?"; +} + +std::optional> ShaderResources::MovedDispatchBuffers(const CompiledShader& shader, Recorder* recorder, std::size_t maxBytes, MoveProof& proof) const { + proof = {}; + const auto refuse = [&](MoveRefusal why) { + proof.refusal = why; + return std::optional>{}; + }; + std::vector moved; + if (shader.program == nullptr || usesBda) return refuse(MoveRefusal::Shape); + if (_set == VK_NULL_HANDLE) return moved; + for (const auto& binding : shader.program->bindings) { + if (binding.role != ShaderRecompiler::DescriptorRole::GuestBuffers) continue; + const auto kept = std::find_if(bindings.begin(), bindings.end(), [&](const Binding& item) { return item.layout.binding == binding.binding; }); + if (kept == bindings.end() || kept->allocations.size() != binding.count || binding.guestDescriptor.size() != static_cast(binding.count) * 4u) return refuse(MoveRefusal::Shape); + for (std::uint32_t element = 0; element < binding.count; ++element) { + const auto* words = binding.guestDescriptor.data() + static_cast(element) * 4u; + const ShaderRecompiler::ShaderBufferResource descriptor{{words[0], words[1], words[2], words[3]}}; + const auto address = descriptor.Base48(); + const auto size = descriptor.GetSize(); + const auto& item = allocations[kept->allocations[element]]; + const bool empty = size == 0 || address == 0; + if (!item.guest) { + if (empty) continue; + return refuse(MoveRefusal::NotGuest); + } + if (item.address == address && item.size == size) continue; + const bool written = element >= binding.bufferWritten.size() || binding.bufferWritten[element]; + if (written || empty || item.written) return refuse(MoveRefusal::Written); + if (size > context.limits.maxStorageBufferRange) return refuse(MoveRefusal::Limit); + const auto begin = address - item.adjustment; + const auto bytes = static_cast(GuestBufferMemory::ViewBytes(size, item.adjustment)); + if (maxBytes != 0 && proof.bytes + bytes > maxBytes) return refuse(MoveRefusal::Limit); + if (!GuestMemory::Accessible(reinterpret_cast(begin), bytes)) return refuse(MoveRefusal::Inaccessible); + const bool pending = recorder != nullptr ? recorder->PendingWriteOverlaps(begin, bytes) : Recorder::SnapshotWriteOverlaps(begin, bytes); + const std::array, 1> range{{{begin, begin + bytes}}}; + if (guestMemory.WritesOverlap(begin, bytes) || pending || StorageTexture::AnyPendingOverlaps(range) || AnyShadowedOverlaps(begin, bytes)) return refuse(MoveRefusal::Overlap); + moved.push_back({kept->allocations[element], address, static_cast(size)}); + ++proof.buffers; + proof.bytes += bytes; + } + } + return moved; +} + std::size_t ShaderResources::MaxDrawSnapshotBytes() { const char* text = std::getenv("APS5_DRAW_SNAPSHOT_MAX_MIB"); const auto mib = text != nullptr ? std::strtoull(text, nullptr, 10) : 64ull; return static_cast(mib * 1024ull * 1024ull); } -std::shared_ptr ShaderResources::PrepareDrawBindings(Recorder& recorder, std::span moved) const { +std::shared_ptr ShaderResources::PrepareDrawBindings(Recorder& recorder, std::span moved, bool movedOnly) const { if ((_set == VK_NULL_HANDLE && fragment.set == VK_NULL_HANDLE) || usesBda) return {}; const auto reads = guestMemory.InPlaceReads(); const auto maxSnapshotBytes = maxDrawSnapshotBytes; @@ -4561,7 +4632,7 @@ std::shared_ptr ShaderResources::PrepareDrawBindi address = override->address; size = override->size; } else { - if (!item.guest || item.written || guestMemory.WritesOverlap(item.address, item.size)) continue; + if (movedOnly || !item.guest || item.written || guestMemory.WritesOverlap(item.address, item.size)) continue; const bool direct = std::any_of(reads.begin(), reads.end(), [&](const auto& range) { return item.address >= range.first && item.address < range.second && item.size <= range.second - item.address; }); if (!direct || recorder.PendingWriteOverlaps(item.address, item.size)) continue; } diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index e2f1c24796..59df612761 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -4306,6 +4306,85 @@ void descriptorSnapshotTests() { } } +alignas(16) std::array guestMovedFirst{0x51515151, 0x52525252, 0x53535353, 0x54545454, 0x55555555, 0x56565656, 0x57575757, 0x58585858}; +alignas(16) std::array guestMovedSecond{0x61616161, 0x62626262, 0x63636363, 0x64646464, 0x65656565, 0x66666666, 0x67676767, 0x68686868}; + +void dispatchMoveTests() { + using AgcDriver::Graphics::CompiledShader; + using AgcDriver::Graphics::DescriptorCache; + using AgcDriver::Graphics::Recorder; + using AgcDriver::Graphics::ShaderResources; + using Refusal = ShaderResources::MoveRefusal; + mock = MockVulkan{}; + mock.allowCopies = true; + auto context = mockContext(); + { + DescriptorCache cache(context); + context.descriptorCache = &cache; + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + Recorder recorder(context); + ShaderRecompiler::RecompileResult built; + auto inputs = makeBinding(Role::GuestBuffers, 0, 2, join(vsharp(guestFirst.data(), 16), vsharp(guestSecond.data(), 32))); + inputs.bufferWritten = {false, false}; + built.bindings.push_back(inputs); + auto output = makeBinding(Role::GuestBuffers, 4, 1, vsharp(guestThird.data(), 8)); + output.bufferWritten = {true}; + built.bindings.push_back(output); + const CompiledShader shader{ShaderRecompiler::ShaderStage::Compute, &built, 0}; + ShaderResources resources(context, shader); + const auto moveTo = [&](std::size_t element, const void* where, std::uint32_t bytes) { + auto program = built; + const auto words = vsharp(where, bytes); + std::copy(words.begin(), words.end(), program.bindings[0].guestDescriptor.begin() + static_cast(element) * 4); + return program; + }; + const auto prove = [&](const ShaderRecompiler::RecompileResult& program, std::size_t maxBytes, ShaderResources::MoveProof& proof) { + return resources.MovedDispatchBuffers(CompiledShader{ShaderRecompiler::ShaderStage::Compute, &program, 0}, &recorder, maxBytes, proof); + }; + const auto refuses = [&](const ShaderRecompiler::RecompileResult& program, std::size_t maxBytes, Refusal why, const char* what) { + ShaderResources::MoveProof refusal; + const auto listed = prove(program, maxBytes, refusal); + Require(!listed.has_value() && refusal.refusal == why, what); + }; + + ShaderResources::MoveProof proof; + auto moved = prove(built, 0, proof); + Require(moved.has_value() && moved->empty() && proof.refusal == Refusal::None && proof.buffers == 0, "a dispatch with every buffer where it was built moved something"); + + const auto relocated = moveTo(1, guestMovedSecond.data(), 24); + const CompiledShader relocatedShader{ShaderRecompiler::ShaderStage::Compute, &relocated, 0}; + Require(ShaderResources::ContentKey(shader, false, true) == ShaderResources::ContentKey(relocatedShader, false, true), "moving a read-only buffer changed the movable content key"); + Require(ShaderResources::ContentKey(shader, false, false) != ShaderResources::ContentKey(relocatedShader, false, false), "the fixed content key ignored a moved buffer"); + + moved = prove(relocated, 0, proof); + Require(moved.has_value() && moved->size() == 1 && proof.refusal == Refusal::None && proof.buffers == 1 && proof.bytes >= 24, "a moved read-only buffer was not listed"); + Require(moved->front().address == reinterpret_cast(guestMovedSecond.data()) && moved->front().size == 24 && moved->front().words.empty(), "the listed buffer is not the moved range"); + + moved = prove(moveTo(0, guestFirst.data(), 8), 0, proof); + Require(moved.has_value() && moved->size() == 1 && moved->front().address == reinterpret_cast(guestFirst.data()) && moved->front().size == 8, "a buffer that only changed its size was not listed"); + + auto both = relocated; + const auto firstWords = vsharp(guestMovedFirst.data(), 12); + std::copy(firstWords.begin(), firstWords.end(), both.bindings[0].guestDescriptor.begin()); + moved = prove(both, 1024, proof); + Require(moved.has_value() && moved->size() == 2 && proof.buffers == 2 && proof.bytes >= 36, "two moved buffers were not both listed"); + refuses(both, 30, Refusal::Limit, "moved buffers past the byte budget together were served"); + + auto written = relocated; + written.bindings[0].bufferWritten[1] = true; + refuses(written, 0, Refusal::Written, "a moved buffer the dispatch writes was served"); + refuses(moveTo(1, guestMovedSecond.data(), 20000), 0, Refusal::Limit, "a moved buffer over the storage buffer range was served"); + refuses(relocated, 16, Refusal::Limit, "a moved buffer over the byte budget was served"); + refuses(moveTo(1, reinterpret_cast(std::uintptr_t{0x10}), 24), 0, Refusal::Inaccessible, "a moved buffer over unmapped memory was served"); + refuses(moveTo(1, guestThird.data(), 8), 0, Refusal::Overlap, "a moved buffer over the dispatch's own written range was served"); + auto shaped = relocated; + shaped.bindings[0].guestDescriptor.resize(4); + refuses(shaped, 0, Refusal::Shape, "a dispatch with another binding shape was served"); + recorder.Sync(); + } + Require(mock.live == 0, "dispatch move resources leaked Vulkan objects"); +} + void resourceTests() { mock = MockVulkan{}; const auto context = mockContext(); @@ -7987,6 +8066,7 @@ int main(int argc, char** argv) { resourceTests(); oversizedDescriptorTests(); descriptorSnapshotTests(); + dispatchMoveTests(); descriptorCacheTests(); bufferBudgetPlacementTests(); imageTableAllocationTests(); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DepthFastClearHarness.hpp b/core/libs/prx/libSceAgcDriver/tests/execution/DepthFastClearHarness.hpp index 3e21bfbceb..f87d9ce6b3 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DepthFastClearHarness.hpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DepthFastClearHarness.hpp @@ -48,6 +48,8 @@ struct DrawOptions { VkCompareOp depthCompare = VK_COMPARE_OP_LESS; bool stencilTest = false; VkStencilOpState stencil{}; + float minDepth = 0.0f; + float maxDepth = 1.0f; }; inline std::array BufferDescriptor(const void* data, std::uint32_t stride, std::uint32_t count) { @@ -126,7 +128,7 @@ inline std::uint32_t Draw(AgcDriver::VulkanDevice& device, const Shaders& compil state.stencilBack = options.stencil; state.renderExtent = {Width, Height}; state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; - state.viewport = {0, static_cast(Height), static_cast(Width), -static_cast(Height), 0, 1}; + state.viewport = {0, static_cast(Height), static_cast(Width), -static_cast(Height), options.minDepth, options.maxDepth}; state.negativeOneToOne = false; state.scissor = {{0, 0}, {Width, Height}}; state.cullMode = VK_CULL_MODE_NONE; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DispatchMove.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DispatchMove.cpp new file mode 100644 index 0000000000..efcab638b1 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DispatchMove.cpp @@ -0,0 +1,263 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 32; +constexpr std::uint32_t Words = 64; +constexpr std::uint32_t Bytes = Words * 4u; +constexpr std::uint32_t Constant = 0x00010000u; +constexpr std::uint32_t Sentinel = 0xdeadbeefu; +constexpr std::size_t Piece = 65536; +constexpr std::size_t BlockBytes = 5 * Piece; + +alignas(256) std::array Table{}; + +constexpr std::uint32_t Vop2(std::uint32_t opcode, std::uint32_t destination, std::uint32_t source0, std::uint32_t source1) { + return (opcode << 25u) | (destination << 17u) | (source1 << 9u) | source0; +} + +constexpr std::uint32_t BufferOperands = (0x80u << 24u) | (1u << 16u) | (2u << 8u) | 1u; + +alignas(256) constexpr std::array Code{ + Vop2(0x1au, 1u, 0x82u, 0u), + 0xf4080100u, 0xfa000000u, + 0xbf8cc07fu, + 0xe0301000u, BufferOperands, + 0xbf8c3f70u, + 0xf4080100u, 0xfa000010u, + 0xbf8cc07fu, + Vop2(0x25u, 2u, 0xffu, 2u), Constant, + 0xe0701000u, BufferOperands, + 0xbf810000u, +}; + +class GuestBlock { +public: + GuestBlock() { +#ifdef _WIN32 + block = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(BlockBytes, Piece)); + if (block != nullptr) GuestArena::GuestArenaCommit_nid_postfix(block, BlockBytes, PAGE_READWRITE, BlockBytes); +#else + constexpr std::uintptr_t alignment = Piece; + void* mapped = mmap(nullptr, BlockBytes + alignment, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (mapped != MAP_FAILED) { + const auto begin = reinterpret_cast(mapped); + const auto aligned = (begin + alignment - 1) & ~(alignment - 1); + if (aligned != begin) munmap(mapped, aligned - begin); + if (aligned + BlockBytes != begin + BlockBytes + alignment) munmap(reinterpret_cast(aligned + BlockBytes), begin + alignment - aligned); + block = reinterpret_cast(aligned); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(block, BlockBytes); + } +#endif + Require(block != nullptr, "dispatch move: cannot allocate the guest block"); + GuestAllocations::Mutation().Add(block, BlockBytes, true, true, true); + } + + ~GuestBlock() { + GuestAllocations::Mutation().Remove(block); +#ifdef _WIN32 + GuestArena::GuestArenaReset_nid_postfix(block, BlockBytes); + GuestArena::GuestArenaRelease_nid_postfix(block, BlockBytes); +#else + munmap(block, BlockBytes); + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(block, BlockBytes); +#endif + } + + GuestBlock(const GuestBlock&) = delete; + GuestBlock& operator=(const GuestBlock&) = delete; + + std::uint32_t* InputA() const { return reinterpret_cast(block); } + std::uint32_t* InputB() const { return reinterpret_cast(block + Piece + 12); } + std::uint32_t* InputC() const { return reinterpret_cast(block + 2 * Piece + 32); } + std::uint32_t* OutputO() const { return reinterpret_cast(block + 3 * Piece); } + std::uint32_t* OutputP() const { return reinterpret_cast(block + 4 * Piece); } + +private: + std::uint8_t* block = nullptr; +}; + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + +void Fill(std::uint32_t* where, std::uint32_t first) { + for (std::uint32_t index = 0; index < Words; ++index) where[index] = first + index; +} + +void Reset(std::uint32_t* output) { + std::fill(output, output + Words, Sentinel); +} + +ShaderRecompiler::RecompileResult Compile(AgcDriver::VulkanDevice& device, const std::uint32_t* input, std::uint32_t inputBytes, const std::uint32_t* output) { + const auto in = BufferDescriptor(input, inputBytes); + const auto out = BufferDescriptor(output, Bytes); + std::copy(in.begin(), in.end(), Table.begin()); + std::copy(out.begin(), out.end(), Table.begin() + 4); + const auto table = reinterpret_cast(Table.data()); + const std::vector userData{static_cast(table), static_cast(table >> 32u)}; + const std::span code(Code); + const std::span tableWords(Table); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}, {table, std::as_bytes(tableWords)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + return ShaderRecompiler::Recompile(request); +} + +void Run(AgcDriver::VulkanDevice& device, const ShaderRecompiler::RecompileResult& compiled, const std::uint32_t* input, std::uint32_t inputBytes, const std::uint32_t* output, bool wait) { + auto result = compiled; + std::size_t elements = 0; + for (auto& binding : result.bindings) { + if (binding.role != ShaderRecompiler::DescriptorRole::GuestBuffers) continue; + Require(binding.guestDescriptor.size() == static_cast(binding.count) * 4u && binding.bufferWritten.size() == binding.count, "dispatch move: the program's buffer bindings have another shape"); + for (std::uint32_t element = 0; element < binding.count; ++element, ++elements) { + const auto words = binding.bufferWritten[element] ? BufferDescriptor(output, Bytes) : BufferDescriptor(input, inputBytes); + std::copy(words.begin(), words.end(), binding.guestDescriptor.begin() + static_cast(element) * 4); + } + } + Require(elements == 2, "dispatch move: the program does not bind an input and an output"); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(Code.data())); + if (wait) device.WaitIdle(); +} + +void Expect(const std::uint32_t* input, const std::uint32_t* output, std::uint32_t added, const std::string& what) { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto expected = input[tid] + added; + Require(output[tid] == expected, "dispatch move: " + what + ": lane " + std::to_string(tid) + " is " + Hex(output[tid]) + ", expected " + Hex(expected)); + } +} + +struct Step { + std::uint64_t checked, exact, moved, refused; +}; + +Step Since(const AgcDriver::VulkanDevice::DispatchMoveCount& before) { + const auto now = AgcDriver::VulkanDevice::DispatchMoveCounts(); + return {now.checked - before.checked, now.exact - before.exact, now.moved - before.moved, now.refused - before.refused}; +} + +void Print(const char* what, const Step& step) { + std::printf("dispatch move: %s: checked %llu, exact %llu, moved %llu, refused %llu\n", what, static_cast(step.checked), static_cast(step.exact), static_cast(step.moved), static_cast(step.refused)); +} + +bool RunTests(AgcDriver::VulkanDevice& device, const GuestBlock& guest) { + Fill(guest.InputA(), 0xa000u); + Fill(guest.InputB(), 0xb000u); + Fill(guest.InputC(), 0xc000u); + + const auto compiled = Compile(device, guest.InputA(), Bytes, guest.OutputO()); + Reset(guest.OutputO()); + Run(device, compiled, guest.InputA(), Bytes, guest.OutputO(), true); + Expect(guest.InputA(), guest.OutputO(), Constant, "the first dispatch"); + const auto start = AgcDriver::VulkanDevice::DispatchMoveCounts(); + + Reset(guest.OutputO()); + Run(device, compiled, guest.InputB(), Bytes, guest.OutputO(), true); + Expect(guest.InputB(), guest.OutputO(), Constant, "an input moved to another block"); + Reset(guest.OutputO()); + Run(device, compiled, guest.InputA(), Bytes, guest.OutputO(), true); + Expect(guest.InputA(), guest.OutputO(), Constant, "the input back where the template was built"); + Reset(guest.OutputO()); + Run(device, compiled, guest.InputC(), Bytes, guest.OutputO(), true); + Expect(guest.InputC(), guest.OutputO(), Constant, "an input moved to a third block"); + const auto repeats = Since(start); + Print("an exact repeat between two moved ones", repeats); + const bool templates = repeats.checked != 0; + if (templates) { + Require(repeats.exact == 1 && repeats.moved == 2 && repeats.refused == 0, "dispatch move: the repeats were not served as one exact hit and two moved ones"); + } else { + std::puts("dispatch move: the device kept no template (no host imports); the moved-buffer path is not exercised"); + } + + Reset(guest.OutputO()); + Run(device, compiled, guest.InputB(), 192, guest.OutputO(), true); + Expect(guest.InputB(), guest.OutputO(), Constant, "an input moved and shortened"); + + std::array changed{}; + for (std::uint32_t index = 0; index < Words; ++index) changed[index] = 0xd000u + index * 3u; + AgcDriver::GuestMemory::Write(reinterpret_cast(guest.InputB()), std::as_bytes(std::span(changed))); + Reset(guest.OutputO()); + Run(device, compiled, guest.InputB(), Bytes, guest.OutputO(), true); + Expect(changed.data(), guest.OutputO(), Constant, "an input moved to a range the driver stored over"); + + const auto refusals = AgcDriver::VulkanDevice::DispatchMoveCounts(); + for (std::uint32_t index = 0; index < Words; ++index) guest.OutputO()[index] = 0x7000u + index; + Run(device, compiled, guest.OutputO() + Threads, Bytes / 2, guest.OutputO(), true); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto expected = 0x7000u + Threads + tid + Constant; + Require(guest.OutputO()[tid] == expected, "dispatch move: an input moved into the unwritten half of the dispatch's own output: lane " + std::to_string(tid) + " is " + Hex(guest.OutputO()[tid]) + ", expected " + Hex(expected)); + } + + Reset(guest.OutputO()); + Reset(guest.OutputP()); + Run(device, compiled, guest.InputA(), Bytes, guest.OutputP(), false); + Run(device, compiled, guest.OutputP(), Bytes, guest.OutputO(), true); + Expect(guest.InputA(), guest.OutputO(), 2u * Constant, "an input moved onto a range a recorded dispatch still writes"); + const auto refused = Since(refusals); + Print("two inputs moved onto written ranges", refused); + if (templates) Require(refused.refused == 2, "dispatch move: the inputs moved onto written ranges were not both refused"); + + Reset(guest.OutputO()); + Run(device, compiled, guest.InputA(), Bytes, guest.OutputO(), true); + Expect(guest.InputA(), guest.OutputO(), Constant, "the input moved back after the refusals"); + const auto total = AgcDriver::VulkanDevice::DispatchMoveCounts(); + std::printf("dispatch move: in total: checked %llu, exact %llu, moved %llu, refused %llu, dropped by stage A %llu\n", static_cast(total.checked), static_cast(total.exact), static_cast(total.moved), static_cast(total.refused), static_cast(total.filtered)); + return templates; +} + +} + +int main() { + try { + GuestBlock guest; + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + if (!RunTests(*device, guest)) return VulkanTestSkipped; + std::puts("dispatch move tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DispatchMoveDepth.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DispatchMoveDepth.cpp new file mode 100644 index 0000000000..1d44a9207d --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DispatchMoveDepth.cpp @@ -0,0 +1,460 @@ +#include "DepthFastClearHarness.hpp" +#include "VulkanTestDevice.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" +#include "Recompiler.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + + +#ifndef _WIN32 +namespace AgcDriver { + +struct VulkanDeviceTestAccess { + static Graphics::Context ContextOf(const VulkanDevice& device) { return device.graphicsContext(); } +}; + +} +#endif + +namespace { + +using AgcDriver::Graphics::Require; +using DepthFastClearHarness::Covered; +using DepthFastClearHarness::Draw; +using DepthFastClearHarness::Height; +using DepthFastClearHarness::Shaders; +using DepthFastClearHarness::Surface; +using DepthFastClearHarness::Width; + +constexpr std::uint32_t Threads = 32; +constexpr std::uint32_t Bytes = Threads * 4u; +constexpr std::uint32_t Format32Float = 22; +constexpr std::uint32_t Type2D = 9; +constexpr std::uint32_t Sentinel = 0xdeadbeefu; +constexpr std::size_t Piece = 65536; + +alignas(256) constexpr std::array RowCode{ + 0x7e3c0300u, + 0x7e3e0280u, + 0x34060082u, + 0xe0301000u, 0x80040b03u, + 0xbf8c3f70u, + 0x4a181681u, + 0xe0701000u, 0x80000c03u, + 0xf0201108u, 0x00010b1eu, + 0xbf810000u, 0xbf810000u, +}; + +alignas(256) constexpr std::array ProbeCode{ + 0x7e3c0300u, + 0x7e3e020cu, + 0xf0001108u, 0x00010a1eu, + 0x34060082u, + 0xbf8c3f70u, + 0xe0701000u, 0x80000a03u, + 0xbf810000u, 0xbf810000u, +}; + +class GuestBlock { +public: + GuestBlock(std::size_t pieces, bool inWatch) : bytes(pieces * Piece), watched(inWatch) { + if (inWatch) { +#ifdef _WIN32 + block = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(bytes, Piece)); + if (block != nullptr) GuestArena::GuestArenaCommit_nid_postfix(block, bytes, PAGE_READWRITE, bytes); +#else + constexpr std::uintptr_t alignment = Piece; + void* mapped = mmap(nullptr, bytes + alignment, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (mapped != MAP_FAILED) { + const auto begin = reinterpret_cast(mapped); + const auto aligned = (begin + alignment - 1) & ~(alignment - 1); + if (aligned != begin) munmap(mapped, aligned - begin); + if (aligned + bytes != begin + bytes + alignment) munmap(reinterpret_cast(aligned + bytes), begin + alignment - aligned); + block = reinterpret_cast(aligned); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(block, bytes); + } +#endif + } else { +#ifdef _WIN32 + block = static_cast(VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); +#else + block = static_cast(std::aligned_alloc(Piece, bytes)); +#endif + if (block != nullptr) std::memset(block, 0, bytes); + } + Require(block != nullptr, "dispatch move depth: cannot allocate the guest block"); + GuestAllocations::Mutation().Add(block, bytes, true, true, true); + } + + ~GuestBlock() { + GuestAllocations::Mutation().Remove(block); + if (watched) { +#ifdef _WIN32 + GuestArena::GuestArenaReset_nid_postfix(block, bytes); + GuestArena::GuestArenaRelease_nid_postfix(block, bytes); +#else + munmap(block, bytes); + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(block, bytes); +#endif + } else { +#ifdef _WIN32 + VirtualFree(block, 0, MEM_RELEASE); +#else + std::free(block); +#endif + } + } + + GuestBlock(const GuestBlock&) = delete; + GuestBlock& operator=(const GuestBlock&) = delete; + + std::uint8_t* PieceAt(std::size_t index) const { return block + index * Piece; } + +private: + std::size_t bytes = 0; + bool watched = false; + std::uint8_t* block = nullptr; +}; + +struct Buffers { + explicit Buffers(const GuestBlock& block) : block(block) {} + std::uint32_t* InputA() const { return reinterpret_cast(block.PieceAt(4)); } + std::uint32_t* InputB() const { return reinterpret_cast(block.PieceAt(5) + 12); } + std::uint32_t* InputC() const { return reinterpret_cast(block.PieceAt(6) + 32); } + std::uint32_t* Output() const { return reinterpret_cast(block.PieceAt(8)); } + +private: + const GuestBlock& block; +}; + +struct Env { + AgcDriver::VulkanDevice& device; + const Shaders& shaders; + const Buffers& buffers; +}; + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +std::array TextureDescriptor(const void* data) { + const auto address = static_cast(reinterpret_cast(data)); + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (Format32Float << 20u) | (((Width - 1u) & 3u) << 30u), + ((Width - 1u) >> 2u) | ((Height - 1u) << 14u), + 0xfacu | (Type2D << 28u), + 0u, 0u, 0u, 0u, + }; +} + +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + +ShaderRecompiler::RecompileResult Compile(AgcDriver::VulkanDevice& device, std::span code, std::span userData) { + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderRecompiler::ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + return ShaderRecompiler::Recompile(request); +} + +ShaderRecompiler::RecompileResult RowProgram(AgcDriver::VulkanDevice& device, const void* plane, const std::uint32_t* input, const std::uint32_t* output) { + std::vector userData(20, 0u); + const auto out = BufferDescriptor(output, Bytes); + const auto texture = TextureDescriptor(plane); + const auto in = BufferDescriptor(input, Bytes); + std::copy(out.begin(), out.end(), userData.begin()); + std::copy(texture.begin(), texture.end(), userData.begin() + 4); + std::copy(in.begin(), in.end(), userData.begin() + 16); + return Compile(device, RowCode, userData); +} + +void RunRow(AgcDriver::VulkanDevice& device, const ShaderRecompiler::RecompileResult& compiled, const std::uint32_t* input, const std::uint32_t* output) { + auto result = compiled; + std::size_t elements = 0; + for (auto& binding : result.bindings) { + if (binding.role != ShaderRecompiler::DescriptorRole::GuestBuffers) continue; + Require(binding.guestDescriptor.size() == static_cast(binding.count) * 4u, "dispatch move depth: the program's buffer bindings have another shape"); + for (std::uint32_t element = 0; element < binding.count; ++element, ++elements) { + auto* current = binding.guestDescriptor.data() + static_cast(element) * 4u; + const auto built = static_cast(current[0]) | (static_cast(current[1] & 0xffffu) << 32u); + const auto words = built == static_cast(reinterpret_cast(output)) ? BufferDescriptor(output, Bytes) : BufferDescriptor(input, Bytes); + std::copy(words.begin(), words.end(), current); + } + } + Require(elements == 2, "dispatch move depth: the program does not bind an input and an output"); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(RowCode.data())); + device.WaitIdle(); +} + +std::array ReadRow(const Env& env, const void* plane, std::uint32_t y) { + std::vector userData(16, 0u); + auto* output = env.buffers.Output(); + const auto out = BufferDescriptor(output, Bytes); + const auto texture = TextureDescriptor(plane); + std::copy(out.begin(), out.end(), userData.begin()); + std::copy(texture.begin(), texture.end(), userData.begin() + 4); + userData[12] = y; + const auto program = Compile(env.device, ProbeCode, userData); + std::fill(output, output + Threads, Sentinel); + env.device.Dispatch(program, 1, 1, 1, {}, reinterpret_cast(ProbeCode.data())); + env.device.WaitIdle(); + std::array lanes{}; + std::copy(output, output + Threads, lanes.begin()); + return lanes; +} + +void FillInput(std::uint32_t* where, float first) { + for (std::uint32_t lane = 0; lane < Threads; ++lane) where[lane] = std::bit_cast(first + static_cast(lane) / 1024.0f); +} + +void FillPlane(std::uint8_t* plane, float value) { + auto* words = reinterpret_cast(plane); + std::fill(words, words + Piece / sizeof(std::uint32_t), std::bit_cast(value)); +} + +void DrawDepth(const Env& env, const void* plane, float depth) { + const Surface surface{reinterpret_cast(plane), 0, 0, false, VK_FORMAT_D32_SFLOAT}; + const float low = depth <= 0.5f ? 0.0f : 2.0f * depth - 1.0f; + const float high = depth <= 0.5f ? 2.0f * depth : 1.0f; + Require(Draw(env.device, env.shaders, surface, {.depthWrite = true, .depthCompare = VK_COMPARE_OP_ALWAYS, .minDepth = low, .maxDepth = high}) == Covered, "dispatch move depth: the depth producer did not cover the surface"); +} + +struct PlaneModel { + std::array row0{}; + std::uint32_t rest = 0; + + void Set(float value) { + rest = std::bit_cast(value); + row0.fill(rest); + } + + std::array Dispatch(const std::uint32_t* input) { + std::array sums{}; + for (std::uint32_t lane = 0; lane < Threads; ++lane) { + sums[lane] = input[lane] + 1u; + row0[lane] = input[lane]; + } + return sums; + } +}; + +void Totals(const std::string& what); + +void Step(const Env& env, const ShaderRecompiler::RecompileResult& program, PlaneModel& model, const std::uint32_t* input, const std::string& what) { + const auto expected = model.Dispatch(input); + auto* output = env.buffers.Output(); + std::fill(output, output + Threads, Sentinel); + RunRow(env.device, program, input, output); + for (std::uint32_t lane = 0; lane < Threads; ++lane) { + Require(output[lane] == expected[lane], "dispatch move depth: " + what + ": lane " + std::to_string(lane) + " is " + Hex(output[lane]) + ", expected " + Hex(expected[lane])); + } + Totals(what); +} + +void Verify(const Env& env, const void* plane, const PlaneModel& model, const std::string& what) { + for (const std::uint32_t y : {0u, 1u, Height - 1u}) { + const auto lanes = ReadRow(env, plane, y); + for (std::uint32_t lane = 0; lane < Threads; ++lane) { + const auto expected = y == 0 ? model.row0[lane] : model.rest; + Require(lanes[lane] == expected, "dispatch move depth: " + what + ": row " + std::to_string(y) + ", texel " + std::to_string(lane) + " of the plane is " + Hex(lanes[lane]) + ", expected " + Hex(expected)); + } + } +} + +struct MoveSteps { + std::uint64_t checked, exact, moved, refused; +}; + +MoveSteps Since(const AgcDriver::VulkanDevice::DispatchMoveCount& before) { + const auto now = AgcDriver::VulkanDevice::DispatchMoveCounts(); + return {now.checked - before.checked, now.exact - before.exact, now.moved - before.moved, now.refused - before.refused}; +} + +void Totals(const std::string& what) { + const auto now = AgcDriver::VulkanDevice::DispatchMoveCounts(); + std::printf("dispatch move depth: %s: in total checked %llu, exact %llu, moved %llu, refused %llu, dropped by stage A %llu\n", what.c_str(), static_cast(now.checked), static_cast(now.exact), static_cast(now.moved), static_cast(now.refused), static_cast(now.filtered)); +} + +void DescribeBindings(const ShaderRecompiler::RecompileResult& program) { + for (const auto& binding : program.bindings) { + std::string written; + for (const bool bit : binding.bufferWritten) written += bit ? '1' : '0'; + std::printf("dispatch move depth: binding role %d kind %d count %u, %zu descriptor words, buffers written [%s], images written %zu\n", static_cast(binding.role), static_cast(binding.kind), binding.count, binding.guestDescriptor.size(), written.c_str(), binding.imageWritten.size()); + } +} + +void Print(const char* what, const MoveSteps& step) { + std::printf("dispatch move depth: %s: checked %llu, exact %llu, moved %llu, refused %llu\n", what, static_cast(step.checked), static_cast(step.exact), static_cast(step.moved), static_cast(step.refused)); +} + +void Warm(const Env& env, std::uint8_t* plane) { + FillPlane(plane, 0.5f); + PlaneModel model; + model.Set(0.5f); + const auto program = RowProgram(env.device, plane, env.buffers.InputA(), env.buffers.Output()); + Step(env, program, model, env.buffers.InputA(), "warm-up: the first input"); + Step(env, program, model, env.buffers.InputB(), "warm-up: the second input"); + Step(env, program, model, env.buffers.InputC(), "warm-up: the third input"); +} + +bool Control(const Env& env, std::uint8_t* plane) { + FillPlane(plane, 0.5f); + PlaneModel model; + model.Set(0.5f); + const auto refusals = AgcDriver::Graphics::ShaderResources::DepthGuardRefusals(); + const auto program = RowProgram(env.device, plane, env.buffers.InputA(), env.buffers.Output()); + DescribeBindings(program); + Step(env, program, model, env.buffers.InputA(), "control: the first dispatch"); + Totals("control, after the first dispatch"); + const auto start = AgcDriver::VulkanDevice::DispatchMoveCounts(); + Step(env, program, model, env.buffers.InputB(), "control: an input moved"); + Totals("control, after an input moved"); + Step(env, program, model, env.buffers.InputC(), "control: the input moved again"); + Totals("control, after the input moved again"); + Step(env, program, model, env.buffers.InputC(), "control: an exact repeat"); + Totals("control, after an exact repeat"); + const auto steps = Since(start); + Print("control, a plane without a depth surface", steps); + Verify(env, plane, model, "control"); + Require(AgcDriver::Graphics::ShaderResources::DepthGuardRefusals() == refusals, "dispatch move depth: the depth guard refused a proof with no depth surface in sight"); + Require(steps.refused == 0, "dispatch move depth: the control's repeats were refused"); + if (steps.checked == 0) return false; + Require(steps.checked == 3 && steps.moved == 3, "dispatch move depth: the control's repeats were not all served by the template of the first dispatch"); + return true; +} + +void Mirror(const Env& env, std::uint8_t* plane, const char* where) { + const std::string at = std::string(where) + ", the mirror: "; + FillPlane(plane, 0.5f); + PlaneModel model; + DrawDepth(env, plane, 0.25f); + model.Set(0.25f); + const auto program = RowProgram(env.device, plane, env.buffers.InputA(), env.buffers.Output()); + Step(env, program, model, env.buffers.InputA(), at + "the first dispatch"); + Verify(env, plane, model, at + "after the first dispatch"); + DrawDepth(env, plane, 0.75f); + model.Set(0.75f); + const auto start = AgcDriver::VulkanDevice::DispatchMoveCounts(); + Step(env, program, model, env.buffers.InputB(), at + "an input moved after the depth was drawn again"); + Verify(env, plane, model, at + "after the moved dispatch"); + Step(env, program, model, env.buffers.InputB(), at + "an exact repeat"); + DrawDepth(env, plane, 0.25f); + model.Set(0.25f); + Step(env, program, model, env.buffers.InputC(), at + "the input moved again after another redraw"); + Verify(env, plane, model, at + "after the last dispatch"); + Print((at + "repeats and redraws").c_str(), Since(start)); +} + +void Appears(const Env& env, std::uint8_t* plane, const char* where, bool moved, bool templates) { + const std::string at = std::string(where) + (moved ? ", a surface appears, input moved: " : ", a surface appears, exact repeat: "); + FillPlane(plane, 0.125f); + PlaneModel model; + model.Set(0.125f); + const auto program = RowProgram(env.device, plane, env.buffers.InputA(), env.buffers.Output()); + Step(env, program, model, env.buffers.InputA(), at + "the first dispatch, no depth surface yet"); + DrawDepth(env, plane, 0.25f); + model.Set(0.25f); + const auto start = AgcDriver::VulkanDevice::DispatchMoveCounts(); + const auto refusals = AgcDriver::Graphics::ShaderResources::DepthGuardRefusals(); + Step(env, program, model, moved ? env.buffers.InputB() : env.buffers.InputA(), at + "the dispatch after the surface appeared"); + std::printf("dispatch move depth: %sthe depth guard refused %llu proofs\n", at.c_str(), static_cast(AgcDriver::Graphics::ShaderResources::DepthGuardRefusals() - refusals)); + if (templates) Require(AgcDriver::Graphics::ShaderResources::DepthGuardRefusals() > refusals, "dispatch move depth: " + at + "the template over the plain storage image was proved current although a depth surface covers its memory"); + Print((at + "the dispatch after the surface appeared").c_str(), Since(start)); + Totals(at + "after that dispatch"); + Verify(env, plane, model, at + "after that dispatch"); + DrawDepth(env, plane, 0.75f); + model.Set(0.75f); + Step(env, program, model, env.buffers.InputC(), at + "the input moved after the next redraw"); + Verify(env, plane, model, at + "after the last dispatch"); +} + +bool RunTests(const Env& env, const GuestBlock& watched, const GuestBlock& unwatched, std::size_t pass) { + const auto inWatch = pass * 10; + const auto outside = pass * 3; + FillInput(env.buffers.InputA(), 0.0625f); + FillInput(env.buffers.InputB(), 0.109375f); + FillInput(env.buffers.InputC(), 0.15625f); + Warm(env, watched.PieceAt(9)); + const bool templates = Control(env, watched.PieceAt(inWatch)); + if (!templates) std::puts("dispatch move depth: the device kept no template (no host imports); the moved-buffer path is not exercised"); + Mirror(env, watched.PieceAt(inWatch + 1), "inside the write watch"); + Appears(env, watched.PieceAt(inWatch + 2), "inside the write watch", true, templates); + Appears(env, watched.PieceAt(inWatch + 3), "inside the write watch", false, templates); + Mirror(env, unwatched.PieceAt(outside), "outside the write watch"); + Appears(env, unwatched.PieceAt(outside + 1), "outside the write watch", true, templates); + Appears(env, unwatched.PieceAt(outside + 2), "outside the write watch", false, templates); + return templates; +} + +} + +int main() { + try { + const GuestBlock watched(20, true); + const GuestBlock unwatched(6, false); + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + const auto shaders = DepthFastClearHarness::Compile(*device); + const Buffers buffers(watched); + const Env env{*device, shaders, buffers}; + bool templates = true; +#ifndef _WIN32 + const auto context = AgcDriver::VulkanDeviceTestAccess::ContextOf(*device); + const auto decided = AgcDriver::Graphics::PrepareImportWatch(context); + struct Restore { + const AgcDriver::Graphics::Context& context; + AgcDriver::Graphics::ImportWatch decided; + ~Restore() { AgcDriver::Graphics::SetImportWatch(context, decided); } + } restore{context, decided}; + std::size_t pass = 0; + for (const auto mode : {AgcDriver::Graphics::ImportWatch::Watch, AgcDriver::Graphics::ImportWatch::Unwatch}) { + AgcDriver::Graphics::SetImportWatch(context, mode); + std::printf("dispatch move depth: pass %zu, imports %s\n", pass, mode == AgcDriver::Graphics::ImportWatch::Watch ? "under the write watch" : "left unwatched"); + templates = RunTests(env, watched, unwatched, pass++) && templates; + } +#else + templates = RunTests(env, watched, unwatched, 0); +#endif + if (!templates) return VulkanTestSkipped; + std::puts("dispatch move depth tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} From fd5eb8458b8283e91169d5dfdae4fcaadeeb2c70 Mon Sep 17 00:00:00 2001 From: jagrat Singh Date: Wed, 14 Oct 2026 22:59:47 +0530 Subject: [PATCH 0666/1000] feat(libSceSsl): implement certificate validity period and fingerprint queries --- core/libs/prx/libSceSsl/Export.cpp | 64 ++++++++++++++++++++++++++-- core/libs/prx/libSceSsl/SslTypes.hpp | 5 +++ core/libs/tests/GuestSslLoad.cpp | 50 ++++++++++++++++++++++ docs/dev/debt/libSceSsl.md | 5 +-- 4 files changed, 118 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libSceSsl/Export.cpp b/core/libs/prx/libSceSsl/Export.cpp index 0f9622a8ac..a478209f58 100644 --- a/core/libs/prx/libSceSsl/Export.cpp +++ b/core/libs/prx/libSceSsl/Export.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -28,6 +29,26 @@ constexpr int ErrorNotFound = static_cast(0x8095f004); constexpr int ErrorInvalidArg = static_cast(0x8095177a); constexpr int ErrorInvalidId = static_cast(0x8095f006); +constexpr std::int64_t TicksPerSecond = 1000000; +constexpr std::int64_t TicksPerMinute = 60 * TicksPerSecond; +constexpr std::int64_t TicksPerHour = 60 * TicksPerMinute; +constexpr std::int64_t TicksPerDay = 24 * TicksPerHour; +constexpr std::int64_t UnixEpochDays = 719162; + +std::int64_t DaysFromCivil(std::int64_t year, unsigned month, unsigned day) { + year -= month <= 2; + const std::int64_t era = (year >= 0 ? year : year - 399) / 400; + const auto yearOfEra = static_cast(year - era * 400); + const unsigned dayOfYear = (153 * (month > 2 ? month - 3 : month + 9) + 2) / 5 + day - 1; + const unsigned dayOfEra = yearOfEra * 365 + yearOfEra / 4 - yearOfEra / 100 + dayOfYear; + return era * 146097 + static_cast(dayOfEra) - 719468 + UnixEpochDays; +} + +std::uint64_t TimeToTick(const mbedtls_x509_time& time) { + return static_cast(DaysFromCivil(time.year, time.mon, time.day)) * TicksPerDay + + time.hour * TicksPerHour + time.min * TicksPerMinute + time.sec * TicksPerSecond; +} + struct Pool { std::size_t size, current = 0, peak = 0; bool exhausted = false; @@ -518,9 +539,10 @@ int APS5_VABI sceSslWrite(int sslConnectionId, void* buf, size_t len, int flags) return 0; } -int APS5_VABI sceSslClose() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceSslClose(int sslConnectionId) { + (void)sslConnectionId; + NotImplemented_nid_no_patch(__func__); + return 0; } int APS5_VABI sceSslGetSerialNumber(int sslCtxId, void* sslCert, unsigned char* sboData, std::size_t* sboLen) { @@ -625,6 +647,42 @@ int APS5_VABI sceSslGetNameEntryInfo(int sslCtxId, void* certName, int entryNum, return 0; } +int APS5_VABI sceSslGetNotBefore(int sslCtxId, void* sslCert, SceSsl::RtcTick* begin) { + if (!begin) return ErrorInvalidArg; + std::lock_guard lock(State().mutex); + const auto* crt = RequireCertificate(__func__, sslCtxId, sslCert); + begin->tick = TimeToTick(crt->valid_from); + return 0; +} + +int APS5_VABI sceSslGetNotAfter(int sslCtxId, void* sslCert, SceSsl::RtcTick* limit) { + if (!limit) return ErrorInvalidArg; + std::lock_guard lock(State().mutex); + const auto* crt = RequireCertificate(__func__, sslCtxId, sslCert); + limit->tick = TimeToTick(crt->valid_to); + return 0; +} + +int APS5_VABI sceSslGetFingerprint(int sslCtxId, void* sslCert, int hashType, unsigned char* fingerprint) { + if (!fingerprint) return ErrorInvalidArg; + mbedtls_md_type_t mdType; + switch (hashType) { + case 0: mdType = MBEDTLS_MD_MD5; break; + case 1: mdType = MBEDTLS_MD_SHA1; break; + case 2: mdType = MBEDTLS_MD_SHA224; break; + case 3: mdType = MBEDTLS_MD_SHA256; break; + case 4: mdType = MBEDTLS_MD_SHA384; break; + case 5: mdType = MBEDTLS_MD_SHA512; break; + default: return ErrorInvalidArg; + } + const auto* info = mbedtls_md_info_from_type(mdType); + if (!info) return ErrorInvalidArg; + std::lock_guard lock(State().mutex); + const auto* crt = RequireCertificate(__func__, sslCtxId, sslCert); + if (mbedtls_md(info, crt->raw.p, crt->raw.len, fingerprint) != 0) return ErrorInvalidArg; + return 0; +} + int APS5_VABI sceSslGetPem(void) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/prx/libSceSsl/SslTypes.hpp b/core/libs/prx/libSceSsl/SslTypes.hpp index 18fc70f740..f82ac92e0e 100644 --- a/core/libs/prx/libSceSsl/SslTypes.hpp +++ b/core/libs/prx/libSceSsl/SslTypes.hpp @@ -26,9 +26,14 @@ struct MemoryPoolStats { std::uint32_t reserved; }; +struct RtcTick { + std::uint64_t tick; +}; + static_assert(sizeof(Data) == 0x10); static_assert(sizeof(CaCerts) == 0x18); static_assert(sizeof(CaList) == 0x10); static_assert(sizeof(MemoryPoolStats) == 0x20); +static_assert(sizeof(RtcTick) == 0x8); } diff --git a/core/libs/tests/GuestSslLoad.cpp b/core/libs/tests/GuestSslLoad.cpp index 8c26ab31c7..4cb3a27c3e 100644 --- a/core/libs/tests/GuestSslLoad.cpp +++ b/core/libs/tests/GuestSslLoad.cpp @@ -29,6 +29,10 @@ void* APS5_VABI sceSslGetIssuerName(int, void*); int APS5_VABI sceSslGetNameEntryCount(int, void*); int APS5_VABI sceSslGetNameEntryInfo(int, void*, int, char*, std::size_t, char*, std::size_t, std::size_t*); int APS5_VABI sceSslFreeSslCertName(int, void*); +int APS5_VABI sceSslClose(int); +int APS5_VABI sceSslGetNotBefore(int, void*, SceSsl::RtcTick*); +int APS5_VABI sceSslGetNotAfter(int, void*, SceSsl::RtcTick*); +int APS5_VABI sceSslGetFingerprint(int, void*, int, unsigned char*); } namespace { @@ -236,11 +240,54 @@ void CertificateReaders() { Throws([&] { sceSslFreeSslCertName(id, fake); }); Throws([&] { sceSslFreeSslCertName(9999, subject); }); + SceSsl::RtcTick begin{}; + Check(sceSslGetNotBefore(id, cert, &begin) == 0, "get not before failed"); + Check(begin.tick == 63927094442000000ULL, "not before tick mismatch"); + Check(sceSslGetNotBefore(id, cert, nullptr) == static_cast(0x8095177a), "null begin accepted"); + Throws([&] { sceSslGetNotBefore(id, nullptr, &begin); }); + Throws([&] { sceSslGetNotBefore(id, fake, &begin); }); + Throws([&] { sceSslGetNotBefore(9999, cert, &begin); }); + + SceSsl::RtcTick limit{}; + Check(sceSslGetNotAfter(id, cert, &limit) == 0, "get not after failed"); + Check(limit.tick == 64242454442000000ULL, "not after tick mismatch"); + Check(sceSslGetNotAfter(id, cert, nullptr) == static_cast(0x8095177a), "null limit accepted"); + Throws([&] { sceSslGetNotAfter(id, nullptr, &limit); }); + Throws([&] { sceSslGetNotAfter(id, fake, &limit); }); + Throws([&] { sceSslGetNotAfter(9999, cert, &limit); }); + + unsigned char md5[16]{}; + Check(sceSslGetFingerprint(id, cert, 0, md5) == 0, "get md5 fingerprint failed"); + constexpr unsigned char expectedMd5[16] = {0x71, 0x3c, 0xe5, 0x20, 0x88, 0x2b, 0x6b, 0xd0, 0x17, 0x85, 0x07, 0xde, 0xbd, 0xdc, 0xa9, 0x4f}; + Check(std::memcmp(md5, expectedMd5, sizeof(md5)) == 0, "md5 digest mismatch"); + + unsigned char sha1[20]{}; + Check(sceSslGetFingerprint(id, cert, 1, sha1) == 0, "get sha1 fingerprint failed"); + constexpr unsigned char expectedSha1[20] = {0x47, 0xe0, 0x89, 0xab, 0xb7, 0x02, 0xee, 0xb6, 0x2f, 0x8b, 0x5b, 0x7a, 0x13, 0xfe, 0x82, 0x6d, 0x2b, 0xe8, 0xee, 0xa7}; + Check(std::memcmp(sha1, expectedSha1, sizeof(sha1)) == 0, "sha1 digest mismatch"); + + unsigned char sha256[32]{}; + Check(sceSslGetFingerprint(id, cert, 3, sha256) == 0, "get sha256 fingerprint failed"); + constexpr unsigned char expectedSha256[32] = {0x3d, 0x7d, 0xac, 0xd3, 0x04, 0x90, 0x42, 0xe1, 0x57, 0x19, 0x87, 0x27, 0x54, 0x45, 0xae, 0xed, 0xc4, 0xea, 0xd6, 0xd4, 0x20, 0xa0, 0x7f, 0xc4, 0xf5, 0x26, 0xc1, 0xa0, 0x85, 0x87, 0x5a, 0x4c}; + Check(std::memcmp(sha256, expectedSha256, sizeof(sha256)) == 0, "sha256 digest mismatch"); + + Check(sceSslGetFingerprint(id, cert, 3, nullptr) == static_cast(0x8095177a), "null fingerprint buffer accepted"); + Check(sceSslGetFingerprint(id, cert, -1, sha256) == static_cast(0x8095177a), "negative hash type accepted"); + Check(sceSslGetFingerprint(id, cert, 99, sha256) == static_cast(0x8095177a), "invalid hash type accepted"); + Throws([&] { sceSslGetFingerprint(id, nullptr, 3, sha256); }); + Throws([&] { sceSslGetFingerprint(id, fake, 3, sha256); }); + Throws([&] { sceSslGetFingerprint(9999, cert, 3, sha256); }); + + Throws([&] { sceSslClose(1); }); + Check(sceSslFreeCaList(id, &handles) == 0, "free ca list failed"); Check(sceSslTerm_nid_postfix(id) == 0, "term failed"); Throws([&] { sceSslGetSerialNumber(id, cert, serialData, &serialLen); }); Throws([&] { sceSslGetSubjectName(id, cert); }); Throws([&] { sceSslGetIssuerName(id, cert); }); + Throws([&] { sceSslGetNotBefore(id, cert, &begin); }); + Throws([&] { sceSslGetNotAfter(id, cert, &limit); }); + Throws([&] { sceSslGetFingerprint(id, cert, 3, sha256); }); const int id2 = sceSslInit_nid_postfix(1024 * 1024); Check(sceSslLoadCert(id2, 1, &pointer, nullptr, nullptr) == 0, "CA load failed"); @@ -254,6 +301,9 @@ void CertificateReaders() { Throws([&] { sceSslGetSerialNumber(id2, cert2, serialData, &serialLen); }); Throws([&] { sceSslGetSubjectName(id2, cert2); }); Throws([&] { sceSslGetIssuerName(id2, cert2); }); + Throws([&] { sceSslGetNotBefore(id2, cert2, &begin); }); + Throws([&] { sceSslGetNotAfter(id2, cert2, &limit); }); + Throws([&] { sceSslGetFingerprint(id2, cert2, 3, sha256); }); Check(sceSslTerm_nid_postfix(id2) == 0, "term failed"); Throws([&] { sceSslGetNameEntryCount(id2, subject2); }); Throws([&] { sceSslFreeSslCertName(id2, subject2); }); diff --git a/docs/dev/debt/libSceSsl.md b/docs/dev/debt/libSceSsl.md index abc8332e1b..b763182dc6 100644 --- a/docs/dev/debt/libSceSsl.md +++ b/docs/dev/debt/libSceSsl.md @@ -2,8 +2,7 @@ ### Unknown function info -- [sceSslCreateConnection, sceSslDeleteConnection, sceSslSetVerifyCallback, sceSslConnect, sceSslRead, sceSslWrite](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - imported by `ffmpeg.prx`. The repository has no TLS client, so a connection only records its context, socket, host and verification callback, and `sceSslConnect`, `sceSslRead` and `sceSslWrite` throw `NotImplemented_nid_no_patch(__func__)` until there is a transport. Signatures follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Net/Http.cs#L195-L252), which also says the socket stays open after `sceSslDeleteConnection`. An unknown context returns `SSL_ERROR_INVALID_ID` (0x8095F006), as `sceSslGetMemoryPoolStats` does on the console and as [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/4865dd92ca6b6bb907748f125883093af038f53b/src/libs/network.cpp#L2688-L2720) and [sharpemu](https://github.com/sharpemu/sharpemu/blob/29021b5a71bbc80f262d887e6ba18b3a72963f49/src/SharpEmu.Libs/Network/SslExports.cs#L41-L54) return for other context calls; for an unknown connection, a connection whose context was terminated and a null host the console's codes are unknown, so these throw. The socket is not checked when the connection is created, `sceSslTerm` leaves the context's connections to `sceSslDeleteConnection`, and `sceSslSetVerifyCallback` accepts any callback, null included, and returns 0; none of this is verified on the console. `SSL_ERROR_BEFORE_INIT` (0x8095F001) is never returned, as the library keeps no state apart from its contexts -- [sceSslClose](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - unknown signature -- [sceSslGetSerialNumber, sceSslGetSubjectName, sceSslGetIssuerName, sceSslGetNameEntryCount, sceSslGetNameEntryInfo, sceSslFreeSslCertName](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - signatures and context ownership follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Net/Http.cs#L293-L350). Operations take a context handle as their first argument and operate on certificate and name handles scoped to that context. `sceSslGetSerialNumber` copies into a caller-allocated output buffer or queries the required length when the buffer is null or capacity is zero; a buffer smaller than the serial number returns `SSL_ERROR_INVALID_ARG` (0x8095177a). Mbed TLS parses the certificate's serial number and X.509 subject and issuer distinguished names, extracting OID attribute short names (or numeric strings) and value strings. An unknown context, unknown or expired certificate handle or invalid certificate name handle throws; null output pointers or out-of-range entry indices return `SSL_ERROR_INVALID_ARG` (0x8095177a). The error codes, the zero-capacity length query, the NUL-terminated truncation of name entry strings with the full value length in `valueLen`, and the lifetime of name handles (released only by `sceSslFreeSslCertName` or when the context is terminated) have not been measured on a PS5 +- [sceSslCreateConnection, sceSslDeleteConnection, sceSslSetVerifyCallback, sceSslConnect, sceSslRead, sceSslWrite, sceSslClose](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - imported by `ffmpeg.prx`. The repository has no TLS client, so a connection only records its context, socket, host and verification callback, and `sceSslConnect`, `sceSslRead`, `sceSslWrite` and `sceSslClose` throw `NotImplemented_nid_no_patch(__func__)` until there is a transport. Signatures follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Net/Http.cs#L195-L252), which also says the socket stays open after `sceSslDeleteConnection`. An unknown context returns `SSL_ERROR_INVALID_ID` (0x8095F006), as `sceSslGetMemoryPoolStats` does on the console and as [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/4865dd92ca6b6bb907748f125883093af038f53b/src/libs/network.cpp#L2688-L2720) and [sharpemu](https://github.com/sharpemu/sharpemu/blob/29021b5a71bbc80f262d887e6ba18b3a72963f49/src/SharpEmu.Libs/Network/SslExports.cs#L41-L54) return for other context calls; for an unknown connection, a connection whose context was terminated and a null host the console's codes are unknown, so these throw. The socket is not checked when the connection is created, `sceSslTerm` leaves the context's connections to `sceSslDeleteConnection`, and `sceSslSetVerifyCallback` accepts any callback, null included, and returns 0; none of this is verified on the console. `SSL_ERROR_BEFORE_INIT` (0x8095F001) is never returned, as the library keeps no state apart from its contexts +- [sceSslGetSerialNumber, sceSslGetSubjectName, sceSslGetIssuerName, sceSslGetNameEntryCount, sceSslGetNameEntryInfo, sceSslFreeSslCertName, sceSslGetNotBefore, sceSslGetNotAfter, sceSslGetFingerprint](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - signatures and context ownership follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Net/Http.cs#L293-L350), with hash types matching its `SceSslFingerprintHashAlgorithm` enum. Operations take a context handle as their first argument and operate on certificate and name handles scoped to that context. `sceSslGetSerialNumber` copies into a caller-allocated output buffer or queries the required length when the buffer is null or capacity is zero; a buffer smaller than the serial number returns `SSL_ERROR_INVALID_ARG` (0x8095177a). Mbed TLS parses the certificate's serial number, validity period (converted to microseconds since Gregorian year 0001 epoch), fingerprint digests and X.509 subject and issuer distinguished names, extracting OID attribute short names (or numeric strings) and value strings. An unknown context, unknown or expired certificate handle or invalid certificate name handle throws; null output pointers, unsupported hash types or out-of-range entry indices return `SSL_ERROR_INVALID_ARG` (0x8095177a). The error codes, the zero-capacity length query, the NUL-terminated truncation of name entry strings with the full value length in `valueLen`, and the lifetime of name handles (released only by `sceSslFreeSslCertName` or when the context is terminated) have not been measured on a PS5 - [sceSslLoadCert, sceSslGetCaList, sceSslFreeCaList, sceSslGetCaCerts, sceSslFreeCaCerts, sceSslUnloadCert](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - signatures and layouts follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Http.cs). PPSA15508/01.002.000 calls `sceSslLoadCert` with one 1,408-byte PEM CA and null client certificate/key arguments; this confirms that call shape, not console error behavior. Mbed TLS 3.6.7 parses DER/PEM chains and checks an optional client certificate/key pair. Imports are retained per context and committed together; a failed import preserves existing credentials. CA queries return independently owned snapshots; unload clears active credentials while outstanding snapshots remain valid. Appending repeated CA imports, snapshot lifetime across unload, memory overhead and error precedence have not been measured on a PS5. Invalid loads, malformed material, exhausted pools and unsupported keys throw rather than inventing console error codes. No root bundle is shipped; an empty context still returns `NOT_FOUND`. The existing null-list error, unknown-context behavior and legacy clearing of foreign raw CA lists remain unchanged. Certificate reader queries reflect active certificates or outstanding snapshots. Loading credentials neither starts a TLS connection nor authenticates a peer - [sceSslEnableVerifyOption, sceSslDisableVerifyOption, sceSslSetMinSslVersion, sceSslSetAlpn, sceSslGetAlpnSelected](../../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - imported by PPSA01317 and PPSA07411. Signatures follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Net/Http.cs), the source of the other connection signatures. The verify options, the minimum version and the ALPN protocol list are recorded on the connection; nothing reads them until `sceSslConnect` has a transport, and how the console applies them is unknown. The initial verify options are unknown and taken as 0. `sceSslSetMinSslVersion` accepts the `SceSslVersion` values SharpProspero lists, 0 (none) to 5 (TLS 1.2), and throws for others; whether the console accepts a TLS 1.3 value is unknown. `sceSslSetAlpn` stores the string up to its terminator and throws for a null list; whether the console expects a comma-separated list or the TLS wire format is unknown. `sceSslGetAlpnSelected` returns null, as no handshake completes. An unknown connection and a connection whose context was terminated throw, as for `sceSslSetVerifyCallback` From 4ef9056af47c4e3bcd6671e575fbabeb535f22f1 Mon Sep 17 00:00:00 2001 From: simonbronk Date: Fri, 16 Oct 2026 14:08:31 +0200 Subject: [PATCH 0667/1000] fix(nid-names): keep empty symbol filters in name mode --- tools/nid_names.py | 4 ++-- tools/tests/test_nid_names.py | 15 +++++++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/tools/nid_names.py b/tools/nid_names.py index 87bd3db888..82292e7e85 100644 --- a/tools/nid_names.py +++ b/tools/nid_names.py @@ -188,8 +188,8 @@ def main(argv=None): requested_nids = "--nid" in requested_options if requested_nids and requested_options & {"--name", "--symbol"}: parser.error("--name/--symbol cannot be combined with --nid") - if args.name: - rows = [{"name": name, "nid": compute_nid(name)} for name in args.name] + if requested_options & {"--name", "--symbol"}: + rows = [{"name": name, "nid": compute_nid(name)} for name in (args.name or [])] if args.json: print(json.dumps({"symbols": rows}, indent=1)) else: diff --git a/tools/tests/test_nid_names.py b/tools/tests/test_nid_names.py index 179820cbba..ff04a1edab 100644 --- a/tools/tests/test_nid_names.py +++ b/tools/tests/test_nid_names.py @@ -15,6 +15,21 @@ import nid_names class DatabaseCacheTests(unittest.TestCase): + def test_empty_symbol_filters_do_not_open_database(self): + with tempfile.TemporaryDirectory() as directory: + for option in ("--name", "--symbol", "--na", "--sym"): + for json_output in (False, True): + with self.subTest(option=option, json_output=json_output): + result = subprocess.run( + [sys.executable, str(Path(nid_names.__file__)), option, "--db", directory, + *(["--json"] if json_output else [])], capture_output=True, text=True) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(result.stderr, "") + if json_output: + self.assertEqual(json.loads(result.stdout), {"symbols": []}) + else: + self.assertEqual(result.stdout.split(), ["NAME", "NID"]) + def test_interrupted_download_does_not_publish_a_partial_cache(self): with tempfile.TemporaryDirectory() as directory: cache = Path(directory) / "cache" / "aerolib.csv" From dde946d488766ab8d6ab8dc458342d5c9a7ba8bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Sat, 17 Oct 2026 10:00:09 +0200 Subject: [PATCH 0668/1000] ci: label pull requests that close an issue Co-Authored-By: Claude --- .github/pull_request_template.md | 1 + .github/workflows/fix-label.yml | 44 ++++++++++++++++++++++++++++++++ .github/workflows/labels.yml | 1 + CONTRIBUTING.md | 1 + 4 files changed, 47 insertions(+) create mode 100644 .github/workflows/fix-label.yml diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 991703ac12..c084976f5c 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -13,6 +13,7 @@ - [ ] New third-party code is a submodule built from source - [ ] Generic behaviour, not specific to one title - [ ] Depends on: +- [ ] Fixes: - [ ] Unblocks: - [ ] GPUs tested: - [ ] AI-assisted: diff --git a/.github/workflows/fix-label.yml b/.github/workflows/fix-label.yml new file mode 100644 index 0000000000..978c9fb99d --- /dev/null +++ b/.github/workflows/fix-label.yml @@ -0,0 +1,44 @@ +name: Fix label + +on: + schedule: + - cron: '45 * * * *' + workflow_dispatch: + +permissions: {} + +concurrency: + group: fix-label + cancel-in-progress: true + +jobs: + label: + runs-on: ubuntu-latest + permissions: + issues: read + pull-requests: write + env: + GH_TOKEN: ${{ github.token }} + LABEL: fix error + steps: + - run: | + gh api graphql --paginate -f owner="${GITHUB_REPOSITORY%/*}" -f name="${GITHUB_REPOSITORY#*/}" -f query=' + query($owner: String!, $name: String!, $endCursor: String) { + repository(owner: $owner, name: $name) { + pullRequests(states: OPEN, first: 100, after: $endCursor) { + pageInfo { hasNextPage endCursor } + nodes { number closingIssuesReferences { totalCount } labels(first: 100) { nodes { name } } } + } + } + }' --jq '.data.repository.pullRequests.nodes[] + | (.closingIssuesReferences.totalCount > 0) as $fix + | ([.labels.nodes[].name] | index(env.LABEL) != null) as $has + | if $fix and ($has | not) then "add \(.number)" elif $has and ($fix | not) then "remove \(.number)" else empty end' > "$RUNNER_TEMP/fix-todo" + label=$(jq -rn 'env.LABEL | @uri') + while read -r action number; do + if [ "$action" = add ]; then + gh api "repos/$GITHUB_REPOSITORY/issues/$number/labels" -f "labels[]=$LABEL" --silent + else + gh api -X DELETE "repos/$GITHUB_REPOSITORY/issues/$number/labels/$label" --silent + fi + done < "$RUNNER_TEMP/fix-todo" diff --git a/.github/workflows/labels.yml b/.github/workflows/labels.yml index 5fa69ccd0b..ebaaec842e 100644 --- a/.github/workflows/labels.yml +++ b/.github/workflows/labels.yml @@ -35,4 +35,5 @@ jobs: label prx c5def5 'Other PRX libraries' label build bfd4f2 'CMake, submodules, 3rdparty' label docs 0075ca 'Documentation' + label 'fix error' d73a4a 'Closes an issue (Fixes #N)' label stale ededed 'Conflicting with main and no activity for 7 days' diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 513bf25eba..1f7568da8d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -57,6 +57,7 @@ The [pull request template](.github/pull_request_template.md) is the checklist f - Keep the branch up to date with `main` and resolve conflicts yourself; rebasing and force-pushing is fine. - A pull request that conflicts with `main` and has no activity for 7 days is labeled `stale` with a reminder. It is not closed. - If a pull request needs another one first, say so on the checklist line (`Depends on: #N`; `none` otherwise). +- If a pull request fixes a reported issue, write `Fixes #N` on its checklist line (`none` otherwise). GitHub closes the issue on merge, and the `fix error` label is added within the hour. - If a pull request fixes a stop or a wrong result seen on a title, name it on the checklist line with the title ID and its [Game bring-up](https://github.com/boykopovar/AnyPS5/discussions/categories/game-bring-up) discussion (`Unblocks: PPSA01325 #3232`; `none` otherwise). Reviewers review these first. - For GPU changes, list each tested GPU's vendor, model, driver and OS on the checklist line. CI runs GPU tests only on lavapipe, so reviewers may ask for a run on another vendor before merging. - Fixes a title needs go to `main` as pull requests. A Game bring-up discussion lists the open pull requests and local changes the title still needs; update the list when one is merged, closed or replaced. From e5cc0bc0ffa58430fcdeb820e33c404e315002ba Mon Sep 17 00:00:00 2001 From: thuanquang Date: Sat, 17 Oct 2026 14:00:45 +0700 Subject: [PATCH 0669/1000] feat(shader): translate packed f16 in the f16 denormal modes that flush v_pk_add/mul/fma/fmac/min/max_f16 threw in f16 denormal modes 0-2. On RDNA2 (gfx1031) bit 6 clear flushes each denormal input lane to a zero of its sign, and bit 7 clear flushes an add result below 2^-14 and a mul or fma result whose exact value rounded to 11 significant bits with an unbounded exponent is below 2^-14, as the scalar forms do; min/max flush only their inputs. Packed f16 still throws for f16 rounding other than nearest even. Co-Authored-By: Claude Opus 5.5 --- .../tests/execution/Float16MinMaxBits.cpp | 19 +- .../tests/execution/PackedAlu.cpp | 10 +- .../execution/PackedF16DenormalModes.cpp | 203 ++++++++++++++++++ .../Translation/src/FloatInstructions.cpp | 28 ++- docs/dev/debt/shader.md | 4 +- 5 files changed, 233 insertions(+), 31 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/PackedF16DenormalModes.cpp diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/Float16MinMaxBits.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/Float16MinMaxBits.cpp index 8c6b30f646..a70e3086fd 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/Float16MinMaxBits.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/Float16MinMaxBits.cpp @@ -85,7 +85,6 @@ const std::array PackedChecks{{ struct Mode { const char* name; std::optional floatMode; - bool packed = true; }; const std::array Modes{{ @@ -94,8 +93,8 @@ const std::array Modes{{ {"dx10", ShaderRecompiler::ShaderFloatMode{0xc0u, true, false, false}}, {"ieee", ShaderRecompiler::ShaderFloatMode{0xc0u, false, true, false}}, {"none", ShaderRecompiler::ShaderFloatMode{0xc0u, false, false, false}}, - {"f16 denormals flushed, dx10 ieee", ShaderRecompiler::ShaderFloatMode{0x30u, true, true, false}, false}, - {"f16 input denormals flushed", ShaderRecompiler::ShaderFloatMode{0xb0u, false, false, false}, false}, + {"f16 denormals flushed, dx10 ieee", ShaderRecompiler::ShaderFloatMode{0x30u, true, true, false}}, + {"f16 input denormals flushed", ShaderRecompiler::ShaderFloatMode{0xb0u, false, false, false}}, }}; std::uint64_t Seed = 0x2545f4914f6cdd1dull; @@ -214,14 +213,7 @@ struct Model { bits[index] = (registers[source.reg] >> (selectHigh ? 16u : 0u)) & 0xffffu; if (high != 0u ? source.negateHi : source.negate) bits[index] ^= 0x8000u; } - std::uint32_t lane = Numeric(check.kind == Kind::Max, Read(bits[0]), Read(bits[1])).bits; - lane = IsNan(bits[0]) ? bits[1] : lane; - lane = IsNan(bits[1]) ? bits[0] : lane; - if (ieee) { - lane = IsSnan(bits[1]) ? Quiet(bits[1]) : lane; - lane = IsSnan(bits[0]) ? Quiet(bits[0]) : lane; - } - result |= Clamp(check.clamp, lane) << (16u * high); + result |= Clamp(check.clamp, Pair(check.kind == Kind::Max, Read(bits[0]), Read(bits[1])).bits) << (16u * high); } return result; } @@ -267,8 +259,7 @@ int main() { const bool flushInputs = mode.floatMode.has_value() && (mode.floatMode->floatMode & 0x40u) == 0u; const Model model{mode.floatMode.has_value() && mode.floatMode->ieeeMode, !mode.floatMode.has_value() || mode.floatMode->dx10Clamp, flushInputs}; const auto scalar = compile(ScalarCode, mode); - std::optional packed; - if (mode.packed) packed = compile(PackedCode, mode); + const auto packed = compile(PackedCode, mode); for (std::uint32_t batch = 0; batch < Batches; ++batch) { for (std::uint32_t tid = 0; tid < Threads; ++tid) { std::uint32_t* row = &Input[tid * Inputs]; @@ -293,7 +284,7 @@ int main() { } }; run(scalar, ScalarCode, ScalarChecks, false); - if (packed) run(*packed, PackedCode, PackedChecks, true); + run(packed, PackedCode, PackedChecks, true); } } std::printf("f16 min/max tests passed (%llu results)\n", static_cast(compared)); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PackedAlu.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PackedAlu.cpp index d5f6b199f5..b643f84060 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PackedAlu.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PackedAlu.cpp @@ -196,7 +196,7 @@ constexpr const char* Names[48] = { }; bool Ieee = false; -bool FlushHalf = false; +bool DirectedHalf = false; void Fill(std::uint32_t tid, std::uint32_t* words) { std::copy(std::begin(Rows[tid]), std::end(Rows[tid]), words); @@ -236,7 +236,7 @@ void Run(AgcDriver::VulkanDevice& device) { }; request.useCache = false; if (Ieee) request.context.floatMode = ShaderRecompiler::ShaderFloatMode{0xf0u, true, true, false}; - if (FlushHalf) request.context.floatMode = ShaderRecompiler::ShaderFloatMode{0x30u, true, false, false}; + if (DirectedHalf) request.context.floatMode = ShaderRecompiler::ShaderFloatMode{0xf4u, true, false, false}; const auto result = ShaderRecompiler::Recompile(request); device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); device.WaitIdle(); @@ -264,14 +264,14 @@ int main() { Check(); } Ieee = false; - FlushHalf = true; + DirectedHalf = true; bool refused = false; try { Run(*device); } catch (const std::exception& error) { - refused = std::string(error.what()).find("f16 denormals not kept") != std::string::npos; + refused = std::string(error.what()).find("f16 rounding not to nearest even") != std::string::npos; } - Require(refused, "packed alu: flushed f16 denormals were not refused"); + Require(refused, "packed alu: directed f16 rounding was not refused"); std::puts("packed alu tests passed"); return 0; } catch (const std::exception& error) { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PackedF16DenormalModes.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PackedF16DenormalModes.cpp new file mode 100644 index 0000000000..b4570abb67 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PackedF16DenormalModes.cpp @@ -0,0 +1,203 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 16; +constexpr std::uint32_t Inputs = 4; +constexpr std::uint32_t Results = 4; +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; + +alignas(256) constexpr std::array Code{ + 0x34020084u, 0x34040084u, 0xe03c1000u, 0x80000401u, 0xbf8c3f70u, 0xcc0f400au, 0x18020b04u, 0xcc10400bu, + 0x18020b04u, 0xcc0e400cu, 0x1c1a0b04u, 0xe07c1000u, 0x80010a02u, 0xbf810000u, +}; +constexpr std::uint32_t Rows[Threads][3] = { + {0x02000001u, 0x02000000u, 0x02000000u}, + {0x83ff8001u, 0x03ff0001u, 0x00000000u}, + {0x7c0003ffu, 0x00010001u, 0x00000000u}, + {0x7bff0400u, 0x3c008001u, 0x00010000u}, + {0x04013bffu, 0xb8000400u, 0x02000000u}, + {0xbc003c01u, 0x000103ffu, 0x04000000u}, + {0x3c000400u, 0x84010400u, 0x04008400u}, + {0x04000400u, 0x3bff8400u, 0x83ff0400u}, + {0x84000001u, 0x3bff0001u, 0x03ff8400u}, + {0x35553c00u, 0x05550001u, 0x00000000u}, + {0x01553800u, 0x44000001u, 0x00000001u}, + {0x81553800u, 0x44000400u, 0x04000000u}, + {0x00008400u, 0x80003800u, 0x80000000u}, + {0x80003c00u, 0x80003c00u, 0x00000001u}, + {0x5bff1fffu, 0x04002001u, 0x00000000u}, + {0x3bff2001u, 0x3bff1fffu, 0x00018001u}, +}; +constexpr std::uint32_t Expected[4][Threads][3] = { + { + {0x00000000u, 0x00000000u, 0x00000000u}, + {0x00000000u, 0x80008000u, 0x00000000u}, + {0x7c000000u, 0xfe000000u, 0xfe000000u}, + {0x7bff0400u, 0x7bff8000u, 0x7bff0000u}, + {0xb8003bffu, 0x80000000u, 0x80000000u}, + {0xbc003c01u, 0x80000000u, 0x04000000u}, + {0x3c000800u, 0x84010000u, 0x80008400u}, + {0x3bff0000u, 0x00008000u, 0x00000400u}, + {0x3bff0000u, 0x80000000u, 0x80008400u}, + {0x35553c00u, 0x00000000u, 0x00000000u}, + {0x44003800u, 0x00000000u, 0x00000000u}, + {0x44003800u, 0x80000000u, 0x04000000u}, + {0x00003800u, 0x80008000u, 0x80008000u}, + {0x80004000u, 0x00003c00u, 0x00003c00u}, + {0x5bff2400u, 0x23ff0400u, 0x23ff0400u}, + {0x3fff2400u, 0x3bfe0400u, 0x3bfe0400u}, + }, + { + {0x04000000u, 0x00000000u, 0x00000000u}, + {0x00000000u, 0x80008000u, 0x80008000u}, + {0x7c000400u, 0x7c000000u, 0x7c000000u}, + {0x7bff0000u, 0x7bff8000u, 0x7bff8000u}, + {0xb8003bffu, 0x80000000u, 0x80000000u}, + {0xbc003c01u, 0x80000400u, 0x00000400u}, + {0x3c000800u, 0x84010000u, 0x80008400u}, + {0x3bff0000u, 0x00008000u, 0x00000400u}, + {0x3bff0000u, 0x80000000u, 0x80008400u}, + {0x35553c00u, 0x00000000u, 0x00000000u}, + {0x44003800u, 0x05540000u, 0x05540000u}, + {0x44003800u, 0x85540000u, 0x80000000u}, + {0x00003800u, 0x80008000u, 0x80008000u}, + {0x80004000u, 0x00003c00u, 0x00003c00u}, + {0x5bff2400u, 0x23ff0400u, 0x23ff0400u}, + {0x3fff2400u, 0x3bfe0400u, 0x3bfe0000u}, + }, + { + {0x00000000u, 0x00000000u, 0x00000000u}, + {0x00000000u, 0x80008000u, 0x00000000u}, + {0x7c000000u, 0xfe000000u, 0xfe000000u}, + {0x7bff0400u, 0x7bff8000u, 0x7bff0000u}, + {0xb8003bffu, 0x82000400u, 0x82000400u}, + {0xbc003c01u, 0x80000000u, 0x04000000u}, + {0x3c000800u, 0x84010000u, 0x80018400u}, + {0x3bff0000u, 0x04008000u, 0x04000400u}, + {0x3bff0000u, 0x84000000u, 0x84008400u}, + {0x35553c00u, 0x01c70000u, 0x01c70000u}, + {0x44003800u, 0x00000000u, 0x00000000u}, + {0x44003800u, 0x80000200u, 0x04000200u}, + {0x00003800u, 0x80008200u, 0x80008200u}, + {0x80004000u, 0x00003c00u, 0x00003c00u}, + {0x5bff2400u, 0x23ff0400u, 0x23ff0400u}, + {0x3fff2400u, 0x3bfe0400u, 0x3bfe0400u}, + }, + { + {0x04000001u, 0x00000000u, 0x02000000u}, + {0x00000000u, 0x80008000u, 0x80008000u}, + {0x7c000400u, 0x7c000000u, 0x7c000000u}, + {0x7bff03ffu, 0x7bff8000u, 0x7bff8000u}, + {0xb8003bffu, 0x82000400u, 0x80000400u}, + {0xbc003c01u, 0x80010400u, 0x03ff0400u}, + {0x3c000800u, 0x84010000u, 0x80018400u}, + {0x3bff0000u, 0x04008000u, 0x00000400u}, + {0x3bff0002u, 0x84000000u, 0x80008400u}, + {0x35553c00u, 0x01c70001u, 0x01c70001u}, + {0x44003800u, 0x05540000u, 0x05540002u}, + {0x44003800u, 0x85540200u, 0x81540200u}, + {0x00003800u, 0x80008200u, 0x80008200u}, + {0x80004000u, 0x00003c00u, 0x00003c00u}, + {0x5bff2400u, 0x23ff0400u, 0x23ff0400u}, + {0x3fff2400u, 0x3bfe0400u, 0x3bfe03ffu}, + }, +}; + +struct Mode { + const char* name; + ShaderRecompiler::ShaderFloatMode value; + std::uint32_t field; +}; + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + +void Run(AgcDriver::VulkanDevice& device, const Mode& mode) { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + std::copy(std::begin(Rows[tid]), std::end(Rows[tid]), &Input[tid * Inputs]); + Input[tid * Inputs + 3u] = 0u; + } + Output.fill(0xdeadbeefu); + std::vector userData(8, 0u); + const auto input = BufferDescriptor(Input.data(), static_cast(Input.size() * 4u)); + const auto output = BufferDescriptor(Output.data(), static_cast(Output.size() * 4u)); + std::copy(input.begin(), input.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4); + const std::span code(Code); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + request.context.floatMode = mode.value; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); +} + +void Check(const Mode& mode) { + constexpr const char* Names[3] = {"v_pk_add_f16", "v_pk_mul_f16", "v_pk_fma_f16"}; + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + for (std::uint32_t form = 0; form < 3u; ++form) { + const std::uint32_t actual = Output[tid * Results + form]; + const std::uint32_t expected = Expected[mode.field][tid][form]; + Require(actual == expected, std::string(Names[form]) + " " + mode.name + " of " + Hex(Rows[tid][0]) + ", " + Hex(Rows[tid][1]) + ", " + Hex(Rows[tid][2]) + " is " + Hex(actual) + ", expected " + Hex(expected)); + } + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + const Mode modes[] = { + {"f16 denormal mode 0, IEEE mode on", ShaderRecompiler::ShaderFloatMode{0x30u, true, true, false}, 0u}, + {"f16 denormal mode 0, IEEE mode off", ShaderRecompiler::ShaderFloatMode{0x30u, true, false, false}, 0u}, + {"f16 denormal mode 1, IEEE mode on", ShaderRecompiler::ShaderFloatMode{0x70u, true, true, false}, 1u}, + {"f16 denormal mode 1, IEEE mode off", ShaderRecompiler::ShaderFloatMode{0x70u, true, false, false}, 1u}, + {"f16 denormal mode 2, IEEE mode on", ShaderRecompiler::ShaderFloatMode{0xb0u, true, true, false}, 2u}, + {"f16 denormal mode 2, IEEE mode off", ShaderRecompiler::ShaderFloatMode{0xb0u, true, false, false}, 2u}, + {"f16 denormal mode 3, IEEE mode on", ShaderRecompiler::ShaderFloatMode{0xf0u, true, true, false}, 3u}, + {"f16 denormal mode 3, IEEE mode off", ShaderRecompiler::ShaderFloatMode{0xf0u, true, false, false}, 3u}, + }; + for (const Mode& mode : modes) { + Run(*device, mode); + Check(mode); + } + std::puts("packed f16 denormal mode tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/shader/recompiler/Translation/src/FloatInstructions.cpp b/core/shader/recompiler/Translation/src/FloatInstructions.cpp index 774bc47a20..61948b2be5 100644 --- a/core/shader/recompiler/Translation/src/FloatInstructions.cpp +++ b/core/shader/recompiler/Translation/src/FloatInstructions.cpp @@ -11,25 +11,33 @@ void TranslateFloatInstruction(IrBuilder& builder, const RdnaInstruction& instru } bool TranslationContext::packedFloat16(const RdnaInstruction& inst, IrOpcode opcode, bool accumulator, bool quietSnan) { - if (floatMode.has_value() && (floatMode->floatMode & 0xccu) != 0xc0u) { - throw std::runtime_error("packed f16 at pc " + std::to_string(inst.programCounter) + " in FLOAT_MODE " + std::to_string(floatMode->floatMode) + " (f16 denormals not kept or rounding not to nearest even) is not implemented"); + if (floatMode.has_value() && (floatMode->floatMode & 0x0cu) != 0u) { + throw std::runtime_error("packed f16 at pc " + std::to_string(inst.programCounter) + " in FLOAT_MODE " + std::to_string(floatMode->floatMode) + " (f16 rounding not to nearest even) is not implemented"); } + const bool flushResults = floatMode.has_value() && (floatMode->floatMode & 0x80u) == 0u; + const auto flushTinyResult = [&](IrF32 value) { + if (!flushResults) return value; + const IrU32 bits(ir.BitCastU32(value.Value())); + const IrU1 tiny(ir.ULessThan(ir.BitwiseAnd(bits.Value(), ir.Constant(0x7fffffffu)), ir.Constant(opcode == IrOpcode::FPAdd32 ? 0x38800000u : 0x387ff000u))); + return IrF32(ir.BitCastF32(ir.Select(tiny.Value(), ir.BitwiseAnd(bits.Value(), ir.Constant(0x80000000u)), bits.Value()))); + }; + const auto readLane = [&](const RdnaOperand& operand, bool high) { return flushF16InputValue(readF16LaneAsF32(operand, high, true)); }; const auto translateLane = [&](bool high) -> IrF32 { - const IrF32 lhs = readF16LaneAsF32(sourceAt(inst, 0u), high, true); - const IrF32 rhs = readF16LaneAsF32(sourceAt(inst, 1u), high, true); + const IrF32 lhs = readLane(sourceAt(inst, 0u), high); + const IrF32 rhs = readLane(sourceAt(inst, 1u), high); const auto ternary = [&](const IrF32& third) -> IrF32 { - if (opcode == IrOpcode::FPFma32) return fmaF16RoundedToOdd(lhs, rhs, third); + if (opcode == IrOpcode::FPFma32) return flushTinyResult(fmaF16RoundedToOdd(lhs, rhs, third)); return IrF32(ir.Emit(opcode, IrType::F32, {&lhs.Value(), &rhs.Value(), &third.Value()})); }; if (accumulator) { - const IrF32 acc = readF16LaneAsF32(accumulatorOperand(inst), high, true); + const IrF32 acc = readLane(accumulatorOperand(inst), high); return applyF32ResultModifiers(inst.destination, clampF16Overflow(ternary(acc), {&lhs.Value(), &rhs.Value(), &acc.Value()})); } if (inst.sourceCount == 3u) { - const IrF32 third = readF16LaneAsF32(sourceAt(inst, 2u), high, true); + const IrF32 third = readLane(sourceAt(inst, 2u), high); return applyF32ResultModifiers(inst.destination, clampF16Overflow(ternary(third), {&lhs.Value(), &rhs.Value(), &third.Value()})); } - return applyF32ResultModifiers(inst.destination, clampF16Overflow(IrF32(ir.Emit(opcode, IrType::F32, {&lhs.Value(), &rhs.Value()})), {&lhs.Value(), &rhs.Value()})); + return applyF32ResultModifiers(inst.destination, clampF16Overflow(flushTinyResult(IrF32(ir.Emit(opcode, IrType::F32, {&lhs.Value(), &rhs.Value()}))), {&lhs.Value(), &rhs.Value()})); }; const auto laneBits = [&](const RdnaOperand& operand, bool high) { const IrU32 source = readF16SourceBits(operand); @@ -37,7 +45,7 @@ bool TranslationContext::packedFloat16(const RdnaInstruction& inst, IrOpcode opc IrU32 bits(ir.Emit(IrOpcode::BitFieldUExtract, IrType::U32, {&source.Value(), &ir.Constant(selectHigh ? 16u : 0u), &ir.Constant(16u)})); if (operand.absolute) bits = IrU32(ir.BitwiseAnd(bits.Value(), ir.Constant(0x7fffu))); if (high ? operand.negateHi : operand.negate) bits = IrU32(ir.BitwiseXor(bits.Value(), ir.Constant(0x8000u))); - return bits; + return flushF16Input(bits); }; std::array, 3> sourceBits{}; std::array, 3> sourceBitsReady{}; @@ -51,7 +59,7 @@ bool TranslationContext::packedFloat16(const RdnaInstruction& inst, IrOpcode opc return sourceBits[index][lane]; }; const auto minMaxLane = [&](bool high) { return minMaxF16Bits(opcode == IrOpcode::FPMax32, sourceLaneBits(0u, high), sourceLaneBits(1u, high)); }; - const bool native = nativeFloat16 && inst.destination.omod == 0u && (opcode == IrOpcode::FPAdd32 || opcode == IrOpcode::FPMul32); + const bool native = nativeFloat16 && !flushResults && inst.destination.omod == 0u && (opcode == IrOpcode::FPAdd32 || opcode == IrOpcode::FPMul32); const auto nativeResult = [&]() { const IrU32 lhs = packU16Lanes(sourceLaneBits(0u, false), sourceLaneBits(0u, true)); const IrU32 rhs = packU16Lanes(sourceLaneBits(1u, false), sourceLaneBits(1u, true)); diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index 0c9753627b..18c54ff021 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -92,12 +92,12 @@ - [v_div_fixup_f64](../../../core/shader/recompiler/Translation/src/Float64Instructions.cpp) flushes its src1 and src2 to a zero of the same sign when bit 0 of the `FLOAT_MODE` f16/f64 denormal field is clear (0 and 2) and keeps a denormal src0 and result in every mode, as `v_div_fixup_f32` does with the f32 field. Measured on RDNA2 (gfx1031, Radeon RX 6700 XT, AMD Windows driver 32.0.21045.5002 through HIP) over 1728 triples of 12 f64 values (zeros, denormals, the minimum normal, ±1, the largest finite value, infinity and quiet and signaling NaNs) at IEEE=0 and IEEE=1 with denorm16 0..3. Shaders without a known float mode keep the previous lowering. The PS5's gfx1013 is not measured. - [v_mullit_f32](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) follows the hardware measured with the MODE register at IEEE=1 and denormals kept (a signaling NaN src0 is quieted only with IEEE=1, as measured at IEEE=0 too; a positive denormal src2 counts as positive); with f32 denormals flushed it follows the f32 denormals entry below, and denormal modes 1 and 2 throw. - [v_mad_legacy_f32 and v_mac_legacy_f32](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) (VOP3 0x140, VOP2 0x06) round the product to f32 before the add, give a +0 product for a zero factor, and flush their f32 denormal sources, their product and their result to a zero of the same sign in every f32 denormal mode, an exception to the `FLOAT_MODE` model in [f32DenormalFlushFor](../../../core/shader/recompiler/Translation/src/VectorInstructions.cpp). Measured on gfx1010 (RX 5700 XT) with `tools/hw-oracle/template.s`, assembled by the AMD Windows driver's `amd_comgr_2.dll` and launched through its `amdhip64_6.dll` instead of `oracle.c` and ROCr, over the 32 rows of the five `FmaLegacy.cpp` and the four `MacLegacy.cpp` encodings: identical in wave32 and wave64 at IEEE 0 and 1 with f32 denormal modes 0 to 3, except that the two signaling NaN rows are quieted only with IEEE=1 (#1281, #1290). A sum of two zeros takes its sign on the bits (+0 unless both are -0), as lavapipe returned -0 for +0 + -0. The PS5's gfx1013 is not measured; LLVM decodes the encoding as `v_mad_legacy_f32` there, as on gfx1010. RDNA2 differs: on gfx1031 (RX 6700 XT, measured by pandather in #1281) the encoding is `v_fma_legacy_f32` and fused, with the same zero rule and denormal-source flush, so an RDNA2 measurement is not evidence for the console. -- [v_fma_mixlo_f16 and v_fma_mixhi_f16](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) round the exact product-sum once to f16 with the f64 arithmetic of v_interp_p2_f16, so they need the Float64 capability. On RDNA2 (gfx1036) they follow the `FLOAT_MODE` f16 round and denormal fields, not the f32 round field: -1.5 * 2^-24 + 2^-126 from f32 sources (0xbfc00000, 0x33800000, 0x00800000) gives 0x8001 with f16 denormals kept and round to nearest even, 0x8000 with f16 denormals flushed and 0x8002 with f16 round toward -infinity. With f16 denormals kept, every f16 rounding mode is translated: the exact product-sum is rounded once in the mode's direction. All four modes match RDNA2 (gfx1036) on the 2,233 rows of the #2631 sweep, including `FLOAT_MODE` 0xcc (toward zero, used by ps5-opengl) and the exact cancellations toward -infinity; the PS5's gfx1013 is not measured. A result beyond 65504 becomes infinity or 65504 as IEEE 754 gives for the mode. Flushed f16 denormals still throw, and packed f16 still throws in every mode but round to nearest even with f16 denormals kept. `FP16_OVFL` set is not measured. +- [v_fma_mixlo_f16 and v_fma_mixhi_f16](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) round the exact product-sum once to f16 with the f64 arithmetic of v_interp_p2_f16, so they need the Float64 capability. On RDNA2 (gfx1036) they follow the `FLOAT_MODE` f16 round and denormal fields, not the f32 round field: -1.5 * 2^-24 + 2^-126 from f32 sources (0xbfc00000, 0x33800000, 0x00800000) gives 0x8001 with f16 denormals kept and round to nearest even, 0x8000 with f16 denormals flushed and 0x8002 with f16 round toward -infinity. With f16 denormals kept, every f16 rounding mode is translated: the exact product-sum is rounded once in the mode's direction. All four modes match RDNA2 (gfx1036) on the 2,233 rows of the #2631 sweep, including `FLOAT_MODE` 0xcc (toward zero, used by ps5-opengl) and the exact cancellations toward -infinity; the PS5's gfx1013 is not measured. A result beyond 65504 becomes infinity or 65504 as IEEE 754 gives for the mode. Flushed f16 denormals still throw, and packed f16 throws with any f16 rounding but nearest even. `FP16_OVFL` set is not measured. - [Signaling NaN inputs](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) of the f32 and f16 unary instructions (`v_sqrt/rcp/rsq/log/exp/sin/cos/fract/floor/ceil/trunc/rndne/rcp_iflag/frexp_mant`), `v_ldexp_f32/f16`, `v_frexp_mant_f16`, `v_div_fixup_f32/f16` and `v_mullit_f32` are returned quieted only with IEEE=1 and unchanged with IEEE=0, measured on RDNA2 (gfx1035) in both modes, and so are those of the f64 forms (`v_sqrt/rcp/rsq/fract/floor/ceil/trunc/rndne/frexp_mant/ldexp/div_fixup_f64`) and `v_cvt_f64_f32`, measured on RDNA2 (gfx1036) in both modes; shaders without a known float mode follow IEEE=0. `v_pack_b32_f16` quiets each signaling NaN half by the same rule, measured on RDNA2 (gfx1036) in both modes. A signaling NaN result of `v_cubesc/cubetc/cubema_f32` is quieted by the same rule and keeps the sign the instruction gives it, measured on RDNA2 (gfx1036) in both modes. `v_add/sub/subrev/mul/fma/fmac/fmamk/fmaak/mul_legacy_f32` and `v_fma_mix_f32` with f32 sources choose their NaN result on the bits, measured over 1000 operand triples in both modes: the first NaN source in operand order after its modifiers (the subtrahend negated, as the hardware adds its negation), quieted only with IEEE=1, or the default NaN 0xffc00000 for an invalid operation; in the fma forms an invalid product (inf * 0) gives the default NaN even when src2 is a NaN, and in the legacy forms a zero factor makes the product 0 even when the other factor is a NaN (a NaN src2 still wins). `v_mad_legacy_f32` and `v_mac_legacy_f32` follow the same rule with their separately rounded product, as measured on gfx1010 (#1281, #1290): a NaN source quieted only with IEEE=1, inf - inf giving 0xffc00000, the zero rule hiding a NaN factor and a NaN src2 winning. The f16 and f64 arithmetic, `v_fma_mix_f32` with f16 sources and the separately rounded mad/mac forms (not measurable on RDNA2) follow the same rule on hardware but their NaN bits are left to the driver. `v_cvt_f16_f32`, `v_cvt_f32_f64`, `v_dot2_f32_f16`, `v_fma_mixlo_f16` and `v_fma_mixhi_f16` always quiet and `v_div_scale_f32` never does: the f32 signaling NaN 0x7f812345 as src0 or src2 gives 0x7e09 from mixlo and mixhi at IEEE=0 and IEEE=1 on RDNA2 (gfx1036), where `v_fma_mix_f32` gives 0x7f812345 at IEEE=0 and 0x7fc12345 at IEEE=1. The payload of an f16 NaN source of mixlo and mixhi is left to the driver's f16 to f32 conversion, which NVIDIA replaces. - [Scalar f16 add, sub, subrev and mul denormals](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) follow the `FLOAT_MODE` f16 denormal field at nearest-even: bit 0 preserves inputs and bit 1 preserves the result; otherwise a denormal is flushed to its signed zero. Measured on RDNA2 (gfx1036) with IEEE=0/1 and FP16_OVFL=0/1, over 32 operand pairs in all four denormal modes, including signed zeros, denormals, minimum normals, infinities and NaNs. Multiply detects tininess after rounding to 11 significant bits with an unbounded exponent, before gradual half packing: `0x3bff * 0x0400` flushes, while `0x3c01 * 0x03ff` rounds to `0x0400` and is kept. `v_fma_f16`, `v_fmac_f16`, `v_fmamk_f16` and `v_fmaak_f16` follow the same field at nearest-even: inputs are flushed when bit 0 is clear, and the result is flushed when bit 1 is clear if the exact product-sum rounded to 11 significant bits with an unbounded exponent is below 2^-14, so `0x0400 * 0x0400 + 0x8400` rounds to `0x8400` and is kept. Measured on RDNA2 (gfx1031, Radeon RX 6700 XT, AMD Windows driver 32.0.21045.5002 through HIP) at IEEE=0 and IEEE=1 with denorm16 0..3 over 5075 operand triples of `v_fma_f16` and `v_fmac_f16` (4000 from 20 halves around the denormal range, 75 whose product-sum lies just below 2^-14 and 1000 random), and over the first 4075 of them for `v_fmamk_f16` and `v_fmaak_f16`, FP16_OVFL=1, the clamp, neg and abs modifiers and op_sel. Directed rounding and `v_fma_mixlo/mixhi_f16` keep their previous denormal lowering. Shaders without a known float mode keep the previous lowering. Scalar `v_sub_f16`/`v_subrev_f16` share `float16Binary` with add/mul and were measured on gfx1036 over the same operand set as add (1156 non-NaN-input rows per mode); NaN result bits still depend on the driver as recorded above. - [f16 unary, frexp and conversion denormals](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) follow the `FLOAT_MODE` f16 denormal field: `v_ceil_f16`, `v_floor_f16`, `v_fract_f16`, `v_rcp_f16`, `v_rsq_f16`, `v_sqrt_f16`, `v_log_f16`, `v_exp_f16`, `v_sin_f16`, `v_frexp_mant_f16`, `v_frexp_exp_i16_f16`, `v_cvt_norm_i16_f16`, `v_cvt_norm_u16_f16` and `v_cvt_f32_f16` flush a denormal half input to its signed zero when bit 0 is clear (0 and 2), and the f16-result ones flush a denormal half result to its signed zero when bit 1 is clear (0 and 1), after the host rounds it to half. Measured on RDNA2 (gfx1036) at IEEE=0 and IEEE=1 with denorm16 0..3 over every non-NaN f16 input (63,490 rows per mode and instruction): the only mismatches are the 1-ULP driver differences of `v_exp_f16` (2 inputs) and `v_sin_f16` (14), which are the same with denormals kept. NaN inputs are not compared. `v_ldexp_f16` shares the input rule. `v_cvt_pknorm_i16_f16` and `v_cvt_pknorm_u16_f16` flush both half inputs by the same bit, measured on RDNA2 (gfx1030) over every f16 input (each half also negated in the other source) at IEEE=0 and IEEE=1 with denorm16 0..3 and at f16 rounding 1..3: every row matches. Shaders without a known float mode keep the previous lowering. - [v_div_fixup_f16](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) flushes its src1 and src2 to a zero of the same sign when bit 0 of the `FLOAT_MODE` f16 denormal field is clear (0 and 2) and keeps a denormal src0 and result in every mode, as `v_div_fixup_f32` does with the f32 field. Measured on RDNA2 (gfx1031, Radeon RX 6700 XT, AMD Windows driver 32.0.21045.5002 through HIP) over 6859 triples of 19 halves (zeros, denormals, minimum normals, ±0.5, ±1, the largest finite value, infinities and quiet and signaling NaNs) at IEEE=0 and IEEE=1 with denorm16 0..3 and FP16_OVFL 0 and 1, also with neg and abs source modifiers and with op_sel high halves. Shaders without a known float mode keep the previous lowering. The PS5's gfx1013 is not measured. -- [Packed f16 NaN results](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) (`v_pk_add/mul/fma/min/max_f16`) follow the hardware measured on RDNA2 (gfx1035) at IEEE=0 and IEEE=1 with f16 round-to-nearest-even and f16 denormals kept: with IEEE=1 a signaling NaN input is quieted and min/max return the other operand only for a quiet NaN; with IEEE=0 NaN inputs pass through unquieted and min/max ignore signaling. In `v_pk_fma_f16` an invalid product (inf * 0) gives the default NaN even when src2 is a NaN. A shader without a known float mode is translated as IEEE=0, the mode titles use; a mode with another f16 rounding or that flushes f16 denormals throws. +- [Packed f16 NaN results](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) (`v_pk_add/mul/fma/min/max_f16`) follow the hardware measured on RDNA2 (gfx1035) at IEEE=0 and IEEE=1 with f16 round-to-nearest-even and f16 denormals kept: with IEEE=1 a signaling NaN input is quieted and min/max return the other operand only for a quiet NaN; with IEEE=0 NaN inputs pass through unquieted and min/max ignore signaling. In `v_pk_fma_f16` an invalid product (inf * 0) gives the default NaN even when src2 is a NaN. A shader without a known float mode is translated as IEEE=0, the mode titles use; a mode with another f16 rounding throws. In the f16 denormal modes that flush, each denormal input lane is flushed to a zero of its sign when bit 0 of the field is clear (0 and 2), so `inf * 0x0001` is an invalid product there, and when bit 1 is clear (0 and 1) a `v_pk_add_f16` result below 2^-14, or a `v_pk_mul_f16`, `v_pk_fma_f16` or `v_pk_fmac_f16` result whose exact value rounded to 11 significant bits with an unbounded exponent is below 2^-14, is flushed to a zero of its sign; `v_pk_min/max_f16` flush only their inputs. The native f16x2 add and multiply is not used while results are flushed. Measured on RDNA2 (gfx1031, Radeon RX 6700 XT, AMD Windows driver 32.0.21045.5002 through HIP) at IEEE=0 and IEEE=1 with denorm16 0..3, over 19,192 to 162,204 lanes per instruction with op_sel, op_sel_hi, neg_lo, neg_hi and clamp variants and products and sums at the 2^-14 boundary. - [f32 denormals](../../../core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp) follow the shader's `FLOAT_MODE` f32 denormal field (bits 5:4) when it flushes them (0, and shaders without a known float mode, as in the 0xc0 most titles use), measured on RDNA2 (gfx1035) over 1444 rows per instruction at IEEE=0 and IEEE=1. `v_add/sub/subrev/mul/mul_legacy/mullit/fma/fmac/fmamk/fmaak/min/max/min3/max3/med3/fract/floor/ceil/frexp_mant/sin/cos_f32` flush their f32 inputs and their result to a zero of the same sign (`v_mullit_f32` measured on RDNA2 (gfx1036) over 6907 rows per mode at IEEE=0 and IEEE=1, so a positive denormal src2 counts as zero); the f32 compares, `v_cvt_i32/u32/rpi_i32/flr_i32_f32`, `v_frexp_exp_i32_f32`, `v_cvt_f64_f32`, `v_cvt_f16_f32`, `v_cvt_pkrtz_f16_f32`, `v_cvt_pknorm_i16/u16_f32` and `v_cvt_pk_u8_f32` flush their f32 inputs. `v_div_scale_f32` flushes all three inputs to signed zeros, measured on RDNA2 (gfx1036) over 2744 operand triples per mode at IEEE=0 and IEEE=1 with f32 denormals flushed and kept; its scale-down behaviour and host denormal preservation remain incomplete. `v_div_fixup_f32` flushes its src1 and src2 and keeps a denormal src0 and result, measured on RDNA2 (gfx1036) over 6859 rows per mode at IEEE=0 and IEEE=1. Tininess is detected after rounding, measured on RDNA2 (gfx1036) for `v_mul_f32` and `v_mullit_f32` at IEEE=0 and IEEE=1: a product is flushed when its value rounded to 24 bits with an unbounded exponent is below 2^-126, so `0x3f7ffffe * 0x00800001` (2^-126 * (1 - 2^-46)) gives `0x00800000` and `0x3f7fffff * 0x00800000` (2^-126 * (1 - 2^-24)) gives zero. The check rounds in the f32 round mode of `FLOAT_MODE` (bits 1:0), measured in all four modes, while the products themselves are still rounded to nearest even. This applies to `v_mul_f32`, `v_mul_legacy_f32`, `v_mullit_f32`, and to `v_fma_f32`, `v_fmac_f32`, `v_fmamk_f32`, `v_fmaak_f32` and `v_fma_mix_f32` when the addend is zero; with a nonzero addend the fused result is kept, so an exact result just below 2^-126 that rounds to it is not flushed. In the three directed modes the check is computed exactly from the significands. To nearest even (0, the mode of shaders without a known float mode) one factor is multiplied by 2^64, which is exact below 2^64, and the product with the other factor is compared with 2^-62. That multiply is a plain `OpFMul` like the product, and no f32 rounding mode is requested in the SPIR-V, so the check relies on the driver rounding f32 to nearest even by default, as the products do, and a product that rounds up to 2^-126 is the emitted product. `v_fma_mix_f32` follows this rule with mixed f16/f32 sources too, measured on RDNA2 (gfx1036) with f16 in either multiplicand slot, low and high half selection, and an f32 or f16 zero addend: f32 `0x00801002` times f16 `0x3bff` gives `0x00800000` to nearest at IEEE=0 and IEEE=1, while the neighbours `0x00801001` and `0x00801003` give zero and `0x00800001`. The tiny-product boundary was also measured for both signs in all four f32 round modes at IEEE=0; Its f16 sources are flushed to a zero of their sign when bit 0 of the f16/f64 denormal field is clear (0 and 2), whatever the f32 field, measured on RDNA2 (gfx1031, Radeon RX 6700 XT, AMD Windows driver 32.0.21045.5002 through HIP) at IEEE=0 and IEEE=1 with f32 denormal modes 0 and 3 and f16 denormal modes 0 to 3, over 2744 triples of 14 halves with all three sources f16 and 1960 rows with an f32 multiplicand. `v_fma_mixlo_f16` and `v_fma_mixhi_f16` flush their f32 sources too and keep a product below 2^-126, measured on RDNA2 (gfx1036). `v_mad_legacy_f32` and `v_mac_legacy_f32` flush their inputs, their product and their result in every mode, see its entry; their product is still checked for tininess on its exact value, so one below 2^-126 gives zero even when it rounds to 2^-126, as the console's separately rounded encoding is not measured at this boundary. Denormal modes 1 and 2 throw for the other instructions. Not covered, and not measurable on RDNA2 (no such opcodes) or not measured: `v_mac_f32`, `v_mad_f32`, `v_madak_f32` and `v_madmk_f32` (rounded separately), `v_dot2_f32_f16` and `v_div_fmas_f32` (on hardware `v_div_fmas_f32` flushes only its result), f32 interpolation and `ds_add_f32` keep the previous lowering; `ds_min/max/cmpst_f32` flush both operands by the field, see the LDS float atomics entry. With f32 denormals kept (3) the results depend on the driver's default denormal behaviour, as no `DenormPreserve` execution mode is requested; `v_ldexp_f32` and the f32 compares are the exceptions. `v_cmp_X_f32` and `v_cmpx_X_f32` compare the operands as sign-magnitude integers with the NaN cases handled apart, so a denormal is not read as zero, measured on RDNA2 (gfx1036) over 256 operand pairs per opcode at IEEE=0 and IEEE=1 with f32 denormals kept and flushed. `v_ldexp_f32` preserves subnormal inputs and rounds a subnormal result in the f32 round mode of `FLOAT_MODE`, measured on RDNA2 (gfx1036) over 2163 rows in each of the four round modes at IEEE=0 and IEEE=1. An overflow rounds like any other result (infinity, or the largest finite value when the mode rounds that sign toward zero). With f32 denormals flushed it flushes a subnormal input and result, measured over the same rows in each round mode at IEEE=0 only. A device whose driver splits `Fma` into a multiply and an add flushes a product below 2^-126 before adding a non-zero addend in `v_fma_f32` and `v_fmac_f32`: `0x1fffffff * 0x1fffffff + 0x00800000` gives `0x00800000` instead of the fused `0x00ffffff` on RADV RENOIR (Mesa 26.2.4), whose compiled shader has `v_mad_f32` and no `v_fma_f32`. [F32DenormalFlush](../../../core/libs/prx/libSceAgcDriver/tests/execution/F32DenormalFlush.cpp) accepts that sum on such a device. - [v_rcp, v_rsq, v_sqrt, v_log, v_exp, v_sin and v_cos](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) (f32 and f16) use the driver's approximation for finite results, not the console's: the low bits can differ. Only NaN, invalid, zero and infinite results and the exact sin/cos zeros are chosen on the bits. Measured on RDNA2 (gfx1036) over every f16 input and 32,832 f32 inputs: RADV returns the hardware's bits for every input of rcp, rsq, sqrt and log and all but one of exp (f16 0x11c5, 1 ULP); NVIDIA (615.71.09) stays within 2 ULP except `v_log_f32` near 1.0 (next entry). [v_sin and v_cos](../../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) take the driver's sine or cosine of the radians of the exact cycle, except within 2^-12 of a turn from a zero of the function, where the result is the linear term of the exact distance to the zero: the hardware's constant 6.2831826 (0x40c90fd5) at the integers, which the hardware applies exactly to inputs below 2^-12 of a turn, and the f32 2π at the half and quarter turns, where the hardware is within 7 ULP of it. Outside those bands, a driver result more than 4 ULP from a Taylor series in turns of the exact cycle is replaced by the series, which is within 2 ULP of the true sine and cosine; the hardware is up to 9 ULP from them (gfx1035, 243,092 f32 inputs), so results stay within 13 ULP of the hardware on any driver. RADV returns the hardware's bits for every f16 input and for 32,422 sine and 32,486 cosine results of the 32,832 f32 inputs; the rest are 1 ULP off, except up to 14 ULP on either side of non-zero integers and up to 15 ULP beyond ±256 cycles, where the hardware's own reduction differs from the exact fraction, and up to 6 ULP inside the half- and quarter-turn bands. NVIDIA's own sine is within 1 ULP on 93% of the f32 inputs and up to 1,166 ULP off just outside the bands, its cosine within 163 ULP; the series replaces those results. A denormal f32 input with f32 denormals kept gives the hardware the input times 6.2831826; both hosts flush it. - [v_ldexp_f32 in denormal modes 1 and 2](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) follows the `FLOAT_MODE` f32 denormal field: mode 1 keeps a subnormal input and flushes a result below 2^-126 to a zero of its sign in every f32 round mode, also one that rounds to 2^-126 at subnormal precision (`0x3fffffff` scaled by 2^-127); mode 2 flushes a subnormal input to a zero of its sign and rounds a subnormal result in the f32 round mode of `FLOAT_MODE`, as mode 3 does. Measured on RDNA2 (gfx1030, Radeon RX 6800, AMD Windows driver 32.0.21045.5002 through HIP) over 10,260 rows (54 inputs of both signs: zeros, subnormals, normals near 2^-126 and 1.0, the largest finite value, infinities and NaNs, each scaled by 190 exponents from -2^31 to 2^31-1) at IEEE=0 and IEEE=1 with each f32 denormal mode 0 to 3 and f32 round mode 0 to 3, DX10_CLAMP=1, f16 denormals kept, f16 round to nearest even and FP16_OVFL=0; with the `clamp` modifier the result is clamped as in the other modes. The PS5's gfx1013 is not measured. From f91e53d595c15fb2b5e24716e99226c28b12d34d Mon Sep 17 00:00:00 2001 From: inmert <54280820+inmert@users.noreply.github.com> Date: Sat, 17 Oct 2026 14:04:51 +0530 Subject: [PATCH 0670/1000] fix(relinker): list libc.prx with libkernel.prx in macOS executables --- .../elfpatcher/src/macos/MacOsMachOPatcher.cpp | 6 +++--- core/relinker/relinker/tests/macos/import_fixture.py | 6 +++--- core/relinker/relinker/tests/test_macos_output.py | 12 ++++++++++++ docs/dev/debt/relinker.md | 1 - 4 files changed, 18 insertions(+), 7 deletions(-) diff --git a/core/relinker/elfpatcher/src/macos/MacOsMachOPatcher.cpp b/core/relinker/elfpatcher/src/macos/MacOsMachOPatcher.cpp index 0b36789a41..f15b546181 100644 --- a/core/relinker/elfpatcher/src/macos/MacOsMachOPatcher.cpp +++ b/core/relinker/elfpatcher/src/macos/MacOsMachOPatcher.cpp @@ -41,7 +41,6 @@ std::vector ReadNeededLibraries(const Domain::SysVDynamicSection& d if (name.empty()) throw Domain::RelinkerException("Invalid DT_NEEDED library: " + name); if (unique.insert(name).second) result.push_back(std::move(name)); } - if (unique.contains("libSceLibcInternal.prx") && !unique.contains("libc.prx")) result.push_back("libc.prx"); return result; } @@ -71,8 +70,9 @@ std::vector MacOsMachOPatcher::Patch(std::vector sou input.Binds.push_back({"_" + import.Name, import.TargetRva, static_cast(import.Addend), import.Weak}); for (const auto& library : ReadNeededLibraries(dynamicSection)) input.Dylibs.push_back(MachODependency(library, "@executable_path")); - if (std::find(input.Dylibs.begin(), input.Dylibs.end(), "@rpath/libkernel.prx") == input.Dylibs.end()) - input.Dylibs.push_back("@rpath/libkernel.prx"); + for (const auto* library : {"@rpath/libkernel.prx", "@rpath/libc.prx"}) + if (std::find(input.Dylibs.begin(), input.Dylibs.end(), library) == input.Dylibs.end()) + input.Dylibs.push_back(library); input.RunPaths = MachORunPaths(runPath, "@executable_path"); input.EntryRva = image.GetEntryRva(); input.Trampolines = &trampolines; diff --git a/core/relinker/relinker/tests/macos/import_fixture.py b/core/relinker/relinker/tests/macos/import_fixture.py index 5e3c0a4914..7746eda98c 100644 --- a/core/relinker/relinker/tests/macos/import_fixture.py +++ b/core/relinker/relinker/tests/macos/import_fixture.py @@ -1,9 +1,9 @@ -"""A minimal PS5-style ELF that imports puts and exit from libc.prx by NID.""" +"""A minimal PS5-style ELF that imports puts and exit by NID from one needed library, libc.prx by default.""" import struct, sys sys.path.insert(0, __import__("os").path.dirname(__import__("os").path.abspath(__file__))) from nid import nid -def fixture(message=b"hello from a PS5 ELF on macOS\0"): +def fixture(message=b"hello from a PS5 ELF on macOS\0", library=b"libc.prx"): image = bytearray(0x6000) image[:16] = b"\x7fELF\x02\x01\x01" + bytes(9) struct.pack_into(" Date: Sat, 17 Oct 2026 11:13:41 +0200 Subject: [PATCH 0671/1000] fix(main): retry temp dir cleanup in loadexec_arguments on Windows Run 39019245350 (main f05fc7d953) failed loadexec_arguments with WinError 5 on unlinking guest_loadexec_host.prx in TemporaryDirectory cleanup: the exited guest process still held the module briefly. #7038 does not touch it (0 of 25 earlier main builds failed). Retry the removal for up to 10 s. Co-Authored-By: Claude --- .../relinker/tests/test_loadexec_arguments.py | 109 ++++++++++-------- 1 file changed, 62 insertions(+), 47 deletions(-) diff --git a/core/relinker/relinker/tests/test_loadexec_arguments.py b/core/relinker/relinker/tests/test_loadexec_arguments.py index facbd24db5..40cca05df1 100644 --- a/core/relinker/relinker/tests/test_loadexec_arguments.py +++ b/core/relinker/relinker/tests/test_loadexec_arguments.py @@ -5,6 +5,7 @@ import struct import subprocess import sys import tempfile +import time from test_windows_import_modules import executable @@ -30,6 +31,17 @@ def fixture(symbol): return image +def remove_tree(path): + for attempt in range(50): + try: + shutil.rmtree(path) + return + except PermissionError: + if attempt == 49: + raise + time.sleep(0.2) + + def main(): relinker = Path(sys.argv[1]).resolve() host = Path(sys.argv[2]).resolve() @@ -39,53 +51,56 @@ def main(): if not windows and sys.platform != 'linux': print('LoadExec arguments: unsupported host skipped') return - with tempfile.TemporaryDirectory(prefix='anyps5-loadexec-') as directory: - work = Path(directory) / 'Документы 東京' - title = work / 'title' - output = work / 'native output' - (title / 'picker').mkdir(parents=True) - output.mkdir() - (title / 'sce_module').mkdir() - (title / 'eboot.bin').write_bytes(fixture('LoadExecFirst_nid_no_patch')) - (title / 'picker' / 'second.bin').write_bytes(fixture('LoadExecSecond_nid_no_patch')) - (title / 'resource.txt').write_text('resource survives\n') - app = output / ('app.exe' if windows else 'app.elf') - command = [str(relinker), 'unused-filter=0', '--executable', 'picker/second.bin'] - if windows: - command += ['--windows'] - run = subprocess.run(command + [str(title / 'eboot.bin'), str(app)], capture_output=True, timeout=10) - assert run.returncode == 0, (run.stdout, run.stderr) - second = output / ('picker.second.bin.exe' if windows else 'picker.second.bin.elf') - assert second.is_file(), list(output.iterdir()) - shutil.copytree(title, output / 'app0') - (output / 'app0' / 'working').mkdir() - libraries = output / 'libs' - libraries.mkdir() - for library in [host, *dependencies]: - shutil.copy2(library, libraries / library.name) - if windows: - for name in ('libgcc_s_seh-1.dll', 'libstdc++-6.dll', 'libwinpthread-1.dll'): - shutil.copy2(toolchain / name, libraries / name) - else: - app.chmod(0o755) - second.chmod(0o755) - environment = {**os.environ, 'ANYPS5_LOADEXEC_TEST_ROOT': str(output), - 'LD_LIBRARY_PATH': str(libraries), 'PATH': str(libraries) + os.pathsep + os.environ['PATH']} - for quoted in (False, True): - events_path = output / 'events.txt' - events_path.unlink(missing_ok=True) - (output / 'pids.txt').unlink(missing_ok=True) - selected = dict(environment) - if quoted: - selected['ANYPS5_TEST_QUOTED_ARGV0'] = '1' - run = subprocess.run([str(app)], cwd=output, env=selected, capture_output=True, timeout=10) - events = (output / 'events.txt').read_text() if (output / 'events.txt').exists() else '' - assert events == 'first\nshutdown\nsecond\nshutdown\nreturned\n', (run.returncode, events, run.stdout, run.stderr) - assert run.returncode == (0 if windows else 37), (run.returncode, events, run.stdout, run.stderr) - pids = (output / 'pids.txt').read_text().splitlines() - assert len(pids) == 3 and len(set(pids)) == (3 if windows else 1), pids - print('LoadExec arguments: first -> second -> first; entry/getargv arguments, Unicode, cwd/resources, ' - 'rejected targets, shutdown without guest or host exit handlers passed') + directory = tempfile.mkdtemp(prefix='anyps5-loadexec-') + try: + work = Path(directory) / 'Документы 東京' + title = work / 'title' + output = work / 'native output' + (title / 'picker').mkdir(parents=True) + output.mkdir() + (title / 'sce_module').mkdir() + (title / 'eboot.bin').write_bytes(fixture('LoadExecFirst_nid_no_patch')) + (title / 'picker' / 'second.bin').write_bytes(fixture('LoadExecSecond_nid_no_patch')) + (title / 'resource.txt').write_text('resource survives\n') + app = output / ('app.exe' if windows else 'app.elf') + command = [str(relinker), 'unused-filter=0', '--executable', 'picker/second.bin'] + if windows: + command += ['--windows'] + run = subprocess.run(command + [str(title / 'eboot.bin'), str(app)], capture_output=True, timeout=10) + assert run.returncode == 0, (run.stdout, run.stderr) + second = output / ('picker.second.bin.exe' if windows else 'picker.second.bin.elf') + assert second.is_file(), list(output.iterdir()) + shutil.copytree(title, output / 'app0') + (output / 'app0' / 'working').mkdir() + libraries = output / 'libs' + libraries.mkdir() + for library in [host, *dependencies]: + shutil.copy2(library, libraries / library.name) + if windows: + for name in ('libgcc_s_seh-1.dll', 'libstdc++-6.dll', 'libwinpthread-1.dll'): + shutil.copy2(toolchain / name, libraries / name) + else: + app.chmod(0o755) + second.chmod(0o755) + environment = {**os.environ, 'ANYPS5_LOADEXEC_TEST_ROOT': str(output), + 'LD_LIBRARY_PATH': str(libraries), 'PATH': str(libraries) + os.pathsep + os.environ['PATH']} + for quoted in (False, True): + events_path = output / 'events.txt' + events_path.unlink(missing_ok=True) + (output / 'pids.txt').unlink(missing_ok=True) + selected = dict(environment) + if quoted: + selected['ANYPS5_TEST_QUOTED_ARGV0'] = '1' + run = subprocess.run([str(app)], cwd=output, env=selected, capture_output=True, timeout=10) + events = (output / 'events.txt').read_text() if (output / 'events.txt').exists() else '' + assert events == 'first\nshutdown\nsecond\nshutdown\nreturned\n', (run.returncode, events, run.stdout, run.stderr) + assert run.returncode == (0 if windows else 37), (run.returncode, events, run.stdout, run.stderr) + pids = (output / 'pids.txt').read_text().splitlines() + assert len(pids) == 3 and len(set(pids)) == (3 if windows else 1), pids + print('LoadExec arguments: first -> second -> first; entry/getargv arguments, Unicode, cwd/resources, ' + 'rejected targets, shutdown without guest or host exit handlers passed') + finally: + remove_tree(directory) if __name__ == '__main__': From 9b29e696c12cecf8879d6a985582dde316e0d362 Mon Sep 17 00:00:00 2001 From: Azaluz <68714936+Azaluz@users.noreply.github.com> Date: Sat, 17 Oct 2026 12:13:07 +0300 Subject: [PATCH 0672/1000] fix(agc): keep nonbinding import probes from pinning buffers --- .../Graphics/src/GuestBufferMemory.cpp | 23 +-- .../libSceAgcDriver/tests/BdaResources.cpp | 133 ++++++++++++++++++ 2 files changed, 147 insertions(+), 9 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index 3af6b42bd8..1f7e3407ec 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -1025,6 +1025,7 @@ ImportRun windowRun(const HostImports& state, ImportRun run, std::uint64_t begin return run; } +const HostImport* peekImport(const HostImports& state, std::uint64_t begin, std::uint64_t end); const HostImport* findImport(HostImports& state, std::uint64_t begin, std::uint64_t end); bool importsStale(const Context& context, const HostImports& state); const HostImport* importRunAt(const Context& context, HostImports& state, ImportRun run, bool joined, std::uint64_t address, const GuestAllocations::Lease& lease, std::uint64_t end, bool evict); @@ -1057,8 +1058,8 @@ const HostImport* importAllocation(const Context& context, HostImports& state, s } const HostImport* importJoined(const Context& context, HostImports& state, std::uint64_t begin, std::uint64_t end, const GuestAllocations::Lease& lease) { - if (const auto* low = findImport(state, begin, begin + 1); low != nullptr && low->joined) begin = low->base; - if (const auto* high = findImport(state, end - 1, end); high != nullptr && high->joined) end = high->base + high->bytes; + if (const auto* low = peekImport(state, begin, begin + 1); low != nullptr && low->joined) begin = low->base; + if (const auto* high = peekImport(state, end - 1, end); high != nullptr && high->joined) end = high->base + high->bytes; auto run = joinedRun(lease, begin, end); if (run.ranges.empty()) return nullptr; if (const auto failed = state.failedJoins.find(run.base); failed != state.failedJoins.end() && sameRun(failed->second, run)) return nullptr; @@ -1308,14 +1309,18 @@ void refreshImports(const Context& context, HostImports& state, const GuestAlloc for (auto it = state.failedJoins.begin(); it != state.failedJoins.end();) it = sameRun(it->second, joinedRun(lease, it->second.base, it->second.base + it->second.bytes)) ? std::next(it) : state.failedJoins.erase(it); } -const HostImport* findImport(HostImports& state, std::uint64_t begin, std::uint64_t end) { +const HostImport* peekImport(const HostImports& state, std::uint64_t begin, std::uint64_t end) { auto found = state.imports.upper_bound(begin); if (found == state.imports.begin()) return nullptr; --found; const auto& entry = found->second; - if (begin < entry.base || end > entry.base + entry.bytes) return nullptr; - entry.lastUse = ++state.useCounter; - return &entry; + return begin < entry.base || end > entry.base + entry.bytes ? nullptr : &entry; +} + +const HostImport* findImport(HostImports& state, std::uint64_t begin, std::uint64_t end) { + const auto* entry = peekImport(state, begin, end); + if (entry != nullptr) entry->lastUse = ++state.useCounter; + return entry; } const HostImport* importAccess(const Context& context, HostImports& state, std::uint64_t begin, std::uint64_t end, const GuestAllocations::Lease& lease) { @@ -2680,7 +2685,7 @@ bool HostImportCovers(const Context& context, std::uint64_t address, std::size_t std::lock_guard lock(state.mutex); // Imports of a replaced device are not this device's; a stale registry only means an import // may be dropped at the next reconcile, which the binding path handles. - return state.device == context.device && findImport(state, address, address + bytes) != nullptr; + return state.device == context.device && peekImport(state, address, address + bytes) != nullptr; } GuestBufferMemory::GuestBufferMemory(const Context& context) : context(context) {} @@ -3743,7 +3748,7 @@ void GuestBufferMemory::UploadFinish(bool addressable) { const auto& lease = importRanges(); const auto index = leasedIndexContaining(lease, region.begin); if (entry == nullptr && index != lease.size() && lease[index]->address == region.begin) { - const auto* low = findImport(state, region.begin, region.begin + 1); + const auto* low = peekImport(state, region.begin, region.begin + 1); const auto joinBase = low != nullptr && low->joined ? low->base : region.begin; const auto joins = state.sparseRunJoins.find(joinBase); const auto made = joins == state.sparseRunJoins.end() ? 0u : joins->second; @@ -3851,7 +3856,7 @@ void GuestBufferMemory::copyRegion(Region& region, bool addressable) { if (profile && context.hostImportAlignment != 0) { auto& state = Imports(); std::lock_guard lock(state.mutex); - inImport = findImport(state, region.begin, region.end) != nullptr; + inImport = peekImport(state, region.begin, region.end) != nullptr; } const auto allocateStart = std::chrono::steady_clock::now(); if (region.sparse && region.buffer == nullptr && (region.hostBacked || region.writable)) { diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp index 98aaf121d1..dd9ef7ac25 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp @@ -550,6 +550,137 @@ void importJoinTests(const Context& context) { ::operator delete(block, std::align_val_t{window}); } +class ImportBudgetRanges { +public: + static constexpr std::size_t piece = 65536; + static constexpr std::size_t bytes = 3 * piece; + + explicit ImportBudgetRanges(const Context& context) : importing(context) { + block = static_cast(::operator new(4 * bytes, std::align_val_t{piece})); + importing.hostImportAlignment = piece; + ClearHostImports(importing.device); + SetHostImportWindow(2 * piece); + SetHostImportRangeWindow(0); + SetHostImportBudget(2 * bytes); + GuestAllocations::Mutation mutation; + for (std::size_t index = 0; index < 4; ++index) { + std::memset(block + index * bytes, static_cast(0x31 + index), bytes); + mutation.Add(block + index * bytes, bytes, true, false, true); + } + } + + ~ImportBudgetRanges() { + ClearHostImports(importing.device); + { + GuestAllocations::Mutation mutation; + for (std::size_t index = 0; index < 4; ++index) mutation.Remove(block + index * bytes); + } + SetHostImportBudget(0); + SetHostImportRangeWindow(std::uint64_t{64} << 20u); + SetHostImportWindow(0); + ::operator delete(block, std::align_val_t{piece}); + } + + ImportBudgetRanges(const ImportBudgetRanges&) = delete; + ImportBudgetRanges& operator=(const ImportBudgetRanges&) = delete; + + std::uintptr_t Address(std::size_t index) const { return reinterpret_cast(block + index * bytes); } + + void CheckUnchanged() const { + for (std::size_t index = 0; index < 4; ++index) { + const auto expected = static_cast(0x31 + index); + Require(std::all_of(block + index * bytes, block + (index + 1) * bytes, [expected](std::byte value) { return value == expected; }), "import budget probes changed guest bytes"); + } + } + + Context importing; + +private: + std::byte* block = nullptr; +}; + +void importCoverageHintBudgetTests(const Context& context, const BdaTestAccess& access) { + for (const bool acquired : {true, false}) { + ImportBudgetRanges ranges(context); + const auto a = ranges.Address(0); + const auto c = ranges.Address(2); + const auto d = ranges.Address(3); + const auto* first = HostImportFor(ranges.importing, a, 16); + Require(first != nullptr && first->base == a && first->bytes == ranges.bytes, "coverage budget fixture did not import its first range"); + const auto firstBuffer = first->buffer; + const auto firstSerial = HostImportSerial(ranges.importing, a, 16, false); + for (int use = 0; use < 300; ++use) Require(HostImportFor(ranges.importing, d, 16) != nullptr, "coverage budget fixture did not import its hot range"); + const auto hotSerial = HostImportSerial(ranges.importing, d, 16, false); + Require(firstSerial != 0 && hotSerial != 0 && HostImportLiveBytes() == 2 * ranges.bytes, "coverage budget fixture has unexpected import ownership or usage"); + { + GuestBufferMemory memory(ranges.importing); + memory.AddReadable(c, 32); + memory.UploadPrepare(false); + if (acquired) { + const auto* held = HostImportFor(ranges.importing, a, 16); + Require(held != nullptr && held->buffer == firstBuffer, "a genuine import acquisition replaced the retained range"); + } else { + Require(HostImportCovers(ranges.importing, a, 16), "a coverage hint missed the existing range"); + } + memory.UploadFinish(false); + std::uint32_t adjustment = 0; + const auto view = memory.Descriptor(c, 32, adjustment); + Require(view.range == 32 + adjustment, "a disjoint descriptor has an incorrect extent"); + Require(HostImportSerial(ranges.importing, d, 16, false) == hotSerial && HostImportLiveBytes() == 2 * ranges.bytes, "a coverage probe displaced the hot import or changed the budget"); + if (acquired) { + Require(view.buffer != firstBuffer, "a disjoint descriptor uses the retained range's import"); + Require(HostImportSerial(ranges.importing, a, 16, false) == firstSerial && HostImportSerial(ranges.importing, c, 16, false) == 0, "a genuine import acquisition did not protect its buffer from eviction"); + Require(memory.CopiedBytes() == 32, "a disjoint descriptor at the budget was not copied while an import was held"); + const auto copied = access.bytes(view.buffer); + const auto offset = static_cast(view.offset + adjustment); + Require(offset + 32 <= copied.size() && std::memcmp(copied.data() + offset, reinterpret_cast(c), 32) == 0, "the descriptor copied behind a held import lost its guest bytes"); + } else { + const auto importedSerial = HostImportSerial(ranges.importing, c, 16, false); + Require(HostImportSerial(ranges.importing, a, 16, false) == 0 && importedSerial != 0 && importedSerial != firstSerial, "a coverage hint prevented eviction of an unused import"); + const auto* imported = ExistingHostImportFor(ranges.importing, c, 32); + Require(imported != nullptr && imported->base == c && imported->bytes == ranges.bytes && view.buffer == imported->buffer && view.offset + adjustment == c - imported->base, "a descriptor does not bind the import obtained after a coverage hint"); + Require(memory.CopiedBytes() == 0, "a coverage hint forced the disjoint imported descriptor to copy"); + } + memory.WriteBack(); + } + ranges.CheckUnchanged(); + } +} + +void failedImportJoinProbeBudgetTests(const Context& context, const BdaTestAccess& access) { + ImportBudgetRanges ranges(context); + const auto a = ranges.Address(0); + const auto crossing = ranges.Address(1) - 16; + const auto c = ranges.Address(2); + const auto d = ranges.Address(3); + const auto* first = HostImportFor(ranges.importing, a, 16); + Require(first != nullptr && first->base == a && first->bytes == ranges.bytes, "failed join budget fixture did not import its first range"); + const auto firstSerial = HostImportSerial(ranges.importing, a, 16, false); + for (int use = 0; use < 300; ++use) Require(HostImportFor(ranges.importing, d, 16) != nullptr, "failed join budget fixture did not import its hot range"); + const auto hotSerial = HostImportSerial(ranges.importing, d, 16, false); + Require(firstSerial != 0 && hotSerial != 0 && HostImportLiveBytes() == 2 * ranges.bytes, "failed join budget fixture has unexpected import ownership or usage"); + { + GuestBufferMemory memory(ranges.importing); + memory.AddReadable(crossing, 32); + memory.AddReadable(c, 32); + memory.Upload(false); + std::uint32_t adjustment = 0; + const auto copiedView = memory.Descriptor(crossing, 32, adjustment); + const auto copied = access.bytes(copiedView.buffer); + const auto offset = static_cast(copiedView.offset + adjustment); + Require(copiedView.range == 32 + adjustment && offset + 32 <= copied.size() && std::memcmp(copied.data() + offset, reinterpret_cast(crossing), 32) == 0, "the refused joined descriptor lost either side's guest bytes"); + Require(HostImportSerial(ranges.importing, crossing, 32, false) == 0, "a join over the available budget unexpectedly produced an import"); + const auto importedSerial = HostImportSerial(ranges.importing, c, 16, false); + Require(HostImportSerial(ranges.importing, a, 16, false) == 0 && importedSerial != 0 && importedSerial != firstSerial, "a failed join endpoint probe prevented eviction of an unused import"); + const auto* imported = ExistingHostImportFor(ranges.importing, c, 32); + const auto view = memory.Descriptor(c, 32, adjustment); + Require(imported != nullptr && imported->base == c && imported->bytes == ranges.bytes && view.buffer == imported->buffer && view.offset + adjustment == c - imported->base && view.range == 32 + adjustment, "the descriptor after a failed join does not bind its complete import"); + Require(memory.CopiedBytes() == 32 && HostImportLiveBytes() == 2 * ranges.bytes && HostImportSerial(ranges.importing, d, 16, false) == hotSerial, "a failed join forced a second copy or displaced the hot import"); + memory.WriteBack(); + } + ranges.CheckUnchanged(); +} + // One registered range of `cells` import windows of `window` bytes, with the window and the import budget set for the // test and reset after it. class WindowedRange { @@ -1934,6 +2065,8 @@ void RunBdaResourceTests(const Context& context, const BdaTestAccess& access) { spaceWriteMergedPastImportTests(context, access); spaceReadOverWritableImportTableTests(context); importJoinTests(context); + importCoverageHintBudgetTests(context, access); + failedImportJoinProbeBudgetTests(context, access); importWholeRangeOverWindowTests(context); importRefusedWholeRangeTests(context); importNeighbouringEdgesTests(context); From a58a709116c54e004dd231e6c12109bdcf9ead64 Mon Sep 17 00:00:00 2001 From: AlxcBTW <187763517+AlxcBTW@users.noreply.github.com> Date: Sat, 17 Oct 2026 10:49:23 +0200 Subject: [PATCH 0673/1000] fix(agc): compare moved dispatch buffers at their build-time clamped size A read-only V# the build clamps (size above the descriptor range limit cut to the mapped end, stride overhang cut to the allocation end) was compared with the template by its raw size, so an unchanged buffer counted as moved, was refused over the limit and the dispatch rebuilt every time; a moved one with an overhang was snapshotted past the allocation end. The build and the move proof now take the range from one function (ShaderResources::guestBufferBytes), compare clamped with clamped and snapshot the clamped range. An overlay without a descriptor set is refused like one that could not be made, and recording one throws instead of binding the template's set. --- .../Execution/src/VulkanDevice.cpp | 4 +- .../Graphics/include/ShaderResources.hpp | 1 + .../Graphics/src/ShaderResources.cpp | 38 ++++++---- .../prx/libSceAgcDriver/tests/Graphics.cpp | 69 ++++++++++++++++++- 4 files changed, 95 insertions(+), 17 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 9bf731eaf1..895a8a6cdc 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -4560,7 +4560,8 @@ void VulkanDevice::recordDispatch(RecordedDispatch& record) { ++d.preBarriersRecorded; } context.Resolved(&Graphics::DeviceFunctions::cmdBindPipeline, "vkCmdBindPipeline")(commands, VK_PIPELINE_BIND_POINT_COMPUTE, record.objects->pipeline); - if (record.bindings != nullptr && record.bindings->allocation.set != VK_NULL_HANDLE) context.Resolved(&Graphics::DeviceFunctions::cmdBindDescriptorSets, "vkCmdBindDescriptorSets")(commands, VK_PIPELINE_BIND_POINT_COMPUTE, record.objects->layout, 0, 1, &record.bindings->allocation.set, 0, nullptr); + if (record.bindings != nullptr) Graphics::Require(record.bindings->allocation.set != VK_NULL_HANDLE, "dispatch overlay has no descriptor set"); + if (record.bindings != nullptr) context.Resolved(&Graphics::DeviceFunctions::cmdBindDescriptorSets, "vkCmdBindDescriptorSets")(commands, VK_PIPELINE_BIND_POINT_COMPUTE, record.objects->layout, 0, 1, &record.bindings->allocation.set, 0, nullptr); else resources.Bind(commands, VK_PIPELINE_BIND_POINT_COMPUTE, record.objects->layout); if (const auto dwords = record.shader->program->workgroupMemoryDwords; dwords != 0) { auto memory = state->WorkgroupMemoryFor(context, record.x, record.y, record.z, dwords); @@ -4862,6 +4863,7 @@ VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::Rec moves.checked.fetch_add(1, std::memory_order_relaxed); if (moved.has_value() && !moved->empty()) { overlay = cached->PrepareDrawBindings(*state->recorder, *moved, true); + if (overlay != nullptr && overlay->allocation.set == VK_NULL_HANDLE) overlay.reset(); if (overlay == nullptr) { proof.refusal = Graphics::ShaderResources::MoveRefusal::Bindings; moved.reset(); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp index ef87148b53..de985f99ca 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp @@ -382,6 +382,7 @@ private: // same way but is left out of the write set (no write-back, no pending-write note). `atomic` // is its bufferAtomic (see GuestBufferMemory::AddWritable). std::size_t addGuestBuffer(std::span words, const ColorTarget* target, std::uint64_t indexAddress, std::size_t indexBytes, bool written, bool atomic, bool read); + std::uint64_t guestBufferBytes(std::span words) const; std::size_t addDataBuffer(std::span words); // Stage A: the layout entry of an image binding (samplers are taken at once, the sampler cache // locks itself); stage B looks the sampled textures and storage images up (resolveImageBinding). diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index f479b74778..c4556e3b7e 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -3554,11 +3554,8 @@ DescriptorCache::Stats DescriptorCache::Counters() const { return stats; } -std::size_t ShaderResources::addGuestBuffer(std::span words, const ColorTarget* target, std::uint64_t indexAddress, std::size_t indexBytes, bool written, bool atomic, bool read) { - Require(words.size() == 4, "buffer descriptor must contain four DWORDs"); - Require((words[1] & 0x40000000u) == 0, "buffer descriptor has reserved bits set"); +std::uint64_t ShaderResources::guestBufferBytes(std::span words) const { const ShaderRecompiler::ShaderBufferResource descriptor{{words[0], words[1], words[2], words[3]}}; - Require(descriptor.Type() == 0u, "buffer descriptor uses an unsupported type"); const auto address = descriptor.Base48(); auto byteSize = descriptor.GetSize(); if (byteSize > context.limits.maxStorageBufferRange) { @@ -3566,22 +3563,33 @@ std::size_t ShaderResources::addGuestBuffer(std::span words if (mappedEnd <= address) byteSize = 0; else if (mappedEnd - address < context.limits.maxStorageBufferRange) byteSize = mappedEnd - address; } + if (byteSize == 0 || address == 0) return 0; + if (const std::uint64_t stride = descriptor.Stride(); stride != 0 && byteSize <= context.limits.maxStorageBufferRange) { + constexpr std::uint64_t GuestPageBytes = 0x4000; + const auto end = address + byteSize; + const auto overhang = end % GuestPageBytes; + if (overhang != 0 && overhang < stride) { + const auto allocationEnd = RegisteredReadableEnd(address); + if (allocationEnd > address && allocationEnd < end && end - allocationEnd < stride) byteSize = allocationEnd - address; + } + } + return byteSize; +} + +std::size_t ShaderResources::addGuestBuffer(std::span words, const ColorTarget* target, std::uint64_t indexAddress, std::size_t indexBytes, bool written, bool atomic, bool read) { + Require(words.size() == 4, "buffer descriptor must contain four DWORDs"); + Require((words[1] & 0x40000000u) == 0, "buffer descriptor has reserved bits set"); + const ShaderRecompiler::ShaderBufferResource descriptor{{words[0], words[1], words[2], words[3]}}; + Require(descriptor.Type() == 0u, "buffer descriptor uses an unsupported type"); + const auto address = descriptor.Base48(); + const auto byteSize = guestBufferBytes(words); if (byteSize == 0 || address == 0) { allocations.push_back({0, EmptyBufferBytes, false, nullptr, ShaderRecompiler::DescriptorRole::GuestBuffers, false}); return allocations.size() - 1; } Require(byteSize <= context.limits.maxStorageBufferRange, "shader buffer exceeds descriptor range limit"); Require(byteSize <= std::numeric_limits::max(), "shader buffer size exceeds host address space"); - auto size = static_cast(byteSize); - if (const std::uint64_t stride = descriptor.Stride(); stride != 0) { - constexpr std::uint64_t GuestPageBytes = 0x4000; - const auto end = address + size; - const auto overhang = end % GuestPageBytes; - if (overhang != 0 && overhang < stride) { - const auto allocationEnd = RegisteredReadableEnd(address); - if (allocationEnd > address && allocationEnd < end && end - allocationEnd < stride) size = static_cast(allocationEnd - address); - } - } + const auto size = static_cast(byteSize); Require(target == nullptr || !overlap(address, size, target->address, target->bytes), "shader buffer aliases the render target"); // APS5_ALL_BUFFERS_WRITTEN=1: every element is noted as written, as before bufferWritten existed. static const bool allWritten = std::getenv("APS5_ALL_BUFFERS_WRITTEN") != nullptr; @@ -4560,7 +4568,7 @@ std::optional> ShaderResources::MovedD const auto* words = binding.guestDescriptor.data() + static_cast(element) * 4u; const ShaderRecompiler::ShaderBufferResource descriptor{{words[0], words[1], words[2], words[3]}}; const auto address = descriptor.Base48(); - const auto size = descriptor.GetSize(); + const auto size = guestBufferBytes(std::span(words, 4)); const auto& item = allocations[kept->allocations[element]]; const bool empty = size == 0 || address == 0; if (!item.guest) { diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 59df612761..a7c947787f 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -4373,7 +4373,7 @@ void dispatchMoveTests() { auto written = relocated; written.bindings[0].bufferWritten[1] = true; refuses(written, 0, Refusal::Written, "a moved buffer the dispatch writes was served"); - refuses(moveTo(1, guestMovedSecond.data(), 20000), 0, Refusal::Limit, "a moved buffer over the storage buffer range was served"); + refuses(moveTo(1, guestMovedSecond.data(), 20000), 0, Refusal::Written, "a moved buffer over the storage buffer range that no registered allocation holds was served"); refuses(relocated, 16, Refusal::Limit, "a moved buffer over the byte budget was served"); refuses(moveTo(1, reinterpret_cast(std::uintptr_t{0x10}), 24), 0, Refusal::Inaccessible, "a moved buffer over unmapped memory was served"); refuses(moveTo(1, guestThird.data(), 8), 0, Refusal::Overlap, "a moved buffer over the dispatch's own written range was served"); @@ -4382,6 +4382,73 @@ void dispatchMoveTests() { refuses(shaped, 0, Refusal::Shape, "a dispatch with another binding shape was served"); recorder.Sync(); } + { + constexpr std::size_t blockBytes = 65536; + void* block = ::operator new(blockBytes, std::align_val_t{blockBytes}); + std::memset(block, 0x5a, blockBytes); + { + auto* mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix(); + GuestAllocations::GuestAllocationsAdd_nid_postfix(mutation, block, blockBytes, true, true, true); + GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation); + } + const auto* end = static_cast(block) + blockBytes; + const auto strided = [](const void* pointer, std::uint32_t stride, std::uint32_t records) { + auto words = vsharp(pointer, records); + words[1] |= stride << 16u; + return words; + }; + DescriptorCache cache(context); + context.descriptorCache = &cache; + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + Recorder recorder(context); + const auto proveAgainst = [&](const ShaderRecompiler::RecompileResult& built, const std::vector& incoming, ShaderResources::MoveProof& proof, std::optional>& moved) { + const CompiledShader shader{ShaderRecompiler::ShaderStage::Compute, &built, 0}; + ShaderResources resources(context, shader); + auto program = built; + program.bindings[0].guestDescriptor = incoming; + moved = resources.MovedDispatchBuffers(CompiledShader{ShaderRecompiler::ShaderStage::Compute, &program, 0}, &recorder, 0, proof); + }; + const auto readOnly = [&](const std::vector& words) { + ShaderRecompiler::RecompileResult built; + auto binding = makeBinding(Role::GuestBuffers, 0, 1, words); + binding.bufferWritten = {false}; + built.bindings.push_back(binding); + return built; + }; + const auto address = [](const void* pointer) { return reinterpret_cast(pointer); }; + ShaderResources::MoveProof proof; + std::optional> moved; + + const auto* tail = end - 8192; + const auto* nearEnd = end - 4096; + const auto oversized = readOnly(vsharp(tail, 0xffffffffu)); + proveAgainst(oversized, vsharp(tail, 0xffffffffu), proof, moved); + Require(moved.has_value() && moved->empty() && proof.refusal == Refusal::None && proof.buffers == 0, "an unchanged V# the build cut to its allocation end was not an exact hit"); + proveAgainst(oversized, vsharp(nearEnd, 0xffffffffu), proof, moved); + Require(moved.has_value() && moved->size() == 1 && proof.buffers == 1 && proof.bytes == 4096 && moved->front().address == address(nearEnd) && moved->front().size == 4096, "a moved V# past the range limit was not snapshotted at its clamped range"); + proveAgainst(oversized, vsharp(nearEnd, 0xfffffff0u), proof, moved); + Require(moved.has_value() && moved->size() == 1 && moved->front().size == 4096, "another oversized size over the same allocation end listed another range"); + proveAgainst(oversized, vsharp(tail, 4096), proof, moved); + Require(moved.has_value() && moved->size() == 1 && moved->front().address == address(tail) && moved->front().size == 4096, "a V# that now fits the range limit was not listed at its own size"); + + proveAgainst(oversized, vsharp(block, 0xffffffffu), proof, moved); + Require(!moved.has_value() && proof.refusal == Refusal::Limit, "a moved V# the allocation holds past the range limit was served"); + + const auto overhang = readOnly(strided(tail, 24, 342)); + proveAgainst(overhang, strided(tail, 24, 342), proof, moved); + Require(moved.has_value() && moved->empty() && proof.refusal == Refusal::None, "an unchanged V# with a stride overhang past the allocation was not an exact hit"); + proveAgainst(overhang, strided(nearEnd, 24, 171), proof, moved); + Require(moved.has_value() && moved->size() == 1 && moved->front().address == address(nearEnd) && moved->front().size == 4096 && proof.bytes == 4096, "a moved V# with a stride overhang was snapshotted past the allocation end"); + proveAgainst(overhang, strided(nearEnd, 24, 100), proof, moved); + Require(moved.has_value() && moved->size() == 1 && moved->front().size == 2400, "a moved V# inside the allocation was not listed at its own size"); + recorder.Sync(); + { + auto* mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix(); + GuestAllocations::GuestAllocationsRemove_nid_postfix(mutation, block); + GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation); + } + ::operator delete(block, std::align_val_t{blockBytes}); + } Require(mock.live == 0, "dispatch move resources leaked Vulkan objects"); } From 38551363fd24bde0b94a9f9296e988b6e26c27a4 Mon Sep 17 00:00:00 2001 From: David Sera Date: Sat, 17 Oct 2026 12:26:38 +0300 Subject: [PATCH 0674/1000] test(agc): skip the emulated color compare clamp cases without MinLod The offset cases compile `image_sample_cb_cl_o`, whose LOD clamp the emitter reaches only through the device's `shaderResourceMinLod`; without it the emitter refuses the shader (the gate #388 left in place). The test had no capability guard, so on such a device every one of those cases failed at the first of them, `point, wrap with negative offsets`, instead of skipping them as `ImageSampleLodClamp` and `ImageSampleBiasA16` already do. Found on llvmpipe (Mesa 25.0.7, Debian 13), which lacks the feature that lavapipe reports from Mesa 25.2.8: `agc_driver_emulated_color_compare` and its `_srgb_decode` variant both failed. The other cases still run; the note names the cases that were skipped. --- .../tests/execution/EmulatedColorCompare.cpp | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/EmulatedColorCompare.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/EmulatedColorCompare.cpp index 276ea252d6..1d96a16670 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/EmulatedColorCompare.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/EmulatedColorCompare.cpp @@ -360,6 +360,7 @@ int main() { try { const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; + const bool lodClamp = TargetHasCapability(device->Target(), spv::CapabilityMinLod); const auto* forced = std::getenv("APS5_SRGB_SHADER_DECODE"); if (forced != nullptr && std::string_view(forced) == "1") Require((device->Target().srgbDecodeFormats & 3u) == 3u, "forced R8 and RG8 sRGB shader decoding is unavailable"); FillTexels(); @@ -408,7 +409,7 @@ int main() { } Run(*device, {ClampEdge, FilterPoint}, "point, clamp to edge"); Run(*device, {ClampWrap, FilterPoint}, "point, wrap"); - Run(*device, {ClampWrap, FilterPoint}, "point, wrap with negative offsets", true); + if (lodClamp) Run(*device, {ClampWrap, FilterPoint}, "point, wrap with negative offsets", true); Run(*device, {ClampEdge, FilterBilinear}, "bilinear, 2D array instruction on a 2D texture", false, false, true); Run(*device, {ClampEdge, FilterBilinear}, "bilinear, clamp to edge"); Run(*device, {ClampWrap, FilterBilinear}, "bilinear, wrap"); @@ -417,12 +418,14 @@ int main() { Run(*device, {ClampBorder, FilterBilinear, BorderWhite}, "bilinear, white border"); Run(*device, {ClampBorder, FilterBilinear, BorderBlack}, "bilinear, black border"); Run(*device, {ClampEdge, FilterPoint, BorderBlack, 0u, 0x0140u}, "point, clamp to edge, sampler LOD bias"); - Run(*device, {ClampEdge, FilterPoint}, "point, clamp to edge, offsets, bias and LOD clamp", true); - Run(*device, {ClampWrap, FilterBilinear}, "bilinear, wrap, offsets, bias and LOD clamp", true); - Run(*device, {ClampBorder, FilterBilinear, BorderWhite, 0u, 0x3f00u}, "bilinear, white border, offsets, bias and LOD clamp", true); + if (lodClamp) { + Run(*device, {ClampEdge, FilterPoint}, "point, clamp to edge, offsets, bias and LOD clamp", true); + Run(*device, {ClampWrap, FilterBilinear}, "bilinear, wrap, offsets, bias and LOD clamp", true); + Run(*device, {ClampBorder, FilterBilinear, BorderWhite, 0u, 0x3f00u}, "bilinear, white border, offsets, bias and LOD clamp", true); + } for (unsigned iteration = 0; iteration < 2; ++iteration) { Run(*device, {ClampEdge, FilterPoint}, "cached point comparison", false, true); - Run(*device, {ClampWrap, FilterBilinear}, "cached bilinear comparison with offsets", true, true); + if (lodClamp) Run(*device, {ClampWrap, FilterBilinear}, "cached bilinear comparison with offsets", true, true); } Reject(*device, Format32Float, {ClampEdge, FilterPoint}, "native comparison with a nonconstant texel offset requires", OffsetCode, false); RunChannels(*device, QuadCode, {4, 5, 6, 7}, 4, 0, "view R,G,B,A"); @@ -443,7 +446,7 @@ int main() { Reject(*device, Format8888UNorm, {ClampHalfBorder, FilterPoint}, "wrap, clamp-to-edge or clamp-to-border"); Reject(*device, Format8888UNorm, {ClampBorder, FilterPoint, BorderTable}, "border color table"); Reject(*device, Format8888UNorm, {ClampEdge, FilterBilinear, BorderBlack, ReductionMin}, "min or max reduction"); - std::puts("emulated color compare tests passed"); + std::puts(lodClamp ? "emulated color compare tests passed" : "emulated color compare tests passed, the offset and LOD clamp cases were skipped without shaderResourceMinLod"); return 0; } catch (const std::exception& error) { std::cerr << error.what() << '\n'; From ce57c2d7993e113a117b7f7c9922818cee833f89 Mon Sep 17 00:00:00 2001 From: Aryan Sharma Date: Fri, 9 Oct 2026 11:04:53 +0530 Subject: [PATCH 0675/1000] feat(libc): implement sceLibcBacktraceSelf on Linux PPSA28997 (IL2CPP) calls sceLibcBacktraceGetBufferSize and sceLibcBacktraceSelf. On Linux the host call stack is captured with backtrace and each frame is resolved to its module and offset with dladdr, then written as a linked list of frame records. The record layout is inferred and listed in TechnicalDebt. Windows throws. --- core/libs/prx/libc/CMakeLists.txt | 1 + core/libs/prx/libc/src/Backtrace.cpp | 104 ++++++++++++++++++++++++++- docs/dev/debt/libc.md | 1 + 3 files changed, 104 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libc/CMakeLists.txt b/core/libs/prx/libc/CMakeLists.txt index aa0c4d3711..20024b686e 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -18,6 +18,7 @@ target_sources(libc PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/Math.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Random.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/HostEntropy.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/Backtrace.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Time.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/FileAndHeap.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/FileLock.cpp diff --git a/core/libs/prx/libc/src/Backtrace.cpp b/core/libs/prx/libc/src/Backtrace.cpp index 5286225707..3a2fd8bda8 100644 --- a/core/libs/prx/libc/src/Backtrace.cpp +++ b/core/libs/prx/libc/src/Backtrace.cpp @@ -1,16 +1,116 @@ #include +#include +#include +#include +#include +#include + #include "prx/libc/include/General.hpp" +#ifndef _WIN32 +#include +#include + +namespace { + +constexpr int MaxFrames = 256; +constexpr std::size_t NameOffset = 0x10; +constexpr char GuestSuffix[] = ".guest.prx"; + +struct Frame { + std::uint32_t offset; + std::string module; +}; + +struct FrameRecord { + FrameRecord* next; + std::uint32_t offset; + std::uint32_t reserved; +}; +static_assert(sizeof(FrameRecord) == NameOffset); + +std::vector CaptureFrames(int limit) { + void* addresses[MaxFrames]; + const int captured = backtrace(addresses, MaxFrames); + std::vector frames; + for (int index = 2; index < captured && (limit < 0 || static_cast(frames.size()) < limit); ++index) { + Dl_info info{}; + if (dladdr(addresses[index], &info) == 0 || info.dli_fbase == nullptr || info.dli_fname == nullptr) continue; + const auto offset = reinterpret_cast(addresses[index]) - reinterpret_cast(info.dli_fbase); + if (offset > UINT32_MAX) continue; + std::string module = info.dli_fname; + if (const auto slash = module.find_last_of('/'); slash != std::string::npos) module.erase(0, slash + 1); + if (module.size() > sizeof(GuestSuffix) - 1 && module.compare(module.size() - (sizeof(GuestSuffix) - 1), std::string::npos, GuestSuffix) == 0) + module.resize(module.size() - (sizeof(GuestSuffix) - 1)); + frames.push_back({static_cast(offset), std::move(module)}); + } + return frames; +} + +std::size_t RecordSize(const Frame& frame) { + return (NameOffset + frame.module.size() + 1 + 7) & ~std::size_t{7}; +} + +} // namespace + extern "C" { -int APS5_VABI sceLibcBacktraceGetBufferSize_nid_postfix(int, int*, std::size_t*) { +int APS5_VABI sceLibcBacktraceGetBufferSize_nid_postfix(int limit, int* count, std::size_t* size) { + if (count == nullptr || size == nullptr) throw std::invalid_argument("sceLibcBacktraceGetBufferSize: null output"); + const auto frames = CaptureFrames(limit); + std::size_t total = 0; + for (const auto& frame : frames) total += RecordSize(frame); + *count = static_cast(frames.size()); + *size = total; + return 0; +} + +int APS5_VABI sceLibcBacktraceSelf_nid_postfix(int limit, void* buffer, std::size_t size, int* count) { + if (buffer == nullptr || count == nullptr) throw std::invalid_argument("sceLibcBacktraceSelf: null argument"); + const auto frames = CaptureFrames(limit); + auto* cursor = static_cast(buffer); + FrameRecord* previous = nullptr; + int written = 0; + for (const auto& frame : frames) { + const auto bytes = RecordSize(frame); + if (static_cast(cursor - static_cast(buffer)) + bytes > size) break; + auto* record = reinterpret_cast(cursor); + record->next = nullptr; + record->offset = frame.offset; + record->reserved = 0; + std::memcpy(cursor + NameOffset, frame.module.c_str(), frame.module.size() + 1); + if (previous != nullptr) previous->next = record; + previous = record; + cursor += bytes; + ++written; + } + *count = written; + return 0; +} + +} + +#else + +extern "C" { + +int APS5_VABI sceLibcBacktraceGetBufferSize_nid_postfix(int limit, int* count, std::size_t* size) { + (void)limit; + (void)count; + (void)size; NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceLibcBacktraceSelf_nid_postfix(int, void*, std::size_t, int*) { +int APS5_VABI sceLibcBacktraceSelf_nid_postfix(int limit, void* buffer, std::size_t size, int* count) { + (void)limit; + (void)buffer; + (void)size; + (void)count; NotImplemented_nid_no_patch(__func__); return 0; } } + +#endif diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 3921c33eee..da4cc6bcb9 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -2,6 +2,7 @@ ### Unknown function info +- [sceLibcBacktraceGetBufferSize, sceLibcBacktraceSelf](../../core/libs/prx/libc/src/Backtrace.cpp) (libc) - signatures and frame record layout inferred from a title (next pointer, u32 module offset, module name at +0x10); the u32 at +0xC is unknown and written as 0. Module names are host file names without the `.guest.prx` suffix, so their case may differ from the PS5 module name. Windows throws - [fdopen](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) validates descriptor access modes as FreeBSD 11.4 [fdopen.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/stdio/fdopen.c) does: a requested mode incompatible with a descriptor not opened for both reading and writing returns null with `EINVAL`, leaving the descriptor open. Windows reads the handle's granted access through the same host helper as `fcntl(F_GETFL)`; a closed descriptor returns null with `EBADF`. Console behavior is unverified. - [scriptingGetMem, scriptingFreeMem](../../../core/libs/prx/libc/src/ApplicationHeap.cpp) (libc) - runtime allocator hooks for Unity titles. The allocation signature `(alignment, size)`, minimum alignment 16 and null result for a non-power-of-two alignment follow [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/df5b9b92be7dfd463b47756b707d6d356a9f0550/src/libs/libKernel.cpp); the free hook's one-pointer signature follows [Raeen's recorded contract](https://github.com/Whoisraeen/Raeen/blob/2db56923204b89719916b95e29c2f3c4e1d9f90b/docs/unity-dlsym-blocker.md). Both use the registered application allocator. No console measurement; null allocation returns null and other allocator errors throw. From ec832e0bcb57f322b6bea35cbbf82ee2444db350 Mon Sep 17 00:00:00 2001 From: Aryan Sharma Date: Fri, 9 Oct 2026 16:00:25 +0530 Subject: [PATCH 0676/1000] fix(libc): limit sceLibcBacktraceSelf to Linux and test its record layout --- core/libs/prx/libc/CMakeLists.txt | 5 ++ core/libs/prx/libc/src/Backtrace.cpp | 4 +- core/libs/tests/GuestBacktrace.cpp | 116 +++++++++++++++++++++++++++ docs/dev/debt/libc.md | 2 +- 4 files changed, 124 insertions(+), 3 deletions(-) create mode 100644 core/libs/tests/GuestBacktrace.cpp diff --git a/core/libs/prx/libc/CMakeLists.txt b/core/libs/prx/libc/CMakeLists.txt index 20024b686e..d9cb2373b6 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -645,6 +645,11 @@ if(BUILD_TESTING) add_test_executable(cpu_topology_tests ${LIBS_INCLUDE_DIR}/tests/CpuTopology.cpp) target_include_directories(cpu_topology_tests PRIVATE ${LIBS_INCLUDE_DIR}) add_test(NAME cpu_topology COMMAND cpu_topology_tests) + add_test_executable(guest_backtrace_tests ${LIBS_INCLUDE_DIR}/tests/GuestBacktrace.cpp) + target_include_directories(guest_backtrace_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_backtrace_tests PRIVATE libc) + set_target_properties(guest_backtrace_tests PROPERTIES ENABLE_EXPORTS ON) + add_test(NAME guest_backtrace COMMAND guest_backtrace_tests) endif() add_test_executable(log_macros_tests ${LIBS_INCLUDE_DIR}/tests/LogMacros.cpp) target_include_directories(log_macros_tests PRIVATE ${LIBS_INCLUDE_DIR}) diff --git a/core/libs/prx/libc/src/Backtrace.cpp b/core/libs/prx/libc/src/Backtrace.cpp index 3a2fd8bda8..29c7dae111 100644 --- a/core/libs/prx/libc/src/Backtrace.cpp +++ b/core/libs/prx/libc/src/Backtrace.cpp @@ -7,7 +7,7 @@ #include "prx/libc/include/General.hpp" -#ifndef _WIN32 +#ifdef __linux__ #include #include @@ -29,7 +29,7 @@ struct FrameRecord { }; static_assert(sizeof(FrameRecord) == NameOffset); -std::vector CaptureFrames(int limit) { +[[gnu::noinline]] std::vector CaptureFrames(int limit) { void* addresses[MaxFrames]; const int captured = backtrace(addresses, MaxFrames); std::vector frames; diff --git a/core/libs/tests/GuestBacktrace.cpp b/core/libs/tests/GuestBacktrace.cpp new file mode 100644 index 0000000000..d460b3a9e6 --- /dev/null +++ b/core/libs/tests/GuestBacktrace.cpp @@ -0,0 +1,116 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceLibcBacktraceGetBufferSize(int, int*, std::size_t*); +int APS5_VABI sceLibcBacktraceSelf(int, void*, std::size_t, int*); +} + +namespace { + +constexpr std::size_t NameOffset = 0x10; +constexpr unsigned char Fill = 0xcc; + +struct Record { + const std::byte* next; + std::uint32_t offset; + std::uint32_t reserved; + std::string module; + std::size_t bytes; +}; + +void Require(bool condition, const char* message) { + if (condition) return; + std::fprintf(stderr, "guest backtrace: %s\n", message); + std::abort(); +} + +std::vector Parse(const std::vector& buffer, std::size_t used, int count) { + std::vector records; + const auto* base = reinterpret_cast(buffer.data()); + const auto* cursor = count == 0 ? nullptr : base; + while (cursor != nullptr) { + Require(cursor >= base && static_cast(cursor - base) + NameOffset < used, "record outside the written bytes"); + Require(reinterpret_cast(cursor) % 8 == 0, "record is not 8-byte aligned"); + Record record{}; + std::memcpy(&record.next, cursor, sizeof(record.next)); + std::memcpy(&record.offset, cursor + 8, sizeof(record.offset)); + std::memcpy(&record.reserved, cursor + 12, sizeof(record.reserved)); + const auto* name = reinterpret_cast(cursor + NameOffset); + const auto limit = used - static_cast(cursor - base) - NameOffset; + const auto length = strnlen(name, limit); + Require(length < limit, "module name is not terminated inside the record"); + record.module.assign(name, length); + record.bytes = (NameOffset + length + 1 + 7) & ~std::size_t{7}; + Require(record.next == nullptr || record.next == cursor + record.bytes, "next does not point to the following record"); + records.push_back(record); + cursor = record.next; + } + Require(static_cast(records.size()) == count, "record list length differs from the reported count"); + return records; +} + +std::size_t Used(const std::vector& records) { + std::size_t total = 0; + for (const auto& record : records) total += record.bytes; + return total; +} + +} + +[[gnu::noinline]] void CheckBacktrace() { + Dl_info self{}; + Require(dladdr(reinterpret_cast(&CheckBacktrace), &self) != 0 && self.dli_fname != nullptr && self.dli_saddr != nullptr, "dladdr of the test failed"); + std::string executable = self.dli_fname; + executable.erase(0, executable.find_last_of('/') + 1); + + int count = -1; + std::size_t size = 0; + Require(sceLibcBacktraceGetBufferSize(-1, &count, &size) == 0, "GetBufferSize failed"); + Require(count > 1 && size >= static_cast(count) * NameOffset, "GetBufferSize reported too little"); + + std::vector buffer(size + 64, Fill); + int written = -1; + Require(sceLibcBacktraceSelf(-1, buffer.data(), size, &written) == 0, "Self failed"); + Require(written == count, "Self wrote a different count than GetBufferSize reported"); + const auto records = Parse(buffer, size, written); + Require(Used(records) == size, "Self used a different size than GetBufferSize reported"); + for (std::size_t index = size; index < buffer.size(); ++index) Require(buffer[index] == Fill, "Self wrote past the buffer size"); + for (const auto& record : records) Require(record.reserved == 0 && !record.module.empty(), "record has a nonzero reserved word or no module"); + Require(records.front().module == executable, "first frame is not in the calling module"); + Dl_info caller{}; + const auto address = reinterpret_cast(self.dli_fbase) + records.front().offset; + Require(dladdr(reinterpret_cast(address), &caller) != 0 && caller.dli_saddr == self.dli_saddr, "first frame is not the caller"); + + for (const int limit : {0, 1, 2}) { + Require(sceLibcBacktraceGetBufferSize(limit, &count, &size) == 0 && count == limit, "GetBufferSize ignored the frame limit"); + std::vector limited(size + 64, Fill); + Require(sceLibcBacktraceSelf(limit, limited.data(), limited.size(), &written) == 0 && written == limit, "Self ignored the frame limit"); + const auto limitedRecords = Parse(limited, limited.size(), written); + Require(Used(limitedRecords) == size, "limited Self used a different size than GetBufferSize reported"); + for (int index = 0; index < limit; ++index) Require(limitedRecords[index].module == records[index].module, "limited frames differ from the full trace"); + } + + const auto first = records.front().bytes; + for (const std::size_t available : {std::size_t{0}, first - 1, first, first + records[1].bytes - 1, first + records[1].bytes}) { + std::vector small(available + 64, Fill); + written = -1; + Require(sceLibcBacktraceSelf(-1, small.data(), available, &written) == 0, "Self with a small buffer failed"); + const int expected = available < first ? 0 : available < first + records[1].bytes ? 1 : 2; + Require(written == expected, "Self with a small buffer wrote the wrong number of records"); + const auto smallRecords = Parse(small, available, written); + Require(written == 0 || smallRecords.back().next == nullptr, "last record is not terminated"); + for (std::size_t index = Used(smallRecords); index < small.size(); ++index) Require(small[index] == Fill, "Self wrote past the last record that fits"); + } +} + +int main() { + CheckBacktrace(); +} diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index da4cc6bcb9..f9d48bf2ea 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -2,7 +2,7 @@ ### Unknown function info -- [sceLibcBacktraceGetBufferSize, sceLibcBacktraceSelf](../../core/libs/prx/libc/src/Backtrace.cpp) (libc) - signatures and frame record layout inferred from a title (next pointer, u32 module offset, module name at +0x10); the u32 at +0xC is unknown and written as 0. Module names are host file names without the `.guest.prx` suffix, so their case may differ from the PS5 module name. Windows throws +- [sceLibcBacktraceGetBufferSize, sceLibcBacktraceSelf](../../core/libs/prx/libc/src/Backtrace.cpp) (libc) - signatures and frame record layout inferred from a title (next pointer, u32 module offset, module name at +0x10); the u32 at +0xC is unknown and written as 0. Module names are host file names without the `.guest.prx` suffix, so their case may differ from the PS5 module name. Implemented only on Linux; other platforms throw - [fdopen](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) validates descriptor access modes as FreeBSD 11.4 [fdopen.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/stdio/fdopen.c) does: a requested mode incompatible with a descriptor not opened for both reading and writing returns null with `EINVAL`, leaving the descriptor open. Windows reads the handle's granted access through the same host helper as `fcntl(F_GETFL)`; a closed descriptor returns null with `EBADF`. Console behavior is unverified. - [scriptingGetMem, scriptingFreeMem](../../../core/libs/prx/libc/src/ApplicationHeap.cpp) (libc) - runtime allocator hooks for Unity titles. The allocation signature `(alignment, size)`, minimum alignment 16 and null result for a non-power-of-two alignment follow [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/df5b9b92be7dfd463b47756b707d6d356a9f0550/src/libs/libKernel.cpp); the free hook's one-pointer signature follows [Raeen's recorded contract](https://github.com/Whoisraeen/Raeen/blob/2db56923204b89719916b95e29c2f3c4e1d9f90b/docs/unity-dlsym-blocker.md). Both use the registered application allocator. No console measurement; null allocation returns null and other allocator errors throw. From 7e89abbab07465e953b6d37abd7c906609f4b237 Mon Sep 17 00:00:00 2001 From: Aryan Sharma Date: Wed, 14 Oct 2026 21:02:57 +0530 Subject: [PATCH 0677/1000] test(libc): backtrace tests call the _nid_postfix exports The sceLibcBacktrace implementations were renamed to the _nid_postfix convention on this branch, so the test declarations must match. --- core/libs/tests/GuestBacktrace.cpp | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/core/libs/tests/GuestBacktrace.cpp b/core/libs/tests/GuestBacktrace.cpp index d460b3a9e6..25e958cdb9 100644 --- a/core/libs/tests/GuestBacktrace.cpp +++ b/core/libs/tests/GuestBacktrace.cpp @@ -9,8 +9,8 @@ #include extern "C" { -int APS5_VABI sceLibcBacktraceGetBufferSize(int, int*, std::size_t*); -int APS5_VABI sceLibcBacktraceSelf(int, void*, std::size_t, int*); +int APS5_VABI sceLibcBacktraceGetBufferSize_nid_postfix(int, int*, std::size_t*); +int APS5_VABI sceLibcBacktraceSelf_nid_postfix(int, void*, std::size_t, int*); } namespace { @@ -73,12 +73,12 @@ std::size_t Used(const std::vector& records) { int count = -1; std::size_t size = 0; - Require(sceLibcBacktraceGetBufferSize(-1, &count, &size) == 0, "GetBufferSize failed"); + Require(sceLibcBacktraceGetBufferSize_nid_postfix(-1, &count, &size) == 0, "GetBufferSize failed"); Require(count > 1 && size >= static_cast(count) * NameOffset, "GetBufferSize reported too little"); std::vector buffer(size + 64, Fill); int written = -1; - Require(sceLibcBacktraceSelf(-1, buffer.data(), size, &written) == 0, "Self failed"); + Require(sceLibcBacktraceSelf_nid_postfix(-1, buffer.data(), size, &written) == 0, "Self failed"); Require(written == count, "Self wrote a different count than GetBufferSize reported"); const auto records = Parse(buffer, size, written); Require(Used(records) == size, "Self used a different size than GetBufferSize reported"); @@ -90,9 +90,9 @@ std::size_t Used(const std::vector& records) { Require(dladdr(reinterpret_cast(address), &caller) != 0 && caller.dli_saddr == self.dli_saddr, "first frame is not the caller"); for (const int limit : {0, 1, 2}) { - Require(sceLibcBacktraceGetBufferSize(limit, &count, &size) == 0 && count == limit, "GetBufferSize ignored the frame limit"); + Require(sceLibcBacktraceGetBufferSize_nid_postfix(limit, &count, &size) == 0 && count == limit, "GetBufferSize ignored the frame limit"); std::vector limited(size + 64, Fill); - Require(sceLibcBacktraceSelf(limit, limited.data(), limited.size(), &written) == 0 && written == limit, "Self ignored the frame limit"); + Require(sceLibcBacktraceSelf_nid_postfix(limit, limited.data(), limited.size(), &written) == 0 && written == limit, "Self ignored the frame limit"); const auto limitedRecords = Parse(limited, limited.size(), written); Require(Used(limitedRecords) == size, "limited Self used a different size than GetBufferSize reported"); for (int index = 0; index < limit; ++index) Require(limitedRecords[index].module == records[index].module, "limited frames differ from the full trace"); @@ -102,7 +102,7 @@ std::size_t Used(const std::vector& records) { for (const std::size_t available : {std::size_t{0}, first - 1, first, first + records[1].bytes - 1, first + records[1].bytes}) { std::vector small(available + 64, Fill); written = -1; - Require(sceLibcBacktraceSelf(-1, small.data(), available, &written) == 0, "Self with a small buffer failed"); + Require(sceLibcBacktraceSelf_nid_postfix(-1, small.data(), available, &written) == 0, "Self with a small buffer failed"); const int expected = available < first ? 0 : available < first + records[1].bytes ? 1 : 2; Require(written == expected, "Self with a small buffer wrote the wrong number of records"); const auto smallRecords = Parse(small, available, written); From 2e0d310c91c7b8285b9b8df0b556921dc37fb046 Mon Sep 17 00:00:00 2001 From: Aryan Sharma Date: Sat, 17 Oct 2026 14:57:32 +0530 Subject: [PATCH 0678/1000] docs(debt): link the backtrace entry from its debt file --- docs/dev/debt/libc.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index f9d48bf2ea..dbcd923eb9 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -2,7 +2,7 @@ ### Unknown function info -- [sceLibcBacktraceGetBufferSize, sceLibcBacktraceSelf](../../core/libs/prx/libc/src/Backtrace.cpp) (libc) - signatures and frame record layout inferred from a title (next pointer, u32 module offset, module name at +0x10); the u32 at +0xC is unknown and written as 0. Module names are host file names without the `.guest.prx` suffix, so their case may differ from the PS5 module name. Implemented only on Linux; other platforms throw +- [sceLibcBacktraceGetBufferSize, sceLibcBacktraceSelf](../../../core/libs/prx/libc/src/Backtrace.cpp) (libc) - signatures and frame record layout inferred from a title (next pointer, u32 module offset, module name at +0x10); the u32 at +0xC is unknown and written as 0. Module names are host file names without the `.guest.prx` suffix, so their case may differ from the PS5 module name. Implemented only on Linux; other platforms throw - [fdopen](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) validates descriptor access modes as FreeBSD 11.4 [fdopen.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/stdio/fdopen.c) does: a requested mode incompatible with a descriptor not opened for both reading and writing returns null with `EINVAL`, leaving the descriptor open. Windows reads the handle's granted access through the same host helper as `fcntl(F_GETFL)`; a closed descriptor returns null with `EBADF`. Console behavior is unverified. - [scriptingGetMem, scriptingFreeMem](../../../core/libs/prx/libc/src/ApplicationHeap.cpp) (libc) - runtime allocator hooks for Unity titles. The allocation signature `(alignment, size)`, minimum alignment 16 and null result for a non-power-of-two alignment follow [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/df5b9b92be7dfd463b47756b707d6d356a9f0550/src/libs/libKernel.cpp); the free hook's one-pointer signature follows [Raeen's recorded contract](https://github.com/Whoisraeen/Raeen/blob/2db56923204b89719916b95e29c2f3c4e1d9f90b/docs/unity-dlsym-blocker.md). Both use the registered application allocator. No console measurement; null allocation returns null and other allocator errors throw. From 4b656882ba4418651c9012b703dad38f7c58cc4a Mon Sep 17 00:00:00 2001 From: Alexandros Pournaras Date: Fri, 16 Oct 2026 18:58:00 +0300 Subject: [PATCH 0679/1000] fix(libSceNet): store SO_ONESBCAST in sceNetSetsockopt --- core/libs/prx/libSceNet/Export.cpp | 5 +++++ core/libs/tests/GuestSceNet.cpp | 23 +++++++++++++++++++++++ docs/dev/debt/libSceNet.md | 1 + 3 files changed, 29 insertions(+) diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index 232bf7fdc3..8fd8578762 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -87,6 +87,7 @@ constexpr int NET_SO_ERROR = 0x1007; constexpr int NET_SO_NBIO = 0x1200; constexpr int NET_SO_REUSEADDR = 0x4; constexpr int NET_SO_BROADCAST = 0x20; +constexpr int NET_SO_ONESBCAST = 0x10000; constexpr int NET_SO_SNDBUF = 0x1001; constexpr int NET_SO_RCVBUF = 0x1002; constexpr int NET_SO_SCE_SNDTIMEO = 0x1105; @@ -252,6 +253,7 @@ struct Sock { int reported_error = 0; int snd_buffer = 0; int rcv_buffer = 0; + int onesbcast = 0; }; #ifdef _WIN32 @@ -1177,6 +1179,7 @@ int APS5_VABI sceNetSetsockopt(int s, int level, int optname, const void* optval const int native_option = optname == NET_SO_REUSEADDR ? SO_REUSEADDR : optname == NET_SO_BROADCAST ? SO_BROADCAST : optname == NET_SO_SNDBUF ? SO_SNDBUF : SO_RCVBUF; if (::setsockopt(socket.native->value, SOL_SOCKET, native_option, reinterpret_cast(&value), sizeof(value)) != 0) return fail(native_error()); + } else if (optname == NET_SO_ONESBCAST) { } else { return fail(NET_EOPNOTSUPP); } @@ -1188,6 +1191,7 @@ int APS5_VABI sceNetSetsockopt(int s, int level, int optname, const void* optval if (optname == NET_SO_SNDTIMEO) it->second.snd_timeout_us = value; if (optname == NET_SO_SNDBUF) it->second.snd_buffer = value; if (optname == NET_SO_RCVBUF) it->second.rcv_buffer = value; + if (optname == NET_SO_ONESBCAST) it->second.onesbcast = value; } return 0; } @@ -1215,6 +1219,7 @@ int APS5_VABI sceNetGetsockopt(int s, int level, int optname, void* optval, uint case NET_SO_NBIO: value = socket.nonblock ? 1 : 0; break; case NET_SO_RCVTIMEO: value = socket.rcv_timeout_us; break; case NET_SO_SNDTIMEO: value = socket.snd_timeout_us; break; + case NET_SO_ONESBCAST: value = socket.onesbcast; break; case NET_SO_ERROR: { int pending = 0; NativeLength size = sizeof(pending); diff --git a/core/libs/tests/GuestSceNet.cpp b/core/libs/tests/GuestSceNet.cpp index 6b639016d8..e5b47cdeae 100644 --- a/core/libs/tests/GuestSceNet.cpp +++ b/core/libs/tests/GuestSceNet.cpp @@ -872,6 +872,28 @@ static void CheckBufferSizes() { Require(sceNetSocketClose(socket) == 0); } +static void CheckOnesBroadcast() { + const auto read = [](int socket) { + int value = -1; + std::uint32_t size = sizeof(value); + Require(sceNetGetsockopt(socket, 0xffff, 0x10000, &value, &size) == 0 && size == sizeof(value)); + return value; + }; + const int socket = sceNetSocket("ones-broadcast", 2, 2, 17); + const int other = sceNetSocket("ones-broadcast-other", 2, 2, 17); + Require(socket >= 0 && other >= 0); + Require(read(socket) == 0); + for (const int requested : {1, 0, 7}) { + Require(sceNetSetsockopt(socket, 0xffff, 0x10000, &requested, sizeof(requested)) == 0); + Require(read(socket) == requested); + } + Require(read(other) == 0); + const std::uint8_t truncated = 1; + Require(Failed(sceNetSetsockopt(socket, 0xffff, 0x10000, &truncated, sizeof(truncated)), 22)); + Require(read(socket) == 7); + for (const int descriptor : {socket, other}) Require(sceNetSocketClose(descriptor) == 0); +} + static void CheckUnspecifiedIpv6() { std::array unspecified{}; Require(sceNetInetPton(28, "::", unspecified.data()) == 1); @@ -1083,6 +1105,7 @@ int main() { CheckDatagramAddressLengths(28); CheckStreamAddressLengths(); CheckBufferSizes(); + CheckOnesBroadcast(); const int epoll = sceNetEpollCreate("guest-sce-net", 0); Require(epoll >= 0); diff --git a/docs/dev/debt/libSceNet.md b/docs/dev/debt/libSceNet.md index ac41a7b05d..e101cbca34 100644 --- a/docs/dev/debt/libSceNet.md +++ b/docs/dev/debt/libSceNet.md @@ -13,6 +13,7 @@ - [sceNetConnect to AF_UNSPEC](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) dissolves a datagram association as FreeBSD 11.4's `udp_disconnect()` does, which resets the local address to the wildcard address and keeps the port. On Linux a socket bound to an explicit port keeps its local address as well (`127.0.0.1:` stays `127.0.0.1:`), because a bound socket cannot be bound again; one bound to port 0 is bound again to the wildcard address with the port it had - [sceNetSetsockopt](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) sets `SO_RCVTIMEO` and `SO_SNDTIMEO` as the host socket's timeouts. On Windows a receive or send on a stream socket that times out fails with `ETIMEDOUT`, where FreeBSD returns `EAGAIN` or the partial count: Winsock leaves the connection in an indeterminate state after a timeout and reports no partial count ([SOL_SOCKET options](https://learn.microsoft.com/en-us/windows/win32/winsock/sol-socket-socket-options)), so FreeBSD's behaviour needs the timeout waited for without the host option. A receive on a datagram socket that times out fails with `EAGAIN` - [sceNetSetsockopt, sceNetGetsockopt](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) take `SO_SNDTIMEO` and `SO_RCVTIMEO` (0x1005, 0x1006) as a 16-byte `struct timeval`, as FreeBSD 11.4's [`sosetopt` and `sogetopt`](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/sys/kern/uipc_socket.c#L2657-L2690) do, and Sony's 0x1105 and 0x1106 as an `int` of microseconds. A [console log of ProsperoLight](https://github.com/blackbearreloaded/ProsperoLight/issues/49) (firmware 12.20) shows receives timing out every 100 ms after it gave 0x1006 a `{0, 100000}` timeval. The errors for a value shorter than a timeval (`EINVAL`) or out of range (`EDOM`), and `sceNetGetsockopt` cutting the timeval to the caller's length, follow FreeBSD's `sooptcopyin` and `sooptcopyout` and are not measured on a console +- [sceNetSetsockopt](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) stores `SO_ONESBCAST` (`0x10000`, [shadPS4's `ORBIS_NET_SO_ONESBCAST`](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/net/net_types.h)) per socket and `sceNetGetsockopt` reads back the value that was set, as shadPS4 does; PPSA34501 sets it and crashes later when it fails with `EOPNOTSUPP`. Sends ignore it. FreeBSD 11.4's [ip(4)](https://man.freebsd.org/cgi/man.cgi?manpath=FreeBSD+11.4-RELEASE&query=ip&sektion=4) documents the like-named `IPPROTO_IP` option `IP_ONESBCAST`: a datagram sent to an interface's directed broadcast address, such as 192.168.2.255, leaves through that interface with its destination rewritten to 255.255.255.255, and only on a socket allowed to set `SO_BROADCAST`. Here host routing selects the outgoing interface and the destination is not rewritten. shadPS4 also leaves sends unchanged, and its [TODO](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/net/net.cpp#L2467-L2470) reads the option the other way round: a send to 255.255.255.255 would be converted to the interface's broadcast address. Whether the console's `SOL_SOCKET` option behaves like `IP_ONESBCAST`, and what it reads back, is not verified on hardware. FreeBSD 11.4's `sogetopt()` reads several boolean socket options back as their flag bit (`so_options & optname`, `sys/kern/uipc_socket.c`), not as the value set; whether the console does so for `0x10000` is unknown - [sceNetGetsockopt](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) reads `SO_ERROR` from the host socket. Winsock does not clear a socket's pending error when it is read, so on Windows the library reports a pending error once and shows it again only after a new `sceNetConnect`; a second failure with the same code on the same socket without a `sceNetConnect` in between would be hidden. - [sceNetSendmsg and sceNetRecvmsg](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) copy the scatter/gather list through one host buffer and accept only the flags `sceNetSend`/`sceNetRecv` accept. Sending control data throws; a receive never returns control data (`msg_controllen` 0), since no socket option that queues it is supported - [MSG_DONTWAIT](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) in `sceNetSend`, `sceNetSendto`, `sceNetRecv`, `sceNetRecvfrom`, `sceNetRecvmsg` and `sceNetSendmsg` makes that one call non-blocking through the host's `MSG_DONTWAIT`, as FreeBSD 11.4's `soreceive_generic` returns `EWOULDBLOCK` for it (`sys/kern/uipc_socket.c`); the value `0x80` is FreeBSD's (`sys/sys/socket.h`) and [shadPS4's](https://github.com/shadps4-emu/shadPS4/blob/e0a466753a1aacbc8db8253be685bf7ab4d9daae/src/core/libraries/net/net_types.h#L104). Winsock has no per-call non-blocking flag, so on Windows these calls still fail with `EOPNOTSUPP` for it From 27ded2c4d1fff621cf860202c1c16aa5de5d394b Mon Sep 17 00:00:00 2001 From: danielcadev <112592276+danielcadev@users.noreply.github.com> Date: Sat, 17 Oct 2026 04:01:09 -0500 Subject: [PATCH 0680/1000] fix(agc): keep small device buffers in the small pool tier --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 8 ++++- .../Graphics/src/BufferPool.cpp | 2 +- .../prx/libSceAgcDriver/tests/BufferPool.cpp | 33 ++++++++++++++++++- 3 files changed, 40 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index b21af51bda..3f375e9c2a 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -804,8 +804,14 @@ configure_windows_unwind(agc_buffer_pool_tests) if(BUILD_TESTING) set_target_properties(agc_buffer_pool_tests PROPERTIES EXCLUDE_FROM_ALL OFF) add_test(NAME agc_buffer_pool COMMAND agc_buffer_pool_tests) + add_test(NAME agc_buffer_pool_small_device_shared COMMAND agc_buffer_pool_tests --small-device-retention) + set_tests_properties(agc_buffer_pool_small_device_shared PROPERTIES ENVIRONMENT + "APS5_STAGING_POOL_MIB=256;APS5_BUFFER_POOL_SHARED=1") + add_test(NAME agc_buffer_pool_small_device_no_staging COMMAND agc_buffer_pool_tests --small-device-retention) + set_tests_properties(agc_buffer_pool_small_device_no_staging PROPERTIES ENVIRONMENT + "APS5_STAGING_POOL_MIB=0") if(WIN32) - set_tests_properties(agc_buffer_pool PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(agc_buffer_pool agc_buffer_pool_small_device_shared agc_buffer_pool_small_device_no_staging PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() endif() diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp index 6ec77770be..ebf415fe56 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp @@ -223,7 +223,7 @@ VkBufferUsageFlags BufferPool::Usage(VkBufferUsageFlags usage, VkMemoryPropertyF } BufferPool::Tier& BufferPool::tierFor(std::size_t capacity, VkMemoryPropertyFlags properties) { - if (DeviceTiered(properties)) return deviceTier; + if (DeviceTiered(properties) && (sharedTiers() || capacity >= classLimit)) return deviceTier; return !sharedTiers() && capacity < classLimit ? smallTier : largeTier; } diff --git a/core/libs/prx/libSceAgcDriver/tests/BufferPool.cpp b/core/libs/prx/libSceAgcDriver/tests/BufferPool.cpp index f0ac0664ab..2974c1f31e 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BufferPool.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BufferPool.cpp @@ -2,6 +2,7 @@ #include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" #include #include +#include #include #include #include @@ -210,6 +211,31 @@ void SizeClassesAndDirections() { Expect(mock.sizes.at(writeBack.Handle()) == 10 * MiB + 512 * 1024, "a reused device buffer kept the VkBuffer of the earlier size"); } +void SmallDeviceRetention() { + mock = MockDevice{}; + { + auto context = mockContext(); + constexpr auto usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT; + const auto deviceBudget = BufferPool::DeviceBudget(context.memory); + const auto bytes = static_cast((deviceBudget != 0 ? deviceBudget : 512 * MiB) / 2) + MiB; + VkDeviceMemory small = VK_NULL_HANDLE; + { + DeviceBuffer buffer(context, 256, usage); + small = MemoryOf(buffer.Handle()); + } + std::vector> large; + for (int i = 0; i < 2; ++i) large.push_back(std::make_unique(context, bytes, usage)); + large.clear(); + Expect(mock.frees != 0, "large device buffers did not exercise pool budget eviction"); + const auto made = mock.allocations; + const bool isolated = std::getenv("APS5_BUFFER_POOL_SHARED") == nullptr; + DeviceBuffer again(context, 256, usage); + Expect((MemoryOf(again.Handle()) == small) == isolated, "small device buffer retention did not follow the separate or shared pool budget"); + Expect(mock.allocations == made + (isolated ? 0 : 1), "large device buffer churn changed small buffer allocation reuse"); + } + Expect(mock.liveBytes == 0 && mock.liveBuffers.empty(), "device pool tier retention leaked Vulkan objects"); +} + void LargerClass() { mock = MockDevice{}; auto context = mockContext(); @@ -530,11 +556,16 @@ void PartialSlabDoesNotRetry() { } -int main() { +int main(int argc, char** argv) { try { + if (argc == 2 && std::string_view(argv[1]) == "--small-device-retention") { + SmallDeviceRetention(); + return failures == 0 ? 0 : 1; + } SizeClassesAndDirections(); LargerClass(); KeptUntilFence(); + SmallDeviceRetention(); Budget(); AddressAndHostUnchanged(); SmallDeviceClasses(); From dc324d3d5f7ce723aab1fabfb704a93a6d9c7cda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexej=20Szpev=C3=A1k?= Date: Sat, 17 Oct 2026 11:51:00 +0200 Subject: [PATCH 0681/1000] fix(libc): translate the host errno of a failed fopen fopen left the host's errno in place when the host open failed, so on Linux a symlink loop reached the title as 40 and an over-long name as 36, which are EMSGSIZE and EINPROGRESS on FreeBSD. GuestErrnoFromHost_nid_no_patch maps the host value to FreeBSD 11.4's: 1 to 34 are shared except EAGAIN (35), and ELOOP, ENAMETOOLONG, ENOTEMPTY, EDQUOT, EOVERFLOW and EDEADLK get their FreeBSD numbers; anything else is EIO. --- core/libs/prx/libc/CMakeLists.txt | 9 +++ core/libs/prx/libc/include/HostErrno.hpp | 6 ++ core/libs/prx/libc/src/FileAndHeap.cpp | 4 +- core/libs/prx/libc/src/HostErrno.cpp | 35 ++++++++++++ core/libs/tests/GuestFopenErrors.cpp | 73 ++++++++++++++++++++++++ 5 files changed, 125 insertions(+), 2 deletions(-) create mode 100644 core/libs/prx/libc/include/HostErrno.hpp create mode 100644 core/libs/prx/libc/src/HostErrno.cpp create mode 100644 core/libs/tests/GuestFopenErrors.cpp diff --git a/core/libs/prx/libc/CMakeLists.txt b/core/libs/prx/libc/CMakeLists.txt index aa0c4d3711..7b49ac4e76 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -20,6 +20,7 @@ target_sources(libc PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/HostEntropy.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Time.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/FileAndHeap.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/HostErrno.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/FileLock.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/StdioExtras.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/StandardStreams.cpp @@ -65,6 +66,14 @@ if(WIN32) endif() if(BUILD_TESTING) + if(UNIX) + add_test_executable(guest_fopen_errors_tests ${LIBS_INCLUDE_DIR}/tests/GuestFopenErrors.cpp) + target_include_directories(guest_fopen_errors_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_fopen_errors_tests PRIVATE libc) + add_test(NAME guest_fopen_errors COMMAND guest_fopen_errors_tests) + set_tests_properties(guest_fopen_errors PROPERTIES TIMEOUT 20) + endif() + add_test_executable(guest_allocating_strings_tests ${LIBS_INCLUDE_DIR}/tests/GuestAllocatingStrings.cpp) target_include_directories(guest_allocating_strings_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_allocating_strings_tests PRIVATE libc) diff --git a/core/libs/prx/libc/include/HostErrno.hpp b/core/libs/prx/libc/include/HostErrno.hpp new file mode 100644 index 0000000000..5cb2a0853c --- /dev/null +++ b/core/libs/prx/libc/include/HostErrno.hpp @@ -0,0 +1,6 @@ +#ifndef CORE_LIBS_PRX_LIBC_INCLUDE_HOSTERRNO_HPP +#define CORE_LIBS_PRX_LIBC_INCLUDE_HOSTERRNO_HPP + +extern "C" int GuestErrnoFromHost_nid_no_patch(int error); + +#endif diff --git a/core/libs/prx/libc/src/FileAndHeap.cpp b/core/libs/prx/libc/src/FileAndHeap.cpp index d35f8f7c7f..d59281c16d 100644 --- a/core/libs/prx/libc/src/FileAndHeap.cpp +++ b/core/libs/prx/libc/src/FileAndHeap.cpp @@ -26,6 +26,7 @@ #include "prx/libc/include/ApplicationHeap.hpp" #include "prx/libc/include/General.hpp" #include "prx/libc/include/FilesystemError.hpp" +#include "prx/libc/include/HostErrno.hpp" #include "prx/libc/include/AppArchive.hpp" #include "prx/libc/include/GuestArena.hpp" @@ -303,8 +304,7 @@ FileStream* APS5_VABI fopen_nid_postfix(const char* filename, const char* mode) if (const int error = ResolvePathError_nid_no_patch(filename, fpath)) { errno = error; return nullptr; } std::unique_ptr handle(OpenNative(fpath, parsed->native), std::fclose); if (!handle) { - const int error = errno; - errno = error; + errno = GuestErrnoFromHost_nid_no_patch(errno); return nullptr; } // APS5_LOG_OUT("success: \"%s\"", abs_path.c_str()); diff --git a/core/libs/prx/libc/src/HostErrno.cpp b/core/libs/prx/libc/src/HostErrno.cpp new file mode 100644 index 0000000000..5e01470dec --- /dev/null +++ b/core/libs/prx/libc/src/HostErrno.cpp @@ -0,0 +1,35 @@ +#include "prx/libc/include/HostErrno.hpp" + +#include + +namespace { + +constexpr int GuestEio = 5; +constexpr int GuestEdeadlk = 11; +constexpr int GuestEagain = 35; +constexpr int GuestEloop = 62; +constexpr int GuestEnametoolong = 63; +constexpr int GuestEnotempty = 66; +constexpr int GuestEdquot = 69; +constexpr int GuestEoverflow = 84; + +} + +extern "C" int GuestErrnoFromHost_nid_no_patch(int error) { + if (error == EAGAIN) return GuestEagain; +#ifdef EDEADLK + if (error == EDEADLK) return GuestEdeadlk; +#endif + if (error == ENAMETOOLONG) return GuestEnametoolong; + if (error == ENOTEMPTY) return GuestEnotempty; +#ifdef ELOOP + if (error == ELOOP) return GuestEloop; +#endif +#ifdef EDQUOT + if (error == EDQUOT) return GuestEdquot; +#endif +#ifdef EOVERFLOW + if (error == EOVERFLOW) return GuestEoverflow; +#endif + return error > 0 && error <= 34 ? error : GuestEio; +} diff --git a/core/libs/tests/GuestFopenErrors.cpp b/core/libs/tests/GuestFopenErrors.cpp new file mode 100644 index 0000000000..003977dfb5 --- /dev/null +++ b/core/libs/tests/GuestFopenErrors.cpp @@ -0,0 +1,73 @@ +#include "prx/libc/include/general/VabiMacros.hpp" + +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +void* APS5_VABI fopen_nid_postfix(const char* filename, const char* mode); +int APS5_VABI fclose_nid_postfix(void* stream); +int* APS5_VABI __error_nid_postfix(); +} + +namespace { + +void Require(bool value, int line) { + if (!value) { + std::fprintf(stderr, "fopen error check failed at line %d, errno %d\n", line, *__error_nid_postfix()); + std::exit(1); + } +} +#define Check(value) Require((value), __LINE__) + +constexpr int GuestEnoent = 2; +constexpr int GuestEisdir = 21; +constexpr int GuestEloop = 62; +constexpr int GuestEnametoolong = 63; + +const std::string LongName(300, 'n'); + +std::filesystem::path EnterFreshRoot() { + const auto root = std::filesystem::temp_directory_path() / + ("anyps5-fopen-errors-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); + std::filesystem::create_directories(root / "folder"); + std::filesystem::current_path(root); + Check(::symlink("loop", "loop") == 0); + { std::ofstream file("plain.txt"); file << "plain"; } + return root; +} + +bool OpenFails(const char* name, const char* mode, int error) { + *__error_nid_postfix() = 0; + const bool failed = fopen_nid_postfix(name, mode) == nullptr && *__error_nid_postfix() == error; + *__error_nid_postfix() = 0; + return failed; +} + +void Open() { + Check(OpenFails("loop", "r", GuestEloop)); + Check(OpenFails("loop", "w", GuestEloop)); + Check(OpenFails("loop/inside", "r", GuestEloop)); + Check(OpenFails(LongName.c_str(), "r", GuestEnametoolong)); + Check(OpenFails(LongName.c_str(), "w", GuestEnametoolong)); + Check(OpenFails((LongName + "/inside").c_str(), "a", GuestEnametoolong)); + Check(OpenFails("missing.txt", "r", GuestEnoent)); + Check(OpenFails("folder", "w", GuestEisdir)); + void* plain = fopen_nid_postfix("plain.txt", "r"); + Check(plain != nullptr && fclose_nid_postfix(plain) == 0); +} + +} + +int main() { + const auto root = EnterFreshRoot(); + Open(); + std::error_code error; + std::filesystem::current_path(std::filesystem::temp_directory_path(), error); + std::filesystem::remove_all(root, error); + return 0; +} From 880604361389915ca0a02a8266fab9f43fca94b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexej=20Szpev=C3=A1k?= Date: Sat, 17 Oct 2026 11:51:32 +0200 Subject: [PATCH 0682/1000] fix(libc): translate the host errno of a failed freopen freopen closed the stream and returned the host's errno from the failed reopen as is, with the same Linux numbers for a symlink loop or an over-long name as fopen had. It now passes it through GuestErrnoFromHost_nid_no_patch. --- core/libs/prx/libc/src/FileAndHeap.cpp | 2 +- core/libs/tests/GuestFopenErrors.cpp | 23 +++++++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libc/src/FileAndHeap.cpp b/core/libs/prx/libc/src/FileAndHeap.cpp index d59281c16d..891b76a60a 100644 --- a/core/libs/prx/libc/src/FileAndHeap.cpp +++ b/core/libs/prx/libc/src/FileAndHeap.cpp @@ -285,7 +285,7 @@ FileStream* APS5_VABI freopen_nid_postfix(const char* filename, const char* mode if (!path.empty() && parsed->writes) RecordWrittenPath_nid_no_patch(path); return stream; } - const int error = errno; + const int error = GuestErrnoFromHost_nid_no_patch(errno); if (stream->IsDynamic()) delete stream; errno = error; return nullptr; diff --git a/core/libs/tests/GuestFopenErrors.cpp b/core/libs/tests/GuestFopenErrors.cpp index 003977dfb5..df8d642204 100644 --- a/core/libs/tests/GuestFopenErrors.cpp +++ b/core/libs/tests/GuestFopenErrors.cpp @@ -11,6 +11,7 @@ extern "C" { void* APS5_VABI fopen_nid_postfix(const char* filename, const char* mode); int APS5_VABI fclose_nid_postfix(void* stream); +void* APS5_VABI freopen_nid_postfix(const char* filename, const char* mode, void* stream); int* APS5_VABI __error_nid_postfix(); } @@ -61,11 +62,33 @@ void Open() { Check(plain != nullptr && fclose_nid_postfix(plain) == 0); } +bool ReopenFails(const char* name, const char* mode, int error) { + void* stream = fopen_nid_postfix("plain.txt", "r"); + Check(stream != nullptr); + *__error_nid_postfix() = 0; + const bool failed = freopen_nid_postfix(name, mode, stream) == nullptr && *__error_nid_postfix() == error; + *__error_nid_postfix() = 0; + return failed; +} + +void Reopen() { + Check(ReopenFails("loop", "r", GuestEloop)); + Check(ReopenFails("loop/inside", "w", GuestEloop)); + Check(ReopenFails(LongName.c_str(), "r", GuestEnametoolong)); + Check(ReopenFails(LongName.c_str(), "w", GuestEnametoolong)); + Check(ReopenFails("missing.txt", "r", GuestEnoent)); + void* stream = fopen_nid_postfix("plain.txt", "r"); + Check(stream != nullptr); + void* reopened = freopen_nid_postfix("plain.txt", "r", stream); + Check(reopened != nullptr && fclose_nid_postfix(reopened) == 0); +} + } int main() { const auto root = EnterFreshRoot(); Open(); + Reopen(); std::error_code error; std::filesystem::current_path(std::filesystem::temp_directory_path(), error); std::filesystem::remove_all(root, error); From 5433b32b7d9aad9fedb4e57067f6ef5124c29218 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexej=20Szpev=C3=A1k?= Date: Sat, 17 Oct 2026 11:52:14 +0200 Subject: [PATCH 0683/1000] fix(libc): return the open error from fopen_s instead of ENOENT fopen_s returned ENOENT for every failed open, including an invalid mode, a directory opened for writing and the loop and long-name cases. C11 K.3.5.2.1 only asks for a nonzero value; Microsoft's CRT, which shares Dinkumware's library, returns errno. fopen_s now returns the errno fopen set. --- core/libs/prx/libc/src/FileAndHeap.cpp | 3 +-- core/libs/tests/GuestFopenErrors.cpp | 13 +++++++++++++ docs/dev/debt/libc.md | 1 + 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libc/src/FileAndHeap.cpp b/core/libs/prx/libc/src/FileAndHeap.cpp index 891b76a60a..297c64076e 100644 --- a/core/libs/prx/libc/src/FileAndHeap.cpp +++ b/core/libs/prx/libc/src/FileAndHeap.cpp @@ -317,12 +317,11 @@ FileStream* APS5_VABI fopen_nid_postfix(const char* filename, const char* mode) int APS5_VABI fopen_s_nid_postfix(FileStream** result, const char* filename, const char* mode) { constexpr int GuestEinval = 22; - constexpr int GuestEnoent = 2; if (!result) return GuestEinval; *result = nullptr; if (!filename || !mode) return GuestEinval; *result = fopen_nid_postfix(filename, mode); - return *result ? 0 : GuestEnoent; + return *result ? 0 : errno; } int APS5_VABI fclose_nid_postfix(FileStream* stream) { diff --git a/core/libs/tests/GuestFopenErrors.cpp b/core/libs/tests/GuestFopenErrors.cpp index df8d642204..5c72df7df0 100644 --- a/core/libs/tests/GuestFopenErrors.cpp +++ b/core/libs/tests/GuestFopenErrors.cpp @@ -12,6 +12,7 @@ extern "C" { void* APS5_VABI fopen_nid_postfix(const char* filename, const char* mode); int APS5_VABI fclose_nid_postfix(void* stream); void* APS5_VABI freopen_nid_postfix(const char* filename, const char* mode, void* stream); +int APS5_VABI fopen_s_nid_postfix(void** result, const char* filename, const char* mode); int* APS5_VABI __error_nid_postfix(); } @@ -27,6 +28,7 @@ void Require(bool value, int line) { constexpr int GuestEnoent = 2; constexpr int GuestEisdir = 21; +constexpr int GuestEinval = 22; constexpr int GuestEloop = 62; constexpr int GuestEnametoolong = 63; @@ -83,12 +85,23 @@ void Reopen() { Check(reopened != nullptr && fclose_nid_postfix(reopened) == 0); } +void CheckedOpen() { + void* stream = reinterpret_cast(1); + Check(fopen_s_nid_postfix(&stream, "loop", "r") == GuestEloop && stream == nullptr); + Check(fopen_s_nid_postfix(&stream, LongName.c_str(), "w") == GuestEnametoolong && stream == nullptr); + Check(fopen_s_nid_postfix(&stream, "folder", "w") == GuestEisdir && stream == nullptr); + Check(fopen_s_nid_postfix(&stream, "plain.txt", "z") == GuestEinval && stream == nullptr); + Check(fopen_s_nid_postfix(&stream, "missing.txt", "r") == GuestEnoent && stream == nullptr); + Check(fopen_s_nid_postfix(&stream, "plain.txt", "r") == 0 && stream != nullptr && fclose_nid_postfix(stream) == 0); +} + } int main() { const auto root = EnterFreshRoot(); Open(); Reopen(); + CheckedOpen(); std::error_code error; std::filesystem::current_path(std::filesystem::temp_directory_path(), error); std::filesystem::remove_all(root, error); diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 3921c33eee..5f972fb944 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -82,3 +82,4 @@ - [Call tracing](../../../core/libs/prx/libc/src/specifics/windows/CallTrace.cpp) (`--trace-imports`, `APS5_TRACE_CALLS`) exists on Windows only and records the entry of a call, not its arguments or its result. One process-wide ring keeps the last 65536 calls, so a thread that calls often pushes the others out. Calls are also counted per traced target since start-up; an import bound to a target already traced under another name is counted under the first name. The thunk saves the integer argument registers and `xmm0` to `xmm7`; arguments in wider vector registers pass only as long as the recording code stays free of AVX instructions, which is not tested - [_Dtest](../../../core/libs/prx/libc/src/Math.cpp) (libc) follows Dinkumware double classification matching _FDtest: finite values return -1, subnormals -2, zeros 0, infinities 1, and NaNs 2. - [app0.zar](../../../core/libs/prx/libc/src/AppArchive.cpp) (libc) - a [ZArchive](https://github.com/Exzap/ZArchive) beside `app0/` is a read-only layer under it: a path missing from `app0/` is looked up in the archive, case-insensitively. It is not a console format; titles ship their files in a PFS image instead. Resolving an archive-only path through any call other than the archive-aware libkernel ones throws, so `fopen`, `stat`, `access`, `opendir` and the rest do not read the archive yet, and `opendir` of a directory that exists in both places, `app0/` itself included, and `fdopen` of an archived descriptor throw instead of missing its entries or reading the null device. A damaged compressed block throws instead of returning `EIO` +- [fopen_s](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) - returns the errno of the failed open, as C11 K.3.5.2.1 leaves the nonzero value open and Microsoft's CRT returns errno; the console's Dinkumware `fopen_s` is not verified From 418d385c965267ac49e8f437e310906a00e0759c Mon Sep 17 00:00:00 2001 From: akkadbakkad89 Date: Sat, 17 Oct 2026 10:00:04 +0000 Subject: [PATCH 0684/1000] test(agc): cover Intel single-queue devices in recorder fixtures --- .../prx/libSceAgcDriver/tests/Recorder.cpp | 107 ++++++++++++------ 1 file changed, 72 insertions(+), 35 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 75800836fa..4f2406ea36 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -3854,6 +3854,11 @@ void importWatchTests(const Device& device) { std::cout << "host imports or write watching unavailable: the Windows import probe not tested\n"; return; } + if (context.settleQueue == VK_NULL_HANDLE || context.settleQueueMutex == nullptr) { + Require(PrepareImportWatch(context) == ImportWatch::Unwatch, "(w) a device without a settle queue kept host imports watched"); + std::cout << "no settle queue: the Windows import probe skipped\n"; + return; + } const auto probe = ProbeImportWriteProtection(context); Require(probe.failure == nullptr, std::string("(w) the Windows import probe failed at ") + (probe.failure != nullptr ? probe.failure : "") + " (" + std::to_string(static_cast(probe.result)) + ")"); std::cout << "Windows import probe: " << probe.writtenAtImport << " of " << probe.pages << " scratch pages reported written at the import, " << probe.writtenDuringSettle << " by its 4-byte settle read, " << probe.writtenAfterSettle << " after it, " << probe.writtenAfterSubmit << " after a full GPU read, " << probe.writtenAfterSecondSubmit << " after a second, " << probe.writtenAfterFill << " after a GPU write, " << probe.writtenByCpu << " after a CPU store\n"; @@ -5378,35 +5383,37 @@ class CoverPass { public: explicit CoverPass(const Context& context) : context(context), target(context, ColorTarget{0, {8, 8}, VK_FORMAT_R8G8B8A8_UNORM, 8 * 8 * 4, 0}, false) { try { - VkAttachmentDescription attachment{}; - attachment.format = VK_FORMAT_R8G8B8A8_UNORM; - attachment.samples = VK_SAMPLE_COUNT_1_BIT; - attachment.loadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; - attachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE; - attachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; - attachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; - attachment.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; - attachment.finalLayout = VK_IMAGE_LAYOUT_GENERAL; - const VkAttachmentReference color{0, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL}; - VkSubpassDescription subpass{}; - subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; - subpass.colorAttachmentCount = 1; - subpass.pColorAttachments = &color; - VkRenderPassCreateInfo passInfo{VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO}; - passInfo.attachmentCount = 1; - passInfo.pAttachments = &attachment; - passInfo.subpassCount = 1; - passInfo.pSubpasses = &subpass; - Check(context.Function("vkCreateRenderPass")(context.device, &passInfo, nullptr, &renderPass), "vkCreateRenderPass cover"); - const auto view = target.View(); - VkFramebufferCreateInfo framebufferInfo{VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO}; - framebufferInfo.renderPass = renderPass; - framebufferInfo.attachmentCount = 1; - framebufferInfo.pAttachments = &view; - framebufferInfo.width = 8; - framebufferInfo.height = 8; - framebufferInfo.layers = 1; - Check(context.Function("vkCreateFramebuffer")(context.device, &framebufferInfo, nullptr, &framebuffer), "vkCreateFramebuffer cover"); + if (!context.graphicsPipelineLibrary) { + VkAttachmentDescription attachment{}; + attachment.format = VK_FORMAT_R8G8B8A8_UNORM; + attachment.samples = VK_SAMPLE_COUNT_1_BIT; + attachment.loadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; + attachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE; + attachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; + attachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; + attachment.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + attachment.finalLayout = VK_IMAGE_LAYOUT_GENERAL; + const VkAttachmentReference color{0, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL}; + VkSubpassDescription subpass{}; + subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; + subpass.colorAttachmentCount = 1; + subpass.pColorAttachments = &color; + VkRenderPassCreateInfo passInfo{VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO}; + passInfo.attachmentCount = 1; + passInfo.pAttachments = &attachment; + passInfo.subpassCount = 1; + passInfo.pSubpasses = &subpass; + Check(context.Function("vkCreateRenderPass")(context.device, &passInfo, nullptr, &renderPass), "vkCreateRenderPass cover"); + const auto view = target.View(); + VkFramebufferCreateInfo framebufferInfo{VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO}; + framebufferInfo.renderPass = renderPass; + framebufferInfo.attachmentCount = 1; + framebufferInfo.pAttachments = &view; + framebufferInfo.width = 8; + framebufferInfo.height = 8; + framebufferInfo.layers = 1; + Check(context.Function("vkCreateFramebuffer")(context.device, &framebufferInfo, nullptr, &framebuffer), "vkCreateFramebuffer cover"); + } VkPipelineLayoutCreateInfo layoutInfo{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO}; Check(context.Function("vkCreatePipelineLayout")(context.device, &layoutInfo, nullptr, &layout), "vkCreatePipelineLayout cover"); const auto module = [&](const std::uint32_t* code, std::size_t bytes) { @@ -5453,7 +5460,12 @@ public: pipelineInfo.pMultisampleState = &multisample; pipelineInfo.pColorBlendState = &blendState; pipelineInfo.layout = layout; - pipelineInfo.renderPass = renderPass; + const VkFormat colorFormat = VK_FORMAT_R8G8B8A8_UNORM; + VkPipelineRenderingCreateInfoKHR renderingFormats{VK_STRUCTURE_TYPE_PIPELINE_RENDERING_CREATE_INFO_KHR}; + renderingFormats.colorAttachmentCount = 1; + renderingFormats.pColorAttachmentFormats = &colorFormat; + if (context.graphicsPipelineLibrary) pipelineInfo.pNext = &renderingFormats; + else pipelineInfo.renderPass = renderPass; Check(context.Function("vkCreateGraphicsPipelines")(context.device, VK_NULL_HANDLE, 1, &pipelineInfo, nullptr, &pipeline), "vkCreateGraphicsPipelines cover"); } catch (...) { release(); @@ -5470,11 +5482,35 @@ public: const auto commands = start.commands; if (!continued) { recorder.PrepareSampleSlot(); - VkRenderPassBeginInfo begin{VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO}; - begin.renderPass = renderPass; - begin.framebuffer = framebuffer; - begin.renderArea = {{0, 0}, {8, 8}}; - context.Function("vkCmdBeginRenderPass")(commands, &begin, VK_SUBPASS_CONTENTS_INLINE); + if (context.graphicsPipelineLibrary) { + if (!transitioned) { + transitioned = true; + VkImageMemoryBarrier barrier{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; + barrier.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; + barrier.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; + barrier.newLayout = VK_IMAGE_LAYOUT_GENERAL; + barrier.image = target.Image(); + barrier.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + context.Function("vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier); + } + VkRenderingAttachmentInfoKHR attachment{VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO_KHR}; + attachment.imageView = target.View(); + attachment.imageLayout = VK_IMAGE_LAYOUT_GENERAL; + attachment.loadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; + attachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE; + VkRenderingInfoKHR rendering{VK_STRUCTURE_TYPE_RENDERING_INFO_KHR}; + rendering.renderArea = {{0, 0}, {8, 8}}; + rendering.layerCount = 1; + rendering.colorAttachmentCount = 1; + rendering.pColorAttachments = &attachment; + context.Resolved(&DeviceFunctions::cmdBeginRendering, "vkCmdBeginRenderingKHR")(commands, &rendering); + } else { + VkRenderPassBeginInfo begin{VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO}; + begin.renderPass = renderPass; + begin.framebuffer = framebuffer; + begin.renderArea = {{0, 0}, {8, 8}}; + context.Function("vkCmdBeginRenderPass")(commands, &begin, VK_SUBPASS_CONTENTS_INLINE); + } } context.Function("vkCmdBindPipeline")(commands, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline); recorder.NoteSampledDraw(commands); @@ -5503,6 +5539,7 @@ private: } const Context& context; RenderTarget target; + mutable bool transitioned = false; VkRenderPass renderPass = VK_NULL_HANDLE; VkFramebuffer framebuffer = VK_NULL_HANDLE; VkPipelineLayout layout = VK_NULL_HANDLE; From 41ec785705da6c03ce6a4f63b15f0eb89be49e90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexej=20Szpev=C3=A1k?= Date: Sat, 17 Oct 2026 12:00:21 +0200 Subject: [PATCH 0685/1000] fix(libkernel): answer F_GETLK with F_UNLCK on Linux and macOS fcntl threw for F_GETLK with an F_UNLCK request on the hosts that use POSIX locks, as Linux's F_GETLK rejects that type. FreeBSD 11.4's kern_fcntl accepts it and lf_getlock reports the first overlapping lock that lf_blocks lets conflict: another owner's lock, one of the two being a write lock. For an F_UNLCK request that is exactly another owner's write lock, the same set an F_RDLCK request finds, so the host is asked with F_RDLCK and the reply is written back as for the other types. --- core/libs/prx/libkernel/File/src/DescriptorControl.cpp | 4 +--- core/libs/tests/GuestKernelFcntl.cpp | 7 ++++++- docs/dev/debt/libkernel.md | 2 +- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libkernel/File/src/DescriptorControl.cpp b/core/libs/prx/libkernel/File/src/DescriptorControl.cpp index 791af5ad09..174f0571b1 100644 --- a/core/libs/prx/libkernel/File/src/DescriptorControl.cpp +++ b/core/libs/prx/libkernel/File/src/DescriptorControl.cpp @@ -379,10 +379,8 @@ int RecordLock(int fd, int command, std::intptr_t argument) { struct flock host{}; if (lock.type == ReadLock) host.l_type = F_RDLCK; else if (lock.type == WriteLock) host.l_type = F_WRLCK; - else if (lock.type == Unlock) host.l_type = F_UNLCK; + else if (lock.type == Unlock) host.l_type = current == GetLock ? F_RDLCK : F_UNLCK; else return Fail(GuestEinval); - if (current == GetLock && lock.type == Unlock) - throw std::runtime_error("fcntl: F_GETLK with F_UNLCK is not implemented, fd=" + std::to_string(fd)); host.l_whence = lock.whence; host.l_start = static_cast(lock.start); host.l_len = static_cast(lock.length); diff --git a/core/libs/tests/GuestKernelFcntl.cpp b/core/libs/tests/GuestKernelFcntl.cpp index 679a739b64..912ce45b64 100644 --- a/core/libs/tests/GuestKernelFcntl.cpp +++ b/core/libs/tests/GuestKernelFcntl.cpp @@ -287,8 +287,13 @@ static void RecordLocksBelongToTheProcess() { Require(fcntl_nid_postfix(writer, SetLock, &release) == 0); Require(fcntl_nid_postfix(reader, SetLock, &release) == 0); -#ifdef _WIN32 Require(NoConflict(file, {0, 0, 0, Unlock, FromStart, 0})); + Require(NoConflict(reader, {2, 3, 0, Unlock, FromStart, 9})); + Require(sceKernelLseek(file, 7, 0) == 7); + GuestLock currentUnlock{-3, 2, 0, Unlock, FromCurrent, 0}; + Require(fcntl_nid_postfix(file, GetLock, ¤tUnlock) == 0); + Require(currentUnlock.type == Unlock && currentUnlock.start == 4 && currentUnlock.length == 2 && currentUnlock.whence == FromCurrent); +#ifdef _WIN32 GuestLock tooLong{std::numeric_limits::max(), 2, 0, WriteLock, FromStart, 0}; RequireFailure(fcntl_nid_postfix(file, SetLock, &tooLong), GuestEoverflow); RequireFailure(fcntl_nid_postfix(file, GetLock, &tooLong), GuestEoverflow); diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 5791f7ab5e..cc541401bc 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -185,7 +185,7 @@ - [sigaction](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) records the handler, flags and mask of every signal from 1 to 128 and reports them back, following FreeBSD's `sigaction` (`EINVAL` for other signals, and for a handler on `SIGKILL` or `SIGSTOP`); `signal` records its handler with `SA_RESTART`, as FreeBSD's libc does. Only handlers without `SA_SIGINFO` on the signals `signal` already delivers (`SIGINT`, `SIGILL`, `SIGABRT`, `SIGFPE`, `SIGSEGV`, `SIGTERM`) run, the same way `signal` handlers do; their mask and flags are not applied. `SA_SIGINFO` handlers are recorded but never called, since that needs FreeBSD's `siginfo_t` and `ucontext_t`, and neither are the handlers of other signals (`SIGBUS`, `SIGHUP`, ...), whether they are set by `sigaction` or by `signal`, which records them too (every number from 1 to 128, with `SA_RESTART` and an empty mask replacing what `sigaction` stored; `SIGKILL` and `SIGSTOP` accept only the default handler in both calls, as in FreeBSD's `kern_sigaction`). `raise` and `kill` accept every number from 1 to 128 now: a blocked one stays pending on the set of `sigtimedwait`, an unblocked one whose handler is registered runs it on the calling thread, and an unblocked one that has no host equivalent and no handler still fails with `EINVAL`, where FreeBSD runs its default action. `raise` of an `SA_SIGINFO` handler still throws instead of terminating - [kill, killpg](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) follow FreeBSD's `kern_kill` and `killpg1` for the only guest process: `EINVAL` for signals outside 0 to 128 and for negative `killpg` process groups, signal 0 only checks that the target exists, and its own pid, 0 and minus its own pid (the guest is taken to lead its own process group) signal it through `raise`, so they share `raise`'s limits above. Any other pid, and -1, which skips the caller, report `ESRCH`, so `EPERM` is never reported - [sceKernelFcntl](../../../core/libs/prx/libkernel/File/src/Open.cpp) (libkernel) runs the POSIX `fcntl` below and returns its guest errno as `SCE_KERNEL_ERROR_*`, the form the other `sceKernel` file exports use. The return values and error codes are unverified on a console -- [fcntl](../../../core/libs/prx/libkernel/File/src/DescriptorControl.cpp) (libkernel) follows FreeBSD 11.4's [`kern_fcntl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L479-L799) and [`kern_dup`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L812) on file, directory, pipe and character device descriptors; a socket keeps the libSceNet path (`F_GETFL`, `F_SETFL` with `O_NONBLOCK`, `F_GETFD` and `F_SETFD`, `EINVAL` for every other command, see the next entry). The close-on-exec flag and the status flags live in a guest table, the status flags shared by `dup`, `dup2` and `F_DUPFD` as in an open file description; a descriptor the table has not seen, such as 0 to 2, starts from the host's flags, since the console's are not known. `F_GETFL` takes the access mode from the host (the granted access of the handle on Windows), so pipe ends report `O_RDONLY` and `O_WRONLY` where FreeBSD's bidirectional pipes report `O_RDWR`. `F_DUPFD` and `F_DUPFD_CLOEXEC` return the lowest free host descriptor at or above the start, so the bound is the host table (`EMFILE` on Windows, `EINVAL` past `RLIMIT_NOFILE` on Linux), not the console's unknown `RLIMIT_NOFILE` and `kern.maxfilesperproc`. `O_NONBLOCK` switches a host pipe (`PIPE_NOWAIT` on Windows, where an empty read and a full write then report `EAGAIN`) and is only recorded on files, directories and the random device, as their reads never block; on a Windows console or other character device it throws. Changing `O_APPEND` on a Windows file, changing `O_FSYNC` on a file, `O_ASYNC` on a pipe or character device (`SIGIO` is not delivered), `F_GETOWN`, `F_SETOWN` and `F_SETLK_REMOTE` throw; `O_DIRECT`, `F_RDAHEAD` and `F_READAHEAD` are recorded as the hints they are. Record locks (`F_GETLK`, `F_SETLK`, `F_SETLKW` and their forms without `l_sysid`) use the host's POSIX locks on Linux, where `F_GETLK` with `F_UNLCK` throws. On Windows they follow [`kern_fcntl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L607-L752) and [`lf_advlockasync`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_lockf.c#L433-L487) for the one guest process, which owns every lock: `EOVERFLOW` for an `SEEK_CUR` start past `OFF_MAX`, then `EBADF` for `F_RDLCK` without read access or `F_WRLCK` without write access and `EINVAL` for another type, then `EINVAL` and `EOVERFLOW` for the range; as [`lf_blocks`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_lockf.c#L866) never lets locks of one owner conflict, whichever descriptor or thread asks, `F_SETLK` and `F_SETLKW` then succeed without waiting and `F_GETLK` reports `F_UNLCK`, so no lock list is kept and merging, splitting and the release on close have nothing to change. Nothing is held on the host, since Windows byte-range locks are mandatory and would fail the guest's own reads and writes through other descriptors, so other host processes neither see these locks nor are seen; a lock from the end of a directory throws. The return values and error codes are unverified on a console +- [fcntl](../../../core/libs/prx/libkernel/File/src/DescriptorControl.cpp) (libkernel) follows FreeBSD 11.4's [`kern_fcntl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L479-L799) and [`kern_dup`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L812) on file, directory, pipe and character device descriptors; a socket keeps the libSceNet path (`F_GETFL`, `F_SETFL` with `O_NONBLOCK`, `F_GETFD` and `F_SETFD`, `EINVAL` for every other command, see the next entry). The close-on-exec flag and the status flags live in a guest table, the status flags shared by `dup`, `dup2` and `F_DUPFD` as in an open file description; a descriptor the table has not seen, such as 0 to 2, starts from the host's flags, since the console's are not known. `F_GETFL` takes the access mode from the host (the granted access of the handle on Windows), so pipe ends report `O_RDONLY` and `O_WRONLY` where FreeBSD's bidirectional pipes report `O_RDWR`. `F_DUPFD` and `F_DUPFD_CLOEXEC` return the lowest free host descriptor at or above the start, so the bound is the host table (`EMFILE` on Windows, `EINVAL` past `RLIMIT_NOFILE` on Linux), not the console's unknown `RLIMIT_NOFILE` and `kern.maxfilesperproc`. `O_NONBLOCK` switches a host pipe (`PIPE_NOWAIT` on Windows, where an empty read and a full write then report `EAGAIN`) and is only recorded on files, directories and the random device, as their reads never block; on a Windows console or other character device it throws. Changing `O_APPEND` on a Windows file, changing `O_FSYNC` on a file, `O_ASYNC` on a pipe or character device (`SIGIO` is not delivered), `F_GETOWN`, `F_SETOWN` and `F_SETLK_REMOTE` throw; `O_DIRECT`, `F_RDAHEAD` and `F_READAHEAD` are recorded as the hints they are. Record locks (`F_GETLK`, `F_SETLK`, `F_SETLKW` and their forms without `l_sysid`) use the host's POSIX locks on Linux and macOS. `F_GETLK` with `F_UNLCK` asks the host with `F_RDLCK`: as in [`lf_blocks`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_lockf.c#L866) only another owner's overlapping write lock conflicts with either request. On Windows they follow [`kern_fcntl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L607-L752) and [`lf_advlockasync`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_lockf.c#L433-L487) for the one guest process, which owns every lock: `EOVERFLOW` for an `SEEK_CUR` start past `OFF_MAX`, then `EBADF` for `F_RDLCK` without read access or `F_WRLCK` without write access and `EINVAL` for another type, then `EINVAL` and `EOVERFLOW` for the range; as [`lf_blocks`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_lockf.c#L866) never lets locks of one owner conflict, whichever descriptor or thread asks, `F_SETLK` and `F_SETLKW` then succeed without waiting and `F_GETLK` reports `F_UNLCK`, so no lock list is kept and merging, splitting and the release on close have nothing to change. Nothing is held on the host, since Windows byte-range locks are mandatory and would fail the guest's own reads and writes through other descriptors, so other host processes neither see these locks nor are seen; a lock from the end of a directory throws. The return values and error codes are unverified on a console - [fcntl on a guest socket](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) answers `F_GETFD`, `F_SETFD`, `F_GETFL` and `F_SETFL` and fails every other command with `EINVAL`; FreeBSD 11.4's `kern_fcntl` also duplicates the descriptor (`F_DUPFD`, `F_DUPFD_CLOEXEC`) and handles `F_GETOWN` and `F_SETOWN` on a socket. `FD_CLOEXEC` is only recorded (there is no `exec`): `dup`, `dup2` and an accepted socket start without it, `SOCK_CLOEXEC` and `F_SETFD` set it. None of this was measured on a console. - [ioctl, _ioctl](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) handle only `FIONBIO` and `FIONREAD` on a socket, and any other request there fails with `ENOTTY`. On the other descriptors they follow FreeBSD's [`kern_ioctl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/sys_generic.c#L758), [`vn_ioctl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/vfs_vnops.c#L1488) and [`pipe_ioctl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/sys_pipe.c#L1346): `FIONBIO` sets `O_NONBLOCK` as `fcntl` does, `FIOCLEX` and `FIONCLEX` set and clear the close-on-exec flag `F_GETFD` reports, and `FIONREAD` reports the size past the offset of a file, the queued bytes of a pipe's read end, 0 on its write end and `ENOTTY` on the random device, while it throws on a directory or another character device. Every termios request (group `t`, `TIOCGWINSZ` included) fails with `ENOTTY`, since those descriptors are taken as not a terminal, as in `tcgetattr`, and any other request throws. Stremio Plus (PPSA74126) imports `_ioctl` for its statically linked `tc*` functions, which send `TIOCGETA`, `TIOCSETA`, `TIOCSPGRP` and the other termios requests - [Regular file mappings](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) (libkernel) - on Linux `mmap` maps a regular file's descriptor shared or private like a shared memory object, with the same access checks; SQLite's WAL index (`-shm`) in Dusklight PPSA99305 needs it. FreeBSD 11 `mmap(2)` and shadPS4's `MemoryManager::MapFile` map regular files; the console's behaviour is not measured. Other descriptors (pipes, sockets, directories, devices) still fail with `EOPNOTSUPP`, and Windows maps no files. From c7849f8495dc70432b5db0c421b33f8197974ba3 Mon Sep 17 00:00:00 2001 From: milescoviello Date: Sat, 17 Oct 2026 10:07:17 +0000 Subject: [PATCH 0686/1000] test(libkernel): check the timed locks against the holder's release, not elapsed time The waiter took the lock only after the holder unlocked, but under load it saw the holder's lock late and measured less than half the hold time. The holder now sets a flag just before it unlocks and the waiter requires it once its lock is taken. Co-Authored-By: Claude Opus 5.5 --- core/libs/tests/GuestPthreadTimedLocks.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/core/libs/tests/GuestPthreadTimedLocks.cpp b/core/libs/tests/GuestPthreadTimedLocks.cpp index 9a8de83ee7..ae2a8187fa 100644 --- a/core/libs/tests/GuestPthreadTimedLocks.cpp +++ b/core/libs/tests/GuestPthreadTimedLocks.cpp @@ -29,6 +29,7 @@ struct Holder { void* lock; bool mutex; std::atomic held{false}; + std::atomic releasing{false}; Pthread thread = nullptr; }; @@ -39,6 +40,7 @@ static void* APS5_VABI Hold(void* arg) { Require((holder.mutex ? pthread_mutex_lock_nid_postfix(mutex) : pthread_rwlock_wrlock_nid_postfix(rwlock)) == 0); holder.held.store(true); std::this_thread::sleep_for(HoldTime); + holder.releasing.store(true); Require((holder.mutex ? pthread_mutex_unlock_nid_postfix(mutex) : pthread_rwlock_unlock_nid_postfix(rwlock)) == 0); return nullptr; } @@ -48,9 +50,8 @@ static void RequireWaitsForHolder(void* lock, bool mutex, Lock acquire) { Holder holder{lock, mutex}; Require(scePthreadCreate(&holder.thread, nullptr, Hold, &holder, nullptr) == SCE_OK); while (!holder.held.load()) std::this_thread::yield(); - const auto start = std::chrono::steady_clock::now(); Require(acquire() == 0); - Require(std::chrono::steady_clock::now() - start >= HoldTime / 2); + Require(holder.releasing.load()); Require(scePthreadJoin(holder.thread, nullptr) == SCE_OK); } From 9ece09451555bba8373dfbe7c5d44974c626a104 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexej=20Szpev=C3=A1k?= Date: Sat, 17 Oct 2026 12:13:54 +0200 Subject: [PATCH 0687/1000] test(libkernel): cover F_GETLK against another process's record locks The fcntl tests only ever held locks in the guest's own process, where FreeBSD's lf_blocks never reports a conflict, so the host lock path that answers F_GETLK was not exercised. A forked process now holds a read lock, a write lock and a write lock to the end of the file through the host. F_GETLK with F_UNLCK reports only the write locks, F_RDLCK the same and F_WRLCK the read lock, each with the owner's pid and SEEK_SET; F_SETLK over the read lock fails with EAGAIN, and nothing is reported once the owner exits. --- core/libs/prx/libkernel/CMakeLists.txt | 5 + core/libs/tests/GuestRecordLockOwners.cpp | 148 ++++++++++++++++++++++ 2 files changed, 153 insertions(+) create mode 100644 core/libs/tests/GuestRecordLockOwners.cpp diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index ace5ed5a9d..dc2be42553 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -265,6 +265,11 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() if(UNIX) + add_test_executable(guest_record_lock_owners_tests ${LIBS_INCLUDE_DIR}/tests/GuestRecordLockOwners.cpp) + target_include_directories(guest_record_lock_owners_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_record_lock_owners_tests PRIVATE libkernel libc) + add_test(NAME guest_record_lock_owners COMMAND guest_record_lock_owners_tests) + set_tests_properties(guest_record_lock_owners PROPERTIES TIMEOUT 20) add_test_executable(guest_host_standard_error_tests ${LIBS_INCLUDE_DIR}/tests/GuestHostStandardError.cpp) target_include_directories(guest_host_standard_error_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_host_standard_error_tests PRIVATE libkernel libc) diff --git a/core/libs/tests/GuestRecordLockOwners.cpp b/core/libs/tests/GuestRecordLockOwners.cpp new file mode 100644 index 0000000000..cbffd82062 --- /dev/null +++ b/core/libs/tests/GuestRecordLockOwners.cpp @@ -0,0 +1,148 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libkernel/File/include/FileFlags.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceKernelOpen(const char* path, int flags, std::uint16_t mode); +int APS5_VABI sceKernelClose(int d); +int APS5_VABI fcntl_nid_postfix(int descriptor, int command, ...); +int* APS5_VABI __error_nid_postfix(); +} + +namespace { + +void Require(bool value, int line) { + if (!value) { + std::fprintf(stderr, "record lock owner check failed at line %d, errno %d\n", line, *__error_nid_postfix()); + std::exit(1); + } +} +#define Check(value) Require((value), __LINE__) + +constexpr int GetLock = 11; +constexpr int SetLock = 12; +constexpr std::int16_t ReadLock = 1; +constexpr std::int16_t Unlock = 2; +constexpr std::int16_t WriteLock = 3; +constexpr std::int16_t FromStart = 0; +constexpr int GuestEagain = 35; + +struct GuestLock { + std::int64_t start; + std::int64_t length; + std::int32_t pid; + std::int16_t type; + std::int16_t whence; + std::int32_t system; +}; +static_assert(sizeof(GuestLock) == 32); + +const char* const FileName = "owners.bin"; + +pid_t HoldLocks(int ready[2], int release[2]) { + const pid_t child = ::fork(); + Check(child >= 0); + if (child != 0) { + ::close(ready[1]); + ::close(release[0]); + return child; + } + ::close(ready[0]); + ::close(release[1]); + const int fd = ::open(FileName, O_RDWR); + struct flock read{}; + read.l_type = F_RDLCK; + read.l_whence = SEEK_SET; + read.l_start = 0; + read.l_len = 10; + struct flock write = read; + write.l_type = F_WRLCK; + write.l_start = 20; + write.l_len = 10; + struct flock tail = write; + tail.l_start = 50; + tail.l_len = 0; + const bool held = fd >= 0 && ::fcntl(fd, F_SETLK, &read) == 0 && ::fcntl(fd, F_SETLK, &write) == 0 && ::fcntl(fd, F_SETLK, &tail) == 0; + const char state = held ? 'y' : 'n'; + if (::write(ready[1], &state, 1) != 1) ::_exit(2); + char done = 0; + if (::read(release[0], &done, 1) != 1) ::_exit(3); + ::_exit(held ? 0 : 1); +} + +GuestLock Query(int fd, GuestLock request) { + Check(fcntl_nid_postfix(fd, GetLock, &request) == 0); + return request; +} + +bool Unchanged(const GuestLock& reply, const GuestLock& asked) { + return reply.type == Unlock && reply.start == asked.start && reply.length == asked.length && reply.pid == asked.pid && + reply.whence == asked.whence && reply.system == asked.system; +} + +bool Reports(const GuestLock& reply, std::int16_t type, std::int64_t start, std::int64_t length, pid_t owner) { + return reply.type == type && reply.start == start && reply.length == length && reply.pid == owner && reply.whence == FromStart; +} + +void OtherOwner() { + int ready[2]; + int release[2]; + Check(::pipe(ready) == 0 && ::pipe(release) == 0); + const pid_t child = HoldLocks(ready, release); + char state = 0; + Check(::read(ready[0], &state, 1) == 1 && state == 'y'); + + const int fd = sceKernelOpen(FileName, SCE_KERNEL_O_RDWR, 0); + Check(fd >= 0); + + const GuestLock readRange{0, 10, 7, Unlock, FromStart, 3}; + Check(Unchanged(Query(fd, readRange), readRange)); + Check(Reports(Query(fd, {0, 0, 0, Unlock, FromStart, 0}), WriteLock, 20, 10, child)); + Check(Reports(Query(fd, {25, 1, 0, Unlock, FromStart, 0}), WriteLock, 20, 10, child)); + Check(Reports(Query(fd, {60, 5, 0, Unlock, FromStart, 0}), WriteLock, 50, 0, child)); + const GuestLock between{10, 10, 0, Unlock, FromStart, 0}; + Check(Unchanged(Query(fd, between), between)); + + const GuestLock sharedRead{0, 15, 0, ReadLock, FromStart, 0}; + Check(Unchanged(Query(fd, sharedRead), sharedRead)); + Check(Reports(Query(fd, {0, 15, 0, WriteLock, FromStart, 0}), ReadLock, 0, 10, child)); + Check(Reports(Query(fd, {22, 2, 0, ReadLock, FromStart, 0}), WriteLock, 20, 10, child)); + + GuestLock blocked{5, 1, 0, WriteLock, FromStart, 0}; + *__error_nid_postfix() = 0; + Check(fcntl_nid_postfix(fd, SetLock, &blocked) == -1 && *__error_nid_postfix() == GuestEagain); + + const char done = 'd'; + Check(::write(release[1], &done, 1) == 1); + int status = 0; + Check(::waitpid(child, &status, 0) == child && WIFEXITED(status) && WEXITSTATUS(status) == 0); + + const GuestLock afterExit{0, 0, 0, Unlock, FromStart, 0}; + Check(Unchanged(Query(fd, afterExit), afterExit)); + Check(sceKernelClose(fd) == 0); +} + +} + +int main() { + const auto root = std::filesystem::temp_directory_path() / + ("anyps5-record-lock-owners-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); + std::filesystem::create_directories(root); + std::filesystem::current_path(root); + { std::ofstream file(FileName, std::ios::binary); file << std::string(100, 'x'); } + OtherOwner(); + std::error_code error; + std::filesystem::current_path(std::filesystem::temp_directory_path(), error); + std::filesystem::remove_all(root, error); + return 0; +} From 40f4506659a8b864a5635371719c1de2c9615814 Mon Sep 17 00:00:00 2001 From: mrfelek <17274927+mrfelek@users.noreply.github.com> Date: Sat, 17 Oct 2026 11:51:13 +0200 Subject: [PATCH 0688/1000] fix(libSceNgs2): let render skip a grain while another thread holds the system lock Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01BywPXoZACVi8ALV4JKvuPZ --- .../libSceNgs2.native/src/Ngs2Internal.hpp | 1 + .../libs/prx/libSceNgs2.native/src/Render.cpp | 15 +++- .../libs/prx/libSceNgs2.native/src/System.cpp | 81 +++++++++++++++++-- .../libSceNgs2.native/tests/Ngs2Render.cpp | 42 +++++++++- docs/dev/debt/libSceNgs2.native.md | 2 +- 5 files changed, 128 insertions(+), 13 deletions(-) diff --git a/core/libs/prx/libSceNgs2.native/src/Ngs2Internal.hpp b/core/libs/prx/libSceNgs2.native/src/Ngs2Internal.hpp index 9100c4ee6e..e71d56b87c 100644 --- a/core/libs/prx/libSceNgs2.native/src/Ngs2Internal.hpp +++ b/core/libs/prx/libSceNgs2.native/src/Ngs2Internal.hpp @@ -209,6 +209,7 @@ void Ngs2ClearReverb(Ngs2Voice& voice); bool Ngs2ProcessReverb(Ngs2Voice& voice, std::uint32_t grain, std::uint32_t sampleRate); void Ngs2ProcessLegacyUserFx(Ngs2Voice& voice, std::uint32_t grain, std::uint32_t sampleRate); void Ngs2RenderSystem(Ngs2System& system, const Ngs2RenderBufferInfo* bufferInfo, std::uint32_t numBufferInfo); +void Ngs2RenderSilence(const Ngs2System& system, const Ngs2RenderBufferInfo* bufferInfo, std::uint32_t numBufferInfo); float Ngs2DefaultLevel(std::uint32_t sourceChannels, std::uint32_t source, std::uint32_t destChannels, std::uint32_t dest); int Ngs2PanInit(Ngs2PanWork* work, const float* speaker_angles, float unit_angle, uint32_t num_speakers); int Ngs2PanGetVolumeMatrix(Ngs2PanWork* work, const Ngs2PanParam* params, uint32_t num_params, uint32_t matrix_format, float* out_volume_matrix); diff --git a/core/libs/prx/libSceNgs2.native/src/Render.cpp b/core/libs/prx/libSceNgs2.native/src/Render.cpp index c0e983abaf..a3ec266e0a 100644 --- a/core/libs/prx/libSceNgs2.native/src/Render.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Render.cpp @@ -298,7 +298,7 @@ static void WriteOutput(const Ngs2RenderBufferInfo& output, const std::vector OutputBytes(const Ngs2System& system, const Ngs2RenderBufferInfo* bufferInfo, std::uint32_t numBufferInfo) { const auto grain = system.option.num_grain_samples; std::vector outputBytes(numBufferInfo); for (std::uint32_t i = 0; i < numBufferInfo; i++) { @@ -310,6 +310,19 @@ void Ngs2RenderSystem(Ngs2System& system, const Ngs2RenderBufferInfo* bufferInfo outputBytes[i] = static_cast(grain) * output.num_channels * SampleBytes(output.waveform_type); if (output.buffer_size < outputBytes[i]) throw std::invalid_argument("NGS2: render buffer " + std::to_string(i) + " is too small"); } + return outputBytes; +} + +void Ngs2RenderSilence(const Ngs2System& system, const Ngs2RenderBufferInfo* bufferInfo, std::uint32_t numBufferInfo) { + const auto outputBytes = OutputBytes(system, bufferInfo, numBufferInfo); + for (std::uint32_t i = 0; i < numBufferInfo; i++) { + if (outputBytes[i] != 0) std::memset(bufferInfo[i].buffer, 0, outputBytes[i]); + } +} + +void Ngs2RenderSystem(Ngs2System& system, const Ngs2RenderBufferInfo* bufferInfo, std::uint32_t numBufferInfo) { + const auto grain = system.option.num_grain_samples; + const auto outputBytes = OutputBytes(system, bufferInfo, numBufferInfo); std::vector voices; for (auto* rack : system.racks) { for (auto& voice : rack->voices) { diff --git a/core/libs/prx/libSceNgs2.native/src/System.cpp b/core/libs/prx/libSceNgs2.native/src/System.cpp index cf861a6ae8..17e696d525 100644 --- a/core/libs/prx/libSceNgs2.native/src/System.cpp +++ b/core/libs/prx/libSceNgs2.native/src/System.cpp @@ -1,13 +1,16 @@ #include +#include #include #include #include #include +#include #include #include #include #include #include +#include #include #include "prx/libc/include/General.hpp" @@ -30,6 +33,34 @@ std::recursive_mutex& Ngs2Mutex() { return *mutex; } +struct SystemLockState { + std::thread::id owner; + std::uint32_t depth = 0; +}; + +static std::mutex& SystemLockMutex() { + static auto* const mutex = new std::mutex(); + return *mutex; +} + +static std::condition_variable& SystemLockReleased() { + static auto* const released = new std::condition_variable(); + return *released; +} + +static std::map& SystemLocks() { + static auto* const locks = new std::map(); + return *locks; +} + +static void ReleaseSystemLock(SystemLockState& state) { + if (state.depth == 0) return; + if (--state.depth == 0) { + state.owner = {}; + SystemLockReleased().notify_all(); + } +} + Ngs2System* Ngs2FindSystem(Ngs2Handle handle) { auto& systems = Systems(); const auto found = std::find(systems.begin(), systems.end(), reinterpret_cast(handle)); @@ -110,6 +141,8 @@ static int CreateSystem(const Ngs2SystemOption& option, const Ngs2ContextBufferI system->uid = nextUid++; Systems().push_back(system); *handle = reinterpret_cast(system); + std::lock_guard lock(SystemLockMutex()); + SystemLocks()[*handle] = {}; return SCE_NGS2_OK; } @@ -151,6 +184,11 @@ int APS5_VABI sceNgs2SystemDestroy(uintptr_t system_handle, Ngs2ContextBufferInf if (result != SCE_NGS2_OK) return result; } std::erase(Systems(), system); + { + std::lock_guard locks(SystemLockMutex()); + SystemLocks().erase(system_handle); + SystemLockReleased().notify_all(); + } const auto bufferInfo = system->bufferInfo; const auto allocator = system->allocator; std::destroy_at(system); @@ -223,16 +261,25 @@ int APS5_VABI sceNgs2SystemGetUserData(uintptr_t system_handle, uintptr_t* user_ } int APS5_VABI sceNgs2SystemLock(uintptr_t system_handle) { - Ngs2Mutex().lock(); - if (Ngs2FindSystem(system_handle) != nullptr) return SCE_NGS2_OK; - Ngs2Mutex().unlock(); - return SCE_NGS2_ERROR_INVALID_SYSTEM_HANDLE; + const auto self = std::this_thread::get_id(); + std::unique_lock lock(SystemLockMutex()); + auto& locks = SystemLocks(); + SystemLockReleased().wait(lock, [&] { + const auto found = locks.find(system_handle); + return found == locks.end() || found->second.depth == 0 || found->second.owner == self; + }); + const auto found = locks.find(system_handle); + if (found == locks.end()) return SCE_NGS2_ERROR_INVALID_SYSTEM_HANDLE; + found->second.owner = self; + found->second.depth++; + return SCE_NGS2_OK; } int APS5_VABI sceNgs2SystemUnlock(uintptr_t system_handle) { - std::lock_guard lock(Ngs2Mutex()); - if (Ngs2FindSystem(system_handle) == nullptr) return SCE_NGS2_ERROR_INVALID_SYSTEM_HANDLE; - Ngs2Mutex().unlock(); + std::lock_guard lock(SystemLockMutex()); + const auto found = SystemLocks().find(system_handle); + if (found == SystemLocks().end()) return SCE_NGS2_ERROR_INVALID_SYSTEM_HANDLE; + ReleaseSystemLock(found->second); return SCE_NGS2_OK; } @@ -241,6 +288,26 @@ int APS5_VABI sceNgs2SystemRender(uintptr_t system_handle, const Ngs2RenderBuffe auto* system = Ngs2FindSystem(system_handle); if (system == nullptr) return SCE_NGS2_ERROR_INVALID_SYSTEM_HANDLE; if (buffer_info == nullptr || num_buffer_info == 0) APS5_INVALID_ARG_EX; + SystemLockState* state = nullptr; + { + const auto self = std::this_thread::get_id(); + std::lock_guard locks(SystemLockMutex()); + auto& found = SystemLocks().at(system_handle); + if (found.depth != 0 && found.owner != self) { + Ngs2RenderSilence(*system, buffer_info, num_buffer_info); + return SCE_NGS2_OK; + } + found.owner = self; + found.depth++; + state = &found; + } + struct Release { + SystemLockState* state; + ~Release() { + std::lock_guard locks(SystemLockMutex()); + ReleaseSystemLock(*state); + } + } release{state}; Ngs2RenderSystem(*system, buffer_info, num_buffer_info); return SCE_NGS2_OK; } diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp index 8eff47b9cb..c84a09a812 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp @@ -881,23 +881,57 @@ static void TestStereoIntoSurround() { static void TestLock() { const auto system = CreateSystem(); + const auto voice = Voice(CreateRack(system, SCE_NGS2_RACK_ID_SAMPLER)); Require(sceNgs2SystemLock(0x1234) == SCE_NGS2_ERROR_INVALID_SYSTEM_HANDLE); Require(sceNgs2SystemUnlock(0x1234) == SCE_NGS2_ERROR_INVALID_SYSTEM_HANDLE); - Ngs2SystemInfo info{}; - std::thread free([&] { Require(sceNgs2SystemGetInfo(system, &info, sizeof(info)) == SCE_NGS2_OK); }); - free.join(); + Require(sceNgs2SystemUnlock(system) == SCE_NGS2_OK); + Ngs2SystemInfo info{}; Require(sceNgs2SystemLock(system) == SCE_NGS2_OK); + Require(sceNgs2SystemLock(system) == SCE_NGS2_OK); + std::atomic otherDone = false; + std::thread other([&] { + Ngs2SystemInfo otherInfo{}; + std::uint32_t flags = 0; + Require(sceNgs2SystemGetInfo(system, &otherInfo, sizeof(otherInfo)) == SCE_NGS2_OK); + Require(sceNgs2VoiceGetStateFlags(voice, &flags) == SCE_NGS2_OK); + std::vector out(Grain * 2, -1); + const Ngs2RenderBufferInfo buffer{out.data(), out.size() * sizeof(std::int16_t), SCE_NGS2_WAVEFORM_TYPE_PCM_I16L, 1}; + Require(sceNgs2SystemRender(system, &buffer, 1) == SCE_NGS2_OK); + Require(std::all_of(out.begin(), out.begin() + Grain, [](std::int16_t sample) { return sample == 0; })); + Require(std::all_of(out.begin() + Grain, out.end(), [](std::int16_t sample) { return sample == -1; })); + Require(sceNgs2SystemGetInfo(system, &otherInfo, sizeof(otherInfo)) == SCE_NGS2_OK && otherInfo.render_count == 0); + otherDone = true; + }); + for (int i = 0; i < 200 && !otherDone; i++) std::this_thread::sleep_for(std::chrono::milliseconds(10)); + Require(otherDone); + other.join(); + RenderI16(system); + Require(sceNgs2SystemGetInfo(system, &info, sizeof(info)) == SCE_NGS2_OK && info.render_count == 1); + std::atomic done = false; std::thread blocked([&] { - Require(sceNgs2SystemGetInfo(system, &info, sizeof(info)) == SCE_NGS2_OK); + Require(sceNgs2SystemLock(system) == SCE_NGS2_OK); done = true; + RenderI16(system); + Require(sceNgs2SystemUnlock(system) == SCE_NGS2_OK); }); std::this_thread::sleep_for(std::chrono::milliseconds(100)); Require(!done); Require(sceNgs2SystemUnlock(system) == SCE_NGS2_OK); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + Require(!done); + Require(sceNgs2SystemUnlock(system) == SCE_NGS2_OK); blocked.join(); Require(done); + Require(sceNgs2SystemGetInfo(system, &info, sizeof(info)) == SCE_NGS2_OK && info.render_count == 2); + + Require(sceNgs2SystemLock(system) == SCE_NGS2_OK); + std::thread unlocker([&] { Require(sceNgs2SystemUnlock(system) == SCE_NGS2_OK); }); + unlocker.join(); + std::thread renderer([&] { RenderI16(system); }); + renderer.join(); + Require(sceNgs2SystemGetInfo(system, &info, sizeof(info)) == SCE_NGS2_OK && info.render_count == 3); Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); } diff --git a/docs/dev/debt/libSceNgs2.native.md b/docs/dev/debt/libSceNgs2.native.md index 3d04676b7f..313ff6bb86 100644 --- a/docs/dev/debt/libSceNgs2.native.md +++ b/docs/dev/debt/libSceNgs2.native.md @@ -18,7 +18,7 @@ - [NGS2 custom module ids](../../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (libSceNgs2.native) - a custom rack with a module id other than 0x13 (sampler rack), 0x1d and 0x1f (submixer rack) throws. Trails in the Sky 2nd Chapter (PPSA35169) creates a rack with module id 0x20 (`NGS2: custom module id 0x20 is not implemented`, #4246); which module it is, and its structure, are unknown. - [NGS2 reverb rack](../../../core/libs/prx/libSceNgs2.native/src/Reverb.cpp) (libSceNgs2.native, rack 0x2001) - follows the PS4 firmware 11.00 `libSceNgs2.sprx`, read and checked against its output at 11.025 to 48 kHz, including parameter changes during playback (differences at float rounding level); not yet compared with the PS5 library. Structure layouts come from [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `ngs2_reverb.h` and the param ids 0x20010000 (setup) and 0x20010001 (I3DL2) from PPSA22520. Only the default `reverb_size` of 1 is handled, with its 14400-sample reflection buffer; other sizes and the library's half-rate mode throw. System sample rates below about 7.5 kHz, where an allpass delay would have no samples, throw. Setup only accepts equal input and output channel counts - [sceNgs2ReportRegisterHandler, sceNgs2ReportUnregisterHandler](../../../core/libs/prx/libSceNgs2.native/src/Unimplemented.cpp) (libSceNgs2.native, NIDs `uBIN24Tv2MI`, `nPzb7Ly-VjE`) are throwing stubs. The signatures (report type, handler, user data, out handle) and (report handle) follow [SharpProspero](https://github.com/SvenGDK/SharpProspero) and [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `ngs2_report.h`. No report is produced yet, so registering a handler cannot be honoured. PPSA04540 imports both and registers a CPU load handler (type 5) -- [sceNgs2SystemSetSampleRate, sceNgs2SystemSetUserData, sceNgs2SystemGetUserData, sceNgs2SystemLock, sceNgs2SystemUnlock](../../../core/libs/prx/libSceNgs2.native/src/System.cpp) (libSceNgs2.native) - shadPS4 only checks the system handle (`INVALID_SYSTEM_HANDLE`) and SharpProspero names what each call does; neither gives the other limits. A sample rate of 0 and a null user data pointer throw, any other rate is accepted, and changing the rate while a sampler filter is enabled throws because the filter coefficients were computed for the old rate. Lock and unlock take the same lock as `sceNgs2RackLock` +- [sceNgs2SystemSetSampleRate, sceNgs2SystemSetUserData, sceNgs2SystemGetUserData, sceNgs2SystemLock, sceNgs2SystemUnlock](../../../core/libs/prx/libSceNgs2.native/src/System.cpp) (libSceNgs2.native) - shadPS4 only checks the system handle (`INVALID_SYSTEM_HANDLE`) and SharpProspero names what each call does; neither gives the other limits. A sample rate of 0 and a null user data pointer throw, any other rate is accepted, and changing the rate while a sampler filter is enabled throws because the filter coefficients were computed for the old rate. `sceNgs2SystemLock` and `sceNgs2SystemUnlock` follow [prosper's notes](https://github.com/mattias800/prosper/blob/d61eb2c0ad2de879ef74df9c4aa050c512804a18/prosper/src/hle/audio/hle_audio.cpp#L4367-L4384) on the PS5 module: Lock takes a per-system lock and counts nested holds, Unlock releases one hold from any thread and returns 0 without a hold, and `sceNgs2SystemRender` only tries the lock: while another thread holds it, Render clears one grain of each output, leaves the render count and voices unchanged and returns 0. The other calls do not wait for the system lock. Peace Walker (PPSA18216) holds it while it spins on a flag that its audio thread clears only after `sceNgs2SystemRender` and eight `sceNgs2VoiceGetStateFlags` calls, so on the console neither can wait for it. Whether the console's voice and rack calls take the lock at all is not known. `sceNgs2RackLock` still holds the library's lock until `sceNgs2RackUnlock`, as before - [sceNgs2RackGetInfo](../../../core/libs/prx/libSceNgs2.native/src/Rack.cpp) (libSceNgs2.native) - field order from [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `OrbisNgs2RackInfo` with the 64-byte name of the other info structures (216 bytes); `type` is assumed to be the rack id divided by 0x1000, `min_grain_samples` 64 (the system minimum) and `state_flags` 1; `uid` comes from a per-process rack counter, `render_count` is the owning system's render count and `active_voice_count` counts voices that are not empty (paused and stopped voices included). The last process ratio and tick and `active_channel_work_count` are left zero - [sceNgs2VoiceQueryInfo](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - only info id 0x4001 is known: PPSA22520 passes an 8-byte buffer and branches on the first word being 1 or 2, so it is assumed to be the voice channel count (0 before the voice is set up); the second word is zeroed and other ids throw - [sceNgs2VoiceControl patch](../../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - a destination rack handle with input id 0 resolves to that rack's first voice; this matches the Dead Cells call in #4334, but selection for racks with multiple voices has not been checked against a console. Nonzero destination input ids still throw. `sceNgs2VoiceGetPortInfo` reports the resolved voice handle and zero delay. From f2d3980f5ee80db83e6270f5b13992670e2f7ec9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexej=20Szpev=C3=A1k?= Date: Sat, 17 Oct 2026 12:28:11 +0200 Subject: [PATCH 0689/1000] fix(libkernel): report free send space for socket write events on Linux A socket's EVFILT_WRITE and sceKernelAddWriteEvent reported the whole SO_SNDBUF in data, however much was already queued. FreeBSD 11.4's filt_sowrite reports sbspace, the space still free. On Linux the queued bytes from SIOCOUTQ are now taken off SO_SNDBUF. Windows has no query for the queued bytes and macOS is not wired up, so both keep reporting SO_SNDBUF; the debt entry says so. --- core/libs/prx/libkernel/Socket/src/Udp.cpp | 7 +++++++ docs/dev/debt/libkernel.md | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index 5c6fc0b521..f6fe09d817 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -7,6 +7,9 @@ #else #include #include +#ifdef __linux__ +#include +#endif #include #include #include @@ -740,6 +743,10 @@ bool GuestSockets::Ready(int descriptor, bool write, std::int64_t* data, bool* e int size = 0; socklen_t length = sizeof(size); if (getsockopt(socket->value, SOL_SOCKET, SO_SNDBUF, reinterpret_cast(&size), &length) == 0) *data = size; +#ifdef __linux__ + int queued = 0; + if (*data > 0 && ::ioctl(socket->value, SIOCOUTQ, &queued) == 0) *data = std::max(0, *data - queued); +#endif } else { #ifdef _WIN32 unsigned long available = 0; diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 5791f7ab5e..2c90bba7e3 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -126,7 +126,7 @@ - [pthread_mutexattr_getpshared and pthread_mutexattr_setpshared](../../../core/libs/prx/libkernel/Pthread/Posix/Mutex.cpp) (libkernel) store and report private or shared attributes, following FreeBSD 11 libthr [thr_mutexattr.c](https://github.com/freebsd/freebsd-src/blob/stable/11/lib/libthr/thread/thr_mutexattr.c). Invalid values return `EINVAL` without changing the attribute. Guest threads execute in one host process; sharing mutexes across processes is not modeled. Console behavior and invalid-attribute error precedence are unverified. - [pthread_attr_getinheritsched, pthread_attr_setinheritsched, pthread_attr_getscope, pthread_attr_setscope, pthread_attr_getstackaddr](../../../core/libs/prx/libkernel/Pthread/Posix/Attr.cpp) (libkernel) follow FreeBSD 11 libthr ([thr_attr.c](https://github.com/freebsd/freebsd-src/blob/5ed7eb0d97ba4436218e810f61bd059acba984c2/lib/libthr/thread/thr_attr.c), [thr_create.c](https://github.com/freebsd/freebsd-src/blob/5ed7eb0d97ba4436218e810f61bd059acba984c2/lib/libthr/thread/thr_create.c)), not measured on the console: the scope defaults to `PTHREAD_SCOPE_SYSTEM` (2), only it and `PTHREAD_SCOPE_PROCESS` (0) are accepted and other values return `EINVAL`, and an inherit value other than `PTHREAD_INHERIT_SCHED` (4) or `PTHREAD_EXPLICIT_SCHED` (0) returns `ENOTSUP`. `pthread_attr_getstackaddr` returns the configured stack address pointer (null by default). A null output of `pthread_attr_getinheritsched` throws, since libthr writes through it unchecked. A thread keeps the scope it was created with, taken from the creating thread under `PTHREAD_INHERIT_SCHED`, and `pthread_attr_get_np` and `scePthreadAttrGet` report it, but every guest thread is a host thread at system scope, so process scope changes nothing - [Shared memory](../../../core/libs/prx/libkernel/File/src/SharedMemory.cpp) (libkernel) on Linux implements anonymous and named objects, bounded descriptor writes and seeks, and `shm_unlink` with effective read/write permission checks from FreeBSD 11. Named paths up to 1023 bytes use `/anyps5-shm-` plus SHA-1; collisions are not detected, and the namespace is shared across processes in the host mount namespace. Named creation applies the guest umask without changing the host mask. Linux procfs and `/dev/shm` are required. Operations use an advisory directory lock; external host operations bypass it, unrelated objects serialize, and descriptor operations require two spare host descriptors and compare device/inode with the initial descriptor identity on both the pinned-object and ordinary-file fallback paths. [Guest mappings](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) support shared and private mappings of these descriptors through the allocation tracker and pin a duplicate descriptor during validation and mapping; mapping requires one spare host descriptor; file-backed mappings use the write-watch scan fallback because aliases and descriptor writes bypass per-mapping tracking. Named objects and file-backed mappings on Windows remain unsupported. Invalid non-null name pointers, console behaviour and title compatibility remain unverified. -- [sceKernelAddReadEvent and sceKernelAddWriteEvent](../../../core/libs/prx/libkernel/Equeue/Equeue.cpp) (libkernel): a write event reports the socket's send buffer size (`SO_SNDBUF`) in `data`, not the free space left in it, as Windows has no query for that space. Only guest sockets are supported; other descriptors and a low watermark above 1 throw +- [sceKernelAddReadEvent and sceKernelAddWriteEvent](../../../core/libs/prx/libkernel/Equeue/Equeue.cpp) (libkernel): a write event reports the free space of the socket's send buffer in `data`, as FreeBSD 11.4's [`filt_sowrite`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c#L3358-L3382) reports `sbspace`: on Linux the host's `SO_SNDBUF` less the bytes `SIOCOUTQ` counts as queued, which include the host's per-packet overhead, so the space shrinks by somewhat more than the bytes sent. Windows has no query for that space and macOS is not wired up, so both report the whole `SO_SNDBUF`, which does not shrink as data is queued. Only guest sockets are supported; other descriptors and a low watermark above 1 throw - [close](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) and [setsockopt](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) reach a [libSceNet](../../../core/libs/prx/libSceNet/Export.cpp) socket through hooks libSceNet registers, as `select` polls them; the other libkernel socket calls do not. libSceNet numbers its sockets from 32 in its own table, so a CRT file descriptor and a socket can share a number; `close` then closes the socket. On the console both are one kernel descriptor space - [sceKernelPread, sceKernelPwrite, sceKernelPreadv, sceKernelPwritev](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) on Windows transfer at the given offset with an overlapped `ReadFile`/`WriteFile` and restore the descriptor's file pointer afterwards. Guest scalar transfers, positioned transfers, vectored transfers and seeks share a position lock across duplicated descriptors; a vector holds it for the whole operation. Independently opened descriptors and other processes do not share that lock, so vector transfers over their overlapping file ranges are not coordinated. Handles that are not disk files return `ESPIPE`. A pipe or console `readv` can return a short read when no more input is available; each iovec is transferred separately, so a failure after the first transfer returns the bytes already done - [sceKernelAioSubmitReadCommands, sceKernelAioSubmitWriteCommands and their Multiple variants](../../../core/libs/prx/libkernel/Aio/src/Aio.cpp) (libkernel) on Windows transfer through the same positioned path as `sceKernelPread`/`sceKernelPwrite` and restore the descriptor's file pointer afterwards; positioned transfers, plain transfers and seeks of one descriptor are serialized per descriptor (shared across duplicated descriptors), so a concurrent transfer observes a stable file pointer From 31cd0e26042ab046b3ccb7a0741d93c74065ad99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexej=20Szpev=C3=A1k?= Date: Sat, 17 Oct 2026 12:28:11 +0200 Subject: [PATCH 0690/1000] test(libkernel): check socket write event space shrinks and recovers On a Unix stream socketpair the write event's data drops by at least the bytes sent while the peer has not read them, and comes back to the idle value once the peer drains them. Off Linux the test only checks that the value stays positive and never grows. --- core/libs/tests/GuestKqueue.cpp | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/core/libs/tests/GuestKqueue.cpp b/core/libs/tests/GuestKqueue.cpp index 593c82d059..9c68a0d221 100644 --- a/core/libs/tests/GuestKqueue.cpp +++ b/core/libs/tests/GuestKqueue.cpp @@ -263,6 +263,24 @@ static void SocketPair() { Require(Wait(kq, events, 4) == 1 && events[0].filter == EvfiltWrite); RequireFailure(Apply(kq, Change(static_cast(pair[1]), EvfiltWrite, EvDelete)), GuestEnoent); Require(Throws([kq, &pair] { Apply(kq, Change(static_cast(pair[1]), EvfiltRead, EvAdd | EvClear)); })); + const int space = kqueue_nid_postfix(); + Require(Apply(space, Change(static_cast(pair[1]), EvfiltWrite, EvAdd)) == 0); + Require(Poll(space, events, 4) == 1 && events[0].filter == EvfiltWrite); + const std::intptr_t idle = events[0].data; + Require(idle > 0); + const std::string payload(1000, 'p'); + Require(send_nid_postfix(pair[1], payload.data(), payload.size(), 0) == static_cast(payload.size())); + Require(Poll(space, events, 4) == 1 && events[0].filter == EvfiltWrite); +#if defined(__linux__) + Require(events[0].data <= idle - static_cast(payload.size())); +#else + Require(events[0].data > 0 && events[0].data <= idle); +#endif + std::string drained(payload.size(), '\0'); + Require(recv_nid_postfix(pair[0], drained.data(), drained.size(), 0) == static_cast(drained.size())); + Require(Poll(space, events, 4) == 1 && events[0].filter == EvfiltWrite && events[0].data == idle); + Require(Apply(space, Change(static_cast(pair[1]), EvfiltWrite, EvDelete)) == 0); + Require(close_nid_postfix(space) == 0); Require(close_nid_postfix(pair[0]) == 0); RequireFailure(Apply(kq, Change(static_cast(pair[0]), EvfiltRead, EvDelete)), GuestEbadf); Require(close_nid_postfix(pair[1]) == 0); From 39c23664bd40ef0fd0133af5e4b1e211c631d77e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20=C5=A0l=C3=A9gl?= Date: Sat, 17 Oct 2026 08:59:29 +0200 Subject: [PATCH 0691/1000] fix(agc): let the storage image cache budget follow the heap's use like the sampled one StorageTextureBudget gave the storage image cache a quarter of the VK_EXT_memory_budget budget of the largest device-local heap (capped at TextureCacheBudget, never below 512 MiB) whatever else the heap held, while SampledTextureBudget takes what that budget leaves after the heap's other use. On a 10 GB card the storage cache kept 2024-2170 MiB into a level load whose depth and render target allocations then failed with VK_ERROR_OUT_OF_DEVICE_MEMORY with the heap at its reported budget where a run logged it. The storage image budget is now what the reported budget leaves after the heap's use other than the cached storage images, the unused part of the sampled textures' 2 GiB floor and an eighth of the budget; the caps at a quarter of the budget and at TextureCacheBudget and the 512 MiB floor stay. The sampled budget still reserves TextureCacheBudget for the storage cache, at least the storage cap, and the storage budget reserves the sampled textures alive, so neither counts bytes the other may take; while the sampled textures alive stay within their floor the sampled budget falls first, then the storage budget, and above it both fall together. The budget line also prints when the storage image budget moves by more than a tenth, with its previous value, and StorageTextureCacheBudget is exported for the execution test. Co-Authored-By: Claude Fable 5.1 --- .../Graphics/include/ShaderResources.hpp | 3 +- .../Graphics/src/ShaderResources.cpp | 27 +++++++++++++---- .../prx/libSceAgcDriver/tests/Graphics.cpp | 26 +++++++++++----- .../tests/execution/SampledCacheBudget.cpp | 30 +++++++++++++++++++ docs/dev/debt/libSceAgcDriver.md | 1 + docs/user/USAGE.md | 4 +-- 6 files changed, 75 insertions(+), 16 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp index 8e25d227c9..c19fdd47b1 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp @@ -29,9 +29,10 @@ class Recorder; void FlushCachedTextures(VkDevice device); void ClearCachedTextures(VkDevice device); std::uint64_t TextureCacheBudget(const VkPhysicalDeviceMemoryProperties& memory); -std::uint64_t StorageTextureBudget(const VkPhysicalDeviceMemoryProperties& memory, const VkPhysicalDeviceMemoryBudgetPropertiesEXT* reported); +std::uint64_t StorageTextureBudget(const VkPhysicalDeviceMemoryProperties& memory, const VkPhysicalDeviceMemoryBudgetPropertiesEXT* reported, std::uint64_t sampledBytes, std::uint64_t storageBytes); std::uint64_t SampledTextureBudget(const VkPhysicalDeviceMemoryProperties& memory, const VkPhysicalDeviceMemoryBudgetPropertiesEXT* reported, std::uint64_t textureBytes); bool SampledBudgetReportDue(std::uint64_t reported, std::uint64_t budget, std::chrono::steady_clock::duration sinceReport); +std::uint64_t StorageTextureCacheBudget(const Context& context); struct TextureCacheUse { std::size_t sampledEntries = 0; std::uint64_t sampledBytes = 0; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index 926bfc67fa..c422a00ad6 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -145,6 +145,7 @@ struct TextureCache { std::chrono::steady_clock::time_point budgetRead{}; std::uint64_t budget = 0; std::uint64_t reportedBudget = 0; + std::uint64_t reportedStorage = 0; std::chrono::steady_clock::time_point reportedAt{}; std::uint64_t erasures = 0; }; @@ -516,7 +517,7 @@ std::uint64_t storageBudget(const Context& context) { context.memoryProperties2(context.physical, &properties); cached.device = context.physical; cached.read = now; - cached.budget = StorageTextureBudget(context.memory, &reported); + cached.budget = StorageTextureBudget(context.memory, &reported, SampledTextureMemory(), storageCacheBytes()); return cached.budget; } @@ -533,11 +534,17 @@ std::uint64_t sampledBudget(const Context& context, TextureCache& cache) { const auto storage = storageCacheBytes(); const auto budget = SampledTextureBudget(context.memory, &reported, sampled + storage); const bool fresh = cache.budgetDevice != context.physical; - if (const auto heap = largestDeviceLocalHeap(context.memory); heap && (fresh || SampledBudgetReportDue(cache.reportedBudget, budget, now - cache.reportedAt))) { + const auto storageLimit = storageBudget(context); + if (const auto heap = largestDeviceLocalHeap(context.memory); heap && (fresh || SampledBudgetReportDue(cache.reportedBudget, budget, now - cache.reportedAt) || SampledBudgetReportDue(cache.reportedStorage, storageLimit, now - cache.reportedAt))) { char was[48] = ""; - if (!fresh) std::snprintf(was, sizeof(was), " (was %llu MiB)", static_cast(cache.reportedBudget >> 20u)); - std::fprintf(stderr, "[gpu] sampled texture cache budget %llu MiB%s: device-local heap %u has a VK_EXT_memory_budget budget of %llu MiB and uses %llu MiB, %llu MiB of it sampled textures (%llu MiB in the cache) and %llu MiB cached storage images; storage image cache budget %llu MiB\n", static_cast(budget >> 20u), was, *heap, static_cast(reported.heapBudget[*heap] >> 20u), static_cast(reported.heapUsage[*heap] >> 20u), static_cast(sampled >> 20u), static_cast(cache.bytes >> 20u), static_cast(storage >> 20u), static_cast(storageBudget(context) >> 20u)); + char wasStorage[48] = ""; + if (!fresh) { + std::snprintf(was, sizeof(was), " (was %llu MiB)", static_cast(cache.reportedBudget >> 20u)); + std::snprintf(wasStorage, sizeof(wasStorage), " (was %llu MiB)", static_cast(cache.reportedStorage >> 20u)); + } + std::fprintf(stderr, "[gpu] sampled texture cache budget %llu MiB%s: device-local heap %u has a VK_EXT_memory_budget budget of %llu MiB and uses %llu MiB, %llu MiB of it sampled textures (%llu MiB in the cache) and %llu MiB cached storage images; storage image cache budget %llu MiB%s\n", static_cast(budget >> 20u), was, *heap, static_cast(reported.heapBudget[*heap] >> 20u), static_cast(reported.heapUsage[*heap] >> 20u), static_cast(sampled >> 20u), static_cast(cache.bytes >> 20u), static_cast(storage >> 20u), static_cast(storageLimit >> 20u), wasStorage); cache.reportedBudget = budget; + cache.reportedStorage = storageLimit; cache.reportedAt = now; } cache.budgetDevice = context.physical; @@ -1010,10 +1017,14 @@ std::uint64_t TextureCacheBudget(const VkPhysicalDeviceMemoryProperties& memory) return std::max(2048ull << 20u, heap ? memory.memoryHeaps[*heap].size / 4u : 0u); } -std::uint64_t StorageTextureBudget(const VkPhysicalDeviceMemoryProperties& memory, const VkPhysicalDeviceMemoryBudgetPropertiesEXT* reported) { +std::uint64_t StorageTextureBudget(const VkPhysicalDeviceMemoryProperties& memory, const VkPhysicalDeviceMemoryBudgetPropertiesEXT* reported, std::uint64_t sampledBytes, std::uint64_t storageBytes) { const auto heap = largestDeviceLocalHeap(memory); if (reported == nullptr || !heap || reported->heapBudget[*heap] == 0) return TextureCacheBudget(memory); - return std::max(512ull << 20u, std::min(TextureCacheBudget(memory), reported->heapBudget[*heap] / 4u)); + const auto budget = reported->heapBudget[*heap]; + const auto usage = reported->heapUsage[*heap]; + const auto reserved = usage - std::min(usage, sampledBytes + storageBytes) + std::max(2048ull << 20u, sampledBytes) + budget / 8u; + const auto cap = std::min(TextureCacheBudget(memory), budget / 4u); + return std::max(512ull << 20u, std::min(cap, budget > reserved ? budget - reserved : 0u)); } std::uint64_t SampledTextureBudget(const VkPhysicalDeviceMemoryProperties& memory, const VkPhysicalDeviceMemoryBudgetPropertiesEXT* reported, std::uint64_t textureBytes) { @@ -1030,6 +1041,10 @@ bool SampledBudgetReportDue(std::uint64_t reported, std::uint64_t budget, std::c return change > reported / 10u && sinceReport >= std::chrono::seconds(10); } +std::uint64_t StorageTextureCacheBudget(const Context& context) { + return storageBudget(context); +} + TextureCacheUse TextureCacheUsage() { TextureCacheUse use; { diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index e2f1c24796..171067b513 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -4863,21 +4863,33 @@ void storageTextureBudgetTests() { memory.memoryHeaps[1] = {32 * GiB, 0}; memory.memoryHeaps[2] = {16 * GiB, VK_MEMORY_HEAP_DEVICE_LOCAL_BIT}; VkPhysicalDeviceMemoryBudgetPropertiesEXT reported{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_BUDGET_PROPERTIES_EXT}; - Require(StorageTextureBudget(memory, nullptr) == 4 * GiB, "without VK_EXT_memory_budget the storage image cache does not keep a quarter of the 16 GiB heap"); + Require(StorageTextureBudget(memory, nullptr, 0, 0) == 4 * GiB, "without VK_EXT_memory_budget the storage image cache does not keep a quarter of the 16 GiB heap"); reported.heapBudget[0] = 100 * GiB; reported.heapBudget[1] = 100 * GiB; - Require(StorageTextureBudget(memory, &reported) == 4 * GiB, "a zero budget for the largest device-local heap did not fall back to a quarter of the heap"); + Require(StorageTextureBudget(memory, &reported, 0, 0) == 4 * GiB, "a zero budget for the largest device-local heap did not fall back to a quarter of the heap"); reported.heapBudget[2] = 10 * GiB; - Require(StorageTextureBudget(memory, &reported) == 2560 * MiB, "a 10 GiB budget on the 16 GiB heap does not limit the storage image cache to a quarter of the budget"); + Require(StorageTextureBudget(memory, &reported, 0, 0) == 2560 * MiB, "a 10 GiB budget on the 16 GiB heap does not limit the storage image cache to a quarter of the budget"); + reported.heapUsage[2] = 5 * GiB; + Require(StorageTextureBudget(memory, &reported, 1 * GiB, 2 * GiB) == 2560 * MiB, "2 GiB used outside the texture caches on a 10 GiB budget lowered the storage image cache below its quarter"); + reported.heapUsage[2] = 8 * GiB; + Require(StorageTextureBudget(memory, &reported, 1 * GiB, 2 * GiB) == 1792 * MiB, "a 10 GiB budget with 5 GiB used outside the texture caches does not leave 1792 MiB after the sampled textures' 2 GiB floor and 1280 MiB of headroom"); + reported.heapUsage[2] = 9 * GiB; + Require(StorageTextureBudget(memory, &reported, 3 * GiB, 2 * GiB) == 1792 * MiB, "3 GiB of sampled textures were not reserved past their 2 GiB floor"); + reported.heapUsage[2] = 1 * GiB; + Require(StorageTextureBudget(memory, &reported, 1 * GiB, 2 * GiB) == 2560 * MiB, "usage below the cached bytes is not read as no other device memory in use"); + Require(StorageTextureBudget(memory, &reported, 7 * GiB, 1 * GiB) == 1792 * MiB, "7 GiB of sampled textures with the usage below the cached bytes do not leave 1792 MiB after their reservation and 1280 MiB of headroom"); + reported.heapUsage[2] = 9 * GiB + 512 * MiB; + Require(StorageTextureBudget(memory, &reported, 2 * GiB, 512 * MiB) == 512 * MiB, "a heap whose other users leave no room does not keep the 512 MiB floor"); + reported.heapUsage[2] = 0; reported.heapBudget[2] = 6 * GiB; - Require(StorageTextureBudget(memory, &reported) == 1536 * MiB, "a 6 GiB budget on the 16 GiB heap does not limit the storage image cache to a quarter of the budget"); + Require(StorageTextureBudget(memory, &reported, 0, 0) == 1536 * MiB, "a 6 GiB budget on the 16 GiB heap does not limit the storage image cache to a quarter of the budget"); reported.heapBudget[2] = 1 * GiB; - Require(StorageTextureBudget(memory, &reported) == 512 * MiB, "a 1 GiB budget does not keep the 512 MiB floor"); + Require(StorageTextureBudget(memory, &reported, 0, 0) == 512 * MiB, "a 1 GiB budget does not keep the 512 MiB floor"); reported.heapBudget[2] = 40 * GiB; - Require(StorageTextureBudget(memory, &reported) == 4 * GiB, "a budget above the heap size raised the storage image cache past a quarter of the heap"); + Require(StorageTextureBudget(memory, &reported, 0, 0) == 4 * GiB, "a budget above the heap size raised the storage image cache past a quarter of the heap"); memory.memoryHeaps[0].flags = 0; memory.memoryHeaps[2].flags = 0; - Require(StorageTextureBudget(memory, &reported) == 2 * GiB, "a device without a device-local heap does not keep 2 GiB of storage images"); + Require(StorageTextureBudget(memory, &reported, 0, 0) == 2 * GiB, "a device without a device-local heap does not keep 2 GiB of storage images"); } void sampledBudgetReportTests() { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp index 67b8af060c..a1b31188b8 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp @@ -624,6 +624,21 @@ std::uint64_t ReadBudget(const Context& context) { return SampledTextureCacheBudget(context); } +constexpr std::uint64_t PressedUsage = ReportedBudget - 11ull * (1ull << 30u); + +void VKAPI_PTR ReportPressure(VkPhysicalDevice, VkPhysicalDeviceMemoryProperties2* properties) { + auto* reported = static_cast(properties->pNext); + for (std::uint32_t heap = 0; heap < VK_MAX_MEMORY_HEAPS; ++heap) { + reported->heapBudget[heap] = ReportedBudget; + reported->heapUsage[heap] = PressedUsage; + } +} + +std::uint64_t ReadStorageBudget(const Context& context) { + std::this_thread::sleep_for(std::chrono::milliseconds(1100)); + return StorageTextureCacheBudget(context); +} + void memoryTests(Context context) { context.memoryProperties2 = &ReportBudget; const auto before = SampledTextureMemory(); @@ -641,6 +656,20 @@ void memoryTests(Context context) { Require(SampledTextureMemory() == before, "a released texture still counts as sampled texture memory"); } +void storageMemoryTests(Context context) { + constexpr std::uint64_t GiB = 1ull << 30u; + context.memoryProperties2 = &ReportBudget; + Block block(context); + auto held = Sampled(context, block.Surface(0)); + const auto quarter = std::min(StorageTextureBudget(context.memory, nullptr, 0, 0), ReportedBudget / 4u); + Require(ReadStorageBudget(context) == quarter, "52 GiB free of a 64 GiB budget does not leave the storage image cache its quarter"); + context.memoryProperties2 = &ReportPressure; + const auto pressed = ReadStorageBudget(context); + const auto expected = GiB + SampledTextureMemory() + TextureCacheUsage().storageBytes; + Require(pressed == expected, "53 GiB in use of a 64 GiB budget leaves the storage image cache " + std::to_string(pressed >> 20u) + " MiB, not the " + std::to_string(expected >> 20u) + " MiB left after the use outside the texture caches, the sampled textures' 2 GiB floor and 8 GiB of headroom"); + std::printf("reported heap budget %llu MiB with %llu MiB in use: storage image cache budget %llu MiB\n", static_cast(ReportedBudget >> 20u), static_cast(PressedUsage >> 20u), static_cast(pressed >> 20u)); +} + } int main(int argc, char** argv) { @@ -673,6 +702,7 @@ int main(int argc, char** argv) { if (memory) { std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); memoryTests(context); + storageMemoryTests(context); ClearCachedTextures(context.device); std::puts("sampled texture memory budget tests passed"); return 0; diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index ad0c93ad2b..4665757484 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -44,6 +44,7 @@ ### Functional +- [Texture cache budgets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - the storage image cache's budget is what the reported heap budget leaves after the heap's use other than the cached storage images, the unused part of the sampled textures' 2 GiB floor and an eighth of the budget, capped at a quarter of the budget and at `TextureCacheBudget` (a quarter of the heap's size, at least 2 GiB), never below 512 MiB; the sampled cache's budget reserves `TextureCacheBudget` for the storage cache. So the sampled budget falls first while the sampled textures alive stay within their floor, and both fall together above it; each cache trims only at its next insert, a storage image is allocated before its insert reads the budget, and both budgets read the heap at most once a second, so a burst between two reads can still exceed the reported budget. The 2 GiB floor and the eighth are the sampled formula's values reused and the 512 MiB floor and the caps the earlier ones; none was measured beyond one title (PPSA11386) on one 10 GB GPU, and a title whose storage working set exceeds the shrunk budget re-creates storage images at each use. - [Native multisampled DCC fast clears](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) import uniform 0000/0001/1110/1111 keys into a single-layer, level-zero SW_64KB_R_X color image with 2, 4 or 8 fragments. Color and DCC/CMASK ranges must stay write-watched and unchanged; expanded or mixed initial DCC, register CMASK clears, compressed payloads, color transforms, lossy modes, metadata changes, guest publication and writes over a live owner throw. Driver-mediated reads of the color and metadata stay guarded. The metadata extent follows [AMD Addrlib](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/gfx10/gfx10addrlib.cpp#L347-L454) with the [PS5 Mesa model](https://github.com/mihawk-99/PS5_Mesa/blob/0b2d6d1a61d9bbf89cf8beb88a696144f67c61f8/src/amd/vulkan/winsys/ps5/radv_ps5_winsys.c): GFX10.1, 16 pipes, 256-byte interleave, two maximum compressed fragments. Console geometry, concurrent guest mutation and transitions outside this subset are unverified. - [Initial stencil state](../../../core/libs/prx/libSceAgcDriver/Execution/include/QueueState.hpp) uses zero for `DB_STENCIL_CONTROL`, `DB_STENCILREFMASK` and `DB_STENCILREFMASK_BF` (context `0x10b`-`0x10d`), including after `CLEAR_STATE`. The public V0, V4, V5 and V7-V11 [register-default tables](../../../core/libs/prx/libSceAgc/Command/include/RegisterDefaultsData.hpp) supply these values. Linux's [GFX10 clear-state table](https://github.com/torvalds/linux/blob/master/drivers/gpu/drm/amd/amdgpu/clearstate_gfx10.h) agrees on the control register but uses `0x01000000` for the reference masks; the console reset values have not been measured. Planet Coaster (PPSA01736 v01.017) enables stencil tests without writing the control register. diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index 18996014a7..9cdf152a22 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -187,9 +187,9 @@ Besides the recompiled shaders, the shader cache directory holds `pipelines.jour ### Texture cache budget -Sampled textures and storage images stay in device memory in two caches that evict their least recently used entries when they grow past their budgets. The storage image budget is a quarter of the `VK_EXT_memory_budget` budget of the largest device-local heap, capped at a quarter of the heap's size or 2 GiB, whichever is larger, and never below 512 MiB. The sampled texture budget is what the reported budget leaves after the heap's use other than cached textures, that cap and an eighth of the budget, and never below 2 GiB. Both are read again every second; `[gpu] sampled texture cache budget` lines report them at start-up and when they change. `APS5_TEXTURE_CACHE_MIB=` fixes both budgets at `n` MiB, and the lines are then not printed. +Sampled textures and storage images stay in device memory in two caches that evict their least recently used entries when they grow past their budgets. The storage image budget is what the `VK_EXT_memory_budget` budget of the largest device-local heap leaves after the heap's use other than the cached storage images themselves, the part of the sampled textures' 2 GiB floor not yet in use, and an eighth of the budget; it is capped at a quarter of that budget, at most a quarter of the heap's size or 2 GiB, whichever is larger, and never below 512 MiB. The sampled texture budget is what the reported budget leaves after the heap's use other than cached textures, a quarter of the heap's size (at least 2 GiB) and an eighth of the budget, and never below 2 GiB. While the sampled textures alive stay within their floor the sampled budget falls first, to that floor, then the storage budget; above it both fall together. Each cache trims at its next insert. Both budgets are read again every second; `[gpu] sampled texture cache budget` lines report them at start-up and when either moves by more than a tenth, at most every 10 s. `APS5_TEXTURE_CACHE_MIB=` fixes both budgets at `n` MiB, and the lines are then not printed. -A fixed budget above the automatic one can exceed what the card holds next to the rest of the frame. Allocations then go to host memory (`[gpu] ... placed in host memory` lines), where the GPU reads them over PCIe. PPSA16064 on an RTX 4060 8 GB, whose automatic storage image budget is 1797 MiB, placed 868 and 878 MiB in host memory with `APS5_TEXTURE_CACHE_MIB=3072` and walked through its first level at 4.46 and 4.83 FPS; with the variable unset it placed none and ran the same walk at 6.48 and 6.07 FPS. Leave it unset unless a measurement on the card shows otherwise. +A fixed budget above the automatic one can exceed what the card holds next to the rest of the frame. Allocations then go to host memory (`[gpu] ... placed in host memory` lines), where the GPU reads them over PCIe. PPSA16064 on an RTX 4060 8 GB, whose automatic storage image budget was 1797 MiB under the earlier rule, placed 868 and 878 MiB in host memory with `APS5_TEXTURE_CACHE_MIB=3072` and walked through its first level at 4.46 and 4.83 FPS; with the variable unset it placed none and ran the same walk at 6.48 and 6.07 FPS. Leave it unset unless a measurement on the card shows otherwise. ### Storing GPU results at each flip From 9fb282064b35fed31731e075cd75b072a33aadc4 Mon Sep 17 00:00:00 2001 From: pushkarreddyy <212133856+pushkarreddyy@users.noreply.github.com> Date: Fri, 16 Oct 2026 20:50:25 +0530 Subject: [PATCH 0692/1000] fix(libSceAgcDriver): accept unclamped normalized color target formats --- .../libSceAgcDriver/Graphics/src/State.cpp | 7 ++- .../prx/libSceAgcDriver/tests/Graphics.cpp | 43 +++++++++++++++++++ docs/dev/debt/libSceAgcDriver.md | 2 + 3 files changed, 51 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index 279258b2af..3ca9612766 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -1313,6 +1313,11 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) { Require(addition(state.colorBlendOp) && addition(state.alphaBlendOp), "dual-source blending with a MIN or MAX operation is unsupported"); result.dualSourceBlend = true; } + if (const auto info = read(cx, 0x31c + slot * 0xfu); state.blendEnable && (info & 0x8000u) == 0 && ((info >> 8u) & 7u) <= 1u) { + char detail[80]; + std::snprintf(detail, sizeof(detail), " (CB_COLOR_INFO 0x%08x, CB_BLEND%u_CONTROL 0x%08x)", info, slot, blend); + throw std::runtime_error(std::string("AGC graphics: blending unclamped normalized color is not implemented") + detail); + } result.blends[color.exportIndex] = state; } for (std::uint32_t i = 0; i < 3; ++i) { @@ -1472,7 +1477,7 @@ ColorTarget DecodeColorBuffer(const Registers& cx, std::uint32_t slot, bool allo const auto multisampleInfo = color.samples > 1 ? 0x0c006000u : 0u; // ROUND_MODE (bit 18) only affects unorm rounding. With DCC_ENABLE (bit 28) the target is written if ((info & ~(0x00039f7cu | 0x00040000u | 0x10000000u | 0x00002000u | multisampleInfo)) != 0) throw std::runtime_error("AGC graphics: color compression, DCC, endian conversion, nonstandard rounding or color optimization is unsupported (CB_COLOR_INFO 0x" + [&] { char text[16]; std::snprintf(text, sizeof(text), "%08x", info); return std::string(text); }() + ")"); - Require((info & 0x8000u) != 0 || number == 7 || number == 4 || number == 5, "unclamped normalized color is unsupported"); + Require((info & 0x8000u) != 0 || number <= 1u || number == 7 || number == 4 || number == 5, "unclamped normalized color is unsupported"); const auto view = read(cx, 0x31b + stride); Require((view & ~0x3fffffffu) == 0, "reserved CB_COLOR_VIEW bits are set"); const auto slice = view & 0x1fffu; diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index b50b08d210..d161fb8af5 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -1329,6 +1329,48 @@ void BlendBypassTests() { Require(!bypassed.blends.at(0).blendEnable && bypassed.blends.at(0).colorWriteMask != 0, "a target with BLEND_BYPASS still blends"); } +void UnclampedNormalizedColorTests() { + using AgcDriver::Graphics::DecodeState; + constexpr std::uint32_t blending = (1u << 30u) | 4u | (5u << 8u); + const auto target = [](std::uint32_t info) { + auto queue = makeState(); + for (std::uint32_t constant = 0x105; constant < 0x109; ++constant) queue.context[constant] = 0; + queue.context[0x31c] = info; + queue.context[0x1b3] = 2; + queue.context[0x1b4] = 2; + return queue; + }; + auto queue = target(0x0000000cu); + auto state = DecodeState(queue); + Require(state.colors.size() == 1 && state.colors[0].format == VK_FORMAT_R8G8_UNORM && !state.blends.at(0).blendEnable && state.blends.at(0).colorWriteMask != 0, "an 8_8 UNORM target with BLEND_CLAMP clear (CB_COLOR_INFO 0x0000000c, PPSA26344) was refused without blending"); + const auto rejection = AgcDriver::Graphics::DrawRejection(queue, false); + Require(rejection.empty(), "the precheck refused an unclamped UNORM target that does not blend: " + rejection); + Require(AgcDriver::Graphics::DecodeColorBuffer(queue.context, 0).format == VK_FORMAT_R8G8_UNORM, "an unclamped UNORM target was refused outside a draw"); + auto eliminate = target(0x0000000cu); + eliminate.context[0x0] = 0; + eliminate.context[0x202] = 0xcc0020; + const auto pass = AgcDriver::Graphics::DecodeColorMetadataPass(eliminate); + Require(pass.has_value() && pass->mode == AgcDriver::Graphics::ColorMetadataPass::Mode::EliminateFastClear && pass->targets.size() == 1 && pass->targets[0].format == VK_FORMAT_R8G8_UNORM, "a fast-clear eliminate over an unclamped UNORM target was refused"); + queue.context[0x1e0] = blending; + expectFailure([&] { DecodeState(queue); }, "blending unclamped normalized color is not implemented (CB_COLOR_INFO 0x0000000c, CB_BLEND0_CONTROL 0x40000504)"); + queue.context[0x31c] |= 0x10000u; + state = DecodeState(queue); + Require(!state.blends.at(0).blendEnable && state.blends.at(0).colorWriteMask != 0, "an unclamped UNORM target with BLEND_BYPASS was refused or blended"); + queue = target(0x0000010cu); + Require(DecodeState(queue).colors.at(0).format == VK_FORMAT_R8G8_SNORM, "an unclamped SNORM target that does not blend was refused"); + queue.context[0x1e0] = blending; + expectFailure([&] { DecodeState(queue); }, "blending unclamped normalized color is not implemented"); + queue = target(0x00000604u); + expectFailure([&] { DecodeState(queue); }, "unclamped normalized color is unsupported"); + queue = target(0x0000800cu); + queue.context[0x1e0] = blending; + Require(DecodeState(queue).blends.at(0).blendEnable, "a clamped UNORM target stopped blending"); + queue = target(0x00000708u); + queue.context[0x1e0] = blending; + state = DecodeState(queue); + Require(state.colors.at(0).format == VK_FORMAT_R16_SFLOAT && state.blends.at(0).blendEnable, "a 16 float target, which keeps BLEND_CLAMP clear, stopped blending"); +} + void OpaqueDestinationAlphaTests() { auto queue = makeState(); for (std::uint32_t constant = 0x105; constant < 0x109; ++constant) queue.context[constant] = 0; @@ -8143,6 +8185,7 @@ int main(int argc, char** argv) { OpaqueDestinationAlphaTests(); Color1555TargetTests(); BlendBypassTests(); + UnclampedNormalizedColorTests(); CompactedExportTests(); ReversedComponentOrderTests(); ReversedComponentOrderBlendTests(); diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index aac03b3d2f..d2887efaa7 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -166,3 +166,5 @@ - [Flip packets raise the graphics end-of-pipe event](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp) (libSceAgcDriver) - a flip delivers the queue's end-of-pipe event (`sceAgcDriverAddEqEvent`) once the work recorded before it completes, as a `RELEASE_MEM` with `INT_SEL` does. Minecraft (PPSA17221) registers the graphics event, never requests an interrupt in its releases, and waits on the event after every flip while holding its render lock, so without this it never presents a second frame. Whether the console raises the event from the flip itself or from an interrupt its flip sequence contains is not measured; a title that waits only on release interrupts sees one extra event per flip, with `data` counting it. - [Host import windows and budget](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver): a registered range larger than 64 MiB is imported one 64 MiB window at a time for buffer and fill accesses; an access across window edges imports the cells it touches, grown to the union of the range's imports it overlaps. An address-based build (`AcquireRegistered`) imports the range whole and replaces its windows. Live imports stay under a budget: `APS5_HOST_IMPORT_MIB`, else the larger of 6 GiB and half the physical memory, at most the largest host heap less 2 GiB (imports and type-3 mirrors share it; #167's rule). A new import beyond it retires the least recently used imports not used in the last 256 lookups; one that would not fit even then is refused before anything is retired, and imports made when a range is mapped never evict. A refused import is tried again at its next use. The 64 MiB window, the 256-lookup protection and the 2 GiB reserve are choices, not measured. - [Registration preparation at submit](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp) (libSceAgcDriver) - `Driver::Submit` waits for every registration-time shader preparation queued before the call, not only those of the shaders the commands use, so a submission behind a long queue of unrelated preparations waits for all of them on the submitting thread. Waiting there instead of in the queue worker keeps the GPU side of a submission short: a title that sees a submission take seconds treats the GPU as hung (Goat Simulator 3, PPSA04159, then stops submitting and its garbage collector fails a 10 s timeout). On the console shaders are compiled ahead of time, so neither wait exists; how long a title tolerates a slow submit call is not known. +- [Unclamped normalized color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - CB_COLOR_INFO targets with BLEND_CLAMP clear (bit 15) on UNORM and SNORM formats are accepted when blending is disabled (ENABLE clear or BLEND_BYPASS set). Vulkan converts unblended shader values and clamps to the normalized range automatically; blending into an unclamped normalized target remains unmodeled because Vulkan always clamps inputs prior to blending. + From 5b072bc2c4ff7137000c070bbd682d73844019d4 Mon Sep 17 00:00:00 2001 From: milescoviello Date: Thu, 15 Oct 2026 06:20:01 +0000 Subject: [PATCH 0693/1000] fix(libSceSystemService): park guest threads during LoadExec's shutdown as exit does Metal Gear Solid 4 (PPSA18194) aborts on main when its launcher starts the game: sceSystemServiceLoadExec runs the guest shutdown, a render thread that then calls into the stopped AGC driver gets ProcessShutdown back from LibcAwaitExit, and the exception unwinds into title code, which terminates the process before the new executable starts. LibcAwaitExit parks a thread only once an exit has been requested, and LoadExec ran the shutdown without requesting one. It now starts the shutdown the way exit does, so such threads wait until the process is replaced. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceSystemService/Export.cpp | 2 +- core/libs/prx/libSceSystemService/LoadExec.cpp | 4 ++-- core/libs/prx/libc/include/Shutdown.hpp | 1 + core/libs/prx/libc/src/Process.cpp | 4 ++++ core/libs/tests/GuestSystemExit.cpp | 7 ++++++- docs/dev/debt/libSceSystemService.md | 2 +- 6 files changed, 15 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libSceSystemService/Export.cpp b/core/libs/prx/libSceSystemService/Export.cpp index 55d0739a3a..546fad4287 100644 --- a/core/libs/prx/libSceSystemService/Export.cpp +++ b/core/libs/prx/libSceSystemService/Export.cpp @@ -240,7 +240,7 @@ int APS5_VABI sceSystemServiceLoadExec(const char* path, const char* const* argu #if defined(_WIN32) || defined(__linux__) LoadConvertedExecutable_nid_no_patch(std::filesystem::path(launch.executable), arguments); #endif - LibcRunShutdown_nid_postfix(); + LibcStartExit_nid_postfix(); std::fflush(nullptr); StartHostExecutable(launch); LibcTerminate_nid_no_patch(0); diff --git a/core/libs/prx/libSceSystemService/LoadExec.cpp b/core/libs/prx/libSceSystemService/LoadExec.cpp index 360e818297..a10de229ed 100644 --- a/core/libs/prx/libSceSystemService/LoadExec.cpp +++ b/core/libs/prx/libSceSystemService/LoadExec.cpp @@ -125,7 +125,7 @@ Handle Duplicate(HANDLE source, DWORD access) { throw std::system_error(GetLastError(), std::system_category(), "LoadExec: creating the converted executable"); Handle child(process.hProcess, CloseHandle), thread(process.hThread, CloseHandle); try { - LibcRunShutdown_nid_postfix(); + LibcStartExit_nid_postfix(); std::fflush(nullptr); if (ResumeThread(thread.get()) == static_cast(-1)) throw std::system_error(GetLastError(), std::system_category(), "LoadExec: starting the converted executable"); @@ -152,7 +152,7 @@ Handle Duplicate(HANDLE source, DWORD access) { std::vector pointers; for (const auto& argument : arguments) pointers.push_back(const_cast(argument.c_str())); pointers.push_back(nullptr); - LibcRunShutdown_nid_postfix(); + LibcStartExit_nid_postfix(); std::fflush(nullptr); if (const int host = HostStandardError_nid_no_patch(); host >= 0 && dup2(host, 2) != 2) throw std::system_error(errno, std::generic_category(), "LoadExec: giving the host standard error back to descriptor 2"); diff --git a/core/libs/prx/libc/include/Shutdown.hpp b/core/libs/prx/libc/include/Shutdown.hpp index af9ae548d1..8e0f9ec209 100644 --- a/core/libs/prx/libc/include/Shutdown.hpp +++ b/core/libs/prx/libc/include/Shutdown.hpp @@ -11,6 +11,7 @@ extern "C" std::stop_token LibcShutdownToken_nid_postfix(); extern "C" void LibcRequestShutdown_nid_postfix(); extern "C" void LibcRequestExit_nid_postfix(int code); extern "C" [[noreturn]] void LibcAwaitExit_nid_postfix(); +extern "C" void LibcStartExit_nid_postfix(); extern "C" [[noreturn]] void LibcExit_nid_no_patch(int code); extern "C" [[noreturn]] void LibcExitWithoutHandlers_nid_no_patch(int code); extern "C" [[noreturn]] void LibcTerminate_nid_no_patch(int code); diff --git a/core/libs/prx/libc/src/Process.cpp b/core/libs/prx/libc/src/Process.cpp index d47d252bc2..e6d58a919f 100644 --- a/core/libs/prx/libc/src/Process.cpp +++ b/core/libs/prx/libc/src/Process.cpp @@ -161,6 +161,10 @@ extern "C" { std::_Exit(code); } +void LibcStartExit_nid_postfix() { + StartExit(); +} + [[noreturn]] void LibcExit_nid_no_patch(int code) { StartExit(); CxaFinalize_nid_no_patch(nullptr); diff --git a/core/libs/tests/GuestSystemExit.cpp b/core/libs/tests/GuestSystemExit.cpp index 3c8fb41326..8fa6982485 100644 --- a/core/libs/tests/GuestSystemExit.cpp +++ b/core/libs/tests/GuestSystemExit.cpp @@ -88,7 +88,12 @@ void AppendMarker(const std::string& line) { Require(marker != nullptr); std::ofstream(marker, std::ios::app) << line << '\n'; } -void MainShutdown() { AppendMarker("shutdown"); } +void MainShutdown() { + AppendMarker("shutdown"); + std::thread(AwaitExit).detach(); + awaiting.wait(false); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); +} void SecondaryShutdown() { AppendMarker("secondary shutdown"); } void APS5_VABI RemainingHandler(void*) { AppendMarker("remaining handler"); } void APS5_VABI ExitingHandler(void*) { exit_nid_postfix(7); } diff --git a/docs/dev/debt/libSceSystemService.md b/docs/dev/debt/libSceSystemService.md index 409869d29c..8d8ea62347 100644 --- a/docs/dev/debt/libSceSystemService.md +++ b/docs/dev/debt/libSceSystemService.md @@ -17,6 +17,6 @@ ### Functional -- [sceSystemServiceLoadExec](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - besides `"exit"`, it starts the title's own `/app0/eboot.bin` or another executable under `/app0` that the relinker converted with `--executable`, which is looked up beside the running one (`/app0/picker/picker.bin` as `picker.picker.bin` with the running executable's extension). The main executable is the one `APS5_EBOOT` names, which the first call sets to its own path. After the guest shutdown the old process ends without running guest exit handlers or host destructors. `"exit"` ends the process after the host shutdown without running the title's `atexit` and `__cxa_atexit` handlers, since the console's shell closes the title instead; a line the title writes just before through its own thread, such as a forwarded standard error line, may not be printed. [VLC for PS5](https://github.com/ZiZc3/VLC/blob/86bb7d474a303f62bb3c2fc12f728db4846085b8/src/platform_ps5.c#L565-L571) (PPSA85300) relaunches itself with no arguments; RetroArch (PPSA99169) starts its frontend picker with `{"", NULL}`, and the picker starts `eboot.bin` with `{"--ps5-mode=retroarch", NULL}` or `/app0/es-de/es-de.bin` ([picker.cpp](https://github.com/mihawk-99/PS5_RetroArch/blob/bead80725cc11e3c5c65bd9424b0a4494ae36d45/frontends/picker/picker.cpp#L444-L446)), so the list holds only arguments. A list with a first entry becomes the whole guest `argv`, with no program name in front ([LoadExec.cpp](../../../core/libs/prx/libSceSystemService/LoadExec.cpp)): a [PS5 RetroArch console transcript](https://github.com/mihawk-99/PS5_RetroArch/blob/bead80725cc11e3c5c65bd9424b0a4494ae36d45/evidence/loadexec-second-image/relaunch-test.jsonl) records a restart with one argument entering with `argc=1`, `argv[0]="--ps5-relaunch=1"`, and a launch from the home screen with `argc=1`, `argv[0]=""` ([relaunch_ps5.cpp](https://github.com/mihawk-99/PS5_RetroArch/blob/bead80725cc11e3c5c65bd9424b0a4494ae36d45/src/relaunch_ps5.cpp#L161-L164)). For such a list Linux replaces the process with `fexecve`: the PID and the standard streams stay, the other descriptors are closed and the working directory becomes the executable's. Windows starts a suspended process that inherits only the standard streams and a handle of the old process, and its entry stub waits for the old process to end before it loads the title's libraries, so a shell or script that started the title sees it end. A null list or one without a first entry keeps the earlier launch: the new executable gets only its own path as its command line (`CreateProcessW` on Windows, `execv` on Linux), so its `argv[0]` is the host path, and the old process ends without waiting for it. What the console passes for such a list is not known, nor are its argument limits and error codes. Paths outside `/app0`, unconverted executables and macOS hosts throw +- [sceSystemServiceLoadExec](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - besides `"exit"`, it starts the title's own `/app0/eboot.bin` or another executable under `/app0` that the relinker converted with `--executable`, which is looked up beside the running one (`/app0/picker/picker.bin` as `picker.picker.bin` with the running executable's extension). The main executable is the one `APS5_EBOOT` names, which the first call sets to its own path. After the guest shutdown the old process ends without running guest exit handlers or host destructors; as during `exit`, a guest thread that calls a library the shutdown has stopped waits for the end instead of unwinding. `"exit"` ends the process after the host shutdown without running the title's `atexit` and `__cxa_atexit` handlers, since the console's shell closes the title instead; a line the title writes just before through its own thread, such as a forwarded standard error line, may not be printed. [VLC for PS5](https://github.com/ZiZc3/VLC/blob/86bb7d474a303f62bb3c2fc12f728db4846085b8/src/platform_ps5.c#L565-L571) (PPSA85300) relaunches itself with no arguments; RetroArch (PPSA99169) starts its frontend picker with `{"", NULL}`, and the picker starts `eboot.bin` with `{"--ps5-mode=retroarch", NULL}` or `/app0/es-de/es-de.bin` ([picker.cpp](https://github.com/mihawk-99/PS5_RetroArch/blob/bead80725cc11e3c5c65bd9424b0a4494ae36d45/frontends/picker/picker.cpp#L444-L446)), so the list holds only arguments. A list with a first entry becomes the whole guest `argv`, with no program name in front ([LoadExec.cpp](../../../core/libs/prx/libSceSystemService/LoadExec.cpp)): a [PS5 RetroArch console transcript](https://github.com/mihawk-99/PS5_RetroArch/blob/bead80725cc11e3c5c65bd9424b0a4494ae36d45/evidence/loadexec-second-image/relaunch-test.jsonl) records a restart with one argument entering with `argc=1`, `argv[0]="--ps5-relaunch=1"`, and a launch from the home screen with `argc=1`, `argv[0]=""` ([relaunch_ps5.cpp](https://github.com/mihawk-99/PS5_RetroArch/blob/bead80725cc11e3c5c65bd9424b0a4494ae36d45/src/relaunch_ps5.cpp#L161-L164)). For such a list Linux replaces the process with `fexecve`: the PID and the standard streams stay, the other descriptors are closed and the working directory becomes the executable's. Windows starts a suspended process that inherits only the standard streams and a handle of the old process, and its entry stub waits for the old process to end before it loads the title's libraries, so a shell or script that started the title sees it end. A null list or one without a first entry keeps the earlier launch: the new executable gets only its own path as its command line (`CreateProcessW` on Windows, `execv` on Linux), so its `argv[0]` is the host path, and the old process ends without waiting for it. What the console passes for such a list is not known, nor are its argument limits and error codes. Paths outside `/app0`, unconverted executables and macOS hosts throw - [Running application ID](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - `sceSystemServiceGetAppIdOfRunningBigApp` reports 1 as the running title's app ID. The value is AnyPS5's own: the console's shell assigns app IDs and their values are not known here, so other calls that take an app ID must use the same one. `sceSystemServiceKillApp` with that ID and the arguments -1, 0 and 0, which [SwanStationPS5](https://github.com/darkxex/SwanStationPS5/blob/4cdc981d1a64ffb7f2af73f7a27a9e91a3cb5eb4/src/main.c#L1050-L1058) passes to close itself and [ps5-payload-dev's hbldr](https://github.com/ps5-payload-dev/websrv/blob/46374edee6627885abac717de24da7a1d73af6f1/src/ps5/hbldr.c#L425-L435) passes to close the running game, ends the title as its own `exit(0)` would: the guest's shutdown and `atexit` handlers run and the process exits with status 0. On the console the call returns and the shell then ends the title, and whether those handlers run there is unknown; here there is no shell, so the call does not return. Another app ID or other arguments throw. - [LoadExec descriptors](../../../core/libs/prx/libSceSystemService/LoadExec.cpp) (libSceSystemService) - on Linux, a call with an argument list replaces the process with `fexecve`, which first closes every descriptor above 2; a call without one uses `execv`, and the guest's descriptors stay open across it. On both paths descriptor 2 first gets back the host standard error a guest redirect kept, so the runtime's output does not follow the title's redirect (RetroArch's trace file, VLC's log) into the next executable. Which descriptors the console passes to the new process is not known From e71ade0ac3a3e0ef13eec1f3d8c89b3c38905e6b Mon Sep 17 00:00:00 2001 From: milescoviello Date: Sat, 17 Oct 2026 10:50:52 +0000 Subject: [PATCH 0694/1000] fix(libSceSystemService): run the shutdown callbacks after LoadExec starts the exit Since #5978, StartExit only parks the other guest threads and exit runs LibcRunShutdown itself, so LoadExec's three exits (the converted executable on Windows and Linux, and the host executable) now call LibcStartExit and then LibcRunShutdown, as exit does. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceSystemService/Export.cpp | 1 + core/libs/prx/libSceSystemService/LoadExec.cpp | 2 ++ 2 files changed, 3 insertions(+) diff --git a/core/libs/prx/libSceSystemService/Export.cpp b/core/libs/prx/libSceSystemService/Export.cpp index 546fad4287..6480b49dfc 100644 --- a/core/libs/prx/libSceSystemService/Export.cpp +++ b/core/libs/prx/libSceSystemService/Export.cpp @@ -241,6 +241,7 @@ int APS5_VABI sceSystemServiceLoadExec(const char* path, const char* const* argu LoadConvertedExecutable_nid_no_patch(std::filesystem::path(launch.executable), arguments); #endif LibcStartExit_nid_postfix(); + LibcRunShutdown_nid_postfix(); std::fflush(nullptr); StartHostExecutable(launch); LibcTerminate_nid_no_patch(0); diff --git a/core/libs/prx/libSceSystemService/LoadExec.cpp b/core/libs/prx/libSceSystemService/LoadExec.cpp index a10de229ed..0434f1b817 100644 --- a/core/libs/prx/libSceSystemService/LoadExec.cpp +++ b/core/libs/prx/libSceSystemService/LoadExec.cpp @@ -126,6 +126,7 @@ Handle Duplicate(HANDLE source, DWORD access) { Handle child(process.hProcess, CloseHandle), thread(process.hThread, CloseHandle); try { LibcStartExit_nid_postfix(); + LibcRunShutdown_nid_postfix(); std::fflush(nullptr); if (ResumeThread(thread.get()) == static_cast(-1)) throw std::system_error(GetLastError(), std::system_category(), "LoadExec: starting the converted executable"); @@ -153,6 +154,7 @@ Handle Duplicate(HANDLE source, DWORD access) { for (const auto& argument : arguments) pointers.push_back(const_cast(argument.c_str())); pointers.push_back(nullptr); LibcStartExit_nid_postfix(); + LibcRunShutdown_nid_postfix(); std::fflush(nullptr); if (const int host = HostStandardError_nid_no_patch(); host >= 0 && dup2(host, 2) != 2) throw std::system_error(errno, std::generic_category(), "LoadExec: giving the host standard error back to descriptor 2"); From e1d4ccba496e0e9cdc587f15bd752f1d60077a55 Mon Sep 17 00:00:00 2001 From: milescoviello Date: Sat, 17 Oct 2026 10:58:33 +0000 Subject: [PATCH 0695/1000] fix(libkernel): take NOTE_LOWAT on socket read and write events kevent threw for NOTE_LOWAT. On a socket's EVFILT_READ it now sets the low-water mark as FreeBSD 11's filt_soread does: the event fires once the buffered bytes reach it, or at end of file or on a listening socket, and a later change of the knote replaces it. EVFILT_WRITE takes a mark of 1, where the host's writability stands in for a free byte; other write marks and marks below 1 still throw. A pipe ignores the mark, as filt_piperead does. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libkernel/Equeue/Equeue.cpp | 3 ++ core/libs/prx/libkernel/Equeue/Equeue.hpp | 1 + core/libs/prx/libkernel/Equeue/src/Kqueue.cpp | 28 +++++++++++---- .../Socket/include/SocketRuntime.hpp | 1 + core/libs/prx/libkernel/Socket/src/Udp.cpp | 12 +++++++ core/libs/tests/GuestKqueue.cpp | 36 +++++++++++++++++++ docs/dev/debt/libkernel.md | 4 +-- 7 files changed, 76 insertions(+), 9 deletions(-) diff --git a/core/libs/prx/libkernel/Equeue/Equeue.cpp b/core/libs/prx/libkernel/Equeue/Equeue.cpp index 0d084b3d3b..249f2959f0 100644 --- a/core/libs/prx/libkernel/Equeue/Equeue.cpp +++ b/core/libs/prx/libkernel/Equeue/Equeue.cpp @@ -655,6 +655,9 @@ KernelEqueueEvent EqueueDescriptorEvent(int descriptor, int16_t filter, uint16_t if (!GuestSockets::Ready(static_cast(e->event.ident), e->event.filter == EVFILT_WRITE, &data, &eof)) { return false; } + if (!eof && data < e->lowWater && !GuestSockets::Listening(static_cast(e->event.ident))) { + return false; + } e->event.data = static_cast(data); e->event.flags = static_cast(eof ? (e->event.flags | EV_EOF) : (e->event.flags & ~EV_EOF)); return true; diff --git a/core/libs/prx/libkernel/Equeue/Equeue.hpp b/core/libs/prx/libkernel/Equeue/Equeue.hpp index f1fac92cad..39ad8bf12d 100644 --- a/core/libs/prx/libkernel/Equeue/Equeue.hpp +++ b/core/libs/prx/libkernel/Equeue/Equeue.hpp @@ -61,6 +61,7 @@ struct KernelEqueueEvent { bool enabled = true; uint64_t deadlineNs = 0; uint64_t intervalNs = 0; + std::int64_t lowWater = 0; KernelEvent event; KernelFilter filter; std::deque pendingEvents; diff --git a/core/libs/prx/libkernel/Equeue/src/Kqueue.cpp b/core/libs/prx/libkernel/Equeue/src/Kqueue.cpp index ae13538695..6653807951 100644 --- a/core/libs/prx/libkernel/Equeue/src/Kqueue.cpp +++ b/core/libs/prx/libkernel/Equeue/src/Kqueue.cpp @@ -127,6 +127,19 @@ int DescriptorFilterError(int kqueueDescriptor, std::uintptr_t ident) { return descriptor == kqueueDescriptor ? GUEST_EINVAL : 0; } +std::int64_t DescriptorLowWater(const KernelEvent& change) { + if ((change.fflags & NOTE_LOWAT) == 0 || !GuestSockets::IsOpen(static_cast(change.ident))) { + return 0; + } + if (change.data < 1) { + NotImplemented_nid_no_patch("kevent: NOTE_LOWAT below 1 on a socket"); + } + if (change.filter == EVFILT_WRITE && change.data > 1) { + NotImplemented_nid_no_patch("kevent: NOTE_LOWAT above 1 on EVFILT_WRITE"); + } + return change.data; +} + int AddDescriptorKnote(const KernelEqueueRef& queue, KernelEvent& change, std::uint16_t flags) { const int descriptor = static_cast(change.ident); if (IsKqueueDescriptor(descriptor)) { @@ -142,13 +155,12 @@ int AddDescriptorKnote(const KernelEqueueRef& queue, KernelEvent& change, std::u if (!socket && !pipe) { NotImplemented_nid_no_patch(change.filter == EVFILT_READ ? "kevent: EVFILT_READ on a descriptor that is neither a socket nor the read end of a pipe" : "kevent: EVFILT_WRITE on a non-socket descriptor"); } - if ((change.fflags & NOTE_LOWAT) != 0) { - NotImplemented_nid_no_patch("kevent: NOTE_LOWAT"); - } + const auto lowWater = DescriptorLowWater(change); if ((flags & EV_CLEAR) != 0) { NotImplemented_nid_no_patch(pipe ? "kevent: EV_CLEAR on a pipe" : "kevent: EV_CLEAR on a socket"); } auto event = EqueueDescriptorEvent(descriptor, change.filter, flags, change.udata); + event.lowWater = lowWater; change.fflags = 0; change.data = 0; return GuestErrno(queue->AddEvent(event)); @@ -219,14 +231,16 @@ int ModifyKnote(const KernelEqueueRef& queue, KernelEvent& change) { if (change.filter == EVFILT_TIMER) { NotImplemented_nid_no_patch("kevent: changing an existing EVFILT_TIMER"); } - if ((change.filter == EVFILT_READ || change.filter == EVFILT_WRITE) && (change.fflags & NOTE_LOWAT) != 0) { - NotImplemented_nid_no_patch("kevent: NOTE_LOWAT"); - } + const bool descriptor = change.filter == EVFILT_READ || change.filter == EVFILT_WRITE; + const auto lowWater = descriptor ? DescriptorLowWater(change) : 0; if (change.filter == EVFILT_VNODE) { RequireVnodeNotes(change.fflags); } - return GuestErrno(queue->EditEvent(change.ident, change.filter, [&change](KernelEqueueEvent& event) { + return GuestErrno(queue->EditEvent(change.ident, change.filter, [&change, descriptor, lowWater](KernelEqueueEvent& event) { event.event.udata = change.udata; + if (descriptor) { + event.lowWater = lowWater; + } if ((change.flags & EV_ENABLE) != 0) { event.enabled = true; } diff --git a/core/libs/prx/libkernel/Socket/include/SocketRuntime.hpp b/core/libs/prx/libkernel/Socket/include/SocketRuntime.hpp index c5425e4350..0e135ed198 100644 --- a/core/libs/prx/libkernel/Socket/include/SocketRuntime.hpp +++ b/core/libs/prx/libkernel/Socket/include/SocketRuntime.hpp @@ -16,6 +16,7 @@ int Close(int descriptor); void Forget(int descriptor); bool IsOpen(int descriptor); int Family(int descriptor); +bool Listening(int descriptor); std::int64_t Read(int descriptor, void* buffer, std::size_t length); std::int64_t Write(int descriptor, const void* buffer, std::size_t length); int Stat(int descriptor, FileStat* status); diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index 5c6fc0b521..dcd61bc45e 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -401,6 +401,18 @@ int GuestSockets::Family(int descriptor) { const auto found = sockets.find(descriptor); return found == sockets.end() ? -1 : found->second->family; } +bool GuestSockets::Listening(int descriptor) { + std::shared_ptr socket; + { + std::lock_guard lock(socketsMutex); + const auto found = sockets.find(descriptor); + if (found == sockets.end()) return false; + socket = found->second; + } + int accepting = 0; + socklen_t length = sizeof(accepting); + return getsockopt(socket->value, SOL_SOCKET, SO_ACCEPTCONN, reinterpret_cast(&accepting), &length) == 0 && accepting != 0; +} extern "C" bool GuestSocketIsOpen_nid_no_patch(int descriptor) { return GuestSockets::IsOpen(descriptor); diff --git a/core/libs/tests/GuestKqueue.cpp b/core/libs/tests/GuestKqueue.cpp index 593c82d059..a7f51bd74c 100644 --- a/core/libs/tests/GuestKqueue.cpp +++ b/core/libs/tests/GuestKqueue.cpp @@ -63,6 +63,7 @@ static constexpr std::uint16_t EvForceoneshot = 0x0100; static constexpr std::uint16_t EvError = 0x4000; static constexpr std::uint16_t EvEof = 0x8000; static constexpr std::uint32_t NoteTrigger = 0x01000000; +static constexpr std::uint32_t NoteLowat = 0x0001; static constexpr std::uint32_t NoteFfcopy = 0xc0000000; static constexpr std::uint32_t NoteFfor = 0x80000000; static constexpr std::uint32_t NoteUseconds = 0x0004; @@ -269,6 +270,40 @@ static void SocketPair() { Require(close_nid_postfix(kq) == 0); } +static void LowWaterMarks() { + const int kq = kqueue_nid_postfix(); + int pair[2]; + Require(socketpair_nid_postfix(Unix, Stream, 0, pair) == 0); + const auto reader = static_cast(pair[0]); + KernelEvent events[4]{}; + Require(Apply(kq, Change(reader, EvfiltRead, EvAdd, NoteLowat, 4)) == 0); + Require(send_nid_postfix(pair[1], "ab", 2, 0) == 2); + Require(Poll(kq, events, 4) == 0); + Require(send_nid_postfix(pair[1], "cd", 2, 0) == 2); + Require(Wait(kq, events, 4) == 1 && events[0].ident == reader && events[0].filter == EvfiltRead && events[0].data == 4); + Require(Apply(kq, Change(reader, EvfiltRead, EvAdd, NoteLowat, 8)) == 0); + Require(Poll(kq, events, 4) == 0); + Require(Apply(kq, Change(reader, EvfiltRead, EvAdd)) == 0); + Require(Poll(kq, events, 4) == 1 && events[0].data == 4); + Require(Apply(kq, Change(reader, EvfiltRead, EvAdd, NoteLowat, 8)) == 0); + Require(Poll(kq, events, 4) == 0); + Require(Apply(kq, Change(static_cast(pair[1]), EvfiltWrite, EvAdd | EvOneshot, NoteLowat, 1)) == 0); + Require(Wait(kq, events, 4) == 1 && events[0].filter == EvfiltWrite); + Require(Throws([kq, &pair] { Apply(kq, Change(static_cast(pair[1]), EvfiltWrite, EvAdd, NoteLowat, 2)); })); + Require(Throws([kq, reader] { Apply(kq, Change(reader, EvfiltRead, EvAdd, NoteLowat, 0)); })); + Require(close_nid_postfix(pair[1]) == 0); + Require(Poll(kq, events, 4) == 1 && events[0].ident == reader && (events[0].flags & EvEof) != 0); + Require(close_nid_postfix(pair[0]) == 0); + int pipe[2]; + Require(pipe_nid_postfix(pipe) == 0); + Require(Apply(kq, Change(static_cast(pipe[0]), EvfiltRead, EvAdd, NoteLowat, 64)) == 0); + Require(Poll(kq, events, 4) == 0); + Require(write_nid_postfix(pipe[1], "e", 1) == 1); + Require(Poll(kq, events, 4) == 1 && events[0].data == 1); + Require(close_nid_postfix(pipe[0]) == 0 && close_nid_postfix(pipe[1]) == 0); + Require(close_nid_postfix(kq) == 0); +} + static void DisableAndDispatch() { const int kq = kqueue_nid_postfix(); int pair[2]; @@ -627,6 +662,7 @@ int main() { SocketPair(); DisableAndDispatch(); Errors(); + LowWaterMarks(); FileWatch(); #ifndef _WIN32 RenameAndDeleteWatchedFile(); diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 5791f7ab5e..1ce4e9fa5b 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -109,7 +109,7 @@ - [sceKernelMprotect, sceKernelMtypeprotect](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (libkernel) over a range with a gap that is not registered in the allocation tracker protect the registered parts and skip the gap, and return 0, as FreeBSD 11.4's [`vm_map_protect`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/vm/vm_map.c) does (it has no check for holes); a range whose start is unmapped (also when later pages are mapped) returns `SCE_KERNEL_ERROR_EINVAL` (`mprotect`: `EINVAL`) because an existing test pins that for an unmapped start; FreeBSD answers 0 there too (`vm_map_protect` takes the next entry), so this errno is not taken from the source and was not changed. `sceKernelMprotect` threw for a gap before, which ended the title. Every part is checked for a GPU or image pin before the first one is protected; the parts are then protected one after the other, so a host protection failure in a later part leaves the earlier parts changed (FreeBSD's `vm_map_protect` cannot fail in its second pass). The behaviour on the console is not known. None of this was measured on a console. - [sceKernelMprotect protection bits](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - bits outside `0x3F7` are dropped instead of refused, as FreeBSD 11's [`sys_mprotect`](https://github.com/freebsd/freebsd-src/blob/34bd34750187bae02a0086402c78c031c59dd8b3/sys/vm/vm_mmap.c#L606) does with `VM_PROT_ALL`. Onimusha: Way of the Sword (PPSA27836 01.000.001) maps direct memory with `0xf2` and then protects it with `0xfffffff2`. Not measured on a console; the map calls and `sceKernelMtypeprotect` still refuse undefined bits - [JIT shared memory](../../../core/libs/prx/libkernel/DirectMemory/Jit.cpp) (libkernel) - `sceKernelJitCreateSharedMemory`, `sceKernelJitCreateAliasOfSharedMemory` and `sceKernelJitMapSharedMemory` take the signatures of [Orbital](https://github.com/AlexAltea/orbital)'s `tools/ida/db_types.json` (name, length, maximum protection, descriptor out; descriptor, maximum protection, descriptor out; descriptor, protection, address in and out), which ps4sdk, ps5-jar-loader and the call sites in RetroArch PPSA99169, XPSemu PPSA97358 and VLC PPSA85300 match; SharpProspero declares the alias as returning the descriptor. The object is a host anonymous file shared by its descriptors (a `memfd` on Linux, a delete-on-close temporary file mapped as an executable section on Windows), the alias is a duplicate descriptor, and every map maps the whole object, so writes through one view show through the others; a non-null address is a hint, as in `mmap` without `MAP_FIXED`. Not measured on the console: a length that is zero or not a multiple of 16 KiB and protection bits other than CPU read, write and execute are `EINVAL`; an alias or a map asking for more than the descriptor's maximum protection (write and execute counting as read) is `EACCES`, as FreeBSD 11's `vm_map_protect` treats a protection past `max_protection`; a descriptor that is not a JIT object is `EBADF`. The name is ignored, the objects do not count against the flexible memory budget, `mprotect` of a view is not limited to the maximum protection, and `mmap` of the descriptors is unsupported, as for `shm_open`. On Linux the `memfd` is created without `MFD_EXEC`, which Linux 6.3 added and older kernels reject with `EINVAL`, and relies on the protection of the mapping: the flag only sets the file's execute permission bits, and the direct memory `memfd` is mapped executable the same way; a host with `vm.memfd_noexec` set to 2 refuses the creation and the call throws -- [kqueue and kevent](../../../core/libs/prx/libkernel/Equeue/src/Kqueue.cpp) (libkernel) follow FreeBSD 11's [`kern_event.c`](https://github.com/freebsd/freebsd-src/blob/34bd34750187bae02a0086402c78c031c59dd8b3/sys/kern/kern_event.c) (`kqueue_kevent`, `kqueue_register`, `kqueue_scan`, the timer and user filters) over the event queue; nothing is measured on a console. The descriptor is a host null device registered as the kqueue: `close` and `sceKernelClose` close the queue and `dup`/`dup2` of it throw, but `read`, `write` and `fstat` act on the null device instead of failing with `EOPNOTSUPP` or reporting `S_IFIFO`. Read and write events are supported on guest sockets only, plus `EVFILT_READ` on the read end of a pipe (see the kevent on a pipe entry) and the timer, user and [vnode](../../../core/libs/prx/libkernel/Equeue/src/FileEvent.cpp) filters; other filters, `NOTE_LOWAT`, `EV_CLEAR` on a socket or a pipe, `EVFILT_READ` on another kqueue, a `NOTE_NSECONDS` period above one second with a fraction (11.0 converts the fraction with the microsecond macro) and any change to an existing timer (11.0 keeps the armed expiry and applies the new period after it, 11.4's `filt_timertouch` restarts it) throw. `EV_DISABLE`, `EV_DISPATCH` and `EV_FORCEONESHOT` follow kern_event.c: a disabled knote stays registered, stops polling or firing and is not reported, a trigger raised while it is disabled is kept and reported after `EV_ENABLE`, where `knote` would drop it, `EV_DISPATCH` reports once and then disables the knote, and `EV_FORCEONESHOT` is ignored on a new knote while on an existing one it sets `EV_ONESHOT` and fires the knote; a negative `nevents` fails with `EINVAL`. A new user event keeps its `fflags` unmasked and is not triggered by `NOTE_TRIGGER`, because `filt_userattach` reads the already cleared `kn_fflags`. The timer limit `kq_calloutmax` (`ENOMEM`) is not enforced, and closing a kqueue wakes a thread waiting on it in `kevent` with `EBADF`, where `kqueue_drain` makes the close wait for that thread. A level-triggered event is reported once per call and moved behind the other events, as in `kqueue_scan`; the `sceKernel*Equeue` calls keep their own collection loop +- [kqueue and kevent](../../../core/libs/prx/libkernel/Equeue/src/Kqueue.cpp) (libkernel) follow FreeBSD 11's [`kern_event.c`](https://github.com/freebsd/freebsd-src/blob/34bd34750187bae02a0086402c78c031c59dd8b3/sys/kern/kern_event.c) (`kqueue_kevent`, `kqueue_register`, `kqueue_scan`, the timer and user filters) over the event queue; nothing is measured on a console. The descriptor is a host null device registered as the kqueue: `close` and `sceKernelClose` close the queue and `dup`/`dup2` of it throw, but `read`, `write` and `fstat` act on the null device instead of failing with `EOPNOTSUPP` or reporting `S_IFIFO`. Read and write events are supported on guest sockets only, plus `EVFILT_READ` on the read end of a pipe (see the kevent on a pipe entry) and the timer, user and [vnode](../../../core/libs/prx/libkernel/Equeue/src/FileEvent.cpp) filters; other filters, `EV_CLEAR` on a socket or a pipe, `EVFILT_READ` on another kqueue, a `NOTE_NSECONDS` period above one second with a fraction (11.0 converts the fraction with the microsecond macro) and any change to an existing timer (11.0 keeps the armed expiry and applies the new period after it, 11.4's `filt_timertouch` restarts it) throw. `EV_DISABLE`, `EV_DISPATCH` and `EV_FORCEONESHOT` follow kern_event.c: a disabled knote stays registered, stops polling or firing and is not reported, a trigger raised while it is disabled is kept and reported after `EV_ENABLE`, where `knote` would drop it, `EV_DISPATCH` reports once and then disables the knote, and `EV_FORCEONESHOT` is ignored on a new knote while on an existing one it sets `EV_ONESHOT` and fires the knote; a negative `nevents` fails with `EINVAL`. A new user event keeps its `fflags` unmasked and is not triggered by `NOTE_TRIGGER`, because `filt_userattach` reads the already cleared `kn_fflags`. The timer limit `kq_calloutmax` (`ENOMEM`) is not enforced, and closing a kqueue wakes a thread waiting on it in `kevent` with `EBADF`, where `kqueue_drain` makes the close wait for that thread. A level-triggered event is reported once per call and moved behind the other events, as in `kqueue_scan`; the `sceKernel*Equeue` calls keep their own collection loop. `NOTE_LOWAT` on a socket sets the low-water mark as FreeBSD 11's [`filt_soread` and `filt_sowrite`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/uipc_socket.c) do, and a later change of the knote replaces it as `kqueue_register` replaces `kn_sdata`: a read event fires once the buffered bytes reach the mark, or at end of file or on a listening socket. A write event takes a mark of 1 only, where the host's writability stands in for one free byte, since the host does not report the send buffer's free space; other write marks and marks below 1, which make FreeBSD's read filter always fire, throw - [EVFILT_VNODE, sceKernelAddFileEvent and sceKernelDeleteFileEvent](../../../core/libs/prx/libkernel/Equeue/src/FileEvent.cpp) (libkernel) watch the host file or directory behind the descriptor and turn host changes into FreeBSD 11 vnode hints, which [`filt_vfsvnode`](https://github.com/freebsd/freebsd-src/blob/34bd34750187bae02a0086402c78c031c59dd8b3/sys/kern/vfs_subr.c#L5032) adds whole when they share a bit with the requested `fflags`: a write that grows a file is `NOTE_WRITE | NOTE_EXTEND`, another write `NOTE_WRITE`, a shrinking truncate or an attribute change `NOTE_ATTRIB`, a higher link count `NOTE_LINK`, a lower one or a pending delete `NOTE_DELETE`, a new path `NOTE_RENAME`; in a directory, an entry created, removed or renamed is `NOTE_WRITE`, with `NOTE_LINK` for a subdirectory, and on Linux an entry moved from or to another directory adds `NOTE_EXTEND`; inotify queues the two halves of a rename one after the other, so a watcher woken by the first can read it alone, and a half read without the other is classified only after a `getdents64` on the watched directory, which takes the directory lock that `rename` holds until both halves are queued ([`do_renameat2`](https://github.com/torvalds/linux/blob/v6.8/fs/namei.c#L4975) around `vfs_rename` and its `fsnotify_move`, [`iterate_dir`](https://github.com/torvalds/linux/blob/v6.8/fs/readdir.c#L103)), an ordering taken from the kernel source, not from inotify(7), which promises none; a directory registered with `NOTE_EXTEND` on Windows is accepted, as `vfs_kqfilter` does not check it, but never receives it, because the directory handle cannot tell a move between directories from a removal plus an addition. FreeBSD's hints depend on the filesystem and the console's filesystems are not known: a growing `ftruncate` reports `NOTE_WRITE | NOTE_EXTEND` where `vop_setattr_post` reports `NOTE_ATTRIB`, and on Windows `utimes` on a file reports `NOTE_WRITE`. The three Windows notifications are told apart in the error, which names the `parent`, `entries` or `subdirectories` watch, the path it was given and `GetLastError`, but a `FindFirstChangeNotification` that fails still throws rather than returning an error to the guest: the EVFILT_VNODE attach path is [`vfs_kqfilter`](https://github.com/freebsd/freebsd-src/blob/34bd34750187bae02a0086402c78c031c59dd8b3/sys/kern/vfs_subr.c#L4936), which returns `EINVAL`, `ENOMEM` or 0 after `fget` has turned a descriptor that is not open into `EBADF`, and consults no directory permission, so no FreeBSD errno stands for a host that refuses to notify, and what the console does on such a host is not measured. Only the `parent` watch can fail this way in practice: `entries` and `subdirectories` need the `FILE_LIST_DIRECTORY` that opening the watched directory already took. The parent handle produces no hint of its own, as `RunHostWatch` only calls `FindNextChangeNotification` on it, so a later change may leave the watch on its 50 ms poll instead of throwing; that is not done here. Windows uses `FindFirstChangeNotification` on the parent directory (and on a watched directory itself) and compares `GetFileInformationByHandleEx` snapshots at least every 50 ms, because a write through an open handle is reported only when the handle is flushed or closed, so file hints can arrive 50 ms late, and a file deleted while watched, which the POSIX delete of Windows 10 1903 moves into `$Extend\$Deleted` until its last handle closes, reports `NOTE_DELETE` without `NOTE_RENAME` and keeps its handle without a parent directory watch, as that directory cannot be watched; Linux uses inotify on the file and is not run. `NOTE_REVOKE`, `NOTE_OPEN`, `NOTE_CLOSE`, `NOTE_CLOSE_WRITE`, `NOTE_READ`, unknown bits and descriptors other than files, directories, pipes and sockets throw. `sceKernelAddFileEvent(eq, fd, watch, udata)` and `sceKernelDeleteFileEvent(eq, fd)` take the [OpenOrbis](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain/blob/0a1aaf9d/include/orbis/libkernel.h#L35) and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0/src/SharpProspero/Interop/Kernel/KernelEqueue.cs#L164) signatures; `watch` is taken as the `fflags` and the event is added with `EV_CLEAR`, neither confirmed on a title (feat(libkernel): implement kevent EV_DISABLE, EV_DISPATCH and EV_FORCEONESHOT) (fix(libkernel): follow kqueue_register for EV_FORCEONESHOT and modify flags) - [sem_timedwait](../../../core/libs/prx/libkernel/Pthread/Posix/Sem.cpp) (libkernel) consumes an available count before checking the timeout and waits indefinitely for a null timeout, following FreeBSD 11's `sem_timedwait`. Console behavior for null and invalid timeouts when a count is available is unverified. - [tcsendbreak, tcdrain, tcflush, tcflow, tcgetpgrp, tcsetpgrp](../../../core/libs/prx/libkernel/File/src/Terminal.cpp) (libkernel) follow FreeBSD 11.4's [`termios.c`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/gen/termios.c): `tcflush` answers `EINVAL` for a queue other than `TCIFLUSH`, `TCOFLUSH` or `TCIOFLUSH` and `tcflow` for an action outside `TCOOFF`..`TCION` before the descriptor is looked at, and every other call fails as its terminal ioctl does on a descriptor that is not a terminal, with the errors `tcgetattr` gives, so `tcsendbreak` never reaches its 0.4 s pause. FreeBSD's `pipe_ioctl` answers `TIOCGPGRP` and `TIOCSPGRP` on a pipe or FIFO with the descriptor's SIGIO owner, which is not modelled, so `tcgetpgrp` and `tcsetpgrp` throw on a host pipe or FIFO above descriptor 2; descriptors 0 to 2 are taken as non-terminals that are not pipes, whatever the host attaches to them, so a pipe the guest creates on a freed descriptor 0 to 2 gets `ENOTTY` there. The PS5 `libkernel` is assumed to use this FreeBSD code, and `tcdrain` is not a cancellation point here @@ -171,7 +171,7 @@ - [Socket descriptor numbers](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel, libSceNet) - `socket`, `accept`, `socketpair`, `dup`, `sceNetSocket` and `sceNetAccept` take the lowest free number of the host descriptor table that files use, as FreeBSD 11.4 `fdalloc` takes the lowest free entry of the one process table (`kern_descrip.c`), so every number stays below `FD_SETSIZE` (1024) while fewer than 1024 descriptors are open. The number holds a zero-access `NUL` handle on Windows and a duplicate of the host socket on Linux, and the socket tables map it to the socket. The host table also holds the runtime's own files, so the numbers differ from the console's; the first number `socket` and `sceNetSocket` return on a console and its `getdtablesize` are not measured. Paths that bypass the socket tables see the placeholder: libc `fdopen` on a socket opens it, and libkernel `close` of a libSceNet socket releases the number but leaves the libSceNet socket open. Palworld (PPSA23126), The Mound (PPSA26223) and Dying Light 2 (PPSA02262) set socket bits in a 1024-bit `fd_set` - [fstat on a guest socket](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) fills the fields FreeBSD 11's soo_stat and pru_sense_null fill and zeroes the rest: S_IFSOCK, the read bits while the receive side is open or data is queued, the write bits while the send side is open, st_size and st_blksize. Only this process's shutdown closes a side here; a peer's shutdown, close or reset does not clear the bits as FreeBSD's socantrcvmore and socantsendmore do. st_size is the host's FIONREAD, which for a Linux datagram socket is the size of the next datagram where FreeBSD counts every queued byte, and st_blksize is the host's SO_SNDBUF, which Linux reports as twice the size that was set. A socketpair end reports st_dev and st_ino 0, where FreeBSD's uipc_sense reports NODEV and a per-socket number - [poll](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) waits on guest sockets and pipes, through `WSAPoll` on Windows and `poll` on Linux; the `pollfd` layout and flag values are FreeBSD's. A libSceNet socket joins the same host wait through the native socket libSceNet lends to libkernel. `WSAPoll` rejects `POLLPRI`, so on Windows a socket's `POLLPRI` is asked as `POLLRDBAND` and `POLLIN` as `POLLRDNORM`; Winsock reports urgent data until it is read with `MSG_OOB`, where FreeBSD 11.4's `sopoll_generic` stops once the normal stream passes the mark. Guest `send` and `recv` refuse `MSG_OOB` with `EOPNOTSUPP`, so urgent data can only come from an outside peer and Windows keeps reporting `POLLPRI` after the stream passes the mark. A pipe follows FreeBSD's `pipe_poll`: the read end is readable when data is buffered, either end reports `POLLHUP` once the other end is closed, with the requested `POLLIN` and `POLLRDNORM` bits and without `POLLOUT`, which `pollscan` clears, and the write end is writable when `PIPE_BUF` (512) bytes are free on Windows, or when the host kernel says so on Linux. `POLLOUT` on a read end throws: FreeBSD opens both ends for reading and writing and reports it writable, and host pipes are one-way. Windows pipes cannot be waited on, so while a pipe is polled `poll` checks it with `NtQueryInformationFile` and waits on the sockets in slices of 1 ms. A closed descriptor reports `POLLNVAL`, and an open descriptor that is neither a guest socket, a libSceNet socket nor a pipe (files, stdio) throws, as do event bits other than `POLLIN`, `POLLPRI`, `POLLOUT`, `POLLRDNORM`, `POLLRDBAND` and `POLLWRBAND` (the `POLLERR`, `POLLHUP` and `POLLNVAL` bits are ignored in `events`), an infinite timeout with no socket or pipe to wait on. A closed socket descriptor reports `POLLNVAL`; the console's limit on the descriptor count is unknown and not checked -- [kevent on a pipe](../../../core/libs/prx/libkernel/Equeue/src/Kqueue.cpp) (libkernel) - `EVFILT_READ` on the read end of a pipe follows FreeBSD 11's [`filt_piperead`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/sys_pipe.c#L1787-L1806): `data` is the number of buffered bytes, and `EV_EOF` is set once the write end is closed. Windows pipes cannot be waited on, so while a kqueue waits on a pipe it checks it with `NtQueryInformationFile` and waits on its sockets in slices of 1 ms; a wait with no pipe is not sliced. `EVFILT_READ` on the write end of a pipe throws: FreeBSD's `pipe` opens both ends for reading and writing ([`kern_pipe`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/sys_pipe.c#L422)), and host pipes are one-way. `EVFILT_WRITE` on a pipe and `NOTE_LOWAT` on a pipe, which `filt_piperead` ignores, throw too. +- [kevent on a pipe](../../../core/libs/prx/libkernel/Equeue/src/Kqueue.cpp) (libkernel) - `EVFILT_READ` on the read end of a pipe follows FreeBSD 11's [`filt_piperead`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/sys_pipe.c#L1787-L1806): `data` is the number of buffered bytes, and `EV_EOF` is set once the write end is closed. Windows pipes cannot be waited on, so while a kqueue waits on a pipe it checks it with `NtQueryInformationFile` and waits on its sockets in slices of 1 ms; a wait with no pipe is not sliced. `EVFILT_READ` on the write end of a pipe throws: FreeBSD's `pipe` opens both ends for reading and writing ([`kern_pipe`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/sys_pipe.c#L422)), and host pipes are one-way. `EVFILT_WRITE` on a pipe throws too, and `NOTE_LOWAT` on a pipe is ignored, as `filt_piperead` ignores it. - [Socket error numbers](../../../core/libs/prx/libkernel/Socket/include/SocketError.hpp) (libkernel) - a host socket error becomes the FreeBSD 11 `errno.h` number of the same name; the numbers are not checked on the console. Winsock `WSAESHUTDOWN` becomes `EPIPE`, as a send after `shutdown` does on FreeBSD, and a host error with no FreeBSD socket counterpart becomes `EIO`. A nonblocking `connect` in progress reports `EINPROGRESS` on Linux but `EAGAIN` on Windows (`WSAEWOULDBLOCK`), where FreeBSD reports `EINPROGRESS`. On Windows a refused nonblocking `connect` is never reported (`SO_ERROR` is unsupported), so a repeated one keeps reporting `EALREADY` where FreeBSD reports `ECONNREFUSED`. A receive after `shutdown(SHUT_RD)` reports `EPIPE` on Windows where FreeBSD returns 0 - [getsockopt, setsockopt](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) on a guest socket handle the SOL_SOCKET level and `TCP_NODELAY`, after FreeBSD 11.4's [`sosetopt` and `sogetopt`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c): `SO_REUSEADDR`, `SO_KEEPALIVE`, `SO_DONTROUTE`, `SO_BROADCAST`, `SO_OOBINLINE` and `SO_LINGER` act on the host socket, and `SO_ERROR`, `SO_TYPE`, `SO_ACCEPTCONN`, `SO_SNDBUF` and `SO_RCVBUF` are read. A boolean option reads back as its own bit, as FreeBSD returns `so_options & option`. Winsock does not clear a socket's pending error when it is read, so on Windows the library reports a pending error once and shows it again only after a new `connect`; a second failure with the same code on the same socket without a `connect` in between would be hidden. `SO_SNDBUF` and `SO_RCVBUF` read the host's value, which is not the size FreeBSD reports (Linux doubles it). `SO_NOSIGPIPE` is kept per socket and reads back as its bit. On Linux a `send`, `sendto`, `sendmsg` or `write` on that socket passes `MSG_NOSIGNAL` to the host, as [`kern_sendit`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_syscalls.c#L890-L895) and [`soo_write`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/sys_socket.c#L147-L151) leave out the signal. An accepted socket takes it from its listener, as [`sonewconn`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L603) copies the options. `SO_NBIO` (`0x1200`) is the console's own option, not FreeBSD's. It switches the socket between blocking and non-blocking as `FIONBIO` does and reads back as 0 or 1, as `sceNetSetsockopt` and `sceNetGetsockopt` treat it here; what the console reads back for it is not measured. `TCP_NODELAY` (`IPPROTO_TCP`, 1) is set on the host socket and reads back as `TF_NODELAY` (4), as [`tcp_default_ctloutput`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/netinet/tcp_usrreq.c#L1860-L1861) returns it. The `IPPROTO_TCP` level on a datagram or `AF_UNIX` socket fails with `EINVAL`, as [`ip_ctloutput`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/netinet/ip_output.c#L943-L944) and [`uipc_ctloutput`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_usrreq.c#L1228-L1229) answer it, and an accepted socket takes `TCP_NODELAY` through the host. `SO_SNDTIMEO` and `SO_RCVTIMEO` follow [`sosetopt`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L2657-L2689) and [`sogetopt`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L2846-L2850). A 16-byte `timeval` with a negative field or 1000000 microseconds or more fails with `EDOM`, and more than `INT32_MAX` seconds becomes the longest wait. The value is kept and read back through FreeBSD's 32.32 fixed-point conversion ([`tvtosbt`, `sbttotv`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/sys/time.h#L178-L286)), so half a second reads back as 499999 microseconds and one microsecond as 0. A `recv`, `recvfrom`, `recvmsg`, `read` or `readv`, and a `send`, `sendto`, `sendmsg`, `write` or `writev`, on a blocking socket then waits that long for the host to report the socket readable or writable, and fails with `EWOULDBLOCK`; a call with `MSG_DONTWAIT` does not wait. An accepted socket takes both from its listener ([`sonewconn`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L629-L630)). The wait covers only the first transfer: once the socket is writable the host send runs without a limit, where FreeBSD stops a send that cannot finish and returns the bytes sent so far, and a receive whose data another thread takes between the wait and the host call blocks again. `sendfile` ignores the send timeout. The host wait has millisecond steps, so a shorter timeout lasts up to a millisecond longer. A null value fails with `EINVAL`, where FreeBSD's [`kern_setsockopt`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_syscalls.c#L1414-L1415) returns `EFAULT`. Every other SOL_SOCKET option (`SO_DEBUG`, `SO_USELOOPBACK`, `SO_REUSEPORT` and `SO_TIMESTAMP`, which FreeBSD stores and no title has been seen to set, the low-water marks among them), every other `IPPROTO_TCP` option (`TCP_MAXSEG`, `TCP_KEEPIDLE`, `TCP_KEEPINTVL` and `TCP_KEEPCNT` among them) and every other level (`IPPROTO_IP`, `IPPROTO_IPV6`) fail with `ENOPROTOOPT`. None of this was measured on a console. - [connect to AF_UNSPEC](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) dissolves a datagram association as FreeBSD 11.4's `udp_disconnect()` does, which resets the local address to the wildcard address and keeps the port. On Linux a socket bound to an explicit port keeps its local address as well (`127.0.0.1:` stays `127.0.0.1:`), because a bound socket cannot be bound again; one bound to port 0 is bound again to the wildcard address with the port it had From 91d8416fdd017d9de1283a53b005b84bba2895da Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 17 Oct 2026 11:00:59 +0000 Subject: [PATCH 0696/1000] fix(ssl): compile out the Mbed TLS self-test counters With MBEDTLS_SELF_TEST, ecp.c counts point additions, doublings and field multiplications in unsynchronized globals. Handshakes on separate connections run in parallel, each under its own session lock, so the counters race; ThreadSanitizer reports it on the parallel transport test. Nothing calls the self-test functions. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RoU3ZF76A6uR8sCA7NMLch --- core/libs/prx/libSceSsl/MbedConfig.hpp | 1 + 1 file changed, 1 insertion(+) diff --git a/core/libs/prx/libSceSsl/MbedConfig.hpp b/core/libs/prx/libSceSsl/MbedConfig.hpp index 5555fc58d7..aab4833dc9 100644 --- a/core/libs/prx/libSceSsl/MbedConfig.hpp +++ b/core/libs/prx/libSceSsl/MbedConfig.hpp @@ -3,3 +3,4 @@ #define MBEDTLS_PLATFORM_MEMORY #undef MBEDTLS_ECDH_VARIANT_EVEREST_ENABLED #undef MBEDTLS_SSL_PROTO_TLS1_3 +#undef MBEDTLS_SELF_TEST From 4ff3b054515ad1bf40b876a9cceaa678267312dd Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Sat, 17 Oct 2026 11:05:41 +0000 Subject: [PATCH 0697/1000] fix(agc): count every physical pointer write form in the BDA store scan The scan listed stores, memory copies and atomics, and took only typed OpTypePointer declarations as physical pointer types. A module that wrote guest memory through an untyped PhysicalStorageBuffer pointer (SPV_KHR_untyped_pointers), through a GLSL.std.450 Modf or Frexp output pointer, or with a cooperative matrix store would have been taken for one that only reads through the BDA table, and its draws would have continued a render pass they must end. The recompiler emits none of these today, so no module's answer changes. Untyped physical pointer types now count as physical pointer types, an extended instruction with a physical pointer operand counts as a store, and the cooperative matrix stores store through their pointer operand. Pointer type declarations no longer add their storage class to the pointer values. The tests build modules for every write form the scan knows, the reads it must not take for writes, and the three forms above. --- .../Graphics/src/ShaderAddressResources.cpp | 15 ++- .../libSceAgcDriver/tests/BdaContracts.cpp | 110 ++++++++++++++++++ 2 files changed, 121 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderAddressResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderAddressResources.cpp index 03b171e774..2915b76a5a 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderAddressResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderAddressResources.cpp @@ -19,6 +19,9 @@ std::uint32_t pointerOperandStored(std::uint32_t opcode, std::span 1 ? instruction[1] : 0u; case spv::OpAtomicExchange: case spv::OpAtomicCompareExchange: @@ -44,8 +47,11 @@ std::uint32_t pointerOperandStored(std::uint32_t opcode, std::span(opcode)) { + case spv::OpTypePointer: + case spv::OpTypeUntypedPointerKHR: + case spv::OpTypeForwardPointer: case spv::OpName: case spv::OpMemberName: case spv::OpDecorate: @@ -91,18 +97,19 @@ bool SpirvStoresThroughAddresses(std::span words) { if (words.size() < 5 || words[0] != spv::MagicNumber) return true; std::unordered_set pointerTypes; const bool readable = forEachInstruction(words, [&](std::uint32_t opcode, std::span instruction) { - if (opcode == spv::OpTypePointer && instruction.size() > 2 && instruction[2] == spv::StorageClassPhysicalStorageBuffer) pointerTypes.insert(instruction[1]); + if ((opcode == spv::OpTypePointer || opcode == spv::OpTypeUntypedPointerKHR) && instruction.size() > 2 && instruction[2] == spv::StorageClassPhysicalStorageBuffer) pointerTypes.insert(instruction[1]); }); if (!readable) return true; if (pointerTypes.empty()) return false; std::unordered_set pointers; forEachInstruction(words, [&](std::uint32_t opcode, std::span instruction) { - if (instruction.size() > 2 && !namesOrDecorates(opcode) && opcode != spv::OpTypePointer && pointerTypes.contains(instruction[1])) pointers.insert(instruction[2]); + if (instruction.size() > 2 && !definesNoPointerValue(opcode) && pointerTypes.contains(instruction[1])) pointers.insert(instruction[2]); }); bool stores = false; forEachInstruction(words, [&](std::uint32_t opcode, std::span instruction) { const auto pointer = pointerOperandStored(opcode, instruction); - stores = stores || (pointer != 0 && pointers.contains(pointer)); + const bool extendedWithPointer = opcode == spv::OpExtInst && std::any_of(instruction.begin() + static_cast(std::min(5u, instruction.size())), instruction.end(), [&](std::uint32_t operand) { return pointers.contains(operand); }); + stores = stores || extendedWithPointer || (pointer != 0 && pointers.contains(pointer)); }); return stores; } diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaContracts.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaContracts.cpp index 0802a3f3fe..6e30d1bace 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaContracts.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaContracts.cpp @@ -2,10 +2,119 @@ #include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp" #include "ControlFlow/RequestSerializer.hpp" #include "SpirvBackend/SpirvBda.hpp" +#include #include +#include +#include +#include +#include +#include namespace { +struct ScanWords { + std::vector words; + void Op(spv::Op opcode, std::initializer_list operands) { + words.push_back((static_cast(operands.size() + 1u) << 16u) | static_cast(opcode)); + words.insert(words.end(), operands.begin(), operands.end()); + } + void OpString(spv::Op opcode, std::initializer_list operands, const char* text) { + std::vector packed((std::strlen(text) + 4u) / 4u, 0u); + std::memcpy(packed.data(), text, std::strlen(text)); + words.push_back((static_cast(operands.size() + packed.size() + 1u) << 16u) | static_cast(opcode)); + words.insert(words.end(), operands.begin(), operands.end()); + words.insert(words.end(), packed.begin(), packed.end()); + } +}; + +enum ScanId : std::uint32_t { + IdVoid = 1, IdVoidFunction, IdUint, IdUlong, IdUintPointer, IdAddress, IdOne, IdDeviceScope, IdNoSemantics, IdPointerPointer, IdPointerFunction, IdBool, IdTrue, IdFloat, IdFloatPointer, IdGlsl, IdFunctionUintPointer, IdUntypedPointer, IdFloatOne, IdMain, IdMainLabel, IdPointer, IdHelper, IdHelperParameter, IdHelperLabel, IdBody, +}; + +bool ScanStores(const std::function& main, const std::function& after = {}) { + ScanWords module{{spv::MagicNumber, 0x00010500u, 0u, 200u, 0u}}; + module.Op(spv::OpCapability, {spv::CapabilityShader}); + module.Op(spv::OpCapability, {spv::CapabilityInt64}); + module.Op(spv::OpCapability, {spv::CapabilityPhysicalStorageBufferAddresses}); + module.OpString(spv::OpExtInstImport, {IdGlsl}, "GLSL.std.450"); + module.Op(spv::OpMemoryModel, {spv::AddressingModelPhysicalStorageBuffer64, spv::MemoryModelGLSL450}); + module.OpString(spv::OpEntryPoint, {spv::ExecutionModelGLCompute, IdMain}, "main"); + module.Op(spv::OpTypeVoid, {IdVoid}); + module.Op(spv::OpTypeFunction, {IdVoidFunction, IdVoid}); + module.Op(spv::OpTypeInt, {IdUint, 32u, 0u}); + module.Op(spv::OpTypeInt, {IdUlong, 64u, 0u}); + module.Op(spv::OpTypeFloat, {IdFloat, 32u}); + module.Op(spv::OpTypeBool, {IdBool}); + module.Op(spv::OpTypePointer, {IdUintPointer, spv::StorageClassPhysicalStorageBuffer, IdUint}); + module.Op(spv::OpTypePointer, {IdPointerPointer, spv::StorageClassPhysicalStorageBuffer, IdUintPointer}); + module.Op(spv::OpTypePointer, {IdFloatPointer, spv::StorageClassPhysicalStorageBuffer, IdFloat}); + module.Op(spv::OpTypePointer, {IdPointerFunction, spv::StorageClassFunction, IdUint}); + module.Op(spv::OpTypeUntypedPointerKHR, {IdUntypedPointer, spv::StorageClassPhysicalStorageBuffer}); + module.Op(spv::OpTypeFunction, {IdFunctionUintPointer, IdVoid, IdUintPointer}); + module.Op(spv::OpConstant, {IdUlong, IdAddress, 0x1000u, 0u}); + module.Op(spv::OpConstant, {IdUint, IdOne, 1u}); + module.Op(spv::OpConstant, {IdUint, IdDeviceScope, spv::ScopeDevice}); + module.Op(spv::OpConstant, {IdUint, IdNoSemantics, 0u}); + module.Op(spv::OpConstant, {IdFloat, IdFloatOne, 0x3f800000u}); + module.Op(spv::OpConstantTrue, {IdBool, IdTrue}); + module.Op(spv::OpFunction, {IdVoid, IdMain, spv::FunctionControlMaskNone, IdVoidFunction}); + module.Op(spv::OpLabel, {IdMainLabel}); + module.Op(spv::OpConvertUToPtr, {IdUintPointer, IdPointer, IdAddress}); + main(module); + module.Op(spv::OpReturn, {}); + module.Op(spv::OpFunctionEnd, {}); + if (after) after(module); + return AgcDriver::Graphics::SpirvStoresThroughAddresses(module.words); +} + +void RunAddressStoreScanTests() { + using AgcDriver::Graphics::Require; + Require(!ScanStores([](ScanWords& m) { m.Op(spv::OpLoad, {IdUint, IdBody, IdPointer}); }), "store scan: a load through a physical pointer was taken for a store"); + Require(!ScanStores([](ScanWords& m) { m.Op(spv::OpAtomicLoad, {IdUint, IdBody, IdPointer, IdDeviceScope, IdNoSemantics}); }), "store scan: an atomic load was taken for a store"); + Require(ScanStores([](ScanWords& m) { m.Op(spv::OpStore, {IdPointer, IdOne}); }), "store scan: a store was missed"); + Require(ScanStores([](ScanWords& m) { m.Op(spv::OpAtomicStore, {IdPointer, IdDeviceScope, IdNoSemantics, IdOne}); }), "store scan: an atomic store was missed"); + for (const auto opcode : {spv::OpAtomicExchange, spv::OpAtomicIAdd, spv::OpAtomicISub, spv::OpAtomicSMin, spv::OpAtomicUMin, spv::OpAtomicSMax, spv::OpAtomicUMax, spv::OpAtomicAnd, spv::OpAtomicOr, spv::OpAtomicXor}) { + Require(ScanStores([opcode](ScanWords& m) { m.Op(opcode, {IdUint, IdBody, IdPointer, IdDeviceScope, IdNoSemantics, IdOne}); }), "store scan: atomic opcode " + std::to_string(static_cast(opcode)) + " was missed"); + } + Require(ScanStores([](ScanWords& m) { m.Op(spv::OpAtomicIIncrement, {IdUint, IdBody, IdPointer, IdDeviceScope, IdNoSemantics}); }), "store scan: an atomic increment was missed"); + Require(ScanStores([](ScanWords& m) { m.Op(spv::OpAtomicCompareExchange, {IdUint, IdBody, IdPointer, IdDeviceScope, IdNoSemantics, IdNoSemantics, IdOne, IdOne}); }), "store scan: an atomic compare-exchange was missed"); + Require(ScanStores([](ScanWords& m) { + m.Op(spv::OpConvertUToPtr, {IdPointerPointer, IdBody, IdAddress}); + m.Op(spv::OpLoad, {IdUintPointer, IdBody + 1u, IdBody}); + m.Op(spv::OpStore, {IdBody + 1u, IdOne}); + }), "store scan: a store through a pointer loaded from guest memory was missed"); + Require(ScanStores([](ScanWords& m) { + m.Op(spv::OpSelect, {IdUintPointer, IdBody, IdTrue, IdPointer, IdPointer}); + m.Op(spv::OpStore, {IdBody, IdOne}); + }), "store scan: a store through a selected pointer was missed"); + Require(ScanStores([](ScanWords& m) { m.Op(spv::OpFunctionCall, {IdVoid, IdBody, IdHelper, IdPointer}); }, [](ScanWords& m) { + m.Op(spv::OpFunction, {IdVoid, IdHelper, spv::FunctionControlMaskNone, IdFunctionUintPointer}); + m.Op(spv::OpFunctionParameter, {IdUintPointer, IdHelperParameter}); + m.Op(spv::OpLabel, {IdHelperLabel}); + m.Op(spv::OpStore, {IdHelperParameter, IdOne}); + m.Op(spv::OpReturn, {}); + m.Op(spv::OpFunctionEnd, {}); + }), "store scan: a store through a pointer parameter of a called function was missed"); + Require(!ScanStores([](ScanWords& m) { + m.Op(spv::OpVariable, {IdPointerFunction, IdBody, spv::StorageClassFunction}); + m.Op(spv::OpCopyMemory, {IdBody, IdPointer}); + }), "store scan: a copy out of guest memory was taken for a store"); + Require(ScanStores([](ScanWords& m) { + m.Op(spv::OpVariable, {IdPointerFunction, IdBody, spv::StorageClassFunction}); + m.Op(spv::OpCopyMemory, {IdPointer, IdBody}); + }), "store scan: a copy into guest memory was missed"); + Require(ScanStores([](ScanWords& m) { + m.Op(spv::OpConvertUToPtr, {IdUntypedPointer, IdBody, IdAddress}); + m.Op(spv::OpStore, {IdBody, IdOne}); + }), "store scan: a store through an untyped physical pointer was missed"); + Require(ScanStores([](ScanWords& m) { + m.Op(spv::OpConvertUToPtr, {IdFloatPointer, IdBody, IdAddress}); + m.Op(spv::OpExtInst, {IdFloat, IdBody + 1u, IdGlsl, GLSLstd450Modf, IdFloatOne, IdBody}); + }), "store scan: an extended instruction writing through a physical pointer was missed"); + Require(ScanStores([](ScanWords& m) { m.Op(spv::OpCooperativeMatrixStoreKHR, {IdPointer, IdOne, IdNoSemantics}); }), "store scan: a cooperative matrix store was missed"); +} + + template void reject(TAction action, const char* expected) { try { action(); } @@ -21,6 +130,7 @@ void reject(TAction action, const char* expected) { void RunBdaContractTests() { using namespace ShaderRecompiler; using AgcDriver::Graphics::SpirvStoresThroughAddresses; + RunAddressStoreScanTests(); AgcDriver::Graphics::Require(!SpirvStoresThroughAddresses(MakeBdaTestShader(0x1000, 32)), "a BDA read was taken for a store through an address"); AgcDriver::Graphics::Require(!SpirvStoresThroughAddresses(MakeBdaDwordReadTestShader(0x1000, 4, false, false)), "BDA dword reads were taken for a store through an address"); AgcDriver::Graphics::Require(SpirvStoresThroughAddresses(MakeBdaStoreTestShader(0x1000, 7, 32, false)), "a BDA store was not seen"); From f063282659b2c9ed5bbd728f515bc9e222bc3007 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Sat, 17 Oct 2026 11:07:21 +0000 Subject: [PATCH 0698/1000] test(agc): order draws reading through the BDA table against buffer descriptor stores A draw that only reads through the table now leaves its pass open to the next draw, so its ordering against guest memory written through a buffer descriptor rests on the pass hazard tracker's unknown-read rule alone. Two cases cover it in agc_driver_address_passes: a draw that reads through the table what the previous draw stored through a buffer descriptor begins its own pass and sees the store, and a draw storing through a buffer descriptor after one that read through the table begins its own pass. Both fail when the tracker drops that rule. --- .../tests/execution/AddressPasses.cpp | 50 ++++++++++++++++++- 1 file changed, 48 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/AddressPasses.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/AddressPasses.cpp index 9de59a0343..24f031e888 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/AddressPasses.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/AddressPasses.cpp @@ -62,6 +62,7 @@ alignas(256) constexpr std::array VertexCodeUser8 = [] { }(); alignas(256) constexpr std::array PixelRead{0x7e1a0280u, 0xdc308000u, 0x0c02000du, 0xbf8c3f70u, 0x7e10030cu, 0x7e120280u, 0x7e140280u, 0x7e160280u, 0xf800180fu, 0x0b0a0908u, 0xbf810000u}; +alignas(256) constexpr std::array PixelDescriptorStore{0x7e0202f2u, 0x7e040280u, 0xe0701000u, 0x80010102u, 0x7e1002f2u, 0x7e1202f2u, 0x7e1402f2u, 0x7e1602f2u, 0xf800180fu, 0x0b0a0908u, 0xbf810000u}; alignas(256) constexpr std::array PixelStore{0x7e1a0280u, 0x7e1802f2u, 0xdc708000u, 0x00020c0du, 0x7e1002f2u, 0x7e1202f2u, 0x7e1402f2u, 0x7e1602f2u, 0xf800180fu, 0x0b0a0908u, 0xbf810000u}; struct Header { @@ -72,6 +73,7 @@ struct Header { alignas(8) Header VertexHeader{}; alignas(8) Header ReadHeader{}; alignas(8) Header StoreHeader{}; +alignas(8) Header DescriptorStoreHeader{}; void Register(Header& header, const std::uint32_t* code, std::size_t bytes, std::uint8_t type) { header.shader.file_header = 0x34333231u; @@ -136,6 +138,7 @@ void Append(std::vector& commands, std::uint32_t opcode, std::ini struct Step { const std::uint32_t* pixel; std::uint64_t pointer; + std::array descriptor{}; }; void Submit(const GuestBlock& vertices, const GuestBlock& target, std::initializer_list steps) { @@ -173,6 +176,7 @@ void Submit(const GuestBlock& vertices, const GuestBlock& target, std::initializ const auto pixel = reinterpret_cast(step.pixel); Append(commands, 0x76, {0x008, static_cast(pixel >> 8u), static_cast(pixel >> 40u)}); Append(commands, 0x76, {0x00e, static_cast(step.pointer), static_cast(step.pointer >> 32u)}); + Append(commands, 0x76, {0x010, step.descriptor[0], step.descriptor[1], step.descriptor[2], step.descriptor[3]}); Append(commands, 0x2d, {3, 2}); } Packet packet{commands.data(), static_cast(commands.size()), 0, {}}; @@ -184,12 +188,20 @@ AgcDriver::Graphics::DrawPassCounts AddressDraws() { return AgcDriver::Graphics::DrawPassTotals()[1]; } +AgcDriver::Graphics::DrawPassCounts OtherDraws() { + return AgcDriver::Graphics::DrawPassTotals()[0]; +} + +std::array StoredDword(const GuestBlock& data, std::size_t offset) { + return BufferDescriptor(data.At(offset), 0, 4, 0x31016facu); +} + void Expect(bool condition, const std::string& what) { if (!condition) throw std::runtime_error("address passes: " + what); } -std::string Counts(const AgcDriver::Graphics::DrawPassCounts& before, const AgcDriver::Graphics::DrawPassCounts& after) { - return std::to_string(after.draws - before.draws) + " address-based draws, " + std::to_string(after.passesBegun - before.passesBegun) + " began a pass, " + std::to_string(after.passesContinued - before.passesContinued) + " continued one"; +std::string Counts(const AgcDriver::Graphics::DrawPassCounts& before, const AgcDriver::Graphics::DrawPassCounts& after, const char* draws = "address-based draws") { + return std::to_string(after.draws - before.draws) + " " + draws + ", " + std::to_string(after.passesBegun - before.passesBegun) + " began a pass, " + std::to_string(after.passesContinued - before.passesContinued) + " continued one"; } void ReadersShareAPass(const GuestBlock& vertices, const GuestBlock& data, const GuestBlock& target) { @@ -225,6 +237,37 @@ void ReadSeesStore(const GuestBlock& vertices, const GuestBlock& data, const Gue Expect(after.passesBegun - before.passesBegun == 2, "a draw reading through the table continued the pass of a draw that stored through it: " + Counts(before, after)); } +void ReadSeesDescriptorStore(const GuestBlock& vertices, const GuestBlock& data, const GuestBlock& target) { + std::memset(target.Bytes(ColorOffset), 0, ColorBytes); + std::memset(data.Bytes(64), 0, 4); + const auto before = AddressDraws(); + const auto otherBefore = OtherDraws(); + Submit(vertices, target, {{PixelDescriptorStore.data(), 0, StoredDword(data, 64)}, {PixelRead.data(), data.At(64)}}); + const auto after = AddressDraws(); + const auto otherAfter = OtherDraws(); + AgcDriver::GuestMemory::FlushGpuWrites(static_cast(target.At(ColorOffset)), ColorBytes); + float stored = 0; + std::memcpy(&stored, data.Bytes(64), sizeof(stored)); + unsigned brightest = 0; + for (std::size_t i = 0; i < ColorBytes; ++i) brightest = std::max(brightest, static_cast(target.Bytes(ColorOffset)[i])); + std::printf("a draw reading through the table what the previous draw stored through a buffer descriptor: stored %g, brightest color byte %u, %s; the storing draw: %s\n", static_cast(stored), brightest, Counts(before, after).c_str(), Counts(otherBefore, otherAfter, "other draws").c_str()); + Expect(otherAfter.draws - otherBefore.draws == 1, "the draw storing through a buffer descriptor was taken for an address-based one: " + Counts(otherBefore, otherAfter, "other draws")); + Expect(stored == 1.0f, "the store through the buffer descriptor did not reach guest memory"); + Expect(after.draws - before.draws == 1 && after.passesBegun - before.passesBegun == 1, "a draw reading through the table continued the pass of a draw that stored through a buffer descriptor: " + Counts(before, after)); + Expect(brightest == 255u, "the read through the table did not see the previous draw's store through a buffer descriptor"); +} + +void DescriptorStoreAfterTableReadBreaks(const GuestBlock& vertices, const GuestBlock& data, const GuestBlock& target) { + const auto before = AddressDraws(); + const auto otherBefore = OtherDraws(); + Submit(vertices, target, {{PixelRead.data(), data.At(80)}, {PixelDescriptorStore.data(), 0, StoredDword(data, 80)}}); + const auto after = AddressDraws(); + const auto otherAfter = OtherDraws(); + std::printf("a draw storing through a buffer descriptor after a draw reading through the table: %s; the storing draw: %s\n", Counts(before, after).c_str(), Counts(otherBefore, otherAfter, "other draws").c_str()); + Expect(after.draws - before.draws == 1 && otherAfter.draws - otherBefore.draws == 1, "the reading and the storing draw were not one address-based and one other draw"); + Expect(otherAfter.passesBegun - otherBefore.passesBegun == 1, "a draw storing through a buffer descriptor continued the pass of a draw that read through the table: " + Counts(otherBefore, otherAfter, "other draws")); +} + } int main(int argc, char** argv) { @@ -241,6 +284,7 @@ int main(int argc, char** argv) { Register(VertexHeader, VertexCodeUser8.data(), sizeof(VertexCodeUser8), 2); Register(ReadHeader, PixelRead.data(), sizeof(PixelRead), 1); Register(StoreHeader, PixelStore.data(), sizeof(PixelStore), 1); + Register(DescriptorStoreHeader, PixelDescriptorStore.data(), sizeof(PixelDescriptorStore), 1); GuestBlock readOnlyVertices; GuestBlock data; GuestBlock target; @@ -252,6 +296,8 @@ int main(int argc, char** argv) { if (only.empty() || only == "--readers") ReadersShareAPass(readOnlyVertices, data, target); if (only.empty() || only == "--store-after-read") StoreAfterTableReadBreaks(readOnlyVertices, data, target); if (only.empty() || only == "--read-sees-store") ReadSeesStore(readOnlyVertices, data, target); + if (only.empty() || only == "--read-sees-descriptor-store") ReadSeesDescriptorStore(readOnlyVertices, data, target); + if (only.empty() || only == "--descriptor-store-after-read") DescriptorStoreAfterTableReadBreaks(readOnlyVertices, data, target); AgcDriverShutdown_nid_postfix(); std::puts("address passes: draws reading through the BDA table share a render pass, and a store through it still begins one"); return 0; From e5b4b326ca77dbf962fa6d6bd831cdfd4c8f1b64 Mon Sep 17 00:00:00 2001 From: thuanquang Date: Sat, 17 Oct 2026 18:13:41 +0700 Subject: [PATCH 0699/1000] test(agc): accept any NaN where the f16 fma and fma_mix tests expect one agc_driver_f16_fma_denormal_modes and agc_driver_fma_mix_f16_denormal_modes compared NaN results (inf * 0) bit for bit with the RX 6700 XT's, but the NaN bits of f16 arithmetic and of v_fma_mix_f32 with f16 sources are left to the driver, so the tests fail on NVIDIA (0x7fff for 0xfe00). Rows whose expected value is a NaN now accept any NaN; every other row is still exact. Co-Authored-By: Claude Opus 5.5 --- .../tests/execution/F16FmaDenormalModes.cpp | 7 ++++++- .../tests/execution/FmaMixF16DenormalModes.cpp | 7 ++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/F16FmaDenormalModes.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/F16FmaDenormalModes.cpp index 1152f71606..0bf9bf31c7 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/F16FmaDenormalModes.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/F16FmaDenormalModes.cpp @@ -205,13 +205,18 @@ void Run(AgcDriver::VulkanDevice& device, const Mode& mode) { device.WaitIdle(); } +bool Matches(std::uint32_t actual, std::uint32_t expected) { + const auto nan = [](std::uint32_t half) { return (half & 0x7fffu) > 0x7c00u; }; + return actual == expected || (nan(expected) && actual <= 0xffffu && nan(actual)); +} + void Check(const Mode& mode) { constexpr const char* Names[Results] = {"v_fma_f16", "v_fmac_f16"}; for (std::uint32_t tid = 0; tid < Threads; ++tid) { const std::uint32_t expected = Expected[tid][mode.column]; for (std::uint32_t form = 0; form < Results; ++form) { const std::uint32_t actual = Output[tid * Results + form]; - Require(actual == expected, std::string(Names[form]) + " " + mode.name + " of " + Hex(Rows[tid][0]) + ", " + Hex(Rows[tid][1]) + ", " + Hex(Rows[tid][2]) + " is " + Hex(actual) + ", expected " + Hex(expected)); + Require(Matches(actual, expected), std::string(Names[form]) + " " + mode.name + " of " + Hex(Rows[tid][0]) + ", " + Hex(Rows[tid][1]) + ", " + Hex(Rows[tid][2]) + " is " + Hex(actual) + ", expected " + Hex(expected)); } } } diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/FmaMixF16DenormalModes.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/FmaMixF16DenormalModes.cpp index 537b0bb483..c772a521e1 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/FmaMixF16DenormalModes.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/FmaMixF16DenormalModes.cpp @@ -166,6 +166,11 @@ void Run(AgcDriver::VulkanDevice& device, const Mode& mode) { device.WaitIdle(); } +bool Matches(std::uint32_t actual, std::uint32_t expected) { + const auto nan = [](std::uint32_t single) { return (single & 0x7fffffffu) > 0x7f800000u; }; + return actual == expected || (nan(expected) && nan(actual)); +} + void Check(const Mode& mode) { constexpr const char* Names[Results] = {"v_fma_mix_f32 of three f16 sources", "v_fma_mix_f32 of an f16, an f32 and an f16 source"}; const std::uint32_t f32Kept = (mode.value.floatMode >> 4u) & 1u; @@ -174,7 +179,7 @@ void Check(const Mode& mode) { for (std::uint32_t form = 0; form < Results; ++form) { const std::uint32_t actual = Output[tid * Results + form]; const std::uint32_t expected = Expected[f32Kept][f16InputsKept][tid][form]; - Require(actual == expected, std::string(Names[form]) + " " + mode.name + " of " + Hex(Rows[tid][0]) + ", " + Hex(Rows[tid][1]) + ", " + Hex(Rows[tid][2]) + " is " + Hex(actual) + ", expected " + Hex(expected)); + Require(Matches(actual, expected), std::string(Names[form]) + " " + mode.name + " of " + Hex(Rows[tid][0]) + ", " + Hex(Rows[tid][1]) + ", " + Hex(Rows[tid][2]) + " is " + Hex(actual) + ", expected " + Hex(expected)); } } } From 6a8062b487bfabda73c0fc072d7ba0d3ea76604e Mon Sep 17 00:00:00 2001 From: Aryan Sharma Date: Sat, 17 Oct 2026 16:45:41 +0530 Subject: [PATCH 0700/1000] feat(libSceNpTrophy2): declare sceNpTrophy2GetRewardIcon's arguments The bundled PSNCore.prx of PPSA06888 calls sceNpTrophy2GetRewardIcon with (context, handle, reward_id, buffer, size): once with a null buffer to read the size, then with a buffer of that size. The function still throws because the reward icon data is unknown. Co-Authored-By: Claude Haiku 5.5 --- core/libs/prx/libSceNpTrophy2/src/Context.cpp | 8 +++++++- docs/dev/debt/libSceNpTrophy2.md | 4 ++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceNpTrophy2/src/Context.cpp b/core/libs/prx/libSceNpTrophy2/src/Context.cpp index 74048df024..197f834f71 100644 --- a/core/libs/prx/libSceNpTrophy2/src/Context.cpp +++ b/core/libs/prx/libSceNpTrophy2/src/Context.cpp @@ -1,3 +1,4 @@ +#include #include #include #include @@ -35,7 +36,12 @@ int APS5_VABI sceNpTrophy2UnregisterUnlockCallback() { } -int APS5_VABI sceNpTrophy2GetRewardIcon(void) { +int APS5_VABI sceNpTrophy2GetRewardIcon(int context, int handle, int reward_id, void* buffer, size_t* size) { + (void)context; + (void)handle; + (void)reward_id; + (void)buffer; + (void)size; NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/docs/dev/debt/libSceNpTrophy2.md b/docs/dev/debt/libSceNpTrophy2.md index 8e1fa7ef7d..142e30364e 100644 --- a/docs/dev/debt/libSceNpTrophy2.md +++ b/docs/dev/debt/libSceNpTrophy2.md @@ -3,3 +3,7 @@ ### Silent stubs - [libSceNpTrophy2](../../../core/libs/prx/libSceNpTrophy2/src/GameInfo.cpp): `sceNpTrophy2RegisterUnlockCallback` accepts a callback that is never called, as no trophy is ever unlocked, `sceNpTrophy2UnregisterUnlockCallback` only returns success, and `sceNpTrophy2ShowTrophyList` returns success without showing a list (there is no system trophy UI; seen on PPSA15552). + +### Unknown function info + +- [sceNpTrophy2GetRewardIcon](../../../core/libs/prx/libSceNpTrophy2/src/Context.cpp) (libSceNpTrophy2) takes `(int context, int handle, int reward_id, void* buffer, size_t* size)`, from the call sites in the bundled `Media/Plugins/PSNCore.prx` of PPSA06888, the same shape as `sceNpTrophy2GetTrophyIcon`. The title calls it twice: first with a null buffer to read the size, then with a buffer of that size allocated with `malloc`. The meaning of the third argument is unverified, and the reward icon data is unknown, so every call throws From 91fdcbdf1aaab7b4e202d7c6843a2b64850d2b0d Mon Sep 17 00:00:00 2001 From: inmert <54280820+inmert@users.noreply.github.com> Date: Sat, 17 Oct 2026 16:47:29 +0530 Subject: [PATCH 0701/1000] ci: run the relinker TLS coverage tests under AddressSanitizer The segment-prefix-at-eof case from #6401 only catches the 1-byte read past the input when the relinker is built with AddressSanitizer, and no CI job did that. The new job builds the relinker alone in Debug with ANYPS5_ENABLE_ASAN and runs the tls_function_coverage tests. It has to be Debug: at -O3 GCC no longer emits the stray load, so a Release ASan build passes with the fix reverted. --- .github/workflows/build.yml | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e5d2a9a22d..eeb9bf887f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -263,6 +263,35 @@ jobs: - run: cmake --build build-relinker --parallel 3 - run: ctest --test-dir build-relinker --output-on-failure --timeout 120 + relinker-asan: + needs: eligibility + if: ${{ needs.eligibility.outputs.relinker == 'true' }} + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@v7 + with: + ref: ${{ needs.eligibility.outputs.sha }} + persist-credentials: false + - name: Merge the pull request into main + if: ${{ needs.eligibility.outputs.head != '' }} + env: + PR_NUMBER: ${{ needs.eligibility.outputs.pr }} + PR_HEAD: ${{ needs.eligibility.outputs.head }} + run: | + read -ra numbers <<< "$PR_NUMBER" + read -ra heads <<< "$PR_HEAD" + refspecs=() + for number in "${numbers[@]}"; do refspecs+=("+refs/pull/$number/head:refs/remotes/pull/$number"); done + git fetch --no-tags --unshallow origin "${refspecs[@]}" + for i in "${!numbers[@]}"; do + if [ "$(git rev-parse "refs/remotes/pull/${numbers[i]}")" != "${heads[i]}" ]; then echo "::error::Pull request #${numbers[i]} has newer commits; start a new build"; exit 1; fi + if ! git -c user.name=ci -c user.email=ci@localhost merge --no-ff --no-edit "refs/remotes/pull/${numbers[i]}"; then echo "::error::Pull request #${numbers[i]} does not merge cleanly into main and the pull requests before it; rebase it first"; exit 1; fi + done + - run: cmake -S . -B build-relinker-asan -G Ninja -DCMAKE_BUILD_TYPE=Debug -DCMAKE_CXX_COMPILER=g++ -DANYPS5_RELINKER_ONLY=ON -DANYPS5_ENABLE_ASAN=ON -DBUILD_TESTING=ON + - run: cmake --build build-relinker-asan --parallel 4 + - run: ctest --test-dir build-relinker-asan --output-on-failure --timeout 120 --label-regex '^tls_function_coverage$' --no-tests=error + register-context: needs: eligibility if: ${{ needs.eligibility.outputs.register-context == 'true' }} From 1783de553b1b8df1380be4199ca83afb1a41462b Mon Sep 17 00:00:00 2001 From: emirplus Date: Sat, 17 Oct 2026 14:17:42 +0300 Subject: [PATCH 0702/1000] fix(tools): handle trailing separators in select_tests root paths --- tools/select_tests.py | 6 ++++-- tools/tests/test_select_tests.py | 10 ++++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/tools/select_tests.py b/tools/select_tests.py index 9c77894a0a..5def841494 100644 --- a/tools/select_tests.py +++ b/tools/select_tests.py @@ -101,11 +101,13 @@ class Build: def inside(path, root): - return os.path.normcase(path).startswith(os.path.normcase(root) + os.sep) + prefix = os.path.normcase(root).rstrip(os.sep) + os.sep + return os.path.normcase(path).startswith(prefix) def relative(path, root): - return path[len(root) + 1:].replace(os.sep, "/") + root_len = len(root.rstrip(os.sep)) + return path[root_len + 1:].replace(os.sep, "/") def chunks(names): diff --git a/tools/tests/test_select_tests.py b/tools/tests/test_select_tests.py index 434a3da749..8a2099e69b 100644 --- a/tools/tests/test_select_tests.py +++ b/tools/tests/test_select_tests.py @@ -129,6 +129,16 @@ class CaseInsensitivePathTests(unittest.TestCase): self.assertIsNone(build.repo(os.path.join(os.sep, "WORK", "SOURCE", "BUILD", "Generated.h"))) self.assertEqual(build.key(os.path.join("tests", "MemoryPoolTest.exe")), build.key(os.path.join(os.sep, "WORK", "SOURCE", "BUILD", "TESTS", "memorypooltest.exe"))) + def test_trailing_separator_in_root_is_handled(self): + with mock.patch.object(select_tests.os.path, "normcase", str.lower): + build = self.build() + build.source = build.source + os.sep + build.path = build.path + os.sep + self.assertEqual(build.output(os.path.join(os.sep, "work", "source", "build", "tests", "MemoryPoolTest.exe")), "tests/MemoryPoolTest.exe") + self.assertEqual(build.output(os.path.join("tests", "MemoryPoolTest.exe")), "tests/MemoryPoolTest.exe") + self.assertEqual(build.repo(os.path.join(os.sep, "work", "source", "core", "libSceAgc", "Driver.cpp")), "core/libSceAgc/Driver.cpp") + self.assertIsNone(build.repo(os.path.join(os.sep, "WORK", "SOURCE", "BUILD", "Generated.h"))) + if __name__ == "__main__": unittest.main() From 91fbafd94a23a1ad75306a66f1b587dac3c3685b Mon Sep 17 00:00:00 2001 From: milescoviello Date: Sat, 17 Oct 2026 11:20:03 +0000 Subject: [PATCH 0703/1000] fix(libkernel): keep refusing a read low-water mark above 1 on non-stream sockets On Linux, FIONREAD on a datagram socket reports the next datagram only, where FreeBSD's sbavail counts every queued byte, so a read mark above 1 there could never fire. kevent now throws for it as before; stream sockets and marks of 1 are unchanged. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libkernel/Equeue/src/Kqueue.cpp | 3 +++ core/libs/prx/libkernel/Socket/include/SocketRuntime.hpp | 1 + core/libs/prx/libkernel/Socket/src/Udp.cpp | 5 +++++ core/libs/tests/GuestKqueue.cpp | 8 ++++++++ docs/dev/debt/libkernel.md | 2 +- 5 files changed, 18 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libkernel/Equeue/src/Kqueue.cpp b/core/libs/prx/libkernel/Equeue/src/Kqueue.cpp index 6653807951..3381410fb9 100644 --- a/core/libs/prx/libkernel/Equeue/src/Kqueue.cpp +++ b/core/libs/prx/libkernel/Equeue/src/Kqueue.cpp @@ -137,6 +137,9 @@ std::int64_t DescriptorLowWater(const KernelEvent& change) { if (change.filter == EVFILT_WRITE && change.data > 1) { NotImplemented_nid_no_patch("kevent: NOTE_LOWAT above 1 on EVFILT_WRITE"); } + if (change.data > 1 && GuestSockets::Type(static_cast(change.ident)) != 1) { + NotImplemented_nid_no_patch("kevent: NOTE_LOWAT above 1 on a socket that is not a stream"); + } return change.data; } diff --git a/core/libs/prx/libkernel/Socket/include/SocketRuntime.hpp b/core/libs/prx/libkernel/Socket/include/SocketRuntime.hpp index 0e135ed198..5b22edcf5d 100644 --- a/core/libs/prx/libkernel/Socket/include/SocketRuntime.hpp +++ b/core/libs/prx/libkernel/Socket/include/SocketRuntime.hpp @@ -16,6 +16,7 @@ int Close(int descriptor); void Forget(int descriptor); bool IsOpen(int descriptor); int Family(int descriptor); +int Type(int descriptor); bool Listening(int descriptor); std::int64_t Read(int descriptor, void* buffer, std::size_t length); std::int64_t Write(int descriptor, const void* buffer, std::size_t length); diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index dcd61bc45e..b8adfa529c 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -401,6 +401,11 @@ int GuestSockets::Family(int descriptor) { const auto found = sockets.find(descriptor); return found == sockets.end() ? -1 : found->second->family; } +int GuestSockets::Type(int descriptor) { + std::lock_guard lock(socketsMutex); + const auto found = sockets.find(descriptor); + return found == sockets.end() ? -1 : found->second->type; +} bool GuestSockets::Listening(int descriptor) { std::shared_ptr socket; { diff --git a/core/libs/tests/GuestKqueue.cpp b/core/libs/tests/GuestKqueue.cpp index a7f51bd74c..d7e003daaa 100644 --- a/core/libs/tests/GuestKqueue.cpp +++ b/core/libs/tests/GuestKqueue.cpp @@ -291,6 +291,14 @@ static void LowWaterMarks() { Require(Wait(kq, events, 4) == 1 && events[0].filter == EvfiltWrite); Require(Throws([kq, &pair] { Apply(kq, Change(static_cast(pair[1]), EvfiltWrite, EvAdd, NoteLowat, 2)); })); Require(Throws([kq, reader] { Apply(kq, Change(reader, EvfiltRead, EvAdd, NoteLowat, 0)); })); + int datagrams[2]; + Require(socketpair_nid_postfix(Unix, 2, 0, datagrams) == 0); + const auto datagram = static_cast(datagrams[0]); + Require(Throws([kq, datagram] { Apply(kq, Change(datagram, EvfiltRead, EvAdd, NoteLowat, 2)); })); + Require(Apply(kq, Change(datagram, EvfiltRead, EvAdd, NoteLowat, 1)) == 0); + Require(send_nid_postfix(datagrams[1], "f", 1, 0) == 1); + Require(Poll(kq, events, 4) == 1 && events[0].ident == datagram && events[0].data == 1); + Require(close_nid_postfix(datagrams[0]) == 0 && close_nid_postfix(datagrams[1]) == 0); Require(close_nid_postfix(pair[1]) == 0); Require(Poll(kq, events, 4) == 1 && events[0].ident == reader && (events[0].flags & EvEof) != 0); Require(close_nid_postfix(pair[0]) == 0); diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 1ce4e9fa5b..822cea3f67 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -109,7 +109,7 @@ - [sceKernelMprotect, sceKernelMtypeprotect](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (libkernel) over a range with a gap that is not registered in the allocation tracker protect the registered parts and skip the gap, and return 0, as FreeBSD 11.4's [`vm_map_protect`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/vm/vm_map.c) does (it has no check for holes); a range whose start is unmapped (also when later pages are mapped) returns `SCE_KERNEL_ERROR_EINVAL` (`mprotect`: `EINVAL`) because an existing test pins that for an unmapped start; FreeBSD answers 0 there too (`vm_map_protect` takes the next entry), so this errno is not taken from the source and was not changed. `sceKernelMprotect` threw for a gap before, which ended the title. Every part is checked for a GPU or image pin before the first one is protected; the parts are then protected one after the other, so a host protection failure in a later part leaves the earlier parts changed (FreeBSD's `vm_map_protect` cannot fail in its second pass). The behaviour on the console is not known. None of this was measured on a console. - [sceKernelMprotect protection bits](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - bits outside `0x3F7` are dropped instead of refused, as FreeBSD 11's [`sys_mprotect`](https://github.com/freebsd/freebsd-src/blob/34bd34750187bae02a0086402c78c031c59dd8b3/sys/vm/vm_mmap.c#L606) does with `VM_PROT_ALL`. Onimusha: Way of the Sword (PPSA27836 01.000.001) maps direct memory with `0xf2` and then protects it with `0xfffffff2`. Not measured on a console; the map calls and `sceKernelMtypeprotect` still refuse undefined bits - [JIT shared memory](../../../core/libs/prx/libkernel/DirectMemory/Jit.cpp) (libkernel) - `sceKernelJitCreateSharedMemory`, `sceKernelJitCreateAliasOfSharedMemory` and `sceKernelJitMapSharedMemory` take the signatures of [Orbital](https://github.com/AlexAltea/orbital)'s `tools/ida/db_types.json` (name, length, maximum protection, descriptor out; descriptor, maximum protection, descriptor out; descriptor, protection, address in and out), which ps4sdk, ps5-jar-loader and the call sites in RetroArch PPSA99169, XPSemu PPSA97358 and VLC PPSA85300 match; SharpProspero declares the alias as returning the descriptor. The object is a host anonymous file shared by its descriptors (a `memfd` on Linux, a delete-on-close temporary file mapped as an executable section on Windows), the alias is a duplicate descriptor, and every map maps the whole object, so writes through one view show through the others; a non-null address is a hint, as in `mmap` without `MAP_FIXED`. Not measured on the console: a length that is zero or not a multiple of 16 KiB and protection bits other than CPU read, write and execute are `EINVAL`; an alias or a map asking for more than the descriptor's maximum protection (write and execute counting as read) is `EACCES`, as FreeBSD 11's `vm_map_protect` treats a protection past `max_protection`; a descriptor that is not a JIT object is `EBADF`. The name is ignored, the objects do not count against the flexible memory budget, `mprotect` of a view is not limited to the maximum protection, and `mmap` of the descriptors is unsupported, as for `shm_open`. On Linux the `memfd` is created without `MFD_EXEC`, which Linux 6.3 added and older kernels reject with `EINVAL`, and relies on the protection of the mapping: the flag only sets the file's execute permission bits, and the direct memory `memfd` is mapped executable the same way; a host with `vm.memfd_noexec` set to 2 refuses the creation and the call throws -- [kqueue and kevent](../../../core/libs/prx/libkernel/Equeue/src/Kqueue.cpp) (libkernel) follow FreeBSD 11's [`kern_event.c`](https://github.com/freebsd/freebsd-src/blob/34bd34750187bae02a0086402c78c031c59dd8b3/sys/kern/kern_event.c) (`kqueue_kevent`, `kqueue_register`, `kqueue_scan`, the timer and user filters) over the event queue; nothing is measured on a console. The descriptor is a host null device registered as the kqueue: `close` and `sceKernelClose` close the queue and `dup`/`dup2` of it throw, but `read`, `write` and `fstat` act on the null device instead of failing with `EOPNOTSUPP` or reporting `S_IFIFO`. Read and write events are supported on guest sockets only, plus `EVFILT_READ` on the read end of a pipe (see the kevent on a pipe entry) and the timer, user and [vnode](../../../core/libs/prx/libkernel/Equeue/src/FileEvent.cpp) filters; other filters, `EV_CLEAR` on a socket or a pipe, `EVFILT_READ` on another kqueue, a `NOTE_NSECONDS` period above one second with a fraction (11.0 converts the fraction with the microsecond macro) and any change to an existing timer (11.0 keeps the armed expiry and applies the new period after it, 11.4's `filt_timertouch` restarts it) throw. `EV_DISABLE`, `EV_DISPATCH` and `EV_FORCEONESHOT` follow kern_event.c: a disabled knote stays registered, stops polling or firing and is not reported, a trigger raised while it is disabled is kept and reported after `EV_ENABLE`, where `knote` would drop it, `EV_DISPATCH` reports once and then disables the knote, and `EV_FORCEONESHOT` is ignored on a new knote while on an existing one it sets `EV_ONESHOT` and fires the knote; a negative `nevents` fails with `EINVAL`. A new user event keeps its `fflags` unmasked and is not triggered by `NOTE_TRIGGER`, because `filt_userattach` reads the already cleared `kn_fflags`. The timer limit `kq_calloutmax` (`ENOMEM`) is not enforced, and closing a kqueue wakes a thread waiting on it in `kevent` with `EBADF`, where `kqueue_drain` makes the close wait for that thread. A level-triggered event is reported once per call and moved behind the other events, as in `kqueue_scan`; the `sceKernel*Equeue` calls keep their own collection loop. `NOTE_LOWAT` on a socket sets the low-water mark as FreeBSD 11's [`filt_soread` and `filt_sowrite`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/uipc_socket.c) do, and a later change of the knote replaces it as `kqueue_register` replaces `kn_sdata`: a read event fires once the buffered bytes reach the mark, or at end of file or on a listening socket. A write event takes a mark of 1 only, where the host's writability stands in for one free byte, since the host does not report the send buffer's free space; other write marks and marks below 1, which make FreeBSD's read filter always fire, throw +- [kqueue and kevent](../../../core/libs/prx/libkernel/Equeue/src/Kqueue.cpp) (libkernel) follow FreeBSD 11's [`kern_event.c`](https://github.com/freebsd/freebsd-src/blob/34bd34750187bae02a0086402c78c031c59dd8b3/sys/kern/kern_event.c) (`kqueue_kevent`, `kqueue_register`, `kqueue_scan`, the timer and user filters) over the event queue; nothing is measured on a console. The descriptor is a host null device registered as the kqueue: `close` and `sceKernelClose` close the queue and `dup`/`dup2` of it throw, but `read`, `write` and `fstat` act on the null device instead of failing with `EOPNOTSUPP` or reporting `S_IFIFO`. Read and write events are supported on guest sockets only, plus `EVFILT_READ` on the read end of a pipe (see the kevent on a pipe entry) and the timer, user and [vnode](../../../core/libs/prx/libkernel/Equeue/src/FileEvent.cpp) filters; other filters, `EV_CLEAR` on a socket or a pipe, `EVFILT_READ` on another kqueue, a `NOTE_NSECONDS` period above one second with a fraction (11.0 converts the fraction with the microsecond macro) and any change to an existing timer (11.0 keeps the armed expiry and applies the new period after it, 11.4's `filt_timertouch` restarts it) throw. `EV_DISABLE`, `EV_DISPATCH` and `EV_FORCEONESHOT` follow kern_event.c: a disabled knote stays registered, stops polling or firing and is not reported, a trigger raised while it is disabled is kept and reported after `EV_ENABLE`, where `knote` would drop it, `EV_DISPATCH` reports once and then disables the knote, and `EV_FORCEONESHOT` is ignored on a new knote while on an existing one it sets `EV_ONESHOT` and fires the knote; a negative `nevents` fails with `EINVAL`. A new user event keeps its `fflags` unmasked and is not triggered by `NOTE_TRIGGER`, because `filt_userattach` reads the already cleared `kn_fflags`. The timer limit `kq_calloutmax` (`ENOMEM`) is not enforced, and closing a kqueue wakes a thread waiting on it in `kevent` with `EBADF`, where `kqueue_drain` makes the close wait for that thread. A level-triggered event is reported once per call and moved behind the other events, as in `kqueue_scan`; the `sceKernel*Equeue` calls keep their own collection loop. `NOTE_LOWAT` on a socket sets the low-water mark as FreeBSD 11's [`filt_soread` and `filt_sowrite`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/uipc_socket.c) do, and a later change of the knote replaces it as `kqueue_register` replaces `kn_sdata`: a read event fires once the buffered bytes reach the mark, or at end of file or on a listening socket. A read mark above 1 is taken on stream sockets only: on a datagram socket Linux's `FIONREAD` reports the next datagram where FreeBSD's `sbavail` counts every queued byte, so it throws. A write event takes a mark of 1 only, where the host's writability stands in for one free byte, since the host does not report the send buffer's free space; other write marks and marks below 1, which make FreeBSD's read filter always fire, throw - [EVFILT_VNODE, sceKernelAddFileEvent and sceKernelDeleteFileEvent](../../../core/libs/prx/libkernel/Equeue/src/FileEvent.cpp) (libkernel) watch the host file or directory behind the descriptor and turn host changes into FreeBSD 11 vnode hints, which [`filt_vfsvnode`](https://github.com/freebsd/freebsd-src/blob/34bd34750187bae02a0086402c78c031c59dd8b3/sys/kern/vfs_subr.c#L5032) adds whole when they share a bit with the requested `fflags`: a write that grows a file is `NOTE_WRITE | NOTE_EXTEND`, another write `NOTE_WRITE`, a shrinking truncate or an attribute change `NOTE_ATTRIB`, a higher link count `NOTE_LINK`, a lower one or a pending delete `NOTE_DELETE`, a new path `NOTE_RENAME`; in a directory, an entry created, removed or renamed is `NOTE_WRITE`, with `NOTE_LINK` for a subdirectory, and on Linux an entry moved from or to another directory adds `NOTE_EXTEND`; inotify queues the two halves of a rename one after the other, so a watcher woken by the first can read it alone, and a half read without the other is classified only after a `getdents64` on the watched directory, which takes the directory lock that `rename` holds until both halves are queued ([`do_renameat2`](https://github.com/torvalds/linux/blob/v6.8/fs/namei.c#L4975) around `vfs_rename` and its `fsnotify_move`, [`iterate_dir`](https://github.com/torvalds/linux/blob/v6.8/fs/readdir.c#L103)), an ordering taken from the kernel source, not from inotify(7), which promises none; a directory registered with `NOTE_EXTEND` on Windows is accepted, as `vfs_kqfilter` does not check it, but never receives it, because the directory handle cannot tell a move between directories from a removal plus an addition. FreeBSD's hints depend on the filesystem and the console's filesystems are not known: a growing `ftruncate` reports `NOTE_WRITE | NOTE_EXTEND` where `vop_setattr_post` reports `NOTE_ATTRIB`, and on Windows `utimes` on a file reports `NOTE_WRITE`. The three Windows notifications are told apart in the error, which names the `parent`, `entries` or `subdirectories` watch, the path it was given and `GetLastError`, but a `FindFirstChangeNotification` that fails still throws rather than returning an error to the guest: the EVFILT_VNODE attach path is [`vfs_kqfilter`](https://github.com/freebsd/freebsd-src/blob/34bd34750187bae02a0086402c78c031c59dd8b3/sys/kern/vfs_subr.c#L4936), which returns `EINVAL`, `ENOMEM` or 0 after `fget` has turned a descriptor that is not open into `EBADF`, and consults no directory permission, so no FreeBSD errno stands for a host that refuses to notify, and what the console does on such a host is not measured. Only the `parent` watch can fail this way in practice: `entries` and `subdirectories` need the `FILE_LIST_DIRECTORY` that opening the watched directory already took. The parent handle produces no hint of its own, as `RunHostWatch` only calls `FindNextChangeNotification` on it, so a later change may leave the watch on its 50 ms poll instead of throwing; that is not done here. Windows uses `FindFirstChangeNotification` on the parent directory (and on a watched directory itself) and compares `GetFileInformationByHandleEx` snapshots at least every 50 ms, because a write through an open handle is reported only when the handle is flushed or closed, so file hints can arrive 50 ms late, and a file deleted while watched, which the POSIX delete of Windows 10 1903 moves into `$Extend\$Deleted` until its last handle closes, reports `NOTE_DELETE` without `NOTE_RENAME` and keeps its handle without a parent directory watch, as that directory cannot be watched; Linux uses inotify on the file and is not run. `NOTE_REVOKE`, `NOTE_OPEN`, `NOTE_CLOSE`, `NOTE_CLOSE_WRITE`, `NOTE_READ`, unknown bits and descriptors other than files, directories, pipes and sockets throw. `sceKernelAddFileEvent(eq, fd, watch, udata)` and `sceKernelDeleteFileEvent(eq, fd)` take the [OpenOrbis](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain/blob/0a1aaf9d/include/orbis/libkernel.h#L35) and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0/src/SharpProspero/Interop/Kernel/KernelEqueue.cs#L164) signatures; `watch` is taken as the `fflags` and the event is added with `EV_CLEAR`, neither confirmed on a title (feat(libkernel): implement kevent EV_DISABLE, EV_DISPATCH and EV_FORCEONESHOT) (fix(libkernel): follow kqueue_register for EV_FORCEONESHOT and modify flags) - [sem_timedwait](../../../core/libs/prx/libkernel/Pthread/Posix/Sem.cpp) (libkernel) consumes an available count before checking the timeout and waits indefinitely for a null timeout, following FreeBSD 11's `sem_timedwait`. Console behavior for null and invalid timeouts when a count is available is unverified. - [tcsendbreak, tcdrain, tcflush, tcflow, tcgetpgrp, tcsetpgrp](../../../core/libs/prx/libkernel/File/src/Terminal.cpp) (libkernel) follow FreeBSD 11.4's [`termios.c`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/gen/termios.c): `tcflush` answers `EINVAL` for a queue other than `TCIFLUSH`, `TCOFLUSH` or `TCIOFLUSH` and `tcflow` for an action outside `TCOOFF`..`TCION` before the descriptor is looked at, and every other call fails as its terminal ioctl does on a descriptor that is not a terminal, with the errors `tcgetattr` gives, so `tcsendbreak` never reaches its 0.4 s pause. FreeBSD's `pipe_ioctl` answers `TIOCGPGRP` and `TIOCSPGRP` on a pipe or FIFO with the descriptor's SIGIO owner, which is not modelled, so `tcgetpgrp` and `tcsetpgrp` throw on a host pipe or FIFO above descriptor 2; descriptors 0 to 2 are taken as non-terminals that are not pipes, whatever the host attaches to them, so a pipe the guest creates on a freed descriptor 0 to 2 gets `ENOTTY` there. The PS5 `libkernel` is assumed to use this FreeBSD code, and `tcdrain` is not a cancellation point here From 9c6efd3771e0d526ab47665316e76c59165f60bc Mon Sep 17 00:00:00 2001 From: emirplus Date: Sat, 17 Oct 2026 14:21:14 +0300 Subject: [PATCH 0704/1000] fix(tools): recognize indented skip reasons in check_skips list --- tools/check_skips.py | 2 +- tools/tests/test_check_skips.py | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/tools/check_skips.py b/tools/check_skips.py index af4db8a61c..169cc7bc31 100644 --- a/tools/check_skips.py +++ b/tools/check_skips.py @@ -50,7 +50,7 @@ if __name__ == "__main__": parser.error(str(error)) if args.list: for name, output in found: - reason = next((line.strip() for line in output.splitlines() if line.startswith("skipped")), "") + reason = next((line.strip() for line in output.splitlines() if line.strip().lower().startswith("skipped")), "") print(f"{name}: {reason}") raise SystemExit if args.job == "windows": diff --git a/tools/tests/test_check_skips.py b/tools/tests/test_check_skips.py index 5ec9c41943..f56f9cc50a 100644 --- a/tools/tests/test_check_skips.py +++ b/tools/tests/test_check_skips.py @@ -152,6 +152,23 @@ class CheckSkipsTests(unittest.TestCase): self.assertEqual(result.returncode, 0, result.stdout + result.stderr) self.assertIn("all expected", result.stdout) + def test_list_reports_skipped_reasons_including_indented_lines(self): + xml = ( + '' + '' + '\n skipped, host imports are unavailable\n' + '\n Skipped, no display\n' + '' + ) + with tempfile.TemporaryDirectory() as directory: + junit = Path(directory) / "junit.xml" + junit.write_text(xml, encoding="utf-8") + result = subprocess.run([sys.executable, "-I", str(TOOL), str(junit), "--list"], capture_output=True, text=True) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertIn("normal: skipped, device limitation", result.stdout) + self.assertIn("indented: skipped, host imports are unavailable", result.stdout) + self.assertIn("capitalized: Skipped, no display", result.stdout) + From 14acf7f444747ffde02b29a3ae85dc4fc8344ecf Mon Sep 17 00:00:00 2001 From: lorkydey <57268822+Lorkydey@users.noreply.github.com> Date: Sat, 17 Oct 2026 13:25:37 +0200 Subject: [PATCH 0705/1000] feat(libc): implement bounded error message APIs --- core/libs/prx/libc/CMakeLists.txt | 13 +++ core/libs/prx/libc/src/WideConversion.cpp | 51 +++++++++ core/libs/tests/GuestSafeErrors.cpp | 126 ++++++++++++++++++++++ docs/dev/debt/libc.md | 1 + 4 files changed, 191 insertions(+) create mode 100644 core/libs/tests/GuestSafeErrors.cpp diff --git a/core/libs/prx/libc/CMakeLists.txt b/core/libs/prx/libc/CMakeLists.txt index aa0c4d3711..076ae68f47 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -1,3 +1,16 @@ +if(BUILD_TESTING) + add_test_executable(guest_safe_error_tests ${LIBS_INCLUDE_DIR}/tests/GuestSafeErrors.cpp) + target_include_directories(guest_safe_error_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_safe_error_tests PRIVATE libc) + configure_windows_unwind(guest_safe_error_tests) + add_test(NAME guest_safe_errors COMMAND guest_safe_error_tests) + set_tests_properties(guest_safe_errors PROPERTIES TIMEOUT 10) + if(WIN32) + set_tests_properties(guest_safe_errors PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() + target_sources(libc PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/Export.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/General.cpp diff --git a/core/libs/prx/libc/src/WideConversion.cpp b/core/libs/prx/libc/src/WideConversion.cpp index 9b7c52d1ab..09fb1fd1c9 100644 --- a/core/libs/prx/libc/src/WideConversion.cpp +++ b/core/libs/prx/libc/src/WideConversion.cpp @@ -4,7 +4,10 @@ #include #include #include +#include +#include #include +#include extern "C" void APS5_VABI ignore_handler_s_nid_postfix(const char* message, void* pointer, int error) { SonyIgnoreHandlerS(message, pointer, error); @@ -32,3 +35,51 @@ extern "C" std::size_t APS5_VABI wcstombs_nid_postfix(char* destination, const s extern "C" int APS5_VABI wcscpy_s_nid_postfix(std::uint16_t* dest, std::size_t size, const std::uint16_t* src) { return SonyWcscpyS(dest, size, src); } + +extern "C" int APS5_VABI strerror_r_nid_postfix(int error, char* buffer, std::size_t capacity); + +namespace { + +std::size_t ReadErrorMessage(int error, char (&message)[128]) { + const int result = strerror_r_nid_postfix(error, message, sizeof(message)); + if (result != 0 && result != 22) throw std::runtime_error("strerror_s: error message exceeds internal buffer"); + return std::strlen(message); +} + +void ErrorMessageConstraint(char* buffer) { + SonyConstraintHandler handler; + { + std::lock_guard lock(SonyConstraintMutex()); + handler = SonyConstraintHandlerSlot(); + } + if (handler == SonyDefaultConstraintHandler) { + SonyDefaultConstraintHandler("strerror_s", buffer, 22); + } else { + using GuestHandler = void (APS5_VABI *)(const char*, void*, int); + reinterpret_cast(handler)("strerror_s", buffer, 22); + } +} + +} + +extern "C" std::size_t APS5_VABI strerrorlen_s_nid_postfix(int error) { + char message[128]; + return ReadErrorMessage(error, message); +} + +extern "C" int APS5_VABI strerror_s_nid_postfix(char* buffer, std::size_t capacity, int error) { + if (!buffer || capacity == 0 || capacity > (std::numeric_limits::max() >> 1)) { + ErrorMessageConstraint(buffer); + return 22; + } + char message[128]; + const auto length = ReadErrorMessage(error, message); + if (length < capacity) { + std::memcpy(buffer, message, length + 1); + return 0; + } + std::memcpy(buffer, message, capacity - 1); + buffer[capacity - 1] = '\0'; + if (capacity > 3) std::memcpy(buffer + capacity - 4, "...", 3); + return 34; +} diff --git a/core/libs/tests/GuestSafeErrors.cpp b/core/libs/tests/GuestSafeErrors.cpp new file mode 100644 index 0000000000..436ce48e09 --- /dev/null +++ b/core/libs/tests/GuestSafeErrors.cpp @@ -0,0 +1,126 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI strerror_s_nid_postfix(char*, std::size_t, int); +std::size_t APS5_VABI strerrorlen_s_nid_postfix(int); +int APS5_VABI strerror_r_nid_postfix(int, char*, std::size_t); +int* APS5_VABI __error_nid_postfix(); +void* APS5_VABI set_constraint_handler_s_nid_postfix(void*); +void APS5_VABI ignore_handler_s_nid_postfix(const char*, void*, int); +} + +namespace { + +void Require(bool condition, const char* message) { + if (!condition) { + std::fprintf(stderr, "safe error messages: %s\n", message); + std::abort(); + } +} + +thread_local int violations = 0; +thread_local void* violationBuffer = nullptr; + +void APS5_VABI Constraint(const char* message, void* buffer, int error) { + Require(message && std::strcmp(message, "strerror_s") == 0, "constraint callback message and ABI"); + Require(error == 22, "constraint callback error"); + violationBuffer = buffer; + ++violations; + auto previous = set_constraint_handler_s_nid_postfix(reinterpret_cast(Constraint)); + Require(previous == reinterpret_cast(Constraint), "handler is shared and callable without its lock"); + char nested[64]; + Require(strerror_s_nid_postfix(nested, sizeof(nested), 2) == 0, "handler can reenter valid conversion"); +} + +void CheckMessage(int error, const char* expected) { + const std::string full(expected); + *__error_nid_postfix() = 1234; + Require(strerrorlen_s_nid_postfix(error) == full.size(), "full message length excludes null"); + for (std::size_t capacity = 1; capacity <= full.size() + 2; ++capacity) { + std::array buffer; + buffer.fill('#'); + const auto result = strerror_s_nid_postfix(buffer.data() + 1, capacity, error); + std::string wanted = full; + if (capacity <= full.size()) { + wanted.resize(capacity - 1); + if (capacity > 3) wanted.replace(capacity - 4, 3, "..."); + } + Require(result == (capacity <= full.size() ? 34 : 0), "complete versus truncated return"); + Require(std::strcmp(buffer.data() + 1, wanted.c_str()) == 0, "truncated text, ellipsis and terminator"); + Require(buffer[0] == '#', "leading canary"); + for (std::size_t index = wanted.size() + 2; index < buffer.size(); ++index) + Require(buffer[index] == '#', "bytes after the terminator remain untouched"); + Require(*__error_nid_postfix() == 1234, "preserve guest errno"); + } +} + +void CheckMessages() { + CheckMessage(0, "No error: 0"); + CheckMessage(2, "No such file or directory"); + CheckMessage(11, "Resource deadlock avoided"); + CheckMessage(35, "Resource temporarily unavailable"); + CheckMessage(96, "Previous owner died"); + CheckMessage(-1, "Unknown error: -1"); + CheckMessage(97, "Unknown error: 97"); + CheckMessage(std::numeric_limits::min(), "Unknown error: -2147483648"); + CheckMessage(std::numeric_limits::max(), "Unknown error: 2147483647"); + for (int error = 0; error < 97; ++error) { + char expected[128]; + Require(strerror_r_nid_postfix(error, expected, sizeof(expected)) == 0, "existing guest error table"); + CheckMessage(error, expected); + } + Require(violations == 0, "truncation is not a constraint violation"); +} + +void CheckConstraints() { + std::array buffer; + buffer.fill('!'); + constexpr auto maximum = std::numeric_limits::max(); + for (const auto capacity : {std::size_t{0}, (maximum >> 1) + 1, maximum}) { + const int before = violations; + Require(strerror_s_nid_postfix(buffer.data(), capacity, 2) == 22, "reject invalid capacity"); + Require(violations == before + 1 && violationBuffer == buffer.data(), "one callback with destination"); + for (const char value : buffer) Require(value == '!', "constraint failure leaves destination untouched"); + } + for (const auto capacity : {std::size_t{0}, std::size_t{1}, maximum}) { + const int before = violations; + Require(strerror_s_nid_postfix(nullptr, capacity, 2) == 22, "reject null destination"); + Require(violations == before + 1 && violationBuffer == nullptr, "one callback for null destination"); + } + set_constraint_handler_s_nid_postfix(reinterpret_cast(ignore_handler_s_nid_postfix)); + Require(strerror_s_nid_postfix(nullptr, 1, 2) == 22, "installed ignore handler returns to caller"); + set_constraint_handler_s_nid_postfix(reinterpret_cast(Constraint)); +} + +void CheckThreads() { + const auto worker = [](int error, const char* message) { + for (int iteration = 0; iteration < 100; ++iteration) { + CheckMessage(error, message); + Require(strerror_s_nid_postfix(nullptr, 1, error) == 22, "thread constraint callback"); + } + Require(violations == 100, "callbacks execute in the calling thread"); + }; + std::thread first(worker, 35, "Resource temporarily unavailable"); + std::thread second(worker, -1, "Unknown error: -1"); + first.join(); + second.join(); +} + +} + +int main() { + auto previous = set_constraint_handler_s_nid_postfix(reinterpret_cast(Constraint)); + CheckMessages(); + CheckConstraints(); + CheckThreads(); + set_constraint_handler_s_nid_postfix(previous); + std::puts("safe error message checks passed"); +} diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 3921c33eee..fe15f001f2 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -26,6 +26,7 @@ - [memcpy_s and memmove_s](../../../core/libs/prx/libc/src/Strings.cpp) (libc) use the C11 K.3.7.1.1 and K.3.7.1.2 size constraints, with RSIZE_MAX taken as SIZE_MAX >> 1 as in memset_s. An oversized destination size returns ERANGE (34) without touching the destination; the console's RSIZE_MAX and exact error value are unverified. Constraint violations return an error without invoking the registered constraint handler, as before. - [snwprintf_s](../../../core/libs/prx/libc/src/FormattingWide.cpp) (libc) - follows C11 K.3.9.1.3 with `RSIZE_MAX` assumed to be `SIZE_MAX >> 1`. A runtime-constraint violation only returns a negative value: `set_constraint_handler_s` is not exported and the console's default handler is unknown. An invalid multibyte `%s` argument is copied byte by byte instead of being reported as an encoding error, as in `vswprintf` - [swprintf_s and vswprintf_s](../../../core/libs/prx/libc/src/FormattingWide.cpp) (libc) - follow C11 K.3.9.1.4 with `RSIZE_MAX` assumed to be `SIZE_MAX >> 1`: the complete output, including the terminating null, must fit in the buffer, otherwise it is a runtime-constraint violation that sets `buffer[0]` to zero and returns a negative value, and the buffer is not partially written. Argument and encoding checks match `snwprintf_s` +- [strerror_s and strerrorlen_s](../../../core/libs/prx/libc/src/WideConversion.cpp) (libc) follow [C11 K.3.7.4.2 and K.3.7.4.3](https://www.open-std.org/jtc1/sc22/wg14/www/docs/n1570.pdf), using the existing FreeBSD error messages and unknown-error formatting. Truncation writes an ellipsis when capacity exceeds 3; constraint failures leave the destination untouched and invoke the registered guest-ABI handler. The console's `RSIZE_MAX` (`SIZE_MAX >> 1` here), nonzero returns (`EINVAL` 22 for constraints, `ERANGE` 34 for truncation), handler arguments and default abort behavior are unverified. Messages remain in the C locale, and guest errno is preserved. - [vsnwprintf_s](../../../core/libs/prx/libc/src/FormattingWide.cpp) (libc) - `va_list` counterpart of `snwprintf_s` (C11 K.3.9.1.3): truncates to the buffer and returns the full length, with the same constraints and argument checks - [vsnprintf_s and vsscanf_s](../../../core/libs/prx/libc/src/Formatting.cpp) (libc) - `vsnprintf_s` forwards to `vsnprintf` semantics: the C11 K.3.7.1.5 runtime constraints (null buffer, `size == 0`, `%n`, null `%s`) are not checked, so `size == 0` returns the would-be length instead of a negative value, and no constraint handler is invoked. `vsscanf_s` returns the count of items converted before an undersized `%s`/`%c`/`%[` destination (0 when it is the first), not EOF. Neither is confirmed on a PS5 title - [strndup and strdup](../../../core/libs/prx/libc/src/AllocatingStrings.cpp) (libc) return null with `errno` set to `EINVAL` (22) when `source` is null, preventing a host segfault on invalid guest input. POSIX.1-2008 and FreeBSD 11.4 libc leave `strndup(NULL)` and `strdup(NULL)` undefined (faulting); exact console behavior on hardware is unverified. From a9377e34baff4ed10c8f1efeb6ecbabe5b01754b Mon Sep 17 00:00:00 2001 From: XairRrR Date: Thu, 15 Oct 2026 22:59:35 +0200 Subject: [PATCH 0706/1000] feat(pad): touch the left or right touchpad half from the input command file APS5_PAD_INPUT_FILE commands can set bit 0x200000 or 0x400000 to put a finger on the left or right half of the touchpad, as the TouchLeft and TouchRight keys do, so scripted runs can press touchpad combinations. tools/pad_input.py queues them as touch-left and touch-right. AI-assisted: yes --- .../libScePad/include/InputCommandFile.hpp | 11 ++++++-- core/libs/prx/libScePad/src/PadState.cpp | 4 +-- core/libs/tests/GuestPad.cpp | 26 ++++++++++++++++++- docs/user/USAGE.md | 3 ++- tools/pad_input.py | 5 ++-- tools/tests/test_pad_input.py | 4 +++ 6 files changed, 45 insertions(+), 8 deletions(-) diff --git a/core/libs/prx/libScePad/include/InputCommandFile.hpp b/core/libs/prx/libScePad/include/InputCommandFile.hpp index a4a1c4d405..5ee3b1fb73 100644 --- a/core/libs/prx/libScePad/include/InputCommandFile.hpp +++ b/core/libs/prx/libScePad/include/InputCommandFile.hpp @@ -16,6 +16,8 @@ public: std::uint32_t buttons = 0; bool hasSticks = false; std::array sticks{}; + bool touchLeft = false; + bool touchRight = false; bool operator==(const Input&) const = default; }; @@ -65,10 +67,12 @@ private: const auto parsed = std::from_chars(first, last, values[i]); if (first == last || parsed.ec != std::errc{} || parsed.ptr != last) return {}; } - if (values[0] == 0 || (values[1] & ~0x10fffeull) != 0 || values[2] == 0 || values[2] > 5000) return {}; + if (values[0] == 0 || (values[1] & ~(0x10fffeull | TouchLeftBit | TouchRightBit)) != 0 || values[2] == 0 || values[2] > 5000) return {}; Command command; command.sequence = values[0]; - command.input.buttons = static_cast(values[1]); + command.input.buttons = static_cast(values[1] & 0x10fffeull); + command.input.touchLeft = (values[1] & TouchLeftBit) != 0; + command.input.touchRight = (values[1] & TouchRightBit) != 0; command.durationMs = static_cast(values[2]); for (int i = 3; i < count; ++i) { if (values[i] > 255) return {}; @@ -78,6 +82,9 @@ private: return command; } + static constexpr std::uint64_t TouchLeftBit = 0x200000; + static constexpr std::uint64_t TouchRightBit = 0x400000; + std::filesystem::path path; std::uint64_t lastSequence = 0; std::uint64_t nextPoll = 0; diff --git a/core/libs/prx/libScePad/src/PadState.cpp b/core/libs/prx/libScePad/src/PadState.cpp index e8446a411c..21f63c8232 100644 --- a/core/libs/prx/libScePad/src/PadState.cpp +++ b/core/libs/prx/libScePad/src/PadState.cpp @@ -179,10 +179,10 @@ PadData BuildData(const PadInputState& state, std::uint64_t changeTime, std::uin } ++touchNum; } - if (touchNum == 0 && (state.touchLeft || state.touchRight)) { + if (touchNum == 0 && (state.touchLeft || state.touchRight || injected.touchLeft || injected.touchRight)) { data.buttons |= 0x100000; touchNum = 1; - data.touch_data_touch0_x = state.touchRight ? 1440 : 480; + data.touch_data_touch0_x = state.touchRight || injected.touchRight ? 1440 : 480; data.touch_data_touch0_y = 471; data.touch_data_touch0_id = 0; } diff --git a/core/libs/tests/GuestPad.cpp b/core/libs/tests/GuestPad.cpp index 2392d45159..84b0ad50ab 100644 --- a/core/libs/tests/GuestPad.cpp +++ b/core/libs/tests/GuestPad.cpp @@ -10,6 +10,7 @@ #include #include #include +#include extern "C" { int APS5_VABI scePadOpen_nid_postfix(int, int, int, const void*); @@ -96,8 +97,16 @@ static void CheckInputCommands() { write("7 8192 300 1 2 3 4"); const auto accepted = commands.Poll(2601000); Require(accepted.buttons == 0x2000 && accepted.hasSticks && accepted.sticks[3] == 4); + write("8 2097152 300"); + const auto left = commands.Poll(3001000); + Require(left.buttons == 0 && left.touchLeft && !left.touchRight && !left.hasSticks); + write("9 4210688 300"); + const auto right = commands.Poll(3401000); + Require(right.buttons == 0x4000 && !right.touchLeft && right.touchRight); + write("10 8388608 300"); + Require(commands.Poll(3801000) == none); std::filesystem::remove(path); - Require(commands.Buttons(3001000) == 0); + Require(commands.Buttons(4201000) == 0); Pad::InputCommandFile disabled(std::filesystem::path{}); Require(disabled.Buttons(0) == 0); } @@ -248,6 +257,21 @@ static void CheckInjectedState(const std::filesystem::path& path) { const auto centred = Pad::ReadState(); Require(centred.left_stick_x == 42 && centred.right_stick_y == 17); Require(centred.timestamp > moved.timestamp); + for (const auto& [command, x] : {std::pair{"3 2097152 300\n", 480}, std::pair{"4 4194304 300\n", 1440}}) { + std::ofstream(path) << command; + PadData touched{}; + const auto touchDeadline = std::chrono::steady_clock::now() + std::chrono::seconds(2); + do { + touched = Pad::ReadState(); + if (touched.touch_data_touch_num != 0) break; + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } while (std::chrono::steady_clock::now() < touchDeadline); + Require(touched.touch_data_touch_num == 1 && touched.touch_data_touch0_x == x && touched.touch_data_touch0_y == 471); + Require(touched.buttons == (0x2000 | 0x100000)); + std::this_thread::sleep_for(std::chrono::milliseconds(310)); + const auto lifted = Pad::ReadState(); + Require(lifted.touch_data_touch_num == 0 && lifted.buttons == 0x2000); + } PadPublishInput_nid_postfix(PadInputState{}); } diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index 18996014a7..cecd8d1d3c 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -144,9 +144,10 @@ python3 tools/pad_input.py /tmp/anyps5-pad down --hold-ms 500 python3 tools/pad_input.py /tmp/anyps5-pad release python3 tools/pad_input.py /tmp/anyps5-pad release --left-stick 128 0 --hold-ms 2000 python3 tools/pad_input.py /tmp/anyps5-pad cross --left-stick 255 128 --right-stick 0 128 +python3 tools/pad_input.py /tmp/anyps5-pad touch-right ``` -Run the helper in a second terminal. Commands are polled at most every 100 ms during controller reads, and their buttons add to physical controller input. A press lasts 300 ms by default, starting when the application reads it, and releases automatically; `--hold-ms` accepts 1–5000 ms. `--left-stick X Y` and `--right-stick X Y` hold the sticks for the same time: each axis is 0-255, 128 is the centre, X grows to the right and Y grows downward. While such a command is active both sticks come from it instead of the physical controller, and a stick that was not named stays centred. Wait for the application to observe a release before sending another press. The file holds only the latest command; rapid commands can replace one another. On POSIX, replacing an existing command file retains its permission mode; a privileged helper also retains its owner and group so the game user can still read it. Commands left over when the application first reads the controller are ignored. Without the environment variable, file input is disabled. +Run the helper in a second terminal. Commands are polled at most every 100 ms during controller reads, and their buttons add to physical controller input. A press lasts 300 ms by default, starting when the application reads it, and releases automatically; `--hold-ms` accepts 1–5000 ms. `--left-stick X Y` and `--right-stick X Y` hold the sticks for the same time: each axis is 0-255, 128 is the centre, X grows to the right and Y grows downward. While such a command is active both sticks come from it instead of the physical controller, and a stick that was not named stays centred. `touch-left` and `touch-right` press the touchpad with a finger on its left or right half, as the `TouchLeft` and `TouchRight` keys do; in the command file they are bits `0x200000` and `0x400000` of the button field. Wait for the application to observe a release before sending another press. The file holds only the latest command; rapid commands can replace one another. On POSIX, replacing an existing command file retains its permission mode; a privileged helper also retains its owner and group so the game user can still read it. Commands left over when the application first reads the controller are ignored. Without the environment variable, file input is disabled. ### System fonts diff --git a/tools/pad_input.py b/tools/pad_input.py index 38522df145..a83c7a1116 100644 --- a/tools/pad_input.py +++ b/tools/pad_input.py @@ -12,7 +12,8 @@ BUTTONS = { "up": 0x10, "right": 0x20, "down": 0x40, "left": 0x80, "l2": 0x100, "r2": 0x200, "l1": 0x400, "r1": 0x800, "triangle": 0x1000, "circle": 0x2000, "cross": 0x4000, - "square": 0x8000, "touchpad": 0x100000, "release": 0, + "square": 0x8000, "touchpad": 0x100000, + "touch-left": 0x200000, "touch-right": 0x400000, "release": 0, } @@ -32,7 +33,7 @@ def previous_sequence(path): values = [int(token) for token in tokens] if any(value > 2**64 - 1 for value in values): return 0 - if values[0] == 0 or values[1] & ~0x10fffe or not 1 <= values[2] <= 5000: + if values[0] == 0 or values[1] & ~0x70fffe or not 1 <= values[2] <= 5000: return 0 if any(axis > 255 for axis in values[3:]): return 0 diff --git a/tools/tests/test_pad_input.py b/tools/tests/test_pad_input.py index 38c9514571..01b48a9e68 100644 --- a/tools/tests/test_pad_input.py +++ b/tools/tests/test_pad_input.py @@ -77,6 +77,10 @@ class PadInputTests(unittest.TestCase): self.assertEqual(self.queue(2000, "release", "--right-stick", "0", "255"), [2000, 0, 300, 128, 128, 0, 255]) + def test_touchpad_halves_queue_their_bits_and_preserve_sequence(self): + self.assertEqual(self.queue(1000, "touch-left"), [1000, 0x200000, 300]) + self.assertEqual(self.queue(1000, "touch-right"), [1001, 0x400000, 300]) + def test_valid_stick_command_and_unsigned_tokens_preserve_sequence(self): self.path.write_text("+2000 +16384 +300 +0 +255 +128 +128\n", encoding="utf-8") self.assertEqual(self.queue(1000, "release"), [2001, 0, 300]) From 3f1dcad6ddcba68a941a8243b1f95e1c48c67b4a Mon Sep 17 00:00:00 2001 From: Romans Pokrovskis Date: Sat, 17 Oct 2026 14:35:57 +0300 Subject: [PATCH 0707/1000] test(agc): honor the GPU selector in pooled buffer checks --- .../libSceAgcDriver/tests/execution/PooledBufferRange.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PooledBufferRange.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PooledBufferRange.cpp index f30cddb737..0276764a57 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PooledBufferRange.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PooledBufferRange.cpp @@ -143,11 +143,13 @@ public: std::vector devices(count); Check(enumerate(instance, &count, devices.data()), "vkEnumeratePhysicalDevices"); const auto properties = function("vkGetPhysicalDeviceProperties"); + const auto* requested = std::getenv("ANYPS5_GPU"); + const bool named = requested != nullptr && *requested != '\0'; context.physical = devices.front(); for (const auto candidate : devices) { VkPhysicalDeviceProperties described{}; properties(candidate, &described); - if (described.deviceType == VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU) { + if (named ? std::strstr(described.deviceName, requested) != nullptr : described.deviceType == VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU) { context.physical = candidate; break; } @@ -156,7 +158,6 @@ public: properties(context.physical, &selected); context.limits = selected.limits; std::printf("device %s vendor=0x%x device=0x%x driver=0x%x\n", selected.deviceName, selected.vendorID, selected.deviceID, selected.driverVersion); - const auto* requested = std::getenv("ANYPS5_GPU"); Require(requested == nullptr || std::strstr(selected.deviceName, requested) != nullptr, "selected device does not match ANYPS5_GPU"); VkPhysicalDeviceFeatures supported{}; function("vkGetPhysicalDeviceFeatures")(context.physical, &supported); From 032ee506b20b034e971604943a187de0e2cd38d6 Mon Sep 17 00:00:00 2001 From: XairRrR Date: Sat, 17 Oct 2026 13:36:47 +0200 Subject: [PATCH 0708/1000] fix(agc): keep the mesh index descriptor on relocated draw stages relocateDrawEntry copied a cached variant field by field and left out meshIndexDescriptor, which the variant gained in #5791 before #4943 merged. lookupDraw compares a mesh stage's descriptor with the draw's index buffer words before validating anything else, and no draw has an all-zero one, so every relocated mesh-path stage was refused as a differing front stage. With no hits the shader set's relocation trial stopped admitting new aliases too. The copy now takes the donor's descriptor, so a relocated mesh stage matches the same index buffer the donor was built for and misses on any other. agc_driver_mesh_draw_relocation draws a mesh-path triangle whose vertex buffer descriptor is loaded through a user-data pointer, at eight addresses with the same contents: the last four now hit the cached draw (before: four differing front stages, no hit). AI-assisted: yes --- .../src/Driver/Draw/DrawRelocate.cpp | 1 + .../tests/execution/MeshDrawRelocation.cpp | 152 ++++++++++++++++++ 2 files changed, 153 insertions(+) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/MeshDrawRelocation.cpp diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRelocate.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRelocate.cpp index caff5d1bd1..da86f3e422 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRelocate.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRelocate.cpp @@ -217,6 +217,7 @@ void Driver::relocateDrawEntry(const QueueState& queue, std::uint64_t alias, std copy->words = variant->words; copy->forgetSerial = variant->forgetSerial; copy->compiled = variant->compiled; + copy->meshIndexDescriptor = variant->meshIndexDescriptor; copy->shader = variant->shader; copy->dataPositions = variant->dataPositions; copy->dataSlots = variant->dataSlots; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/MeshDrawRelocation.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/MeshDrawRelocation.cpp new file mode 100644 index 0000000000..1b64d9d3df --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/MeshDrawRelocation.cpp @@ -0,0 +1,152 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" +#include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" +#include "../MeshGeometryFixture.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using AgcDriver::DriverDetail::DrawMiss; + +constexpr std::uint32_t Width = 64; +constexpr std::uint32_t Height = 64; +alignas(256) std::array Pixels{}; +alignas(256) constexpr std::array Indices{0, 1, 2, 0}; +alignas(256) constexpr std::array, 3> Vertices{{ + {-0.9f, -0.8f, 0, 1, 1, 1, 1, 1}, {-0.1f, -0.8f, 0, 1, 1, 1, 1, 1}, {-0.5f, 0.8f, 0, 1, 1, 1, 1, 1} +}}; +alignas(256) constexpr std::array PixelCode{0x7e0e02f2u, 0xf800180fu, 0x07070707u, 0xbf810000u}; + +constexpr std::array LoadBufferDescriptor{0xf4080204u, 0xfa000000u, 0xbf8cc07fu}; +alignas(256) constexpr auto GeometryCode = [] { + std::array code{}; + std::copy(LoadBufferDescriptor.begin(), LoadBufferDescriptor.end(), code.begin()); + std::copy(AgcDriver::Test::MeshGeometryCode.begin(), AgcDriver::Test::MeshGeometryCode.end(), code.begin() + LoadBufferDescriptor.size()); + return code; +}(); + +alignas(16) std::array, 8> Descriptors{}; + +struct Header { + Shader shader{}; + ShaderUserData users{}; +}; + +Header GeometryHeader{}; +Header PixelHeader{}; + +void Register(Header& header, const std::uint32_t* code, std::size_t size, std::uint8_t type) { + header.shader.file_header = 0x34333231u; + header.shader.version = 0x18u; + header.shader.header_size = sizeof(header); + header.shader.shader_size = static_cast(size); + header.shader.code = code; + header.shader.type = type; + header.shader.user_data = &header.users; + AgcDriverRegisterShader_nid_postfix(&header.shader); +} + +void Write(std::vector& commands, std::uint32_t opcode, std::uint32_t offset, std::uint32_t value) { + commands.insert(commands.end(), {0xc0010000u | (opcode << 8u), offset, value}); +} + +std::vector Setup(const std::array& descriptor) { + const auto color = reinterpret_cast(Pixels.data()); + const auto geometry = reinterpret_cast(GeometryCode.data()); + const auto pixel = reinterpret_cast(PixelCode.data()); + const auto table = reinterpret_cast(descriptor.data()); + const std::vector> context{ + {0x2d5, 0x20}, {0x1ff, 12}, {0x2ce, 3}, {0x29b, 2}, {0x2ab, 4}, {0x260, 0}, + {0x1b6, 0}, {0x207, 0}, {0x200, 0}, {0x203, 0x800}, + {0x2dc, 0xaa00}, {0x2f8, 0}, {0x292, 2}, {0x293, 0}, + {0x80, 0}, {0x8d, 0}, {0x83, 0xffff}, {0x8c, 0xa}, + {0x2f9, 0x2d}, {0x313, 0x6000}, {0x30e, 0xffffffff}, {0x30f, 0xffffffff}, + {0x206, 0x43f}, {0x204, 0x80000}, {0x205, 0x240}, + {0x8e, 0xf}, {0x8f, 0xf}, {0x202, 0xcc0010}, + {0x1c4, 0}, {0x1c5, 9}, {0x1c3, 4}, {0x31c, 0x28028}, + {0x31b, 0}, {0x31d, 0}, {0x3b0, ((Height - 1u) << 14u) | (Width - 1u)}, + {0x3b8, 0x9000000}, {0x1e0, 0}, {0xc, 0}, {0xd, (Height << 16u) | Width}, + {0x81, 0x80000000}, {0x82, (Height << 16u) | Width}, + {0x90, 0x80000000}, {0x91, (Height << 16u) | Width}, + {0x94, 0x80000000}, {0x95, (Height << 16u) | Width}, + {0x318, static_cast(color >> 8u)}, {0x390, static_cast(color >> 40u)}, + {0x10f, std::bit_cast(32.0f)}, {0x110, std::bit_cast(32.0f)}, + {0x111, std::bit_cast(32.0f)}, {0x112, std::bit_cast(32.0f)}, + {0x113, std::bit_cast(1.0f)}, {0x114, 0}, + {0xb4, 0}, {0xb5, std::bit_cast(1.0f)} + }; + const std::vector> shaders{ + {0xc8, static_cast(geometry >> 8u)}, {0xc9, static_cast(geometry >> 40u)}, + {0x8a, 0x600c0000u}, {0x8b, (8u << 19u) | (3u << 16u) | (2u << 1u)}, + {0x8c, static_cast(table)}, {0x8d, static_cast(table >> 32u)}, + {0x8, static_cast(pixel >> 8u)}, {0x9, static_cast(pixel >> 40u)}, {0xb, 0} + }; + std::vector commands; + Write(commands, 0x79, 0x242, 4); + Write(commands, 0x79, 0x25b, (12u << 9u) | 4u); + for (const auto& [offset, value] : context) Write(commands, 0x69, offset, value); + for (const auto& [offset, value] : shaders) Write(commands, 0x76, offset, value); + commands.insert(commands.end(), {0xc0002a00u, 0u}); + return commands; +} + +AgcDriver::DriverDetail::DrawEntryCounters Draw(const std::array& descriptor) { + Pixels.fill(std::byte{0}); + auto commands = Setup(descriptor); + const auto address = reinterpret_cast(Indices.data()); + commands.insert(commands.end(), {0xc0042700u, 3u, static_cast(address), static_cast(address >> 32u), 3u, 0u}); + Packet packet{commands.data(), static_cast(commands.size()), 0, {}}; + Require(sceAgcDriverSubmitDcb(&packet) == 0, "mesh submission failed"); + AgcDriverWaitIdle_nid_postfix(); + Require(std::to_integer(Pixels[(24u * Width + 16u) * 4u]) > 200u, "the mesh draw did not cover its triangle"); + return AgcDriver::DriverDetail::Driver::Get().DrawCacheCounters(); +} + +void MovedDescriptorTableHitsTheCache() { + const auto vertices = reinterpret_cast(Vertices.data()); + for (auto& descriptor : Descriptors) descriptor = {static_cast(vertices), static_cast(vertices >> 32u) | (32u << 16u), static_cast(Vertices.size()), 0x01016facu}; + const auto front = static_cast(DrawMiss::FrontDiffering); + AgcDriver::DriverDetail::DrawEntryCounters counters{}; + for (std::size_t i = 0; i < 4; ++i) counters = Draw(Descriptors[i]); + Require(counters.lookups == 4 && counters.hits == 0, "the draws that teach the moved table made " + std::to_string(counters.hits) + " hits in " + std::to_string(counters.lookups) + " lookups, expected 0 in 4"); + const auto learned = counters; + for (std::size_t i = 4; i < Descriptors.size(); ++i) counters = Draw(Descriptors[i]); + Require(counters.misses[front] == learned.misses[front], "a relocated mesh stage was refused as a differing front stage (" + std::to_string(counters.misses[front] - learned.misses[front]) + " times)"); + Require(counters.hits - learned.hits == Descriptors.size() - 4, "mesh draws whose table moved again did not reuse the cached draw (" + std::to_string(counters.hits - learned.hits) + " hits in " + std::to_string(Descriptors.size() - 4) + " draws)"); +} + +} + +int main() { + try { + { + const auto device = OpenVulkanTestDevice(); + if (!device || !TargetHasCapability(device->Target(), spv::CapabilityMeshShadingEXT)) return VulkanTestSkipped; + if (device->DeviceName().starts_with("llvmpipe")) { + std::puts("skipped, the device has no mesh shader compiler that handles these programs (llvmpipe)"); + return VulkanTestSkipped; + } + } + Register(GeometryHeader, GeometryCode.data(), sizeof(GeometryCode), 2); + Register(PixelHeader, PixelCode.data(), sizeof(PixelCode), 1); + MovedDescriptorTableHitsTheCache(); + AgcDriverShutdown_nid_postfix(); + std::cout << "A mesh draw whose descriptor table moved reused the cached draw\n"; + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + AgcDriverShutdown_nid_postfix(); + return 1; + } +} From 4076fcebb9606e35646529a8ca681809a65d2339 Mon Sep 17 00:00:00 2001 From: Ondra97 <47064087+Ondra97@users.noreply.github.com> Date: Sat, 17 Oct 2026 11:34:56 +0200 Subject: [PATCH 0709/1000] fix(agc): retain submitted batches when profiling fails --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 20 +- .../Graphics/src/Resources.cpp | 2 +- .../libSceAgcDriver/tests/CommandBatch.cpp | 20 ++ .../tests/CommandBatchFailure.cpp | 206 ++++++++++++++++++ .../tests/CommandBatchFailure.hpp | 12 + .../tests/execution/PooledBufferRange.cpp | 2 + 6 files changed, 260 insertions(+), 2 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/CommandBatch.cpp create mode 100644 core/libs/prx/libSceAgcDriver/tests/CommandBatchFailure.cpp create mode 100644 core/libs/prx/libSceAgcDriver/tests/CommandBatchFailure.hpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 5688bcc59a..9a136c499e 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1218,6 +1218,23 @@ if(BUILD_TESTING) endif() endif() +add_test_executable(agc_command_batch_failure_tests EXCLUDE_FROM_ALL + tests/CommandBatch.cpp tests/CommandBatchFailure.cpp Graphics/src/Resources.cpp tests/AllocationFailure.cpp) +target_include_directories(agc_command_batch_failure_tests PRIVATE + ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/core/shader/recompiler ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR}) +target_link_libraries(agc_command_batch_failure_tests PRIVATE libSceAgcDriver libc) +set_target_properties(agc_command_batch_failure_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_command_batch_failure_tests) +if(BUILD_TESTING) + set_target_properties(agc_command_batch_failure_tests PROPERTIES EXCLUDE_FROM_ALL OFF) + add_test(NAME agc_command_batch_failure COMMAND agc_command_batch_failure_tests) + set_tests_properties(agc_command_batch_failure PROPERTIES TIMEOUT 30) + if(WIN32) + set_tests_properties(agc_command_batch_failure PROPERTIES + ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() +endif() + add_test_executable(agc_host_import_retire_tests EXCLUDE_FROM_ALL tests/HostImportRetireHold.cpp) target_include_directories(agc_host_import_retire_tests PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/core/shader/recompiler ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR}) target_link_libraries(agc_host_import_retire_tests PRIVATE libSceAgcDriver libc) @@ -1414,7 +1431,8 @@ foreach(agcExecutionTestName IN LISTS ANYPS5_AGC_EXECUTION_TESTS) endforeach() if(NOT ANYPS5_AGC_EXECUTION_TESTS OR "PooledBufferRange" IN_LIST ANYPS5_AGC_EXECUTION_TESTS) - target_sources(agc_driver_pooled_buffer_range_tests PRIVATE Graphics/src/BufferPool.cpp tests/AllocationFailure.cpp) + target_sources(agc_driver_pooled_buffer_range_tests PRIVATE + Graphics/src/BufferPool.cpp Graphics/src/Resources.cpp tests/AllocationFailure.cpp tests/CommandBatchFailure.cpp) endif() target_link_libraries(agc_driver_gpu_map_on_fiber_tests PRIVATE libSceFiber) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Resources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Resources.cpp index a5ab4d1d84..cb30b6074f 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Resources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Resources.cpp @@ -373,9 +373,9 @@ void CommandBatch::Submit() { submission.pCommandBuffers = &commands; timing.Mark("command_end"); Check(context.Function("vkQueueSubmit")(context.queue, 1, &submission, fence), "vkQueueSubmit graphics"); - timing.Mark("queue_submit"); pending = true; submitted = true; + timing.Mark("queue_submit"); } void CommandBatch::Wait() { diff --git a/core/libs/prx/libSceAgcDriver/tests/CommandBatch.cpp b/core/libs/prx/libSceAgcDriver/tests/CommandBatch.cpp new file mode 100644 index 0000000000..65b5f0e36a --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/CommandBatch.cpp @@ -0,0 +1,20 @@ +#include "CommandBatchFailure.hpp" +#include "AllocationFailure.hpp" +#include +#include +#include + +int main() { + try { + AgcDriver::Graphics::Context context{}; + context.device = reinterpret_cast(std::uintptr_t{1}); + context.queue = reinterpret_cast(std::uintptr_t{2}); + context.pool = reinterpret_cast(std::uintptr_t{3}); + AgcDriver::Tests::RunCommandBatchFailureTests(context, false); + return 0; + } catch (const std::exception& error) { + AgcDriver::Tests::SetAllocationFailureCountdown(-1); + std::fprintf(stderr, "%s\n", error.what()); + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/CommandBatchFailure.cpp b/core/libs/prx/libSceAgcDriver/tests/CommandBatchFailure.cpp new file mode 100644 index 0000000000..71385b5298 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/CommandBatchFailure.cpp @@ -0,0 +1,206 @@ +#include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp" +#include "AllocationFailure.hpp" +#include "CommandBatchFailure.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using namespace AgcDriver::Graphics; +using AgcDriver::Tests::SetAllocationFailureCountdown; + +enum class Fault { None, Timing, End, Submit }; + +struct Probe { + PFN_vkGetDeviceProcAddr real = nullptr; + Fault fault = Fault::None; + VkFence fence = VK_NULL_HANDLE; + bool pending = false; + unsigned submitted = 0; + unsigned queries = 0; + unsigned idleWaits = 0; + unsigned fenceWaits = 0; + unsigned premature = 0; + unsigned safetyWaits = 0; + unsigned nativePending = 0; + unsigned freed = 0; + unsigned destroyed = 0; +}; + +Probe probe; +VkDevice activeDevice = VK_NULL_HANDLE; +VkQueue activeQueue = VK_NULL_HANDLE; + +void GuardRelease(VkDevice device, VkQueue queue) { + if (!probe.pending) return; + ++probe.premature; + ++probe.safetyWaits; + if (probe.real != nullptr) { + const auto status = reinterpret_cast(probe.real(device, "vkGetFenceStatus"))(device, probe.fence); + if (status == VK_NOT_READY) ++probe.nativePending; + else if (status != VK_SUCCESS) std::terminate(); + const auto wait = reinterpret_cast(probe.real(device, "vkQueueWaitIdle")); + if (wait(queue) != VK_SUCCESS) std::terminate(); + } + probe.pending = false; +} + +VKAPI_ATTR VkResult VKAPI_CALL AllocateCommands(VkDevice, const VkCommandBufferAllocateInfo*, VkCommandBuffer* commands) { + *commands = reinterpret_cast(std::uintptr_t{4}); + return VK_SUCCESS; +} + +VKAPI_ATTR VkResult VKAPI_CALL CreateFence(VkDevice, const VkFenceCreateInfo*, const VkAllocationCallbacks*, VkFence* fence) { + *fence = reinterpret_cast(std::uintptr_t{5}); + return VK_SUCCESS; +} + +VKAPI_ATTR VkResult VKAPI_CALL BeginCommands(VkCommandBuffer, const VkCommandBufferBeginInfo*) { return VK_SUCCESS; } + +VKAPI_ATTR VkResult VKAPI_CALL EndCommands(VkCommandBuffer commands) { + if (probe.fault == Fault::End) return VK_ERROR_OUT_OF_HOST_MEMORY; + if (probe.real == nullptr) return VK_SUCCESS; + return reinterpret_cast(probe.real(activeDevice, "vkEndCommandBuffer"))(commands); +} + +VKAPI_ATTR VkResult VKAPI_CALL Submit(VkQueue queue, std::uint32_t count, const VkSubmitInfo* submissions, VkFence fence) { + if (probe.fault == Fault::Submit) return VK_ERROR_OUT_OF_HOST_MEMORY; + const auto result = probe.real != nullptr ? reinterpret_cast(probe.real(activeDevice, "vkQueueSubmit"))(queue, count, submissions, fence) : VK_SUCCESS; + if (result == VK_SUCCESS) { + ++probe.submitted; + probe.pending = true; + probe.fence = fence; + if (probe.fault == Fault::Timing) SetAllocationFailureCountdown(0); + } + return result; +} + +VKAPI_ATTR VkResult VKAPI_CALL FenceStatus(VkDevice device, VkFence fence) { + ++probe.queries; + const auto result = probe.real != nullptr ? reinterpret_cast(probe.real(device, "vkGetFenceStatus"))(device, fence) : VK_NOT_READY; + if (result == VK_SUCCESS) probe.pending = false; + return result; +} + +VKAPI_ATTR VkResult VKAPI_CALL QueueIdle(VkQueue queue) { + ++probe.idleWaits; + const auto result = probe.real != nullptr ? reinterpret_cast(probe.real(activeDevice, "vkQueueWaitIdle"))(queue) : VK_SUCCESS; + if (result == VK_SUCCESS) probe.pending = false; + return result; +} + +VKAPI_ATTR VkResult VKAPI_CALL WaitFences(VkDevice device, std::uint32_t count, const VkFence* fences, VkBool32 all, std::uint64_t timeout) { + ++probe.fenceWaits; + const auto result = probe.real != nullptr ? reinterpret_cast(probe.real(device, "vkWaitForFences"))(device, count, fences, all, timeout) : VK_SUCCESS; + if (result == VK_SUCCESS) probe.pending = false; + return result; +} + +VKAPI_ATTR void VKAPI_CALL FreeCommands(VkDevice device, VkCommandPool pool, std::uint32_t count, const VkCommandBuffer* commands) { + GuardRelease(device, activeQueue); + probe.freed += count; + if (probe.real != nullptr) reinterpret_cast(probe.real(device, "vkFreeCommandBuffers"))(device, pool, count, commands); +} + +VKAPI_ATTR void VKAPI_CALL DestroyFence(VkDevice device, VkFence fence, const VkAllocationCallbacks* allocator) { + GuardRelease(device, activeQueue); + ++probe.destroyed; + if (probe.real != nullptr) reinterpret_cast(probe.real(device, "vkDestroyFence"))(device, fence, allocator); +} + +VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL DeviceProc(VkDevice device, const char* name) { + if (std::strcmp(name, "vkEndCommandBuffer") == 0) return reinterpret_cast(EndCommands); + if (std::strcmp(name, "vkQueueSubmit") == 0) return reinterpret_cast(Submit); + if (std::strcmp(name, "vkGetFenceStatus") == 0) return reinterpret_cast(FenceStatus); + if (std::strcmp(name, "vkQueueWaitIdle") == 0) return reinterpret_cast(QueueIdle); + if (std::strcmp(name, "vkWaitForFences") == 0) return reinterpret_cast(WaitFences); + if (std::strcmp(name, "vkFreeCommandBuffers") == 0) return reinterpret_cast(FreeCommands); + if (std::strcmp(name, "vkDestroyFence") == 0) return reinterpret_cast(DestroyFence); + if (probe.real != nullptr) return probe.real(device, name); + if (std::strcmp(name, "vkAllocateCommandBuffers") == 0) return reinterpret_cast(AllocateCommands); + if (std::strcmp(name, "vkCreateFence") == 0) return reinterpret_cast(CreateFence); + if (std::strcmp(name, "vkBeginCommandBuffer") == 0) return reinterpret_cast(BeginCommands); + return nullptr; +} + +bool RunCase(const Context& original, Fault fault, bool wait, bool native) { + probe = {}; + probe.real = native ? original.deviceProc : nullptr; + probe.fault = fault; + activeDevice = original.device; + activeQueue = original.queue; + auto context = original; + context.deviceProc = DeviceProc; + std::unique_ptr output; + constexpr std::size_t bytes = 256; + constexpr std::uint32_t pattern = 0x5a3c71e9u; + if (native) { + output = std::make_unique(context, bytes, VK_BUFFER_USAGE_TRANSFER_DST_BIT); + std::memset(output->Bytes().data(), 0, bytes); + } + bool allocationFailed = false; + bool apiFailed = false; + bool waitFailed = false; + { + AgcDriver::FrameTiming frame(1, true); + AgcDriver::PerformanceContext timing(&frame); + try { + CommandBatch batch(context); + if (output) { + context.Function("vkCmdFillBuffer")(batch.Handle(), output->Handle(), 0, bytes, pattern); + RecordMemoryBarrier(context, batch.Handle(), VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_HOST_READ_BIT); + } + try { + batch.Submit(); + } catch (const std::bad_alloc&) { + if (!wait) throw; + allocationFailed = true; + } + SetAllocationFailureCountdown(-1); + if (wait && probe.submitted != 0) { + try { + batch.Wait(); + } catch (const std::exception&) { + waitFailed = true; + } + } + } catch (const std::bad_alloc&) { + allocationFailed = true; + } catch (const std::runtime_error& error) { + apiFailed = std::string(error.what()).find(fault == Fault::End ? "vkEndCommandBuffer" : "vkQueueSubmit graphics") != std::string::npos; + } + SetAllocationFailureCountdown(-1); + } + bool pixels = true; + if (output && probe.submitted != 0) { + output->Invalidate(); + const auto* words = reinterpret_cast(output->Bytes().data()); + pixels = std::all_of(words, words + bytes / sizeof(*words), [](auto word) { return word == pattern; }); + } + const bool rejected = fault == Fault::End || fault == Fault::Submit; + const bool complete = rejected ? probe.queries == 0 && probe.idleWaits == 0 && probe.fenceWaits == 0 : wait ? probe.fenceWaits == 1 : probe.queries == 1; + const bool passed = allocationFailed == (fault == Fault::Timing) && apiFailed == rejected && !waitFailed && !probe.pending && probe.submitted == (rejected ? 0u : 1u) && probe.premature == 0 && probe.freed == 1 && probe.destroyed == 1 && complete && pixels; + std::printf("command batch fault=%u wait=%u allocation failed=%u submitted=%u premature=%u safety waits=%u queries=%u idle waits=%u fence waits=%u readbacks=%u correct=%u wait failed=%u native pending=%u\n", static_cast(fault), wait, allocationFailed, probe.submitted, probe.premature, probe.safetyWaits, probe.queries, probe.idleWaits, probe.fenceWaits, output && probe.submitted != 0 ? 1u : 0u, pixels, waitFailed, probe.nativePending); + return passed; +} + +} + +void AgcDriver::Tests::RunCommandBatchFailureTests(const AgcDriver::Graphics::Context& context, bool native) { + unsigned failures = 0; + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + for (const auto fault : {Fault::Timing, Fault::None}) { + for (const auto wait : {false, true}) if (!RunCase(context, fault, wait, native)) ++failures; + } + for (const auto fault : {Fault::End, Fault::Submit}) if (!RunCase(context, fault, false, native)) ++failures; + Require(failures == 0, "command batch failure handling released pending work or lost its submission state"); +} diff --git a/core/libs/prx/libSceAgcDriver/tests/CommandBatchFailure.hpp b/core/libs/prx/libSceAgcDriver/tests/CommandBatchFailure.hpp new file mode 100644 index 0000000000..ed90a19297 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/CommandBatchFailure.hpp @@ -0,0 +1,12 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_TESTS_COMMANDBATCHFAILURE_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_TESTS_COMMANDBATCHFAILURE_HPP + +#include "prx/libSceAgcDriver/Graphics/include/Context.hpp" + +namespace AgcDriver::Tests { + +void RunCommandBatchFailureTests(const Graphics::Context& context, bool native); + +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PooledBufferRange.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PooledBufferRange.cpp index f30cddb737..567e438c97 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PooledBufferRange.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PooledBufferRange.cpp @@ -3,6 +3,7 @@ #include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" #include "VulkanTestDevice.hpp" #include "../AllocationFailure.hpp" +#include "../CommandBatchFailure.hpp" #include #include #include @@ -479,6 +480,7 @@ int main() { std::printf("skipped, no usable Vulkan device: %s\n", error.what()); return VulkanTestSkipped; } + AgcDriver::Tests::RunCommandBatchFailureTests(device->GetContext(), true); SlabMetadataFailures(device->GetContext()); RobustReadPastEnd(device->GetContext()); ExactSizes(device->GetContext()); From 85bc66e5c4904906e41de8dfaaa48ad4009fc9fc Mon Sep 17 00:00:00 2001 From: Ondra97 <47064087+Ondra97@users.noreply.github.com> Date: Sat, 17 Oct 2026 11:45:40 +0200 Subject: [PATCH 0710/1000] test(agc): separate command batch probes from slab return fixtures --- .../prx/libSceAgcDriver/tests/execution/PooledBufferRange.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PooledBufferRange.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PooledBufferRange.cpp index 567e438c97..4dfbb20a9f 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PooledBufferRange.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PooledBufferRange.cpp @@ -1,9 +1,9 @@ +#include "../CommandBatchFailure.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Graphics/include/BufferPool.hpp" #include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" #include "VulkanTestDevice.hpp" #include "../AllocationFailure.hpp" -#include "../CommandBatchFailure.hpp" #include #include #include @@ -480,10 +480,10 @@ int main() { std::printf("skipped, no usable Vulkan device: %s\n", error.what()); return VulkanTestSkipped; } - AgcDriver::Tests::RunCommandBatchFailureTests(device->GetContext(), true); SlabMetadataFailures(device->GetContext()); RobustReadPastEnd(device->GetContext()); ExactSizes(device->GetContext()); + AgcDriver::Tests::RunCommandBatchFailureTests(device->GetContext(), true); device.reset(); Require(AllocationErrors == 0 && LiveBytes == 0, "native buffer cleanup left an invalid memory state"); std::puts("pooled buffer range tests passed"); From 4796eac2dfcb40628dc08d6c4b8d973df4346f3c Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Sat, 17 Oct 2026 11:42:18 +0000 Subject: [PATCH 0711/1000] test(agc): check that a read through the BDA table sees a target as with its pass broken Continuing a pass across a draw that only reads through the table is right only if no pass end records a target write-back that the read depends on. agc_driver_address_passes --targets renders a color whose bytes spell 1.0f into the target, then reads the target's first dword through the table into the same target, once with a tiled target and once with a linear one. The tiled read continues the rendering draw's pass and reads the bytes from before it: tiled results reach guest memory only by explicit write-backs, none of which falls between the two draws. The linear read reads the rendered color: a linear target over a host import is stored back right after its draw (the store ends the pass), and one without an import is drawn synchronously. agc_driver_address_passes_targets_split runs the same cases with APS5_NO_ADDRESS_READ_PASSES=1 and must read the same bytes; a scratch build that writes the targets back whenever a draw reading through the table breaks the pass fails it. --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 8 ++++++ .../tests/execution/AddressPasses.cpp | 26 +++++++++++++++++-- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index ab672d4463..762f1c3ad5 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1406,6 +1406,14 @@ foreach(agcExecutionTestSource IN LISTS agcExecutionTestSources) configure_windows_unwind(${agcExecutionTest}) list(APPEND agcExecutionTests ${agcExecutionTest}) endforeach() +if(BUILD_TESTING) + add_test(NAME agc_driver_address_passes_targets_split COMMAND agc_driver_address_passes_tests --targets) + set_tests_properties(agc_driver_address_passes_targets_split PROPERTIES ENVIRONMENT "APS5_NO_ADDRESS_READ_PASSES=1" SKIP_RETURN_CODE 77 TIMEOUT 60) + if(WIN32) + set_tests_properties(agc_driver_address_passes_targets_split PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() foreach(agcExecutionTestName IN LISTS ANYPS5_AGC_EXECUTION_TESTS) if(NOT EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/tests/execution/${agcExecutionTestName}.cpp) message(FATAL_ERROR "ANYPS5_AGC_EXECUTION_TESTS names ${agcExecutionTestName}, which has no source in tests/execution") diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/AddressPasses.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/AddressPasses.cpp index 24f031e888..9278d67787 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/AddressPasses.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/AddressPasses.cpp @@ -62,6 +62,7 @@ alignas(256) constexpr std::array VertexCodeUser8 = [] { }(); alignas(256) constexpr std::array PixelRead{0x7e1a0280u, 0xdc308000u, 0x0c02000du, 0xbf8c3f70u, 0x7e10030cu, 0x7e120280u, 0x7e140280u, 0x7e160280u, 0xf800180fu, 0x0b0a0908u, 0xbf810000u}; +alignas(256) constexpr std::array PixelColorWord{0x7e100280u, 0x7e120280u, 0x7e1402ffu, 0x3f008081u, 0x7e1602ffu, 0x3e7cfcfdu, 0xf800180fu, 0x0b0a0908u, 0xbf810000u}; alignas(256) constexpr std::array PixelDescriptorStore{0x7e0202f2u, 0x7e040280u, 0xe0701000u, 0x80010102u, 0x7e1002f2u, 0x7e1202f2u, 0x7e1402f2u, 0x7e1602f2u, 0xf800180fu, 0x0b0a0908u, 0xbf810000u}; alignas(256) constexpr std::array PixelStore{0x7e1a0280u, 0x7e1802f2u, 0xdc708000u, 0x00020c0du, 0x7e1002f2u, 0x7e1202f2u, 0x7e1402f2u, 0x7e1602f2u, 0xf800180fu, 0x0b0a0908u, 0xbf810000u}; @@ -74,6 +75,7 @@ alignas(8) Header VertexHeader{}; alignas(8) Header ReadHeader{}; alignas(8) Header StoreHeader{}; alignas(8) Header DescriptorStoreHeader{}; +alignas(8) Header ColorWordHeader{}; void Register(Header& header, const std::uint32_t* code, std::size_t bytes, std::uint8_t type) { header.shader.file_header = 0x34333231u; @@ -141,7 +143,7 @@ struct Step { std::array descriptor{}; }; -void Submit(const GuestBlock& vertices, const GuestBlock& target, std::initializer_list steps) { +void Submit(const GuestBlock& vertices, const GuestBlock& target, std::initializer_list steps, bool linear = false) { const auto color = target.At(ColorOffset); const auto vertex = reinterpret_cast(VertexCodeUser8.data()); const auto table = vertices.At(VertexTableOffset); @@ -156,7 +158,7 @@ void Submit(const GuestBlock& vertices, const GuestBlock& target, std::initializ {0x8e, 0xf}, {0x8f, 0xf}, {0x202, 0xcc0010}, {0x1c4, 0}, {0x1c5, 9}, {0x1c3, 4}, {0x31c, 0x28028}, {0x31b, 0}, {0x31d, 0}, {0x3b0, (63u << 14u) | 3u}, - {0x3b8, 0x9000000u | (RenderTargetTileMode << 14u)}, {0x1e0, 0}, + {0x3b8, linear ? 0x9000000u : 0x9000000u | (RenderTargetTileMode << 14u)}, {0x1e0, 0}, {0xc, 0}, {0xd, 0x40040}, {0x81, 0x80000000}, {0x82, 0x40040}, {0x90, 0x80000000}, {0x91, 0x40040}, @@ -268,6 +270,21 @@ void DescriptorStoreAfterTableReadBreaks(const GuestBlock& vertices, const Guest Expect(otherAfter.passesBegun - otherBefore.passesBegun == 1, "a draw storing through a buffer descriptor continued the pass of a draw that read through the table: " + Counts(otherBefore, otherAfter, "other draws")); } +void TargetReadAsWhenSplit(const GuestBlock& vertices, const GuestBlock& target, bool linear) { + const bool split = std::getenv("APS5_NO_ADDRESS_READ_PASSES") != nullptr; + Submit(vertices, target, {{PixelRead.data(), target.At(ColorOffset)}}, linear); + std::memset(target.Bytes(ColorOffset), 0, ColorBytes); + const auto before = AddressDraws(); + Submit(vertices, target, {{PixelColorWord.data(), 0}, {PixelRead.data(), target.At(ColorOffset)}}, linear); + const auto after = AddressDraws(); + AgcDriver::GuestMemory::FlushGpuWrites(static_cast(target.At(ColorOffset)), ColorBytes); + const unsigned red = target.Bytes(ColorOffset)[0]; + const auto what = std::string(linear ? "linear" : "tiled") + " target" + (split ? " with APS5_NO_ADDRESS_READ_PASSES=1" : ""); + std::printf("%s: a draw reading through the table the target the previous draw rendered read %s (red byte %u), %s\n", what.c_str(), red == 255u ? "that draw's color" : "the bytes before it", red, Counts(before, after).c_str()); + Expect(red == (linear ? 255u : 0u), what + ": a draw reading through the table the target the previous draw rendered read " + (red == 255u ? "that draw's color" : "the bytes before it") + "; whether or not the pass breaks between them, it reads " + (linear ? "that draw's color (stored back after the draw)" : "the bytes before it (no write-back between them)")); + if (!linear && !split) Expect(after.passesContinued - before.passesContinued == 1, what + ": the reading draw did not continue the rendering draw's pass, so the case does not test it: " + Counts(before, after)); +} + } int main(int argc, char** argv) { @@ -285,6 +302,7 @@ int main(int argc, char** argv) { Register(ReadHeader, PixelRead.data(), sizeof(PixelRead), 1); Register(StoreHeader, PixelStore.data(), sizeof(PixelStore), 1); Register(DescriptorStoreHeader, PixelDescriptorStore.data(), sizeof(PixelDescriptorStore), 1); + Register(ColorWordHeader, PixelColorWord.data(), sizeof(PixelColorWord), 1); GuestBlock readOnlyVertices; GuestBlock data; GuestBlock target; @@ -298,6 +316,10 @@ int main(int argc, char** argv) { if (only.empty() || only == "--read-sees-store") ReadSeesStore(readOnlyVertices, data, target); if (only.empty() || only == "--read-sees-descriptor-store") ReadSeesDescriptorStore(readOnlyVertices, data, target); if (only.empty() || only == "--descriptor-store-after-read") DescriptorStoreAfterTableReadBreaks(readOnlyVertices, data, target); + if (only.empty() || only == "--targets") { + TargetReadAsWhenSplit(readOnlyVertices, target, false); + TargetReadAsWhenSplit(readOnlyVertices, target, true); + } AgcDriverShutdown_nid_postfix(); std::puts("address passes: draws reading through the BDA table share a render pass, and a store through it still begins one"); return 0; From 4bc8e2c68328a912fb8ebc50deaac3854a90ec65 Mon Sep 17 00:00:00 2001 From: pushkarreddyy <212133856+pushkarreddyy@users.noreply.github.com> Date: Sat, 17 Oct 2026 17:14:24 +0530 Subject: [PATCH 0712/1000] feat(libSceVideoOut): implement color space conversion and blend space exports --- core/libs/SceTypes.hpp | 5 + core/libs/prx/libSceVideoOut/CMakeLists.txt | 11 +++ .../libs/prx/libSceVideoOut/Unimplemented.cpp | 15 +++ .../prx/libSceVideoOut/include/Output.hpp | 7 ++ core/libs/prx/libSceVideoOut/src/Output.cpp | 42 +++++++++ core/libs/tests/GuestVideoOutColorSpace.cpp | 93 +++++++++++++++++++ docs/dev/debt/libSceVideoOut.md | 1 + 7 files changed, 174 insertions(+) create mode 100644 core/libs/tests/GuestVideoOutColorSpace.cpp diff --git a/core/libs/SceTypes.hpp b/core/libs/SceTypes.hpp index 3ebfe78a12..cb24e34230 100644 --- a/core/libs/SceTypes.hpp +++ b/core/libs/SceTypes.hpp @@ -2032,6 +2032,11 @@ struct VideoOutColorSettings { std::uint32_t reserved[3] = {}; }; +struct VideoOutColorSpaceConversionSettings { + std::uint32_t internalData[8] = {}; +}; +static_assert(sizeof(VideoOutColorSpaceConversionSettings) == 32); + using atexit_func_t = void (*)(); diff --git a/core/libs/prx/libSceVideoOut/CMakeLists.txt b/core/libs/prx/libSceVideoOut/CMakeLists.txt index 49cce22aee..e2aaff9966 100644 --- a/core/libs/prx/libSceVideoOut/CMakeLists.txt +++ b/core/libs/prx/libSceVideoOut/CMakeLists.txt @@ -184,6 +184,17 @@ if(BUILD_TESTING) set_tests_properties(guest_video_out_output_mode PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + + add_test_executable(guest_video_out_color_space_tests ${LIBS_INCLUDE_DIR}/tests/GuestVideoOutColorSpace.cpp) + target_include_directories(guest_video_out_color_space_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_video_out_color_space_tests PRIVATE libSceVideoOut libSceAgcDriver libkernel libc) + configure_windows_unwind(guest_video_out_color_space_tests) + add_test(NAME guest_video_out_color_space COMMAND guest_video_out_color_space_tests) + set_tests_properties(guest_video_out_color_space PROPERTIES TIMEOUT 20 SKIP_RETURN_CODE 77 RESOURCE_LOCK videoout_app0) + if(WIN32) + set_tests_properties(guest_video_out_color_space PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() endif() if(BUILD_TESTING) diff --git a/core/libs/prx/libSceVideoOut/Unimplemented.cpp b/core/libs/prx/libSceVideoOut/Unimplemented.cpp index ef58c55add..7db06b36c4 100644 --- a/core/libs/prx/libSceVideoOut/Unimplemented.cpp +++ b/core/libs/prx/libSceVideoOut/Unimplemented.cpp @@ -27,4 +27,19 @@ int APS5_VABI sceVideoOutColorSettingsSetGamma_(VideoOutColorSettings* settings, return sceVideoOutColorSettingsSetGamma(settings, gamma); } +int APS5_VABI sceVideoOutColorSpaceConversionSettingsSetDefaultSdrToHdrConversion_(VideoOutColorSpaceConversionSettings* settings, uint32_t settings_size) { + if (settings_size < sizeof(VideoOutColorSpaceConversionSettings)) throw std::runtime_error("sceVideoOutColorSpaceConversionSettingsSetDefaultSdrToHdrConversion_: VIDEO_OUT_ERROR_INVALID_VALUE"); + return sceVideoOutColorSpaceConversionSettingsSetDefaultSdrToHdrConversion(settings); +} + +int APS5_VABI sceVideoOutColorSpaceConversionSettingsSetSdrToHdrConversion_(VideoOutColorSpaceConversionSettings* settings, float lumaScale, uint32_t option, uint32_t settings_size) { + if (settings_size < sizeof(VideoOutColorSpaceConversionSettings)) throw std::runtime_error("sceVideoOutColorSpaceConversionSettingsSetSdrToHdrConversion_: VIDEO_OUT_ERROR_INVALID_VALUE"); + return sceVideoOutColorSpaceConversionSettingsSetSdrToHdrConversion(settings, lumaScale, option); +} + +int APS5_VABI sceVideoOutAdjustColorSpaceConversion_(int handle, const VideoOutColorSpaceConversionSettings* settings, uint32_t settings_size) { + if (settings_size < sizeof(VideoOutColorSpaceConversionSettings)) throw std::runtime_error("sceVideoOutAdjustColorSpaceConversion_: VIDEO_OUT_ERROR_INVALID_VALUE"); + return sceVideoOutAdjustColorSpaceConversion(handle, settings); +} + } diff --git a/core/libs/prx/libSceVideoOut/include/Output.hpp b/core/libs/prx/libSceVideoOut/include/Output.hpp index 4ecf913417..569e97ee77 100644 --- a/core/libs/prx/libSceVideoOut/include/Output.hpp +++ b/core/libs/prx/libSceVideoOut/include/Output.hpp @@ -28,6 +28,13 @@ int APS5_VABI sceVideoOutLatencyControlWaitBeforeInput(int handle); int APS5_VABI sceVideoOutLatencyMeasureSetStartPoint(int handle, uint32_t point); int APS5_VABI sceVideoOutColorSettingsSetGamma(VideoOutColorSettings* settings, float gamma); int APS5_VABI sceVideoOutAdjustColor(int handle, const VideoOutColorSettings* settings); +int APS5_VABI sceVideoOutColorSpaceConversionSettingsSetDefaultSdrToHdrConversion(VideoOutColorSpaceConversionSettings* settings); +int APS5_VABI sceVideoOutColorSpaceConversionSettingsSetSdrToHdrConversion(VideoOutColorSpaceConversionSettings* settings, float lumaScale, uint32_t option); +int APS5_VABI sceVideoOutAdjustColorSpaceConversion(int handle, const VideoOutColorSpaceConversionSettings* settings); +int APS5_VABI sceVideoOutColorSpaceConversionSettingsSetDefaultSdrToHdrConversion_(VideoOutColorSpaceConversionSettings* settings, uint32_t settings_size); +int APS5_VABI sceVideoOutColorSpaceConversionSettingsSetSdrToHdrConversion_(VideoOutColorSpaceConversionSettings* settings, float lumaScale, uint32_t option, uint32_t settings_size); +int APS5_VABI sceVideoOutAdjustColorSpaceConversion_(int handle, const VideoOutColorSpaceConversionSettings* settings, uint32_t settings_size); +int APS5_VABI sceVideoOutSetGlobalBlendSpace(int handle, uint32_t space, uint32_t option); int APS5_VABI sceVideoOutVrrPegToFixedRate(int handle, uint64_t arg1, uint64_t arg2); int APS5_VABI sceVideoOutVrrUnpegFromFixedRate(int handle); diff --git a/core/libs/prx/libSceVideoOut/src/Output.cpp b/core/libs/prx/libSceVideoOut/src/Output.cpp index aa9edaa6ff..79af66728a 100644 --- a/core/libs/prx/libSceVideoOut/src/Output.cpp +++ b/core/libs/prx/libSceVideoOut/src/Output.cpp @@ -339,6 +339,48 @@ int APS5_VABI sceVideoOutAdjustColor(int handle, const VideoOutColorSettings* se LibcAwaitExit_nid_postfix(); } +int APS5_VABI sceVideoOutColorSpaceConversionSettingsSetDefaultSdrToHdrConversion(VideoOutColorSpaceConversionSettings* settings) { + if (settings == nullptr) { + throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_ADDRESS"); + } + std::memset(settings, 0, sizeof(VideoOutColorSpaceConversionSettings)); + return 0; +} + +int APS5_VABI sceVideoOutColorSpaceConversionSettingsSetSdrToHdrConversion(VideoOutColorSpaceConversionSettings* settings, float lumaScale, uint32_t option) { + if (settings == nullptr) { + throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_ADDRESS"); + } + (void)lumaScale; + (void)option; + return 0; +} + +int APS5_VABI sceVideoOutAdjustColorSpaceConversion(int handle, const VideoOutColorSpaceConversionSettings* settings) try { + if (settings == nullptr) { + throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_ADDRESS"); + } + auto cfg = VideoOutDriver::Get().GetConfig(handle); + if (cfg == nullptr) { + throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_HANDLE"); + } + return 0; +} catch (const ProcessShutdown&) { + LibcAwaitExit_nid_postfix(); +} + +int APS5_VABI sceVideoOutSetGlobalBlendSpace(int handle, uint32_t space, uint32_t option) try { + auto cfg = VideoOutDriver::Get().GetConfig(handle); + if (cfg == nullptr) { + throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_HANDLE"); + } + (void)space; + (void)option; + return 0; +} catch (const ProcessShutdown&) { + LibcAwaitExit_nid_postfix(); +} + int APS5_VABI sceVideoOutVrrUnpegFromFixedRate(int handle) try { if (VideoOutDriver::Get().GetConfig(handle) == nullptr) { throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_HANDLE"); diff --git a/core/libs/tests/GuestVideoOutColorSpace.cpp b/core/libs/tests/GuestVideoOutColorSpace.cpp new file mode 100644 index 0000000000..1c081f65f9 --- /dev/null +++ b/core/libs/tests/GuestVideoOutColorSpace.cpp @@ -0,0 +1,93 @@ +#include "SceTypes.hpp" +#include "prx/libc/include/Shutdown.hpp" +#include "tests/VideoOutTestEnvironment.hpp" +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceVideoOutOpen(int userId, int busType, int index, const void* param); +int APS5_VABI sceVideoOutClose(int handle); +int APS5_VABI sceVideoOutColorSpaceConversionSettingsSetDefaultSdrToHdrConversion(VideoOutColorSpaceConversionSettings* settings); +int APS5_VABI sceVideoOutColorSpaceConversionSettingsSetSdrToHdrConversion(VideoOutColorSpaceConversionSettings* settings, float lumaScale, uint32_t option); +int APS5_VABI sceVideoOutAdjustColorSpaceConversion(int handle, const VideoOutColorSpaceConversionSettings* settings); +int APS5_VABI sceVideoOutColorSpaceConversionSettingsSetDefaultSdrToHdrConversion_(VideoOutColorSpaceConversionSettings* settings, uint32_t settings_size); +int APS5_VABI sceVideoOutColorSpaceConversionSettingsSetSdrToHdrConversion_(VideoOutColorSpaceConversionSettings* settings, float lumaScale, uint32_t option, uint32_t settings_size); +int APS5_VABI sceVideoOutAdjustColorSpaceConversion_(int handle, const VideoOutColorSpaceConversionSettings* settings, uint32_t settings_size); +int APS5_VABI sceVideoOutSetGlobalBlendSpace(int handle, uint32_t space, uint32_t option); +} + +static constexpr int SYSTEM_USER = 255; +static constexpr int MAIN_BUS = 0; + +static void Require(bool value) { + if (!value) std::abort(); +} + +static void ExpectFailure(auto&& action) { + bool failed = false; + try { + action(); + } catch (const std::runtime_error&) { + failed = true; + } + Require(failed); +} + +int main() { + VideoOutTestEnvironment environment; + VideoOutColorSpaceConversionSettings settings{}; + std::memset(&settings, 0x5A, sizeof(settings)); + + ExpectFailure([] { sceVideoOutColorSpaceConversionSettingsSetDefaultSdrToHdrConversion(nullptr); }); + Require(sceVideoOutColorSpaceConversionSettingsSetDefaultSdrToHdrConversion(&settings) == 0); + for (std::size_t index = 0; index < 8; ++index) { + Require(settings.internalData[index] == 0); + } + + ExpectFailure([] { sceVideoOutColorSpaceConversionSettingsSetSdrToHdrConversion(nullptr, 1.0f, 0); }); + Require(sceVideoOutColorSpaceConversionSettingsSetSdrToHdrConversion(&settings, 1.0f, 0) == 0); + + ExpectFailure([&] { sceVideoOutAdjustColorSpaceConversion(0, nullptr); }); + ExpectFailure([&] { sceVideoOutAdjustColorSpaceConversion(-1, &settings); }); + ExpectFailure([&] { sceVideoOutAdjustColorSpaceConversion(0, &settings); }); + ExpectFailure([&] { sceVideoOutAdjustColorSpaceConversion(1, &settings); }); + + ExpectFailure([&] { sceVideoOutColorSpaceConversionSettingsSetDefaultSdrToHdrConversion_(&settings, sizeof(settings) - 1); }); + Require(sceVideoOutColorSpaceConversionSettingsSetDefaultSdrToHdrConversion_(&settings, sizeof(settings)) == 0); + + ExpectFailure([&] { sceVideoOutColorSpaceConversionSettingsSetSdrToHdrConversion_(&settings, 1.0f, 0, sizeof(settings) - 1); }); + Require(sceVideoOutColorSpaceConversionSettingsSetSdrToHdrConversion_(&settings, 1.0f, 0, sizeof(settings)) == 0); + + ExpectFailure([&] { sceVideoOutAdjustColorSpaceConversion_(1, &settings, sizeof(settings) - 1); }); + ExpectFailure([&] { sceVideoOutAdjustColorSpaceConversion_(1, &settings, sizeof(settings)); }); + + ExpectFailure([] { sceVideoOutSetGlobalBlendSpace(-1, 0, 0); }); + ExpectFailure([] { sceVideoOutSetGlobalBlendSpace(0, 0, 0); }); + ExpectFailure([] { sceVideoOutSetGlobalBlendSpace(1, 0, 0); }); + + int handle = 0; + try { + handle = sceVideoOutOpen(SYSTEM_USER, MAIN_BUS, 0, nullptr); + } catch (const std::runtime_error& error) { + if (std::getenv("ANYPS5_REQUIRE_DISPLAY") != nullptr) throw; + std::printf("skipped, no display or Vulkan device: %s\n", error.what()); + return 77; + } + Require(handle > 0); + + Require(sceVideoOutAdjustColorSpaceConversion(handle, &settings) == 0); + Require(sceVideoOutAdjustColorSpaceConversion_(handle, &settings, sizeof(settings)) == 0); + Require(sceVideoOutSetGlobalBlendSpace(handle, 0, 0) == 0); + + Require(sceVideoOutClose(handle) == 0); + ExpectFailure([&] { sceVideoOutAdjustColorSpaceConversion(handle, &settings); }); + ExpectFailure([&] { sceVideoOutAdjustColorSpaceConversion_(handle, &settings, sizeof(settings)); }); + ExpectFailure([&] { sceVideoOutSetGlobalBlendSpace(handle, 0, 0); }); + + LibcRunShutdown_nid_postfix(); + return 0; +} diff --git a/docs/dev/debt/libSceVideoOut.md b/docs/dev/debt/libSceVideoOut.md index d76e687766..8a5caa007a 100644 --- a/docs/dev/debt/libSceVideoOut.md +++ b/docs/dev/debt/libSceVideoOut.md @@ -9,6 +9,7 @@ ### Unknown function info +- [sceVideoOutColorSpaceConversionSettingsSetDefaultSdrToHdrConversion_, sceVideoOutColorSpaceConversionSettingsSetSdrToHdrConversion_, sceVideoOutAdjustColorSpaceConversion_, sceVideoOutSetGlobalBlendSpace](../../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - PPSA30630 imports these to configure color space conversion and global blend space. Color space conversion settings are accepted with size validation (32 bytes) but conversion blocks and blend space parameters are not applied to the presentation swapchain - [sceVideoOutDeleteVrrActiveStatusEvent](../../../core/libs/prx/libSceVideoOut/src/Event.cpp) (libSceVideoOut, NID `-q-vbO859Tw`) - the name hashes to the NID PPSA02530 imports and is listed in [sce_symbols](https://github.com/zecoxao/sce_symbols) `aerolib.csv` at `fe2cc2948e`. The signature `(eq, handle)` and the behaviour, removing the handle's VRR status registration from the queue as the other `Delete*Event` calls do, follow sceVideoOutAddVrrActiveStatusEvent; no title has been observed calling it - [sceVideoOutGetOutputStatusWithVrr](../../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut, NID `gWT7X8H0bYs`) - `(int handle, void* status)`, the 80-byte layout and the return codes follow the clean-room description in #2746. Bytes 0-47 are `sceVideoOutGetOutputStatus`'s; bytes 48-53, which the console copies from an internal query whose fields are not established, are written as zero, as are the rest, so the 16-bit value at byte 52 never adjusts `refreshRate`. A null `status` returns `0x80290002` and an unknown handle `0x8029000B`. The console's earlier global-flag check (`0x8029001C`) and its object-field check (`0x8029001B`) test states that are not modelled and are never reported. - [sceVideoOutRegisterBuffers2](../../../core/libs/prx/libSceVideoOut/src/Buffer.cpp) (libSceVideoOut) - buffer option 8 (STRICT_COLORIMETRY) is presented as option 0, with the stored values unchanged; what the console changes for it is unknown. Only options 0 and 8 are accepted From 350b8d2b882bde6caf5f51ddbfdc421b5c370795 Mon Sep 17 00:00:00 2001 From: AlxcBTW <187763517+AlxcBTW@users.noreply.github.com> Date: Sat, 17 Oct 2026 13:41:39 +0200 Subject: [PATCH 0713/1000] fix(agc): keep the template of another address when a dispatch move is refused Read-only addresses are not part of the dispatch key, so the fresh build after a refused move replaced the template that serves the other address, and the two addresses rebuilt each other. A refused dispatch now uses or builds an entry under the exact key (read-only bases included) and leaves the movable template alone. The [dispmove] line counts the rebuilds after a refusal, the builds inserted beside the template and the dispatches served by an exact entry. Restores the MarkGpuWrites comment. --- .../Execution/include/VulkanDevice.hpp | 2 +- .../Execution/src/VulkanDevice.cpp | 40 ++++++++++++++++--- .../Graphics/include/ShaderResources.hpp | 2 + .../tests/execution/DispatchMove.cpp | 28 +++++++++++-- 4 files changed, 62 insertions(+), 10 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp index ff47f6adbc..65e8704f2f 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp @@ -186,7 +186,7 @@ public: }; static ComputePipelineCreation ComputePipelineCounts(); struct DispatchMoveCount { - std::uint64_t checked, exact, moved, refused, filtered; + std::uint64_t checked, exact, moved, refused, filtered, rebuilt, kept, served; }; static DispatchMoveCount DispatchMoveCounts(); void ResolveMemory(std::uint64_t address, std::size_t bytes, bool writable); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 895a8a6cdc..a0088c4054 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -3732,8 +3732,8 @@ std::size_t DispatchMoveBytes() { // The content key of a compute stage, naming the device: the cache is process-wide and the driver // replaces the headless device with the windowed one while workers may still use the old one, so // the key names the device that built the entry (its descriptor set and pooled buffers belong to it). -ResourceCache::Key DispatchContentKey(const Graphics::CompiledShader& shader, VkDevice device) { - auto key = Graphics::ShaderResources::ContentKey(shader, !TemplateDataRefresh(), DispatchMove()); +ResourceCache::Key DispatchContentKey(const Graphics::CompiledShader& shader, VkDevice device, bool movable = true) { + auto key = Graphics::ShaderResources::ContentKey(shader, !TemplateDataRefresh(), movable && DispatchMove()); const auto deviceHandle = reinterpret_cast(device); key.push_back(static_cast(deviceHandle)); key.push_back(static_cast(deviceHandle >> 32u)); @@ -3764,6 +3764,7 @@ struct PreparedDispatch { std::shared_ptr resources; std::shared_ptr cached; ResourceCache::Key key; + bool exact = false; // APS5_PROFILE_DRAW: stage A's time, added to the [dispatch] phase totals by the dispatch, and // the prepare's parts for the driver's 'prepare:' rows (PreparePhase order). double prepareMs = 0; @@ -3965,6 +3966,7 @@ constexpr std::size_t MoveRefusals = static_cast(Graphics::ShaderRe struct DispatchMoveCounters { std::atomic checked{0}, exact{0}, moved{0}, buffers{0}, bytes{0}; + std::atomic rebuilt{0}, kept{0}, served{0}; std::array, 5> sizes{}; std::array, MoveRefusals> refused{}; std::array, MoveRefusals> filtered{}; @@ -4001,11 +4003,14 @@ void reportDispatchMove() { const auto moved = take(moves.moved); const auto buffers = take(moves.buffers); const auto bytes = take(moves.bytes); + const auto rebuilt = take(moves.rebuilt); + const auto kept = take(moves.kept); + const auto served = take(moves.served); unsigned long long sizes[5]; for (std::size_t i = 0; i < 5; ++i) sizes[i] = take(moves.sizes[i]); const auto refused = list(moves.refused); const auto filtered = list(moves.filtered); - AgcDriver::ProfilePrint_nid_no_patch("[dispmove] (10 s) %llu template hits checked: %llu with every read-only buffer where it was built, %llu with %llu moved ones (%.2f MiB snapshotted; per dispatch up to 1 KiB %llu, 16 KiB %llu, 256 KiB %llu, 4 MiB %llu, more %llu); refused %s; stage A dropped cached objects: %s\n", checked, exact, moved, buffers, static_cast(bytes) / 1048576.0, sizes[0], sizes[1], sizes[2], sizes[3], sizes[4], refused.c_str(), filtered.c_str()); + AgcDriver::ProfilePrint_nid_no_patch("[dispmove] (10 s) %llu template hits checked: %llu with every read-only buffer where it was built, %llu with %llu moved ones (%.2f MiB snapshotted; per dispatch up to 1 KiB %llu, 16 KiB %llu, 256 KiB %llu, 4 MiB %llu, more %llu); refused %s; after a refusal: %llu rebuilt, %llu inserted beside the movable template (kept), %llu served by an exact entry; stage A dropped cached objects: %s\n", checked, exact, moved, buffers, static_cast(bytes) / 1048576.0, sizes[0], sizes[1], sizes[2], sizes[3], sizes[4], refused.c_str(), rebuilt, kept, served, filtered.c_str()); } void noteDispatchMoveSize(std::size_t bytes) { @@ -4173,7 +4178,7 @@ VkDevice VulkanDevice::Device() const { VulkanDevice::DispatchMoveCount VulkanDevice::DispatchMoveCounts() { auto& moves = DispatchMoves(); - DispatchMoveCount count{moves.checked.load(), moves.exact.load(), moves.moved.load(), 0, 0}; + DispatchMoveCount count{moves.checked.load(), moves.exact.load(), moves.moved.load(), 0, 0, moves.rebuilt.load(), moves.kept.load(), moves.served.load()}; for (std::size_t i = 1; i < MoveRefusals; ++i) { count.refused += moves.refused[i].load(); count.filtered += moves.filtered[i].load(); @@ -4307,7 +4312,9 @@ std::shared_ptr VulkanDevice::PrepareDispatch(const ShaderReco Graphics::ShaderResources::MoveProof proof; if (!cached->MovedDispatchBuffers(compute, nullptr, DispatchMoveBytes(), proof).has_value()) { DispatchMoves().filtered[static_cast(proof.refusal)].fetch_add(1, std::memory_order_relaxed); - cached = nullptr; + prepared->key = DispatchContentKey(compute, context.device, false); + prepared->exact = true; + cached = state->resourceCache.Find(prepared->key); } } prepared->cached = cached; @@ -4775,6 +4782,7 @@ VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::Rec // The object came from the resource cache: a compute template whose data buffers take this // dispatch's words at the record (ShaderResources::RefreshData). bool fromCache = false; + bool refusedMove = false; double revalidateMs = 0; std::shared_ptr overlay; // The batches the pre-sync (PrepareDispatch) waited for are reaped first: their completions @@ -4796,6 +4804,11 @@ VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::Rec // The image lookups of this call by outcome (stage B, a Revalidate or a locked build), for the // [indirect] line: reset here, read after the resources phase. if (profile && arguments != 0) Graphics::ThreadLookupOutcomes() = {}; + const auto noteRefusalBuild = [](bool inserted) { + auto& moves = DispatchMoves(); + moves.rebuilt.fetch_add(1, std::memory_order_relaxed); + if (inserted) moves.kept.fetch_add(1, std::memory_order_relaxed); + }; // The resource cache insert evicts the oldest entry, destroying its ShaderResources here when the // cache was the last owner: timed apart from the build. const auto insert = [&] { @@ -4844,6 +4857,7 @@ VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::Rec } if (cacheable) { ++d.cacheMisses; + if (prepared->exact) noteRefusalBuild(resources->Reusable()); if (resources->Reusable()) insert(); } } else if (cacheable) { @@ -4851,12 +4865,13 @@ VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::Rec // object it found: revalidated as a map hit would be (an entry replaced meanwhile is left // alone by the pointer-conditional Remove). APS5_NO_PREPARED_FIND=1 looks the key up again. static const bool preparedFind = std::getenv("APS5_NO_PREPARED_FIND") == nullptr; + const bool exactEntry = prepared != nullptr && prepared->exact; contentKey = prepared != nullptr && !prepared->key.empty() ? std::move(prepared->key) : DispatchContentKey(shaders[0], context.device); auto cached = preparedFind && prepared != nullptr && prepared->cached != nullptr ? std::move(prepared->cached) : state->resourceCache.Find(contentKey); if (cached != nullptr) { if (cached->Revalidate(shaders[0])) { bool usable = true; - if (DispatchMove()) { + if (DispatchMove() && !exactEntry) { auto& moves = DispatchMoves(); Graphics::ShaderResources::MoveProof proof; auto moved = cached->MovedDispatchBuffers(shaders[0], state->recorder.get(), DispatchMoveBytes(), proof); @@ -4881,7 +4896,19 @@ VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::Rec noteDispatchMoveSize(proof.bytes); } } + if (!usable) { + contentKey = DispatchContentKey(shaders[0], context.device, false); + refusedMove = true; + cached = state->resourceCache.Find(contentKey); + if (cached != nullptr && cached->Revalidate(shaders[0])) usable = true; + else if (cached != nullptr) { + state->resourceCache.Remove(contentKey, cached.get()); + ++d.cacheInvalidated; + state->recorder->Keep(std::move(cached)); + } + } if (usable) { + if (refusedMove || exactEntry) DispatchMoves().served.fetch_add(1, std::memory_order_relaxed); resources = std::move(cached); fromCache = true; ++d.cacheHits; @@ -4904,6 +4931,7 @@ VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::Rec if (profile) timer.add(PhaseResourcesFullBuild, std::chrono::duration(std::chrono::steady_clock::now() - buildStart).count()); if (cacheable) { ++d.cacheMisses; + if (refusedMove) noteRefusalBuild(resources->Reusable()); if (resources->Reusable()) insert(); } } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp index de985f99ca..7b0b62313d 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp @@ -180,6 +180,8 @@ public: std::optional> MovedDispatchBuffers(const CompiledShader& shader, Recorder* recorder, std::size_t maxBytes, MoveProof& proof) const; static std::size_t MaxDrawSnapshotBytes(); void WriteBack(); + // Deferred completion: MarkGpuWrites registers the results the recorded work leaves on the GPU + // (storage images stay there; buffer ranges are noted so CPU reads wait); WriteBackBuffers runs // once the work completed. void MarkGpuWrites(Recorder& recorder); void WriteBackBuffers(); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DispatchMove.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DispatchMove.cpp index efcab638b1..770cd6ad19 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DispatchMove.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DispatchMove.cpp @@ -169,12 +169,12 @@ void Expect(const std::uint32_t* input, const std::uint32_t* output, std::uint32 } struct Step { - std::uint64_t checked, exact, moved, refused; + std::uint64_t checked, exact, moved, refused, rebuilt, kept, served; }; Step Since(const AgcDriver::VulkanDevice::DispatchMoveCount& before) { const auto now = AgcDriver::VulkanDevice::DispatchMoveCounts(); - return {now.checked - before.checked, now.exact - before.exact, now.moved - before.moved, now.refused - before.refused}; + return {now.checked - before.checked, now.exact - before.exact, now.moved - before.moved, now.refused - before.refused, now.rebuilt - before.rebuilt, now.kept - before.kept, now.served - before.served}; } void Print(const char* what, const Step& step) { @@ -236,11 +236,33 @@ bool RunTests(AgcDriver::VulkanDevice& device, const GuestBlock& guest) { Expect(guest.InputA(), guest.OutputO(), 2u * Constant, "an input moved onto a range a recorded dispatch still writes"); const auto refused = Since(refusals); Print("two inputs moved onto written ranges", refused); - if (templates) Require(refused.refused == 2, "dispatch move: the inputs moved onto written ranges were not both refused"); + if (templates) { + Require(refused.refused == 2, "dispatch move: the inputs moved onto written ranges were not both refused"); + Require(refused.rebuilt == 2 && refused.kept == 2 && refused.served == 0, "dispatch move: the two refusals were not two rebuilds inserted beside the template"); + } + const auto afterRefusals = AgcDriver::VulkanDevice::DispatchMoveCounts(); Reset(guest.OutputO()); Run(device, compiled, guest.InputA(), Bytes, guest.OutputO(), true); Expect(guest.InputA(), guest.OutputO(), Constant, "the input moved back after the refusals"); + const auto back = Since(afterRefusals); + Print("the input back after the refusals", back); + if (templates) Require(back.checked == 1 && back.exact == 1 && back.moved == 0 && back.refused == 0 && back.rebuilt == 0, "dispatch move: the input back at the template's address was not an exact hit after the refusals"); + + for (int round = 0; round < 2; ++round) { + const auto alternating = AgcDriver::VulkanDevice::DispatchMoveCounts(); + Reset(guest.OutputO()); + Reset(guest.OutputP()); + Run(device, compiled, guest.InputA(), Bytes, guest.OutputP(), false); + Run(device, compiled, guest.OutputP(), Bytes, guest.OutputO(), true); + Expect(guest.InputA(), guest.OutputO(), 2u * Constant, "an input refused again while A alternates"); + Reset(guest.OutputO()); + Run(device, compiled, guest.InputA(), Bytes, guest.OutputO(), true); + Expect(guest.InputA(), guest.OutputO(), Constant, "the input at A after the refused one"); + const auto step = Since(alternating); + Print("A alternating with a refused input", step); + if (templates) Require(step.refused == 1 && step.served == 1 && step.rebuilt == 0 && step.exact == 2 && step.moved == 0, "dispatch move: a refused input rebuilt, or evicted the template of A, while A alternated with it"); + } const auto total = AgcDriver::VulkanDevice::DispatchMoveCounts(); std::printf("dispatch move: in total: checked %llu, exact %llu, moved %llu, refused %llu, dropped by stage A %llu\n", static_cast(total.checked), static_cast(total.exact), static_cast(total.moved), static_cast(total.refused), static_cast(total.filtered)); return templates; From 6ad392dad3b7cabf25d279c5c6d793d707d6fdcc Mon Sep 17 00:00:00 2001 From: Maz Date: Sat, 17 Oct 2026 16:03:12 +0400 Subject: [PATCH 0714/1000] fix(libkernel): report absolute Windows symbolic links lstat and readlink aborted on an absolute symbolic link because only a relative reparse point was accepted. Read the link text, and still refuse reparse points that are not symbolic links. --- .../File/include/WindowsSymbolicLink.hpp | 29 ++++++++++++++----- .../prx/libkernel/File/src/NativeStat.cpp | 4 +-- core/libs/prx/libkernel_sys/Export.cpp | 4 +-- core/libs/tests/GuestFilesystem.cpp | 20 ++----------- core/libs/tests/GuestKernelSys.cpp | 11 +++---- 5 files changed, 33 insertions(+), 35 deletions(-) diff --git a/core/libs/prx/libkernel/File/include/WindowsSymbolicLink.hpp b/core/libs/prx/libkernel/File/include/WindowsSymbolicLink.hpp index 849b88004e..4b49188e2e 100644 --- a/core/libs/prx/libkernel/File/include/WindowsSymbolicLink.hpp +++ b/core/libs/prx/libkernel/File/include/WindowsSymbolicLink.hpp @@ -26,8 +26,22 @@ class WindowsSymbolicLink { }; static constexpr ULONG relativeFlag = 1; + static std::wstring TextAt(const std::byte* data, DWORD size, USHORT offset, USHORT length, const char* operation) { + const std::size_t begin = sizeof(ReparseData) + offset; + if (length % sizeof(wchar_t) != 0 || begin + length > size) + throw std::runtime_error(std::string(operation) + ": malformed symbolic link reparse data"); + std::wstring name(length / sizeof(wchar_t), L'\0'); + if (!name.empty()) std::memcpy(name.data(), data + begin, name.size() * sizeof(wchar_t)); + return name; + } + + static std::wstring WithoutNtPrefix(std::wstring name) { + if (name.rfind(L"\\??\\", 0) == 0 || name.rfind(L"\\\\?\\", 0) == 0) name.erase(0, 4); + return name; + } + public: - static bool ReadRelative(const std::filesystem::path& native, std::string& target, const char* operation) { + static bool Read(const std::filesystem::path& native, std::string& target, const char* operation) { const HANDLE handle = CreateFileW(native.c_str(), 0, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS, nullptr); if (handle == INVALID_HANDLE_VALUE) @@ -42,12 +56,13 @@ public: ReparseData header{}; if (size < sizeof(header)) return false; std::memcpy(&header, data.data(), sizeof(header)); - if (header.tag != IO_REPARSE_TAG_SYMLINK || (header.flags & relativeFlag) == 0) return false; - const std::size_t begin = sizeof(header) + header.substituteNameOffset; - if (begin + header.substituteNameLength > size) - throw std::runtime_error(std::string(operation) + ": malformed symbolic link reparse data"); - std::wstring name(header.substituteNameLength / sizeof(wchar_t), L'\0'); - std::memcpy(name.data(), data.data() + begin, name.size() * sizeof(wchar_t)); + if (header.tag != IO_REPARSE_TAG_SYMLINK) return false; + const bool relative = (header.flags & relativeFlag) != 0; + std::wstring name = relative + ? TextAt(data.data(), size, header.substituteNameOffset, header.substituteNameLength, operation) + : TextAt(data.data(), size, header.printNameOffset, header.printNameLength, operation); + if (!relative && name.empty()) + name = WithoutNtPrefix(TextAt(data.data(), size, header.substituteNameOffset, header.substituteNameLength, operation)); target = std::filesystem::path(name).generic_string(); return true; } diff --git a/core/libs/prx/libkernel/File/src/NativeStat.cpp b/core/libs/prx/libkernel/File/src/NativeStat.cpp index b110e980df..b433847495 100644 --- a/core/libs/prx/libkernel/File/src/NativeStat.cpp +++ b/core/libs/prx/libkernel/File/src/NativeStat.cpp @@ -51,8 +51,8 @@ static int DoLstat(const std::filesystem::path& p, NativeStat* st) { const auto attributes = GetFileAttributesW(p.c_str()); if (attributes == INVALID_FILE_ATTRIBUTES || (attributes & FILE_ATTRIBUTE_REPARSE_POINT) == 0) return DoStat(p, st); std::string target; - if (!File::WindowsSymbolicLink::ReadRelative(p, target, "lstat")) - NotImplemented_nid_no_patch("lstat of a Windows reparse point other than a relative symbolic link"); + if (!File::WindowsSymbolicLink::Read(p, target, "lstat")) + NotImplemented_nid_no_patch("lstat of a Windows reparse point other than a symbolic link"); const auto handle = CreateFileW(p.c_str(), FILE_READ_ATTRIBUTES, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS, nullptr); if (handle == INVALID_HANDLE_VALUE) return File::WindowsFileTime::Failure(GetLastError()); diff --git a/core/libs/prx/libkernel_sys/Export.cpp b/core/libs/prx/libkernel_sys/Export.cpp index 481ff4b823..a1643984d3 100644 --- a/core/libs/prx/libkernel_sys/Export.cpp +++ b/core/libs/prx/libkernel_sys/Export.cpp @@ -130,8 +130,8 @@ std::int64_t ReadNativeLink(const std::filesystem::path& native, char* buffer, s const DWORD attributes = GetFileAttributesW(native.wstring().c_str()); if (attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0) { std::string target; - if (!File::WindowsSymbolicLink::ReadRelative(native, target, "readlink")) { - NotImplemented_nid_no_patch("readlink of a Windows reparse point other than a relative symbolic link"); + if (!File::WindowsSymbolicLink::Read(native, target, "readlink")) { + NotImplemented_nid_no_patch("readlink of a Windows reparse point other than a symbolic link"); return -1; } return CopyLinkTarget(target, buffer, size); diff --git a/core/libs/tests/GuestFilesystem.cpp b/core/libs/tests/GuestFilesystem.cpp index b87cf1344c..e17c262c18 100644 --- a/core/libs/tests/GuestFilesystem.cpp +++ b/core/libs/tests/GuestFilesystem.cpp @@ -713,24 +713,10 @@ int main() { Require(stat_nid_postfix(fileLink.string().c_str(), &followed) == 0 && (followed.st_mode & 0170000) == 0100000); Require(followed.st_ino == status.st_ino && linkStatus.st_ino != status.st_ino && linkStatus.st_dev == status.st_dev); Require(stat_nid_postfix(directoryLink.string().c_str(), &followed) == 0 && (followed.st_mode & 0170000) == 0040000); - const auto checkUnsupportedLink = [](const std::filesystem::path& link) { - FileStat untouched; - std::memset(&untouched, 0x5a, sizeof(untouched)); - constexpr int sentinelError = 123; - *__error_nid_postfix() = sentinelError; - bool rejected = false; - try { - lstat_nid_postfix(link.string().c_str(), &untouched); - } catch (const std::runtime_error& error) { - rejected = std::strcmp(error.what(), "lstat of a Windows reparse point other than a relative symbolic link not implemented") == 0; - } - const bool errorUnchanged = *__error_nid_postfix() == sentinelError; - const auto* bytes = reinterpret_cast(&untouched); - const bool statusUnchanged = std::all_of(bytes, bytes + sizeof(untouched), [](unsigned char value) { return value == 0x5a; }); - return rejected && errorUnchanged && statusUnchanged; - }; Require(stat_nid_postfix(fileLink.string().c_str(), &status) == 0 && status.st_size == 5); - Require(checkUnsupportedLink(absoluteLink)); + const auto absoluteTarget = std::filesystem::absolute(present).generic_string(); + Require(checkRelativeLink(absoluteLink, static_cast(absoluteTarget.size()))); + Require(stat_nid_postfix(absoluteLink.string().c_str(), &followed) == 0 && followed.st_ino == status.st_ino); Require(std::filesystem::remove(fileLink) && std::filesystem::remove(directoryLink) && DeleteFileW(danglingLink.c_str()) != 0 && std::filesystem::remove(absoluteLink) && std::filesystem::remove(targetDirectory)); } diff --git a/core/libs/tests/GuestKernelSys.cpp b/core/libs/tests/GuestKernelSys.cpp index d16ff6789a..4d004d5378 100644 --- a/core/libs/tests/GuestKernelSys.cpp +++ b/core/libs/tests/GuestKernelSys.cpp @@ -287,15 +287,12 @@ int main() { Require(symlink_nid_postfix("sub", directoryLink.c_str()) == 0); Require(readlink_nid_postfix(directoryLink.c_str(), buffer, sizeof(buffer)) == 3 && std::memcmp(buffer, "sub", 3) == 0); const auto absolute = std::filesystem::absolute(root / "absolute"); + const auto absoluteTarget = std::filesystem::absolute(original).generic_string(); Require(CreateSymbolicLinkW(absolute.wstring().c_str(), std::filesystem::absolute(original).wstring().c_str(), SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE) != 0); - bool refused = false; - try { - readlink_nid_postfix((root / "absolute").string().c_str(), buffer, sizeof(buffer)); - } catch (const std::runtime_error& failure) { - refused = std::string(failure.what()) == "readlink of a Windows reparse point other than a relative symbolic link not implemented"; - } - Require(refused); + std::memset(buffer, '#', sizeof(buffer)); + Require(readlink_nid_postfix((root / "absolute").string().c_str(), buffer, sizeof(buffer)) == static_cast(absoluteTarget.size())); + Require(std::memcmp(buffer, absoluteTarget.data(), absoluteTarget.size()) == 0 && buffer[absoluteTarget.size()] == '#'); #else Require(std::filesystem::is_symlink(std::filesystem::symlink_status(symbolic))); Require(readlink_nid_postfix(symbolic.c_str(), buffer, sizeof(buffer)) == 12 && std::memcmp(buffer, "original.bin", 12) == 0); From 82dff584941bbc7ac9c7dad3328b9becc3b92ff4 Mon Sep 17 00:00:00 2001 From: Pouare514 Date: Sat, 17 Oct 2026 14:02:46 +0200 Subject: [PATCH 0715/1000] fix(libkernel): scope flexible release to tracked ranges with release-then-record --- .../prx/libkernel/DirectMemory/Export.cpp | 54 +++++++++++------- core/libs/tests/GuestMemory.cpp | 55 +++++++++++++++++++ 2 files changed, 90 insertions(+), 19 deletions(-) diff --git a/core/libs/prx/libkernel/DirectMemory/Export.cpp b/core/libs/prx/libkernel/DirectMemory/Export.cpp index 8533a87c32..3cc16b6552 100644 --- a/core/libs/prx/libkernel/DirectMemory/Export.cpp +++ b/core/libs/prx/libkernel/DirectMemory/Export.cpp @@ -105,22 +105,7 @@ size_t _flexibleUsedLocked() { return used; } -int _mapFlexible(void** addr, size_t len, int prot, int flags) { - const size_t configured = _configuredFlexibleSize(); - const size_t images = GuestImageSegmentBytes_nid_no_patch(); - std::lock_guard lock(g_flexibleLock); - const size_t used = images + _flexibleUsedLocked(); - if (len > configured - std::min(configured, used)) { - std::fprintf(stderr, "[memory] flexible memory exhausted: request 0x%zx with 0x%zx of 0x%zx in use\n", len, used, configured); - return SCE_KERNEL_ERROR_ENOMEM; - } - const int result = DoMapAnon(addr, len, prot, flags); - if (result == 0) g_flexibleRanges[reinterpret_cast(*addr)] = len; - return result; -} - -void _releaseFlexible(uintptr_t start, size_t len) { - std::lock_guard lock(g_flexibleLock); +void _releaseFlexibleLocked(uintptr_t start, size_t len) { const uintptr_t end = start + len; auto it = g_flexibleRanges.upper_bound(start); if (it != g_flexibleRanges.begin()) --it; @@ -134,6 +119,29 @@ void _releaseFlexible(uintptr_t start, size_t len) { } } +int _mapFlexible(void** addr, size_t len, int prot, int flags) { + const size_t configured = _configuredFlexibleSize(); + const size_t images = GuestImageSegmentBytes_nid_no_patch(); + std::lock_guard lock(g_flexibleLock); + const size_t used = images + _flexibleUsedLocked(); + if (len > configured - std::min(configured, used)) { + std::fprintf(stderr, "[memory] flexible memory exhausted: request 0x%zx with 0x%zx of 0x%zx in use\n", len, used, configured); + return SCE_KERNEL_ERROR_ENOMEM; + } + const int result = DoMapAnon(addr, len, prot, flags); + if (result == 0) { + const auto start = reinterpret_cast(*addr); + _releaseFlexibleLocked(start, len); + g_flexibleRanges[start] = len; + } + return result; +} + +void _releaseFlexible(uintptr_t start, size_t len) { + std::lock_guard lock(g_flexibleLock); + _releaseFlexibleLocked(start, len); +} + std::mutex g_lockedRangesLock; std::map g_lockedRanges; @@ -224,12 +232,16 @@ int APS5_VABI sceKernelEnableDmemAliasing(void) { int APS5_VABI sceKernelMapDirectMemory(void** addr, size_t len, int prot, int flags, int64_t direct_memory_start, size_t alignment) { (void)alignment; - return DoMapDirect(addr, len, prot, flags, direct_memory_start, alignment); + const int result = DoMapDirect(addr, len, prot, flags, direct_memory_start, alignment); + if (result == 0) _releaseFlexible(reinterpret_cast(*addr), len); + return result; } int APS5_VABI sceKernelMapDirectMemory2(void** addr, size_t len, int type, int prot, int flags, int64_t direct_memory_start, size_t alignment) { (void)type; - return DoMapDirect(addr, len, prot, flags, direct_memory_start, alignment); + const int result = DoMapDirect(addr, len, prot, flags, direct_memory_start, alignment); + if (result == 0) _releaseFlexible(reinterpret_cast(*addr), len); + return result; } int APS5_VABI sceKernelMapFlexibleMemory(void** addr_in_out, size_t len, int prot, int flags) { @@ -241,6 +253,7 @@ int APS5_VABI sceKernelSetVirtualRangeName(const void* addr, uint64_t len, const int APS5_VABI sceKernelMapNamedDirectMemory(void** addr, size_t len, int prot, int flags, int64_t direct_memory_start, size_t alignment, const char* name) { const int result = DoMapDirect(addr, len, prot, flags, direct_memory_start, alignment); + if (result == 0) _releaseFlexible(reinterpret_cast(*addr), len); if (result == 0 && name) sceKernelSetVirtualRangeName(*addr, len, name); return result; } @@ -294,7 +307,9 @@ int APS5_VABI sceKernelReleaseDirectMemory(int64_t start, size_t len) { } int APS5_VABI sceKernelReserveVirtualRange(void** addr, size_t len, int flags, size_t alignment) { - return DoReserveVirtual(addr, len, flags, alignment); + const int result = DoReserveVirtual(addr, len, flags, alignment); + if (result == 0) _releaseFlexible(reinterpret_cast(*addr), len); + return result; } int APS5_VABI sceKernelVirtualQuery(const void* addr, int flags, VirtualQueryInfo* info, uint64_t info_size) { @@ -501,6 +516,7 @@ int APS5_VABI sceKernelBatchMap2(KernelBatchMapEntry* entries, int num_entries, switch (entry.operation) { case OpMapDirect: result = DoMapDirect(&entry.start, entry.length, static_cast(entry.protection), flags, static_cast(entry.offset), 0); + if (result == 0) _releaseFlexible(reinterpret_cast(entry.start), entry.length); break; case OpUnmap: result = sceKernelMunmap(reinterpret_cast(entry.start), entry.length); diff --git a/core/libs/tests/GuestMemory.cpp b/core/libs/tests/GuestMemory.cpp index 368b25dfd3..24780cfbb1 100644 --- a/core/libs/tests/GuestMemory.cpp +++ b/core/libs/tests/GuestMemory.cpp @@ -237,6 +237,59 @@ static void CheckInternalNamedFlexibleMapping() { Require(sceKernelAvailableFlexibleMemorySize(&available) == 0 && available == before); } +static void CheckFlexibleFixedReplacement() { + constexpr std::size_t page = 0x4000; + std::size_t before = 0; + std::size_t available = 0; + Require(sceKernelAvailableFlexibleMemorySize(&before) == 0); + void* base = nullptr; + Require(sceKernelMapFlexibleMemory(&base, page * 4, 3, 0) == 0 && base != nullptr); + Require(sceKernelAvailableFlexibleMemorySize(&available) == 0 && available == before - page * 4); + auto* bytes = static_cast(base); + for (std::size_t index = 0; index < 4; ++index) bytes[index * page] = static_cast(17 + index); + void* middle = bytes + page; + void* replacement = middle; + Require(sceKernelMapFlexibleMemory(&replacement, page, 3, 0x10) == 0 && replacement == middle); + Require(bytes[0] == 17 && bytes[page] == 0 && bytes[page * 2] == 19 && bytes[page * 3] == 20); + Require(sceKernelAvailableFlexibleMemorySize(&available) == 0 && available == before - page * 4); + Require(sceKernelMunmap(base, page * 4) == 0); + Require(sceKernelAvailableFlexibleMemorySize(&available) == 0 && available == before); +} + +static void CheckMunmapScopedFlexibleRelease() { + constexpr std::size_t page = 0x4000; + std::size_t before = 0; + std::size_t available = 0; + Require(sceKernelAvailableFlexibleMemorySize(&before) == 0); + void* base = nullptr; + Require(sceKernelMapFlexibleMemory(&base, page * 2, 3, 0) == 0 && base != nullptr); + Require(sceKernelAvailableFlexibleMemorySize(&available) == 0 && available == before - page * 2); + std::int64_t phys = 0; + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, page, 0, 0, &phys) == 0); + void* direct = static_cast(base) + page; + Require(sceKernelMapDirectMemory(&direct, page, 3, 0x10, phys, 0) == 0 && direct == static_cast(base) + page); + std::size_t mid = 0; + Require(sceKernelAvailableFlexibleMemorySize(&mid) == 0); + Require(sceKernelMunmap(direct, page) == 0); + Require(sceKernelAvailableFlexibleMemorySize(&available) == 0 && available == mid); + void* reserved = nullptr; + Require(sceKernelReserveVirtualRange(&reserved, page * 2, 0, 0) == 0 && reserved != nullptr); + Require(sceKernelAvailableFlexibleMemorySize(&available) == 0 && available == mid); + Require(sceKernelMunmap(reserved, page * 2) == 0); + Require(sceKernelAvailableFlexibleMemorySize(&available) == 0 && available == mid); + Require(sceKernelMunmap(base, page) == 0); + Require(sceKernelAvailableFlexibleMemorySize(&available) == 0 && available == mid + page); + Require(sceKernelReleaseDirectMemory(phys, page) == 0); + void* fresh = nullptr; + Require(sceKernelMapFlexibleMemory(&fresh, page * 4, 3, 0) == 0 && fresh != nullptr); + Require(sceKernelAvailableFlexibleMemorySize(&available) == 0 && available == before - page * 4); + Require(sceKernelMunmap(static_cast(fresh) + page, page * 2) == 0); + Require(sceKernelAvailableFlexibleMemorySize(&available) == 0 && available == before - page * 2); + Require(sceKernelMunmap(fresh, page) == 0); + Require(sceKernelMunmap(static_cast(fresh) + page * 3, page) == 0); + Require(sceKernelAvailableFlexibleMemorySize(&available) == 0 && available == before); +} + static void CheckGuestModuleUsesFlexibleMemory() { constexpr std::size_t moduleData = 0x10000; const auto path = std::filesystem::relative(GUEST_MEMORY_MODULE).generic_string(); @@ -2650,6 +2703,8 @@ int main(int argc, char** argv) { CheckVirtualQueryWireLayout(); CheckNamedAndHintedMappings(); CheckInternalNamedFlexibleMapping(); + CheckFlexibleFixedReplacement(); + CheckMunmapScopedFlexibleRelease(); CheckGuestModuleUsesFlexibleMemory(); CheckBatchMapStopsAtInvalidEntry(); CheckCheckedReleaseDirectMemory(); From 975c3ee6b3bad3519eb0f0757d49e076d9116195 Mon Sep 17 00:00:00 2001 From: Romans Pokrovskis Date: Sat, 17 Oct 2026 15:04:14 +0300 Subject: [PATCH 0716/1000] test(agc): distinguish stores beside resident DCC metadata --- .../tests/execution/AlphaToCoverage.cpp | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/AlphaToCoverage.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/AlphaToCoverage.cpp index d2c7fbdcc1..4f6288141b 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/AlphaToCoverage.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/AlphaToCoverage.cpp @@ -514,6 +514,27 @@ void DccFastClearTests(AgcDriver::VulkanDevice& device, std::uint32_t selectedSa } } } + if (code == 0 && !alphaOnMsb && cmaskMode == 0) { + const auto dccBytes = AgcDriver::Graphics::MultisampledDccBytes(target); + Require(dccBytes + 8u <= metadata.bytes, "the DCC fixture has no adjacent-page control"); + const std::array adjacent{std::byte{0x7b}, std::byte{0x7b}, std::byte{0x7b}, std::byte{0x7b}}; + AgcDriver::GuestMemory::Write(metadata.Address() + dccBytes, adjacent); + device.ColorMetadataPass(pass); + Require(ReadBack(device, resolved) == stored, "an adjacent driver store changed the resident DCC target"); + static_cast(metadata.data)[dccBytes + 4u] = 0x7c; + AgcDriver::GuestMemory::CollectWritesUncached(metadata.Address() + dccBytes, 8u); + device.ColorMetadataPass(pass); + Require(ReadBack(device, resolved) == stored, "an adjacent CPU store changed the resident DCC target"); + const std::array same{std::byte{0}}; + AgcDriver::GuestMemory::Write(metadata.Address(), same); + bool refused = false; + try { + device.ColorMetadataPass(pass); + } catch (const std::runtime_error& error) { + refused = std::string_view(error.what()).find("writes to a resident multisampled DCC") != std::string_view::npos; + } + Require(refused, "a same-value driver DCC store did not invalidate multisampled DCC residency"); + } if (code == 0 && alphaOnMsb && cmaskMode == 0) { static_cast(source.data)[0] = 0; bool refused = false; From 6eebcc2ce1e3ba4a53b923489e62b2be8cd9484e Mon Sep 17 00:00:00 2001 From: Romans Pokrovskis Date: Sat, 17 Oct 2026 15:05:34 +0300 Subject: [PATCH 0717/1000] fix(agc): exclude neighboring stores from DCC invalidation --- .../libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp index 5fc7544a28..0ba4cc82ba 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp @@ -344,7 +344,7 @@ private: for (const auto& range : dccRanges) { Require(GuestMemory::Watched(range.address, range.bytes), "multisampled DCC target reuse requires watched guest memory"); GuestMemory::CollectWritesUncached(range.address, range.bytes); - Require(GuestMemory::UnchangedSince(range.address, range.bytes, range.generation), "writes to a resident multisampled DCC target or its metadata are not implemented"); + Require(!GuestMemory::StoredOver(range.address, range.bytes, range.generation), "writes to a resident multisampled DCC target or its metadata are not implemented"); if (auto* recorder = Recorder::Active(); recorder != nullptr) Require(!recorder->PendingWriteOverlaps(range.address, range.bytes), "pending writes to a resident multisampled DCC target or its metadata are not implemented"); } } From e88b7de3c1191922185acd8fe73fa8aa427443c7 Mon Sep 17 00:00:00 2001 From: milescoviello Date: Sat, 17 Oct 2026 12:06:42 +0000 Subject: [PATCH 0718/1000] fix(libkernel): measure the host stack reserve from the frame address MeasureStackReserve took the distance from the stack top to a local variable. In an ANYPS5_ENABLE_ASAN build that local lives on AddressSanitizer's fake stack (detect_stack_use_after_return is on by default on Linux), away from the thread's stack, so the reserve could come out negative. scePthreadCreate then asked the guest arena for a stack of nearly 2^64 bytes and threw 'guest address space arena exhausted'. The frame address is on the real stack in every build. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libkernel/Pthread/src/Thread.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libkernel/Pthread/src/Thread.cpp b/core/libs/prx/libkernel/Pthread/src/Thread.cpp index d453f09671..b6d7cd0bbb 100644 --- a/core/libs/prx/libkernel/Pthread/src/Thread.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Thread.cpp @@ -434,7 +434,7 @@ static void* MeasureStackReserve(void* reserve) { std::uintptr_t low = 0; std::uintptr_t high = 0; if (HostStackLimits(&low, &high)) - *static_cast(reserve) = high - reinterpret_cast(&low); + *static_cast(reserve) = high - reinterpret_cast(__builtin_frame_address(0)); return nullptr; } From 724b16624f004f81a8e6cc9408bf28a505b9be8b Mon Sep 17 00:00:00 2001 From: Sam Date: Fri, 16 Oct 2026 06:50:25 +0100 Subject: [PATCH 0719/1000] fix(agc): restrict lazy sampled view usage Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 6 ++ .../libSceAgcDriver/Graphics/src/Texture.cpp | 8 ++- .../prx/libSceAgcDriver/tests/Recorder.cpp | 69 +++++++++++++++++++ 3 files changed, 82 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 4eba5fa87e..8b0d6e736d 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1112,6 +1112,12 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() add_test(NAME agc_storage_upload_lifetime COMMAND agc_driver_recorder_tests --storage-upload-lifetime-only) + add_test(NAME agc_sampled_storage_base_level_usage COMMAND agc_driver_recorder_tests --sampled-storage-base-level-usage-only) + set_tests_properties(agc_sampled_storage_base_level_usage PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30) + if(WIN32) + set_tests_properties(agc_sampled_storage_base_level_usage PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test(NAME agc_storage_upload_failure COMMAND agc_driver_recorder_tests --storage-upload-failure-only) set_tests_properties(agc_storage_upload_lifetime agc_storage_upload_failure PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30) if(WIN32) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index ed284ea658..ec3a8317f3 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -864,7 +864,13 @@ VkImageView Texture::SrgbView(bool firstLayer, bool baseLevel) const { VkImageView Texture::BaseLevelView() const { Require(baseLevelInfo.has_value(), "a base-level view needs a mipmapped 1D, 2D or 2D array texture"); std::lock_guard lock(baseLevelMutex); - if (!baseLevelView) Check(context.Function("vkCreateImageView")(context.device, &*baseLevelInfo, nullptr, &baseLevelView), "vkCreateImageView base level"); + if (!baseLevelView) { + auto info = *baseLevelInfo; + VkImageViewUsageCreateInfo usage{VK_STRUCTURE_TYPE_IMAGE_VIEW_USAGE_CREATE_INFO}; + usage.usage = VK_IMAGE_USAGE_SAMPLED_BIT; + info.pNext = &usage; + Check(context.Function("vkCreateImageView")(context.device, &info, nullptr, &baseLevelView), "vkCreateImageView base level"); + } return baseLevelView; } diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index b496c3428b..c1a5d7cf62 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -6997,6 +6997,70 @@ void singleCubeTests(const Device& device, Recorder& recorder) { } } +struct SampledViewProbe { + Context& context; + PFN_vkGetDeviceProcAddr resolver; + std::uint32_t views = 0; + bool checking = false; + static inline SampledViewProbe* active = nullptr; + explicit SampledViewProbe(Context& context) : context(context), resolver(context.deviceProc) { + Require(active == nullptr, "sampled view probe is already active"); + active = this; + context.deviceProc = resolve; + } + ~SampledViewProbe() { + context.deviceProc = resolver; + active = nullptr; + } + static VKAPI_ATTR VkResult VKAPI_CALL createView(VkDevice device, const VkImageViewCreateInfo* info, const VkAllocationCallbacks* callbacks, VkImageView* created) { + if (active->checking && info->format == VK_FORMAT_R8G8B8A8_SRGB) { + const auto* next = static_cast(info->pNext); + while (next != nullptr && next->sType != VK_STRUCTURE_TYPE_IMAGE_VIEW_USAGE_CREATE_INFO) next = next->pNext; + if (next == nullptr || reinterpret_cast(next)->usage != VK_IMAGE_USAGE_SAMPLED_BIT) return VK_ERROR_VALIDATION_FAILED_EXT; + ++active->views; + } + return reinterpret_cast(active->resolver(device, "vkCreateImageView"))(device, info, callbacks, created); + } + static VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL resolve(VkDevice device, const char* name) { + if (std::strcmp(name, "vkCreateImageView") == 0) return reinterpret_cast(createView); + return active->resolver(device, name); + } +}; + +void sampledStorageBaseLevelUsageTests(const Device& device, Recorder& recorder) { + auto context = device.GetContext(); + TextureDetiler detiler(context); + context.detiler = &detiler; + GuestTextureResource resource{}; + resource.width = resource.height = 8; + resource.depthOrLastArray = 1; + resource.mipCount = 2; + resource.lastLevel = 1; + resource.minLod = context.imageViewMinLod ? 0x100 : 0; + resource.tileMode = TextureTileMode::kLinear; + resource.dimension = TextureDimension::k2DArray; + resource.format = 130; + resource.dstSelX = 4; + resource.dstSelY = 5; + resource.dstSelZ = 6; + resource.dstSelW = 7; + const auto geometry = DescribeSurface(resource); + std::vector memory(static_cast(geometry.guestBytes) + 256, std::byte{0x80}); + resource.baseAddress = (reinterpret_cast(memory.data()) + 255) & ~std::uintptr_t{255}; + auto storage = StorageTexture::Create(context, detiler, resource, 0); + recorder.Keep(storage); + recorder.Sync(); + SampleProgram program(context, recorder); + auto sampledContext = context; + SampledViewProbe probe(sampledContext); + Texture sampled(sampledContext, storage, resource, VkComponentMapping{}); + probe.checking = true; + const auto base = sampled.BaseLevelView(); + probe.checking = false; + expectRed(program.Red(base, sampled.Layout(), 0), 0.21586f, "sampled sRGB base level"); + Require(sampled.BaseLevelView() == base && probe.views == 1, "the lazy base-level view of a sampled storage image must restrict usage to SAMPLED and be reused"); +} + void firstLayerViewTests(const Device& device, Recorder& recorder) { const auto& context = device.GetContext(); TextureDetiler detiler(context); @@ -8229,6 +8293,10 @@ int main(int argc, char** argv) { std::cout << "Single cube snapshot and storage sampling tests passed\n"; return 0; } + if (argc == 2 && std::string_view(argv[1]) == "--sampled-storage-base-level-usage-only") { + sampledStorageBaseLevelUsageTests(device, recorder); + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--unchanged-cpu-stamps-only") { unchangedCpuStampTests(device, recorder); return 0; @@ -8446,6 +8514,7 @@ int main(int argc, char** argv) { imageMemoTests(device, recorder); minLodTests(device, recorder); firstLayerViewTests(device, recorder); + sampledStorageBaseLevelUsageTests(device, recorder); singleCubeTests(device, recorder); atomicViewTests(device, recorder); metadataPassTests(device, recorder); From 86b05a0dc686d4fd44372940fb3916fa2334316e Mon Sep 17 00:00:00 2001 From: Sam <85789460+Sam-mon7@users.noreply.github.com> Date: Sat, 17 Oct 2026 11:18:03 +0100 Subject: [PATCH 0720/1000] fix(agc): keep cached pipelines across lazy buffer pool creation Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 4 ++- .../libSceAgcDriver/Graphics/src/Pipeline.cpp | 11 ++++-- .../prx/libSceAgcDriver/tests/Graphics.cpp | 35 +++++++++++++++++++ 3 files changed, 46 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 4eba5fa87e..f581a3dc12 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1611,10 +1611,12 @@ if(BUILD_TESTING) set_tests_properties(agc_sparse_buffer_holes PROPERTIES TIMEOUT 30) add_test(NAME agc_pipeline_journal COMMAND agc_driver_graphics_tests --pipeline-journal) add_test(NAME agc_pipeline_library_clear_waits_for_build COMMAND agc_driver_graphics_tests --pipeline-library-clear-waits-for-build) + add_test(NAME agc_pipeline_store_lazy_buffer_pool COMMAND agc_driver_graphics_tests --pipeline-store-lazy-buffer-pool) set_tests_properties(agc_pipeline_journal PROPERTIES TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") set_tests_properties(agc_pipeline_library_clear_waits_for_build PROPERTIES TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + set_tests_properties(agc_pipeline_store_lazy_buffer_pool PROPERTIES TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") if(WIN32) - set_tests_properties(agc_pipeline_journal agc_pipeline_library_clear_waits_for_build PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + set_tests_properties(agc_pipeline_journal agc_pipeline_library_clear_waits_for_build agc_pipeline_store_lazy_buffer_pool PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() set_tests_properties(agc_driver_sparse_buffer_holes PROPERTIES TIMEOUT 30) if(WIN32) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp index d9a9c96bdc..6e7cc83862 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp @@ -911,6 +911,7 @@ struct PipelineStore { // The device's buffer pool at insertion: it is made and reset with the device, so it tells // the device instance apart from a later one the loader gave the same handle value. std::weak_ptr pool; + bool tracksPool; std::uint64_t hash; std::vector key; std::shared_ptr pipeline; @@ -936,8 +937,12 @@ PipelineStore& Pipelines() { // Whether the entry's objects belong to the device the context names. A context without a buffer // pool (tests) is identified by the handle alone. -bool alive(const PipelineStore::Entry& entry, const Context& context) { +bool alive(PipelineStore::Entry& entry, const Context& context) { if (entry.device != context.device) return false; + if (!entry.tracksPool && context.bufferPool != nullptr) { + entry.pool = context.bufferPool; + entry.tracksPool = true; + } return context.bufferPool == nullptr || entry.pool.lock() == context.bufferPool; } @@ -1048,10 +1053,10 @@ std::shared_ptr CachedPipeline(const Context& context, const State& st // the title's own pipelines evict it first, and a full store takes none. if (replaying && store.entries.size() >= bound) return pipeline; if (replaying) { - store.entries.push_front({context.device, context.bufferPool, hash, std::vector(key), pipeline}); + store.entries.push_front({context.device, context.bufferPool, context.bufferPool != nullptr, hash, std::vector(key), pipeline}); store.index[hash] = store.entries.begin(); } else { - store.entries.push_back({context.device, context.bufferPool, hash, std::vector(key), pipeline}); + store.entries.push_back({context.device, context.bufferPool, context.bufferPool != nullptr, hash, std::vector(key), pipeline}); store.index[hash] = std::prev(store.entries.end()); } while (store.entries.size() > bound) { diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 95c09675a9..715cbefdac 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -7706,6 +7706,35 @@ void ResourceContentKeyTests(bool benchmark) { // into a second device) rebuilds the recorded graphics pipeline so the same configuration hits the // store, hands the compute result to its hook, recovers a tail torn inside a record's padding and // stops when cancelled. +void pipelineStoreLazyBufferPoolTests() { + using namespace AgcDriver::Graphics; + mock = MockVulkan{}; + auto context = mockContext(); + context.device = makeHandle(); + context.graphicsPipelineLibrary = true; + context.pipelineLinkTimeOptimization = false; + context.limits.framebufferNoAttachmentsSampleCounts = VK_SAMPLE_COUNT_1_BIT; + ShaderRecompiler::RecompileResult vertex, fragment; + vertex.spirv = makeModule({}); + fragment.spirv = makeModule({.fragment = true}); + vertex.variantId = 201; + fragment.variantId = 202; + const std::array shaders{{{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, {ShaderRecompiler::ShaderStage::Fragment, &fragment, PipelinePushSlotBytes}}}; + std::array mainKey{31}, fragmentKey{32}; + const PipelineSetLayouts resources{makeHandle(), makeHandle(), true, mainKey, fragmentKey}; + Require(context.bufferPool == nullptr, "the lazy buffer pool fixture already owns a buffer pool"); + auto first = CachedPipeline(context, State{}, VertexInputLayout{}, resources, shaders); + const auto created = mock.pipelineCreateCount; + static_cast(GetBufferPool(context)); + auto again = CachedPipeline(context, State{}, VertexInputLayout{}, resources, shaders); + Require(again == first && mock.pipelineCreateCount == created, "creating the buffer pool dropped a cached pipeline of the live device"); + ClearCachedPipelines(context.device); + again.reset(); + first.reset(); + context.bufferPool.reset(); + Require(mock.live == 0, "lazy buffer pool pipeline tests leaked Vulkan objects"); +} + void pipelineJournalTests() { using namespace AgcDriver::Graphics; using Stage = ShaderRecompiler::ShaderStage; @@ -8008,6 +8037,11 @@ int main(int argc, char** argv) { std::cout << "pipeline library clear waits for active part builds\n"; return 0; } + if (argc == 2 && std::string_view(argv[1]) == "--pipeline-store-lazy-buffer-pool") { + pipelineStoreLazyBufferPoolTests(); + std::cout << "cached pipelines survive lazy buffer pool creation\n"; + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--sparse-buffer-holes") { bool passed = false; { @@ -8123,6 +8157,7 @@ int main(int argc, char** argv) { RunGuestLeaseWaitTests(); ResourceContentKeyTests(false); stateTests(); + pipelineStoreLazyBufferPoolTests(); retainedPixelStageTests(); pipelineLibrarySetTests(); pipelineLibraryClearWaitsForBuildTests(); From 432fe5cf94fbb51a00ff389f162bebc19ffcfa01 Mon Sep 17 00:00:00 2001 From: Ondra97 <47064087+Ondra97@users.noreply.github.com> Date: Sat, 17 Oct 2026 10:42:33 +0200 Subject: [PATCH 0721/1000] fix(agc): discard device buffers after memory binding fails --- .../Graphics/include/Resources.hpp | 1 + .../Graphics/src/Resources.cpp | 9 ++- .../prx/libSceAgcDriver/tests/BufferPool.cpp | 36 ++++++++++ .../tests/execution/PooledBufferRange.cpp | 72 +++++++++++++++++++ 4 files changed, 116 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Resources.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Resources.hpp index 162a2bd327..674cff0c47 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Resources.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Resources.hpp @@ -64,6 +64,7 @@ private: VkBufferUsageFlags usage; std::shared_ptr cache; bool hostPlaced = false; + bool ready = false; }; // Records a whole-range buffer copy. diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Resources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Resources.cpp index a5ab4d1d84..c485d2f0c9 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Resources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Resources.cpp @@ -151,7 +151,10 @@ DeviceBuffer::DeviceBuffer(const Context& context, std::size_t size, VkBufferUsa memory = allocation->memory; allocationBytes = allocation->allocationBytes; capacity = allocation->bytes; - if (allocation->bufferBytes == size) return; + if (allocation->bufferBytes == size) { + ready = true; + return; + } context.Function("vkDestroyBuffer")(context.device, buffer, nullptr); buffer = VK_NULL_HANDLE; try { @@ -160,6 +163,7 @@ DeviceBuffer::DeviceBuffer(const Context& context, std::size_t size, VkBufferUsa release(); throw; } + ready = true; return; } try { @@ -177,6 +181,7 @@ DeviceBuffer::DeviceBuffer(const Context& context, std::size_t size, VkBufferUsa hostPlaced = allocation.memoryTypeIndex != context.MemoryType(requirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); memory = cache->AllocateMemory(allocation, "vkAllocateMemory device buffer"); Check(context.Function("vkBindBufferMemory")(context.device, buffer, memory, 0), "vkBindBufferMemory device"); + ready = true; } catch (...) { release(); throw; @@ -188,7 +193,7 @@ DeviceBuffer::~DeviceBuffer() { } void DeviceBuffer::release() noexcept { - if (buffer && memory && cache && !hostPlaced) { + if (ready && buffer && memory && cache && !hostPlaced) { cache->Put({buffer, memory, nullptr, 0, allocationBytes, capacity, usage, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, 0, false, size}); return; } diff --git a/core/libs/prx/libSceAgcDriver/tests/BufferPool.cpp b/core/libs/prx/libSceAgcDriver/tests/BufferPool.cpp index 0f3c9812ca..18332be7ce 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BufferPool.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BufferPool.cpp @@ -51,6 +51,8 @@ struct MockDevice { VkResult pressureResult = VK_ERROR_OUT_OF_DEVICE_MEMORY; std::uint64_t attempts = 0; VkResult mapResult = VK_SUCCESS; + VkResult bindResult = VK_SUCCESS; + unsigned binds = 0; unsigned maps = 0; unsigned unmaps = 0; }; @@ -91,6 +93,8 @@ VKAPI_ATTR VkResult VKAPI_CALL mockAllocateMemory(VkDevice, const VkMemoryAlloca } VKAPI_ATTR VkResult VKAPI_CALL mockBindBufferMemory(VkDevice, VkBuffer buffer, VkDeviceMemory memory, VkDeviceSize offset) { + ++mock.binds; + if (mock.bindResult != VK_SUCCESS) return mock.bindResult; if (offset + mock.sizes.at(buffer) > mock.memoryBytes.at(memory)) return VK_ERROR_OUT_OF_DEVICE_MEMORY; mock.bound[buffer] = {memory, offset}; return VK_SUCCESS; @@ -626,6 +630,37 @@ void PartialSlabDoesNotRetry() { Expect(mock.liveBytes == 0 && mock.liveBuffers.empty(), "partial slab failure leaked a resource"); } +void DeviceBindingFailures() { + constexpr auto bytes = 3 * MiB; + constexpr auto usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT; + for (const auto result : {VK_ERROR_OUT_OF_HOST_MEMORY, VK_ERROR_OUT_OF_DEVICE_MEMORY}) { + mock = MockDevice{}; + { + auto context = mockContext(); + mock.bindResult = result; + bool threw = false; + try { + DeviceBuffer failed(context, bytes, usage); + } catch (const std::runtime_error& error) { + threw = std::string(error.what()).find("Vulkan result " + std::to_string(result)) != std::string::npos; + } + const auto retained = mock.liveBytes; + Expect(threw && mock.binds == 1, "a failed device buffer bind lost its error"); + Expect(retained == 0 && mock.liveBuffers.empty(), "a failed device buffer was retained for reuse"); + mock.bindResult = VK_SUCCESS; + unsigned invalid = 0; + for (unsigned retry = 0; retry < 3; ++retry) { + DeviceBuffer recovered(context, bytes, usage); + if (!mock.bound.contains(recovered.Handle())) ++invalid; + } + Expect(invalid == 0, "a retry reused an unbound device buffer"); + Expect(mock.binds == 2 && mock.allocations == 2, "successfully bound device buffers stopped being reused"); + std::printf("device bind result=%d retained=%llu invalid retries=%u binds=%u\n", static_cast(result), static_cast(retained), invalid, mock.binds); + } + Expect(mock.liveBytes == 0 && mock.liveBuffers.empty(), "device bind recovery leaked a resource"); + } +} + } int main(int argc, char** argv) { @@ -656,6 +691,7 @@ int main(int argc, char** argv) { AllocationFailure(VK_ERROR_OUT_OF_DEVICE_MEMORY, VK_ERROR_DEVICE_LOST, true); SuccessfulAllocationKeepsPool(); PartialSlabDoesNotRetry(); + DeviceBindingFailures(); HostPlacement(); } catch (const std::exception& error) { std::fprintf(stderr, "FAIL: %s\n", error.what()); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PooledBufferRange.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PooledBufferRange.cpp index f30cddb737..0d90102213 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PooledBufferRange.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PooledBufferRange.cpp @@ -37,6 +37,10 @@ PFN_vkAllocateMemory RealAllocateMemory = nullptr; PFN_vkFreeMemory RealFreeMemory = nullptr; PFN_vkMapMemory RealMapMemory = nullptr; PFN_vkUnmapMemory RealUnmapMemory = nullptr; +PFN_vkBindBufferMemory RealBindBufferMemory = nullptr; +VkResult BindFailure = VK_SUCCESS; +unsigned BindAttempts = 0; +unsigned SuccessfulBinds = 0; struct Allocation { VkDeviceMemory memory = VK_NULL_HANDLE; @@ -110,7 +114,20 @@ VKAPI_ATTR VkResult VKAPI_CALL CountedCreateBuffer(VkDevice device, const VkBuff return RealCreateBuffer(device, info, allocator, buffer); } +VKAPI_ATTR VkResult VKAPI_CALL CountedBindBufferMemory(VkDevice device, VkBuffer buffer, VkDeviceMemory memory, VkDeviceSize offset) { + ++BindAttempts; + if (BindFailure != VK_SUCCESS) { + const auto result = BindFailure; + BindFailure = VK_SUCCESS; + return result; + } + const auto result = RealBindBufferMemory(device, buffer, memory, offset); + if (result == VK_SUCCESS) ++SuccessfulBinds; + return result; +} + VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL CountingDeviceProc(VkDevice device, const char* name) { + if (std::strcmp(name, "vkBindBufferMemory") == 0) return reinterpret_cast(&CountedBindBufferMemory); if (std::strcmp(name, "vkCreateBuffer") == 0) return reinterpret_cast(&CountedCreateBuffer); if (std::strcmp(name, "vkAllocateMemory") == 0) return reinterpret_cast(&CountedAllocateMemory); if (std::strcmp(name, "vkFreeMemory") == 0) return reinterpret_cast(&CountedFreeMemory); @@ -187,6 +204,8 @@ public: RealFreeMemory = reinterpret_cast(RealDeviceProc(context.device, "vkFreeMemory")); RealMapMemory = reinterpret_cast(RealDeviceProc(context.device, "vkMapMemory")); RealUnmapMemory = reinterpret_cast(RealDeviceProc(context.device, "vkUnmapMemory")); + RealBindBufferMemory = reinterpret_cast(RealDeviceProc(context.device, "vkBindBufferMemory")); + Require(RealBindBufferMemory != nullptr, "missing vkBindBufferMemory"); Require(RealAllocateMemory != nullptr && RealFreeMemory != nullptr && RealMapMemory != nullptr && RealUnmapMemory != nullptr, "missing Vulkan memory functions"); context.deviceProc = &CountingDeviceProc; function("vkGetPhysicalDeviceMemoryProperties")(context.physical, &context.memory); @@ -295,6 +314,58 @@ void SlabMetadataFailures(const Context& context) { Require(failures == 0, "slab metadata allocation failure leaked native Vulkan memory or published an unusable block"); } +void DeviceBindingFailures(const Context& context) { + constexpr std::size_t bytes = std::size_t{3} << 20u; + constexpr std::size_t copiedBytes = 256; + constexpr auto usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT; + unsigned failures = 0; + for (const auto result : {VK_ERROR_OUT_OF_HOST_MEMORY, VK_ERROR_OUT_OF_DEVICE_MEMORY}) { + const auto initialBytes = LiveBytes; + { + auto probe = context; + probe.bufferPool = std::make_shared(probe); + AgcDriver::Graphics::Buffer upload(probe, copiedBytes, VK_BUFFER_USAGE_TRANSFER_SRC_BIT); + AgcDriver::Graphics::Buffer readback(probe, copiedBytes, VK_BUFFER_USAGE_TRANSFER_DST_BIT); + const auto before = LiveBytes; + const auto attempts = BindAttempts; + const auto bound = SuccessfulBinds; + BindFailure = result; + bool threw = false; + try { + AgcDriver::Graphics::DeviceBuffer failed(probe, bytes, usage); + } catch (const std::runtime_error& error) { + threw = std::string(error.what()).find("Vulkan result " + std::to_string(result)) != std::string::npos; + } + BindFailure = VK_SUCCESS; + const auto retained = LiveBytes - before; + const bool rejected = threw && BindAttempts == attempts + 1 && SuccessfulBinds == bound; + unsigned invalid = 0; + for (unsigned retry = 0; retry < 3; ++retry) { + AgcDriver::Graphics::DeviceBuffer recovered(probe, bytes, usage); + if (SuccessfulBinds != bound + 1) { + ++invalid; + continue; + } + std::memset(upload.Bytes().data(), 0x21 + retry, copiedBytes); + std::memset(readback.Bytes().data(), 0, copiedBytes); + AgcDriver::Graphics::CommandBatch batch(probe); + AgcDriver::Graphics::RecordMemoryBarrier(probe, batch.Handle(), VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_HOST_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT); + AgcDriver::Graphics::CopyBuffer(probe, batch.Handle(), upload.Handle(), 0, recovered.Handle(), 0, copiedBytes); + AgcDriver::Graphics::RecordMemoryBarrier(probe, batch.Handle(), VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT); + AgcDriver::Graphics::CopyBuffer(probe, batch.Handle(), recovered.Handle(), 0, readback.Handle(), 0, copiedBytes); + AgcDriver::Graphics::RecordMemoryBarrier(probe, batch.Handle(), VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_HOST_READ_BIT); + batch.SubmitAndWait(); + readback.Invalidate(); + Require(std::memcmp(upload.Bytes().data(), readback.Bytes().data(), copiedBytes) == 0, "a recovered device buffer returned incorrect GPU copy data"); + } + if (!rejected || retained != 0 || invalid != 0 || BindAttempts != attempts + 2) ++failures; + std::printf("device bind result=%d retained=%llu invalid retries=%u binds=%u\n", static_cast(result), static_cast(retained), invalid, BindAttempts - attempts); + } + Require(LiveBytes == initialBytes && AllocationErrors == 0, "device bind failure cleanup leaked native Vulkan memory"); + } + Require(failures == 0, "a failed device buffer bind retained or reused an unbound buffer"); +} + void Emit(std::vector& words, std::uint32_t opcode, std::initializer_list operands) { words.push_back(static_cast((operands.size() + 1u) << 16u) | opcode); words.insert(words.end(), operands); @@ -481,6 +552,7 @@ int main() { } SlabMetadataFailures(device->GetContext()); RobustReadPastEnd(device->GetContext()); + DeviceBindingFailures(device->GetContext()); ExactSizes(device->GetContext()); device.reset(); Require(AllocationErrors == 0 && LiveBytes == 0, "native buffer cleanup left an invalid memory state"); From 785c1079794662177df7d5e85530b353e8df0948 Mon Sep 17 00:00:00 2001 From: Ondra97 <47064087+Ondra97@users.noreply.github.com> Date: Sat, 17 Oct 2026 10:55:35 +0200 Subject: [PATCH 0722/1000] test(agc): separate binding and cache pressure regressions --- core/libs/prx/libSceAgcDriver/tests/BufferPool.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/BufferPool.cpp b/core/libs/prx/libSceAgcDriver/tests/BufferPool.cpp index 18332be7ce..a251940693 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BufferPool.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BufferPool.cpp @@ -678,6 +678,7 @@ int main(int argc, char** argv) { SmallDeviceClasses(); SlabMetadataFailure(); SlabMapFailure(); + DeviceBindingFailures(); Slabs(); for (const auto result : {VK_ERROR_OUT_OF_DEVICE_MEMORY, VK_ERROR_OUT_OF_HOST_MEMORY}) { AllocationRecovery(result, false); @@ -691,7 +692,6 @@ int main(int argc, char** argv) { AllocationFailure(VK_ERROR_OUT_OF_DEVICE_MEMORY, VK_ERROR_DEVICE_LOST, true); SuccessfulAllocationKeepsPool(); PartialSlabDoesNotRetry(); - DeviceBindingFailures(); HostPlacement(); } catch (const std::exception& error) { std::fprintf(stderr, "FAIL: %s\n", error.what()); From d951775d1535090487d9fb15e79ed7562be9c422 Mon Sep 17 00:00:00 2001 From: Yossi Yadgar Date: Sat, 17 Oct 2026 15:18:40 +0300 Subject: [PATCH 0723/1000] fix(shader): take 16-bit interpolation to the barycentric emulation without the extension v_interp_p1ll_f16, v_interp_p1lv_f16 and v_interp_p2_f16 read each vertex's parameter and threw on a device without VK_KHR_fragment_shader_barycentric. Without the extension the pixel shader is translated once for fixed-function interpolation and, when that cannot express it, again with the barycentric emulation, whose generated geometry stage passes each vertex's parameter as a flat input. The throw came in the first pass, before the second could run. Without it the 16-bit parameters keep the shader off fixed-function interpolation, so it takes the emulation and reads the halves bit for bit, as v_interp_mov_f32 already does. --- .../recompiler/Translation/src/AttributeInstructions.cpp | 3 --- 1 file changed, 3 deletions(-) diff --git a/core/shader/recompiler/Translation/src/AttributeInstructions.cpp b/core/shader/recompiler/Translation/src/AttributeInstructions.cpp index 44588aa9f7..f06b151231 100644 --- a/core/shader/recompiler/Translation/src/AttributeInstructions.cpp +++ b/core/shader/recompiler/Translation/src/AttributeInstructions.cpp @@ -99,9 +99,6 @@ void TranslationContext::vInterpMovF32(const RdnaInstruction& inst) { } IrF32 TranslationContext::interpolationParameterF16(const RdnaInstruction& inst, std::uint32_t mode) { - if (!fragmentShaderBarycentricEnabled) { - throw std::runtime_error("16-bit interpolation of pixel input " + std::to_string(inst.source1.value) + " requires fragmentShaderBarycentric"); - } if (pixelInput == nullptr || !pixelInput->InputIsFp16(inst.source1.value)) { throw std::runtime_error("pixel input " + std::to_string(inst.source1.value) + " is read with 16-bit interpolation without FP16_INTERP_MODE"); } From f397af4ca65c2b97124e0599176c9cce416313e5 Mon Sep 17 00:00:00 2001 From: Yossi Yadgar Date: Sat, 17 Oct 2026 15:18:41 +0300 Subject: [PATCH 0724/1000] test(agc): check that 16-bit interpolation takes the barycentric emulation Without barycentrics the test shader now recompiles with the emulation active instead of throwing, and with them it does not take it. The offline target declares Float64, which the exact interpolation sums use. --- .../prx/libSceAgcDriver/tests/execution/InterpolationF16.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/InterpolationF16.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/InterpolationF16.cpp index 77dbe795c3..0ba230d638 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/InterpolationF16.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/InterpolationF16.cpp @@ -263,6 +263,8 @@ ShaderRecompiler::SpirvTarget OfflineTarget(bool barycentric) { } void ExpectFailure(std::span code, std::uint32_t input0, bool barycentric, const char* expected, const char* message) { + static constexpr std::array capabilities{spv::CapabilityFloat64}; + target.supportedCapabilities = capabilities; try { static_cast(RecompilePixel(code, 64u, input0, OfflineTarget(barycentric), 0u)); } catch (const std::exception& error) { @@ -281,7 +283,8 @@ void ExpectRejected(std::uint32_t word0, std::uint32_t word1, const char* expect int main() { try { - ExpectFailure(PixelCode, Fp16Input, false, "requires fragmentShaderBarycentric", "16-bit interpolation was accepted without barycentrics"); + Require(RecompilePixel(PixelCode, 64u, Fp16Input, OfflineTarget(false), 0u).barycentricEmulation.active, "16-bit interpolation without barycentrics does not emulate them"); + Require(!RecompilePixel(PixelCode, 64u, Fp16Input, OfflineTarget(true), 0u).barycentricEmulation.active, "16-bit interpolation emulates barycentrics on a device that has them"); ExpectFailure(PixelCode, 0x03000000u, true, "without FP16_INTERP_MODE", "16-bit interpolation of a 32-bit input was accepted"); ExpectFailure(PixelCode, 0x03080420u, true, "passes its vertices through unchanged", "16-bit interpolation of a pass-through input was accepted"); ExpectRejected(0xd75a001bu, 0x0c6a0200u, "interpolation modifiers", "v_interp_p2_f16 accepted an output modifier"); From 5697cea47142e0efc45d26c630736b8f103d551c Mon Sep 17 00:00:00 2001 From: Yossi Yadgar Date: Sat, 17 Oct 2026 15:18:41 +0300 Subject: [PATCH 0725/1000] test(agc): draw 16-bit interpolation through the emulated barycentrics On a device without VK_KHR_fragment_shader_barycentric the draw now builds the emulation's geometry stage and checks the same results in wave64 and wave32 as on a device with the extension. It skips only on a device with neither the extension nor geometry shaders, or without the Float64 capability the exact sums need. --- .../tests/execution/InterpolationF16.cpp | 28 +++++++++++++------ 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/InterpolationF16.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/InterpolationF16.cpp index 0ba230d638..4d8dee9d60 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/InterpolationF16.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/InterpolationF16.cpp @@ -160,10 +160,14 @@ void Draw(AgcDriver::VulkanDevice& device, std::uint32_t waveSize) { const auto vertexResult = ShaderRecompiler::Recompile(vertex); const auto vertexPush = static_cast(vertexResult.pushConstants.size()); const auto pixelResult = RecompilePixel(PixelCode, waveSize, Fp16Input, target, vertexPush); - const std::array shaders{{ - {ShaderStage::Vertex, &vertexResult, 0}, - {ShaderStage::Fragment, &pixelResult, PixelPushOffset(vertexPush, target)} - }}; + Require(pixelResult.barycentricEmulation.active != target.fragmentShaderBarycentricEnabled, "16-bit interpolation must emulate barycentrics exactly when the device lacks them"); + std::vector shaders{{ShaderStage::Vertex, &vertexResult, 0}}; + ShaderRecompiler::RecompileResult geometry; + if (pixelResult.barycentricEmulation.active) { + geometry = ShaderRecompiler::BuildBarycentricGeometryShader(vertexResult, pixelResult, target, device.GeometryLimits()); + shaders.push_back({ShaderStage::Geometry, &geometry, 0}); + } + shaders.push_back({ShaderStage::Fragment, &pixelResult, PixelPushOffset(vertexPush, target)}); AgcDriver::Graphics::State state{}; state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, waveSize, waveSize, std::nullopt, std::nullopt}; @@ -259,12 +263,12 @@ ShaderRecompiler::SpirvTarget OfflineTarget(bool barycentric) { target.spirvVersion = 0x00010300u; target.subgroupSize = 64; target.fragmentShaderBarycentricEnabled = barycentric; + static constexpr std::array capabilities{spv::CapabilityFloat64}; + target.supportedCapabilities = capabilities; return target; } void ExpectFailure(std::span code, std::uint32_t input0, bool barycentric, const char* expected, const char* message) { - static constexpr std::array capabilities{spv::CapabilityFloat64}; - target.supportedCapabilities = capabilities; try { static_cast(RecompilePixel(code, 64u, input0, OfflineTarget(barycentric), 0u)); } catch (const std::exception& error) { @@ -294,12 +298,18 @@ int main() { ExpectRejected(0xd743001du, 0x003e0140u, "not a vector register", "v_interp_p1lv_f16 accepted a scalar P0"); const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; - if (!device->Target().fragmentShaderBarycentricEnabled) { - std::puts("skipped, the device has no fragmentShaderBarycentric"); + if (!device->Target().fragmentShaderBarycentricEnabled && !device->GeometryLimits()) { + std::puts("skipped, the device has neither fragmentShaderBarycentric nor geometryShader"); return VulkanTestSkipped; } for (const auto waveSize : {64u, 32u}) { - Draw(*device, waveSize); + try { + Draw(*device, waveSize); + } catch (const std::exception& error) { + if (std::string(error.what()).find("need the Float64 capability") == std::string::npos) throw; + std::puts("skipped, the device has no Float64 capability for the exact interpolation sums"); + return VulkanTestSkipped; + } Check(waveSize); } std::puts("16-bit interpolation tests passed"); From b1fa1b94f1bcf6e0fdf6d5cf1184b921634a5e0d Mon Sep 17 00:00:00 2001 From: Yossi Yadgar Date: Sat, 17 Oct 2026 15:18:41 +0300 Subject: [PATCH 0726/1000] docs(debt): note how 16-bit interpolation reads its parameters without the extension --- docs/dev/debt/shader.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index 5a9518896e..89fa8f70d7 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -83,7 +83,7 @@ - [NGG mesh entry](../../../core/shader/recompiler/Translation/src/InstructionTranslator.cpp) uses the guest wave size (32 or 64) for wave indices, per-wave vertex and primitive counts, and the workgroup wave count in `s3`. The four-bit wave fields follow Mesa's [get_wave_id_in_tg, get_tgsize and get_thread_id_in_tg](https://chromium.googlesource.com/chromiumos/third_party/mesa/+/refs/heads/stabilize-13982.70.B-chromeos-amd/src/gallium/drivers/radeonsi/gfx10_shader_ngg.c); partial waves and groups exceeding 15 waves throw. Indexed and non-indexed wave32 mesh rendering with 32, 64 and 128 threads was checked by pixel readback on Windows with an NVIDIA RTX 4070 Laptop GPU. The guest register values have not been compared with console hardware. - [Mesh shader grid size](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) (shader recompiler) - every mesh shader loads `NumWorkgroups.x` and declares no vertices when it is 0, which no dispatch produces. The read is a workaround for RADV (Mesa 26.2.4, seen on an RX 9070 XT, GFX12): binding a mesh pipeline whose shader reads neither `NumWorkgroups` nor `DrawIndex` keeps the user SGPR count and draw-id flag of the vertex pipeline bound before it in the same command buffer (`radv_bind_pre_rast_shader` updates them only when the shader has the `AC_UD_VS_BASE_VERTEX_START_INSTANCE` slot), so `radv_emit_userdata_mesh` opens a 3-register `SET_SH_REG` and writes one value, the CP takes the following `DISPATCH_MESH_DIRECT` as register data and the GPU hangs (gfx ring timeout, MODE1 reset). Kena: Bridge of Spirits (PPSA01802) hits it on its layered volume draws. Reading `NumWorkgroups` gives every mesh shader the grid-size slot, so the count and the values match; drop the read once a fixed Mesa is the minimum - [Shader recompilation](../../../core/shader/recompiler/Recompiler.cpp) currently occurs right before it was transferred to Vulkan with caching, but should be moved to the [relinker](../../../core/relinker/main.cpp) stage. For this purpose, [shader/recompiler](../../../core/shader/recompiler) was written completely independently from [libs/prx](../../../core/libs/prx). -- [v_interp_p1ll_f16, v_interp_p1lv_f16 and v_interp_p2_f16](../../../core/shader/recompiler/Translation/src/AttributeInstructions.cpp) read the 16-bit halves of each vertex's parameter through `VK_KHR_fragment_shader_barycentric` and throw without it, as fixed-function interpolation cannot split a 32-bit attribute. P10 and P20 are the vertex differences rounded to f16 to nearest even, which is not confirmed on hardware. v_interp_p1ll_f16 and v_interp_p1lv_f16 round the exact `P10 * I + P0` once to f32 and v_interp_p2_f16 rounds the exact `P20 * J + S2` once to f16, as measured on RDNA2 (#697); the exact sums are computed in f64, so these instructions need the Float64 capability. They follow the shader's float mode, measured on RDNA2 (gfx1035) in all eight combinations of `IEEE_MODE` and kept/flushed f32 and f16 denormals over 768 special and 1,024 random rows. With IEEE=1 a NaN operand is returned quieted; with IEEE=0 it is returned unquieted, except that v_interp_p2_f16's f16 result is always quiet. Flushed f32 denormals flush I/J, v_interp_p2_f16's f32 S2 and the f32 result; flushed f16 denormals flush P0, P10/P20, v_interp_p1lv_f16's f16 S2 and the f16 result. Denormal modes 1 and 2 (flush only inputs or only outputs) and rounding other than to nearest even are not measured and throw. An unknown mode follows IEEE=0 with denormals kept. Lavapipe has no barycentrics, so the draw test is skipped in CI; `agc_driver_interpolation_modes` checks the arithmetic in every mode against the hardware rows on any device with Float64. +- [v_interp_p1ll_f16, v_interp_p1lv_f16 and v_interp_p2_f16](../../../core/shader/recompiler/Translation/src/AttributeInstructions.cpp) read the 16-bit halves of each vertex's parameter through `VK_KHR_fragment_shader_barycentric`, as fixed-function interpolation cannot split a 32-bit attribute. Without the extension the pixel shader takes the [barycentric emulation](../../../core/shader/recompiler/SpirvBackend/src/BarycentricGeometryShader.cpp), whose generated geometry stage passes each vertex's parameter to it as a flat input, so the halves arrive bit for bit; a device without geometry shaders throws when that stage is built. P10 and P20 are the vertex differences rounded to f16 to nearest even, which is not confirmed on hardware. v_interp_p1ll_f16 and v_interp_p1lv_f16 round the exact `P10 * I + P0` once to f32 and v_interp_p2_f16 rounds the exact `P20 * J + S2` once to f16, as measured on RDNA2 (#697); the exact sums are computed in f64, so these instructions need the Float64 capability. They follow the shader's float mode, measured on RDNA2 (gfx1035) in all eight combinations of `IEEE_MODE` and kept/flushed f32 and f16 denormals over 768 special and 1,024 random rows. With IEEE=1 a NaN operand is returned quieted; with IEEE=0 it is returned unquieted, except that v_interp_p2_f16's f16 result is always quiet. Flushed f32 denormals flush I/J, v_interp_p2_f16's f32 S2 and the f32 result; flushed f16 denormals flush P0, P10/P20, v_interp_p1lv_f16's f16 S2 and the f16 result. Denormal modes 1 and 2 (flush only inputs or only outputs) and rounding other than to nearest even are not measured and throw. An unknown mode follows IEEE=0 with denormals kept. Lavapipe has no barycentrics, so the draw test is skipped in CI; `agc_driver_interpolation_modes` checks the arithmetic in every mode against the hardware rows on any device with Float64. - Without `VK_KHR_fragment_shader_barycentric`, a pixel shader whose I/J VGPRs [only feed `v_interp_p1_f32`/`v_interp_p2_f32` of their own pair](../../../core/shader/recompiler/Optimization/src/HostInterpolationChecker.cpp) reads the attribute Vulkan interpolated. Any other read of them or of a raw vertex parameter takes the weights and the three vertex values from a [generated geometry stage](../../../core/shader/recompiler/SpirvBackend/src/BarycentricGeometryShader.cpp), which needs `geometryShader` and a vertex shader before it: tessellation, mesh and rect-list pipelines, primitives other than triangles, more than 32 fragment input locations and interfaces beyond the device's geometry limits throw. - [v_fma_f64 and v_div_fmas_f64](../../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterFloat64.cpp) with a subnormal operand or result round once, as IEEE 754 fusedMultiplyAdd does with f64 denormals kept, measured on RDNA2 (gfx1035) at IEEE=0 and IEEE=1, `v_div_fmas_f64` with VCC clear and set. The f16/f64 denormal field of `FLOAT_MODE` (bits 7:6) is not read: when it flushes, the hardware flushes the operands and the result of `v_fma_f64` (tininess before rounding) and only the result of `v_div_fmas_f64`, while these keep them. Shaders with f64 instructions declare `DenormPreserve` for 64-bit floats only when `shaderDenormPreserveFloat64` is true and Vulkan reports `denormBehaviorIndependence` as `ALL`; other independence modes are not handled by the current backend because they require coupled execution modes. See [VkShaderFloatControlsIndependence](https://docs.vulkan.org/refpages/latest/refpages/source/VkShaderFloatControlsIndependence.html). Elsewhere (including lavapipe), the driver may flush f64 subnormals, and `agc_driver_f64_fma_subnormals` is skipped. - [SINT storage images](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) of 8- and 16-bit components saturate each stored 32-bit value to the component range, as adriadam10 measured for image stores on RDNA2 (gfx1035) in #944 (448 rows match); the PS5's gfx1013 is not measured. A D16 store sign-extends each 16-bit half before that saturation, as measured on RDNA2 (gfx1035) for `image_store d16` to all eight SINT formats with DMASK 0xf, 0x7, 0x5, 0x3, 0x2, 0xa and 0x1 (1792 rows match). From e8627d607b6b411876727ba49c373af4e8c28ef8 Mon Sep 17 00:00:00 2001 From: screamm Date: Mon, 12 Oct 2026 01:07:46 +0200 Subject: [PATCH 0727/1000] feat(libSceContentDelete): add sceContentDeleteById and sceContentDeleteByPath Cult of the Lamb (PPSA06465, #2686) stops at start-up on the first one: FAIL: unresolved ELF import pXJh3aVk8Ks from libSceContentDelete.prx Both exports have been on main without an implementation (#2740); the call itself has not been observed yet. Orbital (cc408367, tools/ida/db_types.json:541-542) and SharpProspero (5dfdb4b0, Interop/Content/ContentDelete.cs:25-42) agree on the signatures: a 64-bit SceContentSearchContentId and a UTF-8 path. The content library is empty on the host, since libSceContentExport exports nothing into it, so no id or path can be found. SharpProspero gives only NoPermission (0x809D5006) and no public source gives the not-found code, so both calls throw with the id or path named once the null path and the initialization are checked, like the "not in the host content library" throws of #3724. sceContentDeleteById moves from guest_content_unimplemented to the new guest_content_delete test. NIDs pXJh3aVk8Ks and qC3QW+AltHs match tools/nid_names.py and the shadPS4 aerolib list. Co-Authored-By: Claude Fable 5.1 (cherry picked from commit 928184824a4acdf36a58287034bff8154c861835) --- core/libs/prx/libSceContentDelete/Export.cpp | 17 ++++--- .../prx/libSceContentExport/CMakeLists.txt | 12 ++--- core/libs/tests/GuestContentDelete.cpp | 49 +++++++++++++++++++ core/libs/tests/GuestContentUnimplemented.cpp | 33 ------------- docs/dev/debt/libSceContentDelete.md | 4 +- 5 files changed, 68 insertions(+), 47 deletions(-) create mode 100644 core/libs/tests/GuestContentDelete.cpp delete mode 100644 core/libs/tests/GuestContentUnimplemented.cpp diff --git a/core/libs/prx/libSceContentDelete/Export.cpp b/core/libs/prx/libSceContentDelete/Export.cpp index 4c80330e30..098239068f 100644 --- a/core/libs/prx/libSceContentDelete/Export.cpp +++ b/core/libs/prx/libSceContentDelete/Export.cpp @@ -8,6 +8,10 @@ namespace { std::atomic g_initialized{false}; + +void RequireInitialized(const char* function) { + if (!g_initialized.load()) throw std::logic_error(std::string(function) + ": not initialized"); +} } extern "C" { @@ -19,14 +23,15 @@ int APS5_VABI sceContentDeleteInitialize(const ContentDeleteInitParam* init_para return 0; } -int APS5_VABI sceContentDeleteByPath(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceContentDeleteByPath(const char* contentPath) { + if (contentPath == nullptr) APS5_INVALID_ARG_EX; + RequireInitialized(__func__); + throw std::logic_error(std::string(__func__) + ": " + contentPath + " is not in the host content library, which is empty, and the console's error for it is unknown"); } -int APS5_VABI sceContentDeleteById(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceContentDeleteById(std::int64_t contentId) { + RequireInitialized(__func__); + throw std::logic_error(std::string(__func__) + ": content id " + std::to_string(contentId) + " is not in the host content library, which is empty, and the console's error for it is unknown"); } int APS5_VABI sceContentDeleteTerminate(void) { diff --git a/core/libs/prx/libSceContentExport/CMakeLists.txt b/core/libs/prx/libSceContentExport/CMakeLists.txt index 1ea0a503ae..4dd9a470cb 100644 --- a/core/libs/prx/libSceContentExport/CMakeLists.txt +++ b/core/libs/prx/libSceContentExport/CMakeLists.txt @@ -13,13 +13,13 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() - add_test_executable(guest_content_unimplemented_tests ${LIBS_INCLUDE_DIR}/tests/GuestContentUnimplemented.cpp) - target_include_directories(guest_content_unimplemented_tests PRIVATE ${LIBS_INCLUDE_DIR}) - target_link_libraries(guest_content_unimplemented_tests PRIVATE libSceContentDelete libc) - configure_windows_unwind(guest_content_unimplemented_tests) - add_test(NAME guest_content_unimplemented COMMAND guest_content_unimplemented_tests) + add_test_executable(guest_content_delete_tests ${LIBS_INCLUDE_DIR}/tests/GuestContentDelete.cpp) + target_include_directories(guest_content_delete_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_content_delete_tests PRIVATE libSceContentDelete libc) + configure_windows_unwind(guest_content_delete_tests) + add_test(NAME guest_content_delete COMMAND guest_content_delete_tests) if(WIN32) - set_tests_properties(guest_content_unimplemented PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(guest_content_delete PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() endif() diff --git a/core/libs/tests/GuestContentDelete.cpp b/core/libs/tests/GuestContentDelete.cpp new file mode 100644 index 0000000000..8a79bb03db --- /dev/null +++ b/core/libs/tests/GuestContentDelete.cpp @@ -0,0 +1,49 @@ +#include "SceTypes.hpp" +#include +#include +#include +#include + +extern "C" int APS5_VABI sceContentDeleteInitialize(const ContentDeleteInitParam* init_param); +extern "C" int APS5_VABI sceContentDeleteTerminate(void); +extern "C" int APS5_VABI sceContentDeleteById(std::int64_t contentId); +extern "C" int APS5_VABI sceContentDeleteByPath(const char* contentPath); + +namespace { + +void Require(bool value) { if (!value) std::abort(); } + +template +std::string LogicErrorMessage(TAction action) { + try { + action(); + } catch (const std::invalid_argument&) { + return "invalid argument"; + } catch (const std::logic_error& error) { + return error.what(); + } + return "returned"; +} + +bool Contains(const std::string& text, const std::string& part) { return text.find(part) != std::string::npos; } + +} + +int main() { + Require(LogicErrorMessage([] { sceContentDeleteById(1); }) == "sceContentDeleteById: not initialized"); + Require(LogicErrorMessage([] { sceContentDeleteByPath("/user/av_contents/photo/a.png"); }) == "sceContentDeleteByPath: not initialized"); + Require(LogicErrorMessage([] { sceContentDeleteByPath(nullptr); }) == "invalid argument"); + + ContentDeleteInitParam param{}; + param.heap_size = 0x10000; + Require(sceContentDeleteInitialize(¶m) == 0); + + Require(LogicErrorMessage([] { sceContentDeleteByPath(nullptr); }) == "invalid argument"); + Require(Contains(LogicErrorMessage([] { sceContentDeleteByPath("/user/av_contents/photo/a.png"); }), "sceContentDeleteByPath: /user/av_contents/photo/a.png is not in the host content library")); + Require(Contains(LogicErrorMessage([] { sceContentDeleteById(0x123456789); }), "sceContentDeleteById: content id 4886718345 is not in the host content library")); + Require(Contains(LogicErrorMessage([] { sceContentDeleteById(-1); }), "sceContentDeleteById: content id -1 is not in the host content library")); + + Require(sceContentDeleteTerminate() == 0); + Require(LogicErrorMessage([] { sceContentDeleteById(1); }) == "sceContentDeleteById: not initialized"); + return 0; +} diff --git a/core/libs/tests/GuestContentUnimplemented.cpp b/core/libs/tests/GuestContentUnimplemented.cpp deleted file mode 100644 index a70891bf8a..0000000000 --- a/core/libs/tests/GuestContentUnimplemented.cpp +++ /dev/null @@ -1,33 +0,0 @@ -#include "prx/libc/include/general/VabiMacros.hpp" - -#include -#include -#include - -extern "C" { -int APS5_VABI sceContentDeleteById(); - -} - -namespace { - -bool ReportsNotImplemented(const char* name, int (APS5_VABI *function)()) { - try { - function(); - } catch (const std::runtime_error& error) { - if (std::string(error.what()) == std::string(name) + " not implemented") return true; - std::fprintf(stderr, "%s threw: %s\n", name, error.what()); - return false; - } - std::fprintf(stderr, "%s returned instead of reporting that it is not implemented\n", name); - return false; -} - -} - -int main() { - bool correct = true; - correct &= ReportsNotImplemented("sceContentDeleteById", sceContentDeleteById); - - return correct ? 0 : 1; -} diff --git a/docs/dev/debt/libSceContentDelete.md b/docs/dev/debt/libSceContentDelete.md index e771fdeec7..8c1cabe7b5 100644 --- a/docs/dev/debt/libSceContentDelete.md +++ b/docs/dev/debt/libSceContentDelete.md @@ -1,5 +1,5 @@ # libSceContentDelete technical debt -### Unknown function info +### Functional -- [sceContentDeleteById](../../../core/libs/prx/libSceContentDelete/Export.cpp) (libSceContentDelete) - imported by PPSA06465, which fails to load without it; added without an implementation because its signature is unknown +- [sceContentDeleteById and sceContentDeleteByPath](../../../core/libs/prx/libSceContentDelete/Export.cpp) (libSceContentDelete) delete from a content library that is empty on the host, as nothing is exported to it, so both throw for every content id and path once the arguments and the initialization are checked: no reference gives the console's error for content that is not in the library ([SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Content/ContentDelete.cs#L25-L42) names only `0x809D5006`, no permission). The signatures, a 64-bit content id and a UTF-8 path, follow [Orbital](https://github.com/AlexAltea/orbital/blob/cc408367da6a6ebbf5ac1ac559fefeadc73b2831/tools/ida/db_types.json#L541-L542) and SharpProspero, which agree. A null path and a call outside `sceContentDeleteInitialize` and `sceContentDeleteTerminate` throw, as the console's errors for them are unknown, and the order of the two checks is unverified. From e040cb7f75314d6f8bae77951de377ca56472a0a Mon Sep 17 00:00:00 2001 From: Romans Pokrovskis Date: Sat, 17 Oct 2026 15:22:26 +0300 Subject: [PATCH 0728/1000] fix(agc): retain the unchanged DCC generation proof --- .../libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp index 0ba4cc82ba..b85b734633 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp @@ -344,7 +344,7 @@ private: for (const auto& range : dccRanges) { Require(GuestMemory::Watched(range.address, range.bytes), "multisampled DCC target reuse requires watched guest memory"); GuestMemory::CollectWritesUncached(range.address, range.bytes); - Require(!GuestMemory::StoredOver(range.address, range.bytes, range.generation), "writes to a resident multisampled DCC target or its metadata are not implemented"); + Require(GuestMemory::UnchangedSince(range.address, range.bytes, range.generation) || !GuestMemory::StoredOver(range.address, range.bytes, range.generation), "writes to a resident multisampled DCC target or its metadata are not implemented"); if (auto* recorder = Recorder::Active(); recorder != nullptr) Require(!recorder->PendingWriteOverlaps(range.address, range.bytes), "pending writes to a resident multisampled DCC target or its metadata are not implemented"); } } From ff9bf22281da6a113a5ed91b19120168d1b60ec8 Mon Sep 17 00:00:00 2001 From: thuanquang Date: Sat, 17 Oct 2026 19:27:52 +0700 Subject: [PATCH 0729/1000] fix(libkernel): end guest thread, fiber and context frame chains at a zero record Unreal Engine walks the frame-pointer chain until it reads a return address of 0 and never checks the frame pointer itself. Guest threads started with whatever rbp the host code held, so the walk ran into host start-up frames (glibc's saved rbp 0x21 in a LEGO Batman core dump, address 8 in Returnal on Windows) and faulted. Fibers ended their chain with a null frame, and makecontext started its function with rbp 0, so the same walker read address 8 there too. Guest threads now start through a thunk that points rbp at a {saved rbp 0, return address 0} record, as the relinker's entry stub does for the main thread; the fiber trampolines push the same record, and the makecontext start function is built with frame pointers so its frame, over the zero return address makecontext stores, ends the chain. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceFiber/Export.cpp | 6 ++ .../prx/libSceLibcInternal/CMakeLists.txt | 1 + core/libs/prx/libkernel/CMakeLists.txt | 12 ++++ .../libs/prx/libkernel/Pthread/src/Thread.cpp | 28 +++++++- core/libs/tests/Fiber.cpp | 14 ++-- core/libs/tests/GuestPthreadFrameChain.cpp | 70 +++++++++++++++++++ core/libs/tests/GuestUserContext.cpp | 20 ++++++ 7 files changed, 145 insertions(+), 6 deletions(-) create mode 100644 core/libs/tests/GuestPthreadFrameChain.cpp diff --git a/core/libs/prx/libSceFiber/Export.cpp b/core/libs/prx/libSceFiber/Export.cpp index ea130e26f6..021ab5269c 100644 --- a/core/libs/prx/libSceFiber/Export.cpp +++ b/core/libs/prx/libSceFiber/Export.cpp @@ -167,6 +167,9 @@ Aps5FiberSwitchStack_nid_no_patch: Aps5FiberTrampoline_nid_no_patch: mov %r12, %rcx and $-16, %rsp + push $0 + push $0 + mov %rsp, %rbp sub $32, %rsp call Aps5FiberMain_nid_no_patch ud2 @@ -236,6 +239,9 @@ asm(".text\n" APS5_ASM_FUNCTION("Aps5FiberTrampoline_nid_no_patch") R"( mov %r12, %rdi and $-16, %rsp + push $0 + push $0 + mov %rsp, %rbp call )" APS5_ASM_SYMBOL("Aps5FiberMain_nid_no_patch") R"( ud2 )" APS5_ASM_FUNCTION_END("Aps5FiberTrampoline_nid_no_patch")); diff --git a/core/libs/prx/libSceLibcInternal/CMakeLists.txt b/core/libs/prx/libSceLibcInternal/CMakeLists.txt index b5dde2fe3f..7a106e74c8 100644 --- a/core/libs/prx/libSceLibcInternal/CMakeLists.txt +++ b/core/libs/prx/libSceLibcInternal/CMakeLists.txt @@ -1,5 +1,6 @@ add_library(libSceLibcInternal SHARED EXCLUDE_FROM_ALL Export.cpp FileTime.cpp Getopt.cpp SignalList.cpp Spawn.cpp UserContext.cpp) target_include_directories(libSceLibcInternal PRIVATE ${LIBS_INCLUDE_DIR}) +set_source_files_properties(UserContext.cpp PROPERTIES COMPILE_OPTIONS -fno-omit-frame-pointer) target_link_libraries(libSceLibcInternal PRIVATE ${CMAKE_DL_LIBS}) target_sources(libSceLibcInternal PRIVATE Directory.cpp) configure_windows_unwind(libSceLibcInternal) diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index fe84fb3f31..0a421bb1d9 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -470,6 +470,17 @@ if(BUILD_TESTING) set_tests_properties(guest_pthread_attr_stack PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_pthread_frame_chain_tests ${LIBS_INCLUDE_DIR}/tests/GuestPthreadFrameChain.cpp) + target_include_directories(guest_pthread_frame_chain_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_compile_options(guest_pthread_frame_chain_tests PRIVATE -fno-omit-frame-pointer) + target_link_libraries(guest_pthread_frame_chain_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_pthread_frame_chain_tests) + add_test(NAME guest_pthread_frame_chain COMMAND guest_pthread_frame_chain_tests) + set_tests_properties(guest_pthread_frame_chain PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_pthread_frame_chain PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_pthread_mutexattr_tests ${LIBS_INCLUDE_DIR}/tests/GuestPthreadMutexattr.cpp) target_include_directories(guest_pthread_mutexattr_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_pthread_mutexattr_tests PRIVATE libkernel libc) @@ -707,6 +718,7 @@ if(BUILD_TESTING) endif() add_test_executable(guest_user_context_tests ${LIBS_INCLUDE_DIR}/tests/GuestUserContext.cpp) target_include_directories(guest_user_context_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_compile_options(guest_user_context_tests PRIVATE -fno-omit-frame-pointer) target_link_libraries(guest_user_context_tests PRIVATE libSceLibcInternal libkernel libc) configure_windows_unwind(guest_user_context_tests) add_test(NAME guest_user_context COMMAND guest_user_context_tests) diff --git a/core/libs/prx/libkernel/Pthread/src/Thread.cpp b/core/libs/prx/libkernel/Pthread/src/Thread.cpp index d453f09671..58739adcea 100644 --- a/core/libs/prx/libkernel/Pthread/src/Thread.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Thread.cpp @@ -3,6 +3,7 @@ #include "../include/WindowsThreadLocal.hpp" #include "../include/Cancel.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libc/include/general/AsmMacros.hpp" #include "prx/libkernel/KernelErrors.hpp" #include "prx/libc/include/CpuTopology.hpp" #include "prx/libkernel/DirectMemory/DirectMemory.hpp" @@ -523,6 +524,31 @@ void ThreadCancel::EndWait() { BeginWait(nullptr); } +extern "C" void* APS5_VABI Aps5CallGuestEntry_nid_no_patch(PthreadEntry entry, void* arg); + +asm(".text\n" + APS5_ASM_FUNCTION("Aps5CallGuestEntry_nid_no_patch") R"( + .cfi_startproc + push %rbp + .cfi_adjust_cfa_offset 8 + .cfi_rel_offset %rbp, 0 + push $0 + .cfi_adjust_cfa_offset 8 + push $0 + .cfi_adjust_cfa_offset 8 + mov %rsp, %rbp + mov %rdi, %rax + mov %rsi, %rdi + call *%rax + add $16, %rsp + .cfi_adjust_cfa_offset -16 + pop %rbp + .cfi_adjust_cfa_offset -8 + .cfi_restore %rbp + ret + .cfi_endproc +)" APS5_ASM_FUNCTION_END("Aps5CallGuestEntry_nid_no_patch")); + static void RunThread(std::unique_ptr args) { APS5_LOG_OUT("RunThread entry=0x%llx arg=%p self=%p", static_cast(reinterpret_cast(args->entry)), args->arg, static_cast(args->self)); const auto entry = args->entry; @@ -539,7 +565,7 @@ static void RunThread(std::unique_ptr args) { #ifndef _WIN32 pthread_sigmask(SIG_SETMASK, &signalMask, nullptr); #endif - finishThread(self, entry(arg)); + finishThread(self, Aps5CallGuestEntry_nid_no_patch(entry, arg)); CurrentGuestState().current = nullptr; } diff --git a/core/libs/tests/Fiber.cpp b/core/libs/tests/Fiber.cpp index 9e305c3080..46ffffffb9 100644 --- a/core/libs/tests/Fiber.cpp +++ b/core/libs/tests/Fiber.cpp @@ -180,13 +180,17 @@ void APS5_VABI FrameChainEntry(std::uint64_t, std::uint64_t) { auto* frame = static_cast(__builtin_frame_address(0)); const auto low = reinterpret_cast(frame); const auto high = low + (1u << 20); - for (int depth = 0; depth < 100 && frame != nullptr; ++depth) { + for (int depth = 0; depth < 100; ++depth) { const auto address = reinterpret_cast(frame); Require(address >= low && address + 2 * sizeof(std::uintptr_t) <= high, "frame chain stays on the fiber's stack"); - frame = reinterpret_cast(frame[0]); - Require(frame == nullptr || reinterpret_cast(frame) > address, "frame chain climbs the stack"); + if (frame[1] == 0) { + g_chainEnded = frame[0] == 0; + break; + } + auto* next = reinterpret_cast(frame[0]); + Require(reinterpret_cast(next) > address, "frame chain climbs the stack to a frame whose return address is 0"); + frame = next; } - g_chainEnded = frame == nullptr; std::uint64_t received = 0; for (;;) sceFiberReturnToThread(0, &received); } @@ -207,7 +211,7 @@ void RunFrameChain() { auto* fiber = reinterpret_cast(&g_chainStorage); Require(_sceFiberInitializeImpl_nid_postfix(fiber, "chain", FrameChainEntry, 0, g_chainContext.data(), g_chainContext.size(), nullptr, 0) == 0, "initialize chain"); Require(sceFiberRun_nid_postfix(fiber, 0, nullptr) == 0, "run chain"); - Require(g_chainEnded, "frame chain of a fiber ends with a null frame"); + Require(g_chainEnded, "frame chain of a fiber ends at a frame whose saved frame pointer and return address are 0"); Require(sceFiberFinalize(fiber) == 0, "finalize chain"); } diff --git a/core/libs/tests/GuestPthreadFrameChain.cpp b/core/libs/tests/GuestPthreadFrameChain.cpp new file mode 100644 index 0000000000..d9c491090f --- /dev/null +++ b/core/libs/tests/GuestPthreadFrameChain.cpp @@ -0,0 +1,70 @@ +#include "SceTypes.hpp" +#include +#include +#include + +extern "C" { +int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, PthreadEntry entry, void* arg, const char* name); +int APS5_VABI scePthreadJoin(Pthread thread, void** retval); +} + +static constexpr int SCE_OK = 0; +static constexpr std::uintptr_t ChainReach = std::uintptr_t{64} << 10; + +static void Require(bool value, const char* what) { + if (value) return; + std::fprintf(stderr, "guest pthread frame chain: %s\n", what); + std::abort(); +} + +struct Walk { + int frames = 0; + bool leftStack = false; + bool ended = false; + bool endedOnZeroRecord = false; +}; + +static Walk g_walk; + +[[gnu::noinline]] static void WalkFrameChain() { + auto* frame = static_cast(__builtin_frame_address(0)); + const auto low = reinterpret_cast(frame); + for (int depth = 0; depth < 100; ++depth) { + const auto address = reinterpret_cast(frame); + if (address < low || address - low > ChainReach) { + g_walk.leftStack = true; + return; + } + ++g_walk.frames; + if (frame[1] == 0) { + g_walk.ended = true; + g_walk.endedOnZeroRecord = frame[0] == 0; + return; + } + auto* next = reinterpret_cast(frame[0]); + if (reinterpret_cast(next) <= address) { + g_walk.leftStack = true; + return; + } + frame = next; + } +} + +static void* APS5_VABI WalkingEntry(void* arg) { + WalkFrameChain(); + return arg; +} + +int main() { + Pthread thread = nullptr; + Require(scePthreadCreate(&thread, nullptr, WalkingEntry, &g_walk, "frame chain") == SCE_OK, "create a guest thread"); + void* result = nullptr; + Require(scePthreadJoin(thread, &result) == SCE_OK, "join the guest thread"); + Require(result == &g_walk, "the guest entry gets its argument and its return value reaches scePthreadJoin"); + Require(!g_walk.leftStack, "the frame chain of a guest thread stays on its stack and climbs it"); + Require(g_walk.ended, "the frame chain of a guest thread ends at a frame whose return address is 0"); + Require(g_walk.endedOnZeroRecord, "the last frame of a guest thread's chain also has a saved frame pointer of 0"); + Require(g_walk.frames == 3, "the chain is the walker's frame, the guest entry's frame and the zero record"); + std::puts("guest pthread frame chain tests passed"); + return 0; +} diff --git a/core/libs/tests/GuestUserContext.cpp b/core/libs/tests/GuestUserContext.cpp index 304b77b34d..c616e2260d 100644 --- a/core/libs/tests/GuestUserContext.cpp +++ b/core/libs/tests/GuestUserContext.cpp @@ -149,6 +149,7 @@ Stack g_jobStack; Stack g_exitStack; std::uint64_t g_arguments[6] = {}; int g_entries = 0; +bool g_contextChainEnded = false; Fibre g_dispatcher = {}; Fibre g_job = {}; int g_jobRuns = 0; @@ -193,6 +194,23 @@ void ThrowAndCatch() { Require(caught, "a host exception is caught on a context stack"); } +[[gnu::noinline]] void WalkContextChain() { + auto* frame = static_cast(__builtin_frame_address(0)); + const auto low = reinterpret_cast(g_contextStack.bytes); + const auto high = low + StackBytes; + for (int depth = 0; depth < 100; ++depth) { + const auto address = reinterpret_cast(frame); + if (address < low || address + 2 * sizeof(std::uintptr_t) > high) return; + if (frame[1] == 0) { + g_contextChainEnded = frame[0] == 0; + return; + } + auto* next = reinterpret_cast(frame[0]); + if (reinterpret_cast(next) <= address) return; + frame = next; + } +} + [[gnu::noinline]] void GrowStack(int depth) { volatile unsigned char page[16 * 1024]; page[0] = static_cast(depth); @@ -341,6 +359,7 @@ extern "C" void APS5_VABI ContextBody(std::uint64_t first, std::uint64_t second, g_arguments[4] = fifth; g_arguments[5] = sixth; CheckStackBounds(&g_contextStack, "the stack bounds follow setcontext onto the context stack"); + WalkContextChain(); ThrowAndCatch(); ++g_entries; } @@ -401,6 +420,7 @@ void CheckMakecontext() { Require(false, "setcontext into the made context returned"); } Require(g_entries == 1, "the made context runs once and resumes uc_link"); + Require(g_contextChainEnded, "the frame chain of a made context ends on its stack at a frame whose saved frame pointer and return address are 0"); for (unsigned index = 0; index < 6; ++index) Require(g_arguments[index] == 0x8000000000000001ull + index, "makecontext passes six 64-bit arguments"); const auto stackLow = reinterpret_cast(g_contextStack.bytes); Require(EntryStackPointer > stackLow && EntryStackPointer < stackLow + StackBytes, "the made context runs on its own stack"); From 2b9c131e0cd8a7a1de34eb3e73df14eae548a8aa Mon Sep 17 00:00:00 2001 From: Alexius0815 <79040541+Alexius0815@users.noreply.github.com> Date: Sat, 17 Oct 2026 14:24:53 +0200 Subject: [PATCH 0730/1000] fix(shader): forward gl_ClipDistance through the generated rect-list stages Since #3878 every vertex program that exports a position also writes gl_ClipDistance for the all-zero position distance. The tessellation stages generated for rect lists declared only gl_Position in gl_in, so their input interface no longer matched the vertex outputs; on MoltenVK the control stage then reads all-zero positions and every rect-list draw stops with "rect-list requires finite nondegenerate axis-aligned positions" (reported in #1). The control and evaluation stages now declare the vertex program's gl_ClipDistance array in their per-vertex blocks and forward it. The generated fourth corner gets the clip distances interpolated with the same weights as its position, so the all-zero position clip keeps working for rect lists. Validation accepts gl_ClipDistance as a tessellation input and as a tessellation-control output. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01QXdkh9PHBCPqGcGQPeikAe --- .../Graphics/src/ShaderValidation.cpp | 9 ++-- .../SpirvBackend/SpirvClipDistances.hpp | 46 +++++++++++++++++++ .../SpirvBackend/src/RectListShaders.cpp | 38 +++++++++++++-- 3 files changed, 85 insertions(+), 8 deletions(-) create mode 100644 core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvClipDistances.hpp diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp index f2c1970381..34bd3349af 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp @@ -115,7 +115,7 @@ struct Module { return; } // One gl_ClipDistance array holds the guest clip distances and the recompiler's all-zero position distance. - if (!input && ((value == spv::BuiltInClipDistance && (vertex || stage == Stage::TessellationEvaluation || stage == Stage::Mesh)) || (value == spv::BuiltInCullDistance && (vertex || stage == Stage::Mesh)))) { + if (!input && ((value == spv::BuiltInClipDistance && (vertex || stage == Stage::TessellationControl || stage == Stage::TessellationEvaluation || stage == Stage::Mesh)) || (value == spv::BuiltInCullDistance && (vertex || stage == Stage::Mesh)))) { const auto signature = Signature(type); auto& count = value == spv::BuiltInClipDistance ? clipDistances : cullDistances; Require(count == 0 && signature.size() == 6 && signature.starts_with("f32[") && signature[4] >= '1' && signature[4] <= '8' && signature[5] == ']', "invalid or duplicate clip or cull distance output"); @@ -131,7 +131,8 @@ struct Module { return; } if (stage == Stage::TessellationControl || stage == Stage::TessellationEvaluation) { - Require(input && ((value == spv::BuiltInPosition && signature == "f32x4") || (value == spv::BuiltInTessCoord && stage == Stage::TessellationEvaluation && signature == "f32x3") || ((value == spv::BuiltInInvocationId || value == spv::BuiltInPrimitiveId || value == spv::BuiltInPatchVertices) && (signature == "i32" || signature == "u32"))), "unsupported tessellation built-in"); + const bool clipInput = value == spv::BuiltInClipDistance && signature.size() == 6 && signature.starts_with("f32[") && signature[4] >= '1' && signature[4] <= '8' && signature[5] == ']'; + Require(input && (clipInput || (value == spv::BuiltInPosition && signature == "f32x4") || (value == spv::BuiltInTessCoord && stage == Stage::TessellationEvaluation && signature == "f32x3") || ((value == spv::BuiltInInvocationId || value == spv::BuiltInPrimitiveId || value == spv::BuiltInPatchVertices) && (signature == "i32" || signature == "u32"))), "unsupported tessellation built-in"); return; } if (!input && value == spv::BuiltInPrimitiveLineIndicesEXT) primitiveIndices = 2u; @@ -252,7 +253,7 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi fragment && capability == spv::CapabilityStencilExportEXT; - if ((vertex || evaluation || mesh) && (capability == spv::CapabilityClipDistance || capability == spv::CapabilityCullDistance)) { + if ((vertex || control || evaluation || mesh) && (capability == spv::CapabilityClipDistance || capability == spv::CapabilityCullDistance)) { char word[16]; std::snprintf(word, sizeof(word), "0x%08x", state.paClVsOutCntl); Require(capability == spv::CapabilityClipDistance ? shaderClipDistance : (vertex || mesh) && shaderCullDistance, std::string("vertex clip or cull distance needs shaderClipDistance or shaderCullDistance, which the device lacks (PA_CL_VS_OUT_CNTL=") + word + ")"); @@ -264,7 +265,7 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi capability == spv::CapabilityImage1D || capability == spv::CapabilityImageGatherExtended || capability == spv::CapabilityImageQuery || - ((vertex || evaluation || mesh) && shaderClipDistance && capability == spv::CapabilityClipDistance) || + ((vertex || control || evaluation || mesh) && shaderClipDistance && capability == spv::CapabilityClipDistance) || (vertex && capability == spv::CapabilityDrawParameters) || (fragment && geometryShader && capability == spv::CapabilityGeometry) || (fragment && sampleRateShading && capability == spv::CapabilitySampleRateShading) || diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvClipDistances.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvClipDistances.hpp new file mode 100644 index 0000000000..2d915b38eb --- /dev/null +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvClipDistances.hpp @@ -0,0 +1,46 @@ +#ifndef CORE_SHADER_RECOMPILIER_SPIRVBACKEND_INCLUDE_SPIRVBACKEND_SPIRVCLIPDISTANCES_HPP +#define CORE_SHADER_RECOMPILIER_SPIRVBACKEND_INCLUDE_SPIRVBACKEND_SPIRVCLIPDISTANCES_HPP + +#include +#include +#include +#include +#include +#include +#include + +namespace ShaderRecompiler { + +[[nodiscard]] inline std::uint32_t VertexClipDistanceCount(std::span words) { + std::set builtins; + std::map pointees; + std::map arrays; + std::map constants; + std::map outputs; + for (std::size_t i = 5; i < words.size();) { + const auto count = words[i] >> 16u; + const auto opcode = words[i] & 0xffffu; + if (count == 0u || i + count > words.size()) throw std::runtime_error("Clip distances: malformed vertex SPIR-V"); + const auto* word = words.data() + i; + if (opcode == spv::OpDecorate && count == 4u && word[2] == spv::DecorationBuiltIn && word[3] == spv::BuiltInClipDistance) builtins.insert(word[1]); + if (opcode == spv::OpTypePointer && count == 4u) pointees[word[1]] = word[3]; + if (opcode == spv::OpTypeArray && count == 4u) arrays[word[1]] = word[3]; + if (opcode == spv::OpConstant && count == 4u) constants[word[2]] = word[3]; + if (opcode == spv::OpVariable && count >= 4u && word[3] == spv::StorageClassOutput) outputs[word[2]] = word[1]; + i += count; + } + for (const auto variable : builtins) { + const auto output = outputs.find(variable); + if (output == outputs.end()) continue; + const auto pointee = pointees.find(output->second); + const auto array = pointee == pointees.end() ? arrays.end() : arrays.find(pointee->second); + const auto length = array == arrays.end() ? constants.end() : constants.find(array->second); + if (length == constants.end() || length->second == 0u || length->second > 8u) throw std::runtime_error("Clip distances: the vertex gl_ClipDistance output is not an array of one to eight elements"); + return length->second; + } + return 0u; +} + +} + +#endif diff --git a/core/shader/recompiler/SpirvBackend/src/RectListShaders.cpp b/core/shader/recompiler/SpirvBackend/src/RectListShaders.cpp index 59757fa18c..7dad183575 100644 --- a/core/shader/recompiler/SpirvBackend/src/RectListShaders.cpp +++ b/core/shader/recompiler/SpirvBackend/src/RectListShaders.cpp @@ -1,6 +1,7 @@ #include "Recompiler.hpp" #include "BdaAbi.hpp" #include "RuntimeAbi.hpp" +#include "SpirvBackend/SpirvClipDistances.hpp" #include "SpirvBackend/SpirvModule.hpp" #if ANYPS5_ENABLE_SPIRV_TOOLS #include "SpirvBackend/SpirvOptimizer.hpp" @@ -31,7 +32,7 @@ void require(bool condition, const char* message) { class RectListEmitter { public: - RectListEmitter(const std::vector& parameters, spv::ExecutionModel model, std::uint32_t version, std::uint32_t faultBinding) : builder(version), faultBinding(faultBinding), version(version), parameters(parameters) { + RectListEmitter(const std::vector& parameters, spv::ExecutionModel model, std::uint32_t version, std::uint32_t faultBinding, std::uint32_t clipDistances) : builder(version), faultBinding(faultBinding), version(version), clipDistances(clipDistances), parameters(parameters) { builder.AddMemoryModel(spv::AddressingModelLogical, spv::MemoryModelGLSL450); voidType = type(spv::OpTypeVoid); @@ -41,7 +42,14 @@ public: vec4FloatType = type(spv::OpTypeVector, floatType, 4u); functionType = type(spv::OpTypeFunction, voidType); - perVertexType = builder.DecoratedType(spv::OpTypeStruct, {{spv::OpMemberDecorate, {0u, spv::DecorationBuiltIn, spv::BuiltInPosition}}, {spv::OpMemberDecorate, {0u, spv::DecorationInvariant}}, {spv::OpDecorate, {spv::DecorationBlock}}}, vec4FloatType); + if (clipDistances != 0u) { + clipArrayType = array(floatType, clipDistances); + perVertexType = builder.DecoratedType(spv::OpTypeStruct, {{spv::OpMemberDecorate, {0u, spv::DecorationBuiltIn, spv::BuiltInPosition}}, {spv::OpMemberDecorate, {0u, spv::DecorationInvariant}}, {spv::OpMemberDecorate, {1u, spv::DecorationBuiltIn, spv::BuiltInClipDistance}}, {spv::OpDecorate, {spv::DecorationBlock}}}, vec4FloatType, clipArrayType); + ptrInputClip = pointer(spv::StorageClassInput, clipArrayType); + ptrOutputClip = pointer(spv::StorageClassOutput, clipArrayType); + } else { + perVertexType = builder.DecoratedType(spv::OpTypeStruct, {{spv::OpMemberDecorate, {0u, spv::DecorationBuiltIn, spv::BuiltInPosition}}, {spv::OpMemberDecorate, {0u, spv::DecorationInvariant}}, {spv::OpDecorate, {spv::DecorationBlock}}}, vec4FloatType); + } ptrInputVec4Float = pointer(spv::StorageClassInput, vec4FloatType); ptrOutputVec4Float = pointer(spv::StorageClassOutput, vec4FloatType); @@ -116,6 +124,20 @@ public: const auto position = result(spv::OpSelect, vec4FloatType, isFourth, position3, load(vec4FloatType, access(ptrInputVec4Float, glIn, index, intConstant(0)))); store(access(ptrOutputVec4Float, glOut, invocation, intConstant(0)), position); + if (clipDistances != 0u) { + std::array clips {}; + for (std::uint32_t i = 0; i < clips.size(); i++) clips[i] = load(clipArrayType, access(ptrInputClip, glIn, intConstant(i), intConstant(1))); + const auto own = load(clipArrayType, access(ptrInputClip, glIn, index, intConstant(1))); + std::vector values; + for (std::uint32_t k = 0; k < clipDistances; k++) { + auto sum = constant(floatType, 0); + for (std::uint32_t i = 0; i < clips.size(); i++) sum = result(spv::OpFAdd, floatType, sum, result(spv::OpFMul, floatType, result(spv::OpCompositeExtract, floatType, clips[i], k), barycentric[i])); + values.push_back(result(spv::OpSelect, floatType, fourth, sum, result(spv::OpCompositeExtract, floatType, own, k))); + } + const auto clip = builder.AllocateId(); + builder.AddFunction(spv::OpCompositeConstruct, clipArrayType, clip, std::span(values)); + store(access(ptrOutputClip, glOut, invocation, intConstant(1)), clip); + } for (std::uint32_t i = 0; i < parameters.size(); i++) { if (parameters[i].missing) { @@ -149,6 +171,7 @@ public: const auto position = load(vec4FloatType, access(ptrInputVec4Float, glIn, index, intConstant(0))); store(access(ptrOutputVec4Float, glOut, intConstant(0)), position); + if (clipDistances != 0u) store(access(ptrOutputClip, glOut, intConstant(1)), load(clipArrayType, access(ptrInputClip, glIn, index, intConstant(1)))); for (std::uint32_t i = 0; i < parameters.size(); i++) { store(outputs[i], load(vec4FloatType, access(ptrInputVec4Float, inputs[i], index))); } @@ -271,6 +294,7 @@ private: void defineEntry(spv::ExecutionModel model) { builder.EmitCapability(spv::CapabilityShader); builder.EmitCapability(spv::CapabilityTessellation); + if (clipDistances != 0u) builder.EmitCapability(spv::CapabilityClipDistance); main = result(spv::OpFunction, voidType, spv::FunctionControlMaskNone, functionType); if (model == spv::ExecutionModelTessellationControl) { builder.AddExecutionMode(main, spv::ExecutionModeOutputVertices, 4u); @@ -343,6 +367,7 @@ private: SpirvModule builder; std::uint32_t faultBinding; std::uint32_t version; + std::uint32_t clipDistances; std::uint32_t fault = 0; const std::vector& parameters; std::vector interfaces; @@ -360,6 +385,9 @@ private: std::uint32_t vec4FloatType = 0; std::uint32_t functionType = 0; std::uint32_t perVertexType = 0; + std::uint32_t clipArrayType = 0; + std::uint32_t ptrInputClip = 0; + std::uint32_t ptrOutputClip = 0; std::uint32_t ptrInputFloat = 0; std::uint32_t ptrInputVec4Float = 0; std::uint32_t ptrOutputFloat = 0; @@ -394,9 +422,11 @@ RectListShaders BuildRectListShaders(const RecompileResult& vertex, const Recomp } const auto components = static_cast((parameters.size() + 1) * 4); require(limits.maxPatchSize >= 4 && components <= limits.maxControlPerVertexInputComponents && components <= limits.maxControlPerVertexOutputComponents && components <= limits.maxEvaluationInputComponents && components <= limits.maxEvaluationOutputComponents && limits.maxControlPerPatchOutputComponents >= 6 && components * 4 + 6 <= limits.maxControlTotalOutputComponents, "tessellation interface exceeds device limits"); + const auto clipDistances = VertexClipDistanceCount(vertex.spirv.Words()); + require(clipDistances == 0u || std::find(target.supportedCapabilities.begin(), target.supportedCapabilities.end(), spv::CapabilityClipDistance) != target.supportedCapabilities.end(), "the vertex clip distances need shaderClipDistance"); const auto faultBinding = RuntimeAbi::BindingNumber(RuntimeAbi::Stage::TessellationControl, RuntimeAbi::Binding::FaultBuffer); - RectListEmitter control(parameters, spv::ExecutionModelTessellationControl, target.spirvVersion, faultBinding); - RectListEmitter evaluation(parameters, spv::ExecutionModelTessellationEvaluation, target.spirvVersion, faultBinding); + RectListEmitter control(parameters, spv::ExecutionModelTessellationControl, target.spirvVersion, faultBinding, clipDistances); + RectListEmitter evaluation(parameters, spv::ExecutionModelTessellationEvaluation, target.spirvVersion, faultBinding, clipDistances); RectListShaders shaders; shaders.control.spirv = control.EmitControl(); shaders.control.bindings.push_back({DescriptorKind::StorageBuffer, DescriptorRole::FaultBuffer, 0, faultBinding, 1, {}}); From a276d2f7185b88c6b049ec8e496573408fb45600 Mon Sep 17 00:00:00 2001 From: pushkarreddyy <212133856+pushkarreddyy@users.noreply.github.com> Date: Sat, 17 Oct 2026 18:14:12 +0530 Subject: [PATCH 0731/1000] feat(libs): declare missing RUDP and signaling imports for PPSA31327 --- .../prx/libSceNpSessionSignaling/Export.cpp | 10 ++++++++++ core/libs/prx/libSceRudp/Export.cpp | 20 +++++++++++++++++++ docs/dev/debt/libSceNpSessionSignaling.md | 2 ++ docs/dev/debt/libSceRudp.md | 2 ++ 4 files changed, 34 insertions(+) diff --git a/core/libs/prx/libSceNpSessionSignaling/Export.cpp b/core/libs/prx/libSceNpSessionSignaling/Export.cpp index d3086ee40a..e75842f866 100644 --- a/core/libs/prx/libSceNpSessionSignaling/Export.cpp +++ b/core/libs/prx/libSceNpSessionSignaling/Export.cpp @@ -99,4 +99,14 @@ int APS5_VABI sceNpSessionSignalingGetGroupFromSessionId(void) { return 0; } +int APS5_VABI sceNpSessionSignalingGetGroupInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSessionSignalingGetConnectionFromNetAddress2() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceRudp/Export.cpp b/core/libs/prx/libSceRudp/Export.cpp index 2bf31d06e5..0dcf019ce6 100644 --- a/core/libs/prx/libSceRudp/Export.cpp +++ b/core/libs/prx/libSceRudp/Export.cpp @@ -286,4 +286,24 @@ int APS5_VABI sceRudpGetMaxSegmentSize(uint16_t* size) { return 0; } +int APS5_VABI sceRudpPollCreate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRudpPollControl() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRudpPollWait() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRudpPollDestroy() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/docs/dev/debt/libSceNpSessionSignaling.md b/docs/dev/debt/libSceNpSessionSignaling.md index 9f9b7ad514..c23308b9fe 100644 --- a/docs/dev/debt/libSceNpSessionSignaling.md +++ b/docs/dev/debt/libSceNpSessionSignaling.md @@ -6,3 +6,5 @@ - [sceNpSessionSignalingGetMemoryInfo](../../../core/libs/prx/libSceNpSessionSignaling/Export.cpp) (libSceNpSessionSignaling) - unknown signature - [sceNpSessionSignalingGetConnectionStatistics](../../../core/libs/prx/libSceNpSessionSignaling/Export.cpp) (libSceNpSessionSignaling) - unknown signature - [sceNpSessionSignalingGetConnectionStatus](../../../core/libs/prx/libSceNpSessionSignaling/Export.cpp) (libSceNpSessionSignaling) - `(int32 context id, int32 connection id, int32* status, SceNetInAddr* peer address, uint16* peer port)` is ASTRO BOT's (PPSA21564 01.007.000) call at 0x30622B: the context id from a 32-bit field, the connection id, three stack outputs, a negative result taken as an error, then the address passed to `sceNetInetNtop(AF_INET, ...)` and the port to `sceNetNtohs`. It matches PS4's `sceNpSignalingGetConnectionStatus`. The title reaches it only for a connection id from the signaling library, which offline never hands one out. No session ever activates offline, so every connection id answers `CONN_NOT_FOUND` (0x8055330C, from [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/64d198190fa05d37cbb1fa29c50f967c376fce55/src/core/libraries/np/np_error.h)) without writing the outputs, and a null status answers `INVALID_ARGUMENT`. The context id is not checked, as contexts are not tracked. The console's answer for an unknown context or connection is unverified +- PPSA31327 imports declared without parameters, signatures unknown: [sceNpSessionSignalingGetGroupInfo, sceNpSessionSignalingGetConnectionFromNetAddress2](../../../core/libs/prx/libSceNpSessionSignaling/Export.cpp) (libSceNpSessionSignaling), imported by Yakuza 0 Director's Cut and not called before the main menu. + diff --git a/docs/dev/debt/libSceRudp.md b/docs/dev/debt/libSceRudp.md index f2e4d7383b..38a4add132 100644 --- a/docs/dev/debt/libSceRudp.md +++ b/docs/dev/debt/libSceRudp.md @@ -10,3 +10,5 @@ - [sceRudpGetStatus](../../../core/libs/prx/libSceRudp/Export.cpp) - PPSA01521 passes an output pointer and byte count `0xF8`. Measured on a PS5 (firmware 13.60): the status is 0xF8 bytes; before `sceRudpInit` every call returns `RUDP_ERROR_NOT_INITIALIZED`, and after it a null pointer or a byte count of 0 or above 0xF8 returns `RUDP_ERROR_INVALID_ARGUMENT`. The layout matches `SceRudpStatus` in [Vita3K](https://github.com/AndnixSH/Vita3K-PS-Vita-OS-Fork/blob/master/vita3k/modules/SceRudp/SceLibRudp.cpp) with `currentContexts` at 0x64 and `allocs`, `frees`, `memCurrent` and `memPeak` at 0xC8 to 0xD4. Only `currentContexts` is reported: the traffic counters stay 0 because nothing is sent, and the allocator counters stay 0 because the library does not allocate from the title's pool (the console reports 6 allocations and 0x1260 bytes right after `sceRudpInit`, and 4 more allocations and 0x3F0 bytes per context). The console also writes `currentContexts` and the allocator counters when the byte count is smaller than their offset; only the requested bytes are written here. - [sceRudpGetMaxSegmentSize](../../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp) - measured on a PS5 (firmware 13.60) in #3073: before `sceRudpInit` it returns `RUDP_ERROR_NOT_INITIALIZED` and leaves the value untouched, also for a null pointer; after it a null pointer returns `RUDP_ERROR_INVALID_ARGUMENT` and a valid one gets 1410 with result 0. The library keeps no configurable segment size (there is no `sceRudpSetMaxSegmentSize`), so 1410 is always reported. The `uint16_t*` parameter is inferred: the measurement does not show how many bytes the console writes, PPSA03490 passes a pointer to a 16-bit value and [RPCS3](https://github.com/RPCS3/rpcs3/blob/master/rpcs3/Emu/Cell/Modules/cellRudp.cpp) declares the PS3 `cellRudpSetMaxSegmentSize(u16 mss)`, so only 16 bits are written - PPSA06888 imports declared without parameters, signatures unknown: [sceRudpEnableInternalIOThread2, sceRudpGetOption, sceRudpGetRemoteInfo](../../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp), imported by its bundled `Media/Plugins/SceRudp.prx` and not called during start-up. Names from the NID table of [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/rudp/rudp.cpp), which declares them without parameters too +- PPSA31327 imports declared without parameters, signatures unknown: [sceRudpPollCreate, sceRudpPollControl, sceRudpPollWait, sceRudpPollDestroy](../../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp), imported by Yakuza 0 Director's Cut and not called before the main menu. Names from the NID table of [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/rudp/rudp.cpp) + From 3e1b7be111a17f08b92debae5c02a107ff79349b Mon Sep 17 00:00:00 2001 From: JosepBove Date: Wed, 14 Oct 2026 19:20:55 +0200 Subject: [PATCH 0732/1000] fix(agc): clear the host depth image on a fill of a depth or stencil plane A fill that covers a depth surface's whole stencil plane with four equal bytes, or its whole depth plane with a value in 0.0-1.0, now reaches the host image through the pending fast clear the next time the surface is bound, sampled or stored to. An HTILE fill or HTILE clear kernel noted afterwards replaces the value with DB_DEPTH_CLEAR/DB_STENCIL_CLEAR. NoteDepthMetadataFill only reacted to fills of DB_HTILE_DATA_BASE, so the Cohtml UI of Heretic + Hexen (PPSA26525) and DOOM + DOOM II (PPSA21444), which resets its stencil plane to 1 with a 0x01010101 fill every frame and then draws with stencil Equal 1 and 2, kept the stencil of earlier frames and showed black squares and clipped text. agc_driver_depth_plane_fill pins the rules. --- .../Graphics/src/DepthSurface.cpp | 77 ++++++++++++-- .../tests/execution/DepthPlaneFill.cpp | 100 ++++++++++++++++++ docs/dev/debt/libSceAgcDriver.md | 1 + 3 files changed, 168 insertions(+), 10 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneFill.cpp diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp index 4db10e6b76..a2e45f7940 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp @@ -257,7 +257,9 @@ public: const VkImageSubresourceRange range{aspects, 0, 1, 0, 1}; constexpr VkAccessFlags access = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT; RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, access, VK_ACCESS_TRANSFER_WRITE_BIT); - const VkClearDepthStencilValue clear{clearDepth, clearStencil}; + const VkClearDepthStencilValue clear{depthFill.value_or(clearDepth), stencilFill.value_or(clearStencil)}; + depthFill.reset(); + stencilFill.reset(); context.Function("vkCmdClearDepthStencilImage")(commands, image, VK_IMAGE_LAYOUT_GENERAL, &clear, 1, &range); RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, access); if (batch) batch->SubmitAndWait(); @@ -322,8 +324,25 @@ public: return texture; } + void ForgetClear(VkImageAspectFlags aspects) { + pendingClear &= ~aspects; + if ((aspects & VK_IMAGE_ASPECT_DEPTH_BIT) != 0) depthFill.reset(); + if ((aspects & VK_IMAGE_ASPECT_STENCIL_BIT) != 0) stencilFill.reset(); + } + + void NoteHtileClear(VkImageAspectFlags aspects) { + ForgetClear(aspects); + pendingClear |= aspects; + } + + void NotePlaneFill(VkImageAspectFlagBits aspect, float depth, std::uint8_t stencil) { + pendingClear |= aspect; + if (aspect == VK_IMAGE_ASPECT_DEPTH_BIT) depthFill = depth; + else stencilFill = stencil; + } + void CopyAspectFrom(const DepthSurface& source, VkImageAspectFlagBits aspect) { - pendingClear &= ~static_cast(aspect); + ForgetClear(aspect); auto* recorder = RecorderFor(context); std::unique_ptr batch; if (recorder == nullptr) batch = std::make_unique(context); @@ -370,6 +389,8 @@ public: VkImageAspectFlags pendingClear = 0; float clearDepth = 0.0f; std::uint8_t clearStencil = 0; + std::optional depthFill; + std::optional stencilFill; bool StorageViewMatches(std::span words, const GuestTextureResource& resource) const { if (resource.width != target.extent.width || resource.height != target.extent.height) return false; @@ -951,8 +972,13 @@ void takePlane(DepthSurface& surface, PlaneOwners& owners, std::uint64_t address auto& entry = owners[{surface.context.device, address}]; auto* owner = entry.surface; if (owner != nullptr && owner->active->load(std::memory_order_acquire) && owner != &surface && owner->target.format == surface.target.format && owner->target.samples == surface.target.samples && owner->target.extent.width == surface.target.extent.width && owner->target.extent.height == surface.target.extent.height) { - if ((owner->pendingClear & aspect) != 0) surface.pendingClear |= aspect; - else surface.CopyAspectFrom(*owner, aspect); + if ((owner->pendingClear & aspect) != 0) { + surface.pendingClear |= aspect; + if (aspect == VK_IMAGE_ASPECT_DEPTH_BIT) surface.depthFill = owner->depthFill; + else surface.stencilFill = owner->stencilFill; + } else { + surface.CopyAspectFrom(*owner, aspect); + } } entry = {&surface, ++planeBinds}; } @@ -1222,9 +1248,9 @@ VkImageView DepthSurfaceView(const Context& context, const DepthTarget& target) if (created && !inherits && target.htileAddress == 0) surface->TakeStorage(); if (cleared && (!created || inherits)) { if (target.samples != 1u) surface->sampling.refusal = "native multisample depth fast-clear transition is not implemented"; - surface->pendingClear |= VK_IMAGE_ASPECT_DEPTH_BIT; + surface->NoteHtileClear(VK_IMAGE_ASPECT_DEPTH_BIT); } - surface->pendingClear |= htileCleared; + surface->NoteHtileClear(htileCleared); surface->ApplyFastClear(); if (!created || inherits) surface->written = ++depthWrites; surface->NoteMemory(); @@ -1290,7 +1316,7 @@ void NoteHtileDepthClear(std::uint64_t htileAddress, std::size_t bytes, std::siz } if (!HtileFillCovers(surface->target.htileAddress, surface->target.extent, htileAddress, bytes)) continue; if (surface->target.samples != 1u) surface->sampling.refusal = "native multisample depth fast-clear transition is not implemented"; - surface->pendingClear |= VK_IMAGE_ASPECT_DEPTH_BIT; + surface->NoteHtileClear(VK_IMAGE_ASPECT_DEPTH_BIT); existing = true; } if (!existing) clearedHtiles()[htileAddress] = bytes; @@ -1418,6 +1444,26 @@ bool HtileFillCovers(std::uint64_t htile, VkExtent2D extent, std::uint64_t addre return address + bytes - htile >= HtileMetadataBytes(extent); } +bool PlaneFillCovers(std::uint64_t plane, std::uint64_t planeBytes, std::uint64_t address, std::size_t bytes) { + return plane != 0 && plane >= address && plane + planeBytes <= address + bytes; +} + +std::optional StencilFillValue(std::uint32_t pattern) { + const auto byte = static_cast(pattern & 0xffu); + if (pattern != byte * 0x01010101u) return std::nullopt; + return byte; +} + +std::optional DepthFillValue(std::uint32_t pattern, bool d16) { + if (d16) { + if ((pattern >> 16u) != (pattern & 0xffffu)) return std::nullopt; + return static_cast(pattern & 0xffffu) / 65535.0f; + } + const auto value = std::bit_cast(pattern); + if (!(value >= 0.0f && value <= 1.0f)) return std::nullopt; + return value; +} + void NoteDepthMetadataFill(std::uint64_t address, std::size_t bytes, std::uint32_t pattern) { std::lock_guard lock(surfacesMutex()); forgetHtileReads(address, bytes); @@ -1425,9 +1471,20 @@ void NoteDepthMetadataFill(std::uint64_t address, std::size_t bytes, std::uint32 if (!surface->active->load(std::memory_order_acquire)) continue; const auto& target = surface->target; if (target.samples != 1u && target.htileAddress != 0u && address < target.htileAddress + HtileMetadataBytes(target.extent) && target.htileAddress < address + bytes) surface->sampling.refusal = "native multisample depth metadata fill transition is not implemented"; - if (!HtileFillCovers(target.htileAddress, target.extent, address, bytes)) continue; - const VkImageAspectFlags written = VK_IMAGE_ASPECT_DEPTH_BIT | (target.htileStencil ? VK_IMAGE_ASPECT_STENCIL_BIT : 0u); - surface->pendingClear = (surface->pendingClear & ~written) | HtileFillClears(pattern, target.htileStencil); + if (HtileFillCovers(target.htileAddress, target.extent, address, bytes)) { + const VkImageAspectFlags written = VK_IMAGE_ASPECT_DEPTH_BIT | (target.htileStencil ? VK_IMAGE_ASPECT_STENCIL_BIT : 0u); + surface->ForgetClear(written); + surface->NoteHtileClear(HtileFillClears(pattern, target.htileStencil)); + continue; + } + if (target.samples != 1) continue; + const bool d16 = target.format == VK_FORMAT_D16_UNORM || target.format == VK_FORMAT_D16_UNORM_S8_UINT; + if (PlaneFillCovers(target.address, DepthSliceBytes(target.extent, d16 ? 2u : 4u), address, bytes)) { + if (const auto depth = DepthFillValue(pattern, d16)) surface->NotePlaneFill(VK_IMAGE_ASPECT_DEPTH_BIT, *depth, 0); + } + if (PlaneFillCovers(target.stencilAddress, DepthSliceBytes(target.extent, 1u), address, bytes)) { + if (const auto stencil = StencilFillValue(pattern)) surface->NotePlaneFill(VK_IMAGE_ASPECT_STENCIL_BIT, 0.0f, *stencil); + } } } diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneFill.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneFill.cpp new file mode 100644 index 0000000000..d833893a47 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneFill.cpp @@ -0,0 +1,100 @@ +#include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" +#include "DepthFastClearHarness.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include + +namespace { + +using DepthFastClearHarness::Compile; +using DepthFastClearHarness::Covered; +using DepthFastClearHarness::Draw; +using DepthFastClearHarness::DrawOptions; +using DepthFastClearHarness::Height; +using DepthFastClearHarness::HtileBytes; +using DepthFastClearHarness::Require; +using DepthFastClearHarness::Surface; +using DepthFastClearHarness::Width; + +constexpr std::size_t DepthPlaneBytes = 128u * 128u * 4u; +constexpr std::size_t StencilPlaneBytes = 256u * 256u; + +alignas(4096) std::array Depth{}; +alignas(4096) std::array Stencil{}; +alignas(4096) std::array HtileDepth{}; +alignas(256) std::array Htile{}; + +VkStencilOpState StencilState(VkCompareOp compare, VkStencilOp pass, std::uint32_t reference) { + VkStencilOpState state{}; + state.failOp = VK_STENCIL_OP_KEEP; + state.passOp = pass; + state.depthFailOp = VK_STENCIL_OP_KEEP; + state.compareOp = compare; + state.compareMask = 0xffu; + state.writeMask = 0xffu; + state.reference = reference; + return state; +} + +DrawOptions StencilEquals(std::uint32_t reference) { + return {.depthTest = false, .stencilTest = true, .stencil = StencilState(VK_COMPARE_OP_EQUAL, VK_STENCIL_OP_KEEP, reference)}; +} + +DrawOptions StencilWrite(std::uint32_t reference) { + return {.depthTest = false, .stencilTest = true, .stencil = StencilState(VK_COMPARE_OP_ALWAYS, VK_STENCIL_OP_REPLACE, reference)}; +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + Require(AgcDriver::Graphics::DepthSliceBytes({Width, Height}, 4u) == DepthPlaneBytes, "the depth plane size of the test surface changed"); + Require(AgcDriver::Graphics::DepthSliceBytes({Width, Height}, 1u) == StencilPlaneBytes, "the stencil plane size of the test surface changed"); + const auto shaders = Compile(*device); + const Surface surface{reinterpret_cast(Depth.data()), reinterpret_cast(Stencil.data()), 0, false, VK_FORMAT_D32_SFLOAT_S8_UINT}; + const auto depth = surface.depth; + const auto stencil = surface.stencil; + + Require(Draw(*device, shaders, surface, StencilWrite(2u)) == Covered, "the stencil mark draw did not cover the target"); + Require(Draw(*device, shaders, surface, StencilEquals(2u)) == Covered, "the stencil mark was not kept"); + AgcDriver::Graphics::NoteDepthMetadataFill(stencil, StencilPlaneBytes - 4u, 0x01010101u); + Require(Draw(*device, shaders, surface, StencilEquals(2u)) == Covered, "a fill shorter than the stencil plane changed the stencil"); + AgcDriver::Graphics::NoteDepthMetadataFill(stencil, StencilPlaneBytes, 0x01020304u); + Require(Draw(*device, shaders, surface, StencilEquals(2u)) == Covered, "a fill whose bytes differ changed the stencil"); + AgcDriver::Graphics::NoteDepthMetadataFill(stencil, StencilPlaneBytes, 0x01010101u); + Require(Draw(*device, shaders, surface, StencilEquals(2u)) == 0u, "a fill of the whole stencil plane with 1 left the stencil mark 2"); + Require(Draw(*device, shaders, surface, StencilEquals(1u)) == Covered, "a fill of the whole stencil plane with 1 did not clear the stencil to 1"); + Require(Draw(*device, shaders, surface, StencilWrite(2u)) == Covered, "the stencil could not be written after the fill"); + AgcDriver::Graphics::NoteDepthMetadataFill(stencil - 4096u, StencilPlaneBytes + 8192u, 0u); + Require(Draw(*device, shaders, surface, StencilEquals(0u)) == Covered, "a fill around the whole stencil plane with 0 did not clear the stencil to 0"); + + Require(Draw(*device, shaders, surface, {.depthWrite = true}) == Covered, "the depth was not written at 0.5"); + Require(Draw(*device, shaders, surface, {}) == 0u, "a draw at depth 0.5 passed LESS against the 0.5 written before it"); + AgcDriver::Graphics::NoteDepthMetadataFill(depth, DepthPlaneBytes, 0x3f800000u); + Require(Draw(*device, shaders, surface, {}) == Covered, "a fill of the whole depth plane with 1.0 did not clear the depth to 1.0"); + AgcDriver::Graphics::NoteDepthMetadataFill(depth, DepthPlaneBytes, 0u); + Require(Draw(*device, shaders, surface, {}) == 0u, "a fill of the whole depth plane with 0.0 did not clear the depth to 0.0"); + AgcDriver::Graphics::NoteDepthMetadataFill(depth, DepthPlaneBytes, 0xff000000u); + Require(Draw(*device, shaders, surface, {}) == 0u, "a fill of the depth plane with a value outside 0 to 1 changed the depth"); + AgcDriver::Graphics::NoteDepthMetadataFill(depth, DepthPlaneBytes - 4u, 0x3f800000u); + Require(Draw(*device, shaders, surface, {}) == 0u, "a fill shorter than the depth plane changed the depth"); + + Htile.fill(0xffffffffu); + const Surface htiled{reinterpret_cast(HtileDepth.data()), 0, reinterpret_cast(Htile.data()), false, VK_FORMAT_D32_SFLOAT}; + Require(Draw(*device, shaders, htiled, {.depthWrite = true}) == Covered, "the depth of the surface with HTILE was not written at 0.5"); + AgcDriver::Graphics::NoteDepthMetadataFill(htiled.depth, DepthPlaneBytes, 0u); + AgcDriver::Graphics::NoteDepthMetadataFill(htiled.htile, HtileBytes, 0u); + Require(Draw(*device, shaders, htiled, {.depthWrite = true}) == Covered, "an HTILE fill after a depth plane fill of 0.0 did not clear the depth to DB_DEPTH_CLEAR (1.0)"); + AgcDriver::Graphics::NoteDepthMetadataFill(htiled.depth, DepthPlaneBytes, 0u); + AgcDriver::Graphics::NoteHtileDepthClear(htiled.htile, HtileBytes); + Require(Draw(*device, shaders, htiled, {}) == Covered, "an HTILE clear kernel after a depth plane fill of 0.0 did not clear the depth to DB_DEPTH_CLEAR (1.0)"); + std::puts("depth plane fill tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 71210637ea..799238fde3 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -38,6 +38,7 @@ - [HtileFillCovers](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver) - a fill is taken to cover a depth surface's whole HTILE when it spans 32 KiB per 1024x512 pixel block from DB_HTILE_DATA_BASE, the pipe-aligned SW_64KB_Z_X metadata extent AMD PAL's gfx10 addrlib gives for GB_ADDR_CONFIG 0x00100044 (1-8 samples, D16/D32); other metadata layouts are not decoded. A partial fill is ignored (the surface keeps its contents); not measured on the console. - [Depth fast clear through HTILE](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver) - a surface cleared by an HTILE fill is cleared to DB_DEPTH_CLEAR, also when it is next sampled. For a depth-only HTILE the clear word also carries a value (radv writes `minz = maxz = round(depth * 0x3fff)`, `HTILE_Z_CLEAR_REG` in Mesa's `src/amd/common/ac_descriptors.h`, and allows TC-compatible fast clears only to 0.0 or 1.0); which of the two the texture unit returns for a cleared tile was not measured. - [Depth-only and stencil HTILE fill](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver) - in a whole-HTILE fill, ZMask (bits 0-3) 0 clears the depth and SMem (bits 8-9) 0 clears the stencil, while SMem 3 leaves the stencil alone (`HtileFillClears`); other SMem values are taken to leave it alone too. The bit meanings follow the gfx10 HTILE layout as the code reads it, not a measurement; `agc_driver_depth_fast_clear_submit` pins them for the paths the Driver takes. +- [Depth and stencil plane fills](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver) - a fill that covers a known depth surface's whole stencil plane (`DepthSliceBytes` of its extent at one byte per texel) with four equal bytes clears the host stencil to that byte the next time the surface is bound, sampled or stored to, and a fill that covers the whole depth plane (two or four bytes per texel) clears the depth when the dword is a float in 0.0-1.0, or two equal 16-bit halves read as UNORM for D16 (`NoteDepthMetadataFill`). A partial fill, a dword whose bytes differ, a depth value outside 0.0-1.0 (0xff000000 is seen), a multisampled surface, or a fill made before the surface exists leave the host image as it was. An HTILE fill or a recognized HTILE clear kernel noted after a plane fill replaces its value with DB_DEPTH_CLEAR/DB_STENCIL_CLEAR. On the console the fill writes the plane the depth block reads; the surfaces this was seen on keep the stencil out of HTILE (PPSA26525 Heretic + Hexen and PPSA21444 DOOM + DOOM II reset the Cohtml UI stencil plane to 1 with a 0x01010101 fill every frame, then draw with stencil Equal 1 and 2), and what the console returns for a plane fill under a compressed HTILE was not measured. `agc_driver_depth_plane_fill` pins the rules. - [HTILE address of a depth array slice](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - `decodeDepth` adds the `DB_DEPTH_VIEW` slice offset to the Z and stencil addresses but not to `htileAddress`, so every slice of a layered depth target shares slice 0's HTILE address and a fill of slice 0's HTILE marks all slices cleared. Astro's Playroom fills the HTILE of each of its three shadow cascades 0x40000 apart (0x24000 bytes of tiles each) back to back before drawing, so the result is still right there; a title that clears one slice between draws to another would lose depth. The per-slice HTILE stride is not derived yet. - [sceAgcDriverGetHsOffchipParam](../../../core/libs/prx/libSceAgcDriver/Unimplemented.cpp) (libSceAgcDriver, NID `r28hEh6cNH0`) - declared as a throwing stub. The two `uint16_t` output pointers and integer return type follow [ps5-vulkan](https://github.com/blackbearreloaded/ps5-vulkan/blob/4bb0c8b1423698a7369efc22b829bf16a99e7128/native/tess_driver_import_stub.c#L10), which reads the pair before it binds a tessellation factor ring. The meaning of the two values, their initial state and how they relate to `sceAgcDriverSetHsOffchipParam`, which keeps nothing here, are unknown; the stub does not write caller memory. PPSA99011 imports it - [Vertex input and color target overlap](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (AGC driver) - direct draws exclude the descriptor prefix skipped by `firstVertex` or `firstInstance` from the alias check; indexed draws conservatively use index zero as the lower bound before adding `firstVertex`. Indirect draws still check the full descriptor extent. Reads of the active target's bytes remain unsupported; same-draw console ordering is not established. From 6e331b38234aa8bb675a4347d0a9f6a50c39216e Mon Sep 17 00:00:00 2001 From: Maz Date: Sat, 17 Oct 2026 16:46:55 +0400 Subject: [PATCH 0733/1000] test(libkernel): compare absolute links by the file they name --- core/libs/tests/GuestFilesystem.cpp | 5 +++-- core/libs/tests/GuestKernelSys.cpp | 9 ++++++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/core/libs/tests/GuestFilesystem.cpp b/core/libs/tests/GuestFilesystem.cpp index e17c262c18..a42206a49d 100644 --- a/core/libs/tests/GuestFilesystem.cpp +++ b/core/libs/tests/GuestFilesystem.cpp @@ -714,8 +714,9 @@ int main() { Require(followed.st_ino == status.st_ino && linkStatus.st_ino != status.st_ino && linkStatus.st_dev == status.st_dev); Require(stat_nid_postfix(directoryLink.string().c_str(), &followed) == 0 && (followed.st_mode & 0170000) == 0040000); Require(stat_nid_postfix(fileLink.string().c_str(), &status) == 0 && status.st_size == 5); - const auto absoluteTarget = std::filesystem::absolute(present).generic_string(); - Require(checkRelativeLink(absoluteLink, static_cast(absoluteTarget.size()))); + FileStat absoluteStatus{}; + Require(lstat_nid_postfix(absoluteLink.string().c_str(), &absoluteStatus) == 0); + Require((absoluteStatus.st_mode & 0170000) == 0120000 && absoluteStatus.st_size > 0 && absoluteStatus.st_nlink == 1); Require(stat_nid_postfix(absoluteLink.string().c_str(), &followed) == 0 && followed.st_ino == status.st_ino); Require(std::filesystem::remove(fileLink) && std::filesystem::remove(directoryLink) && DeleteFileW(danglingLink.c_str()) != 0 && std::filesystem::remove(absoluteLink) && std::filesystem::remove(targetDirectory)); diff --git a/core/libs/tests/GuestKernelSys.cpp b/core/libs/tests/GuestKernelSys.cpp index 4d004d5378..89a102b08d 100644 --- a/core/libs/tests/GuestKernelSys.cpp +++ b/core/libs/tests/GuestKernelSys.cpp @@ -287,12 +287,15 @@ int main() { Require(symlink_nid_postfix("sub", directoryLink.c_str()) == 0); Require(readlink_nid_postfix(directoryLink.c_str(), buffer, sizeof(buffer)) == 3 && std::memcmp(buffer, "sub", 3) == 0); const auto absolute = std::filesystem::absolute(root / "absolute"); - const auto absoluteTarget = std::filesystem::absolute(original).generic_string(); Require(CreateSymbolicLinkW(absolute.wstring().c_str(), std::filesystem::absolute(original).wstring().c_str(), SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE) != 0); std::memset(buffer, '#', sizeof(buffer)); - Require(readlink_nid_postfix((root / "absolute").string().c_str(), buffer, sizeof(buffer)) == static_cast(absoluteTarget.size())); - Require(std::memcmp(buffer, absoluteTarget.data(), absoluteTarget.size()) == 0 && buffer[absoluteTarget.size()] == '#'); + const auto absoluteLength = readlink_nid_postfix((root / "absolute").string().c_str(), buffer, sizeof(buffer)); + Require(absoluteLength > 0 && static_cast(absoluteLength) < sizeof(buffer) && buffer[absoluteLength] == '#'); + const std::string absoluteTarget(buffer, static_cast(absoluteLength)); + Require(absoluteTarget.find('?') == std::string::npos); + std::error_code absoluteError; + Require(std::filesystem::equivalent(absoluteTarget, original, absoluteError) && !absoluteError); #else Require(std::filesystem::is_symlink(std::filesystem::symlink_status(symbolic))); Require(readlink_nid_postfix(symbolic.c_str(), buffer, sizeof(buffer)) == 12 && std::memcmp(buffer, "original.bin", 12) == 0); From 778153c29e63801716f84822ad549e863ef1038e Mon Sep 17 00:00:00 2001 From: Pouare514 Date: Sat, 17 Oct 2026 14:47:38 +0200 Subject: [PATCH 0734/1000] fix(import-audit): walk host-to-host .prx dependencies of built libs --- docs/user/USAGE.md | 2 + tools/import_audit.py | 108 +++++++++++++++++++--- tools/tests/test_import_audit.py | 149 +++++++++++++++++++++++++++++++ 3 files changed, 249 insertions(+), 10 deletions(-) diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index 18996014a7..787714a154 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -250,6 +250,8 @@ Each registry has a companion with the same prefix and the suffix `.needed.json` The report also lists needed libraries that have no file in `--libs` or `--modules`, including dependencies with zero imports. These do not increase the import or reference counts. Windows can skip a missing dependency when no import needs it; Linux still needs the file. The audit also lists imports exported only by a library other than the one they name; those resolve on Linux and can fail on Windows. +The audit also walks the built libraries' own host dependencies: each `.prx` under `--libs` has its PE imports (or ELF `DT_NEEDED` entries) checked for `.prx` names with no file in `--libs` or `--modules`, following present libraries transitively. Gaps fail the audit and are reported as `Importer: Chain: -> ... -> ` lines, since the loader fails on them even when every game import resolves. + | Option | Effect | |--------|--------| | `--libs ` | Built `.prx` directory. Required; repeat for several. | diff --git a/tools/import_audit.py b/tools/import_audit.py index 1659810ae5..549317a6cf 100644 --- a/tools/import_audit.py +++ b/tools/import_audit.py @@ -220,7 +220,7 @@ def elf_imports(path, data): return symbols, needed -def pe_exports(path, data): +def pe_headers(path, data): header = unpack("6} imports {summary['references_by_class'][kind]:>6} references") @@ -587,6 +669,10 @@ def render(summary, records, missing, stops): lines.extend(f" {library}: {count} imports" for library, count in missing.items()) if any(count == 0 for count in missing.values()): lines.append(" Libraries with 0 imports are declared dependencies; Windows can skip them if no import needs them") + if missing_hosts: + lines.append("") + lines.append(f"Host dependencies with no file in --libs or --modules ({len(missing_hosts)})") + lines.extend(f" Importer: {entry['importer']} Chain: {' -> '.join(entry['chain'])}" for entry in missing_hosts) if summary["library_mismatch"]: lines.append("") lines.append(f"{summary['library_mismatch']} imports are exported only by a library other than the one they name: they resolve on Linux and can fail on Windows") @@ -618,11 +704,13 @@ def main(argv=None): imports = [entry for _, entries in registries for entry in entries] needed = sorted({library for path in dependency_registries for library in read_needed_registry(path)}) exports, libraries = built_libraries(args.libs) + host_dependencies = built_host_dependencies(args.libs) module_imports, module_exports = read_modules(args.modules) records, missing = audit(imports, exports, source_stubs(args.source), libraries, set(module_imports), module_imports, module_exports, needed_libraries=needed) + missing_hosts = audit_host_dependencies(host_dependencies, libraries, set(module_imports)) if args.names: attach_names(records, load_names(args.names)) - summary = summarize(records, sum(record["references"] for record in records), missing) + summary = summarize(records, sum(record["references"] for record in records), missing, missing_hosts) stops = [{"registry": str(path), "stop": current_stop(entries, records)} for path, entries in registries] if args.json: commit, dirty = git_stamp(Path(args.source)) @@ -638,8 +726,8 @@ def main(argv=None): except OSError as error: print(f"FAIL: {error.strerror}: {error.filename}" if error.filename else f"FAIL: {error}", file=sys.stderr) return 2 - print(render(summary, records, missing, stops)) - return 1 if summary["unique_by_class"]["absent"] or missing or (args.strict_providers and summary["library_mismatch"]) else 0 + print(render(summary, records, missing, stops, missing_hosts)) + return 1 if summary["unique_by_class"]["absent"] or missing or missing_hosts or (args.strict_providers and summary["library_mismatch"]) else 0 if __name__ == "__main__": diff --git a/tools/tests/test_import_audit.py b/tools/tests/test_import_audit.py index 3441ae3f73..9e6bf68d9b 100644 --- a/tools/tests/test_import_audit.py +++ b/tools/tests/test_import_audit.py @@ -136,6 +136,39 @@ def build_pe(names): return bytes(image) + raw + b"\xc3" * (16 * max(1, len(names))) +def build_pe_with_imports(dlls, names=()): + idata_rva, edata_rva, text_rva, raw_offset = 0x1000, 0x2000, 0x3000, 0x200 + directory, strings = b"", b"" + for dll in dlls: + directory += struct.pack(" libSceIme.prx", out) + self.assertIn("Importer: libSceVideoOut.prx Chain: libSceVideoOut.prx -> libSceKeyboard.prx", out) + self.assertNotIn("KERNEL32", out) + + def test_transitive_host_dependency_reports_chain(self): + work = self.work + (work.libs / "libSceA.prx").write_bytes(build_pe_with_imports(["libSceB.prx"], ["AAAAAAAAAAA"])) + (work.libs / "libSceB.prx").write_bytes(build_pe_with_imports(["libSceMissing.prx"], ["BBBBBBBBBBB"])) + registry = work.root / "game.json" + write_registry(registry, []) + result = work.root / "audit.json" + code, out, err = work.run(registry, "--json", str(result)) + self.assertEqual((code, err), (1, "")) + data = json.loads(result.read_text()) + self.assertEqual(data["missing_host_dependencies"], [ + {"importer": "libSceA.prx", "chain": ["libSceA.prx", "libSceB.prx", "libSceMissing.prx"]}, + {"importer": "libSceB.prx", "chain": ["libSceB.prx", "libSceMissing.prx"]}, + ]) + self.assertIn("Importer: libSceA.prx Chain: libSceA.prx -> libSceB.prx -> libSceMissing.prx", out) + + def test_host_dependency_satisfied_by_module(self): + work = self.work + (work.libs / "libSceA.prx").write_bytes(build_pe_with_imports(["libSceIme.prx"], ["AAAAAAAAAAA"])) + (work.modules / "libSceIme.prx").write_bytes(exporting()) + registry = work.root / "game.json" + write_registry(registry, []) + code, out, err = work.run(registry) + self.assertEqual((code, err), (0, "")) + self.assertNotIn("Host dependencies", out) + + def test_complete_host_dependencies_pass(self): + work = self.work + (work.libs / "libc.prx").write_bytes(build_pe_with_imports([], ["AAAAAAAAAAA"])) + (work.libs / "libSceVideoOut.prx").write_bytes(build_pe_with_imports(["libc.prx"], ["BBBBBBBBBBB"])) + registry = work.root / "game.json" + write_registry(registry, []) + code, out, err = work.run(registry) + self.assertEqual((code, err), (0, "")) + self.assertNotIn("Host dependencies", out) + + def test_elf_host_dependencies_are_walked(self): + work = self.work + (work.libs / "libSceA.prx").write_bytes(build_elf( + [("AAAAAAAAAAA", 1, GLOBAL_FUNCTION)], ["libSceGhost.prx", "KERNEL32.dll"])) + registry = work.root / "game.json" + write_registry(registry, []) + result = work.root / "audit.json" + code, out, err = work.run(registry, "--json", str(result)) + self.assertEqual((code, err), (1, "")) + data = json.loads(result.read_text()) + self.assertEqual(data["missing_host_dependencies"], [ + {"importer": "libSceA.prx", "chain": ["libSceA.prx", "libSceGhost.prx"]}, + ]) + self.assertNotIn("KERNEL32", out) + + def test_conflicting_host_libraries_are_ambiguous(self): + first, second = self.work.libs, self.work.root / "other-libs" + second.mkdir() + (first / "libSceA.prx").write_bytes(build_pe_with_imports(["libSceB.prx"], ["AAAAAAAAAAA"])) + (second / "libSceA.prx").write_bytes(build_pe_with_imports([], ["AAAAAAAAAAA"])) + with self.assertRaisesRegex(import_audit.AuditError, "ambiguous"): + import_audit.built_host_dependencies([first, second]) + def test_elf_unnamed_defined_symbol_is_not_an_export(self): image = bytearray(exporting("alpha")) From a03ccc6be16bbc0963685da2f816b7940d92ec03 Mon Sep 17 00:00:00 2001 From: thibaut Date: Sat, 17 Oct 2026 11:47:26 +0200 Subject: [PATCH 0735/1000] fix(shader): write cache files of 2 GiB and more in chunks On Windows (MinGW libstdc++) one std::ofstream::write of 2 GiB or more never returns: the whole temporary file is written but WriteFileAtomically hangs, the entry is never replaced and the temporary file stays behind. A Vulkan pipeline cache of 2.1 GB hung every save and left a 2 GB .tmp per run. Write in chunks of at most 1 GiB. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01XBXM4pgkMHYEtqN1EQni2P --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 5 ++ .../libSceAgcDriver/tests/ShaderDiskCache.cpp | 62 +++++++++++++++++++ .../recompiler/ShaderCacheDirectory.hpp | 8 ++- 3 files changed, 74 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 4eba5fa87e..aaa3940cd0 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1592,6 +1592,11 @@ if(BUILD_TESTING) if(WIN32) set_tests_properties(agc_driver_partially_resident_pending_stores PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test(NAME agc_shader_disk_cache_large_file COMMAND agc_shader_disk_cache_tests --large-file) + set_tests_properties(agc_shader_disk_cache_large_file PROPERTIES SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" TIMEOUT 240) + if(WIN32) + set_tests_properties(agc_shader_disk_cache_large_file PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() if(WIN32) add_test(NAME agc_driver_untracked_snapshot_hint COMMAND agc_driver_partially_resident_snapshot_tests --untracked-hint) set_tests_properties(agc_driver_untracked_snapshot_hint PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" TIMEOUT 30 diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp index d508666730..6adaebcddd 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp @@ -17,7 +17,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -29,6 +31,7 @@ #include #include #include +#include #include #ifdef _WIN32 #ifndef NOMINMAX @@ -730,6 +733,64 @@ void verifyFloatModeSerialization() { require(back.request.context.floatMode == mode, "the float mode did not survive serialization"); } +int runLargeFileWrite() { + constexpr std::size_t size = (std::size_t{2} << 30u) + 4096u; + const auto directory = std::filesystem::temp_directory_path() / "aps5-shader-disk-cache-large-file-test"; + std::error_code error; + std::filesystem::remove_all(directory, error); + std::filesystem::create_directories(directory, error); + const auto space = std::filesystem::space(directory, error); + if (error || space.available < size + (std::uintmax_t{1} << 30u)) { + std::cout << "skipping: not enough free disk space for a file of more than 2 GiB\n"; + return 77; + } + std::vector bytes; + try { + bytes.resize(size); + } catch (const std::bad_alloc&) { + std::cout << "skipping: cannot allocate a buffer of more than 2 GiB\n"; + return 77; + } + const std::array samples{0u, (std::size_t{1} << 30u) - 1u, std::size_t{1} << 30u, (std::size_t{2} << 30u) - 1u, std::size_t{2} << 30u, size - 1u}; + for (std::size_t index = 0; index < samples.size(); ++index) bytes[samples[index]] = static_cast(0xa0u + index); + const auto path = directory / "pipelines.bin"; + auto written = std::make_shared>(); + auto result = written->get_future(); + std::thread([written, &bytes, path] { + try { + written->set_value(WriteFileAtomically(path, bytes)); + } catch (...) { + written->set_exception(std::current_exception()); + } + }).detach(); + if (result.wait_for(std::chrono::seconds(120)) != std::future_status::ready) { + std::cerr << "WriteFileAtomically did not return within 120 s for a file of more than 2 GiB\n"; + std::cerr.flush(); + std::_Exit(1); + } + require(result.get(), "cannot write a cache file of more than 2 GiB"); + require(std::filesystem::file_size(path) == size, "the cache file of more than 2 GiB has the wrong size"); + { + std::ifstream file(path, std::ios::binary); + require(static_cast(file), "cannot open the cache file of more than 2 GiB"); + for (std::size_t index = 0; index < samples.size(); ++index) { + file.seekg(static_cast(samples[index])); + char value = 0; + require(file.get(value) && static_cast(value) == bytes[samples[index]], "the cache file of more than 2 GiB differs at byte " + std::to_string(samples[index])); + } + } + std::size_t entries = 0; + for (const auto& entry : std::filesystem::directory_iterator(directory)) { + static_cast(entry); + ++entries; + } + require(entries == 1u, "the cache file of more than 2 GiB left a temporary file behind"); + std::filesystem::remove_all(directory, error); + require(!error, "cannot remove the large file test directory: " + error.message()); + std::cout << "large cache file write test passed\n"; + return 0; +} + void verifyAtomicReplacement(const std::filesystem::path& directory) { struct Reader { #ifdef _WIN32 @@ -2124,6 +2185,7 @@ int main(int argc, char** argv) { std::cout << "artifact codec tests passed\n"; return 0; } + if (argc == 2 && std::string_view(argv[1]) == "--large-file") return runLargeFileWrite(); if (argc == 2 && std::string_view(argv[1]) == "--load") return runLoadingProcess(); if (argc == 2 && std::string_view(argv[1]) == "--load-prebuilt") return runPrebuiltLoadingProcess(); if (argc == 2 && std::string_view(argv[1]) == "--no-failure-memo") return runWithoutFailureMemo(); diff --git a/core/shader/recompiler/ShaderCacheDirectory.hpp b/core/shader/recompiler/ShaderCacheDirectory.hpp index ee9393e078..84fbd1ae36 100644 --- a/core/shader/recompiler/ShaderCacheDirectory.hpp +++ b/core/shader/recompiler/ShaderCacheDirectory.hpp @@ -1,6 +1,7 @@ #ifndef CORE_SHADER_RECOMPILER_SHADERCACHEDIRECTORY_HPP #define CORE_SHADER_RECOMPILER_SHADERCACHEDIRECTORY_HPP +#include #include #include #include @@ -56,7 +57,12 @@ inline bool WriteFileAtomically(const std::filesystem::path& path, std::span(bytes.data()), static_cast(bytes.size())); + constexpr std::size_t largestWrite = std::size_t{1} << 30u; + for (std::size_t offset = 0; offset < bytes.size() && file;) { + const auto chunk = std::min(bytes.size() - offset, largestWrite); + file.write(reinterpret_cast(bytes.data() + offset), static_cast(chunk)); + offset += chunk; + } file.close(); written = !file.fail(); } From a295b771ebbd616242bda388b3fc2de583a874dc Mon Sep 17 00:00:00 2001 From: pushkarreddyy <212133856+pushkarreddyy@users.noreply.github.com> Date: Sat, 17 Oct 2026 18:31:54 +0530 Subject: [PATCH 0736/1000] fix(libkernel): return EFAULT from sceKernelWriteThrottlingStatus for a null status --- core/libs/prx/libkernel/File/src/Stdio.cpp | 2 +- core/libs/tests/GuestFilesystem.cpp | 1 + docs/dev/debt/libkernel.md | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index 510c992a55..7c4033d405 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -970,7 +970,7 @@ int APS5_VABI sceKernelFdatasync(int fd) { } int APS5_VABI sceKernelWriteThrottlingStatus(std::uint64_t* status) { - if (status == nullptr) throw std::invalid_argument("sceKernelWriteThrottlingStatus: status is null"); + if (status == nullptr) return SceErrorFromErrno(GUEST_EFAULT); status[0] = std::numeric_limits::max(); status[1] = 0; status[2] = 0; diff --git a/core/libs/tests/GuestFilesystem.cpp b/core/libs/tests/GuestFilesystem.cpp index b87cf1344c..8147f4d743 100644 --- a/core/libs/tests/GuestFilesystem.cpp +++ b/core/libs/tests/GuestFilesystem.cpp @@ -304,6 +304,7 @@ int main() { std::uint64_t throttling[4] = {1, 2, 3, 4}; Require(sceKernelWriteThrottlingStatus(throttling) == 0); Require(throttling[0] == 0xffffffffu && throttling[1] == 0 && throttling[2] == 0 && throttling[3] == 0); + Require(sceKernelWriteThrottlingStatus(nullptr) == static_cast(0x8002000eu)); Require(sceKernelDebugOutText(0, nullptr) == static_cast(0x8002000eu)); auto* captured = std::tmpfile(); Require(captured != nullptr); diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 5791f7ab5e..876f32d897 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -120,7 +120,7 @@ - [setitimer](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) follows FreeBSD's `sys_setitimer` and `kern_setitimer`: `EINVAL` for timers other than `ITIMER_REAL`, `ITIMER_VIRTUAL` and `ITIMER_PROF`, and for a value, or the interval of a non-zero value, with a negative field, `tv_usec` of 1000000 or more, or more than `INT32_MAX / 2` seconds; a null value reads the timer as `getitimer` does, with `EFAULT` when the output is null too. `ITIMER_REAL` shares the timer thread of `alarm`, which clears its interval: a zero value disarms it, the previous value is returned, and each expiry re-arms it by `it_interval`. An expiry delivers nothing: with `SIG_IGN` it is dropped, with the default disposition the process exits with status 142, and with a handler, or with `SIGALRM` blocked, it throws, since guest `SIGALRM` handlers are never run and signals are never left pending. The value is not raised to `tick / 16` as `itimerfix` does, and an interval below 1 ms throws, since the console's `hz`, and so that rounding, is unknown (1 ms covers any `hz` of 63 or more). `ITIMER_VIRTUAL` and `ITIMER_PROF` throw when armed, as process CPU time is not accounted, and read and disarm as zero. PPSA99420 imports it and fails to load without it - On Linux, [direct memory](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) lives in one sparse memfd at file offset = physical address, so the [GPU imports](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) can share one udmabuf buffer object per 64 MiB of it. Released direct memory is not punched out of the file, because a live import of its chunk pins the pages and the GPU would keep reading the old ones: its pages stay committed (the process keeps the high-water mark of the direct memory it used) and are zeroed when the range is allocated again. - [system](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) runs no command. It follows FreeBSD 11's `lib/libc/stdlib/system.c` for a sandbox without `/bin/sh`: a null command returns 1 unconditionally, as that version does, and any command returns the wait status of a child that could not exec the shell and called `_exit(127)` (`0x7f00`). Whether the console's application sandbox has no shell, and whether `vfork` succeeds there (FreeBSD returns -1 when it fails), is unverified -- [sceKernelWriteThrottlingStatus](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) reports no write throttling: the host SSD has no write budget, so the first qword of the 32-byte status is set to `UINT32_MAX` and the rest is zeroed. PPSA17221 reads only that qword: it multiplies it by 64 KiB and compares the product with byte thresholds up to 0x4AF00000 to pick one of five tiers. When the call fails the title assumes 0x4B000001 bytes, which selects the same top tier as `UINT32_MAX`. What the value measures on hardware (a remaining budget or a rate) and the meaning of the other three qwords are unknown +- [sceKernelWriteThrottlingStatus](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) reports no write throttling: the host SSD has no write budget, so the first qword of the 32-byte status is set to `UINT32_MAX` and the rest is zeroed; returns `EFAULT` (`0x8002000e`) when passed a null status buffer pointer. PPSA17221 reads only that qword: it multiplies it by 64 KiB and compares the product with byte thresholds up to 0x4AF00000 to pick one of five tiers. When the call fails the title assumes 0x4B000001 bytes, which selects the same top tier as `UINT32_MAX`. What the value measures on hardware (a remaining budget or a rate) and the meaning of the other three qwords are unknown - [Windows directory descriptors](../../../core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp) (libkernel) take one snapshot of the directory, on the first `getdents`, `getdirentries` or non-zero `lseek` (`SEEK_SET` or `SEEK_CUR`), and keep it until it is discarded; `lseek` to offset 0 only discards the snapshot, and the next read lists the directory again, so every rewind costs O(entries) on the next read. `lseek` takes the offset as an entry index and `SEEK_CUR` with offset 0 only reports the index. `SEEK_END` throws, where the NUL descriptor used to return 0, because FreeBSD defines it relative to the directory size, which is not modelled. A negative result returns `EINVAL` and a `SEEK_CUR` offset that overflows returns `EOVERFLOW`, as FreeBSD `vn_seek` reports them. Names whose UTF-8 encoding exceeds the one-byte `d_namlen` limit are not representable in this guest directory-record layout: `getdents` returns `ENAMETOOLONG` without consuming the entry. Repeated calls therefore report the same error and entries after that name cannot be reached through this descriptor; FreeBSD's `getdents` behavior for host filenames that exceed its `MAXNAMLEN` is not verified. `getdirentries` still reports 0 through `basep` instead of the position of the block it read, so only 0 and values returned by `lseek` are valid offsets; FreeBSD offsets are filesystem cookies - [/dev/random and /dev/urandom](../../../core/libs/prx/libkernel/File/src/RandomDevice.cpp) (libkernel) opened for reading return a descriptor backed by the host null device whose reads are filled with host OS entropy (`getentropy`, `rand_s` on Windows); both paths behave the same, never block, and positioned reads ignore the offset (FreeBSD `random(4)`). Only the exact paths are recognised, `fstat`, `lseek` and `mmap` see the null device, and opening them for writing throws - [pthread_mutexattr_getpshared and pthread_mutexattr_setpshared](../../../core/libs/prx/libkernel/Pthread/Posix/Mutex.cpp) (libkernel) store and report private or shared attributes, following FreeBSD 11 libthr [thr_mutexattr.c](https://github.com/freebsd/freebsd-src/blob/stable/11/lib/libthr/thread/thr_mutexattr.c). Invalid values return `EINVAL` without changing the attribute. Guest threads execute in one host process; sharing mutexes across processes is not modeled. Console behavior and invalid-attribute error precedence are unverified. From bd2197e9197594ad9cbe451885ef7b0e6ab5bde5 Mon Sep 17 00:00:00 2001 From: Ori Shamir Date: Sat, 17 Oct 2026 15:35:18 +0300 Subject: [PATCH 0737/1000] fix(agc): wake an idle queue 0 for completions the draw pipeline adds With pipelined draws, queue 0 hands the end-of-pipe labels of a submission to the draw pipeline and can enter its idle wait before the pipeline commits them. When such a label raises an end-of-pipe interrupt behind recorded work, the pipeline attaches the delivery to the batch as a completion and submits it, but queue 0 only reaps when it saw completions pending before it went to sleep, and nothing woke it. The batch was then reaped, and the interrupt delivered, only when the next submission arrived, so a title that waits for that interrupt before submitting more work stopped. Queue 0's idle wait now also ends when completions are pending, and the pipeline's end of a submission (and the REWIND path's open-work submit) wakes it when they are. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 2 + .../Execution/include/Driver/Driver.hpp | 1 + .../src/Driver/Packets/PacketExecution.cpp | 5 +- .../src/Driver/Queues/QueueWorker.cpp | 12 +- .../execution/QueueZeroLateInterrupt.cpp | 128 ++++++++++++++++++ 5 files changed, 144 insertions(+), 4 deletions(-) create mode 100755 core/libs/prx/libSceAgcDriver/tests/execution/QueueZeroLateInterrupt.cpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 4eba5fa87e..58045f74ed 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1425,6 +1425,8 @@ endif() target_link_libraries(agc_driver_gpu_map_on_fiber_tests PRIVATE libSceFiber) +target_link_libraries(agc_driver_queue_zero_late_interrupt_tests PRIVATE libkernel) + target_link_libraries(agc_driver_preparation_scope_settle_tests PRIVATE libSceAgc) target_include_directories(agc_driver_alpha_to_coverage_tests PRIVATE $) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp index 4bceb20d24..84c2a17592 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp @@ -103,6 +103,7 @@ private: static bool& completionDeferred(); void deferCompletion(const Submission& submission); void submitOpenWork(); + void wakeQueueZeroForCompletions(); void completeSubmission(std::uint64_t serial, std::uint64_t received, std::span labelWrites); static bool waitFree(const Submission& submission); bool queue0Before(std::uint64_t received) const; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp index 02458b6484..1c35594004 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp @@ -411,7 +411,10 @@ void Driver::execute(const Submission& submission) { return; } if ((std::atomic_ref(*const_cast(submission.rewindTail - 1)).load(std::memory_order_acquire) & 0x80000000u) != 0) { - DrawPipeline::Queue0().Enqueue([this] { submitOpenWork(); }, {}); + DrawPipeline::Queue0().Enqueue([this] { + submitOpenWork(); + wakeQueueZeroForCompletions(); + }, {}); executeRewindTail(submission); return; } diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/QueueWorker.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/QueueWorker.cpp index bbdc2b32d0..ed1cb37640 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/QueueWorker.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/QueueWorker.cpp @@ -85,6 +85,12 @@ void Driver::submitOpenWork() { if (localDevice != nullptr) localDevice->SubmitRecorded(true); } +void Driver::wakeQueueZeroForCompletions() { + if (!completionsPending()) return; + std::lock_guard lock(mutex); + changed.notify_all(); +} + void Driver::completeSubmission(std::uint64_t serial, std::uint64_t received, std::span labelWrites) { struct Complete { Driver& driver; @@ -98,7 +104,7 @@ void Driver::completeSubmission(std::uint64_t serial, std::uint64_t received, st driver.forgetUnfinishedWrites(driver.workers.at(0), labelWrites); const auto found = std::find(driver.queue0Uncommitted.begin(), driver.queue0Uncommitted.end(), received); if (found != driver.queue0Uncommitted.end()) driver.queue0Uncommitted.erase(found); - notify = driver.idleWaiters != 0 || driver.orderHolders.load(std::memory_order_acquire) != 0; + notify = driver.idleWaiters != 0 || driver.orderHolders.load(std::memory_order_acquire) != 0 || completionsPending(); } if (notify) driver.changed.notify_all(); } @@ -214,9 +220,9 @@ void Driver::run(std::uint32_t id) noexcept { && ((!storeIdleResults && Graphics::StorageTexture::AnyPending()) || idleEpoch.load(std::memory_order_acquire) != idleEpochAtCheck)) continue; if (id == 0) queue0Dormant.store(true, std::memory_order_relaxed); const auto idleEpochAtWait = idleEpoch.load(std::memory_order_acquire); - changed.wait(lock, [&] { return ready() || (id == 0 && idleEpoch.load(std::memory_order_acquire) != idleEpochAtWait); }); + changed.wait(lock, [&] { return ready() || (id == 0 && (idleEpoch.load(std::memory_order_acquire) != idleEpochAtWait || completionsPending())); }); if (id == 0) queue0Dormant.store(false, std::memory_order_relaxed); - if (id == 0 && !ready() && idleEpoch.load(std::memory_order_acquire) != idleEpochAtWait) continue; + if (id == 0 && !ready() && (idleEpoch.load(std::memory_order_acquire) != idleEpochAtWait || completionsPending())) continue; break; } if (poll(ready)) break; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/QueueZeroLateInterrupt.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/QueueZeroLateInterrupt.cpp new file mode 100755 index 0000000000..fabc8899ae --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/QueueZeroLateInterrupt.cpp @@ -0,0 +1,128 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Synchronization/SynchronizationStatistics.hpp" +#include "prx/libSceAgcDriver/Eq/include/Event.hpp" +#include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libkernel/Equeue/Equeue.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" int APS5_VABI sceKernelCreateEqueue(KernelEqueue* eq, const char* name); +extern "C" int APS5_VABI sceKernelDeleteEqueue(KernelEqueue eq); + +namespace { + +constexpr std::uint32_t Lanes = 32; +constexpr std::uint32_t BufferBytes = Lanes * 4u; +constexpr std::size_t BlockBytes = 65536; + +alignas(256) constexpr std::array Code{0x34020082u, 0xe0301000u, 0x80000201u, 0xbf8c3f70u, 0x4a060481u, 0xe0701000u, 0x80010301u, 0xbf810000u}; + +void check(bool condition, const std::string& what) { + if (!condition) throw std::runtime_error("queue zero late interrupt: " + what); +} + +std::uint32_t* NewBlock() { +#ifdef _WIN32 + auto* block = static_cast(VirtualAlloc(nullptr, BlockBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); +#else + auto* block = static_cast(std::aligned_alloc(BlockBytes, BlockBytes)); +#endif + check(block != nullptr, "cannot allocate a guest block"); + std::memset(block, 0, BlockBytes); + GuestAllocations::Mutation().Add(block, BlockBytes, true, true, false); + return block; +} + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +} + +int main() { + try { + { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + } + struct ShutdownGuard { + ~ShutdownGuard() { try { AgcDriverShutdown_nid_postfix(); } catch (...) {} } + } shutdown; + + Shader shader{}; + shader.file_header = 0x34333231; + shader.version = 0x18; + shader.header_size = sizeof(Shader); + shader.shader_size = sizeof(Code); + shader.code = Code.data(); + AgcDriverRegisterShader_nid_postfix(&shader); + + auto* input = NewBlock(); + auto* output = NewBlock(); + auto* label = NewBlock(); + for (std::uint32_t lane = 0; lane < Lanes; ++lane) input[lane] = 0x5b000000u | lane; + + KernelEqueue eq = 0; + check(sceKernelCreateEqueue(&eq, "AGC late interrupt") == 0, "event queue creation failed"); + auto owner = EqueuePin_nid_postfix(eq); + int tag = 0; + check(sceAgcDriverAddEqEvent(eq, 0, &tag) == 0, "graphics event registration failed"); + + const auto codeAddress = reinterpret_cast(Code.data()); + const auto labelAddress = reinterpret_cast(label); + const auto in = BufferDescriptor(input, BufferBytes); + const auto out = BufferDescriptor(output, BufferBytes); + std::vector words{ + 0xc0027600u, 0x20cu, static_cast(codeAddress >> 8u), static_cast(codeAddress >> 40u), + 0xc0037600u, 0x207u, Lanes, 1u, 1u, + 0xc0017600u, 0x213u, 8u << 1u, + 0xc0087600u, 0x240u, in[0], in[1], in[2], in[3], out[0], out[1], out[2], out[3], + 0xc0031500u, 1u, 1u, 1u, 0x8041u, + 0xc0064900u, 0x06000528u, (2u << 29u) | (2u << 24u) | (1u << 16u), static_cast(labelAddress), static_cast(labelAddress >> 32u), 1u, 0u, 0u, + }; + Packet packet{words.data(), static_cast(words.size()), 0, {}}; + + const auto dormant = [] { return AgcDriver::DriverDetail::queue0Dormant.load(std::memory_order_relaxed); }; + check(sceAgcDriverSubmitDcb(&packet) == 0, "submission failed"); + + std::array events{}; + int delivered = 0; + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(2); + while ((delivered = owner->GetTriggeredEvents(events.data(), 1)) == 0 && std::chrono::steady_clock::now() < deadline) std::this_thread::sleep_for(std::chrono::milliseconds(1)); + check(delivered == 1 && events[0].udata == &tag, "an end-of-pipe interrupt the draw pipeline committed after queue 0 went idle was not delivered"); + + AgcDriverWaitIdle_nid_postfix(); + check(label[0] == 1u, "the end-of-pipe label was not written"); + for (std::uint32_t lane = 0; lane < Lanes; ++lane) check(output[lane] == input[lane] + 1u, "the dispatch before the interrupt stored a wrong value"); + + check(sceAgcDriverDeleteEqEvent(eq, 0) == 0, "graphics event deletion failed"); + owner.reset(); + check(sceKernelDeleteEqueue(eq) == 0, "event queue deletion failed"); + AgcDriverShutdown_nid_postfix(); + std::puts("queue zero late interrupt tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} From dd70ca83fed064298a39e2fb246846dd0bf3eeba Mon Sep 17 00:00:00 2001 From: Maz Date: Sat, 17 Oct 2026 17:06:37 +0400 Subject: [PATCH 0738/1000] test(libkernel): read absolute links into a buffer that fits --- core/libs/tests/GuestKernelSys.cpp | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/core/libs/tests/GuestKernelSys.cpp b/core/libs/tests/GuestKernelSys.cpp index 89a102b08d..e9f439fafe 100644 --- a/core/libs/tests/GuestKernelSys.cpp +++ b/core/libs/tests/GuestKernelSys.cpp @@ -289,13 +289,18 @@ int main() { const auto absolute = std::filesystem::absolute(root / "absolute"); Require(CreateSymbolicLinkW(absolute.wstring().c_str(), std::filesystem::absolute(original).wstring().c_str(), SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE) != 0); - std::memset(buffer, '#', sizeof(buffer)); - const auto absoluteLength = readlink_nid_postfix((root / "absolute").string().c_str(), buffer, sizeof(buffer)); - Require(absoluteLength > 0 && static_cast(absoluteLength) < sizeof(buffer) && buffer[absoluteLength] == '#'); - const std::string absoluteTarget(buffer, static_cast(absoluteLength)); - Require(absoluteTarget.find('?') == std::string::npos); + char absoluteBuffer[512]; + std::memset(absoluteBuffer, '#', sizeof(absoluteBuffer)); + const auto absoluteLength = readlink_nid_postfix((root / "absolute").string().c_str(), absoluteBuffer, sizeof(absoluteBuffer)); + const bool absoluteFits = absoluteLength > 0 && static_cast(absoluteLength) < sizeof(absoluteBuffer) && + absoluteBuffer[absoluteLength] == '#'; + const std::string absoluteTarget = absoluteFits ? std::string(absoluteBuffer, static_cast(absoluteLength)) : std::string(); std::error_code absoluteError; - Require(std::filesystem::equivalent(absoluteTarget, original, absoluteError) && !absoluteError); + const bool absoluteMatches = absoluteFits && absoluteTarget.find('?') == std::string::npos && + std::filesystem::equivalent(absoluteTarget, original, absoluteError) && !absoluteError; + if (!absoluteMatches) + std::fprintf(stderr, "absolute link returned %lld bytes\n", static_cast(absoluteLength)); + Require(absoluteMatches); #else Require(std::filesystem::is_symlink(std::filesystem::symlink_status(symbolic))); Require(readlink_nid_postfix(symbolic.c_str(), buffer, sizeof(buffer)) == 12 && std::memcmp(buffer, "original.bin", 12) == 0); From 274f092e70008495d19d80697e80a9506bccb6b8 Mon Sep 17 00:00:00 2001 From: pushkarreddyy <212133856+pushkarreddyy@users.noreply.github.com> Date: Sat, 17 Oct 2026 17:30:56 +0530 Subject: [PATCH 0739/1000] fix(libkernel): return EFAULT from sceKernelStat and sceKernelUnlink for a null path --- core/libs/prx/libkernel/File/src/Open.cpp | 11 ++++------- core/libs/tests/GuestPosixScalarIo.cpp | 6 ++++++ 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libkernel/File/src/Open.cpp b/core/libs/prx/libkernel/File/src/Open.cpp index f3a34a761f..1f04eda811 100644 --- a/core/libs/prx/libkernel/File/src/Open.cpp +++ b/core/libs/prx/libkernel/File/src/Open.cpp @@ -316,16 +316,15 @@ std::int64_t APS5_VABI sceKernelLseek(int d, std::int64_t offset, int whence) { return result; } +constexpr int GuestEfault = 14; + int APS5_VABI sceKernelStat(const char* path, FileStat* sb) { - if (path == nullptr) { - throw std::invalid_argument(std::string(__func__) + ": path is null"); - } + if (path == nullptr) return SceErrorFromErrno(GuestEfault); if (*path == '\0') return SceErrorFromErrno(2); std::filesystem::path native; if (const int error = ResolvePathError_nid_no_patch(path, native, true, true)) return SceKernelError(error); std::error_code error; constexpr int GuestEnotdir = 20; - constexpr int GuestEfault = 14; FileStat archived{}; if (File::FillArchivedStat(native, &archived)) { if (std::string_view(path).back() == '/' && (archived.st_mode & 0170000) != 0040000) return SceErrorFromErrno(GuestEnotdir); @@ -346,9 +345,7 @@ int APS5_VABI sceKernelStat(const char* path, FileStat* sb) { } int APS5_VABI sceKernelUnlink(const char* path) { - if (path == nullptr) { - throw std::invalid_argument(std::string(__func__) + ": path is null"); - } + if (path == nullptr) return SceErrorFromErrno(GuestEfault); if (*path == '\0') return SceErrorFromErrno(2); std::filesystem::path native; if (const int error = ResolvePathError_nid_no_patch(path, native)) return SceKernelError(error); diff --git a/core/libs/tests/GuestPosixScalarIo.cpp b/core/libs/tests/GuestPosixScalarIo.cpp index a7ed4abe6a..3cc3e35f7a 100644 --- a/core/libs/tests/GuestPosixScalarIo.cpp +++ b/core/libs/tests/GuestPosixScalarIo.cpp @@ -35,6 +35,8 @@ std::int64_t APS5_VABI pwrite_nid_disambig1_nid_postfix(int, const void*, std::s std::int64_t APS5_VABI sceKernelRead(int, void*, std::size_t); std::int64_t APS5_VABI sceKernelWrite(int, const void*, std::size_t); std::int64_t APS5_VABI sceKernelLseek(int, std::int64_t, int); +int APS5_VABI sceKernelStat(const char*, FileStat*); +int APS5_VABI sceKernelUnlink(const char*); } static void Check(bool value, int line) { @@ -153,6 +155,10 @@ int main() { Require(sceKernelWrite(descriptor, nullptr, 0) == 0); Require(sceKernelRead(-1, nullptr, 0) == static_cast(0x80020009u)); Require(sceKernelWrite(-1, nullptr, 0) == static_cast(0x80020009u)); + FileStat nullStat{}; + Require(sceKernelStat(nullptr, &nullStat) == static_cast(0x8002000eu)); + Require(sceKernelStat(nullptr, nullptr) == static_cast(0x8002000eu)); + Require(sceKernelUnlink(nullptr) == static_cast(0x8002000eu)); Require(close_nid_postfix(descriptor) == 0); const int readOnly = open_nid_postfix(fileName.c_str(), SCE_KERNEL_O_RDONLY, 0); Require(readOnly >= 0); From 147d58cf9a0f172e55dcd365c7aa5471b45639bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexej=20Szpev=C3=A1k?= Date: Sat, 17 Oct 2026 15:13:12 +0200 Subject: [PATCH 0740/1000] feat(libkernel): poll regular files and directories as vop_stdpoll does poll threw for any descriptor that is neither a socket nor a pipe, so a title polling a file it also reads stopped there. FreeBSD 11.4 hands a vnode to VOP_POLL, and both vop_stdpoll and vop_nopoll return POLLIN, POLLRDNORM and POLLOUT of the requested events for every event poll accepts and never POLLPRI, POLLRDBAND or POLLWRBAND. A file, a directory or a file read from app0.zar now answers that without a host call. File::IsVnode tells such a descriptor apart from the host file type without throwing, so a host socket lent by libSceNet still reaches its own path. --- .../File/include/DescriptorControl.hpp | 1 + .../libkernel/File/src/DescriptorControl.cpp | 12 +++++++ core/libs/prx/libkernel/Socket/src/Udp.cpp | 8 ++++- core/libs/tests/GuestPoll.cpp | 32 +++++++++++++++++++ docs/dev/debt/libkernel.md | 2 +- 5 files changed, 53 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libkernel/File/include/DescriptorControl.hpp b/core/libs/prx/libkernel/File/include/DescriptorControl.hpp index d7345faec2..81a9018c42 100644 --- a/core/libs/prx/libkernel/File/include/DescriptorControl.hpp +++ b/core/libs/prx/libkernel/File/include/DescriptorControl.hpp @@ -15,6 +15,7 @@ void RememberDescriptor(int fd, int openFlags); void ShareDescriptor(int from, int to); void ForgetDescriptor(int fd); bool Appends(int fd); +bool IsVnode(int fd); } diff --git a/core/libs/prx/libkernel/File/src/DescriptorControl.cpp b/core/libs/prx/libkernel/File/src/DescriptorControl.cpp index 791af5ad09..2860fa1ce7 100644 --- a/core/libs/prx/libkernel/File/src/DescriptorControl.cpp +++ b/core/libs/prx/libkernel/File/src/DescriptorControl.cpp @@ -572,4 +572,16 @@ bool Appends(int fd) { return found != g_entries.end() && found->second.status && (*found->second.status & Append) != 0; } +bool IsVnode(int fd) { + if (fd < 0 || File::IsRandomDevice(fd)) return false; + if (File::IsArchivedFile(fd)) return true; +#ifdef _WIN32 + if (!DescriptorIsOpen_nid_no_patch(fd)) return false; + return File::DirectoryDescriptorPath(fd) || ::GetFileType(WindowsDescriptorHandle_nid_no_patch(fd)) == FILE_TYPE_DISK; +#else + struct stat status{}; + return ::fstat(fd, &status) == 0 && (S_ISREG(status.st_mode) || S_ISDIR(status.st_mode)); +#endif +} + } diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index 5c6fc0b521..36ccdcefc2 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -450,6 +450,7 @@ std::shared_ptr Find(int descriptor) { } constexpr short GuestPollReadable = 0x1 | 0x40; constexpr short GuestPollWritable = 0x4; +constexpr short GuestPollVnodeReady = GuestPollReadable | GuestPollWritable; constexpr short GuestPollHangUp = 0x10; constexpr short GuestPollError = 0x8; [[noreturn]] void ThrowReadEndWritable(int descriptor) { @@ -1390,13 +1391,18 @@ int APS5_VABI poll_nid_postfix(GuestPollDescriptor* descriptors, std::uint32_t c ++ready; continue; } + if (File::IsVnode(entry.descriptor)) { + entry.revents = static_cast(entry.events & GuestPollVnodeReady); + if (entry.revents) ++ready; + continue; + } #ifdef _WIN32 short revents = 0; if (!PipeReadiness(entry.descriptor, entry.events, revents)) #else if (!IsPipe(entry.descriptor, entry.events)) #endif - throw std::runtime_error("poll: descriptor " + std::to_string(entry.descriptor) + " is neither a socket nor a pipe"); + throw std::runtime_error("poll: descriptor " + std::to_string(entry.descriptor) + " is neither a socket, a pipe, a file nor a directory"); pipes.push_back(i); continue; } diff --git a/core/libs/tests/GuestPoll.cpp b/core/libs/tests/GuestPoll.cpp index f96ae8f616..2098072586 100644 --- a/core/libs/tests/GuestPoll.cpp +++ b/core/libs/tests/GuestPoll.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include struct PollDescriptor { int descriptor; @@ -31,6 +32,9 @@ std::int64_t APS5_VABI send_nid_postfix(int, const void*, std::uint64_t, int); std::int64_t APS5_VABI recv_nid_postfix(int, void*, std::uint64_t, int); int APS5_VABI close_nid_postfix(int); int APS5_VABI open_nid_postfix(const char*, int, int); +int APS5_VABI mkdir_nid_postfix(const char*, unsigned short); +int APS5_VABI rmdir_nid_postfix(const char*); +int APS5_VABI unlink_nid_postfix(const char*); int APS5_VABI poll_nid_postfix(PollDescriptor*, std::uint32_t, int); int APS5_VABI pipe_nid_postfix(int*); std::int64_t APS5_VABI read_nid_postfix(int, void*, std::uint64_t); @@ -55,6 +59,33 @@ constexpr short Out = 0x4; constexpr short Hup = 0x10; constexpr short Invalid = 0x20; constexpr short ReadNormal = 0x40; +constexpr short WriteBand = 0x100; +static void PollsFilesAndDirectories(int socket) { + constexpr int ReadWrite = 0x0002, Create = 0x0200, DirectoryFlag = 0x00020000; + const auto stamp = std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()); + const std::string filePath = "anyps5-poll-file-" + stamp; + const std::string directoryPath = "anyps5-poll-directory-" + stamp; + const int file = open_nid_postfix(filePath.c_str(), ReadWrite | Create, 0644); + Require(file >= 0); + Require(mkdir_nid_postfix(directoryPath.c_str(), 0755) == 0); + const int directory = open_nid_postfix(directoryPath.c_str(), DirectoryFlag, 0); + Require(directory >= 0); + PollDescriptor vnode{file, static_cast(In | Out | ReadNormal | Priority | ReadBand | WriteBand), -1}; + Require(poll_nid_postfix(&vnode, 1, 0) == 1 && vnode.revents == (In | Out | ReadNormal)); + vnode.events = static_cast(Priority | ReadBand | WriteBand); + Require(poll_nid_postfix(&vnode, 1, 0) == 0 && vnode.revents == 0); + const auto start = std::chrono::steady_clock::now(); + Require(poll_nid_postfix(&vnode, 1, 30) == 0); + Require(std::chrono::steady_clock::now() - start >= std::chrono::milliseconds(25)); + PollDescriptor folder{directory, static_cast(In | Out), -1}; + Require(poll_nid_postfix(&folder, 1, -1) == 1 && folder.revents == (In | Out)); + PollDescriptor mixed[3]{{socket, In, -1}, {file, Out, -1}, {directory, ReadNormal, -1}}; + Require(poll_nid_postfix(mixed, 3, -1) == 2); + Require(mixed[0].revents == 0 && mixed[1].revents == Out && mixed[2].revents == ReadNormal); + Require(close_nid_postfix(directory) == 0 && close_nid_postfix(file) == 0); + Require(rmdir_nid_postfix(directoryPath.c_str()) == 0 && unlink_nid_postfix(filePath.c_str()) == 0); +} + static void TestPipes(int socket) { int ends[2]; Require(pipe_nid_postfix(ends) == 0); @@ -221,6 +252,7 @@ int main() { Require(poll_nid_postfix(nullptr, 1, 0) == -1 && *__error_nid_postfix() == 14); Require(poll_nid_postfix(&single, 1, -2) == -1 && *__error_nid_postfix() == 22); TestPipes(listener); + PollsFilesAndDirectories(listener); Require(close_nid_postfix(server) == 0); const int file = open_nid_postfix("/dev/urandom", 0, 0); Require(file >= 0); diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 5791f7ab5e..a79017eaf2 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -170,7 +170,7 @@ - [read, write and vector I/O](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) on a guest socket use `recv` and `send` without flags; `sceKernelReadv` and `sceKernelWritev` gather or scatter through one socket operation, preserving stream short reads and datagram boundaries. The positioned `pread`, `pwrite`, `preadv` and `pwritev` forms, and their `sceKernel` forms, return `ESPIPE` for an open guest socket and `EBADF` for a closed one. `EPIPE` from a write raises the host's `SIGPIPE` on Linux unless the socket has `SO_NOSIGPIPE` set, and the guest cannot ignore it: `signal(SIGPIPE, SIG_IGN)` and `sigaction` record the disposition, as they do for every number from 1 to 128, but only the six signals with host equivalents (2, 4, 6, 8, 11 and 15) take effect on the host or are delivered, so the host `SIGPIPE` is not stopped; nothing is raised on Windows; a `write` longer than `INT_MAX` bytes is `EMSGSIZE` where the console sends it. AIO on a guest socket still goes to the host, which has no such descriptor - [Socket descriptor numbers](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel, libSceNet) - `socket`, `accept`, `socketpair`, `dup`, `sceNetSocket` and `sceNetAccept` take the lowest free number of the host descriptor table that files use, as FreeBSD 11.4 `fdalloc` takes the lowest free entry of the one process table (`kern_descrip.c`), so every number stays below `FD_SETSIZE` (1024) while fewer than 1024 descriptors are open. The number holds a zero-access `NUL` handle on Windows and a duplicate of the host socket on Linux, and the socket tables map it to the socket. The host table also holds the runtime's own files, so the numbers differ from the console's; the first number `socket` and `sceNetSocket` return on a console and its `getdtablesize` are not measured. Paths that bypass the socket tables see the placeholder: libc `fdopen` on a socket opens it, and libkernel `close` of a libSceNet socket releases the number but leaves the libSceNet socket open. Palworld (PPSA23126), The Mound (PPSA26223) and Dying Light 2 (PPSA02262) set socket bits in a 1024-bit `fd_set` - [fstat on a guest socket](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) fills the fields FreeBSD 11's soo_stat and pru_sense_null fill and zeroes the rest: S_IFSOCK, the read bits while the receive side is open or data is queued, the write bits while the send side is open, st_size and st_blksize. Only this process's shutdown closes a side here; a peer's shutdown, close or reset does not clear the bits as FreeBSD's socantrcvmore and socantsendmore do. st_size is the host's FIONREAD, which for a Linux datagram socket is the size of the next datagram where FreeBSD counts every queued byte, and st_blksize is the host's SO_SNDBUF, which Linux reports as twice the size that was set. A socketpair end reports st_dev and st_ino 0, where FreeBSD's uipc_sense reports NODEV and a per-socket number -- [poll](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) waits on guest sockets and pipes, through `WSAPoll` on Windows and `poll` on Linux; the `pollfd` layout and flag values are FreeBSD's. A libSceNet socket joins the same host wait through the native socket libSceNet lends to libkernel. `WSAPoll` rejects `POLLPRI`, so on Windows a socket's `POLLPRI` is asked as `POLLRDBAND` and `POLLIN` as `POLLRDNORM`; Winsock reports urgent data until it is read with `MSG_OOB`, where FreeBSD 11.4's `sopoll_generic` stops once the normal stream passes the mark. Guest `send` and `recv` refuse `MSG_OOB` with `EOPNOTSUPP`, so urgent data can only come from an outside peer and Windows keeps reporting `POLLPRI` after the stream passes the mark. A pipe follows FreeBSD's `pipe_poll`: the read end is readable when data is buffered, either end reports `POLLHUP` once the other end is closed, with the requested `POLLIN` and `POLLRDNORM` bits and without `POLLOUT`, which `pollscan` clears, and the write end is writable when `PIPE_BUF` (512) bytes are free on Windows, or when the host kernel says so on Linux. `POLLOUT` on a read end throws: FreeBSD opens both ends for reading and writing and reports it writable, and host pipes are one-way. Windows pipes cannot be waited on, so while a pipe is polled `poll` checks it with `NtQueryInformationFile` and waits on the sockets in slices of 1 ms. A closed descriptor reports `POLLNVAL`, and an open descriptor that is neither a guest socket, a libSceNet socket nor a pipe (files, stdio) throws, as do event bits other than `POLLIN`, `POLLPRI`, `POLLOUT`, `POLLRDNORM`, `POLLRDBAND` and `POLLWRBAND` (the `POLLERR`, `POLLHUP` and `POLLNVAL` bits are ignored in `events`), an infinite timeout with no socket or pipe to wait on. A closed socket descriptor reports `POLLNVAL`; the console's limit on the descriptor count is unknown and not checked +- [poll](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) waits on guest sockets and pipes, through `WSAPoll` on Windows and `poll` on Linux; the `pollfd` layout and flag values are FreeBSD's. A libSceNet socket joins the same host wait through the native socket libSceNet lends to libkernel. `WSAPoll` rejects `POLLPRI`, so on Windows a socket's `POLLPRI` is asked as `POLLRDBAND` and `POLLIN` as `POLLRDNORM`; Winsock reports urgent data until it is read with `MSG_OOB`, where FreeBSD 11.4's `sopoll_generic` stops once the normal stream passes the mark. Guest `send` and `recv` refuse `MSG_OOB` with `EOPNOTSUPP`, so urgent data can only come from an outside peer and Windows keeps reporting `POLLPRI` after the stream passes the mark. A pipe follows FreeBSD's `pipe_poll`: the read end is readable when data is buffered, either end reports `POLLHUP` once the other end is closed, with the requested `POLLIN` and `POLLRDNORM` bits and without `POLLOUT`, which `pollscan` clears, and the write end is writable when `PIPE_BUF` (512) bytes are free on Windows, or when the host kernel says so on Linux. `POLLOUT` on a read end throws: FreeBSD opens both ends for reading and writing and reports it writable, and host pipes are one-way. Windows pipes cannot be waited on, so while a pipe is polled `poll` checks it with `NtQueryInformationFile` and waits on the sockets in slices of 1 ms. A regular file, a directory or a file read from app0.zar is always ready for `POLLIN`, `POLLRDNORM` and `POLLOUT` and never for `POLLPRI`, `POLLRDBAND` or `POLLWRBAND`, as FreeBSD 11.4's [`vop_stdpoll`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/vfs_default.c#L563-L574) and `vop_nopoll` answer every event `poll` accepts; which of the two the console's filesystems use is not known, and they agree on these events. A closed descriptor reports `POLLNVAL`, and an open descriptor that is neither a guest socket, a libSceNet socket, a pipe, a file nor a directory (stdio, devices) throws, as do event bits other than `POLLIN`, `POLLPRI`, `POLLOUT`, `POLLRDNORM`, `POLLRDBAND` and `POLLWRBAND` (the `POLLERR`, `POLLHUP` and `POLLNVAL` bits are ignored in `events`), an infinite timeout with no socket or pipe to wait on. A closed socket descriptor reports `POLLNVAL`; the console's limit on the descriptor count is unknown and not checked - [kevent on a pipe](../../../core/libs/prx/libkernel/Equeue/src/Kqueue.cpp) (libkernel) - `EVFILT_READ` on the read end of a pipe follows FreeBSD 11's [`filt_piperead`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/sys_pipe.c#L1787-L1806): `data` is the number of buffered bytes, and `EV_EOF` is set once the write end is closed. Windows pipes cannot be waited on, so while a kqueue waits on a pipe it checks it with `NtQueryInformationFile` and waits on its sockets in slices of 1 ms; a wait with no pipe is not sliced. `EVFILT_READ` on the write end of a pipe throws: FreeBSD's `pipe` opens both ends for reading and writing ([`kern_pipe`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/sys_pipe.c#L422)), and host pipes are one-way. `EVFILT_WRITE` on a pipe and `NOTE_LOWAT` on a pipe, which `filt_piperead` ignores, throw too. - [Socket error numbers](../../../core/libs/prx/libkernel/Socket/include/SocketError.hpp) (libkernel) - a host socket error becomes the FreeBSD 11 `errno.h` number of the same name; the numbers are not checked on the console. Winsock `WSAESHUTDOWN` becomes `EPIPE`, as a send after `shutdown` does on FreeBSD, and a host error with no FreeBSD socket counterpart becomes `EIO`. A nonblocking `connect` in progress reports `EINPROGRESS` on Linux but `EAGAIN` on Windows (`WSAEWOULDBLOCK`), where FreeBSD reports `EINPROGRESS`. On Windows a refused nonblocking `connect` is never reported (`SO_ERROR` is unsupported), so a repeated one keeps reporting `EALREADY` where FreeBSD reports `ECONNREFUSED`. A receive after `shutdown(SHUT_RD)` reports `EPIPE` on Windows where FreeBSD returns 0 - [getsockopt, setsockopt](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) on a guest socket handle the SOL_SOCKET level and `TCP_NODELAY`, after FreeBSD 11.4's [`sosetopt` and `sogetopt`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c): `SO_REUSEADDR`, `SO_KEEPALIVE`, `SO_DONTROUTE`, `SO_BROADCAST`, `SO_OOBINLINE` and `SO_LINGER` act on the host socket, and `SO_ERROR`, `SO_TYPE`, `SO_ACCEPTCONN`, `SO_SNDBUF` and `SO_RCVBUF` are read. A boolean option reads back as its own bit, as FreeBSD returns `so_options & option`. Winsock does not clear a socket's pending error when it is read, so on Windows the library reports a pending error once and shows it again only after a new `connect`; a second failure with the same code on the same socket without a `connect` in between would be hidden. `SO_SNDBUF` and `SO_RCVBUF` read the host's value, which is not the size FreeBSD reports (Linux doubles it). `SO_NOSIGPIPE` is kept per socket and reads back as its bit. On Linux a `send`, `sendto`, `sendmsg` or `write` on that socket passes `MSG_NOSIGNAL` to the host, as [`kern_sendit`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_syscalls.c#L890-L895) and [`soo_write`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/sys_socket.c#L147-L151) leave out the signal. An accepted socket takes it from its listener, as [`sonewconn`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L603) copies the options. `SO_NBIO` (`0x1200`) is the console's own option, not FreeBSD's. It switches the socket between blocking and non-blocking as `FIONBIO` does and reads back as 0 or 1, as `sceNetSetsockopt` and `sceNetGetsockopt` treat it here; what the console reads back for it is not measured. `TCP_NODELAY` (`IPPROTO_TCP`, 1) is set on the host socket and reads back as `TF_NODELAY` (4), as [`tcp_default_ctloutput`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/netinet/tcp_usrreq.c#L1860-L1861) returns it. The `IPPROTO_TCP` level on a datagram or `AF_UNIX` socket fails with `EINVAL`, as [`ip_ctloutput`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/netinet/ip_output.c#L943-L944) and [`uipc_ctloutput`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_usrreq.c#L1228-L1229) answer it, and an accepted socket takes `TCP_NODELAY` through the host. `SO_SNDTIMEO` and `SO_RCVTIMEO` follow [`sosetopt`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L2657-L2689) and [`sogetopt`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L2846-L2850). A 16-byte `timeval` with a negative field or 1000000 microseconds or more fails with `EDOM`, and more than `INT32_MAX` seconds becomes the longest wait. The value is kept and read back through FreeBSD's 32.32 fixed-point conversion ([`tvtosbt`, `sbttotv`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/sys/time.h#L178-L286)), so half a second reads back as 499999 microseconds and one microsecond as 0. A `recv`, `recvfrom`, `recvmsg`, `read` or `readv`, and a `send`, `sendto`, `sendmsg`, `write` or `writev`, on a blocking socket then waits that long for the host to report the socket readable or writable, and fails with `EWOULDBLOCK`; a call with `MSG_DONTWAIT` does not wait. An accepted socket takes both from its listener ([`sonewconn`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L629-L630)). The wait covers only the first transfer: once the socket is writable the host send runs without a limit, where FreeBSD stops a send that cannot finish and returns the bytes sent so far, and a receive whose data another thread takes between the wait and the host call blocks again. `sendfile` ignores the send timeout. The host wait has millisecond steps, so a shorter timeout lasts up to a millisecond longer. A null value fails with `EINVAL`, where FreeBSD's [`kern_setsockopt`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_syscalls.c#L1414-L1415) returns `EFAULT`. Every other SOL_SOCKET option (`SO_DEBUG`, `SO_USELOOPBACK`, `SO_REUSEPORT` and `SO_TIMESTAMP`, which FreeBSD stores and no title has been seen to set, the low-water marks among them), every other `IPPROTO_TCP` option (`TCP_MAXSEG`, `TCP_KEEPIDLE`, `TCP_KEEPINTVL` and `TCP_KEEPCNT` among them) and every other level (`IPPROTO_IP`, `IPPROTO_IPV6`) fail with `ENOPROTOOPT`. None of this was measured on a console. From 03ae66bea499427515132edfda849a54a4b9e1a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexej=20Szpev=C3=A1k?= Date: Sat, 17 Oct 2026 15:13:43 +0200 Subject: [PATCH 0741/1000] feat(libkernel): poll the random device as randomdev_poll does poll threw for /dev/random and /dev/urandom, which select already treats as ready. FreeBSD 11.4's randomdev_poll, once the device is seeded, narrows a request that has POLLIN or POLLRDNORM to those bits and returns any other request whole, POLLOUT and the band bits included. poll now answers that, and the test that relied on the random device throwing uses a kqueue descriptor, which poll still does not support. --- core/libs/prx/libkernel/Socket/src/Udp.cpp | 7 +++++++ core/libs/tests/GuestPoll.cpp | 24 +++++++++++++++++----- core/libs/tests/GuestSocketDescriptors.cpp | 2 +- docs/dev/debt/libkernel.md | 2 +- 4 files changed, 28 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index 36ccdcefc2..2f533cea64 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -26,6 +26,7 @@ #include "prx/libkernel/File/include/DescriptorControl.hpp" #include "prx/libkernel/Equeue/Equeue.hpp" #include "prx/libkernel/File/include/File.hpp" +#include "prx/libkernel/File/include/RandomDevice.hpp" #include "prx/libkernel/Time/include/TimedWait.hpp" #include "prx/libkernel/Socket/include/SocketPoll.hpp" #include "prx/libkernel/Socket/include/SocketConnect.hpp" @@ -1391,6 +1392,12 @@ int APS5_VABI poll_nid_postfix(GuestPollDescriptor* descriptors, std::uint32_t c ++ready; continue; } + if (File::IsRandomDevice(entry.descriptor)) { + const short readable = static_cast(entry.events & GuestPollReadable); + entry.revents = readable != 0 ? readable : entry.events; + if (entry.revents) ++ready; + continue; + } if (File::IsVnode(entry.descriptor)) { entry.revents = static_cast(entry.events & GuestPollVnodeReady); if (entry.revents) ++ready; diff --git a/core/libs/tests/GuestPoll.cpp b/core/libs/tests/GuestPoll.cpp index 2098072586..5d229666e8 100644 --- a/core/libs/tests/GuestPoll.cpp +++ b/core/libs/tests/GuestPoll.cpp @@ -37,6 +37,7 @@ int APS5_VABI rmdir_nid_postfix(const char*); int APS5_VABI unlink_nid_postfix(const char*); int APS5_VABI poll_nid_postfix(PollDescriptor*, std::uint32_t, int); int APS5_VABI pipe_nid_postfix(int*); +int APS5_VABI kqueue_nid_postfix(); std::int64_t APS5_VABI read_nid_postfix(int, void*, std::uint64_t); std::int64_t APS5_VABI write_nid_postfix(int, const char*, std::int64_t); int APS5_VABI fcntl_nid_postfix(int, int, ...); @@ -254,16 +255,29 @@ int main() { TestPipes(listener); PollsFilesAndDirectories(listener); Require(close_nid_postfix(server) == 0); - const int file = open_nid_postfix("/dev/urandom", 0, 0); - Require(file >= 0); - PollDescriptor mixed[2]{{listener, In, 0}, {file, In, 0}}; + for (const char* path : {"/dev/urandom", "/dev/random"}) { + const int random = open_nid_postfix(path, 0, 0); + Require(random >= 0); + PollDescriptor device{random, static_cast(In | Out | Priority), -1}; + Require(poll_nid_postfix(&device, 1, 0) == 1 && device.revents == In); + device.events = static_cast(ReadNormal | Out); + Require(poll_nid_postfix(&device, 1, 0) == 1 && device.revents == ReadNormal); + device.events = static_cast(Out | Priority | ReadBand | WriteBand); + Require(poll_nid_postfix(&device, 1, -1) == 1 && device.revents == (Out | Priority | ReadBand | WriteBand)); + PollDescriptor mixed[2]{{listener, In, 0}, {random, Out, 0}}; + Require(poll_nid_postfix(mixed, 2, -1) == 1 && mixed[0].revents == 0 && mixed[1].revents == Out); + Require(close_nid_postfix(random) == 0); + } + const int queue = kqueue_nid_postfix(); + Require(queue >= 0); + PollDescriptor unknown[2]{{listener, In, 0}, {queue, In, 0}}; bool unsupported = false; try { - poll_nid_postfix(mixed, 2, 0); + poll_nid_postfix(unknown, 2, 0); } catch (const std::runtime_error&) { unsupported = true; } Require(unsupported); - Require(close_nid_postfix(file) == 0); + Require(close_nid_postfix(queue) == 0); Require(close_nid_postfix(listener) == 0); } diff --git a/core/libs/tests/GuestSocketDescriptors.cpp b/core/libs/tests/GuestSocketDescriptors.cpp index e8340085ae..e99dc9b442 100644 --- a/core/libs/tests/GuestSocketDescriptors.cpp +++ b/core/libs/tests/GuestSocketDescriptors.cpp @@ -156,7 +156,7 @@ static void CloseReleasesNumber() { unsigned char bytes[4]{}; Require(read_nid_postfix(socket, bytes, sizeof(bytes)) == sizeof(bytes)); PollDescriptor entry{socket, In, 0}; - Require(Throws([&] { poll_nid_postfix(&entry, 1, 0); })); + Require(poll_nid_postfix(&entry, 1, 0) == 1 && entry.revents == In); for (const int file : files) Require(close_nid_postfix(file) == 0); const int kernelClosed = Socket(); Require(sceKernelClose(kernelClosed) == 0); diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index a79017eaf2..30e9e8e2b9 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -170,7 +170,7 @@ - [read, write and vector I/O](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) on a guest socket use `recv` and `send` without flags; `sceKernelReadv` and `sceKernelWritev` gather or scatter through one socket operation, preserving stream short reads and datagram boundaries. The positioned `pread`, `pwrite`, `preadv` and `pwritev` forms, and their `sceKernel` forms, return `ESPIPE` for an open guest socket and `EBADF` for a closed one. `EPIPE` from a write raises the host's `SIGPIPE` on Linux unless the socket has `SO_NOSIGPIPE` set, and the guest cannot ignore it: `signal(SIGPIPE, SIG_IGN)` and `sigaction` record the disposition, as they do for every number from 1 to 128, but only the six signals with host equivalents (2, 4, 6, 8, 11 and 15) take effect on the host or are delivered, so the host `SIGPIPE` is not stopped; nothing is raised on Windows; a `write` longer than `INT_MAX` bytes is `EMSGSIZE` where the console sends it. AIO on a guest socket still goes to the host, which has no such descriptor - [Socket descriptor numbers](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel, libSceNet) - `socket`, `accept`, `socketpair`, `dup`, `sceNetSocket` and `sceNetAccept` take the lowest free number of the host descriptor table that files use, as FreeBSD 11.4 `fdalloc` takes the lowest free entry of the one process table (`kern_descrip.c`), so every number stays below `FD_SETSIZE` (1024) while fewer than 1024 descriptors are open. The number holds a zero-access `NUL` handle on Windows and a duplicate of the host socket on Linux, and the socket tables map it to the socket. The host table also holds the runtime's own files, so the numbers differ from the console's; the first number `socket` and `sceNetSocket` return on a console and its `getdtablesize` are not measured. Paths that bypass the socket tables see the placeholder: libc `fdopen` on a socket opens it, and libkernel `close` of a libSceNet socket releases the number but leaves the libSceNet socket open. Palworld (PPSA23126), The Mound (PPSA26223) and Dying Light 2 (PPSA02262) set socket bits in a 1024-bit `fd_set` - [fstat on a guest socket](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) fills the fields FreeBSD 11's soo_stat and pru_sense_null fill and zeroes the rest: S_IFSOCK, the read bits while the receive side is open or data is queued, the write bits while the send side is open, st_size and st_blksize. Only this process's shutdown closes a side here; a peer's shutdown, close or reset does not clear the bits as FreeBSD's socantrcvmore and socantsendmore do. st_size is the host's FIONREAD, which for a Linux datagram socket is the size of the next datagram where FreeBSD counts every queued byte, and st_blksize is the host's SO_SNDBUF, which Linux reports as twice the size that was set. A socketpair end reports st_dev and st_ino 0, where FreeBSD's uipc_sense reports NODEV and a per-socket number -- [poll](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) waits on guest sockets and pipes, through `WSAPoll` on Windows and `poll` on Linux; the `pollfd` layout and flag values are FreeBSD's. A libSceNet socket joins the same host wait through the native socket libSceNet lends to libkernel. `WSAPoll` rejects `POLLPRI`, so on Windows a socket's `POLLPRI` is asked as `POLLRDBAND` and `POLLIN` as `POLLRDNORM`; Winsock reports urgent data until it is read with `MSG_OOB`, where FreeBSD 11.4's `sopoll_generic` stops once the normal stream passes the mark. Guest `send` and `recv` refuse `MSG_OOB` with `EOPNOTSUPP`, so urgent data can only come from an outside peer and Windows keeps reporting `POLLPRI` after the stream passes the mark. A pipe follows FreeBSD's `pipe_poll`: the read end is readable when data is buffered, either end reports `POLLHUP` once the other end is closed, with the requested `POLLIN` and `POLLRDNORM` bits and without `POLLOUT`, which `pollscan` clears, and the write end is writable when `PIPE_BUF` (512) bytes are free on Windows, or when the host kernel says so on Linux. `POLLOUT` on a read end throws: FreeBSD opens both ends for reading and writing and reports it writable, and host pipes are one-way. Windows pipes cannot be waited on, so while a pipe is polled `poll` checks it with `NtQueryInformationFile` and waits on the sockets in slices of 1 ms. A regular file, a directory or a file read from app0.zar is always ready for `POLLIN`, `POLLRDNORM` and `POLLOUT` and never for `POLLPRI`, `POLLRDBAND` or `POLLWRBAND`, as FreeBSD 11.4's [`vop_stdpoll`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/vfs_default.c#L563-L574) and `vop_nopoll` answer every event `poll` accepts; which of the two the console's filesystems use is not known, and they agree on these events. A closed descriptor reports `POLLNVAL`, and an open descriptor that is neither a guest socket, a libSceNet socket, a pipe, a file nor a directory (stdio, devices) throws, as do event bits other than `POLLIN`, `POLLPRI`, `POLLOUT`, `POLLRDNORM`, `POLLRDBAND` and `POLLWRBAND` (the `POLLERR`, `POLLHUP` and `POLLNVAL` bits are ignored in `events`), an infinite timeout with no socket or pipe to wait on. A closed socket descriptor reports `POLLNVAL`; the console's limit on the descriptor count is unknown and not checked +- [poll](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) waits on guest sockets and pipes, through `WSAPoll` on Windows and `poll` on Linux; the `pollfd` layout and flag values are FreeBSD's. A libSceNet socket joins the same host wait through the native socket libSceNet lends to libkernel. `WSAPoll` rejects `POLLPRI`, so on Windows a socket's `POLLPRI` is asked as `POLLRDBAND` and `POLLIN` as `POLLRDNORM`; Winsock reports urgent data until it is read with `MSG_OOB`, where FreeBSD 11.4's `sopoll_generic` stops once the normal stream passes the mark. Guest `send` and `recv` refuse `MSG_OOB` with `EOPNOTSUPP`, so urgent data can only come from an outside peer and Windows keeps reporting `POLLPRI` after the stream passes the mark. A pipe follows FreeBSD's `pipe_poll`: the read end is readable when data is buffered, either end reports `POLLHUP` once the other end is closed, with the requested `POLLIN` and `POLLRDNORM` bits and without `POLLOUT`, which `pollscan` clears, and the write end is writable when `PIPE_BUF` (512) bytes are free on Windows, or when the host kernel says so on Linux. `POLLOUT` on a read end throws: FreeBSD opens both ends for reading and writing and reports it writable, and host pipes are one-way. Windows pipes cannot be waited on, so while a pipe is polled `poll` checks it with `NtQueryInformationFile` and waits on the sockets in slices of 1 ms. A regular file, a directory or a file read from app0.zar is always ready for `POLLIN`, `POLLRDNORM` and `POLLOUT` and never for `POLLPRI`, `POLLRDBAND` or `POLLWRBAND`, as FreeBSD 11.4's [`vop_stdpoll`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/vfs_default.c#L563-L574) and `vop_nopoll` answer every event `poll` accepts; which of the two the console's filesystems use is not known, and they agree on these events. `/dev/random` and `/dev/urandom` follow FreeBSD 11.4's [`randomdev_poll`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/dev/random/randomdev.c#L274-L285) for a seeded device: a request with `POLLIN` or `POLLRDNORM` gets only those bits back, and any other request gets every requested bit. A closed descriptor reports `POLLNVAL`, and an open descriptor that is neither a guest socket, a libSceNet socket, a pipe, a file, a directory nor the random device (stdio, other devices) throws, as do event bits other than `POLLIN`, `POLLPRI`, `POLLOUT`, `POLLRDNORM`, `POLLRDBAND` and `POLLWRBAND` (the `POLLERR`, `POLLHUP` and `POLLNVAL` bits are ignored in `events`), an infinite timeout with no socket or pipe to wait on. A closed socket descriptor reports `POLLNVAL`; the console's limit on the descriptor count is unknown and not checked - [kevent on a pipe](../../../core/libs/prx/libkernel/Equeue/src/Kqueue.cpp) (libkernel) - `EVFILT_READ` on the read end of a pipe follows FreeBSD 11's [`filt_piperead`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/sys_pipe.c#L1787-L1806): `data` is the number of buffered bytes, and `EV_EOF` is set once the write end is closed. Windows pipes cannot be waited on, so while a kqueue waits on a pipe it checks it with `NtQueryInformationFile` and waits on its sockets in slices of 1 ms; a wait with no pipe is not sliced. `EVFILT_READ` on the write end of a pipe throws: FreeBSD's `pipe` opens both ends for reading and writing ([`kern_pipe`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/sys_pipe.c#L422)), and host pipes are one-way. `EVFILT_WRITE` on a pipe and `NOTE_LOWAT` on a pipe, which `filt_piperead` ignores, throw too. - [Socket error numbers](../../../core/libs/prx/libkernel/Socket/include/SocketError.hpp) (libkernel) - a host socket error becomes the FreeBSD 11 `errno.h` number of the same name; the numbers are not checked on the console. Winsock `WSAESHUTDOWN` becomes `EPIPE`, as a send after `shutdown` does on FreeBSD, and a host error with no FreeBSD socket counterpart becomes `EIO`. A nonblocking `connect` in progress reports `EINPROGRESS` on Linux but `EAGAIN` on Windows (`WSAEWOULDBLOCK`), where FreeBSD reports `EINPROGRESS`. On Windows a refused nonblocking `connect` is never reported (`SO_ERROR` is unsupported), so a repeated one keeps reporting `EALREADY` where FreeBSD reports `ECONNREFUSED`. A receive after `shutdown(SHUT_RD)` reports `EPIPE` on Windows where FreeBSD returns 0 - [getsockopt, setsockopt](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) on a guest socket handle the SOL_SOCKET level and `TCP_NODELAY`, after FreeBSD 11.4's [`sosetopt` and `sogetopt`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c): `SO_REUSEADDR`, `SO_KEEPALIVE`, `SO_DONTROUTE`, `SO_BROADCAST`, `SO_OOBINLINE` and `SO_LINGER` act on the host socket, and `SO_ERROR`, `SO_TYPE`, `SO_ACCEPTCONN`, `SO_SNDBUF` and `SO_RCVBUF` are read. A boolean option reads back as its own bit, as FreeBSD returns `so_options & option`. Winsock does not clear a socket's pending error when it is read, so on Windows the library reports a pending error once and shows it again only after a new `connect`; a second failure with the same code on the same socket without a `connect` in between would be hidden. `SO_SNDBUF` and `SO_RCVBUF` read the host's value, which is not the size FreeBSD reports (Linux doubles it). `SO_NOSIGPIPE` is kept per socket and reads back as its bit. On Linux a `send`, `sendto`, `sendmsg` or `write` on that socket passes `MSG_NOSIGNAL` to the host, as [`kern_sendit`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_syscalls.c#L890-L895) and [`soo_write`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/sys_socket.c#L147-L151) leave out the signal. An accepted socket takes it from its listener, as [`sonewconn`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L603) copies the options. `SO_NBIO` (`0x1200`) is the console's own option, not FreeBSD's. It switches the socket between blocking and non-blocking as `FIONBIO` does and reads back as 0 or 1, as `sceNetSetsockopt` and `sceNetGetsockopt` treat it here; what the console reads back for it is not measured. `TCP_NODELAY` (`IPPROTO_TCP`, 1) is set on the host socket and reads back as `TF_NODELAY` (4), as [`tcp_default_ctloutput`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/netinet/tcp_usrreq.c#L1860-L1861) returns it. The `IPPROTO_TCP` level on a datagram or `AF_UNIX` socket fails with `EINVAL`, as [`ip_ctloutput`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/netinet/ip_output.c#L943-L944) and [`uipc_ctloutput`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_usrreq.c#L1228-L1229) answer it, and an accepted socket takes `TCP_NODELAY` through the host. `SO_SNDTIMEO` and `SO_RCVTIMEO` follow [`sosetopt`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L2657-L2689) and [`sogetopt`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L2846-L2850). A 16-byte `timeval` with a negative field or 1000000 microseconds or more fails with `EDOM`, and more than `INT32_MAX` seconds becomes the longest wait. The value is kept and read back through FreeBSD's 32.32 fixed-point conversion ([`tvtosbt`, `sbttotv`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/sys/time.h#L178-L286)), so half a second reads back as 499999 microseconds and one microsecond as 0. A `recv`, `recvfrom`, `recvmsg`, `read` or `readv`, and a `send`, `sendto`, `sendmsg`, `write` or `writev`, on a blocking socket then waits that long for the host to report the socket readable or writable, and fails with `EWOULDBLOCK`; a call with `MSG_DONTWAIT` does not wait. An accepted socket takes both from its listener ([`sonewconn`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L629-L630)). The wait covers only the first transfer: once the socket is writable the host send runs without a limit, where FreeBSD stops a send that cannot finish and returns the bytes sent so far, and a receive whose data another thread takes between the wait and the host call blocks again. `sendfile` ignores the send timeout. The host wait has millisecond steps, so a shorter timeout lasts up to a millisecond longer. A null value fails with `EINVAL`, where FreeBSD's [`kern_setsockopt`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_syscalls.c#L1414-L1415) returns `EFAULT`. Every other SOL_SOCKET option (`SO_DEBUG`, `SO_USELOOPBACK`, `SO_REUSEPORT` and `SO_TIMESTAMP`, which FreeBSD stores and no title has been seen to set, the low-water marks among them), every other `IPPROTO_TCP` option (`TCP_MAXSEG`, `TCP_KEEPIDLE`, `TCP_KEEPINTVL` and `TCP_KEEPCNT` among them) and every other level (`IPPROTO_IP`, `IPPROTO_IPV6`) fail with `ENOPROTOOPT`. None of this was measured on a console. From ff4ecc6ee3b36d84185116e431f4a33e202cb180 Mon Sep 17 00:00:00 2001 From: Ori Shamir Date: Sat, 17 Oct 2026 16:19:04 +0300 Subject: [PATCH 0742/1000] test(agc): order the late interrupt's commit after queue 0 waits The test relied on queue 0 reaching its idle wait before the draw pipeline committed the end-of-pipe label. It now forces that order: a first submission runs the dispatch and leaves its batch unreaped (APS5_NO_OPPORTUNISTIC_REAP), queue 0 must be waiting, then the label is submitted while the test holds the GPU mutex, so the pipeline commits it only after queue 0 handed it over and waits again. Co-Authored-By: Claude Opus 5.5 --- .../execution/QueueZeroLateInterrupt.cpp | 33 ++++++++++++++++--- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/QueueZeroLateInterrupt.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/QueueZeroLateInterrupt.cpp index fabc8899ae..00f3ca4d2e 100755 --- a/core/libs/prx/libSceAgcDriver/tests/execution/QueueZeroLateInterrupt.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/QueueZeroLateInterrupt.cpp @@ -1,6 +1,7 @@ #include "prx/libSceAgcDriver/Execution/include/Driver.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Synchronization/SynchronizationStatistics.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Eq/include/Event.hpp" #include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" #include "prx/libc/include/GuestAllocations.hpp" @@ -20,6 +21,7 @@ #include #include #include +#include #include #include #include @@ -60,6 +62,11 @@ std::array BufferDescriptor(const void* data, std::uint32_t by } int main() { +#ifdef _WIN32 + _putenv_s("APS5_NO_OPPORTUNISTIC_REAP", "1"); +#else + setenv("APS5_NO_OPPORTUNISTIC_REAP", "1", 1); +#endif try { { const auto device = OpenVulkanTestDevice(); @@ -92,18 +99,34 @@ int main() { const auto labelAddress = reinterpret_cast(label); const auto in = BufferDescriptor(input, BufferBytes); const auto out = BufferDescriptor(output, BufferBytes); - std::vector words{ + std::vector dispatch{ 0xc0027600u, 0x20cu, static_cast(codeAddress >> 8u), static_cast(codeAddress >> 40u), 0xc0037600u, 0x207u, Lanes, 1u, 1u, 0xc0017600u, 0x213u, 8u << 1u, 0xc0087600u, 0x240u, in[0], in[1], in[2], in[3], out[0], out[1], out[2], out[3], 0xc0031500u, 1u, 1u, 1u, 0x8041u, - 0xc0064900u, 0x06000528u, (2u << 29u) | (2u << 24u) | (1u << 16u), static_cast(labelAddress), static_cast(labelAddress >> 32u), 1u, 0u, 0u, }; - Packet packet{words.data(), static_cast(words.size()), 0, {}}; - + std::vector release{0xc0064900u, 0x06000528u, (2u << 29u) | (2u << 24u) | (1u << 16u), static_cast(labelAddress), static_cast(labelAddress >> 32u), 1u, 0u, 0u}; + const auto submit = [](std::vector& words) { + Packet packet{words.data(), static_cast(words.size()), 0, {}}; + check(sceAgcDriverSubmitDcb(&packet) == 0, "submission failed"); + }; + const auto waitFor = [](const auto& done, const char* what) { + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(2); + while (!done() && std::chrono::steady_clock::now() < deadline) std::this_thread::sleep_for(std::chrono::milliseconds(1)); + check(done(), what); + }; const auto dormant = [] { return AgcDriver::DriverDetail::queue0Dormant.load(std::memory_order_relaxed); }; - check(sceAgcDriverSubmitDcb(&packet) == 0, "submission failed"); + auto& pipeline = AgcDriver::DriverDetail::DrawPipeline::Queue0(); + + submit(dispatch); + waitFor([&] { return dormant() && pipeline.Outstanding() == 0; }, "queue 0 did not wait for work after the dispatch"); + { + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + submit(release); + waitFor([&] { return pipeline.Outstanding() != 0; }, "queue 0 did not hand the end-of-pipe label to the draw pipeline"); + waitFor(dormant, "queue 0 did not wait for work while the draw pipeline held the end-of-pipe label"); + } std::array events{}; int delivered = 0; From 35f7466e504b3aa2282b083a7c5bacc51802ed29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexej=20Szpev=C3=A1k?= Date: Sat, 17 Oct 2026 15:14:44 +0200 Subject: [PATCH 0743/1000] fix(libkernel): answer select on files and the random device as FreeBSD's poll routines do selscan asks a descriptor once, with the flags of every set it is in (POLLRDNORM for the read set, POLLWRNORM for the write set, POLLRDBAND for the exception set, plus POLLHUP and POLLERR), and marks each set whose flags the answer shares. A seeded random device's randomdev_poll narrows a request with POLLRDNORM to that bit and returns any other request whole, so in the read set it is readable and in no other set, and otherwise it is in each write and exception set it was asked in; select reported it readable and writable together and never exceptional. A regular file or directory went to the host poll, which on Windows threw; vop_stdpoll makes it readable and writable and never exceptional, which select now answers in process on every host, as poll does. --- core/libs/prx/libkernel/Socket/src/Socket.cpp | 4 ++- core/libs/tests/GuestSelect.cpp | 35 +++++++++++++++++++ docs/dev/debt/libkernel.md | 2 +- 3 files changed, 39 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libkernel/Socket/src/Socket.cpp b/core/libs/prx/libkernel/Socket/src/Socket.cpp index c4adb38dfd..b4a0ceb699 100644 --- a/core/libs/prx/libkernel/Socket/src/Socket.cpp +++ b/core/libs/prx/libkernel/Socket/src/Socket.cpp @@ -3,6 +3,7 @@ #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" #include "prx/libc/include/PreciseWait.hpp" +#include "prx/libkernel/File/include/DescriptorControl.hpp" #include "prx/libkernel/File/include/File.hpp" #include "prx/libkernel/File/include/RandomDevice.hpp" #include "prx/libkernel/Socket/include/SocketPoll.hpp" @@ -193,8 +194,9 @@ int APS5_VABI select_nid_postfix(int nfds, void* readfds, void* writefds, void* kernel.entries.push_back({descriptor, events, 0}); kernel.slots.push_back(requested.size()); requested.push_back({descriptor, events, 0}); - } else if (File::IsRandomDevice(descriptor)) devices.push_back({descriptor, events, static_cast(events & (KernelSocketPoll::Readable | KernelSocketPoll::Writable))}); + } else if (File::IsRandomDevice(descriptor)) devices.push_back({descriptor, events, static_cast((events & KernelSocketPoll::Readable) != 0 ? KernelSocketPoll::Readable : events)}); else if (descriptor <= 2) devices.push_back({descriptor, events, static_cast(descriptor == 0 ? 0 : events & KernelSocketPoll::Writable)}); + else if (File::IsVnode(descriptor)) devices.push_back({descriptor, events, static_cast(events & (KernelSocketPoll::Readable | KernelSocketPoll::Writable))}); else { net.entries.push_back({descriptor, events, 0}); net.slots.push_back(requested.size()); diff --git a/core/libs/tests/GuestSelect.cpp b/core/libs/tests/GuestSelect.cpp index 466d204c8c..35fe5cea49 100644 --- a/core/libs/tests/GuestSelect.cpp +++ b/core/libs/tests/GuestSelect.cpp @@ -3,6 +3,7 @@ #include #include #include +#include struct GuestTimeval { std::int64_t seconds; @@ -33,6 +34,8 @@ std::int64_t APS5_VABI send_nid_postfix(int, const void*, std::uint64_t, int); std::int64_t APS5_VABI recv_nid_postfix(int, void*, std::uint64_t, int); std::int64_t APS5_VABI write_nid_postfix(int, const char*, std::int64_t); std::int64_t APS5_VABI read_nid_postfix(int, void*, std::uint64_t); +int APS5_VABI open_nid_postfix(const char*, int, int); +int APS5_VABI unlink_nid_postfix(const char*); int* APS5_VABI __error_nid_postfix(); } @@ -88,6 +91,38 @@ int main() { Require(buffer[0] == byte); } + { + const std::string path = "anyps5-select-file-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()); + const int file = open_nid_postfix(path.c_str(), 0x0002 | 0x0200, 0644); + const int random = open_nid_postfix("/dev/urandom", 0, 0); + Require(file >= 0 && random >= 0); + GuestDescriptorSet read{}; + GuestDescriptorSet write{}; + GuestDescriptorSet except{}; + for (const int descriptor : {file, random}) { + read.Add(descriptor); + write.Add(descriptor); + except.Add(descriptor); + } + read.Add(reader); + const int highest = file > random ? (file > reader ? file : reader) : (random > reader ? random : reader); + const GuestTimeval zero{0, 0}; + Require(select_nid_postfix(highest + 1, read.words.data(), write.words.data(), except.words.data(), &zero) == 3); + Require(read.Has(file) && write.Has(file) && !except.Has(file)); + Require(read.Has(random) && !write.Has(random) && !except.Has(random)); + GuestDescriptorSet randomWrite{}; + GuestDescriptorSet randomExcept{}; + randomWrite.Add(random); + randomExcept.Add(random); + Require(select_nid_postfix(random + 1, nullptr, randomWrite.words.data(), randomExcept.words.data(), &zero) == 2); + Require(randomWrite.Has(random) && randomExcept.Has(random)); + Require(!read.Has(reader)); + GuestDescriptorSet onlyExcept{}; + onlyExcept.Add(file); + Require(select_nid_postfix(file + 1, nullptr, nullptr, onlyExcept.words.data(), &zero) == 0 && !onlyExcept.Has(file)); + Require(close_nid_postfix(random) == 0 && close_nid_postfix(file) == 0 && unlink_nid_postfix(path.c_str()) == 0); + } + { GuestDescriptorSet write{}; write.Add(writer); diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 30e9e8e2b9..7296e4e945 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -64,7 +64,7 @@ - [Mapping hints with no room above them](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (libkernel) - a mapping without `MAP_FIXED` whose hint lies above the guest address space, or has no free range at or above it, is placed from the bottom of the guest arena, as FreeBSD 11.4's [`vm_mmap_object`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/vm/vm_mmap.c#L1569) retries through [`vm_map_find_min`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/vm/vm_map.c#L1555-L1584) from its minimum address. FreeBSD's minimum is the end of the largest possible data segment rather than the start of the arena, and the console's address-space layout is not measured. PS5CEMU-HAR (PPSA99360) gives `sceKernelReserveVirtualRange` a hint near its own code, which is a host address here because the relinked executable lies outside the guest arena, so its JIT gets memory more than 2 GiB from its code and does not start - [_sceKernelRtldThreadAtexitIncrement, _sceKernelRtldThreadAtexitDecrement](../../../core/libs/prx/libkernel/System/src/Rtld.cpp) (libkernel) - the argument is the `dso` address a title's `libc.prx` passes to `__cxa_thread_atexit` (PPSA14632), which fails when Increment returns nonzero. As in the [fpPS4](https://github.com/red-prig/fpPS4) reimplementation, Increment takes a reference on the loaded image containing the address and Decrement releases one; the variants without the leading underscore are assumed to match - [tcgetattr, tcsetattr](../../../core/libs/prx/libkernel/File/src/Terminal.cpp) (libkernel) - every file and pipe descriptor is taken as not a terminal (`ENOTTY`), and a socket gets `EADDRNOTAVAIL` for `AF_INET` and `EOPNOTSUPP` otherwise, as FreeBSD's `in_control` and `pru_control_notsupp` answer a termios ioctl; what descriptors 0 to 2 of a title are on the console, and whether any of them is a terminal there, is unknown -- [select](../../../core/libs/prx/libkernel/Socket/src/Socket.cpp) (libkernel) - descriptor 0 is never reported readable and descriptors 1 and 2 are always writable, as for a console title with no input; reads from descriptor 0 still go to the host stdin. `/dev/random` and `/dev/urandom` are always readable and writable, as FreeBSD reports a seeded random device. Guest sockets and libSceNet sockets are polled; other descriptors are polled through the host on Linux, while on Windows pipes are checked every millisecond with the readiness test `poll` uses and any other descriptor throws. Dungeons (PPSA16064) selects on descriptor 0, and RetroArch (PPSA99169) selects on its web server's wake-up pipe next to its listening socket +- [select](../../../core/libs/prx/libkernel/Socket/src/Socket.cpp) (libkernel) - descriptor 0 is never reported readable and descriptors 1 and 2 are always writable, as for a console title with no input; reads from descriptor 0 still go to the host stdin. `/dev/random` and `/dev/urandom` follow `randomdev_poll` for a seeded device under [`selscan`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/sys_generic.c#L1178-L1236), which asks once with the flags of every set the descriptor is in: in the read set they are readable and reported in no other set, and otherwise they are reported in the write and exception sets they were asked in. A regular file, a directory or a file read from app0.zar is always readable and writable and never in the exception set, as `vop_stdpoll` answers, on every host. Guest sockets and libSceNet sockets are polled; other descriptors are polled through the host on Linux, while on Windows pipes are checked every millisecond with the readiness test `poll` uses and any other descriptor throws. Dungeons (PPSA16064) selects on descriptor 0, and RetroArch (PPSA99169) selects on its web server's wake-up pipe next to its listening socket - [select](../../../core/libs/prx/libkernel/Socket/src/Socket.cpp) (libkernel) - a descriptor the socket poller reports as unknown that is below `GuestSockets::FirstDescriptor` is polled on the host instead of throwing (on Windows only a pipe, as above); an invalid host descriptor fails with `EBADF`, and mixed host and socket sets are polled in short alternating slices so neither side waits out the full slice alone. Wobbly Life (PPSA29413) selects on file descriptors while streaming its bundles - [__progname](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - unknown data export - [environ](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - data export pointing to a fixed empty environment so titles that import it load; it is not the environment libc's `getenv` reads, so it lists none of those variables and `setenv`/`putenv` changes do not appear in it From acad67d946a1d024fe32bd09c0e77772c0247f95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexej=20Szpev=C3=A1k?= Date: Sat, 17 Oct 2026 15:15:13 +0200 Subject: [PATCH 0744/1000] feat(libkernel): poll descriptors 0 to 2 with the console model select uses select takes descriptors 0 to 2 as a console title's terminal with no input: 0 is never readable and 1 and 2 are always writable. poll threw for them unless the host happened to attach a pipe, so the same title could select on its standard streams but not poll them. poll now gives the same answer, with POLLOUT, the one bit ttydev_poll reports for a writable terminal, for 1 and 2 and nothing for 0. --- core/libs/prx/libkernel/Socket/src/Udp.cpp | 5 +++++ core/libs/tests/GuestPoll.cpp | 7 +++++++ docs/dev/debt/libkernel.md | 2 +- 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index 2f533cea64..f4aa7dfcee 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -1398,6 +1398,11 @@ int APS5_VABI poll_nid_postfix(GuestPollDescriptor* descriptors, std::uint32_t c if (entry.revents) ++ready; continue; } + if (entry.descriptor <= 2) { + entry.revents = static_cast(entry.descriptor == 0 ? 0 : entry.events & GuestPollWritable); + if (entry.revents) ++ready; + continue; + } if (File::IsVnode(entry.descriptor)) { entry.revents = static_cast(entry.events & GuestPollVnodeReady); if (entry.revents) ++ready; diff --git a/core/libs/tests/GuestPoll.cpp b/core/libs/tests/GuestPoll.cpp index 5d229666e8..131e75eb9d 100644 --- a/core/libs/tests/GuestPoll.cpp +++ b/core/libs/tests/GuestPoll.cpp @@ -268,6 +268,13 @@ int main() { Require(poll_nid_postfix(mixed, 2, -1) == 1 && mixed[0].revents == 0 && mixed[1].revents == Out); Require(close_nid_postfix(random) == 0); } + PollDescriptor console[3]{{0, static_cast(In | ReadNormal | Out), -1}, {1, static_cast(In | Out | WriteBand), -1}, {2, Out, -1}}; + Require(poll_nid_postfix(console, 3, 0) == 2); + Require(console[0].revents == 0 && console[1].revents == Out && console[2].revents == Out); + PollDescriptor input{0, In, -1}; + const auto inputStart = std::chrono::steady_clock::now(); + Require(poll_nid_postfix(&input, 1, 30) == 0 && input.revents == 0); + Require(std::chrono::steady_clock::now() - inputStart >= std::chrono::milliseconds(25)); const int queue = kqueue_nid_postfix(); Require(queue >= 0); PollDescriptor unknown[2]{{listener, In, 0}, {queue, In, 0}}; diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 7296e4e945..8ac2f9f7b9 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -170,7 +170,7 @@ - [read, write and vector I/O](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) on a guest socket use `recv` and `send` without flags; `sceKernelReadv` and `sceKernelWritev` gather or scatter through one socket operation, preserving stream short reads and datagram boundaries. The positioned `pread`, `pwrite`, `preadv` and `pwritev` forms, and their `sceKernel` forms, return `ESPIPE` for an open guest socket and `EBADF` for a closed one. `EPIPE` from a write raises the host's `SIGPIPE` on Linux unless the socket has `SO_NOSIGPIPE` set, and the guest cannot ignore it: `signal(SIGPIPE, SIG_IGN)` and `sigaction` record the disposition, as they do for every number from 1 to 128, but only the six signals with host equivalents (2, 4, 6, 8, 11 and 15) take effect on the host or are delivered, so the host `SIGPIPE` is not stopped; nothing is raised on Windows; a `write` longer than `INT_MAX` bytes is `EMSGSIZE` where the console sends it. AIO on a guest socket still goes to the host, which has no such descriptor - [Socket descriptor numbers](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel, libSceNet) - `socket`, `accept`, `socketpair`, `dup`, `sceNetSocket` and `sceNetAccept` take the lowest free number of the host descriptor table that files use, as FreeBSD 11.4 `fdalloc` takes the lowest free entry of the one process table (`kern_descrip.c`), so every number stays below `FD_SETSIZE` (1024) while fewer than 1024 descriptors are open. The number holds a zero-access `NUL` handle on Windows and a duplicate of the host socket on Linux, and the socket tables map it to the socket. The host table also holds the runtime's own files, so the numbers differ from the console's; the first number `socket` and `sceNetSocket` return on a console and its `getdtablesize` are not measured. Paths that bypass the socket tables see the placeholder: libc `fdopen` on a socket opens it, and libkernel `close` of a libSceNet socket releases the number but leaves the libSceNet socket open. Palworld (PPSA23126), The Mound (PPSA26223) and Dying Light 2 (PPSA02262) set socket bits in a 1024-bit `fd_set` - [fstat on a guest socket](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) fills the fields FreeBSD 11's soo_stat and pru_sense_null fill and zeroes the rest: S_IFSOCK, the read bits while the receive side is open or data is queued, the write bits while the send side is open, st_size and st_blksize. Only this process's shutdown closes a side here; a peer's shutdown, close or reset does not clear the bits as FreeBSD's socantrcvmore and socantsendmore do. st_size is the host's FIONREAD, which for a Linux datagram socket is the size of the next datagram where FreeBSD counts every queued byte, and st_blksize is the host's SO_SNDBUF, which Linux reports as twice the size that was set. A socketpair end reports st_dev and st_ino 0, where FreeBSD's uipc_sense reports NODEV and a per-socket number -- [poll](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) waits on guest sockets and pipes, through `WSAPoll` on Windows and `poll` on Linux; the `pollfd` layout and flag values are FreeBSD's. A libSceNet socket joins the same host wait through the native socket libSceNet lends to libkernel. `WSAPoll` rejects `POLLPRI`, so on Windows a socket's `POLLPRI` is asked as `POLLRDBAND` and `POLLIN` as `POLLRDNORM`; Winsock reports urgent data until it is read with `MSG_OOB`, where FreeBSD 11.4's `sopoll_generic` stops once the normal stream passes the mark. Guest `send` and `recv` refuse `MSG_OOB` with `EOPNOTSUPP`, so urgent data can only come from an outside peer and Windows keeps reporting `POLLPRI` after the stream passes the mark. A pipe follows FreeBSD's `pipe_poll`: the read end is readable when data is buffered, either end reports `POLLHUP` once the other end is closed, with the requested `POLLIN` and `POLLRDNORM` bits and without `POLLOUT`, which `pollscan` clears, and the write end is writable when `PIPE_BUF` (512) bytes are free on Windows, or when the host kernel says so on Linux. `POLLOUT` on a read end throws: FreeBSD opens both ends for reading and writing and reports it writable, and host pipes are one-way. Windows pipes cannot be waited on, so while a pipe is polled `poll` checks it with `NtQueryInformationFile` and waits on the sockets in slices of 1 ms. A regular file, a directory or a file read from app0.zar is always ready for `POLLIN`, `POLLRDNORM` and `POLLOUT` and never for `POLLPRI`, `POLLRDBAND` or `POLLWRBAND`, as FreeBSD 11.4's [`vop_stdpoll`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/vfs_default.c#L563-L574) and `vop_nopoll` answer every event `poll` accepts; which of the two the console's filesystems use is not known, and they agree on these events. `/dev/random` and `/dev/urandom` follow FreeBSD 11.4's [`randomdev_poll`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/dev/random/randomdev.c#L274-L285) for a seeded device: a request with `POLLIN` or `POLLRDNORM` gets only those bits back, and any other request gets every requested bit. A closed descriptor reports `POLLNVAL`, and an open descriptor that is neither a guest socket, a libSceNet socket, a pipe, a file, a directory nor the random device (stdio, other devices) throws, as do event bits other than `POLLIN`, `POLLPRI`, `POLLOUT`, `POLLRDNORM`, `POLLRDBAND` and `POLLWRBAND` (the `POLLERR`, `POLLHUP` and `POLLNVAL` bits are ignored in `events`), an infinite timeout with no socket or pipe to wait on. A closed socket descriptor reports `POLLNVAL`; the console's limit on the descriptor count is unknown and not checked +- [poll](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) waits on guest sockets and pipes, through `WSAPoll` on Windows and `poll` on Linux; the `pollfd` layout and flag values are FreeBSD's. A libSceNet socket joins the same host wait through the native socket libSceNet lends to libkernel. `WSAPoll` rejects `POLLPRI`, so on Windows a socket's `POLLPRI` is asked as `POLLRDBAND` and `POLLIN` as `POLLRDNORM`; Winsock reports urgent data until it is read with `MSG_OOB`, where FreeBSD 11.4's `sopoll_generic` stops once the normal stream passes the mark. Guest `send` and `recv` refuse `MSG_OOB` with `EOPNOTSUPP`, so urgent data can only come from an outside peer and Windows keeps reporting `POLLPRI` after the stream passes the mark. A pipe follows FreeBSD's `pipe_poll`: the read end is readable when data is buffered, either end reports `POLLHUP` once the other end is closed, with the requested `POLLIN` and `POLLRDNORM` bits and without `POLLOUT`, which `pollscan` clears, and the write end is writable when `PIPE_BUF` (512) bytes are free on Windows, or when the host kernel says so on Linux. `POLLOUT` on a read end throws: FreeBSD opens both ends for reading and writing and reports it writable, and host pipes are one-way. Windows pipes cannot be waited on, so while a pipe is polled `poll` checks it with `NtQueryInformationFile` and waits on the sockets in slices of 1 ms. A regular file, a directory or a file read from app0.zar is always ready for `POLLIN`, `POLLRDNORM` and `POLLOUT` and never for `POLLPRI`, `POLLRDBAND` or `POLLWRBAND`, as FreeBSD 11.4's [`vop_stdpoll`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/vfs_default.c#L563-L574) and `vop_nopoll` answer every event `poll` accepts; which of the two the console's filesystems use is not known, and they agree on these events. `/dev/random` and `/dev/urandom` follow FreeBSD 11.4's [`randomdev_poll`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/dev/random/randomdev.c#L274-L285) for a seeded device: a request with `POLLIN` or `POLLRDNORM` gets only those bits back, and any other request gets every requested bit. Open descriptors 0 to 2 follow the console model `select` uses: descriptor 0 never reports an event and descriptors 1 and 2 report `POLLOUT`, whatever the host attaches to them. A closed descriptor reports `POLLNVAL`, and an open descriptor that is neither a guest socket, a libSceNet socket, a pipe, a file, a directory, the random device nor descriptor 0 to 2 (other devices, a kqueue) throws, as do event bits other than `POLLIN`, `POLLPRI`, `POLLOUT`, `POLLRDNORM`, `POLLRDBAND` and `POLLWRBAND` (the `POLLERR`, `POLLHUP` and `POLLNVAL` bits are ignored in `events`), an infinite timeout with no socket or pipe to wait on. A closed socket descriptor reports `POLLNVAL`; the console's limit on the descriptor count is unknown and not checked - [kevent on a pipe](../../../core/libs/prx/libkernel/Equeue/src/Kqueue.cpp) (libkernel) - `EVFILT_READ` on the read end of a pipe follows FreeBSD 11's [`filt_piperead`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/sys_pipe.c#L1787-L1806): `data` is the number of buffered bytes, and `EV_EOF` is set once the write end is closed. Windows pipes cannot be waited on, so while a kqueue waits on a pipe it checks it with `NtQueryInformationFile` and waits on its sockets in slices of 1 ms; a wait with no pipe is not sliced. `EVFILT_READ` on the write end of a pipe throws: FreeBSD's `pipe` opens both ends for reading and writing ([`kern_pipe`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/sys_pipe.c#L422)), and host pipes are one-way. `EVFILT_WRITE` on a pipe and `NOTE_LOWAT` on a pipe, which `filt_piperead` ignores, throw too. - [Socket error numbers](../../../core/libs/prx/libkernel/Socket/include/SocketError.hpp) (libkernel) - a host socket error becomes the FreeBSD 11 `errno.h` number of the same name; the numbers are not checked on the console. Winsock `WSAESHUTDOWN` becomes `EPIPE`, as a send after `shutdown` does on FreeBSD, and a host error with no FreeBSD socket counterpart becomes `EIO`. A nonblocking `connect` in progress reports `EINPROGRESS` on Linux but `EAGAIN` on Windows (`WSAEWOULDBLOCK`), where FreeBSD reports `EINPROGRESS`. On Windows a refused nonblocking `connect` is never reported (`SO_ERROR` is unsupported), so a repeated one keeps reporting `EALREADY` where FreeBSD reports `ECONNREFUSED`. A receive after `shutdown(SHUT_RD)` reports `EPIPE` on Windows where FreeBSD returns 0 - [getsockopt, setsockopt](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) on a guest socket handle the SOL_SOCKET level and `TCP_NODELAY`, after FreeBSD 11.4's [`sosetopt` and `sogetopt`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c): `SO_REUSEADDR`, `SO_KEEPALIVE`, `SO_DONTROUTE`, `SO_BROADCAST`, `SO_OOBINLINE` and `SO_LINGER` act on the host socket, and `SO_ERROR`, `SO_TYPE`, `SO_ACCEPTCONN`, `SO_SNDBUF` and `SO_RCVBUF` are read. A boolean option reads back as its own bit, as FreeBSD returns `so_options & option`. Winsock does not clear a socket's pending error when it is read, so on Windows the library reports a pending error once and shows it again only after a new `connect`; a second failure with the same code on the same socket without a `connect` in between would be hidden. `SO_SNDBUF` and `SO_RCVBUF` read the host's value, which is not the size FreeBSD reports (Linux doubles it). `SO_NOSIGPIPE` is kept per socket and reads back as its bit. On Linux a `send`, `sendto`, `sendmsg` or `write` on that socket passes `MSG_NOSIGNAL` to the host, as [`kern_sendit`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_syscalls.c#L890-L895) and [`soo_write`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/sys_socket.c#L147-L151) leave out the signal. An accepted socket takes it from its listener, as [`sonewconn`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L603) copies the options. `SO_NBIO` (`0x1200`) is the console's own option, not FreeBSD's. It switches the socket between blocking and non-blocking as `FIONBIO` does and reads back as 0 or 1, as `sceNetSetsockopt` and `sceNetGetsockopt` treat it here; what the console reads back for it is not measured. `TCP_NODELAY` (`IPPROTO_TCP`, 1) is set on the host socket and reads back as `TF_NODELAY` (4), as [`tcp_default_ctloutput`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/netinet/tcp_usrreq.c#L1860-L1861) returns it. The `IPPROTO_TCP` level on a datagram or `AF_UNIX` socket fails with `EINVAL`, as [`ip_ctloutput`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/netinet/ip_output.c#L943-L944) and [`uipc_ctloutput`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_usrreq.c#L1228-L1229) answer it, and an accepted socket takes `TCP_NODELAY` through the host. `SO_SNDTIMEO` and `SO_RCVTIMEO` follow [`sosetopt`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L2657-L2689) and [`sogetopt`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L2846-L2850). A 16-byte `timeval` with a negative field or 1000000 microseconds or more fails with `EDOM`, and more than `INT32_MAX` seconds becomes the longest wait. The value is kept and read back through FreeBSD's 32.32 fixed-point conversion ([`tvtosbt`, `sbttotv`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/sys/time.h#L178-L286)), so half a second reads back as 499999 microseconds and one microsecond as 0. A `recv`, `recvfrom`, `recvmsg`, `read` or `readv`, and a `send`, `sendto`, `sendmsg`, `write` or `writev`, on a blocking socket then waits that long for the host to report the socket readable or writable, and fails with `EWOULDBLOCK`; a call with `MSG_DONTWAIT` does not wait. An accepted socket takes both from its listener ([`sonewconn`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L629-L630)). The wait covers only the first transfer: once the socket is writable the host send runs without a limit, where FreeBSD stops a send that cannot finish and returns the bytes sent so far, and a receive whose data another thread takes between the wait and the host call blocks again. `sendfile` ignores the send timeout. The host wait has millisecond steps, so a shorter timeout lasts up to a millisecond longer. A null value fails with `EINVAL`, where FreeBSD's [`kern_setsockopt`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_syscalls.c#L1414-L1415) returns `EFAULT`. Every other SOL_SOCKET option (`SO_DEBUG`, `SO_USELOOPBACK`, `SO_REUSEPORT` and `SO_TIMESTAMP`, which FreeBSD stores and no title has been seen to set, the low-water marks among them), every other `IPPROTO_TCP` option (`TCP_MAXSEG`, `TCP_KEEPIDLE`, `TCP_KEEPINTVL` and `TCP_KEEPCNT` among them) and every other level (`IPPROTO_IP`, `IPPROTO_IPV6`) fail with `ENOPROTOOPT`. None of this was measured on a console. From 3573289f2f5add4a236cf11e40e718079446485c Mon Sep 17 00:00:00 2001 From: milescoviello Date: Wed, 14 Oct 2026 23:16:00 +0000 Subject: [PATCH 0745/1000] fix(libSceNet): set an option on a libkernel socket through sceNetSetsockopt MXGP 26 (PPSA30839) opens TCP sockets with libkernel's socket() and turns on TCP_NODELAY with sceNetSetsockopt. libSceNet only looked in its own socket table, so the call failed with SCE_NET_ERROR_EBADF. On the console both calls work on the same kernel sockets. A descriptor libSceNet does not own but libkernel has open as a socket now goes to libkernel's setsockopt(), which answers the options it models. Anything else still fails with EBADF. The fallback runs after g_mutex is released. guest_sce_net sets SO_SNDBUF on a libkernel TCP socket through sceNetSetsockopt, and checks that the call fails with EBADF once the socket is closed. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceNet/Export.cpp | 14 ++++++++++++-- core/libs/tests/GuestSceNet.cpp | 10 ++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index 8fd8578762..1bb330b707 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -36,6 +36,7 @@ #include "prx/libc/include/General.hpp" #include "prx/libc/include/GuestArena.hpp" #include "prx/libkernel/Socket/include/SocketPoll.hpp" +#include "prx/libkernel/Socket/include/SocketRuntime.hpp" // Guest socket descriptors retain PS5 semantics while their transport is backed by host sockets. @@ -1146,13 +1147,22 @@ int APS5_VABI sceNetShutdown(int s, int how) { return error == 0 ? 0 : fail(error); } +extern "C" int APS5_VABI setsockopt_nid_postfix(int descriptor, int level, int option, const void* value, std::uint32_t length); + int APS5_VABI sceNetSetsockopt(int s, int level, int optname, const void* optval, uint32_t optlen) { Sock socket; + bool found = false; { std::lock_guard lk(g_mutex); auto it = g_socks.find(s); - if (it == g_socks.end()) return fail(NET_EBADF); - socket = it->second; + if (it != g_socks.end()) { + socket = it->second; + found = true; + } + } + if (!found) { + if (!GuestSocketIsOpen_nid_no_patch(s)) return fail(NET_EBADF); + return setsockopt_nid_postfix(s, level, optname, optval, optlen) == 0 ? 0 : fail(*__error_nid_postfix()); } if (level != NET_SOL_SOCKET) return fail(NET_EOPNOTSUPP); if (optname == NET_SO_RCVTIMEO || optname == NET_SO_SNDTIMEO) { diff --git a/core/libs/tests/GuestSceNet.cpp b/core/libs/tests/GuestSceNet.cpp index e5b47cdeae..c7fec08d99 100644 --- a/core/libs/tests/GuestSceNet.cpp +++ b/core/libs/tests/GuestSceNet.cpp @@ -855,6 +855,15 @@ static void CheckStreamAddressLengths() { for (const int descriptor : {accepted, client, listener}) Require(sceNetSocketClose(descriptor) == 0); } +static void CheckKernelSocketOption() { + const int kernel = socket_nid_postfix(2, 1, 0); + Require(kernel >= 0); + const int size = 65536; + Require(sceNetSetsockopt(kernel, 0xffff, 0x1001, &size, sizeof(size)) == 0); + Require(close_nid_postfix(kernel) == 0); + Require(Failed(sceNetSetsockopt(kernel, 0xffff, 0x1001, &size, sizeof(size)), 9)); +} + static void CheckBufferSizes() { const int socket = sceNetSocket("buffer-sizes", 2, 2, 17); Require(socket >= 0); @@ -1106,6 +1115,7 @@ int main() { CheckStreamAddressLengths(); CheckBufferSizes(); CheckOnesBroadcast(); + CheckKernelSocketOption(); const int epoll = sceNetEpollCreate("guest-sce-net", 0); Require(epoll >= 0); From 9c9311af8f89c95693da4e9f7b0e680b7cf7c231 Mon Sep 17 00:00:00 2001 From: milescoviello Date: Fri, 16 Oct 2026 14:11:02 +0000 Subject: [PATCH 0746/1000] test(libSceNet): set TCP_NODELAY on a libkernel socket through sceNetSetsockopt The call MXGP 26 makes; libkernel answers it since #5565. Co-Authored-By: Claude Opus 5.5 --- core/libs/tests/GuestSceNet.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/libs/tests/GuestSceNet.cpp b/core/libs/tests/GuestSceNet.cpp index c7fec08d99..68f3dabef2 100644 --- a/core/libs/tests/GuestSceNet.cpp +++ b/core/libs/tests/GuestSceNet.cpp @@ -860,6 +860,8 @@ static void CheckKernelSocketOption() { Require(kernel >= 0); const int size = 65536; Require(sceNetSetsockopt(kernel, 0xffff, 0x1001, &size, sizeof(size)) == 0); + const int one = 1; + Require(sceNetSetsockopt(kernel, 6, 1, &one, sizeof(one)) == 0); Require(close_nid_postfix(kernel) == 0); Require(Failed(sceNetSetsockopt(kernel, 0xffff, 0x1001, &size, sizeof(size)), 9)); } From 09bc5a179911c20f4f9ab2676689a8bddfabc0c2 Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Thu, 15 Oct 2026 14:12:35 +0000 Subject: [PATCH 0747/1000] fix(relinker): preserve GNU stack and RELRO headers on Linux --- core/relinker/CMakeLists.txt | 3 + .../elfpatcher/src/general/SegmentFilter.cpp | 1 + .../relinker/tests/test_linux_gnu_segments.py | 56 +++++++++++++++++++ 3 files changed, 60 insertions(+) create mode 100644 core/relinker/relinker/tests/test_linux_gnu_segments.py diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index 1348d601a6..4c454a975a 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -408,6 +408,9 @@ if(BUILD_TESTING) add_test(NAME windows_fixed_base COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_windows_fixed_base.py $) set_tests_properties(windows_fixed_base PROPERTIES TIMEOUT 20) + add_test(NAME linux_gnu_segments COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_linux_gnu_segments.py $) + set_tests_properties(linux_gnu_segments PROPERTIES TIMEOUT 20) add_test(NAME elf_offsets COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_elf_offsets.py $) add_test(NAME conversion_diagnostics COMMAND ${Python3_EXECUTABLE} diff --git a/core/relinker/elfpatcher/src/general/SegmentFilter.cpp b/core/relinker/elfpatcher/src/general/SegmentFilter.cpp index f387b4e7c0..18f9cd3735 100644 --- a/core/relinker/elfpatcher/src/general/SegmentFilter.cpp +++ b/core/relinker/elfpatcher/src/general/SegmentFilter.cpp @@ -13,6 +13,7 @@ bool SegmentFilter::_isSceSpecificSegment(const std::uint32_t type) const { bool SegmentFilter::ShouldSkip(const Domain::ProgramHeader& ph) const { if (ph.Type == PT_OS_PROCPARAM) return false; if (ph.Type == PT_GNU_EH_FRAME) return false; + if (ph.Type == PT_GNU_STACK || ph.Type == PT_GNU_RELRO) return false; if (_isSceSpecificSegment(ph.Type)) return true; if (ph.Type == PT_DYNAMIC || ph.Type == PT_PHDR || ph.Type == PT_INTERP) return true; if (ph.Type == PT_NOTE && ph.MappedAddress == 0 && ph.FileSize > 0) return true; diff --git a/core/relinker/relinker/tests/test_linux_gnu_segments.py b/core/relinker/relinker/tests/test_linux_gnu_segments.py new file mode 100644 index 0000000000..3ecdc8d915 --- /dev/null +++ b/core/relinker/relinker/tests/test_linux_gnu_segments.py @@ -0,0 +1,56 @@ +"""Check Linux output preserves GNU stack and RELRO program headers.""" + +from pathlib import Path +import struct +import subprocess +import sys +import tempfile + +from test_linux_load_alignment import fixture + +PT_GNU_STACK = 0x6474E551 +PT_GNU_RELRO = 0x6474E552 +PT_SCE_VERSION = 0x6FFFFF01 +BASE = 0x400000 + + +def image(): + elf = fixture() + struct.pack_into(" Date: Sat, 17 Oct 2026 13:27:03 +0000 Subject: [PATCH 0748/1000] fix(relinker): omit RELRO when lazy binding --- .../include/elfpatcher/general/SegmentFilter.hpp | 2 ++ .../relinker/elfpatcher/src/general/SegmentFilter.cpp | 1 + core/relinker/relinker/src/pipeline/Conversion.cpp | 2 +- .../relinker/tests/test_linux_gnu_segments.py | 11 ++++++----- 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/core/relinker/elfpatcher/include/elfpatcher/general/SegmentFilter.hpp b/core/relinker/elfpatcher/include/elfpatcher/general/SegmentFilter.hpp index 4d404c68d4..76b2e1c6f7 100644 --- a/core/relinker/elfpatcher/include/elfpatcher/general/SegmentFilter.hpp +++ b/core/relinker/elfpatcher/include/elfpatcher/general/SegmentFilter.hpp @@ -7,9 +7,11 @@ namespace Elfpatcher { class SegmentFilter : public ISegmentFilter { public: + explicit SegmentFilter(bool lazyBinding = false) : _lazyBinding(lazyBinding) {} bool ShouldSkip(const Domain::ProgramHeader& ph) const override; private: + bool _lazyBinding; [[nodiscard]] bool _isSceSpecificSegment(std::uint32_t type) const; }; diff --git a/core/relinker/elfpatcher/src/general/SegmentFilter.cpp b/core/relinker/elfpatcher/src/general/SegmentFilter.cpp index 18f9cd3735..114c7e1a3c 100644 --- a/core/relinker/elfpatcher/src/general/SegmentFilter.cpp +++ b/core/relinker/elfpatcher/src/general/SegmentFilter.cpp @@ -13,6 +13,7 @@ bool SegmentFilter::_isSceSpecificSegment(const std::uint32_t type) const { bool SegmentFilter::ShouldSkip(const Domain::ProgramHeader& ph) const { if (ph.Type == PT_OS_PROCPARAM) return false; if (ph.Type == PT_GNU_EH_FRAME) return false; + if (ph.Type == PT_GNU_RELRO && _lazyBinding) return true; if (ph.Type == PT_GNU_STACK || ph.Type == PT_GNU_RELRO) return false; if (_isSceSpecificSegment(ph.Type)) return true; if (ph.Type == PT_DYNAMIC || ph.Type == PT_PHDR || ph.Type == PT_INTERP) return true; diff --git a/core/relinker/relinker/src/pipeline/Conversion.cpp b/core/relinker/relinker/src/pipeline/Conversion.cpp index a0b9e36493..5fda86d3ae 100644 --- a/core/relinker/relinker/src/pipeline/Conversion.cpp +++ b/core/relinker/relinker/src/pipeline/Conversion.cpp @@ -109,7 +109,7 @@ ConversionOutput Conversion::Convert(ConversionInput input, const ConversionOpti patcher = std::make_shared( std::make_shared(), std::make_shared( - std::make_shared(), + std::make_shared(options.LazyBinding), byteWriter ), std::make_shared(byteWriter), diff --git a/core/relinker/relinker/tests/test_linux_gnu_segments.py b/core/relinker/relinker/tests/test_linux_gnu_segments.py index 3ecdc8d915..a45e51f101 100644 --- a/core/relinker/relinker/tests/test_linux_gnu_segments.py +++ b/core/relinker/relinker/tests/test_linux_gnu_segments.py @@ -36,19 +36,20 @@ def main(): work = Path(directory) source = work / "input.elf" source.write_bytes(image()) - for options, base in (([], 0), (["--linux-fixed-base", hex(BASE)], BASE)): + for options, base, keeps_relro in (([], 0, True), (["--linux-fixed-base", hex(BASE)], BASE, True), (["--lazy-binding"], 0, False)): output = work / ("fixed.elf" if base else "plain.elf") result = subprocess.run([str(relinker), "--skip-sce-module", *options, str(source), str(output)], capture_output=True, text=True, timeout=20) assert result.returncode == 0, (result.stdout, result.stderr) found = {header[0]: header for header in headers(output.read_bytes())} assert PT_GNU_STACK in found, "PT_GNU_STACK was dropped" - assert PT_GNU_RELRO in found, "PT_GNU_RELRO was dropped" + assert (PT_GNU_RELRO in found) == keeps_relro, "PT_GNU_RELRO presence does not match the binding mode" stack = found[PT_GNU_STACK] - relro = found[PT_GNU_RELRO] assert stack[1] == 6 and stack[2:6] == (0, 0, 0, 0), stack - assert relro[3:5] == (0x500 + base, 0x500 + base), relro - assert relro[5:7] == (0x100, 0x100), relro + if keeps_relro: + relro = found[PT_GNU_RELRO] + assert relro[3:5] == (0x500 + base, 0x500 + base), relro + assert relro[5:7] == (0x100, 0x100), relro print("Linux GNU segment tests passed") From 02c7da7677e06a9ec5526703ef9c7827fb28d42a Mon Sep 17 00:00:00 2001 From: javierUDP Date: Thu, 15 Oct 2026 15:58:57 -0300 Subject: [PATCH 0749/1000] fix: keep the Python tools and tests on Python 3.9 `Path.write_text(newline=...)` and `zip(strict=True)` were added in Python 3.10. macOS ships 3.9.6 as /usr/bin/python3, the interpreter CMake finds when no newer one is installed, so `prepare_release_notes` raised TypeError when the release workflow wrote its notes and the `pr_overlap_tools`, `release_package` and `linux_fixed_base` tests failed on a stock macOS host. Writing the encoded text keeps the newline translation off on every host, and the dynamic tag counts are compared before the pairs are walked. --- core/relinker/relinker/tests/test_linux_fixed_base.py | 5 ++++- docs/dev/BUILD.md | 2 +- tools/prepare_release_notes.py | 4 ++-- tools/tests/test_pr_overlap.py | 6 +++--- 4 files changed, 10 insertions(+), 7 deletions(-) diff --git a/core/relinker/relinker/tests/test_linux_fixed_base.py b/core/relinker/relinker/tests/test_linux_fixed_base.py index b181458c12..e5904a65f7 100644 --- a/core/relinker/relinker/tests/test_linux_fixed_base.py +++ b/core/relinker/relinker/tests/test_linux_fixed_base.py @@ -104,7 +104,10 @@ def main(): assert after == (*before[:3], before[3] + shift, before[4] + shift, *before[5:]), (before, after) assert min(header[3] for header in fixed_headers if header[0] == PT_LOAD) == BASE, "image does not start at the base" - for (tag, value), (fixed_tag, fixed_value) in zip(dynamic_tags(plain), dynamic_tags(fixed), strict=True): + plain_dynamic = dynamic_tags(plain) + fixed_dynamic = dynamic_tags(fixed) + assert len(plain_dynamic) == len(fixed_dynamic), (len(plain_dynamic), len(fixed_dynamic)) + for (tag, value), (fixed_tag, fixed_value) in zip(plain_dynamic, fixed_dynamic): assert tag == fixed_tag and fixed_value == value + (BASE if tag in ADDRESS_TAGS else 0), (tag, value, fixed_value) plain_relocations = relocations(plain) diff --git a/docs/dev/BUILD.md b/docs/dev/BUILD.md index f5ac09b199..03e4c72be7 100644 --- a/docs/dev/BUILD.md +++ b/docs/dev/BUILD.md @@ -2,7 +2,7 @@ ## Relinker only -The relinker can be built without initializing submodules or configuring SDL, Vulkan, FFmpeg, FreeType or the shader recompiler. It requires CMake, a C++20 compiler and a build tool. Python 3 enables the Python regression tests. +The relinker can be built without initializing submodules or configuring SDL, Vulkan, FFmpeg, FreeType or the shader recompiler. It requires CMake, a C++20 compiler and a build tool. Python 3.9 or newer enables the Python regression tests. ```sh cmake -S . -B build-relinker -G Ninja -DCMAKE_BUILD_TYPE=Release -DANYPS5_RELINKER_ONLY=ON -DBUILD_TESTING=ON diff --git a/tools/prepare_release_notes.py b/tools/prepare_release_notes.py index 55d3345e1e..63119d81a1 100644 --- a/tools/prepare_release_notes.py +++ b/tools/prepare_release_notes.py @@ -28,8 +28,8 @@ def prepare_notes(source, body, full): if not isinstance(notes, str): raise ValueError("Generated release notes body must be a string") full.parent.mkdir(parents=True, exist_ok=True) - full.write_text(notes, encoding="utf-8", newline="") - body.write_text(release_body(notes), encoding="utf-8", newline="") + full.write_bytes(notes.encode("utf-8")) + body.write_bytes(release_body(notes).encode("utf-8")) def main(): diff --git a/tools/tests/test_pr_overlap.py b/tools/tests/test_pr_overlap.py index 225db40dc9..c8245f7112 100644 --- a/tools/tests/test_pr_overlap.py +++ b/tools/tests/test_pr_overlap.py @@ -300,8 +300,8 @@ class CommentStateTests(unittest.TestCase): return pr_overlap.report(found, f"https://github.com/owner/project/blob/{commit}") def run_step(self, report, comment): - (self.root / "overlap" / "42.md").write_text(report, encoding="utf-8", newline="\n") - (self.root / "comment").write_text(json.dumps([5, comment]) + "\n" if comment else "", encoding="utf-8", newline="\n") + (self.root / "overlap" / "42.md").write_bytes(report.encode("utf-8")) + (self.root / "comment").write_bytes((json.dumps([5, comment]) + "\n" if comment else "").encode("utf-8")) calls = self.root / "calls" calls.write_text("", encoding="utf-8") script = "\n".join([ @@ -310,7 +310,7 @@ class CommentStateTests(unittest.TestCase): 'gh() { printf \'%s\\n\' "$*" >> "$RUNNER_TEMP/calls"; case "$*" in "api repos/"*) cat "$RUNNER_TEMP/comment";; esac; }', self.step, ]) - (self.root / "step.sh").write_text(script, encoding="utf-8", newline="\n") + (self.root / "step.sh").write_bytes(script.encode("utf-8")) environment = dict(os.environ, RUNNER_TEMP=str(self.root), GH_REPO="owner/project") result = subprocess.run(["bash", "step.sh"], cwd=self.root, env=environment, capture_output=True, text=True, timeout=20) self.assertEqual(result.returncode, 0, result.stdout + result.stderr) From b38f146ce6e90539aa0949f6157a3d75eb73bdc9 Mon Sep 17 00:00:00 2001 From: javierUDP Date: Thu, 15 Oct 2026 17:11:58 -0300 Subject: [PATCH 0750/1000] docs(relinker): record what Rosetta executes on an M5 The Rosetta conversion limits were measured on an M1 running macOS 15.3.1 and an M5 Max. A survey on an M5 running macOS 26.4 executes ANDN, RDRAND, the rest of BMI1 and BMI2, LZCNT, PREFETCHW, CLFLUSHOPT, AVX, AVX2, FMA, F16C and MOVBE while CPUID reports none of them, and raises an illegal instruction for RDSEED, CLWB, RDPID, ADCX and ADOX. --- docs/dev/debt/relinker.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/relinker.md b/docs/dev/debt/relinker.md index afce9f8d50..acab2b4e54 100644 --- a/docs/dev/debt/relinker.md +++ b/docs/dev/debt/relinker.md @@ -17,7 +17,7 @@ - In executables and guest modules with a `PT_SCE_DYNLIBDATA` segment, the [code analysis](../../../core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp) of the Windows and macOS outputs follows code only from the end of `PT_INTERP` to the end of the PLT after the last function of the unwind table, the end the pipeline's scans above use; a guest module with PLT relocations and no PLT after its last unwind function fails as the executable does. Code after that PLT is not analyzed: a TLS access in it is not rewritten, an entry point or pointer into it is ignored, and a function symbol or init/fini array entry pointing into it fails the conversion. - A relocation against a dynamic symbol entry that is all zero, like the null symbol, takes the value 0 plus its addend, as the gABI defines for `STN_UNDEF` ([pipeline](../../../core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp)). The Linux output imports such an entry as an unnamed weak symbol, which the host loader resolves to 0; the Windows and macOS outputs write the value into the slot ([relocation builder](../../../core/relinker/elfpatcher/src/windows/WindowsRelocationBuilder.cpp)). What the console's loader does with such an entry is not measured. OliSe Player (PPSA01153) has seven of them, and its code calls through those slots only when they are not 0 ([#4437](https://github.com/boykopovar/AnyPS5/issues/4437)). - The Linux placement of the `--to-intel` stubs in the [ELF patcher](../../../core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp) is covered only by a synthetic test. -- `--to-rosetta` lowers what Rosetta lacks on top of `--to-intel` ([matcher](../../../core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp)). AVX2, FMA, BMI1/2, F16C and MOVBE were observed to execute on an M5 Max without being reported through CPUID; this is not a guarantee for other Rosetta hosts. ANDN and RDRAND fault on an M1 running macOS 15.3.1. ANDN is lowered, but RDRAND and other BMI instructions still depend on the host. RDSEED becomes RDRAND and CLWB a NOP. RDPID returns processor 0, because Rosetta's TSC_AUX is the constant 0xAAAAAAAB; the same value reaches RDTSCP's ECX, which is not converted. ADCX/ADOX fault under Rosetta and are not converted: compilers do not emit them, and code that checks CPUID sees no ADX. The conversions are covered by the converter tests. ANDN lowering is also executed by an x86-64 test build under Rosetta on the M1; the other conversions were originally checked on an M5 Max, outside this repository's CI. +- `--to-rosetta` lowers what Rosetta lacks on top of `--to-intel` ([matcher](../../../core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp)). AVX2, FMA, BMI1/2, F16C and MOVBE were observed to execute on an M5 Max without being reported through CPUID; this is not a guarantee for other Rosetta hosts. ANDN and RDRAND fault on an M1 running macOS 15.3.1 and execute on an M5 running macOS 26.4, where the rest of BMI1 and BMI2, LZCNT, PREFETCHW, CLFLUSHOPT, AVX, AVX2, FMA, F16C and MOVBE execute as well although CPUID leaf 7 reads zero and leaves 1 and 0x80000001 report none of them, and where RDSEED, CLWB, RDPID, ADCX and ADOX raise an illegal instruction. ANDN is lowered, but RDRAND and other BMI instructions still depend on the host. RDSEED becomes RDRAND and CLWB a NOP. RDPID returns processor 0, because Rosetta's TSC_AUX is the constant 0xAAAAAAAB; the same value reaches RDTSCP's ECX, which is not converted. ADCX/ADOX fault under Rosetta and are not converted: compilers do not emit them, and code that checks CPUID sees no ADX. The conversions are covered by the converter tests. ANDN lowering is also executed by an x86-64 test build under Rosetta on the M1; the other conversions were originally checked on an M5 Max, outside this repository's CI. - `--to-intel` replaces the approximate legacy SSE and VEX.128 register forms of RSQRTPS/RCPPS and VRSQRTPS/VRCPPS with [correctly rounded](../../../core/relinker/codegen/src/x86/ReciprocalLowering.cpp) 1/sqrt(x) and 1/x, not with the AMD approximation: its tables are not public. Intel's approximation of 1/sqrt(1.0) is 0x3F7FF000, and two Newton-Raphson steps on it settle on 0.99999994 instead of 1.0; a renormalised identity quaternion then turns into a NaN axis in PPSA21564, which the console does not show. Refined results now reach the exact value; unrefined ones can still differ from the console in the low bits. Each site costs an out-of-line stub (two jumps, a spill below the red zone, SQRTPS/DIVPS). Legacy SSE forms preserve the destination's upper YMM bits; VEX forms clear them. The 256-bit, scalar and memory forms keep the native instruction, as does a site shorter than 5 bytes with no movable instruction after it for the jump (5 sites in PPSA21564, counted before a following RIP-relative instruction could move into the stub; not recounted). - `--trace-imports` ([entry stub](../../../core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp)) exists for Windows output only. Only imports bound through a PLT slot, or through a GOT slot whose symbol is a function, are traced; a function reached through any other relocation, or looked up at run time with `sceKernelDlsym`, is not, and neither are weak imports or the imports of modules loaded at run time From c17c6a5114cad72497454dfd95c7c63423518bad Mon Sep 17 00:00:00 2001 From: mattogepeto Date: Thu, 15 Oct 2026 23:20:38 +0200 Subject: [PATCH 0751/1000] fix(libkernel): return EFAULT from clock_gettime for a null result pointer clock_gettime threw invalid_argument when the result pointer was null, before looking at the clock id. FreeBSD 11.4's sys_clock_gettime reads the clock through kern_clock_gettime, which fails with EINVAL for an unknown clock, and only then copies the result out, so a null pointer fails with EFAULT. Read the clock into a local value first, then fail with -1 and EFAULT when the pointer is null. A clock that is not supported still throws before the pointer is looked at. Test the null pointer for the supported clocks. Written with AI assistance (Claude). Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libkernel/Time/Time.cpp | 20 ++++++++++++++------ core/libs/tests/GuestFreebsdClocks.cpp | 16 ++++++++++++++++ 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libkernel/Time/Time.cpp b/core/libs/prx/libkernel/Time/Time.cpp index e25277e821..059c0236f7 100644 --- a/core/libs/prx/libkernel/Time/Time.cpp +++ b/core/libs/prx/libkernel/Time/Time.cpp @@ -348,10 +348,7 @@ static std::uint64_t ProcessCpuResolutionNanos() { } #endif -int APS5_VABI clock_gettime_nid_postfix(int clockId, KernelTimespec* tp) { - if (tp == nullptr) { - APS5_INVALID_ARG_EX; - } +static int ReadClock(int clockId, KernelTimespec* tp) { if (IsCpuClock(clockId)) { std::uint64_t nanos = 0; if (!GuestThreadCpuNanos(static_cast(static_cast(clockId) & CPU_CLOCK_ID_MASK), &nanos)) { @@ -401,7 +398,7 @@ int APS5_VABI clock_gettime_nid_postfix(int clockId, KernelTimespec* tp) { tp->tv_nsec = static_cast(nanos % 1000000000ULL); return 0; } - throw std::runtime_error(std::string(__func__) + ": unsupported clock_id " + std::to_string(clockId)); + throw std::runtime_error(std::string("clock_gettime") + ": unsupported clock_id " + std::to_string(clockId)); #else clockid_t nativeId; switch (clockId) { @@ -434,7 +431,7 @@ int APS5_VABI clock_gettime_nid_postfix(int clockId, KernelTimespec* tp) { nativeId = CLOCK_PROCESS_CPUTIME_ID; break; default: - throw std::runtime_error(std::string(__func__) + ": unsupported clock_id " + std::to_string(clockId)); + throw std::runtime_error(std::string("clock_gettime") + ": unsupported clock_id " + std::to_string(clockId)); } struct timespec ts{}; if (clock_gettime(nativeId, &ts) != 0) { @@ -446,6 +443,17 @@ int APS5_VABI clock_gettime_nid_postfix(int clockId, KernelTimespec* tp) { #endif } +int APS5_VABI clock_gettime_nid_postfix(int clockId, KernelTimespec* tp) { + KernelTimespec value{}; + if (ReadClock(clockId, &value) != 0) return -1; + if (tp == nullptr) { + *__error_nid_postfix() = GUEST_EFAULT; + return -1; + } + *tp = value; + return 0; +} + int APS5_VABI gettimeofday_nid_postfix(KernelTimeval* tv, KernelTimezone* tz) { KernelTimeval ignoredTime{}; if (tv == nullptr) tv = &ignoredTime; diff --git a/core/libs/tests/GuestFreebsdClocks.cpp b/core/libs/tests/GuestFreebsdClocks.cpp index e74dc2cd38..40972ab464 100644 --- a/core/libs/tests/GuestFreebsdClocks.cpp +++ b/core/libs/tests/GuestFreebsdClocks.cpp @@ -79,6 +79,21 @@ static void UnsupportedResolutionStillThrows() { Require(sceKernelClockGetres(3, nullptr) == fault); } +static void OmittedTimeIsFaultAfterClockValidation() { + for (const int clockId : {GUEST_CLOCK_REALTIME, GUEST_CLOCK_VIRTUAL, GUEST_CLOCK_PROF, GUEST_CLOCK_MONOTONIC, + GUEST_CLOCK_REALTIME_PRECISE, GUEST_CLOCK_SECOND, GUEST_CLOCK_THREAD_CPUTIME_ID, GUEST_CLOCK_PROCESS_CPUTIME_ID}) { + *__error_nid_postfix() = 0; + Require(clock_gettime_nid_postfix(clockId, nullptr) == -1 && *__error_nid_postfix() == 14); + } + bool rejected = false; + try { + clock_gettime_nid_postfix(3, nullptr); + } catch (const std::runtime_error&) { + rejected = true; + } + Require(rejected); +} + static KernelTimespec Read(int clockId) { KernelTimespec time{-1, -1}; Require(clock_gettime_nid_postfix(clockId, &time) == SCE_OK); @@ -297,6 +312,7 @@ static void CalendarConversionsFollowTheHostZone() { int main() { if (!OmittedResolutionSucceeds()) return 1; UnsupportedResolutionStillThrows(); + OmittedTimeIsFaultAfterClockValidation(); SettimeValidatesLikeFreebsd(); CalendarConversionsInitializeTheirState(); CalendarConversionsFollowTheHostZone(); From 9dc65429991c7619e13c7827867063c6811aba50 Mon Sep 17 00:00:00 2001 From: mattogepeto Date: Thu, 15 Oct 2026 23:18:14 +0200 Subject: [PATCH 0752/1000] fix(libkernel): report EINVAL for getdirentries on a non-directory sceKernelGetdirentries on a descriptor that is not a directory returned ENOTDIR from the host getdents64 on Linux, and ENOTDIR for every descriptor missing from the directory table on Windows, including invalid ones. FreeBSD 11.4's kern_getdirentries returns EINVAL when the vnode is not a directory. Return EINVAL for a regular file on both hosts and EBADF for an invalid descriptor on Windows. Test getdents and getdirentries on a regular file and list the pipe case, which still fails with ESPIPE on Linux, in the library's technical debt file. Written with AI assistance (Claude). Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libkernel/File/src/Stdio.cpp | 6 +++++- core/libs/tests/GuestFilesystem.cpp | 9 +++++++++ docs/dev/debt/libkernel.md | 1 + 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index 510c992a55..862aa1f7e8 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -985,6 +985,10 @@ int APS5_VABI sceKernelGetdirentries(int fd, char* buf, int nbytes, int64_t* bas File::RefuseArchivedFile(fd, "getdirentries"); if (buf == nullptr) return SceErrorFromErrno(GUEST_EFAULT); if (nbytes <= 0) return SceErrorFromErrno(GUEST_EINVAL); + if (!File::DirectoryDescriptorPath(fd)) { + if (WithoutParameterHandler([fd] { return ::_get_osfhandle(fd); }) == -1) return SceErrorFromErrno(GUEST_EBADF); + return SceErrorFromErrno(GUEST_EINVAL); + } if (basep != nullptr) *basep = 0; return File::ReadDirectoryDescriptor(fd, buf, nbytes); } @@ -1007,7 +1011,7 @@ int APS5_VABI sceKernelGetdirentries(int fd, char* buf, int nbytes, int64_t* bas if (basep != nullptr) *basep = static_cast(base); std::vector host(static_cast(nbytes) < 4096 ? 4096 : static_cast(nbytes)); const auto read = ::syscall(SYS_getdents64, fd, host.data(), host.size()); - if (read < 0) return SceErrorFromErrno(errno); + if (read < 0) return SceErrorFromErrno(errno == ENOTDIR ? GUEST_EINVAL : errno); std::size_t written = 0; off_t resume = base; for (long offset = 0; offset < read;) { diff --git a/core/libs/tests/GuestFilesystem.cpp b/core/libs/tests/GuestFilesystem.cpp index b87cf1344c..aca41f4a8a 100644 --- a/core/libs/tests/GuestFilesystem.cpp +++ b/core/libs/tests/GuestFilesystem.cpp @@ -55,6 +55,8 @@ int APS5_VABI mkdir_nid_postfix(const char*, unsigned short); int APS5_VABI sceKernelOpen(const char*, int, unsigned short); int APS5_VABI sceKernelClose(int); int APS5_VABI sceKernelGetdents(int, char*, int); +int APS5_VABI getdents_nid_postfix(int, char*, int); +int APS5_VABI getdirentries_nid_postfix(int, char*, int, int64_t*); std::int64_t APS5_VABI sceKernelRead(int, void*, std::size_t); std::int64_t APS5_VABI sceKernelReadv(int, const void*, int); std::int64_t APS5_VABI sceKernelPread(int, void*, std::size_t, std::int64_t); @@ -833,6 +835,13 @@ int main() { Require(sceKernelPwrite(directoryDescriptor, "x", 1, 0) == static_cast(0x80020009u)); std::vector listing(4096); Require(sceKernelGetdents(directoryDescriptor, listing.data(), static_cast(listing.size())) > 0); + const int plainFile = open_nid_postfix(presentName.c_str(), 0, 0); + Require(plainFile >= 0); + Require(sceKernelGetdents(plainFile, listing.data(), static_cast(listing.size())) == static_cast(0x80020016u)); + Require(getdents_nid_postfix(plainFile, listing.data(), static_cast(listing.size())) == -1 && *__error_nid_postfix() == 22); + int64_t plainBase = 0; + Require(getdirentries_nid_postfix(plainFile, listing.data(), static_cast(listing.size()), &plainBase) == -1 && *__error_nid_postfix() == 22); + Require(close_nid_postfix(plainFile) == 0); Require(close_nid_postfix(directoryDescriptor) == 0); const int flaggedDirectory = sceKernelOpen(rootName.c_str(), 0x20000, 0); Require(flaggedDirectory >= 0 && sceKernelClose(flaggedDirectory) == 0); diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 5791f7ab5e..da7053539c 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -122,6 +122,7 @@ - [system](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) runs no command. It follows FreeBSD 11's `lib/libc/stdlib/system.c` for a sandbox without `/bin/sh`: a null command returns 1 unconditionally, as that version does, and any command returns the wait status of a child that could not exec the shell and called `_exit(127)` (`0x7f00`). Whether the console's application sandbox has no shell, and whether `vfork` succeeds there (FreeBSD returns -1 when it fails), is unverified - [sceKernelWriteThrottlingStatus](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) reports no write throttling: the host SSD has no write budget, so the first qword of the 32-byte status is set to `UINT32_MAX` and the rest is zeroed. PPSA17221 reads only that qword: it multiplies it by 64 KiB and compares the product with byte thresholds up to 0x4AF00000 to pick one of five tiers. When the call fails the title assumes 0x4B000001 bytes, which selects the same top tier as `UINT32_MAX`. What the value measures on hardware (a remaining budget or a rate) and the meaning of the other three qwords are unknown - [Windows directory descriptors](../../../core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp) (libkernel) take one snapshot of the directory, on the first `getdents`, `getdirentries` or non-zero `lseek` (`SEEK_SET` or `SEEK_CUR`), and keep it until it is discarded; `lseek` to offset 0 only discards the snapshot, and the next read lists the directory again, so every rewind costs O(entries) on the next read. `lseek` takes the offset as an entry index and `SEEK_CUR` with offset 0 only reports the index. `SEEK_END` throws, where the NUL descriptor used to return 0, because FreeBSD defines it relative to the directory size, which is not modelled. A negative result returns `EINVAL` and a `SEEK_CUR` offset that overflows returns `EOVERFLOW`, as FreeBSD `vn_seek` reports them. Names whose UTF-8 encoding exceeds the one-byte `d_namlen` limit are not representable in this guest directory-record layout: `getdents` returns `ENAMETOOLONG` without consuming the entry. Repeated calls therefore report the same error and entries after that name cannot be reached through this descriptor; FreeBSD's `getdents` behavior for host filenames that exceed its `MAXNAMLEN` is not verified. `getdirentries` still reports 0 through `basep` instead of the position of the block it read, so only 0 and values returned by `lseek` are valid offsets; FreeBSD offsets are filesystem cookies +- [getdents and getdirentries](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) return `EINVAL` for a descriptor that is not a directory, as [`kern_getdirentries`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/vfs_syscalls.c) does for a vnode that is not `VDIR`. On Linux a host pipe or FIFO still fails with `ESPIPE`, from the `lseek` that reads the base offset before `getdents64`, where `getvnode` returns `EINVAL` for a non-vnode descriptor - [/dev/random and /dev/urandom](../../../core/libs/prx/libkernel/File/src/RandomDevice.cpp) (libkernel) opened for reading return a descriptor backed by the host null device whose reads are filled with host OS entropy (`getentropy`, `rand_s` on Windows); both paths behave the same, never block, and positioned reads ignore the offset (FreeBSD `random(4)`). Only the exact paths are recognised, `fstat`, `lseek` and `mmap` see the null device, and opening them for writing throws - [pthread_mutexattr_getpshared and pthread_mutexattr_setpshared](../../../core/libs/prx/libkernel/Pthread/Posix/Mutex.cpp) (libkernel) store and report private or shared attributes, following FreeBSD 11 libthr [thr_mutexattr.c](https://github.com/freebsd/freebsd-src/blob/stable/11/lib/libthr/thread/thr_mutexattr.c). Invalid values return `EINVAL` without changing the attribute. Guest threads execute in one host process; sharing mutexes across processes is not modeled. Console behavior and invalid-attribute error precedence are unverified. - [pthread_attr_getinheritsched, pthread_attr_setinheritsched, pthread_attr_getscope, pthread_attr_setscope, pthread_attr_getstackaddr](../../../core/libs/prx/libkernel/Pthread/Posix/Attr.cpp) (libkernel) follow FreeBSD 11 libthr ([thr_attr.c](https://github.com/freebsd/freebsd-src/blob/5ed7eb0d97ba4436218e810f61bd059acba984c2/lib/libthr/thread/thr_attr.c), [thr_create.c](https://github.com/freebsd/freebsd-src/blob/5ed7eb0d97ba4436218e810f61bd059acba984c2/lib/libthr/thread/thr_create.c)), not measured on the console: the scope defaults to `PTHREAD_SCOPE_SYSTEM` (2), only it and `PTHREAD_SCOPE_PROCESS` (0) are accepted and other values return `EINVAL`, and an inherit value other than `PTHREAD_INHERIT_SCHED` (4) or `PTHREAD_EXPLICIT_SCHED` (0) returns `ENOTSUP`. `pthread_attr_getstackaddr` returns the configured stack address pointer (null by default). A null output of `pthread_attr_getinheritsched` throws, since libthr writes through it unchecked. A thread keeps the scope it was created with, taken from the creating thread under `PTHREAD_INHERIT_SCHED`, and `pthread_attr_get_np` and `scePthreadAttrGet` report it, but every guest thread is a host thread at system scope, so process scope changes nothing From 0936fe9b533894fe472bc014f182b213d7f7ddf2 Mon Sep 17 00:00:00 2001 From: lakhantheboss <61951686+lakhantheboss@users.noreply.github.com> Date: Sat, 17 Oct 2026 16:58:06 +0530 Subject: [PATCH 0753/1000] fix(savedata): return parameter error on null arguments in getMountInfo and getParam --- .../libs/prx/libSceSaveData.native/Export.cpp | 4 +- core/libs/tests/GuestSaveDataMountStatus.cpp | 39 +++++++++++++++++++ docs/dev/debt/libSceSaveData.native.md | 1 + 3 files changed, 42 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceSaveData.native/Export.cpp b/core/libs/prx/libSceSaveData.native/Export.cpp index 5635289707..0159c8e4f5 100644 --- a/core/libs/prx/libSceSaveData.native/Export.cpp +++ b/core/libs/prx/libSceSaveData.native/Export.cpp @@ -403,7 +403,7 @@ int APS5_VABI sceSaveDataGetEventResult(const void* event_param, SaveDataEvent* static int getMountInfo(const SaveDataMountPoint* mount_point, SaveDataMountInfo* info) { if (mount_point == nullptr || info == nullptr) { - throw std::runtime_error("sceSaveDataGetMountInfo: null argument"); + return SAVE_DATA_ERROR_PARAMETER; } std::lock_guard lock(g_slots_mutex); const int slot = find_slot_by_mount_point(mount_point->data); @@ -425,7 +425,7 @@ int APS5_VABI sceSaveDataGetMountInfo(const SaveDataMountPoint* mount_point, Sav static int getParam(const SaveDataMountPoint* mount_point, uint32_t param_type, void* param_buf, size_t param_buf_size, size_t* got_size) { if (mount_point == nullptr || param_buf == nullptr) { - throw std::runtime_error("sceSaveDataGetParam: null argument"); + return SAVE_DATA_ERROR_PARAMETER; } std::lock_guard lock(g_slots_mutex); const int slot = find_slot_by_mount_point(mount_point->data); diff --git a/core/libs/tests/GuestSaveDataMountStatus.cpp b/core/libs/tests/GuestSaveDataMountStatus.cpp index abdbe40aef..5e7786a5dd 100644 --- a/core/libs/tests/GuestSaveDataMountStatus.cpp +++ b/core/libs/tests/GuestSaveDataMountStatus.cpp @@ -12,6 +12,8 @@ extern "C" { int APS5_VABI sceSaveDataInitialize3(const void*); int APS5_VABI sceSaveDataMount3(const SaveDataMount3*, SaveDataMountResult*); int APS5_VABI sceSaveDataUmount2(std::uint32_t, const SaveDataMountPoint*); +int APS5_VABI sceSaveDataGetMountInfo(const SaveDataMountPoint*, SaveDataMountInfo*); +int APS5_VABI sceSaveDataGetParam(const SaveDataMountPoint*, std::uint32_t, void*, std::size_t, std::size_t*); } namespace { @@ -20,6 +22,7 @@ constexpr std::uint32_t MountReadWrite = 2; constexpr std::uint32_t MountCreate = 4; constexpr std::uint32_t MountCreate2 = 32; constexpr int SaveDataErrorParameter = -2137063424; +constexpr int SaveDataErrorNotMounted = -2137063420; constexpr int SaveDataErrorBusy = -2137063421; constexpr int SaveDataErrorNotFound = -2137063416; @@ -31,6 +34,40 @@ void Check(bool condition, const std::string& what) { ++failures; } +void CheckUnmountedCalls() { + SaveDataMountPoint point{}; + std::memcpy(point.data, "test", sizeof("test")); + SaveDataMountInfo info{}; + char buf[128]{}; + std::size_t gotSize = 0; + Check(sceSaveDataGetMountInfo(nullptr, &info) == SaveDataErrorParameter, "getMountInfo null point is PARAMETER"); + Check(sceSaveDataGetMountInfo(&point, nullptr) == SaveDataErrorParameter, "getMountInfo null info is PARAMETER"); + Check(sceSaveDataGetMountInfo(&point, &info) == SaveDataErrorNotMounted, "getMountInfo unmounted point is NOT_MOUNTED"); + Check(sceSaveDataGetParam(nullptr, 1, buf, sizeof(buf), &gotSize) == SaveDataErrorParameter, "getParam null point is PARAMETER"); + Check(sceSaveDataGetParam(&point, 1, nullptr, sizeof(buf), &gotSize) == SaveDataErrorParameter, "getParam null buf is PARAMETER"); + Check(sceSaveDataGetParam(&point, 1, buf, sizeof(buf), &gotSize) == SaveDataErrorNotMounted, "getParam unmounted point is NOT_MOUNTED"); +} + +void CheckMountInfoAndParam(const SaveDataMountPoint* point) { + SaveDataMountInfo info{}; + char buf[128]{}; + char smallBuf[64]{}; + std::size_t gotSize = 0; + SaveDataMountPoint unmounted{}; + std::memcpy(unmounted.data, "unmounted", sizeof("unmounted")); + Check(sceSaveDataGetMountInfo(nullptr, &info) == SaveDataErrorParameter, "getMountInfo null point is PARAMETER"); + Check(sceSaveDataGetMountInfo(point, nullptr) == SaveDataErrorParameter, "getMountInfo null info is PARAMETER"); + Check(sceSaveDataGetMountInfo(&unmounted, &info) == SaveDataErrorNotMounted, "getMountInfo unmounted point is NOT_MOUNTED"); + Check(sceSaveDataGetMountInfo(point, &info) == 0, "getMountInfo on mounted save succeeds"); + Check(sceSaveDataGetParam(nullptr, 1, buf, sizeof(buf), &gotSize) == SaveDataErrorParameter, "getParam null point is PARAMETER"); + Check(sceSaveDataGetParam(point, 1, nullptr, sizeof(buf), &gotSize) == SaveDataErrorParameter, "getParam null buf is PARAMETER"); + Check(sceSaveDataGetParam(point, 1, smallBuf, sizeof(smallBuf), &gotSize) == SaveDataErrorParameter, "getParam small buf is PARAMETER"); + Check(sceSaveDataGetParam(&unmounted, 1, buf, sizeof(buf), &gotSize) == SaveDataErrorNotMounted, "getParam unmounted point is NOT_MOUNTED"); + Check(sceSaveDataGetParam(point, 1, buf, sizeof(buf), &gotSize) == 0, "getParam on mounted save succeeds"); + Check(gotSize == sizeof(buf), "getParam reports expected size"); + Check(sceSaveDataGetParam(point, 1, buf, sizeof(buf), nullptr) == 0, "getParam with null got_size on mounted save succeeds"); +} + void MountAndCheck(const char* name, std::uint32_t mode, std::uint32_t expectedStatus, const std::string& what) { SceSaveDataDirName dirName{}; std::memcpy(dirName.data, name, std::strlen(name) + 1); @@ -42,6 +79,7 @@ void MountAndCheck(const char* name, std::uint32_t mode, std::uint32_t expectedS Check(code == 0, what + ": mounts"); if (code != 0) return; Check(result.mount_status == expectedStatus, what + ": status is " + std::to_string(result.mount_status) + ", expected " + std::to_string(expectedStatus)); + CheckMountInfoAndParam(&result.mount_point); Check(sceSaveDataUmount2(0, &result.mount_point) == 0, what + ": unmounts"); } @@ -106,6 +144,7 @@ int main() { std::filesystem::create_directories(root); const auto previous = std::filesystem::current_path(); std::filesystem::current_path(root); + CheckUnmountedCalls(); Check(sceSaveDataInitialize3(nullptr) == 0, "SaveData initializes"); MountAndCheck("fresh", MountReadWrite | MountCreate2, 1u, "CREATE2 of a missing save"); MountAndCheck("fresh", MountReadWrite | MountCreate2, 0u, "CREATE2 of an existing save"); diff --git a/docs/dev/debt/libSceSaveData.native.md b/docs/dev/debt/libSceSaveData.native.md index c12f2befd8..2b1ea19197 100644 --- a/docs/dev/debt/libSceSaveData.native.md +++ b/docs/dev/debt/libSceSaveData.native.md @@ -16,4 +16,5 @@ - [sceSaveDataDirNameSearch2](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) runs the same search as `sceSaveDataDirNameSearch` with the same condition and result structures, as [SharpEmu](https://github.com/sharpemu/sharpemu/blob/main/src/SharpEmu.Libs/SaveData/SaveDataExports.cs) does, its only source; what distinguishes it on the console is unknown. - [sceSaveDataMount3](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - a failed mount (BUSY, EXISTS, NOT_FOUND, MOUNT_FULL) leaves the result as it was, as shadPS4 does; not checked on hardware. Animal Well (PPSA22520) depends on it: it checks for its save with a mount it never unmounts, then saves with a second mount of the same directory, ignores its BUSY and keeps using the first mount point for `sceSaveDataPrepare`, its file and `sceSaveDataUmount2` - [sceSaveDataMount3](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - an unterminated or empty directory name, `.`, `..`, or a name containing `/`, `\` or `:` returns `SAVE_DATA_ERROR_PARAMETER`, which the [SDK 12.000 reference](https://github.com/peterpuppy/bm/blob/3f9d1ba6b896a53080460c903f5c15af8affb80d/features/ps5/document/output/psn_12/System/Save_Data/SaveData-Reference/sce-save-data-mount3.md#L196-L204) lists for an invalid parameter, and leaves the result as it was, as [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/569ec04d1a77127c601eed522ad5bba187173f9d/src/libs/libSaveData.cpp#L652-L658) does. The [SDK](https://github.com/peterpuppy/bm/blob/3f9d1ba6b896a53080460c903f5c15af8affb80d/features/ps5/document/output/psn_12/System/Save_Data/SaveData-Reference/sce-save-data-dir-name.md#L32-L36) lets an application use only letters, digits, `-`, `.` and `@` in a name, and KytyPS5 also accepts `_` ([`valid_path_component`](https://github.com/KytyPS5/KytyPS5/blob/569ec04d1a77127c601eed522ad5bba187173f9d/src/libs/libSaveData.cpp#L276-L287)); which other names the console's mount rejects is not checked on hardware +- [sceSaveDataGetMountInfo, sceSaveDataGetParam](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - null pointer arguments return `SAVE_DATA_ERROR_PARAMETER` instead of throwing C++ exceptions; unmounted targets return `SAVE_DATA_ERROR_NOT_MOUNTED`, and parameter type or buffer size violations return `SAVE_DATA_ERROR_PARAMETER` after slot lookup From 507032298f5ce4b69629a99405772570f1a71d14 Mon Sep 17 00:00:00 2001 From: Dextroid17 <217507631+Dextroid17@users.noreply.github.com> Date: Sat, 17 Oct 2026 09:30:15 -0400 Subject: [PATCH 0754/1000] test(agc): count mock Vulkan objects atomically --- .../prx/libSceAgcDriver/tests/Graphics.cpp | 30 ++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 95c09675a9..cbba137501 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -30,6 +30,7 @@ #include #include #include +#include #include #include #include @@ -3655,9 +3656,36 @@ struct MockDescriptorWrite { std::vector images; }; +struct LiveCounter { + std::atomic value{0}; + LiveCounter() = default; + LiveCounter(const LiveCounter& other) : value(other.value.load()) {} + LiveCounter& operator=(const LiveCounter& other) { + value.store(other.value.load()); + return *this; + } + operator std::int64_t() const { return value.load(); } + LiveCounter& operator++() { + ++value; + return *this; + } + LiveCounter& operator--() { + --value; + return *this; + } + LiveCounter& operator+=(std::int64_t count) { + value += count; + return *this; + } + LiveCounter& operator-=(std::int64_t count) { + value -= count; + return *this; + } +}; + struct MockVulkan { std::uint64_t next = 1; - std::int64_t live = 0; + LiveCounter live; std::map bufferSizes; std::map bufferUsage; std::map allocationFlags; From 256da9617cde64a2a649f2db98a0d2217415bd01 Mon Sep 17 00:00:00 2001 From: Ondra97 <47064087+Ondra97@users.noreply.github.com> Date: Sat, 17 Oct 2026 15:33:03 +0200 Subject: [PATCH 0755/1000] fix(agc): release unpublished descriptor cache objects --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 16 ++ .../Graphics/src/ShaderResources.cpp | 8 +- .../tests/DescriptorCacheFailure.cpp | 169 ++++++++++++++++++ 3 files changed, 192 insertions(+), 1 deletion(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/DescriptorCacheFailure.cpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index c6950a121b..b120e356ee 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1313,6 +1313,22 @@ add_test_executable(agc_driver_graphics_tests EXCLUDE_FROM_ALL tests/DepthSurfaceReuse.cpp ) +if(BUILD_TESTING) + add_test_executable(agc_descriptor_cache_failure_tests + tests/DescriptorCacheFailure.cpp + Graphics/src/ShaderResources.cpp) + target_include_directories(agc_descriptor_cache_failure_tests PRIVATE + $) + target_link_libraries(agc_descriptor_cache_failure_tests PRIVATE libSceAgcDriver shader_recompiler_agc_driver libc libkernel) + configure_windows_unwind(agc_descriptor_cache_failure_tests) + add_test(NAME agc_descriptor_cache_failure COMMAND agc_descriptor_cache_failure_tests) + set_tests_properties(agc_descriptor_cache_failure PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(agc_descriptor_cache_failure PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() + target_include_directories(agc_driver_graphics_tests PRIVATE ${LIBS_INCLUDE_DIR} $ diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index ab7d9b47ce..c0abdf782d 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -3410,7 +3410,12 @@ VkDescriptorSetLayout DescriptorCache::Layout(std::span key ValidateDescriptorLayout(context, bindings, updateAfterBind); ++stats.layoutMisses; const auto layout = CreateSetLayout(context, bindings, updateAfterBind, flags); - layouts.emplace(std::move(keyCopy), layout); + try { + layouts.emplace(std::move(keyCopy), layout); + } catch (...) { + destroyLayout(context.device, layout, nullptr); + throw; + } return layout; } @@ -3484,6 +3489,7 @@ DescriptorCache::SetAllocation DescriptorCache::Allocate(VkDescriptorSetLayout l poolInfo.maxSets = ChainPoolSets; poolInfo.poolSizeCount = static_cast(ChainPoolCreateSizes.size()); poolInfo.pPoolSizes = ChainPoolCreateSizes.data(); + chain.reserve(chain.size() + 1); VkDescriptorPool pool = VK_NULL_HANDLE; Check(context.Function("vkCreateDescriptorPool")(context.device, &poolInfo, nullptr, &pool), "vkCreateDescriptorPool"); chain.push_back({pool}); diff --git a/core/libs/prx/libSceAgcDriver/tests/DescriptorCacheFailure.cpp b/core/libs/prx/libSceAgcDriver/tests/DescriptorCacheFailure.cpp new file mode 100644 index 0000000000..070d6ab58e --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/DescriptorCacheFailure.cpp @@ -0,0 +1,169 @@ +#include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp" +#include +#include +#include +#include +#include +#include +#include + +namespace { +thread_local bool failNextAllocation = false; +} + +void* operator new(std::size_t size) { + if (failNextAllocation) { + failNextAllocation = false; + throw std::bad_alloc(); + } + if (void* memory = std::malloc(size == 0 ? 1 : size)) return memory; + throw std::bad_alloc(); +} + +void* operator new[](std::size_t size) { return ::operator new(size); } +void operator delete(void* memory) noexcept { std::free(memory); } +void operator delete[](void* memory) noexcept { std::free(memory); } +void operator delete(void* memory, std::size_t) noexcept { std::free(memory); } +void operator delete[](void* memory, std::size_t) noexcept { std::free(memory); } + +namespace { +using namespace AgcDriver::Graphics; + +struct VulkanObjects { + std::uintptr_t next = 1; + std::uint32_t layouts = 0; + std::uint32_t pools = 0; + std::uint32_t createdLayouts = 0; + std::uint32_t createdPools = 0; + bool failAfterLayout = false; + bool failAfterPool = false; + std::array poolUsed{}; +} objects; + +void RequireTest(bool condition, const char* message) { + if (condition) return; + std::fprintf(stderr, "%s\n", message); + std::abort(); +} + +VKAPI_ATTR VkResult VKAPI_CALL CreateLayout(VkDevice, const VkDescriptorSetLayoutCreateInfo*, const VkAllocationCallbacks*, VkDescriptorSetLayout* layout) { + *layout = reinterpret_cast(objects.next++); + ++objects.layouts; + ++objects.createdLayouts; + failNextAllocation = objects.failAfterLayout; + return VK_SUCCESS; +} + +VKAPI_ATTR void VKAPI_CALL DestroyLayout(VkDevice, VkDescriptorSetLayout, const VkAllocationCallbacks*) { + RequireTest(objects.layouts != 0, "layout destroyed twice"); + --objects.layouts; +} + +VKAPI_ATTR VkResult VKAPI_CALL CreatePool(VkDevice, const VkDescriptorPoolCreateInfo*, const VkAllocationCallbacks*, VkDescriptorPool* pool) { + RequireTest(objects.next < objects.poolUsed.size(), "mock pool capacity exceeded"); + *pool = reinterpret_cast(objects.next++); + ++objects.pools; + ++objects.createdPools; + failNextAllocation = objects.failAfterPool; + return VK_SUCCESS; +} + +VKAPI_ATTR void VKAPI_CALL DestroyPool(VkDevice, VkDescriptorPool, const VkAllocationCallbacks*) { + RequireTest(objects.pools != 0, "pool destroyed twice"); + --objects.pools; +} + +VKAPI_ATTR VkResult VKAPI_CALL AllocateSets(VkDevice, const VkDescriptorSetAllocateInfo* info, VkDescriptorSet* set) { + auto& used = objects.poolUsed.at(reinterpret_cast(info->descriptorPool)); + if (used) return VK_ERROR_OUT_OF_POOL_MEMORY; + used = true; + *set = reinterpret_cast(objects.next++); + return VK_SUCCESS; +} + +VKAPI_ATTR VkResult VKAPI_CALL ResetPool(VkDevice, VkDescriptorPool pool, VkDescriptorPoolResetFlags) { + objects.poolUsed.at(reinterpret_cast(pool)) = false; + return VK_SUCCESS; +} + +VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL Resolve(VkDevice, const char* name) { + if (std::strcmp(name, "vkCreateDescriptorSetLayout") == 0) return reinterpret_cast(CreateLayout); + if (std::strcmp(name, "vkDestroyDescriptorSetLayout") == 0) return reinterpret_cast(DestroyLayout); + if (std::strcmp(name, "vkCreateDescriptorPool") == 0) return reinterpret_cast(CreatePool); + if (std::strcmp(name, "vkDestroyDescriptorPool") == 0) return reinterpret_cast(DestroyPool); + if (std::strcmp(name, "vkAllocateDescriptorSets") == 0) return reinterpret_cast(AllocateSets); + if (std::strcmp(name, "vkResetDescriptorPool") == 0) return reinterpret_cast(ResetPool); + return nullptr; +} + +Context TestContext() { + Context context{}; + context.deviceProc = Resolve; + context.limits.maxPerStageResources = 128; + context.limits.maxDescriptorSetStorageBuffers = 4096; + context.descriptorIndexingLimits.maxPerStageUpdateAfterBindResources = 128; + context.descriptorIndexingLimits.maxDescriptorSetUpdateAfterBindStorageBuffers = 4096; + return context; +} + +const VkDescriptorSetLayoutBinding binding{0, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, VK_SHADER_STAGE_COMPUTE_BIT, nullptr}; +const std::array key{binding.binding, binding.descriptorType, binding.descriptorCount, binding.stageFlags}; +const VkDescriptorPoolSize size{VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1}; + +bool LayoutFailure(bool updateAfterBind) { + objects = {}; + bool failed = false; + { + DescriptorCache cache(TestContext()); + objects.failAfterLayout = true; + try { + cache.Layout(key, std::span(&binding, 1), updateAfterBind); + } catch (const std::bad_alloc&) { + failed = true; + } + failNextAllocation = false; + objects.failAfterLayout = false; + RequireTest(failed && objects.createdLayouts == 1, "layout publication failure was not exercised"); + const auto layout = cache.Layout(key, std::span(&binding, 1), updateAfterBind); + RequireTest(layout != VK_NULL_HANDLE && objects.createdLayouts == 2, "layout creation did not recover"); + RequireTest(cache.Layout(key, std::span(&binding, 1), updateAfterBind) == layout && objects.createdLayouts == 2, "layout cache hit created another object"); + } + std::printf("layout updateAfterBind=%d: unowned=%u\n", updateAfterBind, objects.layouts); + return objects.layouts == 0; +} + +bool PoolFailure(bool updateAfterBind, bool grow) { + objects = {}; + { + DescriptorCache cache(TestContext()); + const auto layout = cache.Layout(key, std::span(&binding, 1), updateAfterBind); + DescriptorCache::SetAllocation first{}; + if (grow) first = cache.Allocate(layout, std::span(&size, 1), updateAfterBind); + objects.failAfterPool = true; + DescriptorCache::SetAllocation allocation{}; + try { + allocation = cache.Allocate(layout, std::span(&size, 1), updateAfterBind); + } catch (const std::bad_alloc&) { + } + failNextAllocation = false; + objects.failAfterPool = false; + RequireTest(objects.createdPools == (grow ? 2u : 1u), "pool publication path was not exercised"); + if (allocation.set != VK_NULL_HANDLE) cache.Free(allocation); + if (grow) cache.Free(first); + const auto recovered = cache.Allocate(layout, std::span(&size, 1), updateAfterBind); + RequireTest(recovered.set != VK_NULL_HANDLE, "pool allocation did not recover"); + cache.Free(recovered); + } + std::printf("pool updateAfterBind=%d grow=%d: unowned=%u\n", updateAfterBind, grow, objects.pools); + return objects.layouts == 0 && objects.pools == 0; +} +} + +int main() { + int failures = 0; + for (const bool updateAfterBind : {false, true}) { + if (!LayoutFailure(updateAfterBind)) ++failures; + for (const bool grow : {false, true}) if (!PoolFailure(updateAfterBind, grow)) ++failures; + } + return failures == 0 ? 0 : 1; +} From 372705c8d05844550d06459366a2ba95c5db8c7e Mon Sep 17 00:00:00 2001 From: pushkarreddyy <212133856+pushkarreddyy@users.noreply.github.com> Date: Sat, 17 Oct 2026 19:18:07 +0530 Subject: [PATCH 0756/1000] fix(tools): accept top-level event arrays and empty documents in trace_digest --- tools/tests/test_trace_digest.py | 8 ++++++++ tools/trace_digest.py | 3 ++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/tools/tests/test_trace_digest.py b/tools/tests/test_trace_digest.py index af1ae2e0f7..66fef3fe19 100644 --- a/tools/tests/test_trace_digest.py +++ b/tools/tests/test_trace_digest.py @@ -97,6 +97,14 @@ class TraceDigestTests(unittest.TestCase): self.assertEqual(thread["folded_ms_per_frame"], 1.5) self.assertEqual(thread["untraced_ms_per_frame"], 0) + def test_trace_accepts_event_array_and_empty_document(self): + trace_from_array = trace_digest.Trace(EVENTS) + self.assertEqual(len(trace_from_array.presents), 3) + empty_dict = trace_digest.Trace({}) + self.assertEqual(empty_dict.presents, []) + empty_list = trace_digest.Trace([]) + self.assertEqual(empty_list.presents, []) + def result(self): return trace_digest.digest(trace_digest.load_trace(self.trace), trace_digest.load_summary(None, self.trace), trace_digest.load_stacks(self.stacks), worst=2, top=3) diff --git a/tools/trace_digest.py b/tools/trace_digest.py index 000276e4f9..1a8a8aa6ec 100644 --- a/tools/trace_digest.py +++ b/tools/trace_digest.py @@ -32,7 +32,8 @@ class Trace: self.presents = [] self.folded = collections.defaultdict(float) self.batched = [] - for event in document["traceEvents"]: + events = document if isinstance(document, list) else (document.get("traceEvents") or []) + for event in events: phase = event.get("ph") if phase == "M" and event.get("name") == "thread_name": self.threads[event["tid"]] = event["args"]["name"] From 1c9d9d6bc5492b1580efc55a7dbed4a3d68bc1ed Mon Sep 17 00:00:00 2001 From: milescoviello Date: Sat, 17 Oct 2026 13:27:45 +0000 Subject: [PATCH 0757/1000] fix(build): run the CPU-side tests in an ANYPS5_ENABLE_ASAN build Under -DANYPS5_ENABLE_ASAN=ON with ASAN_OPTIONS=detect_leaks=0, 14 of the 849 tests outside the AGC driver, shader and VideoOut suites failed on main for reasons that come from the ASan build itself, not from the code they test: - libc copies the main executable's file-backed writable pages before remapping them anonymously. In an instrumented test executable those pages hold ASan's redzones around globals, so the memcpy reported a global-buffer-overflow at load (patched_libraries, guest_memory_lifetime). The ASan build copies the pages word by word without instrumentation. - Five tests check that a local variable is on the thread's stack. With detect_stack_use_after_return, which is on by default on Linux, instrumented locals live on ASan's fake stack. These tests now run with it off. guest_kernel_is_stack also bounded the worker stack by the application area without the ASan build's arena offset. - The relinker module tests and loadexec_arguments load instrumented libraries into a Python process or a relinked guest, so the ASan runtime is not first in the initial library list. They now run with verify_asan_link_order=0. Preloading libasan instead put its __cxa_throw interceptor ahead of libc.prx's and broke the unwind of an exception that LoadExec throws and catches. - guest_allocator_failures limits RLIMIT_DATA to 128 MiB and then commits 300 GiB. ASan maps 37.5 GiB of shadow for that commit, which the limit refuses, and aborts. The ASan build skips that section. - fiber checks the address of a local against the fiber's context too, so it runs without fake stacks as well. Its CheckActiveContext then reads the fiber stack word by word above that local, through the redzones of the frames on that stack, and is no longer instrumented. - agc_multisample_guest_access passed aligned_alloc a size that is not a multiple of the alignment, which C11 does not allow and ASan rejects. It now rounds the size up. add_asan_test_options appends to ASAN_OPTIONS for one test in an ASan build and does nothing otherwise. Co-Authored-By: Claude Opus 5.5 --- CMakeLists.txt | 6 ++++++ core/libs/prx/libSceAgcDriver/tests/Recorder.cpp | 2 +- core/libs/prx/libSceFiber/CMakeLists.txt | 1 + core/libs/prx/libSceSystemService/CMakeLists.txt | 1 + core/libs/prx/libc/src/GuestAllocations.cpp | 12 ++++++++++++ core/libs/prx/libkernel/CMakeLists.txt | 5 +++++ core/libs/tests/Fiber.cpp | 2 +- core/libs/tests/GuestAllocatorFailures.cpp | 4 ++++ core/libs/tests/GuestKernelIsStack.cpp | 4 ++++ core/relinker/CMakeLists.txt | 3 +++ 10 files changed, 38 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 2f79844c69..7d4eb6957c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -17,6 +17,12 @@ function(add_test_executable target) endif() endfunction() +function(add_asan_test_options test options) + if(ANYPS5_ENABLE_ASAN) + set_property(TEST ${test} APPEND PROPERTY ENVIRONMENT_MODIFICATION "ASAN_OPTIONS=string_append::${options}") + endif() +endfunction() + option(BUILD_TESTING "Build and register relinker tests" OFF) option(ANYPS5_RELINKER_ONLY "Build the relinker without system libraries or third-party dependencies" OFF) option(ANYPS5_BUILD_WASM "Build the browser relinker" OFF) diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index b496c3428b..9b9adfdc01 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -6531,7 +6531,7 @@ void fmaskDecompressMsaaTests(const Device& device, Recorder& recorder) { #ifdef _WIN32 void* block = VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); #else - void* block = std::aligned_alloc(65536, bytes); + void* block = std::aligned_alloc(65536, (bytes + 65535u) & ~std::size_t{65535u}); #endif Require(block != nullptr, "cannot allocate the FMASK decompress block"); std::memset(block, 0, bytes); diff --git a/core/libs/prx/libSceFiber/CMakeLists.txt b/core/libs/prx/libSceFiber/CMakeLists.txt index 6aa8ddda2a..883b7fc154 100644 --- a/core/libs/prx/libSceFiber/CMakeLists.txt +++ b/core/libs/prx/libSceFiber/CMakeLists.txt @@ -8,6 +8,7 @@ if(BUILD_TESTING) target_include_directories(fiber_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(fiber_tests PRIVATE libSceFiber libc) add_test(NAME fiber COMMAND fiber_tests) + add_asan_test_options(fiber detect_stack_use_after_return=0) if(WIN32) set_tests_properties(fiber PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") diff --git a/core/libs/prx/libSceSystemService/CMakeLists.txt b/core/libs/prx/libSceSystemService/CMakeLists.txt index 1a87c592e5..78aa978dad 100644 --- a/core/libs/prx/libSceSystemService/CMakeLists.txt +++ b/core/libs/prx/libSceSystemService/CMakeLists.txt @@ -27,6 +27,7 @@ if(BUILD_TESTING) $ $ $ $ $ $ ${CMAKE_CXX_COMPILER}) set_tests_properties(loadexec_arguments PROPERTIES TIMEOUT 30) + add_asan_test_options(loadexec_arguments verify_asan_link_order=0) endif() add_test_executable(guest_system_exit_tests ${LIBS_INCLUDE_DIR}/tests/GuestSystemExit.cpp) target_include_directories(guest_system_exit_tests PRIVATE ${LIBS_INCLUDE_DIR}) diff --git a/core/libs/prx/libc/src/GuestAllocations.cpp b/core/libs/prx/libc/src/GuestAllocations.cpp index 07084ef2a4..438a819c8f 100644 --- a/core/libs/prx/libc/src/GuestAllocations.cpp +++ b/core/libs/prx/libc/src/GuestAllocations.cpp @@ -95,12 +95,24 @@ std::vector> fileBackedWritableImage() { return mappings; } +#ifdef ANYPS5_ENABLE_ASAN +[[gnu::no_sanitize("address")]] void copyImagePages(void* target, const void* source, std::size_t bytes) { + auto* to = static_cast(target); + const auto* from = static_cast(source); + for (std::size_t word = 0; word < bytes / sizeof(std::uintptr_t); ++word) to[word] = from[word]; +} +#endif + bool backWritableImageAnonymously() { for (const auto& [address, bytes] : fileBackedWritableImage()) { auto* image = reinterpret_cast(address); auto* copy = mmap(nullptr, bytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); if (copy == MAP_FAILED) throw std::system_error(errno, std::generic_category(), "mmap of an anonymous main image copy failed"); +#ifdef ANYPS5_ENABLE_ASAN + copyImagePages(copy, image, bytes); +#else std::memcpy(copy, image, bytes); +#endif if (mremap(copy, bytes, bytes, MREMAP_MAYMOVE | MREMAP_FIXED, image) == MAP_FAILED) throw std::system_error(errno, std::generic_category(), "mremap of an anonymous main image copy failed"); } return true; diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index ebf43259ba..5d85a9e4d9 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -376,6 +376,7 @@ if(BUILD_TESTING) configure_windows_unwind(guest_kernel_is_stack_tests) add_test(NAME guest_kernel_is_stack COMMAND guest_kernel_is_stack_tests) set_tests_properties(guest_kernel_is_stack PROPERTIES TIMEOUT 20) + add_asan_test_options(guest_kernel_is_stack detect_stack_use_after_return=0) if(WIN32) set_tests_properties(guest_kernel_is_stack PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") @@ -406,6 +407,7 @@ if(BUILD_TESTING) configure_windows_unwind(guest_kernel_stack_protect_tests) add_test(NAME guest_kernel_stack_protect COMMAND guest_kernel_stack_protect_tests) set_tests_properties(guest_kernel_stack_protect PROPERTIES TIMEOUT 20) + add_asan_test_options(guest_kernel_stack_protect detect_stack_use_after_return=0) if(WIN32) set_tests_properties(guest_kernel_stack_protect PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") @@ -416,6 +418,7 @@ if(BUILD_TESTING) configure_windows_unwind(guest_thread_self_tests) add_test(NAME guest_thread_self COMMAND guest_thread_self_tests) set_tests_properties(guest_thread_self PROPERTIES TIMEOUT 20) + add_asan_test_options(guest_thread_self detect_stack_use_after_return=0) if(WIN32) set_tests_properties(guest_thread_self PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") @@ -516,6 +519,7 @@ if(BUILD_TESTING) configure_windows_unwind(guest_pthread_stack_tests) add_test(NAME guest_pthread_stack COMMAND guest_pthread_stack_tests) set_tests_properties(guest_pthread_stack PROPERTIES TIMEOUT 20) + add_asan_test_options(guest_pthread_stack detect_stack_use_after_return=0) if(WIN32) set_tests_properties(guest_pthread_stack PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") @@ -816,6 +820,7 @@ if(BUILD_TESTING) configure_windows_unwind(guest_raise_exception_tests) add_test(NAME guest_raise_exception COMMAND guest_raise_exception_tests) set_tests_properties(guest_raise_exception PROPERTIES TIMEOUT 30) + add_asan_test_options(guest_raise_exception detect_stack_use_after_return=0) if(WIN32) set_tests_properties(guest_raise_exception PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") diff --git a/core/libs/tests/Fiber.cpp b/core/libs/tests/Fiber.cpp index 9e305c3080..a6407053b7 100644 --- a/core/libs/tests/Fiber.cpp +++ b/core/libs/tests/Fiber.cpp @@ -51,7 +51,7 @@ FiberStorage g_secondStorage; auto* const g_first = reinterpret_cast(&g_firstStorage); auto* const g_second = reinterpret_cast(&g_secondStorage); -void CheckActiveContext(FiberObject* fiber, std::uint64_t expectedSize) { +[[gnu::no_sanitize("address")]] void CheckActiveContext(FiberObject* fiber, std::uint64_t expectedSize) { FiberInfo info{}; info.size = sizeof(info); Require(sceFiberGetInfo(fiber, &info) == 0, "active fiber context info"); diff --git a/core/libs/tests/GuestAllocatorFailures.cpp b/core/libs/tests/GuestAllocatorFailures.cpp index 22629e0166..177e5496f5 100644 --- a/core/libs/tests/GuestAllocatorFailures.cpp +++ b/core/libs/tests/GuestAllocatorFailures.cpp @@ -107,6 +107,9 @@ int main() { free_nid_postfix(block); } +#ifdef ANYPS5_ENABLE_ASAN + std::fputs("skipped: AddressSanitizer cannot map the shadow of a 300 GiB commit under a 128 MiB RLIMIT_DATA, so the failed commit and arena release checks did not run\n", stderr); +#else if (GuestArena::GuestArenaAvailable_nid_postfix()) { #ifdef __linux__ rlimit originalLimit{}; @@ -175,6 +178,7 @@ int main() { if (large == nullptr) report("malloc(1 MiB) after a failed huge malloc", "returned NULL"); free_nid_postfix(large); } +#endif void* untouched = &failures; *__error_nid_postfix() = 77; diff --git a/core/libs/tests/GuestKernelIsStack.cpp b/core/libs/tests/GuestKernelIsStack.cpp index 941ffdc975..968cdc981f 100644 --- a/core/libs/tests/GuestKernelIsStack.cpp +++ b/core/libs/tests/GuestKernelIsStack.cpp @@ -13,7 +13,11 @@ int APS5_VABI sceKernelVirtualQuery(const void* addr, int flags, VirtualQueryInf } static constexpr int SCE_OK = 0; +#ifdef ANYPS5_ENABLE_ASAN +static constexpr std::uintptr_t APPLICATION_AREA_END = 0x000010FC00000000ull; +#else static constexpr std::uintptr_t APPLICATION_AREA_END = 0x000000FC00000000ull; +#endif static constexpr int SCE_KERNEL_ERROR_EACCES = static_cast(0x8002000d); static int global = 0; diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index 1348d601a6..a0b72b06c9 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -532,6 +532,9 @@ if(BUILD_TESTING) add_test(NAME guest_module_fini COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_module_fini.py $ $) + add_asan_test_options(guest_module_args verify_asan_link_order=0) + add_asan_test_options(guest_deferred_dependencies verify_asan_link_order=0) + add_asan_test_options(guest_module_fini verify_asan_link_order=0) add_test(NAME guest_needed_nested_modules COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_needed_nested_modules.py $) set(windows_stdio_host_arguments) From 9834a9f851a2599e7110f2babafa31aa662da26c Mon Sep 17 00:00:00 2001 From: MrMha <114929652+MrMha@users.noreply.github.com> Date: Sat, 17 Oct 2026 17:59:40 +0400 Subject: [PATCH 0758/1000] fix: reject library archive file and directory conflicts --- tools/build_web.py | 8 ++++++-- tools/tests/test_build_web.py | 20 ++++++++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/tools/build_web.py b/tools/build_web.py index 7af1cc6992..cd19113603 100644 --- a/tools/build_web.py +++ b/tools/build_web.py @@ -63,14 +63,18 @@ def publish_libraries(output): files = archive.infolist() if not files or len(files) > 4096: raise ValueError("Invalid library archive") - names = set() + names = {} for file in files: if not file.filename.startswith("libs/") or any(part in ("..", ".", "") for part in file.filename.rstrip("/").split("/")) or "\\" in file.filename: raise ValueError("Invalid library archive layout") name = file.filename.rstrip("/").lower() if name in names: raise ValueError(f"Duplicate library archive path: {file.filename}") - names.add(name) + names[name] = file.is_dir() + for name in names: + parts = name.split("/") + if any(names.get("/".join(parts[:index])) is False for index in range(1, len(parts))): + raise ValueError(f"Library archive file is also used as a directory: {name}") expanded = sum(file.file_size for file in files) if expanded > MAX_EXPANDED_BYTES: raise ValueError("Release archive exceeds maximum expanded size") diff --git a/tools/tests/test_build_web.py b/tools/tests/test_build_web.py index 47c057babc..3a56e49429 100644 --- a/tools/tests/test_build_web.py +++ b/tools/tests/test_build_web.py @@ -41,6 +41,26 @@ class LibraryPublicationTests(unittest.TestCase): report = json.loads(self.manifest.read_text(encoding="utf-8")) self.assertTrue(all(asset["expandedBytes"] == 4096 for asset in report["platforms"].values())) + def test_file_ancestors_are_rejected_before_publication(self): + valid = library_zip([("libs/libc.prx", b"valid")]) + for entries in (("libs/nested", "libs/nested/libc.prx"), + ("libs/NESTED/libc.prx", "libs/nested")): + with self.subTest(entries=entries): + invalid = library_zip([(name, b"content") for name in entries]) + with self.assertRaisesRegex(ValueError, "file is also used as a directory"): + self.publish(invalid, valid) + self.assertEqual(self.manifest.read_bytes(), self.previous) + self.assertEqual(list(self.libraries.iterdir()), [self.existing]) + + + def test_explicit_and_implicit_archive_directories_are_valid(self): + for entries in ([('libs/sub/libc.prx', b'content')], + [('libs/sub/', b''), ('libs/sub/libc.prx', b'content')]): + with self.subTest(entries=entries): + payload = library_zip(entries) + self.publish(payload, payload) + self.assertEqual(json.loads(self.manifest.read_text(encoding="utf-8"))["tag"], "v1") + def setUp(self): temporary = tempfile.TemporaryDirectory() From a6c123ca4acc2f0bfec2415e129d3aa840e7c97e Mon Sep 17 00:00:00 2001 From: mattogepeto Date: Thu, 15 Oct 2026 23:20:58 +0200 Subject: [PATCH 0759/1000] fix(libkernel): return EPROTOTYPE for a socket type inet does not have socket() returned EPROTONOSUPPORT for every socket type it does not create. FreeBSD 11.4's socreate returns EPROTOTYPE when no protocol is found, the protocol is 0 and the type is not 0, and EPROTONOSUPPORT when a protocol was given. Its inet domain (in_proto.c) has SOCK_RAW entries in every kernel and a SOCK_SEQPACKET entry only with SCTP, and no SOCK_RDM or other type. Return EPROTOTYPE for a type the inet domain never has, such as SOCK_RDM, with protocol 0. SOCK_RAW and SOCK_SEQPACKET keep EPROTONOSUPPORT, since whether the console creates them is not known. Test the unknown types and mismatched protocols for AF_INET and AF_INET6, and list the socket types and families that are not created in the library's technical debt file. Written with AI assistance (Claude). Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libkernel/Socket/src/Udp.cpp | 4 +++- core/libs/tests/GuestSocketDescriptors.cpp | 17 +++++++++++++++++ docs/dev/debt/libkernel.md | 1 + 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index 5c6fc0b521..4512bf1fc0 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -924,7 +924,9 @@ int APS5_VABI socket_nid_postfix(int family, int type, int protocol) { if (family != 2 && family != 28) return Fail(47); const int nativeType = plainType == 1 ? SOCK_STREAM : plainType == 2 ? SOCK_DGRAM : -1; const int expectedProtocol = plainType == 1 ? 6 : 17; - if (nativeType == -1 || (protocol != 0 && protocol != expectedProtocol)) return Fail(43); + const bool inetKnowsType = plainType == 3 || plainType == 5; + if (nativeType == -1) return Fail(protocol == 0 && plainType != 0 && !inetKnowsType ? 41 : 43); + if (protocol != 0 && protocol != expectedProtocol) return Fail(43); #ifdef _WIN32 static const int startup = [] { WSADATA data{}; return WSAStartup(MAKEWORD(2, 2), &data); }(); if (startup) return Fail(5); diff --git a/core/libs/tests/GuestSocketDescriptors.cpp b/core/libs/tests/GuestSocketDescriptors.cpp index e8340085ae..76618bf409 100644 --- a/core/libs/tests/GuestSocketDescriptors.cpp +++ b/core/libs/tests/GuestSocketDescriptors.cpp @@ -181,7 +181,24 @@ static void DuplicateKeepsSocket() { Require(close_nid_postfix(sender) == 0); } +static void SocketTypeErrors() { + constexpr int PrototypeError = 41; + constexpr int ProtocolError = 43; + constexpr int FamilyError = 47; + constexpr int Inet6 = 28; + for (const int family : {Inet, Inet6}) { + Require(socket_nid_postfix(family, 99, 0) == -1 && *__error_nid_postfix() == PrototypeError); + Require(socket_nid_postfix(family, 4, 0) == -1 && *__error_nid_postfix() == PrototypeError); + Require(socket_nid_postfix(family, 0, 0) == -1 && *__error_nid_postfix() == ProtocolError); + Require(socket_nid_postfix(family, 99, 6) == -1 && *__error_nid_postfix() == ProtocolError); + Require(socket_nid_postfix(family, 1, 17) == -1 && *__error_nid_postfix() == ProtocolError); + Require(socket_nid_postfix(family, 2, 6) == -1 && *__error_nid_postfix() == ProtocolError); + } + Require(socket_nid_postfix(99, 1, 0) == -1 && *__error_nid_postfix() == FamilyError); +} + int main() { + SocketTypeErrors(); SelectSocketAndFile(); CloseReleasesNumber(); DuplicateKeepsSocket(); diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 5791f7ab5e..7acb437253 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -175,6 +175,7 @@ - [Socket error numbers](../../../core/libs/prx/libkernel/Socket/include/SocketError.hpp) (libkernel) - a host socket error becomes the FreeBSD 11 `errno.h` number of the same name; the numbers are not checked on the console. Winsock `WSAESHUTDOWN` becomes `EPIPE`, as a send after `shutdown` does on FreeBSD, and a host error with no FreeBSD socket counterpart becomes `EIO`. A nonblocking `connect` in progress reports `EINPROGRESS` on Linux but `EAGAIN` on Windows (`WSAEWOULDBLOCK`), where FreeBSD reports `EINPROGRESS`. On Windows a refused nonblocking `connect` is never reported (`SO_ERROR` is unsupported), so a repeated one keeps reporting `EALREADY` where FreeBSD reports `ECONNREFUSED`. A receive after `shutdown(SHUT_RD)` reports `EPIPE` on Windows where FreeBSD returns 0 - [getsockopt, setsockopt](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) on a guest socket handle the SOL_SOCKET level and `TCP_NODELAY`, after FreeBSD 11.4's [`sosetopt` and `sogetopt`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c): `SO_REUSEADDR`, `SO_KEEPALIVE`, `SO_DONTROUTE`, `SO_BROADCAST`, `SO_OOBINLINE` and `SO_LINGER` act on the host socket, and `SO_ERROR`, `SO_TYPE`, `SO_ACCEPTCONN`, `SO_SNDBUF` and `SO_RCVBUF` are read. A boolean option reads back as its own bit, as FreeBSD returns `so_options & option`. Winsock does not clear a socket's pending error when it is read, so on Windows the library reports a pending error once and shows it again only after a new `connect`; a second failure with the same code on the same socket without a `connect` in between would be hidden. `SO_SNDBUF` and `SO_RCVBUF` read the host's value, which is not the size FreeBSD reports (Linux doubles it). `SO_NOSIGPIPE` is kept per socket and reads back as its bit. On Linux a `send`, `sendto`, `sendmsg` or `write` on that socket passes `MSG_NOSIGNAL` to the host, as [`kern_sendit`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_syscalls.c#L890-L895) and [`soo_write`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/sys_socket.c#L147-L151) leave out the signal. An accepted socket takes it from its listener, as [`sonewconn`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L603) copies the options. `SO_NBIO` (`0x1200`) is the console's own option, not FreeBSD's. It switches the socket between blocking and non-blocking as `FIONBIO` does and reads back as 0 or 1, as `sceNetSetsockopt` and `sceNetGetsockopt` treat it here; what the console reads back for it is not measured. `TCP_NODELAY` (`IPPROTO_TCP`, 1) is set on the host socket and reads back as `TF_NODELAY` (4), as [`tcp_default_ctloutput`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/netinet/tcp_usrreq.c#L1860-L1861) returns it. The `IPPROTO_TCP` level on a datagram or `AF_UNIX` socket fails with `EINVAL`, as [`ip_ctloutput`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/netinet/ip_output.c#L943-L944) and [`uipc_ctloutput`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_usrreq.c#L1228-L1229) answer it, and an accepted socket takes `TCP_NODELAY` through the host. `SO_SNDTIMEO` and `SO_RCVTIMEO` follow [`sosetopt`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L2657-L2689) and [`sogetopt`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L2846-L2850). A 16-byte `timeval` with a negative field or 1000000 microseconds or more fails with `EDOM`, and more than `INT32_MAX` seconds becomes the longest wait. The value is kept and read back through FreeBSD's 32.32 fixed-point conversion ([`tvtosbt`, `sbttotv`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/sys/time.h#L178-L286)), so half a second reads back as 499999 microseconds and one microsecond as 0. A `recv`, `recvfrom`, `recvmsg`, `read` or `readv`, and a `send`, `sendto`, `sendmsg`, `write` or `writev`, on a blocking socket then waits that long for the host to report the socket readable or writable, and fails with `EWOULDBLOCK`; a call with `MSG_DONTWAIT` does not wait. An accepted socket takes both from its listener ([`sonewconn`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L629-L630)). The wait covers only the first transfer: once the socket is writable the host send runs without a limit, where FreeBSD stops a send that cannot finish and returns the bytes sent so far, and a receive whose data another thread takes between the wait and the host call blocks again. `sendfile` ignores the send timeout. The host wait has millisecond steps, so a shorter timeout lasts up to a millisecond longer. A null value fails with `EINVAL`, where FreeBSD's [`kern_setsockopt`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_syscalls.c#L1414-L1415) returns `EFAULT`. Every other SOL_SOCKET option (`SO_DEBUG`, `SO_USELOOPBACK`, `SO_REUSEPORT` and `SO_TIMESTAMP`, which FreeBSD stores and no title has been seen to set, the low-water marks among them), every other `IPPROTO_TCP` option (`TCP_MAXSEG`, `TCP_KEEPIDLE`, `TCP_KEEPINTVL` and `TCP_KEEPCNT` among them) and every other level (`IPPROTO_IP`, `IPPROTO_IPV6`) fail with `ENOPROTOOPT`. None of this was measured on a console. - [connect to AF_UNSPEC](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) dissolves a datagram association as FreeBSD 11.4's `udp_disconnect()` does, which resets the local address to the wildcard address and keeps the port. On Linux a socket bound to an explicit port keeps its local address as well (`127.0.0.1:` stays `127.0.0.1:`), because a bound socket cannot be bound again; one bound to port 0 is bound again to the wildcard address with the port it had +- [socket](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) creates only `SOCK_STREAM` and `SOCK_DGRAM` sockets in `AF_INET` and `AF_INET6`. A type that FreeBSD 11.4's inet domain has no protocol for in any configuration ([`in_proto.c`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/netinet/in_proto.c) has no `SOCK_RDM` entry and no unknown type) fails with `EPROTOTYPE` when the protocol is 0 and `EPROTONOSUPPORT` otherwise, as [`socreate`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c) does. `SOCK_RAW`, which `in_proto.c` always has (`socreate` creates it for a privileged process), and `SOCK_SEQPACKET`, which it has only in a kernel built with `SCTP`, fail with `EPROTONOSUPPORT`; whether the console allows either is not known. `SOCK_DGRAM` with `IPPROTO_UDPLITE` and `AF_UNIX` (`socketpair` supports it) fail with `EPROTONOSUPPORT` and `EAFNOSUPPORT` where FreeBSD creates the socket - [msync](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) (libkernel) follows FreeBSD 11's `kern_msync` and `vm_map_sync`, not measured on the console: the address is rounded down to the page, a range that wraps is `EINVAL`, as is `MS_ASYNC` with `MS_INVALIDATE`, and a range whose start or any page is not mapped (as `sceKernelVirtualQuery` reports it, reservations included) is `ENOMEM`; a page unmapped from inside a larger mapping still counts as mapped there, since `sceKernelVirtualQuery` keeps reporting the whole mapping. The guest has no file mappings, so a successful call writes nothing back. `MS_INVALIDATE` on a mapped range throws: FreeBSD returns `EBUSY` when the range holds `mlock`ed pages, and locked pages are not tracked - [madvise](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) (libkernel) accepts `MADV_PROTECT` (10) as a no-op hint without modifying process protection. FreeBSD 11's `kern_madvise` (`sys/vm/vm_mmap.c`) handles it before the address check, ignores the address and length, and calls `kern_procctl(..., PROC_SPROTECT, PPROT_SET)`, which requires `PRIV_VM_MADV_PROTECT` and fails with `EPERM` without it. Returning 0 unconditionally assumes the calling process is privileged. Console behaviour is unverified - [mincore](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) (libkernel) follows FreeBSD 11's `sys_mincore` (`sys/vm/vm_mmap.c`), not measured on the console: the address is rounded down to the page, length 0 returns 0, a null vector over a mapped range returns `EFAULT`, an overflowing range or any unmapped page in the range (as reported by `sceKernelVirtualQuery`) returns `ENOMEM`. For valid ranges, a page reports `MINCORE_INCORE` (0x1) when `sceKernelVirtualQuery` reports it committed and 0 when it is only reserved; actual residency, referenced and modified bits are not modelled. Length 0 returns 0 when the start page is mapped and `ENOMEM` otherwise, and the range and mapping checks come before the null vector `EFAULT`, as in `kern_mincore` Java 25 LAN / DNS (PPSA99869) imports it From 8eecd5060055d01fa7a29547e58aff963ef9e0e2 Mon Sep 17 00:00:00 2001 From: MrMha <114929652+MrMha@users.noreply.github.com> Date: Sat, 17 Oct 2026 18:01:02 +0400 Subject: [PATCH 0760/1000] fix: exclude test export aliases from import audit classification --- tools/import_audit.py | 2 +- tools/tests/test_import_audit.py | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/tools/import_audit.py b/tools/import_audit.py index ce551a47a7..a75e4384b8 100644 --- a/tools/import_audit.py +++ b/tools/import_audit.py @@ -454,7 +454,7 @@ def source_stubs(source): shared = {} for library in sorted(item for item in Path(source).iterdir() if item.is_dir()): aliases = {} - for file in library.rglob("*.cpp"): + for file in nid_names.source_files(library): for nid, name in nid_names.source_aliases(file.read_text(errors="ignore")): aliases[name] = nid group = progress.scan_library(library) diff --git a/tools/tests/test_import_audit.py b/tools/tests/test_import_audit.py index 9fd1ec2cac..10125129fa 100644 --- a/tools/tests/test_import_audit.py +++ b/tools/tests/test_import_audit.py @@ -224,6 +224,20 @@ class ImportAuditTests(unittest.TestCase): registry.write_text(json.dumps([dict(nid="AAAAAAAAAAA", library="libSceA.prx", **fields)]), encoding="utf-8") self.assertEqual(import_audit.read_registry(registry), [("AAAAAAAAAAA", "libSceA.prx", expected)]) + def test_test_file_aliases_cannot_override_production_stubs(self): + source = self.work.source + text = 'APS5_EXPORT("AAAAAAAAAAA", sceExample);\n' + STUB_SOURCE.format(name="sceExample") + write_source(source, "libSceExample", text) + test = source / "libSceExample" / "tests" / "Alias.cpp" + test.parent.mkdir() + test.write_text('APS5_EXPORT("BBBBBBBBBBB", sceExample);\n', encoding="utf-8") + stubs = import_audit.source_stubs(source) + self.assertEqual(stubs, {("libSceExample.prx", "AAAAAAAAAAA"): "sceExample"}) + records, _ = import_audit.audit([("AAAAAAAAAAA", "libSceExample.prx", False)], + {"AAAAAAAAAAA": ["libSceExample.prx"]}, stubs, + {"libSceExample.prx"}, set()) + self.assertEqual(records[0]["class"], "stub") + def setUp(self): self.work = Workspace() From 38ed5aa7019772f6daae9d6c72b6045114cd97a9 Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Sat, 17 Oct 2026 14:02:26 +0000 Subject: [PATCH 0761/1000] docs(usage): explain unlisted PlayGo chunk choices --- docs/user/USAGE.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index db549d01c5..15a4f6d625 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -157,6 +157,17 @@ Games that open the console's system font sets (`sceFontOpenFontSet`) need font Games ask for the console's language with `sceSystemServiceParamGetInt` and show their text in it when they have it. It is English (United States) unless `ANYPS5_LANGUAGE` is set to another console language number: 0 Japanese, 1 English (United States), 2 French, 3 Spanish, 4 German, 5 Italian, 6 Dutch, 7 Portuguese (Portugal), 8 Russian, 9 Korean, 10 Chinese (Traditional), 11 Chinese (Simplified), 12 Finnish, 13 Swedish, 14 Danish, 15 Norwegian, 16 Polish, 17 Portuguese (Brazil), 18 English (United Kingdom), 19 Turkish, 20 Spanish (Latin America), 21 Arabic, 22 French (Canada), 23 Czech, 24 Hungarian, 25 Greek, 26 Romanian, 27 Thai, 28 Vietnamese, 29 Indonesian, 30 Ukrainian. For example, `ANYPS5_LANGUAGE=21` selects Arabic. An empty value is the same as an unset one; any other value stops the game at its first language query with an error that names the variable. +### PlayGo dumps without metadata + +When a dump has neither `sce_sys/playgo-chunk.dat` nor `sce_sys/playgo-scenario.json`, AnyPS5 cannot tell which nonzero PlayGo chunk IDs exist. Requests for those IDs stop with an error by default. Set `APS5_PLAYGO_UNLISTED_CHUNKS` to choose an answer for that dump: + +| Value | Answer for an unlisted chunk ID | Observed title that needs it | +|-------|---------------------------------|------------------------------| +| `installed` | Report it as installed (`LOCAL_FAST`). | High on Life (PPSA15388) needs this to load its saves. | +| `absent` | Return `SCE_PLAYGO_ERROR_BAD_CHUNK_ID`. | Sifu (PPSA03001) uses this result to end its chunk-ID search. | + +Leave the variable unset to keep the explicit error. Any other value also stops with an error. Dumps that include PlayGo metadata are unaffected. These title-specific results come from emulator runs and have not been verified against a console. + ### GPU selection The game runs on the first Vulkan 1.1 device with graphics and compute queues and swapchain presentation, preferring a discrete GPU over an integrated one. Set `ANYPS5_GPU` to a part of a device name, compared without regard to case, to run on another device; the names are printed at start-up in the `Physical device candidate` lines. When no usable device contains the text, the start fails and the error lists the device names. From 7bb76080c68fbc9bdf2f1eb9f1b3f96e529b1aeb Mon Sep 17 00:00:00 2001 From: pushkarreddyy <212133856+pushkarreddyy@users.noreply.github.com> Date: Sat, 17 Oct 2026 17:51:10 +0530 Subject: [PATCH 0762/1000] fix(libkernel): return EFAULT from truncate, utimes, mkdir, rmdir, and rename for a null path --- core/libs/prx/libkernel/File/src/Stdio.cpp | 10 +++++----- core/libs/tests/GuestPosixScalarIo.cpp | 12 ++++++++++++ 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index 510c992a55..eacf4004dc 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -1053,7 +1053,7 @@ int APS5_VABI getdents_nid_postfix(int fd, char* buf, int nbytes) { int APS5_VABI sceKernelMkdir(const char* path, uint16_t mode) { (void)mode; - if (path == nullptr) throw std::invalid_argument("sceKernelMkdir: path is null"); + if (!path) return SceErrorFromErrno(GUEST_EFAULT); if (!*path) return SceErrorFromErrno(GUEST_ENOENT); std::filesystem::path native; if (const int error = ResolvePathError_nid_no_patch(path, native)) return SceKernelError(error); @@ -1270,7 +1270,7 @@ int64_t APS5_VABI pwritev_nid_postfix(int d, const KernelIovec* iov, int iovcnt, } int APS5_VABI sceKernelRename(const char* from, const char* to) { - if (from == nullptr || to == nullptr) throw std::invalid_argument("sceKernelRename: path is null"); + if (!from || !to) return SceErrorFromErrno(GUEST_EFAULT); if (*from == '\0' || *to == '\0') return SceErrorFromErrno(GUEST_ENOENT); std::filesystem::path source; if (const int error = ResolvePathError_nid_no_patch(from, source)) return SceKernelError(error); @@ -1292,7 +1292,7 @@ int APS5_VABI sceKernelRename(const char* from, const char* to) { } int APS5_VABI sceKernelRmdir(const char* path) { - if (path == nullptr) throw std::invalid_argument("sceKernelRmdir: path is null"); + if (!path) return SceErrorFromErrno(GUEST_EFAULT); if (*path == '\0') return SceErrorFromErrno(GUEST_ENOENT); std::filesystem::path native; if (const int error = ResolvePathError_nid_no_patch(path, native)) return SceKernelError(error); @@ -1339,7 +1339,7 @@ int APS5_VABI fchmod_nid_postfix(int d, int mode) { } int APS5_VABI sceKernelTruncate_nid_postfix(const char* path, std::int64_t length) { - if (path == nullptr) throw std::invalid_argument("sceKernelTruncate: path is null"); + if (!path) return SceErrorFromErrno(GUEST_EFAULT); if (*path == '\0') return SceErrorFromErrno(GUEST_ENOENT); if (length < 0) return SceErrorFromErrno(GUEST_EINVAL); std::filesystem::path native; @@ -1359,7 +1359,7 @@ int APS5_VABI truncate_nid_postfix(const char* path, std::int64_t length) { } int APS5_VABI sceKernelUtimes_nid_postfix(const char* path, const KernelTimeval* times) { - if (path == nullptr) throw std::invalid_argument("sceKernelUtimes: path is null"); + if (!path) return SceErrorFromErrno(GUEST_EFAULT); if (*path == '\0') return SceErrorFromErrno(GUEST_ENOENT); if (times != nullptr) { for (int i = 0; i < 2; ++i) { diff --git a/core/libs/tests/GuestPosixScalarIo.cpp b/core/libs/tests/GuestPosixScalarIo.cpp index 3cc3e35f7a..938c32f67b 100644 --- a/core/libs/tests/GuestPosixScalarIo.cpp +++ b/core/libs/tests/GuestPosixScalarIo.cpp @@ -37,6 +37,11 @@ std::int64_t APS5_VABI sceKernelWrite(int, const void*, std::size_t); std::int64_t APS5_VABI sceKernelLseek(int, std::int64_t, int); int APS5_VABI sceKernelStat(const char*, FileStat*); int APS5_VABI sceKernelUnlink(const char*); +int APS5_VABI sceKernelTruncate_nid_postfix(const char*, std::int64_t); +int APS5_VABI sceKernelUtimes_nid_postfix(const char*, const void*); +int APS5_VABI sceKernelMkdir(const char*, std::uint16_t); +int APS5_VABI sceKernelRmdir(const char*); +int APS5_VABI sceKernelRename(const char*, const char*); } static void Check(bool value, int line) { @@ -159,6 +164,13 @@ int main() { Require(sceKernelStat(nullptr, &nullStat) == static_cast(0x8002000eu)); Require(sceKernelStat(nullptr, nullptr) == static_cast(0x8002000eu)); Require(sceKernelUnlink(nullptr) == static_cast(0x8002000eu)); + Require(sceKernelTruncate_nid_postfix(nullptr, 0) == static_cast(0x8002000eu)); + Require(sceKernelUtimes_nid_postfix(nullptr, nullptr) == static_cast(0x8002000eu)); + Require(sceKernelMkdir(nullptr, 0777) == static_cast(0x8002000eu)); + Require(sceKernelRmdir(nullptr) == static_cast(0x8002000eu)); + Require(sceKernelRename(nullptr, "dest") == static_cast(0x8002000eu)); + Require(sceKernelRename("src", nullptr) == static_cast(0x8002000eu)); + Require(sceKernelRename(nullptr, nullptr) == static_cast(0x8002000eu)); Require(close_nid_postfix(descriptor) == 0); const int readOnly = open_nid_postfix(fileName.c_str(), SCE_KERNEL_O_RDONLY, 0); Require(readOnly >= 0); From 26c3a82e8d185d8b22c2b5bd376a4d9bd9bf1c95 Mon Sep 17 00:00:00 2001 From: MrMha <114929652+MrMha@users.noreply.github.com> Date: Sat, 17 Oct 2026 18:08:43 +0400 Subject: [PATCH 0763/1000] fix(conventions): ignore non-code duplicate export matches --- tools/check_conventions.py | 11 +++++-- ..._check_conventions_wrapped_declarations.py | 33 +++++++++++++++++++ 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/tools/check_conventions.py b/tools/check_conventions.py index 2d62d2dce1..dc4fb2ad5b 100644 --- a/tools/check_conventions.py +++ b/tools/check_conventions.py @@ -173,10 +173,17 @@ class Check: result = subprocess.run(["git", "-c", "core.quotePath=false", "grep", "-nE", "--full-name", pattern, self.head, "--", ":(top)core/libs/prx"], capture_output=True, encoding="utf-8", errors="replace") hits = [line.split(":", 3)[1:] for line in filter(None, result.stdout.split("\n"))] self.read(path for path, _, _ in hits) + sources = {} for path, number, text in hits: - name = EXPORT.search(text).group(1) + if path not in sources: + sources[path] = cpp_text(self.show(path)).split("\n") + source = sources[path] + match = next((match for match in EXPORT.finditer(source[int(number) - 1]) if match.group(1) in exports), None) + if match is None: + continue + name = match.group(1) own, own_number = exports[name] - if library(path) != library(own) and "tests" not in PurePosixPath(path).parts and function_body(self.show(path).splitlines(), int(number) - 1) is not None: + if library(path) != library(own) and "tests" not in PurePosixPath(path).parts and function_body(source, int(number) - 1) is not None: self.report("duplicate-export", own, own_number, f"{name} is also in {library(path)}") def build(self): diff --git a/tools/tests/test_check_conventions_wrapped_declarations.py b/tools/tests/test_check_conventions_wrapped_declarations.py index c05d890dbb..62c6f9642c 100644 --- a/tools/tests/test_check_conventions_wrapped_declarations.py +++ b/tools/tests/test_check_conventions_wrapped_declarations.py @@ -63,6 +63,39 @@ class WrappedDeclarationTests(unittest.TestCase): self.assertEqual(result.returncode, 1, result.stdout + result.stderr) self.assertIn("[duplicate-export]", result.stdout) + def test_multiline_raw_literal_is_not_a_duplicate(self): + source = 'const char* example = R"example(\n' + DEFINITION + ')example";\n' + result = self.check("Export.cpp", source) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertNotIn("[duplicate-export]", result.stdout) + + def test_block_comment_is_not_a_duplicate(self): + result = self.check("Export.cpp", "/*\n" + DEFINITION + "*/\n") + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertNotIn("[duplicate-export]", result.stdout) + + def test_literal_before_unrelated_export_is_not_a_duplicate(self): + source = 'const char* example = "APS5_VABI sceFooBar()"; int APS5_VABI sceOther() { return 1; }\n' + result = self.check("Export.cpp", source) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertNotIn("[duplicate-export]", result.stdout) + + def test_line_comment_before_definition_is_not_a_duplicate(self): + result = self.check("Export.cpp", "// " + DEFINITION + "int unrelated() { return 42; }\n") + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertNotIn("[duplicate-export]", result.stdout) + + def test_definition_after_unicode_raw_literal_is_a_duplicate(self): + source = 'const char* text = R"text(first\u2028+middle\u2029+last)text";\n' + DEFINITION + result = self.check("Export.cpp", source) + self.assertEqual(result.returncode, 1, result.stdout + result.stderr) + self.assertIn("[duplicate-export]", result.stdout) + + def test_definition_after_commented_example_is_a_duplicate(self): + result = self.check("Export.cpp", "/*\n" + DEFINITION + "*/\n" + DEFINITION) + self.assertEqual(result.returncode, 1, result.stdout + result.stderr) + self.assertEqual(result.stdout.count("[duplicate-export]"), 1, result.stdout) + if __name__ == "__main__": unittest.main() From 0f1564cc20c36728dae306593a5e6c883abb78bf Mon Sep 17 00:00:00 2001 From: ee6aba54 <340958984+ee6aba54@users.noreply.github.com> Date: Sat, 17 Oct 2026 16:19:32 +0200 Subject: [PATCH 0764/1000] fix(agc-driver): pass the cache release to the buffer pool through its context --- .../libSceAgcDriver/Execution/src/VulkanDevice.cpp | 2 +- .../Graphics/include/BufferPool.hpp | 5 +---- .../libSceAgcDriver/Graphics/include/Context.hpp | 1 + .../libSceAgcDriver/Graphics/src/BufferPool.cpp | 14 ++------------ core/libs/prx/libSceAgcDriver/tests/BufferPool.cpp | 7 +++---- 5 files changed, 8 insertions(+), 21 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index bb9ab86152..7fad42678e 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -1837,7 +1837,6 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : state->functionsReady = true; } Graphics::PrepareImportWatch(graphicsContext()); - Graphics::BufferPool::SetCacheReclaim(Graphics::ReleaseTextureCaches); state->bufferPool = std::make_shared(graphicsContext()); state->emptyBuffer = std::make_unique(graphicsContext(), Graphics::EmptyBufferBytes, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); state->pipelineCache = std::make_unique(graphicsContext(), state->properties); @@ -3522,6 +3521,7 @@ Graphics::Context VulkanDevice::buildContext() const { context.sampleLocationGridSampleCounts = state->sampleLocationGridSampleCounts; context.memoryProperties2 = state->memoryBudget ? state->InstanceFunction("vkGetPhysicalDeviceMemoryProperties2") : nullptr; context.singlePassStorage = state->storageImageWithoutFormat; + context.cacheReclaim = Graphics::ReleaseTextureCaches; return context; } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/BufferPool.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/BufferPool.hpp index 4e52e01226..3101ae4f90 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/BufferPool.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/BufferPool.hpp @@ -2,7 +2,6 @@ #define CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_BUFFERPOOL_HPP #include "prx/libSceAgcDriver/Graphics/include/Context.hpp" -#include #include #include #include @@ -56,8 +55,6 @@ class BufferPool { public: explicit BufferPool(const Context& context); static VkDeviceSize DeviceBudget(const VkPhysicalDeviceMemoryProperties& memory, const VkPhysicalDeviceMemoryBudgetPropertiesEXT* reported = nullptr); - using CacheReclaim = VkDeviceSize (*)(VkDeviceSize bytes); - static void SetCacheReclaim(CacheReclaim reclaim); static VkDeviceSize ReclaimTarget(VkDeviceSize bytes); ~BufferPool(); BufferPool(const BufferPool&) = delete; @@ -131,8 +128,8 @@ private: void evictOldest(Tier& tier, std::vector& evicted); // The retained-slot bound of each tier (APS5_BUFFER_POOL_SLOTS, default `defaultSlots`), read once. static std::size_t MaxSlots(); - static std::atomic& cacheReclaim(); std::size_t maxSlots; + VkDeviceSize (*cacheReclaim)(VkDeviceSize bytes); VkDevice device; PFN_vkUnmapMemory unmap; PFN_vkDestroyBuffer destroyBuffer; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp index cba5321c23..aea1a78224 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp @@ -171,6 +171,7 @@ struct Context { std::uint32_t settleFamily = 0; std::mutex* settleQueueMutex = nullptr; PFN_vkGetPhysicalDeviceMemoryProperties2 memoryProperties2 = nullptr; + VkDeviceSize (*cacheReclaim)(VkDeviceSize bytes) = nullptr; RenderCache* renderCache = nullptr; DrawQueue* drawQueue = nullptr; GraphicsPipelineCache* graphicsPipelines = nullptr; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp index ca001a763d..8713612877 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp @@ -1,7 +1,6 @@ #include "prx/libSceAgcDriver/Execution/include/ProfileOutput.hpp" #include "prx/libSceAgcDriver/Graphics/include/BufferPool.hpp" #include -#include #include #include #include @@ -37,7 +36,7 @@ constexpr VkBufferUsageFlags deviceUsage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | } -BufferPool::BufferPool(const Context& context) : device(context.device), unmap(context.Function("vkUnmapMemory")), destroyBuffer(context.Function("vkDestroyBuffer")), freeMemory(context.Function("vkFreeMemory")), allocateMemory(context.Function("vkAllocateMemory")), mapMemory(context.Function("vkMapMemory")), maxSlots(std::max(1, std::min(MaxSlots(), context.limits.maxMemoryAllocationCount / 6u))) { +BufferPool::BufferPool(const Context& context) : device(context.device), unmap(context.Function("vkUnmapMemory")), destroyBuffer(context.Function("vkDestroyBuffer")), freeMemory(context.Function("vkFreeMemory")), allocateMemory(context.Function("vkAllocateMemory")), mapMemory(context.Function("vkMapMemory")), maxSlots(std::max(1, std::min(MaxSlots(), context.limits.maxMemoryAllocationCount / 6u))), cacheReclaim(context.cacheReclaim) { smallTier.budget = smallBudget; largeTier.budget = budget; deviceTier.budget = deviceBudget(context); @@ -69,7 +68,7 @@ VkDeviceMemory BufferPool::AllocateMemory(const VkMemoryAllocateInfo& allocation } } if (exhausted(result)) { - if (const auto reclaim = cacheReclaim().load(std::memory_order_acquire); reclaim != nullptr) released = reclaim(ReclaimTarget(allocation.allocationSize)); + if (cacheReclaim != nullptr) released = cacheReclaim(ReclaimTarget(allocation.allocationSize)); if (released != 0) { memory = VK_NULL_HANDLE; result = allocateMemory(device, &allocation, nullptr, &memory); @@ -86,15 +85,6 @@ VkDeviceMemory BufferPool::AllocateMemory(const VkMemoryAllocateInfo& allocation return VK_NULL_HANDLE; } -std::atomic& BufferPool::cacheReclaim() { - static std::atomic reclaim{nullptr}; - return reclaim; -} - -void BufferPool::SetCacheReclaim(CacheReclaim reclaim) { - cacheReclaim().store(reclaim, std::memory_order_release); -} - VkDeviceSize BufferPool::ReclaimTarget(VkDeviceSize bytes) { return bytes + std::max(bytes, VkDeviceSize{256} << 20u); } diff --git a/core/libs/prx/libSceAgcDriver/tests/BufferPool.cpp b/core/libs/prx/libSceAgcDriver/tests/BufferPool.cpp index 5be5aece06..b5967eacf1 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BufferPool.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BufferPool.cpp @@ -528,14 +528,11 @@ void CacheReclaimRecovery() { Expect(BufferPool::ReclaimTarget(4 * MiB) == 260 * MiB, "a 4 MiB request does not ask the caches for 256 MiB on top of itself"); Expect(BufferPool::ReclaimTarget(512 * MiB) == 1024 * MiB, "a 512 MiB request does not ask the caches for twice its size"); constexpr auto usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT; - BufferPool::SetCacheReclaim(mockCacheReclaim); - struct Reset { - ~Reset() { BufferPool::SetCacheReclaim(nullptr); } - } reset; mock = MockDevice{}; reclaimMock = ReclaimMock{}; { auto context = mockContext(); + context.cacheReclaim = mockCacheReclaim; { DeviceBuffer idle(context, 2 * MiB, usage); } ExternalAllocation(10 * MiB); mock.limit = 12 * MiB; @@ -554,6 +551,7 @@ void CacheReclaimRecovery() { reclaimMock = ReclaimMock{}; { auto context = mockContext(); + context.cacheReclaim = mockCacheReclaim; ExternalAllocation(10 * MiB); mock.limit = 12 * MiB; const auto before = mock.attempts; @@ -569,6 +567,7 @@ void CacheReclaimRecovery() { reclaimMock = ReclaimMock{}; { auto context = mockContext(); + context.cacheReclaim = mockCacheReclaim; mock.limit = 2 * MiB; const auto before = mock.attempts; std::string failure; From b31b95d5d595614585693345f8f82766129cd25a Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Sat, 17 Oct 2026 07:23:59 -0700 Subject: [PATCH 0765/1000] refactor(agc): keep the address-pass changes clear of open PRs' lines The scan tests' includes now sit before , where #6706 and #5859 add theirs after it, and the pass counter's address flag is set after the read-only check instead of next to the pass reads #6566 rewrites. No behaviour change. --- core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp | 2 +- core/libs/prx/libSceAgcDriver/tests/BdaContracts.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index c21d23480a..2a5444e531 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -1778,7 +1778,6 @@ void recordDraw(const Context& context, const State& state, const Pm4::DrawParam const bool capture = CaptureInputsEnabled(); const bool meshIndirect = state.stages.mesh && args != nullptr; const bool addressBased = resources.HoldsLease(); - outcome.addressDraw = addressBased; auto passReads = addressBased ? std::vector>{} : resources.DeviceReads(); if (gpuIndirect) { passReads.emplace_back(args->arguments, args->arguments + args->RangeBytes()); @@ -1793,6 +1792,7 @@ void recordDraw(const Context& context, const State& state, const Pm4::DrawParam const bool unknownMemory = addressBased || resources.UsesAddressTable() || resources.HasCopiedWrites(); static const bool addressReadPasses = std::getenv("APS5_NO_ADDRESS_READ_PASSES") == nullptr; const bool readOnlyBda = addressReadPasses && resources.BdaReadOnly(); + outcome.addressDraw = addressBased; const bool unknownWrites = ((addressBased || resources.UsesAddressTable()) && !readOnlyBda) || resources.HasCopiedWrites(); const PassAccess passAccess{passReads, resources.GpuWrites(), passImages, passAttachments, unknownMemory, unknownWrites}; const bool fenced = state.depth.has_value() || !record.proxies.empty(); diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaContracts.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaContracts.cpp index 6e30d1bace..312d83bede 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaContracts.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaContracts.cpp @@ -3,12 +3,12 @@ #include "ControlFlow/RequestSerializer.hpp" #include "SpirvBackend/SpirvBda.hpp" #include -#include #include #include #include #include #include +#include namespace { From c05b2567be23e0a1af21064fe0a58efa384963ac Mon Sep 17 00:00:00 2001 From: AlxcBTW <187763517+AlxcBTW@users.noreply.github.com> Date: Fri, 16 Oct 2026 23:14:22 +0200 Subject: [PATCH 0766/1000] perf(agc): replace the plane copies around a depth plane's UAV clear by a depth clear A compute kernel that stores one colour to every texel of a storage image over a depth plane (a shadow atlas clear) copied the whole plane depth image -> staging buffer -> mirror before the dispatch and mirror -> staging buffer -> depth image after it, although the kernel reads nothing and writes every texel. A dispatch of the 15-word 8x8x1 image clear kernel (direct, no partial last group) whose T# lies on a depth surface now reads its colour from guest memory under the GPU mutex, the place the kernel reads it. When the dispatch covers the whole plane, the plane is one layer of level 0, the colour is a value the plane's format holds exactly (0 or 1 for a 16-bit unorm view, 0 or 0xffff for a 16-bit uint view, 0 or a normal float in (0, 1] for a 32-bit view) and nothing is waiting to write the colour, the dispatch still writes the mirror but the copy-in is left out, and the copy-out becomes a clear of the depth image to that value. The colour is read again when the copy-out is replaced; a changed value keeps the copy-out. Every other dispatch, stencil planes and declined values keep both copies. agc_driver_depth_clear_hle_tests runs the clear sequences with the clear taken and with the copies, and compares the depth image texel by texel through the mirror, the guest memory and the depth tests. --- .../src/Driver/Dispatch/Dispatch.cpp | 13 + .../Graphics/include/DepthSurface.hpp | 43 ++ .../Graphics/src/DepthSurface.cpp | 220 +++++++- .../tests/execution/DepthClearHle.cpp | 516 ++++++++++++++++++ docs/dev/debt/libSceAgcDriver.md | 2 +- 5 files changed, 790 insertions(+), 4 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/DepthClearHle.cpp diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp index b506cff5c1..4b67aa557c 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp @@ -508,6 +508,19 @@ void Driver::dispatch(QueueState& queue, std::span packet, bool writersNoted = false; const bool noteWrites = writeEvidenceEnabled() || traceCapSync(); + std::optional depthPlaneClear; + if (indirectArguments == 0 && !partialLastGroup) { + if (const auto clear = Graphics::MatchStorageImageClear(std::span(snapshot.code).subspan(codeOffset), userData, compute, groups)) { + const auto clearAddress = Graphics::StorageImageClearAddress(*clear); + if (clearAddress != 0 && Graphics::DepthSurfaceAt(clearAddress)) { + GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Dispatch); + std::lock_guard clearLock(GuestMemory::GpuMutex()); + recordLabelsForPacket(localDevice.get(), submission.queue); + depthPlaneClear.emplace(*clear); + } + } + } + for (;;) { std::shared_ptr prepared; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp index 2f54bee302..23776808b0 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp @@ -48,6 +48,49 @@ float HostDepthBiasConstant(const Context& context, VkFormat format, float const std::shared_ptr DepthSurfaceStorage(const Context& context, std::span words, const GuestTextureResource& resource, std::uint32_t mip); bool DepthSurfaceStoreBack(const Context& context, const StorageTexture& storage); +struct StorageImageClear { + std::array image{}; + std::array color{}; + std::uint32_t width = 0; + std::uint32_t height = 0; +}; +std::optional MatchStorageImageClear(std::span code, std::span userData, const ShaderRecompiler::ShaderComputeStageInfo& compute, const std::array& groups); +std::uint64_t StorageImageClearAddress(const StorageImageClear& clear); +struct DepthPlaneOverwrite { + std::array image{}; + GuestTextureResource view{}; + std::uint32_t bits = 0; + std::uint32_t width = 0; + std::uint32_t height = 0; + std::uint32_t raw = 0; + std::uint64_t colorAddress = 0; + std::uint32_t colorBits = 0; + float depth = 0.0f; +}; +class DepthPlaneClear { +public: + explicit DepthPlaneClear(const StorageImageClear& clear); + ~DepthPlaneClear(); + DepthPlaneClear(const DepthPlaneClear&) = delete; + DepthPlaneClear& operator=(const DepthPlaneClear&) = delete; + bool Active() const { return installed; } + +private: + DepthPlaneOverwrite value; + DepthPlaneOverwrite* previous = nullptr; + bool installed = false; +}; +struct DepthClearStatistics { + std::uint64_t matched = 0; + std::uint64_t taken = 0; + std::uint64_t copiesSkipped = 0; + std::uint64_t clears = 0; + std::uint64_t bytesSkipped = 0; + std::array declined{}; +}; +DepthClearStatistics DepthClearCounts(); +void SetDepthPlaneClearForTest(bool enabled); + } #endif diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp index 4db10e6b76..a647b69343 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -119,6 +120,104 @@ DepthSamplingState prepareDepthSampling(const Context& context, const DepthTarge return state; } +std::atomic depthPlaneClearEnabled{true}; + +enum DepthClearDecline : std::size_t { DeclineLayers, DeclineFormat, DeclineValue, DeclineColor, DeclinePending, DeclineCoverage, DeclineSize, DeclineCount }; +static_assert(DeclineCount <= 8, "DepthClearStatistics::declined holds eight reasons"); + +struct DepthClearCounters { + std::atomic matched{0}; + std::atomic taken{0}; + std::atomic copiesSkipped{0}; + std::atomic clears{0}; + std::atomic bytesSkipped{0}; + std::array, 8> declined{}; +}; + +DepthClearCounters& depthClearCounters() { + static DepthClearCounters counters; + return counters; +} + +thread_local DepthPlaneOverwrite* currentOverwrite = nullptr; + +std::optional decodeOverwrite(const StorageImageClear& clear) { + auto& counters = depthClearCounters(); + ++counters.matched; + const auto decline = [&](DepthClearDecline why) -> std::optional { + ++counters.declined[why]; + return std::nullopt; + }; + DepthPlaneOverwrite overwrite; + overwrite.image = clear.image; + try { + overwrite.view = DecodeTextureResource(clear.image); + } catch (...) { + return decline(DeclineFormat); + } + const auto& view = overwrite.view; + const bool oneLayer = view.dimension == TextureDimension::k2D || (view.dimension == TextureDimension::k2DArray && view.baseArray == 0 && view.depthOrLastArray == 0); + if (!oneLayer || view.baseLevel != 0) return decline(DeclineLayers); + VkFormat format = VK_FORMAT_UNDEFINED; + try { + format = ResolveTextureFormat(view.format); + } catch (...) { + return decline(DeclineFormat); + } + enum class Kind { Unorm16, Uint16, Float32, Uint32 }; + Kind kind; + switch (format) { + case VK_FORMAT_R16_UNORM: kind = Kind::Unorm16; break; + case VK_FORMAT_R16_UINT: kind = Kind::Uint16; break; + case VK_FORMAT_R32_SFLOAT: kind = Kind::Float32; break; + case VK_FORMAT_R32_UINT: + case VK_FORMAT_R32_SINT: kind = Kind::Uint32; break; + default: return decline(DeclineFormat); + } + const std::uint64_t base = clear.color[0] | (static_cast(clear.color[1] & 0xffffu) << 32u); + const std::uint64_t stride = (clear.color[1] >> 16u) & 0x3fffu; + const std::uint64_t extent = stride == 0 ? clear.color[2] : static_cast(clear.color[2]) * stride; + if (base == 0 || (base & 3u) != 0u || extent < 16u || !GuestMemory::Accessible(reinterpret_cast(base), 16)) return decline(DeclineColor); + if (const auto* recorder = Recorder::Active(); recorder != nullptr && recorder->PendingWriteOverlaps(base, 16)) return decline(DeclinePending); + const std::pair colorRange{base, base + 16u}; + if (StorageTexture::AnyPendingOverlaps(std::span(&colorRange, 1))) return decline(DeclinePending); + std::uint32_t c0 = 0; + try { + GuestMemory::Read(base, std::as_writable_bytes(std::span(&c0, 1)), 4); + } catch (...) { + return decline(DeclineColor); + } + switch (kind) { + case Kind::Unorm16: + if (c0 == 0u || c0 == 0x80000000u) overwrite.raw = 0u; + else if (c0 == 0x3f800000u) overwrite.raw = 0xffffu; + else return decline(DeclineValue); + overwrite.bits = 16u; + overwrite.depth = static_cast(overwrite.raw) / 65535.0f; + break; + case Kind::Uint16: + if (c0 != 0u && c0 != 0xffffu) return decline(DeclineValue); + overwrite.raw = c0; + overwrite.bits = 16u; + overwrite.depth = static_cast(overwrite.raw) / 65535.0f; + break; + case Kind::Float32: + case Kind::Uint32: { + const auto depth = std::bit_cast(c0); + if (c0 != 0u && !(std::isnormal(depth) && depth > 0.0f && depth <= 1.0f)) return decline(DeclineValue); + overwrite.raw = c0; + overwrite.bits = 32u; + overwrite.depth = depth; + break; + } + } + overwrite.colorAddress = base; + overwrite.colorBits = c0; + overwrite.width = clear.width; + overwrite.height = clear.height; + return overwrite; +} + class DepthSurface { public: DepthSurface(const Context& context, const DepthTarget& target, DepthSamplingState sampling = {}) : context(context), target(target), hostFormat(HostDepthFormat(context, target.format)), sampling(std::move(sampling)) { @@ -249,6 +348,10 @@ public: } void RecordClear(VkImageAspectFlags aspects) { + RecordClearValue(aspects, clearDepth, clearStencil); + } + + void RecordClearValue(VkImageAspectFlags aspects, float depth, std::uint8_t stencil) { written = ++depthWrites; auto* recorder = RecorderFor(context); std::unique_ptr batch; @@ -257,7 +360,7 @@ public: const VkImageSubresourceRange range{aspects, 0, 1, 0, 1}; constexpr VkAccessFlags access = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT; RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, access, VK_ACCESS_TRANSFER_WRITE_BIT); - const VkClearDepthStencilValue clear{clearDepth, clearStencil}; + const VkClearDepthStencilValue clear{depth, stencil}; context.Function("vkCmdClearDepthStencilImage")(commands, image, VK_IMAGE_LAYOUT_GENERAL, &clear, 1, &range); RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, access); if (batch) batch->SubmitAndWait(); @@ -400,6 +503,7 @@ public: throw std::runtime_error(text); } RequireStorable(resource); + auto* overwrite = stencil ? nullptr : PlaneOverwritten(words); ApplyFastClear(); auto& plane = stencil ? stencilPlane : depthPlane; if (plane.storage == nullptr || plane.storage->Descriptor().format != resource.format || plane.storage->Descriptor().tileMode != resource.tileMode || plane.storage->Descriptor().baseAddress != resource.baseAddress || plane.storage->Descriptor().dimension != resource.dimension) { @@ -407,15 +511,67 @@ public: if (plane.storage != nullptr) plane.replaced.push_back(plane.storage); plane.storage = StorageTexture::Create(context, *context.detiler, resource, 0); } - copyPlane(plane, plane.storage, stencil, true); + if (overwrite != nullptr) { + ++depthClearCounters().copiesSkipped; + } else { + copyPlane(plane, plane.storage, stencil, true); + } return plane.storage; } + bool OverwritesWholePlane(const DepthPlaneOverwrite& overwrite) const { + const bool d16 = target.format == VK_FORMAT_D16_UNORM || target.format == VK_FORMAT_D16_UNORM_S8_UINT; + return overwrite.bits == (d16 ? 16u : 32u) && overwrite.width >= target.extent.width && overwrite.height >= target.extent.height; + } + + DepthPlaneOverwrite* PlaneOverwritten(std::span words) const { + auto* overwrite = currentOverwrite; + if (overwrite == nullptr || words.size() < overwrite->image.size() || !std::equal(overwrite->image.begin(), overwrite->image.end(), words.begin())) return nullptr; + const bool d16 = target.format == VK_FORMAT_D16_UNORM || target.format == VK_FORMAT_D16_UNORM_S8_UINT; + auto& declined = depthClearCounters().declined; + if (overwrite->bits != (d16 ? 16u : 32u)) { + ++declined[DeclineSize]; + return nullptr; + } + if (overwrite->width < target.extent.width || overwrite->height < target.extent.height) { + ++declined[DeclineCoverage]; + return nullptr; + } + return overwrite; + } + + bool StoreOverwrite(const StorageTexture& mirror) { + auto* overwrite = currentOverwrite; + if (overwrite == nullptr || !OverwritesWholePlane(*overwrite)) return false; + const auto& described = mirror.Descriptor(); + const auto& view = overwrite->view; + if (described.baseAddress != view.baseAddress || described.format != view.format || described.tileMode != view.tileMode || described.dimension != view.dimension) return false; + std::uint32_t current = 0; + try { + GuestMemory::Read(overwrite->colorAddress, std::as_writable_bytes(std::span(¤t, 1)), 4); + } catch (...) { + return false; + } + if (current != overwrite->colorBits) return false; + auto* recorder = RecorderFor(context); + Require(recorder != nullptr, "depth surface storage clears need the recorder of the surface's device"); + const bool d16 = target.format == VK_FORMAT_D16_UNORM || target.format == VK_FORMAT_D16_UNORM_S8_UINT; + const auto bytes = static_cast(target.extent.width) * target.extent.height * (d16 ? 2u : 4u); + recorder->Commands(); + const auto timing = recorder->BeginGpuTiming(Recorder::CommandClass::Copy); + RecordClearValue(VK_IMAGE_ASPECT_DEPTH_BIT, overwrite->depth, clearStencil); + recorder->EndGpuTiming(timing, bytes); + auto& counters = depthClearCounters(); + ++counters.clears; + counters.bytesSkipped += 2u * bytes; + return true; + } + bool StoreBack(const StorageTexture& candidate) { for (const bool stencil : {false, true}) { auto& plane = stencil ? stencilPlane : depthPlane; if (plane.storage.get() == &candidate) { - copyPlane(plane, plane.storage, stencil, false); + if (stencil || !StoreOverwrite(candidate)) copyPlane(plane, plane.storage, stencil, false); written = ++depthWrites; return true; } @@ -1265,6 +1421,64 @@ void NoteMemoryValueFill(const MemoryValueFill& fill) { NoteDepthMetadataFill(fill.address, fill.bytes, value); } +std::optional MatchStorageImageClear(std::span code, std::span userData, const ShaderRecompiler::ShaderComputeStageInfo& compute, const std::array& groups) { + static constexpr std::array kernel{0xd7460004u, 0x0401060cu, 0xf4280204u, 0xfa000000u, 0xd7460005u, 0x0405060du, 0x7e0c020eu, 0xbf8cc07fu, 0x7e000208u, 0x7e020209u, 0x7e04020au, 0x7e06020bu, 0xf0200f28u, 0x00000004u, 0xbf810000u}; + if (code.size() < kernel.size() || code[0] != kernel[0] || !std::equal(kernel.begin(), kernel.end(), code.begin())) return std::nullopt; + if (userData.size() != 12u) return std::nullopt; + if (compute.numThreads != std::array{8u, 8u, 1u}) return std::nullopt; + if (!compute.groupIdEnable[0] || !compute.groupIdEnable[1] || !compute.groupIdEnable[2]) return std::nullopt; + if (compute.threadIdComponentCount < 2u) return std::nullopt; + std::array covered{}; + for (std::size_t axis = 0; axis < 3u; ++axis) covered[axis] = compute.partialThreads[axis] != 0u ? compute.partialThreads[axis] : static_cast(groups[axis]) * compute.numThreads[axis]; + if (covered[0] == 0u || covered[1] == 0u || covered[2] != 1u) return std::nullopt; + StorageImageClear clear; + std::copy_n(userData.begin(), clear.image.size(), clear.image.begin()); + std::copy_n(userData.begin() + static_cast(clear.image.size()), clear.color.size(), clear.color.begin()); + clear.width = static_cast(std::min(covered[0], std::numeric_limits::max())); + clear.height = static_cast(std::min(covered[1], std::numeric_limits::max())); + return clear; +} + +std::uint64_t StorageImageClearAddress(const StorageImageClear& clear) { + try { + return DecodeTextureResource(clear.image).baseAddress; + } catch (...) { + return 0u; + } +} + +DepthPlaneClear::DepthPlaneClear(const StorageImageClear& clear) { + if (!depthPlaneClearEnabled.load(std::memory_order_relaxed)) return; + const auto decoded = decodeOverwrite(clear); + if (decoded) { + value = *decoded; + previous = currentOverwrite; + currentOverwrite = &value; + installed = true; + ++depthClearCounters().taken; + } +} + +DepthPlaneClear::~DepthPlaneClear() { + if (installed) currentOverwrite = previous; +} + +DepthClearStatistics DepthClearCounts() { + const auto& counters = depthClearCounters(); + DepthClearStatistics statistics; + statistics.matched = counters.matched.load(); + statistics.taken = counters.taken.load(); + statistics.copiesSkipped = counters.copiesSkipped.load(); + statistics.clears = counters.clears.load(); + statistics.bytesSkipped = counters.bytesSkipped.load(); + for (std::size_t reason = 0; reason < statistics.declined.size(); ++reason) statistics.declined[reason] = counters.declined[reason].load(); + return statistics; +} + +void SetDepthPlaneClearForTest(bool enabled) { + depthPlaneClearEnabled.store(enabled, std::memory_order_relaxed); +} + std::size_t HtileDepthClearBytes_nid_no_patch(std::span code, std::span userData, const ShaderRecompiler::ShaderComputeStageInfo& compute, std::span packet, std::uint64_t indirectArguments) { if (userData.size() != 6u || packet.size() < 5u || indirectArguments != 0u || compute.PartialGroups() || !compute.groupIdEnable[0] || compute.threadIdComponentCount == 0u || compute.threadIdComponentCount > 3u) return 0u; if (packet[1] == 0u || packet[1] > 0x400000u || packet[2] != 1u || packet[3] != 1u || (packet[4] & 0x23u) != 1u) return 0u; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DepthClearHle.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DepthClearHle.cpp new file mode 100644 index 0000000000..93f7886211 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DepthClearHle.cpp @@ -0,0 +1,516 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/Shutdown.hpp" +#include "DepthFastClearHarness.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace AgcDriver { + +struct VulkanDeviceTestAccess { + static Graphics::Context ContextOf(const VulkanDevice& device) { return device.graphicsContext(); } +}; + +} + +namespace { + +using DepthFastClearHarness::Compile; +using DepthFastClearHarness::Covered; +using DepthFastClearHarness::Draw; +using DepthFastClearHarness::Height; +using DepthFastClearHarness::Require; +using DepthFastClearHarness::Shaders; +using DepthFastClearHarness::Surface; +using DepthFastClearHarness::Width; + +namespace Graphics = AgcDriver::Graphics; + +constexpr std::uint32_t Format16UNorm = 7; +constexpr std::uint32_t Format16Uint = 11; +constexpr std::uint32_t Format32Float = 22; +constexpr std::uint32_t Format32Uint = 20; +constexpr std::uint32_t Type2DArray = 13; +constexpr std::uint32_t TileLinear = 0; +constexpr std::uint32_t TileZ64KBX = 0x18; + +constexpr std::array ClearKernel{0xd7460004u, 0x0401060cu, 0xf4280204u, 0xfa000000u, 0xd7460005u, 0x0405060du, 0x7e0c020eu, 0xbf8cc07fu, 0x7e000208u, 0x7e020209u, 0x7e04020au, 0x7e06020bu, 0xf0200f28u, 0x00000004u, 0xbf810000u}; + +class GuestBlock { +public: + GuestBlock() { + constexpr std::size_t bytes = 65536; +#ifdef _WIN32 + block = static_cast(VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); +#else + block = static_cast(std::aligned_alloc(bytes, bytes)); +#endif + Require(block != nullptr, "depth clear: cannot allocate a guest block"); + std::memset(block, 0, bytes); + GuestAllocations::Mutation().Add(block, bytes, true, true, true); + } + + GuestBlock(const GuestBlock&) = delete; + GuestBlock& operator=(const GuestBlock&) = delete; + + std::uint8_t* Data() const { return block; } + + std::uint64_t Address() const { return static_cast(reinterpret_cast(block)); } + +private: + std::uint8_t* block = nullptr; +}; + +GuestBlock& NewBlock() { + static auto* blocks = new std::deque(); + return blocks->emplace_back(); +} + +std::uint32_t Low(std::uint64_t value) { return static_cast(value); } + +std::uint32_t High(std::uint64_t value) { return static_cast(value >> 32u); } + +std::vector Pm4Packet(std::uint32_t opcode, std::initializer_list body) { + std::vector words{0xc0000000u | (static_cast(body.size() - 1) << 16u) | (opcode << 8u)}; + words.insert(words.end(), body); + return words; +} + +void Submit(std::vector> packets) { + std::vector words; + for (const auto& packet : packets) words.insert(words.end(), packet.begin(), packet.end()); + Packet dcb{words.data(), static_cast(words.size()), 0, {}}; + Require(sceAgcDriverSubmitDcb(&dcb) == 0, "depth clear: DCB submission failed"); + AgcDriverWaitIdle_nid_postfix(); +} + +std::array TextureDescriptor(std::uint64_t address, std::uint32_t format, std::uint32_t tile = TileLinear, std::uint32_t type = Type2DArray) { + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (format << 20u) | (((Width - 1u) & 3u) << 30u), + ((Width - 1u) >> 2u) | ((Height - 1u) << 14u), + 0xfacu | (tile << 20u) | (type << 28u), + 0u, 0u, 0u, 0u, + }; +} + +std::array BufferDescriptor(std::uint64_t address, std::uint32_t bytes) { + return {Low(address), High(address) & 0xffffu, bytes, 0x31016facu}; +} + +void DispatchClear(const GuestBlock& code, const GuestBlock& plane, const GuestBlock& color, std::uint32_t format, std::uint32_t groupsX, std::uint32_t groupsY, std::uint32_t tile = TileLinear) { + const auto kernel = code.Address(); + const auto texture = TextureDescriptor(plane.Address(), format, tile); + const auto buffer = BufferDescriptor(color.Address(), 16u); + std::vector> packets{ + Pm4Packet(0x76, {0x20c, Low(kernel >> 8u)}), + Pm4Packet(0x76, {0x20d, High(kernel >> 8u) & 0xffu}), + Pm4Packet(0x76, {0x207, 8}), + Pm4Packet(0x76, {0x208, 8}), + Pm4Packet(0x76, {0x209, 1}), + Pm4Packet(0x76, {0x213, (12u << 1u) | 0x80u | 0x100u | 0x200u | (1u << 11u)}), + }; + for (std::uint32_t word = 0; word < 8u; ++word) packets.push_back(Pm4Packet(0x76, {0x240u + word, texture[word]})); + for (std::uint32_t word = 0; word < 4u; ++word) packets.push_back(Pm4Packet(0x76, {0x248u + word, buffer[word]})); + packets.push_back(Pm4Packet(0x15, {groupsX, groupsY, 1, 0x41})); + Submit(std::move(packets)); +} + +void RunClear(AgcDriver::VulkanDevice& device, const GuestBlock& code, const GuestBlock& plane, const GuestBlock& color, std::uint32_t format, std::uint32_t groupsX, std::uint32_t groupsY, std::uint32_t tile = TileLinear) { + std::vector userData(12); + const auto texture = TextureDescriptor(plane.Address(), format, tile); + const auto buffer = BufferDescriptor(color.Address(), 16u); + std::copy(texture.begin(), texture.end(), userData.begin()); + std::copy(buffer.begin(), buffer.end(), userData.begin() + 8); + const std::span kernel(reinterpret_cast(code.Data()), ClearKernel.size()); + const ShaderRecompiler::ShaderComputeStageInfo compute{{8, 8, 1}, 0u, {true, true, true}, false, 2u, {0, 0, 0}}; + const std::array memory{{ + {static_cast(code.Address()), std::as_bytes(kernel)}, + {static_cast(color.Address()), std::span(reinterpret_cast(color.Data()), 16)}, + }}; + ShaderRecompiler::RecompileRequest request{ + {ShaderRecompiler::ShaderStage::Compute, static_cast(code.Address()), kernel, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + const auto program = ShaderRecompiler::Recompile(request); + std::optional scope; + if (const auto clear = Graphics::MatchStorageImageClear(kernel, userData, compute, {groupsX, groupsY, 1u})) scope.emplace(*clear); + device.Dispatch(program, groupsX, groupsY, 1, {}, static_cast(code.Address())); + scope.reset(); + device.WaitIdle(); +} + +void SetColor(GuestBlock& color, float value) { + const std::array channels{value, value, value, value}; + std::memcpy(color.Data(), channels.data(), sizeof(channels)); + AgcDriver::GuestMemory::MarkWritten(color.Address(), sizeof(channels)); +} + +void SetRaw(GuestBlock& color, std::uint32_t raw) { + const std::array channels{raw, 0u, 0u, 0u}; + std::memcpy(color.Data(), channels.data(), sizeof(channels)); + AgcDriver::GuestMemory::MarkWritten(color.Address(), sizeof(channels)); +} + +Graphics::DepthClearStatistics Difference(const Graphics::DepthClearStatistics& now, const Graphics::DepthClearStatistics& before) { + Graphics::DepthClearStatistics difference; + difference.matched = now.matched - before.matched; + difference.taken = now.taken - before.taken; + difference.copiesSkipped = now.copiesSkipped - before.copiesSkipped; + difference.clears = now.clears - before.clears; + difference.bytesSkipped = now.bytesSkipped - before.bytesSkipped; + for (std::size_t reason = 0; reason < difference.declined.size(); ++reason) difference.declined[reason] = now.declined[reason] - before.declined[reason]; + return difference; +} + +std::uint64_t Declined(const Graphics::DepthClearStatistics& statistics) { + std::uint64_t total = 0; + for (const auto count : statistics.declined) total += count; + return total; +} + +void MatcherCases() { + std::vector userData(12); + for (std::uint32_t word = 0; word < userData.size(); ++word) userData[word] = 0x1000u + word; + ShaderRecompiler::ShaderComputeStageInfo compute{{8, 8, 1}, 0u, {true, true, true}, false, 2u, {0, 0, 0}}; + const std::array groups{8, 4, 1}; + std::vector code(ClearKernel.begin(), ClearKernel.end()); + code.insert(code.end(), {0u, 0u, 0u, 0u}); + + const auto clear = Graphics::MatchStorageImageClear(code, userData, compute, groups); + Require(clear.has_value(), "the clear kernel was not recognized"); + Require(clear->width == 64u && clear->height == 32u, "the clear covers " + std::to_string(clear->width) + " x " + std::to_string(clear->height) + " texels, expected 64 x 32"); + for (std::uint32_t word = 0; word < 8u; ++word) Require(clear->image[word] == 0x1000u + word, "the clear's T# is not user data 0-7"); + for (std::uint32_t word = 0; word < 4u; ++word) Require(clear->color[word] == 0x1008u + word, "the clear's colour V# is not user data 8-11"); + + for (std::size_t word = 0; word < ClearKernel.size(); ++word) { + auto changed = code; + changed[word] ^= 0x00000100u; + Require(!Graphics::MatchStorageImageClear(changed, userData, compute, groups), "a kernel with word " + std::to_string(word) + " changed was recognized"); + } + Require(!Graphics::MatchStorageImageClear(std::span(code).first(ClearKernel.size() - 1), userData, compute, groups), "a truncated kernel was recognized"); + Require(Graphics::MatchStorageImageClear(std::span(code).first(ClearKernel.size()), userData, compute, groups).has_value(), "the kernel alone, without trailing words, was not recognized"); + + auto shortUserData = userData; + shortUserData.pop_back(); + Require(!Graphics::MatchStorageImageClear(code, shortUserData, compute, groups), "eleven user registers were accepted (s12 would be a user register, not the group id)"); + auto longUserData = userData; + longUserData.push_back(0u); + Require(!Graphics::MatchStorageImageClear(code, longUserData, compute, groups), "thirteen user registers were accepted (s12 would be a user register, not the group id)"); + + auto wide = compute; + wide.numThreads = {8, 8, 2}; + Require(!Graphics::MatchStorageImageClear(code, userData, wide, groups), "8 x 8 x 2 threads were accepted"); + wide.numThreads = {16, 4, 1}; + Require(!Graphics::MatchStorageImageClear(code, userData, wide, groups), "16 x 4 threads were accepted"); + for (std::size_t axis = 0; axis < 3u; ++axis) { + auto missing = compute; + missing.groupIdEnable[axis] = false; + Require(!Graphics::MatchStorageImageClear(code, userData, missing, groups), "a dispatch without the group id of axis " + std::to_string(axis) + " was accepted"); + } + auto oneComponent = compute; + oneComponent.threadIdComponentCount = 1u; + Require(!Graphics::MatchStorageImageClear(code, userData, oneComponent, groups), "a dispatch with only the x thread id was accepted"); + + Require(!Graphics::MatchStorageImageClear(code, userData, compute, {8, 4, 2}), "two layers of groups were accepted"); + Require(!Graphics::MatchStorageImageClear(code, userData, compute, {0, 4, 1}), "zero groups along x were accepted"); + Require(!Graphics::MatchStorageImageClear(code, userData, compute, {8, 0, 1}), "zero groups along y were accepted"); + + auto partial = compute; + partial.partialThreads = {60, 30, 1}; + const auto cut = Graphics::MatchStorageImageClear(code, userData, partial, groups); + Require(cut.has_value() && cut->width == 60u && cut->height == 30u, "a dispatch of 60 x 30 threads does not cover 60 x 30 texels"); + +} + +std::vector DepthImageTexels(AgcDriver::VulkanDevice& device, const GuestBlock& plane, std::uint32_t format, std::uint32_t texelBytes) { + const auto context = AgcDriver::VulkanDeviceTestAccess::ContextOf(device); + const auto words = TextureDescriptor(plane.Address(), format); + const auto resource = Graphics::DecodeTextureResource(words); + std::shared_ptr mirror; + { + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + mirror = Graphics::DepthSurfaceStorage(context, words, resource, 0); + } + Require(mirror != nullptr, "the plane has no mirror"); + mirror->MarkDirty(); + const std::size_t pitch = std::max(Width, 256u / texelBytes); + Graphics::StorageTexture::FlushPending(plane.Address(), pitch * Height * texelBytes, nullptr, "test"); + device.WaitIdle(); + std::vector texels(static_cast(Width) * Height); + for (std::size_t y = 0; y < Height; ++y) { + for (std::size_t x = 0; x < Width; ++x) { + const auto at = (y * pitch + x) * texelBytes; + if (texelBytes == 2u) { + std::uint16_t value = 0; + std::memcpy(&value, plane.Data() + at, sizeof(value)); + texels[y * Width + x] = value; + } else { + std::memcpy(&texels[y * Width + x], plane.Data() + at, sizeof(std::uint32_t)); + } + } + } + return texels; +} + +void RequireDepthTexels(AgcDriver::VulkanDevice& device, const GuestBlock& plane, std::uint32_t format, std::uint32_t texelBytes, std::uint32_t expected, const std::string& label) { + const auto texels = DepthImageTexels(device, plane, format, texelBytes); + for (std::size_t texel = 0; texel < texels.size(); ++texel) Require(texels[texel] == expected, label + "depth image texel " + std::to_string(texel) + " holds " + std::to_string(texels[texel]) + ", expected " + std::to_string(expected)); +} + +Graphics::DepthClearStatistics ClearSequence(AgcDriver::VulkanDevice& device, const Shaders& shaders, int mode) { + Graphics::SetDepthPlaneClearForTest(mode != 0); + const auto before = Graphics::DepthClearCounts(); + const std::string label = "mode " + std::to_string(mode) + ": "; + auto& code = NewBlock(); + std::memcpy(code.Data(), ClearKernel.data(), sizeof(ClearKernel)); + auto& plane = NewBlock(); + auto& color = NewBlock(); + const Surface surface{plane.Address(), 0, 0, false, VK_FORMAT_D32_SFLOAT}; + const auto drawLess = [&] { return Draw(device, shaders, surface, {.depthCompare = VK_COMPARE_OP_LESS}); }; + const auto drawGreater = [&] { return Draw(device, shaders, surface, {.depthCompare = VK_COMPARE_OP_GREATER}); }; + const auto drawEqual = [&] { return Draw(device, shaders, surface, {.depthCompare = VK_COMPARE_OP_EQUAL}); }; + + Require(Draw(device, shaders, surface, {.depthWrite = true, .depthCompare = VK_COMPARE_OP_ALWAYS}) == Covered, label + "the producer did not cover the plane"); + Require(drawLess() == 0u, label + "the plane does not hold the 0.5 the producer wrote"); + Require(drawEqual() == Covered, label + "the plane does not hold exactly 0.5"); + + SetColor(color, 1.0f); + RunClear(device, code, plane, color, Format32Float, 8, 4); + Require(drawLess() == Covered, label + "after a clear to 1.0 the depth test 0.5 < 1.0 did not pass everywhere"); + Require(drawGreater() == 0u, label + "after a clear to 1.0 a texel passed 0.5 > depth"); + + SetColor(color, 0.25f); + RunClear(device, code, plane, color, Format32Float, 8, 4); + Require(drawLess() == 0u, label + "after a clear to 0.25 a texel passed 0.5 < depth"); + Require(drawGreater() == Covered, label + "after a clear to 0.25 the depth test 0.5 > 0.25 did not pass everywhere"); + + SetColor(color, 0.5f); + RunClear(device, code, plane, color, Format32Float, 8, 4); + Require(drawEqual() == Covered, label + "after a clear to 0.5 the plane does not hold exactly 0.5"); + + SetColor(color, 0.75f); + RunClear(device, code, plane, color, Format32Float, 4, 4); + Require(drawGreater() == 0u, label + "after a clear of the left half to 0.75 a texel passed 0.5 > depth"); + Require(drawEqual() == Covered / 2u, label + "after a clear of the left half the right half does not keep 0.5"); + Require(drawLess() == Covered / 2u, label + "after a clear of the left half the left half does not hold 0.75"); + + SetColor(color, 2.0f); + RunClear(device, code, plane, color, Format32Float, 8, 4); + Require(drawEqual() == 0u, label + "after a clear to 2.0 a texel still holds exactly 0.5"); + + SetColor(color, 0.125f); + RunClear(device, code, plane, color, Format32Float, 8, 4); + Require(drawGreater() == Covered, label + "after the last full clear the depth test 0.5 > 0.125 did not pass everywhere"); + Require(drawLess() == 0u, label + "after the last full clear a texel passed 0.5 < depth"); + + Graphics::StorageTexture::FlushPending(plane.Address(), Width * Height * sizeof(float), nullptr, "test"); + device.WaitIdle(); + for (std::uint32_t texel = 0; texel < Width * Height; ++texel) { + float value = 0.0f; + std::memcpy(&value, plane.Data() + texel * sizeof(float), sizeof(value)); + Require(value == 0.125f, label + "guest depth texel " + std::to_string(texel) + " is " + std::to_string(value) + " after the flush, expected 0.125"); + } + RequireDepthTexels(device, plane, Format32Float, 4, std::bit_cast(0.125f), label); + return Difference(Graphics::DepthClearCounts(), before); +} + +void IntegerViewCase(AgcDriver::VulkanDevice& device, const Shaders& shaders, int mode) { + Graphics::SetDepthPlaneClearForTest(mode != 0); + const auto before = Graphics::DepthClearCounts(); + const std::string label = "integer view, mode " + std::to_string(mode) + ": "; + auto& code = NewBlock(); + std::memcpy(code.Data(), ClearKernel.data(), sizeof(ClearKernel)); + auto& plane = NewBlock(); + auto& color = NewBlock(); + const Surface surface{plane.Address(), 0, 0, false, VK_FORMAT_D32_SFLOAT}; + Require(Draw(device, shaders, surface, {.depthWrite = true, .depthCompare = VK_COMPARE_OP_ALWAYS}) == Covered, label + "the producer did not cover the plane"); + const std::array bits{std::bit_cast(0.25f), 0u, 0u, 0u}; + std::memcpy(color.Data(), bits.data(), sizeof(bits)); + AgcDriver::GuestMemory::MarkWritten(color.Address(), sizeof(bits)); + RunClear(device, code, plane, color, Format32Uint, 8, 4, TileZ64KBX); + Require(Draw(device, shaders, surface, {.depthCompare = VK_COMPARE_OP_GREATER}) == Covered, label + "after a clear to the bits of 0.25 the depth test 0.5 > 0.25 did not pass everywhere"); + Require(Draw(device, shaders, surface, {.depthCompare = VK_COMPARE_OP_LESS}) == 0u, label + "after a clear to the bits of 0.25 a texel passed 0.5 < depth"); + const auto difference = Difference(Graphics::DepthClearCounts(), before); + if (mode == 0) Require(difference.matched == 0u && difference.clears == 0u, label + "the lever counted with the switch off"); + else Require(difference.clears == 1u, label + "the clear was not taken (" + std::to_string(difference.clears) + " clears, " + std::to_string(Declined(difference)) + " declined)"); +} + +void DriverMatchCase(AgcDriver::VulkanDevice& device, const Shaders& shaders, int mode) { + Graphics::SetDepthPlaneClearForTest(mode != 0); + const auto before = Graphics::DepthClearCounts(); + const std::string label = "driver submit, mode " + std::to_string(mode) + ": "; + auto& code = NewBlock(); + std::memcpy(code.Data(), ClearKernel.data(), sizeof(ClearKernel)); + auto& plane = NewBlock(); + auto& color = NewBlock(); + const Surface surface{plane.Address(), 0, 0, false, VK_FORMAT_D32_SFLOAT}; + Require(Draw(device, shaders, surface, {.depthWrite = true, .depthCompare = VK_COMPARE_OP_ALWAYS}) == Covered, label + "the producer did not cover the plane"); + SetColor(color, 1.0f); + DispatchClear(code, plane, color, Format32Float, 8, 4); + const auto difference = Difference(Graphics::DepthClearCounts(), before); + if (mode == 0) Require(difference.matched == 0u && difference.taken == 0u, label + "the driver's dispatch reached the lever with the switch off"); + else Require(difference.matched == 1u && difference.taken == 1u, label + "the driver's dispatch matched " + std::to_string(difference.matched) + " and took " + std::to_string(difference.taken) + " clears (declined " + std::to_string(Declined(difference)) + "), expected 1 and 1"); +} + +void SixteenBitCase(AgcDriver::VulkanDevice& device, const Shaders& shaders, int mode) { + Graphics::SetDepthPlaneClearForTest(mode != 0); + const auto before = Graphics::DepthClearCounts(); + const std::string label = "16-bit plane, mode " + std::to_string(mode) + ": "; + auto& code = NewBlock(); + std::memcpy(code.Data(), ClearKernel.data(), sizeof(ClearKernel)); + auto& plane = NewBlock(); + auto& color = NewBlock(); + const Surface surface{plane.Address(), 0, 0, false, VK_FORMAT_D16_UNORM}; + const auto drawLess = [&] { return Draw(device, shaders, surface, {.depthCompare = VK_COMPARE_OP_LESS}); }; + const auto drawGreater = [&] { return Draw(device, shaders, surface, {.depthCompare = VK_COMPARE_OP_GREATER}); }; + Require(Draw(device, shaders, surface, {.depthWrite = true, .depthCompare = VK_COMPARE_OP_ALWAYS}) == Covered, label + "the producer did not cover the plane"); + + SetColor(color, 1.0f); + try { + RunClear(device, code, plane, color, Format16UNorm, 8, 4); + } catch (const std::exception& error) { + if (std::string(error.what()).find("unsupported format") == std::string::npos) throw; + std::printf("16-bit plane skipped: %s\n", error.what()); + return; + } + Require(drawLess() == Covered, label + "after a clear to 1.0 the depth test 0.5 < 1.0 did not pass everywhere"); + Require(drawGreater() == 0u, label + "after a clear to 1.0 a texel passed 0.5 > depth"); + RequireDepthTexels(device, plane, Format16UNorm, 2, 0xffffu, label + "clear to 1.0: "); + + SetColor(color, 0.0f); + RunClear(device, code, plane, color, Format16UNorm, 8, 4); + Require(drawGreater() == Covered, label + "after a clear to 0.0 the depth test 0.5 > 0.0 did not pass everywhere"); + Require(drawLess() == 0u, label + "after a clear to 0.0 a texel passed 0.5 < depth"); + RequireDepthTexels(device, plane, Format16UNorm, 2, 0u, label + "clear to 0.0: "); + + SetColor(color, 0.5f); + RunClear(device, code, plane, color, Format16UNorm, 8, 4); + + auto& rawPlane = NewBlock(); + const Surface rawSurface{rawPlane.Address(), 0, 0, false, VK_FORMAT_D16_UNORM}; + const auto rawLess = [&] { return Draw(device, shaders, rawSurface, {.depthCompare = VK_COMPARE_OP_LESS}); }; + const auto rawGreater = [&] { return Draw(device, shaders, rawSurface, {.depthCompare = VK_COMPARE_OP_GREATER}); }; + Require(Draw(device, shaders, rawSurface, {.depthWrite = true, .depthCompare = VK_COMPARE_OP_ALWAYS}) == Covered, label + "the producer of the uint plane did not cover it"); + SetRaw(color, 0x4000u); + RunClear(device, code, rawPlane, color, Format16Uint, 8, 4, TileZ64KBX); + Require(rawGreater() == Covered, label + "after a clear to the raw value 0x4000 the depth test 0.5 > 0.25 did not pass everywhere"); + Require(rawLess() == 0u, label + "after a clear to the raw value 0x4000 a texel passed 0.5 < depth"); + + SetRaw(color, 0xffffu); + RunClear(device, code, rawPlane, color, Format16Uint, 8, 4, TileZ64KBX); + Require(rawLess() == Covered, label + "after a clear to the raw value 0xffff the depth test 0.5 < 1.0 did not pass everywhere"); + Require(rawGreater() == 0u, label + "after a clear to the raw value 0xffff a texel passed 0.5 > depth"); + + SetRaw(color, 0u); + RunClear(device, code, rawPlane, color, Format16Uint, 8, 4, TileZ64KBX); + Require(rawGreater() == Covered, label + "after a clear to the raw value 0 the depth test 0.5 > 0.0 did not pass everywhere"); + Require(rawLess() == 0u, label + "after a clear to the raw value 0 a texel passed 0.5 < depth"); + + const auto difference = Difference(Graphics::DepthClearCounts(), before); + if (mode == 0) { + Require(difference.matched == 0u && difference.clears == 0u, label + "the lever counted with the switch off"); + return; + } + Require(difference.clears == 4u, label + "the lever took " + std::to_string(difference.clears) + " clears, expected 4 (unorm 1.0 and 0.0, raw 0xffff and 0)"); + Require(difference.declined[2] >= 2u, label + "the unorm clear to 0.5 and the raw clear to 0x4000 were not declined for their value (" + std::to_string(difference.declined[2]) + ")"); + Require(difference.bytesSkipped == difference.clears * 2u * Width * Height * sizeof(std::uint16_t), label + "the copies left out add up to " + std::to_string(difference.bytesSkipped) + " bytes"); +} + +void StencilCase(AgcDriver::VulkanDevice& device, const Shaders& shaders, int mode) { + Graphics::SetDepthPlaneClearForTest(mode != 0); + const auto before = Graphics::DepthClearCounts(); + const std::string label = "depth and stencil, mode " + std::to_string(mode) + ": "; + auto& code = NewBlock(); + std::memcpy(code.Data(), ClearKernel.data(), sizeof(ClearKernel)); + auto& plane = NewBlock(); + auto& stencilPlane = NewBlock(); + auto& color = NewBlock(); + const Surface surface{plane.Address(), stencilPlane.Address(), 0, false, VK_FORMAT_D32_SFLOAT_S8_UINT}; + const VkStencilOpState writeFive{VK_STENCIL_OP_KEEP, VK_STENCIL_OP_REPLACE, VK_STENCIL_OP_KEEP, VK_COMPARE_OP_ALWAYS, 0xffu, 0xffu, 5u}; + const VkStencilOpState equalFive{VK_STENCIL_OP_KEEP, VK_STENCIL_OP_KEEP, VK_STENCIL_OP_KEEP, VK_COMPARE_OP_EQUAL, 0xffu, 0u, 5u}; + Require(Draw(device, shaders, surface, {.depthWrite = true, .depthCompare = VK_COMPARE_OP_ALWAYS, .stencilTest = true, .stencil = writeFive}) == Covered, label + "the depth and stencil producer did not cover the surface"); + SetColor(color, 1.0f); + RunClear(device, code, plane, color, Format32Float, 8, 4); + Require(Draw(device, shaders, surface, {.depthCompare = VK_COMPARE_OP_LESS}) == Covered, label + "after a clear to 1.0 the depth test 0.5 < 1.0 did not pass everywhere"); + Require(Draw(device, shaders, surface, {.depthTest = false, .stencilTest = true, .stencil = equalFive}) == Covered, label + "the depth clear changed the stencil values"); + const auto difference = Difference(Graphics::DepthClearCounts(), before); + if (mode == 0) Require(difference.matched == 0u && difference.clears == 0u, label + "the lever counted with the switch off"); + else Require(difference.clears == 1u, label + "the clear was not taken (" + std::to_string(difference.clears) + " clears, " + std::to_string(Declined(difference)) + " declined)"); +} + +} + +int main() { + int failures = 0; + const auto run = [&](const char* name, auto&& body) { + try { + body(); + } catch (const std::exception& error) { + std::cerr << name << ": " << error.what() << '\n'; + ++failures; + } + }; + try { + run("matcher", [] { MatcherCases(); }); + const auto device = OpenVulkanTestDevice(); + if (!device) return failures != 0 ? 1 : VulkanTestSkipped; + const auto shaders = Compile(*device); + + run("switch off", [&] { + const auto off = ClearSequence(*device, shaders, 0); + Require(off.matched == 0u && off.taken == 0u && off.clears == 0u && off.copiesSkipped == 0u, "the lever counted with the switch off"); + }); + + run("lever", [&] { + const auto on = ClearSequence(*device, shaders, 1); + Require(on.matched >= 6u, "the lever matched " + std::to_string(on.matched) + " clear dispatches, expected 6"); + Require(on.clears == 4u, "the lever took " + std::to_string(on.clears) + " clears, expected 4 (1.0, 0.25, 0.5, 0.125)"); + Require(on.copiesSkipped >= on.clears, "fewer copy-ins were left out than clears taken"); + Require(on.bytesSkipped == on.clears * 2u * Width * Height * sizeof(float), "the copies left out add up to " + std::to_string(on.bytesSkipped) + " bytes"); + Require(on.declined[2] >= 1u, "the clear to 2.0 was not declined for its value (" + std::to_string(on.declined[2]) + ")"); + Require(on.declined[5] >= 1u, "the clear of half the plane was not declined for its coverage (" + std::to_string(on.declined[5]) + ")"); + }); + + Graphics::SetDepthPlaneClearForTest(true); + + for (const int mode : {0, 1}) run(mode == 0 ? "integer view, switch off" : "integer view, lever", [&] { IntegerViewCase(*device, shaders, mode); }); + for (const int mode : {0, 1}) run(mode == 0 ? "16-bit plane, switch off" : "16-bit plane, lever", [&] { SixteenBitCase(*device, shaders, mode); }); + for (const int mode : {0, 1}) run(mode == 0 ? "depth and stencil, switch off" : "depth and stencil, lever", [&] { StencilCase(*device, shaders, mode); }); + for (const int mode : {0, 1}) run(mode == 0 ? "driver submit, switch off" : "driver submit, lever", [&] { DriverMatchCase(*device, shaders, mode); }); + Graphics::SetDepthPlaneClearForTest(true); + LibcRunShutdown_nid_postfix(); + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + try { LibcRunShutdown_nid_postfix(); } catch (...) {} + return 1; + } + if (failures != 0) return 1; + std::puts("depth clear tests passed"); + return 0; +} diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 71210637ea..be59a4a9d7 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -51,7 +51,7 @@ - [Multisampled color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) lose their guest-read guards and cannot be reused after a successful allocation removal or unmap covers their full guarded range. Retired Vulkan images remain allocated until device teardown to preserve outstanding references. Partial unmaps keep the guard; partial allocation reuse and guest access to live multisampled data remain unsupported. - [Stencil reference export replay](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) on devices without `VK_EXT_shader_stencil_export` supports at most sixteen values proved from constant, select and phi exports, with an export proved on every kept path. It records instances, then their triangles, then the reference classes of each triangle. One pipeline filters fragments at shader termination against a dynamic push value before late stencil and depth tests. The [Vulkan stencil-reference replacement rule](https://docs.vulkan.org/refpages/latest/refpages/source/FragStencilRefEXT.html) discards bits above the attachment's stencil width; supported stencil attachments have eight bits. Numeric subgroup observations throw except bounded own-lane mask plumbing and group control with kept-lane and derivative-helper coverage proved. WQM inputs must exclude helper contributions, since [non-quad subgroup operations may treat helpers as inactive](https://docs.vulkan.org/glslext/latest/glslext/khr/GL_KHR_shader_subgroup.html). Readonly guarded SRT reads may report internal host faults; these draws wait and check faults before publishing results, and loops throw. Indirect draws, restart, strips, fans, tessellation, mesh, multisampling, early tests, ordered pixel execution, guest shader memory writes, other fault-producing memory access, clocks, primitive ID inputs, attachment feedback and pipeline face/channel splits throw. An exhausted stage push range throws. Equivalence to console ordering has not been measured on console hardware. -- [Reused depth storage](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp): a storage view with another extent or a format that cannot hold the cached depth plane uses guest memory. Depth rendering is not written back to those guest bytes; this admission rule does not prove allocation reuse or preserve a depth writer through an incompatible view. A 2D storage view, or a one-layer 2D array view of layer 0, of level 0 with the plane's extent and format (32-bit float or the 32-bit depth-bits layout for the depth plane, 16-bit unorm for D16, 8-bit uint for the stencil plane) is a storage image that is [filled](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) from the depth image before every use and, when the shader writes it, copied back after the work: a whole-plane copy through a staging buffer each way. A written view is also left pending like any other storage image, so the plane's content, drawn depth included, reaches guest memory on the next flush of that range. A 2D array view of more than one slice of the depth plane, or of a later slice, is a storage image over the slices from 0 to its last (`LayeredDepthStorage`); only the slices from its base slice to its last are copied: a slice that has a depth surface at the plane's address plus a whole depth slice per layer (`DepthSliceBytes`, as for the depth plane copies below) is copied from that surface before every use and back to it after the work, every bound slice each time, through a staging buffer; the other slices hold guest memory like any storage image. These images are kept per address, extent, slice count, base slice, format and tile mode until the device goes away, outside the storage cache's budget. Such a view needs a layer pitch equal to that slice size, and another view of the same memory (the 2D view of its first slice, a view from another base slice, or a sampled copy) is a separate image, copied in before and out after each use, so views that are written before a flush and share a slice do not merge: the later copy-out wins, so their results are not merged. A matching view still throws when it is a cube, a 2D array of more than one layer of the stencil plane or from a later slice, or another level, when the surface is multisampled, when it names the depth plane of a 16-bit depth with stencil surface the device stores in a wider format, and when it was written after another view of the same plane (another format, tile mode, address or dimension) replaced it, as two such views bound in one dispatch are. Stored depth values outside 0..1 are not clamped, and Vulkan leaves that copy undefined without `VK_EXT_depth_range_unrestricted`. `agc_driver_depth_storage_dispatch` pins these paths; none is measured on the console. +- [Reused depth storage](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp): a storage view with another extent or a format that cannot hold the cached depth plane uses guest memory. Depth rendering is not written back to those guest bytes; this admission rule does not prove allocation reuse or preserve a depth writer through an incompatible view. A 2D storage view, or a one-layer 2D array view of layer 0, of level 0 with the plane's extent and format (32-bit float or the 32-bit depth-bits layout for the depth plane, 16-bit unorm for D16, 8-bit uint for the stencil plane) is a storage image that is [filled](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) from the depth image before every use and, when the shader writes it, copied back after the work: a whole-plane copy through a staging buffer each way. A dispatch of the 15-word 8x8x1 image clear kernel ([`MatchStorageImageClear`](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp): one colour read from the V# in `user[8..11]` stored to every texel of the T# in `user[0..7]`) that covers the whole plane and stores 0 or 1 through a 16-bit unorm view, 0 or 0xffff through a 16-bit uint view, or 0 or a normal float in (0, 1] through a 32-bit float or integer view leaves out the copy-in and replaces the copy-out by a clear of the depth image to that value; the dispatch still writes the storage image. The colour is read again when the copy-out is replaced, and a changed value keeps the copy-out. Indirect dispatches, partial last groups, dispatches that queue on the draw pipeline, other values and formats, a plane the kernel covers only partly and stencil planes keep both copies. A written view is also left pending like any other storage image, so the plane's content, drawn depth included, reaches guest memory on the next flush of that range. A 2D array view of more than one slice of the depth plane, or of a later slice, is a storage image over the slices from 0 to its last (`LayeredDepthStorage`); only the slices from its base slice to its last are copied: a slice that has a depth surface at the plane's address plus a whole depth slice per layer (`DepthSliceBytes`, as for the depth plane copies below) is copied from that surface before every use and back to it after the work, every bound slice each time, through a staging buffer; the other slices hold guest memory like any storage image. These images are kept per address, extent, slice count, base slice, format and tile mode until the device goes away, outside the storage cache's budget. Such a view needs a layer pitch equal to that slice size, and another view of the same memory (the 2D view of its first slice, a view from another base slice, or a sampled copy) is a separate image, copied in before and out after each use, so views that are written before a flush and share a slice do not merge: the later copy-out wins, so their results are not merged. A matching view still throws when it is a cube, a 2D array of more than one layer of the stencil plane or from a later slice, or another level, when the surface is multisampled, when it names the depth plane of a 16-bit depth with stencil surface the device stores in a wider format, and when it was written after another view of the same plane (another format, tile mode, address or dimension) replaced it, as two such views bound in one dispatch are. Stored depth values outside 0..1 are not clamped, and Vulkan leaves that copy undefined without `VK_EXT_depth_range_unrestricted`. `agc_driver_depth_storage_dispatch` pins these paths; none is measured on the console. - [Collapsed color array views](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) preserve the selected slice in the XOR address equation when the view is represented as a shifted 2D surface. Native Vulkan coverage checks R_X draws, array reads, publication and re-upload for slices 1-3; Lost Soul Aside PPSA26358 validates the RGBA16F cube mip chain on Windows/NVIDIA. Other devices and console comparisons remain unverified. Full-array views retain their layer addressing. - A [depth surface](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver) is retired when a color target or a written storage image it does not serve overlaps it, or when it is sampled through a view it cannot serve and guest memory over it was written since it was last bound; lookups then skip it, so sampling reads the memory like any texture. Bound again, it keeps its own depth instead of the bytes written over it. Without write watching, a view it cannot serve still throws. A retired surface also gives up its depth and stencil planes, so another surface sharing them no longer copies from it. - [Depth plane copies](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver): a depth plane sampled as a 2D array or a cube, or a D16 plane read as `R16_UINT`, is copied into an image of its own (`DepthPlaneCopy`), kept per base address, extent, format and slice count; a slice comes from the live depth surface when one is bound at the slice's address and from guest memory (detiled) otherwise. A draw or dispatch that binds the plane copies again only the slices that changed and records nothing when none did: a surface slice when its image or write serial differs from the one copied (the serial is bumped when the surface is created, when a draw binds it as its depth target, when a pending fast clear is applied and when a written storage view of the plane is copied back), a guest-memory slice when its bytes were written since they were last read (`GuestMemory::UnchangedSince`), a snapshot write overlaps them, or its address, size or tile mode changed. The surface of the latest depth draw is copied at every use until another depth target is bound, since that draw may still be recorded after the copy. A slice's address is the base plus a whole 64 KiB-block depth slice per layer (`DepthSliceBytes`), and the cube face order is the array order; both are inferred from the titles' use, not measured on hardware. A 2D-array T# whose `BASE_ARRAY` is past slice 0 reads slices `BASE_ARRAY` to `LAST_ARRAY` of the copy, which holds slices 0 to `LAST_ARRAY`, and a 2D binding of it reads slice `BASE_ARRAY`; that follows the colour 2D-array semantics and the titles that hit it (PPSA01556, PPSA03912), and was not measured on hardware for depth surfaces. Planes with mips, a cube or a 2D T# with a base slice other than 0, a `BASE_ARRAY` past `LAST_ARRAY`, a cube with other than 1 or 6 slices, and other views of the planes still throw, and so do these copies of a 16-bit plane with stencil that the device stores as D24 or D32 (16-bit depth with stencil, below). From af4655125dcec6a4ed9e900bc385042c6e9d8344 Mon Sep 17 00:00:00 2001 From: milescoviello Date: Sat, 17 Oct 2026 14:33:32 +0000 Subject: [PATCH 0767/1000] test(agc): keep the pixel user data alive through its recompile The fragment request of agc_driver_vertex_clip_cull_distances took its user data from a temporary vector, so GuestContext::userData (a span) pointed at freed memory by the time Recompile read it. The ASan build reports a heap-use-after-free in materializeSnapshot; other builds read whatever the freed block still holds. The vector is now a local that outlives the request, like the vertex stage's. Co-Authored-By: Claude Opus 5.5 --- .../tests/execution/VertexClipCullDistances.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/VertexClipCullDistances.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/VertexClipCullDistances.cpp index 5b5a66615f..08adb9fde4 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/VertexClipCullDistances.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/VertexClipCullDistances.cpp @@ -129,9 +129,10 @@ void Draw(AgcDriver::VulkanDevice& device, std::uint32_t waveSize, const Row& ro pixel.targetOutputMode[0] = 9; pixel.targetExportMapping.fill(0xe4u); const std::array pixelMemory{{{reinterpret_cast(PixelCode.data()), std::as_bytes(std::span(PixelCode))}}}; + const std::vector pixelUserData(4, 0u); ShaderRecompiler::RecompileRequest fragment{ {ShaderStage::Fragment, reinterpret_cast(PixelCode.data()), PixelCode, 0, {}}, - {waveSize, 0, std::vector(4, 0u), std::nullopt, pixel, std::nullopt, pixelMemory}, + {waveSize, 0, pixelUserData, std::nullopt, pixel, std::nullopt, pixelMemory}, device.Target(), PixelPushLayout(vertexPush, device.Target()) }; From b808658488ad2ae4022da877f27afbd577c03202 Mon Sep 17 00:00:00 2001 From: mattogepeto Date: Sat, 17 Oct 2026 16:35:15 +0200 Subject: [PATCH 0768/1000] test(libkernel): expect the FreeBSD errors in the Windows directory tests guest_directory_close expected ENOTDIR from getdents on a descriptor number reused by a regular file, and guest_directory_buffer expected ENOTDIR for descriptor -1. kern_getdirentries returns EINVAL for a vnode that is not a directory and getvnode returns EBADF for a descriptor that is not open, which is what sceKernelGetdirentries now reports on Windows. Written with AI assistance (Claude). Co-Authored-By: Claude Opus 5.5 --- core/libs/tests/GuestDirectoryBuffer.cpp | 2 +- core/libs/tests/GuestDirectoryClose.cpp | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/core/libs/tests/GuestDirectoryBuffer.cpp b/core/libs/tests/GuestDirectoryBuffer.cpp index bfd1beb0dd..8ceba21364 100644 --- a/core/libs/tests/GuestDirectoryBuffer.cpp +++ b/core/libs/tests/GuestDirectoryBuffer.cpp @@ -66,7 +66,7 @@ static void CheckName(const std::u8string& utf8Name) { Require(sceKernelGetdents(directory, buffer.data(), 8) == 0); Require(sceKernelGetdents(directory, buffer.data(), 256) == 0); Require(sceKernelClose(directory) == 0); - Require(sceKernelGetdents(-1, buffer.data(), 256) == SCE_KERNEL_ERROR_ENOTDIR); + Require(sceKernelGetdents(-1, buffer.data(), 256) == SCE_KERNEL_ERROR_EBADF); std::filesystem::remove_all(root); } diff --git a/core/libs/tests/GuestDirectoryClose.cpp b/core/libs/tests/GuestDirectoryClose.cpp index b4ab7b0fc2..68d3d60176 100644 --- a/core/libs/tests/GuestDirectoryClose.cpp +++ b/core/libs/tests/GuestDirectoryClose.cpp @@ -25,7 +25,7 @@ static void Check(bool value, int line) { } #define Require(value) Check((value), __LINE__) -static constexpr int ErrorEnotdir = static_cast(0x80020014u); +static constexpr int ErrorEinval = static_cast(0x80020016u); static void VerifyCloseReuse(const std::filesystem::path& root, const char* scenario, bool useUnderscoreClose) { @@ -49,7 +49,7 @@ static void VerifyCloseReuse(const std::filesystem::path& root, const char* scen Require(file == directory); std::array entries{}; - Require(sceKernelGetdents(file, entries.data(), static_cast(entries.size())) == ErrorEnotdir); + Require(sceKernelGetdents(file, entries.data(), static_cast(entries.size())) == ErrorEinval); Require((useUnderscoreClose ? _close_nid_postfix(file) : close_nid_postfix(file)) == 0); } From d33f0371a2f4e0a5c73d1d00615f5005107ff73d Mon Sep 17 00:00:00 2001 From: MrMha <114929652+MrMha@users.noreply.github.com> Date: Sat, 17 Oct 2026 18:39:37 +0400 Subject: [PATCH 0769/1000] fix: expand Ninja response files before syntax checks --- tools/check_compile.py | 2 +- tools/tests/test_check_compile.py | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/tools/check_compile.py b/tools/check_compile.py index 6c596ccb5f..31efd898a5 100644 --- a/tools/check_compile.py +++ b/tools/check_compile.py @@ -34,7 +34,7 @@ def entries(build): ninja = shutil.which("ninja") if ninja is None: raise ValueError(f"no {manifest} and ninja is not on PATH") - result = capture([ninja, "-C", str(build), "-t", "compdb"]) + result = capture([ninja, "-C", str(build), "-t", "compdb", "-x"]) if result.returncode != 0: raise ValueError(f"ninja -t compdb: {result.stderr.strip()}") return json.loads(result.stdout) diff --git a/tools/tests/test_check_compile.py b/tools/tests/test_check_compile.py index e0844ab9fe..65cc0ee3ca 100644 --- a/tools/tests/test_check_compile.py +++ b/tools/tests/test_check_compile.py @@ -14,6 +14,24 @@ TOOL = Path(__file__).resolve().parents[1] / "check_compile.py" class CheckCompileTests(unittest.TestCase): + def test_ninja_response_file_is_expanded_before_syntax_rewriting(self): + compiler = shutil.which("clang") or shutil.which("gcc") + if compiler is None or shutil.which("ninja") is None: + self.skipTest("needs Ninja and a C compiler") + source = self.source("unit.c", "#ifndef REQUIRED\n#error missing definition\n#endif\nint x;\n") + command = subprocess.list2cmdline([compiler]) if os.name == "nt" else '"' + compiler + '"' + (self.build / "build.ninja").write_text( + "rule cc\n command = " + command + " @$out.rsp\n" + " rspfile = $out.rsp\n rspfile_content = -DREQUIRED -c $in -o $out -MD -MF $out.d\n" + "build unit.o: cc ../unit.c\n", encoding="utf-8") + self.assertFalse((self.build / "unit.o.rsp").exists()) + result = self.tool("unit.c") + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertIn("1 translation units compile", result.stdout) + self.assertFalse((self.build / "unit.o").exists()) + self.assertFalse((self.build / "unit.o.d").exists()) + self.assertFalse((self.build / "unit.o.rsp").exists()) + def test_syntax_check_preserves_existing_clang_compilation_fragment(self): compiler = shutil.which("clang") if compiler is None: From 701ae3b857844f6d0494183f57f76fbef0d9feb6 Mon Sep 17 00:00:00 2001 From: MrMha <114929652+MrMha@users.noreply.github.com> Date: Sat, 17 Oct 2026 18:40:01 +0400 Subject: [PATCH 0770/1000] fix: quote configure seed cache variable names --- tools/configure_seed.py | 2 +- tools/tests/test_configure_seed.py | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/tools/configure_seed.py b/tools/configure_seed.py index 31a3eead18..071d48e1a1 100644 --- a/tools/configure_seed.py +++ b/tools/configure_seed.py @@ -30,7 +30,7 @@ def main(): parser.add_argument("seed", help="file to pass to cmake -C") args = parser.parse_args() found = checks(args.cache) - Path(args.seed).write_text("".join(f"set({name} {quote(value)} CACHE INTERNAL {quote(doc)})\n" + Path(args.seed).write_text("".join(f"set({quote(name)} {quote(value)} CACHE INTERNAL {quote(doc)})\n" for name, (value, doc) in sorted(found.items())), encoding="utf-8") print(f"{len(found)} check results") return 0 diff --git a/tools/tests/test_configure_seed.py b/tools/tests/test_configure_seed.py index 375ea8c68c..e1a1413fca 100644 --- a/tools/tests/test_configure_seed.py +++ b/tools/tests/test_configure_seed.py @@ -26,6 +26,15 @@ set(MINI_OPTION "value" CACHE STRING "An option, not a check result") @unittest.skipUnless(shutil.which("ninja") and (shutil.which("gcc") or shutil.which("cc")), "needs ninja and a C compiler") class ConfigureSeedTests(unittest.TestCase): + def test_check_names_with_spaces_round_trip_through_cmake(self): + cache = self.root / "named-check.txt" + cache.write_text('//Test HAVE FEATURE\nHAVE FEATURE:INTERNAL=1\n', encoding="utf-8") + output = self.configure("named-check", "-C", str(self.seed(cache, "named-check.cmake"))) + self.assertNotRegex(output, r"CMake Error") + seeded = self.root / "named-check" / "CMakeCache.txt" + self.assertEqual(configure_seed.checks(seeded)["HAVE FEATURE"][0], "1") + self.assertNotIn("HAVE:INTERNAL=", seeded.read_text(encoding="utf-8")) + @classmethod def setUpClass(cls): cls.directory = tempfile.TemporaryDirectory() From 14c2b0bd46c36ae76ebfdc81253e6334e1a38708 Mon Sep 17 00:00:00 2001 From: MrMha <114929652+MrMha@users.noreply.github.com> Date: Sat, 17 Oct 2026 18:40:23 +0400 Subject: [PATCH 0771/1000] fix: preserve root-relative links when moving debt entries --- tools/debt_split.py | 2 +- tools/tests/test_debt_split.py | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/tools/debt_split.py b/tools/debt_split.py index 0a425396f6..6f8bfc054c 100644 --- a/tools/debt_split.py +++ b/tools/debt_split.py @@ -46,7 +46,7 @@ def key(kind, text): def relink(text, source, destination): def replace(match): target = match.group(1) - if re.match(r"^[A-Za-z][A-Za-z0-9+.-]*:|//", target): + if re.match(r"^[A-Za-z][A-Za-z0-9+.-]*:|/", target): return match.group(0) path, _, anchor = target.partition("#") resolved = posixpath.normpath(posixpath.join(posixpath.dirname(source), path)) if path else source diff --git a/tools/tests/test_debt_split.py b/tools/tests/test_debt_split.py index 7e54432bf2..14eb2bac77 100644 --- a/tools/tests/test_debt_split.py +++ b/tools/tests/test_debt_split.py @@ -25,6 +25,17 @@ ORIGINAL = index([A_STUB, B_STUB, C_STUB], [A_FUNCTIONAL, SHADER, SHARED]) class DebtSplitTests(unittest.TestCase): + def test_split_preserves_root_relative_links(self): + targets = ("/AnyPS5/guide.html#usage", "/images/example.png", "/") + references = " ".join(f"[reference]({target})" for target in targets) + self.write(INDEX, ORIGINAL.replace(A_STUB, A_STUB + " " + references)) + self.split() + moved = self.read("docs/dev/debt/libSceA.md") + for target in targets: + with self.subTest(target=target): + self.assertIn(f"[reference]({target})", moved) + self.assertIn("../../../core/libs/prx/libSceA/Export.cpp", moved) + def setUp(self): self.directory = tempfile.TemporaryDirectory() self.root = Path(self.directory.name) From e80d47b7d0d6fe06eab39d2498beaf316f7e430c Mon Sep 17 00:00:00 2001 From: MrMha <114929652+MrMha@users.noreply.github.com> Date: Sat, 17 Oct 2026 18:40:43 +0400 Subject: [PATCH 0772/1000] fix: preserve SELF inputs when an output aliases them --- tools/tests/test_unwrap_self.py | 17 +++++++++++++++++ tools/unwrap_self.py | 5 ++++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/tools/tests/test_unwrap_self.py b/tools/tests/test_unwrap_self.py index 587c7e8b0c..8021984348 100644 --- a/tools/tests/test_unwrap_self.py +++ b/tools/tests/test_unwrap_self.py @@ -82,6 +82,23 @@ def read_program_headers(path): class UnwrapTest(unittest.TestCase): + def test_output_aliases_cannot_replace_the_input_container(self): + container = build_container(default_segments(), default_records(), default_payloads()) + source = Path(self.directory) / "input.self" + source.write_bytes(container) + alias = Path(self.directory) / "alias.elf" + import os + os.link(source, alias) + for output in (source, source.parent / "." / source.name, alias): + with self.subTest(output=output): + result = subprocess.run([sys.executable, str(Path(unwrap_self.__file__)), str(source), str(output)], + capture_output=True, text=True, timeout=30) + self.assertEqual(result.returncode, 2, result.stderr) + self.assertIn("output aliases the input", result.stderr) + self.assertNotIn("Traceback", result.stderr) + self.assertEqual(source.read_bytes(), container) + self.assertEqual(alias.read_bytes(), container) + def convert(self, container, name="input.self"): directory = Path(self.directory) source = directory / name diff --git a/tools/unwrap_self.py b/tools/unwrap_self.py index 4dd65de5ac..91fb662116 100644 --- a/tools/unwrap_self.py +++ b/tools/unwrap_self.py @@ -184,7 +184,10 @@ def check_decrypted(data, headers): def unwrap(path, output, verbose=False): - data = bytes(Path(path).read_bytes()) + source, destination = Path(path), Path(output) + if source.resolve() == destination.resolve() or (destination.exists() and source.samefile(destination)): + raise Refused("output aliases the input container") + data = bytes(source.read_bytes()) if len(data) < FIRST_REGION: raise Refused("file is smaller than the SELF header area") magic = struct.unpack_from(" Date: Sat, 17 Oct 2026 18:41:08 +0400 Subject: [PATCH 0773/1000] fix: refuse invalid load segment metadata when unwrapping SELF --- tools/tests/test_unwrap_self.py | 27 +++++++++++++++++++++++++++ tools/unwrap_self.py | 7 +++++++ 2 files changed, 34 insertions(+) diff --git a/tools/tests/test_unwrap_self.py b/tools/tests/test_unwrap_self.py index 587c7e8b0c..a881a6082c 100644 --- a/tools/tests/test_unwrap_self.py +++ b/tools/tests/test_unwrap_self.py @@ -241,6 +241,33 @@ class UnwrapTest(unittest.TestCase): self.assertEqual(data[headers[1][2]:headers[1][2] + headers[1][5]], pattern(9, 0x80)) self.assertEqual((Path(self.directory) / "input.self").read_bytes(), original) + def test_invalid_load_metadata_is_refused_before_replacing_output(self): + output = Path(self.directory) / "output.elf" + for field, value, message in ((40, 0xff, "file size exceeds memory size"), + (48, 3, "alignment is not a power of two"), + (48, 24, "alignment is not a power of two"), + (16, 0xffffffffffffffff, "memory range exceeds ELF64")): + with self.subTest(field=field, value=value): + container = bytearray(build_container(default_segments(), default_records(), default_payloads())) + struct.pack_into(" memory_size: + raise Refused("segment %d file size exceeds memory size" % index) + if alignment and alignment & (alignment - 1): + raise Refused("segment %d alignment is not a power of two" % index) + if memory_size > (1 << 64) - vaddr: + raise Refused("segment %d memory range exceeds ELF64" % index) headers.append({ "index": index, "type": kind, "flags": flags, "offset": file_offset, "vaddr": vaddr, "size": file_size, "memory": memory_size, From d915e5835a6d7c1fda2f53e216a45fbc4a3d8c60 Mon Sep 17 00:00:00 2001 From: mattogepeto Date: Sat, 17 Oct 2026 16:41:22 +0200 Subject: [PATCH 0774/1000] fix(libkernel): check for a directory with fstat before getdents on Linux The Linux path mapped ENOTDIR from getdents64 to EINVAL, but a pipe or FIFO still failed with ESPIPE from the lseek that reads the base offset first. FreeBSD 11.4's kern_getdirentries returns EINVAL for any descriptor that is not a directory vnode, pipes included, and EBADF for one that is not open. Check the descriptor with fstat before the lseek: EBADF and other host errors are passed through, and anything that is not a directory returns EINVAL. The getdents64 error line is unchanged again, and the technical debt entry is removed since no difference is left. Written with AI assistance (Claude). Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libkernel/File/src/Stdio.cpp | 5 ++++- docs/dev/debt/libkernel.md | 1 - 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index 862aa1f7e8..90767bf005 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -1006,12 +1006,15 @@ int APS5_VABI sceKernelGetdirentries(int fd, char* buf, int nbytes, int64_t* bas constexpr std::size_t GuestMaxName = 255; if (buf == nullptr) return SceErrorFromErrno(GUEST_EFAULT); if (nbytes <= 0) return SceErrorFromErrno(GUEST_EINVAL); + struct stat status{}; + if (::fstat(fd, &status) != 0) return SceErrorFromErrno(errno); + if (!S_ISDIR(status.st_mode)) return SceErrorFromErrno(GUEST_EINVAL); const off_t base = ::lseek(fd, 0, SEEK_CUR); if (base < 0) return SceErrorFromErrno(errno); if (basep != nullptr) *basep = static_cast(base); std::vector host(static_cast(nbytes) < 4096 ? 4096 : static_cast(nbytes)); const auto read = ::syscall(SYS_getdents64, fd, host.data(), host.size()); - if (read < 0) return SceErrorFromErrno(errno == ENOTDIR ? GUEST_EINVAL : errno); + if (read < 0) return SceErrorFromErrno(errno); std::size_t written = 0; off_t resume = base; for (long offset = 0; offset < read;) { diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index da7053539c..5791f7ab5e 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -122,7 +122,6 @@ - [system](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) runs no command. It follows FreeBSD 11's `lib/libc/stdlib/system.c` for a sandbox without `/bin/sh`: a null command returns 1 unconditionally, as that version does, and any command returns the wait status of a child that could not exec the shell and called `_exit(127)` (`0x7f00`). Whether the console's application sandbox has no shell, and whether `vfork` succeeds there (FreeBSD returns -1 when it fails), is unverified - [sceKernelWriteThrottlingStatus](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) reports no write throttling: the host SSD has no write budget, so the first qword of the 32-byte status is set to `UINT32_MAX` and the rest is zeroed. PPSA17221 reads only that qword: it multiplies it by 64 KiB and compares the product with byte thresholds up to 0x4AF00000 to pick one of five tiers. When the call fails the title assumes 0x4B000001 bytes, which selects the same top tier as `UINT32_MAX`. What the value measures on hardware (a remaining budget or a rate) and the meaning of the other three qwords are unknown - [Windows directory descriptors](../../../core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp) (libkernel) take one snapshot of the directory, on the first `getdents`, `getdirentries` or non-zero `lseek` (`SEEK_SET` or `SEEK_CUR`), and keep it until it is discarded; `lseek` to offset 0 only discards the snapshot, and the next read lists the directory again, so every rewind costs O(entries) on the next read. `lseek` takes the offset as an entry index and `SEEK_CUR` with offset 0 only reports the index. `SEEK_END` throws, where the NUL descriptor used to return 0, because FreeBSD defines it relative to the directory size, which is not modelled. A negative result returns `EINVAL` and a `SEEK_CUR` offset that overflows returns `EOVERFLOW`, as FreeBSD `vn_seek` reports them. Names whose UTF-8 encoding exceeds the one-byte `d_namlen` limit are not representable in this guest directory-record layout: `getdents` returns `ENAMETOOLONG` without consuming the entry. Repeated calls therefore report the same error and entries after that name cannot be reached through this descriptor; FreeBSD's `getdents` behavior for host filenames that exceed its `MAXNAMLEN` is not verified. `getdirentries` still reports 0 through `basep` instead of the position of the block it read, so only 0 and values returned by `lseek` are valid offsets; FreeBSD offsets are filesystem cookies -- [getdents and getdirentries](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) return `EINVAL` for a descriptor that is not a directory, as [`kern_getdirentries`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/vfs_syscalls.c) does for a vnode that is not `VDIR`. On Linux a host pipe or FIFO still fails with `ESPIPE`, from the `lseek` that reads the base offset before `getdents64`, where `getvnode` returns `EINVAL` for a non-vnode descriptor - [/dev/random and /dev/urandom](../../../core/libs/prx/libkernel/File/src/RandomDevice.cpp) (libkernel) opened for reading return a descriptor backed by the host null device whose reads are filled with host OS entropy (`getentropy`, `rand_s` on Windows); both paths behave the same, never block, and positioned reads ignore the offset (FreeBSD `random(4)`). Only the exact paths are recognised, `fstat`, `lseek` and `mmap` see the null device, and opening them for writing throws - [pthread_mutexattr_getpshared and pthread_mutexattr_setpshared](../../../core/libs/prx/libkernel/Pthread/Posix/Mutex.cpp) (libkernel) store and report private or shared attributes, following FreeBSD 11 libthr [thr_mutexattr.c](https://github.com/freebsd/freebsd-src/blob/stable/11/lib/libthr/thread/thr_mutexattr.c). Invalid values return `EINVAL` without changing the attribute. Guest threads execute in one host process; sharing mutexes across processes is not modeled. Console behavior and invalid-attribute error precedence are unverified. - [pthread_attr_getinheritsched, pthread_attr_setinheritsched, pthread_attr_getscope, pthread_attr_setscope, pthread_attr_getstackaddr](../../../core/libs/prx/libkernel/Pthread/Posix/Attr.cpp) (libkernel) follow FreeBSD 11 libthr ([thr_attr.c](https://github.com/freebsd/freebsd-src/blob/5ed7eb0d97ba4436218e810f61bd059acba984c2/lib/libthr/thread/thr_attr.c), [thr_create.c](https://github.com/freebsd/freebsd-src/blob/5ed7eb0d97ba4436218e810f61bd059acba984c2/lib/libthr/thread/thr_create.c)), not measured on the console: the scope defaults to `PTHREAD_SCOPE_SYSTEM` (2), only it and `PTHREAD_SCOPE_PROCESS` (0) are accepted and other values return `EINVAL`, and an inherit value other than `PTHREAD_INHERIT_SCHED` (4) or `PTHREAD_EXPLICIT_SCHED` (0) returns `ENOTSUP`. `pthread_attr_getstackaddr` returns the configured stack address pointer (null by default). A null output of `pthread_attr_getinheritsched` throws, since libthr writes through it unchecked. A thread keeps the scope it was created with, taken from the creating thread under `PTHREAD_INHERIT_SCHED`, and `pthread_attr_get_np` and `scePthreadAttrGet` report it, but every guest thread is a host thread at system scope, so process scope changes nothing From 8f0caa17963d5f25e7c48ea471958946bedbcb6c Mon Sep 17 00:00:00 2001 From: mattogepeto Date: Sat, 17 Oct 2026 16:41:52 +0200 Subject: [PATCH 0775/1000] docs(libkernel): move the socket type debt entry away from other changes The entry sat right after "connect to AF_UNSPEC", where the open #6884 adds its own entry, so whichever merges second would conflict. Move it after the mlockall entry, which no open pull request changes. The text is unchanged. Written with AI assistance (Claude). Co-Authored-By: Claude Opus 5.5 --- docs/dev/debt/libkernel.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 7acb437253..532741d184 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -175,11 +175,11 @@ - [Socket error numbers](../../../core/libs/prx/libkernel/Socket/include/SocketError.hpp) (libkernel) - a host socket error becomes the FreeBSD 11 `errno.h` number of the same name; the numbers are not checked on the console. Winsock `WSAESHUTDOWN` becomes `EPIPE`, as a send after `shutdown` does on FreeBSD, and a host error with no FreeBSD socket counterpart becomes `EIO`. A nonblocking `connect` in progress reports `EINPROGRESS` on Linux but `EAGAIN` on Windows (`WSAEWOULDBLOCK`), where FreeBSD reports `EINPROGRESS`. On Windows a refused nonblocking `connect` is never reported (`SO_ERROR` is unsupported), so a repeated one keeps reporting `EALREADY` where FreeBSD reports `ECONNREFUSED`. A receive after `shutdown(SHUT_RD)` reports `EPIPE` on Windows where FreeBSD returns 0 - [getsockopt, setsockopt](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) on a guest socket handle the SOL_SOCKET level and `TCP_NODELAY`, after FreeBSD 11.4's [`sosetopt` and `sogetopt`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c): `SO_REUSEADDR`, `SO_KEEPALIVE`, `SO_DONTROUTE`, `SO_BROADCAST`, `SO_OOBINLINE` and `SO_LINGER` act on the host socket, and `SO_ERROR`, `SO_TYPE`, `SO_ACCEPTCONN`, `SO_SNDBUF` and `SO_RCVBUF` are read. A boolean option reads back as its own bit, as FreeBSD returns `so_options & option`. Winsock does not clear a socket's pending error when it is read, so on Windows the library reports a pending error once and shows it again only after a new `connect`; a second failure with the same code on the same socket without a `connect` in between would be hidden. `SO_SNDBUF` and `SO_RCVBUF` read the host's value, which is not the size FreeBSD reports (Linux doubles it). `SO_NOSIGPIPE` is kept per socket and reads back as its bit. On Linux a `send`, `sendto`, `sendmsg` or `write` on that socket passes `MSG_NOSIGNAL` to the host, as [`kern_sendit`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_syscalls.c#L890-L895) and [`soo_write`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/sys_socket.c#L147-L151) leave out the signal. An accepted socket takes it from its listener, as [`sonewconn`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L603) copies the options. `SO_NBIO` (`0x1200`) is the console's own option, not FreeBSD's. It switches the socket between blocking and non-blocking as `FIONBIO` does and reads back as 0 or 1, as `sceNetSetsockopt` and `sceNetGetsockopt` treat it here; what the console reads back for it is not measured. `TCP_NODELAY` (`IPPROTO_TCP`, 1) is set on the host socket and reads back as `TF_NODELAY` (4), as [`tcp_default_ctloutput`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/netinet/tcp_usrreq.c#L1860-L1861) returns it. The `IPPROTO_TCP` level on a datagram or `AF_UNIX` socket fails with `EINVAL`, as [`ip_ctloutput`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/netinet/ip_output.c#L943-L944) and [`uipc_ctloutput`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_usrreq.c#L1228-L1229) answer it, and an accepted socket takes `TCP_NODELAY` through the host. `SO_SNDTIMEO` and `SO_RCVTIMEO` follow [`sosetopt`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L2657-L2689) and [`sogetopt`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L2846-L2850). A 16-byte `timeval` with a negative field or 1000000 microseconds or more fails with `EDOM`, and more than `INT32_MAX` seconds becomes the longest wait. The value is kept and read back through FreeBSD's 32.32 fixed-point conversion ([`tvtosbt`, `sbttotv`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/sys/time.h#L178-L286)), so half a second reads back as 499999 microseconds and one microsecond as 0. A `recv`, `recvfrom`, `recvmsg`, `read` or `readv`, and a `send`, `sendto`, `sendmsg`, `write` or `writev`, on a blocking socket then waits that long for the host to report the socket readable or writable, and fails with `EWOULDBLOCK`; a call with `MSG_DONTWAIT` does not wait. An accepted socket takes both from its listener ([`sonewconn`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L629-L630)). The wait covers only the first transfer: once the socket is writable the host send runs without a limit, where FreeBSD stops a send that cannot finish and returns the bytes sent so far, and a receive whose data another thread takes between the wait and the host call blocks again. `sendfile` ignores the send timeout. The host wait has millisecond steps, so a shorter timeout lasts up to a millisecond longer. A null value fails with `EINVAL`, where FreeBSD's [`kern_setsockopt`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_syscalls.c#L1414-L1415) returns `EFAULT`. Every other SOL_SOCKET option (`SO_DEBUG`, `SO_USELOOPBACK`, `SO_REUSEPORT` and `SO_TIMESTAMP`, which FreeBSD stores and no title has been seen to set, the low-water marks among them), every other `IPPROTO_TCP` option (`TCP_MAXSEG`, `TCP_KEEPIDLE`, `TCP_KEEPINTVL` and `TCP_KEEPCNT` among them) and every other level (`IPPROTO_IP`, `IPPROTO_IPV6`) fail with `ENOPROTOOPT`. None of this was measured on a console. - [connect to AF_UNSPEC](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) dissolves a datagram association as FreeBSD 11.4's `udp_disconnect()` does, which resets the local address to the wildcard address and keeps the port. On Linux a socket bound to an explicit port keeps its local address as well (`127.0.0.1:` stays `127.0.0.1:`), because a bound socket cannot be bound again; one bound to port 0 is bound again to the wildcard address with the port it had -- [socket](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) creates only `SOCK_STREAM` and `SOCK_DGRAM` sockets in `AF_INET` and `AF_INET6`. A type that FreeBSD 11.4's inet domain has no protocol for in any configuration ([`in_proto.c`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/netinet/in_proto.c) has no `SOCK_RDM` entry and no unknown type) fails with `EPROTOTYPE` when the protocol is 0 and `EPROTONOSUPPORT` otherwise, as [`socreate`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c) does. `SOCK_RAW`, which `in_proto.c` always has (`socreate` creates it for a privileged process), and `SOCK_SEQPACKET`, which it has only in a kernel built with `SCTP`, fail with `EPROTONOSUPPORT`; whether the console allows either is not known. `SOCK_DGRAM` with `IPPROTO_UDPLITE` and `AF_UNIX` (`socketpair` supports it) fail with `EPROTONOSUPPORT` and `EAFNOSUPPORT` where FreeBSD creates the socket - [msync](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) (libkernel) follows FreeBSD 11's `kern_msync` and `vm_map_sync`, not measured on the console: the address is rounded down to the page, a range that wraps is `EINVAL`, as is `MS_ASYNC` with `MS_INVALIDATE`, and a range whose start or any page is not mapped (as `sceKernelVirtualQuery` reports it, reservations included) is `ENOMEM`; a page unmapped from inside a larger mapping still counts as mapped there, since `sceKernelVirtualQuery` keeps reporting the whole mapping. The guest has no file mappings, so a successful call writes nothing back. `MS_INVALIDATE` on a mapped range throws: FreeBSD returns `EBUSY` when the range holds `mlock`ed pages, and locked pages are not tracked - [madvise](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) (libkernel) accepts `MADV_PROTECT` (10) as a no-op hint without modifying process protection. FreeBSD 11's `kern_madvise` (`sys/vm/vm_mmap.c`) handles it before the address check, ignores the address and length, and calls `kern_procctl(..., PROC_SPROTECT, PPROT_SET)`, which requires `PRIV_VM_MADV_PROTECT` and fails with `EPERM` without it. Returning 0 unconditionally assumes the calling process is privileged. Console behaviour is unverified - [mincore](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) (libkernel) follows FreeBSD 11's `sys_mincore` (`sys/vm/vm_mmap.c`), not measured on the console: the address is rounded down to the page, length 0 returns 0, a null vector over a mapped range returns `EFAULT`, an overflowing range or any unmapped page in the range (as reported by `sceKernelVirtualQuery`) returns `ENOMEM`. For valid ranges, a page reports `MINCORE_INCORE` (0x1) when `sceKernelVirtualQuery` reports it committed and 0 when it is only reserved; actual residency, referenced and modified bits are not modelled. Length 0 returns 0 when the start page is mapped and `ENOMEM` otherwise, and the range and mapping checks come before the null vector `EFAULT`, as in `kern_mincore` Java 25 LAN / DNS (PPSA99869) imports it - [mlockall, munlockall](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) (libkernel) - `mlockall` returns `EINVAL` for zero or unknown flags. Valid `MCL_CURRENT` throws because it would lock host stacks, heaps, modules and driver mappings along with guest memory; `MCL_FUTURE` throws because later mappings are not tracked for locking. FreeBSD 11.4's `sys_munlockall` (`sys/vm/vm_mmap.c`:1133-1165) checks `priv_check(td, PRIV_VM_MUNLOCK)` first (returning `EPERM` without it) and unwires the user map (`vm_map_unwire` over `vm_map_min`...`vm_map_max` with `VM_MAP_WIRE_HOLESOK`). Returning 0 unconditionally assumes the calling process is privileged. `munlockall` unlocks memory locked by `mlock` (`VirtualUnlock` on Windows, tolerating `ERROR_NOT_LOCKED` for pages never locked, and `munlockall` on Linux). On main, XPSemu (PPSA97358) fails to load at these imports (mlockall #4386, munlockall); their calls in QEMU `os-posix.c` execute only when `-overcommit mem-lock=on` is enabled. The call has not been observed in a run with this implementation. +- [socket](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) creates only `SOCK_STREAM` and `SOCK_DGRAM` sockets in `AF_INET` and `AF_INET6`. A type that FreeBSD 11.4's inet domain has no protocol for in any configuration ([`in_proto.c`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/netinet/in_proto.c) has no `SOCK_RDM` entry and no unknown type) fails with `EPROTOTYPE` when the protocol is 0 and `EPROTONOSUPPORT` otherwise, as [`socreate`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c) does. `SOCK_RAW`, which `in_proto.c` always has (`socreate` creates it for a privileged process), and `SOCK_SEQPACKET`, which it has only in a kernel built with `SCTP`, fail with `EPROTONOSUPPORT`; whether the console allows either is not known. `SOCK_DGRAM` with `IPPROTO_UDPLITE` and `AF_UNIX` (`socketpair` supports it) fail with `EPROTONOSUPPORT` and `EAFNOSUPPORT` where FreeBSD creates the socket - [sigtimedwait](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) waits on a process-wide pending set instead of throwing: a signal raised while blocked stays pending and is taken by `sigwait`, `sigwaitinfo` or `sigtimedwait`, delivered to the handler when `_sigprocmask` unblocks it, or delivered by `pause`. The set covers all 128 signals and the lowest number that is pending and in the set is selected (FreeBSD specifies the lowest number only among real-time signals). Selection, `EAGAIN` on timeout, `EINVAL` for a `tv_nsec` outside 0 to 1000000000 while nothing is pending and `NULL` timeout blocking indefinitely follow FreeBSD's [sigtimedwait(2)](https://man.freebsd.org/cgi/man.cgi?query=sigtimedwait). `sigwait` returns 0 with the number in its output, as POSIX specifies, and `sigwaitinfo` and `sigtimedwait` return the number, as FreeBSD does. The guest `siginfo_t` follows FreeBSD 11's `sys/sys/siginfo.h`: 80 bytes, `si_signo` at 0, `si_code` at 8, `si_pid` at 12, `si_uid` at 16, `si_addr` at 24, `si_value` at 32 and `si_band` at 40; only `si_signo`, `si_code` (`SI_USER`) and `si_pid` (the guest pid) are written, `si_uid` stays 0 because no uid is modelled, and the console's layout is not verified. PPSA85300 imports `sigtimedwait` and fails to load without it; its only call site is VLC's `vlc_writev` ([filesystem.c](https://github.com/videolan/vlc/blob/6de05adcbaf2e8b85fe86aad4169393098628119/src/posix/filesystem.c#L243-L278)), which polls with a zero timeout to drain a `SIGPIPE` only after `writev` fails with `EPIPE`. The waits are not cancellation points and an unblocked caught signal does not interrupt them with `EINTR`, where FreeBSD's `sigwaitinfo` and `sigtimedwait` may return it. `SIGPIPE` has no host equivalent and is never raised by a host write here (see `read, write and vector I/O`), so VLC's drain finds nothing - [pause, sigsuspend, sigwait, sigwaitinfo](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) take the pending set of `sigtimedwait` instead of throwing. `pause` waits for a pending signal that is not blocked and whose disposition can be delivered, runs its handler on the calling thread and returns -1 with `EINTR`; an ignored signal is discarded and the wait continues, and it never returns for a signal whose handler cannot run. A signal whose registered handler has `SA_SIGINFO` still throws, and one whose disposition is the default action and that has no host equivalent (every signal except 2, 4, 6, 8, 11 and 15) is neither delivered nor discarded: it stays pending until a wait takes it, where FreeBSD runs the default action, which terminates the process for most of them. PPSA68091, PPSA99169, PPSA99420 and PPSA97358 import them and fail to load without them. PPSA68091 calls `pause` at one site (0x8F7266), in a loop that waits while a global holds another thread's `errno` address. PPSA99169 only stores the address of `pause` in a table of symbol addresses (0x30DB1). PPSA99420 calls `pause` at 0x3BD80F6 and 0x3C9E220, `sigwait` at 0x3C9E4C7 and `sigwaitinfo` at 0x3C9E588. PPSA97358 calls `sigwait` from QEMU's `sigwait_compat` thread ([compatfd.c](https://github.com/ZiZc3/XPSemu/blob/8eec07b4e31f66dce8e0b05c1ca1281af303e933/util/compatfd.c#L28-L53)), which `qemu_signal_init` ([main-loop.c](https://github.com/ZiZc3/XPSemu/blob/8eec07b4e31f66dce8e0b05c1ca1281af303e933/util/main-loop.c#L110-L147)) starts for `SIGIO`, `SIGALRM` and `SIGBUS` when the main loop is created, so it reaches the wait at start-up, and from the vCPU thread of the dummy accelerator ([dummy-cpus.c](https://github.com/ZiZc3/XPSemu/blob/8eec07b4e31f66dce8e0b05c1ca1281af303e933/accel/dummy-cpus.c#L48-L56)). `sigsuspend` swaps the process-wide mask of `_sigprocmask` for its argument, waits as `pause` does through the same helper, then restores the mask through `_sigprocmask`, and returns -1 with `EINTR`; a null set throws, as the console faults in libthr instead of returning `EFAULT`. All four words of the argument replace the mask and are restored afterwards, as in `_sigprocmask`. Java 25 LAN / DNS (PPSA99869, #5501) imports it and fails to load without it, after `sysctlnametomib`, `fegetenv` and `mincore`. None of the five waits is a cancellation point - [getsid](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) returns the calling process ID for pid 0 and the calling process, and `ESRCH` for other process IDs, assuming the single guest process is its own session leader. PPSA74126 imports it and fails to load without it From f83b6c0ee0c257bc405ce621a530612ee7d4305d Mon Sep 17 00:00:00 2001 From: MrMha <114929652+MrMha@users.noreply.github.com> Date: Sat, 17 Oct 2026 18:41:57 +0400 Subject: [PATCH 0776/1000] fix: report oracle CLI input errors before running tools --- tools/hw-oracle/hw_oracle.py | 24 +++++++++++++++--------- tools/hw-oracle/test_hw_oracle.py | 27 ++++++++++++++++++++++++++- 2 files changed, 41 insertions(+), 10 deletions(-) diff --git a/tools/hw-oracle/hw_oracle.py b/tools/hw-oracle/hw_oracle.py index b81f53d966..0f43800a93 100644 --- a/tools/hw-oracle/hw_oracle.py +++ b/tools/hw-oracle/hw_oracle.py @@ -100,7 +100,7 @@ def assemble(body, work, wave64, *, ieee, denorm32, denorm16, dx10_clamp, round3 return work / "k.co" -def run(body, rows, extra=b"", wave64=False, coarse=False, *, ieee, denorm32, denorm16, dx10_clamp, round32, round16, fp16_overflow, lds=DEFAULT_LDS): +def validated_rows(rows): validated = [] for index, row in enumerate(rows, 1): try: @@ -110,7 +110,11 @@ def run(body, rows, extra=b"", wave64=False, coarse=False, *, ieee, denorm32, de if len(values) != 4 or any(value < 0 or value > 0xffffffff for value in values): raise ValueError(f"row {index} needs 4 unsigned 32-bit integers") validated.append(values) - rows = validated + return validated + + +def run(body, rows, extra=b"", wave64=False, coarse=False, *, ieee, denorm32, denorm16, dx10_clamp, round32, round16, fp16_overflow, lds=DEFAULT_LDS): + rows = validated_rows(rows) wave = 64 if wave64 else 32 padded = rows + [(0, 0, 0, 0)] * (-len(rows) % wave) env = dict(os.environ) @@ -161,13 +165,15 @@ def main(): parser.add_argument("--outs", type=int, choices=range(1, 17), default=16, metavar="N", help="print v10..v(10+N-1)") parser.add_argument("--json", action="store_true", help="emit inputs, results and run settings as JSON; target is the assembly target, not a GPU model or driver") args = parser.parse_args() - rows = [tuple(parse_word(v) for v in line.split()) for line in args.rows.read_text().splitlines() if line.strip()] - if not rows: - parser.error("rows file must contain at least one input row") - if any(len(r) != 4 for r in rows): - sys.exit("every row needs 4 values") - extra = args.extra.read_bytes() if args.extra else b"" - body = args.body.read_text() + try: + rows = validated_rows(tuple(parse_word(v) for v in line.split()) + for line in args.rows.read_text(encoding="utf-8").splitlines() if line.strip()) + if not rows: + parser.error("rows file must contain at least one input row") + extra = args.extra.read_bytes() if args.extra else b"" + body = args.body.read_text(encoding="utf-8") + except (OSError, ValueError) as error: + parser.error(str(error)) modes = {name: getattr(args, name) for name in ("ieee", "denorm32", "denorm16", "dx10_clamp", "round32", "round16", "fp16_overflow")} results = run(body, rows, extra, args.wave64, args.coarse, **modes, lds=args.lds) if args.json: diff --git a/tools/hw-oracle/test_hw_oracle.py b/tools/hw-oracle/test_hw_oracle.py index a3321dc3e1..d40e731880 100644 --- a/tools/hw-oracle/test_hw_oracle.py +++ b/tools/hw-oracle/test_hw_oracle.py @@ -79,6 +79,30 @@ class OracleCacheTests(unittest.TestCase): class InputRowsTests(unittest.TestCase): + def test_cli_reports_invalid_input_before_tool_lookup(self): + with tempfile.TemporaryDirectory() as tmp: + body, rows = Path(tmp) / "body.s", Path(tmp) / "rows.txt" + body.write_text("s_nop 0", encoding="utf-8") + argv = ["hw_oracle.py", str(body), str(rows)] + argv += [part for name, value in MODES.items() + for part in ["--" + name.replace("_", "-"), str(value)]] + for contents in (None, b"not a number\n", b"1 2 3\n", b"-1 0 0 0\n", b"4294967296 0 0 0\n", b"\xff"): + with self.subTest(contents=contents): + rows.unlink(missing_ok=True) + if contents is not None: + rows.write_bytes(contents) + with patch("sys.argv", argv), patch.object(hw_oracle, "assemble") as assemble, \ + patch.object(hw_oracle, "oracle") as oracle, \ + contextlib.redirect_stdout(io.StringIO()) as output, \ + contextlib.redirect_stderr(io.StringIO()) as errors, \ + self.assertRaises(SystemExit) as exited: + hw_oracle.main() + self.assertEqual(exited.exception.code, 2) + self.assertIn("error:", errors.getvalue()) + self.assertEqual(output.getvalue(), "") + assemble.assert_not_called() + oracle.assert_not_called() + def test_successful_dispatch_without_output_cannot_reuse_previous_results(self): with tempfile.TemporaryDirectory() as directory: runner = Path(directory) / "runner.py" @@ -257,8 +281,9 @@ class JsonReportTests(unittest.TestCase): self.rows.write_text("0x100000000 0 0 0\n", encoding="utf-8") output = io.StringIO() with patch.object(hw_oracle, "target", side_effect=AssertionError("target lookup")) as target: - with self.assertRaisesRegex(ValueError, "row 1"): + with self.assertRaises(SystemExit) as exited: self.cli("--json", output=output) + self.assertEqual(exited.exception.code, 2) target.assert_not_called() self.assertEqual(output.getvalue(), "") From fcea0e6edb9cf1fca33eb36ff855c8f53cbde385 Mon Sep 17 00:00:00 2001 From: MrMha <114929652+MrMha@users.noreply.github.com> Date: Sat, 17 Oct 2026 18:42:20 +0400 Subject: [PATCH 0777/1000] fix: bound browser library downloads while streaming --- demo/web/tests/archive-root.test.mjs | 50 ++++++++++++++++++++++++++++ demo/web/worker.mjs | 25 ++++++++++++-- 2 files changed, 72 insertions(+), 3 deletions(-) diff --git a/demo/web/tests/archive-root.test.mjs b/demo/web/tests/archive-root.test.mjs index 8a77539c39..1fd8b56d96 100644 --- a/demo/web/tests/archive-root.test.mjs +++ b/demo/web/tests/archive-root.test.mjs @@ -150,6 +150,56 @@ for (const directories of [[], parents, [...parents].reverse()]) { } finally { await archive.close(); } } +{ + const originalFetch = globalThis.fetch; + const originalSelf = globalThis.self; + globalThis.self = { postMessage() {} }; + try { + const { downloadLibraries } = await import(pathToFileURL(path.join(process.argv[2], "worker.mjs"))); + const bytes = Uint8Array.from({ length: 32 }, (_, index) => index); + const sha256 = [...new Uint8Array(await crypto.subtle.digest("SHA-256", bytes))].map(byte => byte.toString(16).padStart(2, "0")).join(""); + const asset = { path: "libraries/linux-1234.zip", size: bytes.length, expandedBytes: 100, sha256 }; + let cancelled = false; + let pulled = 0; + const configureDownload = (chunks, size = bytes.length) => { + cancelled = false; + pulled = 0; + globalThis.fetch = async url => { + if (url === "./release.json") return Response.json({ tag: "v1", platforms: { linux: { ...asset, size } } }); + if (url.startsWith("https://api.github.com/")) return Response.json({ tag_name: "v1" }); + return new Response(new ReadableStream({ + pull(controller) { + if (pulled < chunks.length) controller.enqueue(chunks[pulled++]); + else controller.close(); + }, + cancel() { cancelled = true; } + })); + }; + }; + configureDownload(Array.from({ length: 8 }, () => bytes)); + await assert.rejects(downloadLibraries("linux"), /System library download is incomplete/); + assert.equal(cancelled, true); + assert(pulled < 8); + for (const chunks of [[], [bytes.subarray(0, 31)]]) { + configureDownload(chunks); + await assert.rejects(downloadLibraries("linux"), /System library download is incomplete/); + } + configureDownload([bytes.subarray(0, 3), bytes.subarray(3)]); + const downloaded = await downloadLibraries("linux"); + assert.deepEqual(new Uint8Array(await downloaded.file.arrayBuffer()), bytes); + assert.equal(downloaded.expandedBytes, 100); + for (const size of [-1, 1.5, 512 * 1024 * 1024 + 1]) { + configureDownload([bytes], size); + await assert.rejects(downloadLibraries("linux"), /Invalid release manifest/); + assert.equal(pulled, 0); + } + } finally { + globalThis.fetch = originalFetch; + if (originalSelf === undefined) delete globalThis.self; + else globalThis.self = originalSelf; + } +} + const folder = new Archive([{ path: "outer/inner/title/main.elf", file: new Blob([header]) }]); try { assert.equal((await folder.inspect(() => {})).root, "outer/inner/title/"); } finally { await folder.close(); } diff --git a/demo/web/worker.mjs b/demo/web/worker.mjs index d2cbdb4377..fcd816ffe8 100644 --- a/demo/web/worker.mjs +++ b/demo/web/worker.mjs @@ -21,7 +21,7 @@ export async function downloadLibraries(target) { if (!response.ok) throw new Error(`Cannot load release manifest (${response.status})`); const release = await response.json(); const asset = release.platforms[target]; - if (!asset || !/^libraries\/(windows|linux)-[a-f0-9]+\.zip$/u.test(asset.path) || !/^[a-f0-9]{64}$/u.test(asset.sha256)) throw new Error("Invalid release manifest"); + if (!asset || !Number.isSafeInteger(asset.size) || asset.size < 0 || asset.size > 512 * 1024 * 1024 || !/^libraries\/(windows|linux)-[a-f0-9]+\.zip$/u.test(asset.path) || !/^[a-f0-9]{64}$/u.test(asset.sha256)) throw new Error("Invalid release manifest"); const latestResponse = await fetch("https://api.github.com/repos/boykopovar/AnyPS5/releases/latest", { headers: { Accept: "application/vnd.github+json" } }); if (!latestResponse.ok) { const rateLimited = latestResponse.status === 429 || (latestResponse.status === 403 && latestResponse.headers.get("x-ratelimit-remaining") === "0"); @@ -33,8 +33,27 @@ export async function downloadLibraries(target) { } const download = await fetch(asset.path); if (!download.ok) throw new Error(`Cannot download system libraries (${download.status})`); - const bytes = await download.arrayBuffer(); - if (bytes.byteLength !== asset.size) throw new Error("System library download is incomplete"); + const reader = download.body.getReader(); + const chunks = []; + let size = 0; + try { + for (;;) { + const { value, done } = await reader.read(); + if (done) break; + size += value.byteLength; + if (size > asset.size) throw new Error("System library download is incomplete"); + chunks.push(value); + } + if (size !== asset.size) throw new Error("System library download is incomplete"); + } catch (error) { + await reader.cancel(error).catch(() => {}); + throw error; + } finally { + reader.releaseLock(); + } + const bytes = new Uint8Array(size); + let offset = 0; + for (const chunk of chunks) { bytes.set(chunk, offset); offset += chunk.byteLength; } const digest = [...new Uint8Array(await crypto.subtle.digest("SHA-256", bytes))].map(byte => byte.toString(16).padStart(2, "0")).join(""); if (digest !== asset.sha256) throw new Error("System library checksum mismatch"); return { file: new Blob([bytes]), release, expandedBytes: asset.expandedBytes }; From 6bc11326ec1c97d1a747aaf0f7a0e974188d5b1e Mon Sep 17 00:00:00 2001 From: MrMha <114929652+MrMha@users.noreply.github.com> Date: Sat, 17 Oct 2026 18:46:49 +0400 Subject: [PATCH 0778/1000] fix: reject file and directory conflicts in browser output --- demo/web/archive.mjs | 15 ++++++++++++++ demo/web/tests/archive-root.test.mjs | 31 ++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/demo/web/archive.mjs b/demo/web/archive.mjs index 82b00bed6f..eca9ca9778 100644 --- a/demo/web/archive.mjs +++ b/demo/web/archive.mjs @@ -347,6 +347,21 @@ export async function packageArchive(archive, converted, outputName, libraryFile if (!module.name.startsWith("app0/sce_module/")) throw new Error("Invalid converted module path"); claim(module.name); } + const outputEntries = [ + ...libraryEntries.map(entry => ({ name: entry.filename, directory: entry.directory })), + ...originalFiles.map(item => ({ name: "app0/" + item.name, directory: item.entry.directory })), + { name: outputName }, + ...(converted.diagnostics ? [{ name: "windows-diagnostics-imports.txt" }] : []), + ...converted.modules.map(module => ({ name: module.name })) + ]; + const files = new Set(outputEntries.filter(entry => !entry.directory).map(entry => entry.name.toLowerCase())); + for (const { name } of outputEntries) { + const parts = name.replace(/\/$/u, "").toLowerCase().split("/"); + for (let index = 1; index < parts.length; index++) { + const parent = parts.slice(0, index).join("/"); + if (files.has(parent)) throw new Error(`Output path collision: ${parent}`); + } + } const binary = { versionMadeBy: 0x31e, externalFileAttributes: (0o100755 << 16) >>> 0 }; await writer.add(outputName, new Uint8ArrayReader(converted.executable), binary); if (converted.diagnostics) await writer.add("windows-diagnostics-imports.txt", new Uint8ArrayReader(new TextEncoder().encode(converted.diagnostics))); diff --git a/demo/web/tests/archive-root.test.mjs b/demo/web/tests/archive-root.test.mjs index 8a77539c39..519b777975 100644 --- a/demo/web/tests/archive-root.test.mjs +++ b/demo/web/tests/archive-root.test.mjs @@ -14,6 +14,37 @@ async function zipped(entries) { return writer.close(); } +const { packageArchive } = await import(pathToFileURL(path.join(process.argv[2], "archive.mjs"))); +const libraryBundle = await zipped([["libs/provider.prx", false]]); +for (const conflict of ["sce_module/provider.guest.prx/child", "sce_module/PROVIDER.GUEST.PRX/child", "sce_module/provider.guest.prx/"]) { + const input = new Archive(await zipped([["main.elf", false], ["sce_module/provider.prx", false], [conflict, conflict.endsWith("/")]])); + try { + await input.inspect(() => {}); + const written = []; + const destination = { + async directory(name) { written.push(name); }, + async file(name) { written.push(name); return new WritableStream(); } + }; + const converted = { executable: header, modules: [{ name: "app0/sce_module/provider.guest.prx", bytes: header }] }; + await assert.rejects(packageArchive(input, converted, "main", libraryBundle, destination, () => {}, true), /Output path collision/); + assert.deepEqual(written, []); + } finally { await input.close(); } +} + +const distinctOutput = new Archive(await zipped([["main.elf", false], ["sce_module/provider.prx", false], ["assets/", true]])); +try { + await distinctOutput.inspect(() => {}); + const written = []; + await packageArchive(distinctOutput, { executable: header, modules: [{ name: "app0/sce_module/provider.guest.prx", bytes: header }] }, + "main", libraryBundle, { + async directory(name) { written.push(name); }, + async file(name) { written.push(name); return new WritableStream(); } + }, () => {}, true); + assert(written.includes("app0/assets/")); + assert(written.includes("app0/sce_module/provider.guest.prx")); + assert(written.includes("libs/provider.prx")); +} finally { await distinctOutput.close(); } + const metadata = new Uint8Array(86); const metadataView = new DataView(metadata.buffer); metadataView.setUint32(0, 0x00051607); From 2578949658293847db4357c57ede28fbeb7f8c21 Mon Sep 17 00:00:00 2001 From: milescoviello Date: Sat, 17 Oct 2026 14:46:37 +0000 Subject: [PATCH 0779/1000] fix(agc): run the AGC GPU tests in an ANYPS5_ENABLE_ASAN build After #7147 the CPU-side tests pass under AddressSanitizer, but 18 of the 518 AGC driver and shader tests still failed there with ASAN_OPTIONS=detect_leaks=0: - Guest memory is raw memory, not a C++ object. ReadRawShader reads a shader's first 256 bytes before it knows the program's length, and ShaderMemory copies the 64-byte block around each word it is asked for. The tests place shaders and descriptors in small globals and locals, so those reads ran past them into ASan's redzones (13 tests, global- and stack-buffer-overflow). GuestMemory::CopyGuestBytes copies guest bytes with memcpy, and in an ASan build without instrumentation; Read and ShaderMemory's block load use it. - The depth surface reuse checks in agc_driver_graphics use the made-up guest addresses 0x80000000 and 0x81000000, which lie in ASan's low shadow, so the HTILE read there looked mapped and read shadow memory. They now use 0x30000000 and 0x31000000. - agc_driver_flat_store maps its watched blocks at 8 TiB and 32 TiB to cover page numbers on both sides of 16 TiB. 8 TiB is in ASan's high shadow; the ASan build uses 0x60000000 for the low one. - agc_driver_gpu_map_on_fiber requires the fiber to use at most 8 KiB of its stack while its mapping is imported on a host thread. ASan's redzones make the same frames 9200 bytes; the ASan build allows 16 KiB. It also requires a mapping on an ordinary thread to import on that thread's stack. Like MeasureStackReserve before #7115, the driver's inline-import check (Lifecycle.cpp) and onOwnLiveStack (GuestMemory.cpp) took the address of a local, which ASan's fake stack moves off the thread's stack, so the ASan build always imported on a host thread. Both now measure from __builtin_frame_address(0). Co-Authored-By: Claude Opus 5.5 --- .../Execution/include/GuestMemory.hpp | 1 + .../Execution/src/Driver/Lifecycle.cpp | 3 +-- .../Execution/src/GuestMemory.cpp | 22 ++++++++++++++++--- .../Execution/src/ShaderMemory.cpp | 2 +- .../tests/DepthSurfaceReuse.cpp | 4 ++-- .../tests/execution/FlatStore.cpp | 4 +++- .../tests/execution/GpuMapOnFiber.cpp | 4 ++++ 7 files changed, 31 insertions(+), 9 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp index 2388684997..5e8ce98cc3 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp @@ -26,6 +26,7 @@ struct Commitment { bool whole = false; }; Commitment DescribeCommitted(std::uint64_t address, std::size_t bytes, bool writable = false); +void CopyGuestBytes(void* destination, const void* source, std::size_t bytes); void Read(std::uint64_t address, std::span destination, std::size_t alignment = 1); // Variants for resources in heaps the guest commits on demand (partially resident textures, GPU // scratch heaps): uncommitted pages read as zeros, are not compared, and are never stored. diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp index 91c50d6f3f..5a5ff651f3 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp @@ -73,8 +73,7 @@ bool StackHoldsImport() { const auto low = bounds.low; const auto high = bounds.high; #endif - const volatile unsigned char marker = 0; - const auto frame = reinterpret_cast(&marker); + const auto frame = reinterpret_cast(__builtin_frame_address(0)); return frame >= low && frame < high && frame - low >= InlineImportStackBytes; } diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp index d0859ab061..c6199b4bfe 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp @@ -657,8 +657,7 @@ bool onOwnLiveStack(std::uintptr_t address, std::size_t bytes) { } }; thread_local const Bounds bounds; - const volatile unsigned char marker = 0; - const auto frame = reinterpret_cast(&marker); + const auto frame = reinterpret_cast(__builtin_frame_address(0)); return bounds.high != 0 && frame >= bounds.low && frame < bounds.high && address >= frame && address < bounds.high && bytes <= bounds.high - address; } @@ -2100,9 +2099,26 @@ void Read(std::uint64_t address, std::span destination, std::size_t a if (trace) std::fprintf(stderr, "[unreadable] read from +0x%llx\n", ModuleOffset(__builtin_return_address(0))); throw; } - std::memcpy(destination.data(), source, destination.size()); + CopyGuestBytes(destination.data(), source, destination.size()); } +#ifdef ANYPS5_ENABLE_ASAN +[[gnu::no_sanitize("address")]] void CopyGuestBytes(void* destination, const void* source, std::size_t bytes) { + auto* to = static_cast(destination); + const auto* from = static_cast(source); + std::size_t index = 0; + for (; index + sizeof(std::uint64_t) <= bytes; index += sizeof(std::uint64_t)) { + const std::uint64_t word = *reinterpret_cast(from + index); + std::memcpy(to + index, &word, sizeof(word)); + } + for (; index < bytes; ++index) to[index] = static_cast(from)[index]; +} +#else +void CopyGuestBytes(void* destination, const void* source, std::size_t bytes) { + std::memcpy(destination, source, bytes); +} +#endif + namespace { void copyCommitted(std::uint64_t address, std::span destination) { diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/ShaderMemory.cpp index ffbecd5b70..d658572a98 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/ShaderMemory.cpp @@ -219,7 +219,7 @@ bool ShaderMemory::read(void* context, std::uint64_t address, std::uint32_t* val const auto index = static_cast((address % PageBytes) / sizeof(*value)); if (!page.valid.Test(index) && page.lazy) { const auto first = index / BlockWords * BlockWords; - std::memcpy(&page.words[first], reinterpret_cast((address & ~static_cast(PageBytes - 1)) + first * sizeof(std::uint32_t)), BlockWords * sizeof(std::uint32_t)); + GuestMemory::CopyGuestBytes(&page.words[first], reinterpret_cast((address & ~static_cast(PageBytes - 1)) + first * sizeof(std::uint32_t)), BlockWords * sizeof(std::uint32_t)); page.valid.SetBlock(index); } if (!page.valid.Test(index)) { diff --git a/core/libs/prx/libSceAgcDriver/tests/DepthSurfaceReuse.cpp b/core/libs/prx/libSceAgcDriver/tests/DepthSurfaceReuse.cpp index ebb60efe01..006e4efd53 100644 --- a/core/libs/prx/libSceAgcDriver/tests/DepthSurfaceReuse.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/DepthSurfaceReuse.cpp @@ -458,8 +458,8 @@ void RunDepthSurfaceReuseTests() { const auto otherWithA = pairing(otherDepth, stencilA); Require(imageCopies.size() == 1 && imageCopies[0] == std::make_tuple(withA, otherWithA, static_cast(VK_IMAGE_ASPECT_STENCIL_BIT)), "a stencil plane paired with another depth plane must carry its stencil over"); { - constexpr std::uint64_t arrayBase = 0x80000000; - constexpr std::uint64_t htile = 0x81000000; + constexpr std::uint64_t arrayBase = 0x30000000; + constexpr std::uint64_t htile = 0x31000000; constexpr VkExtent2D extent{1024, 512}; const auto sliceHtile = HtileMetadataBytes(extent); DepthTarget first{arrayBase, 0, extent, VK_FORMAT_D32_SFLOAT, 1.0f, 0}; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/FlatStore.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/FlatStore.cpp index f249083ba2..2d6da41fd0 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/FlatStore.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/FlatStore.cpp @@ -31,7 +31,9 @@ constexpr std::uint32_t MaskedThreads = 16; constexpr std::size_t BlockBytes = 65536; constexpr std::uint32_t CheckedBytes = 0x2000; constexpr std::uint8_t Fill = 0xcd; -#ifdef __linux__ +#if defined(__linux__) && defined(ANYPS5_ENABLE_ASAN) +constexpr std::array GuestWindowAddresses{0x60000000, 0x200000000000}; +#elif defined(__linux__) constexpr std::array GuestWindowAddresses{0x80000000000, 0x200000000000}; #endif diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/GpuMapOnFiber.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/GpuMapOnFiber.cpp index aefdae5293..5a4da94b89 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/GpuMapOnFiber.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/GpuMapOnFiber.cpp @@ -34,7 +34,11 @@ using AgcDriver::Graphics::Require; constexpr std::size_t HostStackContextBytes = 16 * 1024; constexpr std::size_t ContextBytes = 64 * 1024; constexpr std::size_t MappingBytes = 4 * 65536; +#ifdef ANYPS5_ENABLE_ASAN +constexpr std::size_t FiberStackLimit = 16 * 1024; +#else constexpr std::size_t FiberStackLimit = 8 * 1024; +#endif constexpr std::uint64_t Fill = 0x5aa5c33c5aa5c33cull; alignas(16) std::array FiberContext; From 7974214c086522560a30157ade054747beb259df Mon Sep 17 00:00:00 2001 From: MrMha <114929652+MrMha@users.noreply.github.com> Date: Sat, 17 Oct 2026 18:53:29 +0400 Subject: [PATCH 0780/1000] fix: report trace digest input failures without a traceback --- tools/tests/test_trace_digest.py | 19 +++++++++++++++++++ tools/trace_digest.py | 7 ++++++- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/tools/tests/test_trace_digest.py b/tools/tests/test_trace_digest.py index af1ae2e0f7..87ac383b55 100644 --- a/tools/tests/test_trace_digest.py +++ b/tools/tests/test_trace_digest.py @@ -46,6 +46,25 @@ STACKS = "# samples=5 depth=64 truncated=1\n# module a.prx 0x1000 0x1\nntdll.dll class TraceDigestTests(unittest.TestCase): + def test_cli_reports_unreadable_inputs_without_partial_output(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + compressed = gzip.compress(b'{"traceEvents": []}') + files = {"invalid.json": b"{", "broken.json.gz": b"not gzip", "intervals.jsonl": b"{\n", + "truncated.json.gz": compressed[:-4], + "corrupt.json.gz": compressed[:10] + b"\xff" * 4 + compressed[14:]} + for name, data in files.items(): + (root / name).write_bytes(data) + for name in ("missing.json", *files): + with self.subTest(name=name): + result = subprocess.run([sys.executable, trace_digest.__file__, str(root / name)], + capture_output=True, text=True, encoding="utf-8", timeout=20) + self.assertEqual(result.returncode, 2, result.stderr) + self.assertTrue(result.stderr.startswith("FAIL: "), result.stderr) + self.assertNotIn("Traceback", result.stderr) + self.assertEqual(result.stdout, "") + + def setUp(self): self.directory = tempfile.TemporaryDirectory() root = Path(self.directory.name) diff --git a/tools/trace_digest.py b/tools/trace_digest.py index 000276e4f9..dfc7a3d2d8 100644 --- a/tools/trace_digest.py +++ b/tools/trace_digest.py @@ -5,6 +5,7 @@ import gzip import html import json import sys +import zlib from pathlib import Path DESCRIPTION = """Digest of an APS5_TRACE_FRAMES / APS5_TRACE_SECONDS capture (aps5-trace-*.json.gz) for an issue or a @@ -340,4 +341,8 @@ def main(arguments=None): if __name__ == "__main__": if hasattr(sys.stdout, "reconfigure"): sys.stdout.reconfigure(encoding="utf-8", errors="replace") - sys.exit(main()) + try: + sys.exit(main()) + except (EOFError, OSError, ValueError, zlib.error) as error: + print(f"FAIL: {error}", file=sys.stderr) + sys.exit(2) From 32da8d8584703601744d7a82432b968cf5f7db73 Mon Sep 17 00:00:00 2001 From: Leandro Soares Date: Sat, 17 Oct 2026 12:01:55 -0300 Subject: [PATCH 0781/1000] fix(tools): match guest module aliases when auditing game imports --- tools/import_audit.py | 6 ++++-- tools/tests/test_import_audit.py | 13 +++++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/tools/import_audit.py b/tools/import_audit.py index ce551a47a7..704b1ec45a 100644 --- a/tools/import_audit.py +++ b/tools/import_audit.py @@ -709,8 +709,10 @@ def main(argv=None): exports, libraries = built_libraries(args.libs) host_dependencies = built_host_dependencies(args.libs) module_imports, module_exports = read_modules(args.modules) - records, missing = audit(imports, exports, source_stubs(args.source), libraries, set(module_imports), module_imports, module_exports, needed_libraries=needed) - missing_hosts = audit_host_dependencies(host_dependencies, libraries, set(module_imports)) + modules = set(module_imports) + modules |= {name.removesuffix(".guest.prx") for name in modules if name.endswith(".guest.prx")} + records, missing = audit(imports, exports, source_stubs(args.source), libraries, modules, module_imports, module_exports, needed_libraries=needed) + missing_hosts = audit_host_dependencies(host_dependencies, libraries, modules) if args.names: attach_names(records, load_names(args.names)) summary = summarize(records, sum(record["references"] for record in records), missing, missing_hosts) diff --git a/tools/tests/test_import_audit.py b/tools/tests/test_import_audit.py index 9fd1ec2cac..b8557abef7 100644 --- a/tools/tests/test_import_audit.py +++ b/tools/tests/test_import_audit.py @@ -487,6 +487,19 @@ class ImportAuditTests(unittest.TestCase): self.assertEqual(records[("AAAAAAAAAAA", "provider.data")]["providers"], ["libSceA.prx"]) self.assertEqual(records[("BBBBBBBBBBB", "consumer.prx")]["providers"], ["provider.data"]) + def test_converted_module_matches_imports_of_its_base_name(self): + work = self.work + (work.libs / "libc.prx").write_bytes(exporting()) + (work.modules / "libSceFace.prx.guest.prx").write_bytes(exporting("AAAAAAAAAAA")) + registry, result = work.root / "game.json", work.root / "audit.json" + write_registry(registry, [("AAAAAAAAAAA", "libSceFace.prx")]) + code, _, err = work.run(registry, "--json", str(result)) + self.assertEqual((code, err), (0, "")) + data = json.loads(result.read_text()) + self.assertEqual(data["missing_libraries"], {}) + record = next(record for record in data["imports"] if record["library"] == "libSceFace.prx") + self.assertEqual(record["class"], "module") + def test_bad_named_modules_and_malformed_elf_are_not_ignored(self): work = self.work (work.libs / "libc.prx").write_bytes(exporting()) From 4f639d5f1c4e012d8115f6a75a7c75d9cdf1ba27 Mon Sep 17 00:00:00 2001 From: Ondra97 <47064087+Ondra97@users.noreply.github.com> Date: Sat, 17 Oct 2026 17:02:32 +0200 Subject: [PATCH 0782/1000] fix(agc): count pipeline jobs after queue admission --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 18 +++ .../Graphics/src/PipelineLibrary.cpp | 3 +- .../tests/PipelineQueueFailure.cpp | 133 ++++++++++++++++++ 3 files changed, 153 insertions(+), 1 deletion(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/PipelineQueueFailure.cpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index c6950a121b..28cd42fdf5 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -776,6 +776,24 @@ if(BUILD_TESTING) endif() endif() +add_test_executable(agc_pipeline_queue_failure_tests EXCLUDE_FROM_ALL tests/PipelineQueueFailure.cpp + Graphics/src/PipelineLibrary.cpp tests/AllocationFailure.cpp) +target_include_directories(agc_pipeline_queue_failure_tests PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/core/shader/recompiler ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR}) +target_link_libraries(agc_pipeline_queue_failure_tests PRIVATE libSceAgcDriver libc) +set_target_properties(agc_pipeline_queue_failure_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_pipeline_queue_failure_tests) +if(BUILD_TESTING) + set_target_properties(agc_pipeline_queue_failure_tests PROPERTIES EXCLUDE_FROM_ALL OFF) + foreach(mode wait clear stop) + add_test(NAME agc_pipeline_queue_failure_${mode} COMMAND agc_pipeline_queue_failure_tests ${mode}) + set_tests_properties(agc_pipeline_queue_failure_${mode} PROPERTIES TIMEOUT 15) + if(WIN32) + set_tests_properties(agc_pipeline_queue_failure_${mode} PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + endforeach() +endif() + add_test_executable(agc_pass_hazards_tests EXCLUDE_FROM_ALL tests/PassHazards.cpp Graphics/src/PassHazards.cpp) target_include_directories(agc_pass_hazards_tests PRIVATE ${LIBS_INCLUDE_DIR} ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR}) target_link_libraries(agc_pass_hazards_tests PRIVATE libc) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp index 955f2b4d28..7670cde694 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp @@ -87,8 +87,9 @@ void enqueue(LibraryStore& store, VkDevice device, std::function job) { store.worker = std::thread(runJobs, std::ref(store)); runningWorkers.fetch_add(1, std::memory_order_acq_rel); } - ++store.pending[device]; + auto& pending = store.pending[device]; store.jobs.emplace_back(device, std::move(job)); + ++pending; store.work.notify_one(); } diff --git a/core/libs/prx/libSceAgcDriver/tests/PipelineQueueFailure.cpp b/core/libs/prx/libSceAgcDriver/tests/PipelineQueueFailure.cpp new file mode 100644 index 0000000000..4b819211da --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/PipelineQueueFailure.cpp @@ -0,0 +1,133 @@ +#include "prx/libSceAgcDriver/Graphics/include/PipelineLibrary.hpp" +#include "AllocationFailure.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace AgcDriver::Graphics; +using AgcDriver::Tests::SetAllocationFailureCountdown; +using namespace std::chrono_literals; + +namespace { + +void Check(bool condition, const char* message) { + if (condition) return; + std::fprintf(stderr, "FAIL: %s\n", message); + std::fflush(stderr); + std::_Exit(1); +} + +void Schedule(VkDevice device, std::function work) { + OptimizedPipeline pipeline; + pipeline.device = device; + pipeline.relink = std::move(work); + pipeline.deferred = true; + pipeline.Schedule(); +} + +} + +int main(int argc, char** argv) { + Check(argc == 2, "expected wait, clear or stop"); + const std::string_view mode(argv[1]); + Check(mode == "wait" || mode == "clear" || mode == "stop", "unknown drain mode"); + const auto device = reinterpret_cast(std::uintptr_t{1}); + const auto otherDevice = reinterpret_cast(std::uintptr_t{2}); + std::atomic completed{0}; + Schedule(device, [&] { ++completed; }); + WaitForOptimizedPipelines(device); + Check(completed == 1, "the initial job did not run"); + + bool allocationFailed = false; + OptimizedPipeline fresh; + fresh.device = otherDevice; + fresh.relink = [&] { ++completed; }; + fresh.deferred = true; + SetAllocationFailureCountdown(0); + try { fresh.Schedule(); } catch (const std::bad_alloc&) { allocationFailed = true; } + SetAllocationFailureCountdown(-1); + Check(allocationFailed, "the new device allocation did not fail"); + WaitForOptimizedPipelines(otherDevice); + Check(completed == 1, "a rejected job ran"); + + std::mutex mutex; + std::condition_variable changed; + bool entered = false; + bool released = false; + Schedule(device, [&] { + std::unique_lock lock(mutex); + entered = true; + changed.notify_all(); + changed.wait(lock, [&] { return released; }); + }); + { + std::unique_lock lock(mutex); + Check(changed.wait_for(lock, 3s, [&] { return entered; }), "the worker did not start"); + } + + allocationFailed = false; + unsigned accepted = 0; + for (unsigned attempt = 0; attempt < 512 && !allocationFailed; ++attempt) { + OptimizedPipeline pipeline; + pipeline.device = device; + pipeline.relink = [&] { ++completed; }; + pipeline.deferred = true; + SetAllocationFailureCountdown(0); + try { + pipeline.Schedule(); + ++accepted; + } catch (const std::bad_alloc&) { + allocationFailed = true; + } + SetAllocationFailureCountdown(-1); + } + Check(allocationFailed, "the queued jobs never needed another allocation"); + Check(completed == 1, "queued jobs ran while the worker was held"); + { + std::lock_guard lock(mutex); + released = true; + } + changed.notify_all(); + + bool drained = false; + std::thread drain([&] { + if (mode == "stop") StopPipelineLibraryWorker(); + if (mode == "clear") ClearPipelineLibraries(device); + else WaitForOptimizedPipelines(device); + { + std::lock_guard lock(mutex); + drained = true; + } + changed.notify_all(); + }); + { + std::unique_lock lock(mutex); + Check(changed.wait_for(lock, 3s, [&] { return drained; }), "a failed queue allocation left an unfinished job"); + } + drain.join(); + if (mode != "stop") { + Check(completed == accepted + 1, "an accepted job was lost or ran twice"); + Schedule(device, [&] { ++completed; }); + Schedule(otherDevice, [&] { ++completed; }); + WaitForOptimizedPipelines(device); + WaitForOptimizedPipelines(otherDevice); + Check(completed == accepted + 3, "the queue did not recover after allocation failure"); + StopPipelineLibraryWorker(); + } + WaitForOptimizedPipelines(device); + ClearPipelineLibraries(device); + ClearPipelineLibraries(otherDevice); + Check(PipelineLibraryWorkers() == 0, "the pipeline worker did not stop"); + std::printf("%.*s: rejected new-device and queue-growth allocations; %u jobs accepted before rejection, queue drained\n", + static_cast(mode.size()), mode.data(), accepted); + return 0; +} From 50330afd66b47762e793d43d8407950631677ca3 Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Sat, 17 Oct 2026 15:02:33 +0000 Subject: [PATCH 0783/1000] perf(tools): speed up pull request overlap checks --- tools/pr_overlap.py | 16 ++++++++++++---- tools/tests/test_pr_overlap_own_branch.py | 13 +++++++++++++ 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/tools/pr_overlap.py b/tools/pr_overlap.py index c0d361a54b..a7e2f394fa 100644 --- a/tools/pr_overlap.py +++ b/tools/pr_overlap.py @@ -3,6 +3,7 @@ import json import os import re import subprocess +from concurrent.futures import ThreadPoolExecutor from itertools import combinations from pathlib import Path from urllib.parse import quote, urlencode @@ -21,8 +22,8 @@ def git(*args): def own(prs, sha): - return {n: pr for n, pr in prs.items() - if pr["sha"] != sha and subprocess.run(["git", "merge-base", "--is-ancestor", pr["sha"], sha], capture_output=True).returncode != 0} + ancestors = set(git("rev-list", sha).splitlines()) + return {n: pr for n, pr in prs.items() if pr["sha"] not in ancestors} def gh(*args): @@ -124,6 +125,11 @@ def scan(pr): pr["exports"] = exports(diff) +def scan_all(prs, jobs): + with ThreadPoolExecutor(max_workers=jobs) as executor: + list(executor.map(scan, prs.values())) + + def hunks(pr, path): diff = git("diff", "-U0", "--no-color", "refs/pr/base", pr["tree"], "--", path) found = [] @@ -236,9 +242,12 @@ if __name__ == "__main__": parser.add_argument("--out", type=Path, help="write .md for every open pull request (empty when clean)") parser.add_argument("--branch", help="check a local branch against the open pull requests instead") parser.add_argument("--repo", help="owner/name; defaults to the upstream of the current checkout") + parser.add_argument("--jobs", type=int, default=4, help="number of pull request comparisons to run at once") args = parser.parse_args() if bool(args.out) == bool(args.branch): parser.error("pass exactly one of --out or --branch") + if args.jobs < 1: + parser.error("--jobs must be at least 1") REPO = args.repo or upstream() prs = open_prs(args.base) fetch(args.base, prs) @@ -246,8 +255,7 @@ if __name__ == "__main__": sha = git("rev-parse", args.branch).strip() prs = own(prs, sha) prs[0] = {"ref": sha, "author": None, "depends": set()} - for pr in prs.values(): - scan(pr) + scan_all(prs, args.jobs) rows = {n: {} for n in prs} pairs = [(0, n) for n in prs if n] if args.branch else combinations(prs, 2) for a, b in pairs: diff --git a/tools/tests/test_pr_overlap_own_branch.py b/tools/tests/test_pr_overlap_own_branch.py index c27c8624e9..802d57c74b 100644 --- a/tools/tests/test_pr_overlap_own_branch.py +++ b/tools/tests/test_pr_overlap_own_branch.py @@ -3,6 +3,7 @@ import subprocess import sys import tempfile import unittest +from unittest.mock import patch from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parents[1])) @@ -33,6 +34,18 @@ class OwnBranchTests(unittest.TestCase): finally: os.chdir(old) + def test_scans_all_pull_requests_with_a_bounded_worker_pool(self): + prs = {number: {"sha": str(number)} for number in range(1, 9)} + scanned = [] + + def scan(pr): + scanned.append(pr["sha"]) + + with patch.object(pr_overlap, "scan", side_effect=scan): + pr_overlap.scan_all(prs, 3) + + self.assertCountEqual(scanned, [str(number) for number in prs]) + if __name__ == "__main__": unittest.main() From bf0c51e4c19e6cea182bfeea7e21a4e50b895f16 Mon Sep 17 00:00:00 2001 From: XairRrR Date: Thu, 15 Oct 2026 16:59:32 +0200 Subject: [PATCH 0784/1000] perf(recompiler): scan image-table key buffers in bulk Narrowing a table column read its key buffer one key at a time, with a page lookup and a single-word read per key; PPSA05684 scans about 20,000 keys per table snapshot. The scan now checks each page once and reads the keys with the bulk reader, and maps a key to its record without dividing when the multiplication cannot wrap. Gameplay, image-table time per snapshot: about 6.5 ms before, 5 ms after. AI-assisted: yes --- .../libSceAgcDriver/tests/ShaderMemory.cpp | 38 +++++++++++++++++++ .../Optimization/src/ResourceMaterializer.cpp | 36 +++++++++++++----- 2 files changed, 64 insertions(+), 10 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 51dee75d38..e8d81d8fff 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -1240,6 +1240,43 @@ void verifyImageTableBulkReads() { require(cutByRun->snapshot.tables == cutByWord->snapshot.tables && cutWordReads.wordReads == 12u && cutRunReads.wordReads == 0u && cutRunReads.bulkWords == 12u, "image table bulk reads: a record cut by an unmapped page read other words in runs than word by word"); } +void verifyImageTableBulkKeyScan() { + using namespace ShaderRecompiler; + namespace Abi = ImageTableAbi; + static TableTexture textures[2]; + alignas(4096) static TableGuest guest; + std::fill(guest.heap.begin(), guest.heap.end(), std::array{}); + guest.heap[0] = TableT(textures[0].bytes.data(), TableFormat8888UNorm, TableType2D); + guest.heap[1] = TableT(textures[1].bytes.data(), TableFormat32UInt, TableType2D); + guest.heap[3] = guest.heap[0]; + guest.materials = {{{0u, 1u, 0u, 0u}, {0u, 0x08000003u, 0u, 0u}, {0u, 1u, 0u, 0u}}}; + FillTableSrt(guest, guest.heap.data(), 32u, 4u); + const auto userData = TableUserData(guest); + const auto request = TableRequest(TableMaterialCode, userData); + const auto capture = [&](CountingMemory& memory, bool bulk) { + SrtRuntime runtime; + runtime.userData = request.context.userData; + runtime.shaderBase = request.shader.codeAddress; + runtime.userContext = &memory; + runtime.readMemory = &CountingRead; + runtime.readSpecializationMemory = &CountingRead; + runtime.accessible = &CountingAccessible; + if (bulk) runtime.readWords = &CountingReadWords; + return CaptureResources(request, runtime); + }; + const auto begin = reinterpret_cast(guest.materials.data()); + CountingMemory byWordReads{begin, begin + sizeof(guest.materials)}; + CountingMemory byRunReads = byWordReads; + const auto byWord = capture(byWordReads, false); + const auto byRun = capture(byRunReads, true); + require(byWordReads.wordReads == guest.materials.size(), "image table bulk key scan: without a bulk reader the keys were not read one word each"); + require(byRunReads.wordReads == 0u, "image table bulk key scan: the keys were read word by word although a bulk reader exists"); + require(byRun->snapshot.tables == byWord->snapshot.tables, "image table bulk key scan: scanning the keys in bulk changed the snapshot"); + const auto& snapshot = byRun->snapshot.tables.tables.at(0); + const auto outside = Abi::PoisonCode(static_cast(Abi::PoisonReason::OutsideDomain)); + require(snapshot.keys == 4u && snapshot.codes[0] == snapshot.codes[3] && snapshot.codes[0] != snapshot.codes[1] && snapshot.codes[2] == outside, "image table bulk key scan: the narrowed codes are wrong"); +} + void verifyImageTables() { using namespace ShaderRecompiler; namespace Abi = ImageTableAbi; @@ -5212,6 +5249,7 @@ int main(int argc, char** argv) { verifyImageTableKeys(); verifyImageTables(); verifyImageTableBulkReads(); + verifyImageTableBulkKeyScan(); verifyImageTableSrtKey(); verifyImageTableMaskedOffset(); verifyImageTableKeyUnion(); diff --git a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp index ff8686cb2f..878a44dfce 100644 --- a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp @@ -505,27 +505,42 @@ private: skip(); return; } - for (std::uint64_t index = 0; index < positions; index++) { - const auto address = base + first + index * domain.stride; - if (!PageAccessible(address, base, base + size)) { - skip(); - return; + if (runtime.readWords != nullptr && positions != 0u && domain.stride % 4u == 0u && domain.stride <= 64u) { + const auto words = static_cast(((positions - 1u) * domain.stride) / 4u + 1u); + for (std::uint64_t page = (base + first) & ~(PageBytes - 1u); page < base + first + words * 4u; page += PageBytes) { + if (!PageAccessible(std::max(page, base + first), base, base + size)) { + skip(); + return; + } + } + if (keyWords.size() < words) keyWords.resize(words); + if (!runtime.readWords(runtime.userContext, base + first, keyWords.data(), words)) throw std::runtime_error("image table: failed to read guest memory at 0x" + hexText(base + first)); + const auto step = static_cast(domain.stride / 4u); + values.reserve(values.size() + positions); + for (std::size_t index = 0; index < words; index += step) values.push_back(keyWords[index]); + } else { + for (std::uint64_t index = 0; index < positions; index++) { + const auto address = base + first + index * domain.stride; + if (!PageAccessible(address, base, base + size)) { + skip(); + return; + } + values.push_back(Read(address)); } - values.push_back(Read(address)); } if (positions != 0u) scanned.emplace_back(base, size); } records.states.assign(records.keys, RecordState::OutsideDomain); records.outside = false; + const std::uint32_t unwrapped = std::numeric_limits::max() / column.stride; for (const auto value : values) { const std::uint32_t relative = value * column.stride; - const bool aligned = relative % column.stride == 0u; - const auto record = relative / column.stride; - if (aligned && record < records.keys) { + const auto record = value <= unwrapped ? value : relative % column.stride == 0u ? relative / column.stride : records.keys; + if (record < records.keys) { records.states[record] = RecordState::Read; continue; } - if (column.address || ImageTableAbi::ScalarBufferDword(relative + column.addend, column.offset, records.size).has_value()) records.outside = true; + if (!records.outside && (column.address || ImageTableAbi::ScalarBufferDword(relative + column.addend, column.offset, records.size).has_value())) records.outside = true; } records.narrowed = true; for (const auto& [base, size] : scanned) tables.ranges.emplace_back(base, size); @@ -774,6 +789,7 @@ private: std::unordered_map wordsIndex; std::unordered_map pages; std::unordered_map wholePages; + std::vector keyWords; }; void materializeSnapshot(const IrResourcePlan& plan, const SrtRuntime& runtime, SrtWalker& walker, ResourceSnapshot& snapshot, std::vector& activeSources) { From 0c6c732176343e55dfd0fbf6830178c10ec2a643 Mon Sep 17 00:00:00 2001 From: Flamefusion Date: Sat, 17 Oct 2026 16:21:17 +0530 Subject: [PATCH 0785/1000] fix(agc): patch relocated user words into stage data hits A draw served through a relocated entry can also be a stage data hit on the same stage. lookupDraw builds the stage data result first: patchDataWords copies the variant's compiled result and refreshes the flattened SRT data words. Only afterwards does draw apply the relocation's shaderDataPatches, and only to matched[i]->compiled. programResults[i] prefers stageData[i].compiled, so the stage was drawn with the push constants and ShaderData user-word slots of the donor draw. Pointers the shader reads at run time, which relocation had moved, kept the donor's old values. After applying a variant's shaderDataPatches, the stage data result of the same stage is now rebuilt from the patched variant with the words the lookup refreshed, so it carries both the new user words and the refreshed data words. Variants without pending patches are skipped as before. Found by reading the hit path: patchDataWords copies *variant.compiled (VariantValidation.cpp) before applyShaderDataPatches replaces it (DrawRelocate.cpp), and Draw.cpp selects the stage data copy when one exists. Builds on Windows (MinGW GCC 15.2). A regression test that drives a relocated stage data hit is still to be added. --- .../prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp index 5dea7922ea..5af1a8aaf7 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp @@ -272,8 +272,10 @@ DrawVerdict Driver::draw(QueueState& queue, std::span packe bool verifyHit = false; const bool stageEntries = useDrawEntries && !(graphics.stages.tessellation && !drawParameters.indirect && drawParameters.instanceCount > 1u); lookupDraw(submission, localDevice, graphics, pixel, programs, roles, vertexInfos, stageEntries, registerKey, profile, drawKey, entry, matched, matchedRegions, drawHit, verifyHit, phaseTiming, phaseMs, dataEntry, dataCandidates, stageData); - for (auto& variant : matched) { - if (variant != nullptr) applyShaderDataPatches(*variant); + for (std::size_t i = 0; i < matched.size(); ++i) { + if (matched[i] == nullptr || matched[i]->shaderDataPatches.empty()) continue; + applyShaderDataPatches(*matched[i]); + if (stageData[i].compiled != nullptr) stageData[i].compiled = patchDataWords(*matched[i], stageData[i].words); } const auto relocatedDecode = decode; if (!userPatches.empty()) { From d8be71258825b769c538ee687e39290f521bec5d Mon Sep 17 00:00:00 2001 From: Flamefusion Date: Sat, 17 Oct 2026 20:45:21 +0530 Subject: [PATCH 0786/1000] test(agc): a relocated stage data hit draws with the moved user words Adds MovedForwardedWordReachesStageDataHit to the draw cache relocation test (agc_driver_draw_cache_relocation). The fragment shader loads a pointer from the table its user words s[0:1] point to, loads a float through that pointer and exports it as red and alpha. It also exports bit 4 of user word s0 as green and blue, so the compiled stage forwards s0 into its shader data while the table read keeps s0 relocatable. The float is read through the loaded pointer, not through a user word, so it is a stage data position and not part of the relocation alias. Six draws through tables at 32-byte aligned addresses (bit 4 clear) that hold the same pointer teach the relocation and must reuse the cached draw at least once; their pixels must be 255,0,0,255. The float is then changed in place and one draw goes through a table at a 16-byte offset (bit 4 set). It must be one lookup, one hit and one stage data hit: a relocated entry whose fragment stage validates as EqualData. Every written pixel must be 64,255,255,64: the refreshed data word and the moved user word. Before 800ee777b the stage data copy kept the donor's shader data, and the test fails with: AGC graphics: with graphics pipeline libraries: a relocated stage data hit did not draw with the moved user word: pixel 0 is 64,0,0,64, expected 64,255,255,64 DrawFrom takes the fragment shader as a parameter. The test binary runs in 1.5 s. Tested on Windows 11 (MinGW GCC 15.2) on an NVIDIA GeForce RTX 3050 Laptop GPU through Vulkan; agc_driver_draw_cache_reuse and its _off, _verify and _inline variants still pass. --- .../tests/execution/DrawCacheRelocation.cpp | 63 ++++++++++++++++++- 1 file changed, 60 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DrawCacheRelocation.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DrawCacheRelocation.cpp index 358d6a2c99..b5650189c7 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DrawCacheRelocation.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DrawCacheRelocation.cpp @@ -1,4 +1,5 @@ #include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" #include "VulkanTestDevice.hpp" #include @@ -22,7 +23,18 @@ using AgcDriver::DriverDetail::DrawMiss; alignas(256) constexpr std::array VertexCode{0x7e000208u, 0x7e020280u, 0x7e040280u, 0x7e0602f2u, 0xf80008cfu, 0x03020100u, 0xbf810000u, 0xbf810000u}; alignas(256) constexpr std::array SecondVertexCode = VertexCode; alignas(256) constexpr std::array PixelCode{0xf4000100u, 0xfa000000u, 0x7e000204u, 0x7e020280u, 0xf800180fu, 0x00010100u, 0xbf810000u, 0xbf810000u}; +alignas(256) constexpr std::array TriangleVertexCode{ + 0x7e000d05u, 0x080200f4u, 0x10020300u, 0x100202f4u, 0x060202f3u, 0x060400f3u, 0x10040500u, 0x060404f3u, + 0x7e060280u, 0x7e0802f2u, 0xf80008cfu, 0x04030201u, 0xbf810000u, +}; +alignas(256) constexpr std::array ForwardedPixelCode{ + 0xf4040080u, 0xfa000000u, 0xbf8cc07fu, 0xf4000101u, 0xfa000000u, 0xbf8cc07fu, 0x7e000204u, 0xd5480001u, + 0x02050800u, 0x7e020d01u, 0xf800180fu, 0x00010100u, 0xbf810000u, 0xbf810000u, 0xbf810000u, 0xbf810000u, +}; alignas(16) std::array Other{0.5f, 0.0f, 0.0f, 0.0f}; +alignas(16) std::array ForwardedData{}; +alignas(32) std::array, 6> AlignedTables{}; +alignas(32) std::array OffsetTable{}; alignas(256) std::array ColorMemory{}; struct Header { @@ -33,6 +45,8 @@ struct Header { alignas(8) Header VertexHeader{}; alignas(8) Header PixelHeader{}; alignas(8) Header SecondVertexHeader{}; +alignas(8) Header TriangleVertexHeader{}; +alignas(8) Header ForwardedPixelHeader{}; const Shader* Registered(Header& header, const std::uint32_t* code, std::size_t bytes, std::uint8_t type) { header.shader.file_header = 0x34333231u; @@ -51,10 +65,10 @@ void Append(std::vector& commands, std::uint32_t opcode, std::ini commands.insert(commands.end(), payload); } -std::vector DrawFrom(std::initializer_list sources, const std::uint32_t* vertexCode = VertexCode.data()) { +std::vector DrawFrom(std::initializer_list sources, const std::uint32_t* vertexCode = VertexCode.data(), const std::uint32_t* pixelCode = PixelCode.data()) { const auto color = reinterpret_cast(ColorMemory.data()); const auto vertex = reinterpret_cast(vertexCode); - const auto pixel = reinterpret_cast(PixelCode.data()); + const auto pixel = reinterpret_cast(pixelCode); const std::vector> context{ {0x2d5, 0x2000}, {0x1b6, 0}, {0x207, 0}, {0x200, 0}, {0x203, 0x800}, @@ -97,7 +111,7 @@ void MovedDataHitsTheCache(bool libraries) { Registered(VertexHeader, VertexCode.data(), sizeof(VertexCode), 2); Registered(PixelHeader, PixelCode.data(), sizeof(PixelCode), 1); const auto mode = std::string(libraries ? "with" : "without") + " graphics pipeline libraries"; - const auto draw = [&](std::initializer_list sources) { + const auto draw = [&](std::initializer_list sources) { auto commands = DrawFrom(sources); Packet packet{commands.data(), static_cast(commands.size()), 0, {}}; Require(sceAgcDriverSubmitDcb(&packet) == 0, "the draw submission failed"); @@ -145,6 +159,48 @@ void UnrelocatedAliasesStayOut(bool libraries) { Require(counters.hits - start.hits == twice.size(), mode + ": the exact key of a repeated draw lost its hits next to draws whose alias never relocates (" + std::to_string(counters.hits - start.hits) + " hits in " + std::to_string(twice.size()) + " draws)"); } +void MovedForwardedWordReachesStageDataHit(bool libraries) { + Registered(TriangleVertexHeader, TriangleVertexCode.data(), sizeof(TriangleVertexCode), 2); + Registered(ForwardedPixelHeader, ForwardedPixelCode.data(), sizeof(ForwardedPixelCode), 1); + const auto mode = std::string(libraries ? "with" : "without") + " graphics pipeline libraries"; + constexpr std::byte Kept{0x40}; + const auto draw = [&](const void* table) { + ColorMemory.fill(Kept); + auto commands = DrawFrom({table}, TriangleVertexCode.data(), ForwardedPixelCode.data()); + Packet packet{commands.data(), static_cast(commands.size()), 0, {}}; + Require(sceAgcDriverSubmitDcb(&packet) == 0, "the draw submission failed"); + AgcDriverWaitIdle_nid_postfix(); + AgcDriver::GuestMemory::FlushGpuWrites(reinterpret_cast(ColorMemory.data()), ColorMemory.size()); + return AgcDriver::DriverDetail::Driver::Get().DrawCacheCounters(); + }; + const auto pixels = [&](std::array expected) { + std::size_t drawn = 0; + for (std::size_t at = 0; at < ColorMemory.size(); at += 4) { + const std::array pixel{static_cast(ColorMemory[at]), static_cast(ColorMemory[at + 1]), static_cast(ColorMemory[at + 2]), static_cast(ColorMemory[at + 3])}; + if (pixel == std::array{0x40, 0x40, 0x40, 0x40}) continue; + if (pixel != expected) return "pixel " + std::to_string(at / 4) + " is " + std::to_string(pixel[0]) + "," + std::to_string(pixel[1]) + "," + std::to_string(pixel[2]) + "," + std::to_string(pixel[3]) + ", expected " + std::to_string(expected[0]) + "," + std::to_string(expected[1]) + "," + std::to_string(expected[2]) + "," + std::to_string(expected[3]); + ++drawn; + } + return drawn == 0 ? std::string("the draw wrote no pixel") : std::string(); + }; + const auto data = static_cast(reinterpret_cast(ForwardedData.data())); + for (auto& table : AlignedTables) table[0] = data; + OffsetTable[2] = data; + ForwardedData = {1.0f, 0.0f, 0.0f, 0.0f}; + const auto start = AgcDriver::DriverDetail::Driver::Get().DrawCacheCounters(); + AgcDriver::DriverDetail::DrawEntryCounters counters{}; + for (const auto& table : AlignedTables) counters = draw(table.data()); + Require(counters.hits != start.hits, mode + ": draws whose pointer table moved to identical contents never reused the cached draw (" + std::to_string(counters.lookups - start.lookups) + " lookups, " + std::to_string(counters.inserts - start.inserts) + " inserts)"); + const auto taught = pixels({255, 0, 0, 255}); + Require(taught.empty(), mode + ": a draw through a pointer table with bit 4 of its address clear: " + taught); + ForwardedData[0] = 0.25f; + const auto before = counters; + counters = draw(&OffsetTable[2]); + Require(counters.lookups == before.lookups + 1 && counters.hits == before.hits + 1 && counters.stageDataHits == before.stageDataHits + 1, mode + ": a draw whose pointer table moved and whose data changed in place was not a relocated stage data hit (" + std::to_string(counters.hits - before.hits) + " hits, " + std::to_string(counters.stageDataHits - before.stageDataHits) + " stage data hits)"); + const auto moved = pixels({64, 255, 255, 64}); + Require(moved.empty(), mode + ": a relocated stage data hit did not draw with the moved user word: " + moved); +} + } int main() { @@ -158,6 +214,7 @@ int main() { if (!libraries && std::getenv("APS5_NO_GPL") == nullptr) return VulkanTestSkipped; MovedDataHitsTheCache(libraries); UnrelocatedAliasesStayOut(libraries); + MovedForwardedWordReachesStageDataHit(libraries); AgcDriverShutdown_nid_postfix(); std::cout << "A draw whose data moved reused the cached draw " << (libraries ? "with" : "without") << " graphics pipeline libraries\n"; return 0; From fd77d6939404499bd15468dafb4e0578b27ea4f4 Mon Sep 17 00:00:00 2001 From: XairRrR Date: Sat, 17 Oct 2026 15:56:10 +0200 Subject: [PATCH 0787/1000] perf(recompiler): keep image-table indexes in per-thread scratch Each image-table snapshot and resolution built node-based indexes and dropped them at the end of the call: an unordered_map of distinct descriptor words in TableSnapshotter, and in ResolveImageTables a map of poison records, a set of expected poison codes, a map of bound elements and an unordered_map of records per table. A table of N distinct entries cost several allocations per entry on every dispatch. The words, element and poison indexes are now open-addressing tables over the vectors they index, and the per-table records a dense array by code. Their slots live in per-thread scratch stamped per use, so a call reuses the previous call's storage without clearing it; scratch above 2 MiB is released after the call. Expected poison codes are a bit vector. AI-assisted: yes --- .../libSceAgcDriver/tests/ShaderMemory.cpp | 81 ++++++ .../Optimization/src/ResourceMaterializer.cpp | 231 +++++++++++++++--- 2 files changed, 278 insertions(+), 34 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 51dee75d38..ae800fd8d0 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -33,6 +33,7 @@ #include #include #include +#include #include #include #include @@ -54,6 +55,26 @@ namespace { +thread_local std::uint64_t allocationCount = 0; + +} + +void* operator new(std::size_t bytes) { + allocationCount++; + if (auto* allocation = std::malloc(bytes == 0 ? 1 : bytes)) return allocation; + throw std::bad_alloc(); +} + +void operator delete(void* allocation) noexcept { + std::free(allocation); +} + +void operator delete(void* allocation, std::size_t) noexcept { + std::free(allocation); +} + +namespace { + ShaderRecompiler::SpirvTarget BufferTarget() { static constexpr std::array capabilities{spv::CapabilityShader, spv::CapabilityImageGatherExtended, spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess, spv::CapabilityClipDistance}; static constexpr std::array extensions{"SPV_KHR_physical_storage_buffer", "SPV_KHR_8bit_storage"}; @@ -1240,6 +1261,65 @@ void verifyImageTableBulkReads() { require(cutByRun->snapshot.tables == cutByWord->snapshot.tables && cutWordReads.wordReads == 12u && cutRunReads.wordReads == 0u && cutRunReads.bulkWords == 12u, "image table bulk reads: a record cut by an unmapped page read other words in runs than word by word"); } +void verifyImageTableScratchReuse() { + using namespace ShaderRecompiler; + namespace Abi = ImageTableAbi; + constexpr std::uint32_t Entries = 3000; + static TableTexture textures[4]; + alignas(4096) static TableGuest guest; + alignas(4096) static std::array, Entries> heap{}; + const auto place = [&](std::uint32_t shift) { + for (std::uint32_t entry = 0; entry < Entries; entry++) heap[entry] = TableT(textures[entry % 4u].bytes.data(), TableFormat8888UNorm, TableType2D, 1u + (entry + shift) % Entries); + }; + FillTableSrt(guest, heap.data(), 32u, Entries); + const auto userData = TableUserData(guest); + const auto request = TableRequest(TableWholeCode, userData); + auto staticRequest = request; + const std::array absentResources{}; + staticRequest.context.userData = absentResources; + staticRequest.context.memory = {}; + auto staticProgram = PrepareResourceProgram(staticRequest); + ResourceMaterializer{}.ApplyStaticInterface(staticProgram); + const auto& info = staticProgram.Resources().info; + struct Resolved { + ImageTableSnapshot snapshot; + ResolvedImageTables tables; + std::uint64_t allocations = 0; + }; + const auto resolve = [&] { + CountingMemory memory; + SrtRuntime runtime; + runtime.userData = request.context.userData; + runtime.shaderBase = request.shader.codeAddress; + runtime.userContext = &memory; + runtime.readMemory = &CountingRead; + runtime.readSpecializationMemory = &CountingRead; + runtime.accessible = &CountingAccessible; + runtime.readWords = &CountingReadWords; + Resolved out; + const auto before = allocationCount; + const auto capture = CaptureResources(request, runtime); + auto tables = ResourceMaterializer::ResolveImageTables(info, capture->snapshot); + out.allocations = allocationCount - before; + out.snapshot = capture->snapshot.tables; + out.tables = std::move(tables); + return out; + }; + place(0u); + static_cast(resolve()); + place(7u); + const auto warm = resolve(); + const auto fresh = std::async(std::launch::async, resolve).get(); + require(warm.snapshot == fresh.snapshot && warm.tables.map == fresh.tables.map && warm.tables.faults == fresh.tables.faults && warm.tables.poison.size() == fresh.tables.poison.size(), "image table scratch: a snapshot resolved after another one on the same thread differs from one resolved on a new thread"); + require(warm.snapshot.words.size() == Entries && warm.tables.elements.size() == Entries && warm.tables.map[Abi::HeaderElementCount] == Entries, "image table scratch: the distinct records were not all indexed"); + const auto codes = TableCodes(warm.tables.map, 0); + for (std::uint32_t entry = 0; entry < Entries; entry++) { + const auto element = warm.tables.map.at(codes.at(entry) + Abi::ImageRecordElement); + require(warm.tables.elements.at(element).words == fresh.tables.elements.at(element).words && warm.tables.elements[element].words[2] == heap[entry][2], "image table scratch: a record resolved to another entry's element"); + } + require(warm.allocations < Entries / 4u, "image table scratch: snapshotting and resolving a table allocated per record"); +} + void verifyImageTables() { using namespace ShaderRecompiler; namespace Abi = ImageTableAbi; @@ -5212,6 +5292,7 @@ int main(int argc, char** argv) { verifyImageTableKeys(); verifyImageTables(); verifyImageTableBulkReads(); + verifyImageTableScratchReuse(); verifyImageTableSrtKey(); verifyImageTableMaskedOffset(); verifyImageTableKeyUnion(); diff --git a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp index ff8686cb2f..55e1cc6966 100644 --- a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp @@ -6,6 +6,7 @@ #include "RdnaDecoder/RdnaImageOpDecoder.hpp" #include "SpirvBackend/SpirvBufferFormat.hpp" #include "ImageTableAbi.hpp" +#include "prx/libc/include/HostThreadLocal.hpp" #include #include #include @@ -19,12 +20,10 @@ #include #include #include -#include #include #include #include #include -#include #include #include @@ -388,6 +387,153 @@ void traceTable(const IrResourcePlan& plan, std::uint32_t table, const TableColu std::fprintf(stderr, "[image-table] shader 0x%llx table %u (%s): base 0x%llx size 0x%llx stride 0x%x addend 0x%x offset 0x%x dwords %u, %u records, %u keys%s, %u structural poison\n", static_cast(plan.shaderHash), table, column.sampler ? "sampler" : "image", static_cast(trace.base), static_cast(trace.size), column.stride, column.addend, column.offset, column.dwordCount, trace.records, trace.keys, trace.narrowed ? " (narrowed)" : "", trace.poison); } +template +struct SlotScratch { + std::vector slots; + std::uint32_t stamp = 0; + bool leased = false; +}; + +template +SlotScratch& threadScratch() { + return HostThreadLocal, TTag>(); +} + +constexpr std::size_t KeptScratchBytes = std::size_t{2} << 20u; + +template +class SlotLease { +public: + explicit SlotLease(SlotScratch& shared) : scratch(shared.leased ? own : shared) { + scratch.leased = true; + Clear(); + } + + SlotLease(const SlotLease&) = delete; + SlotLease& operator=(const SlotLease&) = delete; + + ~SlotLease() { + if (scratch.slots.size() * sizeof(TSlot) > KeptScratchBytes) std::vector().swap(scratch.slots); + scratch.leased = false; + } + + void Clear() { + if (++scratch.stamp != 0u) return; + std::fill(scratch.slots.begin(), scratch.slots.end(), TSlot{}); + scratch.stamp = 1u; + } + + std::vector& Slots() { + return scratch.slots; + } + + const std::vector& Slots() const { + return scratch.slots; + } + + std::uint32_t Stamp() const { + return scratch.stamp; + } + +private: + SlotScratch own; + SlotScratch& scratch; +}; + +struct DenseSlot { + std::uint32_t stamp = 0; + std::uint32_t value = 0; +}; + +class DenseScratchMap { +public: + DenseScratchMap(SlotScratch& shared, std::size_t count) : lease(shared), keys(count) { + if (lease.Slots().size() < keys) lease.Slots().resize(keys); + } + + void Clear() { + lease.Clear(); + } + + std::optional Find(std::size_t key) const { + if (key >= keys) throw std::out_of_range("dense scratch map key is out of range"); + const auto& slot = lease.Slots()[key]; + return slot.stamp == lease.Stamp() ? std::optional(slot.value) : std::nullopt; + } + + void Set(std::size_t key, std::uint32_t value) { + if (key >= keys) throw std::out_of_range("dense scratch map key is out of range"); + lease.Slots()[key] = {lease.Stamp(), value}; + } + +private: + SlotLease lease; + std::size_t keys; +}; + +struct HashSlot { + std::uint64_t hash = 0; + std::uint32_t stamp = 0; + std::uint32_t value = 0; +}; + +std::uint64_t hashWords(std::span words, std::uint64_t seed) { + std::uint64_t hash = 0xcbf29ce484222325ull ^ seed; + for (const auto word : words) hash = (hash ^ word) * 0x100000001b3ull; + hash ^= hash >> 33u; + hash *= 0xff51afd7ed558ccdull; + return hash ^ (hash >> 33u); +} + +class HashScratchIndex { +public: + explicit HashScratchIndex(SlotScratch& shared) : lease(shared) {} + + template + std::optional Find(std::uint64_t hash, TEqual&& equal) const { + const auto& slots = lease.Slots(); + if (slots.empty()) return std::nullopt; + const auto mask = slots.size() - 1u; + for (auto at = static_cast(hash) & mask;; at = (at + 1u) & mask) { + const auto& slot = slots[at]; + if (slot.stamp != lease.Stamp()) return std::nullopt; + if (slot.hash == hash && equal(slot.value)) return slot.value; + } + } + + void Insert(std::uint64_t hash, std::uint32_t value) { + if ((entries + 1u) * 2u > lease.Slots().size()) grow(); + place(lease.Slots(), hash, value); + entries++; + } + +private: + static constexpr std::size_t MinimumSlots = 64; + + void place(std::vector& slots, std::uint64_t hash, std::uint32_t value) const { + const auto mask = slots.size() - 1u; + auto at = static_cast(hash) & mask; + while (slots[at].stamp == lease.Stamp()) at = (at + 1u) & mask; + slots[at] = {hash, lease.Stamp(), value}; + } + + void grow() { + std::vector grown(std::max(MinimumSlots, lease.Slots().size() * 2u)); + for (const auto& slot : lease.Slots()) { + if (slot.stamp == lease.Stamp()) place(grown, slot.hash, slot.value); + } + lease.Slots().swap(grown); + } + + SlotLease lease; + std::size_t entries = 0; +}; + +struct WordsIndexTag {}; +struct ElementIndexTag {}; +struct PoisonIndexTag {}; +struct TableRecordsTag {}; + class TableSnapshotter { public: TableSnapshotter(const IrResourcePlan& plan, const SrtRuntime& runtime, SrtWalker& walker, const std::vector& activeSources, ImageTableSnapshot& tables) : plan(plan), runtime(runtime), walker(walker), activeSources(activeSources), tables(tables) {} @@ -533,9 +679,12 @@ private: } std::uint32_t WordsIndex(const DescriptorValue& value) { - const auto [found, inserted] = wordsIndex.try_emplace({value.dwords, value.dwordCount, value.availableDwordMask}, static_cast(tables.words.size())); - if (inserted) tables.words.push_back(value); - return found->second; + const auto hash = hashWords(value.dwords, (static_cast(value.dwordCount) << 8u) | value.availableDwordMask); + if (const auto found = wordsIndex.Find(hash, [&](std::uint32_t index) { return tables.words[index] == value; })) return *found; + const auto index = static_cast(tables.words.size()); + tables.words.push_back(value); + wordsIndex.Insert(hash, index); + return index; } ColumnRecords& Column(std::uint32_t source) { @@ -763,15 +912,7 @@ private: const std::vector& activeSources; ImageTableSnapshot& tables; std::map columns; - struct WordsHash { - using Key = std::tuple, std::uint32_t, std::uint8_t>; - std::size_t operator()(const Key& key) const noexcept { - std::uint64_t hash = 0xcbf29ce484222325ull ^ std::get<1>(key) ^ std::get<2>(key); - for (const auto word : std::get<0>(key)) hash = (hash ^ word) * 0x100000001b3ull; - return static_cast(hash ^ (hash >> 32u)); - } - }; - std::unordered_map wordsIndex; + HashScratchIndex wordsIndex{threadScratch()}; std::unordered_map pages; std::unordered_map wholePages; }; @@ -1069,16 +1210,29 @@ ResolvedImageTables ResourceMaterializer::ResolveImageTables(const ShaderInfo& i out.map[Abi::HeaderVersion] = Abi::Version; out.poison.push_back({{}, 0u, NoTable, PoisonReason::OutsideSnapshot}); out.poison.push_back({{}, 0u, NoTable, PoisonReason::OutsideDomain}); - std::map, std::uint32_t>, std::uint32_t> poisonIndex; - std::set expected; + HashScratchIndex poisonIndex{threadScratch()}; + std::vector expected; + std::size_t expectedCount = 0; const auto poison = [&](std::uint32_t resource, PoisonReason reason, const DescriptorValue* words) { const auto value = words != nullptr ? *words : DescriptorValue{}; - const auto [found, inserted] = poisonIndex.try_emplace({resource, reason, value.dwords, value.dwordCount}, static_cast(out.poison.size())); - if (inserted) out.poison.push_back({value.dwords, value.dwordCount, resource, reason}); - expected.insert(found->second); - return Abi::PoisonCode(found->second); + const auto hash = hashWords(value.dwords, (static_cast(resource) << 40u) ^ (static_cast(reason) << 8u) ^ value.dwordCount); + auto index = poisonIndex.Find(hash, [&](std::uint32_t known) { + const auto& record = out.poison[known]; + return record.resource == resource && record.reason == reason && record.words == value.dwords && record.dwordCount == value.dwordCount; + }); + if (!index.has_value()) { + index = static_cast(out.poison.size()); + out.poison.push_back({value.dwords, value.dwordCount, resource, reason}); + poisonIndex.Insert(hash, *index); + } + if (*index >= expected.size()) expected.resize(out.poison.size(), false); + if (!expected[*index]) { + expected[*index] = true; + expectedCount++; + } + return Abi::PoisonCode(*index); }; - std::map, RdnaImageDimension, bool, DescriptorBindingKind>, std::uint32_t> elementIndex; + HashScratchIndex elementIndex{threadScratch()}; std::vector directSamplers(info.images.size(), 0u); std::vector reducingSampler(info.images.size(), false); for (const auto& pair : info.sampledPairs) { @@ -1111,15 +1265,22 @@ ResolvedImageTables ResourceMaterializer::ResolveImageTables(const ShaderInfo& i auto element = words.dwords; if (mode.conversionFormat != IrBufferFormat::Invalid || mode.depthBits) element[3] = (element[3] & ~0xfffu) | ShaderImageIdentitySwizzle; const auto kind = DescriptorBindingForImage(mode); - const auto [found, inserted] = elementIndex.try_emplace({element, mode.dimension, mode.depthCompare, kind}, static_cast(out.elements.size())); - if (inserted && out.elements.size() >= Abi::ImageElementBudget) { - elementIndex.erase(found); + const auto hash = hashWords(element, (static_cast(mode.dimension) << 16u) ^ (static_cast(mode.depthCompare) << 8u) ^ static_cast(kind)); + auto index = elementIndex.Find(hash, [&](std::uint32_t known) { + const auto& entry = out.elements[known]; + return entry.words == element && entry.dimension == mode.dimension && entry.depthCompare == mode.depthCompare && entry.kind == kind; + }); + if (!index.has_value() && out.elements.size() >= Abi::ImageElementBudget) { record[Abi::ImageRecordFlags] = poison(resource, PoisonReason::Budget, &words); } else { - if (inserted) out.elements.push_back({element, mode.dimension, mode.depthCompare, kind, 0u}); - out.elements[found->second].samplers |= directSamplers[resource]; + if (!index.has_value()) { + index = static_cast(out.elements.size()); + out.elements.push_back({element, mode.dimension, mode.depthCompare, kind, 0u}); + elementIndex.Insert(hash, *index); + } + out.elements[*index].samplers |= directSamplers[resource]; record[Abi::ImageRecordFlags] = decoded << Abi::ModeShift; - record[Abi::ImageRecordElement] = found->second; + record[Abi::ImageRecordElement] = *index; } } } @@ -1153,6 +1314,7 @@ ResolvedImageTables ResourceMaterializer::ResolveImageTables(const ShaderInfo& i return offset; }; std::vector outside(count, false); + DenseScratchMap records(threadScratch(), tables.words.size()); const auto fill = [&](std::uint32_t resource, std::uint32_t table, bool sampler) { const auto& column = tables.tables.at(table); const auto header = Abi::TableHeader(table); @@ -1171,7 +1333,7 @@ ResolvedImageTables ResourceMaterializer::ResolveImageTables(const ShaderInfo& i const auto mapStart = static_cast(out.map.size()); out.map[header + Abi::TableMapStart] = mapStart; out.map.resize(out.map.size() + column.keys, 0u); - std::unordered_map records; + records.Clear(); for (std::uint32_t key = 0; key < column.keys; key++) { const auto code = column.codes[key]; std::uint32_t mapped = Abi::NullCode; @@ -1179,11 +1341,12 @@ ResolvedImageTables ResourceMaterializer::ResolveImageTables(const ShaderInfo& i const auto reason = static_cast(code & ~Abi::PoisonFlag); mapped = reason == PoisonReason::OutsideDomain ? Abi::PoisonCode(Abi::OutsideDomainPoison) : poison(resource, reason, nullptr); } else if (out.map[header + Abi::TableFault] == 0u) { - const auto& words = tables.words.at(code); - if (sampler || (words.availableDwordMask & (1u << 3u)) == 0u || !nullImageDescriptor(info.images[resource], words)) { - const auto [found, inserted] = records.try_emplace(code, 0u); - if (inserted) found->second = sampler ? samplerRecord(resource, words) : imageRecord(resource, words); - mapped = found->second; + if (const auto known = records.Find(code)) { + mapped = *known; + } else { + const auto& words = tables.words[code]; + if (sampler || (words.availableDwordMask & (1u << 3u)) == 0u || !nullImageDescriptor(info.images[resource], words)) mapped = sampler ? samplerRecord(resource, words) : imageRecord(resource, words); + records.Set(code, mapped); } } out.map[mapStart + key] = mapped; @@ -1208,7 +1371,7 @@ ResolvedImageTables ResourceMaterializer::ResolveImageTables(const ShaderInfo& i out.map.resize(out.map.size() + out.elements.size(), 0u); out.map[Abi::HeaderSamplerFaults] = static_cast(out.map.size()); out.map.resize(out.map.size() + out.samplers.size() / Abi::SamplerEntryElements, 0u); - out.faults = static_cast(expected.size() + static_cast(std::count(outside.begin(), outside.end(), true))); + out.faults = static_cast(expectedCount + static_cast(std::count(outside.begin(), outside.end(), true))); return out; } From 10c1c24cea3bff6453247a5578c1f23b0c64d607 Mon Sep 17 00:00:00 2001 From: Destroyers212x <65466724+Destroyers212x@users.noreply.github.com> Date: Sat, 17 Oct 2026 11:22:28 -0400 Subject: [PATCH 0788/1000] docs(relinker): record Rosetta instruction support on an M1 with macOS 26.6.2 Co-Authored-By: Claude Opus 5.5 --- docs/dev/debt/relinker.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/relinker.md b/docs/dev/debt/relinker.md index acab2b4e54..1b585e36fa 100644 --- a/docs/dev/debt/relinker.md +++ b/docs/dev/debt/relinker.md @@ -17,7 +17,7 @@ - In executables and guest modules with a `PT_SCE_DYNLIBDATA` segment, the [code analysis](../../../core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp) of the Windows and macOS outputs follows code only from the end of `PT_INTERP` to the end of the PLT after the last function of the unwind table, the end the pipeline's scans above use; a guest module with PLT relocations and no PLT after its last unwind function fails as the executable does. Code after that PLT is not analyzed: a TLS access in it is not rewritten, an entry point or pointer into it is ignored, and a function symbol or init/fini array entry pointing into it fails the conversion. - A relocation against a dynamic symbol entry that is all zero, like the null symbol, takes the value 0 plus its addend, as the gABI defines for `STN_UNDEF` ([pipeline](../../../core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp)). The Linux output imports such an entry as an unnamed weak symbol, which the host loader resolves to 0; the Windows and macOS outputs write the value into the slot ([relocation builder](../../../core/relinker/elfpatcher/src/windows/WindowsRelocationBuilder.cpp)). What the console's loader does with such an entry is not measured. OliSe Player (PPSA01153) has seven of them, and its code calls through those slots only when they are not 0 ([#4437](https://github.com/boykopovar/AnyPS5/issues/4437)). - The Linux placement of the `--to-intel` stubs in the [ELF patcher](../../../core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp) is covered only by a synthetic test. -- `--to-rosetta` lowers what Rosetta lacks on top of `--to-intel` ([matcher](../../../core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp)). AVX2, FMA, BMI1/2, F16C and MOVBE were observed to execute on an M5 Max without being reported through CPUID; this is not a guarantee for other Rosetta hosts. ANDN and RDRAND fault on an M1 running macOS 15.3.1 and execute on an M5 running macOS 26.4, where the rest of BMI1 and BMI2, LZCNT, PREFETCHW, CLFLUSHOPT, AVX, AVX2, FMA, F16C and MOVBE execute as well although CPUID leaf 7 reads zero and leaves 1 and 0x80000001 report none of them, and where RDSEED, CLWB, RDPID, ADCX and ADOX raise an illegal instruction. ANDN is lowered, but RDRAND and other BMI instructions still depend on the host. RDSEED becomes RDRAND and CLWB a NOP. RDPID returns processor 0, because Rosetta's TSC_AUX is the constant 0xAAAAAAAB; the same value reaches RDTSCP's ECX, which is not converted. ADCX/ADOX fault under Rosetta and are not converted: compilers do not emit them, and code that checks CPUID sees no ADX. The conversions are covered by the converter tests. ANDN lowering is also executed by an x86-64 test build under Rosetta on the M1; the other conversions were originally checked on an M5 Max, outside this repository's CI. +- `--to-rosetta` lowers what Rosetta lacks on top of `--to-intel` ([matcher](../../../core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp)). AVX2, FMA, BMI1/2, F16C and MOVBE were observed to execute on an M5 Max without being reported through CPUID; this is not a guarantee for other Rosetta hosts. ANDN and RDRAND fault on an M1 running macOS 15.3.1 and execute on an M1 running macOS 26.6.2, so on the M1 the difference follows the macOS version; on that host BEXTR, BLSI, BLSMSK, BLSR, TZCNT, LZCNT, POPCNT, BZHI, MULX, PDEP, PEXT, RORX, SARX, SHLX, SHRX, MOVBE, FMA, AVX2 and F16C execute as well, its CPUID leaf 1 reports none of MOVBE, FMA, AVX or F16C, and RDSEED, RDPID, CLWB, ADCX and ADOX raise SIGILL. They also execute on an M5 running macOS 26.4, where the rest of BMI1 and BMI2, LZCNT, PREFETCHW, CLFLUSHOPT, AVX, AVX2, FMA, F16C and MOVBE execute as well although CPUID leaf 7 reads zero and leaves 1 and 0x80000001 report none of them, and where RDSEED, CLWB, RDPID, ADCX and ADOX raise an illegal instruction. ANDN is lowered, but RDRAND and other BMI instructions still depend on the host. RDSEED becomes RDRAND and CLWB a NOP. RDPID returns processor 0, because Rosetta's TSC_AUX is the constant 0xAAAAAAAB; the same value reaches RDTSCP's ECX, which is not converted. ADCX/ADOX fault under Rosetta and are not converted: compilers do not emit them, and code that checks CPUID sees no ADX. The conversions are covered by the converter tests. ANDN lowering is also executed by an x86-64 test build under Rosetta on the M1; the other conversions were originally checked on an M5 Max, outside this repository's CI. - `--to-intel` replaces the approximate legacy SSE and VEX.128 register forms of RSQRTPS/RCPPS and VRSQRTPS/VRCPPS with [correctly rounded](../../../core/relinker/codegen/src/x86/ReciprocalLowering.cpp) 1/sqrt(x) and 1/x, not with the AMD approximation: its tables are not public. Intel's approximation of 1/sqrt(1.0) is 0x3F7FF000, and two Newton-Raphson steps on it settle on 0.99999994 instead of 1.0; a renormalised identity quaternion then turns into a NaN axis in PPSA21564, which the console does not show. Refined results now reach the exact value; unrefined ones can still differ from the console in the low bits. Each site costs an out-of-line stub (two jumps, a spill below the red zone, SQRTPS/DIVPS). Legacy SSE forms preserve the destination's upper YMM bits; VEX forms clear them. The 256-bit, scalar and memory forms keep the native instruction, as does a site shorter than 5 bytes with no movable instruction after it for the jump (5 sites in PPSA21564, counted before a following RIP-relative instruction could move into the stub; not recounted). - `--trace-imports` ([entry stub](../../../core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp)) exists for Windows output only. Only imports bound through a PLT slot, or through a GOT slot whose symbol is a function, are traced; a function reached through any other relocation, or looked up at run time with `sceKernelDlsym`, is not, and neither are weak imports or the imports of modules loaded at run time From 6ce6850d12a8b55d80381095ca23736f9dd7116c Mon Sep 17 00:00:00 2001 From: simonbronk Date: Sat, 17 Oct 2026 17:23:32 +0200 Subject: [PATCH 0789/1000] fix(libSceNet): guard recvfrom sender outputs before receiving AI-assisted: OpenAI Codex. Windows native checks: 17 related CTests and 10 regression repetitions pass. No affected title verified. --- core/libs/prx/libSceNet/Export.cpp | 6 ++++ core/libs/tests/GuestSocketHostWrite.cpp | 45 ++++++++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index 8fd8578762..e5537bf33d 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -987,6 +987,8 @@ int64_t APS5_VABI sceNetRecvfrom(int s, void* buf, size_t len, int flags, void* if (it == g_socks.end()) return fail(NET_EBADF); socket = it->second; } + const GuestArena::HostWrite lengthOutput(from == nullptr ? nullptr : fromlen, from == nullptr ? 0 : sizeof(*fromlen)); + if (!lengthOutput.Open()) return fail(NET_EFAULT); #ifdef _WIN32 if (const int wait = wait_for_local_address(s, socket, len == 0)) { if (wait != RECEIVE_NOTHING) return fail(wait); @@ -994,6 +996,10 @@ int64_t APS5_VABI sceNetRecvfrom(int s, void* buf, size_t len, int flags, void* return 0; } #endif + const auto addressBytes = from != nullptr && socket.type == NET_SOCK_DGRAM ? + std::min(*fromlen, socket.family == NET_AF_INET ? 16u : 28u) : 0u; + const GuestArena::HostWrite addressOutput(addressBytes == 0 ? nullptr : from, addressBytes); + if (!addressOutput.Open()) return fail(NET_EFAULT); const GuestArena::HostWrite destination(len == 0 ? nullptr : buf, len); if (!destination.Open()) return fail(NET_EFAULT); sockaddr_storage peer{}; diff --git a/core/libs/tests/GuestSocketHostWrite.cpp b/core/libs/tests/GuestSocketHostWrite.cpp index 06e23b986e..982eb625d7 100644 --- a/core/libs/tests/GuestSocketHostWrite.cpp +++ b/core/libs/tests/GuestSocketHostWrite.cpp @@ -243,6 +243,51 @@ int main(int argc, char** argv) { if (mask & 1) Require(Protection(address) == PAGE_READONLY, "sender address rearmed"); if (mask & 2) Require(Protection(length) == PAGE_READONLY, "sender length rearmed"); } + if (api == 5) { + for (const bool readOnlyAddress : {true, false}) { + auto* address = static_cast(outputMapping) + 16; + auto* length = reinterpret_cast(static_cast(outputMapping) + page + 16); + { + const GuestArena::HostWrite addressDestination(address, 32); + const GuestArena::HostWrite lengthDestination(length, sizeof(*length)); + Require(addressDestination.Open() && lengthDestination.Open(), "initialize refused sender outputs"); + std::memset(address, 0xa5, 32); + *length = 32; + } + void* refused = static_cast(outputMapping) + (readOnlyAddress ? 0 : page); + Require(sceKernelMprotect(refused, page, 1) == 0, "make one sender output read-only"); + send(); + waitForPacket(); + std::array recovery{}; + Require(receive(recovery.data(), recovery.size(), 0, address, length) == static_cast(0x8041010eu) && *error == 14, + "read-only sender output fails with EFAULT"); + Require(*length == 32 && std::all_of(address, address + 32, [](unsigned char value) { return value == 0xa5; }), + "refused sender outputs remain unchanged"); + Require(receive(recovery.data(), recovery.size(), 0, nullptr, nullptr) == sizeof(payload) && + std::memcmp(recovery.data(), payload, sizeof(payload)) == 0, "refused sender output preserves queued data"); + Require(sceKernelMprotect(refused, page, 3) == 0, "restore sender output permission"); + } + auto* address = static_cast(outputMapping) + 16; + Require(sceKernelMprotect(outputMapping, page, 1) == 0, "make an unused sender address read-only"); + std::uint32_t zero = 0; + std::array recovery{}; + send(); + waitForPacket(); + Require(receive(recovery.data(), recovery.size(), 0, address, &zero) == sizeof(payload) && zero == 0 && + std::memcmp(recovery.data(), payload, sizeof(payload)) == 0, "zero-capacity sender address needs no write access"); + Require(sceKernelMprotect(outputMapping, page, 3) == 0, "restore unused sender address permission"); + address = static_cast(outputMapping) + page - senderAddress.size(); + Require(sceKernelMprotect(static_cast(outputMapping) + page, page, 1) == 0, + "make bytes beyond the sender address read-only"); + std::uint32_t oversized = 32; + send(); + waitForPacket(); + Require(receive(recovery.data(), recovery.size(), 0, address, &oversized) == sizeof(payload) && + oversized == senderAddress.size() && std::memcmp(address, senderAddress.data(), oversized) == 0, + "guard only the sender address bytes the receive writes"); + Require(sceKernelMprotect(static_cast(outputMapping) + page, page, 3) == 0, + "restore bytes beyond the sender address"); + } Require(sceKernelMunmap(outputTwin, page * 2) == 0, "unmap the second sender output mapping"); Require(sceKernelMunmap(outputMapping, page * 2) == 0, "unmap sender outputs"); Require(sceKernelReleaseDirectMemory(outputPhysical, page * 2) == 0, "release sender output memory"); From cfa5b9b47c9b9c6e7cc5d88b1c60df2bc940e684 Mon Sep 17 00:00:00 2001 From: mrfelek Date: Sat, 17 Oct 2026 17:32:35 +0200 Subject: [PATCH 0790/1000] feat(libSceAvPlayer): accept VP9 video and Opus audio streams Sources whose streams are VP9 or Opus were dropped by IsStreamSupported, so a WebM movie added with sceAvPlayerAddSource had no streams and the player reported an error. The bundled FFmpeg already decodes both; their frames go through the existing NV12 and PCM output paths. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01BywPXoZACVi8ALV4JKvuPZ --- core/libs/prx/libSceAvPlayer/src/Source.cpp | 2 +- core/libs/tests/GuestAvPlayer.cpp | 145 ++++++++++++++++++++ 2 files changed, 146 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAvPlayer/src/Source.cpp b/core/libs/prx/libSceAvPlayer/src/Source.cpp index 3844a906b5..b9c1e162f3 100644 --- a/core/libs/prx/libSceAvPlayer/src/Source.cpp +++ b/core/libs/prx/libSceAvPlayer/src/Source.cpp @@ -63,7 +63,7 @@ std::uint64_t FrameMillis(const AVFrame& frame, AVRational timeBase) { bool IsStreamSupported(const AVStream& stream) { const auto codec = stream.codecpar->codec_id; - return codec == AV_CODEC_ID_H264 || codec == AV_CODEC_ID_HEVC || codec == AV_CODEC_ID_AAC; + return codec == AV_CODEC_ID_H264 || codec == AV_CODEC_ID_HEVC || codec == AV_CODEC_ID_AAC || codec == AV_CODEC_ID_VP9 || codec == AV_CODEC_ID_OPUS; } float DisplayAspect(int width, int height, AVRational sampleAspect) { diff --git a/core/libs/tests/GuestAvPlayer.cpp b/core/libs/tests/GuestAvPlayer.cpp index 7fc12bf694..5e4b500e60 100644 --- a/core/libs/tests/GuestAvPlayer.cpp +++ b/core/libs/tests/GuestAvPlayer.cpp @@ -73,6 +73,7 @@ constexpr std::int32_t EventPlay = 3; constexpr std::int32_t EventPause = 4; constexpr std::int32_t EventWarning = 0x20; const char* const MoviePath = "app0/avplayer.mp4"; +const char* const WebmPath = "app0/avplayer.webm"; constexpr std::array Movie{ 0x00, 0x00, 0x00, 0x20, 0x66, 0x74, 0x79, 0x70, 0x69, 0x73, 0x6f, 0x6d, 0x00, 0x00, 0x02, 0x00, @@ -452,6 +453,109 @@ constexpr std::array Movie{ 0x5b, 0xdc }; +constexpr std::array WebmMovie{ + 0x1a, 0x45, 0xdf, 0xa3, 0x9f, 0x42, 0x86, 0x81, 0x01, 0x42, 0xf7, 0x81, 0x01, 0x42, 0xf2, 0x81, + 0x04, 0x42, 0xf3, 0x81, 0x08, 0x42, 0x82, 0x84, 0x77, 0x65, 0x62, 0x6d, 0x42, 0x87, 0x81, 0x04, + 0x42, 0x85, 0x81, 0x02, 0x18, 0x53, 0x80, 0x67, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x0a, + 0x11, 0x4d, 0x9b, 0x74, 0xba, 0x4d, 0xbb, 0x8b, 0x53, 0xab, 0x84, 0x15, 0x49, 0xa9, 0x66, 0x53, + 0xac, 0x81, 0xa1, 0x4d, 0xbb, 0x8b, 0x53, 0xab, 0x84, 0x16, 0x54, 0xae, 0x6b, 0x53, 0xac, 0x81, + 0xc6, 0x4d, 0xbb, 0x8c, 0x53, 0xab, 0x84, 0x12, 0x54, 0xc3, 0x67, 0x53, 0xac, 0x82, 0x01, 0x79, + 0x4d, 0xbb, 0x8c, 0x53, 0xab, 0x84, 0x1c, 0x53, 0xbb, 0x6b, 0x53, 0xac, 0x82, 0x05, 0xf4, 0xec, + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x59, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x15, 0x49, 0xa9, 0x66, 0xa0, 0x2a, 0xd7, 0xb1, 0x83, 0x0f, 0x42, 0x40, 0x4d, 0x80, 0x84, + 0x4c, 0x61, 0x76, 0x66, 0x57, 0x41, 0x84, 0x4c, 0x61, 0x76, 0x66, 0x44, 0x89, 0x88, 0x40, 0x6a, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x16, 0x54, 0xae, 0x6b, 0x40, 0xad, 0xae, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x3f, 0xd7, 0x81, 0x01, 0x73, 0xc5, 0x88, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x01, 0x9c, 0x81, 0x00, 0x22, 0xb5, 0x9c, 0x83, 0x65, 0x6e, 0x67, 0x88, 0x81, 0x00, + 0x86, 0x85, 0x56, 0x5f, 0x56, 0x50, 0x39, 0x83, 0x81, 0x01, 0x23, 0xe3, 0x83, 0x84, 0x01, 0xfc, + 0xa0, 0x55, 0xe0, 0x90, 0xb0, 0x81, 0x64, 0xba, 0x81, 0x3c, 0x9a, 0x81, 0x02, 0x55, 0xb0, 0x84, + 0x55, 0xb9, 0x81, 0x01, 0xae, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5c, 0xd7, 0x81, 0x02, + 0x73, 0xc5, 0x88, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x9c, 0x81, 0x00, 0x22, 0xb5, + 0x9c, 0x83, 0x65, 0x6e, 0x67, 0x88, 0x81, 0x00, 0x86, 0x86, 0x41, 0x5f, 0x4f, 0x50, 0x55, 0x53, + 0x56, 0xaa, 0x83, 0x63, 0x2e, 0xa0, 0x56, 0xbb, 0x84, 0x04, 0xc4, 0xb4, 0x00, 0x83, 0x81, 0x02, + 0xe1, 0x91, 0x9f, 0x81, 0x02, 0xb5, 0x88, 0x40, 0xe7, 0x70, 0x00, 0x00, 0x00, 0x00, 0x00, 0x62, + 0x64, 0x81, 0x10, 0x63, 0xa2, 0x93, 0x4f, 0x70, 0x75, 0x73, 0x48, 0x65, 0x61, 0x64, 0x01, 0x02, + 0x38, 0x01, 0x80, 0xbb, 0x00, 0x00, 0x00, 0x00, 0x00, 0x12, 0x54, 0xc3, 0x67, 0x40, 0xa5, 0x73, + 0x73, 0xd1, 0x63, 0xc0, 0x8b, 0x63, 0xc5, 0x88, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, + 0x67, 0xc8, 0x9c, 0x45, 0xa3, 0x87, 0x45, 0x4e, 0x43, 0x4f, 0x44, 0x45, 0x52, 0x44, 0x87, 0x8f, + 0x4c, 0x61, 0x76, 0x63, 0x20, 0x6c, 0x69, 0x62, 0x76, 0x70, 0x78, 0x2d, 0x76, 0x70, 0x39, 0x67, + 0xc8, 0xa1, 0x45, 0xa3, 0x88, 0x44, 0x55, 0x52, 0x41, 0x54, 0x49, 0x4f, 0x4e, 0x44, 0x87, 0x93, + 0x30, 0x30, 0x3a, 0x30, 0x30, 0x3a, 0x30, 0x30, 0x2e, 0x32, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, + 0x30, 0x30, 0x00, 0x73, 0x73, 0xce, 0x63, 0xc0, 0x8b, 0x63, 0xc5, 0x88, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x02, 0x67, 0xc8, 0x99, 0x45, 0xa3, 0x87, 0x45, 0x4e, 0x43, 0x4f, 0x44, 0x45, + 0x52, 0x44, 0x87, 0x8c, 0x4c, 0x61, 0x76, 0x63, 0x20, 0x6c, 0x69, 0x62, 0x6f, 0x70, 0x75, 0x73, + 0x67, 0xc8, 0xa1, 0x45, 0xa3, 0x88, 0x44, 0x55, 0x52, 0x41, 0x54, 0x49, 0x4f, 0x4e, 0x44, 0x87, + 0x93, 0x30, 0x30, 0x3a, 0x30, 0x30, 0x3a, 0x30, 0x30, 0x2e, 0x32, 0x30, 0x38, 0x30, 0x30, 0x30, + 0x30, 0x30, 0x30, 0x00, 0x1f, 0x43, 0xb6, 0x75, 0x43, 0xca, 0xe7, 0x81, 0x00, 0xa3, 0x87, 0x82, + 0x00, 0x00, 0x80, 0xfc, 0xff, 0xfe, 0xa3, 0xb0, 0x81, 0x00, 0x00, 0x80, 0x82, 0x49, 0x83, 0x42, + 0x00, 0x06, 0x30, 0x03, 0xb6, 0x00, 0x38, 0x24, 0x1c, 0x18, 0x00, 0x00, 0x00, 0x60, 0x7d, 0x8f, + 0x18, 0xc0, 0x00, 0x00, 0x78, 0xb6, 0x32, 0x06, 0x00, 0x15, 0x8b, 0x45, 0x38, 0x14, 0x97, 0x0e, + 0x63, 0x08, 0xb7, 0x40, 0x1a, 0xde, 0xd0, 0x00, 0xa3, 0x87, 0x82, 0x00, 0x15, 0x80, 0xfc, 0xff, + 0xfe, 0xa3, 0x40, 0x9f, 0x81, 0x00, 0x21, 0x00, 0x86, 0x00, 0x40, 0x92, 0xf0, 0x01, 0x00, 0x00, + 0x00, 0x08, 0x00, 0x00, 0x73, 0x19, 0x60, 0x5c, 0x40, 0x00, 0x00, 0x00, 0x02, 0x09, 0x3d, 0x02, + 0x8e, 0x7f, 0xb0, 0xdb, 0xaa, 0x7f, 0xff, 0xe7, 0x42, 0xdf, 0xdf, 0xe1, 0x6f, 0x3f, 0xff, 0xf3, + 0xa1, 0x6f, 0xef, 0xf0, 0xb7, 0x9f, 0xff, 0xf9, 0xd0, 0xb7, 0xf7, 0xf8, 0x5b, 0xcf, 0xff, 0xfc, + 0xe8, 0x5b, 0xfb, 0xfc, 0x2d, 0xe7, 0xff, 0xfe, 0x74, 0x2d, 0xfd, 0xfe, 0x16, 0xf3, 0xff, 0xff, + 0x3a, 0x16, 0xfe, 0xff, 0x0b, 0x79, 0xff, 0xff, 0x9d, 0x0b, 0x7f, 0x7f, 0x85, 0x81, 0xcc, 0xbc, + 0xff, 0xff, 0xce, 0x85, 0xbf, 0xbf, 0xc2, 0xde, 0x7f, 0xff, 0xe7, 0x42, 0xdf, 0xdf, 0xe1, 0x6f, + 0x3f, 0xff, 0xf3, 0xa1, 0x6f, 0xef, 0xf0, 0xb7, 0x9f, 0xff, 0xf9, 0xd0, 0xb7, 0xf7, 0xf8, 0x5b, + 0xcf, 0xff, 0xfc, 0xe8, 0x5b, 0xfb, 0xfc, 0x2d, 0xe7, 0xff, 0xfe, 0x74, 0x2d, 0xfd, 0xfe, 0x16, + 0xf3, 0xff, 0xff, 0x3a, 0x16, 0xfe, 0xff, 0x0b, 0x79, 0xff, 0xff, 0x9d, 0x0b, 0x7f, 0x7f, 0x85, + 0x80, 0x93, 0x00, 0xa3, 0x87, 0x82, 0x00, 0x29, 0x80, 0xfc, 0xff, 0xfe, 0xa3, 0x87, 0x82, 0x00, + 0x3d, 0x80, 0xfc, 0xff, 0xfe, 0xa3, 0x40, 0x9f, 0x81, 0x00, 0x43, 0x00, 0x86, 0x00, 0x40, 0x92, + 0xf0, 0x01, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x73, 0x19, 0x58, 0xc6, 0x80, 0x00, 0x00, 0x00, + 0x04, 0x12, 0x79, 0xde, 0x97, 0xff, 0x05, 0xc3, 0x79, 0xff, 0xff, 0x8f, 0xe5, 0xff, 0x82, 0xe1, + 0xbc, 0xff, 0xff, 0xc7, 0xf2, 0xff, 0xc1, 0x70, 0xde, 0x7f, 0xff, 0xe3, 0xf9, 0x7f, 0xe0, 0xb8, + 0x6f, 0x3f, 0xff, 0xf1, 0xfc, 0xbf, 0xf0, 0x5c, 0x37, 0x9f, 0xff, 0xf8, 0xfe, 0x5f, 0xf8, 0x2e, + 0x1b, 0xcf, 0xff, 0xfc, 0x7f, 0x2f, 0xfc, 0x17, 0x0d, 0xe7, 0xff, 0xfe, 0x3f, 0x97, 0xfe, 0x0b, + 0x86, 0x07, 0x32, 0xf3, 0xff, 0xff, 0x1f, 0xcb, 0xff, 0x05, 0xc3, 0x79, 0xff, 0xff, 0x8f, 0xe5, + 0xff, 0x82, 0xe1, 0xbc, 0xff, 0xff, 0xc7, 0xf2, 0xff, 0xc1, 0x70, 0xde, 0x7f, 0xff, 0xe3, 0xf9, + 0x7f, 0xe0, 0xb8, 0x6f, 0x3f, 0xff, 0xf1, 0xfc, 0xbf, 0xf0, 0x5c, 0x37, 0x9f, 0xff, 0xf8, 0xfe, + 0x5f, 0xf8, 0x2e, 0x1b, 0xcf, 0xff, 0xfc, 0x7f, 0x2f, 0xfc, 0x17, 0x0d, 0xe7, 0xff, 0xfe, 0x3f, + 0x97, 0xfe, 0x0b, 0x86, 0x02, 0x4c, 0x00, 0xa3, 0x87, 0x82, 0x00, 0x51, 0x80, 0xfc, 0xff, 0xfe, + 0xa3, 0x87, 0x82, 0x00, 0x65, 0x80, 0xfc, 0xff, 0xfe, 0xa3, 0x40, 0x9d, 0x81, 0x00, 0x64, 0x00, + 0x86, 0x00, 0x40, 0x92, 0xf0, 0x01, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x73, 0x19, 0x4c, 0xe5, + 0x00, 0x00, 0x00, 0x00, 0x08, 0x24, 0xf3, 0x40, 0x0f, 0xfb, 0xaa, 0xcb, 0xcf, 0xff, 0xfc, 0x2a, + 0x53, 0xfb, 0x1c, 0x85, 0x4f, 0xff, 0xfc, 0x2a, 0x53, 0xfb, 0x1c, 0x85, 0x4f, 0xff, 0xfc, 0x2a, + 0x53, 0xfb, 0x1c, 0x85, 0x4f, 0xff, 0xfc, 0x2a, 0x53, 0xfb, 0x1c, 0x85, 0x4f, 0xff, 0xfc, 0x2a, + 0x53, 0xfb, 0x1c, 0x85, 0x4f, 0xff, 0xfc, 0x2a, 0x53, 0xfb, 0x1c, 0x85, 0x4f, 0xff, 0xfc, 0x2a, + 0x53, 0xfb, 0x1c, 0x80, 0x27, 0xfc, 0x9f, 0xff, 0xfc, 0x2a, 0x53, 0xfb, 0x1c, 0x85, 0x4f, 0xff, + 0xfc, 0x2a, 0x53, 0xfb, 0x1c, 0x85, 0x4f, 0xff, 0xfc, 0x2a, 0x53, 0xfb, 0x1c, 0x85, 0x4f, 0xff, + 0xfc, 0x2a, 0x53, 0xfb, 0x1c, 0x85, 0x4f, 0xff, 0xfc, 0x2a, 0x53, 0xfb, 0x1c, 0x85, 0x4f, 0xff, + 0xfc, 0x2a, 0x53, 0xfb, 0x1c, 0x85, 0x4f, 0xff, 0xfc, 0x2a, 0x53, 0xfb, 0x1c, 0x85, 0x4f, 0xff, + 0xfc, 0x2a, 0x53, 0xfb, 0x1c, 0x80, 0x0c, 0xc0, 0x00, 0xa3, 0x87, 0x82, 0x00, 0x79, 0x80, 0xfc, + 0xff, 0xfe, 0xa3, 0x40, 0x9f, 0x81, 0x00, 0x85, 0x00, 0x86, 0x00, 0x40, 0x92, 0xf0, 0x01, 0x00, + 0x00, 0x00, 0x08, 0x00, 0x00, 0x73, 0x19, 0x37, 0x9e, 0x40, 0x00, 0x00, 0x00, 0x02, 0x09, 0x3c, + 0x9e, 0x10, 0x3f, 0xa7, 0x97, 0xf2, 0x3f, 0xff, 0xee, 0x24, 0x7f, 0xd7, 0xcf, 0x67, 0x9f, 0xff, + 0xf7, 0x12, 0x3f, 0xeb, 0xe7, 0xb3, 0xcf, 0xff, 0xfb, 0x89, 0x1f, 0xf5, 0xf3, 0xd9, 0xe7, 0xff, + 0xfd, 0xc4, 0x8f, 0xfa, 0xf9, 0xec, 0xf3, 0xff, 0xfe, 0xe2, 0x47, 0xfd, 0x7c, 0xf6, 0x79, 0xff, + 0xff, 0x71, 0x23, 0xfe, 0xbe, 0x7b, 0x3c, 0xff, 0xff, 0xb8, 0x91, 0xff, 0x5f, 0x3d, 0x80, 0xe6, + 0x5e, 0x7f, 0xff, 0xdc, 0x48, 0xff, 0xaf, 0x9e, 0xcf, 0x3f, 0xff, 0xee, 0x24, 0x7f, 0xd7, 0xcf, + 0x67, 0x9f, 0xff, 0xf7, 0x12, 0x3f, 0xeb, 0xe7, 0xb3, 0xcf, 0xff, 0xfb, 0x89, 0x1f, 0xf5, 0xf3, + 0xd9, 0xe7, 0xff, 0xfd, 0xc4, 0x8f, 0xfa, 0xf9, 0xec, 0xf3, 0xff, 0xfe, 0xe2, 0x47, 0xfd, 0x7c, + 0xf6, 0x79, 0xff, 0xff, 0x71, 0x23, 0xfe, 0xbe, 0x7b, 0x3c, 0xff, 0xff, 0xb8, 0x91, 0xff, 0x5f, + 0x3d, 0x80, 0x49, 0x80, 0xa3, 0x87, 0x82, 0x00, 0x8d, 0x80, 0xfc, 0xff, 0xfe, 0xa3, 0x87, 0x82, + 0x00, 0xa1, 0x80, 0xfc, 0xff, 0xfe, 0xa3, 0x40, 0x9d, 0x81, 0x00, 0xa7, 0x00, 0x86, 0x00, 0x40, + 0x92, 0xf0, 0x01, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x73, 0x19, 0x31, 0x74, 0x00, 0x00, 0x00, + 0x00, 0x04, 0x12, 0x79, 0x1a, 0xe4, 0xfe, 0x8f, 0x1c, 0x91, 0xff, 0xff, 0x5b, 0x7d, 0xfe, 0x7a, + 0x80, 0xc1, 0xff, 0xff, 0x5b, 0x7d, 0xfe, 0x7a, 0x80, 0xc1, 0xff, 0xff, 0x5b, 0x7d, 0xfe, 0x7a, + 0x80, 0xc1, 0xff, 0xff, 0x5b, 0x7d, 0xfe, 0x7a, 0x80, 0xc1, 0xff, 0xff, 0x5b, 0x7d, 0xfe, 0x7a, + 0x80, 0xc1, 0xff, 0xff, 0x5b, 0x7d, 0xfe, 0x7a, 0x80, 0xc1, 0xff, 0xff, 0x5b, 0x7d, 0xfe, 0x7a, + 0x80, 0x05, 0xc7, 0x67, 0xff, 0xff, 0x5b, 0x7d, 0xfe, 0x7a, 0x80, 0xc1, 0xff, 0xff, 0x5b, 0x7d, + 0xfe, 0x7a, 0x80, 0xc1, 0xff, 0xff, 0x5b, 0x7d, 0xfe, 0x7a, 0x80, 0xc1, 0xff, 0xff, 0x5b, 0x7d, + 0xfe, 0x7a, 0x80, 0xc1, 0xff, 0xff, 0x5b, 0x7d, 0xfe, 0x7a, 0x80, 0xc1, 0xff, 0xff, 0x5b, 0x7d, + 0xfe, 0x7a, 0x80, 0xc1, 0xff, 0xff, 0x5b, 0x7d, 0xfe, 0x7a, 0x80, 0xc1, 0xff, 0xff, 0x5b, 0x7d, + 0xfe, 0x7a, 0x80, 0x01, 0xd4, 0x00, 0xa3, 0x87, 0x82, 0x00, 0xb5, 0x80, 0xfc, 0xff, 0xfe, 0xa0, + 0x93, 0xa1, 0x87, 0x82, 0x00, 0xc9, 0x00, 0xfc, 0xff, 0xfe, 0x9b, 0x81, 0x07, 0x75, 0xa2, 0x84, + 0x00, 0xcd, 0xfe, 0x60, 0x1c, 0x53, 0xbb, 0x6b, 0x91, 0xbb, 0x8f, 0xb3, 0x81, 0x00, 0xb7, 0x8a, + 0xf7, 0x81, 0x01, 0xf1, 0x82, 0x02, 0x24, 0xf0, 0x81, 0x0c, +}; + void Check(bool condition, const std::string& message) { if (!condition) throw std::runtime_error(message); } @@ -848,6 +952,46 @@ void TestPlayback() { Check(allocations.textures >= 4, "video buffers not allocated as textures"); } +void TestVp9OpusPlayback() { + { + std::ofstream file(WebmPath, std::ios::binary | std::ios::trunc); + file.write(reinterpret_cast(WebmMovie.data()), static_cast(WebmMovie.size())); + Check(static_cast(file), "cannot write the WebM movie"); + } + Events events; + AvPlayerInitData init = InitData(&events); + auto* player = sceAvPlayerInit(&init); + Check(player != nullptr, "WebM init failed"); + Check(sceAvPlayerAddSource(player, "/app0/avplayer.webm") == 0, "WebM add source failed"); + Check(WaitFor([&] { return events.Seen(EventReady); }), "WebM ready event missing"); + Check(sceAvPlayerStreamCount(player) == 2, "unexpected WebM stream count"); + AvPlayerStreamInfo video{}; + Check(sceAvPlayerGetStreamInfo(player, 0, &video) == 0 && video.type == 1 && video.details.video.width == 112 && video.details.video.height == 64, "unexpected VP9 stream"); + AvPlayerStreamInfo audio{}; + Check(sceAvPlayerGetStreamInfo(player, 1, &audio) == 0 && audio.type == 0 && audio.details.audio.channel_count == 2 && audio.details.audio.sample_rate == SampleRate, "unexpected Opus stream"); + Check(sceAvPlayerEnableStream(player, 0) == 0 && sceAvPlayerEnableStream(player, 1) == 0, "enable WebM streams failed"); + Check(sceAvPlayerStart(player) == 0, "WebM start failed"); + AvPlayerFrameInfoEx frame{}; + AvPlayerFrameInfo sound{}; + int frames = 0; + int sounds = 0; + Check(WaitFor([&] { + if (sceAvPlayerGetAudioData(player, &sound)) { + CheckEnglishAudio(sound); + ++sounds; + } + if (sceAvPlayerGetVideoDataEx(player, &frame)) { + CheckVideoFrame(frame); + ++frames; + } + return !sceAvPlayerIsActive(player); + }), "WebM playback never ended"); + Check(frames >= 5 && sounds >= 2, "WebM delivered " + std::to_string(frames) + " frames and " + std::to_string(sounds) + " audio chunks"); + Check(sceAvPlayerClose(player) == 0, "WebM close failed"); + std::lock_guard lock(allocations.mutex); + Check(allocations.blocks.empty(), "WebM allocations leaked"); +} + void TestChangeStream() { Events events; AvPlayerInitData init = InitData(&events); @@ -1423,6 +1567,7 @@ int main(int argc, char** argv) { TestAudioLoopWarningAtPlayback(); TestLoopWarningSharedByStreams(); TestPlayback(); + TestVp9OpusPlayback(); TestChangeStream(); TestExtendedExports(); TestWithoutAllocators(); From c652880f49e6a8d37185cfde5f87e36e995fbec8 Mon Sep 17 00:00:00 2001 From: thibaut Date: Sat, 17 Oct 2026 17:08:38 +0200 Subject: [PATCH 0791/1000] perf(agc): grow the resource cache while the keys it evicts come back Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01XBXM4pgkMHYEtqN1EQni2P --- .../Graphics/include/ShaderResources.hpp | 11 ++ .../Graphics/src/ShaderResources.cpp | 49 +++++- .../tests/execution/ResourceCacheGrowth.cpp | 139 ++++++++++++++++++ 3 files changed, 195 insertions(+), 4 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/ResourceCacheGrowth.cpp diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp index 8e25d227c9..30e1012671 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp @@ -629,12 +629,17 @@ public: static std::uint64_t Touches(); void Clear(); std::size_t Size() const; + std::size_t Capacity() const; std::uint64_t TableElements() const; static std::uint64_t TableElementBudget(); + static constexpr std::size_t InitialEntries = 1024; + static constexpr std::size_t MaximumEntries = 16384; private: void erase(const Key& key); void noteMiss(const Key& key); + void noteEvicted(const Key& key); + void noteReturn(const Key& key); struct KeyHash { std::size_t operator()(const Key& key) const noexcept { std::uint64_t hash = 14695981039346656037ull; @@ -645,6 +650,12 @@ private: mutable std::mutex mutex; std::list>> entries; std::unordered_map index; + std::size_t capacity = 0; + bool fixedCapacity = false; + std::list evictedOrder; + std::unordered_map evictedKeys; + std::size_t evictionsSinceCheck = 0; + std::size_t returnsSinceCheck = 0; }; // The process-wide cache the device and the draw path share (keys name the device; a device clears diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index ab7d9b47ce..fc09e2f367 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -3145,6 +3145,7 @@ std::shared_ptr ResourceCache::Find(const Key& key) { std::lock_guard lock(mutex); const auto found = index.find(key); if (found == index.end()) { + noteReturn(key); noteMiss(key); return nullptr; } @@ -3152,6 +3153,35 @@ std::shared_ptr ResourceCache::Find(const Key& key) { return found->second->second; } +void ResourceCache::noteEvicted(const Key& key) { + if (fixedCapacity) return; + const auto hash = KeyHash{}(key); + if (const auto found = evictedKeys.find(hash); found != evictedKeys.end()) evictedOrder.erase(found->second); + evictedOrder.push_front(hash); + evictedKeys[hash] = evictedOrder.begin(); + while (evictedOrder.size() > capacity) { + evictedKeys.erase(evictedOrder.back()); + evictedOrder.pop_back(); + } + if (++evictionsSinceCheck < capacity / 4u) return; + const bool churning = returnsSinceCheck * 2u > evictionsSinceCheck; + evictionsSinceCheck = 0; + returnsSinceCheck = 0; + if (!churning || capacity >= MaximumEntries) return; + const auto previous = capacity; + capacity = std::min(capacity * 2u, MaximumEntries); + std::fprintf(stderr, "[rescache] capacity %zu -> %zu entries: keys it evicted keep coming back\n", previous, capacity); +} + +void ResourceCache::noteReturn(const Key& key) { + if (fixedCapacity) return; + const auto found = evictedKeys.find(KeyHash{}(key)); + if (found == evictedKeys.end()) return; + evictedOrder.erase(found->second); + evictedKeys.erase(found); + ++returnsSinceCheck; +} + namespace { void traceMiss(ChurnProfile& churn, bool draw, std::uint64_t variants, const ContentKeyDiff* parts) { @@ -3279,12 +3309,14 @@ void ResourceCache::Insert(const Key& key, std::shared_ptr reso } entries.emplace_front(key, std::move(resources)); index.emplace(key, entries.begin()); - static const std::size_t capacity = [] { + if (capacity == 0) { const char* value = std::getenv("APS5_RESOURCE_CACHE_ENTRIES"); - const auto parsed = value ? std::strtoull(value, nullptr, 10) : 1024ull; - return static_cast(parsed != 0 ? parsed : 1024ull); - }(); + const auto parsed = value ? std::strtoull(value, nullptr, 10) : 0ull; + fixedCapacity = parsed != 0; + capacity = fixedCapacity ? static_cast(parsed) : InitialEntries; + } while (entries.size() > capacity) { + noteEvicted(entries.back().first); if (evicted != nullptr) evicted->push_back(std::move(entries.back().second)); index.erase(entries.back().first); entries.pop_back(); @@ -3348,6 +3380,10 @@ void ResourceCache::Clear() { std::lock_guard lock(mutex); index.clear(); entries.clear(); + evictedKeys.clear(); + evictedOrder.clear(); + evictionsSinceCheck = 0; + returnsSinceCheck = 0; } std::size_t ResourceCache::Size() const { @@ -3355,6 +3391,11 @@ std::size_t ResourceCache::Size() const { return entries.size(); } +std::size_t ResourceCache::Capacity() const { + std::lock_guard lock(mutex); + return capacity == 0 ? InitialEntries : capacity; +} + std::uint64_t ResourceCache::TableElementBudget() { static const std::uint64_t elements = [] { const char* value = std::getenv("APS5_RESOURCE_CACHE_TABLE_ELEMENTS"); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ResourceCacheGrowth.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ResourceCacheGrowth.cpp new file mode 100644 index 0000000000..fbb0ccec87 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ResourceCacheGrowth.cpp @@ -0,0 +1,139 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp" +#include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; + +constexpr std::uint32_t Dispatches = 1536; +constexpr std::uint32_t Passes = 4; +constexpr std::size_t SrtBytes = 64; +constexpr std::size_t OutputBytes = 256; +constexpr std::size_t SrtOffset = 4096; +constexpr std::size_t OutputOffset = SrtOffset + static_cast(Dispatches) * SrtBytes; +constexpr std::size_t BlockBytes = (OutputOffset + static_cast(Dispatches) * OutputBytes + 65535) & ~static_cast(65535); +constexpr std::size_t InitialEntries = 1024; + +constexpr std::array StoreCode{0xf4080100u, 0xfa000000u, 0xbf8c0000u, 0x7e020281u, 0xe0700000u, 0x80010100u, 0xbf810000u}; + +class GuestBlock { +public: + GuestBlock() { +#ifdef _WIN32 + block = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(BlockBytes, 65536)); + if (block != nullptr) GuestArena::GuestArenaCommit_nid_postfix(block, BlockBytes, PAGE_READWRITE, BlockBytes); +#else + constexpr std::uintptr_t alignment = 65536; + void* mapped = mmap(nullptr, BlockBytes + alignment, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (mapped != MAP_FAILED) { + const auto begin = reinterpret_cast(mapped); + const auto aligned = (begin + alignment - 1) & ~(alignment - 1); + if (aligned != begin) munmap(mapped, aligned - begin); + if (aligned + BlockBytes != begin + BlockBytes + alignment) munmap(reinterpret_cast(aligned + BlockBytes), begin + alignment - aligned); + block = reinterpret_cast(aligned); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(block, BlockBytes); + } +#endif + Require(block != nullptr, "resource cache growth: cannot allocate the guest block"); + GuestAllocations::Mutation().Add(block, BlockBytes, true, true, true); + } + + GuestBlock(const GuestBlock&) = delete; + GuestBlock& operator=(const GuestBlock&) = delete; + + std::uint64_t At(std::size_t offset) const { return static_cast(reinterpret_cast(block + offset)); } + std::uint8_t* Bytes(std::size_t offset) const { return block + offset; } + +private: + std::uint8_t* block = nullptr; +}; + +std::vector Pm4(std::uint32_t opcode, std::initializer_list body) { + std::vector words{0xc0000000u | (static_cast(body.size() - 1) << 16u) | (opcode << 8u)}; + words.insert(words.end(), body); + return words; +} + +void Prepare(const GuestBlock& block) { + std::memcpy(block.Bytes(0), StoreCode.data(), sizeof(StoreCode)); + for (std::uint32_t dispatch = 0; dispatch < Dispatches; ++dispatch) { + const auto output = block.At(OutputOffset + static_cast(dispatch) * OutputBytes); + const std::array descriptor{static_cast(output), static_cast((output >> 32u) & 0xffffu), static_cast(OutputBytes), 0x31016facu}; + std::memcpy(block.Bytes(SrtOffset + static_cast(dispatch) * SrtBytes), descriptor.data(), sizeof(descriptor)); + } + std::memset(block.Bytes(OutputOffset), 0, static_cast(Dispatches) * OutputBytes); +} + +void RunPass(const GuestBlock& block) { + const auto kernel = block.At(0); + std::vector words; + for (std::uint32_t dispatch = 0; dispatch < Dispatches; ++dispatch) { + const auto srt = block.At(SrtOffset + static_cast(dispatch) * SrtBytes); + for (const auto& packet : {Pm4(0x76, {0x20c, static_cast(kernel >> 8u)}), Pm4(0x76, {0x20d, static_cast(kernel >> 40u) & 0xffu}), Pm4(0x76, {0x207, 64}), Pm4(0x76, {0x208, 1}), Pm4(0x76, {0x209, 1}), Pm4(0x76, {0x213, 2u << 1u}), Pm4(0x76, {0x240, static_cast(srt)}), Pm4(0x76, {0x241, static_cast(srt >> 32u)}), Pm4(0x15, {1, 1, 1, 0x41})}) { + words.insert(words.end(), packet.begin(), packet.end()); + } + } + Packet dcb{words.data(), static_cast(words.size()), 0, {}}; + Require(sceAgcDriverSubmitDcb(&dcb) == 0, "resource cache growth: the dispatch submission failed"); + AgcDriverWaitIdle_nid_postfix(); +} + +void WorkingSetLargerThanTheInitialBound(const GuestBlock& block) { + auto& cache = AgcDriver::Graphics::SharedResourceCache(); + for (std::uint32_t pass = 0; pass < Passes; ++pass) { + RunPass(block); + if (pass != 0) continue; + for (std::uint32_t dispatch = 0; dispatch < Dispatches; ++dispatch) { + std::uint32_t stored = 0; + AgcDriver::GuestMemory::Read(block.At(OutputOffset + static_cast(dispatch) * OutputBytes), std::as_writable_bytes(std::span(&stored, 1)), 4); + Require(stored == 1u, "resource cache growth: dispatch " + std::to_string(dispatch) + " stored " + std::to_string(stored) + ", expected 1"); + } + } + const auto kept = cache.Size(); + Require(kept >= Dispatches, "resource cache growth: " + std::to_string(Dispatches) + " dispatches with distinct resources repeated " + std::to_string(Passes) + " times in turn, but the resource cache kept only " + std::to_string(kept) + " entries (initial bound " + std::to_string(InitialEntries) + "), so every pass rebuilt every set"); +} + +} + +int main() { + try { + { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + } + GuestBlock block; + Prepare(block); + WorkingSetLargerThanTheInitialBound(block); + AgcDriverShutdown_nid_postfix(); + std::puts("resource cache growth tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + AgcDriverShutdown_nid_postfix(); + return 1; + } +} From 77bf326d51c9a49a28775dc255861b7eadcb56b4 Mon Sep 17 00:00:00 2001 From: milescoviello Date: Sat, 17 Oct 2026 15:39:18 +0000 Subject: [PATCH 0792/1000] test(agc): skip the multisampled target test without storage image multisampling agc_driver_sample_multisample_target draws into a 4x target and reads its samples back through a multisampled storage image. On a device without shaderStorageImageMultisample, such as an Intel Arrow Lake iGPU on ANV, the recompiler refuses that shader ('storage image multisampling is unavailable on the target device') and the test failed. It now skips there, as the recorder's storage multisample checks already do. Co-Authored-By: Claude Opus 5.5 --- .../tests/execution/SampleMultisampleTarget.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp index e165888e8c..dfe6cfab4d 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp @@ -583,6 +583,10 @@ int main(int argc, char** argv) { Block other(BlockBytes); const auto color = DecodeTarget(block); const auto state = TargetState(color); + if (!AgcDriver::VulkanDeviceTestAccess::ContextOf(*device).storageImageMultisample) { + std::puts("multisampled target sampling not tested: storage image multisampling unavailable"); + return VulkanTestSkipped; + } Draw(*device, state); CheckSamples(state, LoadSamples(*device, block), {}); const auto colorBytes = AgcDriver::Graphics::MultisampledColorBytes(color); From e61af865de0d977ae440e64b5e230a182ea98cf4 Mon Sep 17 00:00:00 2001 From: Romans Pokrovskis Date: Sat, 17 Oct 2026 18:52:49 +0300 Subject: [PATCH 0793/1000] fix(libc): open long Windows host paths --- core/libs/prx/libc/CMakeLists.txt | 9 ++ core/libs/prx/libc/src/FileAndHeap.cpp | 9 +- core/libs/tests/GuestFopenLongHostPath.cpp | 116 +++++++++++++++++++++ 3 files changed, 133 insertions(+), 1 deletion(-) create mode 100644 core/libs/tests/GuestFopenLongHostPath.cpp diff --git a/core/libs/prx/libc/CMakeLists.txt b/core/libs/prx/libc/CMakeLists.txt index 4900ae6797..e915fe7167 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -188,6 +188,15 @@ if(BUILD_TESTING) set_tests_properties(file_position PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + if(WIN32) + add_test_executable(guest_fopen_long_host_path_tests ${LIBS_INCLUDE_DIR}/tests/GuestFopenLongHostPath.cpp) + target_include_directories(guest_fopen_long_host_path_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_fopen_long_host_path_tests PRIVATE libc) + configure_windows_unwind(guest_fopen_long_host_path_tests) + add_test(NAME guest_fopen_long_host_path COMMAND guest_fopen_long_host_path_tests) + set_tests_properties(guest_fopen_long_host_path PROPERTIES TIMEOUT 20 ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_wide_formatting_tests ${LIBS_INCLUDE_DIR}/tests/GuestWideFormatting.cpp) target_include_directories(guest_wide_formatting_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_wide_formatting_tests PRIVATE libc) diff --git a/core/libs/prx/libc/src/FileAndHeap.cpp b/core/libs/prx/libc/src/FileAndHeap.cpp index 297c64076e..8807232d13 100644 --- a/core/libs/prx/libc/src/FileAndHeap.cpp +++ b/core/libs/prx/libc/src/FileAndHeap.cpp @@ -68,7 +68,14 @@ static std::FILE* OpenNative(const std::filesystem::path& path, const std::strin const DWORD disposition = access == 'r' ? OPEN_EXISTING : nativeMode.find('x') != std::string::npos ? CREATE_NEW : access == 'w' ? CREATE_ALWAYS : OPEN_ALWAYS; SECURITY_ATTRIBUTES security{sizeof(SECURITY_ATTRIBUTES), nullptr, TRUE}; - const auto handle = CreateFileW(path.c_str(), desired, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, &security, + auto filename = path.native(); + std::error_code pathError; + auto absolutePath = std::filesystem::absolute(path, pathError); + if (pathError) { errno = pathError.default_error_condition().value(); return nullptr; } + const auto absolute = absolutePath.lexically_normal().make_preferred().native(); + if (absolute.size() >= MAX_PATH && !absolute.starts_with(L"\\\\?\\") && !absolute.starts_with(L"\\\\.\\")) + filename = absolute.starts_with(L"\\\\") ? L"\\\\?\\UNC\\" + absolute.substr(2) : L"\\\\?\\" + absolute; + const auto handle = CreateFileW(filename.c_str(), desired, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, &security, disposition, FILE_ATTRIBUTE_NORMAL, nullptr); if (handle == INVALID_HANDLE_VALUE) { errno = WindowsOpenError_nid_no_patch(GetLastError()); diff --git a/core/libs/tests/GuestFopenLongHostPath.cpp b/core/libs/tests/GuestFopenLongHostPath.cpp new file mode 100644 index 0000000000..15d54914af --- /dev/null +++ b/core/libs/tests/GuestFopenLongHostPath.cpp @@ -0,0 +1,116 @@ +#include "prx/libc/include/General.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include +#define NOMINMAX +#include + +extern "C" { +void* APS5_VABI fopen_nid_postfix(const char*, const char*); +int APS5_VABI fclose_nid_postfix(void*); +std::size_t APS5_VABI fread_nid_postfix(void*, std::size_t, std::size_t, void*); +std::size_t APS5_VABI fwrite_nid_postfix(const void*, std::size_t, std::size_t, void*); +int* APS5_VABI __error_nid_postfix(); +} + +namespace { + +void Require(bool value, const char* message) { + if (!value) throw std::runtime_error(message); +} + +std::filesystem::path Extended(const std::filesystem::path& path) { + const auto& native = path.native(); + return native.starts_with(L"\\\\") ? L"\\\\?\\UNC\\" + native.substr(2) : L"\\\\?\\" + native; +} + +using Stream = std::unique_ptr; + +Stream Open(const char* path, const char* mode) { + Stream stream(fopen_nid_postfix(path, mode), fclose_nid_postfix); + if (!stream) { + std::fprintf(stderr, "fopen mode %s failed: guest errno=%d, resolved characters=%zu\n", mode, *__error_nid_postfix(), ResolvePath_nid_no_patch(path).native().size()); + throw std::runtime_error("fopen rejected a valid mapped long host path"); + } + return stream; +} + +void Run() { + auto temp = std::filesystem::absolute(std::filesystem::temp_directory_path()).lexically_normal(); + if (temp.filename().empty()) temp = temp.parent_path(); + const auto root = temp / ("anyps5-fopen-long-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); + Require(root.parent_path() == temp && root.filename().string().starts_with("anyps5-fopen-long-"), "invalid fixture root"); + Require(!std::filesystem::exists(root), "fixture root already exists"); + auto deep = root; + while (deep.native().size() < 290) deep /= std::string(40, 'd'); + Require(deep.native().starts_with(root.native() + L"\\"), "fixture escapes its root"); + struct Cleanup { + std::filesystem::path root; + std::filesystem::path deep; + ~Cleanup() { + RemovePathAlias_nid_no_patch("/long-host-file"); + RemovePathAlias_nid_no_patch("/relative-long-host-file"); + DeleteFileW(Extended(deep / "payload.bin").c_str()); + DeleteFileW(Extended(deep / "new.bin").c_str()); + for (auto path = deep; ; path = path.parent_path()) { + RemoveDirectoryW(Extended(path).c_str()); + if (path == root) break; + } + } + } cleanup{root, deep}; + Require(std::filesystem::create_directories(Extended(deep)), "cannot create fixture directories"); + const auto payload = deep / "payload.bin"; + const auto handle = CreateFileW(Extended(payload).c_str(), GENERIC_WRITE, 0, nullptr, CREATE_NEW, FILE_ATTRIBUTE_NORMAL, nullptr); + Require(handle != INVALID_HANDLE_VALUE, "cannot prepare long-path file"); + DWORD written = 0; + const bool prepared = WriteFile(handle, "abcd", 4, &written, nullptr) && written == 4; + CloseHandle(handle); + Require(prepared, "cannot write fixture payload"); + const auto host = deep.string(); + AddPathAlias_nid_no_patch("/long-host-file", host.c_str()); + Require(ResolvePath_nid_no_patch("/long-host-file/payload.bin").native().size() > MAX_PATH, "fixture does not cross MAX_PATH"); + { + auto stream = Open("/long-host-file/payload.bin", "rb"); + std::array data{}; + Require(fread_nid_postfix(data.data(), 1, data.size(), stream.get()) == 4 && data == std::array{'a', 'b', 'c', 'd'}, "long-path contents differ"); + } + { + auto stream = Open("/long-host-file/new.bin", "wbx"); + Require(fwrite_nid_postfix("new", 1, 3, stream.get()) == 3, "long-path exclusive write failed"); + } + *__error_nid_postfix() = 0; + Require(!fopen_nid_postfix("/long-host-file/new.bin", "wbx") && *__error_nid_postfix() == 17, "existing long-path exclusive open succeeded"); + { + auto stream = Open("/long-host-file/new.bin", "ab"); + Require(fwrite_nid_postfix("!", 1, 1, stream.get()) == 1, "long-path append failed"); + } + { + auto stream = Open("/long-host-file/new.bin", "rb+"); + std::array data{}; + Require(fread_nid_postfix(data.data(), 2, 2, stream.get()) == 2 && data == std::array{'n', 'e', 'w', '!'}, "long-path append bytes differ"); + } + *__error_nid_postfix() = 0; + Require(!fopen_nid_postfix("/long-host-file/missing.bin", "rb") && *__error_nid_postfix() == 2, "missing long-path file did not report ENOENT"); + const auto relative = std::filesystem::relative(deep, std::filesystem::current_path()).string(); + Require(!std::filesystem::path(relative).is_absolute(), "fixture alias is not relative"); + AddPathAlias_nid_no_patch("/relative-long-host-file", relative.c_str()); + { + auto stream = Open("/relative-long-host-file/payload.bin", "rb"); + std::array data{}; + Require(fread_nid_postfix(data.data(), 1, data.size(), stream.get()) == 4 && data == std::array{'a', 'b', 'c', 'd'}, "relative long-path contents differ"); + } + std::puts("long host path read/create/exclusive/append/update/missing/relative checks passed"); +} + +} + +int main() { + try { Run(); } + catch (const std::exception& error) { std::fprintf(stderr, "%s\n", error.what()); return 1; } + return 0; +} From a44455b74084eef4a5d7843f53d67b86a721364f Mon Sep 17 00:00:00 2001 From: Romans Pokrovskis Date: Sat, 17 Oct 2026 18:58:08 +0300 Subject: [PATCH 0794/1000] test(libc): keep relative long-path fixture on one drive --- core/libs/tests/GuestFopenLongHostPath.cpp | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/core/libs/tests/GuestFopenLongHostPath.cpp b/core/libs/tests/GuestFopenLongHostPath.cpp index 15d54914af..b23bff62b3 100644 --- a/core/libs/tests/GuestFopenLongHostPath.cpp +++ b/core/libs/tests/GuestFopenLongHostPath.cpp @@ -96,14 +96,24 @@ void Run() { } *__error_nid_postfix() = 0; Require(!fopen_nid_postfix("/long-host-file/missing.bin", "rb") && *__error_nid_postfix() == 2, "missing long-path file did not report ENOENT"); - const auto relative = std::filesystem::relative(deep, std::filesystem::current_path()).string(); - Require(!std::filesystem::path(relative).is_absolute(), "fixture alias is not relative"); - AddPathAlias_nid_no_patch("/relative-long-host-file", relative.c_str()); + const auto originalDirectory = std::filesystem::current_path(); { + struct RestoreDirectory { + std::filesystem::path original; + ~RestoreDirectory() { + std::error_code error; + std::filesystem::current_path(original, error); + } + } restoreDirectory{originalDirectory}; + std::filesystem::current_path(root); + const auto relative = deep.lexically_relative(root).string(); + Require(!relative.empty() && !std::filesystem::path(relative).is_absolute(), "fixture alias is not relative"); + AddPathAlias_nid_no_patch("/relative-long-host-file", relative.c_str()); auto stream = Open("/relative-long-host-file/payload.bin", "rb"); std::array data{}; Require(fread_nid_postfix(data.data(), 1, data.size(), stream.get()) == 4 && data == std::array{'a', 'b', 'c', 'd'}, "relative long-path contents differ"); } + Require(std::filesystem::current_path() == originalDirectory, "fixture did not restore working directory"); std::puts("long host path read/create/exclusive/append/update/missing/relative checks passed"); } From 745cdb5bf746e604601033dfb077bbaa42a3fe9a Mon Sep 17 00:00:00 2001 From: ThatsLeo Date: Wed, 14 Oct 2026 22:48:23 +0200 Subject: [PATCH 0795/1000] perf(shader): judge first-lane and constant reads by what they read MayEvaluate took ReadFirstLane and ReadConst as evaluable without looking at their sources, so plans kept conditions behind per-lane values. --- .../libSceAgcDriver/tests/ShaderMemory.cpp | 22 +++++++++- .../Optimization/SrtWalker/SrtEvaluator.hpp | 2 +- .../src/SrtWalker/SrtEvaluator.cpp | 41 +++++++++++++++---- docs/dev/ARCHITECTURE.md | 2 +- 4 files changed, 56 insertions(+), 11 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 51dee75d38..7b994fd6f4 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -561,9 +561,19 @@ void verifyMayEvaluate() { counter.AddArgument(&next); auto& latch = make(IrOpcode::ULessThan32, IrType::Bool, {&next, &constant(4)}); auto& sum = make(IrOpcode::CompositeExtractU32x2, IrType::U32, {&make(IrOpcode::IAddCarry32, IrType::U32x2, {&input, &constant(1)}), &constant(0)}); + auto& firstInput = make(IrOpcode::ReadFirstLane, IrType::U32, {&input, &varying}); + auto& firstLane = make(IrOpcode::ReadFirstLane, IrType::U32, {&lane, &varying}); + auto& firstActive = make(IrOpcode::ReadFirstLane, IrType::U32, {&make(IrOpcode::SelectU32, IrType::U32, {&varying, &input, &lane}), &varying}); + auto& firstInactive = make(IrOpcode::ReadFirstLane, IrType::U32, {&make(IrOpcode::SelectU32, IrType::U32, {&varying, &lane, &input}), &varying}); + plan.srtReads = {{&input, 0u}, {&lane, 4u}, {nullptr, 8u}}; + auto& constInput = make(IrOpcode::ReadConst, IrType::U32, {&constant(0), &constant(0)}); + auto& constLane = make(IrOpcode::ReadConst, IrType::U32, {&constant(0), &constant(1)}); + auto& constOutside = make(IrOpcode::ReadConst, IrType::U32, {&constant(0), &constant(3)}); + auto& constItself = make(IrOpcode::ReadConst, IrType::U32, {&constant(0), &constant(2)}); + plan.srtReads[2].value = &constItself; std::array data{4}; SrtRuntime runtime{data}; - for (const auto& [value, expected, name] : std::array, 11>{{ + for (const auto& [value, expected, name] : std::array, 19>{{ {&constant(3), true, "a constant"}, {&input, true, "user data"}, {&uniform, true, "a comparison of user data"}, @@ -575,13 +585,21 @@ void verifyMayEvaluate() { {&divergent, false, "a phi whose operands differ"}, {&latch, false, "a scalar loop counter's latch condition"}, {&sum, true, "the low word of a carry add of user data"}, + {&firstInput, true, "a first-lane read of user data"}, + {&firstLane, false, "a first-lane read of a vector register"}, + {&firstActive, true, "a first-lane read of a select whose active arm is user data"}, + {&firstInactive, false, "a first-lane read of a select whose active arm is a vector register"}, + {&constInput, true, "a constant read whose slot holds user data"}, + {&constLane, false, "a constant read whose slot holds a vector register"}, + {&constOutside, false, "a constant read past the last slot"}, + {&constItself, false, "a constant read whose slot reads itself"}, }}) { require(Detail::Evaluator::MayEvaluate(plan, value) == expected, (std::string("MayEvaluate misjudged ") + name).c_str()); Detail::Evaluator evaluator(plan, runtime); std::uint32_t result = 0; require(evaluator.Evaluate(Detail::IrNode(value), result) == expected, (std::string("MayEvaluate disagreed with the evaluator on ") + name).c_str()); } - const std::array takenAsEvaluable{IrOpcode::Identity, IrOpcode::Phi, IrOpcode::Void, IrOpcode::GetUserData, IrOpcode::GetShaderBase, IrOpcode::ReadFirstLane, IrOpcode::ReadConst, IrOpcode::LoadAddressU32, IrOpcode::ReadConstBuffer}; + const std::array takenAsEvaluable{IrOpcode::Identity, IrOpcode::Phi, IrOpcode::Void, IrOpcode::GetUserData, IrOpcode::GetShaderBase, IrOpcode::LoadAddressU32, IrOpcode::ReadConstBuffer}; for (std::uint16_t raw = 0; raw < static_cast(IrOpcode::Count); ++raw) { const auto opcode = static_cast(raw); if (std::ranges::find(takenAsEvaluable, opcode) != takenAsEvaluable.end()) continue; diff --git a/core/shader/recompiler/Optimization/include/Optimization/SrtWalker/SrtEvaluator.hpp b/core/shader/recompiler/Optimization/include/Optimization/SrtWalker/SrtEvaluator.hpp index 2139ae7a61..7a979b4719 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/SrtWalker/SrtEvaluator.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/SrtWalker/SrtEvaluator.hpp @@ -171,7 +171,7 @@ public: BasicEvaluator(const BasicEvaluator&) = delete; BasicEvaluator& operator=(const BasicEvaluator&) = delete; - static bool MayEvaluate(const IrResourcePlan& program, IrValue* value); + static bool MayEvaluate(const IrResourcePlan& program, IrValue* value, const IrValue* activeMask = nullptr); bool Evaluate(TNode value, std::uint32_t& result); bool EvaluateWide(TNode value, std::uint64_t& result); void ReportInaccessibleReads(InaccessibleRead* sink) { _inaccessible = sink; } diff --git a/core/shader/recompiler/Optimization/src/SrtWalker/SrtEvaluator.cpp b/core/shader/recompiler/Optimization/src/SrtWalker/SrtEvaluator.cpp index d9a5fbccf7..d99fbabc20 100644 --- a/core/shader/recompiler/Optimization/src/SrtWalker/SrtEvaluator.cpp +++ b/core/shader/recompiler/Optimization/src/SrtWalker/SrtEvaluator.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include @@ -327,9 +328,13 @@ std::optional evaluableAlone(const IrResourcePlan& program, const IrValue& } switch (value.Opcode()) { case IrOpcode::GetUserData: - case IrOpcode::GetShaderBase: - case IrOpcode::ReadFirstLane: - case IrOpcode::ReadConst: return true; + case IrOpcode::GetShaderBase: return true; + case IrOpcode::ReadFirstLane: return value.ArgumentCount() < 2 ? std::optional(false) : std::nullopt; + case IrOpcode::ReadConst: { + const auto* slot = value.ArgumentCount() < 2 || value.Argument(1) == nullptr ? nullptr : value.Argument(1)->Resolve(); + if (slot == nullptr || !slot->HasImmediate() || slot->Type() != IrType::U32 || slot->ImmediateU32() >= program.srtReads.size()) return false; + return std::nullopt; + } case IrOpcode::LoadAddressU32: case IrOpcode::ReadConstBuffer: return IsRawRead(program, value); case IrOpcode::Phi: return std::nullopt; @@ -354,10 +359,21 @@ std::optional evaluableAlone(const IrResourcePlan& program, const IrValue& } template -bool BasicEvaluator::MayEvaluate(const IrResourcePlan& program, IrValue* value) { +bool BasicEvaluator::MayEvaluate(const IrResourcePlan& program, IrValue* value, const IrValue* activeMask) { std::unordered_map known; - const auto requirements = [&program](IrValue* current) { + const auto takesActiveArm = [activeMask](const IrValue* current) { + return activeMask != nullptr && IsRuntimeSelect(current->Opcode()) && current->ArgumentCount() == 3 && current->Argument(0)->Resolve() == activeMask; + }; + const auto requirements = [&program, &takesActiveArm](IrValue* current) { std::vector required; + if (takesActiveArm(current)) { + required.push_back(current->Argument(1) != nullptr ? current->Argument(1)->Resolve() : nullptr); + return required; + } + if (current->Opcode() == IrOpcode::ReadConst) { + required.push_back(program.srtReads[current->Argument(1)->Resolve()->ImmediateU32()].value); + return required; + } if (current->IsPhi()) { required.push_back(ResolveInvariantPhi(program, current)); return required; @@ -384,15 +400,23 @@ bool BasicEvaluator::MayEvaluate(const IrResourcePlan& program, IrValue* } return required; }; + std::unordered_set expanding; std::vector> pending{{value->Resolve(), false}}; while (!pending.empty()) { const auto [current, expanded] = pending.back(); pending.pop_back(); if (known.contains(current)) continue; - if (const auto alone = evaluableAlone(program, *current)) { + if (current->Opcode() == IrOpcode::ReadFirstLane && current->ArgumentCount() >= 2) { + auto* source = current->Argument(0) != nullptr ? current->Argument(0)->Resolve() : nullptr; + const auto* mask = current->Argument(1) != nullptr ? current->Argument(1)->Resolve() : nullptr; + known.emplace(current, source != nullptr && MayEvaluate(program, source, mask)); + continue; + } + if (const auto alone = takesActiveArm(current) ? std::nullopt : evaluableAlone(program, *current)) { known.emplace(current, *alone); continue; } + if (!expanded && !expanding.insert(current).second) continue; const auto required = requirements(current); if (!expanded) { pending.emplace_back(current, true); @@ -401,7 +425,10 @@ bool BasicEvaluator::MayEvaluate(const IrResourcePlan& program, IrValue* } continue; } - known.emplace(current, std::ranges::all_of(required, [&known](const IrValue* argument) { return argument != nullptr && known.at(argument); })); + known.emplace(current, std::ranges::all_of(required, [&known](const IrValue* argument) { + const auto found = argument != nullptr ? known.find(argument) : known.end(); + return found != known.end() && found->second; + })); } return known.at(value->Resolve()); } diff --git a/docs/dev/ARCHITECTURE.md b/docs/dev/ARCHITECTURE.md index 3833a11d11..d9e23dc9e6 100644 --- a/docs/dev/ARCHITECTURE.md +++ b/docs/dev/ARCHITECTURE.md @@ -155,7 +155,7 @@ The comparison runs again when any of them changes. A CPU store into unwatched m A cached shader keeps an `IrResourcePlan` (`ResourceMaterializer::ExtractPlan` in [`ResourceMaterializer.cpp`](../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp)) for as long as it is registered: a copy of the IR values its descriptor sources, SRT reads, uniform fill and control-flow conditions depend on, which every capture walks to read the shader's resources from guest memory. The copy is sized to what the walk can use: -- A control-flow condition is kept only when `Detail::Evaluator::MayEvaluate` finds a runtime it could be evaluated for. A condition that depends on a vector register, or on an opcode the evaluator does not evaluate, takes both successors whatever the runtime holds, so the plan stores it as no condition and does not copy its operands. Such conditions are most of a shader's IR when the masked-lane code feeds its branches. +- A control-flow condition is kept only when `Detail::Evaluator::MayEvaluate` finds a runtime it could be evaluated for. A condition that depends on a vector register, or on an opcode the evaluator does not evaluate, takes both successors whatever the runtime holds, so the plan stores it as no condition and does not copy its operands. A first-lane read is judged by its source under the read's active mask, and a constant read by the SRT read it names, as the evaluator reads them. Such conditions are most of a shader's IR when the masked-lane code feeds its branches. - The copied values are allocated in one `IrValueArena` and hold no use lists: the walk only follows arguments. ### Shader MODE register access From e14a4e513898332337e9a4798720a3d982b7a424 Mon Sep 17 00:00:00 2001 From: ThatsLeo Date: Sat, 17 Oct 2026 18:03:32 +0200 Subject: [PATCH 0796/1000] fix(shader): stop first-lane reads that reach themselves from recursing MayEvaluate and the evaluator track the first-lane reads they are inside, so a read reached again through a loop-carried phi or a constant slot is not evaluable instead of recursing. --- .../prx/libSceAgcDriver/tests/ShaderMemory.cpp | 12 ++++++++++-- .../Optimization/SrtWalker/SrtEvaluator.hpp | 4 ++++ .../src/SrtWalker/SrtEvaluator.cpp | 18 +++++++++++++++++- docs/dev/ARCHITECTURE.md | 2 +- 4 files changed, 32 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 7b994fd6f4..b74d3e7889 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -568,12 +568,18 @@ void verifyMayEvaluate() { plan.srtReads = {{&input, 0u}, {&lane, 4u}, {nullptr, 8u}}; auto& constInput = make(IrOpcode::ReadConst, IrType::U32, {&constant(0), &constant(0)}); auto& constLane = make(IrOpcode::ReadConst, IrType::U32, {&constant(0), &constant(1)}); - auto& constOutside = make(IrOpcode::ReadConst, IrType::U32, {&constant(0), &constant(3)}); + auto& constOutside = make(IrOpcode::ReadConst, IrType::U32, {&constant(0), &constant(4)}); auto& constItself = make(IrOpcode::ReadConst, IrType::U32, {&constant(0), &constant(2)}); plan.srtReads[2].value = &constItself; + auto& carried = make(IrOpcode::Phi, IrType::U32, {&input}); + auto& firstCarried = make(IrOpcode::ReadFirstLane, IrType::U32, {&carried, &varying}); + carried.AddArgument(&make(IrOpcode::IAdd32, IrType::U32, {&firstCarried, &constant(1)})); + plan.srtReads.push_back({nullptr, 12u}); + auto& constFirstItself = make(IrOpcode::ReadConst, IrType::U32, {&constant(0), &constant(3)}); + plan.srtReads[3].value = &make(IrOpcode::ReadFirstLane, IrType::U32, {&constFirstItself, &varying}); std::array data{4}; SrtRuntime runtime{data}; - for (const auto& [value, expected, name] : std::array, 19>{{ + for (const auto& [value, expected, name] : std::array, 21>{{ {&constant(3), true, "a constant"}, {&input, true, "user data"}, {&uniform, true, "a comparison of user data"}, @@ -593,6 +599,8 @@ void verifyMayEvaluate() { {&constLane, false, "a constant read whose slot holds a vector register"}, {&constOutside, false, "a constant read past the last slot"}, {&constItself, false, "a constant read whose slot reads itself"}, + {&firstCarried, false, "a first-lane read of a loop-carried value that reaches itself through the read"}, + {&constFirstItself, false, "a constant read whose slot reads itself through a first-lane read"}, }}) { require(Detail::Evaluator::MayEvaluate(plan, value) == expected, (std::string("MayEvaluate misjudged ") + name).c_str()); Detail::Evaluator evaluator(plan, runtime); diff --git a/core/shader/recompiler/Optimization/include/Optimization/SrtWalker/SrtEvaluator.hpp b/core/shader/recompiler/Optimization/include/Optimization/SrtWalker/SrtEvaluator.hpp index 7a979b4719..d666acd0f8 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/SrtWalker/SrtEvaluator.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/SrtWalker/SrtEvaluator.hpp @@ -8,6 +8,7 @@ #include #include #include +#include #include namespace ShaderRecompiler::Detail { @@ -178,6 +179,7 @@ public: void ReportNullRootReads(std::uint32_t* sink) { _nullRoots = sink; } private: + static bool MayEvaluate(const IrResourcePlan& program, IrValue* value, const IrValue* activeMask, std::unordered_set& firstLanes); static float Float32(std::uint64_t bits); static std::uint64_t Float32Bits(float value); @@ -193,6 +195,8 @@ private: BasicEvaluator* _cleanEvaluator = nullptr; TNode _activeMask; bool _masked = false; + const BasicEvaluator* _outer = nullptr; + const void* _firstLane = nullptr; InaccessibleRead* _inaccessible = nullptr; std::uint32_t* _nullRoots = nullptr; EvaluatedValues _cache; diff --git a/core/shader/recompiler/Optimization/src/SrtWalker/SrtEvaluator.cpp b/core/shader/recompiler/Optimization/src/SrtWalker/SrtEvaluator.cpp index d99fbabc20..5e2850b387 100644 --- a/core/shader/recompiler/Optimization/src/SrtWalker/SrtEvaluator.cpp +++ b/core/shader/recompiler/Optimization/src/SrtWalker/SrtEvaluator.cpp @@ -360,6 +360,12 @@ std::optional evaluableAlone(const IrResourcePlan& program, const IrValue& template bool BasicEvaluator::MayEvaluate(const IrResourcePlan& program, IrValue* value, const IrValue* activeMask) { + std::unordered_set firstLanes; + return MayEvaluate(program, value, activeMask, firstLanes); +} + +template +bool BasicEvaluator::MayEvaluate(const IrResourcePlan& program, IrValue* value, const IrValue* activeMask, std::unordered_set& firstLanes) { std::unordered_map known; const auto takesActiveArm = [activeMask](const IrValue* current) { return activeMask != nullptr && IsRuntimeSelect(current->Opcode()) && current->ArgumentCount() == 3 && current->Argument(0)->Resolve() == activeMask; @@ -409,7 +415,12 @@ bool BasicEvaluator::MayEvaluate(const IrResourcePlan& program, IrValue* if (current->Opcode() == IrOpcode::ReadFirstLane && current->ArgumentCount() >= 2) { auto* source = current->Argument(0) != nullptr ? current->Argument(0)->Resolve() : nullptr; const auto* mask = current->Argument(1) != nullptr ? current->Argument(1)->Resolve() : nullptr; - known.emplace(current, source != nullptr && MayEvaluate(program, source, mask)); + if (!firstLanes.insert(current).second) { + known.emplace(current, false); + continue; + } + known.emplace(current, source != nullptr && MayEvaluate(program, source, mask, firstLanes)); + firstLanes.erase(current); continue; } if (const auto alone = takesActiveArm(current) ? std::nullopt : evaluableAlone(program, *current)) { @@ -453,8 +464,13 @@ bool BasicEvaluator::EvaluateInst(const TNode& inst, std::uint64_t& resul case IrOpcode::GetShaderBase: result = _runtime.shaderBase; return true; case IrOpcode::Phi: return EvaluatePhi(inst, result); case IrOpcode::ReadFirstLane: { + for (const auto* outer = this; outer != nullptr; outer = outer->_outer) { + if (outer->_firstLane == inst.Value()) return false; + } const TNode mask = inst.Argument(1); BasicEvaluator active(_program, _runtime, _cleanFlatSlots, _cleanEvaluator, &mask); + active._outer = this; + active._firstLane = inst.Value(); active.ReportInaccessibleReads(_inaccessible); active.ReportNullRootReads(_nullRoots); return active.EvaluateWide(inst.Argument(0), result); diff --git a/docs/dev/ARCHITECTURE.md b/docs/dev/ARCHITECTURE.md index d9e23dc9e6..423a8aa632 100644 --- a/docs/dev/ARCHITECTURE.md +++ b/docs/dev/ARCHITECTURE.md @@ -155,7 +155,7 @@ The comparison runs again when any of them changes. A CPU store into unwatched m A cached shader keeps an `IrResourcePlan` (`ResourceMaterializer::ExtractPlan` in [`ResourceMaterializer.cpp`](../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp)) for as long as it is registered: a copy of the IR values its descriptor sources, SRT reads, uniform fill and control-flow conditions depend on, which every capture walks to read the shader's resources from guest memory. The copy is sized to what the walk can use: -- A control-flow condition is kept only when `Detail::Evaluator::MayEvaluate` finds a runtime it could be evaluated for. A condition that depends on a vector register, or on an opcode the evaluator does not evaluate, takes both successors whatever the runtime holds, so the plan stores it as no condition and does not copy its operands. A first-lane read is judged by its source under the read's active mask, and a constant read by the SRT read it names, as the evaluator reads them. Such conditions are most of a shader's IR when the masked-lane code feeds its branches. +- A control-flow condition is kept only when `Detail::Evaluator::MayEvaluate` finds a runtime it could be evaluated for. A condition that depends on a vector register, or on an opcode the evaluator does not evaluate, takes both successors whatever the runtime holds, so the plan stores it as no condition and does not copy its operands. A first-lane read is judged by its source under the read's active mask, and a constant read by the SRT read it names, as the evaluator reads them. A first-lane read that reaches itself, through a loop-carried phi or a constant slot that holds it, is not evaluable: both `MayEvaluate` and the evaluator track the first-lane reads they are already inside, since each one starts a new walk under its own mask. Such conditions are most of a shader's IR when the masked-lane code feeds its branches. - The copied values are allocated in one `IrValueArena` and hold no use lists: the walk only follows arguments. ### Shader MODE register access From b4786ad4bc82514f97c74364a906dca27b06075a Mon Sep 17 00:00:00 2001 From: hossiendehghan989 <1.86807511e+08+hossiendehghan989@users.noreply.github.com> Date: Sat, 17 Oct 2026 16:11:42 +0000 Subject: [PATCH 0797/1000] fix(input): avoid pad wheel bindings while title reads mouse --- core/libs/prx/libSceVideoOut/src/KeyboardMouse.cpp | 1 + core/libs/prx/libSceVideoOut/tests/KeyboardMouse.cpp | 9 +++++++++ 2 files changed, 10 insertions(+) diff --git a/core/libs/prx/libSceVideoOut/src/KeyboardMouse.cpp b/core/libs/prx/libSceVideoOut/src/KeyboardMouse.cpp index 62ef4ae145..9b58c374de 100644 --- a/core/libs/prx/libSceVideoOut/src/KeyboardMouse.cpp +++ b/core/libs/prx/libSceVideoOut/src/KeyboardMouse.cpp @@ -126,6 +126,7 @@ Pad::InputRequests Pad::KeyboardMouse::HandleEvent(const SDL_Event& event, Clock return requests; } if (event.type == SDL_MOUSEWHEEL) { + if (titleReadsMouse) return requests; int direction = (event.wheel.y > 0) - (event.wheel.y < 0); if (event.wheel.direction == SDL_MOUSEWHEEL_FLIPPED) direction = -direction; if (direction == 0) return requests; diff --git a/core/libs/prx/libSceVideoOut/tests/KeyboardMouse.cpp b/core/libs/prx/libSceVideoOut/tests/KeyboardMouse.cpp index 7dbe86259b..fa9b6f7e3a 100644 --- a/core/libs/prx/libSceVideoOut/tests/KeyboardMouse.cpp +++ b/core/libs/prx/libSceVideoOut/tests/KeyboardMouse.cpp @@ -290,6 +290,9 @@ void testToggleRequests() { void testTitleReadsMouse() { const auto square = static_cast(Pad::PadButton::Square); const auto r2 = static_cast(Pad::PadButton::R2); + const auto up = static_cast(Pad::PadButton::Up); + const auto down = static_cast(Pad::PadButton::Down); + const auto wheelDirections = up | down; auto now = Pad::KeyboardMouse::Clock::now(); Pad::KeyboardMouse keyboardMouse(defaults()); keyboardMouse.HandleEvent(PadSimulation::ButtonEvent(SDL_BUTTON_LEFT, true), now); @@ -301,6 +304,10 @@ void testTitleReadsMouse() { keyboardMouse.HandleEvent(PadSimulation::ButtonEvent(SDL_BUTTON_RIGHT, true), now); const auto clicked = keyboardMouse.Compose(PadInputState{}); check((clicked.buttons & (square | r2)) == 0 && clicked.analogButtonsR2 == 0, "a click while the title reads the mouse pressed a pad button"); + keyboardMouse.HandleEvent(PadSimulation::WheelEvent(1), now); + check((keyboardMouse.Compose(PadInputState{}).buttons & wheelDirections) == 0, "wheel up pressed a pad direction while the title reads the mouse"); + keyboardMouse.HandleEvent(PadSimulation::WheelEvent(-1), now); + check((keyboardMouse.Compose(PadInputState{}).buttons & wheelDirections) == 0, "wheel down pressed a pad direction while the title reads the mouse"); check(!keyboardMouse.HandleEvent(PadSimulation::ButtonEvent(SDL_BUTTON_MIDDLE, true), now).toggleMouse, "a middle click while the title reads the mouse asked to toggle mouse mode"); keyboardMouse.HandleEvent(KeyEvent(SDL_SCANCODE_W, true), now); check(keyboardMouse.Compose(PadInputState{}).sticks[1] == 0, "keys stopped reaching the pad while the title reads the mouse"); @@ -313,6 +320,8 @@ void testTitleReadsMouse() { } check(keyboardMouse.Compose(PadInputState{}).sticks[2] == 128, "motion tilted the right stick while the title reads the mouse"); keyboardMouse.SetTitleReadsMouse(false); + keyboardMouse.HandleEvent(PadSimulation::WheelEvent(1), now); + check((keyboardMouse.Compose(PadInputState{}).buttons & up) != 0, "wheel bindings did not resume after the title stopped reading the mouse"); for (int step = 1; step <= 40; ++step) { now += std::chrono::milliseconds(8); keyboardMouse.HandleEvent(PadSimulation::MotionEvent(200, 0), now); From fd8a008277657a31a68811bef5a5b32a1dc25c7f Mon Sep 17 00:00:00 2001 From: Romans Pokrovskis Date: Sat, 17 Oct 2026 19:13:02 +0300 Subject: [PATCH 0798/1000] test(libc): cover an existing Windows path namespace --- core/libs/tests/GuestFopenLongHostPath.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/core/libs/tests/GuestFopenLongHostPath.cpp b/core/libs/tests/GuestFopenLongHostPath.cpp index b23bff62b3..9ebe1738ce 100644 --- a/core/libs/tests/GuestFopenLongHostPath.cpp +++ b/core/libs/tests/GuestFopenLongHostPath.cpp @@ -54,6 +54,7 @@ void Run() { std::filesystem::path deep; ~Cleanup() { RemovePathAlias_nid_no_patch("/long-host-file"); + RemovePathAlias_nid_no_patch("/extended-host-file"); RemovePathAlias_nid_no_patch("/relative-long-host-file"); DeleteFileW(Extended(deep / "payload.bin").c_str()); DeleteFileW(Extended(deep / "new.bin").c_str()); @@ -73,9 +74,10 @@ void Run() { Require(prepared, "cannot write fixture payload"); const auto host = deep.string(); AddPathAlias_nid_no_patch("/long-host-file", host.c_str()); + AddPathAlias_nid_no_patch("/extended-host-file", Extended(deep).string().c_str()); Require(ResolvePath_nid_no_patch("/long-host-file/payload.bin").native().size() > MAX_PATH, "fixture does not cross MAX_PATH"); - { - auto stream = Open("/long-host-file/payload.bin", "rb"); + for (const auto* path : {"/long-host-file/payload.bin", "/extended-host-file/payload.bin"}) { + auto stream = Open(path, "rb"); std::array data{}; Require(fread_nid_postfix(data.data(), 1, data.size(), stream.get()) == 4 && data == std::array{'a', 'b', 'c', 'd'}, "long-path contents differ"); } From cf84fc44a8f969bb39cc079c0e9b8c7abfc27207 Mon Sep 17 00:00:00 2001 From: Romans Pokrovskis Date: Sat, 17 Oct 2026 19:13:32 +0300 Subject: [PATCH 0799/1000] fix(libc): preserve existing Windows path namespaces --- core/libs/prx/libc/src/FileAndHeap.cpp | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libc/src/FileAndHeap.cpp b/core/libs/prx/libc/src/FileAndHeap.cpp index 8807232d13..4f7330bc0c 100644 --- a/core/libs/prx/libc/src/FileAndHeap.cpp +++ b/core/libs/prx/libc/src/FileAndHeap.cpp @@ -69,12 +69,14 @@ static std::FILE* OpenNative(const std::filesystem::path& path, const std::strin : nativeMode.find('x') != std::string::npos ? CREATE_NEW : access == 'w' ? CREATE_ALWAYS : OPEN_ALWAYS; SECURITY_ATTRIBUTES security{sizeof(SECURITY_ATTRIBUTES), nullptr, TRUE}; auto filename = path.native(); - std::error_code pathError; - auto absolutePath = std::filesystem::absolute(path, pathError); - if (pathError) { errno = pathError.default_error_condition().value(); return nullptr; } - const auto absolute = absolutePath.lexically_normal().make_preferred().native(); - if (absolute.size() >= MAX_PATH && !absolute.starts_with(L"\\\\?\\") && !absolute.starts_with(L"\\\\.\\")) - filename = absolute.starts_with(L"\\\\") ? L"\\\\?\\UNC\\" + absolute.substr(2) : L"\\\\?\\" + absolute; + if (!filename.starts_with(L"\\\\?\\") && !filename.starts_with(L"\\\\.\\")) { + std::error_code pathError; + auto absolutePath = std::filesystem::absolute(path, pathError); + if (pathError) { errno = pathError.default_error_condition().value(); return nullptr; } + const auto absolute = absolutePath.lexically_normal().make_preferred().native(); + if (absolute.size() >= MAX_PATH) + filename = absolute.starts_with(L"\\\\") ? L"\\\\?\\UNC\\" + absolute.substr(2) : L"\\\\?\\" + absolute; + } const auto handle = CreateFileW(filename.c_str(), desired, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, &security, disposition, FILE_ATTRIBUTE_NORMAL, nullptr); if (handle == INVALID_HANDLE_VALUE) { From ee5ff02e7a65c5c6e9be9b059df726d481d4947f Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Sat, 17 Oct 2026 15:46:43 +0000 Subject: [PATCH 0800/1000] fix(libkernel): stat files under long Windows paths --- core/libs/prx/libkernel/File/src/Open.cpp | 18 ++++++++-- core/libs/tests/GuestFilesystem.cpp | 44 +++++++++++++++++++++++ 2 files changed, 60 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libkernel/File/src/Open.cpp b/core/libs/prx/libkernel/File/src/Open.cpp index 1f04eda811..3bb256a3fe 100644 --- a/core/libs/prx/libkernel/File/src/Open.cpp +++ b/core/libs/prx/libkernel/File/src/Open.cpp @@ -101,6 +101,15 @@ static int NativeUnlink(const std::filesystem::path& p) { errno = error; return -1; } +static std::filesystem::path NativeMetadataPath(const std::filesystem::path& path, std::error_code& error) { + auto absolute = std::filesystem::absolute(path, error); + if (error) return {}; + absolute = absolute.lexically_normal().make_preferred(); + const auto& native = absolute.native(); + if (native.size() < MAX_PATH || native.starts_with(L"\\\\?\\") || native.starts_with(L"\\\\.\\")) return absolute; + return native.starts_with(L"\\\\") ? std::filesystem::path(L"\\\\?\\UNC\\" + native.substr(2)) + : std::filesystem::path(L"\\\\?\\" + native); +} static int MapFlags(int sceFlags) { int f = 0; const int acc = sceFlags & SCE_KERNEL_O_ACCMODE; @@ -332,7 +341,12 @@ int APS5_VABI sceKernelStat(const char* path, FileStat* sb) { *sb = archived; return 0; } - const auto status = std::filesystem::status(native, error); + auto metadataPath = native; +#ifdef _WIN32 + metadataPath = NativeMetadataPath(native, error); + if (error) return SceErrorFromErrno(FilesystemError(error)); +#endif + const auto status = std::filesystem::status(metadataPath, error); if (error == std::errc::not_a_directory) return SceErrorFromErrno(GuestEnotdir); if (!std::filesystem::exists(status)) { return SceErrorFromErrno(2); @@ -340,7 +354,7 @@ int APS5_VABI sceKernelStat(const char* path, FileStat* sb) { const std::string_view guestPath(path); if (!guestPath.empty() && guestPath.back() == '/' && !std::filesystem::is_directory(status)) return SceErrorFromErrno(GuestEnotdir); if (sb == nullptr) return SceErrorFromErrno(GuestEfault); - File::FillFileStat(native, sb); + File::FillFileStat(metadataPath, sb); return 0; } diff --git a/core/libs/tests/GuestFilesystem.cpp b/core/libs/tests/GuestFilesystem.cpp index b87cf1344c..80558cd2f3 100644 --- a/core/libs/tests/GuestFilesystem.cpp +++ b/core/libs/tests/GuestFilesystem.cpp @@ -249,6 +249,47 @@ static void TestRelinkedModuleNameCase() { std::filesystem::remove_all(root); } +#ifdef _WIN32 +static std::filesystem::path ExtendedPath(const std::filesystem::path& path) { + const auto& native = path.native(); + if (native.starts_with(L"\\\\?\\") || native.starts_with(L"\\\\.\\")) return path; + return native.starts_with(L"\\\\") ? std::filesystem::path(L"\\\\?\\UNC\\" + native.substr(2)) + : std::filesystem::path(L"\\\\?\\" + native); +} + +static void TestLongHostPathStat() { + auto root = std::filesystem::absolute(std::filesystem::temp_directory_path()).lexically_normal(); + if (root.filename().empty()) root = root.parent_path(); + root /= L"anyps5-stat-long-" + std::to_wstring(std::chrono::steady_clock::now().time_since_epoch().count()); + auto deep = root; + while (deep.native().size() < MAX_PATH + 24) deep /= std::wstring(40, L'd'); + Require(::CreateDirectoryW(ExtendedPath(root).c_str(), nullptr) != 0); + auto directory = root; + for (const auto& component : std::filesystem::path(deep).lexically_relative(root)) { + directory /= component; + Require(::CreateDirectoryW(ExtendedPath(directory).c_str(), nullptr) != 0); + } + const auto file = deep / L"payload.bin"; + const auto handle = ::CreateFileW(ExtendedPath(file).c_str(), GENERIC_WRITE, 0, nullptr, CREATE_NEW, FILE_ATTRIBUTE_NORMAL, nullptr); + Require(handle != INVALID_HANDLE_VALUE); + DWORD written = 0; + const bool wrote = ::WriteFile(handle, "metadata", 8, &written, nullptr) != 0 && written == 8; + ::CloseHandle(handle); + Require(wrote); + const auto host = deep.string(); + AddPathAlias_nid_no_patch("/long-stat-host", host.c_str()); + FileStat status{}; + Require(ResolvePath_nid_no_patch("/long-stat-host/payload.bin").native().size() > MAX_PATH); + Require(sceKernelStat("/long-stat-host/payload.bin", &status) == 0 && status.st_size == 8); + RemovePathAlias_nid_no_patch("/long-stat-host"); + ::DeleteFileW(ExtendedPath(file).c_str()); + for (auto path = deep; ; path = path.parent_path()) { + ::RemoveDirectoryW(ExtendedPath(path).c_str()); + if (path == root) break; + } +} +#endif + #ifdef __linux__ static int DirectoryOpens(int watch) { alignas(inotify_event) char buffer[4096]; @@ -290,6 +331,9 @@ int main() { TestParentTraversal(); TestRelinkedModuleNames(); TestRelinkedModuleNameCase(); +#ifdef _WIN32 + TestLongHostPathStat(); +#endif #ifdef __linux__ TestMissingPathListsDirectoryOnce(); #endif From 2644234a2405fa53b9d4cfa351b4cea15b8d8127 Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Sat, 17 Oct 2026 15:50:18 +0000 Subject: [PATCH 0801/1000] test(libkernel): keep missing long stat paths as ENOENT --- core/libs/tests/GuestFilesystem.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/core/libs/tests/GuestFilesystem.cpp b/core/libs/tests/GuestFilesystem.cpp index 80558cd2f3..968fe80d5f 100644 --- a/core/libs/tests/GuestFilesystem.cpp +++ b/core/libs/tests/GuestFilesystem.cpp @@ -281,6 +281,7 @@ static void TestLongHostPathStat() { FileStat status{}; Require(ResolvePath_nid_no_patch("/long-stat-host/payload.bin").native().size() > MAX_PATH); Require(sceKernelStat("/long-stat-host/payload.bin", &status) == 0 && status.st_size == 8); + Require(sceKernelStat("/long-stat-host/missing.bin", &status) == static_cast(0x80020002u)); RemovePathAlias_nid_no_patch("/long-stat-host"); ::DeleteFileW(ExtendedPath(file).c_str()); for (auto path = deep; ; path = path.parent_path()) { From c0a6caeed322c950247103fc388f0a6046bc4bcc Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Sat, 17 Oct 2026 15:51:26 +0000 Subject: [PATCH 0802/1000] test(libkernel): preserve directory stat on long paths --- core/libs/tests/GuestFilesystem.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/core/libs/tests/GuestFilesystem.cpp b/core/libs/tests/GuestFilesystem.cpp index 968fe80d5f..c3e44eb6a5 100644 --- a/core/libs/tests/GuestFilesystem.cpp +++ b/core/libs/tests/GuestFilesystem.cpp @@ -282,6 +282,7 @@ static void TestLongHostPathStat() { Require(ResolvePath_nid_no_patch("/long-stat-host/payload.bin").native().size() > MAX_PATH); Require(sceKernelStat("/long-stat-host/payload.bin", &status) == 0 && status.st_size == 8); Require(sceKernelStat("/long-stat-host/missing.bin", &status) == static_cast(0x80020002u)); + Require(sceKernelStat("/long-stat-host/", &status) == 0 && (status.st_mode & 0170000) == 0040000); RemovePathAlias_nid_no_patch("/long-stat-host"); ::DeleteFileW(ExtendedPath(file).c_str()); for (auto path = deep; ; path = path.parent_path()) { From 28eaa5bdad25ec393110696ec94782bdcf8ea722 Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Sat, 17 Oct 2026 16:15:51 +0000 Subject: [PATCH 0803/1000] fix(libkernel): preserve extended paths in stat metadata --- core/libs/prx/libkernel/File/src/Open.cpp | 2 ++ core/libs/tests/GuestFilesystem.cpp | 6 ++++++ 2 files changed, 8 insertions(+) diff --git a/core/libs/prx/libkernel/File/src/Open.cpp b/core/libs/prx/libkernel/File/src/Open.cpp index 3bb256a3fe..23fd92fc59 100644 --- a/core/libs/prx/libkernel/File/src/Open.cpp +++ b/core/libs/prx/libkernel/File/src/Open.cpp @@ -102,6 +102,8 @@ static int NativeUnlink(const std::filesystem::path& p) { return -1; } static std::filesystem::path NativeMetadataPath(const std::filesystem::path& path, std::error_code& error) { + const auto& input = path.native(); + if (input.starts_with(L"\\\\?\\") || input.starts_with(L"\\\\.\\")) return path; auto absolute = std::filesystem::absolute(path, error); if (error) return {}; absolute = absolute.lexically_normal().make_preferred(); diff --git a/core/libs/tests/GuestFilesystem.cpp b/core/libs/tests/GuestFilesystem.cpp index c3e44eb6a5..e83f417faa 100644 --- a/core/libs/tests/GuestFilesystem.cpp +++ b/core/libs/tests/GuestFilesystem.cpp @@ -284,6 +284,12 @@ static void TestLongHostPathStat() { Require(sceKernelStat("/long-stat-host/missing.bin", &status) == static_cast(0x80020002u)); Require(sceKernelStat("/long-stat-host/", &status) == 0 && (status.st_mode & 0170000) == 0040000); RemovePathAlias_nid_no_patch("/long-stat-host"); + const auto extendedHost = ExtendedPath(deep).string(); + AddPathAlias_nid_no_patch("/long-stat-extended", extendedHost.c_str()); + Require(ResolvePath_nid_no_patch("/long-stat-extended/payload.bin").native().starts_with(L"\\\\?\\")); + Require(sceKernelStat("/long-stat-extended/payload.bin", &status) == 0 && status.st_size == 8); + Require(sceKernelStat("/long-stat-extended/missing.bin", &status) == static_cast(0x80020002u)); + RemovePathAlias_nid_no_patch("/long-stat-extended"); ::DeleteFileW(ExtendedPath(file).c_str()); for (auto path = deep; ; path = path.parent_path()) { ::RemoveDirectoryW(ExtendedPath(path).c_str()); From 78cabf5dcdc3845c8fb4c8dec7ecf3ce0537f734 Mon Sep 17 00:00:00 2001 From: Muhammad Wasif Ali Wasif <76695420+wachi35676@users.noreply.github.com> Date: Wed, 14 Oct 2026 01:21:17 +0500 Subject: [PATCH 0804/1000] fix(libc): keep a drive-letter component of a guest path under its mount on Windows A guest path is resolved by appending its components to the host root or to a mount alias directory through std::filesystem::path. On Windows a component such as "C:" that came first in the appended part was parsed as a root name, so operator/ replaced the host directory instead of appending: "/savedata0/C:/Windows/win.ini" and "/app0/../C:/Windows/win.ini" resolved to "C:\Windows\win.ini", and "/C:/x" lost the component and resolved to "\x". A colon is an ordinary character in a FreeBSD file name, so those names belong under the mount. Guest paths are now split into component names and joined to the host directory as text. Explicit native drive paths are still passed through. On NTFS a name containing ":" still opens an alternate data stream; the libc debt list now records it. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libc/CMakeLists.txt | 10 +++ core/libs/prx/libc/src/General.cpp | 86 ++++++++++++++------- core/libs/tests/GuestPathDriveComponent.cpp | 51 ++++++++++++ docs/dev/debt/libc.md | 1 + 4 files changed, 122 insertions(+), 26 deletions(-) create mode 100644 core/libs/tests/GuestPathDriveComponent.cpp diff --git a/core/libs/prx/libc/CMakeLists.txt b/core/libs/prx/libc/CMakeLists.txt index 4900ae6797..c5b0240cc3 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -321,6 +321,16 @@ if(BUILD_TESTING) set_tests_properties(guest_path_case PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_path_drive_component_tests ${LIBS_INCLUDE_DIR}/tests/GuestPathDriveComponent.cpp) + target_include_directories(guest_path_drive_component_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_path_drive_component_tests PRIVATE libc) + configure_windows_unwind(guest_path_drive_component_tests) + add_test(NAME guest_path_drive_component COMMAND guest_path_drive_component_tests) + set_tests_properties(guest_path_drive_component PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_path_drive_component PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_directory_tests ${LIBS_INCLUDE_DIR}/tests/GuestDirectory.cpp) target_include_directories(guest_directory_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_directory_tests PRIVATE libc) diff --git a/core/libs/prx/libc/src/General.cpp b/core/libs/prx/libc/src/General.cpp index 0f18419dfd..c9781df447 100644 --- a/core/libs/prx/libc/src/General.cpp +++ b/core/libs/prx/libc/src/General.cpp @@ -111,12 +111,39 @@ std::filesystem::path PathOrRelinkedModule(const std::filesystem::path& path) { return RelinkedModulePath_nid_no_patch(path); } -std::filesystem::path ResolveHostPath(std::filesystem::path root, const std::filesystem::path& relative, bool module = false) { - const auto direct = root / relative; +std::vector SplitNames(std::string_view text) { + std::vector names; + while (!text.empty()) { + const auto separator = text.find('/'); + names.emplace_back(text.substr(0, separator)); + if (separator == std::string_view::npos) break; + text.remove_prefix(separator + 1); + if (text.empty()) names.emplace_back(); + } + return names; +} + +std::string JoinNames(const std::vector& names) { + std::string joined; + for (std::size_t i = 0; i < names.size(); ++i) { + if (i != 0) joined += '/'; + joined += names[i]; + } + return joined; +} + +std::filesystem::path Child(std::filesystem::path parent, const std::string& name) { + parent /= ""; + parent += name; + return parent; +} + +std::filesystem::path ResolveHostPath(std::filesystem::path root, const std::vector& relative, bool module = false) { + const auto direct = Child(root, JoinNames(relative)); std::error_code error; if (std::filesystem::exists(direct, error)) return direct; for (auto part = relative.begin(); part != relative.end(); ++part) { - const auto candidate = root / *part; + const auto candidate = Child(root, *part); const auto status = std::filesystem::symlink_status(candidate, error); if (std::filesystem::exists(status) || (error && error != std::errc::no_such_file_or_directory)) { root = candidate; @@ -124,7 +151,7 @@ std::filesystem::path ResolveHostPath(std::filesystem::path root, const std::fil } const bool last = module && std::next(part) == relative.end(); const auto relinked = RelinkedModuleName(candidate); - NameMatch names[] = {{part->string()}, {relinked.filename().string()}}; + NameMatch names[] = {{*part}, {relinked.filename().string()}}; error = MatchNames(root, std::span(names, last ? 2 : 1)); if (names[0].ambiguous) throw std::runtime_error("Ambiguous case-insensitive guest path: " + candidate.string()); const bool matched = !error && names[0].path; @@ -152,12 +179,12 @@ std::optional ResolveAlias(const std::string& guestPath, } #ifndef _WIN32 if (guestPath.back() == '/') { - auto result = ResolveHostPath(host, relative.substr(prefix.size() + 1)); + auto result = ResolveHostPath(host, SplitNames(std::string_view(relative).substr(prefix.size() + 1))); result /= ""; return module ? PathOrRelinkedModule(result) : result; } #endif - return ResolveHostPath(host, relative.substr(prefix.size() + 1), module).make_preferred(); + return ResolveHostPath(host, SplitNames(std::string_view(relative).substr(prefix.size() + 1)), module).make_preferred(); } return std::nullopt; } @@ -171,8 +198,8 @@ WorkingDirectory& Directories() { static auto* state = new WorkingDirectory; return *state; } -std::optional ProvidedPath(const WorkingDirectory& state, const std::filesystem::path& guest) { - const auto relative = TrimSlashes(guest.relative_path().generic_string().c_str()); +std::optional ProvidedPath(const WorkingDirectory& state, const std::vector& names) { + const auto relative = TrimSlashes(JoinNames(names).c_str()); PathProvider provide = nullptr; { auto& aliases = Aliases(); @@ -182,20 +209,20 @@ std::optional ProvidedPath(const WorkingDirectory& state, } } if (provide == nullptr) return std::nullopt; - const auto supplied = ResolveHostPath(state.root, guest.relative_path()); + const auto supplied = ResolveHostPath(state.root, names); std::error_code error; if (std::filesystem::exists(supplied, error) || error) return supplied; auto provided = provide(state.root); if (provided.empty()) return std::nullopt; return provided.make_preferred(); } -std::filesystem::path HostPath(WorkingDirectory& state, const std::filesystem::path& guest, bool module = false) { - if (auto aliased = ResolveAlias(guest.relative_path().generic_string(), module)) return *aliased; - if (auto provided = ProvidedPath(state, guest)) return *provided; - return ResolveHostPath(state.root, guest.relative_path(), module).make_preferred(); +std::filesystem::path HostPath(WorkingDirectory& state, const std::vector& names, bool module = false) { + if (auto aliased = ResolveAlias(JoinNames(names), module)) return *aliased; + if (auto provided = ProvidedPath(state, names)) return *provided; + return ResolveHostPath(state.root, names, module).make_preferred(); } -void RequireHostDirectory(WorkingDirectory& state, const std::filesystem::path& guest) { - const auto host = HostPath(state, guest); +void RequireHostDirectory(WorkingDirectory& state, const std::vector& names) { + const auto host = HostPath(state, names); std::error_code error; const auto status = std::filesystem::status(host, error); if (std::filesystem::is_directory(status)) return; @@ -213,25 +240,32 @@ void RequireHostDirectory(WorkingDirectory& state, const std::filesystem::path& #endif throw std::filesystem::filesystem_error("Guest path component is not a directory", host, error); } +std::vector WithTrailingSeparator(std::vector names, bool trailing) { + if (trailing && !names.empty()) names.emplace_back(); + return names; +} std::filesystem::path Resolve(WorkingDirectory& state, const char* path, bool module = false, bool archived = false) { std::string text(path); for (auto& character : text) if (character == '\\') character = '/'; - std::filesystem::path input(text); #ifdef _WIN32 // Preserve the existing ability to pass explicit native drive paths. - if (input.has_root_name()) return module ? PathOrRelinkedModule(input) : input; + if (std::filesystem::path input(text); input.has_root_name()) return module ? PathOrRelinkedModule(input) : input; #endif - const auto current = std::filesystem::path("/") / state.current.lexically_relative(state.root); - const auto relative = (input.is_absolute() ? input : current / input).relative_path(); - std::filesystem::path guest("/"); - for (auto part = relative.begin(); part != relative.end(); ++part) { - if (*part == "..") { - const auto parent = guest.lexically_normal(); - if (parent.has_relative_path() && !(archived && AppArchiveHasDirectory_nid_no_patch(HostPath(state, parent)))) RequireHostDirectory(state, parent); + if (text.empty() || text.front() != '/') text = state.current.lexically_relative(state.root).generic_string() + "/" + text; + std::vector names; + bool trailing = true; + const auto parts = SplitNames(text); + for (std::size_t i = 0; i < parts.size(); ++i) { + const auto& name = parts[i]; + if (name.empty() && i + 1 != parts.size()) continue; + if (name == ".." && !names.empty()) { + if (!(archived && AppArchiveHasDirectory_nid_no_patch(HostPath(state, names)))) RequireHostDirectory(state, WithTrailingSeparator(names, trailing)); + names.pop_back(); } - guest /= *part; + trailing = name.empty() || name == "." || name == ".."; + if (!trailing) names.push_back(name); } - return HostPath(state, guest.lexically_normal(), module); + return HostPath(state, WithTrailingSeparator(std::move(names), trailing), module); } #ifdef _WIN32 using WriteMark = FILETIME; diff --git a/core/libs/tests/GuestPathDriveComponent.cpp b/core/libs/tests/GuestPathDriveComponent.cpp new file mode 100644 index 0000000000..c17bafcbfc --- /dev/null +++ b/core/libs/tests/GuestPathDriveComponent.cpp @@ -0,0 +1,51 @@ +#include "prx/libc/include/General.hpp" + +#include +#include +#include +#include +#include + +static void Require(bool value) { if (!value) std::abort(); } + +static std::string Under(const std::filesystem::path& directory, const std::string& names) { + auto text = directory.string(); + for (std::size_t start = 0; start <= names.size();) { + const auto end = std::min(names.find('/', start), names.size()); + text += static_cast(std::filesystem::path::preferred_separator); + text += names.substr(start, end - start); + start = end + 1; + } + return text; +} + +int main() { + const auto host = std::filesystem::canonical(std::filesystem::current_path()); + const auto name = "AnyPS5-Drive-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()); + const auto directory = host / name; + const auto mounted = directory / "Mounted"; + Require(std::filesystem::create_directories(mounted / "Nested")); + { std::ofstream file(directory / "Secret.txt"); file << 'x'; } + auto drive = host.root_name().string(); + if (drive.empty()) drive = "C:"; + std::string outside; + for (const auto& part : directory.relative_path()) outside += "/" + part.string(); + AddPathAlias_nid_no_patch("/drive-mount", mounted.string().c_str()); + + Require(ResolvePath_nid_no_patch(("/drive-mount/" + drive + outside + "/Secret.txt").c_str()).string() == + Under(mounted, drive + outside + "/Secret.txt")); + Require(ResolvePath_nid_no_patch(("/drive-mount/" + drive + "Nested").c_str()).string() == Under(mounted, drive + "Nested")); + Require(ResolvePath_nid_no_patch(("/drive-mount/" + drive).c_str()).string() == Under(mounted, drive)); + Require(ResolvePath_nid_no_patch(("/drive-mount/Nested/" + drive + "/x").c_str()).string() == Under(mounted, "Nested/" + drive + "/x")); + Require(ResolvePath_nid_no_patch(("/" + name + "/../" + drive + outside + "/Secret.txt").c_str()).string() == + Under(host, drive + outside + "/Secret.txt")); + Require(ResolvePath_nid_no_patch(("/" + drive + "/" + name + "/Secret.txt").c_str()).string() == + Under(host, drive + "/" + name + "/Secret.txt")); + Require(ResolvePath_nid_no_patch(("/" + drive).c_str()).string() == Under(host, drive)); + Require(ResolvePath_nid_no_patch(("/" + name + "/" + drive + "/x").c_str()).string() == Under(directory, drive + "/x")); + Require(ResolvePath_nid_no_patch(("/" + name + "/Secret.txt").c_str()) == directory / "Secret.txt"); + Require(ResolvePath_nid_no_patch("/drive-mount/Nested").string() == Under(mounted, "Nested")); + + RemovePathAlias_nid_no_patch("/drive-mount"); + std::filesystem::remove_all(directory); +} diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 79fa0585ce..af6fb19e5c 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -71,6 +71,7 @@ - [The scanf family](../../../core/libs/prx/libc/src/Formatting.cpp) (libc), the `_s` forms included, on Linux throws for the 16-bit wide conversions (`%ls`, `%lc`, `%l[`, `%S`, `%C`): glibc would store a 32-bit `wchar_t` per character into the guest's 16-bit buffer. Windows hands `%ls`, `%lc` and `%l[` to the host, whose `wchar_t` is 16 bits - [The scanf family](../../../core/libs/prx/libc/include/WindowsScanning.hpp) (libc) on Windows returns 0, as C17 and FreeBSD do, when input fails after only suppressed conversions have completed (`sscanf("12", "%*d%d", &value)`). Linux hands the format to glibc, which returns `EOF` there. On Windows the host stops a `%x`, `%X`, `%i` or `%p` after the `x` of a `0x` that no hex digit follows (or that the width cuts off), and a float conversion (`%e`, `%f`, `%g`, `%a`) after the `e` of an exponent with no digits (`1e`), where C17 and FreeBSD stop before the `x` or the `e` - [Guest path resolution](../../../core/libs/prx/libc/src/General.cpp) falls back to ASCII case-insensitive component lookup, following [shadPS4's host filesystem](https://github.com/shadps4-emu/shadPS4/blob/945dbc3cc3eee80ac3e053b438502ed936fa6bb2/src/core/file_sys/backends/host_fs.cpp). PPSA03747 requests `Skyrim.INI`, `Data/Video` and lowercase archive/plugin names that differ from the dump's filenames. Exact host names take precedence; multiple fallback matches throw instead of selecting an arbitrary file. Existing parent names are resolved when creating a new file, and guest mount aliases use the same ASCII comparison. Results are not cached, so renames and newly created files remain visible. Unicode case folding and PS5 behaviour for case-colliding host entries are unverified; host roots are used as supplied. Explicit native Windows drive and UNC paths bypass guest parent-component validation.. A relinked module's `.guest.prx` is matched the same way when the original module is not beside it: Borderlands 4 (PPSA01494) loads `/app0/prx/libRenoirCore.PS5.prx`, which it ships as `librenoircore.ps5.prx` +- [Colons in guest names](../../../core/libs/prx/libc/src/General.cpp) (libc) - on Windows (NTFS) a guest name containing `:` is not a plain file name: `name:stream` opens or creates the alternate data stream `stream` of the file `name`. Creating `/savedata0/C:file` makes an empty file `C` whose stream `file` holds the data, and `stat` of `/savedata0/C` then reports a regular file. `mkdir` of a name containing `:`, such as `C:` or `dir:x`, fails with EINVAL. `name:stream` already behaved this way before drive-letter components were kept under their mount - [tzname](../../../core/libs/prx/libc/src/Time.cpp) (libc) points at the host's zone names, read at load and again by `tzset`: the abbreviations a `TZ` string gives (`EST5EDT` is `EST` and `EDT` on both hosts), but on Windows without `TZ` the CRT's full names such as `GMT Standard Time`, which ICU's [uprv_tzname](https://github.com/unicode-org/icu/blob/457157a92aa053e632cc7fcfd0e12f8a943b2d11/icu4c/source/common/putil.cpp#L1244-L1264) (ES-DE in PPSA99169) cannot map to a zone ID. A zone with no daylight-saving time has `tzname[1]` empty on Windows and equal to `tzname[0]` on Linux, where FreeBSD leaves three spaces. The console's names are not known - [Trailing slash on a mount alias](../../../core/libs/prx/libc/src/General.cpp) (libc) - on Windows a trailing slash after a file name under a mount alias such as `/savedata0` is still dropped, so `unlink`, `rename`, `open` and the other path calls act on the file where FreeBSD 11.4's `lookup()` fails with `ENOTDIR`; Linux and macOS pass the slash to the host, as for paths outside an alias - [localtime, mktime](../../../core/libs/prx/libc/src/Time.cpp) (libc) on Windows convert through the CRT's `localtime_s` and `mktime`, which accept about 1970 to January 3001 UTC and apply the CRT's daylight-saving rule. A time outside that range is converted a whole number of 400-year Gregorian cycles away, where the calendar repeats, so it gets the rule the CRT applies to that year and not the zone's historical one. `gmtime` converts every time whose year fits an `int` on both hosts. From 521a68ad9e58c1f3c801eb24dd585fb60441cbc6 Mon Sep 17 00:00:00 2001 From: Muhammad Wasif Ali Wasif <76695420+wachi35676@users.noreply.github.com> Date: Sat, 17 Oct 2026 21:25:39 +0500 Subject: [PATCH 0805/1000] ci(build): build the pull requests in the register context and write watch jobs Both jobs checked out without a ref, so a dispatched build compiled and tested main instead of the selected pull requests, although the gate starts them because a pull request touched their files. They now check out the gate's main commit and merge the pull requests with the same step as the relinker, Linux and Windows jobs. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/build.yml | 38 +++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e5d2a9a22d..c8547322c9 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -275,6 +275,25 @@ jobs: timeout-minutes: 5 steps: - uses: actions/checkout@v7 + with: + ref: ${{ needs.eligibility.outputs.sha }} + persist-credentials: false + - name: Merge the pull request into main + if: ${{ needs.eligibility.outputs.head != '' }} + shell: bash + env: + PR_NUMBER: ${{ needs.eligibility.outputs.pr }} + PR_HEAD: ${{ needs.eligibility.outputs.head }} + run: | + read -ra numbers <<< "$PR_NUMBER" + read -ra heads <<< "$PR_HEAD" + refspecs=() + for number in "${numbers[@]}"; do refspecs+=("+refs/pull/$number/head:refs/remotes/pull/$number"); done + git fetch --no-tags --unshallow origin "${refspecs[@]}" + for i in "${!numbers[@]}"; do + if [ "$(git rev-parse "refs/remotes/pull/${numbers[i]}")" != "${heads[i]}" ]; then echo "::error::Pull request #${numbers[i]} has newer commits; start a new build"; exit 1; fi + if ! git -c user.name=ci -c user.email=ci@localhost merge --no-ff --no-edit "refs/remotes/pull/${numbers[i]}"; then echo "::error::Pull request #${numbers[i]} does not merge cleanly into main and the pull requests before it; rebase it first"; exit 1; fi + done - run: cmake -S core/libs/tests/register_context -B build/register-context -DCMAKE_BUILD_TYPE=${{ matrix.configuration }} - run: cmake --build build/register-context --parallel - run: ctest --test-dir build/register-context --output-on-failure --repeat until-fail:3 --no-tests=error @@ -290,6 +309,25 @@ jobs: timeout-minutes: 5 steps: - uses: actions/checkout@v7 + with: + ref: ${{ needs.eligibility.outputs.sha }} + persist-credentials: false + - name: Merge the pull request into main + if: ${{ needs.eligibility.outputs.head != '' }} + shell: bash + env: + PR_NUMBER: ${{ needs.eligibility.outputs.pr }} + PR_HEAD: ${{ needs.eligibility.outputs.head }} + run: | + read -ra numbers <<< "$PR_NUMBER" + read -ra heads <<< "$PR_HEAD" + refspecs=() + for number in "${numbers[@]}"; do refspecs+=("+refs/pull/$number/head:refs/remotes/pull/$number"); done + git fetch --no-tags --unshallow origin "${refspecs[@]}" + for i in "${!numbers[@]}"; do + if [ "$(git rev-parse "refs/remotes/pull/${numbers[i]}")" != "${heads[i]}" ]; then echo "::error::Pull request #${numbers[i]} has newer commits; start a new build"; exit 1; fi + if ! git -c user.name=ci -c user.email=ci@localhost merge --no-ff --no-edit "refs/remotes/pull/${numbers[i]}"; then echo "::error::Pull request #${numbers[i]} does not merge cleanly into main and the pull requests before it; rebase it first"; exit 1; fi + done - run: cmake -S core/libs/tests/write_watch -B build/write-watch -DCMAKE_BUILD_TYPE=Release - run: cmake --build build/write-watch --parallel - run: ctest --test-dir build/write-watch --output-on-failure --repeat until-fail:3 --no-tests=error From d71a1c5775b1c44eb86d5ef41efc78fd087ebec0 Mon Sep 17 00:00:00 2001 From: MarcZX100 <211792885+MarcZX100@users.noreply.github.com> Date: Sat, 17 Oct 2026 16:28:55 +0000 Subject: [PATCH 0806/1000] fix(libkernel): accept fractional nanosecond kevent periods --- core/libs/prx/libkernel/Equeue/src/Kqueue.cpp | 3 --- core/libs/tests/GuestKqueue.cpp | 7 +++++++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libkernel/Equeue/src/Kqueue.cpp b/core/libs/prx/libkernel/Equeue/src/Kqueue.cpp index ae13538695..a1d0396298 100644 --- a/core/libs/prx/libkernel/Equeue/src/Kqueue.cpp +++ b/core/libs/prx/libkernel/Equeue/src/Kqueue.cpp @@ -98,9 +98,6 @@ std::optional TimerPeriodNanos(std::int64_t data, std::uint32_t f unit = 1000ULL; break; case NOTE_NSECONDS: - if (data >= 1000000000LL && data % 1000000000LL != 0) { - NotImplemented_nid_no_patch("kevent: EVFILT_TIMER NOTE_NSECONDS period above one second with a fraction"); - } unit = 1; break; default: diff --git a/core/libs/tests/GuestKqueue.cpp b/core/libs/tests/GuestKqueue.cpp index aecb4a9885..008d2f97a5 100644 --- a/core/libs/tests/GuestKqueue.cpp +++ b/core/libs/tests/GuestKqueue.cpp @@ -67,6 +67,7 @@ static constexpr std::uint32_t NoteTrigger = 0x01000000; static constexpr std::uint32_t NoteFfcopy = 0xc0000000; static constexpr std::uint32_t NoteFfor = 0x80000000; static constexpr std::uint32_t NoteUseconds = 0x0004; +static constexpr std::uint32_t NoteNseconds = 0x0008; static constexpr std::uint32_t NoteDelete = 0x0001; static constexpr std::uint32_t NoteWrite = 0x0002; static constexpr std::uint32_t NoteExtend = 0x0004; @@ -167,6 +168,12 @@ static void Timers() { Require(Poll(kq, events, 4) == 0); RequireFailure(Apply(kq, Change(2, EvfiltTimer, EvDelete)), GuestEnoent); + Require(Apply(kq, Change(5, EvfiltTimer, EvAdd | EvOneshot, NoteNseconds, 1500000001)) == 0); + const KernelTimespec twoSeconds{2, 0}; + Require(kevent_nid_postfix(kq, nullptr, 0, events, 4, &twoSeconds) == 1); + Require(events[0].ident == 5 && events[0].data == 1); + Require(Poll(kq, events, 4) == 0); + RequireFailure(Apply(kq, Change(3, EvfiltTimer, EvAdd, 0, -1)), GuestEinval); RequireFailure(Apply(kq, Change(3, EvfiltTimer, EvAdd, 0x10, 5)), GuestEinval); RequireFailure(Apply(kq, Change(3, EvfiltTimer, EvAdd, NoteUseconds | 0x1, 5)), GuestEinval); From 6e0886d76d0300960139ee806bbee7b2167f206d Mon Sep 17 00:00:00 2001 From: milescoviello Date: Sat, 17 Oct 2026 16:32:08 +0000 Subject: [PATCH 0807/1000] perf(shader): drop the result flush of a product behind its tiny-product check A v_mul_f32 in a FLOAT_MODE that flushes f32 denormals emits the nearest-even tininess check, the NaN result selection and then a result flush. The check already turns every product that rounds below 2^-126 into a zero of its sign, so the flush can't change what it keeps. DenormalFlushEliminator now proves that: a select arm guarded by the tiny-product check of the same denormal-free factors, an arm a select takes only when its own bits are NaN, and an and with a mask that has no significand bits are denormal-free. Co-Authored-By: Claude Opus 5.5 --- .../tests/DenormalFlushElimination.cpp | 123 ++++++++++++++++++ .../src/DenormalFlushEliminator.cpp | 62 ++++++++- docs/dev/debt/shader.md | 1 + 3 files changed, 183 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/DenormalFlushElimination.cpp b/core/libs/prx/libSceAgcDriver/tests/DenormalFlushElimination.cpp index 3db6a8ce08..cc952f5ea9 100644 --- a/core/libs/prx/libSceAgcDriver/tests/DenormalFlushElimination.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/DenormalFlushElimination.cpp @@ -1,11 +1,20 @@ #include "IntermediateRepresentation/IrProgram.hpp" #include "Optimization/DenormalFlushEliminator.hpp" +#include "Recompiler.hpp" +#include +#include #include #include #include #include #include +#include +#include +#include +#include +#include #include +#include using namespace ShaderRecompiler; @@ -80,6 +89,46 @@ bool run(const char* name, const std::function& test) { } } +std::vector Recompiled(std::span code) { + const std::array userData{0x10000000u, 0x00100000u, 0x40u, 0x00027facu}; + const std::array capabilities{1u, 61u}; + const std::array extensions{"SPV_KHR_storage_buffer_storage_class"}; + RecompileRequest request{}; + request.shader = {ShaderStage::Compute, 0x20000u, code, 0, {}}; + request.context.waveSize = 32; + request.context.userDataBaseRegister = 0; + request.context.userData = userData; + request.context.compute = ShaderComputeStageInfo{{64u, 1u, 1u}, 0u, {false, false, false}, false, 1u}; + request.target.vulkanVersion = 0x00403000u; + request.target.spirvVersion = 0x00010600u; + request.target.subgroupSize = 32u; + request.target.bdaAbiVersion = 1; + request.target.supportedCapabilities = capabilities; + request.target.supportedExtensions = extensions; + request.target.maxWorkgroupSize = {1024u, 1024u, 64u}; + request.target.maxWorkgroupInvocations = 1024; + request.target.maxWorkgroupSharedMemoryBytes = 49152; + request.layout = {0, 0, 0, 128}; + request.useCache = false; + const auto& words = Recompile(request).spirv.Words(); + return {words.begin(), words.end()}; +} + +std::size_t ExponentMasks(std::span words) { + if (words.size() < 5 || words[0] != spv::MagicNumber) throw std::runtime_error("the recompiler returned no SPIR-V module"); + std::set exponents; + std::size_t masks = 0; + for (std::size_t offset = 5; offset < words.size();) { + const auto count = words[offset] >> spv::WordCountShift; + if (count == 0 || count > words.size() - offset) throw std::runtime_error("the recompiler returned a malformed SPIR-V instruction"); + const auto opcode = words[offset] & spv::OpCodeMask; + if (opcode == spv::OpConstant && count == 4 && words[offset + 3] == 0x7f800000u) exponents.insert(words[offset + 2]); + if (opcode == spv::OpBitwiseAnd && count == 5 && (exponents.contains(words[offset + 3]) || exponents.contains(words[offset + 4]))) ++masks; + offset += count; + } + return masks; +} + } int main() { @@ -252,5 +301,79 @@ int main() { return loop(true) && !loop(false); }); + passed &= run("a product behind its round-to-nearest tiny-product guard needs no flush, one behind another guard does", [] { + Builder b; + const auto value = [&](IrValue& bits) -> IrValue& { return b.emit(IrOpcode::BitCastF32U32, IrType::F32, {&bits}); }; + const auto bitsOf = [&](IrValue& number) -> IrValue& { return b.emit(IrOpcode::BitCastU32F32, IrType::U32, {&number}); }; + const auto guarded = [&](IrValue& lhs, IrValue& rhs, IrValue& guardLhs, IrValue& guardRhs, std::uint32_t threshold) -> IrValue& { + auto& product = b.emit(IrOpcode::FPMul32, IrType::F32, {&lhs, &rhs}); + auto& scaled = b.emit(IrOpcode::FPMul32, IrType::F32, {&b.emit(IrOpcode::FPMul32, IrType::F32, {&guardLhs, &b.constant(0x5f800000u)}), &guardRhs}); + auto& magnitude = b.emit(IrOpcode::BitwiseAnd32, IrType::U32, {&bitsOf(scaled), &b.constant(0x7fffffffu)}); + auto& tiny = b.emit(IrOpcode::ULessThan32, IrType::Bool, {&b.emit(IrOpcode::ISub32, IrType::U32, {&magnitude, &b.constant(1u)}), &b.constant(threshold)}); + auto& sign = b.emit(IrOpcode::BitwiseAnd32, IrType::U32, {&b.emit(IrOpcode::BitwiseXor32, IrType::U32, {&bitsOf(lhs), &bitsOf(rhs)}), &b.constant(0x80000000u)}); + return b.flush(bitsOf(b.emit(IrOpcode::SelectF32, IrType::F32, {&tiny, &value(sign), &product}))); + }; + auto& x = value(b.flush(b.unknown())); + auto& y = value(b.flush(b.unknown())); + auto& raw = value(b.unknown()); + IrValue* removed[] = {&guarded(x, y, x, y, 0x207fffffu), &guarded(x, y, y, x, 0x207fffffu)}; + IrValue* kept[] = {&guarded(x, y, x, raw, 0x207fffffu), &guarded(x, y, x, y, 0x20800000u), &guarded(x, raw, x, raw, 0x207fffffu)}; + for (auto* flushed : removed) b.keep(*flushed); + for (auto* flushed : kept) b.keep(*flushed); + const bool counted = b.eliminate() == 2u; + bool flushes = true; + for (auto* flushed : removed) flushes &= !flushed->HasUses(); + for (auto* flushed : kept) flushes &= flushed->HasUses(); + return counted && flushes; + }); + + passed &= run("an arm a select takes only when its bits are NaN needs no flush, one under another value's NaN test does", [] { + Builder b; + auto& x = b.unknown(); + auto& other = b.unknown(); + const auto magnitude = [&](IrValue& bits) -> IrValue& { return b.emit(IrOpcode::BitwiseAnd32, IrType::U32, {&bits, &b.constant(0x7fffffffu)}); }; + const auto isNan = [&](IrValue& bits) -> IrValue& { return b.emit(IrOpcode::UGreaterThan32, IrType::Bool, {&magnitude(bits), &b.constant(0x7f800000u)}); }; + const auto chosen = [&](IrValue& condition, IrValue& arm) -> IrValue& { return b.flush(b.emit(IrOpcode::SelectU32, IrType::U32, {&condition, &arm, &b.flush(b.unknown())})); }; + IrValue* removed[] = { + &chosen(isNan(x), x), + &chosen(isNan(x), b.emit(IrOpcode::BitwiseOr32, IrType::U32, {&x, &b.constant(0x00400000u)})), + &chosen(b.emit(IrOpcode::FPIsNan32, IrType::Bool, {&b.emit(IrOpcode::BitCastF32U32, IrType::F32, {&x})}), x), + }; + IrValue* kept[] = { + &chosen(isNan(other), x), + &chosen(b.emit(IrOpcode::UGreaterThan32, IrType::Bool, {&b.constant(0x7f800000u), &magnitude(x)}), x), + }; + for (auto* flushed : removed) b.keep(*flushed); + for (auto* flushed : kept) b.keep(*flushed); + const bool counted = b.eliminate() == 3u; + bool flushes = true; + for (auto* flushed : removed) flushes &= !flushed->HasUses(); + for (auto* flushed : kept) flushes &= flushed->HasUses(); + return counted && flushes; + }); + + passed &= run("the sign or exponent bits of any value need no flush, a mask that keeps significand bits does", [] { + Builder b; + auto& sign = b.flush(b.emit(IrOpcode::BitwiseAnd32, IrType::U32, {&b.unknown(), &b.constant(0x80000000u)})); + auto& exponent = b.flush(b.emit(IrOpcode::BitwiseAnd32, IrType::U32, {&b.constant(0xff800000u), &b.unknown()})); + auto& significand = b.flush(b.emit(IrOpcode::BitwiseAnd32, IrType::U32, {&b.unknown(), &b.constant(0x807fffffu)})); + b.keep(sign); + b.keep(exponent); + b.keep(significand); + return b.eliminate() == 2u && !sign.HasUses() && !exponent.HasUses() && significand.HasUses(); + }); + + passed &= run("two chained v_mul_f32 keep only the input flush of the first source", [] { + const std::array code{ + 0x10020100u, + 0x10040301u, + 0xe0700000u, 0x80000200u, + 0xbf810000u, + }; + const auto masks = ExponentMasks(Recompiled(code)); + if (masks != 1u) std::fprintf(stderr, " %zu f32 exponent masks, expected 1\n", masks); + return masks == 1u; + }); + return passed ? 0 : 1; } diff --git a/core/shader/recompiler/Optimization/src/DenormalFlushEliminator.cpp b/core/shader/recompiler/Optimization/src/DenormalFlushEliminator.cpp index d448da5e6a..f8a013e4b7 100644 --- a/core/shader/recompiler/Optimization/src/DenormalFlushEliminator.cpp +++ b/core/shader/recompiler/Optimization/src/DenormalFlushEliminator.cpp @@ -19,6 +19,58 @@ const IrValue* maskedOperand(const IrValue* value, IrOpcode opcode, std::uint32_ return nullptr; } +const IrValue* rightOperand(const IrValue* value, IrOpcode opcode, std::uint32_t bits) { + if (value->Opcode() != opcode || value->ArgumentCount() != 2u) return nullptr; + return isImmediate(value->Argument(1)->Resolve(), bits) ? value->Argument(0)->Resolve() : nullptr; +} + +const IrValue* bitsOf(const IrValue* value) { + value = value->Resolve(); + while ((value->Opcode() == IrOpcode::BitCastU32F32 || value->Opcode() == IrOpcode::BitCastF32U32) && value->ArgumentCount() == 1u) value = value->Argument(0)->Resolve(); + return value; +} + +bool testsForNan(const IrValue* condition, const IrValue* bits) { + condition = condition->Resolve(); + if (condition->Opcode() == IrOpcode::FPIsNan32 && condition->ArgumentCount() == 1u) return bitsOf(condition->Argument(0)) == bits; + const IrValue* magnitude = rightOperand(condition, IrOpcode::UGreaterThan32, 0x7f800000u); + if (magnitude != nullptr) magnitude = maskedOperand(magnitude, IrOpcode::BitwiseAnd32, 0x7fffffffu); + return magnitude != nullptr && bitsOf(magnitude) == bits; +} + +bool takenOnlyAsNan(const IrValue* condition, const IrValue* arm) { + const IrValue* bits = bitsOf(arm); + if (const IrValue* quieted = maskedOperand(bits, IrOpcode::BitwiseOr32, 0x00400000u)) bits = bitsOf(quieted); + return testsForNan(condition, bits); +} + +const IrValue* tinyGuardedProduct(const IrValue* condition, const IrValue* arm) { + const IrValue* product = arm->Resolve(); + if (product->Opcode() != IrOpcode::FPMul32 || product->ArgumentCount() != 2u) return nullptr; + const IrValue* offset = rightOperand(condition->Resolve(), IrOpcode::ULessThan32, 0x207fffffu); + const IrValue* magnitude = offset != nullptr ? rightOperand(offset, IrOpcode::ISub32, 1u) : nullptr; + const IrValue* scaled = magnitude != nullptr ? maskedOperand(magnitude, IrOpcode::BitwiseAnd32, 0x7fffffffu) : nullptr; + if (scaled == nullptr) return nullptr; + scaled = bitsOf(scaled); + if (scaled->Opcode() != IrOpcode::FPMul32 || scaled->ArgumentCount() != 2u) return nullptr; + const IrValue* lhs = product->Argument(0)->Resolve(); + const IrValue* rhs = product->Argument(1)->Resolve(); + for (std::size_t index = 0; index < 2u; ++index) { + const IrValue* factor = maskedOperand(scaled->Argument(index)->Resolve(), IrOpcode::FPMul32, 0x5f800000u); + const IrValue* other = scaled->Argument(1u - index)->Resolve(); + if (factor != nullptr && ((factor == lhs && other == rhs) || (factor == rhs && other == lhs))) return product; + } + return nullptr; +} + +bool clearsSignificand(const IrValue& value) { + for (std::size_t index = 0; index < value.ArgumentCount(); ++index) { + const IrValue* mask = value.Argument(index)->Resolve(); + if (mask->HasImmediate() && (mask->ImmediateU32() & 0x007fffffu) == 0u) return true; + } + return false; +} + IrValue* flushedSource(const IrValue& value) { if (value.Opcode() != IrOpcode::SelectU32 || value.ArgumentCount() != 3u) return nullptr; IrValue* source = value.Argument(2)->Resolve(); @@ -65,10 +117,14 @@ private: return false; case IrOpcode::BitwiseAnd32: if (const IrValue* operand = maskedOperand(&value, IrOpcode::BitwiseAnd32, 0x7fffffffu)) return Contains(operand); - return false; + return value.ArgumentCount() == 2u && clearsSignificand(value); case IrOpcode::SelectU32: - case IrOpcode::SelectF32: - return Contains(value.Argument(1)) && Contains(value.Argument(2)); + case IrOpcode::SelectF32: { + const IrValue* condition = value.Argument(0); + const IrValue* product = tinyGuardedProduct(condition, value.Argument(2)); + const bool taken = takenOnlyAsNan(condition, value.Argument(1)) || Contains(value.Argument(1)); + return taken && (product != nullptr ? Contains(product->Argument(0)) && Contains(product->Argument(1)) : Contains(value.Argument(2))); + } case IrOpcode::FPMin32: case IrOpcode::FPMax32: case IrOpcode::FPMinTri32: diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index 89fa8f70d7..3f379e5cca 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -42,6 +42,7 @@ - Comparison sampling of a color texture through an [image table](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) S# throws at the first dispatch or draw that binds it. - An [image table](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) key with no key domain whose `key * stride` wraps past 2^32 to an offset between records faults only when the build already expects a fault; otherwise the fault buffer is not read and it reads zeros. - [MaskedSelectEliminator](../../../core/shader/recompiler/Optimization/src/MaskedSelectEliminator.cpp) can discard inactive source lanes through `ReadFirstLane` only when its read mask implies the write mask and its result is unobserved outside the read mask. A loop-local read mask requires result uses in the same block without phi transport. The empty-EXEC lane-zero fallback follows AMD's [Vega ISA, section 12.8, V_READFIRSTLANE_B32, page 127](https://www.amd.com/content/dam/amd/en/documents/radeon-tech-docs/instruction-set-architectures/vega-shader-instruction-set-architecture.pdf) and the existing SPIR-V emitter. The optimization is covered by synthetic IR tests but has not been compared with console hardware; a proof that needs a read mask of a kind that is already being solved is refused. +- [DenormalFlushEliminator](../../../core/shader/recompiler/Optimization/src/DenormalFlushEliminator.cpp) drops the result flush of a `v_mul_f32` or `v_mul_legacy_f32` product behind its nearest-even tininess check (see f32 denormals) when both factors are denormal-free: a product the check keeps is then zero, infinite, NaN or at least 2^-126 in magnitude, provided the driver rounds it as it rounds the check's multiply. - [Shader modules built at registration](../../../core/shader/recompiler/Recompiler.cpp) (shader recompiler): on devices with graphics pipeline libraries a vertex or pixel module reads buffer V# words, image modes, swizzles and mip ranges, depth-compare state, export mappings and vertex component selectors at run time instead of specializing on them per draw, so its libraries can be built when the shader is registered. A module that would exceed `SpirvTarget::registrationWordLimit` (200000 SPIR-V words) is emitted specialized as before: on RADV (Radeon 660M, Mesa 26.2.4) a sample of 60 Astro's Playroom pixel shaders compiled 1.9 to 2.2 times slower in registration form below 141k words and 3.8 to 5.7 times slower above 280k words (none in between), so the limit sits in that gap; it was not measured on other drivers. Texel-offset folding, the format word of a buffer accessed with the descriptor's format (`buffer_load_format_*`; as runtime values every format's decoder is compiled, which made CI's lavapipe JIT ten times slower) and the vertex attribute numeric class stay specialized, so shaders with the first two, and pixel shaders whose image needs a multisampled descriptor, get no registration result; these, and every draw whose fixed-function state differs from the prebuild's, still build their libraries at the draw. Compute and the other stages stay specialized: in registration form the `agc_driver_dword_pattern_fill` compute shader hung CI's lavapipe (Mesa 25.2.8) when built with `ANYPS5_ENABLE_SPIRV_TOOLS=ON`, not on RADV, and the cause was not found. - [SPIRV-Tools optimization](../../../core/shader/recompiler/SpirvBackend/src/SpirvOptimizer.cpp) skips CCP and Simplification for modules with storage-only 8- or 16-bit types because the pinned folder produces illegal narrow constants. This also disables unrelated constant folding; the restricted full pipeline mirrors the pinned performance recipe. - [SRT signed comparison](../../../core/shader/recompiler/Optimization/src/SrtWalker/SrtEvaluator.cpp) evaluates `SLessThan32` as signed 32-bit less-than, matching the existing SPIR-V backend and [SPIR-V `OpSLessThan`](https://registry.khronos.org/SPIR-V/specs/unified1/SPIRV.html#OpSLessThan). Signed boundary descriptor expressions are tested on the host; native RDNA2/PS5 execution and title behavior remain unverified. From 14db22d458cc32f68f5f783e7b840e4a9586bbb2 Mon Sep 17 00:00:00 2001 From: alosta Date: Sat, 17 Oct 2026 17:35:07 +0100 Subject: [PATCH 0808/1000] test(libSceRudp): add GuestRudpContextLifecycle behavioral tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds tests for nine implemented libSceRudp functions that previously lacked coverage: - sceRudpActivate (returns 0 always) - sceRudpEnableInternalIOThread (NOT_INITIALIZED before init, 0 after) - sceRudpSetEventHandler (returns 0) - sceRudpBind (NOT_INITIALIZED, INVALID_CONTEXT_ID, 0 on success) - sceRudpSetOption (NOT_INITIALIZED, INVALID_CONTEXT_ID, 0 on success) - sceRudpInitiate (NOT_INITIALIZED, INVALID_CONTEXT_ID, CONN_REFUSED) - sceRudpGetContextStatus (NOT_INITIALIZED, INVALID_CONTEXT_ID, fills status with 0, null status allowed) - sceRudpWrite (NOT_INITIALIZED, INVALID_CONTEXT_ID, CONN_RESET) - sceRudpRead (NOT_INITIALIZED, INVALID_CONTEXT_ID, CONN_RESET) Each assertion mirrors the exact implementation in Export.cpp. Error codes and return values are the constants hardcoded in the source. No console measurement needed — these are pure offline behaviors. AI-assisted: yes --- core/libs/CMakeLists.txt | 10 ++ core/libs/tests/GuestRudpContextLifecycle.cpp | 113 ++++++++++++++++++ 2 files changed, 123 insertions(+) create mode 100644 core/libs/tests/GuestRudpContextLifecycle.cpp diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index 365bc34820..5af7eee300 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -384,6 +384,16 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() + add_test_executable(guest_rudp_context_lifecycle_tests tests/GuestRudpContextLifecycle.cpp) + target_include_directories(guest_rudp_context_lifecycle_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(guest_rudp_context_lifecycle_tests PRIVATE libSceRudp libc) + configure_windows_unwind(guest_rudp_context_lifecycle_tests) + add_test(NAME guest_rudp_context_lifecycle COMMAND guest_rudp_context_lifecycle_tests) + if(WIN32) + set_tests_properties(guest_rudp_context_lifecycle PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_memory_pool_stats_tests tests/GuestMemoryPoolStats.cpp) target_include_directories(guest_memory_pool_stats_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(guest_memory_pool_stats_tests PRIVATE libSceSsl libSceHttp2 libSceNet libkernel libc) diff --git a/core/libs/tests/GuestRudpContextLifecycle.cpp b/core/libs/tests/GuestRudpContextLifecycle.cpp new file mode 100644 index 0000000000..f043d30a99 --- /dev/null +++ b/core/libs/tests/GuestRudpContextLifecycle.cpp @@ -0,0 +1,113 @@ +#include "SceTypes.hpp" + +#include +#include +#include +#include +#include + +extern "C" { +using GuestRudpEventHandler = void(APS5_VABI*)(int, int, int, void*); +int APS5_VABI sceRudpInit_nid_postfix(void*, int); +int APS5_VABI sceRudpEnd(); +int APS5_VABI sceRudpActivate(); +int APS5_VABI sceRudpEnableInternalIOThread(std::uint32_t, std::uint32_t); +int APS5_VABI sceRudpSetEventHandler(GuestRudpEventHandler, void*); +int APS5_VABI sceRudpCreateContext(GuestRudpEventHandler, void*, int*); +int APS5_VABI sceRudpBind(int, std::uint16_t, int, std::uint16_t); +int APS5_VABI sceRudpSetOption(int, int, const void*, std::uint32_t); +int APS5_VABI sceRudpInitiate(int, const void*, std::uint32_t, std::uint16_t); +int APS5_VABI sceRudpGetContextStatus(int, void*, std::uint32_t); +int APS5_VABI sceRudpWrite(int, const void*, std::uint32_t, int); +int APS5_VABI sceRudpRead(int, void*, std::uint32_t, int, void*); +int APS5_VABI sceRudpTerminate(int); +} + +static void Require(bool value) { if (!value) std::abort(); } + +static void APS5_VABI Handler(int, int, int, void*) {} + +int main() { + constexpr int notInitialized = static_cast(0x80770001u); + constexpr int invalidContextId = static_cast(0x80770003u); + constexpr int connRefused = static_cast(0x8077000Au); + constexpr int connReset = static_cast(0x80770009u); + alignas(16) static unsigned char pool[0x10000]; + + Require(sceRudpActivate() == 0); + + Require(sceRudpEnableInternalIOThread(0, 0) == notInitialized); + + Require(sceRudpSetEventHandler(Handler, nullptr) == 0); + + Require(sceRudpBind(0, 0, 0, 0) == notInitialized); + Require(sceRudpBind(12345, 0, 0, 0) == notInitialized); + + Require(sceRudpSetOption(0, 0, nullptr, 0) == notInitialized); + Require(sceRudpSetOption(12345, 0, nullptr, 0) == notInitialized); + + Require(sceRudpInitiate(0, nullptr, 0, 0) == notInitialized); + Require(sceRudpInitiate(12345, nullptr, 0, 0) == notInitialized); + + { + unsigned char status[64] = {}; + Require(sceRudpGetContextStatus(0, status, sizeof(status)) == notInitialized); + Require(sceRudpGetContextStatus(12345, status, sizeof(status)) == notInitialized); + } + + Require(sceRudpWrite(0, nullptr, 0, 0) == notInitialized); + Require(sceRudpWrite(12345, nullptr, 0, 0) == notInitialized); + + Require(sceRudpRead(0, nullptr, 0, 0, nullptr) == notInitialized); + Require(sceRudpRead(12345, nullptr, 0, 0, nullptr) == notInitialized); + + Require(sceRudpInit_nid_postfix(pool, sizeof(pool)) == 0); + + Require(sceRudpActivate() == 0); + + Require(sceRudpEnableInternalIOThread(0, 0) == 0); + Require(sceRudpEnableInternalIOThread(1024, 1) == 0); + + Require(sceRudpBind(12345, 0, 0, 0) == invalidContextId); + + Require(sceRudpSetOption(12345, 0, nullptr, 0) == invalidContextId); + + Require(sceRudpInitiate(12345, nullptr, 0, 0) == invalidContextId); + + { + unsigned char status[64] = {}; + Require(sceRudpGetContextStatus(12345, status, sizeof(status)) == invalidContextId); + } + + Require(sceRudpWrite(12345, nullptr, 0, 0) == invalidContextId); + + Require(sceRudpRead(12345, nullptr, 0, 0, nullptr) == invalidContextId); + + int ctx = -1; + Require(sceRudpCreateContext(Handler, nullptr, &ctx) == 0); + Require(ctx >= 0); + + Require(sceRudpBind(ctx, 0, 0, 0) == 0); + + Require(sceRudpSetOption(ctx, 0, nullptr, 0) == 0); + + Require(sceRudpInitiate(ctx, nullptr, 0, 0) == connRefused); + + { + unsigned char status[64]; + std::memset(status, 0x5a, sizeof(status)); + Require(sceRudpGetContextStatus(ctx, status, sizeof(status)) == 0); + for (std::size_t i = 0; i < sizeof(status); ++i) Require(status[i] == 0); + } + Require(sceRudpGetContextStatus(ctx, nullptr, 64) == 0); + + Require(sceRudpWrite(ctx, nullptr, 0, 0) == connReset); + + Require(sceRudpRead(ctx, nullptr, 0, 0, nullptr) == connReset); + + Require(sceRudpSetEventHandler(Handler, nullptr) == 0); + + Require(sceRudpTerminate(ctx) == 0); + Require(sceRudpEnd() == 0); + return 0; +} \ No newline at end of file From 7c71fc603a962a43d7662b88360f890d83c8b110 Mon Sep 17 00:00:00 2001 From: unknown Date: Sat, 17 Oct 2026 18:46:12 +0200 Subject: [PATCH 0809/1000] fix(libSceHttp): make URI escaping independent of host locale --- core/libs/prx/libSceHttp/src/Uri.cpp | 3 ++- core/libs/tests/GuestHttp.cpp | 30 ++++++++++++++++++++++++++++ docs/dev/debt/libSceHttp.md | 2 ++ 3 files changed, 34 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceHttp/src/Uri.cpp b/core/libs/prx/libSceHttp/src/Uri.cpp index 710a665726..f0c935f845 100644 --- a/core/libs/prx/libSceHttp/src/Uri.cpp +++ b/core/libs/prx/libSceHttp/src/Uri.cpp @@ -205,7 +205,8 @@ int APS5_VABI sceHttpUriEscape(char* out, size_t* require, size_t prepare, const std::string escaped; for (const char* c = in; *c; ++c) { const auto value = static_cast(*c); - if (std::isalnum(value) || value == '-' || value == '_' || value == '.' || value == '~') { + if ((value >= 'a' && value <= 'z') || (value >= 'A' && value <= 'Z') || (value >= '0' && value <= '9') + || value == '-' || value == '_' || value == '.' || value == '~') { escaped.push_back(*c); } else { escaped.push_back('%'); diff --git a/core/libs/tests/GuestHttp.cpp b/core/libs/tests/GuestHttp.cpp index aa184acce1..e4555bd5cc 100644 --- a/core/libs/tests/GuestHttp.cpp +++ b/core/libs/tests/GuestHttp.cpp @@ -2,10 +2,12 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include #include +#include #include #include #include #include +#include #include extern "C" { @@ -45,6 +47,27 @@ static void Require(bool value) { if (!value) std::abort(); } static bool Equal(const char* left, const char* right) { return std::strcmp(left, right) == 0; } +static void CheckUriEscapeBytes() { + constexpr char unreserved[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~"; + constexpr char hex[] = "0123456789ABCDEF"; + for (unsigned value = 1; value <= 255; ++value) { + const char input[] = {static_cast(value), '\0'}; + char expected[] = {'%', hex[value >> 4], hex[value & 15], '\0'}; + if (std::strchr(unreserved, input[0])) { + expected[0] = input[0]; + expected[1] = '\0'; + } + const auto size = std::strlen(expected) + 1; + std::size_t required = 0; + Require(sceHttpUriEscape(nullptr, &required, 0, input) == 0 && required == size); + char output[5] = {'Z', 'Z', 'Z', 'Z', 'Z'}; + Require(sceHttpUriEscape(output, &required, size - 1, input) == static_cast(0x80431022)); + for (char byte : output) Require(byte == 'Z'); + Require(sceHttpUriEscape(output, &required, size, input) == 0 && required == size); + Require(Equal(output, expected) && output[size] == 'Z'); + } +} + static int AuthInfo(int, int, const char*, char*, char*, int, std::uint8_t**, std::uint64_t*, int*, void*) { std::abort(); } static void APS5_VABI RequestStatusCallback(int, int, void* argument) { ++*static_cast(argument); @@ -137,6 +160,13 @@ int main() { Require(sceHttpUriEscape(escaped, nullptr, sizeof(escaped), "a b/~\xC3\xA9") == 0); Require(Equal(escaped, "a%20b%2F~%C3%A9")); Require(sceHttpUriEscape(escaped, nullptr, sizeof(escaped), nullptr) == invalidValue); + CheckUriEscapeBytes(); +#ifdef _WIN32 + const std::string previousCtype = std::setlocale(LC_CTYPE, nullptr); + Require(std::setlocale(LC_CTYPE, ".1252") != nullptr); + CheckUriEscapeBytes(); + Require(std::setlocale(LC_CTYPE, previousCtype.c_str()) != nullptr); +#endif constexpr const char* unescapeCases[][2] = { {"", ""}, {"plain+text", "plain+text"}, {"a%20b%2F~%c3%a9", "a b/~\xC3\xA9"}, diff --git a/docs/dev/debt/libSceHttp.md b/docs/dev/debt/libSceHttp.md index 25cae419d8..c260aa6a53 100644 --- a/docs/dev/debt/libSceHttp.md +++ b/docs/dev/debt/libSceHttp.md @@ -21,4 +21,6 @@ ### Functional +- [sceHttpUriEscape](../../../core/libs/prx/libSceHttp/src/Uri.cpp) escapes every byte except the ASCII unreserved set in [RFC 3986 section 2.3](https://www.rfc-editor.org/rfc/rfc3986#section-2.3), independently of the host locale. Console behaviour for non-ASCII input remains unmeasured. + - [sceHttpAbortWaitRequest, sceHttpWaitRequest](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - the abort takes only the epoll handle, as in [Orbital](https://github.com/AlexAltea/orbital/blob/master/tools/ida/db_types.json) and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Net/Http.cs); behaviour and error codes follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/http.cpp) (PS4), not confirmed on a PS5 title or hardware. An abort stays set: every later wait on the handle, a poll included, returns `SCE_HTTP_ERROR_ABORTED` (0x80431080) until the handle is destroyed. A wait woken by `sceHttpDestroyEpoll`, or by the `sceHttpTerm` that ends the last context, returns 0 events. The epoll calls return `BEFORE_INIT` (0x80431001) while no `sceHttpInit` context is alive and `INVALID_ID` (0x80431100) for an unknown context or handle, and `sceHttpTerm` returns the same errors instead of succeeding for any id. Handles are pointers, so a stale handle whose address a newer epoll reuses is taken as that epoll From f0d68fb4363d60aeb6ecf81e5ba62753cf963f72 Mon Sep 17 00:00:00 2001 From: simonbronk Date: Sat, 17 Oct 2026 18:46:36 +0200 Subject: [PATCH 0810/1000] fix(libc): reject null secure scan destinations Prevent reached null conversion destinations from crashing sscanf_s and vsscanf_s. Return EOF before writes and retain earlier assignments. Add regression coverage through both guest ABI entry points. AI-assisted: yes (OpenAI Codex). Windows WinLibs GCC 15.2 Release: production baseline crashes with the new regression; fixed shared libc passes guest_scan_count, guest_formatting, guest_wide_formatting and guest_strings (4/4). Conventions pass. Full all-target build was deliberately stopped after 83s with 3691 pending targets, not completed. No title or console run; constraint-handler invocation remains unimplemented as documented. No performance or coverage-percentage claim. --- core/libs/prx/libc/src/Formatting.cpp | 6 ++++- core/libs/tests/GuestScanCount.cpp | 39 +++++++++++++++++++++++++++ docs/dev/debt/libc.md | 2 ++ 3 files changed, 46 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libc/src/Formatting.cpp b/core/libs/prx/libc/src/Formatting.cpp index 85f2d9ed8d..7a77dad2de 100644 --- a/core/libs/prx/libc/src/Formatting.cpp +++ b/core/libs/prx/libc/src/Formatting.cpp @@ -142,6 +142,7 @@ int ScanGuest(const char* buffer, const char* format, bool secure, NextPointer n if (conversion == 'n') { if (!suppress) { void* target = nextPointer(); + if (target == nullptr) return EOF; const auto count = input - buffer; if (length == "hh") *static_cast(target) = static_cast(count); else if (length == "h") *static_cast(target) = static_cast(count); @@ -163,7 +164,10 @@ int ScanGuest(const char* buffer, const char* format, bool secure, NextPointer n std::string directive = "%"; void* target = nullptr; if (suppress) directive += '*'; - else target = nextPointer(); + else { + target = nextPointer(); + if (target == nullptr) return EOF; + } if (sized && !suppress) { const auto capacity = static_cast(nextCapacity()); const unsigned long limit = conversion == 'c' ? capacity : (capacity == 0 ? 0 : capacity - 1); diff --git a/core/libs/tests/GuestScanCount.cpp b/core/libs/tests/GuestScanCount.cpp index f77e73b62e..1ddfe19674 100644 --- a/core/libs/tests/GuestScanCount.cpp +++ b/core/libs/tests/GuestScanCount.cpp @@ -1,5 +1,7 @@ #include "prx/libc/include/general/VabiMacros.hpp" +#include "SceTypes.hpp" #include +#include #include #include #include @@ -7,6 +9,24 @@ #include extern "C" int APS5_VABI sscanf_s_nid_postfix(const char*, const char*, ...); +extern "C" int APS5_VABI vsscanf_s_nid_postfix(const char*, const char*, VaList*); + +static int APS5_VABI ScanList(const char* input, const char* format, ...) { +#ifdef _WIN32 + __builtin_sysv_va_list args; + __builtin_sysv_va_start(args, format); +#else + std::va_list args; + va_start(args, format); +#endif + const int result = vsscanf_s_nid_postfix(input, format, reinterpret_cast(args)); +#ifdef _WIN32 + __builtin_sysv_va_end(args); +#else + va_end(args); +#endif + return result; +} static int failures = 0; @@ -36,6 +56,25 @@ template static void CheckCount(const char* modifier, int count) { } int main() { + for (const auto scan : {sscanf_s_nid_postfix, ScanList}) { + Require(scan("7", "%d", static_cast(nullptr)) == EOF, "Null integer destination must return EOF"); + Require(scan("1.5", "%f", static_cast(nullptr)) == EOF, "Null real destination must return EOF"); + Require(scan("abc", "%s", static_cast(nullptr), 4u) == EOF, "Null string destination must return EOF"); + Require(scan("a", "%c", static_cast(nullptr), 1u) == EOF, "Null character destination must return EOF"); + Require(scan("abc", "%[a-z]", static_cast(nullptr), 4u) == EOF, "Null scanset destination must return EOF"); + Require(scan("", "%n", static_cast(nullptr)) == EOF, "Null count destination must return EOF"); + int first = -1; + int later = -1; + Require(scan("7 8 9", "%d%d%d", &first, static_cast(nullptr), &later) == EOF && first == 7 && later == -1, + "Null destination after an assignment must stop with EOF"); + first = -1; + Require(scan("7", "%d%n%d", &first, static_cast(nullptr), &later) == EOF && first == 7 && later == -1, + "Null count after an assignment must stop with EOF"); + first = -1; + Require(scan("x", "%d%d", &first, static_cast(nullptr)) == 0 && first == -1, + "Matching failure must stop before an unreachable null destination"); + Require(scan("7 8", "%*d%d", &first) == 1 && first == 8, "Suppressed conversions must not consume destinations"); + } for (const int count : {0, 3, 127}) { CheckCount("hh", count); CheckCount("h", count); diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 79fa0585ce..271be4e4cb 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -38,6 +38,8 @@ ### Functional +- [sscanf_s and vsscanf_s](../../../core/libs/prx/libc/src/Formatting.cpp) (libc) return `EOF` when a reached conversion has a null destination, stopping before further assignments, following C11 K.3.5.3.7 and K.3.5.3.14. Earlier assignments are retained. The constraint handler is not invoked, and console behaviour is unverified. + - [__isfinitel](../../../core/libs/prx/libc/src/Math.cpp) - classifies the x87 80-bit exponent without narrowing to double or performing floating-point comparisons, following [FreeBSD msun](https://github.com/freebsd/freebsd-src/blob/main/lib/msun/src/s_isfinite.c). The signature and classification follow FreeBSD; console behaviour is unmeasured. Requires little-endian x87 long double stored in 16 bytes, as the existing guest long-double ABI does. - Windows direct-memory backing reserves a paging-file section. A [direct memory page](../../../core/libs/prx/libc/include/WindowsMappings.hpp) mapped at one guest address is private write-watched memory at that address and is not committed in the section; a page mapped at a second address moves its whole resident run into the section (committed there) and every address becomes a view of it, tracked by write protection. An unmapped resident page that is not all zeros is kept in a private reservation until it is mapped again. Committed section contents remain across unmap/remap and are released when the backing and its remaining views are destroyed. A host import alias over part of a resident run is refused. Linux retains its existing backing. Peak gameplay memory and Windows versions other than the tested host remain unverified. From 9d12efa1c6045265211155d7176e0ac3364da774 Mon Sep 17 00:00:00 2001 From: Dextroid17 <217507631+Dextroid17@users.noreply.github.com> Date: Sat, 17 Oct 2026 12:53:05 -0400 Subject: [PATCH 0811/1000] test(libkernel): stagger the leaving-thread raise by the clock, not by yields --- core/libs/tests/GuestRaiseException.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/libs/tests/GuestRaiseException.cpp b/core/libs/tests/GuestRaiseException.cpp index c1b6d0a298..1f882c248d 100644 --- a/core/libs/tests/GuestRaiseException.cpp +++ b/core/libs/tests/GuestRaiseException.cpp @@ -613,7 +613,8 @@ int main() { leaving.started.store(false); std::this_thread::sleep_for(std::chrono::microseconds(200)); Require(sceKernelSignalSema(leaving.sem, 1) == 0); - for (int spin = 0; spin < round % 16 * 64; ++spin) std::this_thread::yield(); + const auto offset = std::chrono::steady_clock::now() + std::chrono::microseconds(round % 16 * 8); + while (std::chrono::steady_clock::now() < offset) {} Require(sceKernelRaiseException(leavingThread, SIGUSR1) == 0); ExpectDelivery(1 + 2 * Repeats + HostRounds + round, leaving.id); leavingRound.store(round + 1); From 20502c00226ee80dda385aeb1edf2639e3c954b9 Mon Sep 17 00:00:00 2001 From: thuanquang Date: Sat, 17 Oct 2026 23:58:59 +0700 Subject: [PATCH 0812/1000] feat(agc): render 5_6_5 UNORM color targets Goosebumps Terror in Little Creek (PPSA27305) aborts in gameplay with "unsupported color format 16 number type 0 component swap 2": it renders into a COLOR_5_6_5 UNORM target. The target now renders into R5G6B5_UNORM_PACK16. AMD keeps X in bits 0-4 and Z in bits 11-15, and Mesa's ac_translate_colorswap programs SWAP_STD_REV for a surface with red in bits 11-15, so with SWAP_STD_REV the exports are written as they are and with SWAP_STD they are written with X and Z exchanged, as for 1_5_5_5. The alternate swaps and other number types keep throwing. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 2 + .../libSceAgcDriver/Graphics/src/State.cpp | 5 +- .../prx/libSceAgcDriver/tests/Graphics.cpp | 30 ++ .../tests/execution/Color565Target.cpp | 307 ++++++++++++++++++ docs/dev/debt/libSceAgcDriver.md | 1 + 5 files changed, 344 insertions(+), 1 deletion(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/Color565Target.cpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index c6950a121b..759b9c74fb 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1487,6 +1487,8 @@ target_include_directories(agc_driver_packed_unorm_color_target_tests PRIVATE $< target_include_directories(agc_driver_color1555_target_tests PRIVATE $) +target_include_directories(agc_driver_color565_target_tests PRIVATE $) + foreach(stage IN ITEMS vert frag) set(agcProvokingShaderSource ${CMAKE_CURRENT_SOURCE_DIR}/tests/shaders/ProvokingVertex.${stage}) set(agcProvokingShaderSpv ${agcTestShaderGeneratedRoot}/ProvokingVertex_${stage}.spv) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index 279258b2af..853808e9c6 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -753,7 +753,7 @@ DecodedColorFormat DecodeColorFormat(std::uint32_t format, std::uint32_t number, }; const bool alternate = swap == 1; const auto single = [&](VkFormat vkFormat, std::uint32_t bytes) { return DecodedColorFormat{vkFormat, bytes, static_cast((0xe4u & ~3u) | swap)}; }; - if (swap > 1 && format != 1 && format != 2 && format != 4 && format != 10 && format != 12) return fail(); + if (swap > 1 && format != 1 && format != 2 && format != 4 && format != 10 && format != 12 && format != 16) return fail(); switch (format) { case 1: if (number == unorm) return single(VK_FORMAT_R8_UNORM, 1); @@ -821,6 +821,9 @@ DecodedColorFormat DecodeColorFormat(std::uint32_t format, std::uint32_t number, if (number == floating) return {VK_FORMAT_R32G32B32A32_SFLOAT, 16}; if (number == uint) return {VK_FORMAT_R32G32B32A32_UINT, 16}; return fail(); + case 16: + if ((swap != 0 && swap != 2) || number != unorm) return fail(); + return {VK_FORMAT_R5G6B5_UNORM_PACK16, 2, static_cast(swap == 2 ? 0xe4u : 0xc6u)}; case 17: if (swap != 0 || number != unorm) return fail(); return {VK_FORMAT_A1R5G5B5_UNORM_PACK16, 2, 0xc6u}; diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index cbba137501..a4951d46cf 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -1694,6 +1694,35 @@ void ReversedBlendRoutingTests() { Require(native == 121u && routed == 932u, "the reversed-order blend routing accepts " + std::to_string(native) + " native and " + std::to_string(routed) + " routed factor combinations, not 121 and 932"); } +void Color565TargetTests() { + auto queue = makeState(); + const auto info = [&](std::uint32_t number, std::uint32_t swap) { + queue.context[0x31c] = (queue.context[0x31c] & ~((0x1fu << 2u) | (7u << 8u) | (3u << 11u))) | (16u << 2u) | (number << 8u) | (swap << 11u); + }; + info(0, 2); + const auto reversed = AgcDriver::Graphics::DecodeState(queue); + Require(reversed.colors.size() == 1 && reversed.colors[0].format == VK_FORMAT_R5G6B5_UNORM_PACK16 && reversed.colors[0].elementBytes == 2 && reversed.colors[0].componentMapping == 0xe4u, "a 5_6_5 target with the reversed component order did not render through R5G6B5 with X in its high bits"); + Require(reversed.blends[0].colorWriteMask == 0xfu, "a 5_6_5 target with the reversed component order did not write all its channels"); + queue.context[0x8e] = 0x1u; + Require(AgcDriver::Graphics::DecodeState(queue).blends[0].colorWriteMask == VK_COLOR_COMPONENT_R_BIT, "a 5_6_5 target with the reversed component order writing only X did not write the R5G6B5 channel in its high bits"); + info(0, 0); + const auto standard = AgcDriver::Graphics::DecodeState(queue); + Require(standard.colors[0].format == VK_FORMAT_R5G6B5_UNORM_PACK16 && standard.colors[0].componentMapping == 0xc6u, "a 5_6_5 target with the standard component order did not render through R5G6B5 with X and Z exchanged"); + Require(AgcDriver::Graphics::ExportMappings(standard)[0] == 0xc6u, "a 5_6_5 target with the standard component order did not export through the X and Z exchange"); + Require(standard.blends[0].colorWriteMask == VK_COLOR_COMPONENT_B_BIT, "a 5_6_5 target with the standard component order writing only X did not write the R5G6B5 channel in its low bits"); + queue.context[0x8e] = 0xfu; + info(0, 2); + for (std::uint32_t i = 0; i < 4; ++i) queue.context[0x105 + i] = 0; + queue.context[0x1e0] = 0x40000504u; + const auto blended = AgcDriver::Graphics::DecodeState(queue); + Require(blended.blends[0].blendEnable && blended.blends[0].srcColorBlendFactor == VK_BLEND_FACTOR_SRC_ALPHA && blended.blends[0].dstColorBlendFactor == VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA, "alpha blending into a 5_6_5 target was not decoded"); + queue.context[0x1e0] = 0; + for (const auto& [number, swap] : {std::pair{1u, 2u}, std::pair{4u, 0u}, std::pair{0u, 1u}, std::pair{0u, 3u}}) { + info(number, swap); + expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "unsupported color format 16 number type " + std::to_string(number) + " component swap " + std::to_string(swap)); + } +} + void Color1555TargetTests() { auto queue = makeState(); queue.context[0x31c] = (queue.context[0x31c] & ~((0x1fu << 2u) | (7u << 8u) | (3u << 11u))) | (17u << 2u); @@ -8178,6 +8207,7 @@ int main(int argc, char** argv) { ConservativeRasterizationTests(); DisabledColorTests(); OpaqueDestinationAlphaTests(); + Color565TargetTests(); Color1555TargetTests(); BlendBypassTests(); CompactedExportTests(); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/Color565Target.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/Color565Target.cpp new file mode 100644 index 0000000000..f320df84eb --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/Color565Target.cpp @@ -0,0 +1,307 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp" +#include "prx/libSceAgcDriver/Graphics/include/State.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" +#include "Recompiler.hpp" +#include "Optimization/ResourceProgram.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::ColorTargetLayout; +using AgcDriver::Graphics::ColorTileMode; +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Width = 256; +constexpr std::uint32_t Height = 128; +constexpr std::uint32_t ScissorWidth = 128; +constexpr std::size_t BlockBytes = 65536; +constexpr std::uint32_t Color565StandardInfo = 0x8040u; +constexpr std::uint32_t Color565ReversedInfo = 0x9040u; +constexpr std::uint32_t TiledAttrib3 = 0x4dc6c000u; +constexpr std::uint32_t LinearAttrib3 = 0x09000000u; +constexpr std::uint32_t Float32Export = 9; + +alignas(256) constexpr std::array VertexCode{ + 0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, +}; + +constexpr std::array, 3> Triangle{{ + {-1.0f, -1.0f, 0.5f, 1.0f}, {3.0f, -1.0f, 0.5f, 1.0f}, {-1.0f, 3.0f, 0.5f, 1.0f} +}}; + +alignas(256) std::array, 16> Programs{}; +std::size_t programCount = 0; + +using Color = std::array; + +std::uint16_t Texel(const Color& color, bool reversed) { + const auto unorm = [](float value, std::uint32_t maximum) { return static_cast(std::lround(std::fmin(std::fmax(static_cast(value), 0.0), 1.0) * maximum)); }; + const auto low = unorm(color[reversed ? 2 : 0], 31); + const auto high = unorm(color[reversed ? 0 : 2], 31); + return static_cast(low | (unorm(color[1], 63) << 5u) | (high << 11u)); +} + +std::span Program(const Color& color) { + Require(programCount < Programs.size(), "too many test programs"); + auto& code = Programs[programCount++]; + std::size_t at = 0; + for (std::uint32_t channel = 0; channel < 4; ++channel) { + code[at++] = 0x7e0002ffu | ((4u + channel) << 17u); + code[at++] = std::bit_cast(color[channel]); + } + code[at++] = 0xf800180fu; + code[at++] = 0x07060504u; + code[at++] = 0xbf810000u; + return std::span(code.data(), at); +} + +struct Block { + explicit Block(std::size_t bytes) : bytes(bytes), watched(AgcDriver::GuestMemory::WriteWatched()) { +#ifdef _WIN32 + if (watched) { + data = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(bytes, BlockBytes)); + GuestArena::GuestArenaCommit_nid_postfix(data, bytes, PAGE_READWRITE, bytes); + } else { + data = static_cast(VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); + } +#else + if (watched) { + void* raw = mmap(nullptr, bytes + BlockBytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + Require(raw != MAP_FAILED, "cannot map the color target"); + const auto begin = reinterpret_cast(raw); + const auto aligned = (begin + BlockBytes - 1) & ~(static_cast(BlockBytes) - 1); + if (aligned != begin) munmap(raw, aligned - begin); + if (aligned + bytes != begin + bytes + BlockBytes) munmap(reinterpret_cast(aligned + bytes), begin + BlockBytes - aligned); + data = reinterpret_cast(aligned); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(data, bytes); + } else { + data = static_cast(std::aligned_alloc(BlockBytes, bytes)); + } +#endif + Require(data != nullptr, "cannot allocate the color target"); + Require(!watched || AgcDriver::GuestMemory::Watched(Address(), bytes), "the color target is not write-watched"); + GuestAllocations::Mutation mutation; + mutation.Add(data, bytes, true, true, true); + } + ~Block() { + AgcDriver::Graphics::StorageTexture::FlushPending(Address(), bytes, nullptr, "test release"); + { + GuestAllocations::Mutation mutation; + mutation.Remove(data); + } +#ifdef _WIN32 + if (watched) { + GuestArena::GuestArenaReset_nid_postfix(data, bytes); + GuestArena::GuestArenaRelease_nid_postfix(data, bytes); + } else { + VirtualFree(data, 0, MEM_RELEASE); + } +#else + if (watched) { + munmap(data, bytes); + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(data, bytes); + } else { + std::free(data); + } +#endif + } + Block(const Block&) = delete; + Block& operator=(const Block&) = delete; + std::uint64_t Address() const { return reinterpret_cast(data); } + std::size_t bytes; + bool watched; + std::uint8_t* data = nullptr; +}; + +AgcDriver::Graphics::ColorTarget DecodeTarget(const Block& block, std::uint32_t attrib3, std::uint32_t info) { + AgcDriver::Registers cx; + cx[0x318] = static_cast(block.Address() >> 8u); + cx[0x31b] = 0; + cx[0x31c] = info; + cx[0x31d] = 0; + cx[0x3b0] = ((Width - 1u) << 14u) | (Height - 1u); + cx[0x3b8] = attrib3; + cx[0x390] = static_cast(block.Address() >> 40u); + auto color = AgcDriver::Graphics::DecodeColorBuffer(cx, 0); + Require(color.format == VK_FORMAT_R5G6B5_UNORM_PACK16 && color.elementBytes == 2 && color.bytes == block.bytes, "the 5_6_5 unorm target did not decode as an R5G6B5 surface"); + return color; +} + +void Draw(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::ColorTarget& color, std::uint32_t exportFormat, std::span pixelCode) { + const auto target = device.Target(); + constexpr std::uint32_t waveSize = 64; + std::vector vertexUserData(4, 0u); + const auto address = reinterpret_cast(Triangle.data()); + const std::array vertexBuffer{static_cast(address), static_cast((address >> 32u) & 0xffffu) | (16u << 16u), static_cast(Triangle.size()), 0x01016facu}; + std::copy(vertexBuffer.begin(), vertexBuffer.end(), vertexUserData.begin()); + const std::array vertexMemory{{{reinterpret_cast(VertexCode.data()), std::as_bytes(std::span(VertexCode))}}}; + ShaderRecompiler::RecompileRequest vertex{ + {ShaderStage::Vertex, reinterpret_cast(VertexCode.data()), VertexCode, 0, {}}, + {waveSize, 0, vertexUserData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, vertexMemory}, + target, + {0, 0, 0, 64} + }; + vertex.useCache = false; + const auto vertexResult = ShaderRecompiler::Recompile(vertex); + const auto vertexPush = static_cast(vertexResult.pushConstants.size()); + + AgcDriver::Graphics::State state{}; + state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, waveSize, waveSize, std::nullopt, std::nullopt}; + state.colors = {color}; + state.color = color; + state.hasColorTarget = true; + state.renderExtent = {Width, Height}; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.viewport = {0, static_cast(Height), static_cast(Width), -static_cast(Height), 0, 1}; + state.negativeOneToOne = false; + state.scissor = {{0, 0}, {ScissorWidth, Height}}; + state.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + VkPipelineColorBlendAttachmentState blend{}; + blend.colorWriteMask = 0xf; + state.blends = {blend}; + state.blend = blend; + state.blendConstants = {}; + + const auto codeAddress = reinterpret_cast(pixelCode.data()); + const AgcDriver::Registers context{{0x1b6, 0x8000}, {0x1b3, 0}, {0x1b4, 0}, {0x203, 0}, {0x1c5, exportFormat}}; + const std::array pixelMemory{{{codeAddress, std::as_bytes(pixelCode)}}}; + ShaderRecompiler::RecompileRequest fragment{ + {ShaderStage::Fragment, codeAddress, pixelCode, 0, {}}, + {waveSize, 0, {}, std::nullopt, AgcDriver::Graphics::DecodePixelStageInfo(context, {}), std::nullopt, pixelMemory}, + target, + PixelPushLayout(vertexPush, target) + }; + const auto handle = ShaderRecompiler::PrepareShader(fragment); + AgcDriver::DriverDetail::ShaderSnapshot snapshot{codeAddress, 0, 1, {pixelCode.begin(), pixelCode.end()}, {}}; + snapshot.prepared->entries.push_back({0, handle}); + fragment.shader.code = snapshot.code; + fragment.context.pixel = AgcDriver::Graphics::DecodePixelStageInfo(context, AgcDriver::Graphics::ExportMappings(state)); + const ShaderRecompiler::SrtRuntime runtime{}; + const auto materialize = [&](const ShaderRecompiler::RecompileRequest& request) { + const auto invocation = AgcDriver::DriverDetail::InvocationFor(snapshot, 0, request); + return invocation.Materialize(*invocation.Capture(runtime)); + }; + const auto pixelResult = materialize(fragment); + Require(pixelResult->variantId == ShaderRecompiler::GetPreparedArtifact(*handle).variantId, "the draw did not use the registered pixel shader"); + const std::array shaders{{ + {ShaderStage::Vertex, &vertexResult, 0}, + {ShaderStage::Fragment, pixelResult.get(), PixelPushOffset(vertexPush, target)} + }}; + const AgcDriver::Pm4::DrawParameters draw{0, static_cast(Triangle.size()), 0, 1, 0, false}; + device.Draw(state, draw, shaders); +} + +std::vector ReadBack(AgcDriver::VulkanDevice& device, const Block& block) { + AgcDriver::Graphics::StorageTexture::FlushPending(block.Address(), block.bytes, nullptr, "test read-back"); + device.WaitIdle(); + std::vector stored(block.bytes / 2u); + AgcDriver::GuestMemory::Read(block.Address(), std::as_writable_bytes(std::span(stored)), 1); + return stored; +} + +std::vector Fill(Block& block) { + std::vector halves(block.bytes / 2u); + for (std::size_t index = 0; index < halves.size(); ++index) halves[index] = static_cast((index * 0x9e3779b9u) >> 16u); + std::memcpy(block.data, halves.data(), block.bytes); + return halves; +} + +void ExpectSurface(AgcDriver::VulkanDevice& device, const Block& block, const ColorTargetLayout& layout, const std::vector& before, std::uint16_t texel, const std::string& what) { + const auto stored = ReadBack(device, block); + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + const auto index = layout.Offset(x, y) / 2u; + const bool inside = x < ScissorWidth; + const auto expected = inside ? texel : before[index]; + if (stored[index] == expected) continue; + char detail[160]; + std::snprintf(detail, sizeof(detail), ": pixel (%u, %u) %s the scissor holds 0x%04x, expected 0x%04x", x, y, inside ? "inside" : "outside", stored[index], expected); + throw std::runtime_error(what + detail); + } + } +} + +void TargetTests(AgcDriver::VulkanDevice& device, Block& block, std::uint32_t attrib3, ColorTileMode mode, bool reversed, const char* kind) { + const ColorTargetLayout layout(Width, Height, mode, 2); + Require(layout.Bytes() == block.bytes, "the test surface size disagrees with its layout"); + const auto color = DecodeTarget(block, attrib3, reversed ? Color565ReversedInfo : Color565StandardInfo); + auto before = Fill(block); + struct Case { + Color color; + std::uint16_t reversedTexel; + std::uint16_t standardTexel; + }; + const std::array cases{{ + {{1.0f, 0.0f, 0.0f, 1.0f}, 0xf800, 0x001f}, + {{0.0f, 1.0f, 0.0f, 0.0f}, 0x07e0, 0x07e0}, + {{0.0f, 0.0f, 1.0f, 0.0f}, 0x001f, 0xf800}, + {{1.0f, 0.0f, 1.0f, 0.5f}, 0xf81f, 0xf81f}, + {{1.0f, 1.0f, 1.0f, 1.0f}, 0xffff, 0xffff}, + {{0.0f, 0.0f, 0.0f, 1.0f}, 0x0000, 0x0000}, + {{3.0f / 31.0f, 40.0f / 63.0f, 21.0f / 31.0f, 1.0f}, (3u << 11u) | (40u << 5u) | 21u, 3u | (40u << 5u) | (21u << 11u)}, + {{21.0f / 31.0f, 3.0f / 63.0f, 10.0f / 31.0f, 0.0f}, (21u << 11u) | (3u << 5u) | 10u, 21u | (3u << 5u) | (10u << 11u)}, + {{1.0f / 31.0f, 62.0f / 63.0f, 30.0f / 31.0f, 0.75f}, (1u << 11u) | (62u << 5u) | 30u, 1u | (62u << 5u) | (30u << 11u)}, + {{30.0f / 31.0f, 1.0f / 63.0f, 1.0f / 31.0f, 0.25f}, (30u << 11u) | (1u << 5u) | 1u, 30u | (1u << 5u) | (1u << 11u)}, + {{2.0f, -1.0f, 10.0f / 31.0f, 0.25f}, (31u << 11u) | 10u, 31u | (10u << 11u)}, + }}; + for (const auto& test : cases) { + const auto& value = test.color; + const auto texel = reversed ? test.reversedTexel : test.standardTexel; + Require(Texel(value, reversed) == texel, "the test's expected texel disagrees with the 5_6_5 field layout"); + Draw(device, color, Float32Export, Program(value)); + char what[192]; + std::snprintf(what, sizeof(what), "a 32_ABGR export of (%.9g, %.9g, %.9g, %.9g) into a %s 5_6_5 unorm target with the %s component order", value[0], value[1], value[2], value[3], kind, reversed ? "reversed" : "standard"); + ExpectSurface(device, block, layout, before, texel, what); + before = ReadBack(device, block); + programCount = 0; + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + Block tiled(ColorTargetLayout(Width, Height, ColorTileMode::RenderTarget, 2).Bytes()); + Block linear(ColorTargetLayout(Width, Height, ColorTileMode::Linear, 2).Bytes()); + for (const bool reversed : {true, false}) { + TargetTests(*device, tiled, TiledAttrib3, ColorTileMode::RenderTarget, reversed, "tiled"); + TargetTests(*device, linear, LinearAttrib3, ColorTileMode::Linear, reversed, "linear"); + } + std::puts("5_6_5 unorm color target tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 71210637ea..0d8679f3b1 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -132,6 +132,7 @@ - [Comp-to-single DCC keys](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp) (libSceAgcDriver) - key 0x10 (`GFX9_DCC_CLEAR_SINGLE` in Mesa `ac_descriptors.h`) uniform over a surface makes every texel the element at the start of its 256-byte block, the top-left texel of the block where radv's `build_clear_dcc_comp_to_single_shader` stores the clear color. Sampled textures and storage images read that expansion (a storage image's first write stores the keys uncompressed, as under a clear code), and a CB DCC decompress pass stores it over the texels. A fast-clear eliminate over such keys throws, as a metadata pass over mixed keys does; color targets that render over such keys use their stored texels, and textures read mixed keys, and comp-to-single keys of a format whose element does not divide 256 bytes, as stored. "Uniform" is judged over the key bytes the driver reads, one per 256 surface bytes (see DCC key stores above), so a pipe-aligned surface whose keys differ past that range is expanded as if all were comp-to-single, as for the other clear codes. Not verified on hardware - [8_SRGB color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver) on a device that cannot attach and blend R8_SRGB render into an RGBA8_SRGB copy of the target, so the sRGB encode, decode and blend rounding are the device's; the console's are not measured. - [8_SRGB color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) on such a device throw unless they are single-mip 2D surfaces with a resident image; linear targets have none. +- [5_6_5 color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) render into R5G6B5. The field holding X is in bits 0-4, Y in 5-10 and Z in 11-15, as for the other AMD formats; the component swap picks which export fills them, following `ac_translate_colorswap` in [Mesa 26.2.2 ac_formats.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-26.2.2/src/amd/common/ac_formats.c), which programs `SWAP_STD_REV` (2) for a surface with red in bits 11-15. With `SWAP_STD_REV` red goes to bits 11-15, R5G6B5's red, so the exports are written as they are; with `SWAP_STD` (0) red goes to bits 0-4 and the exports are written with X and Z exchanged, as for 1_5_5_5. The decoded format, component mapping and write mask are tested, and an execution test draws 32_ABGR exports into tiled and linear targets in both orders and compares every stored texel with that layout on the host device; the export direction is not measured on a PS5. Only UNORM with those two component orders is accepted; `SWAP_ALT` and `SWAP_ALT_REV` keep throwing. Destination alpha reads as 1, as Vulkan defines it for a format without alpha, which is not measured on the console. Constant blend colors with `SWAP_STD` go through the `0xc6` component-swap handling, which is not tested for this format. R5G6B5 has no entry in the DCC and multisample clear-encoding tables, so a CB DCC decompress or eliminate of DCC clear codes other than 0000 and a fast clear of a multisampled 5_6_5 target throw, as for 1_5_5_5. PPSA27305 renders into this format with `SWAP_STD_REV` (discussion #6434). - [1_5_5_5 color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) render into A1R5G5B5. AMD formats keep the first component in the least significant bits (X in bits 0-4, Y in 5-9, Z in 10-14, W in bit 15, as in the [fpPS4](https://github.com/red-prig/fpPS4) `kSurfaceFormat1_5_5_5` doc comment and as measured for textures in the packed 16-bit textures entry), while A1R5G5B5 keeps R in bits 10-14 and B in bits 0-4, so the pixel exports are written with X and Z exchanged, which makes the stored bits follow the guest layout. The decoded format, component mapping and write mask are tested, and an execution test draws 32_ABGR exports into tiled and linear targets and compares every stored texel with the guest layout on the host device; the export direction is not measured on a PS5. Only UNORM with the standard component swap is accepted. Constant blend colors go through the same component-swap handling as other targets with that export mapping (`0xc6`), which is not tested for this format. PPSA06699 renders into this format. - [COPY_DATA from the GPU clock](../../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (libSceAgcDriver, `SRC_SEL` 9) writes the host steady clock in 10 ns ticks when the packet runs, the same counter as the `RELEASE_MEM` timestamp, not the console's GPU clock. The `COPY_DATA` cache policy fields are accepted and have no effect. - [COPY_DATA from a register](../../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (libSceAgcDriver, `SRC_SEL` 0) copies the value the queue holds for a shader, context or user config register (`COUNT_SEL` two consecutive ones) and throws for a register with no value. A dispatch records `COMPUTE_DISPATCH_INITIATOR` and, for `DISPATCH_DIRECT`, `COMPUTE_DIM_X/Y/Z` in the shader registers; `DISPATCH_INDIRECT` clears the three counts. Neither the register values the CP writes nor the copy's ordering against in-flight dispatches have been measured on hardware. From 227d1e44a10edadbec0373339cc4b3fea888e167 Mon Sep 17 00:00:00 2001 From: milescoviello Date: Sat, 17 Oct 2026 17:03:09 +0000 Subject: [PATCH 0813/1000] perf(shader): fold a select nested in a select on the same condition select(c, a, select(c, b, d)) is select(c, a, d), and select(c, select(c, a, b), d) is select(c, a, d). The recompiler leaves these behind when a register is written more than once under the same lane mask and when the f32 NaN selection tests the same source twice, and nothing folded them. The constant folder now takes the inner arm the outer condition already decides, then folds a select whose arms became equal. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 13 ++ .../libSceAgcDriver/tests/ConstantSelect.cpp | 121 ++++++++++++++++++ .../Optimization/src/ConstantFolder.cpp | 16 ++- 3 files changed, 146 insertions(+), 4 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/ConstantSelect.cpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index c6950a121b..89f98916db 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -2493,6 +2493,19 @@ if(BUILD_TESTING) endif() endif() +add_test_executable(agc_constant_select_tests EXCLUDE_FROM_ALL tests/ConstantSelect.cpp) +target_link_libraries(agc_constant_select_tests PRIVATE shader_recompiler_agc_driver libc) +set_target_properties(agc_constant_select_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_constant_select_tests) +if(BUILD_TESTING) + set_target_properties(agc_constant_select_tests PROPERTIES EXCLUDE_FROM_ALL OFF) + add_test(NAME agc_constant_select COMMAND agc_constant_select_tests) + if(WIN32) + set_tests_properties(agc_constant_select PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() + if(BUILD_TESTING) get_directory_property(agcRegisteredTests TESTS) list(FILTER agcRegisteredTests EXCLUDE REGEX "^(agc_pipeline_journal|agc_driver_pipeline_journal)$") diff --git a/core/libs/prx/libSceAgcDriver/tests/ConstantSelect.cpp b/core/libs/prx/libSceAgcDriver/tests/ConstantSelect.cpp new file mode 100644 index 0000000000..883659b077 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/ConstantSelect.cpp @@ -0,0 +1,121 @@ +#include "IntermediateRepresentation/IrBuilder.hpp" +#include "Optimization/ConstantFolder.hpp" +#include "Optimization/DeadCodeEliminator.hpp" +#include +#include +#include + +using namespace ShaderRecompiler; + +namespace { + +void Require(bool condition, const char* message) { + if (!condition) throw std::runtime_error(message); +} + +struct Program { + IrProgram program; + IrBuilder builder{program}; + IrValue* lane = nullptr; + IrValue* condition = nullptr; + IrValue* other = nullptr; + std::uint32_t values = 0; + + Program() { + auto& block = program.CreateBlock(); + program.BlockOrder().push_back(&block); + BlockInfo info; + info.id = 0; + info.terminator.kind = TerminatorKind::Return; + program.Metadata().blockInfo.push_back(info); + program.SetEntryBlock(block); + builder.SetInsertionPoint(block); + lane = &builder.Emit(IrOpcode::LaneId, IrType::U32, {}); + condition = &builder.Emit(IrOpcode::ULessThan32, IrType::Bool, {lane, &builder.Constant(16u)}); + other = &builder.Emit(IrOpcode::ULessThan32, IrType::Bool, {lane, &builder.Constant(8u)}); + } + + IrValue& value() { + return builder.Emit(IrOpcode::IAdd32, IrType::U32, {lane, &builder.Constant(++values)}); + } + + IrValue& select(IrValue& selector, IrValue& onTrue, IrValue& onFalse) { + return builder.Emit(IrOpcode::SelectU32, IrType::U32, {&selector, &onTrue, &onFalse}); + } + + const IrValue* folded(IrValue& result) { + auto& reference = builder.Emit(IrOpcode::ReferenceU32, IrType::Void, {&result}); + ValidateProgram(program, true); + ConstantFolder{}.Fold(program); + DeadCodeEliminator{}.RemoveIdentities(program); + DeadCodeEliminator{}.Eliminate(program); + ValidateProgram(program, true); + return reference.Argument(0)->Resolve(); + } +}; + +bool Selects(const IrValue* value, const IrValue* selector, const IrValue* onTrue, const IrValue* onFalse) { + return value->Opcode() == IrOpcode::SelectU32 && value->Argument(0)->Resolve() == selector && value->Argument(1)->Resolve() == onTrue && value->Argument(2)->Resolve() == onFalse; +} + +void CheckFalseArm() { + Program p; + auto& kept = p.value(); + auto& dropped = p.value(); + auto& fallback = p.value(); + auto& inner = p.select(*p.condition, dropped, fallback); + const auto* result = p.folded(p.select(*p.condition, kept, inner)); + Require(Selects(result, p.condition, &kept, &fallback), "a select in the false arm of a select on the same condition was not folded to its false arm"); + Require(!inner.HasUses(), "the folded inner select is still used"); +} + +void CheckTrueArm() { + Program p; + auto& kept = p.value(); + auto& dropped = p.value(); + auto& fallback = p.value(); + const auto* result = p.folded(p.select(*p.condition, p.select(*p.condition, kept, dropped), fallback)); + Require(Selects(result, p.condition, &kept, &fallback), "a select in the true arm of a select on the same condition was not folded to its true arm"); +} + +void CheckChain() { + Program p; + auto& last = p.value(); + auto& fallback = p.value(); + auto* chain = &p.select(*p.condition, p.value(), fallback); + chain = &p.select(*p.condition, p.value(), *chain); + const auto* result = p.folded(p.select(*p.condition, last, *chain)); + Require(Selects(result, p.condition, &last, &fallback), "a chain of selects on one condition did not fold to the last write and the fallback"); +} + +void CheckEqualArms() { + Program p; + auto& same = p.value(); + const auto* result = p.folded(p.select(*p.condition, same, p.select(*p.condition, p.value(), same))); + Require(result == &same, "a select whose arms become equal after folding was not replaced by that value"); +} + +void CheckOtherCondition() { + Program p; + auto& kept = p.value(); + auto& inner = p.select(*p.other, p.value(), p.value()); + const auto* result = p.folded(p.select(*p.condition, kept, inner)); + Require(Selects(result, p.condition, &kept, &inner), "a select nested under a different condition was folded"); +} + +} + +int main() { + try { + CheckFalseArm(); + CheckTrueArm(); + CheckChain(); + CheckEqualArms(); + CheckOtherCondition(); + std::puts("constant select tests passed"); + return 0; + } catch (const std::exception& error) { + std::fprintf(stderr, "%s\n", error.what()); + return 1; + } +} diff --git a/core/shader/recompiler/Optimization/src/ConstantFolder.cpp b/core/shader/recompiler/Optimization/src/ConstantFolder.cpp index 52643c8990..ea97d46833 100644 --- a/core/shader/recompiler/Optimization/src/ConstantFolder.cpp +++ b/core/shader/recompiler/Optimization/src/ConstantFolder.cpp @@ -200,17 +200,25 @@ bool replaceBinaryIdentity(IrValue& inst, IrType type, std::uint64_t identity) { bool foldSelect(IrValue& inst) { auto& condition = resolveArg(inst, 0); - auto& trueValue = resolveArg(inst, 1); - auto& falseValue = resolveArg(inst, 2); if (isImmediate(condition, IrType::U1)) { - replaceWith(inst, condition.ImmediateBool() ? trueValue : falseValue); + replaceWith(inst, resolveArg(inst, condition.ImmediateBool() ? 1 : 2)); return true; } + bool folded = false; + for (std::size_t arm = 1; arm <= 2; arm++) { + auto& nested = resolveArg(inst, arm); + if (nested.Opcode() == inst.Opcode() && nested.ArgumentCount() == 3u && &resolveArg(nested, 0) == &condition) { + inst.ReplaceArgument(arm, &resolveArg(nested, arm)); + folded = true; + } + } + auto& trueValue = resolveArg(inst, 1); + auto& falseValue = resolveArg(inst, 2); if (trueValue == falseValue) { replaceWith(inst, trueValue); return true; } - return false; + return folded; } bool foldPhi(IrValue& inst) { From 88642b874fe461b5eebf9e3617074131efa3fb9f Mon Sep 17 00:00:00 2001 From: Gjon Gojcevic Date: Sat, 17 Oct 2026 13:05:25 -0400 Subject: [PATCH 0814/1000] fix(shader): declare the 64-bit integer type only for programs with BDA anchors EmitBdaAnchors declared the u64 type before checking for anchors, so every program got a 64-bit integer type, and programs without the Int64 capability failed SPIR-V validation. It now returns before declaring anything when the program has no anchors. A compute program without BDA reads is checked to declare no 64-bit integer type and no Int64 capability. AI-assisted: yes --- .../libSceAgcDriver/tests/ShaderMemory.cpp | 23 +++++++++++++++++++ .../SpirvBackend/src/SpirvBdaRead.cpp | 1 + 2 files changed, 24 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 53f3512c90..d37165196e 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -3737,6 +3737,29 @@ void verifyBdaAnchors() { require(directLoads == (anchored ? loads : 0u), "BDA anchors: an anchored read does not load its dwords directly"); } } + RecompileRequest plain{}; + const std::vector endOnly{0xbf810000u}; + plain.shader = {ShaderStage::Compute, 0x60000u, endOnly, 0, {}}; + plain.context.waveSize = 32; + plain.context.userDataBaseRegister = 0; + plain.context.userData = userData; + plain.context.compute = ShaderComputeStageInfo{{32u, 1u, 1u}, 0u, {false, false, false}, false, 1u}; + plain.target.vulkanVersion = 0x00401000u; + plain.target.spirvVersion = 0x00010300u; + plain.target.subgroupSize = 32; + plain.target.bdaAbiVersion = BdaAbi::Version; + plain.target.fragmentShaderBarycentricEnabled = false; + plain.layout.pushConstantSizeBytes = 128; + plain.useCache = false; + const auto plainWords = Recompile(plain).spirv; + for (std::size_t cursor = 5; cursor < plainWords.size();) { + const auto length = plainWords[cursor] >> 16u; + require(length != 0 && length <= plainWords.size() - cursor, "BDA anchors: truncated SPIR-V instruction"); + const auto op = plainWords[cursor] & 0xffffu; + require(!(op == spv::OpTypeInt && plainWords[cursor + 2] == 64u), "BDA anchors: a program without anchors declares a 64-bit integer type"); + require(!(op == spv::OpCapability && plainWords[cursor + 1] == spv::CapabilityInt64), "BDA anchors: a program without anchors requires Int64"); + cursor += length; + } } void verifyFunctionLdsBound() { diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp index 2237fa9714..c17210edfc 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp @@ -541,6 +541,7 @@ void EmitBdaAnchorRange(SpirvEmitterState& state, SpirvEmitterState::BdaAnchor& } void EmitBdaAnchors(SpirvEmitterState& state) { + if (state.bdaAnchors.empty()) return; const auto u64 = TypeScalarU64(state); for (auto& anchor : state.bdaAnchors) { const auto low = Unary(state, spv::OpUConvert, u64, EmitGetUserData(state, static_cast(anchor.lowRegister))); From 9626d006bf258bec3cfdea1c3b0b32c71a479120 Mon Sep 17 00:00:00 2001 From: Jota <139649225+jotitadev@users.noreply.github.com> Date: Sat, 17 Oct 2026 14:22:48 -0300 Subject: [PATCH 0815/1000] fix(tests): give the large cache file test its own temporary directory --- core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp index 6adaebcddd..d96411c0eb 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp @@ -735,7 +735,7 @@ void verifyFloatModeSerialization() { int runLargeFileWrite() { constexpr std::size_t size = (std::size_t{2} << 30u) + 4096u; - const auto directory = std::filesystem::temp_directory_path() / "aps5-shader-disk-cache-large-file-test"; + const auto directory = std::filesystem::temp_directory_path() / ("aps5-shader-disk-cache-large-file-test-" + std::to_string(std::random_device{}())); std::error_code error; std::filesystem::remove_all(directory, error); std::filesystem::create_directories(directory, error); From 5da6de62adf41fa40ff5972597cca2c3d51694de Mon Sep 17 00:00:00 2001 From: AlxcBTW <187763517+AlxcBTW@users.noreply.github.com> Date: Sat, 17 Oct 2026 19:32:05 +0200 Subject: [PATCH 0816/1000] fix(agc): count stage-A drops of a dispatch move as refusals The [dispmove] line printed refused 0 next to a rebuild count that came mostly from PrepareDispatch dropping the hit. The line now reports the refusals at stage A and at record separately, each with its reasons. --- core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index a0088c4054..19ab8e8617 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -4010,7 +4010,7 @@ void reportDispatchMove() { for (std::size_t i = 0; i < 5; ++i) sizes[i] = take(moves.sizes[i]); const auto refused = list(moves.refused); const auto filtered = list(moves.filtered); - AgcDriver::ProfilePrint_nid_no_patch("[dispmove] (10 s) %llu template hits checked: %llu with every read-only buffer where it was built, %llu with %llu moved ones (%.2f MiB snapshotted; per dispatch up to 1 KiB %llu, 16 KiB %llu, 256 KiB %llu, 4 MiB %llu, more %llu); refused %s; after a refusal: %llu rebuilt, %llu inserted beside the movable template (kept), %llu served by an exact entry; stage A dropped cached objects: %s\n", checked, exact, moved, buffers, static_cast(bytes) / 1048576.0, sizes[0], sizes[1], sizes[2], sizes[3], sizes[4], refused.c_str(), rebuilt, kept, served, filtered.c_str()); + AgcDriver::ProfilePrint_nid_no_patch("[dispmove] (10 s) %llu template hits checked: %llu with every read-only buffer where it was built, %llu with %llu moved ones (%.2f MiB snapshotted; per dispatch up to 1 KiB %llu, 16 KiB %llu, 256 KiB %llu, 4 MiB %llu, more %llu); refused %s at stage A (the hit was dropped before the record), %s at record; after a refusal: %llu rebuilt, %llu inserted beside the movable template (kept), %llu served by an exact entry\n", checked, exact, moved, buffers, static_cast(bytes) / 1048576.0, sizes[0], sizes[1], sizes[2], sizes[3], sizes[4], filtered.c_str(), refused.c_str(), rebuilt, kept, served); } void noteDispatchMoveSize(std::size_t bytes) { From c732321bd3ea18a27680da5c5c1a5b46abbcf021 Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Thu, 15 Oct 2026 00:48:06 +0000 Subject: [PATCH 0817/1000] docs(dev): describe the guest clock scale switch --- docs/dev/BUILD.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/dev/BUILD.md b/docs/dev/BUILD.md index 03e4c72be7..8e1856f32f 100644 --- a/docs/dev/BUILD.md +++ b/docs/dev/BUILD.md @@ -203,6 +203,10 @@ A class range covers a command together with its own barriers. In `VulkanDevice: - `shadow-publish`: copies of unit shadow slabs into the import, with their barriers (`recordCopies` in `UnitShadow.cpp`, reached from `PublishShadow`). Copied bytes. - `template-refresh`: the `vkCmdUpdateBuffer` updates of a cached dispatch's changed data words, as one range from the first update to the last (`ShaderResources::refreshData`, called from `recordDispatch`). Refreshed bytes. +## Guest clock scale + +Set `APS5_TIME_SCALE=` to scale the guest process monotonic clocks and timestamp counter (TSC) from their first observed value. A positive factor below 1 slows guest time; a factor above 1 speeds it up. An unset, zero, negative or non-numeric value uses `1.0` (normal speed). Host sleeps and waits still take real time, so this is a diagnostic aid for titles whose frame or timeout logic depends on guest time; it does not speed up the emulator or GPU work. The setting is read once when the process starts. + ## Driver thread priority Set `APS5_THREAD_PRIORITY=high` to raise the AGC driver's queue workers and the video output presenter above the title's threads. On Linux the threads get nice `APS5_THREAD_NICE` (default -10) through rtkit; on Windows they get `THREAD_PRIORITY_HIGHEST`. `APS5_THREAD_PRIORITY=rt` asks rtkit for real-time priority `APS5_THREAD_RT` (default 10, at most rtkit's `MaxRealtimePriority`) under rtkit's policy, `SCHED_RR` by default; on Windows it sets `THREAD_PRIORITY_TIME_CRITICAL`. rtkit only grants it when the process's hard `RLIMIT_RTTIME` is at most its `RTTimeUSecMax` (200000 us by default), so a hard limit above `APS5_THREAD_RTTIME_US` (default 200000, capped at `RTTimeUSecMax`) is lowered to it and the soft limit to at most half of it. The hard limit cannot be raised again and stays lowered for the whole process even if rtkit then refuses; see [technical debt](TechnicalDebt.md#functional). There is no fallback between the modes: when rtkit or `SetThreadPriority` refuses, a queue worker reports the refusal as a driver failure, which the next submission, wait or shutdown rethrows, and the presenter's start throws it. Each raised thread logs its priority once as `[thread-priority]`. The setting is `off` by default; a value other than `off`, `high` or `rt` is reported the same way as a refusal. From a2ed4abead54662cf752899f6328341008a76b5c Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Sat, 17 Oct 2026 03:26:09 +0000 Subject: [PATCH 0818/1000] docs(dev): clarify guest clock scale coverage --- docs/dev/BUILD.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/dev/BUILD.md b/docs/dev/BUILD.md index 8e1856f32f..33f124a24d 100644 --- a/docs/dev/BUILD.md +++ b/docs/dev/BUILD.md @@ -205,7 +205,9 @@ A class range covers a command together with its own barriers. In `VulkanDevice: ## Guest clock scale -Set `APS5_TIME_SCALE=` to scale the guest process monotonic clocks and timestamp counter (TSC) from their first observed value. A positive factor below 1 slows guest time; a factor above 1 speeds it up. An unset, zero, negative or non-numeric value uses `1.0` (normal speed). Host sleeps and waits still take real time, so this is a diagnostic aid for titles whose frame or timeout logic depends on guest time; it does not speed up the emulator or GPU work. The setting is read once when the process starts. +Set `APS5_TIME_SCALE=` to scale `sceKernelGetProcessTime`, `sceKernelGetProcessTimeCounter` and `sceKernelReadTsc` from their first observed value. A positive factor below 1 slows guest time; a factor above 1 speeds it up. An unset, zero, negative or non-numeric value uses `1.0` (normal speed). On Windows, `sceKernelClockGettime` monotonic clock ids are scaled too; on Linux they return the native host monotonic clock unchanged. A title's own `rdtsc` instruction and `sceKernelGetTscFrequency` are unscaled, so dividing scaled TSC deltas by the reported real frequency gives guest-time intervals. Host sleeps and waits still take real time, so this is a diagnostic aid for titles whose frame or timeout logic depends on guest time; it does not speed up the emulator or GPU work. The setting is read once when the process starts. + +`APS5_PAD_INPUT_FILE` command durations and reload polling use `sceKernelGetProcessTime`, so they follow the scale. At `0.02`, a 1-second injected press lasts about 50 seconds of real time, and a new command can take up to 5 seconds to be noticed. ## Driver thread priority From 4cfb85482ab12a06087658e22635f9014a96edf0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Sat, 17 Oct 2026 19:54:25 +0200 Subject: [PATCH 0819/1000] ci(build): save the Windows ccache from merge queue builds Queue builds are workflow_dispatch runs on main and never saved their ccache, so each one restored the last push build's entry and recompiled everything the queued PRs and the merges since then touched (32 min vs 6 min warm). They now save under their own prefix, restore it first and fall back to the push entries; push builds still restore only push entries, so main never starts from a tree that held unmerged PRs. Co-Authored-By: Claude --- .github/workflows/build.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e5d2a9a22d..e94f541117 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -503,8 +503,9 @@ jobs: - run: echo 'C:\winlibs\mingw64\bin' >> "$GITHUB_PATH" - uses: hendrikmuhs/ccache-action@v1.2.24 with: - key: windows - save: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} + key: ${{ github.event_name == 'workflow_dispatch' && 'dispatch-windows' || 'windows' }} + restore-keys: ${{ github.event_name == 'workflow_dispatch' && 'windows' || '' }} + save: ${{ github.ref == 'refs/heads/main' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') }} evict-old-files: job - id: ffmpeg-cache uses: actions/cache/restore@v6 @@ -577,3 +578,4 @@ jobs: [ -f ctest.log ] || exit 0 awk '/Test +#[0-9]+: / && $NF == "sec" { for (i = 1; i <= NF; i++) if ($i ~ /^#[0-9]+:$/) name = $(i + 1); t = $(NF - 1) + 0; if (t > max[name]) max[name] = t } END { for (name in max) if (max[name] > 30) printf "::warning title=Slow test::%s took %.1f s on %s; tests must finish in 30 s (CONTRIBUTING.md)\n", name, max[name], ENVIRON["RUNNER_OS"] }' ctest.log + From 586028c991f160dd937d9c51d88082ab91e15cd7 Mon Sep 17 00:00:00 2001 From: jagrat Singh Date: Sat, 17 Oct 2026 23:26:26 +0530 Subject: [PATCH 0820/1000] feat(libSceVoice): implement sceVoiceGetResourceInfo --- core/libs/SceTypes.hpp | 8 ++++++++ core/libs/prx/libSceVoice/Export.cpp | 12 ++++++++++-- core/libs/tests/GuestVoice.cpp | 11 +++++++++++ docs/dev/debt/libSceVoice.md | 1 + 4 files changed, 30 insertions(+), 2 deletions(-) diff --git a/core/libs/SceTypes.hpp b/core/libs/SceTypes.hpp index 3ebfe78a12..8390f80bfc 100644 --- a/core/libs/SceTypes.hpp +++ b/core/libs/SceTypes.hpp @@ -921,6 +921,14 @@ struct VoicePortInfo { std::uint16_t reserved; }; +struct VoiceResourceInfo { + std::uint16_t max_voice_input_ports; + std::uint16_t max_voice_output_ports; + std::uint16_t max_device_input_ports; + std::uint16_t max_device_output_ports; + std::uint16_t max_ports; +}; + struct VoiceStartParam { void* container; std::uint32_t mem_size; diff --git a/core/libs/prx/libSceVoice/Export.cpp b/core/libs/prx/libSceVoice/Export.cpp index 6210925cdb..d5a746c74a 100644 --- a/core/libs/prx/libSceVoice/Export.cpp +++ b/core/libs/prx/libSceVoice/Export.cpp @@ -225,8 +225,16 @@ int APS5_VABI sceVoiceGetMuteFlag(uint32_t port_id, bool* muted) { return 0; } -int APS5_VABI sceVoiceGetResourceInfo(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceVoiceGetResourceInfo(VoiceResourceInfo* info) { + if (info == nullptr) APS5_INVALID_ARG_EX; + auto& voice = State(); + std::lock_guard lock(voice.mutex); + if (!voice.initialized) Fail(__func__, "library not initialized"); + info->max_voice_input_ports = 7; + info->max_voice_output_ports = 7; + info->max_device_input_ports = 1; + info->max_device_output_ports = 1; + info->max_ports = 64; return 0; } diff --git a/core/libs/tests/GuestVoice.cpp b/core/libs/tests/GuestVoice.cpp index 8d1d0eeeb4..9b244b42d1 100644 --- a/core/libs/tests/GuestVoice.cpp +++ b/core/libs/tests/GuestVoice.cpp @@ -18,6 +18,7 @@ int APS5_VABI sceVoiceGetMuteFlag(std::uint32_t, bool*); int APS5_VABI sceVoiceGetPortInfo(std::uint32_t, VoicePortInfo*); int APS5_VABI sceVoiceReadFromOPort(std::uint32_t, void*, std::uint32_t*); int APS5_VABI sceVoiceResetPort(std::uint32_t); +int APS5_VABI sceVoiceGetResourceInfo(VoiceResourceInfo*); } namespace { @@ -44,10 +45,19 @@ std::uint32_t CreatePort(std::int32_t type, float volume) { int main() { RequireThrows([] { sceVoiceResetPort(0); }); + RequireThrows([] { VoiceResourceInfo info{}; sceVoiceGetResourceInfo(&info); }); VoiceInitParam init{}; VoiceStartParam start{}; Require(sceVoiceInit(&init, 0) == 0); Require(sceVoiceStart(&start) == 0); + RequireThrows([] { sceVoiceGetResourceInfo(nullptr); }); + VoiceResourceInfo resourceInfo{}; + Require(sceVoiceGetResourceInfo(&resourceInfo) == 0); + Require(resourceInfo.max_voice_input_ports == 7); + Require(resourceInfo.max_voice_output_ports == 7); + Require(resourceInfo.max_device_input_ports == 1); + Require(resourceInfo.max_device_output_ports == 1); + Require(resourceInfo.max_ports == 64); const auto input = CreatePort(2, 0.5f); const auto output = CreatePort(3, 0.25f); Require(sceVoiceConnectIPortToOPort(input, output) == 0); @@ -76,4 +86,5 @@ int main() { Require(sceVoiceResetPort(output) == 0); Require(sceVoiceEnd_nid_postfix() == 0); RequireThrows([&] { sceVoiceResetPort(output); }); + RequireThrows([] { VoiceResourceInfo info{}; sceVoiceGetResourceInfo(&info); }); } diff --git a/docs/dev/debt/libSceVoice.md b/docs/dev/debt/libSceVoice.md index bd90c772f6..8d57d43d4a 100644 --- a/docs/dev/debt/libSceVoice.md +++ b/docs/dev/debt/libSceVoice.md @@ -7,3 +7,4 @@ ### Unknown function info - [sceVoiceResetPort](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - the `(uint32_t portId)` signature is the PS Vita one in [VitaSDK](https://github.com/vitasdk/vita-headers/blob/ae5808f3105c42fa3373ba4ab64ab54e2e3ca848/include/psp2/voice.h#L503-L514) and the PS3 `cellVoiceResetPort` in [RPCS3](https://github.com/RPCS3/rpcs3/blob/be69c054eeac57e2e671697a463473d37fe5a7b2/rpcs3/Emu/Cell/Modules/cellVoice.cpp#L493-L510); that the PS5 library takes the same argument is assumed. A reset clears a port's buffered data, and ports here buffer nothing (`sceVoiceReadFromOPort` returns 0 bytes and `sceVoiceWriteToIPort` throws), so it only checks that the library is initialized and the port exists, as RPCS3 does, and keeps the port's connections, volume and mute flag. An uninitialized library or an unknown port throws like the other port calls; the console's error codes are not measured. PPSA23566 imports it +- [sceVoiceGetResourceInfo](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - the `(VoiceResourceInfo* info)` signature and port limits (7 voice inputs, 7 voice outputs, 1 device input, 1 device output, 64 total ports) are the PS Vita ones in [VitaSDK](https://github.com/vitasdk/vita-headers/blob/ae5808f3105c42fa3373ba4ab64ab54e2e3ca848/include/psp2/voice.h#L150-L157); that the PS5 library uses the same layout and limits is assumed. An uninitialized library or a null output pointer throws like the other Voice calls; console limits and error codes are not measured. PPSA23566 imports it From ef622e2f8f1032d21b9a2781447d42e7ea889363 Mon Sep 17 00:00:00 2001 From: hugodearaujo Date: Sat, 17 Oct 2026 15:03:03 -0300 Subject: [PATCH 0821/1000] feat(agc): import shared ranges whose backing offset is not 64 KiB aligned A sparse import binds whole 64 KiB blocks, so a range whose memfd offset is not a multiple of 64 KiB fell back to one UDMABUF_CREATE and, above udmabuf's size limit, to copies. The sparse buffer now starts at the block below the range: HostImport::lead holds that distance, the import's device address points at the range itself, and every offset into the import's buffer goes through HostImportOffset. --- .../Execution/src/VulkanDevice.cpp | 8 ++--- .../Graphics/include/GuestBufferMemory.hpp | 9 +++-- .../Graphics/src/DccMetadata.cpp | 2 +- .../prx/libSceAgcDriver/Graphics/src/Draw.cpp | 16 ++++----- .../Graphics/src/GuestBufferMemory.cpp | 16 +++++---- .../libSceAgcDriver/Graphics/src/Texture.cpp | 10 +++--- .../Graphics/src/UnitShadow.cpp | 4 +-- .../tests/HostImportSparseDmaBuf.cpp | 34 ++++++++++++++++--- .../tests/execution/SparseDmaBufImport.cpp | 24 ++++++++++++- docs/dev/debt/libSceAgcDriver.md | 2 +- docs/user/USAGE.md | 2 +- 11 files changed, 91 insertions(+), 36 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 62afeaea77..5410f9f521 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -2186,7 +2186,7 @@ int VulkanDevice::WriteLabelOnGpu(std::uint64_t address, std::spanbuffer, address - import->base, bytes, address); }; + const auto recordStore = [&] { recorder.RecordStore(import->buffer, Graphics::HostImportOffset(*import, address), bytes, address); }; // Only a write-back that can land over these bytes (a listed copied writer of the range, as // FillBuffer tests) puts the label behind the completions; any other pending completion // stores elsewhere. Debug aid: APS5_LABEL_GATE_ALL=1 gates on any completion, as before (a @@ -2387,7 +2387,7 @@ bool VulkanDevice::FillBuffer(std::uint64_t address, std::size_t bytes, std::spa Graphics::RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT | VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_HOST_WRITE_BIT, VK_ACCESS_TRANSFER_WRITE_BIT); Graphics::Recorder::CountBarriers(CommandClass::Fill); } - const auto offset = address - import->base; + const auto offset = Graphics::HostImportOffset(*import, address); if (uniform) { ++uniformFills; context.Resolved(&Graphics::DeviceFunctions::cmdFillBuffer, "vkCmdFillBuffer")(commands, import->buffer, offset, bytes, pattern[0]); @@ -2654,7 +2654,7 @@ VulkanDevice::CopyOutcome VulkanDevice::CopyBuffer(std::uint64_t destination, st Graphics::Recorder::CountBarriers(CommandClass::Copy); } const auto gpuTiming = recorder.BeginGpuTiming(programAddress); - const VkBufferCopy region{source - sourceImport->base, destination - destinationImport->base, bytes}; + const VkBufferCopy region{Graphics::HostImportOffset(*sourceImport, source), Graphics::HostImportOffset(*destinationImport, destination), bytes}; context.Resolved(&Graphics::DeviceFunctions::cmdCopyBuffer, "vkCmdCopyBuffer")(commands, sourceImport->buffer, destinationImport->buffer, 1, ®ion); recorder.EndGpuTiming(gpuTiming, bytes); // The copied bytes are visible to everything recorded after and to the host, as after a fill. @@ -4486,7 +4486,7 @@ void VulkanDevice::recordDispatch(RecordedDispatch& record) { const auto gpuTiming = recorder.BeginGpuTiming(record.programAddress != 0 ? record.programAddress : record.shader->program->variantId); if (state->checkpointMarker != nullptr) state->checkpointMarker(commands, reinterpret_cast(static_cast(record.programAddress))); if (argumentImport != nullptr) { - context.Resolved(&Graphics::DeviceFunctions::cmdDispatchIndirect, "vkCmdDispatchIndirect")(commands, argumentImport->buffer, arguments - argumentImport->base); + context.Resolved(&Graphics::DeviceFunctions::cmdDispatchIndirect, "vkCmdDispatchIndirect")(commands, argumentImport->buffer, Graphics::HostImportOffset(*argumentImport, arguments)); } else { const auto* limit = state->properties.limits.maxComputeWorkGroupCount; if (record.x <= limit[0] && record.y <= limit[1] && record.z <= limit[2]) { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp index b8aad61006..89e9675e00 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp @@ -40,8 +40,13 @@ struct HostImport { bool joined = false; std::shared_ptr chunk; mutable std::uint64_t lastUse = 0; + std::uint64_t lead = 0; }; +inline VkDeviceSize HostImportOffset(const HostImport& import, std::uint64_t address) { + return address - import.base + import.lead; +} + enum class ImportWatch : std::uint8_t { Watch, Unwatch }; struct ImportProbe { @@ -63,8 +68,8 @@ ImportWatch PrepareImportWatch(const Context& context); void SetImportWatch(const Context& context, ImportWatch watch); // The host import of the registered allocation containing [address, address + bytes), made on demand -// (alignment and budget permitting), or null. Bytes at `address` are at `address - import->base` in -// the import's buffer. +// (alignment and budget permitting), or null. Bytes at `address` are at `HostImportOffset(*import, +// address)` in the import's buffer. const HostImport* HostImportFor(const Context& context, std::uint64_t address, std::size_t bytes); const HostImport* ExistingHostImportFor(const Context& context, std::uint64_t address, std::size_t bytes); void SetHostImportWindow(std::uint64_t bytes); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp index f8f4d371ec..78928e74c6 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp @@ -234,7 +234,7 @@ void TraceKeyStore(const char* path, std::uint64_t begin, std::size_t count) { bool StoreUncompressedOnGpu(const Context& context, Recorder& recorder, std::uint64_t begin, std::size_t count) { const auto* import = HostImportFor(context, begin, count); if (import == nullptr) return false; - const VkDeviceSize first = begin - import->base; + const VkDeviceSize first = HostImportOffset(*import, begin); const VkDeviceSize last = first + count; const VkDeviceSize fillBegin = std::min((first + 3) & ~VkDeviceSize{3}, last); const VkDeviceSize fillEnd = std::max(last & ~VkDeviceSize{3}, fillBegin); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index 5993201d60..a468efc063 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -258,7 +258,7 @@ void recordCmaskClear(const Context& context, Recorder& recorder, const HostImpo recorder.FlushKeyStoresOverlapping(address, bytes); const auto commands = recorder.Commands(); recorder.Keep(resident); - context.detiler->DispatchCmaskClear(commands, import.buffer, address - import.base, bytes, view, color.extent.width, color.extent.height, color.elementBytes, color.clearWords, color.dccAddress == 0); + context.detiler->DispatchCmaskClear(commands, import.buffer, HostImportOffset(import, address), bytes, view, color.extent.width, color.extent.height, color.elementBytes, color.clearWords, color.dccAddress == 0); recorder.NotePendingWrite(address, bytes); proveCmask(address, bytes, GuestMemory::MarkWritten(address, bytes)); } @@ -950,7 +950,7 @@ const HostImport* InPlaceDrawInput(const Context& context, std::uint64_t address const auto drawWriters = DrawCopiedWriters(); if (std::any_of(drawWriters->begin(), drawWriters->end(), overlaps)) return nullptr; const auto* import = HostImportFor(context, address, bytes); - if (import == nullptr || (address - import->base) % alignment != 0) return nullptr; + if (import == nullptr || HostImportOffset(*import, address) % alignment != 0) return nullptr; recorder->NotePendingRead(address, bytes, Recorder::ReadKind::DrawInput); if (behindGpuWrite != nullptr && behind) *behindGpuWrite = true; return import; @@ -1132,7 +1132,7 @@ DrawInputs prepareDrawInputs(const Context& context, const State& state, const P } if (import != nullptr) { inputs.indexHandle = import->buffer; - inputs.indexOffset = draw.indexAddress - import->base; + inputs.indexOffset = HostImportOffset(*import, draw.indexAddress); inputs.inPlaceReads.emplace_back(draw.indexAddress, draw.indexAddress + indexBytes); } else { inputs.indices = std::move(copy.buffer); @@ -1201,7 +1201,7 @@ DrawInputs prepareDrawInputs(const Context& context, const State& state, const P RequireNoMultisampleTarget(begin, bytes, "a vertex buffer read"); if (const auto* import = InPlaceDrawInput(context, begin, bytes, plan.alignments[range], &inputs.vertexInputBehindGpuWrite)) { rangeHandles.push_back(import->buffer); - rangeOffsets.push_back(begin - import->base); + rangeOffsets.push_back(HostImportOffset(*import, begin)); inputs.inPlaceReads.emplace_back(begin, end); continue; } @@ -1367,8 +1367,8 @@ void recordDrawCommands(const Context& context, VkCommandBuffer commands, const else context.Resolved(&DeviceFunctions::cmdDraw, "vkCmdDraw")(commands, draw.indexCount, draw.instanceCount, draw.firstVertex, draw.firstInstance); } else if (indirect->path == IndirectDrawPath::Gpu) { if (args->countIndirect) { - if (draw.indexed) context.Function("vkCmdDrawIndexedIndirectCountKHR")(commands, argumentBuffer, argumentOffset, indirect->countImport->buffer, args->countAddress - indirect->countImport->base, args->count, args->stride); - else context.Function("vkCmdDrawIndirectCountKHR")(commands, argumentBuffer, argumentOffset, indirect->countImport->buffer, args->countAddress - indirect->countImport->base, args->count, args->stride); + if (draw.indexed) context.Function("vkCmdDrawIndexedIndirectCountKHR")(commands, argumentBuffer, argumentOffset, indirect->countImport->buffer, HostImportOffset(*indirect->countImport, args->countAddress), args->count, args->stride); + else context.Function("vkCmdDrawIndirectCountKHR")(commands, argumentBuffer, argumentOffset, indirect->countImport->buffer, HostImportOffset(*indirect->countImport, args->countAddress), args->count, args->stride); } else if (args->count <= 1 || context.multiDrawIndirect) { if (draw.indexed) context.Resolved(&DeviceFunctions::cmdDrawIndexedIndirect, "vkCmdDrawIndexedIndirect")(commands, argumentBuffer, argumentOffset, args->count, args->stride); else context.Resolved(&DeviceFunctions::cmdDrawIndirect, "vkCmdDrawIndirect")(commands, argumentBuffer, argumentOffset, args->count, args->stride); @@ -1416,7 +1416,7 @@ bool recordIndirectArguments(const Context& context, VkCommandBuffer commands, R if (args->countIndirect) recorder->NotePendingRead(args->countAddress, 4, Recorder::ReadKind::Indirect); } argumentBuffer = indirect.argumentImport->buffer; - argumentOffset = args->arguments - indirect.argumentImport->base; + argumentOffset = HostImportOffset(*indirect.argumentImport, args->arguments); const bool rewritten = args->vertexRule == Rule::Constant || args->instanceRule == Rule::Constant; if (!rewritten) return false; const auto bytes = static_cast(args->RangeBytes()); @@ -1699,7 +1699,7 @@ void captureInputs(const Context& context, Recorder& recorder, VkCommandBuffer c } const auto* imported = HostImportFor(context, begin, bytes); Require(imported != nullptr, "capture input has no host import"); - addSample(begin, bytes, imported->buffer, begin - imported->base, reinterpret_cast(begin)); + addSample(begin, bytes, imported->buffer, HostImportOffset(*imported, begin), reinterpret_cast(begin)); } CaptureTrace::Log("input-capture draw=%llu batch=%llu target=%llx ranges=%zu bytes=%zu", draw, batch, static_cast(target), samples.size(), total); if (samples.empty()) return; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index 3af6b42bd8..15544e6264 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -387,6 +387,7 @@ const char* createDmaBufImport(const Context& context, HostImport& entry, int fi #ifndef _WIN32 constexpr std::uint64_t ImportChunkBytes = std::uint64_t{64} << 20u; constexpr VkBufferUsageFlags ChunkImportUsage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT; +constexpr std::uint64_t SparseBlockBytes = 65536; struct ImportChunk { VkDevice device = VK_NULL_HANDLE; @@ -559,13 +560,15 @@ VkMemoryRequirements dmaBufBufferRequirements(const Context& context, const Host } bool createSparseDmaBufImport(const Context& context, HostImport& entry, int file, std::uint64_t offset) { + const auto lead = offset % SparseBlockBytes; + offset -= lead; const bool ownQueue = context.sparseQueue != VK_NULL_HANDLE && context.sparseQueueMutex != nullptr; if (!context.sparseBinding || entry.bytes <= ImportChunkBytes || (!ownQueue && !GuestMemory::GpuMutex().HeldByThisThread()) || udmabufDevice() < 0) return false; struct stat backing {}; if (fstat(file, &backing) != 0) throwSparseImport(entry, "fstat of the shared backing", VK_ERROR_INVALID_EXTERNAL_HANDLE); const auto chunkTypes = dmaBufBufferRequirements(context, entry, 0, ChunkImportUsage, ImportChunkBytes, nullptr).memoryTypeBits; VkBuffer buffer = VK_NULL_HANDLE; - const auto requirements = dmaBufBufferRequirements(context, entry, VK_BUFFER_CREATE_SPARSE_BINDING_BIT, SparseImportUsage, entry.bytes, &buffer); + const auto requirements = dmaBufBufferRequirements(context, entry, VK_BUFFER_CREATE_SPARSE_BINDING_BIT, SparseImportUsage, lead + entry.bytes, &buffer); const auto alignment = requirements.alignment; const bool chosen = alignment != 0 && offset % alignment == 0 && ImportChunkBytes % alignment == 0 && (chunkTypes & ~requirements.memoryTypeBits) == 0 && (requirements.memoryTypeBits & chunkTypes) != 0 && (offset + requirements.size + ImportChunkBytes - 1) / ImportChunkBytes * ImportChunkBytes <= static_cast(backing.st_size); @@ -616,9 +619,10 @@ bool createSparseDmaBufImport(const Context& context, HostImport& entry, int fil if (address == 0) fail("vkGetBufferDeviceAddressKHR", VK_ERROR_UNKNOWN); entry.buffer = buffer; entry.memory = VK_NULL_HANDLE; - entry.address = address; + entry.address = address + lead; entry.chunk = std::move(chunks); entry.dmaBuf = true; + entry.lead = lead; return true; } #endif @@ -3556,7 +3560,7 @@ void GuestBufferMemory::UploadPrepare(bool addressable) { // device-local buffer that the CPU fallback replaces (none without a recorder to // record the copies: UploadFinish then binds the region in place). bool staged = Recorder::Active() != nullptr && stagingEligible(region, addressable); - const bool misaligned = !bindableInPlace(region.begin - entry->base, addressable); + const bool misaligned = !bindableInPlace(HostImportOffset(*entry, region.begin), addressable); if (staged) { const auto allocateStart = std::chrono::steady_clock::now(); region.buffer = stagingBuffer(context, static_cast(region.end - region.begin), gpuCopyUsage(addressable)); @@ -3760,7 +3764,7 @@ void GuestBufferMemory::UploadFinish(bool addressable) { // A staged region (see stagingEligible) is copied out of the import even when aligned; // without a recorder to record the copies it binds in place like any other. const bool staged = recorder != nullptr && stagingEligible(region, addressable); - if (entry != nullptr && !staged && bindableInPlace(region.begin - entry->base, addressable)) { + if (entry != nullptr && !staged && bindableInPlace(HostImportOffset(*entry, region.begin), addressable)) { region.direct = entry; region.runCut = runImport; region.snapshot.clear(); @@ -3776,7 +3780,7 @@ void GuestBufferMemory::UploadFinish(bool addressable) { copyOnGpu = true; region.deviceLocal = staged; region.copySource = entry->buffer; - region.copySourceBase = entry->base; + region.copySourceBase = entry->base - entry->lead; } } if (region.direct != nullptr) { @@ -4104,7 +4108,7 @@ VkDescriptorBufferInfo GuestBufferMemory::Descriptor(std::uint64_t address, std: return std::string(text); }; if (!(address >= region.begin && address + bytes <= region.end && (region.buffer != nullptr || region.direct != nullptr || region.mirror != nullptr))) Require(false, describe("exceeds")); - const auto base = region.direct != nullptr ? region.direct->base : region.mirror != nullptr ? region.mirror->base : region.begin; + const auto base = region.direct != nullptr ? region.direct->base - region.direct->lead : region.mirror != nullptr ? region.mirror->base : region.begin; const auto offset = address - base; Require(context.limits.minStorageBufferOffsetAlignment != 0, "no storage buffer offset alignment"); Require(base % 4 == 0, "a guest buffer view in a GPU owner that does not start at a DWORD boundary is not implemented"); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index ed284ea658..e974879d80 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -2312,7 +2312,7 @@ void StorageTexture::upload(const std::vector* layers) { batch = std::make_unique(context); commands = batch->Handle(); } - const auto importOffset = descriptor.baseAddress - import->base; + const auto importOffset = HostImportOffset(*import, descriptor.baseAddress); if (direct) { recordDirectUploadBarrier(commands, layers == nullptr); for (std::uint32_t layer = 0; layer < arrayLayers; ++layer) { @@ -2738,7 +2738,7 @@ std::uint64_t StorageTexture::uploadWindows(const HostImport& import, std::span< batch = std::make_unique(context); commands = batch->Handle(); } - const auto importOffset = descriptor.baseAddress - import.base; + const auto importOffset = HostImportOffset(import, descriptor.baseAddress); if (direct) { recordDirectUploadBarrier(commands, discard); for (const auto& window : windows) { @@ -2871,7 +2871,7 @@ std::uint64_t StorageTexture::writeBackWindows(std::span se if (!shadowed.empty() && shadowed.back().end == guestBegin && shadowed.back().slab == destination->slab) shadowed.back().end = pieceEnd; else shadowed.push_back({guestBegin, pieceEnd, destination->slab}); } else { - importCopies[segment].push_back({scratchOffset, guestBegin - import.base, pieceEnd - guestBegin}); + importCopies[segment].push_back({scratchOffset, HostImportOffset(import, guestBegin), pieceEnd - guestBegin}); if (!imported.empty() && imported.back().second == guestBegin) imported.back().second = pieceEnd; else imported.emplace_back(guestBegin, pieceEnd); } @@ -2966,7 +2966,7 @@ std::uint64_t StorageTexture::writeBackWindows(std::span se } for (const auto& [begin, end, slab, import] : seeds) { // The piece's own slab (a seeded unit is one the piece covers partly). - const VkBufferCopy seed{begin - import->base, SlabOffset(*import, *slab, begin), end - begin}; + const VkBufferCopy seed{HostImportOffset(*import, begin), SlabOffset(*import, *slab, begin), end - begin}; context.Resolved(&DeviceFunctions::cmdCopyBuffer, "vkCmdCopyBuffer")(commands, import->buffer, slab->buffer, 1, &seed); NoteShadowSeed(begin, end); } @@ -4779,7 +4779,7 @@ void StorageTexture::writeBackLayers(const std::vector& layers) { for (const auto& [from, to] : keep) { const auto begin = std::max(from, segment.begin); const auto end = std::min(to, segment.end); - if (begin < end) copies.push_back({begin - descriptor.baseAddress, begin - segment.import->base, end - begin}); + if (begin < end) copies.push_back({begin - descriptor.baseAddress, HostImportOffset(*segment.import, begin), end - begin}); } if (!copies.empty()) context.Function("vkCmdCopyBuffer")(commands, tiledScratch->Handle(), segment.import->buffer, static_cast(copies.size()), copies.data()); } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/UnitShadow.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/UnitShadow.cpp index 45d57d64da..dc1bb39758 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/UnitShadow.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/UnitShadow.cpp @@ -606,7 +606,7 @@ std::vector ShadowSources(const Context& context, const HostImport& i std::vector result; if (runs.empty()) return result; const auto importPiece = [&](std::uint64_t begin, std::uint64_t end) { - result.push_back({begin, end, import.buffer, surfaceBase + begin - import.base, false, nullptr}); + result.push_back({begin, end, import.buffer, HostImportOffset(import, surfaceBase + begin), false, nullptr}); if (countReads) Stats().detiledImportBytes.fetch_add(end - begin, std::memory_order_relaxed); }; std::shared_ptr shadow; @@ -668,7 +668,7 @@ std::vector ShadowSources(const Context& context, const HostImport& i slab->lastUse = present; result.push_back({pieceBegin, pieceEnd, slab->buffer, SlabOffset(import, *slab, surfaceBase + pieceBegin), true, slab}); } else { - result.push_back({pieceBegin, pieceEnd, import.buffer, surfaceBase + pieceBegin - import.base, false, nullptr}); + result.push_back({pieceBegin, pieceEnd, import.buffer, HostImportOffset(import, surfaceBase + pieceBegin), false, nullptr}); } (isFresh ? shadowBytes : importBytes) += pieceEnd - pieceBegin; } diff --git a/core/libs/prx/libSceAgcDriver/tests/HostImportSparseDmaBuf.cpp b/core/libs/prx/libSceAgcDriver/tests/HostImportSparseDmaBuf.cpp index 204e3213f7..55d8438206 100644 --- a/core/libs/prx/libSceAgcDriver/tests/HostImportSparseDmaBuf.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/HostImportSparseDmaBuf.cpp @@ -58,6 +58,7 @@ struct MockDevice { std::vector binds; VkResult bindResult = VK_SUCCESS; std::uint64_t nextAddress = 0x500000000ull; + VkDeviceSize sparseAlignment = Alignment; }; MockDevice mock; @@ -95,8 +96,9 @@ VKAPI_ATTR void VKAPI_CALL mockDestroyBuffer(VkDevice, VkBuffer buffer, const Vk } VKAPI_ATTR void VKAPI_CALL mockGetBufferMemoryRequirements(VkDevice, VkBuffer buffer, VkMemoryRequirements* requirements) { - requirements->size = (mock.sizes.at(buffer) + Alignment - 1) / Alignment * Alignment; - requirements->alignment = Alignment; + const auto alignment = mock.sparse.contains(buffer) ? mock.sparseAlignment : Alignment; + requirements->size = (mock.sizes.at(buffer) + alignment - 1) / alignment * alignment; + requirements->alignment = alignment; requirements->memoryTypeBits = 1; } @@ -252,14 +254,35 @@ void ImportsALargeRangeAsOneSparseBuffer() { Expect(memories.size() == chunks, "two parts of the range are bound to the same chunk import"); } -void UnalignedOffsetIsDecidedBeforeImporting() { +void UnalignedOffsetStartsAtTheBlockBelow() { const auto bytes = RangeBytes(); - SharedRange range(bytes, 4096, true); + constexpr std::uint64_t lead = 4096; + SharedRange range(bytes, lead, true); const auto context = mockContext(); mock.binds.clear(); + const auto address = mock.nextAddress; + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + const auto* import = HostImportFor(context, range.Address(), static_cast(bytes)); + Expect(import != nullptr, "a range whose backing offset is not 64 KiB aligned was not imported above udmabuf's size limit"); + if (import == nullptr) return; + Expect(mock.sparse.contains(import->buffer) && import->lead == lead, "the import does not start its sparse buffer at the 64 KiB block below the range"); + Expect(import->address == address + lead && HostImportOffset(*import, range.Address()) == lead, "the import's device address or buffer offset does not point at the range itself"); + Expect(!mock.binds.empty() && mock.binds.front().resourceOffset == 0 && mock.binds.front().memoryOffset == 0, "the first bind does not map the block below the range"); + VkDeviceSize bound = 0; + for (const auto& bind : mock.binds) bound += bind.size; + Expect(bound >= lead + bytes, "the binds do not cover the range after its lead"); +} + +void LargerSparseBlockIsDecidedBeforeImporting() { + const auto bytes = RangeBytes(); + SharedRange range(bytes, Alignment, true); + const auto context = mockContext(); + mock.binds.clear(); + mock.sparseAlignment = 2 * Alignment; const auto memories = mock.memories.size(); std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); const auto* import = HostImportFor(context, range.Address(), static_cast(bytes)); + mock.sparseAlignment = Alignment; Expect(import == nullptr, "a range whose backing offset is not on the sparse block size was imported above udmabuf's size limit"); Expect(mock.memories.size() == memories && mock.binds.empty(), "a chunk was imported or bound for a range that does not take the sparse path"); } @@ -300,7 +323,8 @@ int main() { GuestArena::GuestArenaSetSharedBacking_nid_postfix(Resolve); try { ImportsALargeRangeAsOneSparseBuffer(); - UnalignedOffsetIsDecidedBeforeImporting(); + UnalignedOffsetStartsAtTheBlockBelow(); + LargerSparseBlockIsDecidedBeforeImporting(); FailureAfterTheDecisionThrows(false, VK_SUCCESS, "UDMABUF_CREATE"); FailureAfterTheDecisionThrows(true, VK_ERROR_DEVICE_LOST, "vkQueueBindSparse"); } catch (const std::exception& error) { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SparseDmaBufImport.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SparseDmaBufImport.cpp index 680b8fcfa8..8c514008df 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/SparseDmaBufImport.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SparseDmaBufImport.cpp @@ -165,8 +165,30 @@ int main() { Require(value == 0x5EEDF00Du, "sparse import: the GPU fill did not reach the backing memory across the chunk boundary"); } Require(mapping[boundary - 32769] == 0x11 && mapping[boundary + 32768] == 0x11, "sparse import: the GPU fill reached past its range"); - GuestAllocations::Mutation().Remove(mapping); + + constexpr std::uint64_t Lead = 16384; + auto* unaligned = mapping + Lead; + const auto unalignedBytes = bytes - 65536; + std::memset(mapping + boundary - 65536, 0x22, 131072); + const auto unalignedAddress = reinterpret_cast(unaligned); + device->ImportGuestMemory(Register(unaligned, unalignedBytes), GuestAllocations::GuestAllocationsGeneration_nid_postfix(), false); + Require(Imported(device->Device(), unalignedAddress, static_cast(unalignedBytes)), "sparse import: a range whose backing offset is not 64 KiB aligned was not imported"); + { + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + const std::array pattern{0xA11CE5EDu, 0xA11CE5EDu, 0xA11CE5EDu, 0xA11CE5EDu}; + filled = device->FillBuffer(address + boundary - 32768, 65536, pattern); + device->WaitIdle(); + } + Require(filled, "sparse import: the fill across the chunk boundary of the unaligned range was not served by the import"); + for (std::uint64_t offset = boundary - 32768; offset < boundary + 32768; offset += 4) { + std::uint32_t value = 0; + std::memcpy(&value, mapping + offset, sizeof(value)); + Require(value == 0xA11CE5EDu, "sparse import: the GPU fill of the unaligned range missed its guest address"); + } + Require(mapping[boundary - 32769] == 0x22 && mapping[boundary + 32768] == 0x22, "sparse import: the GPU fill of the unaligned range reached past its range"); + GuestAllocations::Mutation().Remove(unaligned); + GuestAllocations::Mutation().Remove(probe); std::free(probe); std::cout << "AGC sparse dma-buf import tests passed\n"; diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 71210637ea..32aa371b37 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -80,7 +80,7 @@ - Windows host-pointer imports of the guest mapping itself are compared instead of write-watched, except writable imports of private `MEM_WRITE_WATCH` memory of 1 MiB or more when the import probe keeps the write watch and a settle queue exists (see Pages under an import window): a driver can mark imported pages dirty after the synchronous import window. Shared memory imported through a separate writable alias retains protection-based tracking on the guest views; driver access to the import alias does not disarm those views. [GuestMemory](../../../core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp) excludes every overlapping 64 KiB tracker block while its backing is retained. A full block replaced by fresh private backing regains tracking and gets a new generation; protection changes, partial replacements and shared remappings do not restore it. Excluded blocks keep their write-watch marks as a hint only (`UntrackedHint`): a storage image or a sampled texture snapshot over them compares its bytes again after a mark in its range, a driver store, a mapping change or a recorded GPU write instead of at every collect epoch or lookup, and a mark the driver makes without a CPU store costs one comparison. This preserves CPU edits through byte comparisons but limits snapshot reuse on excluded blocks to draws of one collect epoch and disables unit shadows for them; restoring tracking on import retirement without replacing the backing remains unimplemented. - [Pages under an import window](../../../core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp) (libSceAgcDriver) whose write protection was resolved while a host import that leaves its range watched was made are compared with the bytes they held before the import and count as stored over only when those differ. A CPU store of the bytes already there within the window cannot be told apart from an import that resolves the write protection without storing, as the NVIDIA driver (610.57.04) does for every imported page, so such a store does not win over pending results. Where host-pointer and dma-buf imports differ in whether they stay watched, a failed host-pointer import and the dma-buf import after it are two windows, and the pages the failed one resolved count as stored over. On Windows, the NVIDIA driver (617.42) marks every page of a host import of private `MEM_WRITE_WATCH` memory written, asynchronously, until the import's first GPU use completes. Such an import stays watched only after a 4-byte read of it on the settle queue has completed inside the window. Each one waits for that read, which keeps its window open for one round trip on the settle queue, so a same-value CPU store from another thread in that time counts as unchanged too. An import made where no settle queue exists, and an import of under 1 MiB, is compared instead: the settle blocks the importing thread, and a title that imports a 64 KiB buffer around every submission lost about 19 % of its frame rate to that wait (Dripl1x, #6060). Settling without blocking the importing thread (submit the read, keep the import compared until its fence signals, then switch it to the write watch) is not implemented. - [Settled dma-buf imports](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver): on Linux a dma-buf import of 1 MiB or more is read by a 4-byte copy on the settle queue right after it is made (`settleImport`), so the kernel maps its pages for the GPU there and not in the first submission that uses it (about 2 s for a 3 GiB import on RADV). The import's buffer is shared with the settle queue's family. An import made at map time settles on the guest thread that maps the range, inside its map call, so that thread waits while the kernel maps the pages: 0.47 to 2.2 s for the 3 GiB range of Hades (PPSA03355) and 0.49 to 4.3 s for the 4.5 GiB range of ASTRO BOT (PPSA21567) on RADV. Both titles map these ranges at start-up; a title that maps large ranges during gameplay stalls its mapping thread for as long. An import made from a submission settles under the GPU mutex, so its frame still pays for it; a smaller import, and every import on a device with no settle queue (one without a copy-capable queue besides the main one), pays in its first submission. A range imported through a host pointer, as lavapipe imports shared memory, is not settled -- [Sparse dma-buf imports](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver): on Linux a shared-backed range larger than one 64 MiB udmabuf buffer is imported as one sparse buffer bound to the 64 MiB buffers, on a queue of its own when the device has a second family that binds sparse memory. The path is decided before anything is imported, and a failure after that throws. A range takes the single udmabuf import instead, which udmabuf's `size_limit_mb` refuses above 64 MiB by default so the range is copied, when its offset in the backing file is not a multiple of the buffer's sparse block size (64 KiB on RADV), when the sparse buffer does not accept every memory type a chunk import can have, when the backing file ends inside its last chunk, or when it is imported at map time on a device with no second family (the bind then needs the GPU mutex); the NVIDIA proprietary driver imports no dma-buf memory +- [Sparse dma-buf imports](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver): on Linux a shared-backed range larger than one 64 MiB udmabuf buffer is imported as one sparse buffer bound to the 64 MiB buffers, on a queue of its own when the device has a second family that binds sparse memory. The path is decided before anything is imported, and a failure after that throws. A range whose offset in the backing file is not a multiple of 64 KiB starts its sparse buffer at the 64 KiB block below it (`HostImport::lead`). A range takes the single udmabuf import instead, which udmabuf's `size_limit_mb` refuses above 64 MiB by default so the range is copied, when the buffer's sparse block size is larger than 64 KiB (it is 64 KiB on RADV), when the sparse buffer does not accept every memory type a chunk import can have, when the backing file ends inside its last chunk, or when it is imported at map time on a device with no second family (the bind then needs the GPU mutex); the NVIDIA proprietary driver imports no dma-buf memory - [Unnormalized S# coordinates](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp) (libSceAgcDriver) - `FORCE_UNNORMALIZED` uses a Vulkan `unnormalizedCoordinates` sampler, so the S# LOD range, LOD bias and mip filter are not applied, which is exact only on a one-level view. - An [unnormalized S#](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp) (libSceAgcDriver) maps clamp mode 0 (repeat) on X and Y to clamp-to-edge as required by Vulkan unnormalized coordinates, and assigns the magnification filter when minification and magnification filters differ (since unnormalized coordinates sample at LOD 0). Anisotropy, TRUNC_COORD, MC_COORD_TRUNC and other clamp modes still throw. Secondary LOD bias (LOD_BIAS_SEC, bits [19:14]) is accepted and ignored, matching RDNA2 hardware measurements across explicit and implicit samples. High on Life (PPSA15388, #4736) uses secondary LOD bias and unnormalized coordinates with clamp mode 0 and differing min/mag filters during home base and city rendering. - [Background link-time-optimized relink of library pipelines](../../../core/libs/prx/libSceAgcDriver/Execution/include/PipelineLinkOptimization.hpp) (libSceAgcDriver) is skipped on every GPU under NVIDIA's proprietary Vulkan driver (`VK_DRIVER_ID_NVIDIA_PROPRIETARY`), so those pipelines keep their fast-linked form. The optimized link loses the device when bound on an RTX 3080 (Ampere, #2866) and an RTX 4060 (Ada, #3434); an RTX 5060 and an RTX 5080 (Blackwell) on the same driver do not, so the scope is wider than the evidence. No standard Vulkan query tells the architecture apart, and the driver version that fixes it is unknown. `APS5_NO_PIPELINE_LTO` disables the relink on every driver. diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index 15a4f6d625..e8302fa234 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -115,7 +115,7 @@ On Windows, a self-built `libs/` also needs `libgcc_s_seh-1.dll`, `libstdc++-6.d On Windows, direct memory (`sceKernelAllocateDirectMemory`, up to 13824 MiB per title) is committed when the title maps it, not when the title allocates it or first uses its pages. The system commit limit (installed memory plus page file size, the second value of Committed in Task Manager) must cover the mapped direct memory together with all other committed memory. Otherwise `sceKernelMapDirectMemory` throws `replace guest placeholder with resident direct memory` with the Windows error (`commit direct memory pages` when a page is mapped at a second address, `keep unmapped direct memory` when it is unmapped); enlarge the page file or close other applications. -On Linux, when the Vulkan driver imports dma-buf memory (not the NVIDIA proprietary driver), shared direct memory is imported through `/dev/udmabuf`, and the user who runs the game needs read-write access to it. Many distributions create it as `root:kvm` with mode `0660`: add the user to the `kvm` group and log in again (an ACL such as `setfacl -m u:$USER:rw /dev/udmabuf` lasts until the next reboot). Without access, startup prints `[gpu] open /dev/udmabuf: Permission denied`, these ranges are copied instead of imported, and GPU stores to them through FLAT/GLOBAL addresses fail with `BDA access failed`. A range larger than 64 MiB is imported as one sparse buffer over 64 MiB udmabuf buffers when the device binds sparse memory (RADV, ANV). Without sparse binding, or when the range does not start on the device's sparse block size in the backing file, ranges above udmabuf's `size_limit_mb` (64 MiB by default) are copied as well. +On Linux, when the Vulkan driver imports dma-buf memory (not the NVIDIA proprietary driver), shared direct memory is imported through `/dev/udmabuf`, and the user who runs the game needs read-write access to it. Many distributions create it as `root:kvm` with mode `0660`: add the user to the `kvm` group and log in again (an ACL such as `setfacl -m u:$USER:rw /dev/udmabuf` lasts until the next reboot). Without access, startup prints `[gpu] open /dev/udmabuf: Permission denied`, these ranges are copied instead of imported, and GPU stores to them through FLAT/GLOBAL addresses fail with `BDA access failed`. A range larger than 64 MiB is imported as one sparse buffer over 64 MiB udmabuf buffers when the device binds sparse memory (RADV, ANV). Without sparse binding, or when the device's sparse block size is larger than 64 KiB, ranges above udmabuf's `size_limit_mb` (64 MiB by default) are copied as well. Linux: From 9eb7e2444f439661498e6c63607d8fbd37d9c462 Mon Sep 17 00:00:00 2001 From: hugodearaujo Date: Thu, 15 Oct 2026 10:50:40 -0300 Subject: [PATCH 0822/1000] fix(agc): publish unit shadows at an import's lead A unit shadow published its units into the import's buffer at the guest offset from the import's base, without the import's lead, so a sparse import that starts below its range (HostImport::lead) got the units lead bytes before their place. The shadow keeps the lead with the import's base and buffer, and the shadow verify reads the guest bytes at the same place. agc_driver_sparse_dma_buf_import publishes a unit shadow of its unaligned import and checks the unit's guest bytes and the bytes around it; without the lead it fails there. --- .../Graphics/src/UnitShadow.cpp | 10 ++-- .../tests/execution/SparseDmaBufImport.cpp | 47 +++++++++++++++++++ 2 files changed, 53 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/UnitShadow.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/UnitShadow.cpp index dc1bb39758..cd2998c06b 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/UnitShadow.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/UnitShadow.cpp @@ -67,11 +67,12 @@ Statistics& Stats() { struct UnitShadow { Context context; - // The import's range and buffer (taken at creation: a publish under the import table's mutex - // must not look them up), and the unit base (the import's base rounded down to a unit). + // The import's range, buffer and lead (taken at creation: a publish under the import table's + // mutex must not look them up), and the unit base (the import's base rounded down to a unit). std::uint64_t importBase = 0; std::uint64_t importBytes = 0; VkBuffer importBuffer = VK_NULL_HANDLE; + std::uint64_t importLead = 0; std::uint64_t base = 0; // Per unit: 0 = never shadowed, else the tracker generation the slab bytes are current at; // whether a slab -> import copy was recorded at that generation. @@ -162,6 +163,7 @@ std::shared_ptr findOrCreate(Shadows& registry, const Context& conte shadow->importBase = import.base; shadow->importBytes = import.bytes; shadow->importBuffer = import.buffer; + shadow->importLead = import.lead; shadow->base = import.base & ~(UnitBytes - 1); const auto units = (import.base + import.bytes - shadow->base + UnitBytes - 1) / UnitBytes; shadow->generation.assign(static_cast(units), 0); @@ -279,7 +281,7 @@ void verifyPublished(const UnitShadow& shadow, const std::vector& co RecordMemoryBarrier(context, batch.Handle(), VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_HOST_READ_BIT); batch.SubmitAndWait(); host.Invalidate(); - const auto address = shadow.importBase + region.dstOffset; + const auto address = shadow.importBase + region.dstOffset - shadow.importLead; Stats().verified.fetch_add(1, std::memory_order_relaxed); if (!GuestMemory::Accessible(reinterpret_cast(address), static_cast(region.size)) || std::memcmp(reinterpret_cast(address), host.Bytes().data(), static_cast(region.size)) != 0) { Stats().mismatches.fetch_add(1, std::memory_order_relaxed); @@ -336,7 +338,7 @@ std::size_t publishUnits(const std::shared_ptr& shadow, std::uint64_ } const auto [begin, end] = shadow->UnitRange(unit); if (end <= begin) continue; - const VkBufferCopy region{(unit - slab->firstUnit) * UnitBytes + (begin - shadow->UnitBegin(unit)), begin - shadow->importBase, end - begin}; + const VkBufferCopy region{(unit - slab->firstUnit) * UnitBytes + (begin - shadow->UnitBegin(unit)), begin - shadow->importBase + shadow->importLead, end - begin}; if (!copies.empty() && copies.back().slab == slab && copies.back().regions.back().dstOffset + copies.back().regions.back().size == region.dstOffset) { copies.back().regions.back().size += region.size; ranges.back().second = end; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SparseDmaBufImport.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SparseDmaBufImport.cpp index 8c514008df..afa7b82c85 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/SparseDmaBufImport.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SparseDmaBufImport.cpp @@ -1,10 +1,14 @@ #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" #include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" +#include "prx/libSceAgcDriver/Graphics/include/UnitShadow.hpp" #include "prx/libc/include/GuestAllocations.hpp" #include "VulkanTestDevice.hpp" #ifndef _WIN32 #include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" #include #include #include @@ -17,6 +21,7 @@ #include #include #include +#include #ifdef _WIN32 int main() { @@ -187,6 +192,48 @@ int main() { Require(value == 0xA11CE5EDu, "sparse import: the GPU fill of the unaligned range missed its guest address"); } Require(mapping[boundary - 32769] == 0x22 && mapping[boundary + 32768] == 0x22, "sparse import: the GPU fill of the unaligned range reached past its range"); + + if (AgcDriver::Graphics::UnitShadowEnabled()) { + using namespace AgcDriver::Graphics; + constexpr std::uint64_t Unit = 65536; + const auto unitBegin = ((unalignedAddress + Unit - 1) & ~(Unit - 1)) + Unit; + auto* const unitBytes = reinterpret_cast(unitBegin); + std::memset(unitBytes - Lead, 0x33, Unit + 2 * Lead); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(mapping, static_cast(mappingBytes)); + std::size_t published = 0; + { + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + const auto context = AgcDriver::VulkanDeviceTestAccess::ContextOf(*device); + const auto* import = HostImportFor(context, unalignedAddress, static_cast(unalignedBytes)); + Require(import != nullptr && import->lead == Lead, "sparse import: the unaligned import does not start its buffer at the block below it"); + auto* recorder = Recorder::Active(); + if (recorder != nullptr && AgcDriver::GuestMemory::Watched(unitBegin, Unit)) { + AgcDriver::GuestMemory::CollectWritesUncached(unitBegin, Unit); + const auto destination = ShadowDestinationFor(context, *import, unitBegin, unitBegin + Unit); + Require(destination.has_value(), "sparse import: no unit shadow for a whole unit of the unaligned import"); + const auto commands = recorder->Commands(); + recorder->Keep(destination->slab); + context.Function("vkCmdFillBuffer")(commands, destination->buffer, destination->offset, Unit, 0x5AD0BEEFu); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT); + recorder->MarkCovered(VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT); + const ShadowedRange range{unitBegin, unitBegin + Unit, destination->slab}; + MarkShadowed(*import, std::span(&range, 1), AgcDriver::GuestMemory::TrackerGeneration()); + published = PublishShadow(unitBegin, Unit, PublishScope::Whole, PublishReason::Hook); + recorder->Sync(); + } + } + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(mapping, static_cast(mappingBytes)); + if (published == 0) { + std::printf("note: the unaligned import's unit was not shadowed (no recorder or not watched); the publish was not checked\n"); + } else { + for (std::uint64_t offset = 0; offset < Unit; offset += 4) { + std::uint32_t value = 0; + std::memcpy(&value, unitBytes + offset, sizeof(value)); + Require(value == 0x5AD0BEEFu, "sparse import: a unit shadow of the unaligned import was not published at its guest address"); + } + for (std::uint64_t offset = 0; offset < Lead; ++offset) Require((unitBytes - Lead)[offset] == 0x33 && unitBytes[Unit + offset] == 0x33, "sparse import: a unit shadow of the unaligned import was published outside its unit"); + } + } GuestAllocations::Mutation().Remove(unaligned); GuestAllocations::Mutation().Remove(probe); From 52c4589f830dcf1b356386adfca7c0088b479fd6 Mon Sep 17 00:00:00 2001 From: hugodearaujo Date: Fri, 16 Oct 2026 08:21:47 -0300 Subject: [PATCH 0823/1000] test(agc): check the unaligned import's lead only for a dma-buf import A device that imports shared memory through host pointers, such as lavapipe, imports the unaligned range that way, and a host-pointer import has no lead. --- .../libSceAgcDriver/tests/execution/SparseDmaBufImport.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SparseDmaBufImport.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SparseDmaBufImport.cpp index afa7b82c85..949c7ef5f4 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/SparseDmaBufImport.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SparseDmaBufImport.cpp @@ -205,7 +205,9 @@ int main() { std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); const auto context = AgcDriver::VulkanDeviceTestAccess::ContextOf(*device); const auto* import = HostImportFor(context, unalignedAddress, static_cast(unalignedBytes)); - Require(import != nullptr && import->lead == Lead, "sparse import: the unaligned import does not start its buffer at the block below it"); + Require(import != nullptr, "sparse import: the unaligned range has no import"); + if (!import->dmaBuf) std::printf("note: the device imported the unaligned range through a host pointer, which has no lead\n"); + Require(import->lead == (import->dmaBuf ? Lead : 0), "sparse import: the unaligned import does not start its buffer at the block below it"); auto* recorder = Recorder::Active(); if (recorder != nullptr && AgcDriver::GuestMemory::Watched(unitBegin, Unit)) { AgcDriver::GuestMemory::CollectWritesUncached(unitBegin, Unit); From 05b457d49a71609f0d8b8ab31da00ef02adf8026 Mon Sep 17 00:00:00 2001 From: Nicolas Falesy <112726756+nicolasfalesy@users.noreply.github.com> Date: Sat, 17 Oct 2026 14:06:11 -0400 Subject: [PATCH 0824/1000] feat(libkernel): implement pthread_condattr_setpshared PSFlyCast (PPSA99247) imports pthread_condattr_setpshared (3BpP850hBT4) from libkernel and fails to load without it: "./app.elf: symbol lookup error: ./app.elf: undefined symbol: 3BpP850hBT4". The attribute stores PTHREAD_PROCESS_PRIVATE (0) or PTHREAD_PROCESS_SHARED (1); a null or uninitialized attribute and any other value return EINVAL without changing it, following FreeBSD 11 libthr thr_condattr.c and pthread_mutexattr_setpshared here. Sharing across processes is not modeled, as for mutexes. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Sovthwyu4H63RMbRVha2Sy --- core/libs/prx/libkernel/Pthread/Posix/Cond.cpp | 10 ++++++++++ core/libs/prx/libkernel/Pthread/include/Pthread.hpp | 1 + core/libs/tests/GuestPthreadCondattr.cpp | 7 +++++++ docs/dev/debt/libkernel.md | 1 + 4 files changed, 19 insertions(+) diff --git a/core/libs/prx/libkernel/Pthread/Posix/Cond.cpp b/core/libs/prx/libkernel/Pthread/Posix/Cond.cpp index 5c5e64d18c..f939a865e6 100644 --- a/core/libs/prx/libkernel/Pthread/Posix/Cond.cpp +++ b/core/libs/prx/libkernel/Pthread/Posix/Cond.cpp @@ -6,6 +6,9 @@ namespace { +constexpr int POSIX_PROCESS_PRIVATE = 0; +constexpr int POSIX_PROCESS_SHARED = 1; + int toPosix(int result) { if (result == 0) return 0; @@ -64,4 +67,11 @@ int APS5_VABI pthread_condattr_setclock_nid_postfix(PthreadCondattr* attr, Kerne return toPosix(scePthreadCondattrSetclock(attr, clock_id)); } +int APS5_VABI pthread_condattr_setpshared_nid_postfix(PthreadCondattr* attr, int pshared) { + if (!attr || !*attr) return PosixThread::GUEST_EINVAL; + if (pshared != POSIX_PROCESS_PRIVATE && pshared != POSIX_PROCESS_SHARED) return PosixThread::GUEST_EINVAL; + (*attr)->_pshared = pshared; + return 0; +} + } diff --git a/core/libs/prx/libkernel/Pthread/include/Pthread.hpp b/core/libs/prx/libkernel/Pthread/include/Pthread.hpp index 97c4c8d075..2596301626 100644 --- a/core/libs/prx/libkernel/Pthread/include/Pthread.hpp +++ b/core/libs/prx/libkernel/Pthread/include/Pthread.hpp @@ -59,6 +59,7 @@ struct PthreadRwlockPrivate { struct PthreadCondattrPrivate { int _clockid; + int _pshared = 0; }; struct PthreadCondPrivate { diff --git a/core/libs/tests/GuestPthreadCondattr.cpp b/core/libs/tests/GuestPthreadCondattr.cpp index a605eda3a1..64cfef4b62 100644 --- a/core/libs/tests/GuestPthreadCondattr.cpp +++ b/core/libs/tests/GuestPthreadCondattr.cpp @@ -7,6 +7,7 @@ int APS5_VABI scePthreadCondattrInit(PthreadCondattr* attr); int APS5_VABI scePthreadCondattrDestroy(PthreadCondattr* attr); int APS5_VABI scePthreadCondattrSetclock(PthreadCondattr* attr, KernelClockid clockId); int APS5_VABI pthread_condattr_setclock_nid_postfix(PthreadCondattr* attr, KernelClockid clockId); +int APS5_VABI pthread_condattr_setpshared_nid_postfix(PthreadCondattr* attr, int pshared); } static constexpr int SCE_OK = 0; @@ -23,6 +24,8 @@ int main() { Require(scePthreadCondattrDestroy(nullptr) == SceEinval); Require(scePthreadCondattrDestroy(&unset) == SceEinval); Require(pthread_condattr_setclock_nid_postfix(&unset, Monotonic) == GuestEinval); + Require(pthread_condattr_setpshared_nid_postfix(nullptr, 0) == GuestEinval); + Require(pthread_condattr_setpshared_nid_postfix(&unset, 0) == GuestEinval); PthreadCondattr attr = nullptr; Require(scePthreadCondattrInit(&attr) == SCE_OK); @@ -30,6 +33,10 @@ int main() { Require(scePthreadCondattrSetclock(&attr, clock) == SCE_OK); Require(pthread_condattr_setclock_nid_postfix(&attr, clock) == 0); } + for (const int sharing : {0, 1}) + Require(pthread_condattr_setpshared_nid_postfix(&attr, sharing) == 0); + for (const int sharing : {-1, 2}) + Require(pthread_condattr_setpshared_nid_postfix(&attr, sharing) == GuestEinval); Require(scePthreadCondattrDestroy(&attr) == SCE_OK && attr == nullptr); Require(scePthreadCondattrDestroy(&attr) == SceEinval); } diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 5791f7ab5e..da342d864f 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -124,6 +124,7 @@ - [Windows directory descriptors](../../../core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp) (libkernel) take one snapshot of the directory, on the first `getdents`, `getdirentries` or non-zero `lseek` (`SEEK_SET` or `SEEK_CUR`), and keep it until it is discarded; `lseek` to offset 0 only discards the snapshot, and the next read lists the directory again, so every rewind costs O(entries) on the next read. `lseek` takes the offset as an entry index and `SEEK_CUR` with offset 0 only reports the index. `SEEK_END` throws, where the NUL descriptor used to return 0, because FreeBSD defines it relative to the directory size, which is not modelled. A negative result returns `EINVAL` and a `SEEK_CUR` offset that overflows returns `EOVERFLOW`, as FreeBSD `vn_seek` reports them. Names whose UTF-8 encoding exceeds the one-byte `d_namlen` limit are not representable in this guest directory-record layout: `getdents` returns `ENAMETOOLONG` without consuming the entry. Repeated calls therefore report the same error and entries after that name cannot be reached through this descriptor; FreeBSD's `getdents` behavior for host filenames that exceed its `MAXNAMLEN` is not verified. `getdirentries` still reports 0 through `basep` instead of the position of the block it read, so only 0 and values returned by `lseek` are valid offsets; FreeBSD offsets are filesystem cookies - [/dev/random and /dev/urandom](../../../core/libs/prx/libkernel/File/src/RandomDevice.cpp) (libkernel) opened for reading return a descriptor backed by the host null device whose reads are filled with host OS entropy (`getentropy`, `rand_s` on Windows); both paths behave the same, never block, and positioned reads ignore the offset (FreeBSD `random(4)`). Only the exact paths are recognised, `fstat`, `lseek` and `mmap` see the null device, and opening them for writing throws - [pthread_mutexattr_getpshared and pthread_mutexattr_setpshared](../../../core/libs/prx/libkernel/Pthread/Posix/Mutex.cpp) (libkernel) store and report private or shared attributes, following FreeBSD 11 libthr [thr_mutexattr.c](https://github.com/freebsd/freebsd-src/blob/stable/11/lib/libthr/thread/thr_mutexattr.c). Invalid values return `EINVAL` without changing the attribute. Guest threads execute in one host process; sharing mutexes across processes is not modeled. Console behavior and invalid-attribute error precedence are unverified. +- [pthread_condattr_setpshared](../../../core/libs/prx/libkernel/Pthread/Posix/Cond.cpp) (libkernel) stores a private or shared attribute, following FreeBSD 11 libthr [thr_condattr.c](https://github.com/freebsd/freebsd-src/blob/stable/11/lib/libthr/thread/thr_condattr.c): a null or uninitialized attribute and values other than `PTHREAD_PROCESS_PRIVATE` (0) and `PTHREAD_PROCESS_SHARED` (1) return `EINVAL` without changing it. Guest threads execute in one host process; sharing condition variables across processes is not modeled. PSFlyCast (PPSA99247) imports it and fails to load without it. Console behavior is unverified. - [pthread_attr_getinheritsched, pthread_attr_setinheritsched, pthread_attr_getscope, pthread_attr_setscope, pthread_attr_getstackaddr](../../../core/libs/prx/libkernel/Pthread/Posix/Attr.cpp) (libkernel) follow FreeBSD 11 libthr ([thr_attr.c](https://github.com/freebsd/freebsd-src/blob/5ed7eb0d97ba4436218e810f61bd059acba984c2/lib/libthr/thread/thr_attr.c), [thr_create.c](https://github.com/freebsd/freebsd-src/blob/5ed7eb0d97ba4436218e810f61bd059acba984c2/lib/libthr/thread/thr_create.c)), not measured on the console: the scope defaults to `PTHREAD_SCOPE_SYSTEM` (2), only it and `PTHREAD_SCOPE_PROCESS` (0) are accepted and other values return `EINVAL`, and an inherit value other than `PTHREAD_INHERIT_SCHED` (4) or `PTHREAD_EXPLICIT_SCHED` (0) returns `ENOTSUP`. `pthread_attr_getstackaddr` returns the configured stack address pointer (null by default). A null output of `pthread_attr_getinheritsched` throws, since libthr writes through it unchecked. A thread keeps the scope it was created with, taken from the creating thread under `PTHREAD_INHERIT_SCHED`, and `pthread_attr_get_np` and `scePthreadAttrGet` report it, but every guest thread is a host thread at system scope, so process scope changes nothing - [Shared memory](../../../core/libs/prx/libkernel/File/src/SharedMemory.cpp) (libkernel) on Linux implements anonymous and named objects, bounded descriptor writes and seeks, and `shm_unlink` with effective read/write permission checks from FreeBSD 11. Named paths up to 1023 bytes use `/anyps5-shm-` plus SHA-1; collisions are not detected, and the namespace is shared across processes in the host mount namespace. Named creation applies the guest umask without changing the host mask. Linux procfs and `/dev/shm` are required. Operations use an advisory directory lock; external host operations bypass it, unrelated objects serialize, and descriptor operations require two spare host descriptors and compare device/inode with the initial descriptor identity on both the pinned-object and ordinary-file fallback paths. [Guest mappings](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) support shared and private mappings of these descriptors through the allocation tracker and pin a duplicate descriptor during validation and mapping; mapping requires one spare host descriptor; file-backed mappings use the write-watch scan fallback because aliases and descriptor writes bypass per-mapping tracking. Named objects and file-backed mappings on Windows remain unsupported. Invalid non-null name pointers, console behaviour and title compatibility remain unverified. - [sceKernelAddReadEvent and sceKernelAddWriteEvent](../../../core/libs/prx/libkernel/Equeue/Equeue.cpp) (libkernel): a write event reports the socket's send buffer size (`SO_SNDBUF`) in `data`, not the free space left in it, as Windows has no query for that space. Only guest sockets are supported; other descriptors and a low watermark above 1 throw From e5726355067282a81b2a3ac7b8b99328a1302f15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Sat, 17 Oct 2026 20:08:51 +0200 Subject: [PATCH 0825/1000] ci(build): run test jobs only for the parts of build.yml a change touches A change to build.yml ran the relinker, register-context and write-watch jobs whatever it touched. The patch's lines are now mapped to the job sections of the workflow at the head, and a test job runs only if its own section, the eligibility job or the shared part outside jobs changed; without a patch every job runs as before. Co-Authored-By: Claude --- .github/workflows/build.yml | 63 ++++++++++++++++++++++++++++++++++--- 1 file changed, 59 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e94f541117..06267f9a7d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -70,17 +70,70 @@ jobs: const { owner, repo } = context.repo; const workflow = '.github/workflows/build.yml'; const libc = 'core/libs/prx/libc/'; + const jobOfLine = (text) => { + let inJobs = false; + let job = ''; + return text.split('\n').map((line) => { + if (/^\S/.test(line)) { + inJobs = /^jobs:\s*$/.test(line); + job = ''; + } else if (inJobs) { + job = /^ ([\w-]+):\s*$/.exec(line)?.[1] ?? job; + } + return job; + }); + }; + const patchedJobs = (patch, text) => { + const jobs = jobOfLine(text); + const touched = new Set(); + let line = 1; + for (const row of patch.split('\n')) { + const hunk = /^@@ -\d+(?:,\d+)? \+(\d+)/.exec(row); + if (hunk) { + line = Number(hunk[1]); + } else if (row.startsWith('+')) { + touched.add(jobs[line - 1] ?? ''); + line += 1; + } else if (row.startsWith('-')) { + touched.add(jobs[line - 2] ?? '').add(jobs[line - 1] ?? ''); + } else if (row.startsWith(' ')) { + line += 1; + } + } + return touched; + }; + const workflowJobs = new Set(); + const readWorkflow = async (files, ref) => { + const file = files.find((file) => file.filename === workflow || file.previous_filename === workflow); + if (!file) { + return; + } + if (file.filename !== workflow || !file.patch) { + workflowJobs.add('*'); + return; + } + try { + const { data } = await github.rest.repos.getContent({ owner, repo, path: workflow, ref }); + const text = Buffer.from(data.content, 'base64').toString('utf8'); + patchedJobs(file.patch, text).forEach((job) => workflowJobs.add(job)); + } catch (error) { + core.warning(`Could not read ${workflow} at ${ref}, so every test job runs: ${error.message}`); + workflowJobs.add('*'); + } + }; const select = (paths, all) => { const touches = (...prefixes) => all || paths.some((path) => prefixes.some((prefix) => prefix.endsWith('/') ? path.startsWith(prefix) : path === prefix)); + const edits = (job) => ['*', '', 'eligibility', job].some((name) => workflowJobs.has(name)); return { - relinker: touches( + relinker: edits('relinker') || touches( 'core/relinker/', libc, 'core/libs/prx/libkernel/AppMetadata/', 'core/libs/tests/ParamJsonParser.cpp', 'core/shader/recompiler/tests/CacheFileReadTests.cpp', 'core/shader/recompiler/ShaderCacheDirectory.hpp', - 'tools/', 'cmake/', 'CMakeLists.txt', workflow + 'tools/', 'cmake/', 'CMakeLists.txt' ), - registerContext: touches('core/libs/tests/register_context/', 'core/libs/tests/RegisterContext.cpp', libc, workflow), - writeWatch: touches('core/libs/tests/write_watch/', 'core/libs/tests/WriteWatch.cpp', libc, workflow), + registerContext: edits('register-context') || + touches('core/libs/tests/register_context/', 'core/libs/tests/RegisterContext.cpp', libc), + writeWatch: edits('write-watch') || touches('core/libs/tests/write_watch/', 'core/libs/tests/WriteWatch.cpp', libc), }; }; const relinkerHosts = (full, linux) => JSON.stringify([ @@ -102,6 +155,7 @@ jobs: const files = data.files ?? []; all = files.length >= 300; paths = files.flatMap((file) => [file.filename, file.previous_filename].filter(Boolean)); + await readWorkflow(files, context.sha); } catch (error) { core.warning(`Could not list the files of this push, so every test job runs: ${error.message}`); } @@ -167,6 +221,7 @@ jobs: const files = await github.paginate(github.rest.pulls.listFiles, { owner, repo, pull_number, per_page: 100 }); truncated ||= files.length >= 3000; paths.push(...files.flatMap((file) => [file.filename, file.previous_filename].filter(Boolean))); + await readWorkflow(files, pr.head.sha); pulls.push({ number: pull_number, head: pr.head.sha }); } const run = select(paths, truncated); From 1ffffc318d6f0c573f40b5de0da4219cb2d3e63c Mon Sep 17 00:00:00 2001 From: Nicolas Falesy <112726756+nicolasfalesy@users.noreply.github.com> Date: Sat, 17 Oct 2026 14:08:53 -0400 Subject: [PATCH 0826/1000] feat(libkernel): export nmount Prospero Themes (PPSA90210) imports nmount (3YnXspQNyw8) from libkernel and fails to load without it: "./app.elf: symbol lookup error: ./app.elf: undefined symbol: 3YnXspQNyw8". Mounting file systems is not supported, so it throws if called, like execvp does for executable replacement. The title opens its theme library without calling it. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Sovthwyu4H63RMbRVha2Sy --- core/libs/prx/libkernel/File/src/Stdio.cpp | 8 ++++++++ docs/dev/debt/libkernel.md | 1 + 2 files changed, 9 insertions(+) diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index eacf4004dc..d039bcbe1d 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -604,6 +604,14 @@ int APS5_VABI chflags_nid_postfix(const char* path, std::uint64_t flags) { return 0; } +int APS5_VABI nmount_nid_postfix(const KernelIovec* iov, unsigned int niov, int flags) { + (void)iov; + (void)niov; + (void)flags; + NotImplemented_nid_no_patch("nmount: mounting file systems"); + return -1; +} + int APS5_VABI close_nid_postfix(int d) { if (File::IsHostStream(d)) return PosixFailure(GUEST_EBADF); int socketError = 0; diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 5791f7ab5e..6733f5f6de 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -163,6 +163,7 @@ - [getrlimit](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - `RLIMIT_CPU`, `RLIMIT_FSIZE`, `RLIMIT_CORE`, `RLIMIT_SBSIZE`, `RLIMIT_VMEM`/`AS`, `RLIMIT_NPTS`, `RLIMIT_SWAP`, `RLIMIT_KQUEUES` and `RLIMIT_UMTXP` report infinity as in FreeBSD's process-0 defaults, and `RLIMIT_DATA` mirrors the host limit. `RLIMIT_NOFILE` reports, as both soft and hard limit, the number of descriptors after which AnyPS5 itself fails with `EMFILE`: on Windows the C runtime's 8192 low-level descriptors ([`_setmaxstdio`](https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/setmaxstdio)), which guest files, pipes and sockets all take, and on Linux the host's `RLIMIT_NOFILE`, whose soft limit libkernel raises to the hard limit at load. The console's limit is not known; XPSemu (PPSA97358) reads it in QEMU's `os_setup_limits` and raises the soft limit to the hard one when they differ. `RLIMIT_STACK`, `RLIMIT_RSS`, `RLIMIT_MEMLOCK` and `RLIMIT_NPROC` throw because no console values are established. - [setrlimit](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) validates the resource and input in FreeBSD order, maps negative limits to infinity, and lowers a soft limit above the hard limit. A request succeeds when its normalized values match `getrlimit`, or for `RLIMIT_NOFILE` when `rlim_max` does not exceed the current hard limit and `rlim_cur` does not exceed `rlim_max`, storing the updated limits for subsequent `getrlimit` queries; descriptor allocation enforcement of the lowered soft limit is not implemented, and changing other limits throws because guest limit state is not stored. Java 25 LAN / DNS (PPSA99869) sets `rlim_cur = MIN(0x100000, rlim_max)` during VM initialization - [execvp](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) throws: replacing the process image is not supported. PPSA85300 imports it and fails to load without it; its `fork` and `vfork` fail with `ENOSYS` ([libc_missing.c](https://github.com/ZiZc3/VLC/blob/f3253020a64368153233ede29183014c0b6c8979/ps5/compat/libc_missing.c#L290-L292)), so the child process that would call it is never created. +- [nmount](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) throws: mounting file systems is not supported. Prospero Themes (PPSA90210) imports it and fails to load without it; it opens its theme library without calling it, and its strings (`/dev/ssd0.system_ex`, `/system_ex`, `exfatfs`, `fstype`, `fspath`, `from`, `ignoreacl`) point to remounting the console's `/system_ex` partition to write theme files there. - [socketpair](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) - the pair is the host's `AF_UNIX` pair on Linux and, on Windows, a loopback TCP connection for `SOCK_STREAM` or two connected UDP sockets for `SOCK_DGRAM`; `SOCK_SEQPACKET` throws on Windows. A datagram sent to a full receiver gets `ENOBUFS` on the console, waits or gets `EAGAIN` on Linux and is dropped on Windows, and on Windows a datagram `recv` after the peer closed reports `ECONNRESET` instead of the console's `ENOTCONN`, and a stream `send` after the peer closed reports `ECONNABORTED` instead of the console's `EPIPE`. `read` and `write` on a pair end go to the host like on every guest socket. `getpeername` on a pair end succeeds after the peer closed, where the console returns `ENOTCONN`, and `connect`, `bind` and `sendto` with an address on a pair end report an address-family error instead of the console's `EISCONN`. On Windows `poll` on a stream end reports `POLLHUP` without `POLLIN` after the peer's `shutdown(SHUT_WR)` or `close`, where the console reports `POLLIN`, with `POLLHUP` only after `close` - [UDP recv/recvfrom](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) and [sceNetRecv/sceNetRecvfrom](../../../core/libs/prx/libSceNet/Export.cpp) translate Windows WSAEMSGSIZE into the copied byte count for datagram sockets, including MSG_PEEK, and retain the sender address. This follows FreeBSD 11.4 recv(2) and Winsock's documented truncated-datagram behavior; console behavior and existing title impact are unverified. - [MSG_DONTWAIT](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) in `send`, `sendto`, `sendmsg`, `recv`, `recvfrom` and `recvmsg` makes that one call non-blocking through the host's `MSG_DONTWAIT`, as FreeBSD 11.4's `soreceive_generic` returns `EWOULDBLOCK` for it (`sys/kern/uipc_socket.c`). Winsock has no per-call non-blocking flag, so on Windows these calls still fail with `EOPNOTSUPP` for it From 4aaf276999f355da629a58cf9b30c68e84123f79 Mon Sep 17 00:00:00 2001 From: Gjon Gojcevic Date: Sat, 17 Oct 2026 12:56:41 -0400 Subject: [PATCH 0827/1000] fix(recompiler): give a dynamic-mip storage image a slot for every level A dynamic-mip storage image reserved four mip slots, so a compute shader whose T# spans more levels failed with "runtime storage image mip capacity exceeded". PPSA01317 hits it on boot with a 512x512 surface of ten levels and aborts. An RDNA T# addresses at most sixteen levels (4-bit BASE_LEVEL and LAST_LEVEL), so a dynamic-mip image now reserves sixteen slots, and a typed storage heap holds 64 elements so four such images still share one class. Heap arrays keep their compact length. The runtime ABI goes to 14, so existing shader caches are invalidated. AI-assisted: yes --- .../libSceAgcDriver/tests/ShaderMemory.cpp | 20 +++++++++++++++++++ core/shader/recompiler/RuntimeAbi.hpp | 6 +++--- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 51dee75d38..9fcf42b87e 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -5167,6 +5167,25 @@ void verifyVolumeArrayLoadMode() { expectFailure([&] { static_cast(ResourceMaterializer::RuntimeImageMode(store, descriptor, ResourceMaterializer::RuntimeImageModes(store))); }, "incompatible with the static runtime image interface", "2D-array image atomic on a 3D T# was accepted"); } +void verifyDynamicStorageMipChain() { + using namespace ShaderRecompiler; + ImageResource image{}; + image.resourceClass = ImageResourceClass::Storage; + image.numericClass = IrTextureNumericClass::Float; + image.dimension = RdnaImageDimension::Dim2D; + image.mipMode = ImageMipMode::DynamicStorage; + image.written = true; + image.fmaskCompatible = false; + const auto modes = ResourceMaterializer::RuntimeImageModes(image); + const DescriptorValue tenLevels{{0x12366e00u, 0xc2400000u, 0x007fc07fu, 0xd1b903acu, 0x17u, 0x00700090u, 0u, 0u}, 8u}; + const auto selected = ResourceMaterializer::TryRuntimeImageMode(image, tenLevels, modes); + require(selected.mode.has_value(), "a dynamic-mip storage image of a 512x512 surface with ten levels was rejected"); + require(modes[*selected.mode].mipCount >= 10u, "a dynamic-mip storage mode does not hold ten levels"); + auto allLevels = tenLevels; + allLevels.dwords[3] |= 0xf0000u; + require(ResourceMaterializer::TryRuntimeImageMode(image, allLevels, modes).mode.has_value(), "a dynamic-mip storage image with sixteen levels was rejected"); +} + void verifySrgbDepthCompareModes() { using namespace ShaderRecompiler; for (const auto decodeFormats : {0u, 1u, 2u, 3u}) { @@ -5286,6 +5305,7 @@ int main(int argc, char** argv) { verifyGuardedNullPointers(); verifyCubeDepthCompareMode(); verifyVolumeArrayLoadMode(); + verifyDynamicStorageMipChain(); verifySrgbDepthCompareModes(); verifyQuadMaskNeighbourReads(); verifyCoherentGds(); diff --git a/core/shader/recompiler/RuntimeAbi.hpp b/core/shader/recompiler/RuntimeAbi.hpp index ce3489c14f..26929a5881 100644 --- a/core/shader/recompiler/RuntimeAbi.hpp +++ b/core/shader/recompiler/RuntimeAbi.hpp @@ -9,7 +9,7 @@ namespace ShaderRecompiler::RuntimeAbi { -inline constexpr std::uint32_t Version = 13u; +inline constexpr std::uint32_t Version = 14u; inline constexpr std::uint32_t DescriptorSet = 0u; inline constexpr std::uint32_t FragmentDescriptorSet = 1u; inline constexpr std::uint32_t StageCount = 4u; @@ -40,8 +40,8 @@ inline constexpr std::uint32_t BufferCapacity = 128u; inline constexpr std::uint32_t ImageCapacity = 256u; inline constexpr std::uint32_t SampledHeapCapacity = 64u; inline constexpr std::uint32_t BindlessTableSlots = 16u; -inline constexpr std::uint32_t StorageMipSlots = 4u; -inline constexpr std::uint32_t StorageHeapCapacity = 16u; +inline constexpr std::uint32_t StorageMipSlots = 16u; +inline constexpr std::uint32_t StorageHeapCapacity = 64u; inline constexpr std::uint32_t SamplerHeapCapacity = 64u; inline constexpr std::uint32_t StateExportMappings = 0u; inline constexpr std::uint32_t StateDwords = 2u; From 521babcd68ef25566b64d0f47e9690ea66a7fc3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Sat, 17 Oct 2026 20:12:53 +0200 Subject: [PATCH 0828/1000] ci(build): run the relinker job's tests in parallel The relinker job ran its 182 tests one at a time: 233 s on windows-latest, most of it Python tool tests. The full windows job already runs the same tests with --parallel 4. Co-Authored-By: Claude --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 06267f9a7d..24b952b4a2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -316,7 +316,7 @@ jobs: run: echo 'C:\winlibs\mingw64\bin' >> "$GITHUB_PATH" - run: cmake -S . -B build-relinker -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_COMPILER=${{ matrix.compiler }} -DANYPS5_RELINKER_ONLY=ON -DBUILD_TESTING=ON - run: cmake --build build-relinker --parallel 3 - - run: ctest --test-dir build-relinker --output-on-failure --timeout 120 + - run: ctest --test-dir build-relinker --output-on-failure --timeout 120 --parallel 4 register-context: needs: eligibility From 6f4ffeecc0228d86327b1dc16f64d3fc53151e77 Mon Sep 17 00:00:00 2001 From: Lyrilika Date: Sat, 17 Oct 2026 20:17:17 +0200 Subject: [PATCH 0829/1000] fix(tools): check every compilation configuration --- tools/check_compile.py | 27 ++++++++-------- tools/tests/test_check_compile.py | 51 +++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 12 deletions(-) diff --git a/tools/check_compile.py b/tools/check_compile.py index 6c596ccb5f..c4d86db827 100644 --- a/tools/check_compile.py +++ b/tools/check_compile.py @@ -55,7 +55,7 @@ def entry_key(entry, root): def index(build, root): commands = {} for entry in entries(build): - commands.setdefault(entry_key(entry, root), entry) + commands.setdefault(entry_key(entry, root), []).append(entry) return commands @@ -131,20 +131,23 @@ def check(root, build, files): for file in files: source = file.resolve() name = os.path.relpath(source, root).replace(os.sep, "/") - entry = commands.get(name) - if entry is None: + configurations = commands.get(name) + if configurations is None: print(f"{name}: no compile command in {build}", file=sys.stderr) continue checked += 1 - command = (syntax_arguments(entry["arguments"], Path(entry["directory"]), source) - if "arguments" in entry else syntax_only(entry, source)) - result = subprocess.run(command, cwd=entry["directory"], - capture_output=True, text=True, encoding="utf-8", errors="replace") - if result.returncode == 0: - continue - failed += 1 - output = (result.stdout or "") + (result.stderr or "") - sys.stderr.write(output if output.endswith("\n") else output + "\n") + source_failed = False + for entry in configurations: + command = (syntax_arguments(entry["arguments"], Path(entry["directory"]), source) + if "arguments" in entry else syntax_only(entry, source)) + result = subprocess.run(command, cwd=entry["directory"], + capture_output=True, text=True, encoding="utf-8", errors="replace") + if result.returncode == 0: + continue + source_failed = True + output = (result.stdout or "") + (result.stderr or "") + sys.stderr.write(output if output.endswith("\n") else output + "\n") + failed += int(source_failed) return checked, failed diff --git a/tools/tests/test_check_compile.py b/tools/tests/test_check_compile.py index e0844ab9fe..03f437be67 100644 --- a/tools/tests/test_check_compile.py +++ b/tools/tests/test_check_compile.py @@ -96,6 +96,57 @@ class CheckCompileTests(unittest.TestCase): self.assertEqual(result.returncode, 0) self.assertEqual(json.loads(seen.read_text(encoding="utf-8")), ["-fsyntax-only", str(source)]) + def test_every_configuration_is_checked_and_each_source_is_counted_once(self): + source = self.source("unit.cpp") + script = self.compiler( + "import json\nimport sys\nfrom pathlib import Path\n" + "with Path(sys.argv[1]).open('a') as log:\n" + " log.write(json.dumps([str(Path.cwd()), sys.argv[2:]]) + '\\n')\n" + "if sys.argv[2] == '1':\n" + " sys.stderr.write(sys.argv[3] + ': configuration failed\\n')\n" + "sys.exit(int(sys.argv[2]))\n") + seen = self.root / "invocations.jsonl" + second = self.build / "second" + second.mkdir() + for statuses in ((0, 0), (0, 1), (1, 0), (1, 1)): + with self.subTest(statuses=statuses): + seen.write_text("", encoding="utf-8") + first = self.entry("unit.cpp", f'"{sys.executable}" "{script}" "{seen}" ' + f'{statuses[0]} first -c -o first.o "{source}"') + other = {"directory": str(second), "file": os.path.relpath(source, second), + "arguments": [sys.executable, str(script), str(seen), + str(statuses[1]), "second", "-c", "-o", "second.o", + os.path.relpath(source, second)]} + self.manifest([first, other]) + result = self.tool("unit.cpp") + self.assertEqual(result.returncode, int(any(statuses)), result.stderr) + invocations = [json.loads(line) for line in seen.read_text(encoding="utf-8").splitlines()] + self.assertEqual(invocations, [ + [str(self.build), [str(statuses[0]), "first", "-fsyntax-only", str(source)]], + [str(second), [str(statuses[1]), "second", "-fsyntax-only", str(source)]], + ]) + if any(statuses): + self.assertIn("1 of 1 translation units did not compile", result.stderr) + else: + self.assertIn("1 translation units compile", result.stdout) + for label, status in zip(("first", "second"), statuses): + self.assertEqual(f"{label}: configuration failed" in result.stderr, bool(status)) + + def test_a_later_configuration_with_a_real_compiler_error_fails(self): + compiler = shutil.which("clang") + if compiler is None: + self.skipTest("Clang is required to check configuration-dependent compilation") + source = self.source("unit.c", "#ifdef SECOND_TARGET\n#error second target must fail\n#endif\nint x;\n") + self.manifest([ + {"directory": str(self.build), "file": str(source), + "arguments": [compiler, *flags, "-c", str(source)]} + for flags in ([], ["-DSECOND_TARGET"]) + ]) + result = self.tool("unit.c") + self.assertEqual(result.returncode, 1, result.stdout) + self.assertIn("second target must fail", result.stderr) + self.assertIn("1 of 1 translation units did not compile", result.stderr) + @unittest.skipUnless(os.name == "nt", "Windows command-line quoting") def test_windows_paths_and_quoted_defines_survive_the_syntax_check(self): source = self.source("unit.cpp") From 5a1039bbc1fc0d7f3c478dac915e10fe03260081 Mon Sep 17 00:00:00 2001 From: Nithishvelan M Date: Sat, 17 Oct 2026 23:51:50 +0530 Subject: [PATCH 0830/1000] feat(libSceVoiceQoS): implement sceVoiceQoSSetRemoteEndpointAttribute --- core/libs/prx/libSceVoiceQoS/Export.cpp | 5 ++--- core/libs/tests/GuestVoiceQoS.cpp | 8 ++++++++ docs/dev/debt/libSceVoiceQoS.md | 2 +- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceVoiceQoS/Export.cpp b/core/libs/prx/libSceVoiceQoS/Export.cpp index 9813b98413..d8dc8fe238 100644 --- a/core/libs/prx/libSceVoiceQoS/Export.cpp +++ b/core/libs/prx/libSceVoiceQoS/Export.cpp @@ -140,9 +140,8 @@ int APS5_VABI sceVoiceQoSSetLocalEndpointAttribute(int32_t localId, int32_t attr return Locked([&](QoS& qos) { return SetAttribute(qos.locals, localId, attributeId, value, size); }); } -int APS5_VABI sceVoiceQoSSetRemoteEndpointAttribute() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceVoiceQoSSetRemoteEndpointAttribute(int32_t remoteId, int32_t attributeId, const void* value, int32_t size) { + return Locked([&](QoS& qos) { return SetAttribute(qos.remotes, remoteId, attributeId, value, size); }); } int APS5_VABI sceVoiceQoSReadPacket() { diff --git a/core/libs/tests/GuestVoiceQoS.cpp b/core/libs/tests/GuestVoiceQoS.cpp index 272b43a1a0..cfe4c4f3bb 100644 --- a/core/libs/tests/GuestVoiceQoS.cpp +++ b/core/libs/tests/GuestVoiceQoS.cpp @@ -13,6 +13,7 @@ int APS5_VABI sceVoiceQoSDeleteRemoteEndpoint(std::int32_t); int APS5_VABI sceVoiceQoSConnect(std::int32_t*, std::int32_t, std::int32_t); int APS5_VABI sceVoiceQoSDisconnect(std::int32_t); int APS5_VABI sceVoiceQoSSetLocalEndpointAttribute(std::int32_t, std::int32_t, const void*, std::int32_t); +int APS5_VABI sceVoiceQoSSetRemoteEndpointAttribute(std::int32_t, std::int32_t, const void*, std::int32_t); } static void Require(bool value) { if (!value) std::abort(); } @@ -63,6 +64,12 @@ int main(int argc, char** argv) { Require(sceVoiceQoSSetLocalEndpointAttribute(local, 3, &level, -1) == 0); Require(sceVoiceQoSSetLocalEndpointAttribute(local + 100, 3, &level, sizeof(level)) == argumentInvalid); + Require(sceVoiceQoSSetRemoteEndpointAttribute(remote, 3, &level, sizeof(level)) == 0); + Require(sceVoiceQoSSetRemoteEndpointAttribute(remote, 3, nullptr, sizeof(level)) == argumentInvalid); + Require(sceVoiceQoSSetRemoteEndpointAttribute(remote, 3, &level, 0) == 0); + Require(sceVoiceQoSSetRemoteEndpointAttribute(remote, 3, &level, -1) == 0); + Require(sceVoiceQoSSetRemoteEndpointAttribute(remote + 100, 3, &level, sizeof(level)) == argumentInvalid); + Require(sceVoiceQoSDisconnect(connection) == 0); Require(sceVoiceQoSDisconnect(connection) == argumentInvalid); Require(sceVoiceQoSDeleteLocalEndpoint(local) == 0); @@ -74,6 +81,7 @@ int main(int argc, char** argv) { Require(sceVoiceQoSEnd() == notInitialized); Require(sceVoiceQoSDeleteRemoteEndpoint(remote) == notInitialized); Require(sceVoiceQoSCreateLocalEndpoint(&local, 1, 0, 0) == notInitialized); + Require(sceVoiceQoSSetRemoteEndpointAttribute(remote, 3, &level, sizeof(level)) == notInitialized); Require(sceVoiceQoSInit(memory.data(), 0x40000, appType) == 0); Require(sceVoiceQoSDeleteRemoteEndpoint(remote) == argumentInvalid); Require(sceVoiceQoSEnd() == 0); diff --git a/docs/dev/debt/libSceVoiceQoS.md b/docs/dev/debt/libSceVoiceQoS.md index 49bb102e5a..19e9efcc63 100644 --- a/docs/dev/debt/libSceVoiceQoS.md +++ b/docs/dev/debt/libSceVoiceQoS.md @@ -3,7 +3,7 @@ ### Unknown function info - [sceVoiceQoSSetConnectionAttribute](../../../core/libs/prx/libSceVoiceQoS/Export.cpp) (libSceVoiceQoS, NID `KCGOxg8iX7s`) is a throwing stub declared without parameters, like the other endpoint calls of the library: PPSA04159 fails to load without it and its signature is unknown. The name hashes to the imported NID (`tools/nid_names.py`) -- [sceVoiceQoSInit](../../../core/libs/prx/libSceVoiceQoS/Export.cpp) (libSceVoiceQoS) - `0x804E0905` for a second initialization comes from [prosper](https://github.com/mattias800/prosper)'s reading of the module and was not measured: on a PS5 a valid call blocks inside a homebrew payload, so it never returns there. PPSA26344's two call sites (`0xE307BF`, `0x151C45D`) pass an allocated pointer, a 0x40000-byte block and app type 0x10000000; the app type and block are not used. `sceVoiceQoSCreateLocalEndpoint` takes four arguments (`int* id, userId, deviceInId, deviceOutId`) as declared in fpPS4 (`src/ps4_libscevoice.pas`, `ps4_sceVoiceQoSCreateLocalEndpoint`); no public reference exists for the other six signatures (`DeleteLocalEndpoint(id)`, `CreateRemoteEndpoint(int* id)`, `DeleteRemoteEndpoint(id)`, `Connect(int* id, local, remote)`, `Disconnect(connection)`, `SetLocalEndpointAttribute(id, attribute, value, size)`), which are assumed from function naming and call patterns and not confirmed on a PS5 title. Endpoints and connections are tracked by allocated id; duplicate connections and deleting an endpoint while referenced in a connection are permitted as the console is not known to reject them; local endpoint attributes are stored as given (including empty or non-positive sizes), as their ids and defaults are unknown; other endpoint and packet calls remain throwing stubs. The not-initialized error 0x804E0901 is assumed from the 0x804E09xx numbering, and unknown ids return `ARGUMENT_INVALID`. Aliens: Fireteam Elite (PPSA03393) uses all seven implemented calls on its first load path, starting with `sceVoiceQoSCreateLocalEndpoint` when it registers the local talker; it stops with `not implemented` when any of them throws +- [sceVoiceQoSInit](../../../core/libs/prx/libSceVoiceQoS/Export.cpp) (libSceVoiceQoS) - `0x804E0905` for a second initialization comes from [prosper](https://github.com/mattias800/prosper)'s reading of the module and was not measured: on a PS5 a valid call blocks inside a homebrew payload, so it never returns there. PPSA26344's two call sites (`0xE307BF`, `0x151C45D`) pass an allocated pointer, a 0x40000-byte block and app type 0x10000000; the app type and block are not used. `sceVoiceQoSCreateLocalEndpoint` takes four arguments (`int* id, userId, deviceInId, deviceOutId`) as declared in fpPS4 (`src/ps4_libscevoice.pas`, `ps4_sceVoiceQoSCreateLocalEndpoint`); no public reference exists for the other seven signatures (`DeleteLocalEndpoint(id)`, `CreateRemoteEndpoint(int* id)`, `DeleteRemoteEndpoint(id)`, `Connect(int* id, local, remote)`, `Disconnect(connection)`, `SetLocalEndpointAttribute(id, attribute, value, size)`), which are assumed from function naming and call patterns and not confirmed on a PS5 title. Endpoints and connections are tracked by allocated id; duplicate connections and deleting an endpoint while referenced in a connection are permitted as the console is not known to reject them; local endpoint attributes are stored as given (including empty or non-positive sizes), as their ids and defaults are unknown; other endpoint and packet calls remain throwing stubs. The not-initialized error 0x804E0901 is assumed from the 0x804E09xx numbering, and unknown ids return `ARGUMENT_INVALID`. Aliens: Fireteam Elite (PPSA03393) uses all eight implemented calls on its first load path, starting with `sceVoiceQoSCreateLocalEndpoint` when it registers the local talker; it stops with `not implemented` when any of them throws ### Functional From 90fbcba2c40b5e35de9ebb21a69e95c13297ec06 Mon Sep 17 00:00:00 2001 From: Eli Yarson Date: Sat, 17 Oct 2026 15:22:36 -0300 Subject: [PATCH 0831/1000] test(libc): take the overcommit skip only under mode 1 --- core/libs/tests/GuestAllocatorFailures.cpp | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/core/libs/tests/GuestAllocatorFailures.cpp b/core/libs/tests/GuestAllocatorFailures.cpp index 594e03b0d7..d1eb3f8e82 100644 --- a/core/libs/tests/GuestAllocatorFailures.cpp +++ b/core/libs/tests/GuestAllocatorFailures.cpp @@ -162,7 +162,14 @@ int main() { #endif if (committed) { #ifdef __linux__ - std::fprintf(stderr, "skipped: the host can commit 300 GiB (vm.overcommit_memory=%d), so the failed commit and arena release checks did not run\n", overcommitMode()); + int mode = overcommitMode(); + if (mode == 1) { + std::fprintf(stderr, "skipped: the host can commit 300 GiB (vm.overcommit_memory=%d), so the failed commit and arena release checks did not run\n", mode); + } else { + char detail[64]; + std::snprintf(detail, sizeof(detail), "committed 300 GiB with vm.overcommit_memory=%d", mode); + report("malloc(300 GiB)", detail); + } #else std::fputs("skipped: the host can commit 300 GiB, so the failed commit and arena release checks did not run\n", stderr); #endif From 9efdc59ef1e19c80edfc92e826cdc7b8a5933271 Mon Sep 17 00:00:00 2001 From: Romans Pokrovskis Date: Sat, 17 Oct 2026 20:50:04 +0300 Subject: [PATCH 0832/1000] test(agc): exercise a full native depth clear through BDA --- .../tests/execution/NativeDepthBdaClear.cpp | 190 ++++++++++++++++++ 1 file changed, 190 insertions(+) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/NativeDepthBdaClear.cpp diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/NativeDepthBdaClear.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/NativeDepthBdaClear.cpp new file mode 100644 index 0000000000..e51584f8a3 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/NativeDepthBdaClear.cpp @@ -0,0 +1,190 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace AgcDriver { + +struct VulkanDeviceTestAccess { + static Graphics::Context Context(const VulkanDevice& device) { return device.graphicsContext(); } +}; + +} + +namespace { + +using AgcDriver::Graphics::Require; +namespace Memory = AgcDriver::GuestMemory; + +constexpr std::size_t BlockBytes = 1048576; + +class GuestBlock { +public: + GuestBlock() { + Require(Memory::WriteWatched(), "native depth BDA clear requires guest write watching"); +#ifdef _WIN32 + block = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(BlockBytes, BlockBytes)); + Require(block != nullptr, "cannot allocate the native depth BDA clear block"); + section = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_READWRITE, 0, BlockBytes, nullptr); + Require(section != nullptr, "cannot create the native depth BDA clear backing"); + GuestArena::GuestArenaMapDirect_nid_postfix(block, BlockBytes, section, 0, PAGE_READWRITE); +#else + void* raw = mmap(nullptr, 2 * BlockBytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + Require(raw != MAP_FAILED, "cannot map the native depth BDA clear block"); + const auto begin = reinterpret_cast(raw); + const auto aligned = (begin + BlockBytes - 1) & ~(static_cast(BlockBytes) - 1); + if (aligned != begin) munmap(raw, aligned - begin); + if (aligned != begin + BlockBytes) munmap(reinterpret_cast(aligned + BlockBytes), begin + BlockBytes - aligned); + block = reinterpret_cast(aligned); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(block, BlockBytes); +#endif + Require(Memory::Watched(Address(), BlockBytes), "the native depth BDA clear block is not watched"); + std::memset(block, 0xcd, BlockBytes); + GuestAllocations::Mutation().Add(block, BlockBytes, true, true, true); + } + + ~GuestBlock() { + GuestAllocations::Mutation().Remove(block); +#ifdef _WIN32 + GuestArena::GuestArenaReset_nid_postfix(block, BlockBytes); + GuestArena::GuestArenaRelease_nid_postfix(block, BlockBytes); + GuestArena::GuestArenaForgetSection_nid_postfix(section); + CloseHandle(section); +#else + munmap(block, BlockBytes); + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(block, BlockBytes); +#endif + } + + GuestBlock(const GuestBlock&) = delete; + GuestBlock& operator=(const GuestBlock&) = delete; + std::uint64_t Address() const { return reinterpret_cast(block); } + const std::uint8_t* Data() const { return block; } + void FillWith(std::uint8_t value) const { std::memset(block, value, BlockBytes); } + +private: + std::uint8_t* block = nullptr; +#ifdef _WIN32 + HANDLE section = nullptr; +#endif +}; + +int RunNativeClear(AgcDriver::VulkanDevice& device) { + using namespace ShaderRecompiler; + using namespace AgcDriver::Graphics; + const std::lock_guard lock(Memory::GpuMutex()); + if (!Memory::WriteWatched()) { + std::puts("skipped, guest write watching is unavailable"); + return VulkanTestSkipped; + } + const GuestBlock depth, metadata; + metadata.FillWith(0); + constexpr VkExtent2D extent{5u, 3u}; + const auto metadataBytes = static_cast(HtileMetadataBytes(extent)); + alignas(256) static constexpr std::array code{ + 0xd7460000, 0x04010c06, 0x34000084, 0xdc388000, 0x04020000, + 0xbf8c3f70, 0xd7710008, 0x00120805, 0xd7710009, 0x00120a05, + 0xd771000a, 0x00120c05, 0xd771000b, 0x00120e05, + 0xdc788000, 0x00020800, 0xbf810000, + }; + std::array userData{0u, 0u, static_cast(metadata.Address()), static_cast(metadata.Address() >> 32u), 0u, 0u}; + const ShaderComputeStageInfo compute{{64u, 1u, 1u}, 0u, {true, false, false}, false, 1u}; + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(std::span(code))}}}; + const auto groups = static_cast(metadataBytes / 1024u); + const std::array packet{0u, groups, 1u, 1u, 1u}; + Require(HtileDepthClearBytes_nid_no_patch(code, userData, compute, packet, 0u) == metadataBytes, "the native GPU clear kernel is not recognized"); + const auto compile = [&] { + RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {64u, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), {0, 0, 0, 128}, std::nullopt, false, + }; + auto result = Recompile(request); + Require(std::ranges::any_of(result.bindings, [](const auto& binding) { return binding.role == DescriptorRole::BdaPagetable; }), "the native clear shader did not use BDA"); + return result; + }; + device.Dispatch(compile(), groups, 1u, 1u, {}, reinterpret_cast(code.data())); + device.WaitIdle(); + Require(std::all_of(metadata.Data(), metadata.Data() + BlockBytes, [](auto value) { return value == 0u; }), "the warm-up native clear did not retain zero metadata"); + if (!Memory::Watched(depth.Address(), BlockBytes) || !Memory::Watched(metadata.Address(), BlockBytes)) { + std::puts("skipped, native clear imports do not retain guest write watching"); + return VulkanTestSkipped; + } + const auto context = AgcDriver::VulkanDeviceTestAccess::Context(device); + Require(context.recorder != nullptr, "the native clear context has no recorder"); + context.recorder->Activate(); + struct Cleanup { + AgcDriver::VulkanDevice& device; + ~Cleanup() { device.WaitIdle(); ClearCachedTextures(device.Device()); ClearDepthSurfaces(device.Device()); } + } cleanup{device}; + DepthTarget target{depth.Address(), 0u, extent, VK_FORMAT_D32_SFLOAT, 0.0f, 0u, metadata.Address(), false, 4u}; + target.depthSwizzle = 24u; + target.htileDepthOnly = target.iterate256 = target.htilePipeAligned = true; + const auto view = DepthSurfaceView(context, target); + const auto base = target.address >> 8u; + const auto htile = target.htileAddress >> 8u; + const std::array words{ + static_cast(base), static_cast((base >> 32u) & 255u) | (22u << 20u), + 1u | (2u << 14u), (14u << 28u) | (24u << 20u) | (2u << 16u) | 0x204u, 0u, 2u << 4u, + (1u << 10u) | (1u << 19u) | (1u << 21u) | (static_cast(htile & 255u) << 24u), static_cast(htile >> 8u), + }; + const auto sampled = CachedSampledTexture(context, words); + Require(sampled != nullptr, "the native GPU clear has no initial sampled view"); + device.WaitIdle(); + target.clearDepth = 1.0f; + Require(DepthSurfaceView(context, target) == view, "changing the native clear value replaced its attachment"); + NoteHtileDepthClear(target.htileAddress, metadataBytes, metadataBytes); + const auto generation = Memory::TrackerGeneration(); + userData[4] = 0xfffffff0u; + device.Dispatch(compile(), groups, 1u, 1u, {}, reinterpret_cast(code.data())); + device.WaitIdle(); + for (std::size_t offset = 0; offset < BlockBytes; offset += 4u) { + std::uint32_t value = 0; + std::memcpy(&value, metadata.Data() + offset, 4u); + Require(value == (offset < metadataBytes ? 0xfffffff0u : 0u), "the native GPU clear did not write its exact metadata range"); + } + std::printf("Native full BDA clear: exact %zu-byte metadata readback passed; complete=%d\n", metadataBytes, Memory::StoredAllSince_nid_no_patch(target.htileAddress, metadataBytes, generation)); + Require(CachedSampledTexture(context, words) == sampled, "a completed full BDA clear lost its native sampled view"); + Require(DepthSurfaceView(context, target) == view, "a completed full BDA clear replaced its native attachment"); + device.WaitIdle(); + Require(std::all_of(depth.Data(), depth.Data() + BlockBytes, [](auto value) { return value == 0xcdu; }), "the native GPU clear changed guest depth bytes"); + std::puts("Native full BDA clear accepted"); + return 0; +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + return RunNativeClear(*device); + } catch (const std::exception& error) { + std::fprintf(stderr, "%s\n", error.what()); + return 1; + } +} From bf8556f8e7741c76af90514197037bae5c5c4d88 Mon Sep 17 00:00:00 2001 From: Romans Pokrovskis Date: Sat, 17 Oct 2026 21:25:34 +0300 Subject: [PATCH 0833/1000] test(agc): classify native clear import skips --- .../libSceAgcDriver/tests/execution/NativeDepthBdaClear.cpp | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/NativeDepthBdaClear.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/NativeDepthBdaClear.cpp index e51584f8a3..d1564e3562 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/NativeDepthBdaClear.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/NativeDepthBdaClear.cpp @@ -97,10 +97,6 @@ int RunNativeClear(AgcDriver::VulkanDevice& device) { using namespace ShaderRecompiler; using namespace AgcDriver::Graphics; const std::lock_guard lock(Memory::GpuMutex()); - if (!Memory::WriteWatched()) { - std::puts("skipped, guest write watching is unavailable"); - return VulkanTestSkipped; - } const GuestBlock depth, metadata; metadata.FillWith(0); constexpr VkExtent2D extent{5u, 3u}; @@ -131,7 +127,7 @@ int RunNativeClear(AgcDriver::VulkanDevice& device) { device.WaitIdle(); Require(std::all_of(metadata.Data(), metadata.Data() + BlockBytes, [](auto value) { return value == 0u; }), "the warm-up native clear did not retain zero metadata"); if (!Memory::Watched(depth.Address(), BlockBytes) || !Memory::Watched(metadata.Address(), BlockBytes)) { - std::puts("skipped, native clear imports do not retain guest write watching"); + std::puts("skipped, the device does not retain guest write watching for native clear imports"); return VulkanTestSkipped; } const auto context = AgcDriver::VulkanDeviceTestAccess::Context(device); From 9518b73656f63edfa0d3826c709c05cbc3e2027c Mon Sep 17 00:00:00 2001 From: Nithishvelan M Date: Sat, 17 Oct 2026 23:56:59 +0530 Subject: [PATCH 0834/1000] fix(libSceSaveDataDialog.native): return NOT_RUNNING from progress bar updates when not running --- core/libs/prx/libSceSaveDataDialog.native/Export.cpp | 6 ++++++ core/libs/tests/GuestSaveDataDialog.cpp | 7 +++++++ docs/dev/debt/libSceSaveDataDialog.native.md | 2 +- 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceSaveDataDialog.native/Export.cpp b/core/libs/prx/libSceSaveDataDialog.native/Export.cpp index d042e8c8e7..54c4fd0926 100644 --- a/core/libs/prx/libSceSaveDataDialog.native/Export.cpp +++ b/core/libs/prx/libSceSaveDataDialog.native/Export.cpp @@ -104,12 +104,18 @@ int APS5_VABI sceSaveDataDialogTerminate(void) { int APS5_VABI sceSaveDataDialogProgressBarInc(int target, std::uint32_t delta) { (void)target; (void)delta; + if (g_status != SAVE_DATA_DIALOG_STATUS_RUNNING) { + return SAVE_DATA_DIALOG_ERROR_NOT_RUNNING; + } return SAVE_DATA_DIALOG_OK; } int APS5_VABI sceSaveDataDialogProgressBarSetValue(int target, std::uint32_t rate) { (void)target; (void)rate; + if (g_status != SAVE_DATA_DIALOG_STATUS_RUNNING) { + return SAVE_DATA_DIALOG_ERROR_NOT_RUNNING; + } return SAVE_DATA_DIALOG_OK; } diff --git a/core/libs/tests/GuestSaveDataDialog.cpp b/core/libs/tests/GuestSaveDataDialog.cpp index 2a44ff62d5..25b68bdcb5 100644 --- a/core/libs/tests/GuestSaveDataDialog.cpp +++ b/core/libs/tests/GuestSaveDataDialog.cpp @@ -39,11 +39,15 @@ int main() { Require(sceSaveDataDialogClose(nullptr) == SAVE_DATA_DIALOG_ERROR_NOT_RUNNING, "close before init"); SaveDataDialogResult uninitResult{}; Require(sceSaveDataDialogGetResult(&uninitResult) == SAVE_DATA_DIALOG_ERROR_NOT_FINISHED, "get result before init"); + Require(sceSaveDataDialogProgressBarInc(0, 10) == SAVE_DATA_DIALOG_ERROR_NOT_RUNNING, "progress inc before init"); + Require(sceSaveDataDialogProgressBarSetValue(0, 50) == SAVE_DATA_DIALOG_ERROR_NOT_RUNNING, "progress set value before init"); Require(sceSaveDataDialogInitialize() == 0, "initialize"); Require(sceSaveDataDialogInitialize() == SAVE_DATA_DIALOG_ERROR_ALREADY_INITIALIZED, "double init"); Require(sceSaveDataDialogClose(nullptr) == SAVE_DATA_DIALOG_ERROR_NOT_RUNNING, "close while initialized not running"); Require(sceSaveDataDialogGetResult(&uninitResult) == SAVE_DATA_DIALOG_ERROR_NOT_FINISHED, "get result before open"); + Require(sceSaveDataDialogProgressBarInc(0, 10) == SAVE_DATA_DIALOG_ERROR_NOT_RUNNING, "progress inc while initialized not running"); + Require(sceSaveDataDialogProgressBarSetValue(0, 50) == SAVE_DATA_DIALOG_ERROR_NOT_RUNNING, "progress set value while initialized not running"); SaveDataDirName names[2]{{}, {"SDU1"}}; SaveDataDialogItems items{}; @@ -64,6 +68,7 @@ int main() { Require(sceSaveDataDialogProgressBarSetValue(0, 25) == 0, "set progress"); Require(sceSaveDataDialogProgressBarInc(0, 25) == 0, "increment progress"); RequireRunning(); + Require(sceSaveDataDialogProgressBarInc(0, 10) == 0, "inc progress while running"); Require(sceSaveDataDialogProgressBarSetValue(0, 100) == 0, "complete progress"); RequireRunning(); Require(sceSaveDataDialogGetResult(&uninitResult) == SAVE_DATA_DIALOG_ERROR_NOT_FINISHED, @@ -94,4 +99,6 @@ int main() { Require(sceSaveDataDialogTerminate() == 0, "terminate"); Require(sceSaveDataDialogTerminate() == SAVE_DATA_DIALOG_ERROR_NOT_INITIALIZED, "terminate when already terminated"); Require(sceSaveDataDialogGetStatus() == SAVE_DATA_DIALOG_STATUS_NONE, "terminated state"); + Require(sceSaveDataDialogProgressBarInc(0, 10) == SAVE_DATA_DIALOG_ERROR_NOT_RUNNING, "progress inc after terminate"); + Require(sceSaveDataDialogProgressBarSetValue(0, 50) == SAVE_DATA_DIALOG_ERROR_NOT_RUNNING, "progress set value after terminate"); } diff --git a/docs/dev/debt/libSceSaveDataDialog.native.md b/docs/dev/debt/libSceSaveDataDialog.native.md index ee6bb8dc2d..92ab4e7f4c 100644 --- a/docs/dev/debt/libSceSaveDataDialog.native.md +++ b/docs/dev/debt/libSceSaveDataDialog.native.md @@ -2,4 +2,4 @@ ### Silent stubs -- [libSceSaveDataDialog.native](../../../core/libs/prx/libSceSaveDataDialog.native/Export.cpp) shows no UI and ignores progress values. Progress-bar mode (5) stays running until the caller closes it, matching Skyrim PPSA03747's save sequence; other modes still finish immediately and return button 1 and the first nonempty directory name. Console dialog timing and selection behavior remain unverified. Lifecycle state error codes follow shadPS4: `sceSaveDataDialogClose` returns `SAVE_DATA_DIALOG_ERROR_NOT_RUNNING` whenever not running, `sceSaveDataDialogGetResult` returns `SAVE_DATA_DIALOG_ERROR_NOT_FINISHED` whenever not finished (checked before null pointer), and `sceSaveDataDialogTerminate` closes running dialogs and returns OK, rejecting uninitialized calls with `SAVE_DATA_DIALOG_ERROR_NOT_INITIALIZED`. +- [libSceSaveDataDialog.native](../../../core/libs/prx/libSceSaveDataDialog.native/Export.cpp) shows no UI and ignores progress values. Progress-bar mode (5) stays running until the caller closes it, matching Skyrim PPSA03747's save sequence; other modes still finish immediately and return button 1 and the first nonempty directory name. Console dialog timing and selection behavior remain unverified. Lifecycle state error codes follow shadPS4: `sceSaveDataDialogClose` returns `SAVE_DATA_DIALOG_ERROR_NOT_RUNNING` whenever not running, `sceSaveDataDialogGetResult` returns `SAVE_DATA_DIALOG_ERROR_NOT_FINISHED` whenever not finished (checked before null pointer), and `sceSaveDataDialogTerminate` closes running dialogs and returns OK, rejecting uninitialized calls with `SAVE_DATA_DIALOG_ERROR_NOT_INITIALIZED`, and progress updates (`sceSaveDataDialogProgressBarInc`, `sceSaveDataDialogProgressBarSetValue`) return `SAVE_DATA_DIALOG_ERROR_NOT_RUNNING` whenever not running. From 39c430bd487d35fbb933c7256fb1be105b8d5bd3 Mon Sep 17 00:00:00 2001 From: Nithishvelan M Date: Sun, 18 Oct 2026 00:01:22 +0530 Subject: [PATCH 0835/1000] fix(libSceSystemGesture): validate gesture handle and recognizer in sceSystemGestureGetTouchEventByIndex --- core/libs/prx/libSceSystemGesture/Export.cpp | 4 ++-- core/libs/tests/GuestSystemGesture.cpp | 4 ++++ docs/dev/debt/libSceSystemGesture.md | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceSystemGesture/Export.cpp b/core/libs/prx/libSceSystemGesture/Export.cpp index ec9799ca72..22aad137a6 100644 --- a/core/libs/prx/libSceSystemGesture/Export.cpp +++ b/core/libs/prx/libSceSystemGesture/Export.cpp @@ -103,10 +103,10 @@ int APS5_VABI sceSystemGestureGetTouchEventByEventID(int32_t gesture_handle, con } int APS5_VABI sceSystemGestureGetTouchEventByIndex(int32_t gesture_handle, const SystemGestureTouchRecognizer* recognizer, uint32_t index, SystemGestureTouchEvent* event) { - (void)gesture_handle; - (void)recognizer; (void)index; (void)event; + if (gesture_handle != GESTURE_HANDLE) return SCE_SYSTEM_GESTURE_ERROR_INVALID_HANDLE; + if (!recognizer) return SCE_SYSTEM_GESTURE_ERROR_INVALID_ARGUMENT; return SCE_SYSTEM_GESTURE_ERROR_INDEX_OUT_OF_ARRAY; } diff --git a/core/libs/tests/GuestSystemGesture.cpp b/core/libs/tests/GuestSystemGesture.cpp index ee6e1d7e52..426560081b 100644 --- a/core/libs/tests/GuestSystemGesture.cpp +++ b/core/libs/tests/GuestSystemGesture.cpp @@ -14,6 +14,7 @@ int APS5_VABI sceSystemGestureGetPrimitiveTouchEvents(std::int32_t, void*, std:: int APS5_VABI sceSystemGestureGetPrimitiveTouchEventsCount(std::int32_t); int APS5_VABI sceSystemGestureGetTouchEvents(std::int32_t, const void*, void*, std::uint32_t, std::uint32_t*); int APS5_VABI sceSystemGestureGetPrimitiveTouchEventByIndex(std::int32_t, std::uint32_t, void*); +int APS5_VABI sceSystemGestureGetTouchEventByIndex(std::int32_t, const void*, std::uint32_t, void*); int APS5_VABI sceSystemGestureAppendTouchRecognizer(std::int32_t, void*); int APS5_VABI sceSystemGestureCreateTouchRecognizer(std::int32_t, void*, std::int32_t, const void*, const void*); int APS5_VABI sceSystemGestureGetTouchRecognizerInformation(std::int32_t, const void*, void*); @@ -108,6 +109,9 @@ int main() { Require(sceSystemGestureGetTouchEvents(handle + 1, &recognizer, buffer.data(), 4, &count) == invalidHandle); Require(sceSystemGestureGetPrimitiveTouchEventByIndex(handle, 0, buffer.data()) == indexOutOfArray); Require(sceSystemGestureGetPrimitiveTouchEventByIndex(handle + 1, 0, buffer.data()) == invalidHandle); + Require(sceSystemGestureGetTouchEventByIndex(handle, &recognizer, 0, buffer.data()) == indexOutOfArray); + Require(sceSystemGestureGetTouchEventByIndex(handle + 1, &recognizer, 0, buffer.data()) == invalidHandle); + Require(sceSystemGestureGetTouchEventByIndex(handle, nullptr, 0, buffer.data()) == invalidArgument); Require(buffer == original); Require(sceSystemGestureAppendTouchRecognizer(handle, &recognizer) == 0); diff --git a/docs/dev/debt/libSceSystemGesture.md b/docs/dev/debt/libSceSystemGesture.md index 98903ec7d4..dadc3f4a07 100644 --- a/docs/dev/debt/libSceSystemGesture.md +++ b/docs/dev/debt/libSceSystemGesture.md @@ -2,4 +2,4 @@ ### Silent stubs -- [System gesture recognizers](../../../core/libs/prx/libSceSystemGesture/Export.cpp) (libSceSystemGesture) - `sceSystemGestureGetPrimitiveTouchEvents`, `GetPrimitiveTouchEventsCount`, `GetTouchEvents`, `GetPrimitiveTouchEventByIndex` and the recognizer append, remove, reset and update calls report that no touch input exists, so a title never receives a gesture event. Handle and pointer arguments are still checked. Error codes follow the 0x80D1 values main already uses in this file; KytyPS5 uses 0x8089 (INVALID_ARGUMENT 0x80890001, INVALID_HANDLE 0x80890006), and which one the console returns is not verified. `sceSystemGestureGetTouchRecognizerInformation` returns the gesture type and rectangle given to `sceSystemGestureCreateTouchRecognizer` or `UpdateTouchRecognizerRectangle`, stored in the title's recognizer buffer in a layout private to this file, and always reports `updated_time` 0 because no recognizer is ever updated with input. It throws for a recognizer that was not created by `sceSystemGestureCreateTouchRecognizer` or has no rectangle, since the console's result in those cases is not known +- [System gesture recognizers](../../../core/libs/prx/libSceSystemGesture/Export.cpp) (libSceSystemGesture) - `sceSystemGestureGetPrimitiveTouchEvents`, `GetPrimitiveTouchEventsCount`, `GetTouchEvents`, `GetPrimitiveTouchEventByIndex` and the recognizer append, remove, reset and update calls report that no touch input exists, so a title never receives a gesture event. Handle and pointer arguments are still checked across all query entry points including `sceSystemGestureGetTouchEventByIndex`. Error codes follow the 0x80D1 values main already uses in this file; KytyPS5 uses 0x8089 (INVALID_ARGUMENT 0x80890001, INVALID_HANDLE 0x80890006), and which one the console returns is not verified. `sceSystemGestureGetTouchRecognizerInformation` returns the gesture type and rectangle given to `sceSystemGestureCreateTouchRecognizer` or `UpdateTouchRecognizerRectangle`, stored in the title's recognizer buffer in a layout private to this file, and always reports `updated_time` 0 because no recognizer is ever updated with input. It throws for a recognizer that was not created by `sceSystemGestureCreateTouchRecognizer` or has no rectangle, since the console's result in those cases is not known From 18cf27aaba74f72c19c9c7626ce66bdbd1263938 Mon Sep 17 00:00:00 2001 From: milescoviello Date: Thu, 15 Oct 2026 04:01:43 +0000 Subject: [PATCH 0836/1000] fix(libc): allocate the getifaddrs list on the application heap FreeBSD's freeifaddrs is a plain free of the list, and Doom 3 BFG (PPSA99313) releases the list with free. The list came from host malloc, so the guest free stopped on an unregistered allocation; it now comes from the application heap, and freeifaddrs returns it there. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libc/src/Interfaces.cpp | 6 +++--- core/libs/tests/GuestInterfaces.cpp | 8 ++++++++ 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libc/src/Interfaces.cpp b/core/libs/prx/libc/src/Interfaces.cpp index 3391a07053..f61e17fbd4 100644 --- a/core/libs/prx/libc/src/Interfaces.cpp +++ b/core/libs/prx/libc/src/Interfaces.cpp @@ -9,10 +9,10 @@ #include #include #endif +#include "prx/libc/include/ApplicationHeap.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include #include -#include #include #include #include @@ -213,7 +213,7 @@ int APS5_VABI getifaddrs_nid_postfix(GuestInterfaceAddress** list) { std::size_t size = entries.size() * sizeof(GuestInterfaceAddress); for (const auto& entry : entries) size += Aligned(entry.name.size() + 1) + Aligned(entry.address.size()) + Aligned(entry.netmask.size()) + Aligned(entry.destination.size()); - auto* block = static_cast(std::malloc(size)); + auto* block = static_cast(ApplicationHeapAllocate_nid_no_patch(size)); if (!block) { *__error_nid_postfix() = 12; return -1; @@ -243,7 +243,7 @@ int APS5_VABI getifaddrs_nid_postfix(GuestInterfaceAddress** list) { } void APS5_VABI freeifaddrs_nid_postfix(GuestInterfaceAddress* list) { - std::free(list); + ApplicationHeapFree_nid_no_patch(list); } } diff --git a/core/libs/tests/GuestInterfaces.cpp b/core/libs/tests/GuestInterfaces.cpp index eeb24342a3..4a7202cf4e 100644 --- a/core/libs/tests/GuestInterfaces.cpp +++ b/core/libs/tests/GuestInterfaces.cpp @@ -7,7 +7,9 @@ #include #include #endif +#include "prx/libc/include/ApplicationHeap.hpp" #include "prx/libc/include/general/VabiMacros.hpp" +#include #include #include #include @@ -33,6 +35,7 @@ static_assert(offsetof(GuestInterfaceAddress, data) == 48); extern "C" { int APS5_VABI getifaddrs_nid_postfix(GuestInterfaceAddress** list); void APS5_VABI freeifaddrs_nid_postfix(GuestInterfaceAddress* list); +void APS5_VABI free_nid_postfix(void* pointer); int* APS5_VABI __error_nid_postfix(); } @@ -106,6 +109,8 @@ static std::vector HostSnapshot() { } int main() { + std::array api{}; + ApplicationHeapRegister_nid_no_patch(api.data()); constexpr std::uint32_t knownFlags = 0x1 | 0x2 | 0x8 | 0x10 | 0x40 | 0x80 | 0x100 | 0x200 | 0x8000; const std::uint8_t loopbackAddress[] = {127, 0, 0, 1}; const std::uint8_t loopbackMask[] = {255, 0, 0, 0}; @@ -167,6 +172,9 @@ int main() { freeifaddrs_nid_postfix(list); } Require(matched); + GuestInterfaceAddress* released = nullptr; + Require(getifaddrs_nid_postfix(&released) == 0 && released != nullptr); + free_nid_postfix(released); freeifaddrs_nid_postfix(nullptr); *__error_nid_postfix() = 0; Require(getifaddrs_nid_postfix(nullptr) == -1); From 8b81fa09bfded6ad483ccad41e25721a44d008d2 Mon Sep 17 00:00:00 2001 From: milescoviello Date: Wed, 14 Oct 2026 23:09:59 +0000 Subject: [PATCH 0837/1000] fix(libSceNet): close a libkernel socket through sceNetSocketClose MXGP 26 (PPSA30839) opens TCP sockets with libkernel's socket() and later closes them with sceNetSocketClose. libSceNet only looked in its own socket table, so every one of those closes failed with SCE_NET_ERROR_EBADF and the host socket stayed open. On the console both calls work on the same kernel sockets. A descriptor libSceNet does not own but libkernel has open as a socket is now closed through libkernel's close(). Anything else still fails with EBADF. The fallback runs after g_mutex is released, because libkernel's close() first asks libSceNet's socket closer, which takes it. guest_sce_net closes a libkernel TCP socket with sceNetSocketClose, checks that libkernel no longer has it open, and checks that a second close fails with EBADF. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceNet/Export.cpp | 16 +++++++++++++--- core/libs/tests/GuestSceNet.cpp | 10 ++++++++++ 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index 8fd8578762..f7940e7938 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -36,6 +36,7 @@ #include "prx/libc/include/General.hpp" #include "prx/libc/include/GuestArena.hpp" #include "prx/libkernel/Socket/include/SocketPoll.hpp" +#include "prx/libkernel/Socket/include/SocketRuntime.hpp" // Guest socket descriptors retain PS5 semantics while their transport is backed by host sockets. @@ -763,14 +764,23 @@ int APS5_VABI sceNetSocket(const char* name, int family, int type, int protocol) return fd; } +extern "C" int APS5_VABI close_nid_postfix(int d); + int APS5_VABI sceNetSocketClose(int s) { std::shared_ptr native; + bool found = false; { std::lock_guard lk(g_mutex); const auto it = g_socks.find(s); - if (it == g_socks.end()) return fail(NET_EBADF); - native = it->second.native; - g_socks.erase(it); + if (it != g_socks.end()) { + native = it->second.native; + g_socks.erase(it); + found = true; + } + } + if (!found) { + if (!GuestSocketIsOpen_nid_no_patch(s)) return fail(NET_EBADF); + return close_nid_postfix(s) == 0 ? 0 : fail(*__error_nid_postfix()); } KernelReleaseSocketNumber_nid_no_patch(s); if (native) { diff --git a/core/libs/tests/GuestSceNet.cpp b/core/libs/tests/GuestSceNet.cpp index e5b47cdeae..20ba9c8e57 100644 --- a/core/libs/tests/GuestSceNet.cpp +++ b/core/libs/tests/GuestSceNet.cpp @@ -76,6 +76,7 @@ int APS5_VABI open_nid_postfix(const char*, int, int); int APS5_VABI socket_nid_postfix(int, int, int); int APS5_VABI bind_nid_postfix(int, const void*, std::uint32_t); int APS5_VABI close_nid_postfix(int); +bool GuestSocketIsOpen_nid_no_patch(int); int APS5_VABI setsockopt_nid_postfix(int, int, int, const void*, std::uint32_t); int APS5_VABI fcntl_nid_postfix(int, int, ...); struct GuestPollEntry { @@ -991,6 +992,14 @@ static void CheckSocketError() { Require(sceNetSocketClose(refused) == 0); } +static void CheckCloseKernelSocket() { + const int kernel = socket_nid_postfix(2, 1, 0); + Require(kernel >= 0 && GuestSocketIsOpen_nid_no_patch(kernel)); + Require(sceNetSocketClose(kernel) == 0); + Require(!GuestSocketIsOpen_nid_no_patch(kernel)); + Require(Failed(sceNetSocketClose(kernel), 9)); +} + static void CheckDatagramDisconnect() { const int peer = sceNetSocket("disconnect-peer", 2, 2, 17); const int other = sceNetSocket("disconnect-other", 2, 2, 17); @@ -1060,6 +1069,7 @@ int main() { CheckUnspecifiedIpv6(); if (!ipv6Unavailable) CheckIpv6SocketsAreIpv6Only(); CheckDatagramDisconnect(); + CheckCloseKernelSocket(); const int closed = sceNetSocket("closed-by-kernel", 2, 2, 17); Require(closed >= 0); From aaba312e6d40381d88df14b4c4625ff05653d142 Mon Sep 17 00:00:00 2001 From: Nithishvelan M Date: Sun, 18 Oct 2026 00:06:43 +0530 Subject: [PATCH 0838/1000] feat(libSceNetCtl): support NET_CTL_INFO_HTTP_PROXY_PORT and validate null in sceNetCtlGetNatInfo --- core/libs/prx/libSceNetCtl/Export.cpp | 3 ++- core/libs/tests/GuestNetCtl.cpp | 8 +++++++- docs/dev/debt/libSceNetCtl.md | 6 +++++- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceNetCtl/Export.cpp b/core/libs/prx/libSceNetCtl/Export.cpp index d6cac2011c..2541758590 100644 --- a/core/libs/prx/libSceNetCtl/Export.cpp +++ b/core/libs/prx/libSceNetCtl/Export.cpp @@ -352,6 +352,7 @@ int APS5_VABI sceNetCtlGetInfo(int code, NetCtlInfo* info) { case NET_CTL_INFO_PRIMARY_DNS: WriteInfoAddress(info, host.dns.size() > 0 ? host.dns[0] : in_addr{}); break; case NET_CTL_INFO_SECONDARY_DNS: WriteInfoAddress(info, host.dns.size() > 1 ? host.dns[1] : in_addr{}); break; case NET_CTL_INFO_HTTP_PROXY_CONFIG: WriteInfoWord(info, NET_CTL_HTTP_PROXY_OFF); break; + case NET_CTL_INFO_HTTP_PROXY_PORT: WriteInfoWord(info, 0); break; case NET_CTL_INFO_DHCP_HOSTNAME: case NET_CTL_INFO_PPPOE_AUTH_NAME: case NET_CTL_INFO_HTTP_PROXY_SERVER: std::memset(info->opaque, 0, sizeof(info->opaque)); break; @@ -361,7 +362,7 @@ int APS5_VABI sceNetCtlGetInfo(int code, NetCtlInfo* info) { } int APS5_VABI sceNetCtlGetNatInfo(NetCtlNatInfo* nat_info) { - (void)nat_info; + if (!nat_info) return SCE_NET_CTL_ERROR_INVALID_ADDR; return SCE_NET_CTL_ERROR_NOT_CONNECTED; } diff --git a/core/libs/tests/GuestNetCtl.cpp b/core/libs/tests/GuestNetCtl.cpp index 92c51b9a39..5730ecd1a9 100644 --- a/core/libs/tests/GuestNetCtl.cpp +++ b/core/libs/tests/GuestNetCtl.cpp @@ -20,6 +20,7 @@ extern "C" { int APS5_VABI sceNetCtlGetInfo(int, NetCtlInfo*); +int APS5_VABI sceNetCtlGetNatInfo(NetCtlNatInfo*); int APS5_VABI sceNetCtlGetState(int*); } @@ -113,6 +114,7 @@ int main() { Require(QueryWord(4) == 1); Require(QueryWord(11) == 0); Require(QueryWord(19) == 0); + Require(QueryWord(21) == 0); const auto address = QueryAddress(14); const auto host = ntohl(address.s_addr); @@ -129,7 +131,7 @@ int main() { Require(sceNetCtlGetInfo(code, &info) == 0 && info.opaque[0] == 0); } - for (const int code : {5, 6, 7, 8, 9, 10, 21}) { + for (const int code : {5, 6, 7, 8, 9, 10}) { bool threw = false; try { sceNetCtlGetInfo(code, &info); @@ -138,4 +140,8 @@ int main() { } Require(threw); } + + Require(sceNetCtlGetNatInfo(nullptr) == kErrInvalidAddr); + NetCtlNatInfo nat_info{}; + Require(sceNetCtlGetNatInfo(&nat_info) == kErrNotConnected); } diff --git a/docs/dev/debt/libSceNetCtl.md b/docs/dev/debt/libSceNetCtl.md index 317150823e..f7effa839a 100644 --- a/docs/dev/debt/libSceNetCtl.md +++ b/docs/dev/debt/libSceNetCtl.md @@ -1,5 +1,9 @@ # libSceNetCtl technical debt +### Silent stubs + +- [sceNetCtlGetNatInfo](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - `sceNetCtlGetNatInfo` validates null pointers and returns `SCE_NET_CTL_ERROR_NOT_CONNECTED` (`0x80412108`) without performing NAT discovery. + ### Unknown function info -- [sceNetCtlGetInfo](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - answers from the host interface that holds an IPv4 address, preferring the one that carries the default route: its address, netmask, gateway, MTU and first two IPv4 DNS servers. The device is reported as wired, the link as connected, the IP configuration as DHCP and the HTTP proxy as off, whatever the host uses, and the Ethernet address is the one `sceNetGetMacAddress` reports. A gateway or DNS server the interface does not have is reported as `0.0.0.0`; what a console reports then is unknown. The DHCP host name (12), the PPPoE user name (13) and the HTTP proxy server (20) are empty strings, as that configuration sets none of them; the Wi-Fi codes (5-10) and the HTTP proxy port (21) throw +- [sceNetCtlGetInfo](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - answers from the host interface that holds an IPv4 address, preferring the one that carries the default route: its address, netmask, gateway, MTU and first two IPv4 DNS servers. The device is reported as wired, the link as connected, the IP configuration as DHCP and the HTTP proxy as off, whatever the host uses, and the Ethernet address is the one `sceNetGetMacAddress` reports. A gateway or DNS server the interface does not have is reported as `0.0.0.0`; what a console reports then is unknown. The DHCP host name (12), the PPPoE user name (13) and the HTTP proxy server (20) are empty strings, as that configuration sets none of them; the HTTP proxy port (21) is 0; the Wi-Fi codes (5-10) throw From 7c19c66def8fde929b9862dcf48a1d100c9fc49e Mon Sep 17 00:00:00 2001 From: Gjon Gojcevic Date: Sat, 17 Oct 2026 13:06:53 -0400 Subject: [PATCH 0839/1000] fix(shader): fold texel offsets into base-level unnormalized samples An implicit-LOD or gradient sample with a texel offset through an unnormalized S# was refused at bind time with "unnormalized guest sampler is used by an implicit-LOD sample". PPSA01317 aborts on boot on such a pixel image_sample_o. The base-level path already samples these through OpImageSampleExplicitLod with LOD 0, which Vulkan allows for unnormalized samplers, but it dropped the offset. It now adds the offset to the coordinates in that branch, as the explicit-LOD offset fold does, so a sampler whose uses also include offsets takes the base-level path. A normalized S# keeps the offset operand. AI-assisted: yes --- .../libSceAgcDriver/tests/ShaderMemory.cpp | 20 +++++++++++++++++++ .../IrMetadata/Resources.hpp | 2 +- .../SpirvBackend/src/SpirvImageEmitter.cpp | 2 +- 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 51dee75d38..87d83cf38b 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -4272,6 +4272,26 @@ void verifyUnnormalizedSamplers() { pixel.context.userData = normalizedPixelData; const auto pixelNormalized = Recompile(pixel); require(proven(pixelNormalized, false) && implicitSamples(pixelNormalized.spirv.Words()) == 1u, "unnormalized samplers: a pixel image_sample through a normalized S# is not an implicit-LOD sample"); + const std::array pixelOffsetCode{0x7e0202ffu, 0x00000201u, 0xf0c00f08u, 0x00400801u, 0xf800180fu, 0x0b0a0908u, 0xbf810000u}; + pixel.shader = {ShaderStage::Fragment, 0x50000u, pixelOffsetCode, 0, {}}; + pixel.context.userData = pixelData; + pixel.target.supportedCapabilities = capabilities; + pixel.target.nonConstantImageOffsets = true; + const auto pixelOffsetUnnormalized = Recompile(pixel); + require(proven(pixelOffsetUnnormalized, true), "unnormalized texel offsets: a pixel image_sample_o through an unnormalized S# was not flagged"); + auditSpirv(pixelOffsetUnnormalized.spirv.Words()); + require(foldedSamples(pixelOffsetUnnormalized.spirv.Words()), "unnormalized texel offsets: a pixel image_sample_o offset was not added to the base-level coordinates"); + pixel.context.userData = normalizedPixelData; + const auto pixelOffsetNormalized = Recompile(pixel); + std::vector implicitMasks; + const auto offsetWords = pixelOffsetNormalized.spirv.Words(); + for (std::size_t cursor = 5; cursor < offsetWords.size();) { + const auto count = offsetWords[cursor] >> 16u; + require(count != 0 && count <= offsetWords.size() - cursor, "unnormalized texel offsets: truncated SPIR-V instruction"); + if ((offsetWords[cursor] & 0xffffu) == spv::OpImageSampleImplicitLod && count > 5u) implicitMasks.push_back(offsetWords[cursor + 5]); + cursor += count; + } + require(proven(pixelOffsetNormalized, false) && implicitMasks.size() == 1u && (implicitMasks[0] & (spv::ImageOperandsConstOffsetMask | spv::ImageOperandsOffsetMask)) != 0u, "unnormalized texel offsets: a normalized pixel image_sample_o is not an implicit-LOD sample with its offset"); } void verifyUnusedUnnormalizedSampler() { diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp index 85c985a879..4750cccb14 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp @@ -140,7 +140,7 @@ inline bool FoldsTexelOffsets(const SamplerResource& sampler) { } inline bool SamplesBaseLevelIfUnnormalized(const SamplerResource& sampler) { - return (sampler.uses & (SamplerUseImplicitLod | SamplerUseGradient)) != 0u && (sampler.uses & ~static_cast(SamplerUseExplicitLod | SamplerUseImplicitLod | SamplerUseGradient | SamplerUseNonZeroLod)) == 0u && !sampler.depthCompare; + return (sampler.uses & (SamplerUseImplicitLod | SamplerUseGradient)) != 0u && (sampler.uses & ~static_cast(SamplerUseExplicitLod | SamplerUseImplicitLod | SamplerUseGradient | SamplerUseNonZeroLod | SamplerUseOffset)) == 0u && !sampler.depthCompare; } struct SampledResourcePair { diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp index d2ce577837..8fe26ae49d 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp @@ -1465,7 +1465,7 @@ void EmitSampleOp(SpirvValueEmitContext& ctx, const ImageEmitAccess& access, con const auto selector = state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::UnnormalizedBaseLevelBase + mem.sampler, 0u); const auto unnormalized = Binary(state, spv::OpINotEqual, TypeBool(state), selector, ConstantU32(state, 0u)); sample = EmitValueIfElse(state, unnormalized, resultType, [&] { - return emitSample(setup.coord, spv::ImageOperandsLodMask, {ZeroF32(state)}, spv::OpImageSampleExplicitLod); + return emitSample(hasOffset ? FoldedOffsetCoord(ctx, access, setup) : setup.coord, spv::ImageOperandsLodMask, {ZeroF32(state)}, spv::OpImageSampleExplicitLod); }, [&] { return emitSample(setup.coord, operandMask, operands, opcode); }); From c43c93100532785d97b6de2a82edbfa1060f1e0d Mon Sep 17 00:00:00 2001 From: Nithishvelan M Date: Sun, 18 Oct 2026 00:19:48 +0530 Subject: [PATCH 0840/1000] fix(libSceSaveData.native): validate initialization and parameters in transaction resource lifecycle --- core/libs/prx/libSceSaveData.native/Export.cpp | 6 ++++-- core/libs/tests/SaveDataMemoryMetadata.cpp | 17 +++++++++++++++++ docs/dev/debt/libSceSaveData.native.md | 1 + 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceSaveData.native/Export.cpp b/core/libs/prx/libSceSaveData.native/Export.cpp index 5635289707..503004a144 100644 --- a/core/libs/prx/libSceSaveData.native/Export.cpp +++ b/core/libs/prx/libSceSaveData.native/Export.cpp @@ -216,7 +216,8 @@ int APS5_VABI sceSaveDataCommit(const SaveDataCommitParam* param) { } int APS5_VABI sceSaveDataCreateTransactionResource(uint32_t size) { - (void)size; + if (g_initializations == 0) return SAVE_DATA_ERROR_NOT_INITIALIZED; + if (size == 0) return SAVE_DATA_ERROR_PARAMETER; return g_transaction_counter.fetch_add(1); } @@ -270,7 +271,8 @@ int APS5_VABI sceSaveDataDelete(const SaveDataDelete* del) { } int APS5_VABI sceSaveDataDeleteTransactionResource(int32_t resource) { - (void)resource; + if (g_initializations == 0) return SAVE_DATA_ERROR_NOT_INITIALIZED; + if (resource <= 0) return SAVE_DATA_ERROR_PARAMETER; return SAVE_DATA_OK; } diff --git a/core/libs/tests/SaveDataMemoryMetadata.cpp b/core/libs/tests/SaveDataMemoryMetadata.cpp index 1c6f0f10e6..bb39407dbe 100644 --- a/core/libs/tests/SaveDataMemoryMetadata.cpp +++ b/core/libs/tests/SaveDataMemoryMetadata.cpp @@ -20,10 +20,17 @@ extern "C" { int APS5_VABI sceSaveDataInitialize3(const void*); int APS5_VABI sceSaveDataTerminate(); +int APS5_VABI sceSaveDataCreateTransactionResource(std::uint32_t); +int APS5_VABI sceSaveDataDeleteTransactionResource(std::int32_t); int APS5_VABI sceSaveDataSetSaveDataMemory2(const SaveDataMemorySet2*); int APS5_VABI sceSaveDataSetupSaveDataMemory2(const SaveDataMemorySetup2*, SaveDataMemorySetupResult*); } +namespace { +constexpr int kSaveDataNotInitialized = -2137063423; +constexpr int kSaveDataParameter = -2137063424; +} + static void Check(bool value, int line) { if (!value) { std::fprintf(stderr, "Save-data memory check failed at line %d\n", line); @@ -39,6 +46,8 @@ static std::vector Read(const std::filesystem::path& path) { } int main() { + Require(sceSaveDataCreateTransactionResource(1024) == kSaveDataNotInitialized); + Require(sceSaveDataDeleteTransactionResource(1) == kSaveDataNotInitialized); const auto previous = std::filesystem::current_path(); const auto root = std::filesystem::temp_directory_path() / ("anyps5-metadata-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); @@ -54,6 +63,12 @@ int main() { Require(static_cast(file)); } Require(sceSaveDataInitialize3(nullptr) == 0); + Require(sceSaveDataCreateTransactionResource(0) == kSaveDataParameter); + Require(sceSaveDataDeleteTransactionResource(0) == kSaveDataParameter); + Require(sceSaveDataDeleteTransactionResource(-1) == kSaveDataParameter); + const int tx = sceSaveDataCreateTransactionResource(1024); + Require(tx > 0); + Require(sceSaveDataDeleteTransactionResource(tx) == 0); Require(sceSaveDataInitialize3(nullptr) == 0); Require(sceSaveDataTerminate() == 0); SaveDataParam param{}; @@ -307,6 +322,8 @@ int main() { Require(Read(setupBinPath) == expectedBlob512); Require(Read(setupParamPath) == std::vector(param300Bytes, param300Bytes + sizeof(setupParam))); Require(sceSaveDataTerminate() == 0); + Require(sceSaveDataCreateTransactionResource(1024) == kSaveDataNotInitialized); + Require(sceSaveDataDeleteTransactionResource(tx) == kSaveDataNotInitialized); std::filesystem::current_path(previous); std::filesystem::remove_all(root); } diff --git a/docs/dev/debt/libSceSaveData.native.md b/docs/dev/debt/libSceSaveData.native.md index c12f2befd8..40d957b18b 100644 --- a/docs/dev/debt/libSceSaveData.native.md +++ b/docs/dev/debt/libSceSaveData.native.md @@ -2,6 +2,7 @@ ### Unknown function info +- [sceSaveDataCreateTransactionResource, sceSaveDataDeleteTransactionResource](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - transaction resources return synthetic positive identifiers and validate initialization state and non-zero parameters; transactional rollback semantics on host filesystem storage are unmodeled - [sceSaveDataPrepareForTransferring](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native, NID `RvID8BVeg+g`) - declared without parameters as a throwing stub; Minecraft (PPSA17221) imports it beside `sceSaveDataTransferringMountPs4` and `sceSaveDataDirNameSearchPs4` and fails to load without it. The name hashes to the NID but is not in aerolib; it was found by combining words from the libSceSaveData names in aerolib. Its arguments and behaviour are unknown. - [sceSaveDataTransferringMountPs4, sceSaveDataDirNameSearchPs4](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - arguments assumed to be those of `sceSaveDataTransferringMount` and `sceSaveDataDirNameSearch`: KytyPS5 binds both NIDs to the PS5 functions, and [prosper](https://github.com/mattias800/prosper) captured the same result layout on a title for the search. There is no PS4 save area, so the mount returns `NOT_FOUND` and the search reports zero hits - [sceSaveDataConvert](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - the parameter block (user id at `+0x00`, `SceSaveDataDirName` pointer at `+0x08`, 48 opaque bytes after it) is read from PPSA28416's single call, not confirmed by KytyPS5, shadPS4, fpPS4 or SharpProspero, which have no body for it. Host saves have no size limit, so an existing save returns `SCE_OK` without touching its files and no event is queued; what the console returns for a save that is mounted, already converted or needs no conversion is unknown. The directory name is validated as `sceSaveDataDelete` does From b54018e34176470a7e178d3544d7ea467ce16106 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Sat, 17 Oct 2026 20:57:28 +0200 Subject: [PATCH 0841/1000] fix(main): unique directory for the large cache file test Culprit: a03ccc6be (large cache files in chunks); failing run 39057904312. The test used a fixed directory in the temp dir; the runners of one host share it, so a concurrent run left a second entry behind. --- core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp index 6adaebcddd..d96411c0eb 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp @@ -735,7 +735,7 @@ void verifyFloatModeSerialization() { int runLargeFileWrite() { constexpr std::size_t size = (std::size_t{2} << 30u) + 4096u; - const auto directory = std::filesystem::temp_directory_path() / "aps5-shader-disk-cache-large-file-test"; + const auto directory = std::filesystem::temp_directory_path() / ("aps5-shader-disk-cache-large-file-test-" + std::to_string(std::random_device{}())); std::error_code error; std::filesystem::remove_all(directory, error); std::filesystem::create_directories(directory, error); From eb7c5f5c9c9eb0507df21d2bc5d0f9171944de03 Mon Sep 17 00:00:00 2001 From: Nithishvelan M Date: Sun, 18 Oct 2026 00:31:35 +0530 Subject: [PATCH 0842/1000] docs(libSceVoiceQoS): address review notes on endpoint attributes and call count --- docs/dev/debt/libSceVoiceQoS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceVoiceQoS.md b/docs/dev/debt/libSceVoiceQoS.md index 19e9efcc63..a62a38f2d7 100644 --- a/docs/dev/debt/libSceVoiceQoS.md +++ b/docs/dev/debt/libSceVoiceQoS.md @@ -3,7 +3,7 @@ ### Unknown function info - [sceVoiceQoSSetConnectionAttribute](../../../core/libs/prx/libSceVoiceQoS/Export.cpp) (libSceVoiceQoS, NID `KCGOxg8iX7s`) is a throwing stub declared without parameters, like the other endpoint calls of the library: PPSA04159 fails to load without it and its signature is unknown. The name hashes to the imported NID (`tools/nid_names.py`) -- [sceVoiceQoSInit](../../../core/libs/prx/libSceVoiceQoS/Export.cpp) (libSceVoiceQoS) - `0x804E0905` for a second initialization comes from [prosper](https://github.com/mattias800/prosper)'s reading of the module and was not measured: on a PS5 a valid call blocks inside a homebrew payload, so it never returns there. PPSA26344's two call sites (`0xE307BF`, `0x151C45D`) pass an allocated pointer, a 0x40000-byte block and app type 0x10000000; the app type and block are not used. `sceVoiceQoSCreateLocalEndpoint` takes four arguments (`int* id, userId, deviceInId, deviceOutId`) as declared in fpPS4 (`src/ps4_libscevoice.pas`, `ps4_sceVoiceQoSCreateLocalEndpoint`); no public reference exists for the other seven signatures (`DeleteLocalEndpoint(id)`, `CreateRemoteEndpoint(int* id)`, `DeleteRemoteEndpoint(id)`, `Connect(int* id, local, remote)`, `Disconnect(connection)`, `SetLocalEndpointAttribute(id, attribute, value, size)`), which are assumed from function naming and call patterns and not confirmed on a PS5 title. Endpoints and connections are tracked by allocated id; duplicate connections and deleting an endpoint while referenced in a connection are permitted as the console is not known to reject them; local endpoint attributes are stored as given (including empty or non-positive sizes), as their ids and defaults are unknown; other endpoint and packet calls remain throwing stubs. The not-initialized error 0x804E0901 is assumed from the 0x804E09xx numbering, and unknown ids return `ARGUMENT_INVALID`. Aliens: Fireteam Elite (PPSA03393) uses all eight implemented calls on its first load path, starting with `sceVoiceQoSCreateLocalEndpoint` when it registers the local talker; it stops with `not implemented` when any of them throws +- [sceVoiceQoSInit](../../../core/libs/prx/libSceVoiceQoS/Export.cpp) (libSceVoiceQoS) - `0x804E0905` for a second initialization comes from [prosper](https://github.com/mattias800/prosper)'s reading of the module and was not measured: on a PS5 a valid call blocks inside a homebrew payload, so it never returns there. PPSA26344's two call sites (`0xE307BF`, `0x151C45D`) pass an allocated pointer, a 0x40000-byte block and app type 0x10000000; the app type and block are not used. `sceVoiceQoSCreateLocalEndpoint` takes four arguments (`int* id, userId, deviceInId, deviceOutId`) as declared in fpPS4 (`src/ps4_libscevoice.pas`, `ps4_sceVoiceQoSCreateLocalEndpoint`); no public reference exists for the other seven signatures (`DeleteLocalEndpoint(id)`, `CreateRemoteEndpoint(int* id)`, `DeleteRemoteEndpoint(id)`, `Connect(int* id, local, remote)`, `Disconnect(connection)`, `SetLocalEndpointAttribute(id, attribute, value, size)`, `SetRemoteEndpointAttribute(id, attribute, value, size)`), which are assumed from function naming and call patterns and not confirmed on a PS5 title. Endpoints and connections are tracked by allocated id; duplicate connections and deleting an endpoint while referenced in a connection are permitted as the console is not known to reject them; endpoint attributes are stored as given (including empty or non-positive sizes), as their ids and defaults are unknown; other endpoint and packet calls remain throwing stubs. The not-initialized error 0x804E0901 is assumed from the 0x804E09xx numbering, and unknown ids return `ARGUMENT_INVALID`. Aliens: Fireteam Elite (PPSA03393) uses all seven implemented calls on its first load path, starting with `sceVoiceQoSCreateLocalEndpoint` when it registers the local talker; it stops with `not implemented` when any of them throws ### Functional From 18ce9241e6837bca368232dde6a6d714e4732f77 Mon Sep 17 00:00:00 2001 From: Nithishvelan M Date: Sun, 18 Oct 2026 00:39:01 +0530 Subject: [PATCH 0843/1000] fix(libSceMsgDialog.native): validate null argument and busy state across dialog lifecycle --- core/libs/prx/libSceMsgDialog.native/Export.cpp | 7 +++++-- core/libs/tests/GuestMsgDialogNative.cpp | 10 ++++++++++ docs/dev/debt/libSceMsgDialog.native.md | 2 +- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceMsgDialog.native/Export.cpp b/core/libs/prx/libSceMsgDialog.native/Export.cpp index 339bf0049a..faa81c5ec1 100644 --- a/core/libs/prx/libSceMsgDialog.native/Export.cpp +++ b/core/libs/prx/libSceMsgDialog.native/Export.cpp @@ -13,6 +13,7 @@ constexpr int COMMON_DIALOG_RESULT_OK = 0; constexpr int COMMON_DIALOG_ERROR_ALREADY_INITIALIZED = static_cast(0x80B80004u); constexpr int COMMON_DIALOG_ERROR_NOT_INITIALIZED = static_cast(0x80B80003u); constexpr int COMMON_DIALOG_ERROR_NOT_FINISHED = static_cast(0x80B80005u); +constexpr int COMMON_DIALOG_ERROR_BUSY = static_cast(0x80B80007u); constexpr int COMMON_DIALOG_ERROR_NOT_RUNNING = static_cast(0x80B8000Bu); constexpr int COMMON_DIALOG_ERROR_ARG_NULL = static_cast(0x80B8000Du); constexpr int BUTTON_ID_OK = 1; @@ -48,6 +49,7 @@ int APS5_VABI sceMsgDialogInitialize(void) { int APS5_VABI sceMsgDialogOpen(const void* param) { const int status = g_status.load(); if (status == 0) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + if (status == COMMON_DIALOG_STATUS_RUNNING) return COMMON_DIALOG_ERROR_BUSY; if (param == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; g_status = COMMON_DIALOG_STATUS_FINISHED; return 0; @@ -70,8 +72,9 @@ int APS5_VABI sceMsgDialogProgressBarInc(int target, std::uint32_t delta) { int APS5_VABI sceMsgDialogProgressBarSetMsg(int target, const char* msg) { (void)target; - (void)msg; - return g_status.load() == COMMON_DIALOG_STATUS_RUNNING ? 0 : COMMON_DIALOG_ERROR_NOT_RUNNING; + if (g_status.load() != COMMON_DIALOG_STATUS_RUNNING) return COMMON_DIALOG_ERROR_NOT_RUNNING; + if (msg == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; + return 0; } int APS5_VABI sceMsgDialogProgressBarSetValue(int target, std::uint32_t rate) { diff --git a/core/libs/tests/GuestMsgDialogNative.cpp b/core/libs/tests/GuestMsgDialogNative.cpp index bc806a3cd8..2067d9c2de 100644 --- a/core/libs/tests/GuestMsgDialogNative.cpp +++ b/core/libs/tests/GuestMsgDialogNative.cpp @@ -24,22 +24,30 @@ constexpr int kStatusInitialized = 1; constexpr int kStatusFinished = 3; constexpr int kErrAlreadyInitialized = static_cast(0x80B80004u); constexpr int kErrNotInitialized = static_cast(0x80B80003u); +constexpr int kErrNotFinished = static_cast(0x80B80005u); constexpr int kErrNotRunning = static_cast(0x80B8000Bu); +constexpr int kErrArgNull = static_cast(0x80B8000Du); constexpr int kButtonIdOk = 1; } int main() { std::uint8_t param[0x60]{}; + MsgDialogResult result{}; Require(sceMsgDialogTerminate() == kErrNotInitialized); Require(sceMsgDialogClose() == kErrNotRunning); + Require(sceMsgDialogOpen(param) == kErrNotInitialized); + Require(sceMsgDialogGetResult(&result) == kErrNotInitialized); Require(sceMsgDialogUpdateStatus() == 0); Require(sceMsgDialogGetStatus() == 0); Require(sceMsgDialogInitialize() == 0); Require(sceMsgDialogInitialize() == kErrAlreadyInitialized); Require(sceMsgDialogClose() == kErrNotRunning); + Require(sceMsgDialogGetResult(&result) == kErrNotFinished); + Require(sceMsgDialogGetResult(nullptr) == kErrArgNull); + Require(sceMsgDialogOpen(nullptr) == kErrArgNull); Require(sceMsgDialogUpdateStatus() == kStatusInitialized); Require(sceMsgDialogGetStatus() == kStatusInitialized); @@ -62,6 +70,8 @@ int main() { Require(sceMsgDialogTerminate() == 0); Require(sceMsgDialogTerminate() == kErrNotInitialized); + Require(sceMsgDialogOpen(param) == kErrNotInitialized); + Require(sceMsgDialogGetResult(&result) == kErrNotInitialized); Require(sceMsgDialogClose() == kErrNotRunning); Require(sceMsgDialogGetStatus() == 0); } diff --git a/docs/dev/debt/libSceMsgDialog.native.md b/docs/dev/debt/libSceMsgDialog.native.md index 511522085f..bcd6bc6be9 100644 --- a/docs/dev/debt/libSceMsgDialog.native.md +++ b/docs/dev/debt/libSceMsgDialog.native.md @@ -2,4 +2,4 @@ ### Silent stubs -- [libSceMsgDialog.native](../../../core/libs/prx/libSceMsgDialog.native/Export.cpp) - dialogs finish at `sceMsgDialogOpen` without UI and answer with button 1, so `sceMsgDialogClose` returns `NOT_RUNNING`; its result before `sceMsgDialogInitialize` follows libSceMsgDialog. `sceMsgDialogTerminate` resets running or finished dialogs and returns OK, rejecting uninitialized calls with `COMMON_DIALOG_ERROR_NOT_INITIALIZED` following shadPS4. +- [libSceMsgDialog.native](../../../core/libs/prx/libSceMsgDialog.native/Export.cpp) - dialogs finish at `sceMsgDialogOpen` without UI and answer with button 1, so `sceMsgDialogClose` returns `NOT_RUNNING`; its result before `sceMsgDialogInitialize` follows libSceMsgDialog. `sceMsgDialogTerminate` resets running or finished dialogs and returns OK, rejecting uninitialized calls with `COMMON_DIALOG_ERROR_NOT_INITIALIZED` following shadPS4; null pointer arguments return `COMMON_DIALOG_ERROR_ARG_NULL`. From 28b3ce0fe3e87d4fb75bc31ad8030b1a361e58f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Sat, 17 Oct 2026 21:26:00 +0200 Subject: [PATCH 0844/1000] fix(main): run the large cache file test alone with a longer hang limit Windows run 39061030084: agc_shader_disk_cache_large_file wrote 2 GiB in 141 s next to 1399 parallel tests (limit 120 s), and conventions_function_bodies_shard_4 timed out at 30 s on the same loaded runner. The test is now RUN_SERIAL and its hang limit is 200 s (ctest timeout stays 240 s). Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 2 +- core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index c6950a121b..6e6a309b36 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1595,7 +1595,7 @@ if(BUILD_TESTING) set_tests_properties(agc_driver_partially_resident_pending_stores PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() add_test(NAME agc_shader_disk_cache_large_file COMMAND agc_shader_disk_cache_tests --large-file) - set_tests_properties(agc_shader_disk_cache_large_file PROPERTIES SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" TIMEOUT 240) + set_tests_properties(agc_shader_disk_cache_large_file PROPERTIES SKIP_RETURN_CODE 77 RUN_SERIAL TRUE WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" TIMEOUT 240) if(WIN32) set_tests_properties(agc_shader_disk_cache_large_file PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp index d96411c0eb..043759d826 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp @@ -763,8 +763,8 @@ int runLargeFileWrite() { written->set_exception(std::current_exception()); } }).detach(); - if (result.wait_for(std::chrono::seconds(120)) != std::future_status::ready) { - std::cerr << "WriteFileAtomically did not return within 120 s for a file of more than 2 GiB\n"; + if (result.wait_for(std::chrono::seconds(200)) != std::future_status::ready) { + std::cerr << "WriteFileAtomically did not return within 200 s for a file of more than 2 GiB\n"; std::cerr.flush(); std::_Exit(1); } From 7c07ebd0957ca0dabf5291f364add45ad05a742c Mon Sep 17 00:00:00 2001 From: lorkydey <57268822+Lorkydey@users.noreply.github.com> Date: Sat, 17 Oct 2026 21:27:55 +0200 Subject: [PATCH 0845/1000] feat(libc): implement getw and putw --- core/libs/prx/libc/CMakeLists.txt | 13 +++ core/libs/prx/libc/src/BinaryWordIo.cpp | 42 +++++++ core/libs/tests/GuestBinaryWord.cpp | 141 ++++++++++++++++++++++++ docs/dev/debt/libc.md | 1 + 4 files changed, 197 insertions(+) create mode 100644 core/libs/prx/libc/src/BinaryWordIo.cpp create mode 100644 core/libs/tests/GuestBinaryWord.cpp diff --git a/core/libs/prx/libc/CMakeLists.txt b/core/libs/prx/libc/CMakeLists.txt index 4900ae6797..2c2aa701ef 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -1,4 +1,15 @@ if(BUILD_TESTING) + add_test_executable(guest_binary_word_tests ${LIBS_INCLUDE_DIR}/tests/GuestBinaryWord.cpp) + target_include_directories(guest_binary_word_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_binary_word_tests PRIVATE libc) + configure_windows_unwind(guest_binary_word_tests) + add_test(NAME guest_binary_word COMMAND guest_binary_word_tests guest_binary_word.tmp) + set_tests_properties(guest_binary_word PROPERTIES TIMEOUT 10) + if(WIN32) + set_tests_properties(guest_binary_word PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_safe_error_tests ${LIBS_INCLUDE_DIR}/tests/GuestSafeErrors.cpp) target_include_directories(guest_safe_error_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_safe_error_tests PRIVATE libc) @@ -69,6 +80,8 @@ target_sources(libc PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/exception/Exports.cpp ) +target_sources(libc PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/BinaryWordIo.cpp) + target_include_directories(libc PRIVATE ${LIBS_INCLUDE_DIR} ) diff --git a/core/libs/prx/libc/src/BinaryWordIo.cpp b/core/libs/prx/libc/src/BinaryWordIo.cpp new file mode 100644 index 0000000000..733aadff22 --- /dev/null +++ b/core/libs/prx/libc/src/BinaryWordIo.cpp @@ -0,0 +1,42 @@ +#include "prx/libc/include/FileStream.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include + +static_assert(sizeof(int) == 4 && std::endian::native == std::endian::little); + +extern "C" { +std::size_t APS5_VABI fread_nid_postfix(void*, std::size_t, std::size_t, FileStream*); +int APS5_VABI putc_unlocked_nid_postfix(int, FileStream*); +void APS5_VABI flockfile_nid_postfix(FileStream*); +void APS5_VABI funlockfile_nid_postfix(FileStream*); +} + +namespace { + +class LockedStream { + FileStream* stream; + +public: + explicit LockedStream(FileStream* value) : stream(value) { flockfile_nid_postfix(stream); } + ~LockedStream() { funlockfile_nid_postfix(stream); } +}; + +} + +extern "C" int APS5_VABI getw_nid_postfix(FileStream* stream) { + int value = 0; + if (fread_nid_postfix(&value, sizeof(value), 1, stream) != 1) return -1; + return value; +} + +extern "C" int APS5_VABI putw_nid_postfix(int value, FileStream* stream) { + if (stream && !stream->IsOpen()) { errno = 9; return -1; } + const LockedStream lock(stream); + const auto bits = static_cast(value); + for (unsigned shift = 0; shift < 32; shift += 8) + if (putc_unlocked_nid_postfix((bits >> shift) & 0xff, stream) == -1) return -1; + return 0; +} diff --git a/core/libs/tests/GuestBinaryWord.cpp b/core/libs/tests/GuestBinaryWord.cpp new file mode 100644 index 0000000000..2720a1f894 --- /dev/null +++ b/core/libs/tests/GuestBinaryWord.cpp @@ -0,0 +1,141 @@ +#include "prx/libc/include/FileStream.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI getw_nid_postfix(FileStream*); +int APS5_VABI putw_nid_postfix(int, FileStream*); +FileStream* APS5_VABI fopen_nid_postfix(const char*, const char*); +int APS5_VABI fclose_nid_postfix(FileStream*); +std::size_t APS5_VABI fread_nid_postfix(void*, std::size_t, std::size_t, FileStream*); +std::size_t APS5_VABI fwrite_nid_postfix(const void*, std::size_t, std::size_t, FileStream*); +int APS5_VABI fseek_nid_postfix(FileStream*, std::int64_t, int); +std::int64_t APS5_VABI ftell_nid_postfix(FileStream*); +int APS5_VABI ungetc_nid_postfix(int, FileStream*); +int APS5_VABI fgetc_nid_postfix(FileStream*); +void APS5_VABI clearerr_nid_postfix(FileStream*); +void APS5_VABI flockfile_nid_postfix(FileStream*); +void APS5_VABI funlockfile_nid_postfix(FileStream*); +} + +namespace { + +void Require(bool condition, const char* message) { + if (!condition) { + std::fprintf(stderr, "binary word I/O: %s\n", message); + std::abort(); + } +} + +void CheckRoundTrip(const char* path) { + constexpr std::array values{0, 1, -1, std::numeric_limits::min(), std::numeric_limits::max(), + 0x12345678, 0x0d0a1a00, -1234567}; + auto* stream = fopen_nid_postfix(path, "w+b"); + Require(stream != nullptr, "open round-trip file"); + for (const int value : values) Require(putw_nid_postfix(value, stream) == 0, "putw returns zero on success"); + Require(ftell_nid_postfix(stream) == static_cast(4 * values.size()), "write exactly four bytes per word"); + Require(fseek_nid_postfix(stream, 0, SEEK_SET) == 0, "rewind written words"); + std::array bytes; + Require(fread_nid_postfix(bytes.data(), 1, bytes.size(), stream) == bytes.size(), "read raw word bytes"); + for (std::size_t word = 0; word < values.size(); ++word) + for (unsigned byte = 0; byte < 4; ++byte) + Require(bytes[4 * word + byte] == ((static_cast(values[word]) >> (8 * byte)) & 0xff), + "guest little-endian representation"); + Require(fseek_nid_postfix(stream, 0, SEEK_SET) == 0, "rewind for getw"); + for (const int value : values) { + Require(getw_nid_postfix(stream) == value, "read signed word including minus one"); + Require((stream->GuestState().flags & 0x60) == 0, "a stored minus one is not EOF or an error"); + } + Require(getw_nid_postfix(stream) == -1, "EOF returns minus one"); + Require((stream->GuestState().flags & 0x60) == 0x20, "publish EOF without an error"); + Require(fseek_nid_postfix(stream, 20, SEEK_SET) == 0, "seek to a buffered word"); + Require(fgetc_nid_postfix(stream) == 0x78, "read first word byte"); + Require(ungetc_nid_postfix(0x78, stream) == 0x78, "push back first word byte"); + Require(getw_nid_postfix(stream) == 0x12345678, "getw consumes pushed-back bytes"); + Require(ftell_nid_postfix(stream) == 24, "position after pushed-back word"); + Require(fclose_nid_postfix(stream) == 0, "close round-trip file"); +} + +void CheckShortReads(const char* path) { + constexpr std::array bytes{0xff, 0xaa, 0xbb}; + for (std::size_t length = 0; length <= bytes.size(); ++length) { + auto* stream = fopen_nid_postfix(path, "w+b"); + Require(stream != nullptr, "open short word file"); + Require(fwrite_nid_postfix(bytes.data(), 1, length, stream) == length, "write incomplete word"); + Require(fseek_nid_postfix(stream, 0, SEEK_SET) == 0, "rewind incomplete word"); + Require(getw_nid_postfix(stream) == -1, "incomplete word is EOF"); + Require((stream->GuestState().flags & 0x60) == 0x20, "short-read EOF flag"); + Require(ftell_nid_postfix(stream) == static_cast(length), "consume incomplete word bytes"); + clearerr_nid_postfix(stream); + Require((stream->GuestState().flags & 0x60) == 0, "clear short-read status"); + Require(fclose_nid_postfix(stream) == 0, "close incomplete word"); + } +} + +void CheckErrors(const char* path) { + auto* writer = fopen_nid_postfix(path, "wb"); + Require(writer != nullptr, "open write-only file"); + Require(getw_nid_postfix(writer) == -1, "read on write-only stream fails"); + Require((writer->GuestState().flags & 0x60) == 0x40, "publish read error, not EOF"); + Require(fclose_nid_postfix(writer) == 0, "close write-only file"); + auto* reader = fopen_nid_postfix(path, "rb"); + Require(reader != nullptr, "open read-only file"); + Require(putw_nid_postfix(0x12345678, reader) == -1, "write on read-only stream fails"); + Require((reader->GuestState().flags & 0x60) == 0x40, "publish write error"); + Require(fclose_nid_postfix(reader) == 0, "close read-only file"); + FileStream closed(std::tmpfile()); + Require(closed.Close() == 0, "close temporary stream"); + errno = 0; + Require(getw_nid_postfix(&closed) == -1 && errno == 9, "closed read reports EBADF"); + errno = 0; + Require(putw_nid_postfix(1, &closed) == -1 && errno == 9, "closed write reports EBADF"); + bool readThrew = false, writeThrew = false; + try { getw_nid_postfix(nullptr); } catch (const std::runtime_error&) { readThrew = true; } + try { putw_nid_postfix(1, nullptr); } catch (const std::invalid_argument&) { writeThrew = true; } + Require(readThrew && writeThrew, "null streams throw"); +} + +void CheckConcurrentWrites(const char* path) { + auto* stream = fopen_nid_postfix(path, "w+b"); + Require(stream != nullptr, "open shared stream"); + flockfile_nid_postfix(stream); + Require(putw_nid_postfix(0, stream) == 0, "putw accepts a lock held by its caller"); + funlockfile_nid_postfix(stream); + const auto writer = [stream](int value) { + for (int count = 0; count < 256; ++count) + Require(putw_nid_postfix(value, stream) == 0, "concurrent word write"); + }; + std::thread first(writer, 0x11223344), second(writer, 0x55667788); + first.join(); + second.join(); + Require(fseek_nid_postfix(stream, 4, SEEK_SET) == 0, "rewind concurrent words"); + int firstCount = 0, secondCount = 0; + for (int count = 0; count < 512; ++count) { + const int value = getw_nid_postfix(stream); + Require(value == 0x11223344 || value == 0x55667788, "concurrent words do not interleave their bytes"); + if (value == 0x11223344) ++firstCount; + else ++secondCount; + } + Require(firstCount == 256 && secondCount == 256, "both writers preserve every word"); + Require(fclose_nid_postfix(stream) == 0, "close shared stream"); +} + +} + +int main(int argc, char** argv) { + Require(argc == 2, "expected temporary file path"); + CheckRoundTrip(argv[1]); + CheckShortReads(argv[1]); + CheckErrors(argv[1]); + CheckConcurrentWrites(argv[1]); + Require(std::remove(argv[1]) == 0, "remove temporary file"); + std::puts("binary word I/O checks passed"); +} diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 6249f192d0..5c3a3d06da 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -28,6 +28,7 @@ - [snwprintf_s](../../../core/libs/prx/libc/src/FormattingWide.cpp) (libc) - follows C11 K.3.9.1.3 with `RSIZE_MAX` assumed to be `SIZE_MAX >> 1`. A runtime-constraint violation only returns a negative value: `set_constraint_handler_s` is not exported and the console's default handler is unknown. An invalid multibyte `%s` argument is copied byte by byte instead of being reported as an encoding error, as in `vswprintf` - [swprintf_s and vswprintf_s](../../../core/libs/prx/libc/src/FormattingWide.cpp) (libc) - follow C11 K.3.9.1.4 with `RSIZE_MAX` assumed to be `SIZE_MAX >> 1`: the complete output, including the terminating null, must fit in the buffer, otherwise it is a runtime-constraint violation that sets `buffer[0]` to zero and returns a negative value, and the buffer is not partially written. Argument and encoding checks match `snwprintf_s` - [strerror_s and strerrorlen_s](../../../core/libs/prx/libc/src/WideConversion.cpp) (libc) follow [C11 K.3.7.4.2 and K.3.7.4.3](https://www.open-std.org/jtc1/sc22/wg14/www/docs/n1570.pdf), using the existing FreeBSD error messages and unknown-error formatting. Truncation writes an ellipsis when capacity exceeds 3; constraint failures leave the destination untouched and invoke the registered guest-ABI handler. The console's `RSIZE_MAX` (`SIZE_MAX >> 1` here), nonzero returns (`EINVAL` 22 for constraints, `ERANGE` 34 for truncation), handler arguments and default abort behavior are unverified. Messages remain in the C locale, and guest errno is preserved. +- [getw and putw](../../../core/libs/prx/libc/src/BinaryWordIo.cpp) (libc) follow FreeBSD 11.4 [getw.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/stdio/getw.c) and [putw.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/stdio/putw.c): one 32-bit little-endian word, `getw` returns -1 for EOF or an error, and `putw` returns 0 on success or -1 on error while holding the stream lock. They use the existing host-backed streams, so buffering and error timing follow the host CRT. Null streams throw and closed streams return -1 with `EBADF`, as existing byte I/O does. Console behavior is unverified. - [vsnwprintf_s](../../../core/libs/prx/libc/src/FormattingWide.cpp) (libc) - `va_list` counterpart of `snwprintf_s` (C11 K.3.9.1.3): truncates to the buffer and returns the full length, with the same constraints and argument checks - [vsnprintf_s and vsscanf_s](../../../core/libs/prx/libc/src/Formatting.cpp) (libc) - `vsnprintf_s` forwards to `vsnprintf` semantics: the C11 K.3.7.1.5 runtime constraints (null buffer, `size == 0`, `%n`, null `%s`) are not checked, so `size == 0` returns the would-be length instead of a negative value, and no constraint handler is invoked. `vsscanf_s` returns the count of items converted before an undersized `%s`/`%c`/`%[` destination (0 when it is the first), not EOF. Neither is confirmed on a PS5 title - [strndup and strdup](../../../core/libs/prx/libc/src/AllocatingStrings.cpp) (libc) return null with `errno` set to `EINVAL` (22) when `source` is null, preventing a host segfault on invalid guest input. POSIX.1-2008 and FreeBSD 11.4 libc leave `strndup(NULL)` and `strdup(NULL)` undefined (faulting); exact console behavior on hardware is unverified. From 0c92c51b364ad3e3e240dd875fdfddda9680fee0 Mon Sep 17 00:00:00 2001 From: Nithishvelan M Date: Sun, 18 Oct 2026 01:02:13 +0530 Subject: [PATCH 0846/1000] refactor(libSceMsgDialog.native): drop Export.cpp changes and retain lifecycle tests --- core/libs/prx/libSceMsgDialog.native/Export.cpp | 7 ++----- core/libs/tests/GuestMsgDialogNative.cpp | 2 +- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libSceMsgDialog.native/Export.cpp b/core/libs/prx/libSceMsgDialog.native/Export.cpp index faa81c5ec1..339bf0049a 100644 --- a/core/libs/prx/libSceMsgDialog.native/Export.cpp +++ b/core/libs/prx/libSceMsgDialog.native/Export.cpp @@ -13,7 +13,6 @@ constexpr int COMMON_DIALOG_RESULT_OK = 0; constexpr int COMMON_DIALOG_ERROR_ALREADY_INITIALIZED = static_cast(0x80B80004u); constexpr int COMMON_DIALOG_ERROR_NOT_INITIALIZED = static_cast(0x80B80003u); constexpr int COMMON_DIALOG_ERROR_NOT_FINISHED = static_cast(0x80B80005u); -constexpr int COMMON_DIALOG_ERROR_BUSY = static_cast(0x80B80007u); constexpr int COMMON_DIALOG_ERROR_NOT_RUNNING = static_cast(0x80B8000Bu); constexpr int COMMON_DIALOG_ERROR_ARG_NULL = static_cast(0x80B8000Du); constexpr int BUTTON_ID_OK = 1; @@ -49,7 +48,6 @@ int APS5_VABI sceMsgDialogInitialize(void) { int APS5_VABI sceMsgDialogOpen(const void* param) { const int status = g_status.load(); if (status == 0) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; - if (status == COMMON_DIALOG_STATUS_RUNNING) return COMMON_DIALOG_ERROR_BUSY; if (param == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; g_status = COMMON_DIALOG_STATUS_FINISHED; return 0; @@ -72,9 +70,8 @@ int APS5_VABI sceMsgDialogProgressBarInc(int target, std::uint32_t delta) { int APS5_VABI sceMsgDialogProgressBarSetMsg(int target, const char* msg) { (void)target; - if (g_status.load() != COMMON_DIALOG_STATUS_RUNNING) return COMMON_DIALOG_ERROR_NOT_RUNNING; - if (msg == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; - return 0; + (void)msg; + return g_status.load() == COMMON_DIALOG_STATUS_RUNNING ? 0 : COMMON_DIALOG_ERROR_NOT_RUNNING; } int APS5_VABI sceMsgDialogProgressBarSetValue(int target, std::uint32_t rate) { diff --git a/core/libs/tests/GuestMsgDialogNative.cpp b/core/libs/tests/GuestMsgDialogNative.cpp index 2067d9c2de..b25419c969 100644 --- a/core/libs/tests/GuestMsgDialogNative.cpp +++ b/core/libs/tests/GuestMsgDialogNative.cpp @@ -62,7 +62,7 @@ int main() { Require(sceMsgDialogUpdateStatus() == kStatusFinished); Require(sceMsgDialogGetStatus() == kStatusFinished); - MsgDialogResult result{}; + result = MsgDialogResult{}; result.button_id = -1; Require(sceMsgDialogGetResult(&result) == 0); Require(result.result == 0); From 87f2e7a88c006b3fea6c1ba4b3ad28c34035571d Mon Sep 17 00:00:00 2001 From: Nithishvelan M Date: Sun, 18 Oct 2026 01:05:22 +0530 Subject: [PATCH 0847/1000] fix(libSceSaveData.native): drop size 0 and resource validation per review --- core/libs/prx/libSceSaveData.native/Export.cpp | 4 ++-- core/libs/tests/SaveDataMemoryMetadata.cpp | 7 +++---- docs/dev/debt/libSceSaveData.native.md | 2 +- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libSceSaveData.native/Export.cpp b/core/libs/prx/libSceSaveData.native/Export.cpp index 503004a144..61c6d87e85 100644 --- a/core/libs/prx/libSceSaveData.native/Export.cpp +++ b/core/libs/prx/libSceSaveData.native/Export.cpp @@ -216,8 +216,8 @@ int APS5_VABI sceSaveDataCommit(const SaveDataCommitParam* param) { } int APS5_VABI sceSaveDataCreateTransactionResource(uint32_t size) { + (void)size; if (g_initializations == 0) return SAVE_DATA_ERROR_NOT_INITIALIZED; - if (size == 0) return SAVE_DATA_ERROR_PARAMETER; return g_transaction_counter.fetch_add(1); } @@ -271,8 +271,8 @@ int APS5_VABI sceSaveDataDelete(const SaveDataDelete* del) { } int APS5_VABI sceSaveDataDeleteTransactionResource(int32_t resource) { + (void)resource; if (g_initializations == 0) return SAVE_DATA_ERROR_NOT_INITIALIZED; - if (resource <= 0) return SAVE_DATA_ERROR_PARAMETER; return SAVE_DATA_OK; } diff --git a/core/libs/tests/SaveDataMemoryMetadata.cpp b/core/libs/tests/SaveDataMemoryMetadata.cpp index bb39407dbe..4f516231a5 100644 --- a/core/libs/tests/SaveDataMemoryMetadata.cpp +++ b/core/libs/tests/SaveDataMemoryMetadata.cpp @@ -28,7 +28,6 @@ int APS5_VABI sceSaveDataSetupSaveDataMemory2(const SaveDataMemorySetup2*, SaveD namespace { constexpr int kSaveDataNotInitialized = -2137063423; -constexpr int kSaveDataParameter = -2137063424; } static void Check(bool value, int line) { @@ -63,9 +62,9 @@ int main() { Require(static_cast(file)); } Require(sceSaveDataInitialize3(nullptr) == 0); - Require(sceSaveDataCreateTransactionResource(0) == kSaveDataParameter); - Require(sceSaveDataDeleteTransactionResource(0) == kSaveDataParameter); - Require(sceSaveDataDeleteTransactionResource(-1) == kSaveDataParameter); + const int tx0 = sceSaveDataCreateTransactionResource(0); + Require(tx0 > 0); + Require(sceSaveDataDeleteTransactionResource(tx0) == 0); const int tx = sceSaveDataCreateTransactionResource(1024); Require(tx > 0); Require(sceSaveDataDeleteTransactionResource(tx) == 0); diff --git a/docs/dev/debt/libSceSaveData.native.md b/docs/dev/debt/libSceSaveData.native.md index 40d957b18b..cd4c68cccf 100644 --- a/docs/dev/debt/libSceSaveData.native.md +++ b/docs/dev/debt/libSceSaveData.native.md @@ -2,7 +2,7 @@ ### Unknown function info -- [sceSaveDataCreateTransactionResource, sceSaveDataDeleteTransactionResource](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - transaction resources return synthetic positive identifiers and validate initialization state and non-zero parameters; transactional rollback semantics on host filesystem storage are unmodeled +- [sceSaveDataCreateTransactionResource, sceSaveDataDeleteTransactionResource](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - transaction resources return synthetic positive identifiers and validate initialization state; resource parameter validation and transactional rollback semantics on host filesystem storage are unknown - [sceSaveDataPrepareForTransferring](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native, NID `RvID8BVeg+g`) - declared without parameters as a throwing stub; Minecraft (PPSA17221) imports it beside `sceSaveDataTransferringMountPs4` and `sceSaveDataDirNameSearchPs4` and fails to load without it. The name hashes to the NID but is not in aerolib; it was found by combining words from the libSceSaveData names in aerolib. Its arguments and behaviour are unknown. - [sceSaveDataTransferringMountPs4, sceSaveDataDirNameSearchPs4](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - arguments assumed to be those of `sceSaveDataTransferringMount` and `sceSaveDataDirNameSearch`: KytyPS5 binds both NIDs to the PS5 functions, and [prosper](https://github.com/mattias800/prosper) captured the same result layout on a title for the search. There is no PS4 save area, so the mount returns `NOT_FOUND` and the search reports zero hits - [sceSaveDataConvert](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - the parameter block (user id at `+0x00`, `SceSaveDataDirName` pointer at `+0x08`, 48 opaque bytes after it) is read from PPSA28416's single call, not confirmed by KytyPS5, shadPS4, fpPS4 or SharpProspero, which have no body for it. Host saves have no size limit, so an existing save returns `SCE_OK` without touching its files and no event is queued; what the console returns for a save that is mounted, already converted or needs no conversion is unknown. The directory name is validated as `sceSaveDataDelete` does From f2a4236ab6635d46726de60d5ef660272b7c1bbd Mon Sep 17 00:00:00 2001 From: thuanquang Date: Sun, 18 Oct 2026 02:38:00 +0700 Subject: [PATCH 0848/1000] fix(agc): draw rect lists in last provoking vertex mode The generated rect-list control stage gave every corner of a rectangle the flat inputs of its first vertex, so last mode was refused for rect lists. BuildRectListShaders now also builds a control stage that takes them from the rectangle's third vertex when the pixel shader has a flat input, and a draw in last mode uses it. Co-Authored-By: Claude Opus 5.5 --- .../Execution/src/Driver/Draw/Draw.cpp | 5 ++- .../libSceAgcDriver/Graphics/src/Pipeline.cpp | 4 +- .../prx/libSceAgcDriver/tests/Graphics.cpp | 18 +++++--- .../tests/execution/ProvokingVertex.cpp | 45 ++++++++++++++++++- core/shader/recompiler/Recompiler.hpp | 1 + .../SpirvBackend/src/RectListShaders.cpp | 16 +++++-- docs/dev/debt/libSceAgcDriver.md | 2 +- 7 files changed, 75 insertions(+), 16 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp index 5dea7922ea..87a9acb8c4 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp @@ -434,15 +434,16 @@ DrawVerdict Driver::draw(QueueState& queue, std::span packe phaseTiming.Phase(DrawRowVectors); require(programs.size() == 2 && programResults[0] != nullptr && programResults[1] != nullptr, "rect-list requires vertex and fragment programs"); auto rectangle = DrawRectangle(*programs[0].snapshot, programs[1].snapshot, programResults[0]->variantId, programResults[1]->variantId, localDevice->Target()); + auto& control = graphics.provokingVertexMode == VK_PROVOKING_VERTEX_MODE_LAST_VERTEX_EXT ? rectangle.lastVertexControl : rectangle.control; if (rectListBuilt) { - results[rectIndex] = std::move(rectangle.control); + results[rectIndex] = std::move(control); results[rectIndex + 1] = std::move(rectangle.evaluation); phaseTiming.Phase(DrawRowRectList); return; } require(stages.size() == 2, "rect-list requires vertex and fragment programs"); rectIndex = results.size(); - results.push_back(std::move(rectangle.control)); + results.push_back(std::move(control)); results.push_back(std::move(rectangle.evaluation)); stages.insert(stages.begin() + 1, {{Stage::TessellationControl, &results[rectIndex], 0}, {Stage::TessellationEvaluation, &results[rectIndex + 1], 0}}); rectListBuilt = true; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp index d9a9c96bdc..13550dcbdb 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp @@ -106,8 +106,8 @@ void ValidateProvokingVertex(const Context& context, const State& state, std::sp return; } Require(context.provokingVertexLast, "last provoking vertex requires VK_EXT_provoking_vertex with provokingVertexLast enabled"); - Require(!state.rectList && state.stages.path == ShaderPath::Vertex && !state.stages.mesh && !state.stages.tessellation, "last provoking vertex is unsupported for generated primitive pipelines"); - Require(state.topology == VK_PRIMITIVE_TOPOLOGY_POINT_LIST || state.topology == VK_PRIMITIVE_TOPOLOGY_LINE_LIST || state.topology == VK_PRIMITIVE_TOPOLOGY_LINE_STRIP || state.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST || state.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP || state.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN, "last provoking vertex is unsupported for this primitive topology"); + Require(state.stages.path == ShaderPath::Vertex && !state.stages.mesh && !state.stages.tessellation, "last provoking vertex is unsupported for generated primitive pipelines"); + Require(state.rectList || state.topology == VK_PRIMITIVE_TOPOLOGY_POINT_LIST || state.topology == VK_PRIMITIVE_TOPOLOGY_LINE_LIST || state.topology == VK_PRIMITIVE_TOPOLOGY_LINE_STRIP || state.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST || state.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP || state.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN, "last provoking vertex is unsupported for this primitive topology"); for (const auto& shader : shaders) { if (shader.stage != ShaderRecompiler::ShaderStage::Fragment) continue; Require(shader.program != nullptr, "missing compiled fragment shader"); diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index cbba137501..39e9b291a1 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -5748,11 +5748,14 @@ void rectListTests() { fragment.fragmentParameters = {{0, 0, false, false}, {1, 0, true, false}}; auto auxiliary = BuildRectListShaders(vertex, fragment, target); Require(!auxiliary.control.spirv.empty() && !auxiliary.evaluation.spirv.empty(), "rect-list parameter shaders are empty"); + Require(faultAuxiliary.lastVertexControl.Identity() == faultAuxiliary.control.Identity() && auxiliary.lastVertexControl.Identity() != auxiliary.control.Identity() && auxiliary.lastVertexControl.bindings.size() == 1 && auxiliary.lastVertexControl.bdaAbiVersion == auxiliary.control.bdaAbiVersion, "the last-vertex rect-list control shader does not differ from the first-vertex one exactly when a flat parameter is present"); auto parameterQueue = makeState(); parameterQueue.userConfig[0x242] = 17; const auto parameterState = DecodeState(parameterQueue); - const std::array parameterShaders{{{ShaderStage::Vertex, &vertex, 0}, {ShaderStage::TessellationControl, &auxiliary.control, 0}, {ShaderStage::TessellationEvaluation, &auxiliary.evaluation, 0}, {ShaderStage::Fragment, &fragment, 0}}}; - ValidateShaders(parameterShaders, parameterState, VkPhysicalDeviceSubgroupProperties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_PROPERTIES}, false); + for (const auto* control : {&auxiliary.control, &auxiliary.lastVertexControl}) { + const std::array parameterShaders{{{ShaderStage::Vertex, &vertex, 0}, {ShaderStage::TessellationControl, control, 0}, {ShaderStage::TessellationEvaluation, &auxiliary.evaluation, 0}, {ShaderStage::Fragment, &fragment, 0}}}; + ValidateShaders(parameterShaders, parameterState, VkPhysicalDeviceSubgroupProperties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_PROPERTIES}, false); + } fragment.fragmentParameters[0].perVertex = true; auto explicitInterpolation = BuildRectListShaders(vertex, fragment, target); Require(!explicitInterpolation.control.spirv.empty() && !explicitInterpolation.evaluation.spirv.empty(), "rect-list shaders for an explicitly interpolated parameter are empty"); @@ -6617,13 +6620,16 @@ void ProvokingVertexTests() { generated.provokingVertexMode = VK_PROVOKING_VERTEX_MODE_FIRST_VERTEX_EXT; ValidateProvokingVertex(unsupported, generated); } - for (std::uint32_t generated = 0; generated < 3; ++generated) { + for (std::uint32_t generated = 0; generated < 2; ++generated) { auto state = last; - if (generated == 0) state.rectList = true; - if (generated == 1) state.stages.mesh.emplace(); - if (generated == 2) state.stages.tessellation.emplace(); + if (generated == 0) state.stages.mesh.emplace(); + if (generated == 1) state.stages.tessellation.emplace(); expectFailure([&] { ValidateProvokingVertex(supported, state); }, "last provoking vertex is unsupported for generated primitive pipelines"); } + auto rectangle = last; + rectangle.rectList = true; + rectangle.topology = VK_PRIMITIVE_TOPOLOGY_PATCH_LIST; + ValidateProvokingVertex(supported, rectangle, flatShaders); last.provokingVertexMode = VK_PROVOKING_VERTEX_MODE_MAX_ENUM_EXT; expectFailure([&] { ValidateProvokingVertex(supported, last); }, "invalid provoking vertex mode"); } diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ProvokingVertex.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ProvokingVertex.cpp index 2654a113b7..79cdc00701 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ProvokingVertex.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ProvokingVertex.cpp @@ -99,6 +99,46 @@ void Run(AgcDriver::VulkanDevice& device, VkPrimitiveTopology topology, std::spa } } +void RunRectangle(AgcDriver::VulkanDevice& device, const ShaderRecompiler::RecompileResult& vertex, const ShaderRecompiler::RecompileResult& pixel) { + const auto rectangle = ShaderRecompiler::BuildRectListShaders(vertex, pixel, device.Target()); + Pixels.fill(Sentinel); + AgcDriver::Graphics::State state{}; + state.stages.path = AgcDriver::Graphics::ShaderPath::Vertex; + state.color = {reinterpret_cast(Pixels.data()), {Width, Height}, VK_FORMAT_R8G8B8A8_UNORM, Pixels.size(), 0xe4u}; + state.colors = {state.color}; + state.hasColorTarget = true; + state.renderExtent = {Width, Height}; + state.topology = VK_PRIMITIVE_TOPOLOGY_PATCH_LIST; + state.rectList = true; + state.cullMode = VK_CULL_MODE_NONE; + state.scissor = {{0, 0}, {Width, Height}}; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + state.blend.colorWriteMask = 15; + state.blends = {state.blend}; + const AgcDriver::Pm4::DrawParameters draw{0, 3, 0, 1, 0, false}; + for (std::uint32_t panel = 0; panel < 3; ++panel) { + const bool last = panel == 1; + state.viewport = {static_cast(panel * Panel), static_cast(Height), static_cast(Panel), -static_cast(Height), 0, 1}; + state.provokingVertexMode = last ? VK_PROVOKING_VERTEX_MODE_LAST_VERTEX_EXT : VK_PROVOKING_VERTEX_MODE_FIRST_VERTEX_EXT; + const std::array shaders{{ + {ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, + {ShaderRecompiler::ShaderStage::TessellationControl, last ? &rectangle.lastVertexControl : &rectangle.control, 0}, + {ShaderRecompiler::ShaderStage::TessellationEvaluation, &rectangle.evaluation, 0}, + {ShaderRecompiler::ShaderStage::Fragment, &pixel, PixelPushOffset(0u, device.Target())} + }}; + device.Draw(state, draw, shaders); + } + device.WaitIdle(); + for (std::uint32_t panel = 0; panel < 3; ++panel) { + const auto& expected = panel == 1 ? Blue : Red; + const auto name = std::string("guest P0 rect list, ") + (panel == 1 ? "last" : "first") + " provoking vertex"; + for (const auto x : {16u, 48u}) { + for (const auto y : {16u, 48u}) CheckPixel(panel * Panel + x, y, expected, name); + } + CheckPixel(panel * Panel, 0, {64, 64, 64, 64}, name + " background"); + } +} + } int main() { @@ -133,7 +173,10 @@ int main() { {ShaderRecompiler::ShaderStage::Fragment, &guestPixel, PixelPushOffset(0u, device->Target())} }}; for (const auto topology : {VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST, VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP}) Run(*device, topology, guestShaders, true, "guest P0"); - std::puts("provoking vertex rendering tests passed: GLSL and guest P0, triangle list and strip, first/last/first"); + auto exporting = vertex; + exporting.parameterExports = {0}; + RunRectangle(*device, exporting, guestPixel); + std::puts("provoking vertex rendering tests passed: GLSL and guest P0, triangle list and strip, guest P0 rect list, first/last/first"); return 0; } catch (const std::exception& error) { std::cerr << error.what() << '\n'; diff --git a/core/shader/recompiler/Recompiler.hpp b/core/shader/recompiler/Recompiler.hpp index 123c488f40..9ccd25554f 100644 --- a/core/shader/recompiler/Recompiler.hpp +++ b/core/shader/recompiler/Recompiler.hpp @@ -683,6 +683,7 @@ void SetDebugProbeActive(bool active); struct RectListShaders { RecompileResult control; RecompileResult evaluation; + RecompileResult lastVertexControl; }; [[nodiscard]] RectListShaders BuildRectListShaders(const RecompileResult& vertex, const RecompileResult& fragment, const SpirvTarget& target); diff --git a/core/shader/recompiler/SpirvBackend/src/RectListShaders.cpp b/core/shader/recompiler/SpirvBackend/src/RectListShaders.cpp index 59757fa18c..300b471eef 100644 --- a/core/shader/recompiler/SpirvBackend/src/RectListShaders.cpp +++ b/core/shader/recompiler/SpirvBackend/src/RectListShaders.cpp @@ -56,7 +56,7 @@ public: } } - std::vector EmitControl() { + std::vector EmitControl(std::uint32_t flatVertex) { defineEntry(spv::ExecutionModelTessellationControl); const auto floatOne = constant(floatType, std::bit_cast(1.0f)); @@ -123,11 +123,11 @@ public: store(access(ptrOutputVec4Float, outputs[i], invocation), result(spv::OpCompositeConstruct, vec4FloatType, zero, zero, zero, zero)); continue; } - const auto input0 = load(vec4FloatType, access(ptrInputVec4Float, inputs[i], intConstant(0))); if (parameters[i].flat) { - store(access(ptrOutputVec4Float, outputs[i], invocation), input0); + store(access(ptrOutputVec4Float, outputs[i], invocation), load(vec4FloatType, access(ptrInputVec4Float, inputs[i], intConstant(flatVertex)))); continue; } + const auto input0 = load(vec4FloatType, access(ptrInputVec4Float, inputs[i], intConstant(0))); const auto input1 = load(vec4FloatType, access(ptrInputVec4Float, inputs[i], intConstant(1))); const auto input2 = load(vec4FloatType, access(ptrInputVec4Float, inputs[i], intConstant(2))); const auto input3 = interpolate(input0, input1, input2, barycentric); @@ -398,7 +398,7 @@ RectListShaders BuildRectListShaders(const RecompileResult& vertex, const Recomp RectListEmitter control(parameters, spv::ExecutionModelTessellationControl, target.spirvVersion, faultBinding); RectListEmitter evaluation(parameters, spv::ExecutionModelTessellationEvaluation, target.spirvVersion, faultBinding); RectListShaders shaders; - shaders.control.spirv = control.EmitControl(); + shaders.control.spirv = control.EmitControl(0u); shaders.control.bindings.push_back({DescriptorKind::StorageBuffer, DescriptorRole::FaultBuffer, 0, faultBinding, 1, {}}); shaders.control.bdaAbiVersion = BdaAbi::Version; shaders.evaluation.spirv = evaluation.EmitEvaluation(); @@ -406,6 +406,14 @@ RectListShaders BuildRectListShaders(const RecompileResult& vertex, const Recomp shaders.control.spirv = ValidateAndOptimizeSpirv(shaders.control.spirv, target.vulkanVersion, target.spirvVersion); shaders.evaluation.spirv = ValidateAndOptimizeSpirv(shaders.evaluation.spirv, target.vulkanVersion, target.spirvVersion); #endif + shaders.lastVertexControl = shaders.control; + if (std::any_of(parameters.begin(), parameters.end(), [](const Parameter& parameter) { return parameter.flat && !parameter.missing; })) { + RectListEmitter lastVertexControl(parameters, spv::ExecutionModelTessellationControl, target.spirvVersion, faultBinding); + shaders.lastVertexControl.spirv = lastVertexControl.EmitControl(2u); +#if ANYPS5_ENABLE_SPIRV_TOOLS + shaders.lastVertexControl.spirv = ValidateAndOptimizeSpirv(shaders.lastVertexControl.spirv, target.vulkanVersion, target.spirvVersion); +#endif + } return shaders; } diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 71210637ea..0b074cefaf 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -100,7 +100,7 @@ - The [dispatches that follow a compute queue's wait](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchAhead.cpp) (libSceAgcDriver), up to the next wait that memory does not satisfy, a packet that writes guest memory other than a label, or a conditional packet, are resolved (memory captured, shader recompiled) when the wait passes, as the CP reads them right after it; the graphics queue is exempt (`APS5_GROUP_CAPTURE_ALL_QUEUES=1` includes it). The device still reads guest memory when the dispatch is issued; when that memory no longer matches the capture, the dispatch is resolved again from current memory. - [PA_CL_CLIP_CNTL](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - `DX_LINEAR_ATTR_CLIP_ENA` (bit 24) clear is drawn like bit 24 set, with Vulkan's clipping of `NoPerspective` outputs; what a clear bit changes on the console is undocumented. - [10_11_11 unorm color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) render into R32_UINT attachments: the pixel shader packs each export, rounded to nearest even like format-30 image stores, which is assumed to match the CB's conversion. -- [PA_SU_SC_MODE_CNTL.PROVOKING_VTX_LAST](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp) (libSceAgcDriver) uses Vulkan's last-vertex mode for native primitives. Bit 19 follows [opengnm's `flatshade_first` mapping](https://github.com/blackbearreloaded/ps5-opengl/blob/db752da4c89380af228497b5b6852163a4e61069/src/gallium/ps5/ps5_screen.c#L2244); vertex selection follows [Vulkan](https://docs.vulkan.org/spec/latest/chapters/drawing.html#drawing-primitive-topologies), without console measurements. Last mode throws without `VK_EXT_provoking_vertex`, for generated rectangle, mesh or tessellation primitives, explicit per-vertex flat inputs, and triangle strips whose pixel shader reads a vertex slot itself (`v_interp_mov_f32`, or 16-bit interpolation, which is not checked yet), since the vertex order of odd strip triangles [depends on the device](https://docs.vulkan.org/refpages/latest/refpages/source/VkPhysicalDeviceFragmentShaderBarycentricPropertiesKHR.html). `v_interp_p1_f32` and `v_interp_p2_f32` read P10 and P20 against the guest's I and J, which come from `gl_BaryCoordKHR` in the same vertex order as the per-vertex values, so the order cancels out and strips with only those reads draw. A FLAT_SHADE input without pass-through that is read through `v_interp_mov_f32` P10 and not P20 [returns the provoking vertex's value](../../../core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp), like P0. ps5-opengl reads every flat input that way, and its driver passes the GL 4.6 CTS on a PS5 (#1413). The lane itself was not measured on the console, so with the first provoking vertex such a shader throws. +- [PA_SU_SC_MODE_CNTL.PROVOKING_VTX_LAST](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp) (libSceAgcDriver) uses Vulkan's last-vertex mode for native primitives. Bit 19 follows [opengnm's `flatshade_first` mapping](https://github.com/blackbearreloaded/ps5-opengl/blob/db752da4c89380af228497b5b6852163a4e61069/src/gallium/ps5/ps5_screen.c#L2244); vertex selection follows [Vulkan](https://docs.vulkan.org/spec/latest/chapters/drawing.html#drawing-primitive-topologies), without console measurements. A [rect-list](../../../core/shader/recompiler/SpirvBackend/src/RectListShaders.cpp) draw in last mode gives all four corners of each rectangle the flat inputs of its third vertex through the generated control stage, as first mode gives them those of its first, so Vulkan's choice of provoking vertex for the tessellated triangles does not matter; which vertex the console takes for a rectangle is not measured either. Last mode throws without `VK_EXT_provoking_vertex`, for generated mesh or tessellation primitives, explicit per-vertex flat inputs, and triangle strips whose pixel shader reads a vertex slot itself (`v_interp_mov_f32`, or 16-bit interpolation, which is not checked yet), since the vertex order of odd strip triangles [depends on the device](https://docs.vulkan.org/refpages/latest/refpages/source/VkPhysicalDeviceFragmentShaderBarycentricPropertiesKHR.html). `v_interp_p1_f32` and `v_interp_p2_f32` read P10 and P20 against the guest's I and J, which come from `gl_BaryCoordKHR` in the same vertex order as the per-vertex values, so the order cancels out and strips with only those reads draw. A FLAT_SHADE input without pass-through that is read through `v_interp_mov_f32` P10 and not P20 [returns the provoking vertex's value](../../../core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp), like P0. ps5-opengl reads every flat input that way, and its driver passes the GL 4.6 CTS on a PS5 (#1413). The lane itself was not measured on the console, so with the first provoking vertex such a shader throws. - [Dual-source blending](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) takes the MRT1 export as the second source of the MRT0 blend, as RADV and radeonsi program it; what the CB does with other export or target layouts is unknown, so they throw. - [Blending into reversed-order color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - 8_8_8_8 and 16_16_16_16 targets with `COMP_SWAP` 2 or 3 keep the exported alpha in their first channel and red or blue in their fourth, which Vulkan would blend with the color and the alpha equation respectively. The color block is taken to blend in shader export order and apply `COMP_SWAP` afterwards: the alpha equation and every source-alpha factor use the exported alpha, and the destination alpha is the first channel. This follows the `CB_TARGET_MASK` text of the AMD [SI](https://xorg.freedesktop.org/docs/AMD/old/SI_3D_registers.pdf) (p. 229) and [CIK](https://xorg.freedesktop.org/docs/AMD/old/CIK_3D_registers_v2.pdf) (p. 238) register references ("the order of components after blending and before COMP_SWAP is applied"). RADV builds `CB_BLENDn_CONTROL` without regard to the swap ([radv_pipeline_graphics.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-26.2.2/src/amd/vulkan/radv_pipeline_graphics.c#L650-L709)) and renders R4G4B4A4, B4G4R4A4, R5G5B5A1 and B5G5R5A1 with `SWAP_STD_REV` and `SWAP_ALT_REV` ([vk_format.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-26.2.2/src/vulkan/util/vk_format.c#L43-L48), [ac_formats.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-26.2.2/src/amd/common/ac_formats.c#L609-L624)). Mesa CI's Navi10 (gfx1010) CTS run ([job 112381553](https://gitlab.freedesktop.org/mesa/mesa/-/jobs/112381553), [112381554](https://gitlab.freedesktop.org/mesa/mesa/-/jobs/112381554)) passes all 400 `blend.format..states` cases of each of those formats, including separate alpha and destination-alpha factors. It is not measured on gfx1013 or on 8_8_8_8 and 16_16_16_16 targets. Factor pairs Vulkan applies per channel are used directly. Otherwise the pixel shader only copies values: it writes the exported alpha, each channel's own export or the constants 0 and 1 into a second color at location 0 index 1 (and 1 into a channel of the first color whose source factor is ONE, moving its export into the factor), and the blend forms every factor, `1 -` and product from those copies through `SRC1_COLOR`/`SRC1_ALPHA` factors. The blend unit therefore rounds them as it rounds a plain Vulkan blend of the same export; Vulkan does not require a second-source color to be converted like the first, but on an RTX 5080 and lavapipe every reversed-order case of the execution test is bit-identical to the same draw into a `SWAP_STD` target, which the test checks. Factor combinations that copies cannot express (for example `ONE_MINUS_SRC_ALPHA` in the alpha equation with `SRC_COLOR` in the color equation), sRGB, SNORM and integer targets, constant and second-source factors, destination-alpha and `SRC_ALPHA_SATURATE` factors in the color equation, different color and alpha operations, MIN or MAX with factors other than ONE, other color exports when the blend needs the second color, and devices without `dualSrcBlend` throw. Gran Turismo 7 (PPSA01317) blends `CB_BLEND0_CONTROL` 0x65010504 into such a target (#4782). - [DB_SHADER_CONTROL.CONSERVATIVE_Z_EXPORT](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp) (libSceAgcDriver) becomes `DepthLess`/`DepthGreater`. For a pixel shader that breaks the promise, the host driver decides whether its interpolated Z is depth-tested before shading; what the console does then was not measured. From e1341df3b9979f02f35884ce0a148976e82734da Mon Sep 17 00:00:00 2001 From: thuanquang Date: Sun, 18 Oct 2026 02:42:34 +0700 Subject: [PATCH 0849/1000] fix(libSceVideodec2): decode after a flush without a reset sceVideodec2Decode threw when it followed sceVideodec2Flush without sceVideodec2Reset. It now resets FFmpeg's drained decoder and decodes the unit, keeping the pictures the flush has not returned yet. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceVideodec2/Export.cpp | 5 +++- core/libs/tests/GuestVideodec2.cpp | 37 ++++++++++++++++++++++++ docs/dev/debt/libSceVideodec2.md | 1 + 3 files changed, 42 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceVideodec2/Export.cpp b/core/libs/prx/libSceVideodec2/Export.cpp index c98fa42582..ea5ca36c4e 100644 --- a/core/libs/prx/libSceVideodec2/Export.cpp +++ b/core/libs/prx/libSceVideodec2/Export.cpp @@ -555,7 +555,10 @@ int APS5_VABI sceVideodec2Decode_nid_postfix(std::uint64_t handle, const InputDa if (Trace() && decoder.decoded < 3) std::fprintf(stderr, "[videodec2] Decode input=%llu frame=%llu output=%llu au=%llu pts=%llu size=%ux%u fb=%p/%llu\n", static_cast(input->thisSize), static_cast(frame ? frame->thisSize : 0), static_cast(output->thisSize), static_cast(input->auSize), static_cast(input->ptsData), decoder.width, decoder.height, frame ? frame->frameBuffer : nullptr, static_cast(frame ? frame->frameBufferSize : 0)); ++decoder.decoded; const Picture picture{input->ptsData, input->dtsData, input->attachedData}; - if (decoder.flushing) throw std::runtime_error("Videodec2: reset required after flushing"); + if (decoder.flushing) { + avcodec_flush_buffers(decoder.context); + decoder.flushing = false; + } if (!input->auData || input->auSize == 0 || input->auSize > static_cast(std::numeric_limits::max())) throw std::runtime_error("Videodec2: invalid access unit"); if (decoder.codec == CodecVp9) { auto references = decoder.vp9References; diff --git a/core/libs/tests/GuestVideodec2.cpp b/core/libs/tests/GuestVideodec2.cpp index 4fe6f2c40f..f8d3be0ffc 100644 --- a/core/libs/tests/GuestVideodec2.cpp +++ b/core/libs/tests/GuestVideodec2.cpp @@ -646,6 +646,42 @@ std::uint64_t frameBufferBytes() { return memory.maxFrameBufferSize; } +void testDecodeAfterFlush() { + std::vector buffer(frameBufferBytes()); + const auto handle = createDecoder(1, 100); + { + FrameBuffer frame{sizeof(FrameBuffer), buffer.data(), buffer.size(), false}; + OutputInfo output{sizeof(OutputInfo)}; + check(sceVideodec2Flush_nid_postfix(handle, &frame, &output) == 0 && !output.isValid && !frame.isAccepted, "flushing a decoder that decoded nothing gave a picture"); + } + const auto units = accessUnits(false); + for (const std::uint64_t base : {1000u, 5000u}) { + std::size_t pictures = 0; + const auto take = [&](const OutputInfo& output) { + if (!output.isValid) return false; + check(pictures < PictureHashes.size() && owned(buffer, base + DisplayOrderUnits[pictures]), "a picture decoded after a flush is out of display order"); + check(hashNv12(buffer.data(), output.framePitch, output.frameHeight) == PictureHashes[pictures], "picture " + std::to_string(pictures) + " decoded after a flush differs from the reference"); + ++pictures; + return true; + }; + for (std::size_t unit = 0; unit < units.size(); ++unit) { + const InputData input{sizeof(InputData), units[unit].data(), units[unit].size(), base + unit, unit, base + unit}; + FrameBuffer frame{sizeof(FrameBuffer), buffer.data(), buffer.size(), false}; + OutputInfo output{sizeof(OutputInfo)}; + check(sceVideodec2Decode_nid_postfix(handle, &input, &frame, &output) == 0, "decode after a flush failed"); + take(output); + } + for (;;) { + FrameBuffer frame{sizeof(FrameBuffer), buffer.data(), buffer.size(), false}; + OutputInfo output{sizeof(OutputInfo)}; + check(sceVideodec2Flush_nid_postfix(handle, &frame, &output) == 0, "flush failed"); + if (!take(output)) break; + } + check(pictures == PictureHashes.size(), "missing pictures of a stream decoded after a flush"); + } + check(sceVideodec2DeleteDecoder_nid_postfix(handle) == 0, "delete failed"); +} + void testPictureOwnership() { std::vector shared(frameBufferBytes()), other(frameBufferBytes()); { @@ -774,6 +810,7 @@ int main() { testClip(CodecVp9, 0, ivfFrames(), Vp9Hashes); testVp9PictureMetadata(); testTenBit(); + testDecodeAfterFlush(); testPictureOwnership(); testConcurrentOwnership(); std::puts("Videodec2 tests passed"); diff --git a/docs/dev/debt/libSceVideodec2.md b/docs/dev/debt/libSceVideodec2.md index 8b3e7a2f85..3adaaa4524 100644 --- a/docs/dev/debt/libSceVideodec2.md +++ b/docs/dev/debt/libSceVideodec2.md @@ -11,4 +11,5 @@ - [sceVideodec2CreateDecoder](../../../core/libs/prx/libSceVideodec2/Export.cpp) (libSceVideodec2) decodes HEVC (codec type 974921) and VP9 (2382845) through FFmpeg, besides H.264. The tags come from SharpProspero's `Videodec2CodecType` and EVO-PLAYER-PS5's `sce_videodec2.h`, which records them from hardware runs. HEVC input is converted from length-prefixed NAL units to Annex B as for H.264, and VP9 frames are passed through. Output is the same 8-bit NV12 for every codec; a decoder for HEVC Main10 or VP9 Profile 2 is created, but a frame deeper than 8 bits throws, since the console's 10-bit output layout is unknown, and `QueryDecoderMemoryInfo` reports the 8-bit frame size for it. `sceVideodec2GetPictureInfo` and `sceVideodec2GetAvcPictureInfo` throw for HEVC and VP9 pictures, which use other layouts (`SceVideodec2HevcPictureInfo`, `SceVideodec2Vp9PictureInfo`), and `sceVideodec2GetHevcPictureInfo` is not exported; `sceVideodec2GetVp9PictureInfo` has its own VP9 layout - [sceVideodec2GetVp9PictureInfo](../../../core/libs/prx/libSceVideodec2/Export.cpp) uses the 88-byte layout and two-pointer signature from [SharpProspero Videodec2.cs](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Video/Videodec2.cs). Frame metadata follows the [VP9 specification](https://storage.googleapis.com/downloads.webmproject.org/docs/vp9/vp9-bitstream-specification-v0.7-20170222-draft.pdf), sections 5.26 and 6.2: profile, key/intra-only flags, bit depth, color space/range, coded/render dimensions and superframe count are retained per emitted buffer. Level is 0 because the elementary stream does not signal a level; the console value is unverified. Null pointers, undersized structures and querying another codec throw; console error codes and support for other structure sizes are unknown. Existing NV12 output rejects pictures deeper than 8 bits. Superframes are submitted as individual coded frames to keep each output associated with its input metadata. +- [sceVideodec2Decode after sceVideodec2Flush](../../../core/libs/prx/libSceVideodec2/Export.cpp) (libSceVideodec2) without `sceVideodec2Reset` restarts FFmpeg's decoder (`avcodec_flush_buffers`) and decodes the unit, where it threw before. TEKKEN DARK RESURRECTION (PPSA31371), which runs in Sony's PSP emulator, decodes after a flush without a reset (#7118), so the console accepts the call. Pictures the flush decoded but has not returned yet are returned before the new ones. What the console returns for them, and for a stream that continues across the flush without a new IDR picture, is not measured. - [sceVideodec2Decode](../../../core/libs/prx/libSceVideodec2/Export.cpp) (libSceVideodec2) and `sceVideodec2Flush` output the coded picture, with the chroma plane after its luma rows, as the console does for 1080p H.264 and HEVC streams coded at 1920x1088. An HEVC or VP9 picture whose size is not a multiple of 16, such as HEVC coded at 1920x1080, is output at that size; whether the console pads it is unverified. `sceVideodec2GetPictureInfo` reports no frame cropping From 90c3b61c6cf2baf3bb10765c73a846c5bff365b0 Mon Sep 17 00:00:00 2001 From: milescoviello Date: Sat, 17 Oct 2026 19:41:55 +0000 Subject: [PATCH 0850/1000] fix(agc): sample a multisampled target whose format differs only in red/blue order Sampling a native multisampled color target through a texture of another format was refused even when the two formats store the same bytes in the other channel order. Rush 2 and Wave Race 64 render a 3840x2160 8x target as 8_8_8_8 with the alternate component swap (B8G8R8A8_UNORM) and sample it through an 8_8_8_8 UNORM T# (R8G8B8A8_UNORM), and stopped on "sampling multisampled color target ... (vk format 44) as a ... texture ... guest format 56 (vk 37) is not implemented". For the B8G8R8A8/R8G8B8A8 pairs of the same numeric type the texture is now the target's own image with red and blue exchanged in its component mapping, composed with the T#'s swizzle. Other format differences are still refused. Co-Authored-By: Claude Opus 5.5 --- .../Graphics/src/MultisampleTarget.cpp | 23 ++++++++++- .../execution/SampleMultisampleTarget.cpp | 39 +++++++++++++++++-- 2 files changed, 57 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp index 5fc7544a28..e9754eb48b 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp @@ -676,6 +676,25 @@ VkImageView MultisampleTargetView(const Context& context, const ColorTarget& tar return acquire(context, target).view; } +namespace { + +bool redBlueSwapped(VkFormat rendered, VkFormat sampled) { + const auto pair = [&](VkFormat a, VkFormat b) { return (rendered == a && sampled == b) || (rendered == b && sampled == a); }; + return pair(VK_FORMAT_B8G8R8A8_UNORM, VK_FORMAT_R8G8B8A8_UNORM) || pair(VK_FORMAT_B8G8R8A8_SRGB, VK_FORMAT_R8G8B8A8_SRGB) || pair(VK_FORMAT_B8G8R8A8_SNORM, VK_FORMAT_R8G8B8A8_SNORM) || pair(VK_FORMAT_B8G8R8A8_UINT, VK_FORMAT_R8G8B8A8_UINT) || pair(VK_FORMAT_B8G8R8A8_SINT, VK_FORMAT_R8G8B8A8_SINT); +} + +VkComponentMapping swapRedBlue(VkComponentMapping components) { + const auto swapped = [](VkComponentSwizzle swizzle, VkComponentSwizzle identity) { + if (swizzle == VK_COMPONENT_SWIZZLE_IDENTITY) swizzle = identity; + if (swizzle == VK_COMPONENT_SWIZZLE_R) return VK_COMPONENT_SWIZZLE_B; + if (swizzle == VK_COMPONENT_SWIZZLE_B) return VK_COMPONENT_SWIZZLE_R; + return swizzle; + }; + return {swapped(components.r, VK_COMPONENT_SWIZZLE_R), swapped(components.g, VK_COMPONENT_SWIZZLE_G), swapped(components.b, VK_COMPONENT_SWIZZLE_B), swapped(components.a, VK_COMPONENT_SWIZZLE_A)}; +} + +} + std::shared_ptr MultisampleTargetTexture(const Context& context, const GuestTextureResource& resource, VkComponentMapping components) { std::lock_guard lock(targetsMutex()); const auto format = ResolveTextureFormat(resource.format); @@ -684,7 +703,9 @@ std::shared_ptr MultisampleTargetTexture(const Context& context, const if (!target->active->load(std::memory_order_acquire)) continue; if (target->context.device != context.device || target->target.surfaceAddress != resource.baseAddress) continue; const auto& rendered = target->target; - if (rendered.format == format && rendered.extent.width == resource.width && rendered.extent.height == resource.height && rendered.samples == resource.samples && rendered.mip == 0) return target->Sampled(components); + const bool sameShape = rendered.extent.width == resource.width && rendered.extent.height == resource.height && rendered.samples == resource.samples && rendered.mip == 0; + if (sameShape && rendered.format == format) return target->Sampled(components); + if (sameShape && redBlueSwapped(rendered.format, format)) return target->Sampled(swapRedBlue(components)); other = target.get(); } if (other == nullptr) { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp index e165888e8c..3aca0313d7 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp @@ -57,6 +57,8 @@ constexpr std::uint32_t MultisampledAttrib = (SampleShift << 12u) | (SampleShift constexpr std::uint32_t TiledAttrib3 = 0x4dc6c000u; constexpr std::uint32_t Float32Export = 9; constexpr std::uint32_t Format16161616Float = 71; +constexpr std::uint32_t Unorm8x4SwappedInfo = 0x8828u; +constexpr std::uint32_t Format8888Unorm = 56; constexpr std::uint32_t Type2DMsaa = 14; constexpr std::uint32_t TileRenderTarget = 0x1b; constexpr std::uint32_t Threads = 32; @@ -146,6 +148,31 @@ AgcDriver::Graphics::ColorTarget DecodeTarget(const Block& block) { return color; } +AgcDriver::Graphics::ColorTarget DecodeSwappedTarget(const Block& block) { + AgcDriver::Registers cx; + cx[0x318] = static_cast(block.Address() >> 8u); + cx[0x31b] = 0; + cx[0x31c] = Unorm8x4SwappedInfo; + cx[0x31d] = MultisampledAttrib; + cx[0x3b0] = ((Side - 1u) << 14u) | (Side - 1u); + cx[0x3b8] = TiledAttrib3; + cx[0x390] = static_cast(block.Address() >> 40u); + const auto color = AgcDriver::Graphics::DecodeColorBuffer(cx, 0); + Require(color.format == VK_FORMAT_B8G8R8A8_UNORM && color.samples == Samples && color.extent.width == Side && color.extent.height == Side, "the swapped 8_8_8_8 target did not decode as a 16x16 BGRA8 surface of 4 samples"); + return color; +} + +void CheckSwappedSamples(const std::vector>& loaded) { + const std::array swapped{Drawn[2], Drawn[1], Drawn[0], Drawn[3]}; + std::size_t drawn = 0; + for (const auto& texel : loaded) { + if (texel == std::array{}) continue; + for (std::uint32_t channel = 0; channel < 4u; ++channel) Require(std::abs(texel[channel] - swapped[channel]) <= 0.5f / 255.0f, "a sample of a BGRA8 target loaded through an RGBA8 texture does not hold the drawn color with red and blue swapped"); + ++drawn; + } + Require(drawn != 0, "no sample of the BGRA8 target holds the drawn color"); +} + AgcDriver::Graphics::State TargetState(const AgcDriver::Graphics::ColorTarget& color) { VkPipelineColorBlendAttachmentState blend{}; blend.colorWriteMask = 0xf; @@ -211,11 +238,11 @@ void Draw(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::State& sta device.Draw(state, draw, shaders); } -std::array TargetDescriptor(const Block& block, std::uint32_t sampleShift) { +std::array TargetDescriptor(const Block& block, std::uint32_t sampleShift, std::uint32_t format = Format16161616Float) { const auto address = block.Address(); return { static_cast(address >> 8u), - static_cast((address >> 40u) & 0xffu) | (Format16161616Float << 20u) | (((Side - 1u) & 3u) << 30u), + static_cast((address >> 40u) & 0xffu) | (format << 20u) | (((Side - 1u) & 3u) << 30u), ((Side - 1u) >> 2u) | ((Side - 1u) << 14u), 0xfacu | (sampleShift << 16u) | (TileRenderTarget << 20u) | (Type2DMsaa << 28u), 0u, @@ -300,9 +327,9 @@ void RequireStoreRefused(AgcDriver::VulkanDevice& device, const std::array> LoadSamples(AgcDriver::VulkanDevice& device, const Block& block) { +std::vector> LoadSamples(AgcDriver::VulkanDevice& device, const Block& block, std::uint32_t format = Format16161616Float) { std::vector userData; - const auto program = CompileLoad(device, TargetDescriptor(block, SampleShift), userData); + const auto program = CompileLoad(device, TargetDescriptor(block, SampleShift, format), userData); std::vector> loaded(Side * Side * Samples); for (std::uint32_t first = 0; first < loaded.size(); first += Threads) { Buffer.fill(0xdeadbeefu); @@ -617,6 +644,10 @@ int main(int argc, char** argv) { RequireStoreRefused(*device, TargetDescriptor(mixedStore, SampleShift), "writing the 16x16 multisampled texture", "storing to a multisampled texture no draw rendered to whose color memory is not uniform"); std::puts("stores to one sample of each pixel of a drawn 4x target and to samples of zeroed color memory no draw rendered to reach exactly those samples, and a store to non-uniform color memory no draw rendered to is refused"); std::puts("every sample of a 4x color target loaded through its multisampled texture descriptor matches the triangle drawn into it, then the fill of its color memory; a surface no draw rendered to loads the value its color memory repeats and keeps it under a later draw"); + Block swapped(4u * BlockBytes); + Draw(*device, TargetState(DecodeSwappedTarget(swapped))); + CheckSwappedSamples(LoadSamples(*device, swapped, Format8888Unorm)); + std::puts("a 4x BGRA8 color target loaded through an RGBA8 texture descriptor reads its drawn color with red and blue swapped"); Block destination(BlockBytes); struct ReleaseIndexDevice { std::unique_ptr& device; From 6f7ec9b1432d1929430a07312e73a6061cf79e29 Mon Sep 17 00:00:00 2001 From: klosixgaming-arch Date: Sat, 17 Oct 2026 14:53:58 -0500 Subject: [PATCH 0851/1000] docs(playgo): PPSA03490 needs APS5_PLAYGO_UNLISTED_CHUNKS=absent PPSA03490 (Demon Slayer: Kimetsu no Yaiba - The Hinokami Chronicles) has no playgo-* files in sce_sys. Its startup code asks scePlayGoGetLocus about chunk IDs 0, 1, 2, ... and stops when it gets SCE_PLAYGO_ERROR_BAD_CHUNK_ID, appending each ID to an array. With APS5_PLAYGO_UNLISTED_CHUNKS=installed no ID ever fails, the array keeps growing and startup aborts with 'Ran out of memory allocating 5565054976 bytes with alignment 65536'. With =absent the walk ends and the title gets past its logos to the menu and intro. AI-assisted: yes. Written with AI assistance and reviewed by me. --- docs/user/USAGE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index 15a4f6d625..a26aaa7464 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -164,7 +164,7 @@ When a dump has neither `sce_sys/playgo-chunk.dat` nor `sce_sys/playgo-scenario. | Value | Answer for an unlisted chunk ID | Observed title that needs it | |-------|---------------------------------|------------------------------| | `installed` | Report it as installed (`LOCAL_FAST`). | High on Life (PPSA15388) needs this to load its saves. | -| `absent` | Return `SCE_PLAYGO_ERROR_BAD_CHUNK_ID`. | Sifu (PPSA03001) uses this result to end its chunk-ID search. | +| `absent` | Return `SCE_PLAYGO_ERROR_BAD_CHUNK_ID`. | Sifu (PPSA03001) and Demon Slayer: Kimetsu no Yaiba - The Hinokami Chronicles (PPSA03490) use this result to end their chunk-ID search. With `installed`, PPSA03490 keeps asking about higher chunk IDs forever, so its chunk list grows until startup fails with a `Ran out of memory allocating 5565054976 bytes` error. | Leave the variable unset to keep the explicit error. Any other value also stops with an error. Dumps that include PlayGo metadata are unaffected. These title-specific results come from emulator runs and have not been verified against a console. From 7b45d1034eb74ccbe4dea7e9c39fb17c984d9247 Mon Sep 17 00:00:00 2001 From: thuanquang Date: Sun, 18 Oct 2026 03:14:28 +0700 Subject: [PATCH 0852/1000] fix(shader): run buffer atomics through a V# off a DWORD boundary The binding plan refused every buffer atomic through a V# whose base is not a multiple of 4. On RDNA2 such an atomic works when base + offset is on a DWORD boundary and stops the dispatch otherwise, which the runtime buffer atomic already does, so the refusal is dropped. Co-Authored-By: Claude Opus 5.5 --- .../libSceAgcDriver/tests/ShaderDiskCache.cpp | 3 +- .../tests/execution/BufferUnalignedBase.cpp | 44 +++++++++++++------ .../src/DescriptorBindingBuilder.cpp | 1 - docs/dev/debt/shader.md | 2 +- 4 files changed, 33 insertions(+), 17 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp index 043759d826..ace83dd50d 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp @@ -1196,7 +1196,8 @@ void verifyBindingPlanSelection() { throw std::runtime_error("binding plan accepted an unsupported buffer alignment"); }; snapshot.buffers[0].dwords[0] |= 2u; - rejectsAlignment("buffer atomic on a V# whose base is not DWORD aligned"); + const auto atomicPlan = builder.Prepare(compiled.layout, info, IrShaderStage::Compute, snapshot); + require(std::ranges::any_of(atomicPlan.specialization, [](const PipelineSpecializationConstant& constant) { return constant.id == PipelineSpecialization::BufferBase + 3u && constant.value == 2u; }), "a buffer atomic V# off a DWORD boundary did not record its misalignment"); snapshot.buffers[0].dwords[0] &= ~3u; info.buffers[1].typedAlignment = 2u; snapshot.buffers[1].dwords[0] |= 2u; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/BufferUnalignedBase.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/BufferUnalignedBase.cpp index bbd38f1f91..c3520e8346 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/BufferUnalignedBase.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/BufferUnalignedBase.cpp @@ -42,7 +42,10 @@ alignas(256) constexpr std::array PairCode{ 0xe0741000, 0x80020302, 0xbf810000, }; -alignas(256) constexpr std::array AtomicCode{0xe0c81000, 0x80000100, 0xbf810000}; +alignas(256) constexpr std::array AtomicCode{ + 0x34040082, 0x4a040408, 0x4a020081, 0xe0c85000, 0x80000102, 0xbf8c3f70, 0x34060082, 0xe0701000, + 0x80010103, 0xbf810000, +}; constexpr std::array FormattedLoadCode(std::uint32_t format, std::uint32_t offset) { return {0x34020082, 0xe8001000u | (format << 19u) | offset, 0x80000201, 0xbf8c0000, 0xe0701000, 0x80010201, 0xbf810000}; @@ -218,20 +221,32 @@ void RunFormattedStore(AgcDriver::VulkanDevice& device, std::uint32_t viewOffset } } -void RunAtomic(AgcDriver::VulkanDevice& device) { +void DispatchAtomic(AgcDriver::VulkanDevice& device, std::uint32_t misalignment, std::uint32_t addend) { + Output.fill(Fill); std::vector userData; - const auto descriptor = BufferDescriptor(reinterpret_cast(Output.data()) + 2u, 64u); - userData.insert(userData.end(), descriptor.begin(), descriptor.end()); - const std::array memory{{{reinterpret_cast(AtomicCode.data()), std::as_bytes(std::span(AtomicCode))}}}; - try { - const auto result = ShaderRecompiler::Recompile(Request(AtomicCode, userData, device.Target(), memory)); - device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(AtomicCode.data())); - device.WaitIdle(); - } catch (const std::exception& error) { - Require(std::string(error.what()).find("buffer atomic on a V# whose base is not DWORD aligned") != std::string::npos, std::string("buffer unaligned base atomic: unexpected error: ") + error.what()); - return; + for (const auto& descriptor : {BufferDescriptor(reinterpret_cast(Output.data()) + misalignment, (Lanes + 2u) * 4u), BufferDescriptor(Output.data() + 128, Lanes * 4u)}) { + userData.insert(userData.end(), descriptor.begin(), descriptor.end()); } - throw std::runtime_error("buffer unaligned base atomic: an atomic on a V# off a DWORD boundary ran"); + userData.push_back(addend); + const std::array memory{{{reinterpret_cast(AtomicCode.data()), std::as_bytes(std::span(AtomicCode))}}}; + const auto result = ShaderRecompiler::Recompile(Request(AtomicCode, userData, device.Target(), memory)); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(AtomicCode.data())); + device.WaitIdle(); +} + +void RunAtomic(AgcDriver::VulkanDevice& device, std::uint32_t misalignment) { + DispatchAtomic(device, misalignment, 4u - misalignment); + const auto run = "buffer unaligned base atomic, view +" + std::to_string(misalignment) + ", offset lane * 4 + " + std::to_string(4u - misalignment); + for (std::uint32_t word = 0; word < 128u; ++word) { + const auto expected = word >= 1u && word <= Lanes ? Fill + word : Fill; + Require(Output[word] == expected, run + ": dword " + std::to_string(word) + " is " + Hex(Output[word]) + ", expected " + Hex(expected)); + } + for (std::uint32_t lane = 0; lane < Lanes; ++lane) Require(Output[128u + lane] == Fill, run + ": lane " + std::to_string(lane) + " returned " + Hex(Output[128u + lane]) + ", expected " + Hex(Fill)); +} + +void RunAtomicFault(AgcDriver::VulkanDevice& device) { + DispatchAtomic(device, 2u, 0u); + for (std::uint32_t word = 0; word < Output.size(); ++word) Require(Output[word] == Fill, "buffer unaligned base atomic, view +2, offset lane * 4: dword " + std::to_string(word) + " is " + Hex(Output[word]) + ", but an atomic off a DWORD boundary faults the wave on RDNA2"); } } @@ -246,7 +261,7 @@ int main() { RunPair(*device, 1); RunPair(*device, 2); RunPair(*device, 3); - RunAtomic(*device); + for (const auto view : {1u, 2u, 3u}) RunAtomic(*device, view); RunFormattedLoad(*device, 2); RunFormattedStore(*device, 2); for (const auto view : {1u, 3u}) { @@ -257,6 +272,7 @@ int main() { RunFormattedFault(*device, "tbuffer_load_format_x 32_UINT", Load32Code, view); RunFormattedFault(*device, "tbuffer_load_format_x 16_16_UINT", Load16_16Code, view); } + RunAtomicFault(*device); std::puts("buffer unaligned base tests passed"); return 0; } catch (const std::exception& error) { diff --git a/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp b/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp index fb6e05b76f..b0a2147704 100644 --- a/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp +++ b/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp @@ -313,7 +313,6 @@ DescriptorBindingPlan DescriptorBindingBuilder::Prepare(const IrBindingLayout& l const bool present = descriptor.dwords[2] != 0u && (descriptor.dwords[0] != 0u || (descriptor.dwords[1] & 0xffffu) != 0u); if (buffer.descriptorFormatted && (format > static_cast(IrBufferFormat::Format32_32_32_32Float) || (present && format == 0u))) fail("buffer specialization has an unsupported format"); const auto misalignment = present ? descriptor.dwords[0] & 3u : 0u; - if (buffer.atomic && misalignment != 0u) fail("buffer atomic on a V# whose base is not DWORD aligned"); auto alignment = static_cast(buffer.typedAlignment); if (buffer.descriptorFormatted && format != 0u) alignment = std::max(alignment, std::min(GetFormatInfo(static_cast(format)).byteSize, 4u)); if (alignment == 0u || misalignment % alignment != 0u) fail("is a formatted buffer access through a V# whose base is not aligned to its element"); diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index 89fa8f70d7..683e5846bd 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -131,7 +131,7 @@ - [64-bit LDS atomics](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (`ds_*_u64`, `ds_*_b64`, `ds_*_f64`) read and write the two dwords under a workgroup lock stored after the guest LDS, as LDS is a 32-bit array without 64-bit atomics. On RDNA2 (gfx1036) the LDS serialises a 32-bit atomic on a slot's high dword and a 64-bit atomic on that slot: 128 lanes of `ds_add_u32` and 128 lanes of `ds_add_u64` on one slot sum exactly. Only that pair was measured. Every 32-bit LDS atomic of a program that contains a 64-bit LDS atomic therefore takes the same lock, including a 32-bit `flat_atomic_*` whose address lands in the LDS aperture (a 64-bit one takes it already); the flat forms were not measured against a 64-bit atomic either (the other 32-bit LDS atomics, `ds_min/max`, `ds_cmpst`, the float forms, `ds_inc/dec`, `ds_mskor` and `ds_wrxchg`, are extrapolated from `ds_add_u32` and were not measured against a 64-bit atomic); a program without a 64-bit LDS atomic keeps its native 32-bit atomics. `ds_condxchg32_rtn_b64` is two `ds_mskor` halves, each under the lock but not atomic as a pair, so a 64-bit atomic between the two halves is not excluded. Sub-dword writes (`ds_write_b8`, `ds_write_b16` and their `_d16_hi` forms) and plain `ds_read_*`, `ds_write_*`, `ds_append_u32` and `ds_consume_u32` are not taken under the lock, so a plain access racing a 64-bit atomic on the same dwords can see or overwrite one half of it; whether the hardware orders them was not measured. The lock serialises the workgroup's LDS atomics of such a program; the throughput cost was not measured. `VK_KHR_workgroup_memory_explicit_layout` with `shaderSharedInt64Atomics` would make both widths native and is not used. A 64-bit atomic at an address that is 4-byte but not 8-byte aligned faults the wave on gfx1036, as on gfx1035. - [LDS float atomics](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (`ds_min/max/cmpst_f32`, `ds_min/max/cmpst_f64` and their `_rtn` forms) follow the hardware measured on RDNA2 (gfx1036) over 14 x 14 operand sets (zeros, ones, infinities, denormals, quiet and signalling NaNs of both signs and with payloads) in IEEE 0 and 1 with every value 0-3 of the type's denormal field: both operands are flushed to a signed zero when bit 0 of the field is clear (0 and 2; 1 and 3 keep them; the f32 field for the f32 forms, the f16/f64 field for the f64 forms; a shader without a known float mode flushes f32 and keeps f64), then a signalling-NaN data operand is stored quieted, else a signalling-NaN old value is stored quieted, else a quiet-NaN old value is replaced by the data, else a quiet-NaN data operand loses, else the ordered compare with -0 below +0 decides, and the flushed value is what is written, also when the old value wins. `cmpst` compares the flushed bits (both zeros equal, NaN never equal) and leaves memory unchanged when they differ. The returned value is always the old value as it was in memory. `IEEE_MODE` has no effect. The same bit rule serves the buffer float atomics (`AtomicFloatBits`). Not measured: a denormal or signalling-NaN `data1` of `cmpst` (stored as given). `ds_add_f32` and `ds_add_rtn_f32` follow RDNA1 (gfx1010, RX 5700 XT), measured with the [hardware oracle](../HW_ORACLE.md) template run through the Windows driver's comgr and HIP runtime over 1574 operand pairs (signed zeros, denormals, the smallest normals, infinities, quiet and signalling NaNs with payloads, the largest normals, random pairs near the denormal range and pairs that nearly cancel) in all 16 combinations of the f32 denormal field and the f32 round mode, and at IEEE 1 and DX10 clamp 0: both operands are flushed to a signed zero when bit 0 of the field is clear and the result when bit 1 is clear, the sum is rounded to nearest even whatever the round mode, a NaN data operand is stored quieted, else a NaN old value is stored quieted, `+inf + -inf` stores 0xffc00000, and `IEEE_MODE` and `DX10_CLAMP` have no effect. The sum is the host's `OpFAdd` between those flushes, so where the field keeps f32 denormals the result also depends on the host preserving them, which the module does not request. thuanquang measured the same rule on RDNA2 (gfx1031, RX 6700 XT) in #5492, with the same operand classes and 18 modes in wave32 and wave64; the PS5's gfx1013 is not measured. - [LDS in pixel, vertex-path and fetch programs](../../../core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp) (shader recompiler) - is an array private to each host invocation, in `Function` storage, as only compute and mesh programs get workgroup memory; on the console one LDS region is shared by the wave (RDNA 2 ISA sections 3.6.6 and 10.4), so a lane does not see another lane's LDS writes. Sub-dword writes (`ds_write_b8`, `ds_write_b16` and their `_d16_hi` forms), the 32-bit LDS atomics, `ds_append` and `ds_consume` load, update and store the invocation's own dword without an atomic, as Vulkan allows atomics only on `Uniform`, `Workgroup`, `Image`, `StorageBuffer`, `PhysicalStorageBuffer` and `TaskPayloadWorkgroupEXT` pointers (VUID-StandaloneSpirv-None-04686). `ds_append` and `ds_consume` update only the first active lane's copy and return its old value to every lane, so a second append after the first active lane changes, or a read of the counter by another lane, differs from the hardware. Not measured on hardware. -- [V# bases off a DWORD boundary](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (shader recompiler) - raw accesses use the exact bytes and `s_buffer_load` reads `(base & ~3) + (offset & ~3)`, as measured on gfx1035 (Linux, alignment mode UNALIGNED; the console's mode is unknown). Formatted accesses off their element's alignment and atomics throw. +- [V# bases off a DWORD boundary](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (shader recompiler) - raw accesses use the exact bytes and `s_buffer_load` reads `(base & ~3) + (offset & ~3)`, as measured on gfx1035 (Linux, alignment mode UNALIGNED; the console's mode is unknown). Formatted accesses off their element's alignment throw. Atomics use the byte address `base + offset`, base's low two bits included: measured on RDNA2 (gfx1031, Radeon RX 6700 XT, AMD Windows driver 32.0.21045.5002 through HIP) with `buffer_atomic_add`, `buffer_atomic_swap` and `buffer_atomic_cmpswap` and bases 0 to 3 bytes past a DWORD boundary with offsets 0 to 7, an address on a DWORD boundary updates that DWORD and returns its old value, and any other address stops the dispatch, which the recompiler reports as an unaligned access fault. - [32-bit bitfield masks](../../../core/shader/recompiler/Translation/src/IntegerInstructions.cpp) (`S_BFM_B32`, `V_BFM_B32`) use the truncating 32-bit shift specified by the [RDNA 1.0 ISA](https://www.amd.com/content/dam/amd/en/documents/radeon-tech-docs/instruction-set-architectures/rdna-shader-instruction-set-architecture.pdf), sections 12.1 (opcode 36) and 12.12 (opcode 867). Width and offset use their low five bits. This matches gfx1035 (Linux) for every width/offset pair, with and without upper operand bits set, and both leave SCC unchanged; not measured on the console. - [Vector buffer offsets that are not a multiple of 4](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (shader recompiler) - dword accesses count whole dwords from the V# base, where the hardware reads the exact bytes at base + offset. - [Raw `buffer_load_dwordx2`](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) is one 64-bit access on the hardware (with or without glc), so it never sees half of a `buffer_atomic_swap_x2`. Only a glc load that is 8-byte aligned and in bounds, in a program that has the 64-bit storage view (it uses 64-bit buffer atomics), is read with one 64-bit atomic load; every other dwordx2 load is two dword loads and can tear against a concurrent 64-bit write: loads without glc, and loads in a program without 64-bit buffer atomics while another shader writes the pair with `swap_x2`. From a352941e771899df880d69bc22b3019e51155918 Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Sat, 17 Oct 2026 18:51:43 +0000 Subject: [PATCH 0853/1000] fix(libkernel): release flexible accounting after fixed mmap --- .../libkernel/DirectMemory/DirectMemory.hpp | 3 +- .../prx/libkernel/DirectMemory/Export.cpp | 4 ++ .../libkernel/DirectMemory/PosixMemory.cpp | 2 + core/libs/tests/GuestMemory.cpp | 56 +++++++++++++++++++ 4 files changed, 64 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libkernel/DirectMemory/DirectMemory.hpp b/core/libs/prx/libkernel/DirectMemory/DirectMemory.hpp index a17307030f..3178ca0ca9 100644 --- a/core/libs/prx/libkernel/DirectMemory/DirectMemory.hpp +++ b/core/libs/prx/libkernel/DirectMemory/DirectMemory.hpp @@ -37,6 +37,7 @@ size_t DirectMemoryFreeRun(uint64_t offset, uint64_t limit); int DoMapDirect(void** addr, size_t len, int prot, int flags, int64_t physStart, size_t alignment); int DoMapAnon(void** addr, size_t len, int prot, int flags, size_t alignment = PS5_PAGE_SIZE, bool exclusive = false); int DoMapFileBacked_nid_no_patch(void** addr, size_t len, int prot, int flags, int descriptor, std::uint64_t offset, size_t alignment); +void ReleaseFlexibleMappingRange(std::uintptr_t address, size_t len); int DoMapShared(void** addr, size_t len, int prot, HostSharedMemory object); int DoMprotect(const void* addr, size_t len, int prot); int DoMtypeprotect(const void* addr, size_t len, int type, int prot); @@ -49,4 +50,4 @@ void ForgetJitSharedMemory(int descriptor); void DuplicateJitSharedMemory(int from, int to); void GuestPageTableUsage(std::uint64_t granularity, std::uint64_t* cpu_entries, std::uint64_t* gpu_entries); -#endif \ No newline at end of file +#endif diff --git a/core/libs/prx/libkernel/DirectMemory/Export.cpp b/core/libs/prx/libkernel/DirectMemory/Export.cpp index 8533a87c32..ebf8d8a813 100644 --- a/core/libs/prx/libkernel/DirectMemory/Export.cpp +++ b/core/libs/prx/libkernel/DirectMemory/Export.cpp @@ -168,6 +168,10 @@ void ForgetLockedRange(uintptr_t start, uintptr_t end) { } +void ReleaseFlexibleMappingRange(std::uintptr_t address, size_t len) { + _releaseFlexible(address, len); +} + extern "C" { int APS5_VABI sceKernelAllocateDirectMemory(int64_t search_start, int64_t search_end, size_t len, size_t alignment, int memory_type, int64_t* phys_addr_out) { diff --git a/core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp b/core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp index 250d252b47..377f252298 100644 --- a/core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp +++ b/core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp @@ -119,6 +119,7 @@ void* APS5_VABI mmap_nid_postfix(void* address, std::size_t length, int protecti try { const auto result = DoMapAnon(&mapped, rounded, protection, placement, alignment, exclusive); if (result != 0) return failed(GuestNoMemory); + if (fixedAnonymous) ReleaseFlexibleMappingRange(start, rounded); return mapped; } catch (const std::invalid_argument&) { return failed(fixedAnonymous ? GuestInvalid : GuestNoMemory); @@ -160,6 +161,7 @@ void* APS5_VABI mmap_nid_postfix(void* address, std::size_t length, int protecti mapped = address; const auto result = DoMapFileBacked_nid_no_patch(&mapped, rounded, protection, kind | placement, mappingDescriptor, static_cast(offset), alignment); if (result != 0) return failed(result == SCE_KERNEL_ERROR_EINVAL ? GuestInvalid : GuestNoMemory); + if ((placement & GuestMapFixed) != 0) ReleaseFlexibleMappingRange(reinterpret_cast(mapped), rounded); return mapped; } catch (const std::invalid_argument&) { return failed(GuestInvalid); diff --git a/core/libs/tests/GuestMemory.cpp b/core/libs/tests/GuestMemory.cpp index 368b25dfd3..e2710ec5c3 100644 --- a/core/libs/tests/GuestMemory.cpp +++ b/core/libs/tests/GuestMemory.cpp @@ -2535,6 +2535,58 @@ static void CheckPosixFixedAnonymousMapping() { Require(munmap_nid_postfix(memory, page * 4) == 0); } +static void CheckPosixFixedAnonymousReleasesFlexibleAccounting() { + constexpr std::size_t page = 0x4000; + constexpr int fixed = 0x1012; + std::size_t before = 0; + std::size_t available = 0; + Require(sceKernelAvailableFlexibleMemorySize(&before) == 0); + void* base = nullptr; + Require(sceKernelMapFlexibleMemory(&base, page * 3, 3, 0) == 0 && base != nullptr); + Require(sceKernelAvailableFlexibleMemorySize(&available) == 0 && available == before - page * 3); + auto* bytes = static_cast(base); + bytes[0] = 0x31; + bytes[page] = 0x41; + bytes[page * 2] = 0x51; + Require(mmap_nid_postfix(bytes + page, page, 3, fixed, -1, 0) == bytes + page); + Require(bytes[0] == 0x31 && bytes[page] == 0 && bytes[page * 2] == 0x51); + Require(sceKernelAvailableFlexibleMemorySize(&available) == 0 && available == before - page * 2); + Require(sceKernelMunmap(base, page * 3) == 0); + Require(sceKernelAvailableFlexibleMemorySize(&available) == 0 && available == before); +} + +#if defined(__linux__) +static void CheckPosixFixedFileReleasesFlexibleAccounting() { + constexpr std::size_t page = 0x4000; + constexpr int fixedShared = 0x11; + const auto path = std::filesystem::path("anyps5-fixed-file-accounting-" + + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()) + ".bin"); + { + std::ofstream stream(path, std::ios::binary); + std::vector data(page, 0x6d); + stream.write(data.data(), static_cast(data.size())); + } + const int fd = sceKernelOpen(path.string().c_str(), SCE_KERNEL_O_RDONLY, 0); + Require(fd >= 0); + std::size_t before = 0; + std::size_t available = 0; + Require(sceKernelAvailableFlexibleMemorySize(&before) == 0); + void* base = nullptr; + Require(sceKernelMapFlexibleMemory(&base, page * 3, 3, 0) == 0 && base != nullptr); + Require(sceKernelAvailableFlexibleMemorySize(&available) == 0 && available == before - page * 3); + auto* bytes = static_cast(base); + bytes[0] = 0x31; + bytes[page * 2] = 0x51; + Require(mmap_nid_postfix(bytes + page, page, 1, fixedShared, fd, 0) == bytes + page); + Require(bytes[0] == 0x31 && bytes[page] == 0x6d && bytes[page * 2] == 0x51); + Require(sceKernelAvailableFlexibleMemorySize(&available) == 0 && available == before - page * 2); + Require(sceKernelMunmap(base, page * 3) == 0); + Require(sceKernelAvailableFlexibleMemorySize(&available) == 0 && available == before); + Require(sceKernelClose(fd) == 0); + std::filesystem::remove(path); +} +#endif + static void CheckSceKernelMmap() { constexpr std::size_t page = 0x4000; void* mapped = nullptr; @@ -2634,6 +2686,10 @@ int main(int argc, char** argv) { #endif if (argc == 2 && std::strcmp(argv[1], "fixed-anonymous") == 0) { CheckPosixFixedAnonymousMapping(); + CheckPosixFixedAnonymousReleasesFlexibleAccounting(); +#if defined(__linux__) + CheckPosixFixedFileReleasesFlexibleAccounting(); +#endif return 0; } Require(argc == 1); From 162f4af40b3cc8234b8fe9d4a0ac3b6cac9f8ba1 Mon Sep 17 00:00:00 2001 From: milescoviello Date: Wed, 14 Oct 2026 19:34:39 +0000 Subject: [PATCH 0854/1000] fix(libkernel): export sysarch so modules that import it load The Wine port (PPSA99995) ships an ntdll module that imports sysarch (b7uXQmnfB2s). libkernel did not export it, so loading stopped with "undefined symbol: b7uXQmnfB2s" before the title ran. Export sysarch as a stub that reports that it is not implemented: the title loads and runs its launcher without calling it. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libkernel/CMakeLists.txt | 10 ++++++++++ core/libs/prx/libkernel/Unimplemented.cpp | 5 +++++ core/libs/tests/GuestSysarch.cpp | 21 +++++++++++++++++++++ 3 files changed, 36 insertions(+) create mode 100644 core/libs/tests/GuestSysarch.cpp diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 906fcf9cbf..5ea617fe76 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -1026,6 +1026,16 @@ if(BUILD_TESTING) set_tests_properties(guest_getrlimit PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_sysarch_tests ${LIBS_INCLUDE_DIR}/tests/GuestSysarch.cpp) + target_include_directories(guest_sysarch_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_sysarch_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_sysarch_tests) + add_test(NAME guest_sysarch COMMAND guest_sysarch_tests) + set_tests_properties(guest_sysarch PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_sysarch PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_setrlimit_tests ${LIBS_INCLUDE_DIR}/tests/GuestSetrlimit.cpp) target_include_directories(guest_setrlimit_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_setrlimit_tests PRIVATE libkernel libc) diff --git a/core/libs/prx/libkernel/Unimplemented.cpp b/core/libs/prx/libkernel/Unimplemented.cpp index fbb3d6935a..7e0df91b86 100644 --- a/core/libs/prx/libkernel/Unimplemented.cpp +++ b/core/libs/prx/libkernel/Unimplemented.cpp @@ -10,6 +10,11 @@ int APS5_VABI __tls_get_addr_nid_postfix(void) { return 0; } +int APS5_VABI sysarch(int, void*) { + NotImplemented_nid_no_patch("sysarch"); + return 0; +} + APS5_EXPORT("AC1FtjqMCL0", sceKernelUnknown03); int APS5_VABI sceKernelUnknown03(void) { NotImplemented_nid_no_patch("AC1FtjqMCL0"); diff --git a/core/libs/tests/GuestSysarch.cpp b/core/libs/tests/GuestSysarch.cpp new file mode 100644 index 0000000000..82397b9880 --- /dev/null +++ b/core/libs/tests/GuestSysarch.cpp @@ -0,0 +1,21 @@ +#include "prx/libc/include/general/VabiMacros.hpp" + +#include +#include +#include + +extern "C" { +int APS5_VABI sysarch(int number, void* args); +} + +int main() { + try { + sysarch(129, nullptr); + } catch (const std::runtime_error& error) { + if (std::string(error.what()) == "sysarch not implemented") return 0; + std::fprintf(stderr, "sysarch threw: %s\n", error.what()); + return 1; + } + std::fprintf(stderr, "sysarch returned instead of reporting that it is not implemented\n"); + return 1; +} From d7c83db2a5b7007ee3292df5faa3625d23fecf04 Mon Sep 17 00:00:00 2001 From: milescoviello Date: Sat, 17 Oct 2026 11:13:00 +0000 Subject: [PATCH 0855/1000] feat(libkernel): read and set the GS base through sysarch sysarch's AMD64_GET_FSBASE, AMD64_GET_GSBASE and AMD64_SET_GSBASE now follow FreeBSD 11's amd64 sysarch on Linux x86-64 through arch_prctl: the GS base is the calling thread's, a base at or above VM_MAXUSER_ADDRESS returns EINVAL and a null argument EFAULT. AMD64_SET_FSBASE, whose base guest and host code share for their TLS, other operations and other hosts still throw. Wine sets its TEB this way. Co-Authored-By: Claude Opus 5.5 --- .../libs/prx/libkernel/System/src/Process.cpp | 37 +++++++++++++ core/libs/prx/libkernel/Unimplemented.cpp | 5 -- core/libs/tests/GuestSysarch.cpp | 54 +++++++++++++++---- docs/dev/debt/libkernel.md | 1 + 4 files changed, 82 insertions(+), 15 deletions(-) diff --git a/core/libs/prx/libkernel/System/src/Process.cpp b/core/libs/prx/libkernel/System/src/Process.cpp index 01c4a2b11d..1bca3dd663 100644 --- a/core/libs/prx/libkernel/System/src/Process.cpp +++ b/core/libs/prx/libkernel/System/src/Process.cpp @@ -9,6 +9,10 @@ #include #include #endif +#if defined(__linux__) && defined(__x86_64__) +#include +#include +#endif #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" @@ -465,6 +469,39 @@ int APS5_VABI sched_get_priority_min_nid_postfix(int policy) { return 256; } +int APS5_VABI sysarch(int operation, void* parameters) { + constexpr int getFsBase = 128; + constexpr int getGsBase = 130; + constexpr int setGsBase = 131; + constexpr std::uint64_t userAddressEnd = 0x0000800000000000ull; + if (operation != getFsBase && operation != getGsBase && operation != setGsBase) + NotImplemented_nid_no_patch(("sysarch: operation " + std::to_string(operation)).c_str()); +#if defined(__linux__) && defined(__x86_64__) + if (parameters == nullptr) { + *__error_nid_postfix() = guestFault; + return -1; + } + std::uint64_t base = 0; + if (operation == setGsBase) { + std::memcpy(&base, parameters, sizeof(base)); + if (base >= userAddressEnd) { + *__error_nid_postfix() = guestInvalid; + return -1; + } + if (::syscall(SYS_arch_prctl, ARCH_SET_GS, base) != 0) throw std::system_error(errno, std::generic_category(), "sysarch: setting the GS base"); + return 0; + } + if (::syscall(SYS_arch_prctl, operation == getFsBase ? ARCH_GET_FS : ARCH_GET_GS, &base) != 0) + throw std::system_error(errno, std::generic_category(), "sysarch: reading a segment base"); + std::memcpy(parameters, &base, sizeof(base)); + return 0; +#else + static_cast(parameters); + NotImplemented_nid_no_patch("sysarch: segment bases on this host"); + return 0; +#endif +} + int APS5_VABI getrusage_nid_postfix(int who, GuestResourceUsage* usage) { if (who != -1 && who != 0 && who != 1) { *__error_nid_postfix() = guestInvalid; diff --git a/core/libs/prx/libkernel/Unimplemented.cpp b/core/libs/prx/libkernel/Unimplemented.cpp index 7e0df91b86..fbb3d6935a 100644 --- a/core/libs/prx/libkernel/Unimplemented.cpp +++ b/core/libs/prx/libkernel/Unimplemented.cpp @@ -10,11 +10,6 @@ int APS5_VABI __tls_get_addr_nid_postfix(void) { return 0; } -int APS5_VABI sysarch(int, void*) { - NotImplemented_nid_no_patch("sysarch"); - return 0; -} - APS5_EXPORT("AC1FtjqMCL0", sceKernelUnknown03); int APS5_VABI sceKernelUnknown03(void) { NotImplemented_nid_no_patch("AC1FtjqMCL0"); diff --git a/core/libs/tests/GuestSysarch.cpp b/core/libs/tests/GuestSysarch.cpp index 82397b9880..7167b5003f 100644 --- a/core/libs/tests/GuestSysarch.cpp +++ b/core/libs/tests/GuestSysarch.cpp @@ -1,21 +1,55 @@ #include "prx/libc/include/general/VabiMacros.hpp" -#include +#include +#include #include #include extern "C" { int APS5_VABI sysarch(int number, void* args); +int* APS5_VABI __error_nid_postfix(); +} + +static void Require(bool value) { if (!value) std::abort(); } + +template static std::string Thrown(TCall call) { + try { + call(); + } catch (const std::runtime_error& error) { + return error.what(); + } + return {}; } int main() { - try { - sysarch(129, nullptr); - } catch (const std::runtime_error& error) { - if (std::string(error.what()) == "sysarch not implemented") return 0; - std::fprintf(stderr, "sysarch threw: %s\n", error.what()); - return 1; - } - std::fprintf(stderr, "sysarch returned instead of reporting that it is not implemented\n"); - return 1; + Require(Thrown([] { sysarch(129, nullptr); }) == "sysarch: operation 129 not implemented"); + Require(Thrown([] { sysarch(7, nullptr); }) == "sysarch: operation 7 not implemented"); +#if defined(__linux__) && defined(__x86_64__) + static std::uint64_t slots[2] = {0x1234567890abcdefull, 0}; + std::uint64_t base = reinterpret_cast(slots); + Require(sysarch(131, &base) == 0); + std::uint64_t read = 0; + Require(sysarch(130, &read) == 0 && read == base); + std::uint64_t through = 0; + asm volatile("movq %%gs:0, %0" : "=r"(through)); + Require(through == slots[0]); + std::uint64_t fs = 0; + Require(sysarch(128, &fs) == 0 && fs != 0); + std::uint64_t self = 0; + asm volatile("movq %%fs:0, %0" : "=r"(self)); + Require(self == fs); + std::uint64_t outside = 0x0000800000000000ull; + *__error_nid_postfix() = 0; + Require(sysarch(131, &outside) == -1 && *__error_nid_postfix() == 22); + Require(sysarch(130, &read) == 0 && read == base); + *__error_nid_postfix() = 0; + Require(sysarch(131, nullptr) == -1 && *__error_nid_postfix() == 14); + Require(sysarch(130, nullptr) == -1 && *__error_nid_postfix() == 14); + std::uint64_t zero = 0; + Require(sysarch(131, &zero) == 0 && sysarch(130, &read) == 0 && read == 0); +#else + std::uint64_t base = 0; + Require(Thrown([&base] { sysarch(130, &base); }) == "sysarch: segment bases on this host not implemented"); +#endif + return 0; } diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 90a5eb5725..138d71b8bb 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -154,6 +154,7 @@ - [waitpid](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) returns -1 with `ECHILD` (`EINVAL` for unknown options): nothing here creates a child process; child processes would need a real `waitpid`. - [execv](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) throws: replacing the process image is not supported, the same as `execvp`. PPSA97358 imports it and fails to load without it. - [execve](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) throws like `execvp`: replacing the process image is not supported. The console behavior for invalid arguments and executable path lookups is not modeled. +- [sysarch](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - `AMD64_GET_FSBASE`, `AMD64_GET_GSBASE` and `AMD64_SET_GSBASE` follow FreeBSD 11's amd64 [`sysarch`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/amd64/amd64/sys_machdep.c) on Linux x86-64 hosts, through the host's `arch_prctl`. The GS base belongs to the calling thread, a base at or above `0x800000000000` (`VM_MAXUSER_ADDRESS`) returns `EINVAL`, and a null argument returns `EFAULT`. `AMD64_SET_FSBASE` throws, since guest and host code share the thread's FS base for their TLS. The other operations, and every operation on other hosts (Windows keeps its TEB in GS), throw. What a new thread's GS base starts as is not modelled: Linux threads inherit their creator's - [kern.proc.pathname](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) returns `/app0/eboot.bin` for pid -1 and the calling process, following FreeBSD's `sysctl_kern_proc_pathname`; the exact pathname on retail consoles remains unverified. - [machdep.cacheline_size](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) returns -1 with `ENOENT`, matching FreeBSD amd64 where this sysctl OID is not defined. - [hw.pagesize](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) reports the system page size (16384 bytes, PS5_PAGE_SIZE) as an integer through sysctl MIB {6, 7} (CTL_HW, HW_PAGESIZE) and the "hw.pagesize" sysctlbyname lookup, following FreeBSD's [`hw.pagesize`](https://github.com/freebsd/freebsd-src/blob/main/sys/kern/kern_mib.c#L177), a read-only `SYSCTL_INT` of `PAGE_SIZE`. The value on a PS5 follows the 16 KiB page size the rest of libkernel uses and is unmeasured. A write returns -1 with EPERM. From a3ed32e682aa55bd74e5f90ffff41b62a13ae205 Mon Sep 17 00:00:00 2001 From: JJenkx Date: Sat, 17 Oct 2026 16:37:41 -0400 Subject: [PATCH 0856/1000] fix(agc): retain import settle resources per device --- .../Graphics/src/GuestBufferMemory.cpp | 92 +++++++++---------- .../tests/HostImportSparseDmaBuf.cpp | 57 +++++++++++- 2 files changed, 99 insertions(+), 50 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index 3af6b42bd8..0eb80275f7 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -160,6 +160,16 @@ struct ImportFailure { GuestAllocations::Mapped ranges; }; +struct ImportSettle { + VkBuffer settleTarget = VK_NULL_HANDLE; + VkDeviceMemory settleMemory = VK_NULL_HANDLE; + VkCommandBuffer settleCommands = VK_NULL_HANDLE; + VkFence settleFence = VK_NULL_HANDLE; + PFN_vkDestroyFence destroyFence = nullptr; + PFN_vkDestroyBuffer settleDestroyBuffer = nullptr; + PFN_vkFreeMemory settleFreeMemory = nullptr; +}; + // Imports persist across draws, keyed by allocation base, and are dropped when their allocation leaves // the registered set; a dropped import is destroyed once the recorded work that may read it completed. struct HostImports { @@ -184,14 +194,7 @@ struct HostImports { bool unwatchImports = false; bool unwatchDmaBufImports = false; bool watchPrivateImports = false; - VkDevice settleDevice = VK_NULL_HANDLE; - VkBuffer settleTarget = VK_NULL_HANDLE; - VkDeviceMemory settleMemory = VK_NULL_HANDLE; - VkCommandBuffer settleCommands = VK_NULL_HANDLE; - VkFence settleFence = VK_NULL_HANDLE; - PFN_vkDestroyFence destroyFence = nullptr; - PFN_vkDestroyBuffer settleDestroyBuffer = nullptr; - PFN_vkFreeMemory settleFreeMemory = nullptr; + std::map settles; std::uint64_t useCounter = 0; std::uint64_t buildFloor = 0; }; @@ -678,51 +681,45 @@ std::atomic settledImports{0}; const char* settleImport(const Context& context, HostImports& state, const HostImport& entry, VkResult& result) { std::unique_lock queueLock(*context.settleQueueMutex); - if (state.settleDevice != context.device) { - state.settleDevice = context.device; - state.settleTarget = VK_NULL_HANDLE; - state.settleMemory = VK_NULL_HANDLE; - state.settleCommands = VK_NULL_HANDLE; - state.settleFence = VK_NULL_HANDLE; - state.destroyFence = context.Function("vkDestroyFence"); - state.settleDestroyBuffer = context.Function("vkDestroyBuffer"); - state.settleFreeMemory = context.Function("vkFreeMemory"); - } - if (state.settleTarget == VK_NULL_HANDLE) { + auto& settle = state.settles[context.device]; + settle.destroyFence = context.Function("vkDestroyFence"); + settle.settleDestroyBuffer = context.Function("vkDestroyBuffer"); + settle.settleFreeMemory = context.Function("vkFreeMemory"); + if (settle.settleTarget == VK_NULL_HANDLE) { VkBufferCreateInfo info{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO}; info.size = 256; info.usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT; - if ((result = context.Function("vkCreateBuffer")(context.device, &info, nullptr, &state.settleTarget)) != VK_SUCCESS) { - state.settleTarget = VK_NULL_HANDLE; + if ((result = context.Function("vkCreateBuffer")(context.device, &info, nullptr, &settle.settleTarget)) != VK_SUCCESS) { + settle.settleTarget = VK_NULL_HANDLE; return "vkCreateBuffer (settle)"; } VkMemoryRequirements requirements{}; - context.Function("vkGetBufferMemoryRequirements")(context.device, state.settleTarget, &requirements); + context.Function("vkGetBufferMemoryRequirements")(context.device, settle.settleTarget, &requirements); VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; allocation.allocationSize = requirements.size; allocation.memoryTypeIndex = static_cast(std::countr_zero(requirements.memoryTypeBits)); - if ((result = context.Function("vkAllocateMemory")(context.device, &allocation, nullptr, &state.settleMemory)) != VK_SUCCESS || (result = context.Function("vkBindBufferMemory")(context.device, state.settleTarget, state.settleMemory, 0)) != VK_SUCCESS) { - context.Function("vkDestroyBuffer")(context.device, state.settleTarget, nullptr); - if (state.settleMemory != VK_NULL_HANDLE) context.Function("vkFreeMemory")(context.device, state.settleMemory, nullptr); - state.settleTarget = VK_NULL_HANDLE; - state.settleMemory = VK_NULL_HANDLE; + if ((result = context.Function("vkAllocateMemory")(context.device, &allocation, nullptr, &settle.settleMemory)) != VK_SUCCESS || (result = context.Function("vkBindBufferMemory")(context.device, settle.settleTarget, settle.settleMemory, 0)) != VK_SUCCESS) { + context.Function("vkDestroyBuffer")(context.device, settle.settleTarget, nullptr); + if (settle.settleMemory != VK_NULL_HANDLE) context.Function("vkFreeMemory")(context.device, settle.settleMemory, nullptr); + settle.settleTarget = VK_NULL_HANDLE; + settle.settleMemory = VK_NULL_HANDLE; return "the settle target"; } VkCommandBufferAllocateInfo allocate{VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO}; allocate.commandPool = context.settlePool; allocate.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; allocate.commandBufferCount = 1; - if ((result = context.Function("vkAllocateCommandBuffers")(context.device, &allocate, &state.settleCommands)) != VK_SUCCESS) { - state.settleCommands = VK_NULL_HANDLE; + if ((result = context.Function("vkAllocateCommandBuffers")(context.device, &allocate, &settle.settleCommands)) != VK_SUCCESS) { + settle.settleCommands = VK_NULL_HANDLE; return "vkAllocateCommandBuffers (settle)"; } const VkFenceCreateInfo fenceInfo{VK_STRUCTURE_TYPE_FENCE_CREATE_INFO}; - if ((result = context.Function("vkCreateFence")(context.device, &fenceInfo, nullptr, &state.settleFence)) != VK_SUCCESS) { - state.settleFence = VK_NULL_HANDLE; + if ((result = context.Function("vkCreateFence")(context.device, &fenceInfo, nullptr, &settle.settleFence)) != VK_SUCCESS) { + settle.settleFence = VK_NULL_HANDLE; return "vkCreateFence (settle)"; } } - if (state.settleCommands == VK_NULL_HANDLE || state.settleFence == VK_NULL_HANDLE) return "the settle commands"; + if (settle.settleCommands == VK_NULL_HANDLE || settle.settleFence == VK_NULL_HANDLE) return "the settle commands"; VkBuffer source = entry.buffer; VkBuffer alias = VK_NULL_HANDLE; const auto run = [&]() -> const char* { @@ -738,19 +735,19 @@ const char* settleImport(const Context& context, HostImports& state, const HostI source = alias; } #endif - if ((result = context.Function("vkResetFences")(context.device, 1, &state.settleFence)) != VK_SUCCESS) return "vkResetFences (settle)"; - if ((result = context.Function("vkResetCommandBuffer")(state.settleCommands, 0)) != VK_SUCCESS) return "vkResetCommandBuffer (settle)"; + if ((result = context.Function("vkResetFences")(context.device, 1, &settle.settleFence)) != VK_SUCCESS) return "vkResetFences (settle)"; + if ((result = context.Function("vkResetCommandBuffer")(settle.settleCommands, 0)) != VK_SUCCESS) return "vkResetCommandBuffer (settle)"; VkCommandBufferBeginInfo begin{VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO}; begin.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; - if ((result = context.Function("vkBeginCommandBuffer")(state.settleCommands, &begin)) != VK_SUCCESS) return "vkBeginCommandBuffer (settle)"; + if ((result = context.Function("vkBeginCommandBuffer")(settle.settleCommands, &begin)) != VK_SUCCESS) return "vkBeginCommandBuffer (settle)"; const VkBufferCopy region{0, 0, 4}; - context.Function("vkCmdCopyBuffer")(state.settleCommands, source, state.settleTarget, 1, ®ion); - if ((result = context.Function("vkEndCommandBuffer")(state.settleCommands)) != VK_SUCCESS) return "vkEndCommandBuffer (settle)"; + context.Function("vkCmdCopyBuffer")(settle.settleCommands, source, settle.settleTarget, 1, ®ion); + if ((result = context.Function("vkEndCommandBuffer")(settle.settleCommands)) != VK_SUCCESS) return "vkEndCommandBuffer (settle)"; VkSubmitInfo submit{VK_STRUCTURE_TYPE_SUBMIT_INFO}; submit.commandBufferCount = 1; - submit.pCommandBuffers = &state.settleCommands; - if ((result = context.Function("vkQueueSubmit")(context.settleQueue, 1, &submit, state.settleFence)) != VK_SUCCESS) return "vkQueueSubmit (settle)"; - if ((result = context.Function("vkWaitForFences")(context.device, 1, &state.settleFence, VK_TRUE, 10'000'000'000ull)) != VK_SUCCESS) return "vkWaitForFences (settle)"; + submit.pCommandBuffers = &settle.settleCommands; + if ((result = context.Function("vkQueueSubmit")(context.settleQueue, 1, &submit, settle.settleFence)) != VK_SUCCESS) return "vkQueueSubmit (settle)"; + if ((result = context.Function("vkWaitForFences")(context.device, 1, &settle.settleFence, VK_TRUE, 10'000'000'000ull)) != VK_SUCCESS) return "vkWaitForFences (settle)"; return nullptr; }; const char* step = run(); @@ -2331,15 +2328,12 @@ void ClearHostImports(VkDevice device) { auto& state = Imports(); std::lock_guard lock(state.mutex); state.parked.erase(device); - if (state.settleDevice == device) { - if (state.settleFence != VK_NULL_HANDLE) state.destroyFence(device, state.settleFence, nullptr); - if (state.settleTarget != VK_NULL_HANDLE) state.settleDestroyBuffer(device, state.settleTarget, nullptr); - if (state.settleMemory != VK_NULL_HANDLE) state.settleFreeMemory(device, state.settleMemory, nullptr); - state.settleDevice = VK_NULL_HANDLE; - state.settleTarget = VK_NULL_HANDLE; - state.settleMemory = VK_NULL_HANDLE; - state.settleCommands = VK_NULL_HANDLE; - state.settleFence = VK_NULL_HANDLE; + if (const auto found = state.settles.find(device); found != state.settles.end()) { + const auto& settle = found->second; + if (settle.settleFence != VK_NULL_HANDLE) settle.destroyFence(device, settle.settleFence, nullptr); + if (settle.settleTarget != VK_NULL_HANDLE) settle.settleDestroyBuffer(device, settle.settleTarget, nullptr); + if (settle.settleMemory != VK_NULL_HANDLE) settle.settleFreeMemory(device, settle.settleMemory, nullptr); + state.settles.erase(found); } if (state.device != device) return; for (const auto& [address, entry] : state.imports) { diff --git a/core/libs/prx/libSceAgcDriver/tests/HostImportSparseDmaBuf.cpp b/core/libs/prx/libSceAgcDriver/tests/HostImportSparseDmaBuf.cpp index 204e3213f7..24703d1a17 100644 --- a/core/libs/prx/libSceAgcDriver/tests/HostImportSparseDmaBuf.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/HostImportSparseDmaBuf.cpp @@ -52,6 +52,7 @@ struct Bind { struct MockDevice { std::uintptr_t next = 0x1000; std::set buffers; + std::set fences; std::set sparse; std::map sizes; std::map memories; @@ -64,10 +65,25 @@ MockDevice mock; VKAPI_ATTR VkResult VKAPI_CALL mockCreateFence(VkDevice, const VkFenceCreateInfo*, const VkAllocationCallbacks*, VkFence* fence) { *fence = reinterpret_cast(mock.next++); + mock.fences.insert(*fence); return VK_SUCCESS; } -VKAPI_ATTR void VKAPI_CALL mockDestroyFence(VkDevice, VkFence, const VkAllocationCallbacks*) {} +VKAPI_ATTR void VKAPI_CALL mockDestroyFence(VkDevice, VkFence fence, const VkAllocationCallbacks*) { + mock.fences.erase(fence); +} + +VKAPI_ATTR VkResult VKAPI_CALL mockAllocateCommandBuffers(VkDevice, const VkCommandBufferAllocateInfo*, VkCommandBuffer* commands) { + *commands = reinterpret_cast(mock.next++); + return VK_SUCCESS; +} + +VKAPI_ATTR VkResult VKAPI_CALL mockResetFences(VkDevice, std::uint32_t, const VkFence*) { return VK_SUCCESS; } +VKAPI_ATTR VkResult VKAPI_CALL mockResetCommandBuffer(VkCommandBuffer, VkCommandBufferResetFlags) { return VK_SUCCESS; } +VKAPI_ATTR VkResult VKAPI_CALL mockBeginCommandBuffer(VkCommandBuffer, const VkCommandBufferBeginInfo*) { return VK_SUCCESS; } +VKAPI_ATTR VkResult VKAPI_CALL mockEndCommandBuffer(VkCommandBuffer) { return VK_SUCCESS; } +VKAPI_ATTR VkResult VKAPI_CALL mockQueueSubmit(VkQueue, std::uint32_t, const VkSubmitInfo*, VkFence) { return VK_SUCCESS; } +VKAPI_ATTR void VKAPI_CALL mockCmdCopyBuffer(VkCommandBuffer, VkBuffer, VkBuffer, std::uint32_t, const VkBufferCopy*) {} VKAPI_ATTR VkResult VKAPI_CALL mockWaitForFences(VkDevice, std::uint32_t, const VkFence*, VkBool32, std::uint64_t) { return VK_SUCCESS; @@ -133,6 +149,13 @@ VKAPI_ATTR VkDeviceAddress VKAPI_CALL mockGetBufferDeviceAddress(VkDevice, const PFN_vkVoidFunction VKAPI_CALL mockProc(VkDevice, const char* name) { static const std::map table{ + {"vkAllocateCommandBuffers", reinterpret_cast(mockAllocateCommandBuffers)}, + {"vkResetFences", reinterpret_cast(mockResetFences)}, + {"vkResetCommandBuffer", reinterpret_cast(mockResetCommandBuffer)}, + {"vkBeginCommandBuffer", reinterpret_cast(mockBeginCommandBuffer)}, + {"vkEndCommandBuffer", reinterpret_cast(mockEndCommandBuffer)}, + {"vkQueueSubmit", reinterpret_cast(mockQueueSubmit)}, + {"vkCmdCopyBuffer", reinterpret_cast(mockCmdCopyBuffer)}, {"vkCreateFence", reinterpret_cast(mockCreateFence)}, {"vkDestroyFence", reinterpret_cast(mockDestroyFence)}, {"vkWaitForFences", reinterpret_cast(mockWaitForFences)}, @@ -228,6 +251,36 @@ std::uint64_t RangeBytes() { return (UdmabufLimitBytes() / ChunkBytes + 1) * ChunkBytes + (std::uint64_t{1} << 20u); } +void SettlingSurvivesDeviceReplacement(bool reverse) { + const auto baselineBuffers = mock.buffers.size(); + const auto baselineMemories = mock.memories.size(); + const auto baselineFences = mock.fences.size(); + SharedRange range(8u << 20u, 0, true); + auto first = mockContext(); + first.device = reinterpret_cast(mock.next++); + first.settleQueue = reinterpret_cast(mock.next++); + first.settlePool = reinterpret_cast(mock.next++); + std::mutex queueMutex; + first.settleQueueMutex = &queueMutex; + auto second = first; + second.device = reinterpret_cast(mock.next++); + second.settlePool = reinterpret_cast(mock.next++); + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + for (const auto& context : {first, second, first}) { + const auto* imported = HostImportFor(context, range.Address(), 4096); + Expect(imported != nullptr && imported->dmaBuf, "replacement device did not settle its dma-buf import"); + } + ClearHostImports(reverse ? second.device : first.device); + Expect(mock.fences.size() == baselineFences + 1, "clearing one device did not retain exactly the other device's settle fence"); + ClearHostImports(reverse ? first.device : second.device); + Expect(mock.buffers.size() == baselineBuffers, "device replacement leaked a settle buffer"); + Expect(mock.memories.size() == baselineMemories, "device replacement leaked settle memory"); + Expect(mock.fences.size() == baselineFences, "device replacement leaked a settle fence"); + ClearHostImports(first.device); + ClearHostImports(second.device); + Expect(mock.fences.size() == baselineFences, "repeated device cleanup changed the live fences"); +} + void ImportsALargeRangeAsOneSparseBuffer() { const auto bytes = RangeBytes(); SharedRange range(bytes, 0, true); @@ -299,6 +352,8 @@ int main() { SetHostImportBudget(~std::uint64_t{0}); GuestArena::GuestArenaSetSharedBacking_nid_postfix(Resolve); try { + SettlingSurvivesDeviceReplacement(false); + SettlingSurvivesDeviceReplacement(true); ImportsALargeRangeAsOneSparseBuffer(); UnalignedOffsetIsDecidedBeforeImporting(); FailureAfterTheDecisionThrows(false, VK_SUCCESS, "UDMABUF_CREATE"); From 571fecaddce58c7d8e8b9d77716b065e1392e97d Mon Sep 17 00:00:00 2001 From: Gjon Gojcevic Date: Sat, 17 Oct 2026 16:05:47 -0400 Subject: [PATCH 0857/1000] feat(agc): sample texture format 43 through a lossless B10G11R11 copy Texture format 43 holds unsigned floats of 10 bits (R, bit 0), 11 bits (G, bit 10) and 11 bits (B, bit 21). VK_FORMAT_B10G11R11_UFLOAT_PACK32 holds R11 at bit 0, G11 at bit 11 and B10 at bit 22, so storing the guest R field in the Vulkan B field and the guest B field in the Vulkan R field moves every texel without changing a bit of any component. A sampled texture of format 43 is now uploaded that way (a repack pass over the detiled words, TextureRepack.comp) and viewed with R and B swapped back (TextureComponentChannel), so the shader samples it natively: point and bilinear filtering, every mip level and LOD, and cube faces work as for any other float texture. A texture whose results a storage image still holds is copied from that image (vkCmdCopyImageToBuffer, the repack pass, vkCmdCopyBufferToImage) and copied again whenever the image changed, like the depth and block copies. The recompiler treats a sampled format-43 image as a float image without conversion; storage reads and writes keep the packed R32_UINT path, and a 3D texture whose results are still in a storage image is flushed and uploaded instead. PPSA01317 samples a mipmapped format-43 texture at a LOD other than 0 and a format-43 cube map during boot. AI-assisted: yes --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 14 ++ .../Graphics/include/Texture.hpp | 4 + .../Graphics/include/TextureDetiler.hpp | 3 + .../Graphics/include/TextureFormat.hpp | 2 + .../Graphics/shaders/TextureRepack.comp | 20 ++ .../Graphics/src/ShaderResources.cpp | 14 +- .../libSceAgcDriver/Graphics/src/Texture.cpp | 101 +++++++- .../Graphics/src/TextureDetiler.cpp | 51 ++++ .../Graphics/src/TextureFormat.cpp | 10 + .../libSceAgcDriver/tests/TextureFormat.cpp | 48 ++++ .../tests/execution/ImageConvertedUnorm.cpp | 15 +- .../tests/execution/ImageRepackedFloat.cpp | 219 ++++++++++++++++++ .../Optimization/src/ResourceMaterializer.cpp | 7 +- docs/dev/debt/libSceAgcDriver.md | 1 + 14 files changed, 498 insertions(+), 11 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/Graphics/shaders/TextureRepack.comp create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/ImageRepackedFloat.cpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 6e6a309b36..b7b4a2a744 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -83,6 +83,19 @@ add_custom_command( VERBATIM ) +set(agcTextureRepackShaderSource ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/shaders/TextureRepack.comp) +set(agcTextureRepackShaderSpv ${agcTextureDetileGeneratedDir}/TextureRepack.spv) +set(agcTextureRepackShaderHeader ${agcTextureDetileGeneratedDir}/TextureRepack_spv.h) +add_custom_command( + OUTPUT "${agcTextureRepackShaderHeader}" + COMMAND ${CMAKE_COMMAND} -E make_directory "${agcTextureDetileGeneratedDir}" + COMMAND "$" -V --target-env vulkan1.1 -o "${agcTextureRepackShaderSpv}" "${agcTextureRepackShaderSource}" + COMMAND ${CMAKE_COMMAND} -DINPUT=${agcTextureRepackShaderSpv} -DOUTPUT=${agcTextureRepackShaderHeader} + -DSYMBOL=TEXTURE_REPACK_SPV -P "${CMAKE_CURRENT_SOURCE_DIR}/embed_spirv.cmake" + DEPENDS "${agcTextureRepackShaderSource}" glslang-standalone + VERBATIM +) + set(agcSampleCounterShaderSource ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/shaders/SampleCounter.comp) set(agcSampleCounterShaderSpv ${agcTextureDetileGeneratedDir}/SampleCounter.spv) set(agcSampleCounterShaderHeader ${agcTextureDetileGeneratedDir}/SampleCounter_spv.h) @@ -233,6 +246,7 @@ set(agcDriverSources ${agcCmaskClearShaderHeader} ${agcMeshArgumentsShaderHeader} ${agcTextureDetileImageShaderHeader} + ${agcTextureRepackShaderHeader} ${agcSampleCounterShaderHeader} ${agcSampleDumpsShaderHeader} ${agcPresentationShaderHeaders} diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp index dfe074f738..6714cdcb06 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp @@ -71,6 +71,9 @@ public: struct BlockCopyTag {}; Texture(const Context& context, const std::shared_ptr& source, const GuestTextureResource& descriptor, VkComponentMapping components, BlockCopyTag); static bool CanBlockCopyFrom(const StorageTexture& source, const GuestTextureResource& descriptor); + struct RepackCopyTag {}; + Texture(const Context& context, const std::shared_ptr& source, const GuestTextureResource& descriptor, VkComponentMapping components, RepackCopyTag); + static bool CanRepackCopyFrom(const StorageTexture& source, const GuestTextureResource& descriptor); bool IsDepthCopy() const { return perUseCopy == PerUseCopy::RefreshedOnReuse; } bool RefreshDepthCopy(); struct DepthCopyCounts { @@ -147,6 +150,7 @@ private: std::uint64_t copiedVersion = 0; VkFormat depthFormat = VK_FORMAT_UNDEFINED; VkFormat blockFormat = VK_FORMAT_UNDEFINED; + bool repackCopy = false; std::uint32_t copyWidth = 0, copyHeight = 0, copyLevels = 0, copyLayers = 0, copyElementBytes = 0; }; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/TextureDetiler.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/TextureDetiler.hpp index 5799b773ab..934e8f05b1 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/TextureDetiler.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/TextureDetiler.hpp @@ -49,6 +49,7 @@ namespace AgcDriver::Graphics { // leaving the other elements of both buffers alone. void Dispatch(VkCommandBuffer commands, TextureTileMode tileMode, std::uint32_t elementBytes, VkBuffer source, std::uint64_t sourceOffset, VkBuffer destination, std::uint64_t destinationOffset, const TileMipLayout& layout, bool retile = false, std::uint32_t slice = 0, bool thick = false, const DetileWindow& window = {}); void DispatchImage(VkCommandBuffer commands, TextureTileMode tileMode, std::uint32_t elementBytes, VkBuffer tiled, std::uint64_t tiledOffset, VkImageView view, const TileMipLayout& layout, bool retile = false, std::uint32_t slice = 0, const DetileWindow& window = {}); + void DispatchRepack(VkCommandBuffer commands, VkBuffer source, std::uint64_t sourceOffset, VkBuffer destination, std::uint64_t destinationOffset, std::uint64_t bytes); static VkFormat ImageElementFormat(std::uint32_t elementBytes); // Recycles the descriptor sets of the previous batch; call before recording a new command batch. void BeginBatch(); @@ -83,6 +84,8 @@ namespace AgcDriver::Graphics { VkDescriptorSetLayout imageDescriptorLayout = VK_NULL_HANDLE; VkPipelineLayout imagePipelineLayout = VK_NULL_HANDLE; VkShaderModule imageModule = VK_NULL_HANDLE; + VkShaderModule repackModule = VK_NULL_HANDLE; + VkPipeline repackPipeline = VK_NULL_HANDLE; DescriptorRing imageDescriptorSets; }; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp index fc7be9ec72..ff9eaf11df 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp @@ -21,6 +21,8 @@ std::optional FindGuestTextureFormat(VkFormat format, std::uint32 std::optional FindGuestColorTargetFormat(VkFormat format, std::uint32_t elementBytes); std::uint32_t BytesPerElement(std::uint32_t guestFormat); bool IsConvertedTextureFormat(std::uint32_t guestFormat); +bool IsRepackedTextureFormat(std::uint32_t guestFormat); +std::uint32_t RepackTextureWord(std::uint32_t word); bool IsBlockCompressed(std::uint32_t guestFormat); std::uint32_t BlockWidth(std::uint32_t guestFormat); std::uint32_t BlockHeight(std::uint32_t guestFormat); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/shaders/TextureRepack.comp b/core/libs/prx/libSceAgcDriver/Graphics/shaders/TextureRepack.comp new file mode 100644 index 0000000000..6de2f05e4a --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Graphics/shaders/TextureRepack.comp @@ -0,0 +1,20 @@ +#version 450 + +layout(local_size_x = 64) in; + +layout(std430, set = 0, binding = 0) readonly buffer Source { uint source[]; }; +layout(std430, set = 0, binding = 1) buffer Destination { uint destination[]; }; + +layout(push_constant) uniform Push { + uint sourceBase; + uint destinationBase; + uint words; + uint rowWords; +} push; + +void main() { + const uint index = gl_GlobalInvocationID.y * push.rowWords + gl_GlobalInvocationID.x; + if (gl_GlobalInvocationID.x >= push.rowWords || index >= push.words) return; + const uint word = source[push.sourceBase / 4u + index]; + destination[push.destinationBase / 4u + index] = ((word >> 21u) & 0x7ffu) | (((word >> 10u) & 0x7ffu) << 11u) | ((word & 0x3ffu) << 22u); +} diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index ab7d9b47ce..75bdb13d99 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -61,7 +61,7 @@ VkComponentSwizzle ComponentSwizzleFor(std::uint8_t dstSel) { } VkComponentMapping ViewComponents(const GuestTextureResource& resource) { - if (IsConvertedTextureFormat(resource.format)) return {VK_COMPONENT_SWIZZLE_R, VK_COMPONENT_SWIZZLE_G, VK_COMPONENT_SWIZZLE_B, VK_COMPONENT_SWIZZLE_A}; + if (IsConvertedTextureFormat(resource.format) && !IsRepackedTextureFormat(resource.format)) return {VK_COMPONENT_SWIZZLE_R, VK_COMPONENT_SWIZZLE_G, VK_COMPONENT_SWIZZLE_B, VK_COMPONENT_SWIZZLE_A}; const auto channel = [&](std::uint8_t dstSel) { return TextureComponentChannel(resource.format, ComponentSwizzleFor(dstSel)); }; return {channel(resource.dstSelX), channel(resource.dstSelY), channel(resource.dstSelZ), channel(resource.dstSelW)}; } @@ -609,7 +609,8 @@ std::shared_ptr cachedTexture(const Context& context, std::span= DepthCopyMinBytes() && Texture::DepthCopyFormat(ResolveTextureFormat(resource.format)) != VK_FORMAT_UNDEFINED && resource.dimension != TextureDimension::k3D; auto source = StorageTexture::FindPending(address, guestBytes); const bool blockCopy = source != nullptr && !depthCompare && Texture::CanBlockCopyFrom(*source, resource); - if (source != nullptr && !blockCopy && (depthCompare ? !depthCopy || !Texture::CanDepthCopyFrom(*source, resource) : !Texture::CanCopyFrom(*source, resource))) source.reset(); + const bool repackCopy = source != nullptr && !depthCompare && Texture::CanRepackCopyFrom(*source, resource); + if (source != nullptr && !blockCopy && !repackCopy && (depthCompare ? !depthCopy || !Texture::CanDepthCopyFrom(*source, resource) : !Texture::CanCopyFrom(*source, resource))) source.reset(); if (source != nullptr && MetadataMoved(*source, resource)) source.reset(); std::optional keys; bool clearThroughKeys = false; @@ -653,6 +654,7 @@ std::shared_ptr cachedTexture(const Context& context, std::span cachedTexture(const Context& context, std::spanRefreshDepthCopy(); return texture; } + if (source != nullptr && IsRepackedTextureFormat(resource.format)) { + auto texture = std::make_shared(context, source, resource, components, Texture::RepackCopyTag{}); + texture->RefreshDepthCopy(); + return texture; + } if (source != nullptr) return std::make_shared(context, source, resource, components); std::vector snapshot(bytes); ReadTextureSurface(resource, *keys, snapshot); @@ -773,6 +780,9 @@ std::shared_ptr cachedTexture(const Context& context, std::span(context, source, resource, components, Texture::BlockCopyTag{}); entry.texture->RefreshDepthCopy(); + } else if (IsRepackedTextureFormat(resource.format)) { + entry.texture = std::make_shared(context, source, resource, components, Texture::RepackCopyTag{}); + entry.texture->RefreshDepthCopy(); } else { entry.texture = std::make_shared(context, source, resource, components); } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index ed284ea658..fdaffb0d7a 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -351,6 +351,16 @@ Texture::Texture(const Context& context, TextureDetiler& detiler, const GuestTex detiler.Dispatch(commands, descriptor.tileMode, elementBytes, tiled->Handle(), guestLayerOffset + mip.tiledOffset, linear->Handle(), linearLayerOffset + mip.linearOffset, mip, false, layer, geometry.thick, {.pipeBankXor = descriptor.pipeBankXor}); } } + if (IsRepackedTextureFormat(descriptor.format)) { + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT); + for (std::uint32_t layer = 0; layer < arrayLayers; ++layer) { + for (std::uint32_t level = 0; level < mips.size(); ++level) { + if (!geometry.HasLayer(level, layer)) continue; + const auto offset = geometry.LinearLayerOffset(layer) + mips[level].linearOffset; + detiler.DispatchRepack(commands, linear->Handle(), offset, linear->Handle(), offset, mips[level].linearSize); + } + } + } VkBufferMemoryBarrier linearReadBarrier{VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER}; linearReadBarrier.srcAccessMask = VK_ACCESS_SHADER_WRITE_BIT; @@ -495,6 +505,73 @@ bool Texture::CanBlockCopyFrom(const StorageTexture& source, const GuestTextureR return geometry.imageDepth == 1 && source.ImageDepth() == 1 && source.ImageLayers() >= geometry.imageLayers; } +bool Texture::CanRepackCopyFrom(const StorageTexture& source, const GuestTextureResource& descriptor) { + const auto& from = source.Descriptor(); + if (!IsRepackedTextureFormat(descriptor.format) || descriptor.dimension == TextureDimension::k3D || ResolveTextureFormat(from.format) != ResolveTextureFormat(descriptor.format)) return false; + if (descriptor.baseAddress != from.baseAddress || descriptor.pipeBankXor != from.pipeBankXor || descriptor.width != from.width || descriptor.height != from.height || !ArrayLayersServeView(from, descriptor) || descriptor.tileMode != from.tileMode || descriptor.mipCount > from.mipCount) return false; + const auto geometry = DescribeSurface(descriptor); + return geometry.imageDepth == 1 && source.ImageDepth() == 1 && source.ImageLayers() >= geometry.imageLayers; +} + +Texture::Texture(const Context& context, const std::shared_ptr& source, const GuestTextureResource& descriptor, VkComponentMapping components, RepackCopyTag) : context(context), storageSource(source), perUseCopy(PerUseCopy::RefreshedOnReuse), repackCopy(true) { + try { + Require(source != nullptr && CanRepackCopyFrom(*source, descriptor), "storage image cannot serve a repacked copy of the sampled texture"); + const auto format = SampledTextureFormat(context, descriptor.format); + const auto geometry = DescribeSurface(descriptor); + copyWidth = descriptor.width; + copyHeight = descriptor.height; + copyLevels = descriptor.mipCount; + copyLayers = geometry.imageLayers; + copyElementBytes = BytesPerElement(descriptor.format); + AGC_LOG_DEFERRED(stdout, "Texture address=0x%llx %ux%u mips=%u layers=%u repacked copy of storage image (guest format %u -> vk %d)", static_cast(descriptor.baseAddress), descriptor.width, descriptor.height, descriptor.mipCount, copyLayers, descriptor.format, static_cast(format)); + + VkImageCreateInfo imageInfo{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO}; + imageInfo.flags = descriptor.dimension == TextureDimension::kCube ? VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT : 0u; + imageInfo.imageType = ImageTypeFor(descriptor.dimension); + imageInfo.format = format; + imageInfo.extent = {descriptor.width, descriptor.height, 1u}; + imageInfo.mipLevels = copyLevels; + imageInfo.arrayLayers = copyLayers; + imageInfo.samples = VK_SAMPLE_COUNT_1_BIT; + imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL; + imageInfo.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT; + imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + Check(context.Function("vkCreateImage")(context.device, &imageInfo, nullptr, &image), "vkCreateImage repacked copy"); + owned = std::make_shared(context, image, VK_NULL_HANDLE); + VkMemoryRequirements requirements{}; + context.Function("vkGetImageMemoryRequirements")(context.device, image, &requirements); + VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; + allocation.allocationSize = requirements.size; + allocationBytes = requirements.size; + allocation.memoryTypeIndex = context.MemoryType(requirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); + Check(context.Function("vkAllocateMemory")(context.device, &allocation, nullptr, &owned->memory), "vkAllocateMemory repacked copy"); + Check(context.Function("vkBindImageMemory")(context.device, image, owned->memory, 0), "vkBindImageMemory repacked copy"); + + const auto viewLevelCount = std::min(descriptor.lastLevel, descriptor.mipCount - 1u) - descriptor.baseLevel + 1u; + const auto viewLayerCount = geometry.imageLayers - descriptor.baseArray; + if (descriptor.dimension == TextureDimension::kCube) { + Require(viewLayerCount % 6u == 0, "guest cube texture view does not contain a multiple of 6 array slices"); + } + VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; + viewInfo.image = image; + viewInfo.viewType = ViewTypeFor(descriptor.dimension, viewLayerCount); + viewInfo.format = format; + viewFormat = format; + viewInfo.components = components; + viewInfo.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, descriptor.baseLevel, viewLevelCount, descriptor.baseArray, viewLayerCount}; + VkImageViewMinLodCreateInfoEXT minLod{VK_STRUCTURE_TYPE_IMAGE_VIEW_MIN_LOD_CREATE_INFO_EXT}; + ChainMinLod(context, descriptor, viewInfo, minLod); + Check(context.Function("vkCreateImageView")(context.device, &viewInfo, nullptr, &view), "vkCreateImageView repacked copy"); + viewRange = {viewInfo.viewType, viewInfo.subresourceRange.levelCount, viewInfo.subresourceRange.layerCount}; + createFirstLayerView(descriptor, viewInfo); + prepareBaseLevelView(descriptor, viewInfo); + } catch (...) { + release(); + throw; + } +} + Texture::Texture(const Context& context, const std::shared_ptr& source, const GuestTextureResource& descriptor, VkComponentMapping components, BlockCopyTag) : context(context), storageSource(source), perUseCopy(PerUseCopy::RefreshedOnReuse) { try { Require(source != nullptr && CanBlockCopyFrom(*source, descriptor), "storage image cannot serve a block copy of the sampled texture"); @@ -615,7 +692,7 @@ bool Texture::RefreshDepthCopy() { if (blockFormat != VK_FORMAT_UNDEFINED) return refreshBlockCopy(); std::uint64_t linearBytes = 0; for (std::uint32_t level = 0; level < copyLevels; ++level) linearBytes += static_cast(std::max(copyWidth >> level, 1u)) * std::max(copyHeight >> level, 1u) * copyElementBytes * copyLayers; - auto linear = std::make_shared(context, static_cast(linearBytes), VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT); + auto linear = std::make_shared(context, static_cast(linearBytes), VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT | (repackCopy ? VK_BUFFER_USAGE_STORAGE_BUFFER_BIT : 0u)); std::vector fromSource, toDepth; std::uint64_t offset = 0; for (std::uint32_t level = 0; level < copyLevels; ++level) { @@ -628,7 +705,7 @@ bool Texture::RefreshDepthCopy() { region.imageOffset = {0, 0, 0}; region.imageExtent = extent; fromSource.push_back(region); - region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT; + region.imageSubresource.aspectMask = repackCopy ? VK_IMAGE_ASPECT_COLOR_BIT : VK_IMAGE_ASPECT_DEPTH_BIT; toDepth.push_back(region); offset += static_cast(extent.width) * extent.height * copyElementBytes * copyLayers; } @@ -654,7 +731,7 @@ bool Texture::RefreshDepthCopy() { toTransferDst.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; toTransferDst.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; toTransferDst.image = image; - toTransferDst.subresourceRange = {VK_IMAGE_ASPECT_DEPTH_BIT, 0, copyLevels, 0, copyLayers}; + toTransferDst.subresourceRange = {static_cast(repackCopy ? VK_IMAGE_ASPECT_COLOR_BIT : VK_IMAGE_ASPECT_DEPTH_BIT), 0, copyLevels, 0, copyLayers}; barrier(commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 1, &produced, 0, nullptr, 1, &toTransferDst); context.Function("vkCmdCopyImageToBuffer")(commands, storageSource->Image(), VK_IMAGE_LAYOUT_GENERAL, linear->Handle(), static_cast(fromSource.size()), fromSource.data()); VkBufferMemoryBarrier linearReady{VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER}; @@ -665,7 +742,20 @@ bool Texture::RefreshDepthCopy() { linearReady.buffer = linear->Handle(); linearReady.offset = 0; linearReady.size = VK_WHOLE_SIZE; - barrier(commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 1, &linearReady, 0, nullptr); + if (repackCopy) { + Require(context.detiler != nullptr, "a repacked texture copy needs the texture detiler"); + linearReady.dstAccessMask = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT; + barrier(commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, 0, 0, nullptr, 1, &linearReady, 0, nullptr); + for (const auto& region : fromSource) { + const auto bytes = static_cast(region.imageExtent.width) * region.imageExtent.height * copyElementBytes * copyLayers; + context.detiler->DispatchRepack(commands, linear->Handle(), region.bufferOffset, linear->Handle(), region.bufferOffset, bytes); + } + linearReady.srcAccessMask = VK_ACCESS_SHADER_WRITE_BIT; + linearReady.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT; + barrier(commands, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 1, &linearReady, 0, nullptr); + } else { + barrier(commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 1, &linearReady, 0, nullptr); + } context.Function("vkCmdCopyBufferToImage")(commands, linear->Handle(), image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, static_cast(toDepth.size()), toDepth.data()); VkImageMemoryBarrier toShaderRead{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; toShaderRead.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; @@ -680,7 +770,7 @@ bool Texture::RefreshDepthCopy() { if (batch) batch->SubmitAndWait(); copiedOnce = true; copiedVersion = storageSource->Version(); - depthCopyRefreshes.fetch_add(1, std::memory_order_relaxed); + if (!repackCopy) depthCopyRefreshes.fetch_add(1, std::memory_order_relaxed); return true; } @@ -738,6 +828,7 @@ bool Texture::CanCopyFrom(const StorageTexture& source, const GuestTextureResour if (IsBlockCompressed(descriptor.format) || IsBlockCompressed(from.format)) return false; // Same memory, same layout, same texel size: the GPU copy reinterprets the texels exactly as a // guest read through the sampled descriptor would. + if (IsRepackedTextureFormat(descriptor.format)) return false; return descriptor.baseAddress == from.baseAddress && descriptor.pipeBankXor == from.pipeBankXor && descriptor.width == from.width && descriptor.height == from.height && ArrayLayersServeView(from, descriptor) && descriptor.tileMode == from.tileMode && descriptor.mipCount <= from.mipCount && BytesPerElement(descriptor.format) == BytesPerElement(from.format) && BlockWidth(descriptor.format) == BlockWidth(from.format); } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/TextureDetiler.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/TextureDetiler.cpp index 9397681655..af420f61f8 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/TextureDetiler.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/TextureDetiler.cpp @@ -5,6 +5,7 @@ #include "prx/libSceAgcDriver/Graphics/shaders/TextureDetile_spv.h" #include "prx/libSceAgcDriver/Graphics/shaders/CmaskClear_spv.h" #include "prx/libSceAgcDriver/Graphics/shaders/TextureDetileImage_spv.h" +#include "prx/libSceAgcDriver/Graphics/shaders/TextureRepack_spv.h" #include #include #include @@ -115,6 +116,8 @@ void TextureDetiler::release() noexcept { for (const auto pool : descriptorSets.pools) context.Function("vkDestroyDescriptorPool")(context.device, pool, nullptr); for (const auto pool : imageDescriptorSets.pools) context.Function("vkDestroyDescriptorPool")(context.device, pool, nullptr); if (imageModule) context.Function("vkDestroyShaderModule")(context.device, imageModule, nullptr); + if (repackPipeline) context.Function("vkDestroyPipeline")(context.device, repackPipeline, nullptr); + if (repackModule) context.Function("vkDestroyShaderModule")(context.device, repackModule, nullptr); if (imagePipelineLayout) context.Function("vkDestroyPipelineLayout")(context.device, imagePipelineLayout, nullptr); if (imageDescriptorLayout) context.Function("vkDestroyDescriptorSetLayout")(context.device, imageDescriptorLayout, nullptr); for (const auto& entry : pipelines) context.Function("vkDestroyPipeline")(context.device, entry.second, nullptr); @@ -261,6 +264,54 @@ void TextureDetiler::Dispatch(VkCommandBuffer commands, TextureTileMode tileMode context.Function("vkCmdDispatch")(commands, groupsX, groupsY, 1); } +void TextureDetiler::DispatchRepack(VkCommandBuffer commands, VkBuffer source, std::uint64_t sourceOffset, VkBuffer destination, std::uint64_t destinationOffset, std::uint64_t bytes) { + Require(commands != VK_NULL_HANDLE && source != VK_NULL_HANDLE && destination != VK_NULL_HANDLE, "a texel repack needs a command buffer and its buffers"); + Require(bytes != 0 && bytes % 4u == 0 && sourceOffset % 4u == 0 && destinationOffset % 4u == 0, "a texel repack needs whole aligned words"); + if (repackPipeline == VK_NULL_HANDLE) { + VkShaderModuleCreateInfo moduleInfo{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; + moduleInfo.codeSize = sizeof(TEXTURE_REPACK_SPV); + moduleInfo.pCode = TEXTURE_REPACK_SPV; + Check(context.Function("vkCreateShaderModule")(context.device, &moduleInfo, nullptr, &repackModule), "vkCreateShaderModule repack"); + VkPipelineShaderStageCreateInfo stage{VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO}; + stage.stage = VK_SHADER_STAGE_COMPUTE_BIT; + stage.module = repackModule; + stage.pName = "main"; + VkComputePipelineCreateInfo createInfo{VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO}; + createInfo.stage = stage; + createInfo.layout = pipelineLayout; + auto pipelineCacheLock = context.LockPipelineCacheForUse(); + Check(context.Function("vkCreateComputePipelines")(context.device, context.pipelineCache, 1, &createInfo, nullptr, &repackPipeline), "vkCreateComputePipelines repack"); + } + const auto alignment = std::max(context.limits.minStorageBufferOffsetAlignment, 4); + const auto sourceDescriptorOffset = sourceOffset - sourceOffset % alignment; + const auto destinationDescriptorOffset = destinationOffset - destinationOffset % alignment; + const auto sourceRange = sourceOffset - sourceDescriptorOffset + bytes; + const auto destinationRange = destinationOffset - destinationDescriptorOffset + bytes; + Require(sourceRange <= context.limits.maxStorageBufferRange && destinationRange <= context.limits.maxStorageBufferRange, "a texel repack exceeds the device's storage buffer range"); + const auto words = bytes / 4u; + constexpr std::uint64_t rowWords = 64u * 1024u; + const auto rows = (words + rowWords - 1u) / rowWords; + Require(words <= UINT32_MAX && rows <= context.limits.maxComputeWorkGroupCount[1], "a texel repack exceeds the device's dispatch limits"); + const auto set = allocateSet(); + const VkDescriptorBufferInfo sourceInfo{source, sourceDescriptorOffset, sourceRange}; + const VkDescriptorBufferInfo destinationInfo{destination, destinationDescriptorOffset, destinationRange}; + std::array writes{}; + for (std::uint32_t index = 0; index < writes.size(); ++index) { + writes[index].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + writes[index].dstSet = set; + writes[index].dstBinding = index; + writes[index].descriptorCount = 1; + writes[index].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; + writes[index].pBufferInfo = index == 0 ? &sourceInfo : &destinationInfo; + } + context.Function("vkUpdateDescriptorSets")(context.device, static_cast(writes.size()), writes.data(), 0, nullptr); + context.Function("vkCmdBindPipeline")(commands, VK_PIPELINE_BIND_POINT_COMPUTE, repackPipeline); + context.Function("vkCmdBindDescriptorSets")(commands, VK_PIPELINE_BIND_POINT_COMPUTE, pipelineLayout, 0, 1, &set, 0, nullptr); + const std::array push{static_cast(sourceOffset - sourceDescriptorOffset), static_cast(destinationOffset - destinationDescriptorOffset), static_cast(words), static_cast(std::min(words, rowWords))}; + context.Function("vkCmdPushConstants")(commands, pipelineLayout, VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof(push), push.data()); + context.Function("vkCmdDispatch")(commands, static_cast((std::min(words, rowWords) + 63u) / 64u), static_cast(rows), 1); +} + std::uint32_t TextureDetiler::CmaskErrors() { if (cmaskErrors == nullptr) return 0; std::uint32_t codes = 0; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp index ca7f4da8b3..59dd54b906 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp @@ -131,6 +131,7 @@ std::uint32_t SrgbDecodeFormats(PFN_vkGetPhysicalDeviceFormatProperties formatPr } VkFormat SampledTextureFormat(const Context& context, std::uint32_t guestFormat) { + if (IsRepackedTextureFormat(guestFormat)) return VK_FORMAT_B10G11R11_UFLOAT_PACK32; const auto format = static_cast(guestFormat); if ((context.srgbDecodeFormats & ShaderRecompiler::SrgbDecodeBit(format)) == 0u) return ResolveTextureFormat(guestFormat); return ResolveTextureFormat(static_cast(ShaderRecompiler::SrgbUnormFormat(format))); @@ -166,6 +167,7 @@ VkComponentSwizzle TextureComponentChannel(std::uint32_t guestFormat, VkComponen case 133: return bgrb[index]; case 134: return bgra[index]; case 136: return argb[index]; + case 43: return bgra[index]; default: return component; } } @@ -178,6 +180,14 @@ bool IsConvertedTextureFormat(std::uint32_t guestFormat) { return remapGuestFormat(guestFormat) != guestFormat; } +bool IsRepackedTextureFormat(std::uint32_t guestFormat) { + return guestFormat == static_cast(ShaderRecompiler::IrBufferFormat::Format10_11_11Float); +} + +std::uint32_t RepackTextureWord(std::uint32_t word) { + return ((word >> 21u) & 0x7ffu) | (((word >> 10u) & 0x7ffu) << 11u) | ((word & 0x3ffu) << 22u); +} + std::optional FindGuestColorTargetFormat(VkFormat format, std::uint32_t elementBytes) { switch (format) { case VK_FORMAT_B8G8R8A8_UNORM: format = VK_FORMAT_R8G8B8A8_UNORM; break; diff --git a/core/libs/prx/libSceAgcDriver/tests/TextureFormat.cpp b/core/libs/prx/libSceAgcDriver/tests/TextureFormat.cpp index 16f6aa1e66..bf25b268ee 100644 --- a/core/libs/prx/libSceAgcDriver/tests/TextureFormat.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/TextureFormat.cpp @@ -5,6 +5,8 @@ #include "prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp" #include "RdnaDecoder/RdnaDescriptorFormat.hpp" #include +#include +#include #include #include #include @@ -96,6 +98,51 @@ void packedChannelTests() { Require(TextureComponentChannel(133, VK_COMPONENT_SWIZZLE_ONE) == VK_COMPONENT_SWIZZLE_ONE && TextureComponentChannel(56, VK_COMPONENT_SWIZZLE_R) == VK_COMPONENT_SWIZZLE_R, "constant selectors and other formats must keep their channels"); } +float unsignedSmallFloat(std::uint32_t bits, std::uint32_t mantissaBits) { + const auto exponent = bits >> mantissaBits; + const auto mantissa = bits & ((1u << mantissaBits) - 1u); + const auto scale = static_cast(1u << mantissaBits); + if (exponent == 31u) return mantissa == 0u ? std::numeric_limits::infinity() : std::numeric_limits::quiet_NaN(); + if (exponent == 0u) return std::ldexp(static_cast(mantissa) / scale, -14); + return std::ldexp(1.0f + static_cast(mantissa) / scale, static_cast(exponent) - 15); +} + +bool sameValue(float left, float right) { + return (std::isnan(left) && std::isnan(right)) || left == right; +} + +void repackedFormatTests() { + constexpr std::uint32_t guest = 43u; + Require(IsRepackedTextureFormat(guest) && !IsRepackedTextureFormat(36u) && !IsRepackedTextureFormat(20u), "only texture format 43 is sampled through a repacked copy"); + Context context{}; + Require(SampledTextureFormat(context, guest) == VK_FORMAT_B10G11R11_UFLOAT_PACK32 && ResolveTextureFormat(guest) == VK_FORMAT_R32_UINT, "format 43 must be sampled as B10G11R11_UFLOAT and stored as R32_UINT"); + const auto field = [](std::uint32_t word, VkComponentSwizzle channel) -> std::pair { + switch (channel) { + case VK_COMPONENT_SWIZZLE_R: return {word & 0x7ffu, 6u}; + case VK_COMPONENT_SWIZZLE_G: return {(word >> 11u) & 0x7ffu, 6u}; + case VK_COMPONENT_SWIZZLE_B: return {word >> 22u, 5u}; + default: throw std::runtime_error("B10G11R11 has no such channel"); + } + }; + const auto check = [&](std::uint32_t red, std::uint32_t green, std::uint32_t blue) { + const auto word = red | (green << 10u) | (blue << 21u); + const auto repacked = RepackTextureWord(word); + const std::array, 3> expected{{{red, 5u}, {green, 6u}, {blue, 6u}}}; + constexpr std::array components{VK_COMPONENT_SWIZZLE_R, VK_COMPONENT_SWIZZLE_G, VK_COMPONENT_SWIZZLE_B}; + for (std::size_t component = 0; component < components.size(); ++component) { + const auto [bits, mantissaBits] = field(repacked, TextureComponentChannel(guest, components[component])); + Require(bits == expected[component].first && mantissaBits == expected[component].second && sameValue(unsignedSmallFloat(bits, mantissaBits), unsignedSmallFloat(expected[component].first, expected[component].second)), "the repacked word " + std::to_string(repacked) + " does not read guest component " + std::to_string(component) + " of " + std::to_string(word)); + } + }; + for (std::uint32_t red = 0; red < 1024u; ++red) check(red, 0x5a5u, 0x2c3u); + for (std::uint32_t value = 0; value < 2048u; ++value) { + check(0x155u, value, 0x7c0u); + check(0x3e0u, 0x001u, value); + } + Require(std::isinf(unsignedSmallFloat(0x3e0u, 5u)) && std::isnan(unsignedSmallFloat(0x3e1u, 5u)) && unsignedSmallFloat(0x001u, 6u) == std::ldexp(1.0f, -20), "the reference decode misreads Inf, NaN or a denormal"); + Require(TextureComponentChannel(guest, VK_COMPONENT_SWIZZLE_A) == VK_COMPONENT_SWIZZLE_A && TextureComponentChannel(guest, VK_COMPONENT_SWIZZLE_ZERO) == VK_COMPONENT_SWIZZLE_ZERO, "format 43 keeps alpha and constant selectors"); +} + void convertedDccClearTests() { alignas(64) std::array keys{}; GuestTextureResource resource{}; @@ -163,6 +210,7 @@ void RunTextureFormatTests() { srgbDecodeTests(); srgbTwinTests(); packedChannelTests(); + repackedFormatTests(); Require(ResolveTextureFormat(1) == VK_FORMAT_R8_UNORM, "format 1 must resolve to R8_UNORM"); Require(ResolveTextureFormat(2) == VK_FORMAT_R8_SNORM, "format 2 must resolve to R8_SNORM"); Require(BytesPerElement(1) == 1u, "format 1 must be one byte wide"); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageConvertedUnorm.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageConvertedUnorm.cpp index 6c036012f0..252f9eb8b9 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ImageConvertedUnorm.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageConvertedUnorm.cpp @@ -322,6 +322,17 @@ void CheckFloatStore(AgcDriver::VulkanDevice& device, std::uint8_t* texels) { } } +void CheckFloatSample(AgcDriver::VulkanDevice& device, const std::uint8_t* texels, const std::array& expected, const std::string& what) { + Run(device, SampleLz, TextureDescriptor(texels, FloatFormat, SwizzleXYZ1, Threads), PointSampler, 1); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + for (std::uint32_t component = 0; component < 4u; ++component) { + const auto actual = Output[tid * 4u + component]; + const bool nan = (expected[component] & 0x7f800000u) == 0x7f800000u && (expected[component] & 0x7fffffu) != 0u; + Require(nan ? std::isnan(std::bit_cast(actual)) : actual == expected[component], what + ": thread " + std::to_string(tid) + " component " + std::to_string(component) + " is " + Hex(actual) + ", expected " + Hex(expected[component])); + } + } +} + void RequireRefused(AgcDriver::VulkanDevice& device, std::span code, const std::uint8_t* texels, std::uint32_t swizzle, const std::array& extra, const std::string& reason, const std::string& what, std::uint32_t format = UnormFormat) { std::string refusal; try { @@ -353,8 +364,10 @@ int main() { CheckStore(*device, texels + StoreOffset, StoreXyz, 0x7u, "image_store dmask:0x7"); CheckStore(*device, texels + StoreOffset, StoreXz, 0x5u, "image_store dmask:0x5"); CheckFloatStore(*device, texels + FloatOffset); + Run(*device, StoreXyz, TextureDescriptor(texels + FloatOffset, FloatFormat, SwizzleXYZ1, Threads), BufferDescriptor(Input.data(), static_cast(Input.size() * 4u)), 1); + CheckFloatSample(*device, texels + FloatOffset, {0u, 0u, 0x7fc00000u, 0x3f800000u}, "image_sample_lz of R10_G11_B11_FLOAT results still on the GPU"); CheckFloatLoad(*device, texels + FloatOffset); - RequireRefused(*device, SampleLz, texels + FloatOffset, SwizzleXYZ1, PointSampler, "samples or gathers a converted float image", "image_sample_lz of R10_G11_B11_FLOAT", FloatFormat); + CheckFloatSample(*device, texels + FloatOffset, {0x7f800000u, 0x7f800000u, 0x387c0000u, 0x3f800000u}, "image_sample_lz of R10_G11_B11_FLOAT"); RequireRefused(*device, SampleLz, texels, SwizzleXYZ1, PointSampler, "sampling, gathering or querying LOD of a converted unorm image", "image_sample_lz"); RequireRefused(*device, Gather4Lz, texels, SwizzleXYZ1, PointSampler, "sampling, gathering or querying LOD of a converted unorm image", "image_gather4_lz"); RequireRefused(*device, GetLod, texels, SwizzleXYZ1, PointSampler, "sampling, gathering or querying LOD of a converted unorm image", "image_get_lod"); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageRepackedFloat.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageRepackedFloat.cpp new file mode 100644 index 0000000000..a42baf7fca --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageRepackedFloat.cpp @@ -0,0 +1,219 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 32; +constexpr std::uint32_t Words = 32; +constexpr std::uint32_t MipSize = 16; +constexpr std::uint32_t MipLevels = 5; +constexpr std::uint32_t CubeSize = 8; +constexpr std::uint32_t Faces = 6; +constexpr std::uint32_t RepackedFormat = 43; +constexpr std::uint32_t NativeFormat = 36; +constexpr std::uint32_t Type2D = 9; +constexpr std::uint32_t TypeCube = 11; +constexpr std::uint32_t Result = 8; +constexpr std::array PointMipSampler{0u, 0xfffu << 12u, 1u << 26u, 0u}; +constexpr std::array BilinearSampler{0u, 0xfffu << 12u, (1u << 20u) | (1u << 22u), 0u}; +alignas(256) std::array Buffer{}; +alignas(256) std::array Texels{}; + +alignas(256) constexpr std::array Code{ + 0x34020087, 0xe0301000, 0x80000201, 0xe0301004, 0x80000301, 0xe0301008, 0x80000401, 0xe030100c, + 0x80000501, 0xe0301010, 0x80000601, 0xbf8c3f70, 0xf09c0108, 0x00610c02, 0xf0900108, 0x00610d02, + 0xf0800108, 0x00610e02, 0xf0001108, 0x00010f05, 0xbf8c3f70, 0xe0701020, 0x80000c01, 0xe0701024, + 0x80000d01, 0xe0701028, 0x80000e01, 0xe070102c, 0x80000f01, 0xbf810000, +}; + +std::uint32_t GuestWord(std::uint32_t seed) { + seed ^= seed >> 16u; + seed *= 0x7feb352du; + seed ^= seed >> 15u; + seed *= 0x846ca68bu; + return seed ^ (seed >> 16u); +} + +float SmallFloat(std::uint32_t bits, std::uint32_t mantissaBits) { + const auto exponent = bits >> mantissaBits; + const auto mantissa = bits & ((1u << mantissaBits) - 1u); + const auto scale = static_cast(1u << mantissaBits); + if (exponent == 31u) return mantissa == 0u ? std::numeric_limits::infinity() : std::numeric_limits::quiet_NaN(); + if (exponent == 0u) return std::ldexp(static_cast(mantissa) / scale, -14); + return std::ldexp(1.0f + static_cast(mantissa) / scale, static_cast(exponent) - 15); +} + +float GuestComponent(std::uint32_t word, std::uint32_t component) { + if (component == 0u) return SmallFloat(word & 0x3ffu, 5u); + if (component == 1u) return SmallFloat((word >> 10u) & 0x7ffu, 6u); + return SmallFloat(word >> 21u, 6u); +} + +bool Same(float actual, float expected) { + return (std::isnan(actual) && std::isnan(expected)) || std::bit_cast(actual) == std::bit_cast(expected); +} + +std::uint32_t Swizzle(std::uint32_t component) { + return (4u + component) | (5u << 3u) | (6u << 6u) | (7u << 9u); +} + +std::array Descriptor(std::uint32_t format, std::uint32_t size, std::uint32_t type, std::uint32_t swizzle, std::uint32_t lastLevel, std::uint32_t lastArray) { + const auto address = static_cast(reinterpret_cast(Texels.data())); + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (format << 20u) | (((size - 1u) & 3u) << 30u), + ((size - 1u) >> 2u) | ((size - 1u) << 14u), + swizzle | (lastLevel << 16u) | (type << 28u), + lastArray, + lastLevel << 4u, + 0u, + 0u, + }; +} + +template +void Fill(const std::array& descriptor, TWord word) { + const auto geometry = AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::DecodeTextureResource(descriptor)); + Require(geometry.guestBytes <= Texels.size(), "image repacked float: the texture does not fit the texel storage"); + std::fill(Texels.begin(), Texels.end(), 0u); + for (std::uint32_t layer = 0; layer < geometry.layers; ++layer) { + for (std::uint32_t level = 0; level < geometry.mips.size(); ++level) { + const auto& mip = geometry.mips[level]; + for (std::uint32_t y = 0; y < mip.height; ++y) { + for (std::uint32_t x = 0; x < mip.width; ++x) { + const std::uint32_t value = word(layer, level, x, y); + std::memcpy(&Texels[geometry.GuestLayerOffset(layer) + mip.tiledOffset + static_cast(y) * mip.pitchBytes + x * 4u], &value, 4u); + } + } + } + } +} + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +void Run(AgcDriver::VulkanDevice& device, const std::array& texture, const std::array& sampler) { + std::vector userData(16, 0u); + const auto buffer = BufferDescriptor(Buffer.data(), static_cast(Buffer.size() * 4u)); + std::copy(buffer.begin(), buffer.end(), userData.begin()); + std::copy(texture.begin(), texture.end(), userData.begin() + 4); + std::copy(sampler.begin(), sampler.end(), userData.begin() + 12); + const std::span code(Code); + const std::array memory{{{reinterpret_cast(Code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(Code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(Code.data())); + device.WaitIdle(); +} + +void Input(std::uint32_t size, float base, std::uint32_t lod) { + Buffer.fill(0xdeadbeefu); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto x = tid % size; + const auto y = (tid * 3u) % size; + const float u = base + (static_cast(x) + 0.5f) / static_cast(size); + const float v = base + (static_cast(y) + 0.5f) / static_cast(size); + const std::array words{std::bit_cast(u), std::bit_cast(v), std::bit_cast(static_cast(lod)), x, y}; + std::copy(words.begin(), words.end(), &Buffer[tid * Words]); + } +} + +std::uint32_t MipWord(std::uint32_t, std::uint32_t level, std::uint32_t x, std::uint32_t y) { + return GuestWord(level * 4096u + y * 64u + x); +} + +void CheckMipLevels(AgcDriver::VulkanDevice& device) { + for (std::uint32_t lod = 1; lod < MipLevels; ++lod) { + const auto size = MipSize >> lod; + for (std::uint32_t component = 0; component < 3u; ++component) { + const auto descriptor = Descriptor(RepackedFormat, MipSize, Type2D, Swizzle(component), MipLevels - 1u, 0u); + Fill(descriptor, MipWord); + Input(size, 0.0f, lod); + Run(device, descriptor, PointMipSampler); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto x = tid % size; + const auto y = (tid * 3u) % size; + const float sampled = std::bit_cast(Buffer[tid * Words + Result + 1u]); + const float expected = GuestComponent(MipWord(0u, lod, x, y), component); + Require(Same(sampled, expected), "image repacked float: image_sample_l at LOD " + std::to_string(lod) + ", thread " + std::to_string(tid) + " component " + std::to_string(component) + " is " + std::to_string(sampled) + ", expected " + std::to_string(expected)); + const auto loaded = std::bit_cast(Buffer[tid * Words + Result + 3u]); + Require(Same(loaded, GuestComponent(MipWord(0u, 0u, x, y), component)), "image repacked float: image_load of mip 0, thread " + std::to_string(tid) + " component " + std::to_string(component) + " is " + std::to_string(loaded)); + } + } + } +} + +std::uint32_t CubeWord(std::uint32_t face, std::uint32_t, std::uint32_t x, std::uint32_t y) { + const auto exponent = 12u + (face + x + y) % 6u; + return ((exponent << 5u) | ((x * 7u + y) & 31u)) | (((exponent + 1u) << 6u | ((y * 5u + face) & 63u)) << 10u) | (((exponent + 2u) << 6u | ((x * 3u + face * 11u) & 63u)) << 21u); +} + +void CheckCubeFace(AgcDriver::VulkanDevice& device) { + for (std::uint32_t component = 0; component < 3u; ++component) { + const auto repacked = Descriptor(RepackedFormat, CubeSize, TypeCube, Swizzle(component), 0u, Faces - 1u); + Fill(repacked, CubeWord); + Input(CubeSize, 1.0f, 0u); + for (std::uint32_t tid = 0; tid < Threads; ++tid) Buffer[tid * Words] = std::bit_cast(std::bit_cast(Buffer[tid * Words]) + 0.3f / CubeSize); + const auto input = Buffer; + Run(device, repacked, BilinearSampler); + const auto repackedResults = Buffer; + constexpr std::array nativeChannel{2u, 1u, 0u}; + const auto native = Descriptor(NativeFormat, CubeSize, TypeCube, Swizzle(nativeChannel[component]), 0u, Faces - 1u); + Fill(native, [](std::uint32_t face, std::uint32_t level, std::uint32_t x, std::uint32_t y) { return AgcDriver::Graphics::RepackTextureWord(CubeWord(face, level, x, y)); }); + Buffer = input; + Run(device, native, BilinearSampler); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + for (std::uint32_t op = 0; op < 4u; ++op) { + const float actual = std::bit_cast(repackedResults[tid * Words + Result + op]); + const float expected = std::bit_cast(Buffer[tid * Words + Result + op]); + Require(Same(actual, expected), "image repacked float: cube face sample " + std::to_string(op) + ", thread " + std::to_string(tid) + " component " + std::to_string(component) + " is " + std::to_string(actual) + ", the native B10G11R11 cube gives " + std::to_string(expected)); + } + const float face = std::bit_cast(repackedResults[tid * Words + Result + 3u]); + Require(Same(face, GuestComponent(CubeWord(0u, 0u, tid % CubeSize, (tid * 3u) % CubeSize), component)), "image repacked float: image_load of cube face 0, thread " + std::to_string(tid) + " component " + std::to_string(component) + " is " + std::to_string(face)); + } + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + CheckMipLevels(*device); + CheckCubeFace(*device); + std::puts("image repacked float tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp index ff8686cb2f..e108afacf6 100644 --- a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp @@ -225,8 +225,9 @@ ImageDecode inspectImageDescriptor(const DescriptorValue& descriptor, const Imag : "MIMG BY2/BY4 requires a direct, identity-swizzled 2D image whose elements fill one dword: R8, R16 or RG8 for BY2, R8 for BY4"); } } - decoded.conversionFormat = RemapTextureFormat(format) != format ? format : IrBufferFormat::Invalid; - if (format == IrBufferFormat::Format11_11_10UNorm || format == IrBufferFormat::Format10_11_11Float) { + const bool nativeRepacked = format == IrBufferFormat::Format10_11_11Float && !storage && !base.packed; + decoded.conversionFormat = RemapTextureFormat(format) != format && !nativeRepacked ? format : IrBufferFormat::Invalid; + if (format == IrBufferFormat::Format11_11_10UNorm || (format == IrBufferFormat::Format10_11_11Float && !nativeRepacked)) { const bool floating = format == IrBufferFormat::Format10_11_11Float; if (!base.srgbDecodeCompatible) return rejectImage(PoisonReason::Conversion, floating ? "samples or gathers a converted float image, or queries its LOD, which is not implemented" : "sampling, gathering or querying LOD of a converted unorm image is not implemented"); if (!base.depthBitsCompatible) return rejectImage(PoisonReason::Conversion, floating ? "reads or writes a converted float image with 16-bit data, which is not implemented" : "reads or writes a converted unorm image with 16-bit data, which is not implemented"); @@ -244,7 +245,7 @@ ImageDecode inspectImageDescriptor(const DescriptorValue& descriptor, const Imag const bool wideSint = format == IrBufferFormat::Format32SInt || format == IrBufferFormat::Format32_32SInt || format == IrBufferFormat::Format32_32_32_32SInt; const bool narrowSint = format == IrBufferFormat::Format16SInt || format == IrBufferFormat::Format8_8SInt || format == IrBufferFormat::Format16_16SInt || format == IrBufferFormat::Format8_8_8_8SInt || format == IrBufferFormat::Format16_16_16_16SInt; const bool rawSintStorage = storage && (wideSint || (narrowSint && !base.packed)) && base.written && !base.read && !base.atomic; - decoded.numericClass = base.atomic ? IrTextureNumericClass::Uint : SampledTextureNumericClass(format); + decoded.numericClass = base.atomic ? IrTextureNumericClass::Uint : nativeRepacked ? IrTextureNumericClass::Float : SampledTextureNumericClass(format); if (!storage && !base.depthCompare && IsDepthBitsTexture(descriptor.dwords[1], descriptor.dwords[3])) { decoded.depthBits = true; if (!base.depthBitsCompatible) return rejectImage(PoisonReason::DepthBits16, "runtime image reads depth bits as unsupported 16-bit results"); diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 71210637ea..481118a4bd 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -55,6 +55,7 @@ - [Collapsed color array views](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) preserve the selected slice in the XOR address equation when the view is represented as a shifted 2D surface. Native Vulkan coverage checks R_X draws, array reads, publication and re-upload for slices 1-3; Lost Soul Aside PPSA26358 validates the RGBA16F cube mip chain on Windows/NVIDIA. Other devices and console comparisons remain unverified. Full-array views retain their layer addressing. - A [depth surface](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver) is retired when a color target or a written storage image it does not serve overlaps it, or when it is sampled through a view it cannot serve and guest memory over it was written since it was last bound; lookups then skip it, so sampling reads the memory like any texture. Bound again, it keeps its own depth instead of the bytes written over it. Without write watching, a view it cannot serve still throws. A retired surface also gives up its depth and stencil planes, so another surface sharing them no longer copies from it. - [Depth plane copies](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver): a depth plane sampled as a 2D array or a cube, or a D16 plane read as `R16_UINT`, is copied into an image of its own (`DepthPlaneCopy`), kept per base address, extent, format and slice count; a slice comes from the live depth surface when one is bound at the slice's address and from guest memory (detiled) otherwise. A draw or dispatch that binds the plane copies again only the slices that changed and records nothing when none did: a surface slice when its image or write serial differs from the one copied (the serial is bumped when the surface is created, when a draw binds it as its depth target, when a pending fast clear is applied and when a written storage view of the plane is copied back), a guest-memory slice when its bytes were written since they were last read (`GuestMemory::UnchangedSince`), a snapshot write overlaps them, or its address, size or tile mode changed. The surface of the latest depth draw is copied at every use until another depth target is bound, since that draw may still be recorded after the copy. A slice's address is the base plus a whole 64 KiB-block depth slice per layer (`DepthSliceBytes`), and the cube face order is the array order; both are inferred from the titles' use, not measured on hardware. A 2D-array T# whose `BASE_ARRAY` is past slice 0 reads slices `BASE_ARRAY` to `LAST_ARRAY` of the copy, which holds slices 0 to `LAST_ARRAY`, and a 2D binding of it reads slice `BASE_ARRAY`; that follows the colour 2D-array semantics and the titles that hit it (PPSA01556, PPSA03912), and was not measured on hardware for depth surfaces. Planes with mips, a cube or a 2D T# with a base slice other than 0, a `BASE_ARRAY` past `LAST_ARRAY`, a cube with other than 1 or 6 slices, and other views of the planes still throw, and so do these copies of a 16-bit plane with stencil that the device stores as D24 or D32 (16-bit depth with stencil, below). +- [Repacked format-43 copies](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver): a sampled texture of format 43 (10-bit R, 11-bit G and B unsigned floats from bit 0) is uploaded into a `B10G11R11_UFLOAT` image with its R and B fields swapped (`RepackTextureWord`) and viewed with R and B swapped back. A texture whose results a storage image still holds is copied from it again, whole, whenever the image changed (`RefreshDepthCopy`); a 3D one is flushed to guest memory and uploaded instead. Storage reads and writes of format 43 keep the packed `R32_UINT` path. - [SW_64KB_Z_X color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ColorTargetLayout.cpp) (libSceAgcDriver) use the texture equations and 64 KiB block geometry of `SW_64KB_Z_X`, whose addresses are checked against [AMD addrlib GFX10](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/gfx10/gfx10addrlib.cpp) in the texture tiling tests. Any 256-byte pipe/bank XOR below the block size is accepted, as for `SW_64KB_R_X` targets and Z_X textures; which of those bits the console honours, and the render results, are not measured. - [SW_64KB_D_X color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ColorTargetLayout.cpp) (libSceAgcDriver) use the existing texture block/mip geometry and equations from [AMD addrlib GFX10 non-RB+](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/gfx10/gfx10addrlib.cpp) for 16 pipes and 256-byte interleave. The address-reference tests use legal pipe/bank XOR bits for that configuration. Raw column bits outside the source configuration's pipe/bank mask 0xcf00 throw; their console meaning is unresolved. Console address equations and render results have not been measured. - [Per-face depth bias](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver): Vulkan has one depth bias per draw, so a draw that rasterizes both faces with different front and back bias (PA_SU_SC_MODE_CNTL, PA_SU_POLY_OFFSET_FRONT/BACK_*) is recorded twice, front faces with the back faces culled and then back faces with the front faces culled. The vertex stages run twice, so a vertex shader that writes memory writes twice, and all front faces land before all back faces instead of in primitive order, which differs only for blending or stencil updates that depend on the order. From 111d81786b666fcdb02924b9013c967be1addd98 Mon Sep 17 00:00:00 2001 From: Lindamea1 <322051987+Lindamea1@users.noreply.github.com> Date: Sat, 17 Oct 2026 21:15:20 +0100 Subject: [PATCH 0858/1000] fix(libkernel): accept an uninitialized attribute in scePthreadCondInit scePthreadCondInit, and pthread_cond_init through it, threw when attr pointed to a null attribute: a zeroed one that was never initialized or one that scePthreadCondattrDestroy has set to null. FreeBSD 11.4 libthr (thr_cond.c, cond_init) takes a null attribute as the default one and creates a CLOCK_REALTIME condition variable. A PS5 (firmware 13.60) takes a zeroed attribute as the default one in scePthreadMutexInit (#4158). Take it as the default here too, as scePthreadMutexInit and scePthreadCreate already do. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libkernel/Pthread/src/Cond.cpp | 4 +--- core/libs/tests/GuestCondTimedwait.cpp | 11 +++++++++++ 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libkernel/Pthread/src/Cond.cpp b/core/libs/prx/libkernel/Pthread/src/Cond.cpp index fe35bca255..f0686f555d 100644 --- a/core/libs/prx/libkernel/Pthread/src/Cond.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Cond.cpp @@ -145,10 +145,8 @@ int APS5_VABI scePthreadCondattrSetclock(PthreadCondattr* attr, KernelClockid cl int APS5_VABI scePthreadCondInit(PthreadCond* cond, const PthreadCondattr* attr, const char*) { if (!cond) throw std::invalid_argument("Condition variable pointer is null"); - if (attr && !*attr) - throw std::invalid_argument("Condition attributes are not initialized"); auto replacement = std::make_unique(); - if (attr) + if (attr && *attr) replacement->_clockid = (*attr)->_clockid; std::lock_guard lock(condInitializationMutex); std::atomic_ref(*cond).store(replacement.release(), std::memory_order_release); diff --git a/core/libs/tests/GuestCondTimedwait.cpp b/core/libs/tests/GuestCondTimedwait.cpp index 5a9190e1ec..91c18ac93e 100644 --- a/core/libs/tests/GuestCondTimedwait.cpp +++ b/core/libs/tests/GuestCondTimedwait.cpp @@ -16,6 +16,8 @@ int APS5_VABI scePthreadCondInit(PthreadCond* cond, const PthreadCondattr* attr, int APS5_VABI scePthreadCondDestroy(PthreadCond* cond); int APS5_VABI scePthreadCondTimedwait(PthreadCond* cond, PthreadMutex* mutex, unsigned int usec); int APS5_VABI scePthreadCondSignal(PthreadCond* cond); +int APS5_VABI pthread_cond_init_nid_postfix(PthreadCond* cond, const PthreadCondattr* attr); +int APS5_VABI pthread_cond_destroy_nid_postfix(PthreadCond* cond); int APS5_VABI pthread_cond_reltimedwait_np_nid_postfix(PthreadCond* cond, PthreadMutex* mutex, unsigned int usec); int APS5_VABI __cxa_atexit_nid_postfix(void (APS5_VABI *)(void*), void*, void*); void APS5_VABI __pthread_cxa_finalize_nid_postfix(void*); @@ -121,6 +123,15 @@ int main() { Require(scePthreadJoin(thread, nullptr) == SCE_OK); Require(scePthreadCondDestroy(&cond) == SCE_OK); + + PthreadCondattr unset = nullptr; + Require(scePthreadCondInit(&cond, &unset, nullptr) == SCE_OK); + Require(scePthreadMutexLock(&context.mutex) == SCE_OK); + Require(scePthreadCondTimedwait(&cond, &context.mutex, 1000) == SCE_KERNEL_ERROR_ETIMEDOUT); + Require(scePthreadMutexUnlock(&context.mutex) == SCE_OK); + Require(scePthreadCondDestroy(&cond) == SCE_OK); + Require(pthread_cond_init_nid_postfix(&cond, &unset) == 0); + Require(pthread_cond_destroy_nid_postfix(&cond) == 0); Require(scePthreadMutexDestroy(&context.mutex) == SCE_OK); Require(sceKernelSleep(0) == SCE_OK); From 206aaa56cbd0710f6bfbf20b07b084207d4d51c3 Mon Sep 17 00:00:00 2001 From: oskarlgs Date: Sat, 17 Oct 2026 20:58:29 +0000 Subject: [PATCH 0859/1000] fix(relinker): decode near branches with an operand-size prefix as AMD does 66 E8, 66 E9 and 66 0F 80-8F without REX.W take a 16-bit displacement on AMD processors, the console included. Every near relative branch with that prefix truncates its target to 16 bits, so the decoder reports no branch target for it and stubs refuse to move it. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_016TFMtUC4L4wMS4eeEqaNSR --- .../codegen/include/codegen/CodegenTypes.hpp | 1 + .../codegen/src/x86/StubBodyBuilder.cpp | 2 ++ .../codegen/src/x86/X64InstructionDecoder.cpp | 20 ++++++++++++-- .../codegen/tests/DecoderLengthCases.hpp | 8 ++++++ .../codegen/tests/DecoderLengthTests.cpp | 14 ++++++++++ .../codegen/tests/DecoderMetadataCases.hpp | 27 +++++++++++++++++++ docs/dev/debt/relinker.md | 2 +- 7 files changed, 71 insertions(+), 3 deletions(-) diff --git a/core/relinker/codegen/include/codegen/CodegenTypes.hpp b/core/relinker/codegen/include/codegen/CodegenTypes.hpp index 43e5a7110b..72bace4267 100644 --- a/core/relinker/codegen/include/codegen/CodegenTypes.hpp +++ b/core/relinker/codegen/include/codegen/CodegenTypes.hpp @@ -71,6 +71,7 @@ struct DecodedInstructionInfo { std::uint8_t ModRmByte; bool HasBranchTarget; std::int64_t BranchDisp; + bool TruncatesBranchTarget; bool IsTwoByteOpcode; std::uint8_t Opcode; std::uint8_t ModRmRegField; diff --git a/core/relinker/codegen/src/x86/StubBodyBuilder.cpp b/core/relinker/codegen/src/x86/StubBodyBuilder.cpp index e68805215f..f3e18ab213 100644 --- a/core/relinker/codegen/src/x86/StubBodyBuilder.cpp +++ b/core/relinker/codegen/src/x86/StubBodyBuilder.cpp @@ -210,6 +210,8 @@ void StubBodyBuilder::MoveControlFlow(const std::span instru const auto end = _siteOffset + static_cast(instruction.size()); if (info.FlowKind == ControlFlowKind::IndirectCall) throw CodegenException("A register or memory call cannot move into a stub"); + if (info.TruncatesBranchTarget) + throw CodegenException("A relative branch with this prefix cannot move into a stub"); if (info.FlowKind == ControlFlowKind::Call && info.HasRipRelativeDisp) { _pushReturnAddress(end); std::vector jump(instruction.begin(), instruction.end()); diff --git a/core/relinker/codegen/src/x86/X64InstructionDecoder.cpp b/core/relinker/codegen/src/x86/X64InstructionDecoder.cpp index a3f7f3962d..c6cc9de4ac 100644 --- a/core/relinker/codegen/src/x86/X64InstructionDecoder.cpp +++ b/core/relinker/codegen/src/x86/X64InstructionDecoder.cpp @@ -207,7 +207,7 @@ std::size_t X64InstructionDecoder::Decode(const std::uint8_t* data, std::size_t } if (opcode == OneByteCallRel32 || opcode == OneByteJmpRel32) { - immediateSize = ImmSize32; + immediateSize = operandImmediateSize; } if (opcode == OneByteJmpRel8 || opcode == OneByteJrcxz || @@ -241,7 +241,7 @@ std::size_t X64InstructionDecoder::Decode(const std::uint8_t* data, std::size_t } hasModRm = true; } else if (opcode >= TwoByteJccRel32Min && opcode <= TwoByteJccRel32Max) { - immediateSize = ImmSize32; + immediateSize = operandImmediateSize; } else if (opcode == TwoByteMovImm8ModRm || opcode == TwoByte3DNow) { hasModRm = true; immediateSize = ImmSize8; @@ -355,6 +355,7 @@ DecodedInstructionInfo X64InstructionDecoder::DecodeInstruction( info.Length = Decode(data, available); std::size_t pos = 0; + bool operandSizeOverride = false; while (pos < info.Length) { std::uint8_t b = data[pos]; @@ -371,6 +372,8 @@ DecodedInstructionInfo X64InstructionDecoder::DecodeInstruction( info.SegmentPrefix = b; if (b == PrefixAddressSize) info.AddressSize32 = true; + if (b == PrefixOperandSize) + operandSizeOverride = true; info.RexPrefix = 0; ++pos; continue; @@ -417,6 +420,19 @@ DecodedInstructionInfo X64InstructionDecoder::DecodeInstruction( info.Opcode = op; info.IsTwoByteOpcode = twoByteOpcode; + const bool relativeNearBranch = twoByteOpcode + ? op >= TwoByteJccRel32Min && op <= TwoByteJccRel32Max + : op == OneByteCallRel32 || op == OneByteJmpRel32 || op == OneByteJmpRel8 || + (op >= OneByteJccRel8Min && op <= OneByteJccRel8Max) || + op == OneByteJrcxz || (op >= OneByteLoop && op <= OneByteLoopMax); + if (relativeNearBranch && operandSizeOverride && (info.RexPrefix & RexWBit) == 0) { + info.TruncatesBranchTarget = true; + info.FlowKind = twoByteOpcode || (op != OneByteCallRel32 && op != OneByteJmpRel32 && op != OneByteJmpRel8) + ? ControlFlowKind::ConditionalBranch + : op == OneByteCallRel32 ? ControlFlowKind::Call : ControlFlowKind::UnconditionalJump; + return info; + } + if (!twoByteOpcode) { if (op == OneByteCallRel32) { std::int32_t disp = 0; diff --git a/core/relinker/codegen/tests/DecoderLengthCases.hpp b/core/relinker/codegen/tests/DecoderLengthCases.hpp index 10747c9a61..1de81e75ab 100644 --- a/core/relinker/codegen/tests/DecoderLengthCases.hpp +++ b/core/relinker/codegen/tests/DecoderLengthCases.hpp @@ -420,6 +420,9 @@ inline constexpr DecoderLengthCase kDecoderLengthCases[] = { {"660f79c1", 4}, {"660f7c846511223344", 9}, {"660f7d846511223344", 9}, + {"660f801122", 5}, + {"660f841122", 5}, + {"660f8f1122", 5}, {"660fd0846511223344", 9}, {"660fd6846511223344", 9}, {"660fe6846511223344", 9}, @@ -430,6 +433,7 @@ inline constexpr DecoderLengthCase kDecoderLengthCases[] = { {"66358465", 4}, {"663d8465", 4}, {"66480584651122", 7}, + {"66480f8411223344", 8}, {"66480d84651122", 7}, {"66481584651122", 7}, {"66481d84651122", 7}, @@ -450,6 +454,8 @@ inline constexpr DecoderLengthCase kDecoderLengthCases[] = { {"6648be8465112233445566", 11}, {"6648bf8465112233445566", 11}, {"6648c784651122334455667708", 13}, + {"6648e811223344", 7}, + {"6648e911223344", 7}, {"6648f784651122334455667708", 13}, {"66688465", 4}, {"66698465112233445566", 10}, @@ -464,6 +470,8 @@ inline constexpr DecoderLengthCase kDecoderLengthCases[] = { {"66be8465", 4}, {"66bf8465", 4}, {"66c78465112233445566", 10}, + {"66e81122", 4}, + {"66e91122", 4}, {"66f78465112233445566", 10}, {"676984651122334455667708", 12}, {"678184651122334455667708", 12}, diff --git a/core/relinker/codegen/tests/DecoderLengthTests.cpp b/core/relinker/codegen/tests/DecoderLengthTests.cpp index 54dd90df2b..63bca4bfe0 100644 --- a/core/relinker/codegen/tests/DecoderLengthTests.cpp +++ b/core/relinker/codegen/tests/DecoderLengthTests.cpp @@ -99,6 +99,20 @@ int main() { require(info.HasBranchTarget == testCase.HasBranchTarget, "Wrong branch target presence for " + name); require(!testCase.HasBranchTarget || info.BranchDisp == testCase.BranchDisp, "Wrong branch displacement for " + name); } + for (const auto& testCase : kDecoderTruncatedBranchCases) { + const std::string name = testCase.Bytes; + auto bytes = parse(testCase.Bytes); + require(bytes.size() == testCase.Length, "Truncated branch case " + name + " does not hold its own length"); + bytes.insert(bytes.end(), 15, 0x90); + const auto info = decoder.DecodeInstruction(bytes.data(), bytes.size()); + require(info.Length == testCase.Length, "Wrong length for " + name); + require(info.FlowKind == testCase.FlowKind, "Wrong control flow kind for " + name); + require(info.TruncatesBranchTarget && !info.HasBranchTarget, "Branch target of " + name + " was reported without its 16-bit truncation"); + } + for (const auto& testCase : kDecoderBranchCases) { + const auto bytes = parse(testCase.Bytes); + require(!decoder.DecodeInstruction(bytes.data(), bytes.size()).TruncatesBranchTarget, "Branch target of " + std::string(testCase.Bytes) + " was reported as truncated"); + } const auto threeDNow = parse("0f0f0d11223344a6"); const auto threeDNowInfo = decoder.DecodeInstruction(threeDNow.data(), threeDNow.size()); require(threeDNowInfo.HasModRm && threeDNowInfo.ModRmByte == 0x0d, "3DNow! ModRM was not reported"); diff --git a/core/relinker/codegen/tests/DecoderMetadataCases.hpp b/core/relinker/codegen/tests/DecoderMetadataCases.hpp index c685b184ea..d29d0c0191 100644 --- a/core/relinker/codegen/tests/DecoderMetadataCases.hpp +++ b/core/relinker/codegen/tests/DecoderMetadataCases.hpp @@ -787,6 +787,33 @@ inline constexpr DecoderBranchCase kDecoderBranchCases[] = { {"ff28", 2, Codegen::ControlFlowKind::IndirectJump, false, 0}, {"ffd0", 2, Codegen::ControlFlowKind::IndirectCall, false, 0}, {"ffe0", 2, Codegen::ControlFlowKind::IndirectJump, false, 0}, + {"66480f84c0111111", 8, Codegen::ControlFlowKind::ConditionalBranch, true, 286331328}, + {"6648e8c0111111", 7, Codegen::ControlFlowKind::Call, true, 286331328}, + {"6648e9c0111111", 7, Codegen::ControlFlowKind::UnconditionalJump, true, 286331328}, + {"6648ebc0", 4, Codegen::ControlFlowKind::UnconditionalJump, true, -64}, + {"664875c0", 4, Codegen::ControlFlowKind::ConditionalBranch, true, -64}, +}; + +struct DecoderTruncatedBranchCase { + const char* Bytes; + std::size_t Length; + Codegen::ControlFlowKind FlowKind; +}; + +inline constexpr DecoderTruncatedBranchCase kDecoderTruncatedBranchCases[] = { + {"660f80c011", 5, Codegen::ControlFlowKind::ConditionalBranch}, + {"660f8fc011", 5, Codegen::ControlFlowKind::ConditionalBranch}, + {"6670c0", 3, Codegen::ControlFlowKind::ConditionalBranch}, + {"6675c0", 3, Codegen::ControlFlowKind::ConditionalBranch}, + {"667fc0", 3, Codegen::ControlFlowKind::ConditionalBranch}, + {"66e0c0", 3, Codegen::ControlFlowKind::ConditionalBranch}, + {"66e2c0", 3, Codegen::ControlFlowKind::ConditionalBranch}, + {"66e3c0", 3, Codegen::ControlFlowKind::ConditionalBranch}, + {"66e8c011", 4, Codegen::ControlFlowKind::Call}, + {"66e9c011", 4, Codegen::ControlFlowKind::UnconditionalJump}, + {"66ebc0", 3, Codegen::ControlFlowKind::UnconditionalJump}, + {"6667e8c011", 5, Codegen::ControlFlowKind::Call}, + {"6640e8c011", 5, Codegen::ControlFlowKind::Call}, }; #endif diff --git a/docs/dev/debt/relinker.md b/docs/dev/debt/relinker.md index acab2b4e54..770bab7545 100644 --- a/docs/dev/debt/relinker.md +++ b/docs/dev/debt/relinker.md @@ -9,7 +9,7 @@ - [Guest import registries](../../../core/relinker/relinker/src/guest/GuestModuleBuilder.cpp) - declared host imports of bundled modules are recorded with their module name and relocation target, even when another guest module exports the same stripped NID. No-op relocations are omitted. Registries describe import bindings, not implementation correctness or runtime reachability. - Converted Windows main executables [omit `DYNAMIC_BASE`](../../../core/relinker/elfpatcher/src/windows/WindowsPeWriter.cpp) by default, not only with `--windows-fixed-base`: on the console the executable lies inside the 40-bit window that the title's own `sceLibcMspaceCreate` checks (#3043). Relocations are kept, so if `0x140000000` is taken Windows relocates the image, possibly above that window. Verified only on Windows 10. - [SDK memory-management syscall traps](../../../core/relinker/relinker/src/analysis/SyscallScanner.cpp) under `--trap-sdk-syscall-stubs` recognize three exact instruction sequences from [ps5-payload-dev SDK libc/mman.c](https://github.com/ps5-payload-dev/sdk/blob/12cf3dda40/libc/mman.c), observed in Nuvio Maxd 0.5.15 (PPSA99288): syscall 74 and the two syscall 477 paths, with their argument preparation and carry-flag error handling. Matching requires a decoded instruction boundary and the complete sequence inside the scanned code range. Other SDK/compiler output remains rejected. The matched syscall becomes `UD2`; this does not implement mmap or mprotect and a call still faults. -- The [instruction decoder](../../../core/relinker/codegen/src/x86/X64InstructionDecoder.cpp) decodes a near CALL, JMP or Jcc with an operand-size prefix (`66`) as Intel does, with a 32-bit or 8-bit displacement and a 64-bit target. AMD processors, the console included, read a 16-bit displacement for `66 E8`, `66 E9` and `66 0F 80`-`8F` and truncate the target of every near branch with that prefix, rel8 forms included, to 16 bits. Compilers do not emit these forms; the relinker's length and target for them do not match the console. +- The [instruction decoder](../../../core/relinker/codegen/src/x86/X64InstructionDecoder.cpp) reads a 16-bit displacement for `66 E8`, `66 E9` and `66 0F 80`-`8F` without REX.W, as AMD processors, the console included, do (binutils `objdump -M amd64`; REX.W selects the 32-bit form). AMD also truncates the target of every near relative branch with that prefix, rel8, LOOP and JRCXZ forms included, to 16 bits. Instruction metadata marks these branches as truncated and reports no branch target for them, and stubs refuse to move them. Compilers do not emit these forms. - `--to-intel` does not lower RDPRU/MCOMMIT; the [matcher](../../../core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp) fails the relink instead. - The length-changing path of the [instruction rewriter](../../../core/relinker/codegen/src/x86/X64InstructionRewriter.cpp) is not used by the [converter](../../../core/relinker/codegen/src/Amd64OnlyConverter.cpp): it does not adjust data-to-code references (relocations, FDEs, jump tables) or segment sizes, so every substitution keeps the instruction length. - A `PT_SCE_RELRO` segment that no `PT_LOAD` covers, the PS4 layout, is [loaded](../../../core/relinker/relinker/src/parsing/ElfReader.cpp) as a read-write `PT_LOAD` and stays writable after relocation; FreeBSD rtld makes `PT_GNU_RELRO` read-only once relocation is done, and what the console does with `PT_SCE_RELRO` is not checked. From 01f3938fe1bd8041821de85fe92a1fc0830d3727 Mon Sep 17 00:00:00 2001 From: Maz Date: Sun, 18 Oct 2026 01:05:40 +0400 Subject: [PATCH 0860/1000] fix(libkernel): report Windows directory junctions from lstat and readlink --- .../File/include/WindowsSymbolicLink.hpp | 39 +++++++++++++++++++ .../prx/libkernel/File/src/NativeStat.cpp | 2 +- core/libs/prx/libkernel_sys/Export.cpp | 2 +- core/libs/tests/GuestFilesystem.cpp | 11 ++++++ core/libs/tests/GuestKernelSys.cpp | 14 +++++++ docs/dev/debt/libkernel.md | 2 +- docs/dev/debt/libkernel_sys.md | 2 +- 7 files changed, 68 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libkernel/File/include/WindowsSymbolicLink.hpp b/core/libs/prx/libkernel/File/include/WindowsSymbolicLink.hpp index 849b88004e..d4a17f6abb 100644 --- a/core/libs/prx/libkernel/File/include/WindowsSymbolicLink.hpp +++ b/core/libs/prx/libkernel/File/include/WindowsSymbolicLink.hpp @@ -51,6 +51,45 @@ public: target = std::filesystem::path(name).generic_string(); return true; } + + static bool ReadJunction(const std::filesystem::path& native, std::string& target, const char* operation) { + const HANDLE handle = CreateFileW(native.c_str(), 0, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, + OPEN_EXISTING, FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS, nullptr); + if (handle == INVALID_HANDLE_VALUE) + throw std::system_error(static_cast(GetLastError()), std::system_category(), std::string(operation) + ": CreateFileW"); + std::array data{}; + DWORD size = 0; + const BOOL read = DeviceIoControl(handle, FSCTL_GET_REPARSE_POINT, nullptr, 0, data.data(), static_cast(data.size()), &size, nullptr); + const DWORD code = GetLastError(); + CloseHandle(handle); + if (!read) + throw std::system_error(static_cast(code), std::system_category(), std::string(operation) + ": FSCTL_GET_REPARSE_POINT"); + struct MountPoint { + ULONG tag; + USHORT dataLength; + USHORT reserved; + USHORT substituteNameOffset; + USHORT substituteNameLength; + USHORT printNameOffset; + USHORT printNameLength; + } header{}; + if (size < sizeof(header)) return false; + std::memcpy(&header, data.data(), sizeof(header)); + if (header.tag != IO_REPARSE_TAG_MOUNT_POINT) return false; + const auto textAt = [&](USHORT offset, USHORT length) { + const std::size_t begin = sizeof(header) + offset; + if (length % sizeof(wchar_t) != 0 || begin + length > size) + throw std::runtime_error(std::string(operation) + ": malformed junction reparse data"); + std::wstring name(length / sizeof(wchar_t), L'\0'); + if (!name.empty()) std::memcpy(name.data(), data.data() + begin, name.size() * sizeof(wchar_t)); + return name; + }; + std::wstring name = textAt(header.printNameOffset, header.printNameLength); + if (name.empty()) name = textAt(header.substituteNameOffset, header.substituteNameLength); + if (name.rfind(L"\\??\\", 0) == 0 || name.rfind(L"\\\\?\\", 0) == 0) name.erase(0, 4); + target = std::filesystem::path(name).generic_string(); + return true; + } }; } diff --git a/core/libs/prx/libkernel/File/src/NativeStat.cpp b/core/libs/prx/libkernel/File/src/NativeStat.cpp index b110e980df..a0314cb45c 100644 --- a/core/libs/prx/libkernel/File/src/NativeStat.cpp +++ b/core/libs/prx/libkernel/File/src/NativeStat.cpp @@ -51,7 +51,7 @@ static int DoLstat(const std::filesystem::path& p, NativeStat* st) { const auto attributes = GetFileAttributesW(p.c_str()); if (attributes == INVALID_FILE_ATTRIBUTES || (attributes & FILE_ATTRIBUTE_REPARSE_POINT) == 0) return DoStat(p, st); std::string target; - if (!File::WindowsSymbolicLink::ReadRelative(p, target, "lstat")) + if (!File::WindowsSymbolicLink::ReadRelative(p, target, "lstat") && !File::WindowsSymbolicLink::ReadJunction(p, target, "lstat")) NotImplemented_nid_no_patch("lstat of a Windows reparse point other than a relative symbolic link"); const auto handle = CreateFileW(p.c_str(), FILE_READ_ATTRIBUTES, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS, nullptr); diff --git a/core/libs/prx/libkernel_sys/Export.cpp b/core/libs/prx/libkernel_sys/Export.cpp index 481ff4b823..80688e21df 100644 --- a/core/libs/prx/libkernel_sys/Export.cpp +++ b/core/libs/prx/libkernel_sys/Export.cpp @@ -130,7 +130,7 @@ std::int64_t ReadNativeLink(const std::filesystem::path& native, char* buffer, s const DWORD attributes = GetFileAttributesW(native.wstring().c_str()); if (attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0) { std::string target; - if (!File::WindowsSymbolicLink::ReadRelative(native, target, "readlink")) { + if (!File::WindowsSymbolicLink::ReadRelative(native, target, "readlink") && !File::WindowsSymbolicLink::ReadJunction(native, target, "readlink")) { NotImplemented_nid_no_patch("readlink of a Windows reparse point other than a relative symbolic link"); return -1; } diff --git a/core/libs/tests/GuestFilesystem.cpp b/core/libs/tests/GuestFilesystem.cpp index 8147f4d743..b358b88503 100644 --- a/core/libs/tests/GuestFilesystem.cpp +++ b/core/libs/tests/GuestFilesystem.cpp @@ -735,6 +735,17 @@ int main() { Require(std::filesystem::remove(fileLink) && std::filesystem::remove(directoryLink) && DeleteFileW(danglingLink.c_str()) != 0 && std::filesystem::remove(absoluteLink) && std::filesystem::remove(targetDirectory)); } + const auto junctionTarget = root / "junction-target"; + const auto junction = root / "junction"; + Require(std::filesystem::create_directory(junctionTarget)); + const auto junctionCommand = L"cmd /c mklink /J \"" + junction.wstring() + L"\" \"" + junctionTarget.wstring() + L"\""; + Require(_wsystem(junctionCommand.c_str()) == 0); + FileStat junctionStatus{}; + Require(lstat_nid_postfix(junction.string().c_str(), &junctionStatus) == 0); + Require((junctionStatus.st_mode & 0170000) == 0120000 && junctionStatus.st_size > 0); + FileStat junctionFollowed{}; + Require(stat_nid_postfix(junction.string().c_str(), &junctionFollowed) == 0 && (junctionFollowed.st_mode & 0170000) == 0040000); + Require(std::filesystem::remove(junction) && std::filesystem::remove(junctionTarget)); #endif FileStat identity{}; FileStat byDescriptor{}; diff --git a/core/libs/tests/GuestKernelSys.cpp b/core/libs/tests/GuestKernelSys.cpp index d16ff6789a..d2084f6920 100644 --- a/core/libs/tests/GuestKernelSys.cpp +++ b/core/libs/tests/GuestKernelSys.cpp @@ -465,6 +465,20 @@ int main() { Require(fchmodat_nid_postfix(base, longName.c_str(), 0600, 0) == -1 && Error() == 63); #endif Require(sceKernelClose(base) == 0); +#ifdef _WIN32 + const auto junctionTarget = root / "junction-target"; + const auto junction = root / "junction"; + Require(std::filesystem::create_directory(junctionTarget)); + const auto junctionCommand = L"cmd /c mklink /J \"" + junction.wstring() + L"\" \"" + std::filesystem::absolute(junctionTarget).wstring() + L"\""; + Require(_wsystem(junctionCommand.c_str()) == 0); + char junctionBuffer[512]; + std::memset(junctionBuffer, '#', sizeof(junctionBuffer)); + const auto junctionLength = readlink_nid_postfix(junction.string().c_str(), junctionBuffer, sizeof(junctionBuffer)); + Require(junctionLength > 0 && static_cast(junctionLength) < sizeof(junctionBuffer) && junctionBuffer[junctionLength] == '#'); + const std::string junctionText(junctionBuffer, static_cast(junctionLength)); + std::error_code junctionError; + Require(junctionText.find('?') == std::string::npos && std::filesystem::equivalent(junctionText, junctionTarget, junctionError) && !junctionError); +#endif #ifndef _WIN32 if (created == 0) { const auto throughLink = (root / "through-link.bin").string(); diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index dd37013553..7089662ef2 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -116,7 +116,7 @@ - [Windows file timestamps](../../../core/libs/prx/libkernel/File/include/WindowsFileTime.hpp) use the host filesystem's timestamp range and resolution. `utimes`, `futimes`, and `lutimes` reject timestamps outside nonnegative signed 64-bit Windows file-time ticks with `EINVAL`, including exactly 1601-01-01 00:00:00 UTC, whose zero value means "leave unchanged" to `SetFileTime`. Guest directory descriptors still resolve through their recorded path. Filesystems with coarser timestamp resolution cannot retain every guest microsecond - [EVFILT_VNODE on macOS](../../../core/libs/prx/libkernel/Equeue/src/FileEvent.cpp) (libkernel) registers `EVFILT_VNODE` on a duplicate of the descriptor in a host kqueue, whose notes have FreeBSD's values, and drops the `NOTE_LINK` that XNU adds to `NOTE_DELETE` when the watched file or directory is removed, since FreeBSD's `vop_remove_post` and `vop_rmdir_post` report `NOTE_DELETE` alone. As on Windows, a directory registered with `NOTE_EXTEND` is accepted but never receives it: XNU reports a move between directories as `NOTE_WRITE` alone, with `NOTE_LINK` for a subdirectory - [Windows file identity](../../../core/libs/prx/libkernel/File/src/NativeStat.cpp) (libkernel) - `stat` and `fstat` report the volume serial number as `st_dev` and the 64-bit file index folded into the guest's 32-bit `st_ino` (never 0), so two different files can in principle share an `st_ino`; the fold is not taken from a reference. `st_nlink` is clamped to 65535, the width of the guest field. The call keeps the `_wstat64` values when `GetFileInformationByHandle` fails -- [lstat of a Windows symbolic link](../../../core/libs/prx/libkernel/File/src/NativeStat.cpp) (libkernel) reports a link whose target is relative, as `symlink` creates them, with the link's own times, `st_dev` and `st_ino`, mode `S_IFLNK | 0777` and the length of the target `readlink` returns as `st_size`. FreeBSD gives a new link `0777` less the creating process's umask, which the host does not keep, so the permission bits are not the console's. A link with an absolute target, a junction and any other reparse point throw, as for `readlink` +- [lstat of a Windows symbolic link](../../../core/libs/prx/libkernel/File/src/NativeStat.cpp) (libkernel) reports a link whose target is relative, as `symlink` creates them, with the link's own times, `st_dev` and `st_ino`, mode `S_IFLNK | 0777` and the length of the target `readlink` returns as `st_size`. FreeBSD gives a new link `0777` less the creating process's umask, which the host does not keep, so the permission bits are not the console's. A directory junction is reported the same way, using its print name, or its substitute name when the print name is empty, without a `\??\` or `\\?\` prefix. A link with an absolute target and any other reparse point throw, as for `readlink` - [setitimer](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) follows FreeBSD's `sys_setitimer` and `kern_setitimer`: `EINVAL` for timers other than `ITIMER_REAL`, `ITIMER_VIRTUAL` and `ITIMER_PROF`, and for a value, or the interval of a non-zero value, with a negative field, `tv_usec` of 1000000 or more, or more than `INT32_MAX / 2` seconds; a null value reads the timer as `getitimer` does, with `EFAULT` when the output is null too. `ITIMER_REAL` shares the timer thread of `alarm`, which clears its interval: a zero value disarms it, the previous value is returned, and each expiry re-arms it by `it_interval`. An expiry delivers nothing: with `SIG_IGN` it is dropped, with the default disposition the process exits with status 142, and with a handler, or with `SIGALRM` blocked, it throws, since guest `SIGALRM` handlers are never run and signals are never left pending. The value is not raised to `tick / 16` as `itimerfix` does, and an interval below 1 ms throws, since the console's `hz`, and so that rounding, is unknown (1 ms covers any `hz` of 63 or more). `ITIMER_VIRTUAL` and `ITIMER_PROF` throw when armed, as process CPU time is not accounted, and read and disarm as zero. PPSA99420 imports it and fails to load without it - On Linux, [direct memory](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) lives in one sparse memfd at file offset = physical address, so the [GPU imports](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) can share one udmabuf buffer object per 64 MiB of it. Released direct memory is not punched out of the file, because a live import of its chunk pins the pages and the GPU would keep reading the old ones: its pages stay committed (the process keeps the high-water mark of the direct memory it used) and are zeroed when the range is allocated again. - [system](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) runs no command. It follows FreeBSD 11's `lib/libc/stdlib/system.c` for a sandbox without `/bin/sh`: a null command returns 1 unconditionally, as that version does, and any command returns the wait status of a child that could not exec the shell and called `_exit(127)` (`0x7f00`). Whether the console's application sandbox has no shell, and whether `vfork` succeeds there (FreeBSD returns -1 when it fails), is unverified diff --git a/docs/dev/debt/libkernel_sys.md b/docs/dev/debt/libkernel_sys.md index 4caf7940b7..ec4578c5e3 100644 --- a/docs/dev/debt/libkernel_sys.md +++ b/docs/dev/debt/libkernel_sys.md @@ -10,7 +10,7 @@ - [fchmodat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `chmod`; any other relative path is joined to the descriptor's host directory the same way as for `openat`, so guest path aliases and case-insensitive name matching do not apply to it and a `..` component is not kept inside the guest root. A flag other than `0` or `AT_SYMLINK_NOFOLLOW` (0x200) returns `EINVAL`; the mode is masked to `07777`. With `AT_SYMLINK_NOFOLLOW` a symbolic link returns success silently on Windows and Linux, since the host cannot change a link's own mode, while a regular file is changed either way. The mode maps to the host file mode, which on Windows is only the CRT's read-only bit. PPSA99621 calls it through `std::filesystem::permissions`. Console behaviour is unverified - [pathconf](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) looks the path up as FreeBSD 11's `kern_pathconf` does (links followed, `ENAMETOOLONG` for a path of 1024 bytes or more or a component over 255) and returns the constants of FreeBSD 11's UFS `ufs_pathconf` for files and directories alike: `_PC_LINK_MAX` 32767, `_PC_NAME_MAX` 255, `_PC_PATH_MAX` and `_PC_SYMLINK_MAX` 1024, `_PC_PIPE_BUF` 512, `_PC_CHOWN_RESTRICTED` and `_PC_NO_TRUNC` 1, `_PC_FILESIZEBITS` 64, `_PC_REC_MAX_XFER_SIZE` -1, `_PC_PRIO_IO` and `_PC_SYNC_IO` 0. Whether the console's filesystems report the same is not verified. It throws for the names whose UFS value depends on the mount (`_PC_ALLOC_SIZE_MIN`, `_PC_REC_INCR_XFER_SIZE`, `_PC_REC_MIN_XFER_SIZE`, `_PC_MIN_HOLE_SIZE`), the kernel options (`_PC_ACL_EXTENDED`, `_PC_ACL_PATH_MAX`, `_PC_ACL_NFS4`, `_PC_MAC_PRESENT`) or the page size (`_PC_REC_XFER_ALIGN`), and on which UFS and `vop_stdpathconf` disagree (`_PC_MAX_CANON`, `_PC_MAX_INPUT`, `_PC_VDISABLE`, `_PC_ASYNC_IO`); other names fail with `EINVAL` - [mkdirat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `mkdir`. Any other relative path is joined to the descriptor's host directory, found the same way as for `fchdir`, so guest path aliases and case-insensitive name matching do not apply to it, and a `..` component is not kept inside the guest root. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory. On Windows, `mode` is ignored. Console behaviour is unverified -- [readlink](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) on Windows reads a symbolic link's target from its reparse data and returns it with `/` separators, only when the target is relative, as `symlink` creates them. A symbolic link with an absolute target and a junction throw, as a host path has no guest form. Any other reparse point throws too, where FreeBSD reports `EINVAL` for a file that is not a link +- [readlink](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) on Windows reads a symbolic link's target from its reparse data and returns it with `/` separators, only when the target is relative, as `symlink` creates them. A symbolic link with an absolute target throws, as a host path has no guest form. A directory junction returns its print name, or its substitute name when the print name is empty, without a `\??\` or `\\?\` prefix. Any other reparse point throws too, where FreeBSD reports `EINVAL` for a file that is not a link - [readlinkat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `readlink`. Any other relative path is joined to the descriptor's host directory, found the same way as for `fchdir`, so guest path aliases and case-insensitive name matching do not apply to it, and a `..` component is not kept inside the guest root. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory. Console behaviour is unverified - [unlinkat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `rmdir` (with `AT_REMOVEDIR`, 0x800) or `unlink`. Any other relative path is joined to the descriptor's host directory, found the same way as for `fchdir`, so guest path aliases and case-insensitive name matching do not apply to it, and a `..` component is not kept inside the guest root. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory. Removing a directory without `AT_REMOVEDIR` fails with `EPERM`, removing `.` or `..` with `AT_REMOVEDIR` fails with `EINVAL`, and flags other than `AT_REMOVEDIR` fail with `EINVAL`. Console behaviour is unverified - [renameat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `rename`. Any other relative path is joined to its descriptor's host directory, found the same way as for `fchdir`, so guest path aliases and case-insensitive name matching do not apply to relative descriptors, and a `..` component is not kept inside the guest root. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory. Console behaviour is unverified From 000938891895d5081cbc8c10293e2e1052083280 Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Sat, 17 Oct 2026 21:07:00 +0000 Subject: [PATCH 0861/1000] chore(agc): report unsupported 3D mip target state --- core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp | 6 +++++- core/libs/prx/libSceAgcDriver/tests/Graphics.cpp | 6 ++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index 279258b2af..ca8205e6aa 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -1489,7 +1489,11 @@ ColorTarget DecodeColorBuffer(const Registers& cx, std::uint32_t slot, bool allo Require(((attrib3 >> 24u) & 3u) != 0u || (attrib2 & 0x3fffu) == 0u, "1D color targets taller than one row are unsupported"); if (volume) { color.depth = (attrib3 & 0x1fffu) + 1u; - Require(maxMip == 0, "mipmapped 3D color targets are unsupported"); + if (maxMip != 0) { + char message[384]; + std::snprintf(message, sizeof(message), "mipmapped 3D color targets are unsupported (slot %u, %ux%ux%u, viewMip %u, maxMip %u, slices %u..%u, CB_COLOR_INFO 0x%08x, ATTRIB2 0x%08x, ATTRIB3 0x%08x, VIEW 0x%08x)", slot, ((attrib2 >> 14u) & 0x3fffu) + 1u, (attrib2 & 0x3fffu) + 1u, color.depth, viewMip, maxMip, slice, lastSlice, info, attrib2, attrib3, view); + Require(false, message); + } Require(slice < color.depth && lastSlice >= slice, "the color view slices are outside the 3D surface"); color.depthSlice = slice; color.layers = std::min(lastSlice + 1u, color.depth) - slice; diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index cbba137501..e44e89202b 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -2014,6 +2014,12 @@ void DepthStencilTests() { const auto volume = AgcDriver::Graphics::DecodeState(queue); Require(volume.color.address == sliced && volume.color.depth == 4u && volume.color.depthSlice == 2u, "a color view of one 3D depth slice did not keep the surface address and select the slice"); Require(volume.color.layers == 1 && volume.renderLayers == 1 && volume.layerExports == 0, "a color view of one 3D depth slice became layered"); + { + auto mipmapped = queue; + mipmapped.context[0x3b0] |= 2u << 28u; + mipmapped.context[0x31b] = (1u << 26u) | (1u << 13u) | 1u; + expectFailure([&] { AgcDriver::Graphics::DecodeColorBuffer(mipmapped.context, 0); }, "slot 0, 64x4x4, viewMip 1, maxMip 2, slices 1..1, CB_COLOR_INFO 0x00028028, ATTRIB2 0x200fc003, ATTRIB3 0x0a000003, VIEW 0x04002001"); + } queue.context[0x31b] = 4u | (4u << 13u); expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "outside the 3D surface"); queue.context[0x31b] = 2u | (1u << 13u); From 70ff799fdbd526b1d27bd7a976b0dd92a1e1f7fc Mon Sep 17 00:00:00 2001 From: T3cm0 Date: Sat, 17 Oct 2026 16:17:05 -0500 Subject: [PATCH 0862/1000] feat(libkernel): implement three coredump exports from the console module Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libkernel/System/src/Coredump.cpp | 17 +++++++++-------- core/libs/tests/GuestCoredumpAttach.cpp | 17 +++++++++++++++++ docs/dev/debt/libkernel.md | 5 +++-- 3 files changed, 29 insertions(+), 10 deletions(-) diff --git a/core/libs/prx/libkernel/System/src/Coredump.cpp b/core/libs/prx/libkernel/System/src/Coredump.cpp index a644a630f1..3bc229914c 100644 --- a/core/libs/prx/libkernel/System/src/Coredump.cpp +++ b/core/libs/prx/libkernel/System/src/Coredump.cpp @@ -11,6 +11,7 @@ static std::atomic g_coredumpHandler{0}; static std::atomic g_coredumpContext{0}; +static constexpr int SCE_COREDUMP_ERROR_INVALID_ARGUMENT = static_cast(0x81180000u); static constexpr int SCE_COREDUMP_ERROR_NOT_IN_COREDUMP_HANDLER = static_cast(0x81180003u); static void requireRegionOutsideHandler(const char* function, const void* mem, size_t size) { @@ -55,9 +56,10 @@ int APS5_VABI sceCoredumpAttachUserFile(uint32_t userValue, const char* path) { return SCE_COREDUMP_ERROR_NOT_IN_COREDUMP_HANDLER; } -int APS5_VABI sceCoredumpGetStopInfoGpu_Agc(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceCoredumpGetStopInfoGpu_Agc(void* info, size_t size) { + (void)size; + if (info == nullptr) return SCE_COREDUMP_ERROR_INVALID_ARGUMENT; + return SCE_COREDUMP_ERROR_NOT_IN_COREDUMP_HANDLER; } int APS5_VABI sceCoredumpAttachMemoryRegionAsUserFile(uint32_t userValue, const void* mem, size_t size, const char* name) { @@ -99,14 +101,13 @@ int APS5_VABI sceCoredumpGetThreadContextInfo(void* threadContextInfo, size_t th return SCE_COREDUMP_ERROR_NOT_IN_COREDUMP_HANDLER; } -int APS5_VABI sceCoredumpWriteUserString() { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceCoredumpWriteUserString(uint32_t first, const char* text, int third) { + if (text == nullptr || first > 15 || third < 0 || third > 256) return SCE_COREDUMP_ERROR_INVALID_ARGUMENT; return 0; } -int APS5_VABI sceCoredumpConfigDumpMode() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceCoredumpConfigDumpMode(int mode) { + return mode == 1 ? 0 : SCE_COREDUMP_ERROR_INVALID_ARGUMENT; } int APS5_VABI sceCoredumpAttachUserMemoryFile(uint32_t userValue, const void* mem, size_t size) { diff --git a/core/libs/tests/GuestCoredumpAttach.cpp b/core/libs/tests/GuestCoredumpAttach.cpp index 1fb6e65f1e..43e32238e0 100644 --- a/core/libs/tests/GuestCoredumpAttach.cpp +++ b/core/libs/tests/GuestCoredumpAttach.cpp @@ -14,9 +14,13 @@ int APS5_VABI sceCoredumpAttachMemoryRegion(uint32_t userValue, const void* mem, int APS5_VABI sceCoredumpAttachUserMemoryFile(uint32_t userValue, const void* mem, size_t size); int APS5_VABI sceCoredumpGetStopInfoCpu(void* info, size_t size); int APS5_VABI sceCoredumpGetThreadContextInfo(void* threadContextInfo, size_t threadContextInfoSize); +int APS5_VABI sceCoredumpGetStopInfoGpu_Agc(void* info, size_t size); +int APS5_VABI sceCoredumpWriteUserString(uint32_t first, const char* text, int third); +int APS5_VABI sceCoredumpConfigDumpMode(int mode); } namespace { +constexpr int InvalidArgument = static_cast(0x81180000u); constexpr int NotInCoredumpHandler = static_cast(0x81180003u); constexpr uint32_t UserFileTruncateIfExceedLimit = 0x80000000u; bool handlerRan = false; @@ -90,5 +94,18 @@ int main() { Require(sceCoredumpGetThreadContextInfo(context.data() + 8, 144) == NotInCoredumpHandler); Require(region == original && context == originalContext); Require(!handlerRan); + + Require(sceCoredumpGetStopInfoGpu_Agc(region.data() + 8, 8) == NotInCoredumpHandler); + Require(sceCoredumpGetStopInfoGpu_Agc(region.data() + 8, 3) == NotInCoredumpHandler); + Require(sceCoredumpGetStopInfoGpu_Agc(nullptr, 8) == InvalidArgument); + Require(sceCoredumpWriteUserString(1, "text", 4) == 0); + Require(sceCoredumpWriteUserString(0, "", 0) == 0); + Require(sceCoredumpWriteUserString(1, nullptr, 4) == InvalidArgument); + Require(sceCoredumpWriteUserString(15, "x", 256) == 0 && sceCoredumpWriteUserString(0, "x", 0) == 0); + Require(sceCoredumpWriteUserString(16, "x", 1) == InvalidArgument && sceCoredumpWriteUserString(0x80000000u, "x", 1) == InvalidArgument); + Require(sceCoredumpWriteUserString(1, "x", 257) == InvalidArgument && sceCoredumpWriteUserString(1, "x", -1) == InvalidArgument); + Require(region == original); + Require(sceCoredumpConfigDumpMode(1) == 0); + for (const int mode : {0, 2, -1}) Require(sceCoredumpConfigDumpMode(mode) == InvalidArgument); return 0; } diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 2fb9d30137..25f2c1a23d 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -33,8 +33,9 @@ - [AMM remap, multimap and protection changes](../../../core/libs/prx/libkernel/Apr/src/Apr.cpp) (libSceAmpr) - signatures and the protection and mask checks taken from ampr_emu; the effects are inferred from the names, not confirmed on a title. A remap moves every page of a fully mapped range to the new address with the given protection and leaves the old range reserved; a multimap maps the same pages at a second address, anywhere in the AMM ranges, and a pool page goes back to the pool once its last mapping is unmapped; a multimap onto its own source range throws. A protection change sets the masked bits on every page of a fully mapped range; the memory type is recorded and ignored. A command on a range that is not fully mapped throws - [AMM PRT ranges](../../../core/libs/prx/libkernel/Apr/src/Apr.cpp) (libSceAmpr) - signatures, checks and their order taken from ampr_emu, where `MapAsPrt` is a map with the PRT flag and protection 0 and `AllocatePaForPrt` a memory type and protection change with mask 1019, both checking the buffer memory after its size; the effects are inferred from the names, not confirmed on a title. Unbacked PRT pages are zero-filled pages with GPU read access, so CPU reads see zeros instead of faulting and GPU writes fault instead of being dropped. `AllocatePaForPrt` backs each unbacked page with a pool page and sets the type and protection of backed ones; `RemapIntoPrt` moves a fully mapped range into a PRT range, its opcode argument (1011 when 0, as in ampr_emu) is recorded and ignored; `UnmapToPrt` returns the pages to the pool and zero-fills them again. A plain map, unmap or remap over a PRT range ends it; `AllocatePaForPrt`, `RemapIntoPrt` or `UnmapToPrt` outside a PRT range throws - [AMM kernel write commands](../../../core/libs/prx/libkernel/Apr/src/Apr.cpp) (libkernel) - signatures of `sceKernelWriteModifyMtypeProtectCommand` and `sceKernelWriteRemapIntoPrtCommand` taken from [ampr_emu's libkernel hook targets](https://github.com/drakmor/ampr_emu/blob/cfa85df379f6eeeb165d7badf9b648e266fe77b7/include/ampr_libkernel_hook_targets.h), which forward to the real kernel; the measure mode (`dst` null reports the size, `outSize` null skips the report) follows ampr_emu's measure-then-record calls, not confirmed on a title. The range, protection and mask checks mirror the `libSceAmpr` siblings, and the recorded bytes match them exactly so the submitted buffer executes on the existing paths -- [sceCoredumpWriteUserString](../../../core/libs/prx/libkernel/System/src/Coredump.cpp) - imported by PPSA16064, not called during its start-up and added without an implementation; unknown signature -- [sceCoredumpConfigDumpMode](../../../core/libs/prx/libkernel/System/src/Coredump.cpp) (libkernel, NID `gzLt9Qrauk0`) - imported by PPSA16833, whose executable does not load without it; added without an implementation; unknown signature +- [sceCoredumpWriteUserString](../../../core/libs/prx/libkernel/System/src/Coredump.cpp) (libkernel, NID `32KQRUK13kI`) - `(uint32_t, const char* text, int)` follows the console's `libkernel.sprx` (SDK 4.5x build of `J03950001`) at 0x13050: a null `text` returns `0x81180000`; otherwise the three arguments go to syscall 601 (command 10), whose `EPERM` becomes `SCE_COREDUMP_ERROR_NOT_IN_COREDUMP_HANDLER` (0x81180003), `EINVAL` `0x81180000`, and a success returns the value the kernel wrote back. Measured on a PS5 (firmware 13.60) from a homebrew payload with no core dump handler: the call succeeds with 0, the kernel rejects a first argument above 15 or a third outside 0 to 256 with `0x81180000`, and a 2047-character text is accepted. Those checks are reproduced and anything else returns 0; nothing is recorded, as there is no dump, and what the two numbers mean is unknown. PPSA16064 imports it +- [sceCoredumpConfigDumpMode](../../../core/libs/prx/libkernel/System/src/Coredump.cpp) (libkernel, NID `gzLt9Qrauk0`) - `(int mode)` follows the same `libkernel.sprx` at 0x134D0: a mode other than 1 returns `0x81180000`; mode 1 issues syscall 601 (command 16, arguments 0 and 1) and returns 0 whether or not it fails, since only a raw return of 22 would map to `0x81180000`. No dump is produced here, so mode 1 returns 0 without the syscall. PPSA16833 imports it +- [sceCoredumpGetStopInfoGpu_Agc](../../../core/libs/prx/libkernel/System/src/Coredump.cpp) (libkernel, NID `Jrs7UUkGOFo`) - `(void* info, size_t size)` follows the same `libkernel.sprx` at 0x137E0: a null `info` returns `0x81180000`; syscall 601 (command 21) failing with `EPERM` returns `NOT_IN_COREDUMP_HANDLER`; otherwise two `libSceAgcDriver` imports (NIDs `+NIAY3DWpOw` and `LgctzJSGy7s`, names unknown) decide between `0x81180005` and, for a `size` of 8, storing the second one's 64-bit result and returning 0, with any other size returning `0x81180000`. The handler is never invoked here, so a non-null `info` returns `NOT_IN_COREDUMP_HANDLER` without writing, assuming `EPERM` outside the handler as above; the AGC branch is not reached - [sceKernelInternalMemoryGetModuleSegmentInfo](../../../core/libs/prx/libkernel/Module/src/ModuleInfo.cpp) (libkernel) - the `(InternalSegmentInfo* result)` signature, the 16-byte `{address, size}` result and the choice of the main program's first segment follow [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd43e6fddd1ab033e7980cd356d14c964905/kernel/ps4_libkernel.pas#L343-L355) only, which fills it through its `sceKernelGetLibkernelTextLocation` from the first segment of `eboot.bin`; whether the console reports that segment, the text of libkernel itself or the calling module's is unknown. The segment is the first one `sceKernelGetModuleInfo` reports for the main program, and 0 is returned. A null result throws. 44 audited titles that bundle their own `libc.prx` import it; no call has been observed - [sceKernelGetModuleInfoForUnwind](../../../core/libs/prx/libkernel/Module/src/Module.cpp) (libkernel) - on Linux the tables come from `sceKernelGetModuleInfoFromAddr`, whose `eh_frame_size` includes the zero terminator, as measured on firmware 13.60 (reported in [#2419](https://github.com/boykopovar/AnyPS5/issues/2419), not reproduced here); on Windows it is assumed to stop before the terminator. LLVM libunwind's `CFI_Parser::findFDE` stops at the terminator or the length, whichever comes first, so it finds the same FDEs either way. Only the main executable and `.guest.prx` images report tables; other host images report none, as on Windows - [sceKernelGetModuleInfoForUnwind](../../../core/libs/prx/libkernel/Module/src/Module.cpp) (libkernel) - on Windows the `.ehmeta` tables and the image file name are cached per allocation base and dropped after a loader free (`dlclose`, `sceKernelStopUnloadModule`, the thread atexit release) when the module is no longer loaded. A table changed in place, or an image freed by anything other than the loader, is not noticed. An address inside an image of the Windows loader's module list whose tables are cached is resolved with `RtlPcToFileHeader` and no `VirtualQuery`; other addresses, and images without tables, still use `VirtualQuery`. The `.eh_frame` walk that finds its end is done once per image instead of once per call From 3802f887f2ca04d8b5f8a904d0fa14ba95cde865 Mon Sep 17 00:00:00 2001 From: T3cm0 Date: Sat, 17 Oct 2026 16:17:22 -0500 Subject: [PATCH 0863/1000] fix(libc): take sceLibcMspaceReallocalign's alignment before its size Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libc/src/Mspace.cpp | 14 ++++++-------- core/libs/tests/Mspace.cpp | 19 ++++++++++++------- docs/dev/debt/libc.md | 1 + 3 files changed, 19 insertions(+), 15 deletions(-) diff --git a/core/libs/prx/libc/src/Mspace.cpp b/core/libs/prx/libc/src/Mspace.cpp index 143d50eba2..dbd74b1203 100644 --- a/core/libs/prx/libc/src/Mspace.cpp +++ b/core/libs/prx/libc/src/Mspace.cpp @@ -1,5 +1,6 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include +#include #include #include #include @@ -265,19 +266,15 @@ void* APS5_VABI sceLibcMspaceAlignedAlloc_nid_postfix(void* handle, std::size_t return sceLibcMspaceMemalign_nid_postfix(handle, alignment, size); } -void* APS5_VABI sceLibcMspaceReallocalign_nid_postfix(void* handle, void* pointer, std::size_t size, std::size_t alignment) { - if (alignment == 0 || (alignment & (alignment - 1))) { - Error(22); - return nullptr; - } - const auto effective = std::max(alignment, 16); +void* APS5_VABI sceLibcMspaceReallocalign_nid_postfix(void* handle, void* pointer, std::size_t alignment, std::size_t size) { + if (alignment >> 63) return nullptr; + const auto effective = std::bit_ceil(std::max(alignment, MinimumAlignment)); std::lock_guard lock(arenaMutex); auto* arena = Find(handle); if (!pointer) return Allocate(arena, size, effective); std::map::iterator chunk; if (!FindUsed(arena, pointer, chunk)) return nullptr; - if (!size) { Release(*arena, chunk); return nullptr; } - const auto needed = AlignUp(size, Granule); + const auto needed = AlignUp(std::max(size, 1), MinimumAlignment); if (needed < size) { Error(12); return nullptr; } const auto start = chunk->first; const auto capacity = chunk->second.end - start; @@ -311,4 +308,5 @@ std::size_t APS5_VABI sceLibcMspaceMallocUsableSize_nid_postfix(const void* poin Error(22); return 0; } + } diff --git a/core/libs/tests/Mspace.cpp b/core/libs/tests/Mspace.cpp index 386435ded6..8a3571292f 100644 --- a/core/libs/tests/Mspace.cpp +++ b/core/libs/tests/Mspace.cpp @@ -52,7 +52,7 @@ static void CheckAlignment() { Require(aligned(blocks[0])); void* small = sceLibcMspaceMemalign_nid_postfix(arena, 16, 40); Require(aligned(small)); - void* moved = sceLibcMspaceReallocalign_nid_postfix(arena, nullptr, 40, 8); + void* moved = sceLibcMspaceReallocalign_nid_postfix(arena, nullptr, 8, 40); Require(aligned(moved)); void* posix = nullptr; Require(sceLibcMspacePosixMemalign_nid_postfix(arena, &posix, 8, 40) == 0 && aligned(posix)); @@ -102,7 +102,7 @@ static void CheckMovedBlockKeepsUsableBytes() { void* other = sceLibcMspaceMalloc_nid_postfix(arena, 1); const auto otherUsable = fill(other); Require(sceLibcMspaceMalloc_nid_postfix(arena, 1) != nullptr); - void* realigned = sceLibcMspaceReallocalign_nid_postfix(arena, other, 4096, 256); + void* realigned = sceLibcMspaceReallocalign_nid_postfix(arena, other, 256, 4096); Require(realigned != nullptr && realigned != other && kept(realigned, otherUsable)); Require(sceLibcMspaceDestroy_nid_postfix(arena) == 0); } @@ -126,7 +126,7 @@ int main() { Require(sceLibcMspaceRealloc_nid_postfix(arena, first, size) == nullptr); Require(*__error_nid_postfix() == 12); *__error_nid_postfix() = 0; - Require(sceLibcMspaceReallocalign_nid_postfix(arena, first, size, 16) == nullptr); + Require(sceLibcMspaceReallocalign_nid_postfix(arena, first, 16, size) == nullptr); Require(*__error_nid_postfix() == 12); Require(sceLibcMspaceMallocUsableSize_nid_postfix(first) == 128); for (int i = 0; i < 128; ++i) Require(first[i] == i); @@ -194,12 +194,17 @@ int main() { unsigned char* realigned = static_cast(sceLibcMspaceReallocalign_nid_postfix(arena, nullptr, 64, 64)); Require(realigned && (reinterpret_cast(realigned) & 63) == 0); for (int i = 0; i < 64; ++i) realigned[i] = static_cast(i + 1); - unsigned char* regrown = static_cast(sceLibcMspaceReallocalign_nid_postfix(arena, realigned, 256, 64)); + unsigned char* regrown = static_cast(sceLibcMspaceReallocalign_nid_postfix(arena, realigned, 64, 256)); Require(regrown && (reinterpret_cast(regrown) & 63) == 0); for (int i = 0; i < 64; ++i) Require(regrown[i] == static_cast(i + 1)); - Require(sceLibcMspaceReallocalign_nid_postfix(arena, regrown, 16, 0) == nullptr); - Require(sceLibcMspaceReallocalign_nid_postfix(arena, regrown, 16, 3) == nullptr); - Require(sceLibcMspaceReallocalign_nid_postfix(arena, regrown, 0, 16) == nullptr); + *__error_nid_postfix() = 0; + Require(sceLibcMspaceReallocalign_nid_postfix(arena, regrown, 0, 16) == regrown); + Require(sceLibcMspaceReallocalign_nid_postfix(arena, regrown, 3, 16) == regrown); + Require(sceLibcMspaceReallocalign_nid_postfix(arena, regrown, std::size_t{1} << 63, 16) == nullptr); + Require(sceLibcMspaceReallocalign_nid_postfix(arena, regrown, 64, 0) == regrown && regrown[63] == 64); + Require(*__error_nid_postfix() == 0); + unsigned char* rounded = static_cast(sceLibcMspaceReallocalign_nid_postfix(arena, nullptr, 100, 0)); + Require(rounded && (reinterpret_cast(rounded) & 127) == 0); Require(sceLibcMspaceMallocStatsFast_nid_postfix(arena, &stats) == 0 && stats.currentInuseSize >= 100); sceLibcMspaceFree_nid_postfix(arena, memaligned); void* region = sceLibcMspaceMalloc_nid_postfix(arena, 8192); diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index ff2096892f..038605b678 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -18,6 +18,7 @@ - [malloc_usable_size](../../../core/libs/prx/libc/src/ApplicationHeap.cpp) (libc) - with the default allocator it returns the size the block was requested with (the range registered for it), not the rounded-up chunk size Sony's allocator reports; both satisfy "at least the requested size", and bytes past the request would not be write-watched. With an application allocator it calls entry 9 of the replacement table (`malloc_usable_size` in KytyPS5's `MallocReplace` order) and throws when the table leaves it null, like `malloc_stats_fast` - [malloc, calloc, realloc, memalign, aligned_alloc and reallocalign](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) - follow FreeBSD 11.4 `contrib/jemalloc/src/jemalloc.c` for failures, not measured on the console: a size the default heap rejects or the guest arena cannot place (including a `calloc` product that overflows) returns null with `errno` `ENOMEM`, and `memalign` and `aligned_alloc` return null with `EINVAL` for an alignment that is zero or not a power of two. FreeBSD's libc has no `memalign`, so setting `errno` there is assumed. A host commit failure for a block that fits in the arena (Windows `ERROR_NOT_ENOUGH_MEMORY`, `ERROR_OUTOFMEMORY` or `ERROR_COMMITMENT_LIMIT`, Linux `ENOMEM`) resets and releases the reserved range and returns null with `ENOMEM`; any other commit failure releases the range and throws. `reallocalign` returns null with `EINVAL` for an invalid alignment and `ENOMEM` like the others, which is an assumption as it is not in FreeBSD. `realloc(ptr, 0)` frees the block and returns null, where FreeBSD 11.4's jemalloc 4.5 resizes it to one byte and returns a block - [reallocf](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libSceLibcInternal, implemented in libc) - follows FreeBSD 11 `lib/libc/stdlib/reallocf.c` on top of this libc's `realloc`, not measured on the console: size 0 frees the block and returns null as `realloc` does here, so the block is not freed twice; when `realloc` fails because the application allocator returns null or the default heap rejects the size, the block is freed through the same allocator, `errno` is `ENOMEM` and null is returned. Other failures (a commit failure that is not out of memory, an unknown pointer) throw, as they do for `realloc` +- [sceLibcMspaceReallocalign](../../../core/libs/prx/libc/src/Mspace.cpp) (libSceLibcInternal, implemented in libc) takes `(void* heap, void* block, size_t alignment, size_t size)`, the order of the console's `libSceLibcInternal.sprx` (SDK 4.5x build of `J03950001`, 0x13880, whose internal routine at 0x12390 treats the third argument as the alignment and, for a null block, passes it to the routine `sceLibcMspaceMemalign` uses). Measured on a PS5 (firmware 13.60) from a homebrew payload: a null block with alignment 4096 and size 100 returns a 4096-aligned block; an alignment of 0 or 3 succeeds and 100 gives a 128-aligned block, as the routine raises it to at least 32 and to the next power of two; an alignment of 2^63 or more returns null, keeps the block and leaves `errno` alone; and a size of 0 returns a block (the same one when it is already suitable) instead of freeing it. A block that is aligned and large enough is kept where it is, without giving back the bytes past the new size as the console's in-place resize does - [acosl](../../../core/libs/prx/libc/src/Math.cpp) (libc) - computed by the host's math library, like the other libc math functions here, not by the console's (FreeBSD msun ld80 code in Sony's `libc.prx`): results can differ from the console's in the last bit, and on Linux glibc sets `errno` to `EDOM` for `acosl` outside [-1, 1] where msun only raises the floating-point exception. `long double` is the x87 80-bit format, as Sony's `frexpl` uses it (`fldt 0x8(%rsp)`, result in `st(0)`) - [atanl, sinl, cosl, tanl, sinhl, coshl, tanhl, expl, exp2l, logl, log10l and powl](../../../core/libs/prx/libc/src/Math.cpp) (libc) - computed by the host's math library, as `acosl` is, not by the console's FreeBSD msun ld80 code: results can differ from the console's in the last bit (glibc's `log10l(1000)` is not exactly 3), and on Linux glibc sets `errno` to `EDOM` or `ERANGE` where msun only raises the floating-point exception. `ldexpl` and `truncl` are exact operations and give the console's result; `logb` and `remainder` give the console's value too, but on Linux glibc sets `errno` (`ERANGE` for `logb(0)`, `EDOM` for `remainder` by zero or of an infinity) where msun only raises the floating-point exception - [std_execute_once](../../../core/libs/prx/libc/Export.cpp) (libc) - assumed to be the plain-named twin of `std::_Execute_once` (`_ZSt13_Execute_onceRSt9once_flagPFiPvS1_PS1_ES1_`, the callee of Dinkumware `call_once`): it delegates there and gets the [thread.once.callonce] semantics already verified for that symbol (one invocation per flag, concurrent callers block until it completes, and a failing or throwing invocation leaves the flag unready so the next caller runs it). The name equivalence is not confirmed on a title, and the callback convention (called with (null, arg, null), non-zero return meaning success) is the twin's, unverified for this symbol From 22c584acd39f4f3a40f5cd61d4ace063b135de13 Mon Sep 17 00:00:00 2001 From: T3cm0 Date: Sat, 17 Oct 2026 16:17:27 -0500 Subject: [PATCH 0864/1000] feat(libc): implement clearerr_unlocked, fileno_unlocked and fgetln Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libc/src/StandardStreams.cpp | 13 +++++++++++++ core/libs/tests/GuestStandardStreams.cpp | 20 ++++++++++++++++++++ docs/dev/debt/libc.md | 1 + 3 files changed, 34 insertions(+) diff --git a/core/libs/prx/libc/src/StandardStreams.cpp b/core/libs/prx/libc/src/StandardStreams.cpp index 8d136d05b4..9d59cff429 100644 --- a/core/libs/prx/libc/src/StandardStreams.cpp +++ b/core/libs/prx/libc/src/StandardStreams.cpp @@ -1,6 +1,9 @@ #include "prx/libc/include/FileStream.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include +#include +#include +#include #ifdef _WIN32 #include #else @@ -134,6 +137,16 @@ int APS5_VABI fileno_nid_postfix(FileStream* stream) { stream->GuestState().descriptor = static_cast(descriptor); return descriptor; } +void APS5_VABI clearerr_unlocked_nid_postfix(FileStream* stream) { + stream->ClearError(); +} +int APS5_VABI fileno_unlocked_nid_postfix(FileStream* stream) { + if (stream == nullptr || !stream->IsOpen()) return -1; + return fileno_nid_postfix(stream); +} +[[noreturn]] char* APS5_VABI fgetln_nid_postfix(FileStream*, std::size_t*) { + throw std::runtime_error("fgetln: the console's export is a hlt instruction, which ends the process"); +} int APS5_VABI setvbuf_nid_postfix(FileStream* stream, char* buffer, int mode, std::size_t size) { if (stream && !stream->IsOpen()) { errno = 9; return -1; } if (mode < 0 || mode > 2) return -1; diff --git a/core/libs/tests/GuestStandardStreams.cpp b/core/libs/tests/GuestStandardStreams.cpp index 9e99c39e09..dca63ca158 100644 --- a/core/libs/tests/GuestStandardStreams.cpp +++ b/core/libs/tests/GuestStandardStreams.cpp @@ -22,6 +22,9 @@ #endif extern "C" { FileStream* APS5_VABI fopen_nid_postfix(const char*, const char*); +void APS5_VABI clearerr_unlocked_nid_postfix(FileStream*); +int APS5_VABI fileno_unlocked_nid_postfix(FileStream*); +char* APS5_VABI fgetln_nid_postfix(FileStream*, std::size_t*); std::size_t APS5_VABI fread_nid_postfix(void*, std::size_t, std::size_t, FileStream*); std::size_t APS5_VABI fwrite_nid_postfix(const void*, std::size_t, std::size_t, FileStream*); FileStream* APS5_VABI freopen_nid_postfix(const char*, const char*, FileStream*); @@ -169,6 +172,22 @@ static void CheckUnlockedStreams() { Require(std::filesystem::remove(filename)); } +static void CheckUnlockedQueries() { + FileStream stream(std::tmpfile()); + Require(fputc_nid_postfix(0x7f, &stream) == 0x7f); + std::rewind(stream.GetHandle()); + Require(fgetc_nid_postfix(&stream) == 0x7f && feof_nid_postfix(&stream) == 0); + Require(fgetc_nid_postfix(&stream) == -1 && feof_nid_postfix(&stream) != 0); + clearerr_unlocked_nid_postfix(&stream); + Require(feof_nid_postfix(&stream) == 0 && ferror_nid_postfix(&stream) == 0); + Require(fileno_unlocked_nid_postfix(&stream) == fileno_nid_postfix(&stream) && fileno_unlocked_nid_postfix(&stream) >= 0); + Require(fileno_unlocked_nid_postfix(nullptr) == -1); + bool halted = false; + std::size_t length = 0; + try { fgetln_nid_postfix(&stream, &length); } catch (const std::runtime_error&) { halted = true; } + Require(halted); +} + static void CheckTryLock() { FileStream stream(std::tmpfile()); const auto tryFromOtherThread = [&] { @@ -843,5 +862,6 @@ int main(int argc, char** argv) { CheckUnlockedStreamLock(false); CheckUnlockedStreamLock(true); CheckTryLock(); + CheckUnlockedQueries(); return CheckBinaryModes() ? 0 : 1; } diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index ff2096892f..d3ca1c8970 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -47,6 +47,7 @@ - [getc_unlocked, putc_unlocked, getchar_unlocked, putchar_unlocked](../../../core/libs/prx/libc/src/StandardStreams.cpp) (libc) - byte I/O uses the host unlocked operations; guest EOF/error flags and macro counters follow `FileStream`. Linux status queries use `feof_unlocked`/`ferror_unlocked` because [glibc `feof`](https://github.com/bminor/glibc/blob/d2097651cc57834dbfcaa102ddfacae0d86cfb66/libio/feof.c) and [`ferror`](https://github.com/bminor/glibc/blob/d2097651cc57834dbfcaa102ddfacae0d86cfb66/libio/ferror.c) can acquire the stream lock. Windows uses [the CRT no-lock byte operations](https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/fgetc-nolock-fgetwc-nolock) and `feof`/`ferror`, verified under a held stream lock on UCRT. Console behavior and other host CRTs remain unverified. - [fgetws, getwc, getwchar, putwc, putwchar](../../../core/libs/prx/libc/src/FormattingWide.cpp) (libc) - wide stream I/O encodes and decodes wide characters as UTF-8 on the underlying byte stream; `fgetws` reads characters into the destination buffer until newline or EOF and null-terminates, returning null on stream error. Console wide stream buffering and encoding details remain unverified. - [ftrylockfile](../../../core/libs/prx/libc/src/RuntimeSupport.cpp) (libc) - tries the host CRT stream lock that `flockfile`, `funlockfile` and each locking host stdio call on the stream take, so it returns -1 while another thread holds the stream through `flockfile` or is inside such a call, and 0 when it acquires the lock or already owns it (recursively, released by as many `funlockfile` calls), as FreeBSD 11 [`_ftrylockfile`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/stdio/_flock_stub.c#L89-L104) does. A libc export holds the lock only for each host call it makes, not across several host calls or while it updates the guest `FILE` prefix. Linux uses glibc `ftrylockfile`. UCRT has no try variant of `_lock_file`, so Windows calls `TryEnterCriticalSection` on the `CRITICAL_SECTION` that `_lock_file` enters, found through the private UCRT stream layout (`__crt_stdio_stream_data` in `ucrt/inc/corecrt_internal_stdio.h`, unchanged from Windows SDK 10.0.19041 to 10.0.26100); another CRT or a change of that layout breaks it. A null or closed stream throws where FreeBSD dereferences it +- [fgetln](../../../core/libs/prx/libc/src/StandardStreams.cpp) (libSceLibcInternal, implemented in libc) throws: in the console's `libSceLibcInternal.sprx` (SDK 4.5x build of `J03950001`, 0x5FE80) the export is a `hlt` instruction, which faults in user mode and ends the process - [Wide-character conversions](../../../core/libs/prx/libc/src/Multibyte.cpp) (libc) - The wide `wcsto*` functions parse the ASCII prefix of the guest string, as the C locale accepts only ASCII white space and digits. [`wcsrtombs`](../../../core/libs/prx/libc/src/Multibyte.cpp) uses the guest's single-byte C locale through `wcrtomb`; other encodings and shift states are not supported - [realpath](../../../core/libs/prx/libc/src/RealPath.cpp) (libc) follows FreeBSD 11's [`realpath.c`](https://github.com/freebsd/freebsd-src/blob/stable/11/lib/libc/stdlib/realpath.c) except for symlinks. The console's guest filesystem has none, so a host symlink under the guest root is followed for existence and type but not expanded, and `..` drops the previous component lexically, as `Resolve` does for every guest path. Through a host link `linkdir -> sub/deep`, `linkdir/../x` gives `x` beside the link, not beside its target. - [strtod, strtof, strtold](../../../core/libs/prx/libc/src/Strings.cpp) (libc) - `NAN(n-char-sequence)` converts to the quiet NaN that `nan` and `nanf` return, negated after a minus sign, and ignores the sequence as they ignore their tag. This is inferred from C17 7.12.11.2p2, which makes `nan("n")` equivalent to `strtod("NAN(n)", NULL)`, and not observed on the console. No console `nanl` was observed, so `strtold` returns the x87 default quiet NaN (exponent `0x7fff`, significand `0xc000000000000000`) From 5b12f1c3d8788be8d2b3fac5b1cef6ea53def3fc Mon Sep 17 00:00:00 2001 From: milescoviello Date: Sat, 17 Oct 2026 20:45:37 +0000 Subject: [PATCH 0865/1000] perf(agc): share unchanged shader registry chunks between published versions PublishRegisteredShader copied the whole code-address map, with every header vector and snapshot reference, for each registration under the driver mutex, so a title registering its shaders while it loads paid O(n^2). The registry now keeps its entries in address-ordered chunks of at most 64 shared by every version that has not changed them; a write clones only the chunk it touches. Co-Authored-By: Claude Opus 5.5 --- .../include/Driver/Shaders/ShaderRegistry.hpp | 91 ++++++++++++++++++- .../src/Driver/Shaders/ShaderRegistry.cpp | 66 ++++++++++++++ .../tests/execution/PreparedPublication.cpp | 69 ++++++++++++++ 3 files changed, 225 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp index ff30cce469..81ebec7f75 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp @@ -13,10 +13,12 @@ #include #include #include +#include #include #include #include #include +#include #include struct Shader; @@ -32,7 +34,94 @@ class VulkanDevice; namespace AgcDriver::DriverDetail { struct ShaderSnapshot; -using ShaderRegistry = std::map>>; + +class ShaderRegistry { + using Chunk = std::map>>; + using Chunks = std::vector>; + +public: + using key_type = std::uint64_t; + using mapped_type = Chunk::mapped_type; + using value_type = Chunk::value_type; + + class const_iterator { + public: + using iterator_category = std::bidirectional_iterator_tag; + using value_type = ShaderRegistry::value_type; + using difference_type = std::ptrdiff_t; + using pointer = const value_type*; + using reference = const value_type&; + + const_iterator() = default; + reference operator*() const { return *entry; } + pointer operator->() const { return &*entry; } + const_iterator& operator++() { + if (++entry == (*chunk)->end()) { + ++chunk; + entry = chunk == last ? Chunk::const_iterator{} : (*chunk)->cbegin(); + } + return *this; + } + const_iterator operator++(int) { + auto previous = *this; + ++*this; + return previous; + } + const_iterator& operator--() { + if (chunk == last || entry == (*chunk)->cbegin()) { + --chunk; + entry = std::prev((*chunk)->cend()); + } else { + --entry; + } + return *this; + } + const_iterator operator--(int) { + auto previous = *this; + --*this; + return previous; + } + bool operator==(const const_iterator& other) const { return chunk == other.chunk && entry == other.entry; } + + private: + friend class ShaderRegistry; + const_iterator(Chunks::const_iterator chunk, Chunks::const_iterator last, Chunk::const_iterator entry) : chunk(chunk), last(last), entry(entry) {} + Chunks::const_iterator chunk{}; + Chunks::const_iterator last{}; + Chunk::const_iterator entry{}; + }; + using iterator = const_iterator; + + ShaderRegistry() = default; + ShaderRegistry(std::initializer_list entries) { + for (const auto& [address, headers] : entries) emplace(address, headers); + } + + const_iterator begin() const { return chunks.empty() ? end() : const_iterator(chunks.cbegin(), chunks.cend(), chunks.front()->cbegin()); } + const_iterator end() const { return const_iterator(chunks.cend(), chunks.cend(), {}); } + const_iterator find(std::uint64_t address) const; + const_iterator upper_bound(std::uint64_t address) const; + bool contains(std::uint64_t address) const { return find(address) != end(); } + const mapped_type& at(std::uint64_t address) const; + std::size_t size() const { return count; } + bool empty() const { return count == 0; } + mapped_type& operator[](std::uint64_t address); + std::pair emplace(std::uint64_t address, mapped_type headers); + std::size_t erase(std::uint64_t address); + void clear() { + chunks.clear(); + count = 0; + } + +private: + static constexpr std::size_t ChunkEntries = 64; + + std::size_t chunkFor(std::uint64_t address) const; + Chunk& writable(std::size_t index); + + Chunks chunks; + std::size_t count = 0; +}; struct PreparedShaderState { struct Entry { diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp index 1b1299da12..6b8535e6a2 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp @@ -415,6 +415,72 @@ bool SameHeader(const ShaderSnapshot& snapshot, const Shader* shader) { return std::memcmp(&used, ®istered, offsetof(Shader, num_sh_registers) + sizeof(Shader::num_sh_registers)) == 0 || RelocatedRegisters(snapshot, used, registered) || FusedHullHeader(snapshot, used, registered); } +std::size_t ShaderRegistry::chunkFor(std::uint64_t address) const { + const auto above = std::upper_bound(chunks.begin(), chunks.end(), address, [](std::uint64_t key, const std::shared_ptr& chunk) { return key < chunk->begin()->first; }); + return above == chunks.begin() ? 0u : static_cast(above - chunks.begin()) - 1u; +} + +ShaderRegistry::Chunk& ShaderRegistry::writable(std::size_t index) { + auto& chunk = chunks[index]; + if (chunk.use_count() != 1) chunk = std::make_shared(*chunk); + return *chunk; +} + +ShaderRegistry::const_iterator ShaderRegistry::find(std::uint64_t address) const { + if (chunks.empty()) return end(); + const auto index = chunkFor(address); + const auto found = chunks[index]->find(address); + if (found == chunks[index]->end()) return end(); + return const_iterator(chunks.cbegin() + static_cast(index), chunks.cend(), found); +} + +ShaderRegistry::const_iterator ShaderRegistry::upper_bound(std::uint64_t address) const { + if (chunks.empty()) return end(); + const auto index = chunkFor(address); + const auto found = chunks[index]->upper_bound(address); + if (found != chunks[index]->end()) return const_iterator(chunks.cbegin() + static_cast(index), chunks.cend(), found); + if (index + 1u == chunks.size()) return end(); + return const_iterator(chunks.cbegin() + static_cast(index + 1u), chunks.cend(), chunks[index + 1u]->cbegin()); +} + +const ShaderRegistry::mapped_type& ShaderRegistry::at(std::uint64_t address) const { + const auto found = find(address); + if (found == end()) throw std::out_of_range("shader registry has no entry for the code address"); + return found->second; +} + +ShaderRegistry::mapped_type& ShaderRegistry::operator[](std::uint64_t address) { + if (chunks.empty()) chunks.push_back(std::make_shared()); + const auto index = chunks.front()->empty() ? 0u : chunkFor(address); + auto& chunk = writable(index); + const auto [entry, inserted] = chunk.try_emplace(address); + auto& headers = entry->second; + if (!inserted) return headers; + ++count; + if (chunk.size() > ChunkEntries) { + auto upper = std::make_shared(); + for (auto moved = std::next(chunk.begin(), static_cast(chunk.size() / 2u)); moved != chunk.end();) upper->insert(upper->end(), chunk.extract(moved++)); + chunks.insert(chunks.begin() + static_cast(index + 1u), std::move(upper)); + } + return headers; +} + +std::pair ShaderRegistry::emplace(std::uint64_t address, mapped_type headers) { + if (const auto found = find(address); found != end()) return {found, false}; + (*this)[address] = std::move(headers); + return {find(address), true}; +} + +std::size_t ShaderRegistry::erase(std::uint64_t address) { + if (!contains(address)) return 0; + const auto index = chunkFor(address); + auto& chunk = writable(index); + chunk.erase(address); + --count; + if (chunk.empty()) chunks.erase(chunks.begin() + static_cast(index)); + return 1; +} + std::shared_ptr RegisteredHeader(const ShaderRegistry& registry, const Shader* shader) { const auto found = registry.find(reinterpret_cast(const_cast(GuestShaderFields(shader).code))); if (found == registry.end()) return {}; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedPublication.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedPublication.cpp index d0a96ffef6..beaed11d60 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedPublication.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedPublication.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -152,6 +153,73 @@ void HeaderSelection() { Reject([&] { static_cast(RegisteredProgram(*registry, 0x10000, {2, 4})); }); } +void RegistryVersions() { + using Expected = std::map>; + const auto matches = [](const ShaderRegistry& current, const Expected& expected) { + const auto same = [](const auto& entry, const auto& wanted) { return entry.first == wanted.first && entry.second.size() == 1u && entry.second.front() == wanted.second; }; + if (current.size() != expected.size() || !std::equal(current.begin(), current.end(), expected.begin(), expected.end(), same)) return false; + auto backward = current.end(); + for (auto wanted = expected.rbegin(); wanted != expected.rend(); ++wanted) { + if (backward == current.begin() || !same(*--backward, *wanted)) return false; + } + if (backward != current.begin()) return false; + for (const auto& entry : expected) { + for (const auto probe : {std::uint64_t{0}, entry.first - 1u, entry.first, entry.first + 4u, ~std::uint64_t{0}}) { + const auto found = current.upper_bound(probe); + const auto wanted = expected.upper_bound(probe); + if ((found == current.end()) != (wanted == expected.end()) || (found != current.end() && found->first != wanted->first)) return false; + if (current.contains(probe) != expected.contains(probe) || (current.find(probe) == current.end()) == expected.contains(probe)) return false; + } + } + return true; + }; + const auto makeSnapshot = [](std::uint64_t codeAddress) { + auto snapshot = std::make_shared(); + snapshot->codeAddress = codeAddress; + snapshot->headerAddress = codeAddress + 0x80u; + snapshot->code = {0xbf810000u}; + return snapshot; + }; + std::vector> snapshots; + Expected expected; + std::shared_ptr registry; + std::uint64_t state = 1; + while (snapshots.size() < 4096u) { + state = state * 6364136223846793005ull + 1442695040888963407ull; + const auto address = 0x100000u + ((state >> 33u) % 0x100000u) * 0x100u; + if (expected.contains(address)) continue; + const auto snapshot = makeSnapshot(address); + expected.emplace(address, snapshot); + snapshots.push_back(snapshot); + PublishRegisteredShader(registry, snapshot); + } + Require(matches(*registry, expected), "the shader registry did not keep its code addresses in order"); + std::shared_ptr earlier = registry; + const auto previous = expected; + const auto added = makeSnapshot(0x100040u); + expected.emplace(added->codeAddress, added); + std::vector uses; + for (const auto& snapshot : snapshots) uses.push_back(snapshot.use_count()); + PublishRegisteredShader(registry, added); + std::size_t copied = 0; + for (std::size_t index = 0; index < snapshots.size(); ++index) copied += snapshots[index].use_count() != uses[index] ? 1u : 0u; + Require(copied <= 128u, "publishing a shader copied the registry's entries for other code addresses"); + Require(matches(*registry, expected) && matches(*earlier, previous), "publishing a shader changed an earlier registry or lost an entry"); + ShaderRegistry edited = *registry; + auto remaining = expected; + std::size_t index = 0; + for (const auto& entry : expected) { + if (index++ % 3u != 0u) continue; + Require(edited.erase(entry.first) == 1u && edited.erase(entry.first) == 0u, "the shader registry did not erase a code address once"); + remaining.erase(entry.first); + } + Require(matches(edited, remaining) && matches(*registry, expected), "erasing from a copy of the registry changed the original or lost an entry"); + for (const auto& entry : remaining) edited.erase(entry.first); + Require(edited.empty() && edited.begin() == edited.end() && edited.upper_bound(0) == edited.end(), "an emptied shader registry kept entries"); + Require(edited.emplace(added->codeAddress, ShaderRegistry::mapped_type{added}).second && !edited.emplace(added->codeAddress, ShaderRegistry::mapped_type{}).second, "an emptied shader registry did not take a new entry once"); + Require(edited.size() == 1u && edited.at(added->codeAddress).size() == 1u && edited.at(added->codeAddress).front() == added, "an emptied shader registry lost its new entry"); +} + void FusedHullHeaderSelection() { struct Hull { Shader shader{}; @@ -344,6 +412,7 @@ int main() { try { Transactions(); HeaderSelection(); + RegistryVersions(); FusedHullHeaderSelection(); TransactionLock(); PreparationPriority(false); From 1bc029da6d63bfba4be5df9c97099dd095063e14 Mon Sep 17 00:00:00 2001 From: LaunchDoesntCode Date: Sat, 17 Oct 2026 16:08:47 -0500 Subject: [PATCH 0866/1000] fix(libkernel): leave a blocked SIGALRM expiry pending An ITIMER_REAL or alarm expiry while SIGALRM is blocked is marked on the pending set of sigtimedwait instead of throwing, as kern_psignal leaves a blocked signal pending. sigwait, sigwaitinfo and sigtimedwait take it, repeated expiries merge into one pending signal, and unblocking it runs a guest handler on the unblocking thread or, with the default disposition, exits the process with status 142 as FreeBSD's default action does. The expiry rechecks the mask after marking the signal, so an unblock that races it does not leave SIGALRM pending while unblocked, and TakePending clears the bit with the value it read so one signal is taken once. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libkernel/CMakeLists.txt | 5 +- .../libs/prx/libkernel/System/src/Signals.cpp | 32 ++++++--- core/libs/tests/GuestItimer.cpp | 66 ++++++++++++++++++- docs/dev/debt/libkernel.md | 4 +- 4 files changed, 93 insertions(+), 14 deletions(-) diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 906fcf9cbf..1781e8751c 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -769,8 +769,11 @@ if(BUILD_TESTING) target_link_libraries(guest_itimer_tests PRIVATE libkernel libc) configure_windows_unwind(guest_itimer_tests) add_test(NAME guest_itimer COMMAND guest_itimer_tests) + add_test(NAME guest_itimer_pending_default COMMAND guest_itimer_tests pending-default) + set_tests_properties(guest_itimer_pending_default PROPERTIES TIMEOUT 20 PASS_REGULAR_EXPRESSION + "SIGALRM: a pending SIGALRM was unblocked with the default disposition; the process terminates") if(WIN32) - set_tests_properties(guest_itimer PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(guest_itimer guest_itimer_pending_default PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() add_test_executable(guest_parent_process_tests ${LIBS_INCLUDE_DIR}/tests/GuestParentProcess.cpp) diff --git a/core/libs/prx/libkernel/System/src/Signals.cpp b/core/libs/prx/libkernel/System/src/Signals.cpp index 2547ebc59c..c9216736c2 100644 --- a/core/libs/prx/libkernel/System/src/Signals.cpp +++ b/core/libs/prx/libkernel/System/src/Signals.cpp @@ -37,6 +37,7 @@ using GuestHandler = void (APS5_VABI *)(int); constexpr int MaxSignal = 128; constexpr int GuestSigkill = 9; constexpr int GuestSigstop = 17; +constexpr int GuestSigalrm = 14; constexpr int SaRestart = 0x2; constexpr int SaSiginfo = 0x40; std::atomic handlers[MaxSignal + 1]{}; @@ -112,8 +113,7 @@ bool TakePending(int guest) { auto& word = pending.words[SignalWord(guest)]; const std::uint32_t bit = SignalBit(guest); if ((word.load(std::memory_order_relaxed) & bit) == 0) return false; - word.fetch_and(~bit, std::memory_order_relaxed); - return true; + return (word.fetch_and(~bit, std::memory_order_relaxed) & bit) != 0; } bool PendingInSet(const GuestSignalSet& set, int* guest) { @@ -187,8 +187,18 @@ bool CaughtByGuest(int guest) { return Disposition(guest).handler > 1; } +bool TerminatesWithoutHandler(int guest) { + return guest == GuestSigalrm && Disposition(guest).handler == 0; +} + +[[noreturn]] void TerminateBySigalrm(const char* cause) { + std::fprintf(stderr, "SIGALRM: %s with the default disposition; the process terminates\n", cause); + std::fflush(stderr); + std::_Exit(128 + GuestSigalrm); +} + bool Deliverable(int guest) { - return NativeSignal(guest) != 0 || CaughtByGuest(guest); + return NativeSignal(guest) != 0 || CaughtByGuest(guest) || TerminatesWithoutHandler(guest); } bool TakePendingUnblocked(int* guest) { @@ -205,6 +215,7 @@ bool DeliverPendingSignal(int guest) { const GuestSigaction action = Disposition(guest); if (action.handler == 1) return false; if (action.handler > 1 && !Delivered(action)) throw std::runtime_error("signal: SA_SIGINFO handlers are not delivered"); + if (guest == GuestSigalrm && action.handler == 0) TerminateBySigalrm("a pending SIGALRM was unblocked"); const int native = NativeSignal(guest); if (native != 0) { if (std::raise(native) != 0) throw std::runtime_error("signal: delivering the signal to the host failed"); @@ -260,7 +271,6 @@ extern "C" int APS5_VABI _sigprocmask_nid_postfix(int how, const GuestSignalSet* } namespace { -constexpr int GuestSigalrm = 14; struct AlarmState { std::mutex lock; std::condition_variable changed; @@ -288,13 +298,14 @@ void ExpireAlarm(std::chrono::microseconds interval) { handler = dispositions[GuestSigalrm].handler; } if (handler == 1) return; - if (SignalBlocked(GuestSigalrm)) - throw std::runtime_error("ITIMER_REAL (it_interval " + DescribeInterval(interval) + ") expired while SIGALRM is blocked: pending signals are not implemented"); - if (handler == 0) { - std::fprintf(stderr, "SIGALRM: the alarm expired with the default disposition; the process terminates\n"); - std::fflush(stderr); - std::_Exit(128 + GuestSigalrm); + if (SignalBlocked(GuestSigalrm)) { + MarkPending(GuestSigalrm); + std::atomic_thread_fence(std::memory_order_seq_cst); + if (SignalBlocked(GuestSigalrm) || !TakePending(GuestSigalrm)) return; + handler = Disposition(GuestSigalrm).handler; + if (handler == 1) return; } + if (handler == 0) TerminateBySigalrm("the alarm expired"); throw std::runtime_error("ITIMER_REAL (it_interval " + DescribeInterval(interval) + ") expired: delivering SIGALRM to a guest handler is not implemented"); } @@ -553,6 +564,7 @@ int APS5_VABI _sigprocmask_nid_postfix(int how, const GuestSignalSet* set, Guest } } } + std::atomic_thread_fence(std::memory_order_seq_cst); for (int guest = 1; guest <= MaxSignal; ++guest) { if (SignalBlocked(guest) || !Deliverable(guest) || !TakePending(guest)) continue; DeliverPendingSignal(guest); diff --git a/core/libs/tests/GuestItimer.cpp b/core/libs/tests/GuestItimer.cpp index 8958441978..4e1e9d5845 100644 --- a/core/libs/tests/GuestItimer.cpp +++ b/core/libs/tests/GuestItimer.cpp @@ -17,6 +17,13 @@ struct GuestItimerval { KernelTimeval value; }; static_assert(sizeof(GuestItimerval) == 32 && offsetof(GuestItimerval, value) == 16); +struct GuestSignalSet { + std::uint32_t bits[4]; +}; +struct GuestTimespec { + std::int64_t seconds; + std::int64_t nanoseconds; +}; using Handler = void (APS5_VABI *)(int); using TerminateHandler = void (*)(); @@ -25,6 +32,9 @@ int APS5_VABI setitimer_nid_postfix(int, const GuestItimerval*, GuestItimerval*) int APS5_VABI getitimer_nid_postfix(int, GuestItimerval*); unsigned int APS5_VABI GuestAlarm_nid_no_patch(unsigned int); Handler APS5_VABI signal_nid_postfix(int, Handler); +int APS5_VABI _sigprocmask_nid_postfix(int, const GuestSignalSet*, GuestSignalSet*); +int APS5_VABI sigtimedwait_nid_postfix(const GuestSignalSet*, void*, const GuestTimespec*); +int APS5_VABI sigpending_nid_postfix(GuestSignalSet*); int* APS5_VABI __error_nid_postfix(); void* APS5_VABI __cxa_current_primary_exception_nid_postfix(); std::type_info* APS5_VABI __cxa_current_exception_type_nid_postfix(); @@ -64,6 +74,19 @@ static void APS5_VABI OnAlarm(int signal) { delivered = signal; } +static bool AlarmPending() { + GuestSignalSet pending{{0, 0, 0, 0}}; + Require(sigpending_nid_postfix(&pending) == 0, "sigpending failed"); + return (pending.bits[0] & (1u << 13)) != 0; +} + +static const GuestItimerval once{{0, 0}, {0, 20000}}; + +static void ExpireOnce() { + Require(setitimer_nid_postfix(0, &once, nullptr) == 0, "arming the blocked timer failed"); + std::this_thread::sleep_for(std::chrono::milliseconds(150)); +} + static void OnTerminate() { const auto* type = __cxa_current_exception_type_nid_postfix(); const auto* error = static_cast(__cxa_current_primary_exception_nid_postfix()); @@ -77,7 +100,18 @@ static void OnTerminate() { std::_Exit(1); } -int main() { +static int UnblockPendingDefault() { + const GuestSignalSet alarmSet{{1u << 13, 0, 0, 0}}; + Require(_sigprocmask_nid_postfix(1, &alarmSet, nullptr) == 0, "blocking SIGALRM failed"); + ExpireOnce(); + Require(AlarmPending(), "a blocked expiry with the default disposition was not left pending"); + Require(_sigprocmask_nid_postfix(2, &alarmSet, nullptr) == 0, "unblocking SIGALRM failed"); + std::fprintf(stderr, "unblocking a pending SIGALRM with the default disposition did not terminate\n"); + return 1; +} + +int main(int argc, char** argv) { + if (argc > 1 && std::strcmp(argv[1], "pending-default") == 0) return UnblockPendingDefault(); const auto ignore = reinterpret_cast(std::uintptr_t{1}); Require(signal_nid_postfix(14, ignore) == nullptr, "SIGALRM was not initially defaulted"); const GuestItimerval valid{{0, 0}, {30, 0}}; @@ -154,6 +188,36 @@ int main() { Require(setitimer_nid_postfix(0, &zero, &previous) == 0 && Micros(previous.value) > 0, "disarming the alarm failed"); Require(GuestAlarm_nid_no_patch(0) == 0, "setitimer did not cancel the alarm"); + const GuestSignalSet alarmSet{{1u << 13, 0, 0, 0}}; + const GuestTimespec poll{0, 0}; + const GuestTimespec patience{2, 0}; + GuestSignalSet unblocked{}; + Require(_sigprocmask_nid_postfix(1, &alarmSet, &unblocked) == 0, "blocking SIGALRM failed"); + ExpireOnce(); + Require(!AlarmPending(), "an ignored expiry was left pending"); + Require(signal_nid_postfix(14, nullptr) == ignore, "SIGALRM was not defaulted"); + Require(setitimer_nid_postfix(0, &once, nullptr) == 0, "arming the waited timer failed"); + Require(sigtimedwait_nid_postfix(&alarmSet, nullptr, &patience) == 14, "sigtimedwait did not take the blocked expiry"); + Require(!AlarmPending(), "the taken expiry is still pending"); + const GuestItimerval frequent{{0, 20000}, {0, 20000}}; + Require(setitimer_nid_postfix(0, &frequent, nullptr) == 0, "arming the frequent timer failed"); + std::this_thread::sleep_for(std::chrono::milliseconds(150)); + Require(setitimer_nid_postfix(0, &zero, nullptr) == 0, "disarming the frequent timer failed"); + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + Require(sigtimedwait_nid_postfix(&alarmSet, nullptr, &poll) == 14, "the repeated expiries were not left pending"); + *__error_nid_postfix() = 0; + Require(sigtimedwait_nid_postfix(&alarmSet, nullptr, &poll) == -1 && *__error_nid_postfix() == 35, + "the repeated expiries were not merged into one pending signal"); + Require(signal_nid_postfix(14, OnAlarm) == nullptr, "SIGALRM handler was not installed while blocked"); + ExpireOnce(); + Require(delivered == 0 && AlarmPending(), "a blocked expiry with a handler was not left pending"); + Require(_sigprocmask_nid_postfix(3, &unblocked, nullptr) == 0, "unblocking SIGALRM failed"); + Require(delivered == 14, "unblocking SIGALRM did not run its handler"); + Require(_sigprocmask_nid_postfix(1, &alarmSet, nullptr) == 0 && !AlarmPending(), "the delivered expiry is still pending"); + Require(_sigprocmask_nid_postfix(3, &unblocked, nullptr) == 0, "restoring the signal mask failed"); + delivered = 0; + Require(signal_nid_postfix(14, ignore) == OnAlarm, "SIGALRM was not ignored again"); + Require(signal_nid_postfix(14, OnAlarm) == ignore, "SIGALRM handler was not installed"); _ZSt13set_terminatePFvvE_nid_postfix(OnTerminate); const GuestItimerval handled{{0, 250000}, {0, 50000}}; diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 2fb9d30137..31ed41f9df 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -117,7 +117,7 @@ - [EVFILT_VNODE on macOS](../../../core/libs/prx/libkernel/Equeue/src/FileEvent.cpp) (libkernel) registers `EVFILT_VNODE` on a duplicate of the descriptor in a host kqueue, whose notes have FreeBSD's values, and drops the `NOTE_LINK` that XNU adds to `NOTE_DELETE` when the watched file or directory is removed, since FreeBSD's `vop_remove_post` and `vop_rmdir_post` report `NOTE_DELETE` alone. As on Windows, a directory registered with `NOTE_EXTEND` is accepted but never receives it: XNU reports a move between directories as `NOTE_WRITE` alone, with `NOTE_LINK` for a subdirectory - [Windows file identity](../../../core/libs/prx/libkernel/File/src/NativeStat.cpp) (libkernel) - `stat` and `fstat` report the volume serial number as `st_dev` and the 64-bit file index folded into the guest's 32-bit `st_ino` (never 0), so two different files can in principle share an `st_ino`; the fold is not taken from a reference. `st_nlink` is clamped to 65535, the width of the guest field. The call keeps the `_wstat64` values when `GetFileInformationByHandle` fails - [lstat of a Windows symbolic link](../../../core/libs/prx/libkernel/File/src/NativeStat.cpp) (libkernel) reports a link whose target is relative, as `symlink` creates them, with the link's own times, `st_dev` and `st_ino`, mode `S_IFLNK | 0777` and the length of the target `readlink` returns as `st_size`. FreeBSD gives a new link `0777` less the creating process's umask, which the host does not keep, so the permission bits are not the console's. A link with an absolute target, a junction and any other reparse point throw, as for `readlink` -- [setitimer](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) follows FreeBSD's `sys_setitimer` and `kern_setitimer`: `EINVAL` for timers other than `ITIMER_REAL`, `ITIMER_VIRTUAL` and `ITIMER_PROF`, and for a value, or the interval of a non-zero value, with a negative field, `tv_usec` of 1000000 or more, or more than `INT32_MAX / 2` seconds; a null value reads the timer as `getitimer` does, with `EFAULT` when the output is null too. `ITIMER_REAL` shares the timer thread of `alarm`, which clears its interval: a zero value disarms it, the previous value is returned, and each expiry re-arms it by `it_interval`. An expiry delivers nothing: with `SIG_IGN` it is dropped, with the default disposition the process exits with status 142, and with a handler, or with `SIGALRM` blocked, it throws, since guest `SIGALRM` handlers are never run and signals are never left pending. The value is not raised to `tick / 16` as `itimerfix` does, and an interval below 1 ms throws, since the console's `hz`, and so that rounding, is unknown (1 ms covers any `hz` of 63 or more). `ITIMER_VIRTUAL` and `ITIMER_PROF` throw when armed, as process CPU time is not accounted, and read and disarm as zero. PPSA99420 imports it and fails to load without it +- [setitimer](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) follows FreeBSD's `sys_setitimer` and `kern_setitimer`: `EINVAL` for timers other than `ITIMER_REAL`, `ITIMER_VIRTUAL` and `ITIMER_PROF`, and for a value, or the interval of a non-zero value, with a negative field, `tv_usec` of 1000000 or more, or more than `INT32_MAX / 2` seconds; a null value reads the timer as `getitimer` does, with `EFAULT` when the output is null too. `ITIMER_REAL` shares the timer thread of `alarm`, which clears its interval: a zero value disarms it, the previous value is returned, and each expiry re-arms it by `it_interval`. With `SIG_IGN` an expiry is dropped. With `SIGALRM` blocked it is left on the pending set of `sigtimedwait`, as `kern_psignal` does, where `sigwait`, `sigtimedwait` and `sigwaitinfo` take it; unblocking it runs a guest handler on the unblocking thread, and with the default disposition exits the process with status 142. Unblocked, the default disposition exits the process with status 142 and a handler throws, since guest `SIGALRM` handlers are never run from the timer thread. The value is not raised to `tick / 16` as `itimerfix` does, and an interval below 1 ms throws, since the console's `hz`, and so that rounding, is unknown (1 ms covers any `hz` of 63 or more). `ITIMER_VIRTUAL` and `ITIMER_PROF` throw when armed, as process CPU time is not accounted, and read and disarm as zero. PPSA99420 imports it and fails to load without it - On Linux, [direct memory](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) lives in one sparse memfd at file offset = physical address, so the [GPU imports](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) can share one udmabuf buffer object per 64 MiB of it. Released direct memory is not punched out of the file, because a live import of its chunk pins the pages and the GPU would keep reading the old ones: its pages stay committed (the process keeps the high-water mark of the direct memory it used) and are zeroed when the range is allocated again. - [system](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) runs no command. It follows FreeBSD 11's `lib/libc/stdlib/system.c` for a sandbox without `/bin/sh`: a null command returns 1 unconditionally, as that version does, and any command returns the wait status of a child that could not exec the shell and called `_exit(127)` (`0x7f00`). Whether the console's application sandbox has no shell, and whether `vfork` succeeds there (FreeBSD returns -1 when it fails), is unverified - [sceKernelWriteThrottlingStatus](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) reports no write throttling: the host SSD has no write budget, so the first qword of the 32-byte status is set to `UINT32_MAX` and the rest is zeroed; returns `EFAULT` (`0x8002000e`) when passed a null status buffer pointer. PPSA17221 reads only that qword: it multiplies it by 64 KiB and compares the product with byte thresholds up to 0x4AF00000 to pick one of five tiers. When the call fails the title assumes 0x4B000001 bytes, which selects the same top tier as `UINT32_MAX`. What the value measures on hardware (a remaining budget or a rate) and the meaning of the other three qwords are unknown @@ -183,7 +183,7 @@ - [mlockall, munlockall](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) (libkernel) - `mlockall` returns `EINVAL` for zero or unknown flags. Valid `MCL_CURRENT` throws because it would lock host stacks, heaps, modules and driver mappings along with guest memory; `MCL_FUTURE` throws because later mappings are not tracked for locking. FreeBSD 11.4's `sys_munlockall` (`sys/vm/vm_mmap.c`:1133-1165) checks `priv_check(td, PRIV_VM_MUNLOCK)` first (returning `EPERM` without it) and unwires the user map (`vm_map_unwire` over `vm_map_min`...`vm_map_max` with `VM_MAP_WIRE_HOLESOK`). Returning 0 unconditionally assumes the calling process is privileged. `munlockall` unlocks memory locked by `mlock` (`VirtualUnlock` on Windows, tolerating `ERROR_NOT_LOCKED` for pages never locked, and `munlockall` on Linux). On main, XPSemu (PPSA97358) fails to load at these imports (mlockall #4386, munlockall); their calls in QEMU `os-posix.c` execute only when `-overcommit mem-lock=on` is enabled. The call has not been observed in a run with this implementation. - [socket](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) creates only `SOCK_STREAM` and `SOCK_DGRAM` sockets in `AF_INET` and `AF_INET6`. A type that FreeBSD 11.4's inet domain has no protocol for in any configuration ([`in_proto.c`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/netinet/in_proto.c) has no `SOCK_RDM` entry and no unknown type) fails with `EPROTOTYPE` when the protocol is 0 and `EPROTONOSUPPORT` otherwise, as [`socreate`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c) does. `SOCK_RAW`, which `in_proto.c` always has (`socreate` creates it for a privileged process), and `SOCK_SEQPACKET`, which it has only in a kernel built with `SCTP`, fail with `EPROTONOSUPPORT`; whether the console allows either is not known. `SOCK_DGRAM` with `IPPROTO_UDPLITE` and `AF_UNIX` (`socketpair` supports it) fail with `EPROTONOSUPPORT` and `EAFNOSUPPORT` where FreeBSD creates the socket - [sigtimedwait](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) waits on a process-wide pending set instead of throwing: a signal raised while blocked stays pending and is taken by `sigwait`, `sigwaitinfo` or `sigtimedwait`, delivered to the handler when `_sigprocmask` unblocks it, or delivered by `pause`. The set covers all 128 signals and the lowest number that is pending and in the set is selected (FreeBSD specifies the lowest number only among real-time signals). Selection, `EAGAIN` on timeout, `EINVAL` for a `tv_nsec` outside 0 to 1000000000 while nothing is pending and `NULL` timeout blocking indefinitely follow FreeBSD's [sigtimedwait(2)](https://man.freebsd.org/cgi/man.cgi?query=sigtimedwait). `sigwait` returns 0 with the number in its output, as POSIX specifies, and `sigwaitinfo` and `sigtimedwait` return the number, as FreeBSD does. The guest `siginfo_t` follows FreeBSD 11's `sys/sys/siginfo.h`: 80 bytes, `si_signo` at 0, `si_code` at 8, `si_pid` at 12, `si_uid` at 16, `si_addr` at 24, `si_value` at 32 and `si_band` at 40; only `si_signo`, `si_code` (`SI_USER`) and `si_pid` (the guest pid) are written, `si_uid` stays 0 because no uid is modelled, and the console's layout is not verified. PPSA85300 imports `sigtimedwait` and fails to load without it; its only call site is VLC's `vlc_writev` ([filesystem.c](https://github.com/videolan/vlc/blob/6de05adcbaf2e8b85fe86aad4169393098628119/src/posix/filesystem.c#L243-L278)), which polls with a zero timeout to drain a `SIGPIPE` only after `writev` fails with `EPIPE`. The waits are not cancellation points and an unblocked caught signal does not interrupt them with `EINTR`, where FreeBSD's `sigwaitinfo` and `sigtimedwait` may return it. `SIGPIPE` has no host equivalent and is never raised by a host write here (see `read, write and vector I/O`), so VLC's drain finds nothing -- [pause, sigsuspend, sigwait, sigwaitinfo](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) take the pending set of `sigtimedwait` instead of throwing. `pause` waits for a pending signal that is not blocked and whose disposition can be delivered, runs its handler on the calling thread and returns -1 with `EINTR`; an ignored signal is discarded and the wait continues, and it never returns for a signal whose handler cannot run. A signal whose registered handler has `SA_SIGINFO` still throws, and one whose disposition is the default action and that has no host equivalent (every signal except 2, 4, 6, 8, 11 and 15) is neither delivered nor discarded: it stays pending until a wait takes it, where FreeBSD runs the default action, which terminates the process for most of them. PPSA68091, PPSA99169, PPSA99420 and PPSA97358 import them and fail to load without them. PPSA68091 calls `pause` at one site (0x8F7266), in a loop that waits while a global holds another thread's `errno` address. PPSA99169 only stores the address of `pause` in a table of symbol addresses (0x30DB1). PPSA99420 calls `pause` at 0x3BD80F6 and 0x3C9E220, `sigwait` at 0x3C9E4C7 and `sigwaitinfo` at 0x3C9E588. PPSA97358 calls `sigwait` from QEMU's `sigwait_compat` thread ([compatfd.c](https://github.com/ZiZc3/XPSemu/blob/8eec07b4e31f66dce8e0b05c1ca1281af303e933/util/compatfd.c#L28-L53)), which `qemu_signal_init` ([main-loop.c](https://github.com/ZiZc3/XPSemu/blob/8eec07b4e31f66dce8e0b05c1ca1281af303e933/util/main-loop.c#L110-L147)) starts for `SIGIO`, `SIGALRM` and `SIGBUS` when the main loop is created, so it reaches the wait at start-up, and from the vCPU thread of the dummy accelerator ([dummy-cpus.c](https://github.com/ZiZc3/XPSemu/blob/8eec07b4e31f66dce8e0b05c1ca1281af303e933/accel/dummy-cpus.c#L48-L56)). `sigsuspend` swaps the process-wide mask of `_sigprocmask` for its argument, waits as `pause` does through the same helper, then restores the mask through `_sigprocmask`, and returns -1 with `EINTR`; a null set throws, as the console faults in libthr instead of returning `EFAULT`. All four words of the argument replace the mask and are restored afterwards, as in `_sigprocmask`. Java 25 LAN / DNS (PPSA99869, #5501) imports it and fails to load without it, after `sysctlnametomib`, `fegetenv` and `mincore`. None of the five waits is a cancellation point +- [pause, sigsuspend, sigwait, sigwaitinfo](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) take the pending set of `sigtimedwait` instead of throwing. `pause` waits for a pending signal that is not blocked and whose disposition can be delivered, runs its handler on the calling thread and returns -1 with `EINTR`; an ignored signal is discarded and the wait continues, and it never returns for a signal whose handler cannot run. A signal whose registered handler has `SA_SIGINFO` still throws, and one whose disposition is the default action and that has no host equivalent (every signal except 2, 4, 6, 8, 11 and 15) is neither delivered nor discarded: it stays pending until a wait takes it, where FreeBSD runs the default action, which terminates the process for most of them. A pending `SIGALRM` with the default disposition exits the process with status 142 once it is unblocked, as FreeBSD's default action does. PPSA68091, PPSA99169, PPSA99420 and PPSA97358 import them and fail to load without them. PPSA68091 calls `pause` at one site (0x8F7266), in a loop that waits while a global holds another thread's `errno` address. PPSA99169 only stores the address of `pause` in a table of symbol addresses (0x30DB1). PPSA99420 calls `pause` at 0x3BD80F6 and 0x3C9E220, `sigwait` at 0x3C9E4C7 and `sigwaitinfo` at 0x3C9E588. PPSA97358 calls `sigwait` from QEMU's `sigwait_compat` thread ([compatfd.c](https://github.com/ZiZc3/XPSemu/blob/8eec07b4e31f66dce8e0b05c1ca1281af303e933/util/compatfd.c#L28-L53)), which `qemu_signal_init` ([main-loop.c](https://github.com/ZiZc3/XPSemu/blob/8eec07b4e31f66dce8e0b05c1ca1281af303e933/util/main-loop.c#L110-L147)) starts for `SIGIO`, `SIGALRM` and `SIGBUS` when the main loop is created, so it reaches the wait at start-up, and from the vCPU thread of the dummy accelerator ([dummy-cpus.c](https://github.com/ZiZc3/XPSemu/blob/8eec07b4e31f66dce8e0b05c1ca1281af303e933/accel/dummy-cpus.c#L48-L56)). `sigsuspend` swaps the process-wide mask of `_sigprocmask` for its argument, waits as `pause` does through the same helper, then restores the mask through `_sigprocmask`, and returns -1 with `EINTR`; a null set throws, as the console faults in libthr instead of returning `EFAULT`. All four words of the argument replace the mask and are restored afterwards, as in `_sigprocmask`. Java 25 LAN / DNS (PPSA99869, #5501) imports it and fails to load without it, after `sysctlnametomib`, `fegetenv` and `mincore`. None of the five waits is a cancellation point - [getsid](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) returns the calling process ID for pid 0 and the calling process, and `ESRCH` for other process IDs, assuming the single guest process is its own session leader. PPSA74126 imports it and fails to load without it - [sigaction](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) records the handler, flags and mask of every signal from 1 to 128 and reports them back, following FreeBSD's `sigaction` (`EINVAL` for other signals, and for a handler on `SIGKILL` or `SIGSTOP`); `signal` records its handler with `SA_RESTART`, as FreeBSD's libc does. Only handlers without `SA_SIGINFO` on the signals `signal` already delivers (`SIGINT`, `SIGILL`, `SIGABRT`, `SIGFPE`, `SIGSEGV`, `SIGTERM`) run, the same way `signal` handlers do; their mask and flags are not applied. `SA_SIGINFO` handlers are recorded but never called, since that needs FreeBSD's `siginfo_t` and `ucontext_t`, and neither are the handlers of other signals (`SIGBUS`, `SIGHUP`, ...), whether they are set by `sigaction` or by `signal`, which records them too (every number from 1 to 128, with `SA_RESTART` and an empty mask replacing what `sigaction` stored; `SIGKILL` and `SIGSTOP` accept only the default handler in both calls, as in FreeBSD's `kern_sigaction`). `raise` and `kill` accept every number from 1 to 128 now: a blocked one stays pending on the set of `sigtimedwait`, an unblocked one whose handler is registered runs it on the calling thread, and an unblocked one that has no host equivalent and no handler still fails with `EINVAL`, where FreeBSD runs its default action. `raise` of an `SA_SIGINFO` handler still throws instead of terminating - [kill, killpg](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) follow FreeBSD's `kern_kill` and `killpg1` for the only guest process: `EINVAL` for signals outside 0 to 128 and for negative `killpg` process groups, signal 0 only checks that the target exists, and its own pid, 0 and minus its own pid (the guest is taken to lead its own process group) signal it through `raise`, so they share `raise`'s limits above. Any other pid, and -1, which skips the caller, report `ESRCH`, so `EPERM` is never reported From 49b0e805f07dda570ceacf1fa1f2590cc39ac0e3 Mon Sep 17 00:00:00 2001 From: Gjon Gojcevic Date: Sat, 17 Oct 2026 12:33:15 -0400 Subject: [PATCH 0867/1000] feat(agc): implement 11_11_10 float color targets through 32-bit uint attachments Color format 7 (COLOR_11_11_10, float) keeps its 10-bit component in the low bits; with the reversed component order (COMP_SWAP 2) that is blue, with red in the high 11 bits. Vulkan has no such layout, so the target was refused at decode. Render it the way 10_11_11 unorm targets are rendered: an R32_UINT attachment, the reversed export mapping 0xc6, and a new ColorExportPacking::Float11_11_10 that makes the pixel shader pack each export with the format-43 image store conversion (toward zero, overflow to the largest finite value, negatives to zero, NaN kept). Blending, partial writes, 16-bit integer exports, DCC and the other component orders throw. AI-assisted: yes --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 2 + .../Graphics/src/ShaderValidation.cpp | 2 +- .../libSceAgcDriver/Graphics/src/State.cpp | 20 +- .../prx/libSceAgcDriver/tests/Graphics.cpp | 70 +++- .../execution/PackedFloatColorTarget.cpp | 338 ++++++++++++++++++ .../ControlFlow/src/RequestSerializer.cpp | 2 +- .../FragmentOutputSpecialization.hpp | 4 +- core/shader/recompiler/Recompiler.hpp | 3 +- .../SpirvBackend/src/SpirvModuleEmitter.cpp | 9 +- docs/dev/debt/libSceAgcDriver.md | 1 + 10 files changed, 434 insertions(+), 17 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/PackedFloatColorTarget.cpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 95751ed9e4..05a49d6a4d 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1527,6 +1527,8 @@ target_include_directories(agc_driver_reversed_order_blend_tests PRIVATE $) +target_include_directories(agc_driver_packed_float_color_target_tests PRIVATE $) + target_include_directories(agc_driver_packed_unorm_color_target_tests PRIVATE $) target_include_directories(agc_driver_color1555_target_tests PRIVATE $) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp index f04ec34921..a1408f084f 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp @@ -675,7 +675,7 @@ std::set ValidateShaders(std::span shaders, const auto color = std::find_if(state.colors.begin(), state.colors.end(), [&](const ColorTarget& target) { return target.exportIndex == location; }); const bool uintExport = color != state.colors.end() && color->uintExport; const bool packed = color != state.colors.end() && color->packing != ShaderRecompiler::ColorExportPacking::None; - Require(signature == (uintExport || packed ? "vertex:u32x4" : "vertex:f32x4"), packed ? "fragment shader must export uint4 words to its packed 10_11_11 unorm attachments" : uintExport ? "fragment shader must export uint4 colors to its unsigned integer attachments" : "fragment shader must export float4 colors to its attachments"); + Require(signature == (uintExport || packed ? "vertex:u32x4" : "vertex:f32x4"), packed ? "fragment shader must export uint4 words to its packed color attachments" : uintExport ? "fragment shader must export uint4 colors to its unsigned integer attachments" : "fragment shader must export float4 colors to its attachments"); locations.insert(location); } Require((!state.dualSourceBlend && state.reversedBlend == 0) || previous.secondSource == "f32x4", "dual-source blending needs a float4 second color at location 0 index 1"); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index 92c575f0ff..48af1154ab 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -743,6 +743,10 @@ struct DecodedColorFormat { ShaderRecompiler::ColorExportPacking packing = ShaderRecompiler::ColorExportPacking::None; }; +const char* PackedColorTargetName(ShaderRecompiler::ColorExportPacking packing) { + return packing == ShaderRecompiler::ColorExportPacking::Float11_11_10 ? "11_11_10 float" : "10_11_11 unorm"; +} + // CB_COLOR_INFO FORMAT / NUMBER_TYPE / COMP_SWAP to a Vulkan attachment format. AMD formats list // components from the least significant bits, as Vulkan's non-packed formats do. DecodedColorFormat DecodeColorFormat(std::uint32_t format, std::uint32_t number, std::uint32_t swap) { @@ -753,7 +757,7 @@ DecodedColorFormat DecodeColorFormat(std::uint32_t format, std::uint32_t number, }; const bool alternate = swap == 1; const auto single = [&](VkFormat vkFormat, std::uint32_t bytes) { return DecodedColorFormat{vkFormat, bytes, static_cast((0xe4u & ~3u) | swap)}; }; - if (swap > 1 && format != 1 && format != 2 && format != 4 && format != 10 && format != 12 && format != 16) return fail(); + if (swap > 1 && format != 1 && format != 2 && format != 4 && format != 7 && format != 10 && format != 12 && format != 16) return fail(); switch (format) { case 1: if (number == unorm) return single(VK_FORMAT_R8_UNORM, 1); @@ -791,6 +795,9 @@ DecodedColorFormat DecodeColorFormat(std::uint32_t format, std::uint32_t number, if (swap != 0 || (number != floating && number != unorm)) return fail(); if (number == unorm) return {VK_FORMAT_R32_UINT, 4, 0xe4u, ShaderRecompiler::ColorExportPacking::Unorm10_11_11}; return {VK_FORMAT_B10G11R11_UFLOAT_PACK32, 4}; + case 7: + if (swap != 2 || number != floating) return fail(); + return {VK_FORMAT_R32_UINT, 4, 0xc6u, ShaderRecompiler::ColorExportPacking::Float11_11_10}; case 9: // COLOR_2_10_10_10 keeps red in the low bits, the Vulkan A2B10G10R10 packing. if (number != unorm) return fail(); @@ -1306,10 +1313,11 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) { } } if (color.packing != ShaderRecompiler::ColorExportPacking::None) { + const std::string packed = std::string(color.packing == ShaderRecompiler::ColorExportPacking::Float11_11_10 ? "an " : "a ") + PackedColorTargetName(color.packing); const auto slotExport = (exportFormat >> (4u * color.exportIndex)) & 0xfu; - if (slotExport == 5 || slotExport == 6) throw std::runtime_error("AGC graphics: color export format " + std::to_string(slotExport) + " into a 10_11_11 unorm target is unsupported"); - Require(!state.blendEnable, "blending into a 10_11_11 unorm color target is unsupported"); - Require((exportedMask & 7u) == 7u, "partial writes of a 10_11_11 unorm color target are unsupported"); + if (slotExport == 5 || slotExport == 6) throw std::runtime_error("AGC graphics: color export format " + std::to_string(slotExport) + " into " + packed + " target is unsupported"); + Require(!state.blendEnable, "blending into " + packed + " color target is unsupported"); + Require((exportedMask & 7u) == 7u, "partial writes of " + packed + " color target are unsupported"); } if (state.blendEnable && !reversed && (secondSource(state.srcColorBlendFactor) || secondSource(state.dstColorBlendFactor) || secondSource(state.srcAlphaBlendFactor) || secondSource(state.dstAlphaBlendFactor))) { const auto addition = [](VkBlendOp op) { return op == VK_BLEND_OP_ADD || op == VK_BLEND_OP_SUBTRACT || op == VK_BLEND_OP_REVERSE_SUBTRACT; }; @@ -1551,8 +1559,8 @@ ColorTarget DecodeColorBuffer(const Registers& cx, std::uint32_t slot, bool allo color.format = decoded.format; color.componentMapping = decoded.componentMapping; color.packing = decoded.packing; - Require(color.packing == ShaderRecompiler::ColorExportPacking::None || (info & 0x10000000u) == 0, "DCC-compressed 10_11_11 unorm color targets are unsupported"); - Require(color.packing == ShaderRecompiler::ColorExportPacking::None || (info & 0x00040000u) == 0, "truncating (ROUND_MODE) 10_11_11 unorm color targets are unsupported"); + if (color.packing != ShaderRecompiler::ColorExportPacking::None && (info & 0x10000000u) != 0) throw std::runtime_error("AGC graphics: DCC-compressed " + std::string(PackedColorTargetName(color.packing)) + " color targets are unsupported"); + Require(color.packing != ShaderRecompiler::ColorExportPacking::Unorm10_11_11 || (info & 0x00040000u) == 0, "truncating (ROUND_MODE) 10_11_11 unorm color targets are unsupported"); for (std::uint32_t word = 0; word < 2; ++word) { const auto clear = find(cx, 0x323 + word + stride); color.clearWords[word] = clear == cx.end() ? 0u : clear->second; diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index e18ee1bc09..6ae08a843d 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -6956,13 +6956,78 @@ void unorm10_11_11TargetTests() { return AgcDriver::Graphics::ValidateShaders(shaders, target, subgroup, false); }; Require(validate(packed, state) == std::set{0u}, "the packed export did not reach attachment 0"); - expectFailure([&] { validate(unpacked, state); }, "uint4 words to its packed 10_11_11 unorm attachments"); + expectFailure([&] { validate(unpacked, state); }, "uint4 words to its packed color attachments"); auto plainState = state; plainState.colors[0].packing = ColorExportPacking::None; expectFailure([&] { validate(packed, plainState); }, "float4 colors"); auto uintExport = request; uintExport.context.pixel->targetOutputMode[0] = 7; - expectFailure([&] { static_cast(ShaderRecompiler::Recompile(uintExport)); }, "fragment output 0 has no 10_11_11 unorm packing path"); + expectFailure([&] { static_cast(ShaderRecompiler::Recompile(uintExport)); }, "fragment output 0 has no color export packing path"); +} + +void float11_11_10TargetTests() { + using ShaderRecompiler::ColorExportPacking; + auto queue = makeState(); + queue.context[0x31c] = (queue.context.at(0x31c) & ~0x1f7cu) | (7u << 2u) | (7u << 8u) | (2u << 11u) | 0x40000u; + queue.context[0x1b3] = 2; + queue.context[0x1b4] = 2; + const auto state = AgcDriver::Graphics::DecodeState(queue); + Require(state.color.format == VK_FORMAT_R32_UINT && state.color.elementBytes == 4 && state.color.bytes == colorMemory.size() && state.color.componentMapping == 0xc6u && state.color.packing == ColorExportPacking::Float11_11_10, "an 11_11_10 float target did not decode as a packed R32_UINT attachment with blue in the low bits"); + const auto packings = AgcDriver::Graphics::ExportPackings(state); + Require(packings[0] == ColorExportPacking::Float11_11_10 && std::all_of(packings.begin() + 1, packings.end(), [](ColorExportPacking packing) { return packing == ColorExportPacking::None; }), "export 0 did not carry the 11_11_10 float packing alone"); + Require(AgcDriver::Graphics::ExportMappings(state)[0] == 0xc6u, "export 0 did not reverse its components into the 11_11_10 float target"); + auto changed = queue; + changed.context[0x31c] &= ~0x40000u; + Require(AgcDriver::Graphics::DecodeState(changed).color.packing == ColorExportPacking::Float11_11_10, "an 11_11_10 float target without ROUND_MODE was refused"); + for (const auto swap : {0u, 1u, 3u}) { + changed = queue; + changed.context[0x31c] = (changed.context[0x31c] & ~0x1800u) | (swap << 11u); + expectFailure([&] { AgcDriver::Graphics::DecodeState(changed); }, "unsupported color format 7 number type 7 component swap " + std::to_string(swap)); + } + for (const auto number : {0u, 4u}) { + changed = queue; + changed.context[0x31c] = (changed.context[0x31c] & ~0x700u) | (number << 8u) | 0x8000u; + expectFailure([&] { AgcDriver::Graphics::DecodeState(changed); }, "unsupported color format 7 number type " + std::to_string(number) + " component swap 2"); + } + changed = queue; + changed.context[0x1e0] = 0x60010001u; + for (std::uint32_t i = 0; i < 4; ++i) changed.context[0x105 + i] = 0; + expectFailure([&] { AgcDriver::Graphics::DecodeState(changed); }, "blending into an 11_11_10 float color target"); + changed = queue; + changed.context[0x8e] = 6; + expectFailure([&] { AgcDriver::Graphics::DecodeState(changed); }, "partial writes of an 11_11_10 float color target"); + changed.context[0x8e] = 7; + Require(AgcDriver::Graphics::DecodeState(changed).color.packing == ColorExportPacking::Float11_11_10, "an RGB write of an 11_11_10 float target was refused"); + for (const auto format : {5u, 6u}) { + changed = queue; + changed.context[0x1c5] = format; + expectFailure([&] { AgcDriver::Graphics::DecodeState(changed); }, "color export format " + std::to_string(format) + " into an 11_11_10 float target"); + } + changed = queue; + changed.context[0x1c5] = 4; + Require(AgcDriver::Graphics::DecodeState(changed).color.packing == ColorExportPacking::Float11_11_10, "an FP16_ABGR export into an 11_11_10 float target was refused"); + changed = queue; + changed.context[0x31c] |= 0x10000000u; + changed.context[0x325] = 0x1234; + expectFailure([&] { AgcDriver::Graphics::DecodeState(changed); }, "DCC-compressed 11_11_10 float color targets"); + + constexpr std::array code{0xf800180fu, 0x07060504u, 0xbf810000u}; + const auto request = packedExportRequest(queue, state, code); + auto unorm = request; + unorm.context.pixel->targetExportPacking[0] = ColorExportPacking::Unorm10_11_11; + const ShaderRecompiler::RequestSerializer serializer; + Require(serializer.Deserialize(serializer.Serialize(request)).request.context.pixel->targetExportPacking == request.context.pixel->targetExportPacking, "the 11_11_10 float packing was lost in serialization"); + const auto packed = ShaderRecompiler::Recompile(request); + const auto unormPacked = ShaderRecompiler::Recompile(unorm); + Require(packed.variantId == unormPacked.variantId && packed.PipelineVariantId() != unormPacked.PipelineVariantId(), "the 11_11_10 float packing did not key its own specialization of the shared artifact"); + ShaderRecompiler::RecompileResult vertex; + vertex.spirv = makeModule({}); + const VkPhysicalDeviceSubgroupProperties subgroup{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_PROPERTIES}; + const std::array shaders{{{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, {ShaderRecompiler::ShaderStage::Fragment, &packed, 0}}}; + Require(AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false) == std::set{0u}, "the packed float export did not reach attachment 0"); + auto invalid = request; + invalid.context.pixel->targetExportPacking[0] = static_cast(3); + expectFailure([&] { static_cast(ShaderRecompiler::Recompile(invalid)); }, "invalid prepared fragment export packing"); } AgcDriver::QueueState dualSourceQueue() { @@ -8299,6 +8364,7 @@ int main(int argc, char** argv) { memoryValueFillTests(); ZExportTests(); unorm10_11_11TargetTests(); + float11_11_10TargetTests(); DepthBoundsBiasTests(); conservativeZExportTests(); earlyFragmentTestsTests(); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PackedFloatColorTarget.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PackedFloatColorTarget.cpp new file mode 100644 index 0000000000..9b322cb6d0 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PackedFloatColorTarget.cpp @@ -0,0 +1,338 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp" +#include "prx/libSceAgcDriver/Graphics/include/State.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" +#include "Recompiler.hpp" +#include "Optimization/ResourceProgram.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::ColorTargetLayout; +using AgcDriver::Graphics::ColorTileMode; +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Width = 256; +constexpr std::uint32_t Height = 128; +constexpr std::uint32_t ScissorWidth = 128; +constexpr std::size_t BlockBytes = 65536; +constexpr std::uint32_t Float11_11_10Info = 0x4171cu; +constexpr std::uint32_t TiledAttrib3 = 0x4dc6c000u; +constexpr std::uint32_t LinearAttrib3 = 0x09000000u; +constexpr std::uint32_t Float32Export = 9; +constexpr std::uint32_t Float16Export = 4; + +alignas(256) constexpr std::array VertexCode{ + 0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, +}; + +constexpr std::array, 3> Triangle{{ + {-1.0f, -1.0f, 0.5f, 1.0f}, {3.0f, -1.0f, 0.5f, 1.0f}, {-1.0f, 3.0f, 0.5f, 1.0f} +}}; + +alignas(256) std::array, 32> Programs{}; +std::size_t programCount = 0; + +std::uint32_t SmallFloat(float value, std::uint32_t mantissaBits) { + const std::uint32_t infinity = 0x1fu << mantissaBits; + if (std::isnan(value)) { + const auto top = (std::bit_cast(value) & 0x7fffffu) >> (23u - mantissaBits); + return infinity | (top == 0u ? 1u : top); + } + if (!(value > 0.0f)) return 0; + if (std::isinf(value)) return infinity; + const double magnitude = value; + const double scale = std::ldexp(1.0, static_cast(mantissaBits)); + if (magnitude < std::ldexp(1.0, -14)) return static_cast(std::floor(magnitude * std::ldexp(1.0, 14) * scale)); + int exponent = 0; + const double fraction = std::frexp(magnitude, &exponent); + if (exponent - 1 > 15) return infinity - 1u; + return (static_cast(exponent - 1 + 15) << mantissaBits) | static_cast(std::floor((fraction * 2.0 - 1.0) * scale)); +} + +std::uint32_t Packed(std::array rgb) { + return SmallFloat(rgb[2], 5) | (SmallFloat(rgb[1], 6) << 10u) | (SmallFloat(rgb[0], 6) << 21u); +} + +std::uint32_t Half(float value) { + Require(value == 0.0f || (value >= 0.0625f && value <= 1.0f), "the test's half values are normal and at most 1"); + if (value == 0.0f) return 0; + const auto bits = std::bit_cast(value); + Require((bits & 0x1fffu) == 0, "the test's half values are exact"); + return ((((bits >> 23u) & 0xffu) - 112u) << 10u) | ((bits >> 13u) & 0x3ffu); +} + +std::span Program(const std::array& vgprs, bool compressed) { + Require(programCount < Programs.size(), "too many test programs"); + auto& code = Programs[programCount++]; + std::size_t at = 0; + for (std::uint32_t vgpr = 0; vgpr < (compressed ? 2u : 4u); ++vgpr) { + code[at++] = 0x7e0002ffu | ((4u + vgpr) << 17u); + code[at++] = vgprs[vgpr]; + } + code[at++] = compressed ? 0xf8001c0fu : 0xf800180fu; + code[at++] = compressed ? 0x00000504u : 0x07060504u; + code[at++] = 0xbf810000u; + return std::span(code.data(), at); +} + +std::span Float32Program(std::array rgb) { + return Program({std::bit_cast(rgb[0]), std::bit_cast(rgb[1]), std::bit_cast(rgb[2]), std::bit_cast(1.0f)}, false); +} + +std::span Float16Program(std::array rgb) { + return Program({Half(rgb[0]) | (Half(rgb[1]) << 16u), Half(rgb[2]) | (Half(1.0f) << 16u), 0, 0}, true); +} + +struct Block { + explicit Block(std::size_t bytes) : bytes(bytes), watched(AgcDriver::GuestMemory::WriteWatched()) { +#ifdef _WIN32 + if (watched) { + data = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(bytes, BlockBytes)); + GuestArena::GuestArenaCommit_nid_postfix(data, bytes, PAGE_READWRITE, bytes); + } else { + data = static_cast(VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); + } +#else + if (watched) { + void* raw = mmap(nullptr, bytes + BlockBytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + Require(raw != MAP_FAILED, "cannot map the color target"); + const auto begin = reinterpret_cast(raw); + const auto aligned = (begin + BlockBytes - 1) & ~(static_cast(BlockBytes) - 1); + if (aligned != begin) munmap(raw, aligned - begin); + if (aligned + bytes != begin + bytes + BlockBytes) munmap(reinterpret_cast(aligned + bytes), begin + BlockBytes - aligned); + data = reinterpret_cast(aligned); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(data, bytes); + } else { + data = static_cast(std::aligned_alloc(BlockBytes, bytes)); + } +#endif + Require(data != nullptr, "cannot allocate the color target"); + Require(!watched || AgcDriver::GuestMemory::Watched(Address(), bytes), "the color target is not write-watched"); + GuestAllocations::Mutation mutation; + mutation.Add(data, bytes, true, true, true); + } + ~Block() { + AgcDriver::Graphics::StorageTexture::FlushPending(Address(), bytes, nullptr, "test release"); + { + GuestAllocations::Mutation mutation; + mutation.Remove(data); + } +#ifdef _WIN32 + if (watched) { + GuestArena::GuestArenaReset_nid_postfix(data, bytes); + GuestArena::GuestArenaRelease_nid_postfix(data, bytes); + } else { + VirtualFree(data, 0, MEM_RELEASE); + } +#else + if (watched) { + munmap(data, bytes); + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(data, bytes); + } else { + std::free(data); + } +#endif + } + Block(const Block&) = delete; + Block& operator=(const Block&) = delete; + std::uint64_t Address() const { return reinterpret_cast(data); } + std::size_t bytes; + bool watched; + std::uint8_t* data = nullptr; +}; + +AgcDriver::Graphics::ColorTarget DecodeTarget(const Block& block, std::uint32_t attrib3) { + AgcDriver::Registers cx; + cx[0x318] = static_cast(block.Address() >> 8u); + cx[0x31b] = 0; + cx[0x31c] = Float11_11_10Info; + cx[0x31d] = 0; + cx[0x3b0] = ((Width - 1u) << 14u) | (Height - 1u); + cx[0x3b8] = attrib3; + cx[0x390] = static_cast(block.Address() >> 40u); + auto color = AgcDriver::Graphics::DecodeColorBuffer(cx, 0); + Require(color.format == VK_FORMAT_R32_UINT && color.elementBytes == 4 && color.componentMapping == 0xc6u && color.packing == ShaderRecompiler::ColorExportPacking::Float11_11_10 && color.bytes == block.bytes, "the 11_11_10 float target did not decode as a packed R32_UINT surface"); + return color; +} + +void Draw(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::ColorTarget& color, std::uint32_t exportFormat, std::span pixelCode) { + const auto target = device.Target(); + constexpr std::uint32_t waveSize = 64; + std::vector vertexUserData(4, 0u); + const auto address = reinterpret_cast(Triangle.data()); + const std::array vertexBuffer{static_cast(address), static_cast((address >> 32u) & 0xffffu) | (16u << 16u), static_cast(Triangle.size()), 0x01016facu}; + std::copy(vertexBuffer.begin(), vertexBuffer.end(), vertexUserData.begin()); + const std::array vertexMemory{{{reinterpret_cast(VertexCode.data()), std::as_bytes(std::span(VertexCode))}}}; + ShaderRecompiler::RecompileRequest vertex{ + {ShaderStage::Vertex, reinterpret_cast(VertexCode.data()), VertexCode, 0, {}}, + {waveSize, 0, vertexUserData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, vertexMemory}, + target, + {0, 0, 0, 64} + }; + vertex.useCache = false; + const auto vertexResult = ShaderRecompiler::Recompile(vertex); + const auto vertexPush = static_cast(vertexResult.pushConstants.size()); + + AgcDriver::Graphics::State state{}; + state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, waveSize, waveSize, std::nullopt, std::nullopt}; + state.colors = {color}; + state.color = color; + state.hasColorTarget = true; + state.renderExtent = {Width, Height}; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.viewport = {0, static_cast(Height), static_cast(Width), -static_cast(Height), 0, 1}; + state.negativeOneToOne = false; + state.scissor = {{0, 0}, {ScissorWidth, Height}}; + state.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + VkPipelineColorBlendAttachmentState blend{}; + blend.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT; + state.blends = {blend}; + state.blend = blend; + state.blendConstants = {}; + + const auto codeAddress = reinterpret_cast(pixelCode.data()); + const AgcDriver::Registers context{{0x1b6, 0x8000}, {0x1b3, 0}, {0x1b4, 0}, {0x203, 0}, {0x1c5, exportFormat}}; + const std::array pixelMemory{{{codeAddress, std::as_bytes(pixelCode)}}}; + ShaderRecompiler::RecompileRequest fragment{ + {ShaderStage::Fragment, codeAddress, pixelCode, 0, {}}, + {waveSize, 0, {}, std::nullopt, AgcDriver::Graphics::DecodePixelStageInfo(context, {}), std::nullopt, pixelMemory}, + target, + PixelPushLayout(vertexPush, target) + }; + const auto handle = ShaderRecompiler::PrepareShader(fragment); + AgcDriver::DriverDetail::ShaderSnapshot snapshot{codeAddress, 0, 1, {pixelCode.begin(), pixelCode.end()}, {}}; + snapshot.prepared->entries.push_back({0, handle}); + fragment.shader.code = snapshot.code; + fragment.context.pixel = AgcDriver::Graphics::DecodePixelStageInfo(context, AgcDriver::Graphics::ExportMappings(state)); + fragment.context.pixel->targetExportPacking = AgcDriver::Graphics::ExportPackings(state); + const ShaderRecompiler::SrtRuntime runtime{}; + const auto materialize = [&](const ShaderRecompiler::RecompileRequest& request) { + const auto invocation = AgcDriver::DriverDetail::InvocationFor(snapshot, 0, request); + return invocation.Materialize(*invocation.Capture(runtime)); + }; + const auto pixelResult = materialize(fragment); + auto unpacked = fragment; + unpacked.context.pixel->targetExportPacking.fill(ShaderRecompiler::ColorExportPacking::None); + const auto plain = materialize(unpacked); + Require(pixelResult->variantId == ShaderRecompiler::GetPreparedArtifact(*handle).variantId && plain->variantId == pixelResult->variantId, "the draw did not use the registered pixel shader"); + Require(plain->PipelineVariantId() != pixelResult->PipelineVariantId() && materialize(fragment)->PipelineVariantId() == pixelResult->PipelineVariantId(), "the 11_11_10 float packing did not key its own specialization"); + const std::array shaders{{ + {ShaderStage::Vertex, &vertexResult, 0}, + {ShaderStage::Fragment, pixelResult.get(), PixelPushOffset(vertexPush, target)} + }}; + const AgcDriver::Pm4::DrawParameters draw{0, static_cast(Triangle.size()), 0, 1, 0, false}; + device.Draw(state, draw, shaders); +} + +std::vector ReadBack(AgcDriver::VulkanDevice& device, const Block& block) { + AgcDriver::Graphics::StorageTexture::FlushPending(block.Address(), block.bytes, nullptr, "test read-back"); + device.WaitIdle(); + std::vector stored(block.bytes / 4u); + AgcDriver::GuestMemory::Read(block.Address(), std::as_writable_bytes(std::span(stored)), 1); + return stored; +} + +void ExpectSurface(AgcDriver::VulkanDevice& device, const Block& block, const ColorTargetLayout& layout, const std::vector& before, std::uint32_t word, const std::string& what) { + const auto stored = ReadBack(device, block); + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + const auto index = layout.Offset(x, y) / 4u; + const bool inside = x < ScissorWidth; + const auto expected = inside ? word : before[index]; + if (stored[index] == expected) continue; + char detail[160]; + std::snprintf(detail, sizeof(detail), ": pixel (%u, %u) %s the scissor holds 0x%08x, expected 0x%08x", x, y, inside ? "inside" : "outside", stored[index], expected); + throw std::runtime_error(what + detail); + } + } +} + +std::vector Fill(Block& block) { + std::vector words(block.bytes / 4u); + for (std::size_t index = 0; index < words.size(); ++index) words[index] = static_cast(index * 0x9e3779b9u); + std::memcpy(block.data, words.data(), block.bytes); + return words; +} + +std::string Describe(std::array rgb) { + char text[96]; + std::snprintf(text, sizeof(text), "(%.9g, %.9g, %.9g)", rgb[0], rgb[1], rgb[2]); + return text; +} + +void TargetTests(AgcDriver::VulkanDevice& device, Block& block, std::uint32_t attrib3, ColorTileMode mode, const char* kind) { + const ColorTargetLayout layout(Width, Height, mode, 4); + Require(layout.Bytes() == block.bytes, "the test surface size disagrees with its layout"); + const auto color = DecodeTarget(block, attrib3); + auto before = Fill(block); + const float nan = std::numeric_limits::quiet_NaN(); + const float infinity = std::numeric_limits::infinity(); + const std::array, 10> cases{{ + {1.0f, 0.5f, 0.25f}, + {2.0f, 0.75f, 65024.0f}, + {1.0f + 1.9f / 64.0f, 1.0f + 0.9f / 64.0f, 1.0f + 1.9f / 32.0f}, + {65000.0f, 70000.0f, 1.0e30f}, + {nan, -1.0f, infinity}, + {-infinity, -0.0f, std::numeric_limits::denorm_min()}, + {std::ldexp(1.0f, -15), std::ldexp(3.0f, -20), std::ldexp(1.0f, -19)}, + {std::ldexp(1.0f, -21), std::ldexp(1.9f, -20), std::ldexp(1.9f, -19)}, + {std::numeric_limits::min(), 0.1f, 1.0f / 3.0f}, + {3.0f, 5.0f, 7.0f}, + }}; + for (const auto& rgb : cases) { + Draw(device, color, Float32Export, Float32Program(rgb)); + ExpectSurface(device, block, layout, before, Packed(rgb), std::string("a 32_ABGR export of ") + Describe(rgb) + " into a " + kind + " 11_11_10 float target"); + before = ReadBack(device, block); + } + const std::array halves{0.5f, 0.75f, 0.125f}; + Draw(device, color, Float16Export, Float16Program(halves)); + ExpectSurface(device, block, layout, before, Packed(halves), std::string("an FP16_ABGR export of ") + Describe(halves) + " into a " + kind + " 11_11_10 float target"); +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + Block tiled(ColorTargetLayout(Width, Height, ColorTileMode::RenderTarget, 4).Bytes()); + Block linear(ColorTargetLayout(Width, Height, ColorTileMode::Linear, 4).Bytes()); + TargetTests(*device, tiled, TiledAttrib3, ColorTileMode::RenderTarget, "tiled"); + TargetTests(*device, linear, LinearAttrib3, ColorTileMode::Linear, "linear"); + std::puts("11_11_10 float color target tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp index b43d75c3c9..bd80f9bbd6 100644 --- a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp +++ b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp @@ -239,7 +239,7 @@ ConservativeZExport readConservativeZExport(Reader& reader) { ColorExportPacking readColorExportPacking(Reader& reader) { const auto value = reader.ReadU8(); - if (value > static_cast(ColorExportPacking::Unorm10_11_11)) { + if (value > static_cast(ColorExportPacking::Float11_11_10)) { throw std::runtime_error("RequestSerializer: invalid ColorExportPacking value"); } return static_cast(value); diff --git a/core/shader/recompiler/FragmentOutputSpecialization.hpp b/core/shader/recompiler/FragmentOutputSpecialization.hpp index feb37aeb69..00894c6af1 100644 --- a/core/shader/recompiler/FragmentOutputSpecialization.hpp +++ b/core/shader/recompiler/FragmentOutputSpecialization.hpp @@ -32,7 +32,7 @@ inline std::vector SpecializeFragmentOutputs(std::vector= PipelineSpecialization::ExportPackingBase + 8u) continue; - if (constant.value > static_cast(ColorExportPacking::Unorm10_11_11)) throw std::runtime_error("invalid prepared fragment export packing"); + if (constant.value > static_cast(ColorExportPacking::Float11_11_10)) throw std::runtime_error("invalid prepared fragment export packing"); if (constant.value != static_cast(ColorExportPacking::None)) packed.insert(constant.id - PipelineSpecialization::ExportPackingBase); } if (packed.empty() && !dualSource && reversed == 0u) return words; @@ -81,7 +81,7 @@ inline std::vector SpecializeFragmentOutputs(std::vectortargetOutputMode[exp.index]; } -std::uint32_t PackUnorm10_11_11(SpirvEmitterState& state, std::uint32_t color) { - const auto info = GetFormatInfo(IrBufferFormat::Format11_11_10UNorm); +std::uint32_t PackExport(SpirvEmitterState& state, IrBufferFormat format, std::uint32_t color) { + const auto info = GetFormatInfo(format); auto packed = ConstantU32(state, 0u); for (std::uint32_t component = 0; component < info.componentCount; component++) { const auto value = state.module.AllocateId(); @@ -270,8 +270,9 @@ std::uint32_t PackUnorm10_11_11(SpirvEmitterState& state, std::uint32_t color) { std::uint32_t SelectExportPacking(SpirvEmitterState& state, std::uint32_t target, std::uint32_t color) { const auto packing = state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::ExportPackingBase + target, static_cast(ColorExportPacking::None)); - const auto packed = EmitCompareU32Constant(state, spv::OpIEqual, packing, static_cast(ColorExportPacking::Unorm10_11_11)); - const auto word = PackUnorm10_11_11(state, color); + const auto packed = EmitCompareU32Constant(state, spv::OpINotEqual, packing, static_cast(ColorExportPacking::None)); + const auto floating = EmitCompareU32Constant(state, spv::OpIEqual, packing, static_cast(ColorExportPacking::Float11_11_10)); + const auto word = Select(state, TypeU32(state), floating, PackExport(state, IrBufferFormat::Format10_11_11Float, color), PackExport(state, IrBufferFormat::Format11_11_10UNorm, color)); std::array components{}; for (std::uint32_t component = 0; component < components.size(); ++component) { const auto unpacked = state.module.AllocateId(); diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index e5ade18beb..3c26e1aedf 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -102,6 +102,7 @@ - The [dispatches that follow a compute queue's wait](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchAhead.cpp) (libSceAgcDriver), up to the next wait that memory does not satisfy, a packet that writes guest memory other than a label, or a conditional packet, are resolved (memory captured, shader recompiled) when the wait passes, as the CP reads them right after it; the graphics queue is exempt (`APS5_GROUP_CAPTURE_ALL_QUEUES=1` includes it). The device still reads guest memory when the dispatch is issued; when that memory no longer matches the capture, the dispatch is resolved again from current memory. - [PA_CL_CLIP_CNTL](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - `DX_LINEAR_ATTR_CLIP_ENA` (bit 24) clear is drawn like bit 24 set, with Vulkan's clipping of `NoPerspective` outputs; what a clear bit changes on the console is undocumented. - [10_11_11 unorm color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) render into R32_UINT attachments: the pixel shader packs each export, rounded to nearest even like format-30 image stores, which is assumed to match the CB's conversion. +- [11_11_10 float color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) render into R32_UINT attachments: the pixel shader packs each export toward zero like format-43 image stores (overflow to the largest finite value, negatives to zero, NaN kept), which is assumed to match the CB's conversion; only the reversed component order (COMP_SWAP 2) is accepted. - [PA_SU_SC_MODE_CNTL.PROVOKING_VTX_LAST](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp) (libSceAgcDriver) uses Vulkan's last-vertex mode for native primitives. Bit 19 follows [opengnm's `flatshade_first` mapping](https://github.com/blackbearreloaded/ps5-opengl/blob/db752da4c89380af228497b5b6852163a4e61069/src/gallium/ps5/ps5_screen.c#L2244); vertex selection follows [Vulkan](https://docs.vulkan.org/spec/latest/chapters/drawing.html#drawing-primitive-topologies), without console measurements. A [rect-list](../../../core/shader/recompiler/SpirvBackend/src/RectListShaders.cpp) draw in last mode gives all four corners of each rectangle the flat inputs of its third vertex through the generated control stage, as first mode gives them those of its first, so Vulkan's choice of provoking vertex for the tessellated triangles does not matter; which vertex the console takes for a rectangle is not measured either. Last mode throws without `VK_EXT_provoking_vertex`, for generated mesh or tessellation primitives, explicit per-vertex flat inputs, and triangle strips whose pixel shader reads a vertex slot itself (`v_interp_mov_f32`, or 16-bit interpolation, which is not checked yet), since the vertex order of odd strip triangles [depends on the device](https://docs.vulkan.org/refpages/latest/refpages/source/VkPhysicalDeviceFragmentShaderBarycentricPropertiesKHR.html). `v_interp_p1_f32` and `v_interp_p2_f32` read P10 and P20 against the guest's I and J, which come from `gl_BaryCoordKHR` in the same vertex order as the per-vertex values, so the order cancels out and strips with only those reads draw. A FLAT_SHADE input without pass-through that is read through `v_interp_mov_f32` P10 and not P20 [returns the provoking vertex's value](../../../core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp), like P0. ps5-opengl reads every flat input that way, and its driver passes the GL 4.6 CTS on a PS5 (#1413). The lane itself was not measured on the console, so with the first provoking vertex such a shader throws. - [Dual-source blending](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) takes the MRT1 export as the second source of the MRT0 blend, as RADV and radeonsi program it; what the CB does with other export or target layouts is unknown, so they throw. - [Blending into reversed-order color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - 8_8_8_8 and 16_16_16_16 targets with `COMP_SWAP` 2 or 3 keep the exported alpha in their first channel and red or blue in their fourth, which Vulkan would blend with the color and the alpha equation respectively. The color block is taken to blend in shader export order and apply `COMP_SWAP` afterwards: the alpha equation and every source-alpha factor use the exported alpha, and the destination alpha is the first channel. This follows the `CB_TARGET_MASK` text of the AMD [SI](https://xorg.freedesktop.org/docs/AMD/old/SI_3D_registers.pdf) (p. 229) and [CIK](https://xorg.freedesktop.org/docs/AMD/old/CIK_3D_registers_v2.pdf) (p. 238) register references ("the order of components after blending and before COMP_SWAP is applied"). RADV builds `CB_BLENDn_CONTROL` without regard to the swap ([radv_pipeline_graphics.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-26.2.2/src/amd/vulkan/radv_pipeline_graphics.c#L650-L709)) and renders R4G4B4A4, B4G4R4A4, R5G5B5A1 and B5G5R5A1 with `SWAP_STD_REV` and `SWAP_ALT_REV` ([vk_format.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-26.2.2/src/vulkan/util/vk_format.c#L43-L48), [ac_formats.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-26.2.2/src/amd/common/ac_formats.c#L609-L624)). Mesa CI's Navi10 (gfx1010) CTS run ([job 112381553](https://gitlab.freedesktop.org/mesa/mesa/-/jobs/112381553), [112381554](https://gitlab.freedesktop.org/mesa/mesa/-/jobs/112381554)) passes all 400 `blend.format..states` cases of each of those formats, including separate alpha and destination-alpha factors. It is not measured on gfx1013 or on 8_8_8_8 and 16_16_16_16 targets. Factor pairs Vulkan applies per channel are used directly. Otherwise the pixel shader only copies values: it writes the exported alpha, each channel's own export or the constants 0 and 1 into a second color at location 0 index 1 (and 1 into a channel of the first color whose source factor is ONE, moving its export into the factor), and the blend forms every factor, `1 -` and product from those copies through `SRC1_COLOR`/`SRC1_ALPHA` factors. The blend unit therefore rounds them as it rounds a plain Vulkan blend of the same export; Vulkan does not require a second-source color to be converted like the first, but on an RTX 5080 and lavapipe every reversed-order case of the execution test is bit-identical to the same draw into a `SWAP_STD` target, which the test checks. Factor combinations that copies cannot express (for example `ONE_MINUS_SRC_ALPHA` in the alpha equation with `SRC_COLOR` in the color equation), sRGB, SNORM and integer targets, constant and second-source factors, destination-alpha and `SRC_ALPHA_SATURATE` factors in the color equation, different color and alpha operations, MIN or MAX with factors other than ONE, other color exports when the blend needs the second color, and devices without `dualSrcBlend` throw. Gran Turismo 7 (PPSA01317) blends `CB_BLEND0_CONTROL` 0x65010504 into such a target (#4782). From a518a90ad6bf7096093ef1090feaf2bb95b529c2 Mon Sep 17 00:00:00 2001 From: Gjon Gojcevic Date: Sat, 17 Oct 2026 13:03:06 -0400 Subject: [PATCH 0868/1000] fix(agc): refuse rounding 11_11_10 float targets and resolves of packed color targets An 11_11_10 float color target with CB_COLOR_INFO ROUND_MODE (bit 18) clear asks the CB to round, but the export packing truncates toward zero, so such a target now throws. A CB resolve whose source is a packed 10_11_11 unorm or 11_11_10 float target would resolve the raw R32_UINT words, which an integer resolve cannot average, so it now throws at decode. AI-assisted: yes --- core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp | 4 +++- core/libs/prx/libSceAgcDriver/tests/Graphics.cpp | 7 ++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index 48af1154ab..fa31bf9ef8 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -1486,7 +1486,7 @@ ColorTarget DecodeColorBuffer(const Registers& cx, std::uint32_t slot, bool allo Require(color.samples <= 8u, "color targets with more than eight samples are unsupported"); Require((1u << ((attrib >> 15u) & 3u)) == color.samples, "color targets with fewer fragments than samples (EQAA) are unsupported"); const auto multisampleInfo = color.samples > 1 ? 0x0c006000u : 0u; - // ROUND_MODE (bit 18) only affects unorm rounding. With DCC_ENABLE (bit 28) the target is written + // ROUND_MODE (bit 18) only affects unorm rounding and packed float truncation. With DCC_ENABLE (bit 28) the target is written if ((info & ~(0x00039f7cu | 0x00040000u | 0x10000000u | 0x00002000u | multisampleInfo)) != 0) throw std::runtime_error("AGC graphics: color compression, DCC, endian conversion, nonstandard rounding or color optimization is unsupported (CB_COLOR_INFO 0x" + [&] { char text[16]; std::snprintf(text, sizeof(text), "%08x", info); return std::string(text); }() + ")"); Require((info & 0x8000u) != 0 || number <= 1u || number == 7 || number == 4 || number == 5, "unclamped normalized color is unsupported"); const auto view = read(cx, 0x31b + stride); @@ -1561,6 +1561,7 @@ ColorTarget DecodeColorBuffer(const Registers& cx, std::uint32_t slot, bool allo color.packing = decoded.packing; if (color.packing != ShaderRecompiler::ColorExportPacking::None && (info & 0x10000000u) != 0) throw std::runtime_error("AGC graphics: DCC-compressed " + std::string(PackedColorTargetName(color.packing)) + " color targets are unsupported"); Require(color.packing != ShaderRecompiler::ColorExportPacking::Unorm10_11_11 || (info & 0x00040000u) == 0, "truncating (ROUND_MODE) 10_11_11 unorm color targets are unsupported"); + Require(color.packing != ShaderRecompiler::ColorExportPacking::Float11_11_10 || (info & 0x00040000u) != 0, "rounding (ROUND_MODE 0) 11_11_10 float color targets are unsupported"); for (std::uint32_t word = 0; word < 2; ++word) { const auto clear = find(cx, 0x323 + word + stride); color.clearWords[word] = clear == cx.end() ? 0u : clear->second; @@ -1659,6 +1660,7 @@ std::optional DecodeColorMetadataPass(const QueueState& queue Require(destination.samples == 1, "CB resolve into a multisampled target is unsupported"); Require(destination.cmaskAddress == 0, "CB resolve into a fast-clear color target is unsupported (the resolve does not update its CMASK)"); Require(source.format == destination.format && source.extent.width == destination.extent.width && source.extent.height == destination.extent.height, "CB resolve between different formats or extents is unsupported"); + if (source.packing != ShaderRecompiler::ColorExportPacking::None) throw std::runtime_error("AGC graphics: CB resolves of " + std::string(PackedColorTargetName(source.packing)) + " color targets are unsupported"); requireCovered(destination); pass.source = source; pass.targets.push_back(destination); diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 6ae08a843d..d172a6e7f3 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -3065,6 +3065,11 @@ void multisampleTests() { } queue.context[0x32b] = queue.context[0x31c] ^ (1u << 8u); expectFailure([&] { DecodeColorMetadataPass(queue); }, "different formats or extents"); + for (const auto [packedInfo, name] : {std::pair{6u << 2u, "10_11_11 unorm"}, std::pair{(7u << 2u) | (7u << 8u) | (2u << 11u) | 0x40000u, "11_11_10 float"}}) { + auto packed = rasterizedResolve; + for (const auto offset : {0x31cu, 0x32bu}) packed.context[offset] = (packed.context[offset] & ~0x41f7cu) | packedInfo; + expectFailure([&] { DecodeColorMetadataPass(packed); }, std::string("CB resolves of ") + name + " color targets"); + } queue = rasterizedResolve; queue.context[0x31c] |= 0x10000000u; queue.context[0x325] = 0; @@ -6978,7 +6983,7 @@ void float11_11_10TargetTests() { Require(AgcDriver::Graphics::ExportMappings(state)[0] == 0xc6u, "export 0 did not reverse its components into the 11_11_10 float target"); auto changed = queue; changed.context[0x31c] &= ~0x40000u; - Require(AgcDriver::Graphics::DecodeState(changed).color.packing == ColorExportPacking::Float11_11_10, "an 11_11_10 float target without ROUND_MODE was refused"); + expectFailure([&] { AgcDriver::Graphics::DecodeState(changed); }, "rounding (ROUND_MODE 0) 11_11_10 float color targets"); for (const auto swap : {0u, 1u, 3u}) { changed = queue; changed.context[0x31c] = (changed.context[0x31c] & ~0x1800u) | (swap << 11u); From 45c78bb880d86c5a31a447185fe260ca46e38017 Mon Sep 17 00:00:00 2001 From: Gjon Gojcevic Date: Sat, 17 Oct 2026 14:53:05 -0400 Subject: [PATCH 0869/1000] test(agc): refuse an additive blend into an 11_11_10 float target The blend-refusal case used CB_BLEND_CONTROL 0x60010001, which is ONE * source + ZERO * destination: a replace blend that writes the source unchanged. A packed target can draw such a blend with blending disabled, so the case now uses the additive blend 0x60010101, which a packed target must still refuse. AI-assisted: yes --- core/libs/prx/libSceAgcDriver/tests/Graphics.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index d172a6e7f3..bf27788277 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -6995,7 +6995,7 @@ void float11_11_10TargetTests() { expectFailure([&] { AgcDriver::Graphics::DecodeState(changed); }, "unsupported color format 7 number type " + std::to_string(number) + " component swap 2"); } changed = queue; - changed.context[0x1e0] = 0x60010001u; + changed.context[0x1e0] = 0x60010101u; for (std::uint32_t i = 0; i < 4; ++i) changed.context[0x105 + i] = 0; expectFailure([&] { AgcDriver::Graphics::DecodeState(changed); }, "blending into an 11_11_10 float color target"); changed = queue; From dce77846be442c94db17cb62d8e7badf7492af82 Mon Sep 17 00:00:00 2001 From: Gjon Gojcevic Date: Sat, 17 Oct 2026 17:13:00 -0400 Subject: [PATCH 0870/1000] fix(shader): collect packed ancillary readers without recursion lowerPackedAncillary follows the selects, phis and identities that forward the packed ancillary word to its readers by recursing once per link (collectReaders), with a linear scan of the visited list at each step. A long forwarding chain overflowed the stack of the thread that compiles the shader: PPSA01317 crashes about 30 s into boot in that recursion on a guest job thread, with a 0xc00000fd stack overflow inside malloc. The readers are now collected with an explicit stack and a visited set, in the same order. AI-assisted: yes --- .../prx/libSceAgcDriver/tests/ConstantPhi.cpp | 45 +++++++++++++++++++ .../Optimization/src/ConstantFolder.cpp | 30 ++++++------- 2 files changed, 60 insertions(+), 15 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/ConstantPhi.cpp b/core/libs/prx/libSceAgcDriver/tests/ConstantPhi.cpp index 1a811755b3..2cc4a8b370 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ConstantPhi.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ConstantPhi.cpp @@ -2,6 +2,8 @@ #include "Optimization/ConstantFolder.hpp" #include "Optimization/DeadCodeEliminator.hpp" #include +#include +#include #include using namespace ShaderRecompiler; @@ -77,11 +79,54 @@ void CheckDiamond(unsigned mode) { } } +void CheckLongAncillaryForwarding() { + IrProgram program; + auto& block = program.CreateBlock(); + program.BlockOrder().push_back(&block); + BlockInfo info; + info.id = 0u; + info.terminator.kind = TerminatorKind::Return; + program.Metadata().blockInfo.push_back(info); + program.SetEntryBlock(block); + IrBuilder builder(program); + builder.SetInsertionPoint(block); + auto& ancillary = builder.Emit(IrOpcode::GetBuiltin, IrType::U32, {&builder.Constant(static_cast(StageInputKind::PackedAncillary)), &builder.Constant(0u)}); + auto& lane = builder.Emit(IrOpcode::LaneId, IrType::U32, {}); + auto& condition = builder.Emit(IrOpcode::ULessThan32, IrType::Bool, {&lane, &builder.Constant(16u)}); + auto& zero = builder.Constant(0u); + IrValue* forwarded = &ancillary; + for (std::uint32_t link = 0; link < 20000u; ++link) forwarded = &builder.Emit(IrOpcode::SelectU32, IrType::U32, {&condition, &zero, forwarded}); + auto& field = builder.Emit(IrOpcode::BitFieldUExtract, IrType::U32, {forwarded, &builder.Constant(8u), &builder.Constant(4u)}); + builder.Emit(IrOpcode::ReferenceU32, IrType::Void, {&field}); + struct Run { + IrProgram& program; + std::string error; + } run{program, {}}; + pthread_attr_t attributes; + pthread_attr_init(&attributes); + pthread_attr_setstacksize(&attributes, 256u * 1024u); + pthread_t thread; + const auto body = +[](void* argument) -> void* { + auto& state = *static_cast(argument); + try { + ConstantFolder{}.Fold(state.program); + } catch (const std::exception& error) { + state.error = error.what(); + } + return nullptr; + }; + Require(pthread_create(&thread, &attributes, body, &run) == 0, "long ancillary forwarding: cannot start the folding thread"); + pthread_join(thread, nullptr); + pthread_attr_destroy(&attributes); + Require(run.error.empty(), "long ancillary forwarding: folding failed"); +} + } int main() { try { for (unsigned mode = 0; mode < 5u; ++mode) CheckDiamond(mode); + CheckLongAncillaryForwarding(); std::puts("constant phi dominance tests passed"); return 0; } catch (const std::exception& error) { diff --git a/core/shader/recompiler/Optimization/src/ConstantFolder.cpp b/core/shader/recompiler/Optimization/src/ConstantFolder.cpp index 52643c8990..3f3e35b2b9 100644 --- a/core/shader/recompiler/Optimization/src/ConstantFolder.cpp +++ b/core/shader/recompiler/Optimization/src/ConstantFolder.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include @@ -289,26 +290,25 @@ bool forwardsWord(const IrUse& use) { return use.user->Opcode() == IrOpcode::Identity || use.user->Opcode() == IrOpcode::Phi || (use.user->Opcode() == IrOpcode::SelectU32 && use.operand != 0u); } -void collectReaders(IrValue& value, std::vector& readers, std::vector& visited) { - if (std::ranges::find(visited, &value) != visited.end()) { - return; - } - visited.push_back(&value); - const std::vector uses = value.OperandUses(); - for (const IrUse& use : uses) { - if (forwardsWord(use)) { - collectReaders(*use.user, readers, visited); - } else { +void collectReaders(IrValue& value, std::vector& readers) { + std::unordered_set visited{&value}; + std::vector, std::size_t>> pending; + pending.emplace_back(value.OperandUses(), 0u); + while (!pending.empty()) { + auto& [uses, next] = pending.back(); + if (next == uses.size()) { + pending.pop_back(); + continue; + } + const IrUse use = uses[next++]; + if (!forwardsWord(use)) { readers.push_back(use); + } else if (visited.insert(use.user).second) { + pending.emplace_back(use.user->OperandUses(), 0u); } } } -void collectReaders(IrValue& value, std::vector& readers) { - std::vector visited; - collectReaders(value, readers, visited); -} - constexpr std::array, 2> packedFields {{{8u, 4u}, {16u, 13u}}}; std::optional packedField(std::uint32_t offset, std::uint32_t count) { From 6e5d708f8272917defba1b576b5a00e13d0e42c1 Mon Sep 17 00:00:00 2001 From: Gjon Gojcevic Date: Sat, 17 Oct 2026 17:03:04 -0400 Subject: [PATCH 0871/1000] fix(recompiler): drop a sample-adjust S# dword 3 that sets only reserved bits An image_sample_*_a sampler whose dword 3 is built only from values inside the reserved bits 12-29 was refused with "GetSamplerResource dword 3 is not a valid runtime value" when the value was not an OR at the top. The reserved bits of a sample-adjust S# are already dropped term by term from an OR (canonicalizeSampleAdjustDword3); a dword 3 whose possible bits all lie in that mask, such as a shifted field with nothing else, is now dropped whole the same way. PPSA01317 builds such a dword 3 from a wave ballot shifted to bit 12 for image_sample_a in a pixel shader. A plain image_sample with the same S# is still refused. AI-assisted: yes --- core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp | 9 +++++++++ .../recompiler/Optimization/src/ResourceTracker.cpp | 6 +++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 905cf58780..ccbfa20c9c 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -4200,6 +4200,15 @@ void verifyUnnormalizedSamplers() { reject(0xf11c0108u, userData(unnormalized), "unnormalized guest sampler is used by a gather, which is not implemented", "unnormalized samplers: image_gather4_lz was accepted"); reject(0xf1800308u, userData(unnormalized), "unnormalized guest sampler is used by image_get_lod, which is not implemented", "unnormalized samplers: image_get_lod was accepted"); reject(0xf0800f09u, userData(unnormalized), "unnormalized guest sampler is used by an image_sample_*_a variant, which is not implemented", "unnormalized samplers: image_sample_a was accepted"); + const auto reservedDword3 = [&](std::uint32_t mimg) { + auto code = program(mimg); + const std::array computed{0x7d8a0080u, 0x870bff6au, 0x000000ffu, 0x8f0b8c0bu}; + code.insert(code.begin(), computed.begin(), computed.end()); + return code; + }; + const auto adjusted = recompile(reservedDword3(0xf0800f09u), userData(normalized)); + require(proven(adjusted, false), "sampler dword 3: an image_sample_a whose S# dword 3 holds only a ballot in its reserved bits was not compiled"); + expectFailure([&] { static_cast(recompile(reservedDword3(0xf0800f08u), userData(normalized))); }, "GetSamplerResource dword 3 is not a valid runtime value", "sampler dword 3: an image_sample whose S# dword 3 holds a ballot was accepted"); reject(0xf09c0f18u, imageData(unnormalized, TypeCube, Format8888UNorm, 5u), "unnormalized guest sampler samples a 1D-array, 2D-array, 3D, cube or multisampled image", "unnormalized samplers: a cube T# was accepted"); reject(0xf09c0f10u, imageData(unnormalized, Type3D, Format8888UNorm, 3u), "unnormalized guest sampler samples a 1D-array, 2D-array, 3D, cube or multisampled image", "unnormalized samplers: a 3D T# was accepted"); reject(0xf09c0f28u, imageData(unnormalized, Type2DArray, Format8888UNorm, 5u), "unnormalized guest sampler samples a 1D-array, 2D-array, 3D, cube or multisampled image", "unnormalized samplers: a 2D-array sample was accepted"); diff --git a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp index 0ca144b9d0..d7d0e65d29 100644 --- a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp @@ -209,6 +209,9 @@ private: IrValue* canonicalizeSampleAdjustDword3(IrValue* value) { for (;;) { value = value->Resolve(); + if ((possibleU32Bits(value) & ~samplerDword3ReservedMask) == 0u) { + return &m_builder.Constant(0u); + } if (value->Opcode() != IrOpcode::BitwiseOr32) { return value; } @@ -216,9 +219,6 @@ private: IrValue* right = value->Argument(1)->Resolve(); const bool leftReserved = (possibleU32Bits(left) & ~samplerDword3ReservedMask) == 0u; const bool rightReserved = (possibleU32Bits(right) & ~samplerDword3ReservedMask) == 0u; - if (leftReserved && rightReserved) { - return &m_builder.Constant(0u); - } if (leftReserved) { value = right; } else if (rightReserved) { From f7cd8df3095f1738def5ffe08ba21b46e3665009 Mon Sep 17 00:00:00 2001 From: milescoviello Date: Sat, 17 Oct 2026 21:31:10 +0000 Subject: [PATCH 0872/1000] fix(agc): trace the title threads that register shaders Shader registration and the shader ABI resolves run on the title's own threads, which never bound a trace buffer, so their Shader.Register and Shader.Resolve* scopes were missing from every trace and summary. Those entry points now bind the calling thread as `title` with the 32,768-event buffer the shader preparation threads use. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 12 +++ .../src/Driver/Shaders/ShaderRegistry.cpp | 12 +++ .../tests/execution/RegistrationTrace.cpp | 83 +++++++++++++++++++ docs/dev/BUILD.md | 2 +- 4 files changed, 108 insertions(+), 1 deletion(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/RegistrationTrace.cpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 95751ed9e4..5052bd70ce 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1650,6 +1650,18 @@ if(BUILD_TESTING) set_tests_properties(agc_driver_untracked_snapshot_hint PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" TIMEOUT 30 ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + set_tests_properties(agc_driver_registration_trace PROPERTIES + ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_TRACE_FRAMES=0:1;APS5_TRACE_EVENTS=64" TIMEOUT 30) + set_property(TEST agc_driver_registration_trace APPEND PROPERTY ENVIRONMENT_MODIFICATION + "APS5_TRACE_SECONDS=unset:" "APS5_TRACE_SUMMARY_EVERY=unset:") + add_test(NAME agc_driver_registration_trace_disabled COMMAND ${CMAKE_COMMAND} -E env + --unset=APS5_TRACE_FRAMES --unset=APS5_TRACE_SECONDS --unset=APS5_TRACE_SUMMARY_EVERY + $ disabled) + set_tests_properties(agc_driver_registration_trace_disabled PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" TIMEOUT 30) + if(WIN32) + set_tests_properties(agc_driver_registration_trace_disabled PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test(NAME agc_sparse_buffer_holes COMMAND agc_driver_graphics_tests --sparse-buffer-holes) set_tests_properties(agc_driver_native_multisample_depth PROPERTIES TIMEOUT 30) add_test(NAME agc_driver_native_depth_retirement COMMAND agc_driver_native_multisample_depth_tests --retirement) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp index 1b1299da12..2a654e0e53 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp @@ -1245,7 +1245,16 @@ std::vector PrepareGraphicsStages(const DrawDecode& decod return prepared; } +namespace { + +void BindTitleThread() { + if (PerformanceTrace::Configured()) PerformanceTrace::BindThread("title", TraceThread::DefaultCapacity / 8); +} + +} + void Driver::ResolveGraphicsStagesAbi(std::span stages, std::span context, std::span primitive) { + BindTitleThread(); PerformanceContext timingContext(FrameTiming::Preparation()); PerformanceTimer timing("Shader.ResolveGraphicsStagesAbi"); ShaderPreparationTransaction transaction; @@ -1322,6 +1331,7 @@ void Driver::ResolveGraphicsStagesAbi(std::span stages, std } void Driver::ResolveShaderAbi(const Shader* shader, std::span context, std::span primitive) { + BindTitleThread(); PerformanceContext timingContext(FrameTiming::Preparation()); PerformanceTimer timing("Shader.ResolveAbi"); ShaderPreparationTransaction transaction; @@ -1393,6 +1403,7 @@ std::uint64_t NullPixelProgramAddress() { } void Driver::ResolveGraphicsAbi(const Shader* vertex, const Shader* pixel, std::uint32_t primitiveType) { + BindTitleThread(); PerformanceContext timingContext(FrameTiming::Preparation()); PerformanceTimer timing("Shader.ResolveGraphicsAbi"); CheckFailure(); @@ -1519,6 +1530,7 @@ std::shared_ptr Driver::RegisteredShaders() { } void Driver::RegisterShader(const Shader* shader) { + BindTitleThread(); PerformanceContext timingContext(FrameTiming::Preparation()); PerformanceTimer timing("Shader.Register"); ShaderPreparationTransaction transaction; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/RegistrationTrace.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/RegistrationTrace.cpp new file mode 100644 index 0000000000..e2c2e72152 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/RegistrationTrace.cpp @@ -0,0 +1,83 @@ +#include "VulkanTestDevice.hpp" +#include "SceShaders.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +alignas(256) const std::array PixelCode{0xbf810000u}; + +struct PixelHeader { + Shader shader{}; + std::array registers{}; + std::array context{{{0x1b3, 2}, {0x1b4, 2}, {0x1b6, 1}}}; + + PixelHeader() { + const auto address = reinterpret_cast(PixelCode.data()); + shader.file_header = 0x34333231u; + shader.version = 0x18; + shader.header_size = sizeof(PixelHeader); + shader.shader_size = sizeof(PixelCode); + shader.code = PixelCode.data(); + shader.type = 1; + registers = {{{0x008, static_cast(address >> 8u)}, {0x009, static_cast(address >> 40u)}, {0x00a, 0}, {0x00b, 0}}}; + shader.sh_registers = registers.data(); + shader.num_sh_registers = registers.size(); + shader.cx_registers = context.data(); + shader.num_cx_registers = context.size(); + } +}; + +const char* CheckRegistration(bool disabled) { + PixelHeader pixel; + AgcDriverRegisterShader_nid_postfix(&pixel.shader); + auto* trace = AgcDriver::PerformanceTrace::Thread(); + if (disabled) return trace != nullptr ? "an unconfigured trace bound a buffer to a registering thread" : nullptr; + if (trace == nullptr) return "a title thread that registered a shader has no trace buffer"; + const auto snapshot = trace->Collect(); + if (snapshot.name != "title" || snapshot.dropped) return "the registering thread's trace identity or capacity is incorrect"; + const auto registration = [](const auto& event) { return event.scope != nullptr && std::string_view(event.scope) == "Shader.Register" && event.stage == nullptr && event.length > std::chrono::steady_clock::duration::zero(); }; + if (std::none_of(snapshot.slices.begin(), snapshot.slices.end(), registration) || std::none_of(snapshot.totals.begin(), snapshot.totals.end(), registration)) return "the registration was missing from the title thread's trace slices or totals"; + return nullptr; +} + +} + +int main(int argc, char** argv) { + try { + if (!OpenVulkanTestDevice()) return VulkanTestSkipped; + const bool disabled = argc > 1 && std::string_view(argv[1]) == "disabled"; + if (AgcDriver::PerformanceTrace::Configured() == disabled) throw std::runtime_error("unexpected trace configuration"); + if (!disabled) AgcDriver::PerformanceTrace::Present(1, 1, nullptr); + std::string failure = "the registering thread did not run"; + std::thread([&] { + try { + const char* message = CheckRegistration(disabled); + failure = message != nullptr ? message : ""; + } catch (const std::exception& error) { + failure = error.what(); + } + }).join(); + if (!failure.empty()) throw std::runtime_error(failure); + AgcDriverShutdown_nid_postfix(); + std::cout << "registration tracing checked\n"; + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + try { + AgcDriverShutdown_nid_postfix(); + } catch (const std::exception& shutdownError) { + std::cerr << shutdownError.what() << '\n'; + } + return 1; + } +} diff --git a/docs/dev/BUILD.md b/docs/dev/BUILD.md index 03e4c72be7..71dcccda64 100644 --- a/docs/dev/BUILD.md +++ b/docs/dev/BUILD.md @@ -151,7 +151,7 @@ Set `APS5_SAMPLE_WORKER=` to sample queue worker 0 every millisecond (in p Set `APS5_TRACE_FRAMES=:` to record `` presents from present `` (counted from 0), or `APS5_TRACE_SECONDS=:` to record from the first present at least `` seconds after the first present until the first present `` seconds later. Both values are whole numbers and the length is above 0; a malformed value, or both switches together, fails the first present. The window opens and closes at presents. -Set `APS5_TRACE_EVENTS=` to change the queue-worker and presenter buffers for a frame or seconds window. The count is a whole number from 1 through 4,194,304; the default is 262,144. An invalid value fails when one of these threads first binds to the trace, before recording. Each buffer uses `count * sizeof(TraceThread::Slice)` bytes (currently 32 bytes per event): 8 MiB at the default, 96 MiB at `3145728`, and 128 MiB at the maximum, per thread. The buffers are allocated even while waiting for the window to open, and formatting the trace uses additional memory. Shader preparation keeps its explicitly sized 32,768-event buffers, and the GPU range limit stays 262,144. Summary-only sessions allocate no event buffers and do not read this setting. A larger buffer can still fill; use a shorter window or increase the count within the bound. +Set `APS5_TRACE_EVENTS=` to change the queue-worker and presenter buffers for a frame or seconds window. The count is a whole number from 1 through 4,194,304; the default is 262,144. An invalid value fails when one of these threads first binds to the trace, before recording. Each buffer uses `count * sizeof(TraceThread::Slice)` bytes (currently 32 bytes per event): 8 MiB at the default, 96 MiB at `3145728`, and 128 MiB at the maximum, per thread. The buffers are allocated even while waiting for the window to open, and formatting the trace uses additional memory. Shader preparation keeps its explicitly sized 32,768-event buffers, as does a title thread named `title` that binds one the first time it registers a shader or resolves a shader ABI. The GPU range limit stays 262,144. Summary-only sessions allocate no event buffers and do not read this setting. A larger buffer can still fill; use a shorter window or increase the count within the bound. Inside the window the queue workers and the presenter record every `PerformanceTimer` scope and stage as a complete event named after its frame timing key: `Driver.WorkerWait`, `Driver.Packet`, `Driver.Packet.draw`, `Graphics.Draw`, `Driver.Present`, `Driver.Present.inflight_wait`, `Vulkan.Retire` and so on. A slice shorter than 20 us is only counted: its time is in the thread's counter ` folded` (milliseconds per present) and in the summary. A thread writes a present's counter sample at its first slice after the next present, so the last present of the window has no sample; its time is still in the summary. By default, each queue worker and presenter allocates a buffer for 262,144 events when it starts; a full buffer stops that thread's recording, sets `trace_dropped` and puts an instant event `trace buffer full` on the thread's track at the start of the first lost slice (slices are appended when their outer scope ends, so that can precede the last recorded slice by up to one outer-scope length), with the number of lost slices in `args.events_lost`. When the window closes, the presenter formats the trace once and a `ProfileOutput` writer thread writes `aps5-trace--.json.gz` (Chrome trace-event JSON, opened by ui.perfetto.dev or chrome://tracing) and appends one line to `aps5-summary.jsonl`, both in `APS5_TRACE_DIR` or the working directory. A window that is still open when the title exits writes nothing. From c56cafae2e9a351559f964da45b176aed23940b4 Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Sat, 17 Oct 2026 21:43:38 +0000 Subject: [PATCH 0873/1000] fix(relinker): omit uncovered GNU RELRO ranges --- .../general/ProgramHeaderLayoutBuilder.cpp | 44 ++++++++++++++++++- .../relinker/tests/test_linux_gnu_segments.py | 25 +++++++++++ 2 files changed, 67 insertions(+), 2 deletions(-) diff --git a/core/relinker/elfpatcher/src/general/ProgramHeaderLayoutBuilder.cpp b/core/relinker/elfpatcher/src/general/ProgramHeaderLayoutBuilder.cpp index 5b89c5d133..af09d65c99 100644 --- a/core/relinker/elfpatcher/src/general/ProgramHeaderLayoutBuilder.cpp +++ b/core/relinker/elfpatcher/src/general/ProgramHeaderLayoutBuilder.cpp @@ -6,10 +6,49 @@ #include #include +#include #include namespace Elfpatcher { +namespace { + +bool hasWritableLoadCoverage( + const std::vector& headers, + const Domain::ProgramHeader& relro +) { + if (relro.MemorySize == 0) return true; + if (relro.MemorySize > std::numeric_limits::max() - relro.MappedAddress) return false; + const std::uint64_t pageMask = kDefaultLoadAlignment - 1; + const std::uint64_t relroStart = relro.MappedAddress & ~pageMask; + const std::uint64_t relroEndBytes = relro.MappedAddress + relro.MemorySize; + if (relroEndBytes > std::numeric_limits::max() - pageMask) return false; + const std::uint64_t relroEnd = (relroEndBytes + pageMask) & ~pageMask; + if (relroStart == relroEnd) return true; + + std::vector> writableRanges; + for (const auto& header : headers) { + if (header.Type != PT_LOAD || (header.Flags & PF_W) == 0) continue; + const std::uint64_t size = std::max(header.FileSize, header.MemorySize); + if (size > std::numeric_limits::max() - header.MappedAddress) continue; + const std::uint64_t start = header.MappedAddress & ~pageMask; + const std::uint64_t endBytes = header.MappedAddress + size; + if (endBytes > std::numeric_limits::max() - pageMask) continue; + const std::uint64_t end = (endBytes + pageMask) & ~pageMask; + if (start < end) writableRanges.emplace_back(start, end); + } + std::sort(writableRanges.begin(), writableRanges.end()); + std::uint64_t coveredUntil = relroStart; + for (const auto& [start, end] : writableRanges) { + if (start > coveredUntil) break; + if (end > coveredUntil) coveredUntil = end; + if (coveredUntil >= relroEnd) return true; + } + return false; +} + +} + ProgramHeaderLayoutBuilder::ProgramHeaderLayoutBuilder( std::shared_ptr segmentFilter, std::shared_ptr byteWriter @@ -102,8 +141,9 @@ std::uint64_t ProgramHeaderLayoutBuilder::ComputeExtraBlockVaddr( std::vector ProgramHeaderLayoutBuilder::_keptHeaders(const std::vector& originalHeaders) const { std::vector kept; for (const auto& ph : originalHeaders) { - if (!_segmentFilter->ShouldSkip(ph)) - kept.push_back(ph); + if (_segmentFilter->ShouldSkip(ph)) continue; + if (ph.Type == PT_GNU_RELRO && !hasWritableLoadCoverage(originalHeaders, ph)) continue; + kept.push_back(ph); } return kept; } diff --git a/core/relinker/relinker/tests/test_linux_gnu_segments.py b/core/relinker/relinker/tests/test_linux_gnu_segments.py index a45e51f101..bf83662654 100644 --- a/core/relinker/relinker/tests/test_linux_gnu_segments.py +++ b/core/relinker/relinker/tests/test_linux_gnu_segments.py @@ -1,6 +1,7 @@ """Check Linux output preserves GNU stack and RELRO program headers.""" from pathlib import Path +import signal import struct import subprocess import sys @@ -17,6 +18,7 @@ BASE = 0x400000 def image(): elf = fixture() struct.pack_into(" Date: Sun, 18 Oct 2026 00:49:58 +0300 Subject: [PATCH 0874/1000] fix(libkernel): reject PTHREAD_PROCESS_SHARED in pshared setters as the PS5 does On a PS5 (firmware 12.70), of the values 0, 1, 2 and -1, pthread_spin_init, pthread_barrierattr_setpshared and pthread_mutexattr_setpshared accept only PTHREAD_PROCESS_PRIVATE (0); 1, 2 and -1 return EINVAL and leave the attribute unchanged. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libkernel/Pthread/Posix/Barrier.cpp | 3 +-- core/libs/prx/libkernel/Pthread/Posix/Mutex.cpp | 3 +-- core/libs/prx/libkernel/Pthread/Posix/Spinlock.cpp | 3 +-- core/libs/tests/GuestPthreadBarrier.cpp | 13 +++++++------ core/libs/tests/GuestPthreadMutexattr.cpp | 7 +++---- core/libs/tests/GuestPthreadSpin.cpp | 2 +- docs/dev/debt/libkernel.md | 4 +--- 7 files changed, 15 insertions(+), 20 deletions(-) diff --git a/core/libs/prx/libkernel/Pthread/Posix/Barrier.cpp b/core/libs/prx/libkernel/Pthread/Posix/Barrier.cpp index 4d03696ff6..8a4c613626 100644 --- a/core/libs/prx/libkernel/Pthread/Posix/Barrier.cpp +++ b/core/libs/prx/libkernel/Pthread/Posix/Barrier.cpp @@ -18,7 +18,6 @@ struct PthreadBarrierPrivate { namespace { constexpr int PROCESS_PRIVATE = 0; -constexpr int PROCESS_SHARED = 1; } @@ -54,7 +53,7 @@ int APS5_VABI pthread_barrierattr_getpshared_nid_postfix(PthreadBarrierattrPriva int APS5_VABI pthread_barrierattr_setpshared_nid_postfix(PthreadBarrierattrPrivate** attr, int pshared) { if (!attr || !*attr) return 22; - if (pshared != PROCESS_PRIVATE && pshared != PROCESS_SHARED) return 22; + if (pshared != PROCESS_PRIVATE) return 22; (*attr)->pshared = pshared; return 0; } diff --git a/core/libs/prx/libkernel/Pthread/Posix/Mutex.cpp b/core/libs/prx/libkernel/Pthread/Posix/Mutex.cpp index fb378b0a8d..bf93cf4691 100644 --- a/core/libs/prx/libkernel/Pthread/Posix/Mutex.cpp +++ b/core/libs/prx/libkernel/Pthread/Posix/Mutex.cpp @@ -13,7 +13,6 @@ namespace { constexpr int POSIX_EINVAL = 22; constexpr int POSIX_PROCESS_PRIVATE = 0; -constexpr int POSIX_PROCESS_SHARED = 1; constexpr int GUEST_REALTIME_CLOCK = 0; constexpr int POSIX_PRIO_PROTECT = 2; constexpr int POSIX_SCHED_RR = 3; @@ -99,7 +98,7 @@ int APS5_VABI pthread_mutexattr_getpshared_nid_postfix(const PthreadMutexattr* a int APS5_VABI pthread_mutexattr_setpshared_nid_postfix(PthreadMutexattr* attr, int pshared) { if (!attr || !*attr) return POSIX_EINVAL; - if (pshared != POSIX_PROCESS_PRIVATE && pshared != POSIX_PROCESS_SHARED) return POSIX_EINVAL; + if (pshared != POSIX_PROCESS_PRIVATE) return POSIX_EINVAL; (*attr)->pshared = pshared; return 0; } diff --git a/core/libs/prx/libkernel/Pthread/Posix/Spinlock.cpp b/core/libs/prx/libkernel/Pthread/Posix/Spinlock.cpp index 3990f0d293..9101c62da8 100644 --- a/core/libs/prx/libkernel/Pthread/Posix/Spinlock.cpp +++ b/core/libs/prx/libkernel/Pthread/Posix/Spinlock.cpp @@ -15,7 +15,6 @@ constexpr int GUEST_EPERM = 1; constexpr int GUEST_ENOMEM = 12; constexpr int GUEST_EBUSY = 16; constexpr int PROCESS_PRIVATE = 0; -constexpr int PROCESS_SHARED = 1; constexpr int SPIN_COUNT = 100000; bool tryAcquire(PthreadSpinlockPrivate* spinlock) { @@ -29,7 +28,7 @@ extern "C" { int APS5_VABI pthread_spin_init_nid_postfix(PthreadSpinlockPrivate** lock, int pshared) { if (!lock) return PosixThread::GUEST_EINVAL; - if (pshared != PROCESS_PRIVATE && pshared != PROCESS_SHARED) return PosixThread::GUEST_EINVAL; + if (pshared != PROCESS_PRIVATE) return PosixThread::GUEST_EINVAL; auto* spinlock = new (std::nothrow) PthreadSpinlockPrivate; if (!spinlock) return GUEST_ENOMEM; *lock = spinlock; diff --git a/core/libs/tests/GuestPthreadBarrier.cpp b/core/libs/tests/GuestPthreadBarrier.cpp index 6686ebc013..6fbdcb4bad 100644 --- a/core/libs/tests/GuestPthreadBarrier.cpp +++ b/core/libs/tests/GuestPthreadBarrier.cpp @@ -73,12 +73,13 @@ int main() { Require(pthread_barrierattr_getpshared_nid_postfix(&attr, &pshared) == 0 && pshared == PROCESS_PRIVATE); Require(pthread_barrierattr_setpshared_nid_postfix(&attr, 2) == GUEST_EINVAL); Require(pthread_barrierattr_setpshared_nid_postfix(&attr, -1) == GUEST_EINVAL); - Require(pthread_barrierattr_setpshared_nid_postfix(&attr, PROCESS_SHARED) == 0); - Require(pthread_barrierattr_getpshared_nid_postfix(&attr, &pshared) == 0 && pshared == PROCESS_SHARED); - Context shared; - Require(pthread_barrier_init_nid_postfix(&shared.barrier, &attr, WAITERS) == 0); - RunWaiters(shared); - Require(pthread_barrier_destroy_nid_postfix(&shared.barrier) == 0); + Require(pthread_barrierattr_setpshared_nid_postfix(&attr, PROCESS_SHARED) == GUEST_EINVAL); + pshared = -1; + Require(pthread_barrierattr_getpshared_nid_postfix(&attr, &pshared) == 0 && pshared == PROCESS_PRIVATE); + Context afterRejectedShared; + Require(pthread_barrier_init_nid_postfix(&afterRejectedShared.barrier, &attr, WAITERS) == 0); + RunWaiters(afterRejectedShared); + Require(pthread_barrier_destroy_nid_postfix(&afterRejectedShared.barrier) == 0); Require(pthread_barrierattr_setpshared_nid_postfix(&attr, PROCESS_PRIVATE) == 0); pshared = -1; Require(pthread_barrierattr_getpshared_nid_postfix(&attr, &pshared) == 0 && pshared == PROCESS_PRIVATE); diff --git a/core/libs/tests/GuestPthreadMutexattr.cpp b/core/libs/tests/GuestPthreadMutexattr.cpp index 75ba65971c..7e704677e5 100644 --- a/core/libs/tests/GuestPthreadMutexattr.cpp +++ b/core/libs/tests/GuestPthreadMutexattr.cpp @@ -90,11 +90,10 @@ static void CheckProcessSharing() { Require(pthread_mutexattr_getpshared_nid_postfix(&attr, &sharing) == 0 && sharing == 0); Require(pthread_mutexattr_settype_nid_postfix(&attr, 2) == 0); Require(pthread_mutexattr_setpshared_nid_postfix(&attr, 0) == 0); - Require(pthread_mutexattr_setpshared_nid_postfix(&attr, 1) == 0); - Require(pthread_mutexattr_getpshared_nid_postfix(&attr, &sharing) == 0 && sharing == 1); - for (const int invalid : {-1, 2, 2147483647}) + for (const int invalid : {1, -1, 2, 2147483647}) Require(pthread_mutexattr_setpshared_nid_postfix(&attr, invalid) == GUEST_EINVAL); - Require(pthread_mutexattr_getpshared_nid_postfix(&attr, &sharing) == 0 && sharing == 1); + sharing = -1; + Require(pthread_mutexattr_getpshared_nid_postfix(&attr, &sharing) == 0 && sharing == 0); int type = -1; Require(pthread_mutexattr_gettype_nid_postfix(&attr, &type) == 0 && type == 2); Require(Throws([&] { pthread_mutexattr_getpshared_nid_postfix(&attr, nullptr); })); diff --git a/core/libs/tests/GuestPthreadSpin.cpp b/core/libs/tests/GuestPthreadSpin.cpp index 9783df2dd7..ca7e2b6926 100644 --- a/core/libs/tests/GuestPthreadSpin.cpp +++ b/core/libs/tests/GuestPthreadSpin.cpp @@ -51,6 +51,7 @@ static void* APS5_VABI Incrementer(void* arg) { static void ArgumentErrors() { Require(pthread_spin_init_nid_postfix(nullptr, PTHREAD_PROCESS_PRIVATE) == GUEST_EINVAL); PthreadSpinlock lock = nullptr; + Require(pthread_spin_init_nid_postfix(&lock, PTHREAD_PROCESS_SHARED) == GUEST_EINVAL); Require(pthread_spin_init_nid_postfix(&lock, 2) == GUEST_EINVAL); Require(pthread_spin_init_nid_postfix(&lock, -1) == GUEST_EINVAL); Require(lock == nullptr); @@ -116,7 +117,6 @@ static void MutualExclusion() { int main() { ArgumentErrors(); SingleThread(PTHREAD_PROCESS_PRIVATE); - SingleThread(PTHREAD_PROCESS_SHARED); HeldByAnotherThread(); MutualExclusion(); } diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 2fb9d30137..185914b882 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -69,7 +69,6 @@ - [__progname](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - unknown data export - [environ](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - data export pointing to a fixed empty environment so titles that import it load; it is not the environment libc's `getenv` reads, so it lists none of those variables and `setenv`/`putenv` changes do not appear in it - [seteuid, setegid, setuid, setreuid, setregid, setgroups, getgroups](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - the guest process is assumed to run as root, with real, effective and saved user and group ids 0. As in FreeBSD's `sys_seteuid`, `sys_setegid`, `sys_setuid`, `sys_setreuid` and `sys_setregid`, id 0 (or -1 for an unchanged `setreuid`/`setregid` id) is then accepted without a privilege check and changes no credential, so 0 is returned; any other id would change an id, which is not modelled, and throws. `setgroups` keeps the supplementary group list as `kern_setgroups` does (`NGROUPS_MAX` 1023, an empty list leaves the effective group as the only entry, a first entry other than 0 would change the effective group and throws) and `getgroups` reports it. The ids and privileges a title has on a console are unverified -- [pthread_barrierattr_setpshared](../../../core/libs/prx/libkernel/Pthread/Posix/Barrier.cpp) (libkernel) - PTHREAD_PROCESS_SHARED is accepted on the assumption that the console follows FreeBSD 11.0 libthr, which accepts it (9.0 rejects it with EINVAL); unverified on hardware. Guest code runs in one process, so a shared barrier behaves as a private one - [pthread_getname_np](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - a thread that has already been joined returns `ESRCH` (3) and its output buffer is left untouched, as measured on a PS5 (firmware 13.60) by the author of this entry; since [#4481](https://github.com/boykopovar/AnyPS5/pull/4481) the joined thread's object is retained, so the posix wrapper answers `ESRCH` for it instead of reading the retained name. A null name buffer is answered with `EFAULT` before that, an order the console table does not measure. The SCE sibling `scePthreadGetname` has no measured joined-thread behaviour on the console and still reads the retained object's name, so it is unchanged, as is the pre-existing dangling case of a thread that ended detached (its object is released at exit; see the scePthreadJoin entry) - [sceKernelGetOperationMode](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - the signature `(int* mode, int* submode)` comes from PPSA12544, which logs both values after the call and ignores the result. A homebrew payload on a PS5 (firmware 13.60) reads mode 2 and submode 0, nothing else is written, and the result register holds no status, so the function is taken to return nothing. Whether a title process reads the same values is unverified. Null outputs throw - [sceCoredumpGetStopInfoCpu](../../../core/libs/prx/libkernel/System/src/Coredump.cpp) (libkernel) takes `(void* info, size_t size)` as in the [Orbital type database](https://github.com/AlexAltea/orbital/blob/cc408367da6a6ebbf5ac1ac559fefeadc73b2831/tools/ida/db_types.json); the 16-byte, 8-byte-aligned block follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Kernel/KernelSystem.cs), which places coredump reads inside the handler. The handler is never invoked here, so a well-formed request returns `NOT_IN_COREDUMP_HANDLER` without reading or writing the block. Its value `0x81180003` follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/e0a466753a1aacbc8db8253be685bf7ab4d9daae/src/core/libraries/kernel/coredump/coredump_error.h) and the NHL 27 (PPSA34063) call site reported in [#4275](https://github.com/boykopovar/AnyPS5/discussions/4275), not a PS5 measurement. The console validation order is unverified: null, unaligned or wrapping buffers and sizes other than 16 throw instead of guessing an argument error. Information from an active coredump handler remains unsupported @@ -123,7 +122,6 @@ - [sceKernelWriteThrottlingStatus](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) reports no write throttling: the host SSD has no write budget, so the first qword of the 32-byte status is set to `UINT32_MAX` and the rest is zeroed; returns `EFAULT` (`0x8002000e`) when passed a null status buffer pointer. PPSA17221 reads only that qword: it multiplies it by 64 KiB and compares the product with byte thresholds up to 0x4AF00000 to pick one of five tiers. When the call fails the title assumes 0x4B000001 bytes, which selects the same top tier as `UINT32_MAX`. What the value measures on hardware (a remaining budget or a rate) and the meaning of the other three qwords are unknown - [Windows directory descriptors](../../../core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp) (libkernel) take one snapshot of the directory, on the first `getdents`, `getdirentries` or non-zero `lseek` (`SEEK_SET` or `SEEK_CUR`), and keep it until it is discarded; `lseek` to offset 0 only discards the snapshot, and the next read lists the directory again, so every rewind costs O(entries) on the next read. `lseek` takes the offset as an entry index and `SEEK_CUR` with offset 0 only reports the index. `SEEK_END` throws, where the NUL descriptor used to return 0, because FreeBSD defines it relative to the directory size, which is not modelled. A negative result returns `EINVAL` and a `SEEK_CUR` offset that overflows returns `EOVERFLOW`, as FreeBSD `vn_seek` reports them. Names whose UTF-8 encoding exceeds the one-byte `d_namlen` limit are not representable in this guest directory-record layout: `getdents` returns `ENAMETOOLONG` without consuming the entry. Repeated calls therefore report the same error and entries after that name cannot be reached through this descriptor; FreeBSD's `getdents` behavior for host filenames that exceed its `MAXNAMLEN` is not verified. `getdirentries` still reports 0 through `basep` instead of the position of the block it read, so only 0 and values returned by `lseek` are valid offsets; FreeBSD offsets are filesystem cookies - [/dev/random and /dev/urandom](../../../core/libs/prx/libkernel/File/src/RandomDevice.cpp) (libkernel) opened for reading return a descriptor backed by the host null device whose reads are filled with host OS entropy (`getentropy`, `rand_s` on Windows); both paths behave the same, never block, and positioned reads ignore the offset (FreeBSD `random(4)`). Only the exact paths are recognised, `fstat`, `lseek` and `mmap` see the null device, and opening them for writing throws -- [pthread_mutexattr_getpshared and pthread_mutexattr_setpshared](../../../core/libs/prx/libkernel/Pthread/Posix/Mutex.cpp) (libkernel) store and report private or shared attributes, following FreeBSD 11 libthr [thr_mutexattr.c](https://github.com/freebsd/freebsd-src/blob/stable/11/lib/libthr/thread/thr_mutexattr.c). Invalid values return `EINVAL` without changing the attribute. Guest threads execute in one host process; sharing mutexes across processes is not modeled. Console behavior and invalid-attribute error precedence are unverified. - [pthread_condattr_setpshared](../../../core/libs/prx/libkernel/Pthread/Posix/Cond.cpp) (libkernel) stores a private or shared attribute, following FreeBSD 11 libthr [thr_condattr.c](https://github.com/freebsd/freebsd-src/blob/stable/11/lib/libthr/thread/thr_condattr.c): a null or uninitialized attribute and values other than `PTHREAD_PROCESS_PRIVATE` (0) and `PTHREAD_PROCESS_SHARED` (1) return `EINVAL` without changing it. Guest threads execute in one host process; sharing condition variables across processes is not modeled. PSFlyCast (PPSA99247) imports it and fails to load without it. Console behavior is unverified. - [pthread_attr_getinheritsched, pthread_attr_setinheritsched, pthread_attr_getscope, pthread_attr_setscope, pthread_attr_getstackaddr](../../../core/libs/prx/libkernel/Pthread/Posix/Attr.cpp) (libkernel) follow FreeBSD 11 libthr ([thr_attr.c](https://github.com/freebsd/freebsd-src/blob/5ed7eb0d97ba4436218e810f61bd059acba984c2/lib/libthr/thread/thr_attr.c), [thr_create.c](https://github.com/freebsd/freebsd-src/blob/5ed7eb0d97ba4436218e810f61bd059acba984c2/lib/libthr/thread/thr_create.c)), not measured on the console: the scope defaults to `PTHREAD_SCOPE_SYSTEM` (2), only it and `PTHREAD_SCOPE_PROCESS` (0) are accepted and other values return `EINVAL`, and an inherit value other than `PTHREAD_INHERIT_SCHED` (4) or `PTHREAD_EXPLICIT_SCHED` (0) returns `ENOTSUP`. `pthread_attr_getstackaddr` returns the configured stack address pointer (null by default). A null output of `pthread_attr_getinheritsched` throws, since libthr writes through it unchecked. A thread keeps the scope it was created with, taken from the creating thread under `PTHREAD_INHERIT_SCHED`, and `pthread_attr_get_np` and `scePthreadAttrGet` report it, but every guest thread is a host thread at system scope, so process scope changes nothing - [Shared memory](../../../core/libs/prx/libkernel/File/src/SharedMemory.cpp) (libkernel) on Linux implements anonymous and named objects, bounded descriptor writes and seeks, and `shm_unlink` with effective read/write permission checks from FreeBSD 11. Named paths up to 1023 bytes use `/anyps5-shm-` plus SHA-1; collisions are not detected, and the namespace is shared across processes in the host mount namespace. Named creation applies the guest umask without changing the host mask. Linux procfs and `/dev/shm` are required. Operations use an advisory directory lock; external host operations bypass it, unrelated objects serialize, and descriptor operations require two spare host descriptors and compare device/inode with the initial descriptor identity on both the pinned-object and ordinary-file fallback paths. [Guest mappings](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) support shared and private mappings of these descriptors through the allocation tracker and pin a duplicate descriptor during validation and mapping; mapping requires one spare host descriptor; file-backed mappings use the write-watch scan fallback because aliases and descriptor writes bypass per-mapping tracking. Named objects and file-backed mappings on Windows remain unsupported. Invalid non-null name pointers, console behaviour and title compatibility remain unverified. @@ -196,7 +194,7 @@ - [pthread_kill](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) returns the error number like FreeBSD's `pthread_kill`: `EINVAL` for signals outside 0 to 128 and `ESRCH` for a null thread. Signal 0 reports any other thread handle as existing, without checking that the thread is still alive. A signal to the calling thread goes through `raise`, so it shares `raise`'s limits; a signal to another thread is left on the pending set for whichever thread waits for or unblocks it, since it is not delivered to the chosen thread, and a handle that is not a live guest thread returns `ESRCH` without touching it - [select](../../../core/libs/prx/libkernel/Socket/src/Socket.cpp) (libkernel) validates `tv_usec` but saturates a valid timeout beyond the remaining `steady_clock` range at `time_point::max()`, so the wait can continue effectively indefinitely. The console's behavior for a valid timeout outside the host clock's range is unknown. - [_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 -- [pthread_spin_init, pthread_spin_destroy, pthread_spin_lock, pthread_spin_trylock, pthread_spin_unlock](../../../core/libs/prx/libkernel/Pthread/Posix/Spinlock.cpp) (libkernel) follow FreeBSD 11.4 libthr [`thr_pspinlock.c`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_pspinlock.c) and the owner checks of its [`thr_umtx.h`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_umtx.h#L80-L175), unverified on the console: `EINVAL` for a null or destroyed lock and a `pshared` other than 0 or 1, `EBUSY` from `trylock` while any thread holds it, `EPERM` from `unlock` by a thread that does not hold it. `PTHREAD_PROCESS_SHARED` allocates a private lock, as guest code runs in one process. libthr spins forever when the holder locks again; here that call throws +- [pthread_spin_lock](../../../core/libs/prx/libkernel/Pthread/Posix/Spinlock.cpp) (libkernel) - a lock taken again by the thread that holds it throws here. On a PS5 (firmware 12.70) that call had not returned after 2 s, as FreeBSD 11.4 libthr [`thr_pspinlock.c`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_pspinlock.c) spins forever - [sceKernelGetEventError](../../../core/libs/prx/libkernel/Equeue/src/EventAccessors.cpp) (libkernel) returns the event's `data` as an `int` when `EV_ERROR` (0x4000) is set in `flags` and 0 otherwise, as [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/947627d10d4ef4b68ba3c0189aab6c368e70a8d2/src/kernel/eventQueue.cpp#L663) does and as FreeBSD 11's [kevent](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/kern_event.c#L1005) reports a failed change (`flags = EV_ERROR`, errno in `data`). [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd43e6fddd1ab033e7980cd356d14c964905/kernel/ps4_queue.pas#L643) instead returns `fflags` plus `0x80020000` when `fflags` is nonzero. Whether the console reads `data` or `fflags` and returns the raw errno or an SCE error code is unverified. No event delivered by the equeue here carries `EV_ERROR`, so the call returns 0 for every event `sceKernelWaitEqueue` reports - [pthread_cond_reltimedwait_np](../../../core/libs/prx/libkernel/Pthread/Posix/Cond.cpp) (libkernel) takes `(cond, mutex, usec)`, a relative timeout in microseconds, and is `scePthreadCondTimedwait` with POSIX error numbers. FreeBSD 11.4 has no such function; the signature comes only from shadPS4, which registers one function for this NID and for `scePthreadCondTimedwait` ([`condvar.cpp`](https://github.com/shadps4-emu/shadPS4/blob/3a9f57265d28975658f1ca941daa5c879c7a74bd/src/core/libraries/kernel/threads/condvar.cpp#L229-L234)). shadPS4 reads a 64-bit count; here it is the 32-bit `SceKernelUseconds` of the SCE twin, as for `sem_reltimedwait_np`. Unverified on the console - [sysctl kern.arandom](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) serves FreeBSD 11's MIB {1, 37} (CTL_KERN, KERN_ARANDOM) and the "kern.arandom" sysctlbyname lookup through std::random_device, returning min(*oldLength, 256) random bytes. Following FreeBSD's sysctl_kern_arnd, a null buffer reports min(*oldLength, 256) without writing, a null length returns 0 without writing, and requests with a new value return EPERM (1) as the node is read-only. Cryptographic quality depends on the host's random device implementation. From bdf3adb3790a7942cdbe9577b3718eaf67bb21a8 Mon Sep 17 00:00:00 2001 From: ionut27001 <194206737+ionut27001@users.noreply.github.com> Date: Sun, 18 Oct 2026 00:50:09 +0300 Subject: [PATCH 0875/1000] feat(libkernel): add pthread_rwlockattr_getpshared and pthread_rwlockattr_setpshared Measured on a PS5 (firmware 12.70): the getter reports 0, and of the values 0, 1, 2 and -1 the setter accepts only 0 and checks the value before the attribute. A null or uninitialized attribute, or a null output, crashes the console; here it throws. Co-Authored-By: Claude Opus 5.5 --- .../prx/libkernel/Pthread/Posix/Rwlock.cpp | 14 +++++++ core/libs/tests/GuestPthreadRwlock.cpp | 42 +++++++++++++++++++ docs/dev/debt/libkernel.md | 1 + 3 files changed, 57 insertions(+) diff --git a/core/libs/prx/libkernel/Pthread/Posix/Rwlock.cpp b/core/libs/prx/libkernel/Pthread/Posix/Rwlock.cpp index 1faef35844..c07eadf645 100644 --- a/core/libs/prx/libkernel/Pthread/Posix/Rwlock.cpp +++ b/core/libs/prx/libkernel/Pthread/Posix/Rwlock.cpp @@ -12,6 +12,7 @@ namespace { constexpr int GUEST_REALTIME_CLOCK = 0; +constexpr int PROCESS_PRIVATE = 0; int toPosix(int result) { if (result == 0) @@ -56,6 +57,19 @@ int APS5_VABI pthread_rwlockattr_init_nid_postfix(PthreadRwlockattr* attr) { return toPosix(scePthreadRwlockattrInit(attr)); } +int APS5_VABI pthread_rwlockattr_getpshared_nid_postfix(const PthreadRwlockattr* attr, int* pshared) { + if (!attr || !*attr) throw std::runtime_error("pthread_rwlockattr_getpshared: null attr"); + if (!pshared) throw std::runtime_error("pthread_rwlockattr_getpshared: null pshared"); + *pshared = PROCESS_PRIVATE; + return 0; +} + +int APS5_VABI pthread_rwlockattr_setpshared_nid_postfix(PthreadRwlockattr* attr, int pshared) { + if (pshared != PROCESS_PRIVATE) return PosixThread::GUEST_EINVAL; + if (!attr || !*attr) throw std::runtime_error("pthread_rwlockattr_setpshared: null attr"); + return 0; +} + int APS5_VABI pthread_rwlock_rdlock_nid_postfix(PthreadRwlock* rwlock) { initializeStatic(rwlock, __func__); return toPosix(scePthreadRwlockRdlock(rwlock)); diff --git a/core/libs/tests/GuestPthreadRwlock.cpp b/core/libs/tests/GuestPthreadRwlock.cpp index 1274b6be3a..b7f54df37e 100644 --- a/core/libs/tests/GuestPthreadRwlock.cpp +++ b/core/libs/tests/GuestPthreadRwlock.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include extern "C" { @@ -13,6 +14,8 @@ int APS5_VABI pthread_rwlock_destroy_nid_postfix(PthreadRwlock* rwlock); int APS5_VABI pthread_rwlock_init_nid_postfix(PthreadRwlock* rwlock, const PthreadRwlockattr* attr); int APS5_VABI pthread_rwlockattr_init_nid_postfix(PthreadRwlockattr* attr); int APS5_VABI pthread_rwlockattr_destroy_nid_postfix(PthreadRwlockattr* attr); +int APS5_VABI pthread_rwlockattr_getpshared_nid_postfix(const PthreadRwlockattr* attr, int* pshared); +int APS5_VABI pthread_rwlockattr_setpshared_nid_postfix(PthreadRwlockattr* attr, int pshared); int APS5_VABI pthread_rwlock_rdlock_nid_postfix(PthreadRwlock* rwlock); int APS5_VABI pthread_rwlock_wrlock_nid_postfix(PthreadRwlock* rwlock); int APS5_VABI pthread_rwlock_unlock_nid_postfix(PthreadRwlock* rwlock); @@ -38,6 +41,7 @@ static constexpr int GUEST_EBUSY = 16; static constexpr int GUEST_EINVAL = 22; static constexpr int GUEST_ETIMEDOUT = 60; static constexpr int GUEST_REALTIME_CLOCK = 0; +static constexpr int PROCESS_PRIVATE = 0; static constexpr std::int64_t NANOS_PER_SECOND = 1000000000; static void Require(bool value) { if (!value) std::abort(); } @@ -308,7 +312,45 @@ static void CheckWaitingWriterHoldsBackNewReaders() { Require(pthread_rwlock_destroy_nid_postfix(&recursive) == 0); } +template +static bool Throws(Call call) { + try { + call(); + } catch (const std::runtime_error&) { + return true; + } + return false; +} + +static void CheckAttrProcessSharing() { + PthreadRwlockattr attr = nullptr; + Require(pthread_rwlockattr_init_nid_postfix(&attr) == 0); + int pshared = -1; + Require(pthread_rwlockattr_getpshared_nid_postfix(&attr, &pshared) == 0 && pshared == PROCESS_PRIVATE); + for (const int invalid : {1, 2, -1}) { + Require(pthread_rwlockattr_setpshared_nid_postfix(&attr, invalid) == GUEST_EINVAL); + pshared = -1; + Require(pthread_rwlockattr_getpshared_nid_postfix(&attr, &pshared) == 0 && pshared == PROCESS_PRIVATE); + } + Require(pthread_rwlockattr_setpshared_nid_postfix(&attr, PROCESS_PRIVATE) == 0); + pshared = -1; + Require(pthread_rwlockattr_getpshared_nid_postfix(&attr, &pshared) == 0 && pshared == PROCESS_PRIVATE); + Require(Throws([&] { pthread_rwlockattr_getpshared_nid_postfix(&attr, nullptr); })); + + PthreadRwlockattr unset = nullptr; + for (const int invalid : {1, 2, -1}) { + Require(pthread_rwlockattr_setpshared_nid_postfix(nullptr, invalid) == GUEST_EINVAL); + Require(pthread_rwlockattr_setpshared_nid_postfix(&unset, invalid) == GUEST_EINVAL); + } + Require(Throws([&] { pthread_rwlockattr_setpshared_nid_postfix(nullptr, PROCESS_PRIVATE); })); + Require(Throws([&] { pthread_rwlockattr_setpshared_nid_postfix(&unset, PROCESS_PRIVATE); })); + Require(Throws([&] { pthread_rwlockattr_getpshared_nid_postfix(nullptr, &pshared); })); + Require(Throws([&] { pthread_rwlockattr_getpshared_nid_postfix(&unset, &pshared); })); + Require(pthread_rwlockattr_destroy_nid_postfix(&attr) == 0); +} + int main() { + CheckAttrProcessSharing(); CheckWaitingWriterHoldsBackNewReaders(); CheckConcurrentFirstWrlockExcludes(); CheckDeadlinesBeyondTheClockRange(); diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 185914b882..3e1591c87b 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -195,6 +195,7 @@ - [select](../../../core/libs/prx/libkernel/Socket/src/Socket.cpp) (libkernel) validates `tv_usec` but saturates a valid timeout beyond the remaining `steady_clock` range at `time_point::max()`, so the wait can continue effectively indefinitely. The console's behavior for a valid timeout outside the host clock's range is unknown. - [_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 - [pthread_spin_lock](../../../core/libs/prx/libkernel/Pthread/Posix/Spinlock.cpp) (libkernel) - a lock taken again by the thread that holds it throws here. On a PS5 (firmware 12.70) that call had not returned after 2 s, as FreeBSD 11.4 libthr [`thr_pspinlock.c`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_pspinlock.c) spins forever +- [pthread_rwlockattr_getpshared, pthread_rwlockattr_setpshared](../../../core/libs/prx/libkernel/Pthread/Posix/Rwlock.cpp) (libkernel) - `pthread_rwlockattr_destroy` clears the attribute here, so a later get, or a set of 0, throws. On a PS5 (firmware 12.70) destroy leaves the attribute set; a get afterwards returned 0 with a stale value and a set of 0 returned 0 - [sceKernelGetEventError](../../../core/libs/prx/libkernel/Equeue/src/EventAccessors.cpp) (libkernel) returns the event's `data` as an `int` when `EV_ERROR` (0x4000) is set in `flags` and 0 otherwise, as [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/947627d10d4ef4b68ba3c0189aab6c368e70a8d2/src/kernel/eventQueue.cpp#L663) does and as FreeBSD 11's [kevent](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/kern_event.c#L1005) reports a failed change (`flags = EV_ERROR`, errno in `data`). [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd43e6fddd1ab033e7980cd356d14c964905/kernel/ps4_queue.pas#L643) instead returns `fflags` plus `0x80020000` when `fflags` is nonzero. Whether the console reads `data` or `fflags` and returns the raw errno or an SCE error code is unverified. No event delivered by the equeue here carries `EV_ERROR`, so the call returns 0 for every event `sceKernelWaitEqueue` reports - [pthread_cond_reltimedwait_np](../../../core/libs/prx/libkernel/Pthread/Posix/Cond.cpp) (libkernel) takes `(cond, mutex, usec)`, a relative timeout in microseconds, and is `scePthreadCondTimedwait` with POSIX error numbers. FreeBSD 11.4 has no such function; the signature comes only from shadPS4, which registers one function for this NID and for `scePthreadCondTimedwait` ([`condvar.cpp`](https://github.com/shadps4-emu/shadPS4/blob/3a9f57265d28975658f1ca941daa5c879c7a74bd/src/core/libraries/kernel/threads/condvar.cpp#L229-L234)). shadPS4 reads a 64-bit count; here it is the 32-bit `SceKernelUseconds` of the SCE twin, as for `sem_reltimedwait_np`. Unverified on the console - [sysctl kern.arandom](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) serves FreeBSD 11's MIB {1, 37} (CTL_KERN, KERN_ARANDOM) and the "kern.arandom" sysctlbyname lookup through std::random_device, returning min(*oldLength, 256) random bytes. Following FreeBSD's sysctl_kern_arnd, a null buffer reports min(*oldLength, 256) without writing, a null length returns 0 without writing, and requests with a new value return EPERM (1) as the node is read-only. Cryptographic quality depends on the host's random device implementation. From c00c58ae346b0a2389fd93523b8b9c09cd5a4486 Mon Sep 17 00:00:00 2001 From: ionut27001 <194206737+ionut27001@users.noreply.github.com> Date: Sun, 18 Oct 2026 00:50:09 +0300 Subject: [PATCH 0876/1000] fix(libkernel): reject PTHREAD_PROCESS_SHARED in pthread_condattr_setpshared and add pthread_condattr_getpshared On a PS5 (firmware 12.70), of the values 0, 1, 2 and -1, pthread_condattr_setpshared accepts only 0 and leaves the attribute unchanged otherwise. pthread_condattr_getpshared reports 0 and returns EINVAL for a null, uninitialized or destroyed attribute, also with a null output. With an initialized attribute a null output crashes the console; here it throws. Co-Authored-By: Claude Opus 5.5 --- .../libs/prx/libkernel/Pthread/Posix/Cond.cpp | 10 +++++-- core/libs/tests/GuestPthreadCondattr.cpp | 28 ++++++++++++++++--- docs/dev/debt/libkernel.md | 1 - 3 files changed, 32 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libkernel/Pthread/Posix/Cond.cpp b/core/libs/prx/libkernel/Pthread/Posix/Cond.cpp index f939a865e6..6dd1e09509 100644 --- a/core/libs/prx/libkernel/Pthread/Posix/Cond.cpp +++ b/core/libs/prx/libkernel/Pthread/Posix/Cond.cpp @@ -7,7 +7,6 @@ namespace { constexpr int POSIX_PROCESS_PRIVATE = 0; -constexpr int POSIX_PROCESS_SHARED = 1; int toPosix(int result) { if (result == 0) @@ -69,9 +68,16 @@ int APS5_VABI pthread_condattr_setclock_nid_postfix(PthreadCondattr* attr, Kerne int APS5_VABI pthread_condattr_setpshared_nid_postfix(PthreadCondattr* attr, int pshared) { if (!attr || !*attr) return PosixThread::GUEST_EINVAL; - if (pshared != POSIX_PROCESS_PRIVATE && pshared != POSIX_PROCESS_SHARED) return PosixThread::GUEST_EINVAL; + if (pshared != POSIX_PROCESS_PRIVATE) return PosixThread::GUEST_EINVAL; (*attr)->_pshared = pshared; return 0; } +int APS5_VABI pthread_condattr_getpshared_nid_postfix(const PthreadCondattr* attr, int* pshared) { + if (!attr || !*attr) return PosixThread::GUEST_EINVAL; + if (!pshared) throw std::runtime_error("pthread_condattr_getpshared: null pshared"); + *pshared = (*attr)->_pshared; + return 0; +} + } diff --git a/core/libs/tests/GuestPthreadCondattr.cpp b/core/libs/tests/GuestPthreadCondattr.cpp index 64cfef4b62..237ac4374e 100644 --- a/core/libs/tests/GuestPthreadCondattr.cpp +++ b/core/libs/tests/GuestPthreadCondattr.cpp @@ -1,6 +1,7 @@ #include "SceTypes.hpp" #include #include +#include extern "C" { int APS5_VABI scePthreadCondattrInit(PthreadCondattr* attr); @@ -8,6 +9,7 @@ int APS5_VABI scePthreadCondattrDestroy(PthreadCondattr* attr); int APS5_VABI scePthreadCondattrSetclock(PthreadCondattr* attr, KernelClockid clockId); int APS5_VABI pthread_condattr_setclock_nid_postfix(PthreadCondattr* attr, KernelClockid clockId); int APS5_VABI pthread_condattr_setpshared_nid_postfix(PthreadCondattr* attr, int pshared); +int APS5_VABI pthread_condattr_getpshared_nid_postfix(const PthreadCondattr* attr, int* pshared); } static constexpr int SCE_OK = 0; @@ -19,6 +21,7 @@ static void Require(bool value) { if (!value) std::abort(); } int main() { PthreadCondattr unset = nullptr; + int sharing = -1; Require(scePthreadCondattrSetclock(nullptr, Monotonic) == SceEinval); Require(scePthreadCondattrSetclock(&unset, Monotonic) == SceEinval); Require(scePthreadCondattrDestroy(nullptr) == SceEinval); @@ -26,6 +29,8 @@ int main() { Require(pthread_condattr_setclock_nid_postfix(&unset, Monotonic) == GuestEinval); Require(pthread_condattr_setpshared_nid_postfix(nullptr, 0) == GuestEinval); Require(pthread_condattr_setpshared_nid_postfix(&unset, 0) == GuestEinval); + Require(pthread_condattr_getpshared_nid_postfix(nullptr, &sharing) == GuestEinval && sharing == -1); + Require(pthread_condattr_getpshared_nid_postfix(&unset, &sharing) == GuestEinval && sharing == -1); PthreadCondattr attr = nullptr; Require(scePthreadCondattrInit(&attr) == SCE_OK); @@ -33,10 +38,25 @@ int main() { Require(scePthreadCondattrSetclock(&attr, clock) == SCE_OK); Require(pthread_condattr_setclock_nid_postfix(&attr, clock) == 0); } - for (const int sharing : {0, 1}) - Require(pthread_condattr_setpshared_nid_postfix(&attr, sharing) == 0); - for (const int sharing : {-1, 2}) - Require(pthread_condattr_setpshared_nid_postfix(&attr, sharing) == GuestEinval); + Require(pthread_condattr_getpshared_nid_postfix(&attr, &sharing) == 0 && sharing == 0); + for (const int invalid : {1, 2, -1}) { + Require(pthread_condattr_setpshared_nid_postfix(&attr, invalid) == GuestEinval); + sharing = -1; + Require(pthread_condattr_getpshared_nid_postfix(&attr, &sharing) == 0 && sharing == 0); + } + Require(pthread_condattr_setpshared_nid_postfix(&attr, 0) == 0); + sharing = -1; + Require(pthread_condattr_getpshared_nid_postfix(&attr, &sharing) == 0 && sharing == 0); + bool threw = false; + try { + pthread_condattr_getpshared_nid_postfix(&attr, nullptr); + } catch (const std::runtime_error&) { + threw = true; + } + Require(threw); Require(scePthreadCondattrDestroy(&attr) == SCE_OK && attr == nullptr); Require(scePthreadCondattrDestroy(&attr) == SceEinval); + sharing = -1; + Require(pthread_condattr_getpshared_nid_postfix(&attr, &sharing) == GuestEinval && sharing == -1); + Require(pthread_condattr_setpshared_nid_postfix(&attr, 0) == GuestEinval); } diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 3e1591c87b..c7eb3d3028 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -122,7 +122,6 @@ - [sceKernelWriteThrottlingStatus](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) reports no write throttling: the host SSD has no write budget, so the first qword of the 32-byte status is set to `UINT32_MAX` and the rest is zeroed; returns `EFAULT` (`0x8002000e`) when passed a null status buffer pointer. PPSA17221 reads only that qword: it multiplies it by 64 KiB and compares the product with byte thresholds up to 0x4AF00000 to pick one of five tiers. When the call fails the title assumes 0x4B000001 bytes, which selects the same top tier as `UINT32_MAX`. What the value measures on hardware (a remaining budget or a rate) and the meaning of the other three qwords are unknown - [Windows directory descriptors](../../../core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp) (libkernel) take one snapshot of the directory, on the first `getdents`, `getdirentries` or non-zero `lseek` (`SEEK_SET` or `SEEK_CUR`), and keep it until it is discarded; `lseek` to offset 0 only discards the snapshot, and the next read lists the directory again, so every rewind costs O(entries) on the next read. `lseek` takes the offset as an entry index and `SEEK_CUR` with offset 0 only reports the index. `SEEK_END` throws, where the NUL descriptor used to return 0, because FreeBSD defines it relative to the directory size, which is not modelled. A negative result returns `EINVAL` and a `SEEK_CUR` offset that overflows returns `EOVERFLOW`, as FreeBSD `vn_seek` reports them. Names whose UTF-8 encoding exceeds the one-byte `d_namlen` limit are not representable in this guest directory-record layout: `getdents` returns `ENAMETOOLONG` without consuming the entry. Repeated calls therefore report the same error and entries after that name cannot be reached through this descriptor; FreeBSD's `getdents` behavior for host filenames that exceed its `MAXNAMLEN` is not verified. `getdirentries` still reports 0 through `basep` instead of the position of the block it read, so only 0 and values returned by `lseek` are valid offsets; FreeBSD offsets are filesystem cookies - [/dev/random and /dev/urandom](../../../core/libs/prx/libkernel/File/src/RandomDevice.cpp) (libkernel) opened for reading return a descriptor backed by the host null device whose reads are filled with host OS entropy (`getentropy`, `rand_s` on Windows); both paths behave the same, never block, and positioned reads ignore the offset (FreeBSD `random(4)`). Only the exact paths are recognised, `fstat`, `lseek` and `mmap` see the null device, and opening them for writing throws -- [pthread_condattr_setpshared](../../../core/libs/prx/libkernel/Pthread/Posix/Cond.cpp) (libkernel) stores a private or shared attribute, following FreeBSD 11 libthr [thr_condattr.c](https://github.com/freebsd/freebsd-src/blob/stable/11/lib/libthr/thread/thr_condattr.c): a null or uninitialized attribute and values other than `PTHREAD_PROCESS_PRIVATE` (0) and `PTHREAD_PROCESS_SHARED` (1) return `EINVAL` without changing it. Guest threads execute in one host process; sharing condition variables across processes is not modeled. PSFlyCast (PPSA99247) imports it and fails to load without it. Console behavior is unverified. - [pthread_attr_getinheritsched, pthread_attr_setinheritsched, pthread_attr_getscope, pthread_attr_setscope, pthread_attr_getstackaddr](../../../core/libs/prx/libkernel/Pthread/Posix/Attr.cpp) (libkernel) follow FreeBSD 11 libthr ([thr_attr.c](https://github.com/freebsd/freebsd-src/blob/5ed7eb0d97ba4436218e810f61bd059acba984c2/lib/libthr/thread/thr_attr.c), [thr_create.c](https://github.com/freebsd/freebsd-src/blob/5ed7eb0d97ba4436218e810f61bd059acba984c2/lib/libthr/thread/thr_create.c)), not measured on the console: the scope defaults to `PTHREAD_SCOPE_SYSTEM` (2), only it and `PTHREAD_SCOPE_PROCESS` (0) are accepted and other values return `EINVAL`, and an inherit value other than `PTHREAD_INHERIT_SCHED` (4) or `PTHREAD_EXPLICIT_SCHED` (0) returns `ENOTSUP`. `pthread_attr_getstackaddr` returns the configured stack address pointer (null by default). A null output of `pthread_attr_getinheritsched` throws, since libthr writes through it unchecked. A thread keeps the scope it was created with, taken from the creating thread under `PTHREAD_INHERIT_SCHED`, and `pthread_attr_get_np` and `scePthreadAttrGet` report it, but every guest thread is a host thread at system scope, so process scope changes nothing - [Shared memory](../../../core/libs/prx/libkernel/File/src/SharedMemory.cpp) (libkernel) on Linux implements anonymous and named objects, bounded descriptor writes and seeks, and `shm_unlink` with effective read/write permission checks from FreeBSD 11. Named paths up to 1023 bytes use `/anyps5-shm-` plus SHA-1; collisions are not detected, and the namespace is shared across processes in the host mount namespace. Named creation applies the guest umask without changing the host mask. Linux procfs and `/dev/shm` are required. Operations use an advisory directory lock; external host operations bypass it, unrelated objects serialize, and descriptor operations require two spare host descriptors and compare device/inode with the initial descriptor identity on both the pinned-object and ordinary-file fallback paths. [Guest mappings](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) support shared and private mappings of these descriptors through the allocation tracker and pin a duplicate descriptor during validation and mapping; mapping requires one spare host descriptor; file-backed mappings use the write-watch scan fallback because aliases and descriptor writes bypass per-mapping tracking. Named objects and file-backed mappings on Windows remain unsupported. Invalid non-null name pointers, console behaviour and title compatibility remain unverified. - [sceKernelAddReadEvent and sceKernelAddWriteEvent](../../../core/libs/prx/libkernel/Equeue/Equeue.cpp) (libkernel): a write event reports the free space of the socket's send buffer in `data`, as FreeBSD 11.4's [`filt_sowrite`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c#L3358-L3382) reports `sbspace`: on Linux the host's `SO_SNDBUF` less the bytes `SIOCOUTQ` counts as queued, which include the host's per-packet overhead, so the space shrinks by somewhat more than the bytes sent. Windows has no query for that space and macOS is not wired up, so both report the whole `SO_SNDBUF`, which does not shrink as data is queued. Only guest sockets are supported; other descriptors and a low watermark above 1 throw From 7c4bc9a0bac2d8a58ee7bd8d3889aba36828993b Mon Sep 17 00:00:00 2001 From: milescoviello Date: Sat, 17 Oct 2026 21:54:08 +0000 Subject: [PATCH 0877/1000] test(agc): draw exact UNORM8 codes into the swapped multisampled target The case drew 0.5 green, which lies halfway between two UNORM8 codes, so a device that rounds 127.5 up (llvmpipe, LLVM 20.1.2) failed the half-code tolerance with red and blue correctly swapped. It now draws 64, 128 and 32 of 255 from its own pixel program and names the loaded and expected codes when a sample differs. Co-Authored-By: Claude Opus 5.5 --- .../execution/SampleMultisampleTarget.cpp | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp index 3aca0313d7..262dc028c6 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp @@ -78,6 +78,10 @@ alignas(256) constexpr std::array VertexCode{ alignas(256) constexpr std::array PixelCode{ 0x7e0802ff, 0x3e800000, 0x7e0a02ff, 0x3f000000, 0x7e0c02ff, 0x3e000000, 0x7e0e02ff, 0x3f800000, 0xf800180f, 0x07060504, 0xbf810000, }; +constexpr std::array DrawnUnorm8{64.0f / 255.0f, 128.0f / 255.0f, 32.0f / 255.0f, 1.0f}; +alignas(256) constexpr std::array Unorm8PixelCode{ + 0x7e0802ff, std::bit_cast(DrawnUnorm8[0]), 0x7e0a02ff, std::bit_cast(DrawnUnorm8[1]), 0x7e0c02ff, std::bit_cast(DrawnUnorm8[2]), 0x7e0e02ff, std::bit_cast(DrawnUnorm8[3]), 0xf800180f, 0x07060504, 0xbf810000, +}; alignas(256) constexpr std::array LoadCode{ 0x34020087, 0xe0301000, 0x80000201, 0xe0301004, 0x80000301, 0xe0301008, 0x80000401, 0xe030100c, @@ -163,11 +167,19 @@ AgcDriver::Graphics::ColorTarget DecodeSwappedTarget(const Block& block) { } void CheckSwappedSamples(const std::vector>& loaded) { - const std::array swapped{Drawn[2], Drawn[1], Drawn[0], Drawn[3]}; + const std::array swapped{DrawnUnorm8[2], DrawnUnorm8[1], DrawnUnorm8[0], DrawnUnorm8[3]}; + const auto codes = [](const std::array& texel) { + std::string text; + for (const auto value : texel) text += (text.empty() ? "" : ", ") + std::to_string(value * 255.0f); + return "(" + text + ")"; + }; std::size_t drawn = 0; for (const auto& texel : loaded) { if (texel == std::array{}) continue; - for (std::uint32_t channel = 0; channel < 4u; ++channel) Require(std::abs(texel[channel] - swapped[channel]) <= 0.5f / 255.0f, "a sample of a BGRA8 target loaded through an RGBA8 texture does not hold the drawn color with red and blue swapped"); + for (std::uint32_t channel = 0; channel < 4u; ++channel) { + if (std::abs(texel[channel] - swapped[channel]) <= 0.5f / 255.0f) continue; + Require(false, "a sample of a BGRA8 target loaded through an RGBA8 texture reads " + codes(texel) + " in UNORM8 codes instead of the drawn color with red and blue swapped " + codes(swapped)); + } ++drawn; } Require(drawn != 0, "no sample of the BGRA8 target holds the drawn color"); @@ -195,7 +207,7 @@ AgcDriver::Graphics::State TargetState(const AgcDriver::Graphics::ColorTarget& c return state; } -void Draw(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::State& state, std::uint64_t indexAddress = 0, std::uint32_t indexSize = 2u) { +void Draw(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::State& state, std::uint64_t indexAddress = 0, std::uint32_t indexSize = 2u, std::span pixelCode = PixelCode) { const auto target = device.Target(); constexpr std::uint32_t waveSize = 64; std::vector vertexUserData(4, 0u); @@ -212,8 +224,7 @@ void Draw(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::State& sta vertex.useCache = false; const auto vertexResult = ShaderRecompiler::Recompile(vertex); const auto vertexPush = static_cast(vertexResult.pushConstants.size()); - const auto codeAddress = reinterpret_cast(PixelCode.data()); - const std::span pixelCode(PixelCode); + const auto codeAddress = reinterpret_cast(pixelCode.data()); const AgcDriver::Registers context{{0x1b6, 0x8000}, {0x1b3, 0}, {0x1b4, 0}, {0x203, 0}, {0x1c5, Float32Export}}; const std::array pixelMemory{{{codeAddress, std::as_bytes(pixelCode)}}}; ShaderRecompiler::RecompileRequest fragment{ @@ -645,7 +656,7 @@ int main(int argc, char** argv) { std::puts("stores to one sample of each pixel of a drawn 4x target and to samples of zeroed color memory no draw rendered to reach exactly those samples, and a store to non-uniform color memory no draw rendered to is refused"); std::puts("every sample of a 4x color target loaded through its multisampled texture descriptor matches the triangle drawn into it, then the fill of its color memory; a surface no draw rendered to loads the value its color memory repeats and keeps it under a later draw"); Block swapped(4u * BlockBytes); - Draw(*device, TargetState(DecodeSwappedTarget(swapped))); + Draw(*device, TargetState(DecodeSwappedTarget(swapped)), 0, 2u, Unorm8PixelCode); CheckSwappedSamples(LoadSamples(*device, swapped, Format8888Unorm)); std::puts("a 4x BGRA8 color target loaded through an RGBA8 texture descriptor reads its drawn color with red and blue swapped"); Block destination(BlockBytes); From aed1ffbae85041ec22e1e27b93b4bc1d69c80522 Mon Sep 17 00:00:00 2001 From: oskarlgs Date: Sat, 17 Oct 2026 21:55:21 +0000 Subject: [PATCH 0878/1000] fix(relinker): keep the Windows TLS rewrite from moving truncated branches A short TLS access followed by a near branch with an operand-size prefix decided the branch was relative from its flow kind and opcode alone, then re-encoded it to the fall-through address or wrote its rel32 over the prefix and opcode. Such a branch now counts as one that cannot move. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_016TFMtUC4L4wMS4eeEqaNSR --- core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp | 4 ++-- core/relinker/relinker/tests/test_tls_function_coverage.py | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp b/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp index e50f17c0ce..19ff13e378 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp @@ -158,8 +158,8 @@ PeDirectory WindowsTlsBuilder::Build(const std::vector& source, co throw Domain::RelinkerException("Short guest TLS instruction ends its segment", header.Offset + offset); const auto next = decoder.DecodeInstruction(bytes + length, header.FileSize - offset - length); const bool relativeBranch = - (next.FlowKind == Codegen::ControlFlowKind::ConditionalBranch && (next.IsTwoByteOpcode ? next.Opcode >= 0x80 && next.Opcode <= 0x8f : next.Opcode >= 0x70 && next.Opcode <= 0x7f)) || - (next.FlowKind == Codegen::ControlFlowKind::UnconditionalJump && !next.HasRipRelativeDisp && !next.IsTwoByteOpcode && (next.Opcode == 0xe9 || next.Opcode == 0xeb)); + (next.FlowKind == Codegen::ControlFlowKind::ConditionalBranch && !next.TruncatesBranchTarget && (next.IsTwoByteOpcode ? next.Opcode >= 0x80 && next.Opcode <= 0x8f : next.Opcode >= 0x70 && next.Opcode <= 0x7f)) || + (next.FlowKind == Codegen::ControlFlowKind::UnconditionalJump && !next.TruncatesBranchTarget && !next.HasRipRelativeDisp && !next.IsTwoByteOpcode && (next.Opcode == 0xe9 || next.Opcode == 0xeb)); if (!relativeBranch && (next.FlowKind != Codegen::ControlFlowKind::Sequential || next.HasBranchTarget || next.SegmentPrefix != 0)) throw Domain::RelinkerException("Short guest TLS instruction is followed by an instruction that cannot move", header.Offset + offset + length); std::vector movedBytes(bytes + length, bytes + length + next.Length); diff --git a/core/relinker/relinker/tests/test_tls_function_coverage.py b/core/relinker/relinker/tests/test_tls_function_coverage.py index 6c30aada25..ef20c1c1a1 100644 --- a/core/relinker/relinker/tests/test_tls_function_coverage.py +++ b/core/relinker/relinker/tests/test_tls_function_coverage.py @@ -747,6 +747,10 @@ def main(): "short-before-tls": bytes.fromhex("64 48 8b 03 64 48 8b 03 c3"), "short-alu-before-return": bytes.fromhex("64 48 33 03 c3"), "short-before-loop": bytes.fromhex("64 48 33 03 e2 00"), + "short-before-word-jcc8": bytes.fromhex("64 48 33 03 66 75 00 c3"), + "short-before-word-jmp8": bytes.fromhex("64 48 33 03 66 eb 00 c3"), + "short-before-word-jmp16": bytes.fromhex("64 48 33 03 66 e9 00 00 c3"), + "short-before-word-jcc16": bytes.fromhex("64 48 33 03 66 0f 85 00 00 c3"), }.items(): convert(name, make_image("register", "unwind", body=instruction), "Short guest TLS instruction is followed by an instruction that cannot move", error_offset=0x1244) From f4b3f035803c62f53380c21ab0f3e531f11b1636 Mon Sep 17 00:00:00 2001 From: David Date: Sun, 18 Oct 2026 02:16:42 +0400 Subject: [PATCH 0879/1000] refactor(shader): gather the emulated barycentric interface in one place --- .../src/BarycentricGeometryShader.cpp | 58 ++++++++++++------- 1 file changed, 37 insertions(+), 21 deletions(-) diff --git a/core/shader/recompiler/SpirvBackend/src/BarycentricGeometryShader.cpp b/core/shader/recompiler/SpirvBackend/src/BarycentricGeometryShader.cpp index 7c86bc826b..1ab923070f 100644 --- a/core/shader/recompiler/SpirvBackend/src/BarycentricGeometryShader.cpp +++ b/core/shader/recompiler/SpirvBackend/src/BarycentricGeometryShader.cpp @@ -24,6 +24,36 @@ struct PerVertexParameter { std::uint32_t location; }; +struct EmulatedInterface { + std::vector passed; + std::vector perVertex; + BarycentricEmulationLayout layout; + std::uint32_t barycentrics = 0; + std::uint32_t sources = 0; +}; + +EmulatedInterface CollectEmulatedInterface(const RecompileResult& vertex, const RecompileResult& fragment) { + require(fragment.barycentricEmulation.active, "the fragment shader does not emulate barycentrics"); + const auto exported = [&](std::uint32_t location) { return std::find(vertex.parameterExports.begin(), vertex.parameterExports.end(), location) != vertex.parameterExports.end(); }; + EmulatedInterface emulated; + emulated.layout = LayoutBarycentricEmulation(fragment.fragmentParameters, fragment.barycentricEmulation); + for (const auto& parameter : fragment.fragmentParameters) { + require(exported(parameter.sourceLocation), "fragment parameter " + std::to_string(parameter.location) + " is not exported by the vertex shader"); + if (!parameter.perVertex) { + emulated.passed.push_back(parameter.sourceLocation); + continue; + } + const auto relocated = std::find_if(emulated.layout.perVertexLocations.begin(), emulated.layout.perVertexLocations.end(), [&](const auto& entry) { return entry.first == parameter.location; }); + require(relocated != emulated.layout.perVertexLocations.end(), "per-vertex parameter " + std::to_string(parameter.location) + " has no emulated location"); + emulated.perVertex.push_back({parameter.sourceLocation, relocated->second}); + } + emulated.barycentrics = (emulated.layout.smoothLocation != BarycentricEmulationLayout::NoLocation ? 1u : 0u) + (emulated.layout.linearLocation != BarycentricEmulationLayout::NoLocation ? 1u : 0u); + std::set sources(emulated.passed.begin(), emulated.passed.end()); + for (const auto& parameter : emulated.perVertex) sources.insert(parameter.sourceLocation); + emulated.sources = static_cast(sources.size()); + return emulated; +} + class BarycentricGeometryEmitter { public: explicit BarycentricGeometryEmitter(std::uint32_t version) : builder(version) { @@ -171,30 +201,16 @@ RecompileResult BuildBarycentricGeometryShader(const RecompileResult& vertex, co require(limits.has_value(), "geometry shaders are unavailable"); require(target.spirvVersion >= 0x00010300u && target.spirvVersion <= 0x00010500u, "unsupported SPIR-V target version"); require(std::find(target.supportedCapabilities.begin(), target.supportedCapabilities.end(), spv::CapabilityGeometry) != target.supportedCapabilities.end(), "geometry capability is unavailable"); - const auto exported = [&](std::uint32_t location) { return std::find(vertex.parameterExports.begin(), vertex.parameterExports.end(), location) != vertex.parameterExports.end(); }; - const auto layout = LayoutBarycentricEmulation(fragment.fragmentParameters, fragment.barycentricEmulation); - std::vector passed; - std::vector perVertex; - for (const auto& parameter : fragment.fragmentParameters) { - require(exported(parameter.sourceLocation), "fragment parameter " + std::to_string(parameter.location) + " is not exported by the vertex shader"); - if (!parameter.perVertex) { - passed.push_back(parameter.sourceLocation); - continue; - } - const auto relocated = std::find_if(layout.perVertexLocations.begin(), layout.perVertexLocations.end(), [&](const auto& entry) { return entry.first == parameter.location; }); - require(relocated != layout.perVertexLocations.end(), "per-vertex parameter " + std::to_string(parameter.location) + " has no emulated location"); - perVertex.push_back({parameter.sourceLocation, relocated->second}); - } - const auto barycentrics = (layout.smoothLocation != BarycentricEmulationLayout::NoLocation ? 1u : 0u) + (layout.linearLocation != BarycentricEmulationLayout::NoLocation ? 1u : 0u); - const auto outputComponents = 4u + 4u * static_cast(passed.size()) + 3u * barycentrics + 12u * static_cast(perVertex.size()); - std::set sources(passed.begin(), passed.end()); - for (const auto& parameter : perVertex) sources.insert(parameter.sourceLocation); - const auto inputComponents = 4u + 4u * static_cast(sources.size()); - const auto fragmentComponents = 4u * (static_cast(passed.size()) + barycentrics + 3u * static_cast(perVertex.size())); + const auto emulated = CollectEmulatedInterface(vertex, fragment); + const auto passed = static_cast(emulated.passed.size()); + const auto perVertex = static_cast(emulated.perVertex.size()); + const auto outputComponents = 4u + 4u * passed + 3u * emulated.barycentrics + 12u * perVertex; + const auto inputComponents = 4u + 4u * emulated.sources; + const auto fragmentComponents = 4u * (passed + emulated.barycentrics + 3u * perVertex); require(limits->maxGeometryOutputVertices >= 3u && outputComponents <= limits->maxGeometryOutputComponents && 3u * outputComponents <= limits->maxGeometryTotalOutputComponents && inputComponents <= limits->maxGeometryInputComponents && fragmentComponents <= limits->maxFragmentInputComponents, "the emulated barycentric interface exceeds device limits"); BarycentricGeometryEmitter emitter(target.spirvVersion); RecompileResult result; - result.spirv = emitter.Emit(passed, perVertex, layout); + result.spirv = emitter.Emit(emulated.passed, emulated.perVertex, emulated.layout); #if ANYPS5_ENABLE_SPIRV_TOOLS result.spirv = ValidateAndOptimizeSpirv(result.spirv, target.vulkanVersion, target.spirvVersion); #endif From 02a4bdf08ada428b2f210d605ddb942c1068168c Mon Sep 17 00:00:00 2001 From: David Date: Sun, 18 Oct 2026 02:16:51 +0400 Subject: [PATCH 0880/1000] feat(shader): build the barycentric emulation stages from tessellation On a device without geometry shaders, each triangle becomes a three-point patch at tessellation level 1. The evaluation stage takes the corner gl_TessCoord points at as its vertex and writes it as the weights, and passes every vertex's parameters for raw reads. The vertex clip distances are forwarded, so MoltenVK's stage interface matches the vertex output. --- core/shader/recompiler/CMakeLists.txt | 1 + core/shader/recompiler/Recompiler.hpp | 7 + .../src/BarycentricGeometryShader.cpp | 220 ++++++++++++++++++ 3 files changed, 228 insertions(+) diff --git a/core/shader/recompiler/CMakeLists.txt b/core/shader/recompiler/CMakeLists.txt index a14e40ce48..b196d53f5b 100644 --- a/core/shader/recompiler/CMakeLists.txt +++ b/core/shader/recompiler/CMakeLists.txt @@ -221,6 +221,7 @@ function(add_shader_recompiler target runtimeTarget) endif() set(shaderCacheReviewedNames BuildBarycentricGeometryShader + BuildBarycentricTessellationShaders BuildPreparedShaderKey BuildRectListShaders CaptureResources diff --git a/core/shader/recompiler/Recompiler.hpp b/core/shader/recompiler/Recompiler.hpp index 123c488f40..39fc70798c 100644 --- a/core/shader/recompiler/Recompiler.hpp +++ b/core/shader/recompiler/Recompiler.hpp @@ -697,6 +697,13 @@ struct GeometryStageLimits { [[nodiscard]] RecompileResult BuildBarycentricGeometryShader(const RecompileResult& vertex, const RecompileResult& fragment, const SpirvTarget& target, const std::optional& limits); +struct BarycentricTessellationShaders { + RecompileResult control; + RecompileResult evaluation; +}; + +[[nodiscard]] BarycentricTessellationShaders BuildBarycentricTessellationShaders(const RecompileResult& vertex, const RecompileResult& fragment, const SpirvTarget& target, std::uint32_t maxFragmentInputComponents); + } #endif diff --git a/core/shader/recompiler/SpirvBackend/src/BarycentricGeometryShader.cpp b/core/shader/recompiler/SpirvBackend/src/BarycentricGeometryShader.cpp index 1ab923070f..8b9b6d5710 100644 --- a/core/shader/recompiler/SpirvBackend/src/BarycentricGeometryShader.cpp +++ b/core/shader/recompiler/SpirvBackend/src/BarycentricGeometryShader.cpp @@ -1,4 +1,5 @@ #include "Recompiler.hpp" +#include "SpirvBackend/SpirvClipDistances.hpp" #include "SpirvBackend/SpirvModule.hpp" #if ANYPS5_ENABLE_SPIRV_TOOLS #include "SpirvBackend/SpirvOptimizer.hpp" @@ -26,6 +27,7 @@ struct PerVertexParameter { struct EmulatedInterface { std::vector passed; + std::vector passedFlat; std::vector perVertex; BarycentricEmulationLayout layout; std::uint32_t barycentrics = 0; @@ -41,6 +43,7 @@ EmulatedInterface CollectEmulatedInterface(const RecompileResult& vertex, const require(exported(parameter.sourceLocation), "fragment parameter " + std::to_string(parameter.location) + " is not exported by the vertex shader"); if (!parameter.perVertex) { emulated.passed.push_back(parameter.sourceLocation); + emulated.passedFlat.push_back(parameter.flat); continue; } const auto relocated = std::find_if(emulated.layout.perVertexLocations.begin(), emulated.layout.perVertexLocations.end(), [&](const auto& entry) { return entry.first == parameter.location; }); @@ -170,6 +173,195 @@ private: } }; +class BarycentricTessellationEmitter { +public: + BarycentricTessellationEmitter(std::uint32_t version, std::uint32_t clipDistances) : builder(version), clipDistances(clipDistances) { + builder.AddMemoryModel(spv::AddressingModelLogical, spv::MemoryModelGLSL450); + voidType = builder.Type(spv::OpTypeVoid); + boolType = builder.Type(spv::OpTypeBool); + uintType = builder.Type(spv::OpTypeInt, 32u, 0u); + intType = builder.Type(spv::OpTypeInt, 32u, 1u); + floatType = builder.Type(spv::OpTypeFloat, 32u); + vec3FloatType = builder.Type(spv::OpTypeVector, floatType, 3u); + vec4FloatType = builder.Type(spv::OpTypeVector, floatType, 4u); + vertexArrayType = builder.Type(spv::OpTypeArray, vec4FloatType, constant(uintType, 3u)); + functionType = builder.Type(spv::OpTypeFunction, voidType); + if (clipDistances != 0u) { + clipArrayType = builder.Type(spv::OpTypeArray, floatType, constant(uintType, clipDistances)); + perVertexType = builder.DecoratedType(spv::OpTypeStruct, {{spv::OpMemberDecorate, {0u, spv::DecorationBuiltIn, spv::BuiltInPosition}}, {spv::OpMemberDecorate, {1u, spv::DecorationBuiltIn, spv::BuiltInClipDistance}}, {spv::OpDecorate, {spv::DecorationBlock}}}, vec4FloatType, clipArrayType); + ptrInputClip = builder.Type(spv::OpTypePointer, spv::StorageClassInput, clipArrayType); + ptrOutputClip = builder.Type(spv::OpTypePointer, spv::StorageClassOutput, clipArrayType); + } else { + perVertexType = builder.DecoratedType(spv::OpTypeStruct, {{spv::OpMemberDecorate, {0u, spv::DecorationBuiltIn, spv::BuiltInPosition}}, {spv::OpDecorate, {spv::DecorationBlock}}}, vec4FloatType); + } + perVertexArrayType = builder.Type(spv::OpTypeArray, perVertexType, constant(uintType, 3u)); + ptrInputVec4 = builder.Type(spv::OpTypePointer, spv::StorageClassInput, vec4FloatType); + ptrOutputVec4 = builder.Type(spv::OpTypePointer, spv::StorageClassOutput, vec4FloatType); + } + + std::vector EmitControl(const std::vector& sources) { + begin(); + builder.AddExecutionMode(main, spv::ExecutionModeOutputVertices, 3u); + addDomain(); + const auto invocationId = addInterface(spv::StorageClassInput, intType); + decorate(invocationId, spv::DecorationBuiltIn, spv::BuiltInInvocationId); + const auto glIn = addInterface(spv::StorageClassInput, perVertexArrayType); + const auto glOut = addInterface(spv::StorageClassOutput, perVertexArrayType); + const auto floatArray = [&](std::uint32_t size) { return builder.Type(spv::OpTypeArray, floatType, constant(uintType, size)); }; + const auto outer = addInterface(spv::StorageClassOutput, floatArray(4u)); + decorate(outer, spv::DecorationBuiltIn, spv::BuiltInTessLevelOuter); + builder.AddAnnotation(spv::OpDecorate, outer, spv::DecorationPatch); + const auto inner = addInterface(spv::StorageClassOutput, floatArray(2u)); + decorate(inner, spv::DecorationBuiltIn, spv::BuiltInTessLevelInner); + builder.AddAnnotation(spv::OpDecorate, inner, spv::DecorationPatch); + std::vector> parameters; + for (const auto location : sources) { + const auto input = addInterface(spv::StorageClassInput, vertexArrayType); + decorate(input, spv::DecorationLocation, location); + const auto output = addInterface(spv::StorageClassOutput, vertexArrayType); + decorate(output, spv::DecorationLocation, location); + parameters.emplace_back(input, output); + } + builder.EmitEntryPoint(spv::ExecutionModelTessellationControl, main, "main", interfaces); + builder.AddFunction(spv::OpLabel, builder.AllocateId()); + + const auto invocation = load(intType, invocationId); + const auto first = result(spv::OpIEqual, boolType, invocation, constant(intType, 0u)); + const auto writeLevels = builder.AllocateId(); + const auto afterLevels = builder.AllocateId(); + builder.AddFunction(spv::OpSelectionMerge, afterLevels, spv::SelectionControlMaskNone); + builder.AddFunction(spv::OpBranchConditional, first, writeLevels, afterLevels); + builder.AddFunction(spv::OpLabel, writeLevels); + const auto ptrOutputFloat = builder.Type(spv::OpTypePointer, spv::StorageClassOutput, floatType); + const auto one = constant(floatType, std::bit_cast(1.0f)); + for (std::uint32_t i = 0; i < 4u; ++i) store(result(spv::OpAccessChain, ptrOutputFloat, outer, constant(intType, i)), one); + for (std::uint32_t i = 0; i < 2u; ++i) store(result(spv::OpAccessChain, ptrOutputFloat, inner, constant(intType, i)), one); + builder.AddFunction(spv::OpBranch, afterLevels); + builder.AddFunction(spv::OpLabel, afterLevels); + store(result(spv::OpAccessChain, ptrOutputVec4, glOut, invocation, constant(intType, 0u)), load(vec4FloatType, result(spv::OpAccessChain, ptrInputVec4, glIn, invocation, constant(intType, 0u)))); + if (clipDistances != 0u) store(result(spv::OpAccessChain, ptrOutputClip, glOut, invocation, constant(intType, 1u)), load(clipArrayType, result(spv::OpAccessChain, ptrInputClip, glIn, invocation, constant(intType, 1u)))); + for (const auto& [input, output] : parameters) store(result(spv::OpAccessChain, ptrOutputVec4, output, invocation), load(vec4FloatType, result(spv::OpAccessChain, ptrInputVec4, input, invocation))); + return finish(); + } + + std::vector EmitEvaluation(const std::vector& sources, const std::vector& passed, const std::vector& passedFlat, const std::vector& perVertex, const BarycentricEmulationLayout& layout) { + begin(); + addDomain(); + const auto tessCoord = addInterface(spv::StorageClassInput, vec3FloatType); + decorate(tessCoord, spv::DecorationBuiltIn, spv::BuiltInTessCoord); + const auto glIn = addInterface(spv::StorageClassInput, perVertexArrayType); + std::array inputs {}; + for (const auto location : sources) { + inputs[location] = addInterface(spv::StorageClassInput, vertexArrayType); + decorate(inputs[location], spv::DecorationLocation, location); + } + const auto glOut = addInterface(spv::StorageClassOutput, perVertexType); + std::vector passedOutputs; + for (const auto location : passed) { + passedOutputs.push_back(addInterface(spv::StorageClassOutput, vec4FloatType)); + decorate(passedOutputs.back(), spv::DecorationLocation, location); + } + const auto barycentricOutput = [&](std::uint32_t location) { + if (location == BarycentricEmulationLayout::NoLocation) return 0u; + const auto variable = addInterface(spv::StorageClassOutput, vec3FloatType); + decorate(variable, spv::DecorationLocation, location); + return variable; + }; + const auto smooth = barycentricOutput(layout.smoothLocation); + const auto linear = barycentricOutput(layout.linearLocation); + std::vector perVertexOutputs; + for (const auto& parameter : perVertex) { + perVertexOutputs.push_back(addInterface(spv::StorageClassOutput, vertexArrayType)); + decorate(perVertexOutputs.back(), spv::DecorationLocation, parameter.location); + } + builder.EmitEntryPoint(spv::ExecutionModelTessellationEvaluation, main, "main", interfaces); + builder.AddFunction(spv::OpLabel, builder.AllocateId()); + + const auto ptrInputFloat = builder.Type(spv::OpTypePointer, spv::StorageClassInput, floatType); + const auto half = constant(floatType, std::bit_cast(0.5f)); + const auto zero = constant(floatType, 0u); + const auto one = constant(floatType, std::bit_cast(1.0f)); + std::array corner {}; + for (std::uint32_t i = 0; i < 3u; ++i) corner[i] = result(spv::OpFOrdGreaterThan, boolType, load(floatType, result(spv::OpAccessChain, ptrInputFloat, tessCoord, constant(intType, i))), half); + auto index = result(spv::OpSelect, intType, corner[2], constant(intType, 2u), constant(intType, 0u)); + index = result(spv::OpSelect, intType, corner[1], constant(intType, 1u), index); + const auto weight = [&](std::uint32_t vertex) { return result(spv::OpSelect, floatType, result(spv::OpIEqual, boolType, index, constant(intType, vertex)), one, zero); }; + const auto weights = result(spv::OpCompositeConstruct, vec3FloatType, weight(0u), weight(1u), weight(2u)); + + store(result(spv::OpAccessChain, ptrOutputVec4, glOut, constant(intType, 0u)), load(vec4FloatType, result(spv::OpAccessChain, ptrInputVec4, glIn, index, constant(intType, 0u)))); + if (clipDistances != 0u) store(result(spv::OpAccessChain, ptrOutputClip, glOut, constant(intType, 1u)), load(clipArrayType, result(spv::OpAccessChain, ptrInputClip, glIn, index, constant(intType, 1u)))); + for (std::size_t i = 0; i < passed.size(); ++i) store(passedOutputs[i], load(vec4FloatType, result(spv::OpAccessChain, ptrInputVec4, inputs[passed[i]], passedFlat[i] ? constant(intType, 0u) : index))); + if (smooth != 0u) store(smooth, weights); + if (linear != 0u) store(linear, weights); + for (std::size_t i = 0; i < perVertex.size(); ++i) { + for (std::uint32_t source = 0; source < 3u; ++source) store(result(spv::OpAccessChain, ptrOutputVec4, perVertexOutputs[i], constant(intType, source)), load(vec4FloatType, result(spv::OpAccessChain, ptrInputVec4, inputs[perVertex[i].sourceLocation], constant(intType, source)))); + } + return finish(); + } + +private: + SpirvModule builder; + std::uint32_t clipDistances; + std::vector interfaces; + std::uint32_t main = 0; + std::uint32_t voidType = 0; + std::uint32_t boolType = 0; + std::uint32_t uintType = 0; + std::uint32_t intType = 0; + std::uint32_t floatType = 0; + std::uint32_t vec3FloatType = 0; + std::uint32_t vec4FloatType = 0; + std::uint32_t vertexArrayType = 0; + std::uint32_t functionType = 0; + std::uint32_t perVertexType = 0; + std::uint32_t perVertexArrayType = 0; + std::uint32_t clipArrayType = 0; + std::uint32_t ptrInputVec4 = 0; + std::uint32_t ptrOutputVec4 = 0; + std::uint32_t ptrInputClip = 0; + std::uint32_t ptrOutputClip = 0; + + void begin() { + builder.EmitCapability(spv::CapabilityShader); + builder.EmitCapability(spv::CapabilityTessellation); + if (clipDistances != 0u) builder.EmitCapability(spv::CapabilityClipDistance); + main = result(spv::OpFunction, voidType, spv::FunctionControlMaskNone, functionType); + } + + void addDomain() { + builder.AddExecutionMode(main, spv::ExecutionModeTriangles); + builder.AddExecutionMode(main, spv::ExecutionModeSpacingEqual); + builder.AddExecutionMode(main, spv::ExecutionModeVertexOrderCw); + } + + std::vector finish() { + builder.AddFunction(spv::OpReturn); + builder.AddFunction(spv::OpFunctionEnd); + return builder.Finalize(); + } + + std::uint32_t constant(std::uint32_t type, std::uint32_t value) { return builder.Constant(spv::OpConstant, type, value); } + + template + std::uint32_t result(spv::Op opcode, std::uint32_t type, TArgs... operands) { + const auto id = builder.AllocateId(); + builder.AddFunction(opcode, type, id, operands...); + return id; + } + + std::uint32_t load(std::uint32_t type, std::uint32_t pointer) { return result(spv::OpLoad, type, pointer); } + + void store(std::uint32_t pointer, std::uint32_t value) { builder.AddFunction(spv::OpStore, pointer, value); } + + void decorate(std::uint32_t target, spv::Decoration decoration, std::uint32_t value) { builder.AddAnnotation(spv::OpDecorate, target, decoration, value); } + + std::uint32_t addInterface(spv::StorageClass storage, std::uint32_t type) { + const auto variable = builder.DefineGlobalVariable(builder.Type(spv::OpTypePointer, storage, type), storage); + interfaces.push_back(variable); + return variable; + } +}; + } BarycentricEmulationLayout LayoutBarycentricEmulation(std::span parameters, const BarycentricEmulation& emulation) { @@ -217,4 +409,32 @@ RecompileResult BuildBarycentricGeometryShader(const RecompileResult& vertex, co return result; } +BarycentricTessellationShaders BuildBarycentricTessellationShaders(const RecompileResult& vertex, const RecompileResult& fragment, const SpirvTarget& target, std::uint32_t maxFragmentInputComponents) { + require(fragment.barycentricEmulation.active, "the fragment shader does not emulate barycentrics"); + require(target.tessellation.has_value(), "tessellation shaders are unavailable"); + require(target.spirvVersion >= 0x00010300u && target.spirvVersion <= 0x00010500u, "unsupported SPIR-V target version"); + require(std::find(target.supportedCapabilities.begin(), target.supportedCapabilities.end(), spv::CapabilityTessellation) != target.supportedCapabilities.end(), "tessellation capability is unavailable"); + const auto emulated = CollectEmulatedInterface(vertex, fragment); + const auto passed = static_cast(emulated.passed.size()); + const auto perVertex = static_cast(emulated.perVertex.size()); + const auto clipDistances = VertexClipDistanceCount(vertex.spirv.Words()); + require(clipDistances == 0u || std::find(target.supportedCapabilities.begin(), target.supportedCapabilities.end(), spv::CapabilityClipDistance) != target.supportedCapabilities.end(), "the vertex clip distances need shaderClipDistance"); + const auto controlComponents = 4u + clipDistances + 4u * emulated.sources; + const auto evaluationOutputComponents = 4u + clipDistances + 4u * passed + 3u * emulated.barycentrics + 12u * perVertex; + const auto fragmentComponents = 4u * (passed + emulated.barycentrics + 3u * perVertex); + const auto& limits = *target.tessellation; + require(limits.maxPatchSize >= 3u && controlComponents <= limits.maxControlPerVertexInputComponents && controlComponents <= limits.maxControlPerVertexOutputComponents && 3u * controlComponents + 4u <= limits.maxControlTotalOutputComponents && limits.maxControlPerPatchOutputComponents >= 4u && controlComponents <= limits.maxEvaluationInputComponents && evaluationOutputComponents <= limits.maxEvaluationOutputComponents && fragmentComponents <= maxFragmentInputComponents, "the emulated barycentric interface exceeds device limits"); + std::set sourceSet(emulated.passed.begin(), emulated.passed.end()); + for (const auto& parameter : emulated.perVertex) sourceSet.insert(parameter.sourceLocation); + const std::vector sources(sourceSet.begin(), sourceSet.end()); + BarycentricTessellationShaders shaders; + shaders.control.spirv = BarycentricTessellationEmitter(target.spirvVersion, clipDistances).EmitControl(sources); + shaders.evaluation.spirv = BarycentricTessellationEmitter(target.spirvVersion, clipDistances).EmitEvaluation(sources, emulated.passed, emulated.passedFlat, emulated.perVertex, emulated.layout); +#if ANYPS5_ENABLE_SPIRV_TOOLS + shaders.control.spirv = ValidateAndOptimizeSpirv(shaders.control.spirv, target.vulkanVersion, target.spirvVersion); + shaders.evaluation.spirv = ValidateAndOptimizeSpirv(shaders.evaluation.spirv, target.vulkanVersion, target.spirvVersion); +#endif + return shaders; +} + } From 03f4f6718e5adc6d44dfc0028c34af179adf83d8 Mon Sep 17 00:00:00 2001 From: David Date: Sun, 18 Oct 2026 02:16:51 +0400 Subject: [PATCH 0881/1000] feat(agc): emulate barycentrics through tessellation without geometry shaders A triangle list without primitive restart whose pixel shader emulates barycentrics draws through the generated tessellation stages when the device has tessellation but no geometry shaders, as on MoltenVK. The pipeline uses a three-point patch list for it, keeps it out of pipeline libraries and validates the generated stages' modes and interface. --- .../Execution/include/VulkanDevice.hpp | 1 + .../Execution/src/Driver/Draw/Draw.cpp | 14 ++++++++++++-- .../Execution/src/VulkanDevice.cpp | 4 ++++ .../Graphics/include/Pipeline.hpp | 1 + .../libSceAgcDriver/Graphics/src/Pipeline.cpp | 16 +++++++++++----- .../Graphics/src/ShaderValidation.cpp | 17 +++++++++++++---- 6 files changed, 42 insertions(+), 11 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp index 4aa2fa07f2..766df896cc 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp @@ -55,6 +55,7 @@ public: bool CalibrateClock(double& gpuNs, std::chrono::steady_clock::time_point& cpu) const; ShaderRecompiler::SpirvTarget Target() const; std::optional GeometryLimits() const; + std::uint32_t MaxFragmentInputComponents() const; const std::uint32_t* WorkGroupCountLimit() const; std::uint32_t VendorId() const; ShaderRecompiler::SpirvTarget ComputeTarget(std::uint32_t waveSize) const; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp index 5dea7922ea..b8e705e6dc 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp @@ -451,8 +451,18 @@ DrawVerdict Driver::draw(QueueState& queue, std::span packe if (graphics.rectList) buildRectList(); if (!programs.empty() && programResults.back() != nullptr && programResults.back()->barycentricEmulation.active) { require(!graphics.rectList && graphics.stages.path == Graphics::ShaderPath::Vertex && programs.size() == 2 && programResults[0] != nullptr && stages.size() == 2, "a pixel shader that reads barycentrics without VK_KHR_fragment_shader_barycentric needs a vertex shader before it"); - results.push_back(ShaderRecompiler::BuildBarycentricGeometryShader(*programResults[0], *programResults[1], localDevice->Target(), localDevice->GeometryLimits())); - stages.insert(stages.begin() + 1, Graphics::CompiledShader{Stage::Geometry, &results.back(), 0}); + const auto geometryLimits = localDevice->GeometryLimits(); + if (geometryLimits || !localDevice->Target().tessellation) { + results.push_back(ShaderRecompiler::BuildBarycentricGeometryShader(*programResults[0], *programResults[1], localDevice->Target(), geometryLimits)); + stages.insert(stages.begin() + 1, Graphics::CompiledShader{Stage::Geometry, &results.back(), 0}); + } else { + require(graphics.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST && !graphics.primitiveRestart, "a pixel shader that reads barycentrics without VK_KHR_fragment_shader_barycentric or geometry shaders needs a triangle list without primitive restart"); + auto patches = ShaderRecompiler::BuildBarycentricTessellationShaders(*programResults[0], *programResults[1], localDevice->Target(), localDevice->MaxFragmentInputComponents()); + const auto first = results.size(); + results.push_back(std::move(patches.control)); + results.push_back(std::move(patches.evaluation)); + stages.insert(stages.begin() + 1, {{Stage::TessellationControl, &results[first], 0}, {Stage::TessellationEvaluation, &results[first + 1], 0}}); + } } std::vector snapshots; const auto snapshot = [&] { diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 62afeaea77..bec1e91bdc 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -3507,6 +3507,10 @@ std::optional VulkanDevice::GeometryLimit return ShaderRecompiler::GeometryStageLimits{limits.maxGeometryInputComponents, limits.maxGeometryOutputComponents, limits.maxGeometryOutputVertices, limits.maxGeometryTotalOutputComponents, limits.maxFragmentInputComponents}; } +std::uint32_t VulkanDevice::MaxFragmentInputComponents() const { + return state->properties.limits.maxFragmentInputComponents; +} + bool VulkanDevice::PrimitiveListRestart() const { return state->primitiveListRestart; } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp index 6bd3a8dd82..078e3f8818 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp @@ -134,6 +134,7 @@ bool HasStencil(VkFormat format); void ValidateViewport(const Context& context, const VkViewport& viewport); void ValidateDepthBounds(const Context& context, const State& state); void ValidateProvokingVertex(const Context& context, const State& state, std::span shaders = {}); +bool BarycentricPatches(const State& state, std::span shaders); bool ValidateSampleLocations(const Context& context, const State& state); void ValidateShaderPair(const ShaderRecompiler::RecompileResult& vertex, const ShaderRecompiler::RecompileResult& fragment); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp index d9a9c96bdc..0d65ea65f6 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp @@ -96,6 +96,10 @@ void ValidateDepthBounds(const Context& context, const State& state) { Require(!state.depthBoundsTest || context.depthRangeUnrestricted || (state.minDepthBounds >= 0.0f && state.minDepthBounds <= 1.0f && state.maxDepthBounds >= 0.0f && state.maxDepthBounds <= 1.0f), "depth bounds outside [0, 1] require VK_EXT_depth_range_unrestricted"); } +bool BarycentricPatches(const State& state, std::span shaders) { + return !state.rectList && state.stages.path == ShaderPath::Vertex && std::any_of(shaders.begin(), shaders.end(), [](const CompiledShader& shader) { return shader.stage == ShaderRecompiler::ShaderStage::TessellationControl; }); +} + void ValidateProvokingVertex(const Context& context, const State& state, std::span shaders) { Require(state.provokingVertexMode == VK_PROVOKING_VERTEX_MODE_FIRST_VERTEX_EXT || state.provokingVertexMode == VK_PROVOKING_VERTEX_MODE_LAST_VERTEX_EXT, "invalid provoking vertex mode"); if (state.provokingVertexMode == VK_PROVOKING_VERTEX_MODE_FIRST_VERTEX_EXT) { @@ -106,7 +110,7 @@ void ValidateProvokingVertex(const Context& context, const State& state, std::sp return; } Require(context.provokingVertexLast, "last provoking vertex requires VK_EXT_provoking_vertex with provokingVertexLast enabled"); - Require(!state.rectList && state.stages.path == ShaderPath::Vertex && !state.stages.mesh && !state.stages.tessellation, "last provoking vertex is unsupported for generated primitive pipelines"); + Require(!state.rectList && !BarycentricPatches(state, shaders) && state.stages.path == ShaderPath::Vertex && !state.stages.mesh && !state.stages.tessellation, "last provoking vertex is unsupported for generated primitive pipelines"); Require(state.topology == VK_PRIMITIVE_TOPOLOGY_POINT_LIST || state.topology == VK_PRIMITIVE_TOPOLOGY_LINE_LIST || state.topology == VK_PRIMITIVE_TOPOLOGY_LINE_STRIP || state.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST || state.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP || state.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN, "last provoking vertex is unsupported for this primitive topology"); for (const auto& shader : shaders) { if (shader.stage != ShaderRecompiler::ShaderStage::Fragment) continue; @@ -192,7 +196,7 @@ void appendKey(std::vector& key, const TValue& value) { bool libraryKeys(PipelineLibraryKeys& keys, const Context& context, const State& state, const VertexInputLayout& input, const PipelineSetLayouts& resources, std::span shaders) { using Stage = ShaderRecompiler::ShaderStage; - if (!context.graphicsPipelineLibrary || context.pipelineExecutableInfo || state.depthBiasPerFace || state.rectList || SplitsAlternateAlpha(state)) return false; + if (!context.graphicsPipelineLibrary || context.pipelineExecutableInfo || state.depthBiasPerFace || state.rectList || BarycentricPatches(state, shaders) || SplitsAlternateAlpha(state)) return false; keys = {}; for (const auto& shader : shaders) { const auto identity = shader.program->Identity(); @@ -310,6 +314,8 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput Require(state.conservativeRasterization == VK_CONSERVATIVE_RASTERIZATION_MODE_DISABLED_EXT || context.conservativeRasterization, "conservative rasterization requires VK_EXT_conservative_rasterization with at most 1/256 pixel of overestimation and degenerate triangles rasterized"); ValidateProvokingVertex(context, state, shaders); if (state.rectList) Require(context.tessellationShader && context.limits.maxTessellationPatchSize >= 4, "rect-list requires tessellation with four output control points"); + const bool barycentricPatches = BarycentricPatches(state, shaders); + if (barycentricPatches) Require(context.tessellationShader && state.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST && !state.primitiveRestart, "emulated barycentrics through tessellation require a triangle list without primitive restart"); if (std::any_of(shaders.begin(), shaders.end(), [](const CompiledShader& shader) { return shader.stage == ShaderRecompiler::ShaderStage::Geometry; })) Require(context.geometryShader, "device does not support geometry shaders"); if (state.stages.tessellation) { Require(context.tessellationShader, "device does not support tessellation shaders"); @@ -425,7 +431,7 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput input.vertexAttributeDescriptionCount = static_cast(vertexInput.attributes.size()); input.pVertexAttributeDescriptions = vertexInput.attributes.data(); VkPipelineInputAssemblyStateCreateInfo assembly{VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO}; - assembly.topology = state.topology; + assembly.topology = BarycentricPatches(state, shaders) ? VK_PRIMITIVE_TOPOLOGY_PATCH_LIST : state.topology; const bool listTopology = state.topology == VK_PRIMITIVE_TOPOLOGY_POINT_LIST || state.topology == VK_PRIMITIVE_TOPOLOGY_LINE_LIST || state.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; assembly.primitiveRestartEnable = state.primitiveRestart && (!listTopology || context.primitiveListRestart) ? VK_TRUE : VK_FALSE; // Viewport and scissor are set per draw (Begin), so they do not multiply pipelines; the depth @@ -512,8 +518,8 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput pipelineInfo.stageCount = static_cast(stages.size()); pipelineInfo.pStages = stages.data(); VkPipelineTessellationStateCreateInfo tessellation{VK_STRUCTURE_TYPE_PIPELINE_TESSELLATION_STATE_CREATE_INFO}; - if (state.rectList || state.stages.tessellation) { - tessellation.patchControlPoints = state.rectList ? 3u : state.stages.tessellation->inputControlPoints; + if (state.rectList || state.stages.tessellation || barycentricPatches) { + tessellation.patchControlPoints = state.stages.tessellation ? state.stages.tessellation->inputControlPoints : 3u; pipelineInfo.pTessellationState = &tessellation; } pipelineInfo.pVertexInputState = state.stages.mesh ? nullptr : &input; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp index 34bd3349af..2880094519 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp @@ -442,6 +442,7 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi const auto it = module.modes.find(name); Require(it != module.modes.end() && it->second == operands, "missing or incompatible shader execution mode"); }; + const bool barycentricPatches = (control || evaluation) && !state.rectList && !state.stages.tessellation && state.stages.path == ShaderPath::Vertex; if (mesh) { Require(state.stages.mesh.has_value(), "mesh configuration is missing"); const auto& config = *state.stages.mesh; @@ -452,6 +453,12 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi mode(spv::ExecutionModeOutputPrimitivesEXT, {config.maxPrimitives}); mode(config.outputPrimitiveSize == 2u ? spv::ExecutionModeOutputLinesEXT : spv::ExecutionModeOutputTrianglesEXT, {}); Require(module.modes.size() == 4, "unsupported mesh execution mode"); + } else if (barycentricPatches) { + if (control) mode(spv::ExecutionModeOutputVertices, {3u}); + mode(spv::ExecutionModeTriangles, {}); + mode(spv::ExecutionModeSpacingEqual, {}); + mode(spv::ExecutionModeVertexOrderCw, {}); + Require(module.modes.size() == (control ? 4u : 3u), "unsupported generated tessellation execution mode"); } else if (control) { Require(state.rectList || state.stages.tessellation.has_value(), "tessellation configuration is missing"); mode(spv::ExecutionModeOutputVertices, {state.rectList ? 4u : state.stages.tessellation->outputControlPoints}); @@ -504,7 +511,7 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi } else if (geometry) { Require(length->second == 3u, "geometry interface array must hold the three vertices of a triangle"); } else { - const auto expected = state.rectList ? (control && input ? 3u : 4u) : control && input ? state.stages.tessellation->inputControlPoints : state.stages.tessellation->outputControlPoints; + const auto expected = barycentricPatches ? 3u : state.rectList ? (control && input ? 3u : 4u) : control && input ? state.stages.tessellation->inputControlPoints : state.stages.tessellation->outputControlPoints; Require(length->second == expected, "tessellation interface array disagrees with control-point count"); } typeId = outer[2]; @@ -605,7 +612,7 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi Require(shader.vertexAttributes.empty(), "vertex attribute metadata is invalid for this stage"); } Require(descriptors.size() == shader.bindings.size(), "recompiler binding metadata contains undeclared resources"); - Require(push == (!shader.pushConstants.empty() || mesh || (control && !rectListControl)), "recompiler push constant metadata disagrees with SPIR-V"); + Require(push == (!shader.pushConstants.empty() || mesh || (control && !rectListControl && !barycentricPatches)), "recompiler push constant metadata disagrees with SPIR-V"); for (const auto id : module.interface) Require(module.variables.contains(id), "entry point interface contains an unknown variable"); if (mesh) Require(module.primitiveIndices == state.stages.mesh->outputPrimitiveSize, "mesh shader does not export primitive indices"); if (stage == Stage::Vertex || mesh || evaluation || geometry) Require(module.position, "vertex shader does not export position"); @@ -636,7 +643,9 @@ std::set ValidateShaders(std::span shaders, Require(state.stages.mesh.has_value() == mesh && state.stages.tessellation.has_value() == tessellation, "graphics stage configuration disagrees with its path"); Require(!state.rectList || (state.stages.path == ShaderPath::Vertex && state.topology == VK_PRIMITIVE_TOPOLOGY_PATCH_LIST && state.cullMode == VK_CULL_MODE_NONE), "invalid rect-list pipeline state"); const bool geometry = state.stages.path == ShaderPath::Vertex && !state.rectList && shaders.size() == 3u; - Require(geometry || shaders.size() == (tessellation || state.rectList ? 4u : 2u), "incorrect graphics stage count"); + const bool patches = BarycentricPatches(state, shaders) && shaders.back().program != nullptr && shaders.back().program->barycentricEmulation.active; + Require(geometry || shaders.size() == (tessellation || state.rectList || patches ? 4u : 2u), "incorrect graphics stage count"); + if (patches) Require(state.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST, "emulated barycentrics through tessellation require a triangle list"); if (geometry) { Require(shaders.back().program != nullptr && shaders.back().program->barycentricEmulation.active, "a geometry stage is generated only for emulated barycentrics"); Require(state.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST || state.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP || state.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN, "emulated barycentrics require triangle primitives"); @@ -645,7 +654,7 @@ std::set ValidateShaders(std::span shaders, static_cast(AssemblePushConstants(shaders)); Module previous; for (std::size_t i = 0; i < shaders.size(); ++i) { - const auto expected = state.rectList ? (i == 0 ? Stage::Vertex : tessStages[i]) : tessellation ? tessStages[i] : geometry && i == 1 ? Stage::Geometry : i == shaders.size() - 1 ? Stage::Fragment : state.stages.path == ShaderPath::Geometry ? Stage::Mesh : Stage::Vertex; + const auto expected = state.rectList || patches ? (i == 0 ? Stage::Vertex : tessStages[i]) : tessellation ? tessStages[i] : geometry && i == 1 ? Stage::Geometry : i == shaders.size() - 1 ? Stage::Fragment : state.stages.path == ShaderPath::Geometry ? Stage::Mesh : Stage::Vertex; Require(shaders[i].program != nullptr, "missing compiled shader"); Require(shaders[i].stage == expected, "graphics stage order disagrees"); for (const auto& binding : shaders[i].program->bindings) Require(binding.descriptorSet == ShaderRecompiler::RuntimeAbi::DescriptorSet || (shaders[i].stage == ShaderRecompiler::ShaderStage::Fragment && binding.descriptorSet == ShaderRecompiler::RuntimeAbi::FragmentDescriptorSet), "graphics resource uses a descriptor set other than its stage's"); From 48b7d3a34d5ec88bd8f7122c8a0ee0d0fb8138af Mon Sep 17 00:00:00 2001 From: David Date: Sun, 18 Oct 2026 02:16:51 +0400 Subject: [PATCH 0882/1000] test(agc): cover the tessellation route of the barycentric emulation --- .../prx/libSceAgcDriver/tests/Graphics.cpp | 60 +++++++++++++++ .../execution/PixelBarycentricEmulation.cpp | 75 ++++++++++++++----- 2 files changed, 115 insertions(+), 20 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 95c09675a9..16d7bf35e8 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -5550,7 +5550,67 @@ void barycentricEmulationTests() { expectFailure([&] { static_cast(BuildBarycentricGeometryShader(vertex, plain, target, limits)); }, "does not emulate barycentrics"); expectFailure([&] { static_cast(BuildBarycentricGeometryShader(vertex, fragment, target, std::nullopt)); }, "geometry shaders are unavailable"); expectFailure([&] { static_cast(BuildBarycentricGeometryShader(vertex, fragment, target, GeometryStageLimits{64u, 12u, 256u, 1024u, 128u})); }, "exceeds device limits"); + + const std::array tessellationCapabilities{spv::CapabilityShader, spv::CapabilityTessellation}; + SpirvTarget tessellationTarget = target; + tessellationTarget.supportedCapabilities = tessellationCapabilities; + tessellationTarget.tessellation = TessellationTargetLimits{32u, 128u, 128u, 120u, 4096u, 128u, 128u}; + const auto modes = [](const RecompileResult& result) { + std::vector values; + const auto& words = result.spirv.Words(); + for (std::size_t at = 5; at < words.size() && (words[at] >> 16u) != 0; at += words[at] >> 16u) { + if (static_cast(words[at] & 0xffffu) == spv::OpExecutionMode) values.push_back(words[at + 2]); + } + return values; + }; + auto patches = BuildBarycentricTessellationShaders(vertex, fragment, tessellationTarget, 128u); + Require(modes(patches.control) == std::vector{spv::ExecutionModeOutputVertices, spv::ExecutionModeTriangles, spv::ExecutionModeSpacingEqual, spv::ExecutionModeVertexOrderCw}, "the generated control stage does not declare three output vertices and the triangle domain"); + Require(modes(patches.evaluation) == std::vector{spv::ExecutionModeTriangles, spv::ExecutionModeSpacingEqual, spv::ExecutionModeVertexOrderCw}, "the generated evaluation stage does not declare the triangle domain"); + const std::array patchShaders{{{ShaderStage::Vertex, &vertex, 0}, {ShaderStage::TessellationControl, &patches.control, 0}, {ShaderStage::TessellationEvaluation, &patches.evaluation, 0}, {ShaderStage::Fragment, &fragment, 0}}}; + Require(BarycentricPatches(state, patchShaders) && !BarycentricPatches(state, shaders), "generated tessellation stages were not recognised as barycentric patches"); + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + ValidateShaders(patchShaders, state, subgroup, false); + for (const auto topology : {VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP, VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN, VK_PRIMITIVE_TOPOLOGY_POINT_LIST}) { + state.topology = topology; + expectFailure([&] { ValidateShaders(patchShaders, state, subgroup, false); }, "require a triangle list"); + } + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + const std::array plainPatchShaders{{{ShaderStage::Vertex, &vertex, 0}, {ShaderStage::TessellationControl, &patches.control, 0}, {ShaderStage::TessellationEvaluation, &patches.evaluation, 0}, {ShaderStage::Fragment, &plain, 0}}}; + expectFailure([&] { ValidateShaders(plainPatchShaders, state, subgroup, false); }, "stage count"); + expectFailure([&] { static_cast(BuildBarycentricTessellationShaders(vertex, plain, tessellationTarget, 128u)); }, "does not emulate barycentrics"); + auto noTessellation = tessellationTarget; + noTessellation.tessellation.reset(); + expectFailure([&] { static_cast(BuildBarycentricTessellationShaders(vertex, fragment, noTessellation, 128u)); }, "tessellation shaders are unavailable"); + expectFailure([&] { static_cast(BuildBarycentricTessellationShaders(vertex, fragment, target, 128u)); }, "tessellation shaders are unavailable"); + auto noCapability = tessellationTarget; + noCapability.supportedCapabilities = std::span(capabilities).first(1); + expectFailure([&] { static_cast(BuildBarycentricTessellationShaders(vertex, fragment, noCapability, 128u)); }, "tessellation capability is unavailable"); + auto smallPatches = tessellationTarget; + smallPatches.tessellation->maxEvaluationOutputComponents = 4u; + expectFailure([&] { static_cast(BuildBarycentricTessellationShaders(vertex, fragment, smallPatches, 128u)); }, "exceeds device limits"); + expectFailure([&] { static_cast(BuildBarycentricTessellationShaders(vertex, fragment, tessellationTarget, 0u)); }, "exceeds device limits"); + + RecompileResult clipped; + clipped.spirv = makeModule({.parameterOutput = true, .clipDistanceLength = 2}); + clipped.parameterExports = {0u}; + expectFailure([&] { static_cast(BuildBarycentricTessellationShaders(clipped, fragment, tessellationTarget, 128u)); }, "need shaderClipDistance"); + const std::array clipCapabilities{spv::CapabilityShader, spv::CapabilityTessellation, spv::CapabilityClipDistance}; + auto clipTarget = tessellationTarget; + clipTarget.supportedCapabilities = clipCapabilities; + auto clippedPatches = BuildBarycentricTessellationShaders(clipped, fragment, clipTarget, 128u); + const auto memberBuiltIn = [](const RecompileResult& result, spv::BuiltIn builtin) { + const auto& words = result.spirv.Words(); + for (std::size_t at = 5; at < words.size() && (words[at] >> 16u) != 0; at += words[at] >> 16u) { + if (static_cast(words[at] & 0xffffu) == spv::OpMemberDecorate && words[at + 3] == spv::DecorationBuiltIn && words[at + 4] == builtin) return true; + } + return false; + }; + Require(!memberBuiltIn(patches.control, spv::BuiltInClipDistance) && !memberBuiltIn(patches.evaluation, spv::BuiltInClipDistance), "a generated tessellation stage declares clip distances the vertex shader does not write"); + Require(memberBuiltIn(clippedPatches.control, spv::BuiltInClipDistance) && memberBuiltIn(clippedPatches.evaluation, spv::BuiltInClipDistance), "a generated tessellation stage does not forward the vertex clip distances"); + const std::array clippedShaders{{{ShaderStage::Vertex, &clipped, 0}, {ShaderStage::TessellationControl, &clippedPatches.control, 0}, {ShaderStage::TessellationEvaluation, &clippedPatches.evaluation, 0}, {ShaderStage::Fragment, &fragment, 0}}}; + ValidateShaders(clippedShaders, state, subgroup, false, false, false, false, false, false, true); vertex.parameterExports.clear(); + expectFailure([&] { static_cast(BuildBarycentricTessellationShaders(vertex, fragment, tessellationTarget, 128u)); }, "not exported by the vertex shader"); expectFailure([&] { static_cast(BuildBarycentricGeometryShader(vertex, fragment, target, limits)); }, "not exported by the vertex shader"); } diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PixelBarycentricEmulation.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PixelBarycentricEmulation.cpp index 3a9d3178ef..9978674554 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PixelBarycentricEmulation.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PixelBarycentricEmulation.cpp @@ -58,7 +58,12 @@ std::array BufferDescriptor(const void* data, std::uint32_t st return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (stride << 16u), count, 0x01016facu}; } -ShaderRecompiler::RecompileResult Draw(AgcDriver::VulkanDevice& device, std::span pixelCode, std::uint32_t inputs) { +enum class Emulation { + Geometry, + Tessellation, +}; + +ShaderRecompiler::RecompileResult Draw(AgcDriver::VulkanDevice& device, Emulation emulation, std::span pixelCode, std::uint32_t inputs, VkCullModeFlags cullMode = VK_CULL_MODE_NONE) { Pixels.fill(Unwritten); auto target = device.Target(); target.fragmentShaderBarycentricEnabled = false; @@ -98,9 +103,14 @@ ShaderRecompiler::RecompileResult Draw(AgcDriver::VulkanDevice& device, std::spa auto pixelResult = ShaderRecompiler::Recompile(fragment); std::vector shaders{{ShaderStage::Vertex, &vertexResult, 0}}; ShaderRecompiler::RecompileResult geometry; - if (pixelResult.barycentricEmulation.active) { + ShaderRecompiler::BarycentricTessellationShaders patches; + if (pixelResult.barycentricEmulation.active && emulation == Emulation::Geometry) { geometry = ShaderRecompiler::BuildBarycentricGeometryShader(vertexResult, pixelResult, target, device.GeometryLimits()); shaders.push_back({ShaderStage::Geometry, &geometry, 0}); + } else if (pixelResult.barycentricEmulation.active) { + patches = ShaderRecompiler::BuildBarycentricTessellationShaders(vertexResult, pixelResult, target, device.MaxFragmentInputComponents()); + shaders.push_back({ShaderStage::TessellationControl, &patches.control, 0}); + shaders.push_back({ShaderStage::TessellationEvaluation, &patches.evaluation, 0}); } shaders.push_back({ShaderStage::Fragment, &pixelResult, PixelPushOffset(vertexPush, target)}); @@ -114,7 +124,7 @@ ShaderRecompiler::RecompileResult Draw(AgcDriver::VulkanDevice& device, std::spa state.viewport = {0, static_cast(Height), static_cast(Width), -static_cast(Height), 0, 1}; state.negativeOneToOne = false; state.scissor = {{0, 0}, {Width, Height}}; - state.cullMode = VK_CULL_MODE_NONE; + state.cullMode = cullMode; state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; state.blend.colorWriteMask = 15; state.blends = {state.blend}; @@ -232,31 +242,56 @@ void CheckInterpolated() { Require(checked > 200u, "v_interp_p1/p2: too few interior pixels were checked"); } +bool Drawn() { + return std::any_of(Pixels.begin(), Pixels.end(), [](float value) { return value != Unwritten; }); +} + +void CheckWinding(AgcDriver::VulkanDevice& device, Emulation emulation, const std::string& name) { + const auto perspective = PixelInputBit(PixelInput::PerspectiveCenter); + for (const auto cullMode : {VK_CULL_MODE_BACK_BIT, VK_CULL_MODE_FRONT_BIT}) { + Draw(device, emulation, InterpolatedPixelCode, perspective, cullMode); + const bool host = Drawn(); + Draw(device, emulation, BarycentricPixelCode, perspective, cullMode); + Require(Drawn() == host, name + ": culling the " + (cullMode == VK_CULL_MODE_BACK_BIT ? "back" : "front") + " face does not match the draw without emulation"); + } +} + +void RunEmulation(AgcDriver::VulkanDevice& device, Emulation emulation) { + const std::string name = emulation == Emulation::Geometry ? "geometry stage" : "tessellation stages"; + const auto perspective = PixelInputBit(PixelInput::PerspectiveCenter); + const auto linear = PixelInputBit(PixelInput::LinearCenter); + auto result = Draw(device, emulation, BarycentricPixelCode, perspective); + Require(result.barycentricEmulation.active && result.barycentricEmulation.smooth && !result.barycentricEmulation.linear, "reading PERSP_CENTER I/J did not emulate perspective barycentrics"); + const auto smooth = CheckBarycentrics(name + " PERSP_CENTER I/J", true); + result = Draw(device, emulation, BarycentricPixelCode, linear); + Require(result.barycentricEmulation.active && !result.barycentricEmulation.smooth && result.barycentricEmulation.linear, "reading LINEAR_CENTER I/J did not emulate linear barycentrics"); + const auto noPerspective = CheckBarycentrics(name + " LINEAR_CENTER I/J", false); + result = Draw(device, emulation, VertexValuePixelCode, perspective); + Require(result.barycentricEmulation.active && result.fragmentParameters.size() == 1 && result.fragmentParameters[0].perVertex, "v_interp_mov of P10 and P20 did not emulate per-vertex parameters"); + CheckVertexValues(); + result = Draw(device, emulation, InterpolatedPixelCode, perspective); + Require(!result.barycentricEmulation.active, "plain v_interp_p1/p2 needed a generated stage"); + CheckInterpolated(); + CheckWinding(device, emulation, name); + std::printf("barycentric emulation through the %s passed: %u perspective and %u linear I/J pixels, raw vertex values, host interpolation and face culling\n", name.c_str(), smooth, noPerspective); +} + } int main() { try { const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; - if (!device->GeometryLimits()) { - std::puts("skipped, the device lacks geometryShader"); + const bool geometry = device->GeometryLimits().has_value(); + const bool tessellation = device->Target().tessellation.has_value(); + if (!geometry && !tessellation) { + std::puts("skipped, the device lacks geometryShader and tessellationShader"); return VulkanTestSkipped; } - const auto perspective = PixelInputBit(PixelInput::PerspectiveCenter); - const auto linear = PixelInputBit(PixelInput::LinearCenter); - auto result = Draw(*device, BarycentricPixelCode, perspective); - Require(result.barycentricEmulation.active && result.barycentricEmulation.smooth && !result.barycentricEmulation.linear, "reading PERSP_CENTER I/J did not emulate perspective barycentrics"); - const auto smooth = CheckBarycentrics("PERSP_CENTER I/J", true); - result = Draw(*device, BarycentricPixelCode, linear); - Require(result.barycentricEmulation.active && !result.barycentricEmulation.smooth && result.barycentricEmulation.linear, "reading LINEAR_CENTER I/J did not emulate linear barycentrics"); - const auto noPerspective = CheckBarycentrics("LINEAR_CENTER I/J", false); - result = Draw(*device, VertexValuePixelCode, perspective); - Require(result.barycentricEmulation.active && result.fragmentParameters.size() == 1 && result.fragmentParameters[0].perVertex, "v_interp_mov of P10 and P20 did not emulate per-vertex parameters"); - CheckVertexValues(); - result = Draw(*device, InterpolatedPixelCode, perspective); - Require(!result.barycentricEmulation.active, "plain v_interp_p1/p2 needed a geometry stage"); - CheckInterpolated(); - std::printf("barycentric emulation tests passed: %u perspective and %u linear I/J pixels, raw vertex values and host interpolation\n", smooth, noPerspective); + if (geometry) RunEmulation(*device, Emulation::Geometry); + else std::puts("geometry stage skipped, the device lacks geometryShader"); + if (tessellation) RunEmulation(*device, Emulation::Tessellation); + else std::puts("tessellation stages skipped, the device lacks tessellationShader"); return 0; } catch (const std::exception& error) { std::cerr << error.what() << '\n'; From 91d5f577ea9f59fd57834a3b0e99e975c686ce22 Mon Sep 17 00:00:00 2001 From: David Date: Sun, 18 Oct 2026 02:17:01 +0400 Subject: [PATCH 0883/1000] docs(shader): record the tessellation route of the barycentric emulation --- docs/dev/debt/shader.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index 5a9518896e..6fc14e165a 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -84,7 +84,7 @@ - [Mesh shader grid size](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) (shader recompiler) - every mesh shader loads `NumWorkgroups.x` and declares no vertices when it is 0, which no dispatch produces. The read is a workaround for RADV (Mesa 26.2.4, seen on an RX 9070 XT, GFX12): binding a mesh pipeline whose shader reads neither `NumWorkgroups` nor `DrawIndex` keeps the user SGPR count and draw-id flag of the vertex pipeline bound before it in the same command buffer (`radv_bind_pre_rast_shader` updates them only when the shader has the `AC_UD_VS_BASE_VERTEX_START_INSTANCE` slot), so `radv_emit_userdata_mesh` opens a 3-register `SET_SH_REG` and writes one value, the CP takes the following `DISPATCH_MESH_DIRECT` as register data and the GPU hangs (gfx ring timeout, MODE1 reset). Kena: Bridge of Spirits (PPSA01802) hits it on its layered volume draws. Reading `NumWorkgroups` gives every mesh shader the grid-size slot, so the count and the values match; drop the read once a fixed Mesa is the minimum - [Shader recompilation](../../../core/shader/recompiler/Recompiler.cpp) currently occurs right before it was transferred to Vulkan with caching, but should be moved to the [relinker](../../../core/relinker/main.cpp) stage. For this purpose, [shader/recompiler](../../../core/shader/recompiler) was written completely independently from [libs/prx](../../../core/libs/prx). - [v_interp_p1ll_f16, v_interp_p1lv_f16 and v_interp_p2_f16](../../../core/shader/recompiler/Translation/src/AttributeInstructions.cpp) read the 16-bit halves of each vertex's parameter through `VK_KHR_fragment_shader_barycentric` and throw without it, as fixed-function interpolation cannot split a 32-bit attribute. P10 and P20 are the vertex differences rounded to f16 to nearest even, which is not confirmed on hardware. v_interp_p1ll_f16 and v_interp_p1lv_f16 round the exact `P10 * I + P0` once to f32 and v_interp_p2_f16 rounds the exact `P20 * J + S2` once to f16, as measured on RDNA2 (#697); the exact sums are computed in f64, so these instructions need the Float64 capability. They follow the shader's float mode, measured on RDNA2 (gfx1035) in all eight combinations of `IEEE_MODE` and kept/flushed f32 and f16 denormals over 768 special and 1,024 random rows. With IEEE=1 a NaN operand is returned quieted; with IEEE=0 it is returned unquieted, except that v_interp_p2_f16's f16 result is always quiet. Flushed f32 denormals flush I/J, v_interp_p2_f16's f32 S2 and the f32 result; flushed f16 denormals flush P0, P10/P20, v_interp_p1lv_f16's f16 S2 and the f16 result. Denormal modes 1 and 2 (flush only inputs or only outputs) and rounding other than to nearest even are not measured and throw. An unknown mode follows IEEE=0 with denormals kept. Lavapipe has no barycentrics, so the draw test is skipped in CI; `agc_driver_interpolation_modes` checks the arithmetic in every mode against the hardware rows on any device with Float64. -- Without `VK_KHR_fragment_shader_barycentric`, a pixel shader whose I/J VGPRs [only feed `v_interp_p1_f32`/`v_interp_p2_f32` of their own pair](../../../core/shader/recompiler/Optimization/src/HostInterpolationChecker.cpp) reads the attribute Vulkan interpolated. Any other read of them or of a raw vertex parameter takes the weights and the three vertex values from a [generated geometry stage](../../../core/shader/recompiler/SpirvBackend/src/BarycentricGeometryShader.cpp), which needs `geometryShader` and a vertex shader before it: tessellation, mesh and rect-list pipelines, primitives other than triangles, more than 32 fragment input locations and interfaces beyond the device's geometry limits throw. +- Without `VK_KHR_fragment_shader_barycentric`, a pixel shader whose I/J VGPRs [only feed `v_interp_p1_f32`/`v_interp_p2_f32` of their own pair](../../../core/shader/recompiler/Optimization/src/HostInterpolationChecker.cpp) reads the attribute Vulkan interpolated. Any other read of them or of a raw vertex parameter takes the weights and the three vertex values from a [generated geometry stage](../../../core/shader/recompiler/SpirvBackend/src/BarycentricGeometryShader.cpp), which needs `geometryShader` and a vertex shader before it. On a device with `tessellationShader` but no `geometryShader` (MoltenVK), a triangle list without primitive restart takes them from [generated tessellation stages](../../../core/shader/recompiler/SpirvBackend/src/BarycentricGeometryShader.cpp) instead: each triangle is one three-point patch at tessellation level 1, the evaluation stage writes the corner `gl_TessCoord` points at as the weights, and the vertex clip distances are forwarded. A flat parameter takes the first vertex's value there, as the geometry stage's strip gives it; the last provoking vertex throws. The patch keeps the triangle's facing with `VertexOrderCw`, checked with face culling on MoltenVK 1.4.2 (Apple M2) only. Tessellation, mesh and rect-list pipelines, primitives other than triangles, triangle strips and fans without `geometryShader`, more than 32 fragment input locations and interfaces beyond the device's geometry or tessellation limits throw. - [v_fma_f64 and v_div_fmas_f64](../../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterFloat64.cpp) with a subnormal operand or result round once, as IEEE 754 fusedMultiplyAdd does with f64 denormals kept, measured on RDNA2 (gfx1035) at IEEE=0 and IEEE=1, `v_div_fmas_f64` with VCC clear and set. The f16/f64 denormal field of `FLOAT_MODE` (bits 7:6) is not read: when it flushes, the hardware flushes the operands and the result of `v_fma_f64` (tininess before rounding) and only the result of `v_div_fmas_f64`, while these keep them. Shaders with f64 instructions declare `DenormPreserve` for 64-bit floats only when `shaderDenormPreserveFloat64` is true and Vulkan reports `denormBehaviorIndependence` as `ALL`; other independence modes are not handled by the current backend because they require coupled execution modes. See [VkShaderFloatControlsIndependence](https://docs.vulkan.org/refpages/latest/refpages/source/VkShaderFloatControlsIndependence.html). Elsewhere (including lavapipe), the driver may flush f64 subnormals, and `agc_driver_f64_fma_subnormals` is skipped. - [SINT storage images](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) of 8- and 16-bit components saturate each stored 32-bit value to the component range, as adriadam10 measured for image stores on RDNA2 (gfx1035) in #944 (448 rows match); the PS5's gfx1013 is not measured. A D16 store sign-extends each 16-bit half before that saturation, as measured on RDNA2 (gfx1035) for `image_store d16` to all eight SINT formats with DMASK 0xf, 0x7, 0x5, 0x3, 0x2, 0xa and 0x1 (1792 rows match). - [`StoreTexel`](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) writes channel 1 of a two-channel format from the data slot whose DST_SEL is Y, for SINT and UINT alike. With DST_SEL X,Y,Z,W, gfx1035 writes it from the W slot, as adriadam10 measured in #944. @@ -143,7 +143,7 @@ - [FORCE_DEGAMMA on RGBA8_UINT](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) materializes a direct image sampled only by plain biased pixel accesses as RGBA8_SRGB when every paired sampler forces degamma. Measured on AMD integrated RDNA2 gfx1036 (driver 32.0.21036.18, Windows 11, wave32, IEEE off, DX10 clamp on, denormals preserved, round to nearest even, FP16 overflow off): a native `image_sample_b` DIM 2D on a 16x16x1 RGBA8_UINT 3D T# returns integer channels without the bit, but the same floating-point decoded values as RGBA8_UNORM with it, including point min/max reduction and the title's exact sampler. The metadata and image view must both become floating-point sRGB; ignoring the sampler bit or retaining a UINT heap is incorrect. The accepted case requires one slice, mip zero, normalized point sampling and uniform FORCE_DEGAMMA across its samplers. Other image accesses, mixed sampler states, indirect descriptors and other integer formats remain refused. Wave64, other filtering modes and the PS5's gfx1013 are not measured. - The [s_swappc_b64 fetch-shader call at vertex/local/tessellation/mesh entry](../../../core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp) assumes the called fetch-shader blob ends in `s_setpc_b64` to the link pair (the GNM fetch-shader shape shadPS4's `EmitFetch` copies); the blob is not part of the shader program, so its return closes the pairing by assumption and any in-program reader of the link pair throws. Attribute fetches are inlined from the request's parsed fetch-shader plan through the same `GetAttribute` path as the embedded fetch analyzer, honoring the buffer resource `dst_sel`; an attribute writing more than four registers, or an entry call without a parsed plan, throws. The vertex/instance offset accumulator inside the fetch blob is not replayed for external blobs. - The scalar aperture sources [src_shared_base, src_shared_limit, src_private_base, src_private_limit](../../../core/shader/recompiler/RdnaDecoder/src/RdnaInstructionDecoder.cpp) (core/shader/recompiler, scalar source codes 235 to 238) read as the 64-bit apertures the flat routing uses: a base is the aperture's high dword (`SharedApertureHigh`, `PrivateApertureHigh`) over a low dword of 0, a limit is the same high dword over 0xffffffff. A 64-bit read (`s_mov_b64`, a 64-bit VALU source, a wave64 lane mask) gets the whole value and a 32-bit read gets the low dword. This follows LLVM's `SIRegisterInfo.td` (aperture registers are 64-bit, their low 32 bits are always 0 for a base and -1 for a limit) and `getSegmentAperture` (`AMDGPULegalizerInfo.cpp`, `SIISelLowering.cpp`: a 64-bit read whose high half is the aperture), the KFD's `MAKE_LDS_APP_LIMIT` (limit = base | 0xffffffff) and the gfx1030 and gfx1035 readouts on [#1190](https://github.com/boykopovar/AnyPS5/pull/1190) and [#3803](https://github.com/boykopovar/AnyPS5/issues/3803). Not measured on a console: the PS5's high dwords are the inferred values of the flat aperture bases entry, and the wave is taken to be in 64-bit address mode (PTR32 clear) -- [EmitSetAttribute](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) (shader recompiler) - a primitive with a vertex whose clip position is (0,0,0,0) is clipped away through an extra `gl_ClipDistance` element on vertex and tessellation evaluation shaders, matching what a Radeon 610M (RDNA2, RADV) and llvmpipe rasterise; NVIDIA 615.78.08 without it draws the vertex at the viewport centre. Measured for triangles (strip and list) and for a point at (0,0,0,0), which the Radeon 610M does not draw, NVIDIA draws at the viewport centre and llvmpipe at the top-left corner; lines with such a vertex are clipped by the same element and were not measured. Mesh, geometry and the generated rect-list and barycentric stages do not write it, a device without `shaderClipDistance` cannot compile vertex shaders (the compile throws), and guest clip distances are still refused by the pipeline validator. The PS5 rasteriser was not measured. +- [EmitSetAttribute](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) (shader recompiler) - a primitive with a vertex whose clip position is (0,0,0,0) is clipped away through an extra `gl_ClipDistance` element on vertex and tessellation evaluation shaders, matching what a Radeon 610M (RDNA2, RADV) and llvmpipe rasterise; NVIDIA 615.78.08 without it draws the vertex at the viewport centre. Measured for triangles (strip and list) and for a point at (0,0,0,0), which the Radeon 610M does not draw, NVIDIA draws at the viewport centre and llvmpipe at the top-left corner; lines with such a vertex are clipped by the same element and were not measured. Mesh, geometry and the generated barycentric geometry stage do not write it, the generated rect-list and barycentric tessellation stages forward the vertex shader's array, a device without `shaderClipDistance` cannot compile vertex shaders (the compile throws), and guest clip distances are still refused by the pipeline validator. The PS5 rasteriser was not measured. - [PERSP_SAMPLE and LINEAR_SAMPLE](../../../core/shader/recompiler/Translation/src/InstructionTranslator.cpp) (recompiler) hold the pixel-centre barycentrics, like the centroid pairs. With one sample this is the sample position ([Vulkan standard sample locations](https://docs.vulkan.org/spec/latest/chapters/primsrast.html#primsrast-multisampling): (0.5, 0.5)); with more samples the console's per-sample weights are not computed and were not measured. - [LINE_STIPPLE_TEX](../../../core/shader/recompiler/Translation/src/InstructionTranslator.cpp) (recompiler) reads as 0: line stipple is not emulated. RADV enables the input only as the required pixel input of a shader that reads no interpolants, because it initializes the fewest VGPRs ([Mesa](https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/src/amd/vulkan/radv_shader.c) `radv_compute_spi_ps_input`), and never reads it - [Static scalar calls](../../../core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp) (`s_call_b64` and `s_swappc_b64`) require an ordinary even-aligned SGPR link pair (s0:s1 through s104:s105) and a later `s_setpc_b64` return reading that exact pair. A pair serves one call, or several calls whose regions are disjoint, each closed by the first return reading the pair after its target. Outside every region of its calls the pair is free to use wherever no return address can reach: a dataflow over the decoded instructions follows each dword of the pair from the calls that write it until a scalar load, SALU or VALU result is known to overwrite it (a VALU mask counts as one SGPR unless the request says wave64), and an instruction there that may read a dword still holding a return address throws; SOPC compares are exempt, as they only set SCC. Regions must nest or be disjoint; recursion, other shared links, escaping link values (including overlapping tuples and resource descriptors), invalid targets, interleaved regions, cross-region branches or fall-through, relative SGPR reads and dynamic `s_setpc_b64` in a program with calls throw. The analysis checks all decoded instructions, including unreachable code. Arbitrary indirect calls and general call frames are not implemented. From b2d9880700fef64c1d5df394f6afa80d8b8ca558 Mon Sep 17 00:00:00 2001 From: Romans Pokrovskis Date: Sun, 18 Oct 2026 00:08:31 +0300 Subject: [PATCH 0884/1000] feat(agc): support thick 4 KiB standard XOR volumes --- .../include/TextureSwizzleEquations.hpp | 5 ++++ .../Graphics/src/TextureDetiler.cpp | 4 ++-- .../Graphics/src/TextureTiling.cpp | 7 +++--- .../libSceAgcDriver/tests/TextureTiling.cpp | 20 +++++++++++++++- .../tests/execution/ImageXorSwizzleVolume.cpp | 5 +++- .../tests/execution/LayeredVolumeDraw.cpp | 24 ++++++++++++------- docs/dev/debt/libSceAgcDriver.md | 1 + 7 files changed, 51 insertions(+), 15 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/TextureSwizzleEquations.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/TextureSwizzleEquations.hpp index b77267d50a..d408819b49 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/TextureSwizzleEquations.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/TextureSwizzleEquations.hpp @@ -84,6 +84,11 @@ inline constexpr TextureSwizzleEquation kTextureSwizzleEquations[] = { {0x109u, 4u, {0x00000000u, 0x00000000u, 0x00000001u, 0x00001000u, 0x01000000u, 0x00002000u, 0x00000002u, 0x02000000u, 0x00004000u, 0x00000004u, 0x04000000u, 0x00008000u, 0x00000008u, 0x08000000u, 0x00010000u, 0x00000010u}}, {0x109u, 8u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000001u, 0x01000000u, 0x00001000u, 0x00000002u, 0x02000000u, 0x00002000u, 0x00000004u, 0x04000000u, 0x00004000u, 0x00000008u, 0x08000000u, 0x00008000u, 0x00000010u}}, {0x109u, 16u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x01000000u, 0x00001000u, 0x00000001u, 0x02000000u, 0x00002000u, 0x00000002u, 0x04000000u, 0x00004000u, 0x00000004u, 0x08000000u, 0x00008000u, 0x00000008u}}, + {0x115u, 1u, {0x00000001u, 0x00000002u, 0x01000000u, 0x00001000u, 0x02000000u, 0x00002000u, 0x00000004u, 0x04000000u, 0x40004040u, 0x20020008u, 0x08010020u, 0x10008010u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}}, + {0x115u, 2u, {0x00000000u, 0x00000001u, 0x01000000u, 0x00001000u, 0x02000000u, 0x00002000u, 0x00000002u, 0x04000000u, 0x40004020u, 0x20020004u, 0x08010010u, 0x10008008u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}}, + {0x115u, 4u, {0x00000000u, 0x00000000u, 0x00000001u, 0x00001000u, 0x01000000u, 0x00002000u, 0x00000002u, 0x02000000u, 0x20004020u, 0x10020004u, 0x04010010u, 0x08008008u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}}, + {0x115u, 8u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000001u, 0x01000000u, 0x00001000u, 0x00000002u, 0x02000000u, 0x20002020u, 0x10010004u, 0x04008010u, 0x08004008u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}}, + {0x115u, 16u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x01000000u, 0x00001000u, 0x00000001u, 0x02000000u, 0x20002010u, 0x10010002u, 0x04008008u, 0x08004004u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}}, {0x119u, 1u, {0x00000001u, 0x00000002u, 0x01000000u, 0x00001000u, 0x02000000u, 0x00002000u, 0x00000004u, 0x04000000u, 0x40004040u, 0x20020008u, 0x08010020u, 0x10008010u, 0x00000010u, 0x10000000u, 0x00010000u, 0x00000020u}}, {0x119u, 2u, {0x00000000u, 0x00000001u, 0x01000000u, 0x00001000u, 0x02000000u, 0x00002000u, 0x00000002u, 0x04000000u, 0x40004020u, 0x20020004u, 0x08010010u, 0x10008008u, 0x00000008u, 0x10000000u, 0x00010000u, 0x00000010u}}, {0x119u, 4u, {0x00000000u, 0x00000000u, 0x00000001u, 0x00001000u, 0x01000000u, 0x00002000u, 0x00000002u, 0x02000000u, 0x20004020u, 0x10020004u, 0x04010010u, 0x08008008u, 0x00000008u, 0x08000000u, 0x00010000u, 0x00000010u}}, diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/TextureDetiler.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/TextureDetiler.cpp index 9397681655..62bd3365ef 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/TextureDetiler.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/TextureDetiler.cpp @@ -144,8 +144,8 @@ VkPipeline TextureDetiler::pipeline(TextureTileMode tileMode, std::uint32_t elem // per-bit XOR equation for the equation family (2); 20-21 override the block extent in elements. std::array values{elementBytes, BlockBytesFor(tileMode), tileMode == TextureTileMode::kLinear ? 0u : 1u, retile ? 1u : 0u}; if (thick) { - const auto thickMode = tileMode == TextureTileMode::kStandard4KB ? 0x105u : tileMode == TextureTileMode::kStandard64KB ? 0x109u : tileMode == TextureTileMode::kS64KBX ? 0x119u : tileMode == TextureTileMode::kD64KBX ? 0x11au : 0u; - Require(thickMode != 0, "3D textures are only detiled thick from SW_4KB_S, SW_64KB_S, SW_64KB_S_X or SW_64KB_D_X"); + const auto thickMode = tileMode == TextureTileMode::kStandard4KB ? 0x105u : tileMode == TextureTileMode::kS4KBX ? 0x115u : tileMode == TextureTileMode::kStandard64KB ? 0x109u : tileMode == TextureTileMode::kS64KBX ? 0x119u : tileMode == TextureTileMode::kD64KBX ? 0x11au : 0u; + Require(thickMode != 0, "3D textures are only detiled thick from SW_4KB_S, SW_4KB_S_X, SW_64KB_S, SW_64KB_S_X or SW_64KB_D_X"); const auto* equation = FindTextureSwizzleEquation(thickMode, elementBytes); Require(equation != nullptr, "no thick swizzle equation for the element size"); values[2] = 2u; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp index 6eca2ae659..a2c51f1e0c 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp @@ -262,11 +262,12 @@ std::array ThickBlockExtent(TextureTileMode tileMode, std::uin Require(std::has_single_bit(bytesPerElement) && bytesPerElement <= 16u, "unsupported bytes per element for thick texture geometry"); const auto index = static_cast(std::countr_zero(bytesPerElement)); switch (tileMode) { - case TextureTileMode::kStandard4KB: return {1u << thick4KB[index][0], 1u << thick4KB[index][1], 1u << thick4KB[index][2]}; + case TextureTileMode::kStandard4KB: + case TextureTileMode::kS4KBX: return {1u << thick4KB[index][0], 1u << thick4KB[index][1], 1u << thick4KB[index][2]}; case TextureTileMode::kStandard64KB: case TextureTileMode::kS64KBX: case TextureTileMode::kD64KBX: return {1u << thick64KB[index][0], 1u << thick64KB[index][1], 1u << thick64KB[index][2]}; - default: throw std::runtime_error("AGC graphics: thick 3D textures are only supported in SW_4KB_S, SW_64KB_S, SW_64KB_S_X and SW_64KB_D_X, not in swizzle mode " + std::to_string(EquationSwizzleMode(tileMode)) + " at " + std::to_string(bytesPerElement) + " bytes per element"); + default: throw std::runtime_error("AGC graphics: thick 3D textures are only supported in SW_4KB_S, SW_4KB_S_X, SW_64KB_S, SW_64KB_S_X and SW_64KB_D_X, not in swizzle mode " + std::to_string(EquationSwizzleMode(tileMode)) + " at " + std::to_string(bytesPerElement) + " bytes per element"); } } @@ -316,7 +317,7 @@ ThickLayout ComputeThickLayout(TextureTileMode tileMode, std::uint32_t format, s auto firstTailLevel = mipCount; if (tileMode != TextureTileMode::kLinear) { block = ThickBlockExtent(tileMode, bytesPerElement); - blockBytes = tileMode == TextureTileMode::kStandard4KB ? 4096u : 65536u; + blockBytes = tileMode == TextureTileMode::kStandard4KB || tileMode == TextureTileMode::kS4KBX ? 4096u : 65536u; const auto log2BlockBytes = static_cast(std::countr_zero(blockBytes)); const auto tailWidth = log2BlockBytes % 3u == 1u ? block[0] >> 1u : block[0]; const auto tailHeight = log2BlockBytes % 3u == 0u ? block[1] >> 1u : block[1]; diff --git a/core/libs/prx/libSceAgcDriver/tests/TextureTiling.cpp b/core/libs/prx/libSceAgcDriver/tests/TextureTiling.cpp index c72b4f3b02..6c6acca9d2 100644 --- a/core/libs/prx/libSceAgcDriver/tests/TextureTiling.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/TextureTiling.cpp @@ -32,6 +32,7 @@ struct ElementAddress { std::uint32_t y; std::uint32_t z; std::uint64_t address; + std::uint32_t pipeBankXor = 0; }; std::uint64_t equationOffset(const TextureSwizzleEquation& equation, std::uint32_t x, std::uint32_t y, std::uint32_t z) { @@ -60,7 +61,7 @@ void requireThickAddresses(const GuestTextureResource& resource, std::uint32_t b const auto geometry = DescribeSurface(resource); Require(geometry.thick && geometry.mips.size() == resource.mipCount && geometry.guestBytes == guestBytes, what + ": guest size differs from addrlib"); for (std::uint32_t level = 0; level < resource.mipCount; ++level) Require(geometry.mips[level].tail == (level >= firstTailLevel), what + ": mip " + std::to_string(level) + " is on the wrong side of the mip tail"); - const auto blockBytes = resource.tileMode == TextureTileMode::kStandard4KB ? 4096u : 65536u; + const auto blockBytes = resource.tileMode == TextureTileMode::kStandard4KB || resource.tileMode == TextureTileMode::kS4KBX ? 4096u : 65536u; const auto* equation = FindTextureSwizzleEquation(0x100u | (resource.tileMode == TextureTileMode::kStandard4KB ? 5u : resource.tileMode == TextureTileMode::kStandard64KB ? 9u : XorSwizzleMode(resource.tileMode)), bytesPerElement); Require(equation != nullptr, what + ": missing thick swizzle equation"); const auto block = ThickBlockExtent(resource.tileMode, bytesPerElement); @@ -73,6 +74,7 @@ void requireThickAddresses(const GuestTextureResource& resource, std::uint32_t b } else { address += (static_cast(element.y / block[1]) * mip.blocksPerRow + element.x / block[0]) * blockBytes + equationOffset(*equation, element.x, element.y, element.z); } + address ^= element.pipeBankXor; Require(address == element.address, what + ": mip " + std::to_string(element.mip) + " element (" + std::to_string(element.x) + ", " + std::to_string(element.y) + ", " + std::to_string(element.z) + ") detiles from " + std::to_string(address) + " instead of addrlib's " + std::to_string(element.address)); } } @@ -258,6 +260,22 @@ void RunTextureTilingTests() { Require(odd.mips[0].blocksPerRow == 5 && odd.mips[0].tiledOffset == 12288 && odd.mips[0].tiledSize == 40960, "3D mip 0 of a non-power-of-two volume changed"); } + { + constexpr ElementAddress s4xTail1[] = {{0, 0, 0, 0, 0x3000, 0x0}, {0, 32, 19, 11, 0x843c, 0x0}, {0, 8, 9, 8, 0x3e08, 0x0}, {0, 15, 2, 8, 0x3163, 0x700}, {1, 0, 0, 0, 0x1000, 0x0}, {1, 15, 9, 5, 0x1acf, 0x0}, {1, 12, 4, 0, 0x1340, 0x0}, {1, 7, 2, 5, 0x17e7, 0x700}, {2, 0, 0, 0, 0x800, 0x0}, {2, 7, 4, 2, 0x953, 0x0}, {2, 2, 2, 0, 0x822, 0x0}, {2, 0, 1, 1, 0xf0c, 0x700}, {3, 0, 0, 0, 0x300, 0x0}, {3, 3, 1, 0, 0x30b, 0x0}, {3, 3, 1, 0, 0x30b, 0x0}, {3, 0, 0, 0, 0x400, 0x700}, {4, 0, 0, 0, 0x200, 0x0}, {4, 1, 0, 0, 0x201, 0x0}, {4, 0, 0, 0, 0x200, 0x0}, {4, 0, 0, 0, 0x500, 0x700}, {5, 0, 0, 0, 0x100, 0x0}, {5, 0, 0, 0, 0x100, 0x0}, {5, 0, 0, 0, 0x100, 0x0}, {5, 0, 0, 0, 0x600, 0x700}}; + requireThickAddresses(thickVolume(TextureTileMode::kS4KBX, 1, 33, 20, 12, 6), 1, 2, 36864, s4xTail1, "SW_4KB_S_X 8 bpp 33x20x12, 6 levels"); + constexpr ElementAddress s4xTail2[] = {{0, 0, 0, 0, 0x6000, 0x0}, {0, 32, 19, 11, 0xf13c, 0x0}, {0, 8, 9, 8, 0x7408, 0x0}, {0, 15, 2, 8, 0x7962, 0x700}, {1, 0, 0, 0, 0x3000, 0x0}, {1, 15, 9, 5, 0x42ce, 0x0}, {1, 12, 4, 0, 0x4b00, 0x0}, {1, 7, 2, 5, 0x35e6, 0x700}, {2, 0, 0, 0, 0x1000, 0x0}, {2, 7, 4, 2, 0x1352, 0x0}, {2, 2, 2, 0, 0x1060, 0x0}, {2, 0, 1, 1, 0x170c, 0x700}, {3, 0, 0, 0, 0x800, 0x0}, {3, 3, 1, 0, 0x84a, 0x0}, {3, 3, 1, 0, 0x84a, 0x0}, {3, 0, 0, 0, 0xf00, 0x700}, {4, 0, 0, 0, 0x300, 0x0}, {4, 1, 0, 0, 0x302, 0x0}, {4, 0, 0, 0, 0x300, 0x0}, {4, 0, 0, 0, 0x400, 0x700}, {5, 0, 0, 0, 0x200, 0x0}, {5, 0, 0, 0, 0x200, 0x0}, {5, 0, 0, 0, 0x200, 0x0}, {5, 0, 0, 0, 0x500, 0x700}}; + requireThickAddresses(thickVolume(TextureTileMode::kS4KBX, 12, 33, 20, 12, 6), 2, 3, 65536, s4xTail2, "SW_4KB_S_X 16 bpp 33x20x12, 6 levels"); + constexpr ElementAddress s4xWide4[] = {{0, 0, 0, 0, 0x20000, 0x0}, {0, 127, 127, 7, 0x9f4fc, 0x0}, {0, 24, 93, 0, 0x73108, 0x0}, {0, 42, 22, 4, 0x35f60, 0x700}, {1, 0, 0, 0, 0x0, 0x0}, {1, 63, 63, 3, 0x1f0fc, 0x0}, {1, 28, 38, 2, 0x13da0, 0x0}, {1, 55, 52, 1, 0x1e754, 0x700}}; + requireThickAddresses(thickVolume(TextureTileMode::kS4KBX, 56, 128, 128, 8, 2), 4, 2, 655360, s4xWide4, "SW_4KB_S_X 32 bpp 128x128x8, 2 levels"); + constexpr ElementAddress s4xTail4[] = {{0, 0, 0, 0, 0x6000, 0x0}, {0, 32, 19, 11, 0x1fdb8, 0x0}, {0, 8, 9, 8, 0x17808, 0x0}, {0, 15, 2, 8, 0x17564, 0x700}, {1, 0, 0, 0, 0x3000, 0x0}, {1, 15, 9, 5, 0x465c, 0x0}, {1, 12, 4, 0, 0x4b00, 0x0}, {1, 7, 2, 5, 0x3174, 0x700}, {2, 0, 0, 0, 0x1000, 0x0}, {2, 7, 4, 2, 0x13c4, 0x0}, {2, 2, 2, 0, 0x1060, 0x0}, {2, 0, 1, 1, 0x1718, 0x700}, {3, 0, 0, 0, 0x800, 0x0}, {3, 3, 1, 0, 0x84c, 0x0}, {3, 3, 1, 0, 0x84c, 0x0}, {3, 0, 0, 0, 0xf00, 0x700}, {4, 0, 0, 0, 0x300, 0x0}, {4, 1, 0, 0, 0x304, 0x0}, {4, 0, 0, 0, 0x300, 0x0}, {4, 0, 0, 0, 0x400, 0x700}, {5, 0, 0, 0, 0x200, 0x0}, {5, 0, 0, 0, 0x200, 0x0}, {5, 0, 0, 0, 0x200, 0x0}, {5, 0, 0, 0, 0x500, 0x700}}; + requireThickAddresses(thickVolume(TextureTileMode::kS4KBX, 56, 33, 20, 12, 6), 4, 3, 131072, s4xTail4, "SW_4KB_S_X 32 bpp 33x20x12, 6 levels"); + constexpr ElementAddress s4xTail8[] = {{0, 0, 0, 0, 0x9000, 0x0}, {0, 32, 19, 11, 0x2fab0, 0x0}, {0, 8, 9, 8, 0x27420, 0x0}, {0, 15, 2, 8, 0x22448, 0x700}, {1, 0, 0, 0, 0x3000, 0x0}, {1, 15, 9, 5, 0x7a78, 0x0}, {1, 12, 4, 0, 0x4200, 0x0}, {1, 7, 2, 5, 0x3058, 0x700}, {2, 0, 0, 0, 0x1000, 0x0}, {2, 7, 4, 2, 0x1ac8, 0x0}, {2, 2, 2, 0, 0x1140, 0x0}, {2, 0, 1, 1, 0x1730, 0x700}, {3, 0, 0, 0, 0x800, 0x0}, {3, 3, 1, 0, 0x868, 0x0}, {3, 3, 1, 0, 0x868, 0x0}, {3, 0, 0, 0, 0xf00, 0x700}, {4, 0, 0, 0, 0x300, 0x0}, {4, 1, 0, 0, 0x308, 0x0}, {4, 0, 0, 0, 0x300, 0x0}, {4, 0, 0, 0, 0x400, 0x700}, {5, 0, 0, 0, 0x200, 0x0}, {5, 0, 0, 0, 0x200, 0x0}, {5, 0, 0, 0, 0x200, 0x0}, {5, 0, 0, 0, 0x500, 0x700}}; + requireThickAddresses(thickVolume(TextureTileMode::kS4KBX, 71, 33, 20, 12, 6), 8, 3, 196608, s4xTail8, "SW_4KB_S_X 64 bpp 33x20x12, 6 levels"); + constexpr ElementAddress s4xTail16[] = {{0, 0, 0, 0, 0x10000, 0x0}, {0, 32, 19, 11, 0x55bb0, 0x0}, {0, 8, 9, 8, 0x46820, 0x0}, {0, 15, 2, 8, 0x3e040, 0x700}, {1, 0, 0, 0, 0x6000, 0x0}, {1, 15, 9, 5, 0xee70, 0x0}, {1, 12, 4, 0, 0x9400, 0x0}, {1, 7, 2, 5, 0x7850, 0x700}, {2, 0, 0, 0, 0x3000, 0x0}, {2, 7, 4, 2, 0x42c0, 0x0}, {2, 2, 2, 0, 0x3300, 0x0}, {2, 0, 1, 1, 0x3730, 0x700}, {3, 0, 0, 0, 0x1000, 0x0}, {3, 3, 1, 0, 0x1260, 0x0}, {3, 3, 1, 0, 0x1260, 0x0}, {3, 0, 0, 0, 0x1700, 0x700}, {4, 0, 0, 0, 0x800, 0x0}, {4, 1, 0, 0, 0x840, 0x0}, {4, 0, 0, 0, 0x800, 0x0}, {4, 0, 0, 0, 0xf00, 0x700}, {5, 0, 0, 0, 0x300, 0x0}, {5, 0, 0, 0, 0x300, 0x0}, {5, 0, 0, 0, 0x300, 0x0}, {5, 0, 0, 0, 0x400, 0x700}}; + requireThickAddresses(thickVolume(TextureTileMode::kS4KBX, 77, 33, 20, 12, 6), 16, 4, 352256, s4xTail16, "SW_4KB_S_X 128 bpp 33x20x12, 6 levels"); + requireBlockVolumeLayout(TextureTileMode::kS4KBX, 169, 71, 60, 36, 20, 5, "SW_4KB_S_X BC1 60x36x20, 5 levels"); + } + { constexpr ElementAddress chainInTail[] = {{0, 0, 0, 0, 0x800}, {0, 7, 7, 7, 0xffc}, {0, 4, 2, 4, 0xe20}, {0, 2, 7, 0, 0x968}, {0, 7, 0, 7, 0xed4}, {1, 0, 0, 0, 0x300}, {1, 3, 3, 3, 0x3fc}, {1, 2, 1, 2, 0x3c8}, {1, 1, 3, 0, 0x32c}, {1, 3, 0, 3, 0x3d4}}; requireThickAddresses(thickVolume(TextureTileMode::kStandard4KB, 56, 8, 8, 8, 2), 4, 0, 4096, chainInTail, "SW_4KB_S 32 bpp 8x8x8, 2 levels"); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageXorSwizzleVolume.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageXorSwizzleVolume.cpp index 52c5728108..4c3e66ac8f 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ImageXorSwizzleVolume.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageXorSwizzleVolume.cpp @@ -27,6 +27,7 @@ constexpr std::uint32_t Height = 24; constexpr std::uint32_t Depth = 20; constexpr std::uint32_t Format8888UNorm = 56; constexpr std::uint32_t Type3D = 10; +constexpr std::uint32_t TileS4KBX = 0x15; constexpr std::uint32_t TileZ64KBX = 0x18; constexpr std::uint32_t TileS64KBX = 0x19; constexpr std::uint32_t TileD64KBX = 0x1a; @@ -79,6 +80,7 @@ void FillTexels(std::uint32_t tileMode) { Require(geometry.guestBytes <= Texels.size(), "volume does not fit the test allocation"); const auto* equation = FindTextureSwizzleEquation(geometry.thick ? 0x100u | XorSwizzleMode(descriptor.tileMode) : XorSwizzleMode(descriptor.tileMode), 4u); Require(equation != nullptr, "volume swizzle equation is missing"); + const auto blockBytes = ThinBlockLayout(descriptor.tileMode, 4u)[0]; const auto& mip = geometry.mips.at(0); const auto block = geometry.thick ? ThickBlockExtent(descriptor.tileMode, 4u) : std::array{ThinBlockLayout(descriptor.tileMode, 4u)[1], ThinBlockLayout(descriptor.tileMode, 4u)[2], 1u}; for (std::uint32_t z = 0; z < Depth; ++z) { @@ -86,7 +88,7 @@ void FillTexels(std::uint32_t tileMode) { for (std::uint32_t x = 0; x < Width; ++x) { auto offset = geometry.GuestLayerOffset(z) + mip.tiledOffset; if (mip.tail) offset += EquationOffset(*equation, x + mip.tailX, y + mip.tailY, z); - else offset += (static_cast(y / block[1]) * mip.blocksPerRow + x / block[0]) * 65536u + EquationOffset(*equation, x, y, z); + else offset += (static_cast(y / block[1]) * mip.blocksPerRow + x / block[0]) * blockBytes + EquationOffset(*equation, x, y, z); const auto texel = Texel(x, y, z); std::copy(texel.begin(), texel.end(), Texels.begin() + static_cast(offset)); } @@ -145,6 +147,7 @@ int main() { const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; FillInput(); + Run(*device, TileS4KBX, "thick SW_4KB_S_X volume"); Run(*device, TileS64KBX, "thick SW_64KB_S_X volume"); Run(*device, TileD64KBX, "thick SW_64KB_D_X volume"); Run(*device, TileZ64KBX, "thin SW_64KB_Z_X volume"); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/LayeredVolumeDraw.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/LayeredVolumeDraw.cpp index 20ab53b94c..5581d46dcc 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/LayeredVolumeDraw.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/LayeredVolumeDraw.cpp @@ -97,9 +97,9 @@ void BuildBands() { } } -AgcDriver::Graphics::ColorTarget VolumeTarget(std::byte* volume) { +AgcDriver::Graphics::ColorTarget VolumeTarget(std::byte* volume, ColorTileMode tileMode = ColorTileMode::RenderTarget) { AgcDriver::Graphics::ColorTarget color{reinterpret_cast(volume), {Width, Height}, VK_FORMAT_R8G8B8A8_UNORM, 0, 0xe4u}; - color.tileMode = ColorTileMode::RenderTarget; + color.tileMode = tileMode; color.depth = Slices; color.depthSlice = FirstSlice; color.layers = Layers; @@ -239,19 +239,23 @@ std::uint64_t BlockOffset(const AgcDriver::Graphics::TextureSwizzleEquation& equ } void Check(const std::byte* volume, const AgcDriver::Graphics::ColorTarget& color, const std::string& what, const std::array& bands) { + using namespace AgcDriver::Graphics; const auto geometry = AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::SurfaceForTarget(color)); - Require(!geometry.thick, "a 64 KiB render-target volume is not stored as thin slices"); - const auto block = AgcDriver::Graphics::ThinBlockLayout(AgcDriver::Graphics::TextureTileMode::kR64KBX, 4); - Require(Width <= block[1] && Height <= block[2], "the volume slices span several tile blocks"); - const auto* equation = AgcDriver::Graphics::FindTextureSwizzleEquation(static_cast(ColorTileMode::RenderTarget), 4); - Require(equation != nullptr, "missing the 64 KiB R_X swizzle equation"); + Require(geometry.thick == (color.tileMode == ColorTileMode::S4KBX), "unexpected volume addressing family"); + const auto tileMode = ColorTextureTileMode(color.tileMode); + const auto thin = ThinBlockLayout(tileMode, 4); + const auto block = geometry.thick ? ThickBlockExtent(tileMode, 4) : std::array{thin[1], thin[2], 1u}; + const auto& mip = geometry.mips.at(0); + const auto* equation = FindTextureSwizzleEquation((geometry.thick ? 0x100u : 0u) | static_cast(color.tileMode), 4); + Require(equation != nullptr, "missing the volume's swizzle equation"); for (std::uint32_t slice = 0; slice < Slices; ++slice) { const auto* base = volume + geometry.GuestLayerOffset(slice); for (std::uint32_t y = 0; y < Height; ++y) { for (std::uint32_t x = 0; x < Width; ++x) { const bool drawn = slice >= FirstSlice && x / BandWidth == slice - FirstSlice; const auto expected = drawn ? bands[slice - FirstSlice] : Kept; - const auto* texel = base + BlockOffset(*equation, x, y, slice); + const auto offset = (static_cast(y / block[1]) * mip.blocksPerRow + x / block[0]) * thin[0] + BlockOffset(*equation, x, y, slice); + const auto* texel = base + offset; for (std::uint32_t channel = 0; channel < 4; ++channel) { const auto actual = std::to_integer(texel[channel]); Require(actual == expected || (drawn && expected == 128 && actual == 127), what + ": slice " + std::to_string(slice) + " texel (" + std::to_string(x) + ", " + std::to_string(y) + ") channel " + std::to_string(channel) + " is " + std::to_string(actual) + ", expected " + std::to_string(expected) + (drawn ? " (the band of layer " + std::to_string(slice - FirstSlice) + ")" : slice < FirstSlice ? " (outside the color view)" : " (another layer's band)")); @@ -283,6 +287,10 @@ int main(int argc, char** argv) { const auto fieldColor = VolumeTarget(fieldVolume.Data()); Run(*device, fieldColor, false, LayerFieldPixelCode, true); Check(fieldVolume.Data(), fieldColor, "a pixel shader reading the render target index of its packed ancillary word", LayerFieldBands); + const GuestVolume thickVolume((VolumeTarget(nullptr, ColorTileMode::S4KBX).bytes + 65535u) & ~std::size_t{65535u}); + const auto thickColor = VolumeTarget(thickVolume.Data(), ColorTileMode::S4KBX); + Run(*device, thickColor, false, PixelCode, false); + Check(thickVolume.Data(), thickColor, "thick SW_4KB_S_X vertex draw and writeback", WhiteBands); if (vertexOnly) std::puts("layered volume vertex draw tests passed"); else if (target.mesh.has_value()) Run(*device, color, true, PixelCode, false); else std::puts("mesh case skipped, the device has no VK_EXT_mesh_shader"); diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index e5ade18beb..67550c6211 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -172,3 +172,4 @@ - [Registration preparation at submit](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp) (libSceAgcDriver) - `Driver::Submit` waits for every registration-time shader preparation queued before the call, not only those of the shaders the commands use, so a submission behind a long queue of unrelated preparations waits for all of them on the submitting thread. Waiting there instead of in the queue worker keeps the GPU side of a submission short: a title that sees a submission take seconds treats the GPU as hung (Goat Simulator 3, PPSA04159, then stops submitting and its garbage collector fails a 10 s timeout). On the console shaders are compiled ahead of time, so neither wait exists; how long a title tolerates a slow submit call is not known. - [Unclamped normalized color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - CB_COLOR_INFO targets with BLEND_CLAMP clear (bit 15) on UNORM and SNORM formats are accepted when blending is disabled (ENABLE clear or BLEND_BYPASS set). Vulkan converts unblended shader values and clamps to the normalized range automatically; blending into an unclamped normalized target remains unmodeled because Vulkan always clamps inputs prior to blending. +- [Thick SW_4KB_S_X volumes](../../../core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp) use AMD addrlib's GFX10 non-RB+ 16-pipe `GFX10_SW_4K_S3_X_PATINFO` equations for 1, 2, 4, 8 and 16-byte elements, including packed mip tails. The layout follows [AMD PAL c5e80007](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/gfx10/gfx10addrlib.cpp); it has not been measured on console hardware. Other thick swizzle modes outside the supported families still throw. Mipmapped 3D color targets remain refused by the color-register decoder. From 12813101d26fdc9160015bb324fb6bcdada6e718 Mon Sep 17 00:00:00 2001 From: hugodearaujo Date: Sat, 17 Oct 2026 18:50:31 -0300 Subject: [PATCH 0885/1000] fix(ampr): return EBUSY when a command does not fit and EPERM before a buffer is bound --- core/libs/prx/libSceAmpr/Export.cpp | 10 +++++---- core/libs/tests/GuestAmpr.cpp | 35 +++++++++++++++++++++-------- docs/dev/debt/libSceAmpr.md | 2 +- 3 files changed, 33 insertions(+), 14 deletions(-) diff --git a/core/libs/prx/libSceAmpr/Export.cpp b/core/libs/prx/libSceAmpr/Export.cpp index 88f264c00a..fd0d7cf527 100644 --- a/core/libs/prx/libSceAmpr/Export.cpp +++ b/core/libs/prx/libSceAmpr/Export.cpp @@ -15,7 +15,6 @@ #include #include -static constexpr int SCE_AMPR_ERROR_BUFFER_FULL = 0x8002001C; static constexpr int SCE_KERNEL_ERROR_EINVAL = 0x80020016; static constexpr int SCE_KERNEL_ERROR_EPERM = 0x80020001; static constexpr int SCE_KERNEL_ERROR_ESRCH = 0x80020003; @@ -27,7 +26,8 @@ static std::uint32_t RemainingCommandBytes(const Apr::CommandBufferObject& buffe } static int Append(Apr::CommandBufferObject* buffer, const void* command, uint32_t bytes) { - if (!buffer->base || bytes > RemainingCommandBytes(*buffer)) return SCE_AMPR_ERROR_BUFFER_FULL; + if (!buffer->base) return SCE_KERNEL_ERROR_EPERM; + if (bytes > RemainingCommandBytes(*buffer)) return SCE_KERNEL_ERROR_EBUSY; std::memcpy(buffer->base + buffer->offset, command, bytes); buffer->offset += bytes; ++buffer->numCommands; @@ -93,7 +93,8 @@ static std::uint64_t NopBytes(std::uint32_t dwords) { static int AppendNop(Apr::CommandBufferObject* buffer, std::uint32_t dwords, const std::uint32_t* data) { if (!buffer) return SCE_KERNEL_ERROR_EINVAL; const std::uint64_t bytes = NopBytes(dwords); - if (!buffer->base || bytes > RemainingCommandBytes(*buffer)) return SCE_AMPR_ERROR_BUFFER_FULL; + if (!buffer->base) return SCE_KERNEL_ERROR_EPERM; + if (bytes > RemainingCommandBytes(*buffer)) return SCE_KERNEL_ERROR_EBUSY; const Apr::CommandHeader header{Apr::Opcode::Nop, static_cast(bytes)}; std::uint8_t* destination = buffer->base + buffer->offset; std::memset(destination, 0, bytes); @@ -116,7 +117,8 @@ static std::uint64_t MeasureMarker(const char* text) { static int AppendMarker(Apr::CommandBufferObject* buffer, Apr::Opcode opcode, const char* text) { if (!buffer || !text) return SCE_KERNEL_ERROR_EINVAL; const std::uint64_t bytes = MarkerBytes(text); - if (!buffer->base || bytes > RemainingCommandBytes(*buffer)) return SCE_AMPR_ERROR_BUFFER_FULL; + if (!buffer->base) return SCE_KERNEL_ERROR_EPERM; + if (bytes > RemainingCommandBytes(*buffer)) return SCE_KERNEL_ERROR_EBUSY; const Apr::MarkerCommand command{{opcode, static_cast(bytes)}}; std::uint8_t* destination = buffer->base + buffer->offset; std::memset(destination, 0, bytes); diff --git a/core/libs/tests/GuestAmpr.cpp b/core/libs/tests/GuestAmpr.cpp index e7e80e9e27..2340688d38 100644 --- a/core/libs/tests/GuestAmpr.cpp +++ b/core/libs/tests/GuestAmpr.cpp @@ -73,6 +73,7 @@ int APS5_VABI sceKernelAprResolveFilepathsToIdsAndFileSizes(const char**, std::u int APS5_VABI sceKernelAprResolveFilepathsToIdsAndFileSizesForEach(const char**, std::uint32_t, std::uint32_t*, std::uint64_t*, int*); int* APS5_VABI __error_nid_postfix(); int APS5_VABI sceAmprAprCommandBufferReadFile(Apr::CommandBufferObject*, std::uint64_t*, std::uint64_t*, std::uint32_t, void*, std::uint64_t, std::uint64_t); +std::uint32_t APS5_VABI sceAmprMeasureCommandSizeReadFile(); int APS5_VABI sceAmprAprCommandBufferReadFileGather(Apr::CommandBufferObject*, std::uint64_t*, std::uint64_t*, std::uint64_t, std::uint64_t); int APS5_VABI sceAmprAprCommandBufferReadFileScatter(Apr::CommandBufferObject*, std::uint64_t*, std::uint64_t*, void*, std::uint64_t); int APS5_VABI sceAmprAprCommandBufferReadFileGatherScatter(Apr::CommandBufferObject*, std::uint64_t*, std::uint64_t*, void*, std::uint64_t, std::uint64_t); @@ -156,7 +157,8 @@ static void RequireAt(bool value, int line) { namespace { constexpr int invalidArgument = static_cast(0x80020016); -constexpr int bufferFull = static_cast(0x8002001C); +constexpr int permissionDenied = static_cast(0x80020001); +constexpr int busy = static_cast(0x80020010); constexpr std::uint32_t color = 0xFF8040u; struct Recorder { @@ -249,19 +251,36 @@ void TestFullBuffer() { Recorder exact(measured); Require(sceAmprCommandBufferPushMarker(&exact.buffer, marker) == 0); Require(exact.Offset() == measured && exact.Commands() == 1); - Require(sceAmprCommandBufferPopMarker(&exact.buffer) == bufferFull); - Require(sceAmprCommandBufferSetMarker(&exact.buffer, "") == bufferFull); + Require(sceAmprCommandBufferPopMarker(&exact.buffer) == busy); + Require(sceAmprCommandBufferSetMarker(&exact.buffer, "") == busy); Require(exact.Offset() == measured && exact.Commands() == 1); Recorder small(measured - 4); - Require(sceAmprCommandBufferPushMarker(&small.buffer, marker) == bufferFull); - Require(sceAmprCommandBufferSetMarkerWithColor(&small.buffer, marker, &color) == bufferFull); + Require(sceAmprCommandBufferPushMarker(&small.buffer, marker) == busy); + Require(sceAmprCommandBufferSetMarkerWithColor(&small.buffer, marker, &color) == busy); Require(small.Offset() == 0 && small.Commands() == 0); + std::array destination{}; + volatile std::uint64_t done = 0; + const auto read = sceAmprMeasureCommandSizeReadFile(); + Recorder reads(read); + Require(sceAmprAprCommandBufferReadFile(&reads.buffer, &reads.gatherState, &reads.scatterState, 1, destination.data(), destination.size(), 0) == 0); + Require(sceAmprAprCommandBufferReadFile(&reads.buffer, &reads.gatherState, &reads.scatterState, 1, destination.data(), destination.size(), 0) == busy); + Require(sceAmprCommandBufferNop(&reads.buffer, 1) == busy); + Require(sceAmprCommandBufferWriteAddressOnCompletion(&reads.buffer, &done, 1) == busy); + Require(reads.Offset() == read && reads.Commands() == 1); + Apr::CommandBufferObject unbound{}; + std::uint64_t gatherState = 0; + std::uint64_t scatterState = 0; Require(sceAmprCommandBufferConstructor(&unbound) == 0); - Require(sceAmprCommandBufferPushMarker(&unbound, marker) == bufferFull); - Require(sceAmprCommandBufferPopMarker(&unbound) == bufferFull); + Require(sceAmprAprCommandBufferConstructor(&unbound, &gatherState, &scatterState) == 0); + Require(sceAmprCommandBufferPushMarker(&unbound, marker) == permissionDenied); + Require(sceAmprCommandBufferPopMarker(&unbound) == permissionDenied); + Require(sceAmprAprCommandBufferReadFile(&unbound, &gatherState, &scatterState, 1, destination.data(), destination.size(), 0) == permissionDenied); + Require(sceAmprCommandBufferNop(&unbound, 1) == permissionDenied); + Require(sceAmprCommandBufferWriteAddressOnCompletion(&unbound, &done, 1) == permissionDenied); + Require(unbound.offset == 0 && unbound.numCommands == 0); } void TestOffsetPastBuffer() { @@ -878,8 +897,6 @@ void TestGatherScatter() { } constexpr std::uint64_t page = 0x4000; -constexpr int permissionDenied = static_cast(0x80020001); -constexpr int busy = static_cast(0x80020010); constexpr int noSuchSubmission = static_cast(0x80020003); constexpr std::int32_t cpuReadWrite = 0x03; constexpr std::int32_t cpuGpuReadWrite = 0x33; diff --git a/docs/dev/debt/libSceAmpr.md b/docs/dev/debt/libSceAmpr.md index 68774cc30e..a9c31f321e 100644 --- a/docs/dev/debt/libSceAmpr.md +++ b/docs/dev/debt/libSceAmpr.md @@ -4,7 +4,7 @@ - [AMPR WriteAddressFromCounterPairOnCompletion](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - the pair of an even counter n is assumed to give counter n in the low 32 bits and n + 1 in the high 32 bits. Wait compare values, wait flush and counter index ranges follow the [ampr_emu](https://github.com/drakmor/ampr_emu) reimplementation; the flush is accepted and has no effect, as commands run in order - [AMPR command buffer offsets](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - recording with a current offset greater than the bound memory size throws before calculating remaining capacity or writing a command. This validates the emulator's own command-buffer encoding; the console's response to a corrupted opaque buffer object is unknown. -- [AMPR command sizes](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - commands use the emulator's own encoding with an 8-byte header, so `Nop(n)` takes 8 + 4n bytes where the console record is n dwords including its header (`NopWithData(n)`: (n + 1) dwords). The measure functions agree with the appended sizes, but a title that pads to an exact offset or the end of the buffer with `Nop(remaining / 4)` gets `SCE_AMPR_ERROR_BUFFER_FULL` or a shifted offset. The at-start flag of the `_04_00` writes is ignored; commands run in order, each after the previous one completes +- [AMPR command sizes](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - commands use the emulator's own encoding with an 8-byte header, so `Nop(n)` takes 8 + 4n bytes where the console record is n dwords including its header (`NopWithData(n)`: (n + 1) dwords). The measure functions agree with the appended sizes, but a title that pads to an exact offset or the end of the buffer with `Nop(remaining / 4)` gets `SCE_KERNEL_ERROR_EBUSY` or a shifted offset. The at-start flag of the `_04_00` writes is ignored; commands run in order, each after the previous one completes - [APR map begin and end](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - as in ampr_emu, `MapBegin`/`MapDirectBegin` map when they run and `MapEnd` only closes the region; while a region is open a second begin and every completion write return `EPERM`, the at-start `_04_00` writes are accepted - [AMPR markers](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - signatures taken from the [ampr_emu](https://github.com/drakmor/ampr_emu) reimplementation, not confirmed on a title: `sceAmprCommandBufferSetMarkerWithColor` takes the color by pointer, the push and measure variants by value. Colors are not recorded - [__cxa_call_unexpected](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - takes the in-flight exception's unwind header and never returns, as [libcxxabi 18.1.8](https://github.com/llvm/llvm-project/blob/3b5b5c1ec4a3095ab096dd780e84d7ab81f3d7ff/libcxxabi/src/cxa_personality.cpp#L1171-L1211) does. No `std::set_unexpected` is exported and the exception header's unexpected handler is never set, so the handler is always the default one, which calls `std::terminate` ([cxa_default_handlers.cpp](https://github.com/llvm/llvm-project/blob/3b5b5c1ec4a3095ab096dd780e84d7ab81f3d7ff/libcxxabi/src/cxa_default_handlers.cpp#L88-L99)); it therefore forwards to libc's `__cxa_call_terminate`, which begins the catch and runs the current terminate handler. That matches libcxxabi except for which terminate handler runs: there `std::terminate` after `__cxa_begin_catch` ([cxa_exception.cpp L470-L477](https://github.com/llvm/llvm-project/blob/3b5b5c1ec4a3095ab096dd780e84d7ab81f3d7ff/libcxxabi/src/cxa_exception.cpp#L470-L477)) runs the handler saved in the caught exception's header ([cxa_handlers.cpp L72-L93](https://github.com/llvm/llvm-project/blob/3b5b5c1ec4a3095ab096dd780e84d7ab81f3d7ff/libcxxabi/src/cxa_handlers.cpp#L72-L93)), which `__cxa_throw` and `__cxa_init_primary_exception` set to `std::get_terminate()` when the exception is thrown ([cxa_exception.cpp L209-L214](https://github.com/llvm/llvm-project/blob/3b5b5c1ec4a3095ab096dd780e84d7ab81f3d7ff/libcxxabi/src/cxa_exception.cpp#L209-L214), [L279](https://github.com/llvm/llvm-project/blob/3b5b5c1ec4a3095ab096dd780e84d7ab81f3d7ff/libcxxabi/src/cxa_exception.cpp#L279)), while here the current global handler runs, as AnyPS5's own `std::terminate` does. The two differ only if `std::set_terminate` was called between the throw and this call. A null header terminates because this emulator's libc `__cxa_begin_catch` terminates on null (libcxxabi does not define that case). Dynamic exception specifications that would let a replaced unexpected handler throw a permitted type are not modelled From 7f1e12a4e1861f1c057031b09432898bd154c19f Mon Sep 17 00:00:00 2001 From: jagrat Singh Date: Sun, 18 Oct 2026 03:54:32 +0530 Subject: [PATCH 0886/1000] feat(libSceProprietaryVoiceChatHelperPrx): export sceProprietaryVoiceChatHelperGetVoiceChat --- .../prx/libSceProprietaryVoiceChatHelperPrx/Export.cpp | 8 ++++++++ core/libs/tests/GuestProprietaryVoiceChatHelper.cpp | 7 +++++++ docs/dev/debt/libSceProprietaryVoiceChatHelperPrx.md | 2 +- 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceProprietaryVoiceChatHelperPrx/Export.cpp b/core/libs/prx/libSceProprietaryVoiceChatHelperPrx/Export.cpp index 315ce70e71..d1c11f4133 100644 --- a/core/libs/prx/libSceProprietaryVoiceChatHelperPrx/Export.cpp +++ b/core/libs/prx/libSceProprietaryVoiceChatHelperPrx/Export.cpp @@ -59,4 +59,12 @@ int APS5_VABI sceProprietaryVoiceChatHelperGetVoiceChatUsageState(void) { return 0; } +int APS5_VABI sceProprietaryVoiceChatHelperGetVoiceChat(void) { + auto& helper = State(); + std::lock_guard lock(helper.mutex); + RequireInitialized(helper, __func__); + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/tests/GuestProprietaryVoiceChatHelper.cpp b/core/libs/tests/GuestProprietaryVoiceChatHelper.cpp index e5a76a3bb8..27f0e6dff8 100644 --- a/core/libs/tests/GuestProprietaryVoiceChatHelper.cpp +++ b/core/libs/tests/GuestProprietaryVoiceChatHelper.cpp @@ -9,6 +9,7 @@ int APS5_VABI sceProprietaryVoiceChatHelperInitialize(); int APS5_VABI sceProprietaryVoiceChatHelperTerminate(); int APS5_VABI sceProprietaryVoiceChatHelperSetVoiceChatState(); int APS5_VABI sceProprietaryVoiceChatHelperGetVoiceChatUsageState(); +int APS5_VABI sceProprietaryVoiceChatHelperGetVoiceChat(); } static void Require(bool value, const char* what) { @@ -32,6 +33,8 @@ int main() { "setting the state before initialization did not throw"); Require(Throws([] { sceProprietaryVoiceChatHelperGetVoiceChatUsageState(); }), "reading the usage state before initialization did not throw"); + Require(Throws([] { sceProprietaryVoiceChatHelperGetVoiceChat(); }), + "getting voice chat before initialization did not throw"); Require(Throws([] { sceProprietaryVoiceChatHelperTerminate(); }), "terminating before initialization did not throw"); @@ -41,10 +44,14 @@ int main() { Require(sceProprietaryVoiceChatHelperSetVoiceChatState() == 0, "setting the state failed"); Require(Throws([] { sceProprietaryVoiceChatHelperGetVoiceChatUsageState(); }), "reading the unknown usage state did not throw"); + Require(Throws([] { sceProprietaryVoiceChatHelperGetVoiceChat(); }), + "calling unknown voice chat export did not throw"); Require(sceProprietaryVoiceChatHelperTerminate() == 0, "termination failed"); Require(Throws([] { sceProprietaryVoiceChatHelperSetVoiceChatState(); }), "setting the state after termination did not throw"); + Require(Throws([] { sceProprietaryVoiceChatHelperGetVoiceChat(); }), + "getting voice chat after termination did not throw"); Require(sceProprietaryVoiceChatHelperInitialize() == 0, "initialization after termination failed"); Require(sceProprietaryVoiceChatHelperTerminate() == 0, "termination after reinitialization failed"); diff --git a/docs/dev/debt/libSceProprietaryVoiceChatHelperPrx.md b/docs/dev/debt/libSceProprietaryVoiceChatHelperPrx.md index 027e2ab7dc..cf1e21965c 100644 --- a/docs/dev/debt/libSceProprietaryVoiceChatHelperPrx.md +++ b/docs/dev/debt/libSceProprietaryVoiceChatHelperPrx.md @@ -2,4 +2,4 @@ ### Silent stubs -- [libSceProprietaryVoiceChatHelperPrx](../../../core/libs/prx/libSceProprietaryVoiceChatHelperPrx/Export.cpp) implements only the no-system-voice-chat path: the host has no system voice chat for a title's own voice chat to announce itself to, so `sceProprietaryVoiceChatHelperInitialize` and `sceProprietaryVoiceChatHelperTerminate` only track whether the library is initialized, and `sceProprietaryVoiceChatHelperSetVoiceChatState` returns success and does nothing: there is nothing to notify, and its state argument is unknown, so no state is recorded. `sceProprietaryVoiceChatHelperGetVoiceChatUsageState` throws, because nothing it could report is known: with the state argument unreadable there is no recorded usage state, and whether the console returns the state or writes it through an output pointer, and how it encodes it, is unknown. Initializing twice, and any call before `sceProprietaryVoiceChatHelperInitialize` or after `sceProprietaryVoiceChatHelperTerminate`, throws, as the console's error codes for this library are unknown. Whether the console checks initialization order at all is unverified +- [libSceProprietaryVoiceChatHelperPrx](../../../core/libs/prx/libSceProprietaryVoiceChatHelperPrx/Export.cpp) implements only the no-system-voice-chat path: the host has no system voice chat for a title's own voice chat to announce itself to, so `sceProprietaryVoiceChatHelperInitialize` and `sceProprietaryVoiceChatHelperTerminate` only track whether the library is initialized, and `sceProprietaryVoiceChatHelperSetVoiceChatState` returns success and does nothing: there is nothing to notify, and its state argument is unknown, so no state is recorded. `sceProprietaryVoiceChatHelperGetVoiceChat` and `sceProprietaryVoiceChatHelperGetVoiceChatUsageState` throw, because nothing they could report is known: with the state argument unreadable there is no recorded usage state, and whether the console returns the state or writes it through an output pointer, and how it encodes it, is unknown. Initializing twice, and any call before `sceProprietaryVoiceChatHelperInitialize` or after `sceProprietaryVoiceChatHelperTerminate`, throws, as the console's error codes for this library are unknown. Whether the console checks initialization order at all is unverified From 513b2e181ba610c9b6b8cfe0a816da4f40419b14 Mon Sep 17 00:00:00 2001 From: Morten Holm Date: Sun, 18 Oct 2026 00:34:23 +0200 Subject: [PATCH 0887/1000] fix(relinker): move a neighbour without an FS or GS prefix with a 256-bit store first A 256-bit store is 4 bytes, so its stub window takes a neighbouring instruction to make room for the jump. The window took the following instruction first even when it was a guest TLS access (an FS or GS prefix). The macOS writer cannot rewrite a TLS access inside a stub and refuses the module with "AMD-only instruction is also a guest TLS access". The window now tries the following and the preceding instruction without a segment prefix first, and falls back to the old order only when neither works, so outputs that can keep the access in the stub do not change. Co-Authored-By: Claude Sonnet 5.5 --- .../codegen/src/Amd64OnlyConverter.cpp | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/core/relinker/codegen/src/Amd64OnlyConverter.cpp b/core/relinker/codegen/src/Amd64OnlyConverter.cpp index f92c5edc93..a2ea235db6 100644 --- a/core/relinker/codegen/src/Amd64OnlyConverter.cpp +++ b/core/relinker/codegen/src/Amd64OnlyConverter.cpp @@ -55,12 +55,16 @@ public: ) : _seg(seg), _matches(matches), _ph(ph), _branchTargets(branchTargets), _pending(pending), _claimed(claimed), _matcher(matcher) {} [[nodiscard]] std::optional Window(const std::size_t index) const { - if (index + 1 < _matches.size() && (_pending.contains(index + 1) || _movable(index + 1, true))) { - if (auto placement = _relocate(index, index, index + 1)) - return placement; + for (const bool avoidSegment : {true, false}) { + if (index + 1 < _matches.size() && (_pending.contains(index + 1) || _movable(index + 1, true, avoidSegment))) { + if (auto placement = _relocate(index, index, index + 1)) + return placement; + } + if (index > 0 && _movable(index - 1, false, avoidSegment)) { + if (auto placement = _relocate(index, index - 1, index)) + return placement; + } } - if (index > 0 && _movable(index - 1, false)) - return _relocate(index, index - 1, index); return std::nullopt; } @@ -108,8 +112,8 @@ private: return kind != ControlFlowKind::Sequential && kind != ControlFlowKind::ConditionalBranch; } - [[nodiscard]] bool _movable(const std::size_t index, const bool last) const { - return !_claimed.contains(index) && !_pending.contains(index) && (last || !_blocksFollowing(index)); + [[nodiscard]] bool _movable(const std::size_t index, const bool last, const bool avoidSegment = false) const { + return !_claimed.contains(index) && !_pending.contains(index) && (last || !_blocksFollowing(index)) && !(avoidSegment && _info(index).SegmentPrefix != 0); } [[nodiscard]] TrampolineSite _site(const std::uint64_t start, const std::size_t length, LoweredBody body) const { From cf9e5a256735fcf205e1d388e11ab7df83bfdb41 Mon Sep 17 00:00:00 2001 From: Morten Holm Date: Sun, 18 Oct 2026 00:34:23 +0200 Subject: [PATCH 0888/1000] test(relinker): keep an FS or GS access out of the window of a 256-bit store Covers a thread-pointer read (FS and GS form) after the store with a movable instruction before it, and the fallback to the following access when there is no other neighbour. Co-Authored-By: Claude Sonnet 5.5 --- .../codegen/tests/Amd64OnlyConverterTests.cpp | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp b/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp index b88e1323d8..6fbecaf06b 100644 --- a/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp +++ b/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp @@ -825,6 +825,19 @@ void converterWideStoreWindows() { require(!preceded.Trampolines.empty() && preceded.Trampolines[0].Offset == 0x200 && preceded.Trampolines[0].Length == 8 && body(preceded.Trampolines[0]) == expected, "A 256-bit store moved a following instruction that cannot leave its place instead of the one before it"); } + const Bytes fsRead = {0x66, 0x66, 0x66, 0x64, 0x48, 0x8B, 0x04, 0x25, 0x00, 0x00, 0x00, 0x00}; + const Bytes gsRead = {0x65, 0x48, 0x8B, 0x04, 0x25, 0x00, 0x00, 0x00, 0x00}; + for (const auto& segmentRead : {fsRead, gsRead}) { + const auto beforeSegment = convert(withStore(add, segmentRead)); + expected = add; + expected.insert(expected.end(), halves.begin(), halves.end()); + require(beforeSegment.Trampolines.size() == 1 && beforeSegment.Trampolines[0].Offset == 0x200 && beforeSegment.Trampolines[0].Length == 8 && body(beforeSegment.Trampolines[0]) == expected, "A 256-bit store moved a following FS or GS access, which the macOS writer cannot rewrite in a stub, instead of the instruction before it"); + const auto onlySegment = convert(withStore({}, segmentRead)); + expected = halves; + expected.insert(expected.end(), segmentRead.begin(), segmentRead.end()); + require(onlySegment.Trampolines.size() == 1 && onlySegment.Trampolines[0].Offset == 0x200 && onlySegment.Trampolines[0].Length == store.size() + segmentRead.size() && body(onlySegment.Trampolines[0]) == expected, "A 256-bit store with no other movable neighbour did not fall back to the following FS or GS access"); + } + Bytes throwing = {0xC3, 0xEB, 0xFD}; for (int index = 0; index < 3; ++index) throwing.insert(throwing.end(), add.begin(), add.end()); From 0f0ae0708424994ca0b3188e6a6cce9bbf9321d0 Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Sun, 18 Oct 2026 11:45:11 +1300 Subject: [PATCH 0889/1000] ci(intake): let a maintainer reopen bypass the open-PR limit The intake workflow re-closes a reopened PR whose author is still over the open-PR limit, so a maintainer cannot rescue PRs it auto-closed. When the reopening actor has write, maintain or admin permission, skip the limit and remove the "open PR limit" label instead. Contributor reopens behave as before, and if the permission lookup fails the limit still applies. --- .github/workflows/pr-intake.yml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/.github/workflows/pr-intake.yml b/.github/workflows/pr-intake.yml index 949a8cb717..3225af812b 100644 --- a/.github/workflows/pr-intake.yml +++ b/.github/workflows/pr-intake.yml @@ -27,6 +27,27 @@ jobs: if (pr.state !== 'open' || pr.base.ref !== 'main') { return; } + if (context.payload.action === 'reopened') { + let maintainer = false; + try { + const { data: access } = await github.rest.repos.getCollaboratorPermissionLevel({ + owner, repo, username: context.payload.sender.login + }); + maintainer = ['admin', 'maintain', 'write'].includes(access.role_name ?? access.permission); + } catch (error) { + core.warning(`Could not check the permission of ${context.payload.sender.login}, so the open PR limit applies: ${error.message}`); + } + if (maintainer) { + try { + await github.rest.issues.removeLabel({ owner, repo, issue_number: pull_number, name: 'open PR limit' }); + } catch (error) { + if (error.status !== 404) { + core.warning(`Could not remove the open PR limit label: ${error.message}`); + } + } + return; + } + } const { data: mergedPRs } = await github.rest.search.issuesAndPullRequests({ q: `repo:${owner}/${repo} is:pr is:merged base:main author:${pr.user.login}`, per_page: 1 From 7e26c1a4eaa43378f71fe8a58341c835f0005bfe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexej=20Szpev=C3=A1k?= Date: Sat, 17 Oct 2026 15:13:12 +0200 Subject: [PATCH 0890/1000] feat(libkernel): poll regular files and directories as vop_stdpoll does poll threw for any descriptor that is neither a socket nor a pipe, so a title polling a file it also reads stopped there. FreeBSD 11.4 hands a vnode to VOP_POLL, and both vop_stdpoll and vop_nopoll return POLLIN, POLLRDNORM and POLLOUT of the requested events for every event poll accepts and never POLLPRI, POLLRDBAND or POLLWRBAND. A file, a directory or a file read from app0.zar now answers that without a host call. File::IsVnode tells such a descriptor apart from the host file type without throwing, so a host socket lent by libSceNet still reaches its own path. --- .../File/include/DescriptorControl.hpp | 1 + .../libkernel/File/src/DescriptorControl.cpp | 12 +++++++ core/libs/prx/libkernel/Socket/src/Udp.cpp | 8 ++++- core/libs/tests/GuestPoll.cpp | 32 +++++++++++++++++++ docs/dev/debt/libkernel.md | 2 +- 5 files changed, 53 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libkernel/File/include/DescriptorControl.hpp b/core/libs/prx/libkernel/File/include/DescriptorControl.hpp index d7345faec2..81a9018c42 100644 --- a/core/libs/prx/libkernel/File/include/DescriptorControl.hpp +++ b/core/libs/prx/libkernel/File/include/DescriptorControl.hpp @@ -15,6 +15,7 @@ void RememberDescriptor(int fd, int openFlags); void ShareDescriptor(int from, int to); void ForgetDescriptor(int fd); bool Appends(int fd); +bool IsVnode(int fd); } diff --git a/core/libs/prx/libkernel/File/src/DescriptorControl.cpp b/core/libs/prx/libkernel/File/src/DescriptorControl.cpp index 174f0571b1..b20bde40a6 100644 --- a/core/libs/prx/libkernel/File/src/DescriptorControl.cpp +++ b/core/libs/prx/libkernel/File/src/DescriptorControl.cpp @@ -570,4 +570,16 @@ bool Appends(int fd) { return found != g_entries.end() && found->second.status && (*found->second.status & Append) != 0; } +bool IsVnode(int fd) { + if (fd < 0 || File::IsRandomDevice(fd)) return false; + if (File::IsArchivedFile(fd)) return true; +#ifdef _WIN32 + if (!DescriptorIsOpen_nid_no_patch(fd)) return false; + return File::DirectoryDescriptorPath(fd) || ::GetFileType(WindowsDescriptorHandle_nid_no_patch(fd)) == FILE_TYPE_DISK; +#else + struct stat status{}; + return ::fstat(fd, &status) == 0 && (S_ISREG(status.st_mode) || S_ISDIR(status.st_mode)); +#endif +} + } diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index 9bb4ed1b3e..f7e8c87a98 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -470,6 +470,7 @@ std::shared_ptr Find(int descriptor) { } constexpr short GuestPollReadable = 0x1 | 0x40; constexpr short GuestPollWritable = 0x4; +constexpr short GuestPollVnodeReady = GuestPollReadable | GuestPollWritable; constexpr short GuestPollHangUp = 0x10; constexpr short GuestPollError = 0x8; [[noreturn]] void ThrowReadEndWritable(int descriptor) { @@ -1416,13 +1417,18 @@ int APS5_VABI poll_nid_postfix(GuestPollDescriptor* descriptors, std::uint32_t c ++ready; continue; } + if (File::IsVnode(entry.descriptor)) { + entry.revents = static_cast(entry.events & GuestPollVnodeReady); + if (entry.revents) ++ready; + continue; + } #ifdef _WIN32 short revents = 0; if (!PipeReadiness(entry.descriptor, entry.events, revents)) #else if (!IsPipe(entry.descriptor, entry.events)) #endif - throw std::runtime_error("poll: descriptor " + std::to_string(entry.descriptor) + " is neither a socket nor a pipe"); + throw std::runtime_error("poll: descriptor " + std::to_string(entry.descriptor) + " is neither a socket, a pipe, a file nor a directory"); pipes.push_back(i); continue; } diff --git a/core/libs/tests/GuestPoll.cpp b/core/libs/tests/GuestPoll.cpp index f96ae8f616..2098072586 100644 --- a/core/libs/tests/GuestPoll.cpp +++ b/core/libs/tests/GuestPoll.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include struct PollDescriptor { int descriptor; @@ -31,6 +32,9 @@ std::int64_t APS5_VABI send_nid_postfix(int, const void*, std::uint64_t, int); std::int64_t APS5_VABI recv_nid_postfix(int, void*, std::uint64_t, int); int APS5_VABI close_nid_postfix(int); int APS5_VABI open_nid_postfix(const char*, int, int); +int APS5_VABI mkdir_nid_postfix(const char*, unsigned short); +int APS5_VABI rmdir_nid_postfix(const char*); +int APS5_VABI unlink_nid_postfix(const char*); int APS5_VABI poll_nid_postfix(PollDescriptor*, std::uint32_t, int); int APS5_VABI pipe_nid_postfix(int*); std::int64_t APS5_VABI read_nid_postfix(int, void*, std::uint64_t); @@ -55,6 +59,33 @@ constexpr short Out = 0x4; constexpr short Hup = 0x10; constexpr short Invalid = 0x20; constexpr short ReadNormal = 0x40; +constexpr short WriteBand = 0x100; +static void PollsFilesAndDirectories(int socket) { + constexpr int ReadWrite = 0x0002, Create = 0x0200, DirectoryFlag = 0x00020000; + const auto stamp = std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()); + const std::string filePath = "anyps5-poll-file-" + stamp; + const std::string directoryPath = "anyps5-poll-directory-" + stamp; + const int file = open_nid_postfix(filePath.c_str(), ReadWrite | Create, 0644); + Require(file >= 0); + Require(mkdir_nid_postfix(directoryPath.c_str(), 0755) == 0); + const int directory = open_nid_postfix(directoryPath.c_str(), DirectoryFlag, 0); + Require(directory >= 0); + PollDescriptor vnode{file, static_cast(In | Out | ReadNormal | Priority | ReadBand | WriteBand), -1}; + Require(poll_nid_postfix(&vnode, 1, 0) == 1 && vnode.revents == (In | Out | ReadNormal)); + vnode.events = static_cast(Priority | ReadBand | WriteBand); + Require(poll_nid_postfix(&vnode, 1, 0) == 0 && vnode.revents == 0); + const auto start = std::chrono::steady_clock::now(); + Require(poll_nid_postfix(&vnode, 1, 30) == 0); + Require(std::chrono::steady_clock::now() - start >= std::chrono::milliseconds(25)); + PollDescriptor folder{directory, static_cast(In | Out), -1}; + Require(poll_nid_postfix(&folder, 1, -1) == 1 && folder.revents == (In | Out)); + PollDescriptor mixed[3]{{socket, In, -1}, {file, Out, -1}, {directory, ReadNormal, -1}}; + Require(poll_nid_postfix(mixed, 3, -1) == 2); + Require(mixed[0].revents == 0 && mixed[1].revents == Out && mixed[2].revents == ReadNormal); + Require(close_nid_postfix(directory) == 0 && close_nid_postfix(file) == 0); + Require(rmdir_nid_postfix(directoryPath.c_str()) == 0 && unlink_nid_postfix(filePath.c_str()) == 0); +} + static void TestPipes(int socket) { int ends[2]; Require(pipe_nid_postfix(ends) == 0); @@ -221,6 +252,7 @@ int main() { Require(poll_nid_postfix(nullptr, 1, 0) == -1 && *__error_nid_postfix() == 14); Require(poll_nid_postfix(&single, 1, -2) == -1 && *__error_nid_postfix() == 22); TestPipes(listener); + PollsFilesAndDirectories(listener); Require(close_nid_postfix(server) == 0); const int file = open_nid_postfix("/dev/urandom", 0, 0); Require(file >= 0); diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 02c8d01b37..09f80d6298 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -173,7 +173,7 @@ - [read, write and vector I/O](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) on a guest socket use `recv` and `send` without flags; `sceKernelReadv` and `sceKernelWritev` gather or scatter through one socket operation, preserving stream short reads and datagram boundaries. The positioned `pread`, `pwrite`, `preadv` and `pwritev` forms, and their `sceKernel` forms, return `ESPIPE` for an open guest socket and `EBADF` for a closed one. `EPIPE` from a write raises the host's `SIGPIPE` on Linux unless the socket has `SO_NOSIGPIPE` set, and the guest cannot ignore it: `signal(SIGPIPE, SIG_IGN)` and `sigaction` record the disposition, as they do for every number from 1 to 128, but only the six signals with host equivalents (2, 4, 6, 8, 11 and 15) take effect on the host or are delivered, so the host `SIGPIPE` is not stopped; nothing is raised on Windows; a `write` longer than `INT_MAX` bytes is `EMSGSIZE` where the console sends it. AIO on a guest socket still goes to the host, which has no such descriptor - [Socket descriptor numbers](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel, libSceNet) - `socket`, `accept`, `socketpair`, `dup`, `sceNetSocket` and `sceNetAccept` take the lowest free number of the host descriptor table that files use, as FreeBSD 11.4 `fdalloc` takes the lowest free entry of the one process table (`kern_descrip.c`), so every number stays below `FD_SETSIZE` (1024) while fewer than 1024 descriptors are open. The number holds a zero-access `NUL` handle on Windows and a duplicate of the host socket on Linux, and the socket tables map it to the socket. The host table also holds the runtime's own files, so the numbers differ from the console's; the first number `socket` and `sceNetSocket` return on a console and its `getdtablesize` are not measured. Paths that bypass the socket tables see the placeholder: libc `fdopen` on a socket opens it, and libkernel `close` of a libSceNet socket releases the number but leaves the libSceNet socket open. Palworld (PPSA23126), The Mound (PPSA26223) and Dying Light 2 (PPSA02262) set socket bits in a 1024-bit `fd_set` - [fstat on a guest socket](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) fills the fields FreeBSD 11's soo_stat and pru_sense_null fill and zeroes the rest: S_IFSOCK, the read bits while the receive side is open or data is queued, the write bits while the send side is open, st_size and st_blksize. Only this process's shutdown closes a side here; a peer's shutdown, close or reset does not clear the bits as FreeBSD's socantrcvmore and socantsendmore do. st_size is the host's FIONREAD, which for a Linux datagram socket is the size of the next datagram where FreeBSD counts every queued byte, and st_blksize is the host's SO_SNDBUF, which Linux reports as twice the size that was set. A socketpair end reports st_dev and st_ino 0, where FreeBSD's uipc_sense reports NODEV and a per-socket number -- [poll](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) waits on guest sockets and pipes, through `WSAPoll` on Windows and `poll` on Linux; the `pollfd` layout and flag values are FreeBSD's. A libSceNet socket joins the same host wait through the native socket libSceNet lends to libkernel. `WSAPoll` rejects `POLLPRI`, so on Windows a socket's `POLLPRI` is asked as `POLLRDBAND` and `POLLIN` as `POLLRDNORM`; Winsock reports urgent data until it is read with `MSG_OOB`, where FreeBSD 11.4's `sopoll_generic` stops once the normal stream passes the mark. Guest `send` and `recv` refuse `MSG_OOB` with `EOPNOTSUPP`, so urgent data can only come from an outside peer and Windows keeps reporting `POLLPRI` after the stream passes the mark. A pipe follows FreeBSD's `pipe_poll`: the read end is readable when data is buffered, either end reports `POLLHUP` once the other end is closed, with the requested `POLLIN` and `POLLRDNORM` bits and without `POLLOUT`, which `pollscan` clears, and the write end is writable when `PIPE_BUF` (512) bytes are free on Windows, or when the host kernel says so on Linux. `POLLOUT` on a read end throws: FreeBSD opens both ends for reading and writing and reports it writable, and host pipes are one-way. Windows pipes cannot be waited on, so while a pipe is polled `poll` checks it with `NtQueryInformationFile` and waits on the sockets in slices of 1 ms. A closed descriptor reports `POLLNVAL`, and an open descriptor that is neither a guest socket, a libSceNet socket nor a pipe (files, stdio) throws, as do event bits other than `POLLIN`, `POLLPRI`, `POLLOUT`, `POLLRDNORM`, `POLLRDBAND` and `POLLWRBAND` (the `POLLERR`, `POLLHUP` and `POLLNVAL` bits are ignored in `events`), an infinite timeout with no socket or pipe to wait on. A closed socket descriptor reports `POLLNVAL`; the console's limit on the descriptor count is unknown and not checked +- [poll](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) waits on guest sockets and pipes, through `WSAPoll` on Windows and `poll` on Linux; the `pollfd` layout and flag values are FreeBSD's. A libSceNet socket joins the same host wait through the native socket libSceNet lends to libkernel. `WSAPoll` rejects `POLLPRI`, so on Windows a socket's `POLLPRI` is asked as `POLLRDBAND` and `POLLIN` as `POLLRDNORM`; Winsock reports urgent data until it is read with `MSG_OOB`, where FreeBSD 11.4's `sopoll_generic` stops once the normal stream passes the mark. Guest `send` and `recv` refuse `MSG_OOB` with `EOPNOTSUPP`, so urgent data can only come from an outside peer and Windows keeps reporting `POLLPRI` after the stream passes the mark. A pipe follows FreeBSD's `pipe_poll`: the read end is readable when data is buffered, either end reports `POLLHUP` once the other end is closed, with the requested `POLLIN` and `POLLRDNORM` bits and without `POLLOUT`, which `pollscan` clears, and the write end is writable when `PIPE_BUF` (512) bytes are free on Windows, or when the host kernel says so on Linux. `POLLOUT` on a read end throws: FreeBSD opens both ends for reading and writing and reports it writable, and host pipes are one-way. Windows pipes cannot be waited on, so while a pipe is polled `poll` checks it with `NtQueryInformationFile` and waits on the sockets in slices of 1 ms. A regular file, a directory or a file read from app0.zar is always ready for `POLLIN`, `POLLRDNORM` and `POLLOUT` and never for `POLLPRI`, `POLLRDBAND` or `POLLWRBAND`, as FreeBSD 11.4's [`vop_stdpoll`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/vfs_default.c#L563-L574) and `vop_nopoll` answer every event `poll` accepts; which of the two the console's filesystems use is not known, and they agree on these events. A closed descriptor reports `POLLNVAL`, and an open descriptor that is neither a guest socket, a libSceNet socket, a pipe, a file nor a directory (stdio, devices) throws, as do event bits other than `POLLIN`, `POLLPRI`, `POLLOUT`, `POLLRDNORM`, `POLLRDBAND` and `POLLWRBAND` (the `POLLERR`, `POLLHUP` and `POLLNVAL` bits are ignored in `events`), an infinite timeout with no socket or pipe to wait on. A closed socket descriptor reports `POLLNVAL`; the console's limit on the descriptor count is unknown and not checked - [kevent on a pipe](../../../core/libs/prx/libkernel/Equeue/src/Kqueue.cpp) (libkernel) - `EVFILT_READ` on the read end of a pipe follows FreeBSD 11's [`filt_piperead`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/sys_pipe.c#L1787-L1806): `data` is the number of buffered bytes, and `EV_EOF` is set once the write end is closed. Windows pipes cannot be waited on, so while a kqueue waits on a pipe it checks it with `NtQueryInformationFile` and waits on its sockets in slices of 1 ms; a wait with no pipe is not sliced. `EVFILT_READ` on the write end of a pipe throws: FreeBSD's `pipe` opens both ends for reading and writing ([`kern_pipe`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/sys_pipe.c#L422)), and host pipes are one-way. `EVFILT_WRITE` on a pipe throws too, and `NOTE_LOWAT` on a pipe is ignored, as `filt_piperead` ignores it. - [Socket error numbers](../../../core/libs/prx/libkernel/Socket/include/SocketError.hpp) (libkernel) - a host socket error becomes the FreeBSD 11 `errno.h` number of the same name; the numbers are not checked on the console. Winsock `WSAESHUTDOWN` becomes `EPIPE`, as a send after `shutdown` does on FreeBSD, and a host error with no FreeBSD socket counterpart becomes `EIO`. A nonblocking `connect` in progress reports `EINPROGRESS` on Linux but `EAGAIN` on Windows (`WSAEWOULDBLOCK`), where FreeBSD reports `EINPROGRESS`. On Windows a refused nonblocking `connect` is never reported (`SO_ERROR` is unsupported), so a repeated one keeps reporting `EALREADY` where FreeBSD reports `ECONNREFUSED`. A receive after `shutdown(SHUT_RD)` reports `EPIPE` on Windows where FreeBSD returns 0 - [getsockopt, setsockopt](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) on a guest socket handle the SOL_SOCKET level and `TCP_NODELAY`, after FreeBSD 11.4's [`sosetopt` and `sogetopt`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c): `SO_REUSEADDR`, `SO_KEEPALIVE`, `SO_DONTROUTE`, `SO_BROADCAST`, `SO_OOBINLINE` and `SO_LINGER` act on the host socket, and `SO_ERROR`, `SO_TYPE`, `SO_ACCEPTCONN`, `SO_SNDBUF` and `SO_RCVBUF` are read. A boolean option reads back as its own bit, as FreeBSD returns `so_options & option`. Winsock does not clear a socket's pending error when it is read, so on Windows the library reports a pending error once and shows it again only after a new `connect`; a second failure with the same code on the same socket without a `connect` in between would be hidden. `SO_SNDBUF` and `SO_RCVBUF` read the host's value, which is not the size FreeBSD reports (Linux doubles it). `SO_NOSIGPIPE` is kept per socket and reads back as its bit. On Linux a `send`, `sendto`, `sendmsg` or `write` on that socket passes `MSG_NOSIGNAL` to the host, as [`kern_sendit`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_syscalls.c#L890-L895) and [`soo_write`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/sys_socket.c#L147-L151) leave out the signal. An accepted socket takes it from its listener, as [`sonewconn`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L603) copies the options. `SO_NBIO` (`0x1200`) is the console's own option, not FreeBSD's. It switches the socket between blocking and non-blocking as `FIONBIO` does and reads back as 0 or 1, as `sceNetSetsockopt` and `sceNetGetsockopt` treat it here; what the console reads back for it is not measured. `TCP_NODELAY` (`IPPROTO_TCP`, 1) is set on the host socket and reads back as `TF_NODELAY` (4), as [`tcp_default_ctloutput`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/netinet/tcp_usrreq.c#L1860-L1861) returns it. The `IPPROTO_TCP` level on a datagram or `AF_UNIX` socket fails with `EINVAL`, as [`ip_ctloutput`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/netinet/ip_output.c#L943-L944) and [`uipc_ctloutput`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_usrreq.c#L1228-L1229) answer it, and an accepted socket takes `TCP_NODELAY` through the host. `SO_SNDTIMEO` and `SO_RCVTIMEO` follow [`sosetopt`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L2657-L2689) and [`sogetopt`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L2846-L2850). A 16-byte `timeval` with a negative field or 1000000 microseconds or more fails with `EDOM`, and more than `INT32_MAX` seconds becomes the longest wait. The value is kept and read back through FreeBSD's 32.32 fixed-point conversion ([`tvtosbt`, `sbttotv`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/sys/time.h#L178-L286)), so half a second reads back as 499999 microseconds and one microsecond as 0. A `recv`, `recvfrom`, `recvmsg`, `read` or `readv`, and a `send`, `sendto`, `sendmsg`, `write` or `writev`, on a blocking socket then waits that long for the host to report the socket readable or writable, and fails with `EWOULDBLOCK`; a call with `MSG_DONTWAIT` does not wait. An accepted socket takes both from its listener ([`sonewconn`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L629-L630)). The wait covers only the first transfer: once the socket is writable the host send runs without a limit, where FreeBSD stops a send that cannot finish and returns the bytes sent so far, and a receive whose data another thread takes between the wait and the host call blocks again. `sendfile` ignores the send timeout. The host wait has millisecond steps, so a shorter timeout lasts up to a millisecond longer. A null value fails with `EINVAL`, where FreeBSD's [`kern_setsockopt`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_syscalls.c#L1414-L1415) returns `EFAULT`. Every other SOL_SOCKET option (`SO_DEBUG`, `SO_USELOOPBACK`, `SO_REUSEPORT` and `SO_TIMESTAMP`, which FreeBSD stores and no title has been seen to set, the low-water marks among them), every other `IPPROTO_TCP` option (`TCP_MAXSEG`, `TCP_KEEPIDLE`, `TCP_KEEPINTVL` and `TCP_KEEPCNT` among them) and every other level (`IPPROTO_IP`, `IPPROTO_IPV6`) fail with `ENOPROTOOPT`. None of this was measured on a console. From d0c338561acb8b2cefca098e7aa519420eac94b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexej=20Szpev=C3=A1k?= Date: Sat, 17 Oct 2026 15:13:43 +0200 Subject: [PATCH 0891/1000] feat(libkernel): poll the random device as randomdev_poll does poll threw for /dev/random and /dev/urandom, which select already treats as ready. FreeBSD 11.4's randomdev_poll, once the device is seeded, narrows a request that has POLLIN or POLLRDNORM to those bits and returns any other request whole, POLLOUT and the band bits included. poll now answers that, and the test that relied on the random device throwing uses a kqueue descriptor, which poll still does not support. --- core/libs/prx/libkernel/Socket/src/Udp.cpp | 7 +++++++ core/libs/tests/GuestPoll.cpp | 24 +++++++++++++++++----- core/libs/tests/GuestSocketDescriptors.cpp | 2 +- docs/dev/debt/libkernel.md | 2 +- 4 files changed, 28 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index f7e8c87a98..0d08456992 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -29,6 +29,7 @@ #include "prx/libkernel/File/include/DescriptorControl.hpp" #include "prx/libkernel/Equeue/Equeue.hpp" #include "prx/libkernel/File/include/File.hpp" +#include "prx/libkernel/File/include/RandomDevice.hpp" #include "prx/libkernel/Time/include/TimedWait.hpp" #include "prx/libkernel/Socket/include/SocketPoll.hpp" #include "prx/libkernel/Socket/include/SocketConnect.hpp" @@ -1417,6 +1418,12 @@ int APS5_VABI poll_nid_postfix(GuestPollDescriptor* descriptors, std::uint32_t c ++ready; continue; } + if (File::IsRandomDevice(entry.descriptor)) { + const short readable = static_cast(entry.events & GuestPollReadable); + entry.revents = readable != 0 ? readable : entry.events; + if (entry.revents) ++ready; + continue; + } if (File::IsVnode(entry.descriptor)) { entry.revents = static_cast(entry.events & GuestPollVnodeReady); if (entry.revents) ++ready; diff --git a/core/libs/tests/GuestPoll.cpp b/core/libs/tests/GuestPoll.cpp index 2098072586..5d229666e8 100644 --- a/core/libs/tests/GuestPoll.cpp +++ b/core/libs/tests/GuestPoll.cpp @@ -37,6 +37,7 @@ int APS5_VABI rmdir_nid_postfix(const char*); int APS5_VABI unlink_nid_postfix(const char*); int APS5_VABI poll_nid_postfix(PollDescriptor*, std::uint32_t, int); int APS5_VABI pipe_nid_postfix(int*); +int APS5_VABI kqueue_nid_postfix(); std::int64_t APS5_VABI read_nid_postfix(int, void*, std::uint64_t); std::int64_t APS5_VABI write_nid_postfix(int, const char*, std::int64_t); int APS5_VABI fcntl_nid_postfix(int, int, ...); @@ -254,16 +255,29 @@ int main() { TestPipes(listener); PollsFilesAndDirectories(listener); Require(close_nid_postfix(server) == 0); - const int file = open_nid_postfix("/dev/urandom", 0, 0); - Require(file >= 0); - PollDescriptor mixed[2]{{listener, In, 0}, {file, In, 0}}; + for (const char* path : {"/dev/urandom", "/dev/random"}) { + const int random = open_nid_postfix(path, 0, 0); + Require(random >= 0); + PollDescriptor device{random, static_cast(In | Out | Priority), -1}; + Require(poll_nid_postfix(&device, 1, 0) == 1 && device.revents == In); + device.events = static_cast(ReadNormal | Out); + Require(poll_nid_postfix(&device, 1, 0) == 1 && device.revents == ReadNormal); + device.events = static_cast(Out | Priority | ReadBand | WriteBand); + Require(poll_nid_postfix(&device, 1, -1) == 1 && device.revents == (Out | Priority | ReadBand | WriteBand)); + PollDescriptor mixed[2]{{listener, In, 0}, {random, Out, 0}}; + Require(poll_nid_postfix(mixed, 2, -1) == 1 && mixed[0].revents == 0 && mixed[1].revents == Out); + Require(close_nid_postfix(random) == 0); + } + const int queue = kqueue_nid_postfix(); + Require(queue >= 0); + PollDescriptor unknown[2]{{listener, In, 0}, {queue, In, 0}}; bool unsupported = false; try { - poll_nid_postfix(mixed, 2, 0); + poll_nid_postfix(unknown, 2, 0); } catch (const std::runtime_error&) { unsupported = true; } Require(unsupported); - Require(close_nid_postfix(file) == 0); + Require(close_nid_postfix(queue) == 0); Require(close_nid_postfix(listener) == 0); } diff --git a/core/libs/tests/GuestSocketDescriptors.cpp b/core/libs/tests/GuestSocketDescriptors.cpp index 76618bf409..632f528ccf 100644 --- a/core/libs/tests/GuestSocketDescriptors.cpp +++ b/core/libs/tests/GuestSocketDescriptors.cpp @@ -156,7 +156,7 @@ static void CloseReleasesNumber() { unsigned char bytes[4]{}; Require(read_nid_postfix(socket, bytes, sizeof(bytes)) == sizeof(bytes)); PollDescriptor entry{socket, In, 0}; - Require(Throws([&] { poll_nid_postfix(&entry, 1, 0); })); + Require(poll_nid_postfix(&entry, 1, 0) == 1 && entry.revents == In); for (const int file : files) Require(close_nid_postfix(file) == 0); const int kernelClosed = Socket(); Require(sceKernelClose(kernelClosed) == 0); diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 09f80d6298..78581cfca8 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -173,7 +173,7 @@ - [read, write and vector I/O](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) on a guest socket use `recv` and `send` without flags; `sceKernelReadv` and `sceKernelWritev` gather or scatter through one socket operation, preserving stream short reads and datagram boundaries. The positioned `pread`, `pwrite`, `preadv` and `pwritev` forms, and their `sceKernel` forms, return `ESPIPE` for an open guest socket and `EBADF` for a closed one. `EPIPE` from a write raises the host's `SIGPIPE` on Linux unless the socket has `SO_NOSIGPIPE` set, and the guest cannot ignore it: `signal(SIGPIPE, SIG_IGN)` and `sigaction` record the disposition, as they do for every number from 1 to 128, but only the six signals with host equivalents (2, 4, 6, 8, 11 and 15) take effect on the host or are delivered, so the host `SIGPIPE` is not stopped; nothing is raised on Windows; a `write` longer than `INT_MAX` bytes is `EMSGSIZE` where the console sends it. AIO on a guest socket still goes to the host, which has no such descriptor - [Socket descriptor numbers](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel, libSceNet) - `socket`, `accept`, `socketpair`, `dup`, `sceNetSocket` and `sceNetAccept` take the lowest free number of the host descriptor table that files use, as FreeBSD 11.4 `fdalloc` takes the lowest free entry of the one process table (`kern_descrip.c`), so every number stays below `FD_SETSIZE` (1024) while fewer than 1024 descriptors are open. The number holds a zero-access `NUL` handle on Windows and a duplicate of the host socket on Linux, and the socket tables map it to the socket. The host table also holds the runtime's own files, so the numbers differ from the console's; the first number `socket` and `sceNetSocket` return on a console and its `getdtablesize` are not measured. Paths that bypass the socket tables see the placeholder: libc `fdopen` on a socket opens it, and libkernel `close` of a libSceNet socket releases the number but leaves the libSceNet socket open. Palworld (PPSA23126), The Mound (PPSA26223) and Dying Light 2 (PPSA02262) set socket bits in a 1024-bit `fd_set` - [fstat on a guest socket](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) fills the fields FreeBSD 11's soo_stat and pru_sense_null fill and zeroes the rest: S_IFSOCK, the read bits while the receive side is open or data is queued, the write bits while the send side is open, st_size and st_blksize. Only this process's shutdown closes a side here; a peer's shutdown, close or reset does not clear the bits as FreeBSD's socantrcvmore and socantsendmore do. st_size is the host's FIONREAD, which for a Linux datagram socket is the size of the next datagram where FreeBSD counts every queued byte, and st_blksize is the host's SO_SNDBUF, which Linux reports as twice the size that was set. A socketpair end reports st_dev and st_ino 0, where FreeBSD's uipc_sense reports NODEV and a per-socket number -- [poll](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) waits on guest sockets and pipes, through `WSAPoll` on Windows and `poll` on Linux; the `pollfd` layout and flag values are FreeBSD's. A libSceNet socket joins the same host wait through the native socket libSceNet lends to libkernel. `WSAPoll` rejects `POLLPRI`, so on Windows a socket's `POLLPRI` is asked as `POLLRDBAND` and `POLLIN` as `POLLRDNORM`; Winsock reports urgent data until it is read with `MSG_OOB`, where FreeBSD 11.4's `sopoll_generic` stops once the normal stream passes the mark. Guest `send` and `recv` refuse `MSG_OOB` with `EOPNOTSUPP`, so urgent data can only come from an outside peer and Windows keeps reporting `POLLPRI` after the stream passes the mark. A pipe follows FreeBSD's `pipe_poll`: the read end is readable when data is buffered, either end reports `POLLHUP` once the other end is closed, with the requested `POLLIN` and `POLLRDNORM` bits and without `POLLOUT`, which `pollscan` clears, and the write end is writable when `PIPE_BUF` (512) bytes are free on Windows, or when the host kernel says so on Linux. `POLLOUT` on a read end throws: FreeBSD opens both ends for reading and writing and reports it writable, and host pipes are one-way. Windows pipes cannot be waited on, so while a pipe is polled `poll` checks it with `NtQueryInformationFile` and waits on the sockets in slices of 1 ms. A regular file, a directory or a file read from app0.zar is always ready for `POLLIN`, `POLLRDNORM` and `POLLOUT` and never for `POLLPRI`, `POLLRDBAND` or `POLLWRBAND`, as FreeBSD 11.4's [`vop_stdpoll`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/vfs_default.c#L563-L574) and `vop_nopoll` answer every event `poll` accepts; which of the two the console's filesystems use is not known, and they agree on these events. A closed descriptor reports `POLLNVAL`, and an open descriptor that is neither a guest socket, a libSceNet socket, a pipe, a file nor a directory (stdio, devices) throws, as do event bits other than `POLLIN`, `POLLPRI`, `POLLOUT`, `POLLRDNORM`, `POLLRDBAND` and `POLLWRBAND` (the `POLLERR`, `POLLHUP` and `POLLNVAL` bits are ignored in `events`), an infinite timeout with no socket or pipe to wait on. A closed socket descriptor reports `POLLNVAL`; the console's limit on the descriptor count is unknown and not checked +- [poll](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) waits on guest sockets and pipes, through `WSAPoll` on Windows and `poll` on Linux; the `pollfd` layout and flag values are FreeBSD's. A libSceNet socket joins the same host wait through the native socket libSceNet lends to libkernel. `WSAPoll` rejects `POLLPRI`, so on Windows a socket's `POLLPRI` is asked as `POLLRDBAND` and `POLLIN` as `POLLRDNORM`; Winsock reports urgent data until it is read with `MSG_OOB`, where FreeBSD 11.4's `sopoll_generic` stops once the normal stream passes the mark. Guest `send` and `recv` refuse `MSG_OOB` with `EOPNOTSUPP`, so urgent data can only come from an outside peer and Windows keeps reporting `POLLPRI` after the stream passes the mark. A pipe follows FreeBSD's `pipe_poll`: the read end is readable when data is buffered, either end reports `POLLHUP` once the other end is closed, with the requested `POLLIN` and `POLLRDNORM` bits and without `POLLOUT`, which `pollscan` clears, and the write end is writable when `PIPE_BUF` (512) bytes are free on Windows, or when the host kernel says so on Linux. `POLLOUT` on a read end throws: FreeBSD opens both ends for reading and writing and reports it writable, and host pipes are one-way. Windows pipes cannot be waited on, so while a pipe is polled `poll` checks it with `NtQueryInformationFile` and waits on the sockets in slices of 1 ms. A regular file, a directory or a file read from app0.zar is always ready for `POLLIN`, `POLLRDNORM` and `POLLOUT` and never for `POLLPRI`, `POLLRDBAND` or `POLLWRBAND`, as FreeBSD 11.4's [`vop_stdpoll`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/vfs_default.c#L563-L574) and `vop_nopoll` answer every event `poll` accepts; which of the two the console's filesystems use is not known, and they agree on these events. `/dev/random` and `/dev/urandom` follow FreeBSD 11.4's [`randomdev_poll`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/dev/random/randomdev.c#L274-L285) for a seeded device: a request with `POLLIN` or `POLLRDNORM` gets only those bits back, and any other request gets every requested bit. A closed descriptor reports `POLLNVAL`, and an open descriptor that is neither a guest socket, a libSceNet socket, a pipe, a file, a directory nor the random device (stdio, other devices) throws, as do event bits other than `POLLIN`, `POLLPRI`, `POLLOUT`, `POLLRDNORM`, `POLLRDBAND` and `POLLWRBAND` (the `POLLERR`, `POLLHUP` and `POLLNVAL` bits are ignored in `events`), an infinite timeout with no socket or pipe to wait on. A closed socket descriptor reports `POLLNVAL`; the console's limit on the descriptor count is unknown and not checked - [kevent on a pipe](../../../core/libs/prx/libkernel/Equeue/src/Kqueue.cpp) (libkernel) - `EVFILT_READ` on the read end of a pipe follows FreeBSD 11's [`filt_piperead`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/sys_pipe.c#L1787-L1806): `data` is the number of buffered bytes, and `EV_EOF` is set once the write end is closed. Windows pipes cannot be waited on, so while a kqueue waits on a pipe it checks it with `NtQueryInformationFile` and waits on its sockets in slices of 1 ms; a wait with no pipe is not sliced. `EVFILT_READ` on the write end of a pipe throws: FreeBSD's `pipe` opens both ends for reading and writing ([`kern_pipe`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/sys_pipe.c#L422)), and host pipes are one-way. `EVFILT_WRITE` on a pipe throws too, and `NOTE_LOWAT` on a pipe is ignored, as `filt_piperead` ignores it. - [Socket error numbers](../../../core/libs/prx/libkernel/Socket/include/SocketError.hpp) (libkernel) - a host socket error becomes the FreeBSD 11 `errno.h` number of the same name; the numbers are not checked on the console. Winsock `WSAESHUTDOWN` becomes `EPIPE`, as a send after `shutdown` does on FreeBSD, and a host error with no FreeBSD socket counterpart becomes `EIO`. A nonblocking `connect` in progress reports `EINPROGRESS` on Linux but `EAGAIN` on Windows (`WSAEWOULDBLOCK`), where FreeBSD reports `EINPROGRESS`. On Windows a refused nonblocking `connect` is never reported (`SO_ERROR` is unsupported), so a repeated one keeps reporting `EALREADY` where FreeBSD reports `ECONNREFUSED`. A receive after `shutdown(SHUT_RD)` reports `EPIPE` on Windows where FreeBSD returns 0 - [getsockopt, setsockopt](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) on a guest socket handle the SOL_SOCKET level and `TCP_NODELAY`, after FreeBSD 11.4's [`sosetopt` and `sogetopt`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c): `SO_REUSEADDR`, `SO_KEEPALIVE`, `SO_DONTROUTE`, `SO_BROADCAST`, `SO_OOBINLINE` and `SO_LINGER` act on the host socket, and `SO_ERROR`, `SO_TYPE`, `SO_ACCEPTCONN`, `SO_SNDBUF` and `SO_RCVBUF` are read. A boolean option reads back as its own bit, as FreeBSD returns `so_options & option`. Winsock does not clear a socket's pending error when it is read, so on Windows the library reports a pending error once and shows it again only after a new `connect`; a second failure with the same code on the same socket without a `connect` in between would be hidden. `SO_SNDBUF` and `SO_RCVBUF` read the host's value, which is not the size FreeBSD reports (Linux doubles it). `SO_NOSIGPIPE` is kept per socket and reads back as its bit. On Linux a `send`, `sendto`, `sendmsg` or `write` on that socket passes `MSG_NOSIGNAL` to the host, as [`kern_sendit`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_syscalls.c#L890-L895) and [`soo_write`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/sys_socket.c#L147-L151) leave out the signal. An accepted socket takes it from its listener, as [`sonewconn`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L603) copies the options. `SO_NBIO` (`0x1200`) is the console's own option, not FreeBSD's. It switches the socket between blocking and non-blocking as `FIONBIO` does and reads back as 0 or 1, as `sceNetSetsockopt` and `sceNetGetsockopt` treat it here; what the console reads back for it is not measured. `TCP_NODELAY` (`IPPROTO_TCP`, 1) is set on the host socket and reads back as `TF_NODELAY` (4), as [`tcp_default_ctloutput`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/netinet/tcp_usrreq.c#L1860-L1861) returns it. The `IPPROTO_TCP` level on a datagram or `AF_UNIX` socket fails with `EINVAL`, as [`ip_ctloutput`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/netinet/ip_output.c#L943-L944) and [`uipc_ctloutput`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_usrreq.c#L1228-L1229) answer it, and an accepted socket takes `TCP_NODELAY` through the host. `SO_SNDTIMEO` and `SO_RCVTIMEO` follow [`sosetopt`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L2657-L2689) and [`sogetopt`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L2846-L2850). A 16-byte `timeval` with a negative field or 1000000 microseconds or more fails with `EDOM`, and more than `INT32_MAX` seconds becomes the longest wait. The value is kept and read back through FreeBSD's 32.32 fixed-point conversion ([`tvtosbt`, `sbttotv`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/sys/time.h#L178-L286)), so half a second reads back as 499999 microseconds and one microsecond as 0. A `recv`, `recvfrom`, `recvmsg`, `read` or `readv`, and a `send`, `sendto`, `sendmsg`, `write` or `writev`, on a blocking socket then waits that long for the host to report the socket readable or writable, and fails with `EWOULDBLOCK`; a call with `MSG_DONTWAIT` does not wait. An accepted socket takes both from its listener ([`sonewconn`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L629-L630)). The wait covers only the first transfer: once the socket is writable the host send runs without a limit, where FreeBSD stops a send that cannot finish and returns the bytes sent so far, and a receive whose data another thread takes between the wait and the host call blocks again. `sendfile` ignores the send timeout. The host wait has millisecond steps, so a shorter timeout lasts up to a millisecond longer. A null value fails with `EINVAL`, where FreeBSD's [`kern_setsockopt`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_syscalls.c#L1414-L1415) returns `EFAULT`. Every other SOL_SOCKET option (`SO_DEBUG`, `SO_USELOOPBACK`, `SO_REUSEPORT` and `SO_TIMESTAMP`, which FreeBSD stores and no title has been seen to set, the low-water marks among them), every other `IPPROTO_TCP` option (`TCP_MAXSEG`, `TCP_KEEPIDLE`, `TCP_KEEPINTVL` and `TCP_KEEPCNT` among them) and every other level (`IPPROTO_IP`, `IPPROTO_IPV6`) fail with `ENOPROTOOPT`. None of this was measured on a console. From aabd0e77aa4dfcdfac8b755b5496d1bc1f299f52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexej=20Szpev=C3=A1k?= Date: Sat, 17 Oct 2026 15:14:44 +0200 Subject: [PATCH 0892/1000] fix(libkernel): answer select on files and the random device as FreeBSD's poll routines do selscan asks a descriptor once, with the flags of every set it is in (POLLRDNORM for the read set, POLLWRNORM for the write set, POLLRDBAND for the exception set, plus POLLHUP and POLLERR), and marks each set whose flags the answer shares. A seeded random device's randomdev_poll narrows a request with POLLRDNORM to that bit and returns any other request whole, so in the read set it is readable and in no other set, and otherwise it is in each write and exception set it was asked in; select reported it readable and writable together and never exceptional. A regular file or directory went to the host poll, which on Windows threw; vop_stdpoll makes it readable and writable and never exceptional, which select now answers in process on every host, as poll does. --- core/libs/prx/libkernel/Socket/src/Socket.cpp | 4 ++- core/libs/tests/GuestSelect.cpp | 35 +++++++++++++++++++ docs/dev/debt/libkernel.md | 2 +- 3 files changed, 39 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libkernel/Socket/src/Socket.cpp b/core/libs/prx/libkernel/Socket/src/Socket.cpp index c4adb38dfd..b4a0ceb699 100644 --- a/core/libs/prx/libkernel/Socket/src/Socket.cpp +++ b/core/libs/prx/libkernel/Socket/src/Socket.cpp @@ -3,6 +3,7 @@ #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" #include "prx/libc/include/PreciseWait.hpp" +#include "prx/libkernel/File/include/DescriptorControl.hpp" #include "prx/libkernel/File/include/File.hpp" #include "prx/libkernel/File/include/RandomDevice.hpp" #include "prx/libkernel/Socket/include/SocketPoll.hpp" @@ -193,8 +194,9 @@ int APS5_VABI select_nid_postfix(int nfds, void* readfds, void* writefds, void* kernel.entries.push_back({descriptor, events, 0}); kernel.slots.push_back(requested.size()); requested.push_back({descriptor, events, 0}); - } else if (File::IsRandomDevice(descriptor)) devices.push_back({descriptor, events, static_cast(events & (KernelSocketPoll::Readable | KernelSocketPoll::Writable))}); + } else if (File::IsRandomDevice(descriptor)) devices.push_back({descriptor, events, static_cast((events & KernelSocketPoll::Readable) != 0 ? KernelSocketPoll::Readable : events)}); else if (descriptor <= 2) devices.push_back({descriptor, events, static_cast(descriptor == 0 ? 0 : events & KernelSocketPoll::Writable)}); + else if (File::IsVnode(descriptor)) devices.push_back({descriptor, events, static_cast(events & (KernelSocketPoll::Readable | KernelSocketPoll::Writable))}); else { net.entries.push_back({descriptor, events, 0}); net.slots.push_back(requested.size()); diff --git a/core/libs/tests/GuestSelect.cpp b/core/libs/tests/GuestSelect.cpp index 466d204c8c..35fe5cea49 100644 --- a/core/libs/tests/GuestSelect.cpp +++ b/core/libs/tests/GuestSelect.cpp @@ -3,6 +3,7 @@ #include #include #include +#include struct GuestTimeval { std::int64_t seconds; @@ -33,6 +34,8 @@ std::int64_t APS5_VABI send_nid_postfix(int, const void*, std::uint64_t, int); std::int64_t APS5_VABI recv_nid_postfix(int, void*, std::uint64_t, int); std::int64_t APS5_VABI write_nid_postfix(int, const char*, std::int64_t); std::int64_t APS5_VABI read_nid_postfix(int, void*, std::uint64_t); +int APS5_VABI open_nid_postfix(const char*, int, int); +int APS5_VABI unlink_nid_postfix(const char*); int* APS5_VABI __error_nid_postfix(); } @@ -88,6 +91,38 @@ int main() { Require(buffer[0] == byte); } + { + const std::string path = "anyps5-select-file-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()); + const int file = open_nid_postfix(path.c_str(), 0x0002 | 0x0200, 0644); + const int random = open_nid_postfix("/dev/urandom", 0, 0); + Require(file >= 0 && random >= 0); + GuestDescriptorSet read{}; + GuestDescriptorSet write{}; + GuestDescriptorSet except{}; + for (const int descriptor : {file, random}) { + read.Add(descriptor); + write.Add(descriptor); + except.Add(descriptor); + } + read.Add(reader); + const int highest = file > random ? (file > reader ? file : reader) : (random > reader ? random : reader); + const GuestTimeval zero{0, 0}; + Require(select_nid_postfix(highest + 1, read.words.data(), write.words.data(), except.words.data(), &zero) == 3); + Require(read.Has(file) && write.Has(file) && !except.Has(file)); + Require(read.Has(random) && !write.Has(random) && !except.Has(random)); + GuestDescriptorSet randomWrite{}; + GuestDescriptorSet randomExcept{}; + randomWrite.Add(random); + randomExcept.Add(random); + Require(select_nid_postfix(random + 1, nullptr, randomWrite.words.data(), randomExcept.words.data(), &zero) == 2); + Require(randomWrite.Has(random) && randomExcept.Has(random)); + Require(!read.Has(reader)); + GuestDescriptorSet onlyExcept{}; + onlyExcept.Add(file); + Require(select_nid_postfix(file + 1, nullptr, nullptr, onlyExcept.words.data(), &zero) == 0 && !onlyExcept.Has(file)); + Require(close_nid_postfix(random) == 0 && close_nid_postfix(file) == 0 && unlink_nid_postfix(path.c_str()) == 0); + } + { GuestDescriptorSet write{}; write.Add(writer); diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 78581cfca8..5c2d8946cc 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -64,7 +64,7 @@ - [Mapping hints with no room above them](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (libkernel) - a mapping without `MAP_FIXED` whose hint lies above the guest address space, or has no free range at or above it, is placed from the bottom of the guest arena, as FreeBSD 11.4's [`vm_mmap_object`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/vm/vm_mmap.c#L1569) retries through [`vm_map_find_min`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/vm/vm_map.c#L1555-L1584) from its minimum address. FreeBSD's minimum is the end of the largest possible data segment rather than the start of the arena, and the console's address-space layout is not measured. PS5CEMU-HAR (PPSA99360) gives `sceKernelReserveVirtualRange` a hint near its own code, which is a host address here because the relinked executable lies outside the guest arena, so its JIT gets memory more than 2 GiB from its code and does not start - [_sceKernelRtldThreadAtexitIncrement, _sceKernelRtldThreadAtexitDecrement](../../../core/libs/prx/libkernel/System/src/Rtld.cpp) (libkernel) - the argument is the `dso` address a title's `libc.prx` passes to `__cxa_thread_atexit` (PPSA14632), which fails when Increment returns nonzero. As in the [fpPS4](https://github.com/red-prig/fpPS4) reimplementation, Increment takes a reference on the loaded image containing the address and Decrement releases one; the variants without the leading underscore are assumed to match - [tcgetattr, tcsetattr](../../../core/libs/prx/libkernel/File/src/Terminal.cpp) (libkernel) - every file and pipe descriptor is taken as not a terminal (`ENOTTY`), and a socket gets `EADDRNOTAVAIL` for `AF_INET` and `EOPNOTSUPP` otherwise, as FreeBSD's `in_control` and `pru_control_notsupp` answer a termios ioctl; what descriptors 0 to 2 of a title are on the console, and whether any of them is a terminal there, is unknown -- [select](../../../core/libs/prx/libkernel/Socket/src/Socket.cpp) (libkernel) - descriptor 0 is never reported readable and descriptors 1 and 2 are always writable, as for a console title with no input; reads from descriptor 0 still go to the host stdin. `/dev/random` and `/dev/urandom` are always readable and writable, as FreeBSD reports a seeded random device. Guest sockets and libSceNet sockets are polled; other descriptors are polled through the host on Linux, while on Windows pipes are checked every millisecond with the readiness test `poll` uses and any other descriptor throws. Dungeons (PPSA16064) selects on descriptor 0, and RetroArch (PPSA99169) selects on its web server's wake-up pipe next to its listening socket +- [select](../../../core/libs/prx/libkernel/Socket/src/Socket.cpp) (libkernel) - descriptor 0 is never reported readable and descriptors 1 and 2 are always writable, as for a console title with no input; reads from descriptor 0 still go to the host stdin. `/dev/random` and `/dev/urandom` follow `randomdev_poll` for a seeded device under [`selscan`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/sys_generic.c#L1178-L1236), which asks once with the flags of every set the descriptor is in: in the read set they are readable and reported in no other set, and otherwise they are reported in the write and exception sets they were asked in. A regular file, a directory or a file read from app0.zar is always readable and writable and never in the exception set, as `vop_stdpoll` answers, on every host. Guest sockets and libSceNet sockets are polled; other descriptors are polled through the host on Linux, while on Windows pipes are checked every millisecond with the readiness test `poll` uses and any other descriptor throws. Dungeons (PPSA16064) selects on descriptor 0, and RetroArch (PPSA99169) selects on its web server's wake-up pipe next to its listening socket - [select](../../../core/libs/prx/libkernel/Socket/src/Socket.cpp) (libkernel) - a descriptor the socket poller reports as unknown that is below `GuestSockets::FirstDescriptor` is polled on the host instead of throwing (on Windows only a pipe, as above); an invalid host descriptor fails with `EBADF`, and mixed host and socket sets are polled in short alternating slices so neither side waits out the full slice alone. Wobbly Life (PPSA29413) selects on file descriptors while streaming its bundles - [__progname](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - unknown data export - [environ](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - data export pointing to a fixed empty environment so titles that import it load; it is not the environment libc's `getenv` reads, so it lists none of those variables and `setenv`/`putenv` changes do not appear in it From 74629458d38c25f1b7d5c43ce3276ba38c1a24cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexej=20Szpev=C3=A1k?= Date: Sat, 17 Oct 2026 15:15:13 +0200 Subject: [PATCH 0893/1000] feat(libkernel): poll descriptors 0 to 2 with the console model select uses select takes descriptors 0 to 2 as a console title's terminal with no input: 0 is never readable and 1 and 2 are always writable. poll threw for them unless the host happened to attach a pipe, so the same title could select on its standard streams but not poll them. poll now gives the same answer, with POLLOUT, the one bit ttydev_poll reports for a writable terminal, for 1 and 2 and nothing for 0. --- core/libs/prx/libkernel/Socket/src/Udp.cpp | 5 +++++ core/libs/tests/GuestPoll.cpp | 7 +++++++ docs/dev/debt/libkernel.md | 2 +- 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index 0d08456992..e02d0212cf 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -1424,6 +1424,11 @@ int APS5_VABI poll_nid_postfix(GuestPollDescriptor* descriptors, std::uint32_t c if (entry.revents) ++ready; continue; } + if (entry.descriptor <= 2) { + entry.revents = static_cast(entry.descriptor == 0 ? 0 : entry.events & GuestPollWritable); + if (entry.revents) ++ready; + continue; + } if (File::IsVnode(entry.descriptor)) { entry.revents = static_cast(entry.events & GuestPollVnodeReady); if (entry.revents) ++ready; diff --git a/core/libs/tests/GuestPoll.cpp b/core/libs/tests/GuestPoll.cpp index 5d229666e8..131e75eb9d 100644 --- a/core/libs/tests/GuestPoll.cpp +++ b/core/libs/tests/GuestPoll.cpp @@ -268,6 +268,13 @@ int main() { Require(poll_nid_postfix(mixed, 2, -1) == 1 && mixed[0].revents == 0 && mixed[1].revents == Out); Require(close_nid_postfix(random) == 0); } + PollDescriptor console[3]{{0, static_cast(In | ReadNormal | Out), -1}, {1, static_cast(In | Out | WriteBand), -1}, {2, Out, -1}}; + Require(poll_nid_postfix(console, 3, 0) == 2); + Require(console[0].revents == 0 && console[1].revents == Out && console[2].revents == Out); + PollDescriptor input{0, In, -1}; + const auto inputStart = std::chrono::steady_clock::now(); + Require(poll_nid_postfix(&input, 1, 30) == 0 && input.revents == 0); + Require(std::chrono::steady_clock::now() - inputStart >= std::chrono::milliseconds(25)); const int queue = kqueue_nid_postfix(); Require(queue >= 0); PollDescriptor unknown[2]{{listener, In, 0}, {queue, In, 0}}; diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 5c2d8946cc..44d9a9d34b 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -173,7 +173,7 @@ - [read, write and vector I/O](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) on a guest socket use `recv` and `send` without flags; `sceKernelReadv` and `sceKernelWritev` gather or scatter through one socket operation, preserving stream short reads and datagram boundaries. The positioned `pread`, `pwrite`, `preadv` and `pwritev` forms, and their `sceKernel` forms, return `ESPIPE` for an open guest socket and `EBADF` for a closed one. `EPIPE` from a write raises the host's `SIGPIPE` on Linux unless the socket has `SO_NOSIGPIPE` set, and the guest cannot ignore it: `signal(SIGPIPE, SIG_IGN)` and `sigaction` record the disposition, as they do for every number from 1 to 128, but only the six signals with host equivalents (2, 4, 6, 8, 11 and 15) take effect on the host or are delivered, so the host `SIGPIPE` is not stopped; nothing is raised on Windows; a `write` longer than `INT_MAX` bytes is `EMSGSIZE` where the console sends it. AIO on a guest socket still goes to the host, which has no such descriptor - [Socket descriptor numbers](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel, libSceNet) - `socket`, `accept`, `socketpair`, `dup`, `sceNetSocket` and `sceNetAccept` take the lowest free number of the host descriptor table that files use, as FreeBSD 11.4 `fdalloc` takes the lowest free entry of the one process table (`kern_descrip.c`), so every number stays below `FD_SETSIZE` (1024) while fewer than 1024 descriptors are open. The number holds a zero-access `NUL` handle on Windows and a duplicate of the host socket on Linux, and the socket tables map it to the socket. The host table also holds the runtime's own files, so the numbers differ from the console's; the first number `socket` and `sceNetSocket` return on a console and its `getdtablesize` are not measured. Paths that bypass the socket tables see the placeholder: libc `fdopen` on a socket opens it, and libkernel `close` of a libSceNet socket releases the number but leaves the libSceNet socket open. Palworld (PPSA23126), The Mound (PPSA26223) and Dying Light 2 (PPSA02262) set socket bits in a 1024-bit `fd_set` - [fstat on a guest socket](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) fills the fields FreeBSD 11's soo_stat and pru_sense_null fill and zeroes the rest: S_IFSOCK, the read bits while the receive side is open or data is queued, the write bits while the send side is open, st_size and st_blksize. Only this process's shutdown closes a side here; a peer's shutdown, close or reset does not clear the bits as FreeBSD's socantrcvmore and socantsendmore do. st_size is the host's FIONREAD, which for a Linux datagram socket is the size of the next datagram where FreeBSD counts every queued byte, and st_blksize is the host's SO_SNDBUF, which Linux reports as twice the size that was set. A socketpair end reports st_dev and st_ino 0, where FreeBSD's uipc_sense reports NODEV and a per-socket number -- [poll](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) waits on guest sockets and pipes, through `WSAPoll` on Windows and `poll` on Linux; the `pollfd` layout and flag values are FreeBSD's. A libSceNet socket joins the same host wait through the native socket libSceNet lends to libkernel. `WSAPoll` rejects `POLLPRI`, so on Windows a socket's `POLLPRI` is asked as `POLLRDBAND` and `POLLIN` as `POLLRDNORM`; Winsock reports urgent data until it is read with `MSG_OOB`, where FreeBSD 11.4's `sopoll_generic` stops once the normal stream passes the mark. Guest `send` and `recv` refuse `MSG_OOB` with `EOPNOTSUPP`, so urgent data can only come from an outside peer and Windows keeps reporting `POLLPRI` after the stream passes the mark. A pipe follows FreeBSD's `pipe_poll`: the read end is readable when data is buffered, either end reports `POLLHUP` once the other end is closed, with the requested `POLLIN` and `POLLRDNORM` bits and without `POLLOUT`, which `pollscan` clears, and the write end is writable when `PIPE_BUF` (512) bytes are free on Windows, or when the host kernel says so on Linux. `POLLOUT` on a read end throws: FreeBSD opens both ends for reading and writing and reports it writable, and host pipes are one-way. Windows pipes cannot be waited on, so while a pipe is polled `poll` checks it with `NtQueryInformationFile` and waits on the sockets in slices of 1 ms. A regular file, a directory or a file read from app0.zar is always ready for `POLLIN`, `POLLRDNORM` and `POLLOUT` and never for `POLLPRI`, `POLLRDBAND` or `POLLWRBAND`, as FreeBSD 11.4's [`vop_stdpoll`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/vfs_default.c#L563-L574) and `vop_nopoll` answer every event `poll` accepts; which of the two the console's filesystems use is not known, and they agree on these events. `/dev/random` and `/dev/urandom` follow FreeBSD 11.4's [`randomdev_poll`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/dev/random/randomdev.c#L274-L285) for a seeded device: a request with `POLLIN` or `POLLRDNORM` gets only those bits back, and any other request gets every requested bit. A closed descriptor reports `POLLNVAL`, and an open descriptor that is neither a guest socket, a libSceNet socket, a pipe, a file, a directory nor the random device (stdio, other devices) throws, as do event bits other than `POLLIN`, `POLLPRI`, `POLLOUT`, `POLLRDNORM`, `POLLRDBAND` and `POLLWRBAND` (the `POLLERR`, `POLLHUP` and `POLLNVAL` bits are ignored in `events`), an infinite timeout with no socket or pipe to wait on. A closed socket descriptor reports `POLLNVAL`; the console's limit on the descriptor count is unknown and not checked +- [poll](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) waits on guest sockets and pipes, through `WSAPoll` on Windows and `poll` on Linux; the `pollfd` layout and flag values are FreeBSD's. A libSceNet socket joins the same host wait through the native socket libSceNet lends to libkernel. `WSAPoll` rejects `POLLPRI`, so on Windows a socket's `POLLPRI` is asked as `POLLRDBAND` and `POLLIN` as `POLLRDNORM`; Winsock reports urgent data until it is read with `MSG_OOB`, where FreeBSD 11.4's `sopoll_generic` stops once the normal stream passes the mark. Guest `send` and `recv` refuse `MSG_OOB` with `EOPNOTSUPP`, so urgent data can only come from an outside peer and Windows keeps reporting `POLLPRI` after the stream passes the mark. A pipe follows FreeBSD's `pipe_poll`: the read end is readable when data is buffered, either end reports `POLLHUP` once the other end is closed, with the requested `POLLIN` and `POLLRDNORM` bits and without `POLLOUT`, which `pollscan` clears, and the write end is writable when `PIPE_BUF` (512) bytes are free on Windows, or when the host kernel says so on Linux. `POLLOUT` on a read end throws: FreeBSD opens both ends for reading and writing and reports it writable, and host pipes are one-way. Windows pipes cannot be waited on, so while a pipe is polled `poll` checks it with `NtQueryInformationFile` and waits on the sockets in slices of 1 ms. A regular file, a directory or a file read from app0.zar is always ready for `POLLIN`, `POLLRDNORM` and `POLLOUT` and never for `POLLPRI`, `POLLRDBAND` or `POLLWRBAND`, as FreeBSD 11.4's [`vop_stdpoll`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/vfs_default.c#L563-L574) and `vop_nopoll` answer every event `poll` accepts; which of the two the console's filesystems use is not known, and they agree on these events. `/dev/random` and `/dev/urandom` follow FreeBSD 11.4's [`randomdev_poll`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/dev/random/randomdev.c#L274-L285) for a seeded device: a request with `POLLIN` or `POLLRDNORM` gets only those bits back, and any other request gets every requested bit. Open descriptors 0 to 2 follow the console model `select` uses: descriptor 0 never reports an event and descriptors 1 and 2 report `POLLOUT`, whatever the host attaches to them. A closed descriptor reports `POLLNVAL`, and an open descriptor that is neither a guest socket, a libSceNet socket, a pipe, a file, a directory, the random device nor descriptor 0 to 2 (other devices, a kqueue) throws, as do event bits other than `POLLIN`, `POLLPRI`, `POLLOUT`, `POLLRDNORM`, `POLLRDBAND` and `POLLWRBAND` (the `POLLERR`, `POLLHUP` and `POLLNVAL` bits are ignored in `events`), an infinite timeout with no socket or pipe to wait on. A closed socket descriptor reports `POLLNVAL`; the console's limit on the descriptor count is unknown and not checked - [kevent on a pipe](../../../core/libs/prx/libkernel/Equeue/src/Kqueue.cpp) (libkernel) - `EVFILT_READ` on the read end of a pipe follows FreeBSD 11's [`filt_piperead`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/sys_pipe.c#L1787-L1806): `data` is the number of buffered bytes, and `EV_EOF` is set once the write end is closed. Windows pipes cannot be waited on, so while a kqueue waits on a pipe it checks it with `NtQueryInformationFile` and waits on its sockets in slices of 1 ms; a wait with no pipe is not sliced. `EVFILT_READ` on the write end of a pipe throws: FreeBSD's `pipe` opens both ends for reading and writing ([`kern_pipe`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/sys_pipe.c#L422)), and host pipes are one-way. `EVFILT_WRITE` on a pipe throws too, and `NOTE_LOWAT` on a pipe is ignored, as `filt_piperead` ignores it. - [Socket error numbers](../../../core/libs/prx/libkernel/Socket/include/SocketError.hpp) (libkernel) - a host socket error becomes the FreeBSD 11 `errno.h` number of the same name; the numbers are not checked on the console. Winsock `WSAESHUTDOWN` becomes `EPIPE`, as a send after `shutdown` does on FreeBSD, and a host error with no FreeBSD socket counterpart becomes `EIO`. A nonblocking `connect` in progress reports `EINPROGRESS` on Linux but `EAGAIN` on Windows (`WSAEWOULDBLOCK`), where FreeBSD reports `EINPROGRESS`. On Windows a refused nonblocking `connect` is never reported (`SO_ERROR` is unsupported), so a repeated one keeps reporting `EALREADY` where FreeBSD reports `ECONNREFUSED`. A receive after `shutdown(SHUT_RD)` reports `EPIPE` on Windows where FreeBSD returns 0 - [getsockopt, setsockopt](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) on a guest socket handle the SOL_SOCKET level and `TCP_NODELAY`, after FreeBSD 11.4's [`sosetopt` and `sogetopt`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c): `SO_REUSEADDR`, `SO_KEEPALIVE`, `SO_DONTROUTE`, `SO_BROADCAST`, `SO_OOBINLINE` and `SO_LINGER` act on the host socket, and `SO_ERROR`, `SO_TYPE`, `SO_ACCEPTCONN`, `SO_SNDBUF` and `SO_RCVBUF` are read. A boolean option reads back as its own bit, as FreeBSD returns `so_options & option`. Winsock does not clear a socket's pending error when it is read, so on Windows the library reports a pending error once and shows it again only after a new `connect`; a second failure with the same code on the same socket without a `connect` in between would be hidden. `SO_SNDBUF` and `SO_RCVBUF` read the host's value, which is not the size FreeBSD reports (Linux doubles it). `SO_NOSIGPIPE` is kept per socket and reads back as its bit. On Linux a `send`, `sendto`, `sendmsg` or `write` on that socket passes `MSG_NOSIGNAL` to the host, as [`kern_sendit`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_syscalls.c#L890-L895) and [`soo_write`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/sys_socket.c#L147-L151) leave out the signal. An accepted socket takes it from its listener, as [`sonewconn`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L603) copies the options. `SO_NBIO` (`0x1200`) is the console's own option, not FreeBSD's. It switches the socket between blocking and non-blocking as `FIONBIO` does and reads back as 0 or 1, as `sceNetSetsockopt` and `sceNetGetsockopt` treat it here; what the console reads back for it is not measured. `TCP_NODELAY` (`IPPROTO_TCP`, 1) is set on the host socket and reads back as `TF_NODELAY` (4), as [`tcp_default_ctloutput`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/netinet/tcp_usrreq.c#L1860-L1861) returns it. The `IPPROTO_TCP` level on a datagram or `AF_UNIX` socket fails with `EINVAL`, as [`ip_ctloutput`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/netinet/ip_output.c#L943-L944) and [`uipc_ctloutput`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_usrreq.c#L1228-L1229) answer it, and an accepted socket takes `TCP_NODELAY` through the host. `SO_SNDTIMEO` and `SO_RCVTIMEO` follow [`sosetopt`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L2657-L2689) and [`sogetopt`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L2846-L2850). A 16-byte `timeval` with a negative field or 1000000 microseconds or more fails with `EDOM`, and more than `INT32_MAX` seconds becomes the longest wait. The value is kept and read back through FreeBSD's 32.32 fixed-point conversion ([`tvtosbt`, `sbttotv`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/sys/time.h#L178-L286)), so half a second reads back as 499999 microseconds and one microsecond as 0. A `recv`, `recvfrom`, `recvmsg`, `read` or `readv`, and a `send`, `sendto`, `sendmsg`, `write` or `writev`, on a blocking socket then waits that long for the host to report the socket readable or writable, and fails with `EWOULDBLOCK`; a call with `MSG_DONTWAIT` does not wait. An accepted socket takes both from its listener ([`sonewconn`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L629-L630)). The wait covers only the first transfer: once the socket is writable the host send runs without a limit, where FreeBSD stops a send that cannot finish and returns the bytes sent so far, and a receive whose data another thread takes between the wait and the host call blocks again. `sendfile` ignores the send timeout. The host wait has millisecond steps, so a shorter timeout lasts up to a millisecond longer. A null value fails with `EINVAL`, where FreeBSD's [`kern_setsockopt`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_syscalls.c#L1414-L1415) returns `EFAULT`. Every other SOL_SOCKET option (`SO_DEBUG`, `SO_USELOOPBACK`, `SO_REUSEPORT` and `SO_TIMESTAMP`, which FreeBSD stores and no title has been seen to set, the low-water marks among them), every other `IPPROTO_TCP` option (`TCP_MAXSEG`, `TCP_KEEPIDLE`, `TCP_KEEPINTVL` and `TCP_KEEPCNT` among them) and every other level (`IPPROTO_IP`, `IPPROTO_IPV6`) fail with `ENOPROTOOPT`. None of this was measured on a console. From 6d4c7c31506a6e6cc7617da7eff3f51a08c17851 Mon Sep 17 00:00:00 2001 From: HQasper <52220032+HQasper@users.noreply.github.com> Date: Sat, 17 Oct 2026 21:10:48 +0200 Subject: [PATCH 0894/1000] fix(libkernel): select on kqueues as kqueue_poll answers A kqueue descriptor is readable while one of its events is triggered and is never writable or exceptional, as FreeBSD's kqueue_poll answers, on both hosts. On Windows select threw for it, and on Linux its /dev/null placeholder was reported readable and writable. A descriptor that stops being a socket, a kqueue or a pipe during the wait now fails the call with EBADF when it was closed and is selected again with the rest of the timeout when its number was reused, as FreeBSD's kern_select looks every descriptor up again in each pass, instead of throwing. On Windows a host descriptor that is not a pipe throws when select starts, before waiting. Depends on #7139. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libkernel/Equeue/Equeue.cpp | 10 ++++ core/libs/prx/libkernel/Equeue/Equeue.hpp | 3 + core/libs/prx/libkernel/Equeue/src/Kqueue.cpp | 6 ++ core/libs/prx/libkernel/Socket/src/Socket.cpp | 57 ++++++++++++++---- core/libs/tests/GuestSelect.cpp | 58 +++++++++++++++++++ docs/dev/debt/libkernel.md | 4 +- 6 files changed, 126 insertions(+), 12 deletions(-) diff --git a/core/libs/prx/libkernel/Equeue/Equeue.cpp b/core/libs/prx/libkernel/Equeue/Equeue.cpp index 249f2959f0..22d850be74 100644 --- a/core/libs/prx/libkernel/Equeue/Equeue.cpp +++ b/core/libs/prx/libkernel/Equeue/Equeue.cpp @@ -208,6 +208,16 @@ int KernelEqueuePrivate::GetTriggeredEvents(KernelEvent* ev, int num) { return CollectTriggered(ev, num); } +bool KernelEqueuePrivate::HasTriggeredEvents() { + std::unique_lock lock(m_mutex); + if (m_closed) { + return false; + } + TriggerExpiredTimers(MonotonicNs()); + PollEvents(); + return std::any_of(m_events.begin(), m_events.end(), [](const KernelEqueueEvent& event) { return event.enabled && event.triggered; }); +} + int KernelEqueuePrivate::WaitForEvents(KernelEvent* ev, int num, uint32_t micros) { return WaitForEventsUntil(ev, num, micros == 0 ? 0 : TimedWait::DeadlineNanos(micros)); } diff --git a/core/libs/prx/libkernel/Equeue/Equeue.hpp b/core/libs/prx/libkernel/Equeue/Equeue.hpp index 39ad8bf12d..61096a5136 100644 --- a/core/libs/prx/libkernel/Equeue/Equeue.hpp +++ b/core/libs/prx/libkernel/Equeue/Equeue.hpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -84,6 +85,7 @@ public: bool HasEvent(uintptr_t ident, int16_t filter); int EditEvent(uintptr_t ident, int16_t filter, const std::function& edit); int GetTriggeredEvents(KernelEvent* ev, int num); + bool HasTriggeredEvents(); int WaitForEvents(KernelEvent* ev, int num, uint32_t micros); int WaitForEventsUntil(KernelEvent* ev, int num, std::uint64_t deadlineNanos); void RemoveDescriptorEvents(uintptr_t ident); @@ -113,6 +115,7 @@ using KernelEqueueRef = std::shared_ptr; void EqueueDescriptorClosed(int descriptor); void KqueueDescriptorClosed(int descriptor); bool IsKqueueDescriptor(int descriptor); +std::optional KqueueReadable(int descriptor); KernelEqueueEvent EqueueDescriptorEvent(int descriptor, int16_t filter, uint16_t flags, void* udata); void RequireVnodeNotes(uint32_t fflags); void SetVnodeNotes(KernelEqueueEvent& event, uint32_t fflags); diff --git a/core/libs/prx/libkernel/Equeue/src/Kqueue.cpp b/core/libs/prx/libkernel/Equeue/src/Kqueue.cpp index 05cca27e4c..20859f0b23 100644 --- a/core/libs/prx/libkernel/Equeue/src/Kqueue.cpp +++ b/core/libs/prx/libkernel/Equeue/src/Kqueue.cpp @@ -317,6 +317,12 @@ bool IsKqueueDescriptor(int descriptor) { return static_cast(FindKqueue(descriptor)); } +std::optional KqueueReadable(int descriptor) { + const auto queue = FindKqueue(descriptor); + if (!queue) return std::nullopt; + return queue->HasTriggeredEvents(); +} + void KqueueDescriptorClosed(int descriptor) { KernelEqueueRef closed; std::vector queues; diff --git a/core/libs/prx/libkernel/Socket/src/Socket.cpp b/core/libs/prx/libkernel/Socket/src/Socket.cpp index b4a0ceb699..6cc428f617 100644 --- a/core/libs/prx/libkernel/Socket/src/Socket.cpp +++ b/core/libs/prx/libkernel/Socket/src/Socket.cpp @@ -3,6 +3,7 @@ #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" #include "prx/libc/include/PreciseWait.hpp" +#include "prx/libkernel/Equeue/Equeue.hpp" #include "prx/libkernel/File/include/DescriptorControl.hpp" #include "prx/libkernel/File/include/File.hpp" #include "prx/libkernel/File/include/RandomDevice.hpp" @@ -14,6 +15,7 @@ #include #include #include +#include #include #include @@ -43,11 +45,6 @@ bool DescriptorSet(const void* set, int descriptor) { return set != nullptr && ((static_cast(set)[descriptor / 64] >> (descriptor % 64)) & 1u) != 0; } -void UnknownDescriptor(int descriptor) { - const auto what = "select on descriptor " + std::to_string(descriptor) + " (not a libSceNet socket)"; - NotImplemented_nid_no_patch(what.c_str()); -} - void ClearDescriptor(void* set, int descriptor) { if (set != nullptr) static_cast(set)[descriptor / 64] &= ~(std::uint64_t{1} << (descriptor % 64)); } @@ -62,6 +59,21 @@ int KernelSockets(KernelSocketPoll::Entry* entries, int count, int timeoutMillis return GuestSockets::Poll(entries, count, timeoutMilliseconds); } +int Kqueues(KernelSocketPoll::Entry* entries, int count, int timeoutMilliseconds) { + const auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(timeoutMilliseconds); + for (;;) { + int ready = 0; + for (int index = 0; index < count; ++index) { + auto& entry = entries[index]; + const auto readable = KqueueReadable(entry.descriptor); + entry.revents = !readable ? KernelSocketPoll::Unknown : *readable ? static_cast(entry.events & KernelSocketPoll::Readable) : short{0}; + if (entry.revents != 0) ++ready; + } + if (ready > 0 || std::chrono::steady_clock::now() >= deadline) return ready; + PreciseSleepUntil(std::min(deadline, std::chrono::steady_clock::now() + std::chrono::milliseconds(1))); + } +} + #ifndef _WIN32 short HostEventFlags(short events) { @@ -105,9 +117,7 @@ int HostPoll(KernelSocketPoll::Entry* descriptors, int count, int timeoutMillise for (int index = 0; index < count; ++index) { auto& entry = descriptors[index]; if (!DescriptorIsOpen_nid_no_patch(entry.descriptor)) return -GuestEbadf; - if (!GuestSockets::PipeReady(entry.descriptor, entry.events, entry.revents)) { - NotImplemented_nid_no_patch(("select on descriptor " + std::to_string(entry.descriptor) + " (neither a socket nor a pipe)").c_str()); - } + if (!GuestSockets::PipeReady(entry.descriptor, entry.events, entry.revents)) entry.revents = KernelSocketPoll::Unknown; if (entry.revents != 0) ++ready; } if (ready > 0 || std::chrono::steady_clock::now() >= deadline) return ready; @@ -181,6 +191,7 @@ int APS5_VABI select_nid_postfix(int nfds, void* readfds, void* writefds, void* return -1; } PollGroup kernel{&KernelSockets, {}, {}}; + PollGroup kqueues{&Kqueues, {}, {}}; PollGroup net{nullptr, {}, {}}; std::vector devices; std::vector requested; @@ -194,6 +205,10 @@ int APS5_VABI select_nid_postfix(int nfds, void* readfds, void* writefds, void* kernel.entries.push_back({descriptor, events, 0}); kernel.slots.push_back(requested.size()); requested.push_back({descriptor, events, 0}); + } else if (IsKqueueDescriptor(descriptor)) { + kqueues.entries.push_back({descriptor, events, 0}); + kqueues.slots.push_back(requested.size()); + requested.push_back({descriptor, events, 0}); } else if (File::IsRandomDevice(descriptor)) devices.push_back({descriptor, events, static_cast((events & KernelSocketPoll::Readable) != 0 ? KernelSocketPoll::Readable : events)}); else if (descriptor <= 2) devices.push_back({descriptor, events, static_cast(descriptor == 0 ? 0 : events & KernelSocketPoll::Writable)}); else if (File::IsVnode(descriptor)) devices.push_back({descriptor, events, static_cast(events & (KernelSocketPoll::Readable | KernelSocketPoll::Writable))}); @@ -207,6 +222,7 @@ int APS5_VABI select_nid_postfix(int nfds, void* readfds, void* writefds, void* const bool devicesReady = std::any_of(devices.begin(), devices.end(), [](const auto& entry) { return entry.revents != 0; }); std::vector groups; if (!kernel.entries.empty()) groups.push_back(std::move(kernel)); + if (!kqueues.entries.empty()) groups.push_back(std::move(kqueues)); if (!net.entries.empty()) { PollGroup hosts{&HostPoll, {}, {}}; net.poll = g_socketPoller.load(); @@ -230,7 +246,18 @@ int APS5_VABI select_nid_postfix(int nfds, void* readfds, void* writefds, void* net.slots.erase(net.slots.begin() + static_cast(index)); } if (!net.entries.empty()) groups.push_back(std::move(net)); - if (!hosts.entries.empty()) groups.push_back(std::move(hosts)); + if (!hosts.entries.empty()) { + const int probe = HostPoll(hosts.entries.data(), static_cast(hosts.entries.size()), 0); + if (probe < 0) { + *__error_nid_postfix() = -probe; + return -1; + } + for (const auto& entry : hosts.entries) { + if ((entry.revents & KernelSocketPoll::Unknown) != 0) + throw std::runtime_error("select: descriptor " + std::to_string(entry.descriptor) + " is neither a socket, a pipe, a file, a directory nor a kqueue"); + } + groups.push_back(std::move(hosts)); + } } const auto start = std::chrono::steady_clock::now(); const auto deadline = limit == nullptr ? std::chrono::steady_clock::time_point::max() @@ -263,9 +290,19 @@ int APS5_VABI select_nid_postfix(int nfds, void* readfds, void* writefds, void* if (groups.size() == 1 || groupReady > 0 || std::chrono::steady_clock::now() >= sliceEnd) break; } } + if (std::any_of(requested.begin(), requested.end(), [](const auto& entry) { return (entry.revents & KernelSocketPoll::Unknown) != 0; })) { + for (const auto& entry : requested) { + if ((entry.revents & KernelSocketPoll::Unknown) == 0 || DescriptorIsOpen_nid_no_patch(entry.descriptor)) continue; + *__error_nid_postfix() = GuestEbadf; + return -1; + } + if (limit == nullptr) return select_nid_postfix(nfds, readfds, writefds, exceptfds, nullptr); + const auto remaining = std::max(0, std::chrono::duration_cast(deadline - std::chrono::steady_clock::now()).count()); + const GuestTimeval rest{remaining / 1000000, remaining % 1000000}; + return select_nid_postfix(nfds, readfds, writefds, exceptfds, &rest); + } int ready = 0; for (const auto& entry : requested) { - if ((entry.revents & KernelSocketPoll::Unknown) != 0) UnknownDescriptor(entry.descriptor); const bool readable = (entry.events & KernelSocketPoll::Readable) != 0 && (entry.revents & (KernelSocketPoll::Readable | KernelSocketPoll::HangUp | KernelSocketPoll::Error)) != 0; const bool writable = (entry.events & KernelSocketPoll::Writable) != 0 && (entry.revents & (KernelSocketPoll::Writable | KernelSocketPoll::Error)) != 0; const bool urgent = (entry.events & KernelSocketPoll::Urgent) != 0 && (entry.revents & KernelSocketPoll::Urgent) != 0; diff --git a/core/libs/tests/GuestSelect.cpp b/core/libs/tests/GuestSelect.cpp index 35fe5cea49..979415e15d 100644 --- a/core/libs/tests/GuestSelect.cpp +++ b/core/libs/tests/GuestSelect.cpp @@ -1,9 +1,11 @@ #include "prx/libc/include/general/VabiMacros.hpp" +#include "SceTypes.hpp" #include #include #include #include #include +#include struct GuestTimeval { std::int64_t seconds; @@ -36,6 +38,8 @@ std::int64_t APS5_VABI write_nid_postfix(int, const char*, std::int64_t); std::int64_t APS5_VABI read_nid_postfix(int, void*, std::uint64_t); int APS5_VABI open_nid_postfix(const char*, int, int); int APS5_VABI unlink_nid_postfix(const char*); +int APS5_VABI kqueue_nid_postfix(); +int APS5_VABI kevent_nid_postfix(int, const KernelEvent*, int, KernelEvent*, int, const KernelTimespec*); int* APS5_VABI __error_nid_postfix(); } @@ -44,8 +48,62 @@ static void Require(bool value) { if (!value) std::abort(); } constexpr int GuestEbadf = 9; constexpr int GuestEinval = 22; constexpr int GuestDescriptorLimit = 1024; +constexpr std::int16_t EvfiltUser = -11; +constexpr std::uint16_t EvAdd = 0x0001; +constexpr std::uint16_t EvClear = 0x0020; +constexpr std::uint32_t NoteTrigger = 0x01000000; + +static int SelectAll(int descriptor, GuestDescriptorSet& read, GuestDescriptorSet& write, GuestDescriptorSet& except, const GuestTimeval& limit) { + read = {}; + write = {}; + except = {}; + read.Add(descriptor); + write.Add(descriptor); + except.Add(descriptor); + return select_nid_postfix(descriptor + 1, read.words.data(), write.words.data(), except.words.data(), &limit); +} + +static KernelEvent UserEvent(std::uint16_t flags, std::uint32_t fflags) { + KernelEvent change{}; + change.ident = 7; + change.filter = EvfiltUser; + change.flags = flags; + change.fflags = fflags; + return change; +} + +static void Kqueues() { + const int kq = kqueue_nid_postfix(); + Require(kq >= 0 && kq < GuestDescriptorLimit); + GuestDescriptorSet read{}; + GuestDescriptorSet write{}; + GuestDescriptorSet except{}; + const GuestTimeval none{0, 0}; + Require(SelectAll(kq, read, write, except, none) == 0); + Require(!read.Has(kq) && !write.Has(kq) && !except.Has(kq)); + const auto added = UserEvent(EvAdd | EvClear, 0); + Require(kevent_nid_postfix(kq, &added, 1, nullptr, 0, nullptr) == 0); + Require(SelectAll(kq, read, write, except, none) == 0); + std::thread trigger([kq] { + std::this_thread::sleep_for(std::chrono::milliseconds(20)); + const auto triggered = UserEvent(0, NoteTrigger); + Require(kevent_nid_postfix(kq, &triggered, 1, nullptr, 0, nullptr) == 0); + }); + const GuestTimeval second{1, 0}; + Require(SelectAll(kq, read, write, except, second) == 1); + trigger.join(); + Require(read.Has(kq) && !write.Has(kq) && !except.Has(kq)); + Require(SelectAll(kq, read, write, except, none) == 1 && read.Has(kq)); + KernelEvent events[1]{}; + const KernelTimespec zero{0, 0}; + Require(kevent_nid_postfix(kq, nullptr, 0, events, 1, &zero) == 1); + Require(SelectAll(kq, read, write, except, none) == 0); + Require(close_nid_postfix(kq) == 0); +} int main() { + Kqueues(); + int descriptors[2]{-1, -1}; Require(pipe_nid_postfix(descriptors) == 0); const int reader = descriptors[0]; diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 44d9a9d34b..fc87fb2e63 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -64,8 +64,8 @@ - [Mapping hints with no room above them](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (libkernel) - a mapping without `MAP_FIXED` whose hint lies above the guest address space, or has no free range at or above it, is placed from the bottom of the guest arena, as FreeBSD 11.4's [`vm_mmap_object`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/vm/vm_mmap.c#L1569) retries through [`vm_map_find_min`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/vm/vm_map.c#L1555-L1584) from its minimum address. FreeBSD's minimum is the end of the largest possible data segment rather than the start of the arena, and the console's address-space layout is not measured. PS5CEMU-HAR (PPSA99360) gives `sceKernelReserveVirtualRange` a hint near its own code, which is a host address here because the relinked executable lies outside the guest arena, so its JIT gets memory more than 2 GiB from its code and does not start - [_sceKernelRtldThreadAtexitIncrement, _sceKernelRtldThreadAtexitDecrement](../../../core/libs/prx/libkernel/System/src/Rtld.cpp) (libkernel) - the argument is the `dso` address a title's `libc.prx` passes to `__cxa_thread_atexit` (PPSA14632), which fails when Increment returns nonzero. As in the [fpPS4](https://github.com/red-prig/fpPS4) reimplementation, Increment takes a reference on the loaded image containing the address and Decrement releases one; the variants without the leading underscore are assumed to match - [tcgetattr, tcsetattr](../../../core/libs/prx/libkernel/File/src/Terminal.cpp) (libkernel) - every file and pipe descriptor is taken as not a terminal (`ENOTTY`), and a socket gets `EADDRNOTAVAIL` for `AF_INET` and `EOPNOTSUPP` otherwise, as FreeBSD's `in_control` and `pru_control_notsupp` answer a termios ioctl; what descriptors 0 to 2 of a title are on the console, and whether any of them is a terminal there, is unknown -- [select](../../../core/libs/prx/libkernel/Socket/src/Socket.cpp) (libkernel) - descriptor 0 is never reported readable and descriptors 1 and 2 are always writable, as for a console title with no input; reads from descriptor 0 still go to the host stdin. `/dev/random` and `/dev/urandom` follow `randomdev_poll` for a seeded device under [`selscan`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/sys_generic.c#L1178-L1236), which asks once with the flags of every set the descriptor is in: in the read set they are readable and reported in no other set, and otherwise they are reported in the write and exception sets they were asked in. A regular file, a directory or a file read from app0.zar is always readable and writable and never in the exception set, as `vop_stdpoll` answers, on every host. Guest sockets and libSceNet sockets are polled; other descriptors are polled through the host on Linux, while on Windows pipes are checked every millisecond with the readiness test `poll` uses and any other descriptor throws. Dungeons (PPSA16064) selects on descriptor 0, and RetroArch (PPSA99169) selects on its web server's wake-up pipe next to its listening socket -- [select](../../../core/libs/prx/libkernel/Socket/src/Socket.cpp) (libkernel) - a descriptor the socket poller reports as unknown that is below `GuestSockets::FirstDescriptor` is polled on the host instead of throwing (on Windows only a pipe, as above); an invalid host descriptor fails with `EBADF`, and mixed host and socket sets are polled in short alternating slices so neither side waits out the full slice alone. Wobbly Life (PPSA29413) selects on file descriptors while streaming its bundles +- [select](../../../core/libs/prx/libkernel/Socket/src/Socket.cpp) (libkernel) - descriptor 0 is never reported readable and descriptors 1 and 2 are always writable, as for a console title with no input; reads from descriptor 0 still go to the host stdin. `/dev/random` and `/dev/urandom` follow `randomdev_poll` for a seeded device under [`selscan`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/sys_generic.c#L1178-L1236), which asks once with the flags of every set the descriptor is in: in the read set they are readable and reported in no other set, and otherwise they are reported in the write and exception sets they were asked in. A regular file, a directory or a file read from app0.zar is always readable and writable and never in the exception set, as `vop_stdpoll` answers, on every host. Guest sockets and libSceNet sockets are polled. A kqueue is readable while one of its events is triggered and is never writable or in the exception set, as FreeBSD 11.4's [`kqueue_poll`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/kern_event.c#L1900-L1924) answers, on every host. Other descriptors are polled through the host on Linux, while on Windows pipes are checked every millisecond with the readiness test `poll` uses and any other descriptor throws. Dungeons (PPSA16064) selects on descriptor 0, and RetroArch (PPSA99169) selects on its web server's wake-up pipe next to its listening socket +- [select](../../../core/libs/prx/libkernel/Socket/src/Socket.cpp) (libkernel) - a descriptor the socket poller reports as unknown that is below `GuestSockets::FirstDescriptor` is polled on the host instead of throwing (on Windows only a pipe, as above); an invalid host descriptor fails with `EBADF`, and mixed host and socket sets are polled in short alternating slices so neither side waits out the full slice alone. A descriptor that stops being a socket, a kqueue or a pipe during the wait fails the call with `EBADF` when it is closed and is selected again with the rest of the timeout when its number was reused, as FreeBSD 11.4's [`kern_select`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/sys_generic.c#L1129-L1140) looks every descriptor up again in each pass. A close that has already removed the descriptor from the guest's kqueue, pipe or file tables but not yet closed its host descriptor is seen by that new look-up as a descriptor of unknown kind, which throws on Windows and is polled through the host on Linux. Wobbly Life (PPSA29413) selects on file descriptors while streaming its bundles - [__progname](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - unknown data export - [environ](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - data export pointing to a fixed empty environment so titles that import it load; it is not the environment libc's `getenv` reads, so it lists none of those variables and `setenv`/`putenv` changes do not appear in it - [seteuid, setegid, setuid, setreuid, setregid, setgroups, getgroups](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - the guest process is assumed to run as root, with real, effective and saved user and group ids 0. As in FreeBSD's `sys_seteuid`, `sys_setegid`, `sys_setuid`, `sys_setreuid` and `sys_setregid`, id 0 (or -1 for an unchanged `setreuid`/`setregid` id) is then accepted without a privilege check and changes no credential, so 0 is returned; any other id would change an id, which is not modelled, and throws. `setgroups` keeps the supplementary group list as `kern_setgroups` does (`NGROUPS_MAX` 1023, an empty list leaves the effective group as the only entry, a first entry other than 0 would change the effective group and throws) and `getgroups` reports it. The ids and privileges a title has on a console are unverified From d6ad5b7f9497b227c36164be85450a24cef2998b Mon Sep 17 00:00:00 2001 From: Romans Pokrovskis Date: Sun, 18 Oct 2026 01:46:07 +0300 Subject: [PATCH 0895/1000] test(agc): allow native depth fixture command buffer resets --- .../libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp index 65fbaf2e73..ce8741e6ba 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/NativeMultisampleDepth.cpp @@ -104,6 +104,7 @@ public: function("vkGetPhysicalDeviceMemoryProperties")(context.physical, &context.memory); context.Function("vkGetDeviceQueue")(context.device, family, 0u, &context.queue); VkCommandPoolCreateInfo pool{VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO}; + pool.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT; pool.queueFamilyIndex = family; Check(context.Function("vkCreateCommandPool")(context.device, &pool, nullptr, &context.pool), "vkCreateCommandPool"); } catch (...) { release(); throw; } From 9dcf40803404c50b7e965061fecb6d06b16890c0 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Sat, 17 Oct 2026 15:59:06 -0700 Subject: [PATCH 0896/1000] test(agc): expect a per-lane table image store to be supported Main's #6402 runs image stores through an image table, so a T# keyed per lane keeps its table with a supported image_store and an unsupported mark for the other operations. --- core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 3fadd9c234..52c8a5f761 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -2090,7 +2090,7 @@ void verifySelectedRecordDirectOperations() { require(TableBinding(*compiled.result, DescriptorRole::ImageTable) == nullptr && compiled.result->imageTableFaults == 0u && TableCovered(compiled.regions, &heap[2], sizeof(heap[2])), (what + " through a T# whose record the CPU evaluates was not bound from its record").c_str()); const auto wholeCode = WithImageOperation(TableWholeCode, word0, false); const auto whole = GetResourcePlan(TableRequest(wholeCode, userData)); - require(whole->info.images.size() == 1u && whole->info.images[0].table == 0u && whole->info.images[0].tableOperation == TableOperation::Unsupported, (what + " through a T# keyed per lane left the image table or lost its unsupported mark").c_str()); + require(whole->info.images.size() == 1u && whole->info.images[0].table == 0u && whole->info.images[0].tableOperation == (word0 == 0xf0200f08u ? TableOperation::Supported : TableOperation::Unsupported), (what + " through a T# keyed per lane left the image table or has the wrong table operation").c_str()); } const auto mixedCode = WithImageOperation(TableSrtKeyCode, 0xf0200f08u, true); const auto mixed = GetResourcePlan(TableRequest(mixedCode, userData)); From a49b8cce69376fab17f1c7d2f3611cb0bb1ae0fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Sun, 18 Oct 2026 01:01:23 +0200 Subject: [PATCH 0897/1000] fix(main): split conventions comment checks into 12 shards Failing run 39076267931 (push 55f82fee83): windows conventions_function_bodies_shard_4 hit the 30 s CTest timeout (others 15-26 s on the loaded runner). No culprit merge; shard cost sits near the limit since 541a4e705 (6 shards). Doubling the shard count halves each test's runtime. --- CMakeLists.txt | 4 ++-- tools/tests/test_check_conventions_comments.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 7d4eb6957c..51d18f3867 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -124,14 +124,14 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_import_audit.py) add_test(NAME unwrap_self COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_unwrap_self.py) - foreach(shard RANGE 0 5) + foreach(shard RANGE 0 11) if(shard EQUAL 0) set(conventions_test_name conventions_function_bodies) else() set(conventions_test_name conventions_function_bodies_shard_${shard}) endif() add_test(NAME ${conventions_test_name} COMMAND ${Python3_EXECUTABLE} - ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_comments.py --shard ${shard}/6) + ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_comments.py --shard ${shard}/12) set_tests_properties(${conventions_test_name} PROPERTIES TIMEOUT 30) endforeach() add_test(NAME conventions_wrapped_declarations COMMAND ${Python3_EXECUTABLE} diff --git a/tools/tests/test_check_conventions_comments.py b/tools/tests/test_check_conventions_comments.py index b7f46f54f8..e6df79d55d 100644 --- a/tools/tests/test_check_conventions_comments.py +++ b/tools/tests/test_check_conventions_comments.py @@ -11,7 +11,7 @@ from unittest.mock import patch sys.path.insert(0, str(Path(__file__).resolve().parents[1])) from check_conventions import STUB_BODY, SYSTEM_DEPENDENCY, Check, function_body, links, output -CONVENTIONS_TEST_SHARDS = 6 +CONVENTIONS_TEST_SHARDS = 12 def iter_test_cases(suite): From 1ee2c1c51073d2fdd3f6300cff324f64ff325ff4 Mon Sep 17 00:00:00 2001 From: Cleverdee <102689759+Cleverdee@users.noreply.github.com> Date: Fri, 16 Oct 2026 23:58:31 +0500 Subject: [PATCH 0898/1000] fix(recompiler): fold chained immediate addends of image-table record offsets SplitImmediateAddend peeled a single immediate operand off a T# dword's scalar offset. A title that loads a 32-byte record with two s_load_dwordx4, the second at soffset (first soffset + 16) where the first is (key << 5) + C, left IAdd32(key << 5, C) as the base of dwords 4-7, which never matched dword 0's base, and the image failed with "GetImageResource dword 0 is not a valid runtime value". Peel immediate IAdd32 operands until none is left and sum them modulo 2^32, as the scalar adds do. The chained offsets then reach the existing folded-addend path. verifyChainedTableOffset is the pointer table offset carry shader with the second offset chained off the first. Co-Authored-By: Claude Opus 5.5 --- .../libSceAgcDriver/tests/ShaderMemory.cpp | 36 +++++++++++++++++++ .../Optimization/src/ResourceTracker.cpp | 12 ++++--- 2 files changed, 44 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index fcc5051170..0ef746978e 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -1904,6 +1904,40 @@ void verifyPointerTableOffsetCarry() { #endif } +void verifyChainedTableOffset() { + using namespace ShaderRecompiler; + namespace Abi = ImageTableAbi; + static TableTexture textures[2]; + alignas(256) static std::array root{}; + static std::array keys{}; + static std::array output{}; + const auto keysV = TableV(keys.data(), 0u, static_cast(sizeof(keys))); + const auto outputV = TableV(output.data(), 0u, static_cast(sizeof(output))); + std::copy(keysV.begin(), keysV.end(), root.begin()); + std::copy(outputV.begin(), outputV.end(), root.begin() + 4); + const auto at = [&](std::uint32_t offset, std::array words) { + std::copy(words.begin(), words.end(), root.begin() + offset / 4u); + }; + at(0x60u + 3u * 32u, TableT(textures[0].bytes.data(), TableFormat8888UNorm, TableType2D)); + at(0x60u + 5u * 32u, TableT(textures[1].bytes.data(), TableFormat32UInt, TableType2D)); + const auto base = static_cast(reinterpret_cast(root.data())); + const std::array userData{static_cast(base), static_cast(base >> 32u)}; + const std::vector code{0xf4080200u, 0xfa000000u, 0xf4080700u, 0xfa000010u, 0xf4080800u, 0xfa000020u, 0x7e200500u, 0x8f108210u, 0xf4200444u, + 0x20000000u, 0x8711ff11u, 0x000000ffu, 0x8f128511u, 0x8113a012u, 0x81149013u, 0xf4080900u, 0x26000040u, 0xf4080a00u, 0x28000040u, 0xf09c0f08u, + 0x01090000u, 0xe0700000u, 0x80070000u, 0xbf810000u}; + auto request = TableRequest(code, userData); + request.shader.codeAddress = 0x37000u; + const auto plan = GetResourcePlan(request); + require(plan->info.images.size() == 1u && plan->info.images[0].table == 0u, "chained table offset: a T# whose second load offset adds to the first one's was not planned as a table"); + const auto& column = *plan->descriptorSources.at(plan->info.images[0].source).tableColumn; + require(column.address && column.stride == 32u && column.addend == 0x20u && column.offset == 0x40u && column.maxKey == 0xffu && column.foldedAddend == 0x20u && column.foldedCarry == 16u, "chained table offset: the column is wrong"); + const auto compiled = CompileTable(request); + const auto& snapshot = compiled.capture->snapshot.tables.tables.at(0); + const auto& words = compiled.capture->snapshot.tables.words; + require(snapshot.keys == 256u && (snapshot.codes[3] & Abi::PoisonFlag) == 0u && (snapshot.codes[5] & Abi::PoisonFlag) == 0u, "chained table offset: the records were not resolved"); + require(words.at(snapshot.codes[3]).dwords[0] == root[(0x60u + 3u * 32u) / 4u] && words.at(snapshot.codes[3]).dwords[7] == root[(0x60u + 3u * 32u) / 4u + 7u] && words.at(snapshot.codes[5]).dwords[0] == root[(0x60u + 5u * 32u) / 4u] && snapshot.codes[3] != snapshot.codes[5], "chained table offset: a record was read from the wrong offset"); +} + void verifyDescriptorPhis() { using namespace ShaderRecompiler; constexpr std::uint32_t Format8888UNorm = 56; @@ -5307,6 +5341,7 @@ int main(int argc, char** argv) { verifyScalarAddressRule(); verifyPointerImageTable(); verifyPointerTableOffsetCarry(); + verifyChainedTableOffset(); std::cout << "Image table keys, maps, records and registration passed\n"; return 0; } @@ -5338,6 +5373,7 @@ int main(int argc, char** argv) { verifyScalarAddressRule(); verifyPointerImageTable(); verifyPointerTableOffsetCarry(); + verifyChainedTableOffset(); verifyDescriptorPhis(); verifyUniformTableImage(); verifyLaneSelectedDescriptors(); diff --git a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp index d7d0e65d29..a871b4cb57 100644 --- a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp @@ -542,11 +542,15 @@ private: static IrValue* SplitImmediateAddend(IrValue* value, std::uint32_t& addend) { value = value->Resolve(); addend = 0; - if (value->Opcode() == IrOpcode::IAdd32 && value->ArgumentCount() == 2u) { - if (immediateU32(value->Argument(0), addend)) return value->Argument(1)->Resolve(); - if (immediateU32(value->Argument(1), addend)) return value->Argument(0)->Resolve(); + while (value->Opcode() == IrOpcode::IAdd32 && value->ArgumentCount() == 2u) { + std::uint32_t immediate = 0; + IrValue* rest = nullptr; + if (immediateU32(value->Argument(0), immediate)) rest = value->Argument(1)->Resolve(); + else if (immediateU32(value->Argument(1), immediate)) rest = value->Argument(0)->Resolve(); + else break; + addend += immediate; + value = rest; } - addend = 0; return value; } From 7665cb3ef50dcad7350f39d5c333b8aa9ca7a1e4 Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Sun, 18 Oct 2026 12:08:33 +1300 Subject: [PATCH 0899/1000] test(tools): check shader comments in one fixture repository conventions_function_bodies_shard_4 timed out at 30 s on Windows in five runs on 17 Oct (main pushes 39076267931 and 39061030084, dispatches 39074123651 and 39044676931, relinker 39072918668). Shard 4 was the slowest shard in every Windows log (10-16 s on a quiet runner, 5-11 s for the others). One case dominated it: test_comment_in_a_shader_is_reported built six fixture git repositories, 36 of the suite's 390 processes. Process start-up is what costs on Windows. #7314 doubled the shard count, but modulo sharding keeps that case whole, so its shard (now shard 10) still cost 0.63 s of the suite's 3.0 s locally against 0.76 s before. The case now writes all six shaders into one fixture and runs the checker once (6 processes), still asserting each path. The slowest of the 12 shards drops to 0.39 s locally (43 processes), with no timeout raised. --- .../tests/test_check_conventions_comments.py | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/tools/tests/test_check_conventions_comments.py b/tools/tests/test_check_conventions_comments.py index e6df79d55d..cbb130d784 100644 --- a/tools/tests/test_check_conventions_comments.py +++ b/tools/tests/test_check_conventions_comments.py @@ -345,7 +345,8 @@ class SilentStubSectionTests(unittest.TestCase): class TestFixtureExportTests(unittest.TestCase): - def check(self, path, text): + def check(self, path, text=None): + files = path if text is None else {path: text} checker = Path(__file__).resolve().parents[1] / "check_conventions.py" with tempfile.TemporaryDirectory() as directory: root = Path(directory) @@ -367,7 +368,8 @@ class TestFixtureExportTests(unittest.TestCase): write("core/libs/prx/libSceNgs2/tests/Render.cpp", "static int APS5_VABI Process(int value) { return value; }\n") git("add", ".") git("commit", "-qm", "feat: add libraries") - write(path, text) + for relative, content in files.items(): + write(relative, content) git("add", ".") git("commit", "-qm", "feat: add source") env = {key: value for key, value in os.environ.items() @@ -396,12 +398,16 @@ class TestFixtureExportTests(unittest.TestCase): self.assertIn("sceKernelGetProcessTime is also in libkernel", result.stdout) def test_comment_in_a_shader_is_reported(self): + shaders = {} for path in ("core/libs/prx/libSceAgcDriver/Graphics/shaders/Fill.comp", "core/libs/prx/libSceAgcDriver/tests/shaders/Fill.frag", "core/libs/prx/libSceAgcDriver/Graphics/shaders/Fill.glsl"): - for line in (" // fill the target", " /* fill the target */"): - with self.subTest(path=path, line=line): - result = self.check(path, f"#version 450\nvoid main() {{\n{line}\n}}\n") - self.assertEqual(result.returncode, 1, result.stdout + result.stderr) - self.assertIn(f"{path}:3: error: [comment]", result.stdout) + for style, line in (("Line", " // fill the target"), ("Block", " /* fill the target */")): + stem, extension = path.rsplit(".", 1) + shaders[f"{stem}{style}.{extension}"] = f"#version 450\nvoid main() {{\n{line}\n}}\n" + result = self.check(shaders) + self.assertEqual(result.returncode, 1, result.stdout + result.stderr) + for path in shaders: + with self.subTest(path=path): + self.assertIn(f"{path}:3: error: [comment]", result.stdout) def test_shader_without_comments_passes(self): result = self.check("core/libs/prx/libSceAgcDriver/Graphics/shaders/Fill.vert", From 39dd854c84a01485bea2e493572c8bcb62a65aa9 Mon Sep 17 00:00:00 2001 From: blu <131330772+ycbuzi@users.noreply.github.com> Date: Fri, 9 Oct 2026 15:20:01 -0700 Subject: [PATCH 0900/1000] fix(relinker): keep test artifacts in the build tree The tests build their working directories with tempfile.TemporaryDirectory() and std::filesystem::temp_directory_path(), so every executable they generate lands in the user's system temp directory. On Windows, Microsoft Defender quarantines those generated binaries as Trojan:Win32/Wacatac.H!ml, and the nine tests that run one fail with WinError 5 (access denied) on a clean checkout: autorun_exit_status, optional_plt, empty_tls, tls_function_coverage, executable_segments, windows_icon, guest_needed_modules, guest_module_directories and windows_import_modules. Point TMPDIR, TEMP and TMP at a build-local directory for every test registered here, so generated executables stay inside the build tree. Test artifacts are then removed with the build directory, and a contributor who needs an antivirus exclusion can scope it to the build tree instead of the system temp directory. --- core/relinker/CMakeLists.txt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index a0b72b06c9..dab82857bf 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -735,6 +735,12 @@ if(BUILD_TESTING) ) add_test(NAME reloc_table_bounds COMMAND reloc_table_bounds_tests $) endif() + + set(relinkerTestTemp ${CMAKE_CURRENT_BINARY_DIR}/test-temp) + file(MAKE_DIRECTORY ${relinkerTestTemp}) + get_property(relinkerTests DIRECTORY PROPERTY TESTS) + set_tests_properties(${relinkerTests} PROPERTIES ENVIRONMENT_MODIFICATION + "TMPDIR=set:${relinkerTestTemp};TEMP=set:${relinkerTestTemp};TMP=set:${relinkerTestTemp}") endif() if(BUILD_TESTING AND Python3_Interpreter_FOUND) From b771ce4f377e2e971d0d038888db85c383ec62b8 Mon Sep 17 00:00:00 2001 From: Cesar Rodriguez <11452991+Tamalero@users.noreply.github.com> Date: Sat, 10 Oct 2026 20:06:56 -0700 Subject: [PATCH 0901/1000] fix(libkernel): name module-relative call sites and thread ids in Linux wait traces On Linux, APS5_TRACE_WAITS and APS5_TRACE_USLEEP used an image base and a thread id of 0, so "exe+0x..." printed absolute addresses and every site reported "tid 0". The call site is now resolved with dladdr1(RTLD_DL_LINKMAP) and printed relative to its module's load address ("exe+..." for the executable, the module file name otherwise), matching guest offsets in a disassembly; the thread id is gettid(). Descriptions are cached per call site, so polling titles do not pay a dladdr1 per wait. Windows output is unchanged. --- core/libs/prx/libkernel/Time/Time.cpp | 60 ++++++++++++++++++++------- 1 file changed, 45 insertions(+), 15 deletions(-) diff --git a/core/libs/prx/libkernel/Time/Time.cpp b/core/libs/prx/libkernel/Time/Time.cpp index 059c0236f7..12523a767b 100644 --- a/core/libs/prx/libkernel/Time/Time.cpp +++ b/core/libs/prx/libkernel/Time/Time.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -28,6 +29,10 @@ #include #include #include +#ifdef __linux__ +#include +#include +#endif // __linux__ #endif extern "C" int* APS5_VABI __error_nid_postfix(); @@ -179,6 +184,39 @@ std::uint64_t APS5_VABI sceKernelGetProcessTimeCounterFrequency() { return 1000000000ULL; } +static std::string DescribeCallSite(const void* caller) { + const auto address = reinterpret_cast(caller); + char text[192]; +#ifdef _WIN32 + const auto image = reinterpret_cast(GetModuleHandleW(nullptr)); + std::snprintf(text, sizeof(text), "exe+0x%llx", static_cast(address - image)); +#elif defined(__linux__) + Dl_info info{}; + link_map* map = nullptr; + if (dladdr1(caller, &info, reinterpret_cast(&map), RTLD_DL_LINKMAP) == 0 || map == nullptr) { + std::snprintf(text, sizeof(text), "0x%llx", static_cast(address)); + return text; + } + const char* path = map->l_name; + const char* slash = path == nullptr ? nullptr : std::strrchr(path, '/'); + const char* module = path == nullptr || *path == '\0' ? "exe" : slash == nullptr ? path : slash + 1; + std::snprintf(text, sizeof(text), "%s+0x%llx", module, static_cast(address - map->l_addr)); +#else + std::snprintf(text, sizeof(text), "0x%llx", static_cast(address)); +#endif + return text; +} + +static unsigned long CurrentThreadIdForTrace() { +#ifdef _WIN32 + return GetCurrentThreadId(); +#elif defined(__linux__) + return static_cast(gettid()); +#else + return 0; +#endif +} + // Debug aid: APS5_TRACE_USLEEP reports every 2000 calls which guest call sites sleep, so a CPU // thread polling for GPU or I/O completion can be found (offsets are relative to the executable). static void TraceSleep(const void* caller, std::uint64_t microseconds) { @@ -192,15 +230,10 @@ static void TraceSleep(const void* caller, std::uint64_t microseconds) { ++site.first; site.second += microseconds; if (++calls % 2000 != 0) return; -#ifdef _WIN32 - const auto image = reinterpret_cast(GetModuleHandleW(nullptr)); -#else - const std::uintptr_t image = 0; -#endif std::vector>> hot(sites.begin(), sites.end()); std::sort(hot.begin(), hot.end(), [](const auto& a, const auto& b) { return a.second.first > b.second.first; }); std::fprintf(stderr, "[usleep] %llu calls:", static_cast(calls)); - for (std::size_t i = 0; i < hot.size() && i < 6; ++i) std::fprintf(stderr, " exe+0x%llx x%llu (%llu us)", static_cast(hot[i].first - image), static_cast(hot[i].second.first), static_cast(hot[i].second.second)); + for (std::size_t i = 0; i < hot.size() && i < 6; ++i) std::fprintf(stderr, " %s x%llu (%llu us)", DescribeCallSite(reinterpret_cast(hot[i].first)).c_str(), static_cast(hot[i].second.first), static_cast(hot[i].second.second)); std::fprintf(stderr, "\n"); } @@ -211,16 +244,13 @@ void KernelTraceWait_nid_postfix(const char* kind, const void* caller, std::uint static std::mutex mutex; static std::unordered_map sites; static std::uint64_t lastReport = 0; - char key[96]; -#ifdef _WIN32 - const auto image = reinterpret_cast(GetModuleHandleW(nullptr)); - const unsigned long thread = GetCurrentThreadId(); -#else - const std::uintptr_t image = 0; - const unsigned long thread = 0; -#endif - std::snprintf(key, sizeof(key), "%s exe+0x%llx", kind, static_cast(reinterpret_cast(caller) - image)); + static std::unordered_map callSites; + char key[224]; + const unsigned long thread = CurrentThreadIdForTrace(); std::lock_guard lock(mutex); + auto callSite = callSites.find(caller); + if (callSite == callSites.end()) callSite = callSites.emplace(caller, DescribeCallSite(caller)).first; + std::snprintf(key, sizeof(key), "%s %s", kind, callSite->second.c_str()); auto& site = sites[key]; ++site.calls; site.timeouts += timedOut ? 1u : 0u; From 619257798abe5bc12a5064a8042853204c2464ab Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Sat, 17 Oct 2026 09:36:14 +0000 Subject: [PATCH 0902/1000] refactor(libSceSystemService): share the console language setting SystemServiceConsoleLanguage in SystemService.hpp reads ANYPS5_LANGUAGE with the same default, range and error as sceSystemServiceParamGetInt, which now calls it. Other libraries can pick the console language without linking libSceSystemService. --- core/libs/prx/libSceSystemService/Export.cpp | 17 +--------------- .../prx/libSceSystemService/SystemService.hpp | 20 +++++++++++++++++++ 2 files changed, 21 insertions(+), 16 deletions(-) diff --git a/core/libs/prx/libSceSystemService/Export.cpp b/core/libs/prx/libSceSystemService/Export.cpp index 6480b49dfc..0e66ec6bd9 100644 --- a/core/libs/prx/libSceSystemService/Export.cpp +++ b/core/libs/prx/libSceSystemService/Export.cpp @@ -6,7 +6,6 @@ #include #include #include -#include #include #include #include @@ -28,20 +27,6 @@ namespace { -int ConsoleLanguage() { - const char* configured = std::getenv("ANYPS5_LANGUAGE"); - if (configured == nullptr || configured[0] == '\0') return SYSTEM_SERVICE_PARAM_LANG_ENGLISH_US; - const std::string_view text(configured); - int language = -1; - const auto [end, error] = std::from_chars(text.data(), text.data() + text.size(), language); - if (error != std::errc{} || end != text.data() + text.size() || language < SYSTEM_SERVICE_PARAM_LANG_JAPANESE || language > SYSTEM_SERVICE_PARAM_LANG_UKRAINIAN) { - throw std::runtime_error("sceSystemServiceParamGetInt: ANYPS5_LANGUAGE must be a console language number from " + - std::to_string(SYSTEM_SERVICE_PARAM_LANG_JAPANESE) + " to " + std::to_string(SYSTEM_SERVICE_PARAM_LANG_UKRAINIAN) + - ", not \"" + std::string(text) + "\""); - } - return language; -} - struct ConsoleTimeZone { int minutes; bool summer; @@ -298,7 +283,7 @@ int APS5_VABI sceSystemServiceParamGetInt(int paramId, int* value) { } switch (paramId) { case SYSTEM_SERVICE_PARAM_ID_LANG: - case SYSTEM_SERVICE_PARAM_ID_400: *value = ConsoleLanguage(); break; + case SYSTEM_SERVICE_PARAM_ID_400: *value = SystemServiceConsoleLanguage("sceSystemServiceParamGetInt"); break; case SYSTEM_SERVICE_PARAM_ID_DATE_FORMAT: *value = SYSTEM_SERVICE_PARAM_DATE_FORMAT_DDMMYYYY; break; case SYSTEM_SERVICE_PARAM_ID_TIME_FORMAT: *value = SYSTEM_SERVICE_PARAM_TIME_FORMAT_24HOUR; break; case SYSTEM_SERVICE_PARAM_ID_TIME_ZONE: *value = HostTimeZone().minutes; break; diff --git a/core/libs/prx/libSceSystemService/SystemService.hpp b/core/libs/prx/libSceSystemService/SystemService.hpp index 99b73cc0b5..7b1664cc90 100644 --- a/core/libs/prx/libSceSystemService/SystemService.hpp +++ b/core/libs/prx/libSceSystemService/SystemService.hpp @@ -1,8 +1,14 @@ #ifndef CORE_LIBS_PRX_LIBSCESYSTEMSERVICE_SYSTEMSERVICE_HPP #define CORE_LIBS_PRX_LIBSCESYSTEMSERVICE_SYSTEMSERVICE_HPP +#include #include #include +#include +#include +#include +#include +#include constexpr int SYSTEM_SERVICE_OK = 0; constexpr int SYSTEM_SERVICE_ERROR_PARAMETER = -2136932349; @@ -31,4 +37,18 @@ constexpr int SYSTEM_SERVICE_PARAM_TIME_FORMAT_24HOUR = 1; constexpr int SYSTEM_SERVICE_PARAM_GAME_PARENTAL_OFF = 0; constexpr int SYSTEM_SERVICE_PARAM_ENTER_BUTTON_CROSS = 1; +inline int SystemServiceConsoleLanguage(std::string_view caller) { + const char* configured = std::getenv("ANYPS5_LANGUAGE"); + if (configured == nullptr || configured[0] == '\0') return SYSTEM_SERVICE_PARAM_LANG_ENGLISH_US; + const std::string_view text(configured); + int language = -1; + const auto [end, error] = std::from_chars(text.data(), text.data() + text.size(), language); + if (error != std::errc{} || end != text.data() + text.size() || language < SYSTEM_SERVICE_PARAM_LANG_JAPANESE || language > SYSTEM_SERVICE_PARAM_LANG_UKRAINIAN) { + throw std::runtime_error(std::string(caller) + ": ANYPS5_LANGUAGE must be a console language number from " + + std::to_string(SYSTEM_SERVICE_PARAM_LANG_JAPANESE) + " to " + std::to_string(SYSTEM_SERVICE_PARAM_LANG_UKRAINIAN) + + ", not \"" + std::string(text) + "\""); + } + return language; +} + #endif From e40191d7c91da40b070f92a7dfd48c298742bf05 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Sat, 17 Oct 2026 09:36:15 +0000 Subject: [PATCH 0903/1000] fix(libSceNpTrophy2): give progress values a type and a 64-bit value A progress value is {int32 type, 4 reserved bytes, uint64 value}, as KytyPS5 lays it out, which makes the trophy details 1312 bytes and the trophy data 32 bytes. Size checks for every query structure keep the layout fixed, and the technical debt names its single public source. --- .../libSceNpTrophy2/include/NpTrophy2Constants.hpp | 3 ++- .../prx/libSceNpTrophy2/include/NpTrophy2Types.hpp | 14 +++++++++++++- docs/dev/debt/libSceNpTrophy2.md | 2 ++ 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceNpTrophy2/include/NpTrophy2Constants.hpp b/core/libs/prx/libSceNpTrophy2/include/NpTrophy2Constants.hpp index 8102f3785b..6a853b4c28 100644 --- a/core/libs/prx/libSceNpTrophy2/include/NpTrophy2Constants.hpp +++ b/core/libs/prx/libSceNpTrophy2/include/NpTrophy2Constants.hpp @@ -29,7 +29,8 @@ static constexpr std::uint32_t NP_TROPHY2_PROGRESS_PERCENTAGE = 0; static constexpr std::int32_t NP_TROPHY2_TROPHY_GRADE_BRONZE = 4; static constexpr std::int32_t NP_TROPHY2_PROGRESS_TYPE_NONE = 0; -static constexpr std::uint32_t NP_TROPHY2_PROGRESS_VALUE_NONE = 0; +static constexpr std::int32_t NP_TROPHY2_PROGRESS_TYPE_VALUE = 1; +static constexpr std::uint64_t NP_TROPHY2_PROGRESS_VALUE_NONE = 0; static constexpr std::size_t NP_TROPHY2_ICON_SIZE_NONE = 0; diff --git a/core/libs/prx/libSceNpTrophy2/include/NpTrophy2Types.hpp b/core/libs/prx/libSceNpTrophy2/include/NpTrophy2Types.hpp index 30fadecda7..ecfde98357 100644 --- a/core/libs/prx/libSceNpTrophy2/include/NpTrophy2Types.hpp +++ b/core/libs/prx/libSceNpTrophy2/include/NpTrophy2Types.hpp @@ -4,7 +4,11 @@ #include #include -struct NpTrophy2Progress { std::uint32_t value; }; +struct NpTrophy2Progress { + std::int32_t type; + std::uint8_t reserved[4]; + std::uint64_t value; +}; struct NpTrophy2GameDetails { std::uint32_t num_groups; @@ -67,4 +71,12 @@ struct NpTrophy2Data { std::uint64_t timestamp_tick; }; +static_assert(sizeof(NpTrophy2Progress) == 16); +static_assert(sizeof(NpTrophy2GameDetails) == 152); +static_assert(sizeof(NpTrophy2GameData) == 24); +static_assert(sizeof(NpTrophy2GroupDetails) == 152); +static_assert(sizeof(NpTrophy2GroupData) == 32); +static_assert(sizeof(NpTrophy2Details) == 1312); +static_assert(sizeof(NpTrophy2Data) == 32); + #endif diff --git a/docs/dev/debt/libSceNpTrophy2.md b/docs/dev/debt/libSceNpTrophy2.md index 142e30364e..d5e9083695 100644 --- a/docs/dev/debt/libSceNpTrophy2.md +++ b/docs/dev/debt/libSceNpTrophy2.md @@ -6,4 +6,6 @@ ### Unknown function info +- [Query structures](../../../core/libs/prx/libSceNpTrophy2/include/NpTrophy2Types.hpp): a progress value is a 16-byte `{int32 type, 4 reserved bytes, uint64 value}` and the game, group and trophy structures are 152, 24, 152, 32, 1312 and 32 bytes, as [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/9e41d8320a2774b303fbad344c2347e8d0292e83/src/libs/libTrophy.cpp) asserts; no other public source confirms the layout. Progress type 1 marks a progressive trophy's target and progress, as in KytyPS5. + - [sceNpTrophy2GetRewardIcon](../../../core/libs/prx/libSceNpTrophy2/src/Context.cpp) (libSceNpTrophy2) takes `(int context, int handle, int reward_id, void* buffer, size_t* size)`, from the call sites in the bundled `Media/Plugins/PSNCore.prx` of PPSA06888, the same shape as `sceNpTrophy2GetTrophyIcon`. The title calls it twice: first with a null buffer to read the size, then with a buffer of that size allocated with `malloc`. The meaning of the third argument is unverified, and the reward icon data is unknown, so every call throws From d374de5e3871410bae3efb94ca99c4ced7eaa25e Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Sat, 17 Oct 2026 23:25:07 +0000 Subject: [PATCH 0904/1000] feat(libSceNpTrophy2): report the title's trophy data from trophyNN.ucp The title's sce_sys/trophy2/trophyNN.ucp is read on first use with a bounds-checked .ucp reader and a small JSON reader. tropconf.json gives each trophy's grade, group, hidden and reward flags, platinum and unlock condition, and tropmeta_.json its texts in the console language, then en-US, then the file's default language. Contexts remember their user and service label, so the queries find the file. The trophy, group and game queries report these names, grades, groups and counts, list every trophy and group, and give progressive trophies their target. Unlocks are not recorded yet, so every trophy reports as locked and nothing is written to the host. A missing file is ignored and a malformed one is logged once; either way the queries keep their placeholders. A JSON document that nests deeper than 64 levels or holds more than 1048576 values counts as malformed, which keeps the parsed values to about 140 MiB. --- core/libs/prx/libSceNpTrophy2/CMakeLists.txt | 15 + .../libSceNpTrophy2/include/TitleTrophies.hpp | 54 ++ .../libSceNpTrophy2/include/TrophyPackage.hpp | 96 ++++ core/libs/prx/libSceNpTrophy2/src/Context.cpp | 50 +- .../libs/prx/libSceNpTrophy2/src/GameInfo.cpp | 54 +- .../prx/libSceNpTrophy2/src/GroupInfo.cpp | 118 +++-- .../prx/libSceNpTrophy2/src/TitleTrophies.cpp | 140 +++++ .../prx/libSceNpTrophy2/src/TrophyInfo.cpp | 119 +++-- .../prx/libSceNpTrophy2/src/TrophyPackage.cpp | 481 ++++++++++++++++++ .../libSceNpTrophy2/tests/TrophyPackage.cpp | 283 +++++++++++ .../CMakeLists.txt | 12 + core/libs/tests/GuestNpTrophyData.cpp | 188 +++++++ docs/dev/debt/libSceNpTrophy2.md | 8 +- 13 files changed, 1520 insertions(+), 98 deletions(-) create mode 100644 core/libs/prx/libSceNpTrophy2/include/TitleTrophies.hpp create mode 100644 core/libs/prx/libSceNpTrophy2/include/TrophyPackage.hpp create mode 100644 core/libs/prx/libSceNpTrophy2/src/TitleTrophies.cpp create mode 100644 core/libs/prx/libSceNpTrophy2/src/TrophyPackage.cpp create mode 100644 core/libs/prx/libSceNpTrophy2/tests/TrophyPackage.cpp create mode 100644 core/libs/tests/GuestNpTrophyData.cpp diff --git a/core/libs/prx/libSceNpTrophy2/CMakeLists.txt b/core/libs/prx/libSceNpTrophy2/CMakeLists.txt index 165feb0839..cd03e490cb 100644 --- a/core/libs/prx/libSceNpTrophy2/CMakeLists.txt +++ b/core/libs/prx/libSceNpTrophy2/CMakeLists.txt @@ -4,6 +4,8 @@ add_library(libSceNpTrophy2 SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/src/GameInfo.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/TrophyInfo.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/GroupInfo.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/TrophyPackage.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/TitleTrophies.cpp ) target_include_directories(libSceNpTrophy2 PRIVATE @@ -23,3 +25,16 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() endif() + +add_test_executable(np_trophy2_package_tests EXCLUDE_FROM_ALL tests/TrophyPackage.cpp) +target_include_directories(np_trophy2_package_tests PRIVATE ${LIBS_INCLUDE_DIR}) +target_link_libraries(np_trophy2_package_tests PRIVATE libSceNpTrophy2 libc) +configure_windows_unwind(np_trophy2_package_tests) +if(BUILD_TESTING) + set_target_properties(np_trophy2_package_tests PROPERTIES EXCLUDE_FROM_ALL FALSE) + add_test(NAME np_trophy2_package COMMAND np_trophy2_package_tests) + if(WIN32) + set_tests_properties(np_trophy2_package PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceNpTrophy2/include/TitleTrophies.hpp b/core/libs/prx/libSceNpTrophy2/include/TitleTrophies.hpp new file mode 100644 index 0000000000..8caaa30647 --- /dev/null +++ b/core/libs/prx/libSceNpTrophy2/include/TitleTrophies.hpp @@ -0,0 +1,54 @@ +#ifndef CORE_LIBS_PRX_LIBSCENPTROPHY2_INCLUDE_TITLETROPHIES_HPP +#define CORE_LIBS_PRX_LIBSCENPTROPHY2_INCLUDE_TITLETROPHIES_HPP + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "prx/libSceNpTrophy2/include/TrophyPackage.hpp" + +namespace NpTrophy2 { + +inline constexpr std::uint64_t MaxUcpSize = std::uint64_t{256} << 20u; + +struct TitleTrophies { + std::optional package; +}; + +std::string PackagePath(std::uint32_t serviceLabel); +TitleTrophies LoadTitleTrophies(std::uint32_t serviceLabel, std::string_view language); +const TitleTrophies& Title(std::uint32_t serviceLabel); +const TrophyPackage* Package(std::uint32_t serviceLabel); +void CopyText(char* destination, std::size_t size, std::string_view text); + +struct ContextOwner { + std::int32_t userId = 0; + std::uint32_t serviceLabel = 0; +}; + +int CreateContext(std::int32_t userId, std::uint32_t serviceLabel); +void DestroyContext(int context); +std::optional FindContext(int context); + +std::optional ProgressTarget(const TrophyDefinition& trophy); +std::uint64_t TrophyProgress(const TrophyDefinition& trophy, std::optional stat, bool unlocked); + +struct TrophyCounts { + std::uint32_t trophies = 0; + std::uint32_t unlocked = 0; + std::array grades{}; + std::array unlockedGrades{}; + + std::uint32_t Percentage() const; +}; + +TrophyCounts CountTrophies(const TrophyPackage& package, std::optional group); + +} + +#endif diff --git a/core/libs/prx/libSceNpTrophy2/include/TrophyPackage.hpp b/core/libs/prx/libSceNpTrophy2/include/TrophyPackage.hpp new file mode 100644 index 0000000000..1e1e20bd4c --- /dev/null +++ b/core/libs/prx/libSceNpTrophy2/include/TrophyPackage.hpp @@ -0,0 +1,96 @@ +#ifndef CORE_LIBS_PRX_LIBSCENPTROPHY2_INCLUDE_TROPHYPACKAGE_HPP +#define CORE_LIBS_PRX_LIBSCENPTROPHY2_INCLUDE_TROPHYPACKAGE_HPP + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace NpTrophy2 { + +inline constexpr std::uint32_t UcpMagic = 0xB228C60Au; +inline constexpr std::uint32_t UcpVersion = 1; +inline constexpr std::uint64_t UcpHeaderSize = 0x40; +inline constexpr std::uint64_t UcpTocHeaderSize = 0x20; +inline constexpr std::uint64_t UcpEntrySize = 0x40; +inline constexpr std::size_t UcpNameSize = 0x20; +inline constexpr std::uint64_t UcpMaxEntries = 4096; + +using UcpFiles = std::map>; + +UcpFiles ReadUcp(std::string_view data); + +class Json { +public: + enum class Kind { Null, Bool, Number, String, Array, Object }; + + Kind kind = Kind::Null; + bool boolean = false; + std::string text; + std::vector items; + std::vector> members; + + const Json* Find(std::string_view key) const; + bool IsTrue() const; + std::optional Unsigned() const; + std::optional Id() const; +}; + +Json ParseJson(std::string_view text); + +inline constexpr std::int32_t GradePlatinum = 1; +inline constexpr std::int32_t GradeGold = 2; +inline constexpr std::int32_t GradeSilver = 3; +inline constexpr std::int32_t GradeBronze = 4; +inline constexpr std::int32_t BaseGroup = -1; +inline constexpr std::size_t MaxTrophies = 1000; +inline constexpr std::size_t MaxGroups = 50; + +enum class Comparator { None, GreaterEqual, Greater, LessEqual, Less }; + +struct UnlockCondition { + std::uint64_t statId = 0; + std::uint64_t target = 0; + Comparator comparator = Comparator::None; + + bool Reached(std::uint64_t value) const; +}; + +struct TrophyDefinition { + std::int32_t id = 0; + std::int32_t grade = GradeBronze; + std::int32_t groupId = BaseGroup; + bool hidden = false; + bool hasReward = false; + bool progressive = false; + std::optional platinumId; + std::optional statId; + std::optional target; + Comparator comparator = Comparator::None; + std::string name; + std::string detail; + std::string reward; + + std::optional Condition() const; +}; + +struct TrophyPackage { + std::string language; + std::string title; + std::map trophies; + std::map groups; + std::vector skipped; +}; + +TrophyPackage ParseTrophyPackage(std::string_view ucp, std::string_view language); + +std::string_view LanguageTag(int consoleLanguage); + +} + +#endif diff --git a/core/libs/prx/libSceNpTrophy2/src/Context.cpp b/core/libs/prx/libSceNpTrophy2/src/Context.cpp index 197f834f71..90887a7f4a 100644 --- a/core/libs/prx/libSceNpTrophy2/src/Context.cpp +++ b/core/libs/prx/libSceNpTrophy2/src/Context.cpp @@ -1,26 +1,68 @@ #include #include +#include +#include #include #include #include "prx/libSceNpTrophy2/include/NpTrophy2.hpp" +#include "prx/libSceNpTrophy2/include/TitleTrophies.hpp" #include "prx/libc/include/General.hpp" +namespace NpTrophy2 { + +namespace { + +struct Contexts { + std::mutex mutex; + std::map owners; + int next = NP_TROPHY2_CONTEXT_DEFAULT; +}; + +Contexts& contexts() { + static Contexts table; + return table; +} + +} + +int CreateContext(std::int32_t userId, std::uint32_t serviceLabel) { + auto& table = contexts(); + std::lock_guard lock(table.mutex); + const int context = table.next++; + table.owners[context] = {userId, serviceLabel}; + return context; +} + +void DestroyContext(int context) { + auto& table = contexts(); + std::lock_guard lock(table.mutex); + table.owners.erase(context); +} + +std::optional FindContext(int context) { + auto& table = contexts(); + std::lock_guard lock(table.mutex); + const auto found = table.owners.find(context); + if (found == table.owners.end()) return std::nullopt; + return found->second; +} + +} + extern "C" { int APS5_VABI sceNpTrophy2CreateContext(int* context, int user_id, uint32_t service_label, uint64_t options) { if (context == nullptr) { APS5_INVALID_ARG_EX; } - (void)user_id; - (void)service_label; (void)options; - *context = NP_TROPHY2_CONTEXT_DEFAULT; + *context = NpTrophy2::CreateContext(user_id, service_label); return SCE_NP_TROPHY2_OK; } int APS5_VABI sceNpTrophy2DestroyContext(int context) { - (void)context; + NpTrophy2::DestroyContext(context); return SCE_NP_TROPHY2_OK; } diff --git a/core/libs/prx/libSceNpTrophy2/src/GameInfo.cpp b/core/libs/prx/libSceNpTrophy2/src/GameInfo.cpp index 0c5b320f6f..bea174aa49 100644 --- a/core/libs/prx/libSceNpTrophy2/src/GameInfo.cpp +++ b/core/libs/prx/libSceNpTrophy2/src/GameInfo.cpp @@ -1,9 +1,11 @@ #include #include +#include #include #include #include "prx/libSceNpTrophy2/include/NpTrophy2.hpp" +#include "prx/libSceNpTrophy2/include/TitleTrophies.hpp" #include "prx/libc/include/General.hpp" extern "C" { @@ -12,26 +14,50 @@ int APS5_VABI sceNpTrophy2GetGameInfo(int context, int handle, NpTrophy2GameDeta if (details == nullptr && data == nullptr) { APS5_INVALID_ARG_EX; } - (void)context; (void)handle; + const auto owner = NpTrophy2::FindContext(context); + const auto* package = owner ? NpTrophy2::Package(owner->serviceLabel) : nullptr; + if (package == nullptr) { + if (details != nullptr) { + std::memset(details, 0, sizeof(*details)); + details->num_groups = NP_TROPHY2_NUM_GROUPS; + details->num_trophies = NP_TROPHY2_NUM_TROPHIES; + details->num_platinum = NP_TROPHY2_NUM_PLATINUM; + details->num_gold = NP_TROPHY2_NUM_GOLD; + details->num_silver = NP_TROPHY2_NUM_SILVER; + details->num_bronze = NP_TROPHY2_NUM_BRONZE; + std::strncpy(details->title, NP_TROPHY2_GAME_TITLE, sizeof(details->title) - 1); + } + if (data != nullptr) { + std::memset(data, 0, sizeof(*data)); + data->unlocked_trophies = NP_TROPHY2_UNLOCKED_TROPHIES; + data->unlocked_platinum = NP_TROPHY2_UNLOCKED_PLATINUM; + data->unlocked_gold = NP_TROPHY2_UNLOCKED_GOLD; + data->unlocked_silver = NP_TROPHY2_UNLOCKED_SILVER; + data->unlocked_bronze = NP_TROPHY2_UNLOCKED_BRONZE; + data->progress_percentage = NP_TROPHY2_PROGRESS_PERCENTAGE; + } + return SCE_NP_TROPHY2_OK; + } + const auto counts = NpTrophy2::CountTrophies(*package, std::nullopt); if (details != nullptr) { std::memset(details, 0, sizeof(*details)); - details->num_groups = NP_TROPHY2_NUM_GROUPS; - details->num_trophies = NP_TROPHY2_NUM_TROPHIES; - details->num_platinum = NP_TROPHY2_NUM_PLATINUM; - details->num_gold = NP_TROPHY2_NUM_GOLD; - details->num_silver = NP_TROPHY2_NUM_SILVER; - details->num_bronze = NP_TROPHY2_NUM_BRONZE; - std::strncpy(details->title, NP_TROPHY2_GAME_TITLE, sizeof(details->title) - 1); + details->num_groups = static_cast(package->groups.size()); + details->num_trophies = counts.trophies; + details->num_platinum = counts.grades[NpTrophy2::GradePlatinum]; + details->num_gold = counts.grades[NpTrophy2::GradeGold]; + details->num_silver = counts.grades[NpTrophy2::GradeSilver]; + details->num_bronze = counts.grades[NpTrophy2::GradeBronze]; + NpTrophy2::CopyText(details->title, sizeof(details->title), package->title); } if (data != nullptr) { std::memset(data, 0, sizeof(*data)); - data->unlocked_trophies = NP_TROPHY2_UNLOCKED_TROPHIES; - data->unlocked_platinum = NP_TROPHY2_UNLOCKED_PLATINUM; - data->unlocked_gold = NP_TROPHY2_UNLOCKED_GOLD; - data->unlocked_silver = NP_TROPHY2_UNLOCKED_SILVER; - data->unlocked_bronze = NP_TROPHY2_UNLOCKED_BRONZE; - data->progress_percentage = NP_TROPHY2_PROGRESS_PERCENTAGE; + data->unlocked_trophies = counts.unlocked; + data->unlocked_platinum = counts.unlockedGrades[NpTrophy2::GradePlatinum]; + data->unlocked_gold = counts.unlockedGrades[NpTrophy2::GradeGold]; + data->unlocked_silver = counts.unlockedGrades[NpTrophy2::GradeSilver]; + data->unlocked_bronze = counts.unlockedGrades[NpTrophy2::GradeBronze]; + data->progress_percentage = counts.Percentage(); } return SCE_NP_TROPHY2_OK; } diff --git a/core/libs/prx/libSceNpTrophy2/src/GroupInfo.cpp b/core/libs/prx/libSceNpTrophy2/src/GroupInfo.cpp index 5e97582aa7..badd913d14 100644 --- a/core/libs/prx/libSceNpTrophy2/src/GroupInfo.cpp +++ b/core/libs/prx/libSceNpTrophy2/src/GroupInfo.cpp @@ -1,41 +1,85 @@ +#include #include #include #include +#include #include #include #include "prx/libSceNpTrophy2/include/NpTrophy2.hpp" +#include "prx/libSceNpTrophy2/include/TitleTrophies.hpp" #include "prx/libc/include/General.hpp" +namespace { + +void fillPlaceholderDetails(std::int32_t groupId, NpTrophy2GroupDetails* details) { + std::memset(details, 0, sizeof(*details)); + details->group_id = groupId; + details->num_trophies = NP_TROPHY2_NUM_TROPHIES; + details->num_platinum = NP_TROPHY2_NUM_PLATINUM; + details->num_gold = NP_TROPHY2_NUM_GOLD; + details->num_silver = NP_TROPHY2_NUM_SILVER; + details->num_bronze = NP_TROPHY2_NUM_BRONZE; + std::strncpy(details->title, NP_TROPHY2_GROUP_TITLE, sizeof(details->title) - 1); +} + +void fillPlaceholderData(std::int32_t groupId, NpTrophy2GroupData* data) { + std::memset(data, 0, sizeof(*data)); + data->group_id = groupId; + data->unlocked_trophies = NP_TROPHY2_UNLOCKED_TROPHIES; + data->unlocked_platinum = NP_TROPHY2_UNLOCKED_PLATINUM; + data->unlocked_gold = NP_TROPHY2_UNLOCKED_GOLD; + data->unlocked_silver = NP_TROPHY2_UNLOCKED_SILVER; + data->unlocked_bronze = NP_TROPHY2_UNLOCKED_BRONZE; + data->progress_percentage = NP_TROPHY2_PROGRESS_PERCENTAGE; +} + +void fillGroup(const NpTrophy2::TrophyPackage& package, std::int32_t groupId, const std::string& title, NpTrophy2GroupDetails* details, NpTrophy2GroupData* data) { + const auto counts = NpTrophy2::CountTrophies(package, groupId); + if (details != nullptr) { + std::memset(details, 0, sizeof(*details)); + details->group_id = groupId; + details->num_trophies = counts.trophies; + details->num_platinum = counts.grades[NpTrophy2::GradePlatinum]; + details->num_gold = counts.grades[NpTrophy2::GradeGold]; + details->num_silver = counts.grades[NpTrophy2::GradeSilver]; + details->num_bronze = counts.grades[NpTrophy2::GradeBronze]; + NpTrophy2::CopyText(details->title, sizeof(details->title), title); + } + if (data != nullptr) { + std::memset(data, 0, sizeof(*data)); + data->group_id = groupId; + data->unlocked_trophies = counts.unlocked; + data->unlocked_platinum = counts.unlockedGrades[NpTrophy2::GradePlatinum]; + data->unlocked_gold = counts.unlockedGrades[NpTrophy2::GradeGold]; + data->unlocked_silver = counts.unlockedGrades[NpTrophy2::GradeSilver]; + data->unlocked_bronze = counts.unlockedGrades[NpTrophy2::GradeBronze]; + data->progress_percentage = counts.Percentage(); + } +} + +} + extern "C" { int APS5_VABI sceNpTrophy2GetGroupInfo(int context, int handle, int group_id, NpTrophy2GroupDetails* details, NpTrophy2GroupData* data) { if (details == nullptr && data == nullptr) { APS5_INVALID_ARG_EX; } - (void)context; (void)handle; - const std::int32_t normalized_group_id = (group_id < 0 ? NP_TROPHY2_GROUP_ID_BASE : group_id); - if (details != nullptr) { - std::memset(details, 0, sizeof(*details)); - details->group_id = normalized_group_id; - details->num_trophies = NP_TROPHY2_NUM_TROPHIES; - details->num_platinum = NP_TROPHY2_NUM_PLATINUM; - details->num_gold = NP_TROPHY2_NUM_GOLD; - details->num_silver = NP_TROPHY2_NUM_SILVER; - details->num_bronze = NP_TROPHY2_NUM_BRONZE; - std::strncpy(details->title, NP_TROPHY2_GROUP_TITLE, sizeof(details->title) - 1); + const auto owner = NpTrophy2::FindContext(context); + const auto* package = owner ? NpTrophy2::Package(owner->serviceLabel) : nullptr; + if (package == nullptr) { + const std::int32_t normalized_group_id = (group_id < 0 ? NP_TROPHY2_GROUP_ID_BASE : group_id); + if (details != nullptr) fillPlaceholderDetails(normalized_group_id, details); + if (data != nullptr) fillPlaceholderData(normalized_group_id, data); + return SCE_NP_TROPHY2_OK; } - if (data != nullptr) { - std::memset(data, 0, sizeof(*data)); - data->group_id = normalized_group_id; - data->unlocked_trophies = NP_TROPHY2_UNLOCKED_TROPHIES; - data->unlocked_platinum = NP_TROPHY2_UNLOCKED_PLATINUM; - data->unlocked_gold = NP_TROPHY2_UNLOCKED_GOLD; - data->unlocked_silver = NP_TROPHY2_UNLOCKED_SILVER; - data->unlocked_bronze = NP_TROPHY2_UNLOCKED_BRONZE; - data->progress_percentage = NP_TROPHY2_PROGRESS_PERCENTAGE; + const auto group = package->groups.find(group_id); + if (group == package->groups.end()) { + throw std::invalid_argument(std::string(__func__) + ": group " + std::to_string(group_id) + " is not in " + NpTrophy2::PackagePath(owner->serviceLabel)); } + fillGroup(*package, group->first, group->second, details, data); return SCE_NP_TROPHY2_OK; } @@ -43,29 +87,21 @@ int APS5_VABI sceNpTrophy2GetGroupInfoArray(int context, int handle, uint32_t of if (count == nullptr) { APS5_INVALID_ARG_EX; } - (void)context; (void)handle; - const uint32_t out_count = (offset == 0 && limit != 0 ? 1u : 0u); - *count = out_count; - if (out_count != 0 && details_array != nullptr) { - std::memset(details_array, 0, sizeof(*details_array)); - details_array->group_id = NP_TROPHY2_GROUP_ID_BASE; - details_array->num_trophies = NP_TROPHY2_NUM_TROPHIES; - details_array->num_platinum = NP_TROPHY2_NUM_PLATINUM; - details_array->num_gold = NP_TROPHY2_NUM_GOLD; - details_array->num_silver = NP_TROPHY2_NUM_SILVER; - details_array->num_bronze = NP_TROPHY2_NUM_BRONZE; - std::strncpy(details_array->title, NP_TROPHY2_GROUP_TITLE, sizeof(details_array->title) - 1); + const auto owner = NpTrophy2::FindContext(context); + const auto* package = owner ? NpTrophy2::Package(owner->serviceLabel) : nullptr; + if (package == nullptr) { + const uint32_t out_count = (offset == 0 && limit != 0 ? 1u : 0u); + *count = out_count; + if (out_count != 0 && details_array != nullptr) fillPlaceholderDetails(NP_TROPHY2_GROUP_ID_BASE, details_array); + if (out_count != 0 && data_array != nullptr) fillPlaceholderData(NP_TROPHY2_GROUP_ID_BASE, data_array); + return SCE_NP_TROPHY2_OK; } - if (out_count != 0 && data_array != nullptr) { - std::memset(data_array, 0, sizeof(*data_array)); - data_array->group_id = NP_TROPHY2_GROUP_ID_BASE; - data_array->unlocked_trophies = NP_TROPHY2_UNLOCKED_TROPHIES; - data_array->unlocked_platinum = NP_TROPHY2_UNLOCKED_PLATINUM; - data_array->unlocked_gold = NP_TROPHY2_UNLOCKED_GOLD; - data_array->unlocked_silver = NP_TROPHY2_UNLOCKED_SILVER; - data_array->unlocked_bronze = NP_TROPHY2_UNLOCKED_BRONZE; - data_array->progress_percentage = NP_TROPHY2_PROGRESS_PERCENTAGE; + *count = 0; + auto group = package->groups.begin(); + std::advance(group, std::min(offset, package->groups.size())); + for (; group != package->groups.end() && *count < limit; ++group, ++*count) { + fillGroup(*package, group->first, group->second, details_array != nullptr ? &details_array[*count] : nullptr, data_array != nullptr ? &data_array[*count] : nullptr); } return SCE_NP_TROPHY2_OK; } diff --git a/core/libs/prx/libSceNpTrophy2/src/TitleTrophies.cpp b/core/libs/prx/libSceNpTrophy2/src/TitleTrophies.cpp new file mode 100644 index 0000000000..885a0b4206 --- /dev/null +++ b/core/libs/prx/libSceNpTrophy2/src/TitleTrophies.cpp @@ -0,0 +1,140 @@ +#include "prx/libSceNpTrophy2/include/TitleTrophies.hpp" +#include "prx/libSceSystemService/SystemService.hpp" +#include "prx/libc/include/General.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace NpTrophy2 { + +namespace { + +bool tracing() { + static const bool trace = std::getenv("APS5_TRACE_UDS") != nullptr; + return trace; +} + +std::string labelled(const char* format, std::uint32_t serviceLabel) { + char path[64]; + std::snprintf(path, sizeof(path), format, static_cast(serviceLabel)); + return path; +} + +std::optional readTitleFile(const std::string& guestPath) { + const auto path = ResolvePath_nid_no_patch(guestPath.c_str()); + std::error_code error; + const bool exists = std::filesystem::exists(path, error); + if (error) throw std::filesystem::filesystem_error("cannot check the file", path, error); + if (!exists) return std::nullopt; + const auto size = std::filesystem::file_size(path); + if (size > MaxUcpSize) throw std::runtime_error("it has " + std::to_string(size) + " bytes, more than " + std::to_string(MaxUcpSize)); + std::string bytes(static_cast(size), '\0'); + std::ifstream file(path, std::ios::binary); + if (!file.read(bytes.data(), static_cast(bytes.size()))) throw std::runtime_error("cannot read " + path.string()); + return bytes; +} + +template +std::optional loadTitleFile(const std::string& guestPath, const char* without, TParse parse) { + std::optional bytes; + try { + bytes = readTitleFile(guestPath); + } catch (const std::runtime_error& error) { + std::fprintf(stderr, "[trophy] %s is not used, so %s: %s\n", guestPath.c_str(), without, error.what()); + return std::nullopt; + } + if (!bytes) { + if (tracing()) std::fprintf(stderr, "[uds] %s does not exist, so %s\n", guestPath.c_str(), without); + return std::nullopt; + } + try { + return parse(*bytes); + } catch (const std::runtime_error& error) { + std::fprintf(stderr, "[trophy] %s is malformed, so %s: %s\n", guestPath.c_str(), without, error.what()); + return std::nullopt; + } +} + +std::string counted(std::size_t count, const char* one, const char* many) { + return std::to_string(count) + " " + (count == 1 ? one : many); +} + +} + +std::string PackagePath(std::uint32_t serviceLabel) { + return labelled("/app0/sce_sys/trophy2/trophy%02u.ucp", serviceLabel); +} + +TitleTrophies LoadTitleTrophies(std::uint32_t serviceLabel, std::string_view language) { + TitleTrophies title; + const auto packagePath = PackagePath(serviceLabel); + title.package = loadTitleFile(packagePath, "trophy queries report placeholders and stats unlock no trophies", + [language](std::string_view bytes) { return ParseTrophyPackage(bytes, language); }); + if (title.package) { + std::fprintf(stderr, "[trophy] %s: %s, %s, texts in %s\n", packagePath.c_str(), counted(title.package->trophies.size(), "trophy", "trophies").c_str(), + counted(title.package->groups.size(), "group", "groups").c_str(), title.package->language.c_str()); + for (const auto& skipped : title.package->skipped) std::fprintf(stderr, "[trophy] %s: %s\n", packagePath.c_str(), skipped.c_str()); + } + return title; +} + +const TitleTrophies& Title(std::uint32_t serviceLabel) { + static std::mutex mutex; + static std::map> titles; + std::lock_guard lock(mutex); + auto& title = titles[serviceLabel]; + if (!title) title = std::make_unique(LoadTitleTrophies(serviceLabel, LanguageTag(SystemServiceConsoleLanguage("libSceNpTrophy2")))); + return *title; +} + +const TrophyPackage* Package(std::uint32_t serviceLabel) { + const auto& title = Title(serviceLabel); + return title.package ? &*title.package : nullptr; +} + +void CopyText(char* destination, std::size_t size, std::string_view text) { + if (size == 0) throw std::invalid_argument("text cannot be copied into an empty buffer"); + auto length = std::min(text.size(), size - 1); + while (length > 0 && length < text.size() && (static_cast(text[length]) & 0xC0u) == 0x80u) --length; + std::memcpy(destination, text.data(), length); + destination[length] = '\0'; +} + +std::optional ProgressTarget(const TrophyDefinition& trophy) { + if (!trophy.progressive) return std::nullopt; + return trophy.target; +} + +std::uint64_t TrophyProgress(const TrophyDefinition& trophy, std::optional stat, bool unlocked) { + const auto target = ProgressTarget(trophy); + if (!target) throw std::invalid_argument("trophy " + std::to_string(trophy.id) + " is not progressive or has no target"); + if (unlocked) return *target; + return std::min(stat.value_or(0), *target); +} + +TrophyCounts CountTrophies(const TrophyPackage& package, std::optional group) { + TrophyCounts counts; + for (const auto& [id, trophy] : package.trophies) { + if (group && trophy.groupId != *group) continue; + ++counts.trophies; + ++counts.grades[static_cast(trophy.grade)]; + } + return counts; +} + +std::uint32_t TrophyCounts::Percentage() const { + const auto points = [](const std::array& counts) { return counts[GradeGold] * 6u + counts[GradeSilver] * 2u + counts[GradeBronze]; }; + const auto total = points(grades); + return total == 0 ? 0 : points(unlockedGrades) * 100u / total; +} + +} diff --git a/core/libs/prx/libSceNpTrophy2/src/TrophyInfo.cpp b/core/libs/prx/libSceNpTrophy2/src/TrophyInfo.cpp index 5090940b58..001ba17a97 100644 --- a/core/libs/prx/libSceNpTrophy2/src/TrophyInfo.cpp +++ b/core/libs/prx/libSceNpTrophy2/src/TrophyInfo.cpp @@ -1,39 +1,88 @@ +#include #include #include #include +#include #include #include #include "prx/libSceNpTrophy2/include/NpTrophy2.hpp" +#include "prx/libSceNpTrophy2/include/TitleTrophies.hpp" #include "prx/libc/include/General.hpp" +namespace { + +void fillPlaceholderDetails(int trophyId, NpTrophy2Details* details) { + std::memset(details, 0, sizeof(*details)); + details->trophy_id = trophyId; + details->trophy_grade = NP_TROPHY2_TROPHY_GRADE_BRONZE; + details->group_id = NP_TROPHY2_GROUP_ID_BASE; + details->hidden = false; + details->has_reward = false; + details->target.type = NP_TROPHY2_PROGRESS_TYPE_NONE; + details->target.value = NP_TROPHY2_PROGRESS_VALUE_NONE; + std::strncpy(details->name, NP_TROPHY2_TROPHY_NAME, sizeof(details->name) - 1); + std::strncpy(details->description, NP_TROPHY2_TROPHY_DESCRIPTION, sizeof(details->description) - 1); + std::strncpy(details->reward, NP_TROPHY2_TROPHY_REWARD, sizeof(details->reward) - 1); +} + +void fillPlaceholderData(int trophyId, NpTrophy2Data* data) { + std::memset(data, 0, sizeof(*data)); + data->trophy_id = trophyId; + data->unlocked = false; + data->progress.type = NP_TROPHY2_PROGRESS_TYPE_NONE; + data->progress.value = NP_TROPHY2_PROGRESS_VALUE_NONE; + data->timestamp_tick = 0; +} + +void fillDetails(const NpTrophy2::TrophyDefinition& trophy, NpTrophy2Details* details) { + std::memset(details, 0, sizeof(*details)); + details->trophy_id = trophy.id; + details->trophy_grade = trophy.grade; + details->group_id = trophy.groupId; + details->hidden = trophy.hidden; + details->has_reward = trophy.hasReward; + if (const auto target = NpTrophy2::ProgressTarget(trophy)) { + details->target.type = NP_TROPHY2_PROGRESS_TYPE_VALUE; + details->target.value = *target; + } + NpTrophy2::CopyText(details->name, sizeof(details->name), trophy.name); + NpTrophy2::CopyText(details->description, sizeof(details->description), trophy.detail); + NpTrophy2::CopyText(details->reward, sizeof(details->reward), trophy.reward); +} + +void fillData(const NpTrophy2::TrophyDefinition& trophy, NpTrophy2Data* data) { + std::memset(data, 0, sizeof(*data)); + data->trophy_id = trophy.id; + data->unlocked = false; + data->timestamp_tick = 0; + if (!NpTrophy2::ProgressTarget(trophy)) return; + data->progress.type = NP_TROPHY2_PROGRESS_TYPE_VALUE; + data->progress.value = NpTrophy2::TrophyProgress(trophy, std::nullopt, data->unlocked); +} + +} + extern "C" { int APS5_VABI sceNpTrophy2GetTrophyInfo(int context, int handle, int trophy_id, NpTrophy2Details* details, NpTrophy2Data* data) { if (details == nullptr && data == nullptr) { APS5_INVALID_ARG_EX; } - (void)context; (void)handle; - if (details != nullptr) { - std::memset(details, 0, sizeof(*details)); - details->trophy_id = trophy_id; - details->trophy_grade = NP_TROPHY2_TROPHY_GRADE_BRONZE; - details->group_id = NP_TROPHY2_GROUP_ID_BASE; - details->hidden = false; - details->has_reward = false; - details->target.value = NP_TROPHY2_PROGRESS_VALUE_NONE; - std::strncpy(details->name, NP_TROPHY2_TROPHY_NAME, sizeof(details->name) - 1); - std::strncpy(details->description, NP_TROPHY2_TROPHY_DESCRIPTION, sizeof(details->description) - 1); - std::strncpy(details->reward, NP_TROPHY2_TROPHY_REWARD, sizeof(details->reward) - 1); + const auto owner = NpTrophy2::FindContext(context); + const auto* package = owner ? NpTrophy2::Package(owner->serviceLabel) : nullptr; + if (package == nullptr) { + if (details != nullptr) fillPlaceholderDetails(trophy_id, details); + if (data != nullptr) fillPlaceholderData(trophy_id, data); + return SCE_NP_TROPHY2_OK; } - if (data != nullptr) { - std::memset(data, 0, sizeof(*data)); - data->trophy_id = trophy_id; - data->unlocked = false; - data->progress.value = NP_TROPHY2_PROGRESS_VALUE_NONE; - data->timestamp_tick = 0; + const auto trophy = package->trophies.find(trophy_id); + if (trophy == package->trophies.end()) { + throw std::invalid_argument(std::string(__func__) + ": trophy " + std::to_string(trophy_id) + " is not in " + NpTrophy2::PackagePath(owner->serviceLabel)); } + if (details != nullptr) fillDetails(trophy->second, details); + if (data != nullptr) fillData(trophy->second, data); return SCE_NP_TROPHY2_OK; } @@ -41,28 +90,22 @@ int APS5_VABI sceNpTrophy2GetTrophyInfoArray(int context, int handle, uint32_t o if (count == nullptr) { APS5_INVALID_ARG_EX; } - (void)context; (void)handle; - const uint32_t out_count = (offset == 0 && limit != 0 ? 1u : 0u); - *count = out_count; - if (out_count != 0 && details_array != nullptr) { - std::memset(details_array, 0, sizeof(*details_array)); - details_array->trophy_id = NP_TROPHY2_TROPHY_ID_DEFAULT; - details_array->trophy_grade = NP_TROPHY2_TROPHY_GRADE_BRONZE; - details_array->group_id = NP_TROPHY2_GROUP_ID_BASE; - details_array->hidden = false; - details_array->has_reward = false; - details_array->target.value = NP_TROPHY2_PROGRESS_VALUE_NONE; - std::strncpy(details_array->name, NP_TROPHY2_TROPHY_NAME, sizeof(details_array->name) - 1); - std::strncpy(details_array->description, NP_TROPHY2_TROPHY_DESCRIPTION, sizeof(details_array->description) - 1); - std::strncpy(details_array->reward, NP_TROPHY2_TROPHY_REWARD, sizeof(details_array->reward) - 1); + const auto owner = NpTrophy2::FindContext(context); + const auto* package = owner ? NpTrophy2::Package(owner->serviceLabel) : nullptr; + if (package == nullptr) { + const uint32_t out_count = (offset == 0 && limit != 0 ? 1u : 0u); + *count = out_count; + if (out_count != 0 && details_array != nullptr) fillPlaceholderDetails(NP_TROPHY2_TROPHY_ID_DEFAULT, details_array); + if (out_count != 0 && data_array != nullptr) fillPlaceholderData(NP_TROPHY2_TROPHY_ID_DEFAULT, data_array); + return SCE_NP_TROPHY2_OK; } - if (out_count != 0 && data_array != nullptr) { - std::memset(data_array, 0, sizeof(*data_array)); - data_array->trophy_id = NP_TROPHY2_TROPHY_ID_DEFAULT; - data_array->unlocked = false; - data_array->progress.value = NP_TROPHY2_PROGRESS_VALUE_NONE; - data_array->timestamp_tick = 0; + *count = 0; + auto trophy = package->trophies.begin(); + std::advance(trophy, std::min(offset, package->trophies.size())); + for (; trophy != package->trophies.end() && *count < limit; ++trophy, ++*count) { + if (details_array != nullptr) fillDetails(trophy->second, &details_array[*count]); + if (data_array != nullptr) fillData(trophy->second, &data_array[*count]); } return SCE_NP_TROPHY2_OK; } diff --git a/core/libs/prx/libSceNpTrophy2/src/TrophyPackage.cpp b/core/libs/prx/libSceNpTrophy2/src/TrophyPackage.cpp new file mode 100644 index 0000000000..01d0bc8063 --- /dev/null +++ b/core/libs/prx/libSceNpTrophy2/src/TrophyPackage.cpp @@ -0,0 +1,481 @@ +#include "prx/libSceNpTrophy2/include/TrophyPackage.hpp" + +#include +#include +#include +#include +#include +#include + +namespace NpTrophy2 { + +namespace { + +constexpr int MaxJsonDepth = 64; +constexpr std::size_t MaxJsonValues = std::size_t{1} << 20u; + +std::uint64_t bigEndian(std::string_view data, std::uint64_t offset, std::size_t size) { + std::uint64_t value = 0; + for (std::size_t i = 0; i < size; ++i) value = (value << 8u) | static_cast(data[offset + i]); + return value; +} + +std::string hex(std::uint64_t value) { + char text[24]; + std::snprintf(text, sizeof(text), "0x%llx", static_cast(value)); + return text; +} + +void appendUtf8(std::string& out, std::uint32_t code) { + if (code < 0x80) { + out += static_cast(code); + } else if (code < 0x800) { + out += static_cast(0xC0 | (code >> 6)); + out += static_cast(0x80 | (code & 0x3F)); + } else if (code < 0x10000) { + out += static_cast(0xE0 | (code >> 12)); + out += static_cast(0x80 | ((code >> 6) & 0x3F)); + out += static_cast(0x80 | (code & 0x3F)); + } else { + out += static_cast(0xF0 | (code >> 18)); + out += static_cast(0x80 | ((code >> 12) & 0x3F)); + out += static_cast(0x80 | ((code >> 6) & 0x3F)); + out += static_cast(0x80 | (code & 0x3F)); + } +} + +class JsonReader { +public: + explicit JsonReader(std::string_view text) : text(text) {} + + Json Document() { + if (text.starts_with("\xEF\xBB\xBF")) position = 3; + auto document = readValue(0); + skipSpace(); + if (position != text.size()) fail("text follows the document"); + return document; + } + +private: + [[noreturn]] void fail(const std::string& problem) const { + throw std::runtime_error(problem + " at byte " + std::to_string(position)); + } + + void skipSpace() { + while (position < text.size() && (text[position] == ' ' || text[position] == '\t' || text[position] == '\n' || text[position] == '\r')) ++position; + } + + bool take(char expected) { + if (position >= text.size() || text[position] != expected) return false; + ++position; + return true; + } + + void expect(std::string_view word) { + if (text.substr(position, word.size()) != word) fail("expected " + std::string(word)); + position += word.size(); + } + + Json readValue(int depth) { + if (depth >= MaxJsonDepth) fail("values nest deeper than " + std::to_string(MaxJsonDepth) + " levels"); + if (++values > MaxJsonValues) fail("the document holds more than " + std::to_string(MaxJsonValues) + " values"); + skipSpace(); + if (position >= text.size()) fail("the document ends before a value"); + Json result; + switch (text[position]) { + case '{': + result.kind = Json::Kind::Object; + readObject(result, depth); + break; + case '[': + result.kind = Json::Kind::Array; + readArray(result, depth); + break; + case '"': + result.kind = Json::Kind::String; + result.text = readString(); + break; + case 't': + expect("true"); + result.kind = Json::Kind::Bool; + result.boolean = true; + break; + case 'f': + expect("false"); + result.kind = Json::Kind::Bool; + break; + case 'n': + expect("null"); + break; + default: + result.kind = Json::Kind::Number; + result.text = readNumber(); + break; + } + return result; + } + + void readObject(Json& result, int depth) { + ++position; + skipSpace(); + if (take('}')) return; + while (true) { + skipSpace(); + if (position >= text.size() || text[position] != '"') fail("expected a member name"); + auto key = readString(); + skipSpace(); + if (!take(':')) fail("expected ':'"); + result.members.emplace_back(std::move(key), readValue(depth + 1)); + skipSpace(); + if (take('}')) return; + if (!take(',')) fail("expected ',' or '}'"); + } + } + + void readArray(Json& result, int depth) { + ++position; + skipSpace(); + if (take(']')) return; + while (true) { + result.items.push_back(readValue(depth + 1)); + skipSpace(); + if (take(']')) return; + if (!take(',')) fail("expected ',' or ']'"); + } + } + + std::uint32_t readHexDigits() { + if (text.size() - position < 4) fail("a \\u escape is cut short"); + std::uint32_t code = 0; + const auto* end = text.data() + position + 4; + const auto [stop, error] = std::from_chars(text.data() + position, end, code, 16); + if (error != std::errc{} || stop != end) fail("a \\u escape does not have 4 hex digits"); + position += 4; + return code; + } + + std::string readString() { + ++position; + std::string out; + while (true) { + if (position >= text.size()) fail("a string is not closed"); + const char character = text[position++]; + if (character == '"') return out; + if (static_cast(character) < 0x20) fail("a string holds a control character"); + if (character != '\\') { + out += character; + continue; + } + if (position >= text.size()) fail("a string is not closed"); + switch (text[position++]) { + case '"': out += '"'; break; + case '\\': out += '\\'; break; + case '/': out += '/'; break; + case 'b': out += '\b'; break; + case 'f': out += '\f'; break; + case 'n': out += '\n'; break; + case 'r': out += '\r'; break; + case 't': out += '\t'; break; + case 'u': { + auto code = readHexDigits(); + if (code >= 0xDC00 && code < 0xE000) fail("a low surrogate has no high surrogate"); + if (code >= 0xD800 && code < 0xDC00) { + if (text.substr(position, 2) != "\\u") fail("a high surrogate has no low surrogate"); + position += 2; + const auto low = readHexDigits(); + if (low < 0xDC00 || low >= 0xE000) fail("a high surrogate has no low surrogate"); + code = 0x10000 + ((code - 0xD800) << 10) + (low - 0xDC00); + } + appendUtf8(out, code); + break; + } + default: + fail("a string has an unknown escape"); + } + } + } + + bool skipDigits() { + const auto start = position; + while (position < text.size() && text[position] >= '0' && text[position] <= '9') ++position; + return position != start; + } + + std::string readNumber() { + const auto start = position; + take('-'); + if (!take('0') && !skipDigits()) fail("expected a value"); + if (take('.') && !skipDigits()) fail("a number has no digits after '.'"); + if (take('e') || take('E')) { + if (!take('+')) take('-'); + if (!skipDigits()) fail("a number has no exponent digits"); + } + return std::string(text.substr(start, position - start)); + } + + std::string_view text; + std::size_t position = 0; + std::size_t values = 0; +}; + +std::string describe(const Json& value) { + switch (value.kind) { + case Json::Kind::Null: return "null"; + case Json::Kind::Bool: return value.boolean ? "true" : "false"; + case Json::Kind::Number: return value.text; + case Json::Kind::String: return "\"" + value.text + "\""; + case Json::Kind::Array: return "an array"; + case Json::Kind::Object: return "an object"; + } + throw std::logic_error("a JSON value has an unknown kind"); +} + +std::string kindName(Json::Kind kind) { + switch (kind) { + case Json::Kind::Null: return "null"; + case Json::Kind::Bool: return "boolean"; + case Json::Kind::Number: return "number"; + case Json::Kind::String: return "string"; + case Json::Kind::Array: return "array"; + case Json::Kind::Object: return "object"; + } + throw std::logic_error("a JSON value has an unknown kind"); +} + +const Json& member(const Json& object, std::string_view key, Json::Kind kind, const std::string& where) { + const auto* found = object.Find(key); + if (found == nullptr || found->kind != kind) throw std::runtime_error(where + " has no " + std::string(key) + " " + kindName(kind)); + return *found; +} + +Json document(const UcpFiles& files, const std::string& name) { + const auto found = files.find(name); + if (found == files.end()) throw std::runtime_error("it has no " + name); + Json parsed; + try { + parsed = ParseJson(found->second); + } catch (const std::runtime_error& error) { + throw std::runtime_error(name + " is not JSON: " + error.what()); + } + if (parsed.kind != Json::Kind::Object) throw std::runtime_error(name + " is not a JSON object"); + return parsed; +} + +Comparator comparatorOf(const Json& value) { + if (value.kind != Json::Kind::String) return Comparator::None; + if (value.text == "ge") return Comparator::GreaterEqual; + if (value.text == "gt") return Comparator::Greater; + if (value.text == "le") return Comparator::LessEqual; + if (value.text == "lt") return Comparator::Less; + return Comparator::None; +} + +std::string join(const std::vector& parts, std::string_view separator) { + std::string out; + for (const auto& part : parts) { + if (!out.empty()) out += separator; + out += part; + } + return out; +} + +std::string alternatives(const std::vector& parts) { + std::string out; + for (std::size_t index = 0; index < parts.size(); ++index) { + if (index != 0) out += index + 1 == parts.size() ? " or " : ", "; + out += parts[index]; + } + return out; +} + +TrophyDefinition readTrophy(const Json& definition, std::size_t index, std::vector& skipped) { + const auto where = "tropconf.json trophies[" + std::to_string(index) + "]"; + if (definition.kind != Json::Kind::Object) throw std::runtime_error(where + " is not an object"); + const auto* id = definition.Find("id"); + const auto parsedId = id != nullptr ? id->Id() : std::nullopt; + if (!parsedId) throw std::runtime_error(where + " has no id from 0 to 2147483647"); + TrophyDefinition trophy; + trophy.id = *parsedId; + const auto name = "tropconf.json trophy " + std::to_string(trophy.id); + const auto* grade = definition.Find("grade"); + const auto letter = grade != nullptr && grade->kind == Json::Kind::String && grade->text.size() == 1 ? std::string_view("PGSB").find(grade->text[0]) : std::string_view::npos; + if (letter == std::string_view::npos) throw std::runtime_error(name + " has no grade P, G, S or B"); + trophy.grade = GradePlatinum + static_cast(letter); + if (const auto* group = definition.Find("groupId")) { + const auto groupId = group->Id(); + if (!groupId) throw std::runtime_error(name + " has groupId " + describe(*group) + ", not an id from 0 to 2147483647"); + trophy.groupId = *groupId; + } + if (const auto* platinum = definition.Find("platinumTrophyId")) trophy.platinumId = platinum->Id(); + const auto* hidden = definition.Find("hidden"); + trophy.hidden = hidden != nullptr && hidden->IsTrue(); + const auto* reward = definition.Find("hasReward"); + trophy.hasReward = reward != nullptr && reward->IsTrue(); + const auto* condition = definition.Find("unlockCondition"); + if (condition == nullptr || condition->kind != Json::Kind::Object) return trophy; + const auto* progressive = condition->Find("progressive"); + trophy.progressive = progressive != nullptr && progressive->IsTrue(); + if (const auto* stat = condition->Find("udsStatId")) { + trophy.statId = stat->Unsigned(); + if (!trophy.statId) throw std::runtime_error(name + " has udsStatId " + describe(*stat) + ", not an unsigned integer"); + } + std::vector problems; + if (const auto* target = condition->Find("targetValue")) { + trophy.target = target->Unsigned(); + if (!trophy.target) problems.push_back("its targetValue " + describe(*target) + " is not an unsigned integer"); + } else if (trophy.statId) { + problems.emplace_back("it has no targetValue"); + } + if (const auto* comparator = condition->Find("comparator")) { + trophy.comparator = comparatorOf(*comparator); + if (trophy.comparator == Comparator::None) problems.push_back("its comparator " + describe(*comparator) + " is not ge, gt, le or lt"); + } else if (trophy.statId) { + problems.emplace_back("it has no comparator"); + } + if (!problems.empty()) skipped.push_back(name + " never unlocks through stats: " + join(problems, ", ")); + return trophy; +} + +} + +const Json* Json::Find(std::string_view key) const { + for (auto member = members.rbegin(); member != members.rend(); ++member) { + if (member->first == key) return &member->second; + } + return nullptr; +} + +bool Json::IsTrue() const { + return kind == Kind::Bool && boolean; +} + +std::optional Json::Unsigned() const { + if (kind != Kind::Number && kind != Kind::String) return std::nullopt; + std::uint64_t value = 0; + const auto* end = text.data() + text.size(); + const auto [stop, error] = std::from_chars(text.data(), end, value); + if (error != std::errc{} || stop != end) return std::nullopt; + return value; +} + +std::optional Json::Id() const { + const auto value = Unsigned(); + if (!value || *value > static_cast(INT32_MAX)) return std::nullopt; + return static_cast(*value); +} + +Json ParseJson(std::string_view text) { + return JsonReader(text).Document(); +} + +UcpFiles ReadUcp(std::string_view data) { + if (data.size() < UcpHeaderSize) throw std::runtime_error("it has " + std::to_string(data.size()) + " bytes, fewer than the " + std::to_string(UcpHeaderSize) + "-byte header"); + const auto magic = bigEndian(data, 0, 4); + if (magic != UcpMagic) throw std::runtime_error("its magic is " + hex(magic) + ", not " + hex(UcpMagic)); + const auto version = bigEndian(data, 4, 4); + if (version != UcpVersion) throw std::runtime_error("its version is " + std::to_string(version) + ", not " + std::to_string(UcpVersion)); + const auto size = bigEndian(data, 8, 8); + if (size < UcpHeaderSize || size > data.size()) throw std::runtime_error("its size " + hex(size) + " is not from the header size " + hex(UcpHeaderSize) + " to the file size " + hex(data.size())); + const auto count = bigEndian(data, 0x10, 4); + if (count > UcpMaxEntries) throw std::runtime_error("it lists " + std::to_string(count) + " files, more than " + std::to_string(UcpMaxEntries)); + const auto toc = bigEndian(data, 0x14, 4); + if (toc > size || UcpTocHeaderSize + count * UcpEntrySize > size - toc) { + throw std::runtime_error("its table of " + std::to_string(count) + " files at " + hex(toc) + " ends past its size " + hex(size)); + } + UcpFiles files; + for (std::uint64_t index = 0; index < count; ++index) { + const auto entry = toc + UcpTocHeaderSize + index * UcpEntrySize; + const auto field = data.substr(entry, UcpNameSize); + const std::string name(field.substr(0, field.find('\0'))); + const auto offset = bigEndian(data, entry + UcpNameSize, 8); + const auto length = bigEndian(data, entry + UcpNameSize + 8, 8); + if (offset > size || length > size - offset) { + throw std::runtime_error("its file \"" + name + "\" at " + hex(offset) + " with " + hex(length) + " bytes ends past its size " + hex(size)); + } + files.try_emplace(name, data.substr(offset, length)); + } + return files; +} + +bool UnlockCondition::Reached(std::uint64_t value) const { + switch (comparator) { + case Comparator::GreaterEqual: return value >= target; + case Comparator::Greater: return value > target; + case Comparator::LessEqual: return value <= target; + case Comparator::Less: return value < target; + case Comparator::None: break; + } + throw std::logic_error("an unlock condition on stat " + std::to_string(statId) + " has no comparator"); +} + +std::optional TrophyDefinition::Condition() const { + if (grade == GradePlatinum || !statId || !target || comparator == Comparator::None) return std::nullopt; + return UnlockCondition{*statId, *target, comparator}; +} + +TrophyPackage ParseTrophyPackage(std::string_view ucp, std::string_view language) { + const auto files = ReadUcp(ucp); + const auto conf = document(files, "tropconf.json"); + const auto& trophies = member(conf, "trophies", Json::Kind::Array, "tropconf.json"); + if (trophies.items.size() > MaxTrophies) throw std::runtime_error("tropconf.json lists " + std::to_string(trophies.items.size()) + " trophies, more than " + std::to_string(MaxTrophies)); + const auto& defaultLanguage = member(conf, "defaultLanguage", Json::Kind::String, "tropconf.json"); + TrophyPackage package; + const std::array candidates{language, "en-US", defaultLanguage.text}; + std::vector tried; + for (std::size_t index = 0; index < candidates.size() && package.language.empty(); ++index) { + const auto candidate = candidates[index]; + const auto earlier = candidates.begin() + static_cast(index); + if (candidate.empty() || std::find(candidates.begin(), earlier, candidate) != earlier) continue; + if (files.contains("tropmeta_" + std::string(candidate) + ".json")) { + package.language = candidate; + } else { + tried.push_back(index + 1 == candidates.size() ? "its defaultLanguage \"" + std::string(candidate) + "\"" : std::string(candidate)); + } + } + if (package.language.empty()) throw std::runtime_error("it has no tropmeta_.json for " + alternatives(tried)); + const auto metaName = "tropmeta_" + package.language + ".json"; + const auto meta = document(files, metaName); + const auto& metadata = member(meta, "metadata", Json::Kind::Object, metaName); + const auto& titleMetadata = member(metadata, "titleMetadata", Json::Kind::Object, metaName + " metadata"); + package.title = member(titleMetadata, "name", Json::Kind::String, metaName + " titleMetadata").text; + const auto& trophyMetadata = member(metadata, "trophyMetadata", Json::Kind::Array, metaName + " metadata"); + for (std::size_t index = 0; index < trophies.items.size(); ++index) { + auto trophy = readTrophy(trophies.items[index], index, package.skipped); + package.groups.try_emplace(trophy.groupId, trophy.groupId == BaseGroup ? package.title : std::string()); + if (package.groups.size() > MaxGroups) throw std::runtime_error("tropconf.json uses more than " + std::to_string(MaxGroups) + " groups"); + const auto id = trophy.id; + if (!package.trophies.emplace(id, std::move(trophy)).second) throw std::runtime_error("tropconf.json lists trophy " + std::to_string(id) + " twice"); + } + if (const auto* groups = metadata.Find("groupMetadata"); groups != nullptr && groups->kind == Json::Kind::Array) { + for (const auto& group : groups->items) { + const auto* id = group.Find("id"); + const auto* name = group.Find("name"); + const auto groupId = id != nullptr ? id->Id() : std::nullopt; + if (!groupId || name == nullptr || name->kind != Json::Kind::String) continue; + if (const auto found = package.groups.find(*groupId); found != package.groups.end()) found->second = name->text; + } + } + for (const auto& text : trophyMetadata.items) { + const auto* id = text.Find("id"); + const auto trophyId = id != nullptr ? id->Id() : std::nullopt; + const auto found = trophyId ? package.trophies.find(*trophyId) : package.trophies.end(); + if (found == package.trophies.end()) continue; + for (const auto& [key, field] : {std::pair{"name", &found->second.name}, std::pair{"detail", &found->second.detail}, std::pair{"reward", &found->second.reward}}) { + if (const auto* value = text.Find(key); value != nullptr && value->kind == Json::Kind::String) *field = value->text; + } + } + return package; +} + +std::string_view LanguageTag(int consoleLanguage) { + static constexpr std::array tags = { + "ja-JP", "en-US", "fr-FR", "es-ES", "de-DE", "it-IT", "nl-NL", "pt-PT", "ru-RU", "ko-KR", "zh-Hant", + "zh-Hans", "fi-FI", "sv-SE", "da-DK", "no-NO", "pl-PL", "pt-BR", "en-GB", "tr-TR", "es-419", "ar-AE", + "fr-CA", "cs-CZ", "hu-HU", "el-GR", "ro-RO", "th-TH", "vi-VN", "id-ID", "uk-UA"}; + if (consoleLanguage < 0 || consoleLanguage >= static_cast(tags.size())) { + throw std::out_of_range("console language " + std::to_string(consoleLanguage) + " has no locale tag"); + } + return tags[static_cast(consoleLanguage)]; +} + +} diff --git a/core/libs/prx/libSceNpTrophy2/tests/TrophyPackage.cpp b/core/libs/prx/libSceNpTrophy2/tests/TrophyPackage.cpp new file mode 100644 index 0000000000..1611b0ba99 --- /dev/null +++ b/core/libs/prx/libSceNpTrophy2/tests/TrophyPackage.cpp @@ -0,0 +1,283 @@ +#include "prx/libSceNpTrophy2/include/TitleTrophies.hpp" +#include "prx/libSceNpTrophy2/include/TrophyPackage.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using Files = std::vector>; + +void check(bool condition, const char* message) { + if (condition) return; + std::fprintf(stderr, "%s\n", message); + std::abort(); +} + +template +std::string failure(TAction action) { + try { + action(); + } catch (const std::runtime_error& error) { + return error.what(); + } + return {}; +} + +bool contains(const std::string& text, const char* part) { + return text.find(part) != std::string::npos; +} + +void putBigEndian(std::string& data, std::size_t offset, std::uint64_t value, std::size_t size) { + for (std::size_t i = 0; i < size; ++i) data[offset + i] = static_cast((value >> (8 * (size - 1 - i))) & 0xFF); +} + +std::string ucp(const Files& files) { + const std::size_t toc = 0x40; + std::size_t offset = toc + 0x20 + files.size() * 0x40; + std::size_t size = offset; + for (const auto& [name, contents] : files) size += contents.size(); + std::string data(size, '\0'); + putBigEndian(data, 0, 0xB228C60A, 4); + putBigEndian(data, 4, 1, 4); + putBigEndian(data, 8, size, 8); + putBigEndian(data, 0x10, files.size(), 4); + putBigEndian(data, 0x14, toc, 4); + std::size_t entry = toc + 0x20; + for (const auto& [name, contents] : files) { + data.replace(entry, name.size(), name); + putBigEndian(data, entry + 0x20, offset, 8); + putBigEndian(data, entry + 0x28, contents.size(), 8); + data.replace(offset, contents.size(), contents); + offset += contents.size(); + entry += 0x40; + } + return data; +} + +const char TropConf[] = R"({"defaultLanguage":"en-US","trophies":[ + {"id":"0","grade":"P","hidden":false}, + {"id":"1","grade":"G","platinumTrophyId":"0","unlockCondition":{"udsStatId":"10","comparator":"ge","targetValue":"10","progressive":false}}, + {"id":"2","grade":"S","platinumTrophyId":0,"unlockCondition":{"udsStatId":20,"comparator":"gt","targetValue":100}}, + {"id":3,"grade":"B","platinumTrophyId":"0","unlockCondition":{"udsStatId":"30","comparator":"le","targetValue":"5"}}, + {"id":"4","grade":"B","platinumTrophyId":"0","unlockCondition":{"udsStatId":"30","comparator":"lt","targetValue":"3"}}, + {"id":"5","grade":"B","platinumTrophyId":"0","unlockCondition":{"udsStatId":"40","comparator":"ge","targetValue":"5","progressive":true}}, + {"id":"6","grade":"B","groupId":"1","hidden":true,"hasReward":true,"platinumTrophyId":"-1"}, + {"id":"7","grade":"B","groupId":"1","unlockCondition":{"udsStatId":"10","comparator":"eq","targetValue":"-1"}} +]})"; + +const char TropMetaEnglish[] = R"({"metadata":{"titleMetadata":{"name":"Synthetic Game"}, + "trophyMetadata":[{"id":"0","name":"All of them","detail":"Unlock every trophy"},{"id":"1","name":"Ten","detail":"Reach ten"}, + {"id":"6","name":"Secret","detail":"A hidden one","reward":"A hat"},{"id":"99","name":"Not a trophy"}], + "groupMetadata":[{"id":"1","name":"Expansion"},{"id":"9","name":"Unused"}]}})"; + +const char TropMetaFrench[] = R"({"metadata":{"titleMetadata":{"name":"Jeu synth\u00e9tique"},"trophyMetadata":[{"id":"1","name":"Dix"}]}})"; + +std::string trophyPackage() { + return ucp({{"tropconf.json", TropConf}, {"tropmeta_en-US.json", TropMetaEnglish}, {"tropmeta_fr-FR.json", TropMetaFrench}, {"trop0001.png", "not an image"}}); +} + +void testContainer() { + const auto data = ucp({{"a.json", "{}"}, {"b.bin", std::string("\0\1\2", 3)}, {"a.json", "second"}}); + const auto files = NpTrophy2::ReadUcp(data); + check(files.size() == 2 && files.at("a.json") == "{}" && files.at("b.bin") == std::string_view("\0\1\2", 3), "the container files were not read, or a repeated name replaced the first"); + check(NpTrophy2::ReadUcp(ucp({})).empty(), "an empty container was not read"); + const std::string longName(0x20, 'n'); + const auto named = NpTrophy2::ReadUcp(ucp({{longName, "x"}})); + check(named.size() == 1 && named.begin()->first == longName, "a name that fills its 32 bytes was not read whole"); + auto padded = data + "trailing"; + check(NpTrophy2::ReadUcp(padded).size() == 2, "bytes after the container size were not ignored"); + + for (std::size_t length = 0; length < data.size(); ++length) { + check(!failure([&] { NpTrophy2::ReadUcp(std::string_view(data).substr(0, length)); }).empty(), "a truncated container was read"); + } + check(contains(failure([&] { NpTrophy2::ReadUcp(std::string_view(data).substr(0, 0x3F)); }), "fewer than the 64-byte header"), "a short header was not named"); + auto corrupt = data; + corrupt[0] = 0; + check(contains(failure([&] { NpTrophy2::ReadUcp(corrupt); }), "magic is 0x28c60a"), "a wrong magic was not named"); + corrupt = data; + putBigEndian(corrupt, 4, 2, 4); + check(contains(failure([&] { NpTrophy2::ReadUcp(corrupt); }), "version is 2"), "a wrong version was not named"); + corrupt = data; + putBigEndian(corrupt, 8, 0x3F, 8); + check(contains(failure([&] { NpTrophy2::ReadUcp(corrupt); }), "its size 0x3f"), "a size inside the header was accepted"); + corrupt = data; + putBigEndian(corrupt, 8, data.size() + 1, 8); + check(contains(failure([&] { NpTrophy2::ReadUcp(corrupt); }), "is not from the header size"), "a size past the file was accepted"); + corrupt = data; + putBigEndian(corrupt, 0x10, 4097, 4); + check(contains(failure([&] { NpTrophy2::ReadUcp(corrupt); }), "more than 4096"), "too many files were accepted"); + corrupt = data; + putBigEndian(corrupt, 0x10, 40, 4); + check(contains(failure([&] { NpTrophy2::ReadUcp(corrupt); }), "table of 40 files"), "a table past the size was accepted"); + corrupt = data; + putBigEndian(corrupt, 0x14, data.size() + 1, 4); + check(contains(failure([&] { NpTrophy2::ReadUcp(corrupt); }), "ends past its size"), "a table offset past the size was accepted"); + corrupt = data; + putBigEndian(corrupt, 0x60 + 0x20, data.size() + 1, 8); + check(contains(failure([&] { NpTrophy2::ReadUcp(corrupt); }), "its file \"a.json\""), "a file offset past the size was accepted"); + corrupt = data; + putBigEndian(corrupt, 0x60 + 0x28, ~std::uint64_t{0}, 8); + check(contains(failure([&] { NpTrophy2::ReadUcp(corrupt); }), "ends past its size"), "a file length past the size was accepted"); +} + +void testJson() { + const auto value = NpTrophy2::ParseJson("\xEF\xBB\xBF {\"a\": [1, -2.5e+3, \"x\\u00e9\\ud83d\\ude00\\n\", true, false, null, {}], \"a\": 7, \"b\": \"12\"} "); + check(value.kind == NpTrophy2::Json::Kind::Object && value.members.size() == 3, "an object was not parsed"); + check(value.Find("a") != nullptr && value.Find("a")->text == "7", "a repeated key did not keep its last value"); + const auto& list = value.members[0].second.items; + check(list.size() == 7 && list[0].text == "1" && list[1].text == "-2.5e+3" && list[2].text == "x\xC3\xA9\xF0\x9F\x98\x80\n", "array values or escapes were not parsed"); + check(list[3].IsTrue() && !list[4].IsTrue() && list[5].kind == NpTrophy2::Json::Kind::Null && list[6].kind == NpTrophy2::Json::Kind::Object, "literals were not parsed"); + check(value.Find("b")->Unsigned() == std::optional(12) && list[0].Unsigned() == std::optional(1), "numbers and numeric strings are not unsigned"); + check(!list[1].Unsigned() && !list[3].Unsigned() && !NpTrophy2::ParseJson("\"\"").Unsigned() && !NpTrophy2::ParseJson("\" 1\"").Unsigned(), "a value that is not an unsigned integer was read as one"); + check(NpTrophy2::ParseJson("18446744073709551615").Unsigned() == std::optional(UINT64_MAX) && !NpTrophy2::ParseJson("18446744073709551616").Unsigned(), "the unsigned range is wrong"); + check(NpTrophy2::ParseJson("2147483647").Id() == std::optional(INT32_MAX) && !NpTrophy2::ParseJson("2147483648").Id() && !NpTrophy2::ParseJson("-1").Id(), "the id range is wrong"); + std::string deep(64, '['); + deep += std::string(64, ']'); + check(NpTrophy2::ParseJson(deep).kind == NpTrophy2::Json::Kind::Array, "64 levels of nesting were refused"); + for (const char* text : {"", "{", "[1,]", "{\"a\":1,}", "{\"a\" 1}", "{a:1}", "01", "1.", "-", "1e", "+1", "\"\\x\"", "\"\\u12g4\"", "\"\\ud800\"", "\"\\udc00\"", + "\"\\ud800\\u0041\"", "\"a\nb\"", "\"open", "tru", "nul", "{} {}", "[1] x"}) { + check(!failure([text] { NpTrophy2::ParseJson(text); }).empty(), "malformed JSON was parsed"); + } + const auto deeper = "[" + deep + "]"; + check(contains(failure([&deeper] { NpTrophy2::ParseJson(deeper); }), "deeper than 64"), "deep nesting was not refused"); + std::string most = "["; + for (std::size_t value = 2; value < 1048576; ++value) most += "0,"; + most += "0]"; + check(NpTrophy2::ParseJson(most).items.size() == 1048575, "a document of 1048576 values was refused"); + const auto more = "[0," + most.substr(1); + check(contains(failure([&more] { NpTrophy2::ParseJson(more); }), "holds more than 1048576 values"), "a document of more than 1048576 values was parsed"); +} + +void testPackage() { + const auto package = NpTrophy2::ParseTrophyPackage(trophyPackage(), "en-US"); + check(package.language == "en-US" && package.title == "Synthetic Game" && package.trophies.size() == 8, "the package was not read"); + const auto& platinum = package.trophies.at(0); + check(platinum.grade == NpTrophy2::GradePlatinum && platinum.name == "All of them" && platinum.detail == "Unlock every trophy" && !platinum.Condition(), "the platinum was not read"); + const auto& gold = package.trophies.at(1); + check(gold.grade == NpTrophy2::GradeGold && gold.name == "Ten" && gold.platinumId == std::optional(0) && gold.groupId == NpTrophy2::BaseGroup, "trophy 1 was not read"); + const auto condition = gold.Condition(); + check(condition && condition->statId == 10 && condition->target == 10 && condition->comparator == NpTrophy2::Comparator::GreaterEqual && !gold.progressive, "trophy 1's condition was not read"); + check(package.trophies.at(2).grade == NpTrophy2::GradeSilver && package.trophies.at(3).grade == NpTrophy2::GradeBronze, "grades were not read"); + check(package.trophies.at(2).Condition()->comparator == NpTrophy2::Comparator::Greater && package.trophies.at(3).Condition()->comparator == NpTrophy2::Comparator::LessEqual && + package.trophies.at(4).Condition()->comparator == NpTrophy2::Comparator::Less, + "comparators were not read"); + const auto& progressive = package.trophies.at(5); + check(progressive.progressive && NpTrophy2::ProgressTarget(progressive) == std::optional(5) && !NpTrophy2::ProgressTarget(gold), "the progressive target was not read"); + const auto& hidden = package.trophies.at(6); + check(hidden.hidden && hidden.hasReward && hidden.groupId == 1 && hidden.name == "Secret" && hidden.reward == "A hat" && !hidden.platinumId, "trophy 6 was not read"); + check(!package.trophies.at(7).Condition() && package.trophies.at(7).name.empty(), "an unusable condition was kept"); + check(package.skipped.size() == 1 && contains(package.skipped[0], "trophy 7 never unlocks through stats") && contains(package.skipped[0], "\"-1\"") && contains(package.skipped[0], "\"eq\""), + "the unusable condition was not reported"); + check(package.groups.size() == 2 && package.groups.at(NpTrophy2::BaseGroup) == "Synthetic Game" && package.groups.at(1) == "Expansion", "groups were not read"); + + const auto french = NpTrophy2::ParseTrophyPackage(trophyPackage(), "fr-FR"); + check(french.language == "fr-FR" && french.title == "Jeu synth\xC3\xA9tique" && french.trophies.at(1).name == "Dix" && french.trophies.at(6).name.empty(), "the system language was not used"); + check(NpTrophy2::ParseTrophyPackage(trophyPackage(), "de-DE").language == "en-US", "English was not the fallback"); + std::string conf = TropConf; + conf.replace(conf.find("en-US"), 5, "ja-JP"); + check(NpTrophy2::ParseTrophyPackage(ucp({{"tropconf.json", conf}, {"tropmeta_ja-JP.json", TropMetaEnglish}}), "de-DE").language == "ja-JP", "the default language was not the last fallback"); + const auto withoutTexts = ucp({{"tropconf.json", conf}}); + check(contains(failure([&withoutTexts] { NpTrophy2::ParseTrophyPackage(withoutTexts, "de-DE"); }), "no tropmeta_.json for de-DE, en-US or its defaultLanguage \"ja-JP\""), + "a package without texts was read"); + const auto missingTexts = [](const std::string& defaultLanguage, const char* language) { + std::string text = TropConf; + text.replace(text.find("en-US"), 5, defaultLanguage); + const auto data = ucp({{"tropconf.json", text}}); + return failure([&data, language] { NpTrophy2::ParseTrophyPackage(data, language); }); + }; + check(missingTexts("en-US", "en-US") == "it has no tropmeta_.json for en-US", "a missing text named en-US more than once"); + check(missingTexts("ja-JP", "en-US") == "it has no tropmeta_.json for en-US or its defaultLanguage \"ja-JP\"", "a missing text in English named English twice"); + check(missingTexts("en-US", "de-DE") == "it has no tropmeta_.json for de-DE or en-US", "a missing text with an English default named English twice"); + + const auto broken = [](const std::string& replaced, const std::string& replacement) { + std::string text = TropConf; + text.replace(text.find(replaced), replaced.size(), replacement); + const auto data = ucp({{"tropconf.json", text}, {"tropmeta_en-US.json", TropMetaEnglish}}); + return failure([&data] { NpTrophy2::ParseTrophyPackage(data, "en-US"); }); + }; + check(contains(broken(R"("grade":"G")", R"("grade":"X")"), "trophy 1 has no grade"), "a wrong grade was accepted"); + check(contains(broken(R"("grade":"G")", R"("grade":"GS")"), "trophy 1 has no grade"), "a two-letter grade was accepted"); + check(contains(broken(R"({"id":"4",)", R"({"id":"3",)"), "lists trophy 3 twice"), "a repeated trophy was accepted"); + check(contains(broken(R"({"id":"4",)", R"({"id":"-4",)"), "trophies[4] has no id"), "a negative id was accepted"); + check(contains(broken(R"("groupId":"1","hidden")", R"("groupId":"x","hidden")"), "trophy 6 has groupId \"x\""), "a wrong group was accepted"); + check(contains(broken(R"("udsStatId":"10")", R"("udsStatId":"-10")"), "udsStatId \"-10\""), "a negative stat id was accepted"); + check(contains(broken(R"("trophies":[)", R"("trophies":0,"other":[)"), "has no trophies array"), "trophies that are not an array were accepted"); + const auto withoutConf = ucp({{"tropmeta_en-US.json", TropMetaEnglish}}); + check(contains(failure([&withoutConf] { NpTrophy2::ParseTrophyPackage(withoutConf, "en-US"); }), "no tropconf.json"), "a package without tropconf.json was read"); + const auto brokenConf = ucp({{"tropconf.json", "{\"trophies\":["}, {"tropmeta_en-US.json", TropMetaEnglish}}); + check(contains(failure([&brokenConf] { NpTrophy2::ParseTrophyPackage(brokenConf, "en-US"); }), "tropconf.json is not JSON"), "a tropconf.json that is not JSON was read"); + const auto withoutTitle = ucp({{"tropconf.json", TropConf}, {"tropmeta_en-US.json", "{\"metadata\":{}}"}}); + check(contains(failure([&withoutTitle] { NpTrophy2::ParseTrophyPackage(withoutTitle, "en-US"); }), "has no titleMetadata"), "texts without a title were read"); + std::string groups = R"({"defaultLanguage":"en-US","trophies":[)"; + for (int id = 0; id <= 50; ++id) groups += (id == 0 ? "" : ",") + std::string(R"({"id":)") + std::to_string(id) + R"(,"grade":"B","groupId":)" + std::to_string(id) + "}"; + groups += "]}"; + const auto manyGroups = ucp({{"tropconf.json", groups}, {"tropmeta_en-US.json", TropMetaEnglish}}); + check(contains(failure([&manyGroups] { NpTrophy2::ParseTrophyPackage(manyGroups, "en-US"); }), "more than 50 groups"), "more than 50 groups were accepted"); + std::string padded = TropConf; + std::string padding = "\"padding\":["; + for (std::size_t value = 0; value < 1048576; ++value) padding += "0,"; + padded.insert(1, padding + "0],"); + const auto oversized = ucp({{"tropconf.json", padded}, {"tropmeta_en-US.json", TropMetaEnglish}}); + check(contains(failure([&oversized] { NpTrophy2::ParseTrophyPackage(oversized, "en-US"); }), "tropconf.json is not JSON: the document holds more than 1048576 values"), + "a tropconf.json of more than 1048576 values was read"); + check(contains(failure([] { NpTrophy2::ParseTrophyPackage("not a package", "en-US"); }), "fewer than the 64-byte header"), "a package that is not a container was read"); + check(NpTrophy2::LanguageTag(1) == "en-US" && NpTrophy2::LanguageTag(0) == "ja-JP" && NpTrophy2::LanguageTag(2) == "fr-FR" && NpTrophy2::LanguageTag(29) == "id-ID", "language tags are wrong"); + bool thrown = false; + try { + NpTrophy2::LanguageTag(31); + } catch (const std::out_of_range&) { + thrown = true; + } + check(thrown, "language 31 has a tag"); +} + +void testProgressAndCounts() { + const auto package = NpTrophy2::ParseTrophyPackage(trophyPackage(), "en-US"); + const auto& progressive = package.trophies.at(5); + check(NpTrophy2::TrophyProgress(progressive, std::nullopt, false) == 0 && NpTrophy2::TrophyProgress(progressive, 3, false) == 3 && NpTrophy2::TrophyProgress(progressive, 9, false) == 5 && + NpTrophy2::TrophyProgress(progressive, 1, true) == 5, + "progress is not the stat up to the target"); + bool thrown = false; + try { + NpTrophy2::TrophyProgress(package.trophies.at(1), 3, false); + } catch (const std::invalid_argument&) { + thrown = true; + } + check(thrown, "a trophy that is not progressive reported progress"); + + const auto all = NpTrophy2::CountTrophies(package, std::nullopt); + check(all.trophies == 8 && all.unlocked == 0 && all.grades[NpTrophy2::GradePlatinum] == 1 && all.grades[NpTrophy2::GradeGold] == 1 && all.grades[NpTrophy2::GradeSilver] == 1 && + all.grades[NpTrophy2::GradeBronze] == 5, + "the game counts are wrong"); + check(all.Percentage() == 0, "a game without unlocks has progress"); + const auto expansion = NpTrophy2::CountTrophies(package, 1); + check(expansion.trophies == 2 && expansion.unlocked == 0 && expansion.Percentage() == 0, "the group counts are wrong"); + check(NpTrophy2::CountTrophies(package, 5).trophies == 0 && NpTrophy2::CountTrophies(package, 5).Percentage() == 0, "an empty group has trophies"); +} + +} + +int main() { + const auto previous = std::filesystem::current_path(); + const auto root = std::filesystem::temp_directory_path() / ("anyps5-trophy-package-" + std::to_string(std::random_device{}())); + std::filesystem::create_directories(root); + std::filesystem::current_path(root); + testContainer(); + testJson(); + testPackage(); + testProgressAndCounts(); + std::filesystem::current_path(previous); + std::filesystem::remove_all(root); + std::puts("Trophy package tests passed"); + return 0; +} diff --git a/core/libs/prx/libSceNpUniversalDataSystem/CMakeLists.txt b/core/libs/prx/libSceNpUniversalDataSystem/CMakeLists.txt index ffa5b0803b..a7f5848c61 100644 --- a/core/libs/prx/libSceNpUniversalDataSystem/CMakeLists.txt +++ b/core/libs/prx/libSceNpUniversalDataSystem/CMakeLists.txt @@ -13,3 +13,15 @@ target_include_directories(libSceNpUniversalDataSystem PRIVATE ) target_link_libraries(libSceNpUniversalDataSystem PRIVATE libc) configure_windows_unwind(libSceNpUniversalDataSystem) + +if(BUILD_TESTING) + add_test_executable(guest_np_trophy_data_tests ${LIBS_INCLUDE_DIR}/tests/GuestNpTrophyData.cpp) + target_include_directories(guest_np_trophy_data_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_np_trophy_data_tests PRIVATE libSceNpUniversalDataSystem libSceNpTrophy2 libc) + configure_windows_unwind(guest_np_trophy_data_tests) + add_test(NAME guest_np_trophy_data COMMAND guest_np_trophy_data_tests) + if(WIN32) + set_tests_properties(guest_np_trophy_data PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/tests/GuestNpTrophyData.cpp b/core/libs/tests/GuestNpTrophyData.cpp new file mode 100644 index 0000000000..6e07c0e769 --- /dev/null +++ b/core/libs/tests/GuestNpTrophyData.cpp @@ -0,0 +1,188 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libSceNpTrophy2/include/NpTrophy2Types.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceNpTrophy2CreateContext(int*, int, uint32_t, uint64_t); +int APS5_VABI sceNpTrophy2GetGameInfo(int, int, NpTrophy2GameDetails*, NpTrophy2GameData*); +int APS5_VABI sceNpTrophy2GetGroupInfo(int, int, int, NpTrophy2GroupDetails*, NpTrophy2GroupData*); +int APS5_VABI sceNpTrophy2GetGroupInfoArray(int, int, uint32_t, uint32_t, NpTrophy2GroupDetails*, NpTrophy2GroupData*, uint32_t*); +int APS5_VABI sceNpTrophy2GetTrophyInfo(int, int, int, NpTrophy2Details*, NpTrophy2Data*); +int APS5_VABI sceNpTrophy2GetTrophyInfoArray(int, int, uint32_t, uint32_t, NpTrophy2Details*, NpTrophy2Data*, uint32_t*); +} + +static constexpr int InitialUser = 0x10000000; + +static const char TropConf[] = R"({"defaultLanguage":"en-US","trophies":[ + {"id":"0","grade":"P"}, + {"id":"1","grade":"G","platinumTrophyId":"0","unlockCondition":{"udsStatId":"10","comparator":"ge","targetValue":"10","progressive":false}}, + {"id":"2","grade":"S","platinumTrophyId":"0","unlockCondition":{"udsStatId":"20","comparator":"gt","targetValue":"100"}}, + {"id":"3","grade":"B","platinumTrophyId":"0","unlockCondition":{"udsStatId":"30","comparator":"le","targetValue":"5"}}, + {"id":"4","grade":"B","platinumTrophyId":"0","unlockCondition":{"udsStatId":"30","comparator":"lt","targetValue":"3"}}, + {"id":"5","grade":"B","platinumTrophyId":"0","unlockCondition":{"udsStatId":"40","comparator":"ge","targetValue":"5","progressive":true}}, + {"id":"6","grade":"B","groupId":"1","hidden":true,"hasReward":true}, + {"id":"7","grade":"B","groupId":"1"} +]})"; + +static const char TropMeta[] = R"({"metadata":{"titleMetadata":{"name":"Synthetic Game"}, + "trophyMetadata":[{"id":"0","name":"All of them"},{"id":"1","name":"Ten","detail":"Reach ten"},{"id":"5","name":"Five steps"}, + {"id":"6","name":"Secret","detail":"A hidden one","reward":"A hat"}], + "groupMetadata":[{"id":"1","name":"Expansion"}]}})"; + +static void Require(bool value, const char* message) { + if (value) return; + std::fprintf(stderr, "%s\n", message); + std::abort(); +} + +template +static bool ThrowsInvalidArgument(TCall call) { + try { + call(); + } catch (const std::invalid_argument&) { + return true; + } + return false; +} + +static void PutBigEndian(std::string& data, std::size_t offset, std::uint64_t value, std::size_t size) { + for (std::size_t i = 0; i < size; ++i) data[offset + i] = static_cast((value >> (8 * (size - 1 - i))) & 0xFF); +} + +static std::string Ucp(const std::vector>& files) { + std::size_t offset = 0x60 + files.size() * 0x40; + std::size_t size = offset; + for (const auto& file : files) size += file.second.size(); + std::string data(size, '\0'); + PutBigEndian(data, 0, 0xB228C60A, 4); + PutBigEndian(data, 4, 1, 4); + PutBigEndian(data, 8, size, 8); + PutBigEndian(data, 0x10, files.size(), 4); + PutBigEndian(data, 0x14, 0x40, 4); + std::size_t entry = 0x60; + for (const auto& [name, contents] : files) { + data.replace(entry, name.size(), name); + PutBigEndian(data, entry + 0x20, offset, 8); + PutBigEndian(data, entry + 0x28, contents.size(), 8); + data.replace(offset, contents.size(), contents); + offset += contents.size(); + entry += 0x40; + } + return data; +} + +static void Write(const std::filesystem::path& path, const std::string& text) { + std::filesystem::create_directories(path.parent_path()); + std::FILE* file = std::fopen(path.string().c_str(), "wb"); + Require(file != nullptr, "a test file was not created"); + const bool written = std::fwrite(text.data(), 1, text.size(), file) == text.size(); + Require(std::fclose(file) == 0 && written, "a test file was not written"); +} + +static int TrophyContext(std::uint32_t serviceLabel) { + int context = 0; + Require(sceNpTrophy2CreateContext(&context, InitialUser, serviceLabel, 0) == 0, "trophy context was not created"); + return context; +} + +static NpTrophy2Data Trophy(int context, int trophyId, NpTrophy2Details* details = nullptr) { + NpTrophy2Data data{}; + Require(sceNpTrophy2GetTrophyInfo(context, 1, trophyId, details, &data) == 0, "trophy info was not returned"); + Require(data.trophy_id == trophyId, "trophy info is for another trophy"); + return data; +} + +static NpTrophy2GameData Game(int context, NpTrophy2GameDetails* details = nullptr) { + NpTrophy2GameData data{}; + Require(sceNpTrophy2GetGameInfo(context, 1, details, &data) == 0, "game info was not returned"); + return data; +} + +static void ReportTrophyData(int context) { + NpTrophy2GameDetails game{}; + const auto progress = Game(context, &game); + Require(std::strcmp(game.title, "Synthetic Game") == 0 && game.num_groups == 2 && game.num_trophies == 8 && game.num_platinum == 1 && game.num_gold == 1 && game.num_silver == 1 && game.num_bronze == 5, + "the game details are not the title's"); + Require(progress.unlocked_trophies == 0 && progress.progress_percentage == 0, "the game has unlocks before any event"); + + NpTrophy2Details details{}; + Trophy(context, 1, &details); + Require(std::strcmp(details.name, "Ten") == 0 && std::strcmp(details.description, "Reach ten") == 0 && details.trophy_grade == 2 && details.group_id == -1 && !details.hidden && + details.target.type == 0 && details.target.value == 0, + "trophy 1 does not report its name, grade and group"); + Trophy(context, 6, &details); + Require(std::strcmp(details.name, "Secret") == 0 && std::strcmp(details.reward, "A hat") == 0 && details.trophy_grade == 4 && details.group_id == 1 && details.hidden && details.has_reward, + "trophy 6 does not report its hidden flag, reward and group"); + Trophy(context, 0, &details); + Require(details.trophy_grade == 1 && std::strcmp(details.name, "All of them") == 0, "the platinum is not reported"); + const auto steps = Trophy(context, 5, &details); + Require(details.target.type == 1 && details.target.value == 5 && !steps.unlocked && steps.progress.type == 1 && steps.progress.value == 0, "the progressive trophy does not report its target"); + NpTrophy2Data unknown{}; + Require(ThrowsInvalidArgument([context, &unknown] { sceNpTrophy2GetTrophyInfo(context, 1, 99, nullptr, &unknown); }), "a trophy that is not in the package was reported"); + + std::vector list(10); + std::vector listed(10); + std::uint32_t count = 0; + Require(sceNpTrophy2GetTrophyInfoArray(context, 1, 0, 10, list.data(), listed.data(), &count) == 0 && count == 8, "the trophy list does not hold every trophy"); + for (std::uint32_t i = 0; i < count; ++i) Require(list[i].trophy_id == static_cast(i) && listed[i].trophy_id == static_cast(i), "the trophy list is not in id order"); + Require(std::strcmp(list[5].name, "Five steps") == 0 && listed[5].progress.type == 1, "the trophy list does not carry names and progress"); + Require(sceNpTrophy2GetTrophyInfoArray(context, 1, 6, 5, list.data(), nullptr, &count) == 0 && count == 2 && list[0].trophy_id == 6 && list[1].trophy_id == 7, + "the trophy list does not start at its offset"); + Require(sceNpTrophy2GetTrophyInfoArray(context, 1, 8, 5, nullptr, listed.data(), &count) == 0 && count == 0, "the trophy list goes past its end"); + + NpTrophy2GroupDetails group{}; + NpTrophy2GroupData groupData{}; + Require(sceNpTrophy2GetGroupInfo(context, 1, 1, &group, &groupData) == 0 && std::strcmp(group.title, "Expansion") == 0 && group.num_trophies == 2 && group.num_bronze == 2 && groupData.group_id == 1, + "group 1 is not reported"); + NpTrophy2GroupData unknownGroup{}; + Require(ThrowsInvalidArgument([context, &unknownGroup] { sceNpTrophy2GetGroupInfo(context, 1, 7, nullptr, &unknownGroup); }), "a group that is not in the package was reported"); + std::vector groups(4); + Require(sceNpTrophy2GetGroupInfoArray(context, 1, 0, 4, groups.data(), nullptr, &count) == 0 && count == 2 && groups[0].group_id == -1 && std::strcmp(groups[0].title, "Synthetic Game") == 0 && + groups[0].num_trophies == 6 && groups[1].group_id == 1, + "the group list does not hold the base game and group 1"); +} + +static void ReportPlaceholders() { + for (const std::uint32_t serviceLabel : {1u, 2u}) { + const auto context = TrophyContext(serviceLabel); + NpTrophy2GameDetails game{}; + Game(context, &game); + NpTrophy2Details details{}; + Trophy(context, 12, &details); + std::uint32_t count = 0; + NpTrophy2Data listed{}; + Require(std::strcmp(game.title, "Game") == 0 && std::strcmp(details.name, "Trophy") == 0 && + sceNpTrophy2GetTrophyInfoArray(context, 1, 0, 10, nullptr, &listed, &count) == 0 && count == 1, + "a missing or malformed trophy package does not leave the placeholders"); + } +} + +int main() { + const auto previous = std::filesystem::current_path(); + const auto root = std::filesystem::temp_directory_path() / ("anyps5-trophy-data-" + std::to_string(std::random_device{}())); + std::filesystem::create_directories(root); + std::filesystem::current_path(root); + Write("app0/sce_sys/param.json", R"({"titleId":"PPSA00000","localizedParameters":{"en-US":{"titleName":"Example"}}})"); + Write("app0/sce_sys/trophy2/trophy00.ucp", Ucp({{"tropconf.json", TropConf}, {"tropmeta_en-US.json", TropMeta}})); + Write("app0/sce_sys/trophy2/trophy02.ucp", "not a package"); + + const auto context = TrophyContext(0); + ReportTrophyData(context); + ReportPlaceholders(); + + std::filesystem::current_path(previous); + std::filesystem::remove_all(root); + std::puts("Trophy data tests passed"); + return 0; +} diff --git a/docs/dev/debt/libSceNpTrophy2.md b/docs/dev/debt/libSceNpTrophy2.md index d5e9083695..a92744d72c 100644 --- a/docs/dev/debt/libSceNpTrophy2.md +++ b/docs/dev/debt/libSceNpTrophy2.md @@ -6,6 +6,12 @@ ### Unknown function info -- [Query structures](../../../core/libs/prx/libSceNpTrophy2/include/NpTrophy2Types.hpp): a progress value is a 16-byte `{int32 type, 4 reserved bytes, uint64 value}` and the game, group and trophy structures are 152, 24, 152, 32, 1312 and 32 bytes, as [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/9e41d8320a2774b303fbad344c2347e8d0292e83/src/libs/libTrophy.cpp) asserts; no other public source confirms the layout. Progress type 1 marks a progressive trophy's target and progress, as in KytyPS5. +- [Query structures](../../../core/libs/prx/libSceNpTrophy2/include/NpTrophy2Types.hpp): a progress value is a 16-byte `{int32 type, 4 reserved bytes, uint64 value}` and the game, group and trophy structures are 152, 24, 152, 32, 1312 and 32 bytes, as [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/9e41d8320a2774b303fbad344c2347e8d0292e83/src/libs/libTrophy.cpp) asserts; no other public source confirms the layout. Progress type 1 marks a progressive trophy's target and progress, as in KytyPS5. Trophies without a `groupId` belong to group -1, KytyPS5's base game group. - [sceNpTrophy2GetRewardIcon](../../../core/libs/prx/libSceNpTrophy2/src/Context.cpp) (libSceNpTrophy2) takes `(int context, int handle, int reward_id, void* buffer, size_t* size)`, from the call sites in the bundled `Media/Plugins/PSNCore.prx` of PPSA06888, the same shape as `sceNpTrophy2GetTrophyIcon`. The title calls it twice: first with a null buffer to read the size, then with a buffer of that size allocated with `malloc`. The meaning of the third argument is unverified, and the reward icon data is unknown, so every call throws + +### Functional + +- [Trophy queries](../../../core/libs/prx/libSceNpTrophy2/src/TrophyInfo.cpp) report the title's [trophy data](../../../core/libs/prx/libSceNpTrophy2/src/TrophyPackage.cpp) (`sce_sys/trophy2/trophyNN.ucp`). Without the file, or when it is malformed, they report the old placeholders and `sceNpTrophy2GetTrophyInfoArray` lists only trophy 0. Icons are not read. Hidden trophies report their names and descriptions; whether the console blanks them before the unlock is not known. `progress_percentage` weighs gold 6, silver 2 and bronze 1 and leaves the platinum out, as KytyPS5 does (the ratio of the published PSN trophy points); the console's formula is not known. A trophy or group that is not in the file throws. Unlocks are not recorded, so every trophy reports as locked, with no unlock time and a progress of 0, and the unlocked counts and `progress_percentage` are 0. A context that was not created or was destroyed reports placeholders and returns success; the console's errors for it are unknown. +- [Trophy data limits](../../../core/libs/prx/libSceNpTrophy2/src/TrophyPackage.cpp): a `trophyNN.ucp` larger than 256 MiB or listing more than 4096 files, and a JSON file in it that nests deeper than 64 levels or holds more than 1048576 values, are not used, and the title runs as without that file. The limits keep the memory the reader takes to the file and about 140 MiB of parsed JSON; the console's limits are not known. +- [Text language](../../../core/libs/prx/libSceNpTrophy2/src/TrophyPackage.cpp): texts come from `tropmeta_.json` for the console language, then `en-US`, then the file's `defaultLanguage`. The tags for languages 0 to 29 are KytyPS5's; `uk-UA` for 30 (Ukrainian) is an assumption. From 61e12c75fdd5290c11ccefd9116e5f8726363b41 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Sat, 17 Oct 2026 23:26:06 +0000 Subject: [PATCH 0905/1000] docs: describe trophy data in USAGE --- docs/user/USAGE.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index 9d1882c0bb..bc96b850c1 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -157,6 +157,12 @@ Games that open the console's system font sets (`sceFontOpenFontSet`) need font Games ask for the console's language with `sceSystemServiceParamGetInt` and show their text in it when they have it. It is English (United States) unless `ANYPS5_LANGUAGE` is set to another console language number: 0 Japanese, 1 English (United States), 2 French, 3 Spanish, 4 German, 5 Italian, 6 Dutch, 7 Portuguese (Portugal), 8 Russian, 9 Korean, 10 Chinese (Traditional), 11 Chinese (Simplified), 12 Finnish, 13 Swedish, 14 Danish, 15 Norwegian, 16 Polish, 17 Portuguese (Brazil), 18 English (United Kingdom), 19 Turkish, 20 Spanish (Latin America), 21 Arabic, 22 French (Canada), 23 Czech, 24 Hungarian, 25 Greek, 26 Romanian, 27 Thai, 28 Vietnamese, 29 Indonesian, 30 Ukrainian. For example, `ANYPS5_LANGUAGE=21` selects Arabic. An empty value is the same as an unset one; any other value stops the game at its first language query with an error that names the variable. +### Trophies + +The trophy data comes from the game's own `app0/sce_sys/trophy2/trophyNN.ucp`, where `NN` is the service label (`00` for most games). The trophy queries report its names, descriptions and rewards in the console language (`ANYPS5_LANGUAGE`, see above), or in English (United States) or the data's default language when it has no text in that language, and its grades, groups, hidden flags, number of trophies and grade counts. The first trophy query prints what was read, such as `[trophy] /app0/sce_sys/trophy2/trophy00.ucp: 52 trophies, 1 group, texts in en-US`. + +Trophies are not unlocked yet: every trophy reports as locked, and nothing is written for them. A missing or malformed `trophyNN.ucp` makes the queries report placeholders, and a malformed one prints the reason once; `APS5_TRACE_UDS=1` prints a `[uds]` line for a missing one. Icons are not read: `sceNpTrophy2GetGameIcon`, `sceNpTrophy2GetGroupIcon`, `sceNpTrophy2GetTrophyIcon` and `sceNpTrophy2GetRewardIcon` stop the game with an error. No notification is shown, and callbacks registered with `sceNpTrophy2RegisterUnlockCallback` are not called. + ### PlayGo dumps without metadata When a dump has neither `sce_sys/playgo-chunk.dat` nor `sce_sys/playgo-scenario.json`, AnyPS5 cannot tell which nonzero PlayGo chunk IDs exist. Requests for those IDs stop with an error by default. Set `APS5_PLAYGO_UNLISTED_CHUNKS` to choose an answer for that dump: From 95179c1d61b472b713988e51f6a69d4386d78906 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Sat, 17 Oct 2026 23:27:39 +0000 Subject: [PATCH 0906/1000] feat(libSceNpTrophy2): record trophy unlocks per title and user Unlocks are kept in _trophy/_.txt in the title's run directory, one " " line per trophy, and each new unlock logs "[trophy] unlocked trophy <id> for user <id>". GetTrophyInfo, GetTrophyInfoArray, GetGroupInfo, GetGroupInfoArray and GetGameInfo report the recorded unlocks, their times, the unlocked counts by grade and the progress percentage. An unlock also unlocks the platinum the trophy names once every trophy naming that platinum is unlocked. Unlock listeners let a later notification subscribe. Removing a listener waits for its running callback, and a listener removed during a dispatch is not called by it. --- core/libs/prx/libSceNpTrophy2/CMakeLists.txt | 14 ++ .../libSceNpTrophy2/include/TitleTrophies.hpp | 14 +- .../libSceNpTrophy2/include/TrophyStore.hpp | 74 +++++++ core/libs/prx/libSceNpTrophy2/src/Context.cpp | 2 +- .../libs/prx/libSceNpTrophy2/src/GameInfo.cpp | 5 +- .../prx/libSceNpTrophy2/src/GroupInfo.cpp | 10 +- .../prx/libSceNpTrophy2/src/TitleTrophies.cpp | 21 +- .../prx/libSceNpTrophy2/src/TrophyInfo.cpp | 23 ++- .../prx/libSceNpTrophy2/src/TrophyStore.cpp | 190 +++++++++++++++++ .../libSceNpTrophy2/tests/TrophyPackage.cpp | 32 ++- .../prx/libSceNpTrophy2/tests/TrophyStore.cpp | 195 ++++++++++++++++++ docs/dev/debt/libSceNpTrophy2.md | 5 +- 12 files changed, 548 insertions(+), 37 deletions(-) create mode 100644 core/libs/prx/libSceNpTrophy2/include/TrophyStore.hpp create mode 100644 core/libs/prx/libSceNpTrophy2/src/TrophyStore.cpp create mode 100644 core/libs/prx/libSceNpTrophy2/tests/TrophyStore.cpp diff --git a/core/libs/prx/libSceNpTrophy2/CMakeLists.txt b/core/libs/prx/libSceNpTrophy2/CMakeLists.txt index cd03e490cb..a7189fbc13 100644 --- a/core/libs/prx/libSceNpTrophy2/CMakeLists.txt +++ b/core/libs/prx/libSceNpTrophy2/CMakeLists.txt @@ -4,6 +4,7 @@ add_library(libSceNpTrophy2 SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/src/GameInfo.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/TrophyInfo.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/GroupInfo.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/TrophyStore.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/TrophyPackage.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/TitleTrophies.cpp ) @@ -26,6 +27,19 @@ if(BUILD_TESTING) endif() endif() +add_test_executable(np_trophy2_store_tests EXCLUDE_FROM_ALL tests/TrophyStore.cpp) +target_include_directories(np_trophy2_store_tests PRIVATE ${LIBS_INCLUDE_DIR}) +target_link_libraries(np_trophy2_store_tests PRIVATE libSceNpTrophy2 libc) +configure_windows_unwind(np_trophy2_store_tests) +if(BUILD_TESTING) + set_target_properties(np_trophy2_store_tests PROPERTIES EXCLUDE_FROM_ALL FALSE) + add_test(NAME np_trophy2_store COMMAND np_trophy2_store_tests) + if(WIN32) + set_tests_properties(np_trophy2_store PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceNpTrophy2>") + endif() +endif() + add_test_executable(np_trophy2_package_tests EXCLUDE_FROM_ALL tests/TrophyPackage.cpp) target_include_directories(np_trophy2_package_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(np_trophy2_package_tests PRIVATE libSceNpTrophy2 libc) diff --git a/core/libs/prx/libSceNpTrophy2/include/TitleTrophies.hpp b/core/libs/prx/libSceNpTrophy2/include/TitleTrophies.hpp index 8caaa30647..74901778a9 100644 --- a/core/libs/prx/libSceNpTrophy2/include/TitleTrophies.hpp +++ b/core/libs/prx/libSceNpTrophy2/include/TitleTrophies.hpp @@ -4,6 +4,7 @@ #include <array> #include <cstddef> #include <cstdint> +#include <functional> #include <map> #include <optional> #include <string> @@ -11,6 +12,7 @@ #include <vector> #include "prx/libSceNpTrophy2/include/TrophyPackage.hpp" +#include "prx/libSceNpTrophy2/include/TrophyStore.hpp" namespace NpTrophy2 { @@ -26,15 +28,11 @@ const TitleTrophies& Title(std::uint32_t serviceLabel); const TrophyPackage* Package(std::uint32_t serviceLabel); void CopyText(char* destination, std::size_t size, std::string_view text); -struct ContextOwner { - std::int32_t userId = 0; - std::uint32_t serviceLabel = 0; -}; +using UnlockedQuery = std::function<bool(std::int32_t)>; -int CreateContext(std::int32_t userId, std::uint32_t serviceLabel); -void DestroyContext(int context); -std::optional<ContextOwner> FindContext(int context); +UnlockedQuery UnlockedBy(std::int32_t userId, std::uint32_t serviceLabel); +std::optional<std::int32_t> PlatinumToUnlock(const TrophyPackage& package, std::int32_t trophyId, const UnlockedQuery& unlocked); std::optional<std::uint64_t> ProgressTarget(const TrophyDefinition& trophy); std::uint64_t TrophyProgress(const TrophyDefinition& trophy, std::optional<std::uint64_t> stat, bool unlocked); @@ -47,7 +45,7 @@ struct TrophyCounts { std::uint32_t Percentage() const; }; -TrophyCounts CountTrophies(const TrophyPackage& package, std::optional<std::int32_t> group); +TrophyCounts CountTrophies(const TrophyPackage& package, std::optional<std::int32_t> group, const UnlockedQuery& unlocked); } diff --git a/core/libs/prx/libSceNpTrophy2/include/TrophyStore.hpp b/core/libs/prx/libSceNpTrophy2/include/TrophyStore.hpp new file mode 100644 index 0000000000..245966e568 --- /dev/null +++ b/core/libs/prx/libSceNpTrophy2/include/TrophyStore.hpp @@ -0,0 +1,74 @@ +#ifndef CORE_LIBS_PRX_LIBSCENPTROPHY2_INCLUDE_TROPHYSTORE_HPP +#define CORE_LIBS_PRX_LIBSCENPTROPHY2_INCLUDE_TROPHYSTORE_HPP + +#include <cstddef> +#include <cstdint> +#include <filesystem> +#include <map> +#include <mutex> +#include <optional> +#include <string> +#include <utility> + +struct NpTrophy2Unlock { + std::string titleId; + std::int32_t userId = 0; + std::uint32_t serviceLabel = 0; + std::int32_t trophyId = 0; + std::uint64_t tick = 0; +}; + +using NpTrophy2UnlockListener = void (*)(void* context, const NpTrophy2Unlock& unlock); + +namespace NpTrophy2 { + +inline constexpr char TrophyRootGuest[] = "/_trophy"; +inline constexpr std::uint64_t UnixEpochTick = 62135596800000000ull; + +std::uint64_t CurrentTick(); + +class TrophyStore { +public: + explicit TrophyStore(std::filesystem::path directory); + + bool Unlock(std::int32_t userId, std::uint32_t serviceLabel, std::int32_t trophyId, std::uint64_t tick); + std::optional<std::uint64_t> UnlockTick(std::int32_t userId, std::uint32_t serviceLabel, std::int32_t trophyId); + std::size_t UnlockCount(std::int32_t userId, std::uint32_t serviceLabel); + std::filesystem::path RecordPath(std::int32_t userId, std::uint32_t serviceLabel) const; + +private: + using Key = std::pair<std::int32_t, std::uint32_t>; + using Unlocks = std::map<std::int32_t, std::uint64_t>; + + Unlocks& record(const Key& key); + void save(const Key& key, const Unlocks& unlocks) const; + + std::filesystem::path directory; + std::mutex mutex; + std::map<Key, Unlocks> records; +}; + +TrophyStore& TitleStore(); + +struct ContextOwner { + std::int32_t userId = 0; + std::uint32_t serviceLabel = 0; +}; + +int CreateContext(std::int32_t userId, std::uint32_t serviceLabel); +void DestroyContext(int context); +std::optional<ContextOwner> FindContext(int context); +std::optional<std::uint64_t> ContextUnlockTick(int context, std::int32_t trophyId); +std::size_t ContextUnlockCount(int context); + +} + +extern "C" { + +bool NpTrophy2RecordUnlock_nid_no_patch(std::int32_t userId, std::uint32_t serviceLabel, std::int32_t trophyId); +std::uint64_t NpTrophy2AddUnlockListener_nid_no_patch(NpTrophy2UnlockListener listener, void* context); +void NpTrophy2RemoveUnlockListener_nid_no_patch(std::uint64_t id); + +} + +#endif diff --git a/core/libs/prx/libSceNpTrophy2/src/Context.cpp b/core/libs/prx/libSceNpTrophy2/src/Context.cpp index 90887a7f4a..ef3a23e905 100644 --- a/core/libs/prx/libSceNpTrophy2/src/Context.cpp +++ b/core/libs/prx/libSceNpTrophy2/src/Context.cpp @@ -6,7 +6,7 @@ #include <string> #include "prx/libSceNpTrophy2/include/NpTrophy2.hpp" -#include "prx/libSceNpTrophy2/include/TitleTrophies.hpp" +#include "prx/libSceNpTrophy2/include/TrophyStore.hpp" #include "prx/libc/include/General.hpp" namespace NpTrophy2 { diff --git a/core/libs/prx/libSceNpTrophy2/src/GameInfo.cpp b/core/libs/prx/libSceNpTrophy2/src/GameInfo.cpp index bea174aa49..dc11f536ef 100644 --- a/core/libs/prx/libSceNpTrophy2/src/GameInfo.cpp +++ b/core/libs/prx/libSceNpTrophy2/src/GameInfo.cpp @@ -6,6 +6,7 @@ #include "prx/libSceNpTrophy2/include/NpTrophy2.hpp" #include "prx/libSceNpTrophy2/include/TitleTrophies.hpp" +#include "prx/libSceNpTrophy2/include/TrophyStore.hpp" #include "prx/libc/include/General.hpp" extern "C" { @@ -30,7 +31,7 @@ int APS5_VABI sceNpTrophy2GetGameInfo(int context, int handle, NpTrophy2GameDeta } if (data != nullptr) { std::memset(data, 0, sizeof(*data)); - data->unlocked_trophies = NP_TROPHY2_UNLOCKED_TROPHIES; + data->unlocked_trophies = static_cast<std::uint32_t>(NpTrophy2::ContextUnlockCount(context)); data->unlocked_platinum = NP_TROPHY2_UNLOCKED_PLATINUM; data->unlocked_gold = NP_TROPHY2_UNLOCKED_GOLD; data->unlocked_silver = NP_TROPHY2_UNLOCKED_SILVER; @@ -39,7 +40,7 @@ int APS5_VABI sceNpTrophy2GetGameInfo(int context, int handle, NpTrophy2GameDeta } return SCE_NP_TROPHY2_OK; } - const auto counts = NpTrophy2::CountTrophies(*package, std::nullopt); + const auto counts = NpTrophy2::CountTrophies(*package, std::nullopt, NpTrophy2::UnlockedBy(owner->userId, owner->serviceLabel)); if (details != nullptr) { std::memset(details, 0, sizeof(*details)); details->num_groups = static_cast<std::uint32_t>(package->groups.size()); diff --git a/core/libs/prx/libSceNpTrophy2/src/GroupInfo.cpp b/core/libs/prx/libSceNpTrophy2/src/GroupInfo.cpp index badd913d14..887423ddc8 100644 --- a/core/libs/prx/libSceNpTrophy2/src/GroupInfo.cpp +++ b/core/libs/prx/libSceNpTrophy2/src/GroupInfo.cpp @@ -8,6 +8,7 @@ #include "prx/libSceNpTrophy2/include/NpTrophy2.hpp" #include "prx/libSceNpTrophy2/include/TitleTrophies.hpp" +#include "prx/libSceNpTrophy2/include/TrophyStore.hpp" #include "prx/libc/include/General.hpp" namespace { @@ -34,8 +35,9 @@ void fillPlaceholderData(std::int32_t groupId, NpTrophy2GroupData* data) { data->progress_percentage = NP_TROPHY2_PROGRESS_PERCENTAGE; } -void fillGroup(const NpTrophy2::TrophyPackage& package, std::int32_t groupId, const std::string& title, NpTrophy2GroupDetails* details, NpTrophy2GroupData* data) { - const auto counts = NpTrophy2::CountTrophies(package, groupId); +void fillGroup(const NpTrophy2::TrophyPackage& package, const NpTrophy2::ContextOwner& owner, std::int32_t groupId, const std::string& title, NpTrophy2GroupDetails* details, + NpTrophy2GroupData* data) { + const auto counts = NpTrophy2::CountTrophies(package, groupId, NpTrophy2::UnlockedBy(owner.userId, owner.serviceLabel)); if (details != nullptr) { std::memset(details, 0, sizeof(*details)); details->group_id = groupId; @@ -79,7 +81,7 @@ int APS5_VABI sceNpTrophy2GetGroupInfo(int context, int handle, int group_id, Np if (group == package->groups.end()) { throw std::invalid_argument(std::string(__func__) + ": group " + std::to_string(group_id) + " is not in " + NpTrophy2::PackagePath(owner->serviceLabel)); } - fillGroup(*package, group->first, group->second, details, data); + fillGroup(*package, *owner, group->first, group->second, details, data); return SCE_NP_TROPHY2_OK; } @@ -101,7 +103,7 @@ int APS5_VABI sceNpTrophy2GetGroupInfoArray(int context, int handle, uint32_t of auto group = package->groups.begin(); std::advance(group, std::min<std::size_t>(offset, package->groups.size())); for (; group != package->groups.end() && *count < limit; ++group, ++*count) { - fillGroup(*package, group->first, group->second, details_array != nullptr ? &details_array[*count] : nullptr, data_array != nullptr ? &data_array[*count] : nullptr); + fillGroup(*package, *owner, group->first, group->second, details_array != nullptr ? &details_array[*count] : nullptr, data_array != nullptr ? &data_array[*count] : nullptr); } return SCE_NP_TROPHY2_OK; } diff --git a/core/libs/prx/libSceNpTrophy2/src/TitleTrophies.cpp b/core/libs/prx/libSceNpTrophy2/src/TitleTrophies.cpp index 885a0b4206..dcd465e8ce 100644 --- a/core/libs/prx/libSceNpTrophy2/src/TitleTrophies.cpp +++ b/core/libs/prx/libSceNpTrophy2/src/TitleTrophies.cpp @@ -109,6 +109,22 @@ void CopyText(char* destination, std::size_t size, std::string_view text) { destination[length] = '\0'; } +UnlockedQuery UnlockedBy(std::int32_t userId, std::uint32_t serviceLabel) { + return [userId, serviceLabel](std::int32_t trophyId) { return TitleStore().UnlockTick(userId, serviceLabel, trophyId).has_value(); }; +} + +std::optional<std::int32_t> PlatinumToUnlock(const TrophyPackage& package, std::int32_t trophyId, const UnlockedQuery& unlocked) { + const auto trophy = package.trophies.find(trophyId); + if (trophy == package.trophies.end() || !trophy->second.platinumId) return std::nullopt; + const auto platinumId = *trophy->second.platinumId; + const auto platinum = package.trophies.find(platinumId); + if (platinum == package.trophies.end() || platinum->second.grade != GradePlatinum || unlocked(platinumId)) return std::nullopt; + for (const auto& [id, other] : package.trophies) { + if (id != platinumId && other.platinumId == platinumId && !unlocked(id)) return std::nullopt; + } + return platinumId; +} + std::optional<std::uint64_t> ProgressTarget(const TrophyDefinition& trophy) { if (!trophy.progressive) return std::nullopt; return trophy.target; @@ -121,12 +137,15 @@ std::uint64_t TrophyProgress(const TrophyDefinition& trophy, std::optional<std:: return std::min(stat.value_or(0), *target); } -TrophyCounts CountTrophies(const TrophyPackage& package, std::optional<std::int32_t> group) { +TrophyCounts CountTrophies(const TrophyPackage& package, std::optional<std::int32_t> group, const UnlockedQuery& unlocked) { TrophyCounts counts; for (const auto& [id, trophy] : package.trophies) { if (group && trophy.groupId != *group) continue; ++counts.trophies; ++counts.grades[static_cast<std::size_t>(trophy.grade)]; + if (!unlocked(id)) continue; + ++counts.unlocked; + ++counts.unlockedGrades[static_cast<std::size_t>(trophy.grade)]; } return counts; } diff --git a/core/libs/prx/libSceNpTrophy2/src/TrophyInfo.cpp b/core/libs/prx/libSceNpTrophy2/src/TrophyInfo.cpp index 001ba17a97..dfeba5ffbe 100644 --- a/core/libs/prx/libSceNpTrophy2/src/TrophyInfo.cpp +++ b/core/libs/prx/libSceNpTrophy2/src/TrophyInfo.cpp @@ -8,6 +8,7 @@ #include "prx/libSceNpTrophy2/include/NpTrophy2.hpp" #include "prx/libSceNpTrophy2/include/TitleTrophies.hpp" +#include "prx/libSceNpTrophy2/include/TrophyStore.hpp" #include "prx/libc/include/General.hpp" namespace { @@ -26,13 +27,14 @@ void fillPlaceholderDetails(int trophyId, NpTrophy2Details* details) { std::strncpy(details->reward, NP_TROPHY2_TROPHY_REWARD, sizeof(details->reward) - 1); } -void fillPlaceholderData(int trophyId, NpTrophy2Data* data) { +void fillPlaceholderData(int context, int trophyId, NpTrophy2Data* data) { std::memset(data, 0, sizeof(*data)); + const auto tick = NpTrophy2::ContextUnlockTick(context, trophyId); data->trophy_id = trophyId; - data->unlocked = false; + data->unlocked = tick.has_value(); data->progress.type = NP_TROPHY2_PROGRESS_TYPE_NONE; data->progress.value = NP_TROPHY2_PROGRESS_VALUE_NONE; - data->timestamp_tick = 0; + data->timestamp_tick = tick.value_or(0); } void fillDetails(const NpTrophy2::TrophyDefinition& trophy, NpTrophy2Details* details) { @@ -51,11 +53,12 @@ void fillDetails(const NpTrophy2::TrophyDefinition& trophy, NpTrophy2Details* de NpTrophy2::CopyText(details->reward, sizeof(details->reward), trophy.reward); } -void fillData(const NpTrophy2::TrophyDefinition& trophy, NpTrophy2Data* data) { +void fillData(const NpTrophy2::ContextOwner& owner, const NpTrophy2::TrophyDefinition& trophy, NpTrophy2Data* data) { std::memset(data, 0, sizeof(*data)); + const auto tick = NpTrophy2::TitleStore().UnlockTick(owner.userId, owner.serviceLabel, trophy.id); data->trophy_id = trophy.id; - data->unlocked = false; - data->timestamp_tick = 0; + data->unlocked = tick.has_value(); + data->timestamp_tick = tick.value_or(0); if (!NpTrophy2::ProgressTarget(trophy)) return; data->progress.type = NP_TROPHY2_PROGRESS_TYPE_VALUE; data->progress.value = NpTrophy2::TrophyProgress(trophy, std::nullopt, data->unlocked); @@ -74,7 +77,7 @@ int APS5_VABI sceNpTrophy2GetTrophyInfo(int context, int handle, int trophy_id, const auto* package = owner ? NpTrophy2::Package(owner->serviceLabel) : nullptr; if (package == nullptr) { if (details != nullptr) fillPlaceholderDetails(trophy_id, details); - if (data != nullptr) fillPlaceholderData(trophy_id, data); + if (data != nullptr) fillPlaceholderData(context, trophy_id, data); return SCE_NP_TROPHY2_OK; } const auto trophy = package->trophies.find(trophy_id); @@ -82,7 +85,7 @@ int APS5_VABI sceNpTrophy2GetTrophyInfo(int context, int handle, int trophy_id, throw std::invalid_argument(std::string(__func__) + ": trophy " + std::to_string(trophy_id) + " is not in " + NpTrophy2::PackagePath(owner->serviceLabel)); } if (details != nullptr) fillDetails(trophy->second, details); - if (data != nullptr) fillData(trophy->second, data); + if (data != nullptr) fillData(*owner, trophy->second, data); return SCE_NP_TROPHY2_OK; } @@ -97,7 +100,7 @@ int APS5_VABI sceNpTrophy2GetTrophyInfoArray(int context, int handle, uint32_t o const uint32_t out_count = (offset == 0 && limit != 0 ? 1u : 0u); *count = out_count; if (out_count != 0 && details_array != nullptr) fillPlaceholderDetails(NP_TROPHY2_TROPHY_ID_DEFAULT, details_array); - if (out_count != 0 && data_array != nullptr) fillPlaceholderData(NP_TROPHY2_TROPHY_ID_DEFAULT, data_array); + if (out_count != 0 && data_array != nullptr) fillPlaceholderData(context, NP_TROPHY2_TROPHY_ID_DEFAULT, data_array); return SCE_NP_TROPHY2_OK; } *count = 0; @@ -105,7 +108,7 @@ int APS5_VABI sceNpTrophy2GetTrophyInfoArray(int context, int handle, uint32_t o std::advance(trophy, std::min<std::size_t>(offset, package->trophies.size())); for (; trophy != package->trophies.end() && *count < limit; ++trophy, ++*count) { if (details_array != nullptr) fillDetails(trophy->second, &details_array[*count]); - if (data_array != nullptr) fillData(trophy->second, &data_array[*count]); + if (data_array != nullptr) fillData(*owner, trophy->second, &data_array[*count]); } return SCE_NP_TROPHY2_OK; } diff --git a/core/libs/prx/libSceNpTrophy2/src/TrophyStore.cpp b/core/libs/prx/libSceNpTrophy2/src/TrophyStore.cpp new file mode 100644 index 0000000000..501fc72223 --- /dev/null +++ b/core/libs/prx/libSceNpTrophy2/src/TrophyStore.cpp @@ -0,0 +1,190 @@ +#include "prx/libSceNpTrophy2/include/TrophyStore.hpp" +#include "prx/libSceNpTrophy2/include/TitleTrophies.hpp" +#include "prx/libc/include/General.hpp" +#include "prx/libkernel/AppMetadata/include/AppMetadata.hpp" + +#include <algorithm> +#include <charconv> +#include <chrono> +#include <cstdio> +#include <fstream> +#include <stdexcept> +#include <string_view> +#include <system_error> +#include <vector> + +namespace NpTrophy2 { + +namespace { + +template <typename TValue> +bool parseNumber(std::string_view text, TValue& value) { + const auto* end = text.data() + text.size(); + const auto [stop, error] = std::from_chars(text.data(), end, value); + return error == std::errc{} && stop == end; +} + +struct Listener { + std::uint64_t id = 0; + NpTrophy2UnlockListener callback = nullptr; + void* context = nullptr; +}; + +struct Listeners { + std::mutex mutex; + std::recursive_mutex dispatch; + std::vector<Listener> entries; + std::uint64_t next = 1; +}; + +bool registered(Listeners& registry, std::uint64_t id) { + std::lock_guard lock(registry.mutex); + return std::any_of(registry.entries.begin(), registry.entries.end(), [id](const Listener& entry) { return entry.id == id; }); +} + +Listeners& listeners() { + static Listeners registry; + return registry; +} + +} + +std::uint64_t CurrentTick() { + const auto now = std::chrono::duration_cast<std::chrono::microseconds>(std::chrono::system_clock::now().time_since_epoch()).count(); + return UnixEpochTick + static_cast<std::uint64_t>(now); +} + +TrophyStore::TrophyStore(std::filesystem::path directory) : directory(std::move(directory)) {} + +std::filesystem::path TrophyStore::RecordPath(std::int32_t userId, std::uint32_t serviceLabel) const { + return directory / (std::to_string(userId) + "_" + std::to_string(serviceLabel) + ".txt"); +} + +TrophyStore::Unlocks& TrophyStore::record(const Key& key) { + if (const auto found = records.find(key); found != records.end()) return found->second; + Unlocks unlocks; + const auto path = RecordPath(key.first, key.second); + std::error_code error; + const bool exists = std::filesystem::exists(path, error); + if (error) throw std::filesystem::filesystem_error("cannot check the trophy record", path, error); + if (exists) { + std::ifstream file(path, std::ios::binary); + if (!file) throw std::runtime_error("cannot read the trophy record " + path.string()); + std::string line; + std::size_t number = 0; + while (std::getline(file, line)) { + ++number; + const auto space = line.find(' '); + std::int32_t trophyId = 0; + std::uint64_t tick = 0; + const std::string_view text(line); + if (space == std::string::npos || !parseNumber(text.substr(0, space), trophyId) || trophyId < 0 || !parseNumber(text.substr(space + 1), tick) || !unlocks.emplace(trophyId, tick).second) { + throw std::runtime_error("the trophy record " + path.string() + " has a line " + std::to_string(number) + " that is not a new '<trophy id> <unlock tick>' pair"); + } + } + if (file.bad()) throw std::runtime_error("cannot read the trophy record " + path.string()); + } + return records.emplace(key, std::move(unlocks)).first->second; +} + +void TrophyStore::save(const Key& key, const Unlocks& unlocks) const { + std::error_code error; + if (std::filesystem::create_directories(directory, error)) RecordWrittenPath_nid_no_patch(directory); + if (error) throw std::filesystem::filesystem_error("cannot create the trophy directory", directory, error); + const auto path = RecordPath(key.first, key.second); + auto temporary = path; + temporary += ".tmp"; + { + std::ofstream file(temporary, std::ios::binary | std::ios::trunc); + for (const auto& [trophyId, tick] : unlocks) file << trophyId << ' ' << tick << '\n'; + file.flush(); + if (!file) throw std::runtime_error("cannot write the trophy record " + temporary.string()); + } + std::filesystem::rename(temporary, path); + RecordWrittenPath_nid_no_patch(path); +} + +bool TrophyStore::Unlock(std::int32_t userId, std::uint32_t serviceLabel, std::int32_t trophyId, std::uint64_t tick) { + if (trophyId < 0) throw std::invalid_argument("trophy " + std::to_string(trophyId) + " cannot be unlocked: trophy ids are not negative"); + std::lock_guard lock(mutex); + const Key key{userId, serviceLabel}; + auto& unlocks = record(key); + if (unlocks.contains(trophyId)) return false; + auto updated = unlocks; + updated.emplace(trophyId, tick); + save(key, updated); + unlocks = std::move(updated); + return true; +} + +std::optional<std::uint64_t> TrophyStore::UnlockTick(std::int32_t userId, std::uint32_t serviceLabel, std::int32_t trophyId) { + std::lock_guard lock(mutex); + const auto& unlocks = record({userId, serviceLabel}); + const auto found = unlocks.find(trophyId); + if (found == unlocks.end()) return std::nullopt; + return found->second; +} + +std::size_t TrophyStore::UnlockCount(std::int32_t userId, std::uint32_t serviceLabel) { + std::lock_guard lock(mutex); + return record({userId, serviceLabel}).size(); +} + +TrophyStore& TitleStore() { + static TrophyStore store(ResolvePath_nid_no_patch(TrophyRootGuest)); + return store; +} + +std::optional<std::uint64_t> ContextUnlockTick(int context, std::int32_t trophyId) { + const auto owner = FindContext(context); + if (!owner || trophyId < 0) return std::nullopt; + return TitleStore().UnlockTick(owner->userId, owner->serviceLabel, trophyId); +} + +std::size_t ContextUnlockCount(int context) { + const auto owner = FindContext(context); + return owner ? TitleStore().UnlockCount(owner->userId, owner->serviceLabel) : 0; +} + +} + +extern "C" bool NpTrophy2RecordUnlock_nid_no_patch(std::int32_t userId, std::uint32_t serviceLabel, std::int32_t trophyId) { + const auto title = GetAppTitleId_nid_postfix(); + const std::string_view titleId(title.value, static_cast<std::size_t>(std::find(std::begin(title.value), std::end(title.value), '\0') - std::begin(title.value))); + const NpTrophy2Unlock unlock{std::string(titleId), userId, serviceLabel, trophyId, NpTrophy2::CurrentTick()}; + if (!NpTrophy2::TitleStore().Unlock(userId, serviceLabel, trophyId, unlock.tick)) return false; + std::fprintf(stderr, "[trophy] %s unlocked trophy %d for user %d\n", unlock.titleId.c_str(), trophyId, userId); + auto& registry = NpTrophy2::listeners(); + std::lock_guard dispatching(registry.dispatch); + std::vector<NpTrophy2::Listener> current; + { + std::lock_guard lock(registry.mutex); + current = registry.entries; + } + for (const auto& entry : current) { + if (NpTrophy2::registered(registry, entry.id)) entry.callback(entry.context, unlock); + } + const auto* package = NpTrophy2::Package(serviceLabel); + if (package == nullptr) return true; + if (const auto platinum = NpTrophy2::PlatinumToUnlock(*package, trophyId, NpTrophy2::UnlockedBy(userId, serviceLabel))) NpTrophy2RecordUnlock_nid_no_patch(userId, serviceLabel, *platinum); + return true; +} + +extern "C" std::uint64_t NpTrophy2AddUnlockListener_nid_no_patch(NpTrophy2UnlockListener listener, void* context) { + if (listener == nullptr) { + APS5_INVALID_ARG_EX; + } + auto& registry = NpTrophy2::listeners(); + std::lock_guard lock(registry.mutex); + const auto id = registry.next++; + registry.entries.push_back({id, listener, context}); + return id; +} + +extern "C" void NpTrophy2RemoveUnlockListener_nid_no_patch(std::uint64_t id) { + auto& registry = NpTrophy2::listeners(); + std::lock_guard dispatching(registry.dispatch); + std::lock_guard lock(registry.mutex); + const auto removed = std::erase_if(registry.entries, [id](const NpTrophy2::Listener& entry) { return entry.id == id; }); + if (removed == 0) throw std::invalid_argument("no trophy unlock listener has id " + std::to_string(id)); +} diff --git a/core/libs/prx/libSceNpTrophy2/tests/TrophyPackage.cpp b/core/libs/prx/libSceNpTrophy2/tests/TrophyPackage.cpp index 1611b0ba99..5ae534f6f6 100644 --- a/core/libs/prx/libSceNpTrophy2/tests/TrophyPackage.cpp +++ b/core/libs/prx/libSceNpTrophy2/tests/TrophyPackage.cpp @@ -1,5 +1,6 @@ #include "prx/libSceNpTrophy2/include/TitleTrophies.hpp" #include "prx/libSceNpTrophy2/include/TrophyPackage.hpp" +#include "prx/libSceNpTrophy2/include/TrophyStore.hpp" #include <cstddef> #include <cstdint> @@ -8,6 +9,7 @@ #include <filesystem> #include <optional> #include <random> +#include <set> #include <stdexcept> #include <string> #include <utility> @@ -241,8 +243,10 @@ void testPackage() { check(thrown, "language 31 has a tag"); } -void testProgressAndCounts() { +void testProgressAndPlatinum() { const auto package = NpTrophy2::ParseTrophyPackage(trophyPackage(), "en-US"); + std::set<std::int32_t> unlocked; + const NpTrophy2::UnlockedQuery isUnlocked = [&unlocked](std::int32_t id) { return unlocked.contains(id); }; const auto& progressive = package.trophies.at(5); check(NpTrophy2::TrophyProgress(progressive, std::nullopt, false) == 0 && NpTrophy2::TrophyProgress(progressive, 3, false) == 3 && NpTrophy2::TrophyProgress(progressive, 9, false) == 5 && NpTrophy2::TrophyProgress(progressive, 1, true) == 5, @@ -255,14 +259,24 @@ void testProgressAndCounts() { } check(thrown, "a trophy that is not progressive reported progress"); - const auto all = NpTrophy2::CountTrophies(package, std::nullopt); - check(all.trophies == 8 && all.unlocked == 0 && all.grades[NpTrophy2::GradePlatinum] == 1 && all.grades[NpTrophy2::GradeGold] == 1 && all.grades[NpTrophy2::GradeSilver] == 1 && - all.grades[NpTrophy2::GradeBronze] == 5, + unlocked = {1, 2, 3, 4}; + check(!NpTrophy2::PlatinumToUnlock(package, 4, isUnlocked), "the platinum unlocked before trophy 5"); + unlocked.insert(5); + check(NpTrophy2::PlatinumToUnlock(package, 5, isUnlocked) == std::optional<std::int32_t>(0), "the platinum did not unlock after the last trophy that names it"); + check(!NpTrophy2::PlatinumToUnlock(package, 6, isUnlocked) && !NpTrophy2::PlatinumToUnlock(package, 0, isUnlocked) && !NpTrophy2::PlatinumToUnlock(package, 42, isUnlocked), + "a trophy that names no platinum unlocked one"); + unlocked.insert(0); + check(!NpTrophy2::PlatinumToUnlock(package, 5, isUnlocked), "an unlocked platinum was unlocked again"); + + unlocked = {1, 6}; + const auto all = NpTrophy2::CountTrophies(package, std::nullopt, isUnlocked); + check(all.trophies == 8 && all.unlocked == 2 && all.grades[NpTrophy2::GradePlatinum] == 1 && all.grades[NpTrophy2::GradeGold] == 1 && all.grades[NpTrophy2::GradeSilver] == 1 && + all.grades[NpTrophy2::GradeBronze] == 5 && all.unlockedGrades[NpTrophy2::GradeGold] == 1 && all.unlockedGrades[NpTrophy2::GradeBronze] == 1, "the game counts are wrong"); - check(all.Percentage() == 0, "a game without unlocks has progress"); - const auto expansion = NpTrophy2::CountTrophies(package, 1); - check(expansion.trophies == 2 && expansion.unlocked == 0 && expansion.Percentage() == 0, "the group counts are wrong"); - check(NpTrophy2::CountTrophies(package, 5).trophies == 0 && NpTrophy2::CountTrophies(package, 5).Percentage() == 0, "an empty group has trophies"); + check(all.Percentage() == (6 + 1) * 100 / (6 + 2 + 5), "the progress percentage does not weigh gold 6, silver 2 and bronze 1"); + const auto expansion = NpTrophy2::CountTrophies(package, 1, isUnlocked); + check(expansion.trophies == 2 && expansion.unlocked == 1 && expansion.Percentage() == 50, "the group counts are wrong"); + check(NpTrophy2::CountTrophies(package, 5, isUnlocked).trophies == 0 && NpTrophy2::CountTrophies(package, 5, isUnlocked).Percentage() == 0, "an empty group has trophies"); } } @@ -275,7 +289,7 @@ int main() { testContainer(); testJson(); testPackage(); - testProgressAndCounts(); + testProgressAndPlatinum(); std::filesystem::current_path(previous); std::filesystem::remove_all(root); std::puts("Trophy package tests passed"); diff --git a/core/libs/prx/libSceNpTrophy2/tests/TrophyStore.cpp b/core/libs/prx/libSceNpTrophy2/tests/TrophyStore.cpp new file mode 100644 index 0000000000..6a29e7e9c8 --- /dev/null +++ b/core/libs/prx/libSceNpTrophy2/tests/TrophyStore.cpp @@ -0,0 +1,195 @@ +#include "prx/libSceNpTrophy2/include/NpTrophy2Types.hpp" +#include "prx/libSceNpTrophy2/include/TrophyStore.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" + +#include <atomic> +#include <chrono> +#include <cstddef> +#include <cstdint> +#include <cstdio> +#include <cstdlib> +#include <filesystem> +#include <optional> +#include <random> +#include <stdexcept> +#include <string> +#include <thread> +#include <vector> + +extern "C" { +int APS5_VABI sceNpTrophy2CreateContext(int*, int, uint32_t, uint64_t); +int APS5_VABI sceNpTrophy2DestroyContext(int); +int APS5_VABI sceNpTrophy2GetGameInfo(int, int, NpTrophy2GameDetails*, NpTrophy2GameData*); +int APS5_VABI sceNpTrophy2GetTrophyInfo(int, int, int, NpTrophy2Details*, NpTrophy2Data*); +int APS5_VABI sceNpTrophy2GetTrophyInfoArray(int, int, uint32_t, uint32_t, NpTrophy2Details*, NpTrophy2Data*, uint32_t*); +} + +namespace { + +constexpr std::int32_t InitialUser = 0x10000000; + +void check(bool condition, const char* message) { + if (condition) return; + std::fprintf(stderr, "%s\n", message); + std::abort(); +} + +template <typename TAction> +bool throws(TAction action) { + try { + action(); + } catch (const std::exception&) { + return true; + } + return false; +} + +std::string read(const char* path) { + std::string text; + std::FILE* file = std::fopen(path, "rb"); + if (file == nullptr) return text; + char buffer[256]; + for (std::size_t count; (count = std::fread(buffer, 1, sizeof(buffer), file)) != 0;) text.append(buffer, count); + std::fclose(file); + return text; +} + +void write(const char* path, const std::string& text) { + std::FILE* file = std::fopen(path, "wb"); + check(file != nullptr, "test file was not created"); + const bool written = std::fwrite(text.data(), 1, text.size(), file) == text.size(); + check(std::fclose(file) == 0 && written, "test file was not written"); +} + +void testPersistence() { + const std::filesystem::path directory("store"); + { + NpTrophy2::TrophyStore store(directory); + check(!store.UnlockTick(InitialUser, 0, 12) && store.UnlockCount(InitialUser, 0) == 0, "a new store has unlocks"); + check(store.Unlock(InitialUser, 0, 12, 1000), "trophy 12 was not unlocked"); + check(!store.Unlock(InitialUser, 0, 12, 2000), "trophy 12 was unlocked twice"); + check(store.Unlock(InitialUser, 0, 3, 3000), "trophy 3 was not unlocked"); + check(store.Unlock(InitialUser + 1, 0, 12, 4000) && store.Unlock(InitialUser, 1, 12, 5000), "another user or service label shares unlocks"); + check(throws([&store] { store.Unlock(InitialUser, 0, -1, 6000); }), "a negative trophy id was unlocked"); + check(store.RecordPath(InitialUser, 0) == directory / "268435456_0.txt", "the record is not named after the user and service label"); + } + check(read("store/268435456_0.txt") == "3 3000\n12 1000\n", "the record does not list the unlocks"); + NpTrophy2::TrophyStore restarted(directory); + check(restarted.UnlockTick(InitialUser, 0, 12) == std::optional<std::uint64_t>(1000) && restarted.UnlockTick(InitialUser, 0, 3) == std::optional<std::uint64_t>(3000), + "unlock times did not survive a restart"); + check(restarted.UnlockCount(InitialUser, 0) == 2 && restarted.UnlockCount(InitialUser + 1, 0) == 1 && restarted.UnlockCount(InitialUser, 1) == 1 && restarted.UnlockCount(InitialUser, 2) == 0, + "unlock counts did not survive a restart"); + check(!restarted.Unlock(InitialUser, 0, 12, 7000) && restarted.UnlockTick(InitialUser, 0, 12) == std::optional<std::uint64_t>(1000), "a restart forgot an unlock"); + check(restarted.Unlock(InitialUser, 0, 7, 8000) && read("store/268435456_0.txt") == "3 3000\n7 8000\n12 1000\n", "an unlock after a restart lost earlier unlocks"); +} + +void testDamagedRecords() { + const std::filesystem::path directory("damaged"); + for (const char* text : {"12\n", "12 x\n", "x 5\n", "-1 5\n", "12 5\n12 6\n", "12 5 7\n", "\n", "12 5\r\n"}) { + std::filesystem::remove_all(directory); + std::filesystem::create_directories(directory); + write("damaged/268435456_0.txt", text); + NpTrophy2::TrophyStore store(directory); + check(throws([&store] { store.UnlockCount(InitialUser, 0); }), "a damaged record was read"); + } +} + +struct Seen { + std::vector<NpTrophy2Unlock> unlocks; +}; + +void remember(void* context, const NpTrophy2Unlock& unlock) { + static_cast<Seen*>(context)->unlocks.push_back(unlock); +} + +NpTrophy2Data trophyData(int context, int trophyId) { + NpTrophy2Data data{}; + check(sceNpTrophy2GetTrophyInfo(context, 1, trophyId, nullptr, &data) == 0, "GetTrophyInfo failed"); + check(data.trophy_id == trophyId, "GetTrophyInfo answered for another trophy"); + return data; +} + +std::uint32_t unlockedTrophies(int context) { + NpTrophy2GameData data{}; + check(sceNpTrophy2GetGameInfo(context, 1, nullptr, &data) == 0, "GetGameInfo failed"); + return data.unlocked_trophies; +} + +struct SlowListener { + std::atomic<bool> running{false}; + std::atomic<bool> finished{false}; +}; + +void slowly(void* context, const NpTrophy2Unlock&) { + auto* state = static_cast<SlowListener*>(context); + state->running = true; + std::this_thread::sleep_for(std::chrono::milliseconds(200)); + state->finished = true; +} + +void testListenerRemovalWaitsForItsCallback() { + SlowListener state; + const auto listener = NpTrophy2AddUnlockListener_nid_no_patch(slowly, &state); + std::thread unlocking([] { NpTrophy2RecordUnlock_nid_no_patch(InitialUser, 2, 1); }); + while (!state.running) std::this_thread::yield(); + NpTrophy2RemoveUnlockListener_nid_no_patch(listener); + const bool finished = state.finished; + unlocking.join(); + check(finished, "a listener was removed while its callback was still running, so its context could be freed under it"); +} + +void testQueries() { + int context = 0; + check(sceNpTrophy2CreateContext(&context, InitialUser, 0, 0) == 0, "context was not created"); + int other = 0; + check(sceNpTrophy2CreateContext(&other, InitialUser + 1, 0, 0) == 0 && other != context, "second context was not created"); + const auto locked = trophyData(context, 12); + check(!locked.unlocked && locked.timestamp_tick == 0, "trophy 12 is unlocked before its event"); + check(unlockedTrophies(context) == 0, "the game has unlocks before any event"); + + Seen seen; + const auto listener = NpTrophy2AddUnlockListener_nid_no_patch(remember, &seen); + const auto before = NpTrophy2::CurrentTick(); + check(NpTrophy2RecordUnlock_nid_no_patch(InitialUser, 0, 12), "trophy 12 was not recorded"); + check(!NpTrophy2RecordUnlock_nid_no_patch(InitialUser, 0, 12), "trophy 12 was recorded twice"); + check(NpTrophy2RecordUnlock_nid_no_patch(InitialUser, 0, 0), "trophy 0 was not recorded"); + const auto after = NpTrophy2::CurrentTick(); + NpTrophy2RemoveUnlockListener_nid_no_patch(listener); + check(NpTrophy2RecordUnlock_nid_no_patch(InitialUser, 0, 5) && seen.unlocks.size() == 2, "the listener did not see each new unlock once"); + const auto& first = seen.unlocks.front(); + check(first.titleId == "PPSA00000" && first.userId == InitialUser && first.serviceLabel == 0 && first.trophyId == 12 && first.tick >= before && first.tick <= after, + "the listener did not get the unlock's title, user, trophy and time"); + check(before > NpTrophy2::UnixEpochTick, "the current tick is not counted from year 1"); + + const auto unlocked = trophyData(context, 12); + check(unlocked.unlocked && unlocked.timestamp_tick == first.tick, "trophy 12 is not reported unlocked at its time"); + check(!trophyData(context, 13).unlocked, "trophy 13 is reported unlocked"); + check(unlockedTrophies(context) == 3, "the game does not count its unlocks"); + NpTrophy2Data listed{}; + std::uint32_t count = 0; + check(sceNpTrophy2GetTrophyInfoArray(context, 1, 0, 1, nullptr, &listed, &count) == 0 && count == 1 && listed.trophy_id == 0 && listed.unlocked, + "the trophy list does not report trophy 0 unlocked"); + check(!trophyData(other, 12).unlocked && unlockedTrophies(other) == 0, "another user sees the unlocks"); + check(read("_trophy/268435456_0.txt").find("12 ") != std::string::npos, "the unlock was not stored in the title folder"); + check(throws([] { NpTrophy2AddUnlockListener_nid_no_patch(nullptr, nullptr); }), "a null listener was added"); + check(throws([listener] { NpTrophy2RemoveUnlockListener_nid_no_patch(listener); }), "a removed listener was removed again"); + check(sceNpTrophy2DestroyContext(context) == 0 && sceNpTrophy2DestroyContext(other) == 0, "contexts were not destroyed"); +} + +} + +int main() { + const auto previous = std::filesystem::current_path(); + const auto root = std::filesystem::temp_directory_path() / ("anyps5-trophy-store-" + std::to_string(std::random_device{}())); + std::filesystem::create_directories(root / "app0" / "sce_sys"); + std::filesystem::current_path(root); + write("app0/sce_sys/param.json", R"({"titleId":"PPSA00000","localizedParameters":{"en-US":{"titleName":"Example"}}})"); + testPersistence(); + testDamagedRecords(); + testQueries(); + testListenerRemovalWaitsForItsCallback(); + std::filesystem::current_path(previous); + std::filesystem::remove_all(root); + std::puts("Trophy store tests passed"); + return 0; +} diff --git a/docs/dev/debt/libSceNpTrophy2.md b/docs/dev/debt/libSceNpTrophy2.md index a92744d72c..e5060029a4 100644 --- a/docs/dev/debt/libSceNpTrophy2.md +++ b/docs/dev/debt/libSceNpTrophy2.md @@ -2,7 +2,7 @@ ### Silent stubs -- [libSceNpTrophy2](../../../core/libs/prx/libSceNpTrophy2/src/GameInfo.cpp): `sceNpTrophy2RegisterUnlockCallback` accepts a callback that is never called, as no trophy is ever unlocked, `sceNpTrophy2UnregisterUnlockCallback` only returns success, and `sceNpTrophy2ShowTrophyList` returns success without showing a list (there is no system trophy UI; seen on PPSA15552). +- [libSceNpTrophy2](../../../core/libs/prx/libSceNpTrophy2/src/GameInfo.cpp): `sceNpTrophy2RegisterUnlockCallback` accepts a callback that is never called, as the callback's signature and when the console calls it are not known, `sceNpTrophy2UnregisterUnlockCallback` only returns success, and `sceNpTrophy2ShowTrophyList` returns success without showing a list (there is no system trophy UI; seen on PPSA15552). ### Unknown function info @@ -12,6 +12,7 @@ ### Functional -- [Trophy queries](../../../core/libs/prx/libSceNpTrophy2/src/TrophyInfo.cpp) report the title's [trophy data](../../../core/libs/prx/libSceNpTrophy2/src/TrophyPackage.cpp) (`sce_sys/trophy2/trophyNN.ucp`). Without the file, or when it is malformed, they report the old placeholders and `sceNpTrophy2GetTrophyInfoArray` lists only trophy 0. Icons are not read. Hidden trophies report their names and descriptions; whether the console blanks them before the unlock is not known. `progress_percentage` weighs gold 6, silver 2 and bronze 1 and leaves the platinum out, as KytyPS5 does (the ratio of the published PSN trophy points); the console's formula is not known. A trophy or group that is not in the file throws. Unlocks are not recorded, so every trophy reports as locked, with no unlock time and a progress of 0, and the unlocked counts and `progress_percentage` are 0. A context that was not created or was destroyed reports placeholders and returns success; the console's errors for it are unknown. +- [Trophy queries](../../../core/libs/prx/libSceNpTrophy2/src/TrophyInfo.cpp) report the title's [trophy data](../../../core/libs/prx/libSceNpTrophy2/src/TrophyPackage.cpp) (`sce_sys/trophy2/trophyNN.ucp`) and the unlocks in the [trophy store](../../../core/libs/prx/libSceNpTrophy2/src/TrophyStore.cpp). Without the file, or when it is malformed, they report the old placeholders and `sceNpTrophy2GetTrophyInfoArray` lists only trophy 0. Icons are not read. Hidden trophies report their names and descriptions; whether the console blanks them before the unlock is not known. `progress_percentage` weighs gold 6, silver 2 and bronze 1 and leaves the platinum out, as KytyPS5 does (the ratio of the published PSN trophy points); the console's formula is not known. A trophy or group that is not in the file throws. A context that was not created or was destroyed reports placeholders and no unlocks and returns success; the console's errors for it are unknown. - [Trophy data limits](../../../core/libs/prx/libSceNpTrophy2/src/TrophyPackage.cpp): a `trophyNN.ucp` larger than 256 MiB or listing more than 4096 files, and a JSON file in it that nests deeper than 64 levels or holds more than 1048576 values, are not used, and the title runs as without that file. The limits keep the memory the reader takes to the file and about 140 MiB of parsed JSON; the console's limits are not known. - [Text language](../../../core/libs/prx/libSceNpTrophy2/src/TrophyPackage.cpp): texts come from `tropmeta_<tag>.json` for the console language, then `en-US`, then the file's `defaultLanguage`. The tags for languages 0 to 29 are KytyPS5's; `uk-UA` for 30 (Ukrainian) is an assumption. +- [Platinum](../../../core/libs/prx/libSceNpTrophy2/src/TitleTrophies.cpp): an unlock through any path unlocks the platinum that the trophy names in `platinumTrophyId` once every trophy naming that platinum is unlocked, as KytyPS5 does; trophies without `platinumTrophyId` are not required. Without trophy data any id of 0 or more is stored, and the platinum is not unlocked when the trophies it needs are. From 98c700ea02f40d3f73efca9b62c16817a96677b4 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Sat, 17 Oct 2026 09:35:51 +0000 Subject: [PATCH 0907/1000] feat(libSceNpUniversalDataSystem): keep event properties and record posted trophy unlocks Events and property objects keep their typed properties instead of dropping them. PostEvent records an _UnlockTrophy event with an int32 _trophy_id for the user and service label of its context, logs why a trophy event is ignored, and prints every posted event with APS5_TRACE_UDS. --- core/libs/SceTypes.hpp | 4 - .../CMakeLists.txt | 31 ++- .../include/UdsEvent.hpp | 65 +++++ .../src/Context.cpp | 39 ++- .../libSceNpUniversalDataSystem/src/Event.cpp | 65 ++++- .../src/EventPropertyArray.cpp | 38 ++- .../src/EventPropertyObject.cpp | 38 ++- .../src/NpUniversalDataSystem.hpp | 46 +++ .../src/UdsEvent.cpp | 147 ++++++++++ .../tests/Events.cpp | 262 ++++++++++++++++++ core/libs/tests/GuestNpTrophyData.cpp | 59 ++++ core/libs/tests/GuestNpTrophyUnlocks.cpp | 109 ++++++++ docs/dev/debt/libSceNpTrophy2.md | 2 +- docs/dev/debt/libSceNpUniversalDataSystem.md | 9 + 14 files changed, 886 insertions(+), 28 deletions(-) create mode 100644 core/libs/prx/libSceNpUniversalDataSystem/include/UdsEvent.hpp create mode 100644 core/libs/prx/libSceNpUniversalDataSystem/src/UdsEvent.cpp create mode 100644 core/libs/prx/libSceNpUniversalDataSystem/tests/Events.cpp create mode 100644 core/libs/tests/GuestNpTrophyUnlocks.cpp create mode 100644 docs/dev/debt/libSceNpUniversalDataSystem.md diff --git a/core/libs/SceTypes.hpp b/core/libs/SceTypes.hpp index 5d444a5034..237e9166d9 100644 --- a/core/libs/SceTypes.hpp +++ b/core/libs/SceTypes.hpp @@ -1380,10 +1380,6 @@ struct NpUniversalDataSystemMemoryStat { std::size_t current_inuse_size; }; -struct NpUniversalDataSystemEvent {}; -struct NpUniversalDataSystemEventPropertyObject {}; -struct NpUniversalDataSystemEventPropertyArray {}; - struct NpUniversalDataSystemStorageStat { std::size_t in_events; std::size_t out_events; diff --git a/core/libs/prx/libSceNpUniversalDataSystem/CMakeLists.txt b/core/libs/prx/libSceNpUniversalDataSystem/CMakeLists.txt index a7f5848c61..d7b5a34d12 100644 --- a/core/libs/prx/libSceNpUniversalDataSystem/CMakeLists.txt +++ b/core/libs/prx/libSceNpUniversalDataSystem/CMakeLists.txt @@ -5,16 +5,45 @@ add_library(libSceNpUniversalDataSystem SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/src/Event.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/EventPropertyObject.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/EventPropertyArray.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/UdsEvent.cpp ) target_include_directories(libSceNpUniversalDataSystem PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/src ) -target_link_libraries(libSceNpUniversalDataSystem PRIVATE libc) +target_link_libraries(libSceNpUniversalDataSystem PRIVATE libSceNpTrophy2 libc) configure_windows_unwind(libSceNpUniversalDataSystem) +add_test_executable(np_uds_events_tests EXCLUDE_FROM_ALL + tests/Events.cpp + src/Context.cpp + src/Event.cpp + src/EventPropertyArray.cpp + src/EventPropertyObject.cpp + src/UdsEvent.cpp +) +target_include_directories(np_uds_events_tests PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/src) +target_link_libraries(np_uds_events_tests PRIVATE libSceNpTrophy2 libc) +configure_windows_unwind(np_uds_events_tests) if(BUILD_TESTING) + set_target_properties(np_uds_events_tests PROPERTIES EXCLUDE_FROM_ALL FALSE) + add_test(NAME np_uds_events COMMAND np_uds_events_tests) + if(WIN32) + set_tests_properties(np_uds_events PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceNpTrophy2>") + endif() + + add_test_executable(guest_np_trophy_unlocks_tests ${LIBS_INCLUDE_DIR}/tests/GuestNpTrophyUnlocks.cpp) + target_include_directories(guest_np_trophy_unlocks_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_np_trophy_unlocks_tests PRIVATE libSceNpUniversalDataSystem libSceNpTrophy2 libc) + configure_windows_unwind(guest_np_trophy_unlocks_tests) + add_test(NAME guest_np_trophy_unlocks COMMAND guest_np_trophy_unlocks_tests) + if(WIN32) + set_tests_properties(guest_np_trophy_unlocks PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceNpUniversalDataSystem>") + endif() + add_test_executable(guest_np_trophy_data_tests ${LIBS_INCLUDE_DIR}/tests/GuestNpTrophyData.cpp) target_include_directories(guest_np_trophy_data_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_np_trophy_data_tests PRIVATE libSceNpUniversalDataSystem libSceNpTrophy2 libc) diff --git a/core/libs/prx/libSceNpUniversalDataSystem/include/UdsEvent.hpp b/core/libs/prx/libSceNpUniversalDataSystem/include/UdsEvent.hpp new file mode 100644 index 0000000000..d3cae5efb2 --- /dev/null +++ b/core/libs/prx/libSceNpUniversalDataSystem/include/UdsEvent.hpp @@ -0,0 +1,65 @@ +#ifndef CORE_LIBS_PRX_LIBSCENPUNIVERSALDATASYSTEM_INCLUDE_UDSEVENT_HPP +#define CORE_LIBS_PRX_LIBSCENPUNIVERSALDATASYSTEM_INCLUDE_UDSEVENT_HPP + +#include <cstdint> +#include <memory> +#include <string> +#include <string_view> +#include <utility> +#include <variant> +#include <vector> + +struct NpUniversalDataSystemEventPropertyObject; +struct NpUniversalDataSystemEventPropertyArray; + +namespace Uds { + +using Binary = std::vector<std::uint8_t>; +using Value = std::variant<std::int32_t, std::uint32_t, std::int64_t, std::uint64_t, float, double, bool, std::string, Binary, + std::shared_ptr<NpUniversalDataSystemEventPropertyObject>, std::shared_ptr<NpUniversalDataSystemEventPropertyArray>>; + +Value Clone(const Value& value); + +} + +struct NpUniversalDataSystemEventPropertyObject { + std::vector<std::pair<std::string, Uds::Value>> properties; + + Uds::Value& Set(std::string_view key, Uds::Value value); + const Uds::Value* Find(std::string_view key) const; + NpUniversalDataSystemEventPropertyObject Clone() const; +}; + +struct NpUniversalDataSystemEventPropertyArray { + std::vector<Uds::Value> values; + + Uds::Value& Add(Uds::Value value); + NpUniversalDataSystemEventPropertyArray Clone() const; +}; + +struct NpUniversalDataSystemEvent { + std::string name; + NpUniversalDataSystemEventPropertyObject properties; +}; + +namespace Uds { + +inline constexpr std::string_view UnlockTrophyEvent = "_UnlockTrophy"; +inline constexpr std::string_view UpdateTrophyProgressEvent = "_UpdateTrophyProgress"; +inline constexpr std::string_view TrophyIdProperty = "_trophy_id"; + +enum class TrophyEventKind { None, Unlock, Progress, Malformed }; + +struct TrophyEvent { + TrophyEventKind kind = TrophyEventKind::None; + std::int32_t trophyId = -1; + std::string problem; +}; + +TrophyEvent RecognizeTrophyEvent(const NpUniversalDataSystemEvent& event); +std::string Describe(const NpUniversalDataSystemEventPropertyObject& object); +std::string Describe(const NpUniversalDataSystemEvent& event); + +} + +#endif diff --git a/core/libs/prx/libSceNpUniversalDataSystem/src/Context.cpp b/core/libs/prx/libSceNpUniversalDataSystem/src/Context.cpp index da087ae06f..f73c4a2cb2 100644 --- a/core/libs/prx/libSceNpUniversalDataSystem/src/Context.cpp +++ b/core/libs/prx/libSceNpUniversalDataSystem/src/Context.cpp @@ -1,21 +1,58 @@ #include <cstdint> +#include <map> +#include <mutex> +#include <optional> #include <stdexcept> +#include <string> #include "NpUniversalDataSystem.hpp" #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +namespace Uds { + +namespace { + +struct Contexts { + std::mutex mutex; + std::map<int, ContextOwner> owners; + int next = NP_UNIVERSAL_DATA_SYSTEM_CONTEXT_DEFAULT; +}; + +Contexts& contexts() { + static Contexts table; + return table; +} + +} + +std::optional<ContextOwner> FindContext(int context) { + auto& table = contexts(); + std::lock_guard lock(table.mutex); + const auto found = table.owners.find(context); + if (found == table.owners.end()) return std::nullopt; + return found->second; +} + +} + extern "C" { int APS5_VABI sceNpUniversalDataSystemCreateContext(int* context, int user_id, uint32_t service_label, uint64_t options) { if (context == nullptr) { throw std::invalid_argument(std::string(__func__) + ": invalid argument context"); } - *context = NP_UNIVERSAL_DATA_SYSTEM_CONTEXT_DEFAULT; + auto& table = Uds::contexts(); + std::lock_guard lock(table.mutex); + *context = table.next++; + table.owners[*context] = {user_id, service_label}; return SCE_NP_UNIVERSAL_DATA_SYSTEM_OK; } int APS5_VABI sceNpUniversalDataSystemDestroyContext(int context) { + auto& table = Uds::contexts(); + std::lock_guard lock(table.mutex); + table.owners.erase(context); return SCE_NP_UNIVERSAL_DATA_SYSTEM_OK; } diff --git a/core/libs/prx/libSceNpUniversalDataSystem/src/Event.cpp b/core/libs/prx/libSceNpUniversalDataSystem/src/Event.cpp index ae5ea0f668..0ead101091 100644 --- a/core/libs/prx/libSceNpUniversalDataSystem/src/Event.cpp +++ b/core/libs/prx/libSceNpUniversalDataSystem/src/Event.cpp @@ -1,13 +1,59 @@ #include <cstddef> #include <cstdint> +#include <cstdio> +#include <cstdlib> #include <cstring> +#include <memory> +#include <mutex> +#include <set> #include <stdexcept> #include <string> #include "NpUniversalDataSystem.hpp" #include "SceTypes.hpp" +#include "prx/libSceNpTrophy2/include/TrophyStore.hpp" #include "prx/libc/include/General.hpp" +namespace { + +Uds::Owned<NpUniversalDataSystemEvent>& events() { + static Uds::Owned<NpUniversalDataSystemEvent> registry; + return registry; +} + +bool firstProgress(std::int32_t trophyId) { + static std::mutex mutex; + static std::set<std::int32_t> logged; + std::lock_guard lock(mutex); + return logged.insert(trophyId).second; +} + +void ignore(const NpUniversalDataSystemEvent& event, int context, const std::string& reason) { + std::fprintf(stderr, "[trophy] ignored %s posted to context %d: %s\n", Uds::Describe(event).c_str(), context, reason.c_str()); +} + +void post(const NpUniversalDataSystemEvent& event, int context) { + static const bool trace = std::getenv("APS5_TRACE_UDS") != nullptr; + if (trace) std::fprintf(stderr, "[uds] context %d posted %s\n", context, Uds::Describe(event).c_str()); + const auto trophy = Uds::RecognizeTrophyEvent(event); + switch (trophy.kind) { + case Uds::TrophyEventKind::None: + return; + case Uds::TrophyEventKind::Malformed: + ignore(event, context, trophy.problem); + return; + case Uds::TrophyEventKind::Progress: + if (firstProgress(trophy.trophyId)) ignore(event, context, "trophy progress is not recorded"); + return; + case Uds::TrophyEventKind::Unlock: + if (const auto owner = Uds::FindContext(context)) NpTrophy2RecordUnlock_nid_no_patch(owner->userId, owner->serviceLabel, trophy.trophyId); + else ignore(event, context, "the context does not exist"); + return; + } +} + +} + extern "C" { int APS5_VABI sceNpUniversalDataSystemCreateEvent( @@ -19,21 +65,30 @@ int APS5_VABI sceNpUniversalDataSystemCreateEvent( if (event_name == nullptr || new_event == nullptr) { APS5_INVALID_ARG_EX; } - *new_event = new NpUniversalDataSystemEvent; + auto event = std::make_unique<NpUniversalDataSystemEvent>(); + event->name = event_name; + if (prop != nullptr) event->properties = prop->Clone(); + *new_event = events().Adopt(std::move(event)); if (prop_ptr != nullptr) { - *prop_ptr = (prop != nullptr - ? const_cast<NpUniversalDataSystemEventPropertyObject*>(prop) - : new NpUniversalDataSystemEventPropertyObject); + *prop_ptr = &(*new_event)->properties; } return SCE_NP_UNIVERSAL_DATA_SYSTEM_OK; } int APS5_VABI sceNpUniversalDataSystemDestroyEvent(NpUniversalDataSystemEvent* event) { - delete event; + events().Release(event); return SCE_NP_UNIVERSAL_DATA_SYSTEM_OK; } int APS5_VABI sceNpUniversalDataSystemPostEvent(int context, int handle, const void* event, uint64_t options) { + if (event == nullptr) { + APS5_INVALID_ARG_EX; + } + const auto* posted = static_cast<const NpUniversalDataSystemEvent*>(event); + if (!events().Contains(posted)) { + throw std::invalid_argument(std::string(__func__) + ": the event was not created by sceNpUniversalDataSystemCreateEvent or was destroyed"); + } + post(*posted, context); return SCE_NP_UNIVERSAL_DATA_SYSTEM_OK; } diff --git a/core/libs/prx/libSceNpUniversalDataSystem/src/EventPropertyArray.cpp b/core/libs/prx/libSceNpUniversalDataSystem/src/EventPropertyArray.cpp index fd1c988b32..57e985ba8f 100644 --- a/core/libs/prx/libSceNpUniversalDataSystem/src/EventPropertyArray.cpp +++ b/core/libs/prx/libSceNpUniversalDataSystem/src/EventPropertyArray.cpp @@ -1,21 +1,33 @@ #include <cstddef> #include <cstdint> +#include <memory> +#include <string> +#include <utility> #include "NpUniversalDataSystem.hpp" #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +namespace { + +Uds::Owned<NpUniversalDataSystemEventPropertyArray>& arrays() { + static Uds::Owned<NpUniversalDataSystemEventPropertyArray> registry; + return registry; +} + +} + extern "C" { int APS5_VABI sceNpUniversalDataSystemCreateEventPropertyArray(NpUniversalDataSystemEventPropertyArray** new_array) { if (new_array == nullptr) { APS5_INVALID_ARG_EX; } - *new_array = new NpUniversalDataSystemEventPropertyArray; + *new_array = arrays().Adopt(std::make_unique<NpUniversalDataSystemEventPropertyArray>()); return SCE_NP_UNIVERSAL_DATA_SYSTEM_OK; } int APS5_VABI sceNpUniversalDataSystemDestroyEventPropertyArray(NpUniversalDataSystemEventPropertyArray* array) { - delete array; + arrays().Release(array); return SCE_NP_UNIVERSAL_DATA_SYSTEM_OK; } @@ -25,6 +37,7 @@ int APS5_VABI sceNpUniversalDataSystemEventPropertyArraySetString( if (array == nullptr || value == nullptr) { APS5_INVALID_ARG_EX; } + array->Add(std::string(value)); return SCE_NP_UNIVERSAL_DATA_SYSTEM_OK; } @@ -34,6 +47,7 @@ int APS5_VABI sceNpUniversalDataSystemEventPropertyArraySetInt32( if (array == nullptr) { APS5_INVALID_ARG_EX; } + array->Add(value); return SCE_NP_UNIVERSAL_DATA_SYSTEM_OK; } @@ -43,6 +57,7 @@ int APS5_VABI sceNpUniversalDataSystemEventPropertyArraySetUInt32( if (array == nullptr) { APS5_INVALID_ARG_EX; } + array->Add(value); return SCE_NP_UNIVERSAL_DATA_SYSTEM_OK; } @@ -52,6 +67,7 @@ int APS5_VABI sceNpUniversalDataSystemEventPropertyArraySetInt64( if (array == nullptr) { APS5_INVALID_ARG_EX; } + array->Add(value); return SCE_NP_UNIVERSAL_DATA_SYSTEM_OK; } @@ -61,6 +77,7 @@ int APS5_VABI sceNpUniversalDataSystemEventPropertyArraySetUInt64( if (array == nullptr) { APS5_INVALID_ARG_EX; } + array->Add(value); return SCE_NP_UNIVERSAL_DATA_SYSTEM_OK; } @@ -70,6 +87,7 @@ int APS5_VABI sceNpUniversalDataSystemEventPropertyArraySetFloat32( if (array == nullptr) { APS5_INVALID_ARG_EX; } + array->Add(value); return SCE_NP_UNIVERSAL_DATA_SYSTEM_OK; } @@ -79,6 +97,7 @@ NpUniversalDataSystemEventPropertyArray* array, double value) if (array == nullptr) { APS5_INVALID_ARG_EX; } + array->Add(value); return SCE_NP_UNIVERSAL_DATA_SYSTEM_OK; } @@ -88,6 +107,7 @@ int APS5_VABI sceNpUniversalDataSystemEventPropertyArraySetBool( if (array == nullptr) { APS5_INVALID_ARG_EX; } + array->Add(value); return SCE_NP_UNIVERSAL_DATA_SYSTEM_OK; } @@ -97,6 +117,8 @@ int APS5_VABI sceNpUniversalDataSystemEventPropertyArraySetBinary( if (array == nullptr || value == nullptr) { APS5_INVALID_ARG_EX; } + const auto* bytes = static_cast<const std::uint8_t*>(value); + array->Add(Uds::Binary(bytes, bytes + value_size)); return SCE_NP_UNIVERSAL_DATA_SYSTEM_OK; } @@ -108,11 +130,11 @@ int APS5_VABI sceNpUniversalDataSystemEventPropertyArraySetObject( if (array == nullptr) { APS5_INVALID_ARG_EX; } + auto child = std::make_shared<NpUniversalDataSystemEventPropertyObject>(value != nullptr ? value->Clone() : NpUniversalDataSystemEventPropertyObject{}); if (value_ptr != nullptr) { - *value_ptr = (value != nullptr - ? const_cast<NpUniversalDataSystemEventPropertyObject*>(value) - : new NpUniversalDataSystemEventPropertyObject); + *value_ptr = child.get(); } + array->Add(std::move(child)); return SCE_NP_UNIVERSAL_DATA_SYSTEM_OK; } @@ -124,11 +146,11 @@ int APS5_VABI sceNpUniversalDataSystemEventPropertyArraySetArray( if (array == nullptr) { APS5_INVALID_ARG_EX; } + auto child = std::make_shared<NpUniversalDataSystemEventPropertyArray>(value != nullptr ? value->Clone() : NpUniversalDataSystemEventPropertyArray{}); if (value_ptr != nullptr) { - *value_ptr = (value != nullptr - ? const_cast<NpUniversalDataSystemEventPropertyArray*>(value) - : new NpUniversalDataSystemEventPropertyArray); + *value_ptr = child.get(); } + array->Add(std::move(child)); return SCE_NP_UNIVERSAL_DATA_SYSTEM_OK; } diff --git a/core/libs/prx/libSceNpUniversalDataSystem/src/EventPropertyObject.cpp b/core/libs/prx/libSceNpUniversalDataSystem/src/EventPropertyObject.cpp index 48fc34544c..d316c1ea30 100644 --- a/core/libs/prx/libSceNpUniversalDataSystem/src/EventPropertyObject.cpp +++ b/core/libs/prx/libSceNpUniversalDataSystem/src/EventPropertyObject.cpp @@ -1,21 +1,33 @@ #include <cstddef> #include <cstdint> +#include <memory> +#include <string> +#include <utility> #include "NpUniversalDataSystem.hpp" #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +namespace { + +Uds::Owned<NpUniversalDataSystemEventPropertyObject>& objects() { + static Uds::Owned<NpUniversalDataSystemEventPropertyObject> registry; + return registry; +} + +} + extern "C" { int APS5_VABI sceNpUniversalDataSystemCreateEventPropertyObject(NpUniversalDataSystemEventPropertyObject** new_object) { if (new_object == nullptr) { APS5_INVALID_ARG_EX; } - *new_object = new NpUniversalDataSystemEventPropertyObject; + *new_object = objects().Adopt(std::make_unique<NpUniversalDataSystemEventPropertyObject>()); return SCE_NP_UNIVERSAL_DATA_SYSTEM_OK; } int APS5_VABI sceNpUniversalDataSystemDestroyEventPropertyObject(NpUniversalDataSystemEventPropertyObject* object) { - delete object; + objects().Release(object); return SCE_NP_UNIVERSAL_DATA_SYSTEM_OK; } @@ -25,6 +37,7 @@ int APS5_VABI sceNpUniversalDataSystemEventPropertyObjectSetString( if (object == nullptr || key == nullptr || value == nullptr) { APS5_INVALID_ARG_EX; } + object->Set(key, std::string(value)); return SCE_NP_UNIVERSAL_DATA_SYSTEM_OK; } @@ -34,6 +47,7 @@ int APS5_VABI sceNpUniversalDataSystemEventPropertyObjectSetInt32( if (object == nullptr || key == nullptr) { APS5_INVALID_ARG_EX; } + object->Set(key, value); return SCE_NP_UNIVERSAL_DATA_SYSTEM_OK; } @@ -43,6 +57,7 @@ int APS5_VABI sceNpUniversalDataSystemEventPropertyObjectSetUInt32( if (object == nullptr || key == nullptr) { APS5_INVALID_ARG_EX; } + object->Set(key, value); return SCE_NP_UNIVERSAL_DATA_SYSTEM_OK; } @@ -52,6 +67,7 @@ int APS5_VABI sceNpUniversalDataSystemEventPropertyObjectSetInt64( if (object == nullptr || key == nullptr) { APS5_INVALID_ARG_EX; } + object->Set(key, value); return SCE_NP_UNIVERSAL_DATA_SYSTEM_OK; } @@ -61,6 +77,7 @@ int APS5_VABI sceNpUniversalDataSystemEventPropertyObjectSetUInt64( if (object == nullptr || key == nullptr) { APS5_INVALID_ARG_EX; } + object->Set(key, value); return SCE_NP_UNIVERSAL_DATA_SYSTEM_OK; } @@ -70,6 +87,7 @@ int APS5_VABI sceNpUniversalDataSystemEventPropertyObjectSetFloat32( if (object == nullptr || key == nullptr) { APS5_INVALID_ARG_EX; } + object->Set(key, value); return SCE_NP_UNIVERSAL_DATA_SYSTEM_OK; } @@ -79,6 +97,7 @@ int APS5_VABI sceNpUniversalDataSystemEventPropertyObjectSetFloat64( if (object == nullptr || key == nullptr) { APS5_INVALID_ARG_EX; } + object->Set(key, value); return SCE_NP_UNIVERSAL_DATA_SYSTEM_OK; } @@ -88,6 +107,7 @@ int APS5_VABI sceNpUniversalDataSystemEventPropertyObjectSetBool( if (object == nullptr || key == nullptr) { APS5_INVALID_ARG_EX; } + object->Set(key, value); return SCE_NP_UNIVERSAL_DATA_SYSTEM_OK; } @@ -97,6 +117,8 @@ int APS5_VABI sceNpUniversalDataSystemEventPropertyObjectSetBinary( if (object == nullptr || key == nullptr || value == nullptr) { APS5_INVALID_ARG_EX; } + const auto* bytes = static_cast<const std::uint8_t*>(value); + object->Set(key, Uds::Binary(bytes, bytes + value_size)); return SCE_NP_UNIVERSAL_DATA_SYSTEM_OK; } @@ -109,11 +131,11 @@ int APS5_VABI sceNpUniversalDataSystemEventPropertyObjectSetObject( if (object == nullptr || key == nullptr) { APS5_INVALID_ARG_EX; } + auto child = std::make_shared<NpUniversalDataSystemEventPropertyObject>(value != nullptr ? value->Clone() : NpUniversalDataSystemEventPropertyObject{}); if (value_ptr != nullptr) { - *value_ptr = (value != nullptr - ? const_cast<NpUniversalDataSystemEventPropertyObject*>(value) - : new NpUniversalDataSystemEventPropertyObject); + *value_ptr = child.get(); } + object->Set(key, std::move(child)); return SCE_NP_UNIVERSAL_DATA_SYSTEM_OK; } @@ -126,11 +148,11 @@ int APS5_VABI sceNpUniversalDataSystemEventPropertyObjectSetArray( if (object == nullptr || key == nullptr) { APS5_INVALID_ARG_EX; } + auto child = std::make_shared<NpUniversalDataSystemEventPropertyArray>(value != nullptr ? value->Clone() : NpUniversalDataSystemEventPropertyArray{}); if (value_ptr != nullptr) { - *value_ptr = (value != nullptr - ? const_cast<NpUniversalDataSystemEventPropertyArray*>(value) - : new NpUniversalDataSystemEventPropertyArray); + *value_ptr = child.get(); } + object->Set(key, std::move(child)); return SCE_NP_UNIVERSAL_DATA_SYSTEM_OK; } diff --git a/core/libs/prx/libSceNpUniversalDataSystem/src/NpUniversalDataSystem.hpp b/core/libs/prx/libSceNpUniversalDataSystem/src/NpUniversalDataSystem.hpp index 4a02cb68e0..e89083d5bd 100644 --- a/core/libs/prx/libSceNpUniversalDataSystem/src/NpUniversalDataSystem.hpp +++ b/core/libs/prx/libSceNpUniversalDataSystem/src/NpUniversalDataSystem.hpp @@ -3,8 +3,14 @@ #include <cstddef> #include <cstdint> +#include <map> +#include <memory> +#include <mutex> +#include <optional> +#include <utility> #include "SceTypes.hpp" +#include "prx/libSceNpUniversalDataSystem/include/UdsEvent.hpp" static constexpr int SCE_NP_UNIVERSAL_DATA_SYSTEM_OK = 0; static constexpr int SCE_NP_UNIVERSAL_DATA_SYSTEM_ERROR_INVALID_ARGUMENT = -2141900542; @@ -15,4 +21,44 @@ static constexpr int NP_UNIVERSAL_DATA_SYSTEM_HANDLE_DEFAULT = 1; static constexpr const char* NP_UNIVERSAL_DATA_SYSTEM_EMPTY_JSON = "{}"; static constexpr size_t NP_UNIVERSAL_DATA_SYSTEM_EMPTY_EVENT_SIZE = 3; +namespace Uds { + +template <typename TValue> +class Owned { +public: + TValue* Adopt(std::unique_ptr<TValue> value) { + std::lock_guard lock(mutex); + auto* pointer = value.get(); + values.emplace(pointer, std::move(value)); + return pointer; + } + + void Release(const TValue* value) { + std::unique_ptr<TValue> released; + std::lock_guard lock(mutex); + const auto found = values.find(value); + if (found == values.end()) return; + released = std::move(found->second); + values.erase(found); + } + + bool Contains(const TValue* value) { + std::lock_guard lock(mutex); + return values.contains(value); + } + +private: + std::mutex mutex; + std::map<const TValue*, std::unique_ptr<TValue>> values; +}; + +struct ContextOwner { + std::int32_t userId = 0; + std::uint32_t serviceLabel = 0; +}; + +std::optional<ContextOwner> FindContext(int context); + +} + #endif diff --git a/core/libs/prx/libSceNpUniversalDataSystem/src/UdsEvent.cpp b/core/libs/prx/libSceNpUniversalDataSystem/src/UdsEvent.cpp new file mode 100644 index 0000000000..96b8672b36 --- /dev/null +++ b/core/libs/prx/libSceNpUniversalDataSystem/src/UdsEvent.cpp @@ -0,0 +1,147 @@ +#include "prx/libSceNpUniversalDataSystem/include/UdsEvent.hpp" + +#include <cstdio> +#include <type_traits> + +namespace Uds { + +Value Clone(const Value& value) { + if (const auto* object = std::get_if<std::shared_ptr<NpUniversalDataSystemEventPropertyObject>>(&value)) return std::make_shared<NpUniversalDataSystemEventPropertyObject>((*object)->Clone()); + if (const auto* array = std::get_if<std::shared_ptr<NpUniversalDataSystemEventPropertyArray>>(&value)) return std::make_shared<NpUniversalDataSystemEventPropertyArray>((*array)->Clone()); + return value; +} + +namespace { + +void appendQuoted(std::string& out, std::string_view text) { + out += '"'; + for (const char character : text) { + if (character == '"' || character == '\\') { + out += '\\'; + out += character; + } else if (static_cast<unsigned char>(character) < 0x20) { + char escaped[8]; + std::snprintf(escaped, sizeof(escaped), "\\u%04x", static_cast<unsigned>(static_cast<unsigned char>(character))); + out += escaped; + } else { + out += character; + } + } + out += '"'; +} + +void appendValue(std::string& out, const Value& value); + +void appendObject(std::string& out, const NpUniversalDataSystemEventPropertyObject& object) { + out += '{'; + bool first = true; + for (const auto& [key, value] : object.properties) { + if (!first) out += ','; + first = false; + appendQuoted(out, key); + out += ':'; + appendValue(out, value); + } + out += '}'; +} + +void appendValue(std::string& out, const Value& value) { + std::visit([&](const auto& held) { + using Held = std::decay_t<decltype(held)>; + char number[40]; + if constexpr (std::is_same_v<Held, bool>) { + out += held ? "true" : "false"; + } else if constexpr (std::is_same_v<Held, std::int32_t> || std::is_same_v<Held, std::int64_t>) { + std::snprintf(number, sizeof(number), "%lld", static_cast<long long>(held)); + out += number; + } else if constexpr (std::is_same_v<Held, std::uint32_t> || std::is_same_v<Held, std::uint64_t>) { + std::snprintf(number, sizeof(number), "%llu", static_cast<unsigned long long>(held)); + out += number; + } else if constexpr (std::is_same_v<Held, float>) { + std::snprintf(number, sizeof(number), "%.9g", static_cast<double>(held)); + out += number; + } else if constexpr (std::is_same_v<Held, double>) { + std::snprintf(number, sizeof(number), "%.17g", held); + out += number; + } else if constexpr (std::is_same_v<Held, std::string>) { + appendQuoted(out, held); + } else if constexpr (std::is_same_v<Held, Binary>) { + std::snprintf(number, sizeof(number), "<%zu bytes>", held.size()); + out += number; + } else if constexpr (std::is_same_v<Held, std::shared_ptr<NpUniversalDataSystemEventPropertyObject>>) { + appendObject(out, *held); + } else { + out += '['; + for (std::size_t i = 0; i < held->values.size(); ++i) { + if (i != 0) out += ','; + appendValue(out, held->values[i]); + } + out += ']'; + } + }, value); +} + +} + +TrophyEvent RecognizeTrophyEvent(const NpUniversalDataSystemEvent& event) { + TrophyEvent result; + if (event.name == UnlockTrophyEvent) result.kind = TrophyEventKind::Unlock; + else if (event.name == UpdateTrophyProgressEvent) result.kind = TrophyEventKind::Progress; + else return result; + const auto* id = event.properties.Find(TrophyIdProperty); + const auto* value = id != nullptr ? std::get_if<std::int32_t>(id) : nullptr; + if (value != nullptr && *value >= 0) { + result.trophyId = *value; + } else if (result.kind == TrophyEventKind::Unlock) { + result.kind = TrophyEventKind::Malformed; + result.problem = id == nullptr ? "it has no _trophy_id" : value == nullptr ? "its _trophy_id is not an int32" : "its _trophy_id is negative"; + } + return result; +} + +std::string Describe(const NpUniversalDataSystemEventPropertyObject& object) { + std::string out; + appendObject(out, object); + return out; +} + +std::string Describe(const NpUniversalDataSystemEvent& event) { + return event.name + " " + Describe(event.properties); +} + +} + +Uds::Value& NpUniversalDataSystemEventPropertyObject::Set(std::string_view key, Uds::Value value) { + for (auto& [name, stored] : properties) { + if (name == key) { + stored = std::move(value); + return stored; + } + } + return properties.emplace_back(std::string(key), std::move(value)).second; +} + +const Uds::Value* NpUniversalDataSystemEventPropertyObject::Find(std::string_view key) const { + for (const auto& [name, stored] : properties) { + if (name == key) return &stored; + } + return nullptr; +} + +NpUniversalDataSystemEventPropertyObject NpUniversalDataSystemEventPropertyObject::Clone() const { + NpUniversalDataSystemEventPropertyObject copy; + copy.properties.reserve(properties.size()); + for (const auto& [name, stored] : properties) copy.properties.emplace_back(name, Uds::Clone(stored)); + return copy; +} + +Uds::Value& NpUniversalDataSystemEventPropertyArray::Add(Uds::Value value) { + return values.emplace_back(std::move(value)); +} + +NpUniversalDataSystemEventPropertyArray NpUniversalDataSystemEventPropertyArray::Clone() const { + NpUniversalDataSystemEventPropertyArray copy; + copy.values.reserve(values.size()); + for (const auto& stored : values) copy.values.push_back(Uds::Clone(stored)); + return copy; +} diff --git a/core/libs/prx/libSceNpUniversalDataSystem/tests/Events.cpp b/core/libs/prx/libSceNpUniversalDataSystem/tests/Events.cpp new file mode 100644 index 0000000000..92274b6f56 --- /dev/null +++ b/core/libs/prx/libSceNpUniversalDataSystem/tests/Events.cpp @@ -0,0 +1,262 @@ +#include "NpUniversalDataSystem.hpp" +#include "prx/libSceNpTrophy2/include/TrophyStore.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" + +#include <cstddef> +#include <cstdint> +#include <cstdio> +#include <optional> +#include <stdexcept> +#include <string> + +extern "C" { +int APS5_VABI sceNpUniversalDataSystemCreateContext(int*, int, uint32_t, uint64_t); +int APS5_VABI sceNpUniversalDataSystemDestroyContext(int); +int APS5_VABI sceNpUniversalDataSystemCreateEvent(const char*, const NpUniversalDataSystemEventPropertyObject*, NpUniversalDataSystemEvent**, NpUniversalDataSystemEventPropertyObject**); +int APS5_VABI sceNpUniversalDataSystemDestroyEvent(NpUniversalDataSystemEvent*); +int APS5_VABI sceNpUniversalDataSystemPostEvent(int, int, const void*, uint64_t); +int APS5_VABI sceNpUniversalDataSystemCreateEventPropertyObject(NpUniversalDataSystemEventPropertyObject**); +int APS5_VABI sceNpUniversalDataSystemDestroyEventPropertyObject(NpUniversalDataSystemEventPropertyObject*); +int APS5_VABI sceNpUniversalDataSystemEventPropertyObjectSetString(NpUniversalDataSystemEventPropertyObject*, const char*, const char*); +int APS5_VABI sceNpUniversalDataSystemEventPropertyObjectSetInt32(NpUniversalDataSystemEventPropertyObject*, const char*, int32_t); +int APS5_VABI sceNpUniversalDataSystemEventPropertyObjectSetUInt32(NpUniversalDataSystemEventPropertyObject*, const char*, uint32_t); +int APS5_VABI sceNpUniversalDataSystemEventPropertyObjectSetInt64(NpUniversalDataSystemEventPropertyObject*, const char*, int64_t); +int APS5_VABI sceNpUniversalDataSystemEventPropertyObjectSetUInt64(NpUniversalDataSystemEventPropertyObject*, const char*, uint64_t); +int APS5_VABI sceNpUniversalDataSystemEventPropertyObjectSetFloat32(NpUniversalDataSystemEventPropertyObject*, const char*, float); +int APS5_VABI sceNpUniversalDataSystemEventPropertyObjectSetFloat64(NpUniversalDataSystemEventPropertyObject*, const char*, double); +int APS5_VABI sceNpUniversalDataSystemEventPropertyObjectSetBool(NpUniversalDataSystemEventPropertyObject*, const char*, bool); +int APS5_VABI sceNpUniversalDataSystemEventPropertyObjectSetBinary(NpUniversalDataSystemEventPropertyObject*, const char*, const void*, size_t); +int APS5_VABI sceNpUniversalDataSystemEventPropertyObjectSetObject(NpUniversalDataSystemEventPropertyObject*, const char*, const NpUniversalDataSystemEventPropertyObject*, NpUniversalDataSystemEventPropertyObject**); +int APS5_VABI sceNpUniversalDataSystemEventPropertyObjectSetArray(NpUniversalDataSystemEventPropertyObject*, const char*, const NpUniversalDataSystemEventPropertyArray*, NpUniversalDataSystemEventPropertyArray**); +int APS5_VABI sceNpUniversalDataSystemCreateEventPropertyArray(NpUniversalDataSystemEventPropertyArray**); +int APS5_VABI sceNpUniversalDataSystemDestroyEventPropertyArray(NpUniversalDataSystemEventPropertyArray*); +int APS5_VABI sceNpUniversalDataSystemEventPropertyArraySetString(NpUniversalDataSystemEventPropertyArray*, const char*); +int APS5_VABI sceNpUniversalDataSystemEventPropertyArraySetInt32(NpUniversalDataSystemEventPropertyArray*, int32_t); +int APS5_VABI sceNpUniversalDataSystemEventPropertyArraySetUInt32(NpUniversalDataSystemEventPropertyArray*, uint32_t); +int APS5_VABI sceNpUniversalDataSystemEventPropertyArraySetInt64(NpUniversalDataSystemEventPropertyArray*, int64_t); +int APS5_VABI sceNpUniversalDataSystemEventPropertyArraySetUInt64(NpUniversalDataSystemEventPropertyArray*, uint64_t); +int APS5_VABI sceNpUniversalDataSystemEventPropertyArraySetFloat32(NpUniversalDataSystemEventPropertyArray*, float); +int APS5_VABI sceNpUniversalDataSystemEventPropertyArraySetFloat64(NpUniversalDataSystemEventPropertyArray*, double); +int APS5_VABI sceNpUniversalDataSystemEventPropertyArraySetBool(NpUniversalDataSystemEventPropertyArray*, bool); +int APS5_VABI sceNpUniversalDataSystemEventPropertyArraySetBinary(NpUniversalDataSystemEventPropertyArray*, const void*, size_t); +int APS5_VABI sceNpUniversalDataSystemEventPropertyArraySetObject(NpUniversalDataSystemEventPropertyArray*, const NpUniversalDataSystemEventPropertyObject*, NpUniversalDataSystemEventPropertyObject**); +int APS5_VABI sceNpUniversalDataSystemEventPropertyArraySetArray(NpUniversalDataSystemEventPropertyArray*, const NpUniversalDataSystemEventPropertyArray*, NpUniversalDataSystemEventPropertyArray**); +} + +namespace { + +constexpr int InitialUser = 0x10000000; + +void check(bool condition, const char* message) { + if (!condition) throw std::runtime_error(message); +} + +void call(int result, const char* message) { + check(result == 0, message); +} + +template <typename TAction> +bool throwsInvalidArgument(TAction action) { + try { + action(); + } catch (const std::invalid_argument&) { + return true; + } + return false; +} + +NpUniversalDataSystemEventPropertyObject* createObject() { + NpUniversalDataSystemEventPropertyObject* object = nullptr; + call(sceNpUniversalDataSystemCreateEventPropertyObject(&object), "property object was not created"); + check(object != nullptr, "property object is null"); + return object; +} + +void testObjectSetters() { + auto* object = createObject(); + const std::uint8_t bytes[] = {1, 2, 3}; + call(sceNpUniversalDataSystemEventPropertyObjectSetInt32(object, "int32", -5), "SetInt32 failed"); + call(sceNpUniversalDataSystemEventPropertyObjectSetUInt32(object, "uint32", 4000000000u), "SetUInt32 failed"); + call(sceNpUniversalDataSystemEventPropertyObjectSetInt64(object, "int64", -9000000000), "SetInt64 failed"); + call(sceNpUniversalDataSystemEventPropertyObjectSetUInt64(object, "uint64", 18000000000000000000ull), "SetUInt64 failed"); + call(sceNpUniversalDataSystemEventPropertyObjectSetFloat32(object, "float32", 0.5f), "SetFloat32 failed"); + call(sceNpUniversalDataSystemEventPropertyObjectSetFloat64(object, "float64", 0.25), "SetFloat64 failed"); + call(sceNpUniversalDataSystemEventPropertyObjectSetBool(object, "bool", true), "SetBool failed"); + call(sceNpUniversalDataSystemEventPropertyObjectSetString(object, "string", "a\"b\\c\n"), "SetString failed"); + call(sceNpUniversalDataSystemEventPropertyObjectSetBinary(object, "binary", bytes, sizeof(bytes)), "SetBinary failed"); + check(Uds::Describe(*object) == R"({"int32":-5,"uint32":4000000000,"int64":-9000000000,"uint64":18000000000000000000,"float32":0.5,"float64":0.25,"bool":true,"string":"a\"b\\c\u000a","binary":<3 bytes>})", + "properties were not kept with their types"); + const auto* binary = std::get_if<Uds::Binary>(object->Find("binary")); + check(binary != nullptr && *binary == Uds::Binary{1, 2, 3}, "binary bytes were not copied"); + call(sceNpUniversalDataSystemEventPropertyObjectSetString(object, "int32", "replaced"), "SetString over int32 failed"); + const auto* replaced = std::get_if<std::string>(object->Find("int32")); + check(object->properties.size() == 9 && replaced != nullptr && *replaced == "replaced", "setting a key again did not replace its value"); + check(object->Find("missing") == nullptr, "a missing key was found"); + check(throwsInvalidArgument([] { sceNpUniversalDataSystemEventPropertyObjectSetInt32(nullptr, "key", 1); }), "a null object was accepted"); + check(throwsInvalidArgument([object] { sceNpUniversalDataSystemEventPropertyObjectSetInt32(object, nullptr, 1); }), "a null key was accepted"); + call(sceNpUniversalDataSystemDestroyEventPropertyObject(object), "property object was not destroyed"); +} + +void testNestedValues() { + auto* source = createObject(); + auto* parent = createObject(); + call(sceNpUniversalDataSystemEventPropertyObjectSetInt32(source, "x", 1), "SetInt32 on the source failed"); + NpUniversalDataSystemEventPropertyObject* child = nullptr; + call(sceNpUniversalDataSystemEventPropertyObjectSetObject(parent, "child", source, &child), "SetObject failed"); + check(child != nullptr && child != source, "SetObject did not return its own copy"); + call(sceNpUniversalDataSystemEventPropertyObjectSetInt32(source, "x", 2), "SetInt32 after SetObject failed"); + call(sceNpUniversalDataSystemEventPropertyObjectSetBool(child, "y", true), "SetBool on the child failed"); + call(sceNpUniversalDataSystemDestroyEventPropertyObject(source), "source object was not destroyed"); + NpUniversalDataSystemEventPropertyArray* list = nullptr; + call(sceNpUniversalDataSystemEventPropertyObjectSetArray(parent, "list", nullptr, &list), "SetArray failed"); + check(list != nullptr, "SetArray did not return the stored array"); + const std::uint8_t bytes[] = {4, 5}; + call(sceNpUniversalDataSystemEventPropertyArraySetInt32(list, 1), "array SetInt32 failed"); + call(sceNpUniversalDataSystemEventPropertyArraySetUInt32(list, 2), "array SetUInt32 failed"); + call(sceNpUniversalDataSystemEventPropertyArraySetInt64(list, -3), "array SetInt64 failed"); + call(sceNpUniversalDataSystemEventPropertyArraySetUInt64(list, 4), "array SetUInt64 failed"); + call(sceNpUniversalDataSystemEventPropertyArraySetFloat32(list, 1.5f), "array SetFloat32 failed"); + call(sceNpUniversalDataSystemEventPropertyArraySetFloat64(list, 2.5), "array SetFloat64 failed"); + call(sceNpUniversalDataSystemEventPropertyArraySetBool(list, true), "array SetBool failed"); + call(sceNpUniversalDataSystemEventPropertyArraySetString(list, "s"), "array SetString failed"); + call(sceNpUniversalDataSystemEventPropertyArraySetBinary(list, bytes, sizeof(bytes)), "array SetBinary failed"); + NpUniversalDataSystemEventPropertyObject* element = nullptr; + call(sceNpUniversalDataSystemEventPropertyArraySetObject(list, nullptr, &element), "array SetObject failed"); + call(sceNpUniversalDataSystemEventPropertyObjectSetUInt64(element, "z", 3), "SetUInt64 on the array element failed"); + NpUniversalDataSystemEventPropertyArray* inner = nullptr; + call(sceNpUniversalDataSystemEventPropertyArraySetArray(list, nullptr, &inner), "array SetArray failed"); + call(sceNpUniversalDataSystemEventPropertyArraySetBool(inner, false), "SetBool on the inner array failed"); + const char* expected = R"({"child":{"x":1,"y":true},"list":[1,2,-3,4,1.5,2.5,true,"s",<2 bytes>,{"z":3},[false]]})"; + check(Uds::Describe(*parent) == expected, "nested properties were not kept"); + const auto copy = parent->Clone(); + call(sceNpUniversalDataSystemEventPropertyObjectSetInt32(child, "x", 5), "SetInt32 on the child failed"); + call(sceNpUniversalDataSystemEventPropertyArraySetBool(inner, true), "SetBool on the inner array failed"); + check(Uds::Describe(copy) == expected, "a clone shares nested values with its source"); + call(sceNpUniversalDataSystemDestroyEventPropertyObject(child), "destroying a child failed"); + call(sceNpUniversalDataSystemDestroyEventPropertyArray(list), "destroying a child array failed"); + check(Uds::Describe(*parent) == R"({"child":{"x":5,"y":true},"list":[1,2,-3,4,1.5,2.5,true,"s",<2 bytes>,{"z":3},[false,true]]})", + "destroying a child took it from its parent"); + NpUniversalDataSystemEventPropertyArray* array = nullptr; + call(sceNpUniversalDataSystemCreateEventPropertyArray(&array), "property array was not created"); + call(sceNpUniversalDataSystemEventPropertyArraySetInt32(array, 9), "SetInt32 on the created array failed"); + NpUniversalDataSystemEventPropertyArray* stored = nullptr; + call(sceNpUniversalDataSystemEventPropertyObjectSetArray(parent, "list", array, &stored), "SetArray over a key failed"); + call(sceNpUniversalDataSystemDestroyEventPropertyArray(array), "property array was not destroyed"); + check(stored != array && Uds::Describe(*parent) == R"({"child":{"x":5,"y":true},"list":[9]})", "SetArray did not store a copy under its key"); + check(throwsInvalidArgument([] { sceNpUniversalDataSystemEventPropertyArraySetInt32(nullptr, 1); }), "a null array was accepted"); + call(sceNpUniversalDataSystemDestroyEventPropertyObject(parent), "parent object was not destroyed"); +} + +void testEvents() { + auto* properties = createObject(); + call(sceNpUniversalDataSystemEventPropertyObjectSetInt32(properties, "_trophy_id", 12), "SetInt32 failed"); + NpUniversalDataSystemEvent* event = nullptr; + NpUniversalDataSystemEventPropertyObject* eventProperties = nullptr; + call(sceNpUniversalDataSystemCreateEvent("_UnlockTrophy", properties, &event, &eventProperties), "event was not created"); + check(event != nullptr && eventProperties == &event->properties, "CreateEvent did not return the event's property object"); + call(sceNpUniversalDataSystemEventPropertyObjectSetInt32(properties, "_trophy_id", 13), "SetInt32 after CreateEvent failed"); + call(sceNpUniversalDataSystemDestroyEventPropertyObject(properties), "property object was not destroyed"); + check(Uds::Describe(*event) == R"(_UnlockTrophy {"_trophy_id":12})", "the event did not copy its properties"); + call(sceNpUniversalDataSystemEventPropertyObjectSetString(eventProperties, "detail", "boss"), "SetString on the event failed"); + call(sceNpUniversalDataSystemDestroyEventPropertyObject(eventProperties), "destroying the event's property object failed"); + check(Uds::Describe(*event) == R"(_UnlockTrophy {"_trophy_id":12,"detail":"boss"})", "properties set through the returned pointer were lost"); + call(sceNpUniversalDataSystemDestroyEvent(event), "event was not destroyed"); + NpUniversalDataSystemEvent* empty = nullptr; + call(sceNpUniversalDataSystemCreateEvent("_Empty", nullptr, &empty, nullptr), "event without properties was not created"); + check(Uds::Describe(*empty) == "_Empty {}", "an event without properties was not empty"); + call(sceNpUniversalDataSystemDestroyEvent(empty), "event without properties was not destroyed"); + check(throwsInvalidArgument([] { + NpUniversalDataSystemEvent* unnamed = nullptr; + sceNpUniversalDataSystemCreateEvent(nullptr, nullptr, &unnamed, nullptr); + }), "an event without a name was created"); +} + +Uds::TrophyEvent recognize(const char* name, std::optional<Uds::Value> id) { + NpUniversalDataSystemEvent event; + event.name = name; + if (id) event.properties.Set(Uds::TrophyIdProperty, *id); + return Uds::RecognizeTrophyEvent(event); +} + +void testRecognition() { + const auto unlock = recognize("_UnlockTrophy", std::int32_t{12}); + check(unlock.kind == Uds::TrophyEventKind::Unlock && unlock.trophyId == 12, "an unlock event was not recognised"); + const auto first = recognize("_UnlockTrophy", std::int32_t{0}); + check(first.kind == Uds::TrophyEventKind::Unlock && first.trophyId == 0, "an unlock of trophy 0 was not recognised"); + const auto progress = recognize("_UpdateTrophyProgress", std::int32_t{3}); + check(progress.kind == Uds::TrophyEventKind::Progress && progress.trophyId == 3, "a progress event was not recognised"); + const auto anonymous = recognize("_UpdateTrophyProgress", std::nullopt); + check(anonymous.kind == Uds::TrophyEventKind::Progress && anonymous.trophyId == -1, "a progress event without a trophy id was not recognised"); + const auto missing = recognize("_UnlockTrophy", std::nullopt); + check(missing.kind == Uds::TrophyEventKind::Malformed && missing.problem == "it has no _trophy_id", "an unlock without a trophy id was accepted"); + const auto widened = recognize("_UnlockTrophy", std::uint32_t{12}); + check(widened.kind == Uds::TrophyEventKind::Malformed && widened.problem == "its _trophy_id is not an int32", "an unlock with an unsigned trophy id was accepted"); + const auto negative = recognize("_UnlockTrophy", std::int32_t{-1}); + check(negative.kind == Uds::TrophyEventKind::Malformed && negative.problem == "its _trophy_id is negative", "an unlock with a negative trophy id was accepted"); + for (const char* name : {"_unlockTrophy", "UnlockTrophy", "_UnlockTrophy2", "_UnlockTrophy ", "_StartActivity", ""}) { + check(recognize(name, std::int32_t{12}).kind == Uds::TrophyEventKind::None, "an unknown event was taken for a trophy event"); + } +} + +struct Unlocks { + int count = 0; +}; + +void countUnlock(void* context, const NpTrophy2Unlock&) { + ++static_cast<Unlocks*>(context)->count; +} + +void post(int context, const char* name, std::optional<Uds::Value> id) { + NpUniversalDataSystemEvent* event = nullptr; + NpUniversalDataSystemEventPropertyObject* properties = nullptr; + call(sceNpUniversalDataSystemCreateEvent(name, nullptr, &event, &properties), "event was not created"); + if (id) properties->Set(Uds::TrophyIdProperty, *id); + call(sceNpUniversalDataSystemPostEvent(context, 1, event, 0), "event was not posted"); + call(sceNpUniversalDataSystemDestroyEvent(event), "event was not destroyed"); +} + +void testPostingIgnoredEvents() { + Unlocks unlocks; + const auto listener = NpTrophy2AddUnlockListener_nid_no_patch(countUnlock, &unlocks); + int context = 0; + call(sceNpUniversalDataSystemCreateContext(&context, InitialUser, 0, 0), "context was not created"); + int second = 0; + call(sceNpUniversalDataSystemCreateContext(&second, InitialUser, 0, 0), "second context was not created"); + check(second != context, "two contexts share an id"); + post(context, "_StartActivity", std::int32_t{1}); + post(context, "_UnlockTrophy", std::string("1")); + post(context, "_UnlockTrophy", std::nullopt); + post(context, "_UpdateTrophyProgress", std::int32_t{2}); + post(context, "_UpdateTrophyProgress", std::int32_t{2}); + post(second + 1, "_UnlockTrophy", std::int32_t{4}); + call(sceNpUniversalDataSystemDestroyContext(context), "context was not destroyed"); + post(context, "_UnlockTrophy", std::int32_t{4}); + call(sceNpUniversalDataSystemDestroyContext(second), "second context was not destroyed"); + NpTrophy2RemoveUnlockListener_nid_no_patch(listener); + check(unlocks.count == 0, "an event that is not a valid unlock reached the trophy store"); + check(throwsInvalidArgument([] { sceNpUniversalDataSystemPostEvent(1, 1, nullptr, 0); }), "a null event was posted"); + NpUniversalDataSystemEvent stray; + stray.name = "_UnlockTrophy"; + check(throwsInvalidArgument([&stray] { sceNpUniversalDataSystemPostEvent(1, 1, &stray, 0); }), "an event that CreateEvent did not create was posted"); + NpUniversalDataSystemEvent* destroyed = nullptr; + call(sceNpUniversalDataSystemCreateEvent("_UnlockTrophy", nullptr, &destroyed, nullptr), "event was not created"); + call(sceNpUniversalDataSystemDestroyEvent(destroyed), "event was not destroyed"); + check(throwsInvalidArgument([destroyed] { sceNpUniversalDataSystemPostEvent(1, 1, destroyed, 0); }), "a destroyed event was posted"); +} + +} + +int main() { + try { + testObjectSetters(); + testNestedValues(); + testEvents(); + testRecognition(); + testPostingIgnoredEvents(); + std::puts("UDS event tests passed"); + return 0; + } catch (const std::exception& error) { + std::fprintf(stderr, "%s\n", error.what()); + return 1; + } +} diff --git a/core/libs/tests/GuestNpTrophyData.cpp b/core/libs/tests/GuestNpTrophyData.cpp index 6e07c0e769..eabbe68eae 100644 --- a/core/libs/tests/GuestNpTrophyData.cpp +++ b/core/libs/tests/GuestNpTrophyData.cpp @@ -1,5 +1,6 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include "prx/libSceNpTrophy2/include/NpTrophy2Types.hpp" +#include "prx/libSceNpTrophy2/include/TrophyStore.hpp" #include <cstddef> #include <cstdint> @@ -13,7 +14,19 @@ #include <utility> #include <vector> +struct NpUniversalDataSystemEvent; +struct NpUniversalDataSystemEventPropertyObject; + extern "C" { +int APS5_VABI sceNpUniversalDataSystemCreateContext(int*, int, uint32_t, uint64_t); +int APS5_VABI sceNpUniversalDataSystemRegisterContext(int, int, uint64_t); +int APS5_VABI sceNpUniversalDataSystemCreateHandle(int*); +int APS5_VABI sceNpUniversalDataSystemCreateEvent(const char*, const NpUniversalDataSystemEventPropertyObject*, NpUniversalDataSystemEvent**, NpUniversalDataSystemEventPropertyObject**); +int APS5_VABI sceNpUniversalDataSystemDestroyEvent(NpUniversalDataSystemEvent*); +int APS5_VABI sceNpUniversalDataSystemPostEvent(int, int, const void*, uint64_t); +int APS5_VABI sceNpUniversalDataSystemCreateEventPropertyObject(NpUniversalDataSystemEventPropertyObject**); +int APS5_VABI sceNpUniversalDataSystemDestroyEventPropertyObject(NpUniversalDataSystemEventPropertyObject*); +int APS5_VABI sceNpUniversalDataSystemEventPropertyObjectSetInt32(NpUniversalDataSystemEventPropertyObject*, const char*, int32_t); int APS5_VABI sceNpTrophy2CreateContext(int*, int, uint32_t, uint64_t); int APS5_VABI sceNpTrophy2GetGameInfo(int, int, NpTrophy2GameDetails*, NpTrophy2GameData*); int APS5_VABI sceNpTrophy2GetGroupInfo(int, int, int, NpTrophy2GroupDetails*, NpTrophy2GroupData*); @@ -90,6 +103,24 @@ static void Write(const std::filesystem::path& path, const std::string& text) { Require(std::fclose(file) == 0 && written, "a test file was not written"); } +static void PostUnlock(int trophyId) { + static int context = 0; + static int handle = 0; + if (context == 0) { + Require(sceNpUniversalDataSystemCreateContext(&context, InitialUser, 0, 0) == 0, "UDS context was not created"); + Require(sceNpUniversalDataSystemCreateHandle(&handle) == 0, "UDS handle was not created"); + Require(sceNpUniversalDataSystemRegisterContext(context, handle, 0) == 0, "UDS context was not registered"); + } + NpUniversalDataSystemEventPropertyObject* properties = nullptr; + Require(sceNpUniversalDataSystemCreateEventPropertyObject(&properties) == 0, "property object was not created"); + Require(sceNpUniversalDataSystemEventPropertyObjectSetInt32(properties, "_trophy_id", trophyId) == 0, "trophy id was not set"); + NpUniversalDataSystemEvent* event = nullptr; + Require(sceNpUniversalDataSystemCreateEvent("_UnlockTrophy", properties, &event, nullptr) == 0, "event was not created"); + Require(sceNpUniversalDataSystemPostEvent(context, handle, event, 0) == 0, "event was not posted"); + Require(sceNpUniversalDataSystemDestroyEvent(event) == 0, "event was not destroyed"); + Require(sceNpUniversalDataSystemDestroyEventPropertyObject(properties) == 0, "property object was not destroyed"); +} + static int TrophyContext(std::uint32_t serviceLabel) { int context = 0; Require(sceNpTrophy2CreateContext(&context, InitialUser, serviceLabel, 0) == 0, "trophy context was not created"); @@ -109,6 +140,15 @@ static NpTrophy2GameData Game(int context, NpTrophy2GameDetails* details = nullp return data; } +struct Unlocks { + std::vector<std::int32_t> trophies; +}; + +static void Remember(void* context, const NpTrophy2Unlock& unlock) { + Require(unlock.titleId == "PPSA00000" && unlock.userId == InitialUser && unlock.serviceLabel == 0 && unlock.tick != 0, "an unlock does not carry its title, user, label and time"); + static_cast<Unlocks*>(context)->trophies.push_back(unlock.trophyId); +} + static void ReportTrophyData(int context) { NpTrophy2GameDetails game{}; const auto progress = Game(context, &game); @@ -153,6 +193,24 @@ static void ReportTrophyData(int context) { "the group list does not hold the base game and group 1"); } +static void UnlockPlatinum(int context) { + Unlocks unlocks; + const auto listener = NpTrophy2AddUnlockListener_nid_no_patch(Remember, &unlocks); + for (const int trophyId : {1, 2, 3, 6}) PostUnlock(trophyId); + const auto partway = Game(context); + Require(!Trophy(context, 0).unlocked && partway.unlocked_trophies == 4 && partway.unlocked_gold == 1 && partway.unlocked_silver == 1 && partway.unlocked_bronze == 2 && + partway.progress_percentage == (6 + 2 + 2) * 100 / (6 + 2 + 5), + "the game does not count the unlocks by grade"); + PostUnlock(4); + Require(!Trophy(context, 0).unlocked, "the platinum unlocked before every trophy that names it"); + PostUnlock(5); + const auto progressive = Trophy(context, 5); + Require(progressive.unlocked && progressive.progress.value == 5, "an unlocked progressive trophy does not report its target as progress"); + Require(Trophy(context, 0).unlocked && Game(context).unlocked_platinum == 1, "the platinum did not unlock with the last trophy that names it"); + NpTrophy2RemoveUnlockListener_nid_no_patch(listener); + Require(unlocks.trophies == (std::vector<std::int32_t>{1, 2, 3, 6, 4, 5, 0}), "the listener did not see each unlock and the platinum once, in order"); +} + static void ReportPlaceholders() { for (const std::uint32_t serviceLabel : {1u, 2u}) { const auto context = TrophyContext(serviceLabel); @@ -179,6 +237,7 @@ int main() { const auto context = TrophyContext(0); ReportTrophyData(context); + UnlockPlatinum(context); ReportPlaceholders(); std::filesystem::current_path(previous); diff --git a/core/libs/tests/GuestNpTrophyUnlocks.cpp b/core/libs/tests/GuestNpTrophyUnlocks.cpp new file mode 100644 index 0000000000..be3d6e2859 --- /dev/null +++ b/core/libs/tests/GuestNpTrophyUnlocks.cpp @@ -0,0 +1,109 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libSceNpTrophy2/include/NpTrophy2Types.hpp" + +#include <cstddef> +#include <cstdint> +#include <cstdio> +#include <cstdlib> +#include <filesystem> +#include <random> +#include <string> + +struct NpUniversalDataSystemEvent; +struct NpUniversalDataSystemEventPropertyObject; + +extern "C" { +int APS5_VABI sceNpUniversalDataSystemCreateContext(int*, int, uint32_t, uint64_t); +int APS5_VABI sceNpUniversalDataSystemRegisterContext(int, int, uint64_t); +int APS5_VABI sceNpUniversalDataSystemCreateHandle(int*); +int APS5_VABI sceNpUniversalDataSystemCreateEvent(const char*, const NpUniversalDataSystemEventPropertyObject*, NpUniversalDataSystemEvent**, NpUniversalDataSystemEventPropertyObject**); +int APS5_VABI sceNpUniversalDataSystemDestroyEvent(NpUniversalDataSystemEvent*); +int APS5_VABI sceNpUniversalDataSystemPostEvent(int, int, const void*, uint64_t); +int APS5_VABI sceNpUniversalDataSystemCreateEventPropertyObject(NpUniversalDataSystemEventPropertyObject**); +int APS5_VABI sceNpUniversalDataSystemDestroyEventPropertyObject(NpUniversalDataSystemEventPropertyObject*); +int APS5_VABI sceNpUniversalDataSystemEventPropertyObjectSetInt32(NpUniversalDataSystemEventPropertyObject*, const char*, int32_t); +int APS5_VABI sceNpTrophy2CreateContext(int*, int, uint32_t, uint64_t); +int APS5_VABI sceNpTrophy2GetGameInfo(int, int, NpTrophy2GameDetails*, NpTrophy2GameData*); +int APS5_VABI sceNpTrophy2GetTrophyInfo(int, int, int, NpTrophy2Details*, NpTrophy2Data*); +} + +static constexpr int InitialUser = 0x10000000; + +static void Require(bool value, const char* message) { + if (value) return; + std::fprintf(stderr, "%s\n", message); + std::abort(); +} + +static std::string Read(const char* path) { + std::string text; + std::FILE* file = std::fopen(path, "rb"); + if (file == nullptr) return text; + char buffer[256]; + for (std::size_t count; (count = std::fread(buffer, 1, sizeof(buffer), file)) != 0;) text.append(buffer, count); + std::fclose(file); + return text; +} + +static void Post(int context, int handle, const char* name, int trophyId) { + NpUniversalDataSystemEventPropertyObject* properties = nullptr; + Require(sceNpUniversalDataSystemCreateEventPropertyObject(&properties) == 0, "property object was not created"); + Require(sceNpUniversalDataSystemEventPropertyObjectSetInt32(properties, "_trophy_id", trophyId) == 0, "trophy id was not set"); + NpUniversalDataSystemEvent* event = nullptr; + Require(sceNpUniversalDataSystemCreateEvent(name, properties, &event, nullptr) == 0, "event was not created"); + Require(sceNpUniversalDataSystemPostEvent(context, handle, event, 0) == 0, "event was not posted"); + Require(sceNpUniversalDataSystemDestroyEvent(event) == 0, "event was not destroyed"); + Require(sceNpUniversalDataSystemDestroyEventPropertyObject(properties) == 0, "property object was not destroyed"); +} + +static NpTrophy2Data Trophy(int context, int handle, int trophyId) { + NpTrophy2Data data{}; + Require(sceNpTrophy2GetTrophyInfo(context, handle, trophyId, nullptr, &data) == 0, "trophy info was not returned"); + Require(data.trophy_id == trophyId, "trophy info is for another trophy"); + return data; +} + +static std::uint32_t Unlocked(int context, int handle) { + NpTrophy2GameData data{}; + Require(sceNpTrophy2GetGameInfo(context, handle, nullptr, &data) == 0, "game info was not returned"); + return data.unlocked_trophies; +} + +static void UnlockThroughEvents() { + std::filesystem::create_directories("app0/sce_sys"); + std::FILE* param = std::fopen("app0/sce_sys/param.json", "wb"); + Require(param != nullptr, "param.json was not created"); + std::fputs(R"({"titleId":"PPSA00000","localizedParameters":{"en-US":{"titleName":"Example"}}})", param); + std::fclose(param); + int udsContext = 0; + int handle = 0; + Require(sceNpUniversalDataSystemCreateContext(&udsContext, InitialUser, 0, 0) == 0, "UDS context was not created"); + Require(sceNpUniversalDataSystemCreateHandle(&handle) == 0, "UDS handle was not created"); + Require(sceNpUniversalDataSystemRegisterContext(udsContext, handle, 0) == 0, "UDS context was not registered"); + int trophyContext = 0; + Require(sceNpTrophy2CreateContext(&trophyContext, InitialUser, 0, 0) == 0, "trophy context was not created"); + + Require(!Trophy(trophyContext, handle, 12).unlocked && Unlocked(trophyContext, handle) == 0, "trophy 12 is unlocked before its event"); + Post(udsContext, handle, "_UnlockTrophy", 12); + const auto unlocked = Trophy(trophyContext, handle, 12); + Require(unlocked.unlocked && unlocked.timestamp_tick != 0, "the posted unlock is not reported with its time"); + Require(Unlocked(trophyContext, handle) == 1, "the game does not count the posted unlock"); + + Post(udsContext, handle, "_ChallengeComplete", 13); + Require(!Trophy(trophyContext, handle, 13).unlocked, "an unknown event unlocked a trophy"); + Post(udsContext, handle, "_UnlockTrophy", 12); + Require(Trophy(trophyContext, handle, 12).timestamp_tick == unlocked.timestamp_tick && Unlocked(trophyContext, handle) == 1, "a repeated unlock changed the record"); + + Require(Read("_trophy/268435456_0.txt") == "12 " + std::to_string(unlocked.timestamp_tick) + "\n", "the unlock was not stored in the title folder"); +} + +int main() { + const auto previous = std::filesystem::current_path(); + const auto root = std::filesystem::temp_directory_path() / ("anyps5-trophy-unlocks-" + std::to_string(std::random_device{}())); + std::filesystem::create_directories(root); + std::filesystem::current_path(root); + UnlockThroughEvents(); + std::filesystem::current_path(previous); + std::filesystem::remove_all(root); + return 0; +} diff --git a/docs/dev/debt/libSceNpTrophy2.md b/docs/dev/debt/libSceNpTrophy2.md index e5060029a4..b7701b97c1 100644 --- a/docs/dev/debt/libSceNpTrophy2.md +++ b/docs/dev/debt/libSceNpTrophy2.md @@ -15,4 +15,4 @@ - [Trophy queries](../../../core/libs/prx/libSceNpTrophy2/src/TrophyInfo.cpp) report the title's [trophy data](../../../core/libs/prx/libSceNpTrophy2/src/TrophyPackage.cpp) (`sce_sys/trophy2/trophyNN.ucp`) and the unlocks in the [trophy store](../../../core/libs/prx/libSceNpTrophy2/src/TrophyStore.cpp). Without the file, or when it is malformed, they report the old placeholders and `sceNpTrophy2GetTrophyInfoArray` lists only trophy 0. Icons are not read. Hidden trophies report their names and descriptions; whether the console blanks them before the unlock is not known. `progress_percentage` weighs gold 6, silver 2 and bronze 1 and leaves the platinum out, as KytyPS5 does (the ratio of the published PSN trophy points); the console's formula is not known. A trophy or group that is not in the file throws. A context that was not created or was destroyed reports placeholders and no unlocks and returns success; the console's errors for it are unknown. - [Trophy data limits](../../../core/libs/prx/libSceNpTrophy2/src/TrophyPackage.cpp): a `trophyNN.ucp` larger than 256 MiB or listing more than 4096 files, and a JSON file in it that nests deeper than 64 levels or holds more than 1048576 values, are not used, and the title runs as without that file. The limits keep the memory the reader takes to the file and about 140 MiB of parsed JSON; the console's limits are not known. - [Text language](../../../core/libs/prx/libSceNpTrophy2/src/TrophyPackage.cpp): texts come from `tropmeta_<tag>.json` for the console language, then `en-US`, then the file's `defaultLanguage`. The tags for languages 0 to 29 are KytyPS5's; `uk-UA` for 30 (Ukrainian) is an assumption. -- [Platinum](../../../core/libs/prx/libSceNpTrophy2/src/TitleTrophies.cpp): an unlock through any path unlocks the platinum that the trophy names in `platinumTrophyId` once every trophy naming that platinum is unlocked, as KytyPS5 does; trophies without `platinumTrophyId` are not required. Without trophy data any id of 0 or more is stored, and the platinum is not unlocked when the trophies it needs are. +- [Platinum](../../../core/libs/prx/libSceNpTrophy2/src/TitleTrophies.cpp): an unlock through any path unlocks the platinum that the trophy names in `platinumTrophyId` once every trophy naming that platinum is unlocked, as KytyPS5 does; trophies without `platinumTrophyId` are not required. A platinum or a progressive trophy can still be unlocked directly with `_UnlockTrophy`, which KytyPS5 refuses; the console's behaviour is not known. Without trophy data any id of 0 or more is stored, and the platinum is not unlocked when the trophies it needs are. diff --git a/docs/dev/debt/libSceNpUniversalDataSystem.md b/docs/dev/debt/libSceNpUniversalDataSystem.md new file mode 100644 index 0000000000..3fcbc2508f --- /dev/null +++ b/docs/dev/debt/libSceNpUniversalDataSystem.md @@ -0,0 +1,9 @@ +# libSceNpUniversalDataSystem technical debt + +### Unknown function info + +- [Events](../../../core/libs/prx/libSceNpUniversalDataSystem/src/Event.cpp): `sceNpUniversalDataSystemCreateEvent` copies `prop` and returns the event's own property object through `prop_ptr`, as [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/9e41d8320a2774b303fbad344c2347e8d0292e83/src/libs/libTrophy.cpp) does; `sceNpUniversalDataSystemEventPropertyObjectSetObject`, `SetArray` and the array setters for objects and arrays store a copy of `value` and return the stored copy through `value_ptr`. Destroying a property object or array that its Create function did not return (an event's property object or a stored copy) does nothing, as its owner frees it. These ownership rules are not verified on a console, nor are the output of `sceNpUniversalDataSystemEventToString` (still `{}`) and `sceNpUniversalDataSystemEventEstimateSize` (still 3). + +### Functional + +- [Trophy events](../../../core/libs/prx/libSceNpUniversalDataSystem/src/Event.cpp): `sceNpUniversalDataSystemPostEvent` stores an `_UnlockTrophy` event with an int32 `_trophy_id` of 0 or more as an unlock for the user and service label of its context, as KytyPS5 does. Unlocks that a title drives through statistics (`statsExtractionRuleArray` in `sce_sys/uds/udsNN.ucp` and `unlockCondition` in `sce_sys/trophy2/trophyNN.ucp`) are not evaluated, and `_UpdateTrophyProgress`, named only by a third-party unlocker, is logged once per trophy and not interpreted. A context need not be registered, and every post of an event that `sceNpUniversalDataSystemCreateEvent` returned and that is not yet destroyed returns success (a null or any other event pointer stops the game): KytyPS5's errors for an unknown or unregistered context and for a malformed unlock are not verified. The storage statistics stay zero. From 42ddbdf89e33287ede1d35441e1f01c7181a78bc Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Sat, 17 Oct 2026 23:28:50 +0000 Subject: [PATCH 0908/1000] docs: describe trophy unlocks in USAGE --- docs/user/USAGE.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index bc96b850c1..c2f142dfe1 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -159,9 +159,13 @@ Games ask for the console's language with `sceSystemServiceParamGetInt` and show ### Trophies -The trophy data comes from the game's own `app0/sce_sys/trophy2/trophyNN.ucp`, where `NN` is the service label (`00` for most games). The trophy queries report its names, descriptions and rewards in the console language (`ANYPS5_LANGUAGE`, see above), or in English (United States) or the data's default language when it has no text in that language, and its grades, groups, hidden flags, number of trophies and grade counts. The first trophy query prints what was read, such as `[trophy] /app0/sce_sys/trophy2/trophy00.ucp: 52 trophies, 1 group, texts in en-US`. +A game unlocks a trophy by posting a `_UnlockTrophy` event with an int32 `_trophy_id` property to `libSceNpUniversalDataSystem`. The unlock is stored in `_trophy/<user id>_<service label>.txt` in the directory the game runs in (the one that holds `app0/`), one `<trophy id> <unlock time>` line per trophy; the time is an RTC tick, microseconds since 0001-01-01 UTC. Delete the file before starting the game to lock its trophies again. Each new unlock prints one line, such as `[trophy] PPSA01556 unlocked trophy 12 for user 268435456`; unlocking a trophy again prints nothing. `sceNpTrophy2GetTrophyInfo`, `sceNpTrophy2GetTrophyInfoArray` and `sceNpTrophy2GetGameInfo` report the stored unlocks and their times to the game. Every posted `_trophy_id` of 0 or more is stored, even one the trophy data has no trophy for or makes the platinum or a progressive trophy. -Trophies are not unlocked yet: every trophy reports as locked, and nothing is written for them. A missing or malformed `trophyNN.ucp` makes the queries report placeholders, and a malformed one prints the reason once; `APS5_TRACE_UDS=1` prints a `[uds]` line for a missing one. Icons are not read: `sceNpTrophy2GetGameIcon`, `sceNpTrophy2GetGroupIcon`, `sceNpTrophy2GetTrophyIcon` and `sceNpTrophy2GetRewardIcon` stop the game with an error. No notification is shown, and callbacks registered with `sceNpTrophy2RegisterUnlockCallback` are not called. +The trophy data comes from the game's own `app0/sce_sys/trophy2/trophyNN.ucp`, where `NN` is the service label (`00` for most games). The trophy queries report its names, descriptions and rewards in the console language (`ANYPS5_LANGUAGE`, see above), or in English (United States) or the data's default language when it has no text in that language, and its grades, groups, hidden flags, number of trophies and grade counts. A platinum trophy unlocks as soon as every trophy that names it in `platinumTrophyId` is unlocked. The first trophy query or event prints what was read, such as `[trophy] /app0/sce_sys/trophy2/trophy00.ucp: 52 trophies, 1 group, texts in en-US`. + +`APS5_TRACE_UDS=1` prints every posted event, such as `[uds] context 1 posted _UnlockTrophy {"_trophy_id":12}`, and a `[uds]` line for a missing `trophyNN.ucp`. An event that is not used prints the reason in a `[trophy] ignored` line: an `_UnlockTrophy` event without an int32 `_trophy_id` of 0 or more, or posted to a context that does not exist, and the first `_UpdateTrophyProgress` event of each trophy, because trophy progress is not recorded. + +A missing or malformed `trophyNN.ucp` makes the queries report placeholders, and a malformed one prints the reason once. Icons are not read: `sceNpTrophy2GetGameIcon`, `sceNpTrophy2GetGroupIcon`, `sceNpTrophy2GetTrophyIcon` and `sceNpTrophy2GetRewardIcon` stop the game with an error. So does an unlock file in `_trophy/` that is damaged (a line that is not a new `<id> <value>` pair, such as a line an edit left blank or ended with a carriage return) or cannot be read or written. No notification is shown, and callbacks registered with `sceNpTrophy2RegisterUnlockCallback` are not called. ### PlayGo dumps without metadata From 4362803457e4a9322982dd3280577879b2aec0e0 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Sat, 17 Oct 2026 09:57:31 +0000 Subject: [PATCH 0909/1000] feat(libSceNpUniversalDataSystem): unlock trophies through the title's statistics Events other than the trophy events go through the title's sce_sys/uds/udsNN.ucp: stats_definition.json gives the statistics that keep their latest uint32 or uint64 value, and stats_extraction.json the event property ($.name, dotted for nested objects) that sets each one. Statistics are kept per user and service label in _trophy/<user id>_<service label>.stats.txt next to the unlocks. After each event that sets a statistic, every trophy's unlockCondition (ge, gt, le or lt against its targetValue) is checked on the values that event set and the stored values of the other statistics, so a later event setting the same statistic cannot hide what this one reached. Reached trophies unlock through the same path as _UnlockTrophy. Progressive trophies report their statistic as progress, and APS5_TRACE_UDS prints each statistic an event sets. A missing udsNN.ucp is ignored and a malformed one is logged once. --- .../libSceNpTrophy2/include/TitleTrophies.hpp | 20 ++ .../libSceNpTrophy2/include/TrophyPackage.hpp | 21 ++ .../libSceNpTrophy2/include/TrophyStore.hpp | 35 ++ .../prx/libSceNpTrophy2/src/TitleTrophies.cpp | 73 +++++ .../prx/libSceNpTrophy2/src/TrophyInfo.cpp | 3 +- .../prx/libSceNpTrophy2/src/TrophyPackage.cpp | 111 +++++++ .../prx/libSceNpTrophy2/src/TrophyStore.cpp | 134 +++++--- .../libSceNpTrophy2/tests/TrophyPackage.cpp | 137 ++++++++ .../CMakeLists.txt | 10 + .../include/UdsEvent.hpp | 3 + .../libSceNpUniversalDataSystem/src/Event.cpp | 1 + .../src/UdsEvent.cpp | 40 +++ .../tests/Events.cpp | 34 ++ core/libs/tests/GuestNpTrophyStats.cpp | 306 ++++++++++++++++++ docs/dev/debt/libSceNpTrophy2.md | 2 +- docs/dev/debt/libSceNpUniversalDataSystem.md | 3 +- 16 files changed, 893 insertions(+), 40 deletions(-) create mode 100644 core/libs/tests/GuestNpTrophyStats.cpp diff --git a/core/libs/prx/libSceNpTrophy2/include/TitleTrophies.hpp b/core/libs/prx/libSceNpTrophy2/include/TitleTrophies.hpp index 74901778a9..cfea54f627 100644 --- a/core/libs/prx/libSceNpTrophy2/include/TitleTrophies.hpp +++ b/core/libs/prx/libSceNpTrophy2/include/TitleTrophies.hpp @@ -20,9 +20,11 @@ inline constexpr std::uint64_t MaxUcpSize = std::uint64_t{256} << 20u; struct TitleTrophies { std::optional<TrophyPackage> package; + std::optional<StatRules> rules; }; std::string PackagePath(std::uint32_t serviceLabel); +std::string StatRulesPath(std::uint32_t serviceLabel); TitleTrophies LoadTitleTrophies(std::uint32_t serviceLabel, std::string_view language); const TitleTrophies& Title(std::uint32_t serviceLabel); const TrophyPackage* Package(std::uint32_t serviceLabel); @@ -32,6 +34,24 @@ using UnlockedQuery = std::function<bool(std::int32_t)>; UnlockedQuery UnlockedBy(std::int32_t userId, std::uint32_t serviceLabel); +struct StatUpdate { + std::uint64_t statId = 0; + std::uint64_t value = 0; + std::string input; +}; + +struct StatProblem { + std::string rule; + std::string reason; +}; + +struct StatChanges { + std::vector<StatUpdate> updates; + std::vector<StatProblem> problems; +}; + +StatChanges ApplyStatRules(const StatRules& rules, const NpTrophy2Event& event, StatStore& stats, std::int32_t userId, std::uint32_t serviceLabel); +std::vector<std::int32_t> ReachedTrophies(const TrophyPackage& package, const std::map<std::uint64_t, std::uint64_t>& stats, const UnlockedQuery& unlocked); std::optional<std::int32_t> PlatinumToUnlock(const TrophyPackage& package, std::int32_t trophyId, const UnlockedQuery& unlocked); std::optional<std::uint64_t> ProgressTarget(const TrophyDefinition& trophy); std::uint64_t TrophyProgress(const TrophyDefinition& trophy, std::optional<std::uint64_t> stat, bool unlocked); diff --git a/core/libs/prx/libSceNpTrophy2/include/TrophyPackage.hpp b/core/libs/prx/libSceNpTrophy2/include/TrophyPackage.hpp index 1e1e20bd4c..4be4238929 100644 --- a/core/libs/prx/libSceNpTrophy2/include/TrophyPackage.hpp +++ b/core/libs/prx/libSceNpTrophy2/include/TrophyPackage.hpp @@ -89,6 +89,27 @@ struct TrophyPackage { TrophyPackage ParseTrophyPackage(std::string_view ucp, std::string_view language); +enum class StatType { UInt32, UInt64 }; + +struct StatDefinition { + StatType type = StatType::UInt64; + std::uint64_t minimum = 0; + std::uint64_t maximum = UINT64_MAX; +}; + +struct StatRule { + std::string input; + std::uint64_t statId = 0; +}; + +struct StatRules { + std::map<std::uint64_t, StatDefinition> stats; + std::map<std::string, std::vector<StatRule>, std::less<>> events; + std::vector<std::string> skipped; +}; + +StatRules ParseStatRules(std::string_view ucp); + std::string_view LanguageTag(int consoleLanguage); } diff --git a/core/libs/prx/libSceNpTrophy2/include/TrophyStore.hpp b/core/libs/prx/libSceNpTrophy2/include/TrophyStore.hpp index 245966e568..1418d70b58 100644 --- a/core/libs/prx/libSceNpTrophy2/include/TrophyStore.hpp +++ b/core/libs/prx/libSceNpTrophy2/include/TrophyStore.hpp @@ -9,6 +9,7 @@ #include <optional> #include <string> #include <utility> +#include <vector> struct NpTrophy2Unlock { std::string titleId; @@ -20,6 +21,17 @@ struct NpTrophy2Unlock { using NpTrophy2UnlockListener = void (*)(void* context, const NpTrophy2Unlock& unlock); +struct NpTrophy2EventProperty { + std::string path; + std::optional<std::uint64_t> value; + std::string text; +}; + +struct NpTrophy2Event { + std::string name; + std::vector<NpTrophy2EventProperty> properties; +}; + namespace NpTrophy2 { inline constexpr char TrophyRootGuest[] = "/_trophy"; @@ -50,6 +62,28 @@ private: TrophyStore& TitleStore(); +class StatStore { +public: + explicit StatStore(std::filesystem::path directory); + + void Set(std::int32_t userId, std::uint32_t serviceLabel, std::uint64_t statId, std::uint64_t value); + std::optional<std::uint64_t> Value(std::int32_t userId, std::uint32_t serviceLabel, std::uint64_t statId); + std::map<std::uint64_t, std::uint64_t> Values(std::int32_t userId, std::uint32_t serviceLabel); + std::filesystem::path RecordPath(std::int32_t userId, std::uint32_t serviceLabel) const; + +private: + using Key = std::pair<std::int32_t, std::uint32_t>; + using Stats = std::map<std::uint64_t, std::uint64_t>; + + Stats& record(const Key& key); + + std::filesystem::path directory; + std::mutex mutex; + std::map<Key, Stats> records; +}; + +StatStore& TitleStats(); + struct ContextOwner { std::int32_t userId = 0; std::uint32_t serviceLabel = 0; @@ -68,6 +102,7 @@ extern "C" { bool NpTrophy2RecordUnlock_nid_no_patch(std::int32_t userId, std::uint32_t serviceLabel, std::int32_t trophyId); std::uint64_t NpTrophy2AddUnlockListener_nid_no_patch(NpTrophy2UnlockListener listener, void* context); void NpTrophy2RemoveUnlockListener_nid_no_patch(std::uint64_t id); +void NpTrophy2ApplyEvent_nid_no_patch(std::int32_t userId, std::uint32_t serviceLabel, const NpTrophy2Event& event); } diff --git a/core/libs/prx/libSceNpTrophy2/src/TitleTrophies.cpp b/core/libs/prx/libSceNpTrophy2/src/TitleTrophies.cpp index dcd465e8ce..342f6ea8bb 100644 --- a/core/libs/prx/libSceNpTrophy2/src/TitleTrophies.cpp +++ b/core/libs/prx/libSceNpTrophy2/src/TitleTrophies.cpp @@ -10,6 +10,7 @@ #include <fstream> #include <memory> #include <mutex> +#include <set> #include <stdexcept> #include <system_error> #include <utility> @@ -68,12 +69,23 @@ std::string counted(std::size_t count, const char* one, const char* many) { return std::to_string(count) + " " + (count == 1 ? one : many); } +bool firstProblem(const std::string& rule) { + static std::mutex mutex; + static std::set<std::string> reported; + std::lock_guard lock(mutex); + return reported.insert(rule).second; +} + } std::string PackagePath(std::uint32_t serviceLabel) { return labelled("/app0/sce_sys/trophy2/trophy%02u.ucp", serviceLabel); } +std::string StatRulesPath(std::uint32_t serviceLabel) { + return labelled("/app0/sce_sys/uds/uds%02u.ucp", serviceLabel); +} + TitleTrophies LoadTitleTrophies(std::uint32_t serviceLabel, std::string_view language) { TitleTrophies title; const auto packagePath = PackagePath(serviceLabel); @@ -84,6 +96,15 @@ TitleTrophies LoadTitleTrophies(std::uint32_t serviceLabel, std::string_view lan counted(title.package->groups.size(), "group", "groups").c_str(), title.package->language.c_str()); for (const auto& skipped : title.package->skipped) std::fprintf(stderr, "[trophy] %s: %s\n", packagePath.c_str(), skipped.c_str()); } + const auto rulesPath = StatRulesPath(serviceLabel); + title.rules = loadTitleFile<StatRules>(rulesPath, "posted events update no stats", [](std::string_view bytes) { return ParseStatRules(bytes); }); + if (title.rules) { + std::size_t count = 0; + for (const auto& [name, rules] : title.rules->events) count += rules.size(); + std::fprintf(stderr, "[trophy] %s: %s, %s for %s\n", rulesPath.c_str(), counted(title.rules->stats.size(), "stat", "stats").c_str(), counted(count, "rule", "rules").c_str(), + counted(title.rules->events.size(), "event", "events").c_str()); + for (const auto& skipped : title.rules->skipped) std::fprintf(stderr, "[trophy] %s: %s\n", rulesPath.c_str(), skipped.c_str()); + } return title; } @@ -113,6 +134,39 @@ UnlockedQuery UnlockedBy(std::int32_t userId, std::uint32_t serviceLabel) { return [userId, serviceLabel](std::int32_t trophyId) { return TitleStore().UnlockTick(userId, serviceLabel, trophyId).has_value(); }; } +StatChanges ApplyStatRules(const StatRules& rules, const NpTrophy2Event& event, StatStore& stats, std::int32_t userId, std::uint32_t serviceLabel) { + StatChanges changes; + const auto matching = rules.events.find(event.name); + if (matching == rules.events.end()) return changes; + for (const auto& rule : matching->second) { + const auto& definition = rules.stats.at(rule.statId); + const auto property = std::find_if(event.properties.begin(), event.properties.end(), [&rule](const NpTrophy2EventProperty& candidate) { return candidate.path == rule.input; }); + const auto where = event.name + " $." + rule.input + " for stat " + std::to_string(rule.statId); + if (property == event.properties.end()) { + changes.problems.push_back({where, "the event has no such property"}); + } else if (!property->value) { + changes.problems.push_back({where, property->text + " is not an unsigned integer"}); + } else if (*property->value < definition.minimum || *property->value > definition.maximum) { + changes.problems.push_back({where, property->text + " is not from " + std::to_string(definition.minimum) + " to " + std::to_string(definition.maximum)}); + } else { + stats.Set(userId, serviceLabel, rule.statId, *property->value); + changes.updates.push_back({rule.statId, *property->value, rule.input}); + } + } + return changes; +} + +std::vector<std::int32_t> ReachedTrophies(const TrophyPackage& package, const std::map<std::uint64_t, std::uint64_t>& stats, const UnlockedQuery& unlocked) { + std::vector<std::int32_t> reached; + for (const auto& [id, trophy] : package.trophies) { + const auto condition = trophy.Condition(); + if (!condition) continue; + const auto stat = stats.find(condition->statId); + if (stat != stats.end() && condition->Reached(stat->second) && !unlocked(id)) reached.push_back(id); + } + return reached; +} + std::optional<std::int32_t> PlatinumToUnlock(const TrophyPackage& package, std::int32_t trophyId, const UnlockedQuery& unlocked) { const auto trophy = package.trophies.find(trophyId); if (trophy == package.trophies.end() || !trophy->second.platinumId) return std::nullopt; @@ -157,3 +211,22 @@ std::uint32_t TrophyCounts::Percentage() const { } } + +extern "C" void NpTrophy2ApplyEvent_nid_no_patch(std::int32_t userId, std::uint32_t serviceLabel, const NpTrophy2Event& event) { + const auto& title = NpTrophy2::Title(serviceLabel); + if (!title.rules) return; + const auto changes = NpTrophy2::ApplyStatRules(*title.rules, event, NpTrophy2::TitleStats(), userId, serviceLabel); + for (const auto& problem : changes.problems) { + if (NpTrophy2::firstProblem(problem.rule)) std::fprintf(stderr, "[trophy] ignored %s: %s\n", problem.rule.c_str(), problem.reason.c_str()); + } + if (NpTrophy2::tracing()) { + for (const auto& update : changes.updates) { + std::fprintf(stderr, "[uds] stat %llu of user %d is %llu (%s $.%s)\n", static_cast<unsigned long long>(update.statId), userId, + static_cast<unsigned long long>(update.value), event.name.c_str(), update.input.c_str()); + } + } + if (changes.updates.empty() || !title.package) return; + auto stats = NpTrophy2::TitleStats().Values(userId, serviceLabel); + for (const auto& update : changes.updates) stats[update.statId] = update.value; + for (const auto trophyId : NpTrophy2::ReachedTrophies(*title.package, stats, NpTrophy2::UnlockedBy(userId, serviceLabel))) NpTrophy2RecordUnlock_nid_no_patch(userId, serviceLabel, trophyId); +} diff --git a/core/libs/prx/libSceNpTrophy2/src/TrophyInfo.cpp b/core/libs/prx/libSceNpTrophy2/src/TrophyInfo.cpp index dfeba5ffbe..ce29da1513 100644 --- a/core/libs/prx/libSceNpTrophy2/src/TrophyInfo.cpp +++ b/core/libs/prx/libSceNpTrophy2/src/TrophyInfo.cpp @@ -60,8 +60,9 @@ void fillData(const NpTrophy2::ContextOwner& owner, const NpTrophy2::TrophyDefin data->unlocked = tick.has_value(); data->timestamp_tick = tick.value_or(0); if (!NpTrophy2::ProgressTarget(trophy)) return; + const auto stat = trophy.statId ? NpTrophy2::TitleStats().Value(owner.userId, owner.serviceLabel, *trophy.statId) : std::nullopt; data->progress.type = NP_TROPHY2_PROGRESS_TYPE_VALUE; - data->progress.value = NpTrophy2::TrophyProgress(trophy, std::nullopt, data->unlocked); + data->progress.value = NpTrophy2::TrophyProgress(trophy, stat, data->unlocked); } } diff --git a/core/libs/prx/libSceNpTrophy2/src/TrophyPackage.cpp b/core/libs/prx/libSceNpTrophy2/src/TrophyPackage.cpp index 01d0bc8063..5da39d1cb4 100644 --- a/core/libs/prx/libSceNpTrophy2/src/TrophyPackage.cpp +++ b/core/libs/prx/libSceNpTrophy2/src/TrophyPackage.cpp @@ -336,6 +336,105 @@ TrophyDefinition readTrophy(const Json& definition, std::size_t index, std::vect return trophy; } +std::optional<std::string> propertyPath(std::string_view input) { + if (!input.starts_with("$.")) return std::nullopt; + const auto path = input.substr(2); + bool segmentStart = true; + for (const char character : path) { + if (character == '.') { + if (segmentStart) return std::nullopt; + segmentStart = true; + continue; + } + const bool word = (character >= 'a' && character <= 'z') || (character >= 'A' && character <= 'Z') || (character >= '0' && character <= '9') || character == '_'; + if (!word) return std::nullopt; + segmentStart = false; + } + if (segmentStart) return std::nullopt; + return std::string(path); +} + +void readStat(const Json& definition, std::size_t index, StatRules& rules) { + const auto* id = definition.Find("statId"); + const auto statId = id != nullptr ? id->Unsigned() : std::nullopt; + if (!statId) { + rules.skipped.push_back("stats_definition.json statDefinitionArray[" + std::to_string(index) + "] has no unsigned statId"); + return; + } + const auto name = "stat " + std::to_string(*statId); + const auto* aggregation = definition.Find("aggregation"); + if (aggregation == nullptr || aggregation->kind != Json::Kind::String || aggregation->text != "latest") { + rules.skipped.push_back(name + " has aggregation " + (aggregation != nullptr ? describe(*aggregation) : std::string("none")) + "; only \"latest\" is supported"); + return; + } + const auto* type = definition.Find("dataType"); + StatDefinition stat; + if (type != nullptr && type->kind == Json::Kind::String && type->text == "uint32") { + stat.type = StatType::UInt32; + stat.maximum = UINT32_MAX; + } else if (type != nullptr && type->kind == Json::Kind::String && type->text == "uint64") { + stat.type = StatType::UInt64; + stat.maximum = UINT64_MAX; + } else { + rules.skipped.push_back(name + " has dataType " + (type != nullptr ? describe(*type) : std::string("none")) + "; only \"uint32\" and \"uint64\" are supported"); + return; + } + if (const auto* minimum = definition.Find("minValue")) { + const auto value = minimum->Unsigned(); + if (!value || *value > stat.maximum) { + rules.skipped.push_back(name + " has minValue " + describe(*minimum) + ", which its " + type->text + " type cannot hold"); + return; + } + stat.minimum = *value; + } + if (const auto* maximum = definition.Find("maxValue")) { + const auto value = maximum->Unsigned(); + if (!value || *value > stat.maximum || *value < stat.minimum) { + rules.skipped.push_back(name + " has maxValue " + describe(*maximum) + ", which is not from its minValue to its " + type->text + " limit"); + return; + } + stat.maximum = *value; + } + if (!rules.stats.emplace(*statId, stat).second) rules.skipped.push_back(name + " is defined twice; the first definition is used"); +} + +void readRule(const Json& rule, std::size_t index, StatRules& rules) { + const auto where = "stats_extraction.json statsExtractionRuleArray[" + std::to_string(index) + "]"; + const auto* condition = rule.Find("condition"); + const auto* action = rule.Find("action"); + if (condition == nullptr || condition->kind != Json::Kind::Object || action == nullptr || action->kind != Json::Kind::Object) { + rules.skipped.push_back(where + " has no condition and action objects"); + return; + } + const auto* event = condition->Find("eventName"); + if (event == nullptr || event->kind != Json::Kind::String) { + rules.skipped.push_back(where + " has no eventName"); + return; + } + if (condition->members.size() != 1) { + rules.skipped.push_back(where + " filters " + event->text + " by more than its eventName; only eventName conditions are supported"); + return; + } + const auto* output = action->Find("output"); + const auto* id = output != nullptr ? output->Find("statId") : nullptr; + const auto statId = id != nullptr ? id->Unsigned() : std::nullopt; + if (!statId) { + rules.skipped.push_back(where + " for " + event->text + " has no unsigned output statId"); + return; + } + if (!rules.stats.contains(*statId)) { + rules.skipped.push_back(where + " for " + event->text + " writes stat " + std::to_string(*statId) + ", which has no supported definition"); + return; + } + const auto* input = action->Find("input"); + const auto path = input != nullptr && input->kind == Json::Kind::String ? propertyPath(input->text) : std::nullopt; + if (!path) { + rules.skipped.push_back(where + " for " + event->text + " reads " + (input != nullptr ? describe(*input) : std::string("no input")) + "; only \"$.name\" property paths are supported"); + return; + } + rules.events[event->text].push_back({*path, *statId}); +} + } const Json* Json::Find(std::string_view key) const { @@ -467,6 +566,18 @@ TrophyPackage ParseTrophyPackage(std::string_view ucp, std::string_view language return package; } +StatRules ParseStatRules(std::string_view ucp) { + const auto files = ReadUcp(ucp); + const auto definitions = document(files, "stats_definition.json"); + const auto extraction = document(files, "stats_extraction.json"); + const auto& stats = member(definitions, "statDefinitionArray", Json::Kind::Array, "stats_definition.json"); + const auto& extractionRules = member(extraction, "statsExtractionRuleArray", Json::Kind::Array, "stats_extraction.json"); + StatRules rules; + for (std::size_t index = 0; index < stats.items.size(); ++index) readStat(stats.items[index], index, rules); + for (std::size_t index = 0; index < extractionRules.items.size(); ++index) readRule(extractionRules.items[index], index, rules); + return rules; +} + std::string_view LanguageTag(int consoleLanguage) { static constexpr std::array<std::string_view, 31> tags = { "ja-JP", "en-US", "fr-FR", "es-ES", "de-DE", "it-IT", "nl-NL", "pt-PT", "ru-RU", "ko-KR", "zh-Hant", diff --git a/core/libs/prx/libSceNpTrophy2/src/TrophyStore.cpp b/core/libs/prx/libSceNpTrophy2/src/TrophyStore.cpp index 501fc72223..27f753283c 100644 --- a/core/libs/prx/libSceNpTrophy2/src/TrophyStore.cpp +++ b/core/libs/prx/libSceNpTrophy2/src/TrophyStore.cpp @@ -11,6 +11,7 @@ #include <stdexcept> #include <string_view> #include <system_error> +#include <type_traits> #include <vector> namespace NpTrophy2 { @@ -24,6 +25,58 @@ bool parseNumber(std::string_view text, TValue& value) { return error == std::errc{} && stop == end; } +template <typename TKey> +bool negative(TKey key) { + if constexpr (std::is_signed_v<TKey>) return key < 0; + return false; +} + +template <typename TKey> +std::map<TKey, std::uint64_t> readRecord(const std::filesystem::path& path, const std::string& what, const char* pair) { + std::map<TKey, std::uint64_t> values; + std::error_code error; + const bool exists = std::filesystem::exists(path, error); + if (error) throw std::filesystem::filesystem_error("cannot check the " + what, path, error); + if (!exists) return values; + std::ifstream file(path, std::ios::binary); + if (!file) throw std::runtime_error("cannot read the " + what + " " + path.string()); + std::string line; + std::size_t number = 0; + while (std::getline(file, line)) { + ++number; + const auto space = line.find(' '); + TKey key{}; + std::uint64_t value = 0; + const std::string_view text(line); + if (space == std::string::npos || !parseNumber(text.substr(0, space), key) || negative(key) || !parseNumber(text.substr(space + 1), value) || !values.emplace(key, value).second) { + throw std::runtime_error("the " + what + " " + path.string() + " has a line " + std::to_string(number) + " that is not a new '" + pair + "' pair"); + } + } + if (file.bad()) throw std::runtime_error("cannot read the " + what + " " + path.string()); + return values; +} + +template <typename TKey> +void writeRecord(const std::filesystem::path& directory, const std::filesystem::path& path, const std::map<TKey, std::uint64_t>& values, const std::string& what) { + std::error_code error; + if (std::filesystem::create_directories(directory, error)) RecordWrittenPath_nid_no_patch(directory); + if (error) throw std::filesystem::filesystem_error("cannot create the trophy directory", directory, error); + auto temporary = path; + temporary += ".tmp"; + { + std::ofstream file(temporary, std::ios::binary | std::ios::trunc); + for (const auto& [key, value] : values) file << key << ' ' << value << '\n'; + file.flush(); + if (!file) throw std::runtime_error("cannot write the " + what + " " + temporary.string()); + } + std::filesystem::rename(temporary, path); + RecordWrittenPath_nid_no_patch(path); +} + +std::string recordName(std::int32_t userId, std::uint32_t serviceLabel) { + return std::to_string(userId) + "_" + std::to_string(serviceLabel); +} + struct Listener { std::uint64_t id = 0; NpTrophy2UnlockListener callback = nullptr; @@ -57,51 +110,17 @@ std::uint64_t CurrentTick() { TrophyStore::TrophyStore(std::filesystem::path directory) : directory(std::move(directory)) {} std::filesystem::path TrophyStore::RecordPath(std::int32_t userId, std::uint32_t serviceLabel) const { - return directory / (std::to_string(userId) + "_" + std::to_string(serviceLabel) + ".txt"); + return directory / (recordName(userId, serviceLabel) + ".txt"); } TrophyStore::Unlocks& TrophyStore::record(const Key& key) { if (const auto found = records.find(key); found != records.end()) return found->second; - Unlocks unlocks; - const auto path = RecordPath(key.first, key.second); - std::error_code error; - const bool exists = std::filesystem::exists(path, error); - if (error) throw std::filesystem::filesystem_error("cannot check the trophy record", path, error); - if (exists) { - std::ifstream file(path, std::ios::binary); - if (!file) throw std::runtime_error("cannot read the trophy record " + path.string()); - std::string line; - std::size_t number = 0; - while (std::getline(file, line)) { - ++number; - const auto space = line.find(' '); - std::int32_t trophyId = 0; - std::uint64_t tick = 0; - const std::string_view text(line); - if (space == std::string::npos || !parseNumber(text.substr(0, space), trophyId) || trophyId < 0 || !parseNumber(text.substr(space + 1), tick) || !unlocks.emplace(trophyId, tick).second) { - throw std::runtime_error("the trophy record " + path.string() + " has a line " + std::to_string(number) + " that is not a new '<trophy id> <unlock tick>' pair"); - } - } - if (file.bad()) throw std::runtime_error("cannot read the trophy record " + path.string()); - } + auto unlocks = readRecord<std::int32_t>(RecordPath(key.first, key.second), "trophy record", "<trophy id> <unlock tick>"); return records.emplace(key, std::move(unlocks)).first->second; } void TrophyStore::save(const Key& key, const Unlocks& unlocks) const { - std::error_code error; - if (std::filesystem::create_directories(directory, error)) RecordWrittenPath_nid_no_patch(directory); - if (error) throw std::filesystem::filesystem_error("cannot create the trophy directory", directory, error); - const auto path = RecordPath(key.first, key.second); - auto temporary = path; - temporary += ".tmp"; - { - std::ofstream file(temporary, std::ios::binary | std::ios::trunc); - for (const auto& [trophyId, tick] : unlocks) file << trophyId << ' ' << tick << '\n'; - file.flush(); - if (!file) throw std::runtime_error("cannot write the trophy record " + temporary.string()); - } - std::filesystem::rename(temporary, path); - RecordWrittenPath_nid_no_patch(path); + writeRecord(directory, RecordPath(key.first, key.second), unlocks, "trophy record"); } bool TrophyStore::Unlock(std::int32_t userId, std::uint32_t serviceLabel, std::int32_t trophyId, std::uint64_t tick) { @@ -135,6 +154,47 @@ TrophyStore& TitleStore() { return store; } +StatStore::StatStore(std::filesystem::path directory) : directory(std::move(directory)) {} + +std::filesystem::path StatStore::RecordPath(std::int32_t userId, std::uint32_t serviceLabel) const { + return directory / (recordName(userId, serviceLabel) + ".stats.txt"); +} + +StatStore::Stats& StatStore::record(const Key& key) { + if (const auto found = records.find(key); found != records.end()) return found->second; + auto stats = readRecord<std::uint64_t>(RecordPath(key.first, key.second), "stat record", "<stat id> <value>"); + return records.emplace(key, std::move(stats)).first->second; +} + +void StatStore::Set(std::int32_t userId, std::uint32_t serviceLabel, std::uint64_t statId, std::uint64_t value) { + std::lock_guard lock(mutex); + const Key key{userId, serviceLabel}; + auto& stats = record(key); + if (const auto found = stats.find(statId); found != stats.end() && found->second == value) return; + auto updated = stats; + updated[statId] = value; + writeRecord(directory, RecordPath(userId, serviceLabel), updated, "stat record"); + stats = std::move(updated); +} + +std::optional<std::uint64_t> StatStore::Value(std::int32_t userId, std::uint32_t serviceLabel, std::uint64_t statId) { + std::lock_guard lock(mutex); + const auto& stats = record({userId, serviceLabel}); + const auto found = stats.find(statId); + if (found == stats.end()) return std::nullopt; + return found->second; +} + +std::map<std::uint64_t, std::uint64_t> StatStore::Values(std::int32_t userId, std::uint32_t serviceLabel) { + std::lock_guard lock(mutex); + return record({userId, serviceLabel}); +} + +StatStore& TitleStats() { + static StatStore store(ResolvePath_nid_no_patch(TrophyRootGuest)); + return store; +} + std::optional<std::uint64_t> ContextUnlockTick(int context, std::int32_t trophyId) { const auto owner = FindContext(context); if (!owner || trophyId < 0) return std::nullopt; diff --git a/core/libs/prx/libSceNpTrophy2/tests/TrophyPackage.cpp b/core/libs/prx/libSceNpTrophy2/tests/TrophyPackage.cpp index 5ae534f6f6..d1dcdba734 100644 --- a/core/libs/prx/libSceNpTrophy2/tests/TrophyPackage.cpp +++ b/core/libs/prx/libSceNpTrophy2/tests/TrophyPackage.cpp @@ -19,6 +19,8 @@ namespace { using Files = std::vector<std::pair<std::string, std::string>>; +constexpr std::int32_t InitialUser = 0x10000000; + void check(bool condition, const char* message) { if (condition) return; std::fprintf(stderr, "%s\n", message); @@ -84,10 +86,37 @@ const char TropMetaEnglish[] = R"({"metadata":{"titleMetadata":{"name":"Syntheti const char TropMetaFrench[] = R"({"metadata":{"titleMetadata":{"name":"Jeu synth\u00e9tique"},"trophyMetadata":[{"id":"1","name":"Dix"}]}})"; +const char StatsDefinition[] = R"({"statDefinitionArray":[ + {"statId":10,"dataType":"uint64","aggregation":"latest","minValue":"0"}, + {"statId":20,"dataType":"uint64","aggregation":"latest"}, + {"statId":30,"dataType":"uint32","aggregation":"latest"}, + {"statId":"40","dataType":"uint32","aggregation":"latest","maxValue":"50"}, + {"statId":50,"dataType":"uint64","aggregation":"sum"}, + {"statId":60,"dataType":"int32","aggregation":"latest"}, + {"statId":70,"dataType":"uint32","aggregation":"latest","maxValue":"5000000000"}, + {"dataType":"uint32","aggregation":"latest"} +]})"; + +const char StatsExtraction[] = R"({"statsExtractionRuleArray":[ + {"condition":{"eventName":"COLLECT"},"action":{"input":"$.counter","output":{"statId":10}}}, + {"condition":{"eventName":"COLLECT"},"action":{"input":"$.total","output":{"statId":20}}}, + {"condition":{"eventName":"TIME"},"action":{"input":"$.seconds","output":{"statId":30}}}, + {"condition":{"eventName":"STEPS"},"action":{"input":"$.player.steps","output":{"statId":"40"}}}, + {"condition":{"eventName":"SUM"},"action":{"input":"$.counter","output":{"statId":50}}}, + {"condition":{"eventName":"FILTERED","property":{"path":"$.level"}},"action":{"input":"$.counter","output":{"statId":10}}}, + {"condition":{"eventName":"COUNT"},"action":{"output":{"statId":10}}}, + {"condition":{"eventName":"BRACKETS"},"action":{"input":"$['counter']","output":{"statId":10}}}, + {"bad":1} +]})"; + std::string trophyPackage() { return ucp({{"tropconf.json", TropConf}, {"tropmeta_en-US.json", TropMetaEnglish}, {"tropmeta_fr-FR.json", TropMetaFrench}, {"trop0001.png", "not an image"}}); } +std::string statPackage() { + return ucp({{"stats_definition.json", StatsDefinition}, {"stats_extraction.json", StatsExtraction}}); +} + void testContainer() { const auto data = ucp({{"a.json", "{}"}, {"b.bin", std::string("\0\1\2", 3)}, {"a.json", "second"}}); const auto files = NpTrophy2::ReadUcp(data); @@ -279,6 +308,110 @@ void testProgressAndPlatinum() { check(NpTrophy2::CountTrophies(package, 5, isUnlocked).trophies == 0 && NpTrophy2::CountTrophies(package, 5, isUnlocked).Percentage() == 0, "an empty group has trophies"); } +void testStatRules() { + const auto rules = NpTrophy2::ParseStatRules(statPackage()); + check(rules.stats.size() == 4 && rules.stats.at(10).type == NpTrophy2::StatType::UInt64 && rules.stats.at(30).type == NpTrophy2::StatType::UInt32 && rules.stats.at(30).maximum == UINT32_MAX && + rules.stats.at(40).maximum == 50 && rules.stats.at(10).minimum == 0, + "stat definitions were not read"); + check(rules.events.size() == 3 && rules.events.at("COLLECT").size() == 2 && rules.events.at("COLLECT")[1].input == "total" && rules.events.at("COLLECT")[1].statId == 20 && + rules.events.at("STEPS")[0].input == "player.steps" && rules.events.at("TIME")[0].statId == 30, + "extraction rules were not read"); + std::set<std::string> skipped(rules.skipped.begin(), rules.skipped.end()); + const auto reported = [&skipped](const char* part) { + for (const auto& line : skipped) { + if (contains(line, part)) return true; + } + return false; + }; + check(rules.skipped.size() == 9, "unsupported definitions and rules were not all reported"); + check(reported("stat 50 has aggregation \"sum\"") && reported("stat 60 has dataType \"int32\"") && reported("stat 70 has maxValue \"5000000000\"") && reported("statDefinitionArray[7] has no unsigned statId"), + "unsupported definitions were not reported"); + check(reported("for SUM writes stat 50") && reported("filters FILTERED") && reported("for COUNT reads no input") && reported("for BRACKETS reads \"$['counter']\"") && reported("[8] has no condition"), + "unsupported rules were not reported"); + for (const char* input : {"$", "$.", "$..a", "$.a.", "$.a-b", "counter", "$.a b"}) { + const std::string extraction = std::string(R"({"statsExtractionRuleArray":[{"condition":{"eventName":"E"},"action":{"input":")") + input + R"(","output":{"statId":10}}}]})"; + check(NpTrophy2::ParseStatRules(ucp({{"stats_definition.json", StatsDefinition}, {"stats_extraction.json", extraction}})).events.empty(), "an unsupported input path was accepted"); + } + const auto withoutDefinitions = ucp({{"stats_extraction.json", StatsExtraction}}); + check(contains(failure([&withoutDefinitions] { NpTrophy2::ParseStatRules(withoutDefinitions); }), "no stats_definition.json"), "rules without definitions were read"); + const auto withoutRules = ucp({{"stats_definition.json", StatsDefinition}, {"stats_extraction.json", "{}"}}); + check(contains(failure([&withoutRules] { NpTrophy2::ParseStatRules(withoutRules); }), "has no statsExtractionRuleArray"), "rules without an extraction array were read"); +} + +void testConditions() { + const auto package = NpTrophy2::ParseTrophyPackage(trophyPackage(), "en-US"); + std::set<std::int32_t> unlocked; + const NpTrophy2::UnlockedQuery isUnlocked = [&unlocked](std::int32_t id) { return unlocked.contains(id); }; + const auto reached = [&](std::uint64_t stat, std::uint64_t value) { return NpTrophy2::ReachedTrophies(package, {{stat, value}}, isUnlocked); }; + check(reached(10, 9).empty() && reached(10, 10) == std::vector<std::int32_t>{1} && reached(10, 11) == std::vector<std::int32_t>{1}, "ge did not unlock at its target"); + check(reached(20, 100).empty() && reached(20, 101) == std::vector<std::int32_t>{2}, "gt did not unlock past its target"); + check(reached(30, 6).empty() && reached(30, 5) == std::vector<std::int32_t>{3} && reached(30, 3) == std::vector<std::int32_t>{3} && reached(30, 2) == (std::vector<std::int32_t>{3, 4}), + "le and lt did not unlock at and below their targets"); + check(reached(40, 4).empty() && reached(40, 5) == std::vector<std::int32_t>{5}, "the progressive trophy did not unlock at its target"); + check(reached(99, 1000).empty() && NpTrophy2::ReachedTrophies(package, {}, isUnlocked).empty(), "a stat without a trophy, or no stat, unlocked a trophy"); + unlocked.insert(1); + check(reached(10, 10).empty(), "an unlocked trophy was reached again"); + check(NpTrophy2::ReachedTrophies(package, {{10, 10}, {20, 101}, {30, 0}, {40, 9}}, isUnlocked) == (std::vector<std::int32_t>{2, 3, 4, 5}), "one set of stats did not reach every trophy"); +} + +void testStatStore() { + const std::filesystem::path directory("stats"); + { + NpTrophy2::StatStore stats(directory); + check(!stats.Value(InitialUser, 0, 10) && stats.Values(InitialUser, 0).empty(), "a new store has stats"); + stats.Set(InitialUser, 0, 10, 7); + stats.Set(InitialUser, 0, 18446744073709551615ull, 1); + stats.Set(InitialUser, 0, 10, 9); + stats.Set(InitialUser + 1, 0, 10, 3); + stats.Set(InitialUser, 1, 10, 4); + check(stats.RecordPath(InitialUser, 0) == directory / "268435456_0.stats.txt", "the stats are not stored next to the unlocks"); + } + NpTrophy2::StatStore restarted(directory); + check(restarted.Value(InitialUser, 0, 10) == std::optional<std::uint64_t>(9) && restarted.Value(InitialUser + 1, 0, 10) == std::optional<std::uint64_t>(3) && + restarted.Value(InitialUser, 1, 10) == std::optional<std::uint64_t>(4) && restarted.Values(InitialUser, 0).size() == 2, + "stats did not survive a restart"); + for (const char* text : {"10\n", "10 x\n", "x 5\n", "-1 5\n", "10 5\n10 6\n", "10 5 7\n", "\n", "10 -5\n"}) { + std::filesystem::remove_all("damaged"); + std::filesystem::create_directories("damaged"); + std::FILE* file = std::fopen("damaged/268435456_0.stats.txt", "wb"); + check(file != nullptr, "a damaged stat record was not created"); + std::fputs(text, file); + std::fclose(file); + NpTrophy2::StatStore damaged("damaged"); + check(contains(failure([&damaged] { damaged.Values(InitialUser, 0); }), "stat record"), "a damaged stat record was read"); + } +} + +NpTrophy2Event event(const char* name, std::vector<NpTrophy2EventProperty> properties) { + return {name, std::move(properties)}; +} + +void testApplyStatRules() { + const auto rules = NpTrophy2::ParseStatRules(statPackage()); + NpTrophy2::StatStore stats("applied"); + const auto none = NpTrophy2::ApplyStatRules(rules, event("UNKNOWN", {{"counter", 5, "5"}}), stats, InitialUser, 0); + check(none.updates.empty() && none.problems.empty() && stats.Values(InitialUser, 0).empty(), "an unknown event changed a stat"); + const auto collect = NpTrophy2::ApplyStatRules(rules, event("COLLECT", {{"counter", 12, "12"}, {"total", 300, "300"}, {"other", 1, "1"}}), stats, InitialUser, 0); + check(collect.problems.empty() && collect.updates.size() == 2 && collect.updates[0].statId == 10 && collect.updates[0].value == 12 && collect.updates[0].input == "counter" && + collect.updates[1].statId == 20 && collect.updates[1].value == 300, + "an event did not update every stat it feeds"); + check(stats.Value(InitialUser, 0, 10) == std::optional<std::uint64_t>(12) && stats.Value(InitialUser, 0, 20) == std::optional<std::uint64_t>(300), "the updated stats were not stored"); + const auto missing = NpTrophy2::ApplyStatRules(rules, event("COLLECT", {{"counter", 13, "13"}}), stats, InitialUser, 0); + check(missing.updates.size() == 1 && missing.problems.size() == 1 && missing.problems[0].rule == "COLLECT $.total for stat 20" && missing.problems[0].reason == "the event has no such property" && + stats.Value(InitialUser, 0, 20) == std::optional<std::uint64_t>(300), + "a missing property changed its stat"); + const auto wrong = NpTrophy2::ApplyStatRules(rules, event("TIME", {{"seconds", std::nullopt, "-3"}}), stats, InitialUser, 0); + check(wrong.updates.empty() && wrong.problems.size() == 1 && wrong.problems[0].reason == "-3 is not an unsigned integer" && !stats.Value(InitialUser, 0, 30), "a negative value changed a stat"); + const auto wide = NpTrophy2::ApplyStatRules(rules, event("TIME", {{"seconds", 4294967296ull, "4294967296"}}), stats, InitialUser, 0); + check(wide.updates.empty() && wide.problems.size() == 1 && wide.problems[0].reason == "4294967296 is not from 0 to 4294967295", "a uint32 stat took a value it cannot hold"); + const auto bounded = NpTrophy2::ApplyStatRules(rules, event("STEPS", {{"player.steps", 51, "51"}}), stats, InitialUser, 0); + check(bounded.updates.empty() && bounded.problems[0].reason == "51 is not from 0 to 50", "a value above maxValue changed a stat"); + const auto nested = NpTrophy2::ApplyStatRules(rules, event("STEPS", {{"steps", 2, "2"}, {"player.steps", 50, "50"}}), stats, InitialUser, 0); + check(nested.updates.size() == 1 && nested.updates[0].statId == 40 && stats.Value(InitialUser, 0, 40) == std::optional<std::uint64_t>(50), "a nested property was not read"); + const auto latest = NpTrophy2::ApplyStatRules(rules, event("COLLECT", {{"counter", 2, "2"}, {"total", 1, "1"}}), stats, InitialUser, 0); + check(latest.updates.size() == 2 && stats.Value(InitialUser, 0, 10) == std::optional<std::uint64_t>(2), "a stat did not keep the latest value"); +} + } int main() { @@ -290,6 +423,10 @@ int main() { testJson(); testPackage(); testProgressAndPlatinum(); + testStatRules(); + testConditions(); + testStatStore(); + testApplyStatRules(); std::filesystem::current_path(previous); std::filesystem::remove_all(root); std::puts("Trophy package tests passed"); diff --git a/core/libs/prx/libSceNpUniversalDataSystem/CMakeLists.txt b/core/libs/prx/libSceNpUniversalDataSystem/CMakeLists.txt index d7b5a34d12..d9163d1dc7 100644 --- a/core/libs/prx/libSceNpUniversalDataSystem/CMakeLists.txt +++ b/core/libs/prx/libSceNpUniversalDataSystem/CMakeLists.txt @@ -53,4 +53,14 @@ if(BUILD_TESTING) set_tests_properties(guest_np_trophy_data PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceNpUniversalDataSystem>") endif() + + add_test_executable(guest_np_trophy_stats_tests ${LIBS_INCLUDE_DIR}/tests/GuestNpTrophyStats.cpp) + target_include_directories(guest_np_trophy_stats_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_np_trophy_stats_tests PRIVATE libSceNpUniversalDataSystem libSceNpTrophy2 libc) + configure_windows_unwind(guest_np_trophy_stats_tests) + add_test(NAME guest_np_trophy_stats COMMAND guest_np_trophy_stats_tests) + if(WIN32) + set_tests_properties(guest_np_trophy_stats PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceNpUniversalDataSystem>") + endif() endif() diff --git a/core/libs/prx/libSceNpUniversalDataSystem/include/UdsEvent.hpp b/core/libs/prx/libSceNpUniversalDataSystem/include/UdsEvent.hpp index d3cae5efb2..35c8196824 100644 --- a/core/libs/prx/libSceNpUniversalDataSystem/include/UdsEvent.hpp +++ b/core/libs/prx/libSceNpUniversalDataSystem/include/UdsEvent.hpp @@ -11,6 +11,7 @@ struct NpUniversalDataSystemEventPropertyObject; struct NpUniversalDataSystemEventPropertyArray; +struct NpTrophy2Event; namespace Uds { @@ -57,8 +58,10 @@ struct TrophyEvent { }; TrophyEvent RecognizeTrophyEvent(const NpUniversalDataSystemEvent& event); +std::string Describe(const Value& value); std::string Describe(const NpUniversalDataSystemEventPropertyObject& object); std::string Describe(const NpUniversalDataSystemEvent& event); +NpTrophy2Event StatEvent(const NpUniversalDataSystemEvent& event); } diff --git a/core/libs/prx/libSceNpUniversalDataSystem/src/Event.cpp b/core/libs/prx/libSceNpUniversalDataSystem/src/Event.cpp index 0ead101091..be7df0ae46 100644 --- a/core/libs/prx/libSceNpUniversalDataSystem/src/Event.cpp +++ b/core/libs/prx/libSceNpUniversalDataSystem/src/Event.cpp @@ -38,6 +38,7 @@ void post(const NpUniversalDataSystemEvent& event, int context) { const auto trophy = Uds::RecognizeTrophyEvent(event); switch (trophy.kind) { case Uds::TrophyEventKind::None: + if (const auto owner = Uds::FindContext(context)) NpTrophy2ApplyEvent_nid_no_patch(owner->userId, owner->serviceLabel, Uds::StatEvent(event)); return; case Uds::TrophyEventKind::Malformed: ignore(event, context, trophy.problem); diff --git a/core/libs/prx/libSceNpUniversalDataSystem/src/UdsEvent.cpp b/core/libs/prx/libSceNpUniversalDataSystem/src/UdsEvent.cpp index 96b8672b36..201178ddbf 100644 --- a/core/libs/prx/libSceNpUniversalDataSystem/src/UdsEvent.cpp +++ b/core/libs/prx/libSceNpUniversalDataSystem/src/UdsEvent.cpp @@ -1,6 +1,8 @@ #include "prx/libSceNpUniversalDataSystem/include/UdsEvent.hpp" +#include "prx/libSceNpTrophy2/include/TrophyStore.hpp" #include <cstdio> +#include <optional> #include <type_traits> namespace Uds { @@ -81,6 +83,31 @@ void appendValue(std::string& out, const Value& value) { }, value); } +std::optional<std::uint64_t> unsignedValue(const Value& value) { + return std::visit([](const auto& held) -> std::optional<std::uint64_t> { + using Held = std::decay_t<decltype(held)>; + if constexpr (std::is_same_v<Held, std::uint32_t> || std::is_same_v<Held, std::uint64_t>) { + return held; + } else if constexpr (std::is_same_v<Held, std::int32_t> || std::is_same_v<Held, std::int64_t>) { + if (held < 0) return std::nullopt; + return static_cast<std::uint64_t>(held); + } else { + return std::nullopt; + } + }, value); +} + +void flatten(const NpUniversalDataSystemEventPropertyObject& object, const std::string& prefix, std::vector<NpTrophy2EventProperty>& properties) { + for (const auto& [key, value] : object.properties) { + auto path = prefix.empty() ? key : prefix + "." + key; + if (const auto* child = std::get_if<std::shared_ptr<NpUniversalDataSystemEventPropertyObject>>(&value)) { + flatten(**child, path, properties); + continue; + } + properties.push_back({std::move(path), unsignedValue(value), Describe(value)}); + } +} + } TrophyEvent RecognizeTrophyEvent(const NpUniversalDataSystemEvent& event) { @@ -99,6 +126,12 @@ TrophyEvent RecognizeTrophyEvent(const NpUniversalDataSystemEvent& event) { return result; } +std::string Describe(const Value& value) { + std::string out; + appendValue(out, value); + return out; +} + std::string Describe(const NpUniversalDataSystemEventPropertyObject& object) { std::string out; appendObject(out, object); @@ -109,6 +142,13 @@ std::string Describe(const NpUniversalDataSystemEvent& event) { return event.name + " " + Describe(event.properties); } +NpTrophy2Event StatEvent(const NpUniversalDataSystemEvent& event) { + NpTrophy2Event converted; + converted.name = event.name; + flatten(event.properties, {}, converted.properties); + return converted; +} + } Uds::Value& NpUniversalDataSystemEventPropertyObject::Set(std::string_view key, Uds::Value value) { diff --git a/core/libs/prx/libSceNpUniversalDataSystem/tests/Events.cpp b/core/libs/prx/libSceNpUniversalDataSystem/tests/Events.cpp index 92274b6f56..d9cca747e4 100644 --- a/core/libs/prx/libSceNpUniversalDataSystem/tests/Events.cpp +++ b/core/libs/prx/libSceNpUniversalDataSystem/tests/Events.cpp @@ -5,9 +5,12 @@ #include <cstddef> #include <cstdint> #include <cstdio> +#include <memory> #include <optional> #include <stdexcept> #include <string> +#include <tuple> +#include <vector> extern "C" { int APS5_VABI sceNpUniversalDataSystemCreateContext(int*, int, uint32_t, uint64_t); @@ -198,6 +201,36 @@ void testRecognition() { } } +void testStatEvent() { + NpUniversalDataSystemEvent event; + event.name = "STEPS"; + event.properties.Set("count", std::uint32_t{4}); + event.properties.Set("signed", std::int32_t{-2}); + event.properties.Set("wide", std::int64_t{7}); + event.properties.Set("huge", std::uint64_t{18000000000000000000ull}); + event.properties.Set("ratio", 0.5); + event.properties.Set("name", std::string("x")); + auto player = std::make_shared<NpUniversalDataSystemEventPropertyObject>(); + player->Set("steps", std::uint32_t{5}); + auto inner = std::make_shared<NpUniversalDataSystemEventPropertyObject>(); + inner->Set("depth", std::int32_t{1}); + player->Set("inner", inner); + event.properties.Set("player", player); + auto list = std::make_shared<NpUniversalDataSystemEventPropertyArray>(); + list->Add(std::int32_t{1}); + event.properties.Set("list", list); + const auto converted = Uds::StatEvent(event); + const std::vector<std::tuple<std::string, std::optional<std::uint64_t>, std::string>> expected{ + {"count", 4, "4"}, {"signed", std::nullopt, "-2"}, {"wide", 7, "7"}, {"huge", 18000000000000000000ull, "18000000000000000000"}, {"ratio", std::nullopt, "0.5"}, + {"name", std::nullopt, "\"x\""}, {"player.steps", 5, "5"}, {"player.inner.depth", 1, "1"}, {"list", std::nullopt, "[1]"}}; + check(converted.name == "STEPS" && converted.properties.size() == expected.size(), "the stat event does not hold every leaf property"); + for (std::size_t i = 0; i < expected.size(); ++i) { + const auto& [path, value, text] = expected[i]; + const auto& property = converted.properties[i]; + check(property.path == path && property.value == value && property.text == text, "a stat event property has the wrong path, value or text"); + } +} + struct Unlocks { int count = 0; }; @@ -252,6 +285,7 @@ int main() { testNestedValues(); testEvents(); testRecognition(); + testStatEvent(); testPostingIgnoredEvents(); std::puts("UDS event tests passed"); return 0; diff --git a/core/libs/tests/GuestNpTrophyStats.cpp b/core/libs/tests/GuestNpTrophyStats.cpp new file mode 100644 index 0000000000..2464aa36fc --- /dev/null +++ b/core/libs/tests/GuestNpTrophyStats.cpp @@ -0,0 +1,306 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libSceNpTrophy2/include/NpTrophy2Types.hpp" +#include "prx/libSceNpTrophy2/include/TrophyStore.hpp" + +#include <cstddef> +#include <cstdint> +#include <cstdio> +#include <cstdlib> +#include <cstring> +#include <filesystem> +#include <random> +#include <string> +#include <utility> +#include <vector> + +struct NpUniversalDataSystemEvent; +struct NpUniversalDataSystemEventPropertyObject; + +extern "C" { +int APS5_VABI sceNpUniversalDataSystemCreateContext(int*, int, uint32_t, uint64_t); +int APS5_VABI sceNpUniversalDataSystemRegisterContext(int, int, uint64_t); +int APS5_VABI sceNpUniversalDataSystemCreateHandle(int*); +int APS5_VABI sceNpUniversalDataSystemCreateEvent(const char*, const NpUniversalDataSystemEventPropertyObject*, NpUniversalDataSystemEvent**, NpUniversalDataSystemEventPropertyObject**); +int APS5_VABI sceNpUniversalDataSystemDestroyEvent(NpUniversalDataSystemEvent*); +int APS5_VABI sceNpUniversalDataSystemPostEvent(int, int, const void*, uint64_t); +int APS5_VABI sceNpUniversalDataSystemCreateEventPropertyObject(NpUniversalDataSystemEventPropertyObject**); +int APS5_VABI sceNpUniversalDataSystemDestroyEventPropertyObject(NpUniversalDataSystemEventPropertyObject*); +int APS5_VABI sceNpUniversalDataSystemEventPropertyObjectSetInt32(NpUniversalDataSystemEventPropertyObject*, const char*, int32_t); +int APS5_VABI sceNpUniversalDataSystemEventPropertyObjectSetUInt32(NpUniversalDataSystemEventPropertyObject*, const char*, uint32_t); +int APS5_VABI sceNpUniversalDataSystemEventPropertyObjectSetInt64(NpUniversalDataSystemEventPropertyObject*, const char*, int64_t); +int APS5_VABI sceNpUniversalDataSystemEventPropertyObjectSetUInt64(NpUniversalDataSystemEventPropertyObject*, const char*, uint64_t); +int APS5_VABI sceNpUniversalDataSystemEventPropertyObjectSetFloat32(NpUniversalDataSystemEventPropertyObject*, const char*, float); +int APS5_VABI sceNpUniversalDataSystemEventPropertyObjectSetObject(NpUniversalDataSystemEventPropertyObject*, const char*, const NpUniversalDataSystemEventPropertyObject*, NpUniversalDataSystemEventPropertyObject**); +int APS5_VABI sceNpTrophy2CreateContext(int*, int, uint32_t, uint64_t); +int APS5_VABI sceNpTrophy2GetGameInfo(int, int, NpTrophy2GameDetails*, NpTrophy2GameData*); +int APS5_VABI sceNpTrophy2GetTrophyInfo(int, int, int, NpTrophy2Details*, NpTrophy2Data*); +} + +static constexpr int InitialUser = 0x10000000; + +static const char TropConf[] = R"({"defaultLanguage":"en-US","trophies":[ + {"id":"0","grade":"P"}, + {"id":"1","grade":"G","platinumTrophyId":"0","unlockCondition":{"udsStatId":"10","comparator":"ge","targetValue":"10","progressive":false}}, + {"id":"2","grade":"S","platinumTrophyId":"0","unlockCondition":{"udsStatId":"20","comparator":"gt","targetValue":"100"}}, + {"id":"3","grade":"B","platinumTrophyId":"0","unlockCondition":{"udsStatId":"30","comparator":"le","targetValue":"5"}}, + {"id":"4","grade":"B","platinumTrophyId":"0","unlockCondition":{"udsStatId":"30","comparator":"lt","targetValue":"3"}}, + {"id":"5","grade":"B","platinumTrophyId":"0","unlockCondition":{"udsStatId":"40","comparator":"ge","targetValue":"5","progressive":true}}, + {"id":"6","grade":"B","groupId":"1","hidden":true,"hasReward":true}, + {"id":"7","grade":"B","groupId":"1"} +]})"; + +static const char TropMeta[] = R"({"metadata":{"titleMetadata":{"name":"Synthetic Game"}, + "trophyMetadata":[{"id":"0","name":"All of them"},{"id":"1","name":"Ten","detail":"Reach ten"},{"id":"5","name":"Five steps"}, + {"id":"6","name":"Secret","detail":"A hidden one","reward":"A hat"}], + "groupMetadata":[{"id":"1","name":"Expansion"}]}})"; + +static const char StatsDefinition[] = R"({"statDefinitionArray":[ + {"statId":10,"dataType":"uint64","aggregation":"latest"},{"statId":20,"dataType":"uint64","aggregation":"latest"}, + {"statId":30,"dataType":"uint32","aggregation":"latest"},{"statId":40,"dataType":"uint32","aggregation":"latest"}]})"; + +static const char StatsExtraction[] = R"({"statsExtractionRuleArray":[ + {"condition":{"eventName":"COLLECT"},"action":{"input":"$.counter","output":{"statId":10}}}, + {"condition":{"eventName":"COLLECT"},"action":{"input":"$.total","output":{"statId":20}}}, + {"condition":{"eventName":"TIME"},"action":{"input":"$.seconds","output":{"statId":30}}}, + {"condition":{"eventName":"STEPS"},"action":{"input":"$.player.steps","output":{"statId":40}}}]})"; + +static void Require(bool value, const char* message) { + if (value) return; + std::fprintf(stderr, "%s\n", message); + std::abort(); +} + +static void PutBigEndian(std::string& data, std::size_t offset, std::uint64_t value, std::size_t size) { + for (std::size_t i = 0; i < size; ++i) data[offset + i] = static_cast<char>((value >> (8 * (size - 1 - i))) & 0xFF); +} + +static std::string Ucp(const std::vector<std::pair<std::string, std::string>>& files) { + std::size_t offset = 0x60 + files.size() * 0x40; + std::size_t size = offset; + for (const auto& file : files) size += file.second.size(); + std::string data(size, '\0'); + PutBigEndian(data, 0, 0xB228C60A, 4); + PutBigEndian(data, 4, 1, 4); + PutBigEndian(data, 8, size, 8); + PutBigEndian(data, 0x10, files.size(), 4); + PutBigEndian(data, 0x14, 0x40, 4); + std::size_t entry = 0x60; + for (const auto& [name, contents] : files) { + data.replace(entry, name.size(), name); + PutBigEndian(data, entry + 0x20, offset, 8); + PutBigEndian(data, entry + 0x28, contents.size(), 8); + data.replace(offset, contents.size(), contents); + offset += contents.size(); + entry += 0x40; + } + return data; +} + +static void Write(const std::filesystem::path& path, const std::string& text) { + std::filesystem::create_directories(path.parent_path()); + std::FILE* file = std::fopen(path.string().c_str(), "wb"); + Require(file != nullptr, "a test file was not created"); + const bool written = std::fwrite(text.data(), 1, text.size(), file) == text.size(); + Require(std::fclose(file) == 0 && written, "a test file was not written"); +} + +static std::string Read(const char* path) { + std::string text; + std::FILE* file = std::fopen(path, "rb"); + if (file == nullptr) return text; + char buffer[256]; + for (std::size_t count; (count = std::fread(buffer, 1, sizeof(buffer), file)) != 0;) text.append(buffer, count); + std::fclose(file); + return text; +} + +static bool Exists(const char* path) { + std::FILE* file = std::fopen(path, "rb"); + if (file == nullptr) return false; + std::fclose(file); + return true; +} + +struct Uds { + int context = 0; + int handle = 0; +}; + +static Uds OpenUds(std::uint32_t serviceLabel) { + Uds uds; + Require(sceNpUniversalDataSystemCreateContext(&uds.context, InitialUser, serviceLabel, 0) == 0, "UDS context was not created"); + Require(sceNpUniversalDataSystemCreateHandle(&uds.handle) == 0, "UDS handle was not created"); + Require(sceNpUniversalDataSystemRegisterContext(uds.context, uds.handle, 0) == 0, "UDS context was not registered"); + return uds; +} + +template <typename TSet> +static void Post(const Uds& uds, const char* name, TSet set) { + NpUniversalDataSystemEventPropertyObject* properties = nullptr; + Require(sceNpUniversalDataSystemCreateEventPropertyObject(&properties) == 0, "property object was not created"); + set(properties); + NpUniversalDataSystemEvent* event = nullptr; + Require(sceNpUniversalDataSystemCreateEvent(name, properties, &event, nullptr) == 0, "event was not created"); + Require(sceNpUniversalDataSystemPostEvent(uds.context, uds.handle, event, 0) == 0, "event was not posted"); + Require(sceNpUniversalDataSystemDestroyEvent(event) == 0, "event was not destroyed"); + Require(sceNpUniversalDataSystemDestroyEventPropertyObject(properties) == 0, "property object was not destroyed"); +} + +static int TrophyContext(std::uint32_t serviceLabel) { + int context = 0; + Require(sceNpTrophy2CreateContext(&context, InitialUser, serviceLabel, 0) == 0, "trophy context was not created"); + return context; +} + +static NpTrophy2Data Trophy(int context, int trophyId, NpTrophy2Details* details = nullptr) { + NpTrophy2Data data{}; + Require(sceNpTrophy2GetTrophyInfo(context, 1, trophyId, details, &data) == 0, "trophy info was not returned"); + Require(data.trophy_id == trophyId, "trophy info is for another trophy"); + return data; +} + +static NpTrophy2GameData Game(int context, NpTrophy2GameDetails* details = nullptr) { + NpTrophy2GameData data{}; + Require(sceNpTrophy2GetGameInfo(context, 1, details, &data) == 0, "game info was not returned"); + return data; +} + +struct Unlocks { + std::vector<std::pair<std::uint32_t, std::int32_t>> trophies; +}; + +static void Remember(void* context, const NpTrophy2Unlock& unlock) { + Require(unlock.titleId == "PPSA00000" && unlock.userId == InitialUser && unlock.tick != 0, "an unlock does not carry its title, user and time"); + static_cast<Unlocks*>(context)->trophies.emplace_back(unlock.serviceLabel, unlock.trophyId); +} + +static void KeepStatsAcrossRuns(int context) { + NpTrophy2Details details{}; + const auto steps = Trophy(context, 5, &details); + Require(details.target.type == 1 && details.target.value == 5 && !steps.unlocked && steps.progress.type == 1 && steps.progress.value == 2, + "the progressive trophy does not report the stat kept from the last run"); +} + +static void UnlockThroughStats(int context, Unlocks& unlocks) { + const auto uds = OpenUds(0); + Post(uds, "COLLECT", [](NpUniversalDataSystemEventPropertyObject* properties) { + Require(sceNpUniversalDataSystemEventPropertyObjectSetUInt64(properties, "counter", 9) == 0, "counter was not set"); + Require(sceNpUniversalDataSystemEventPropertyObjectSetUInt64(properties, "total", 100) == 0, "total was not set"); + }); + Require(!Trophy(context, 1).unlocked && !Trophy(context, 2).unlocked && unlocks.trophies.empty(), "ge or gt unlocked below its target"); + Post(uds, "COLLECT", [](NpUniversalDataSystemEventPropertyObject* properties) { + Require(sceNpUniversalDataSystemEventPropertyObjectSetUInt32(properties, "counter", 10) == 0, "counter was not set"); + }); + Require(Trophy(context, 1).unlocked && !Trophy(context, 2).unlocked, "ge did not unlock at its target"); + Post(uds, "COLLECT", [](NpUniversalDataSystemEventPropertyObject* properties) { + Require(sceNpUniversalDataSystemEventPropertyObjectSetInt32(properties, "counter", 3) == 0, "counter was not set"); + Require(sceNpUniversalDataSystemEventPropertyObjectSetInt64(properties, "total", 101) == 0, "total was not set"); + }); + Require(Trophy(context, 2).unlocked, "gt did not unlock past its target"); + + Post(uds, "TIME", [](NpUniversalDataSystemEventPropertyObject* properties) { + Require(sceNpUniversalDataSystemEventPropertyObjectSetUInt32(properties, "seconds", 6) == 0, "seconds was not set"); + }); + Require(!Trophy(context, 3).unlocked && !Trophy(context, 4).unlocked, "le or lt unlocked above its target"); + Post(uds, "TIME", [](NpUniversalDataSystemEventPropertyObject* properties) { + Require(sceNpUniversalDataSystemEventPropertyObjectSetUInt32(properties, "seconds", 5) == 0, "seconds was not set"); + }); + Require(Trophy(context, 3).unlocked && !Trophy(context, 4).unlocked, "le did not unlock at its target"); + Post(uds, "TIME", [](NpUniversalDataSystemEventPropertyObject* properties) { + Require(sceNpUniversalDataSystemEventPropertyObjectSetInt32(properties, "seconds", -1) == 0, "seconds was not set"); + }); + Post(uds, "TIME", [](NpUniversalDataSystemEventPropertyObject* properties) { + Require(sceNpUniversalDataSystemEventPropertyObjectSetFloat32(properties, "seconds", 1.0f) == 0, "seconds was not set"); + }); + Require(!Trophy(context, 4).unlocked, "a negative or fractional value reached lt"); + Post(uds, "TIME", [](NpUniversalDataSystemEventPropertyObject* properties) { + Require(sceNpUniversalDataSystemEventPropertyObjectSetUInt32(properties, "seconds", 2) == 0, "seconds was not set"); + }); + Require(Trophy(context, 4).unlocked, "lt did not unlock below its target"); + + Post(uds, "NOTHING", [](NpUniversalDataSystemEventPropertyObject* properties) { + Require(sceNpUniversalDataSystemEventPropertyObjectSetUInt32(properties, "steps", 50) == 0, "steps was not set"); + }); + const auto stepped = [&uds](std::uint32_t steps) { + Post(uds, "STEPS", [steps](NpUniversalDataSystemEventPropertyObject* properties) { + NpUniversalDataSystemEventPropertyObject* player = nullptr; + Require(sceNpUniversalDataSystemEventPropertyObjectSetObject(properties, "player", nullptr, &player) == 0, "player was not set"); + Require(sceNpUniversalDataSystemEventPropertyObjectSetUInt32(player, "steps", steps) == 0, "steps was not set"); + }); + }; + stepped(4); + const auto partway = Trophy(context, 5); + Require(!partway.unlocked && partway.progress.type == 1 && partway.progress.value == 4 && !Trophy(context, 0).unlocked, "the progressive trophy does not report its progress"); + stepped(5); + const auto done = Trophy(context, 5); + Require(done.unlocked && done.progress.value == 5, "the progressive trophy did not unlock at its target"); + Require(Trophy(context, 0).unlocked, "the platinum did not unlock with the last trophy that names it"); + + const std::vector<std::pair<std::uint32_t, std::int32_t>> expected{{0, 1}, {0, 2}, {0, 3}, {0, 4}, {0, 5}, {0, 0}}; + Require(unlocks.trophies == expected, "the listener did not see each stat unlock and the platinum once, in order"); + const auto game = Game(context); + Require(game.unlocked_trophies == 6 && game.unlocked_platinum == 1 && game.unlocked_gold == 1 && game.unlocked_silver == 1 && game.unlocked_bronze == 3 && + game.progress_percentage == (6 + 2 + 3) * 100 / (6 + 2 + 5), + "the game does not count the unlocks by grade"); + Require(Read("_trophy/268435456_0.stats.txt") == "10 3\n20 101\n30 2\n40 5\n", "the stats were not stored next to the unlocks"); + const auto record = Read("_trophy/268435456_0.txt"); + for (const char* line : {"0 ", "1 ", "2 ", "3 ", "4 ", "5 "}) Require(record.find(std::string("\n") + line) != std::string::npos || record.starts_with(line), "an unlock was not stored"); +} + +static void IgnoreMissingAndMalformedFiles(Unlocks& unlocks) { + const auto missing = TrophyContext(1); + NpTrophy2GameDetails game{}; + Game(missing, &game); + Require(std::strcmp(game.title, "Game") == 0, "a title without trophy files does not report placeholders"); + Post(OpenUds(1), "COLLECT", [](NpUniversalDataSystemEventPropertyObject* properties) { + Require(sceNpUniversalDataSystemEventPropertyObjectSetUInt32(properties, "counter", 10) == 0, "counter was not set"); + }); + Require(!Exists("_trophy/268435456_1.stats.txt"), "a title without stat rules stored a stat"); + + const auto brokenTrophies = TrophyContext(2); + Game(brokenTrophies, &game); + Require(std::strcmp(game.title, "Game") == 0, "a malformed trophy package was used"); + Post(OpenUds(2), "COLLECT", [](NpUniversalDataSystemEventPropertyObject* properties) { + Require(sceNpUniversalDataSystemEventPropertyObjectSetUInt32(properties, "counter", 10) == 0, "counter was not set"); + }); + Require(Read("_trophy/268435456_2.stats.txt") == "10 10\n" && !Exists("_trophy/268435456_2.txt"), "a malformed trophy package disabled the stat rules, or stats unlocked a trophy without one"); + + const auto brokenRules = TrophyContext(3); + Game(brokenRules, &game); + Require(std::strcmp(game.title, "Synthetic Game") == 0, "malformed stat rules disabled the trophy package"); + Post(OpenUds(3), "COLLECT", [](NpUniversalDataSystemEventPropertyObject* properties) { + Require(sceNpUniversalDataSystemEventPropertyObjectSetUInt32(properties, "counter", 10) == 0, "counter was not set"); + }); + Require(!Exists("_trophy/268435456_3.stats.txt") && !Trophy(brokenRules, 1).unlocked, "malformed stat rules were used"); + Require(unlocks.trophies.size() == 6, "a missing or malformed file unlocked a trophy"); +} + +int main() { + const auto previous = std::filesystem::current_path(); + const auto root = std::filesystem::temp_directory_path() / ("anyps5-trophy-stats-" + std::to_string(std::random_device{}())); + std::filesystem::create_directories(root); + std::filesystem::current_path(root); + Write("app0/sce_sys/param.json", R"({"titleId":"PPSA00000","localizedParameters":{"en-US":{"titleName":"Example"}}})"); + const auto trophies = Ucp({{"tropconf.json", TropConf}, {"tropmeta_en-US.json", TropMeta}}); + const auto rules = Ucp({{"stats_definition.json", StatsDefinition}, {"stats_extraction.json", StatsExtraction}}); + Write("app0/sce_sys/trophy2/trophy00.ucp", trophies); + Write("app0/sce_sys/uds/uds00.ucp", rules); + Write("app0/sce_sys/trophy2/trophy02.ucp", "not a package"); + Write("app0/sce_sys/uds/uds02.ucp", rules); + Write("app0/sce_sys/trophy2/trophy03.ucp", trophies); + Write("app0/sce_sys/uds/uds03.ucp", rules.substr(0, rules.size() - 1)); + Write("_trophy/268435456_0.stats.txt", "40 2\n"); + + Unlocks unlocks; + const auto listener = NpTrophy2AddUnlockListener_nid_no_patch(Remember, &unlocks); + const auto context = TrophyContext(0); + KeepStatsAcrossRuns(context); + UnlockThroughStats(context, unlocks); + IgnoreMissingAndMalformedFiles(unlocks); + NpTrophy2RemoveUnlockListener_nid_no_patch(listener); + + std::filesystem::current_path(previous); + std::filesystem::remove_all(root); + std::puts("Trophy stat tests passed"); + return 0; +} diff --git a/docs/dev/debt/libSceNpTrophy2.md b/docs/dev/debt/libSceNpTrophy2.md index b7701b97c1..dbf8da133f 100644 --- a/docs/dev/debt/libSceNpTrophy2.md +++ b/docs/dev/debt/libSceNpTrophy2.md @@ -13,6 +13,6 @@ ### Functional - [Trophy queries](../../../core/libs/prx/libSceNpTrophy2/src/TrophyInfo.cpp) report the title's [trophy data](../../../core/libs/prx/libSceNpTrophy2/src/TrophyPackage.cpp) (`sce_sys/trophy2/trophyNN.ucp`) and the unlocks in the [trophy store](../../../core/libs/prx/libSceNpTrophy2/src/TrophyStore.cpp). Without the file, or when it is malformed, they report the old placeholders and `sceNpTrophy2GetTrophyInfoArray` lists only trophy 0. Icons are not read. Hidden trophies report their names and descriptions; whether the console blanks them before the unlock is not known. `progress_percentage` weighs gold 6, silver 2 and bronze 1 and leaves the platinum out, as KytyPS5 does (the ratio of the published PSN trophy points); the console's formula is not known. A trophy or group that is not in the file throws. A context that was not created or was destroyed reports placeholders and no unlocks and returns success; the console's errors for it are unknown. -- [Trophy data limits](../../../core/libs/prx/libSceNpTrophy2/src/TrophyPackage.cpp): a `trophyNN.ucp` larger than 256 MiB or listing more than 4096 files, and a JSON file in it that nests deeper than 64 levels or holds more than 1048576 values, are not used, and the title runs as without that file. The limits keep the memory the reader takes to the file and about 140 MiB of parsed JSON; the console's limits are not known. +- [Trophy data limits](../../../core/libs/prx/libSceNpTrophy2/src/TrophyPackage.cpp): a `trophyNN.ucp` or `udsNN.ucp` larger than 256 MiB or listing more than 4096 files, and a JSON file in it that nests deeper than 64 levels or holds more than 1048576 values, are not used, and the title runs as without that file. The limits keep the memory the reader takes to the file and about 140 MiB of parsed JSON; the console's limits are not known. - [Text language](../../../core/libs/prx/libSceNpTrophy2/src/TrophyPackage.cpp): texts come from `tropmeta_<tag>.json` for the console language, then `en-US`, then the file's `defaultLanguage`. The tags for languages 0 to 29 are KytyPS5's; `uk-UA` for 30 (Ukrainian) is an assumption. - [Platinum](../../../core/libs/prx/libSceNpTrophy2/src/TitleTrophies.cpp): an unlock through any path unlocks the platinum that the trophy names in `platinumTrophyId` once every trophy naming that platinum is unlocked, as KytyPS5 does; trophies without `platinumTrophyId` are not required. A platinum or a progressive trophy can still be unlocked directly with `_UnlockTrophy`, which KytyPS5 refuses; the console's behaviour is not known. Without trophy data any id of 0 or more is stored, and the platinum is not unlocked when the trophies it needs are. diff --git a/docs/dev/debt/libSceNpUniversalDataSystem.md b/docs/dev/debt/libSceNpUniversalDataSystem.md index 3fcbc2508f..5dc32b7964 100644 --- a/docs/dev/debt/libSceNpUniversalDataSystem.md +++ b/docs/dev/debt/libSceNpUniversalDataSystem.md @@ -6,4 +6,5 @@ ### Functional -- [Trophy events](../../../core/libs/prx/libSceNpUniversalDataSystem/src/Event.cpp): `sceNpUniversalDataSystemPostEvent` stores an `_UnlockTrophy` event with an int32 `_trophy_id` of 0 or more as an unlock for the user and service label of its context, as KytyPS5 does. Unlocks that a title drives through statistics (`statsExtractionRuleArray` in `sce_sys/uds/udsNN.ucp` and `unlockCondition` in `sce_sys/trophy2/trophyNN.ucp`) are not evaluated, and `_UpdateTrophyProgress`, named only by a third-party unlocker, is logged once per trophy and not interpreted. A context need not be registered, and every post of an event that `sceNpUniversalDataSystemCreateEvent` returned and that is not yet destroyed returns success (a null or any other event pointer stops the game): KytyPS5's errors for an unknown or unregistered context and for a malformed unlock are not verified. The storage statistics stay zero. +- [Trophy events](../../../core/libs/prx/libSceNpUniversalDataSystem/src/Event.cpp): `sceNpUniversalDataSystemPostEvent` stores an `_UnlockTrophy` event with an int32 `_trophy_id` of 0 or more as an unlock for the user and service label of its context, as KytyPS5 does. `_UpdateTrophyProgress`, named only by a third-party unlocker, is logged once per trophy and not interpreted. A context need not be registered, and every post of an event that `sceNpUniversalDataSystemCreateEvent` returned and that is not yet destroyed returns success (a null or any other event pointer stops the game): KytyPS5's errors for an unknown or unregistered context and for a malformed unlock are not verified. The storage statistics stay zero. +- [Statistics](../../../core/libs/prx/libSceNpUniversalDataSystem/src/Event.cpp): every other event goes through the title's rules in `sce_sys/uds/udsNN.ucp`, which [libSceNpTrophy2](../../../core/libs/prx/libSceNpTrophy2/src/TitleTrophies.cpp) reads. Only statistics with `"aggregation":"latest"` and `dataType` `uint32` or `uint64` are kept, as in KytyPS5; no public source documents the other aggregations or types, so their definitions and rules are skipped. Only rules whose condition is an `eventName` alone and whose input is a `$.name` path (dotted for nested objects) are applied. A value is used when it is an integer of any width that is not negative and fits the statistic's type and `minValue`/`maxValue`; KytyPS5 requires the statistic's exact width, and whether the console converts, clamps or rejects other values is not known. Statistics are stored per user and service label, and every trophy's `unlockCondition` is compared after each event that sets one. From 75ca9e469a1476096df26ed50add6dc7f3fed08e Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Sat, 17 Oct 2026 23:29:17 +0000 Subject: [PATCH 0910/1000] docs: describe trophy statistics in USAGE --- docs/user/USAGE.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index c2f142dfe1..2686db9e09 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -159,13 +159,15 @@ Games ask for the console's language with `sceSystemServiceParamGetInt` and show ### Trophies -A game unlocks a trophy by posting a `_UnlockTrophy` event with an int32 `_trophy_id` property to `libSceNpUniversalDataSystem`. The unlock is stored in `_trophy/<user id>_<service label>.txt` in the directory the game runs in (the one that holds `app0/`), one `<trophy id> <unlock time>` line per trophy; the time is an RTC tick, microseconds since 0001-01-01 UTC. Delete the file before starting the game to lock its trophies again. Each new unlock prints one line, such as `[trophy] PPSA01556 unlocked trophy 12 for user 268435456`; unlocking a trophy again prints nothing. `sceNpTrophy2GetTrophyInfo`, `sceNpTrophy2GetTrophyInfoArray` and `sceNpTrophy2GetGameInfo` report the stored unlocks and their times to the game. Every posted `_trophy_id` of 0 or more is stored, even one the trophy data has no trophy for or makes the platinum or a progressive trophy. +A game unlocks a trophy by posting a `_UnlockTrophy` event with an int32 `_trophy_id` property to `libSceNpUniversalDataSystem`, or through the statistics rules described below. The unlock is stored in `_trophy/<user id>_<service label>.txt` in the directory the game runs in (the one that holds `app0/`), one `<trophy id> <unlock time>` line per trophy; the time is an RTC tick, microseconds since 0001-01-01 UTC. Delete the file before starting the game to lock its trophies again. Each new unlock prints one line, such as `[trophy] PPSA01556 unlocked trophy 12 for user 268435456`; unlocking a trophy again prints nothing. `sceNpTrophy2GetTrophyInfo`, `sceNpTrophy2GetTrophyInfoArray` and `sceNpTrophy2GetGameInfo` report the stored unlocks and their times to the game. Every posted `_trophy_id` of 0 or more is stored, even one the trophy data has no trophy for or makes the platinum or a progressive trophy. The trophy data comes from the game's own `app0/sce_sys/trophy2/trophyNN.ucp`, where `NN` is the service label (`00` for most games). The trophy queries report its names, descriptions and rewards in the console language (`ANYPS5_LANGUAGE`, see above), or in English (United States) or the data's default language when it has no text in that language, and its grades, groups, hidden flags, number of trophies and grade counts. A platinum trophy unlocks as soon as every trophy that names it in `platinumTrophyId` is unlocked. The first trophy query or event prints what was read, such as `[trophy] /app0/sce_sys/trophy2/trophy00.ucp: 52 trophies, 1 group, texts in en-US`. -`APS5_TRACE_UDS=1` prints every posted event, such as `[uds] context 1 posted _UnlockTrophy {"_trophy_id":12}`, and a `[uds]` line for a missing `trophyNN.ucp`. An event that is not used prints the reason in a `[trophy] ignored` line: an `_UnlockTrophy` event without an int32 `_trophy_id` of 0 or more, or posted to a context that does not exist, and the first `_UpdateTrophyProgress` event of each trophy, because trophy progress is not recorded. +Many games unlock trophies through statistics instead: `app0/sce_sys/uds/udsNN.ucp` names, for each event, the property whose value becomes a statistic, and a trophy's `unlockCondition` compares a statistic with a target. The latest value of each statistic is stored in `_trophy/<user id>_<service label>.stats.txt`, one `<stat id> <value>` line per statistic, and is kept across runs; delete it together with the unlock file to start over. A progressive trophy reports its statistic as progress. Only statistics that keep the latest value (`"aggregation":"latest"`) as `uint32` or `uint64`, and rules that copy one property of an event chosen by its name (`$.name` or `$.object.name`), are applied; the others are listed in `[trophy]` lines when the file is read. -A missing or malformed `trophyNN.ucp` makes the queries report placeholders, and a malformed one prints the reason once. Icons are not read: `sceNpTrophy2GetGameIcon`, `sceNpTrophy2GetGroupIcon`, `sceNpTrophy2GetTrophyIcon` and `sceNpTrophy2GetRewardIcon` stop the game with an error. So does an unlock file in `_trophy/` that is damaged (a line that is not a new `<id> <value>` pair, such as a line an edit left blank or ended with a carriage return) or cannot be read or written. No notification is shown, and callbacks registered with `sceNpTrophy2RegisterUnlockCallback` are not called. +`APS5_TRACE_UDS=1` prints every posted event, such as `[uds] context 1 posted _UnlockTrophy {"_trophy_id":12}`, every statistic an event sets, such as `[uds] stat 10 of user 268435456 is 10 (COLLECT $.counter)`, and a `[uds]` line for a missing `trophyNN.ucp` or `udsNN.ucp`. An event that is not used prints the reason in a `[trophy] ignored` line: an `_UnlockTrophy` event without an int32 `_trophy_id` of 0 or more, or posted to a context that does not exist; the first `_UpdateTrophyProgress` event of each trophy, because that event is not interpreted; and, once per rule, an event whose property is missing, negative, not an integer or outside the statistic's range. + +A missing or malformed `trophyNN.ucp` or `udsNN.ucp` turns off only what it provides, and a malformed one prints the reason once: without trophy data the queries report placeholders and statistics unlock nothing, and without statistics rules events change no statistic. Icons are not read: `sceNpTrophy2GetGameIcon`, `sceNpTrophy2GetGroupIcon`, `sceNpTrophy2GetTrophyIcon` and `sceNpTrophy2GetRewardIcon` stop the game with an error. So does an unlock or statistics file in `_trophy/` that is damaged (a line that is not a new `<id> <value>` pair, such as a line an edit left blank or ended with a carriage return) or cannot be read or written. No notification is shown, and callbacks registered with `sceNpTrophy2RegisterUnlockCallback` are not called. ### PlayGo dumps without metadata From e9611f13ae69cf7638df1fe1be09fb18b5e8fe6c Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Sat, 17 Oct 2026 09:37:49 +0000 Subject: [PATCH 0911/1000] test(libSceNpUniversalDataSystem): check that a stat with a sum aggregation unlocks nothing A title whose uds file defines a stat with an aggregation other than latest gets one log line naming the stat. Posting an event for it through the exports stores no stat and unlocks no trophy. --- core/libs/tests/GuestNpTrophyStats.cpp | 59 ++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/core/libs/tests/GuestNpTrophyStats.cpp b/core/libs/tests/GuestNpTrophyStats.cpp index 2464aa36fc..a8e94f7516 100644 --- a/core/libs/tests/GuestNpTrophyStats.cpp +++ b/core/libs/tests/GuestNpTrophyStats.cpp @@ -12,6 +12,11 @@ #include <string> #include <utility> #include <vector> +#ifdef _WIN32 +#include <io.h> +#else +#include <unistd.h> +#endif struct NpUniversalDataSystemEvent; struct NpUniversalDataSystemEventPropertyObject; @@ -58,6 +63,8 @@ static const char StatsDefinition[] = R"({"statDefinitionArray":[ {"statId":10,"dataType":"uint64","aggregation":"latest"},{"statId":20,"dataType":"uint64","aggregation":"latest"}, {"statId":30,"dataType":"uint32","aggregation":"latest"},{"statId":40,"dataType":"uint32","aggregation":"latest"}]})"; +static const char SumDefinition[] = R"({"statDefinitionArray":[{"statId":10,"dataType":"uint64","aggregation":"sum"}]})"; + static const char StatsExtraction[] = R"({"statsExtractionRuleArray":[ {"condition":{"eventName":"COLLECT"},"action":{"input":"$.counter","output":{"statId":10}}}, {"condition":{"eventName":"COLLECT"},"action":{"input":"$.total","output":{"statId":20}}}, @@ -121,6 +128,43 @@ static bool Exists(const char* path) { return true; } +class StderrCapture { +public: + StderrCapture() { + std::fflush(stderr); + file = std::tmpfile(); + Require(file != nullptr, "cannot open a temporary file for stderr"); +#ifdef _WIN32 + saved = _dup(_fileno(stderr)); + Require(saved >= 0 && _dup2(_fileno(file), _fileno(stderr)) == 0, "cannot capture stderr"); +#else + saved = dup(fileno(stderr)); + Require(saved >= 0 && dup2(fileno(file), fileno(stderr)) == fileno(stderr), "cannot capture stderr"); +#endif + } + std::string Finish() { + std::fflush(stderr); +#ifdef _WIN32 + _dup2(saved, _fileno(stderr)); + _close(saved); +#else + dup2(saved, fileno(stderr)); + close(saved); +#endif + std::rewind(file); + std::string text; + char buffer[4096]; + for (std::size_t count; (count = std::fread(buffer, 1, sizeof(buffer), file)) != 0;) text.append(buffer, count); + std::fclose(file); + std::fwrite(text.data(), 1, text.size(), stderr); + return text; + } + +private: + std::FILE* file = nullptr; + int saved = -1; +}; + struct Uds { int context = 0; int handle = 0; @@ -275,6 +319,18 @@ static void IgnoreMissingAndMalformedFiles(Unlocks& unlocks) { Require(unlocks.trophies.size() == 6, "a missing or malformed file unlocked a trophy"); } +static void IgnoreOtherAggregations(Unlocks& unlocks) { + StderrCapture capture; + const auto context = TrophyContext(5); + Post(OpenUds(5), "COLLECT", [](NpUniversalDataSystemEventPropertyObject* properties) { + Require(sceNpUniversalDataSystemEventPropertyObjectSetUInt32(properties, "counter", 10) == 0, "counter was not set"); + }); + const auto log = capture.Finish(); + Require(log.find("[trophy] /app0/sce_sys/uds/uds05.ucp: stat 10 has aggregation \"sum\"; only \"latest\" is supported") != std::string::npos, + "a stat with a sum aggregation was not reported"); + Require(!Trophy(context, 1).unlocked && !Exists("_trophy/268435456_5.stats.txt") && unlocks.trophies.size() == 6, "a stat with a sum aggregation was stored or unlocked a trophy"); +} + int main() { const auto previous = std::filesystem::current_path(); const auto root = std::filesystem::temp_directory_path() / ("anyps5-trophy-stats-" + std::to_string(std::random_device{}())); @@ -289,6 +345,8 @@ int main() { Write("app0/sce_sys/uds/uds02.ucp", rules); Write("app0/sce_sys/trophy2/trophy03.ucp", trophies); Write("app0/sce_sys/uds/uds03.ucp", rules.substr(0, rules.size() - 1)); + Write("app0/sce_sys/trophy2/trophy05.ucp", trophies); + Write("app0/sce_sys/uds/uds05.ucp", Ucp({{"stats_definition.json", SumDefinition}, {"stats_extraction.json", StatsExtraction}})); Write("_trophy/268435456_0.stats.txt", "40 2\n"); Unlocks unlocks; @@ -297,6 +355,7 @@ int main() { KeepStatsAcrossRuns(context); UnlockThroughStats(context, unlocks); IgnoreMissingAndMalformedFiles(unlocks); + IgnoreOtherAggregations(unlocks); NpTrophy2RemoveUnlockListener_nid_no_patch(listener); std::filesystem::current_path(previous); From a8dd45faa3961654a2f6b796092e14c6435ee169 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Sat, 17 Oct 2026 09:38:06 +0000 Subject: [PATCH 0912/1000] fix(libSceNpTrophy2): refuse a posted unlock of an unknown, platinum or progressive trophy With the title's trophy data loaded, an _UnlockTrophy posted through UDS no longer records an id the data does not have, the platinum, or a progressive trophy. Each refusal logs the trophy and the reason, returns false and calls no listener. The statistics path and the platinum award now use an internal record that keeps part 1's behaviour, so a progressive trophy still unlocks through its statistic and the platinum with the last trophy that names it. Without trophy data every id of 0 or more is recorded as before. --- .../libSceNpTrophy2/include/TitleTrophies.hpp | 1 + .../libSceNpTrophy2/include/TrophyStore.hpp | 1 + .../prx/libSceNpTrophy2/src/TitleTrophies.cpp | 10 +- .../prx/libSceNpTrophy2/src/TrophyStore.cpp | 30 +++-- .../prx/libSceNpTrophy2/tests/TrophyStore.cpp | 109 ++++++++++++++++++ core/libs/tests/GuestNpTrophyData.cpp | 15 +-- 6 files changed, 148 insertions(+), 18 deletions(-) diff --git a/core/libs/prx/libSceNpTrophy2/include/TitleTrophies.hpp b/core/libs/prx/libSceNpTrophy2/include/TitleTrophies.hpp index cfea54f627..9d4653450c 100644 --- a/core/libs/prx/libSceNpTrophy2/include/TitleTrophies.hpp +++ b/core/libs/prx/libSceNpTrophy2/include/TitleTrophies.hpp @@ -53,6 +53,7 @@ struct StatChanges { StatChanges ApplyStatRules(const StatRules& rules, const NpTrophy2Event& event, StatStore& stats, std::int32_t userId, std::uint32_t serviceLabel); std::vector<std::int32_t> ReachedTrophies(const TrophyPackage& package, const std::map<std::uint64_t, std::uint64_t>& stats, const UnlockedQuery& unlocked); std::optional<std::int32_t> PlatinumToUnlock(const TrophyPackage& package, std::int32_t trophyId, const UnlockedQuery& unlocked); +std::optional<std::string> PostedUnlockRefusal(const TrophyPackage& package, std::int32_t trophyId); std::optional<std::uint64_t> ProgressTarget(const TrophyDefinition& trophy); std::uint64_t TrophyProgress(const TrophyDefinition& trophy, std::optional<std::uint64_t> stat, bool unlocked); diff --git a/core/libs/prx/libSceNpTrophy2/include/TrophyStore.hpp b/core/libs/prx/libSceNpTrophy2/include/TrophyStore.hpp index 1418d70b58..d9ab341ae0 100644 --- a/core/libs/prx/libSceNpTrophy2/include/TrophyStore.hpp +++ b/core/libs/prx/libSceNpTrophy2/include/TrophyStore.hpp @@ -94,6 +94,7 @@ void DestroyContext(int context); std::optional<ContextOwner> FindContext(int context); std::optional<std::uint64_t> ContextUnlockTick(int context, std::int32_t trophyId); std::size_t ContextUnlockCount(int context); +bool RecordUnlock(std::int32_t userId, std::uint32_t serviceLabel, std::int32_t trophyId); } diff --git a/core/libs/prx/libSceNpTrophy2/src/TitleTrophies.cpp b/core/libs/prx/libSceNpTrophy2/src/TitleTrophies.cpp index 342f6ea8bb..813fa576f7 100644 --- a/core/libs/prx/libSceNpTrophy2/src/TitleTrophies.cpp +++ b/core/libs/prx/libSceNpTrophy2/src/TitleTrophies.cpp @@ -179,6 +179,14 @@ std::optional<std::int32_t> PlatinumToUnlock(const TrophyPackage& package, std:: return platinumId; } +std::optional<std::string> PostedUnlockRefusal(const TrophyPackage& package, std::int32_t trophyId) { + const auto trophy = package.trophies.find(trophyId); + if (trophy == package.trophies.end()) return "the title's trophy data has no such trophy"; + if (trophy->second.grade == GradePlatinum) return "it is the platinum, which unlocks with the last trophy that names it"; + if (trophy->second.progressive) return "it is progressive and unlocks through its statistic"; + return std::nullopt; +} + std::optional<std::uint64_t> ProgressTarget(const TrophyDefinition& trophy) { if (!trophy.progressive) return std::nullopt; return trophy.target; @@ -228,5 +236,5 @@ extern "C" void NpTrophy2ApplyEvent_nid_no_patch(std::int32_t userId, std::uint3 if (changes.updates.empty() || !title.package) return; auto stats = NpTrophy2::TitleStats().Values(userId, serviceLabel); for (const auto& update : changes.updates) stats[update.statId] = update.value; - for (const auto trophyId : NpTrophy2::ReachedTrophies(*title.package, stats, NpTrophy2::UnlockedBy(userId, serviceLabel))) NpTrophy2RecordUnlock_nid_no_patch(userId, serviceLabel, trophyId); + for (const auto trophyId : NpTrophy2::ReachedTrophies(*title.package, stats, NpTrophy2::UnlockedBy(userId, serviceLabel))) NpTrophy2::RecordUnlock(userId, serviceLabel, trophyId); } diff --git a/core/libs/prx/libSceNpTrophy2/src/TrophyStore.cpp b/core/libs/prx/libSceNpTrophy2/src/TrophyStore.cpp index 27f753283c..b19804fe10 100644 --- a/core/libs/prx/libSceNpTrophy2/src/TrophyStore.cpp +++ b/core/libs/prx/libSceNpTrophy2/src/TrophyStore.cpp @@ -206,30 +206,40 @@ std::size_t ContextUnlockCount(int context) { return owner ? TitleStore().UnlockCount(owner->userId, owner->serviceLabel) : 0; } -} - -extern "C" bool NpTrophy2RecordUnlock_nid_no_patch(std::int32_t userId, std::uint32_t serviceLabel, std::int32_t trophyId) { +bool RecordUnlock(std::int32_t userId, std::uint32_t serviceLabel, std::int32_t trophyId) { const auto title = GetAppTitleId_nid_postfix(); const std::string_view titleId(title.value, static_cast<std::size_t>(std::find(std::begin(title.value), std::end(title.value), '\0') - std::begin(title.value))); - const NpTrophy2Unlock unlock{std::string(titleId), userId, serviceLabel, trophyId, NpTrophy2::CurrentTick()}; - if (!NpTrophy2::TitleStore().Unlock(userId, serviceLabel, trophyId, unlock.tick)) return false; + const NpTrophy2Unlock unlock{std::string(titleId), userId, serviceLabel, trophyId, CurrentTick()}; + if (!TitleStore().Unlock(userId, serviceLabel, trophyId, unlock.tick)) return false; std::fprintf(stderr, "[trophy] %s unlocked trophy %d for user %d\n", unlock.titleId.c_str(), trophyId, userId); - auto& registry = NpTrophy2::listeners(); + auto& registry = listeners(); std::lock_guard dispatching(registry.dispatch); - std::vector<NpTrophy2::Listener> current; + std::vector<Listener> current; { std::lock_guard lock(registry.mutex); current = registry.entries; } for (const auto& entry : current) { - if (NpTrophy2::registered(registry, entry.id)) entry.callback(entry.context, unlock); + if (registered(registry, entry.id)) entry.callback(entry.context, unlock); } - const auto* package = NpTrophy2::Package(serviceLabel); + const auto* package = Package(serviceLabel); if (package == nullptr) return true; - if (const auto platinum = NpTrophy2::PlatinumToUnlock(*package, trophyId, NpTrophy2::UnlockedBy(userId, serviceLabel))) NpTrophy2RecordUnlock_nid_no_patch(userId, serviceLabel, *platinum); + if (const auto platinum = PlatinumToUnlock(*package, trophyId, UnlockedBy(userId, serviceLabel))) RecordUnlock(userId, serviceLabel, *platinum); return true; } +} + +extern "C" bool NpTrophy2RecordUnlock_nid_no_patch(std::int32_t userId, std::uint32_t serviceLabel, std::int32_t trophyId) { + if (const auto* package = NpTrophy2::Package(serviceLabel)) { + if (const auto refusal = NpTrophy2::PostedUnlockRefusal(*package, trophyId)) { + std::fprintf(stderr, "[trophy] ignored the posted unlock of trophy %d for user %d: %s\n", trophyId, userId, refusal->c_str()); + return false; + } + } + return NpTrophy2::RecordUnlock(userId, serviceLabel, trophyId); +} + extern "C" std::uint64_t NpTrophy2AddUnlockListener_nid_no_patch(NpTrophy2UnlockListener listener, void* context) { if (listener == nullptr) { APS5_INVALID_ARG_EX; diff --git a/core/libs/prx/libSceNpTrophy2/tests/TrophyStore.cpp b/core/libs/prx/libSceNpTrophy2/tests/TrophyStore.cpp index 6a29e7e9c8..10352c9edf 100644 --- a/core/libs/prx/libSceNpTrophy2/tests/TrophyStore.cpp +++ b/core/libs/prx/libSceNpTrophy2/tests/TrophyStore.cpp @@ -1,4 +1,5 @@ #include "prx/libSceNpTrophy2/include/NpTrophy2Types.hpp" +#include "prx/libSceNpTrophy2/include/TitleTrophies.hpp" #include "prx/libSceNpTrophy2/include/TrophyStore.hpp" #include "prx/libc/include/general/VabiMacros.hpp" @@ -14,7 +15,13 @@ #include <stdexcept> #include <string> #include <thread> +#include <utility> #include <vector> +#ifdef _WIN32 +#include <io.h> +#else +#include <unistd.h> +#endif extern "C" { int APS5_VABI sceNpTrophy2CreateContext(int*, int, uint32_t, uint64_t); @@ -61,6 +68,69 @@ void write(const char* path, const std::string& text) { check(std::fclose(file) == 0 && written, "test file was not written"); } +void putBigEndian(std::string& data, std::size_t offset, std::uint64_t value, std::size_t size) { + for (std::size_t i = 0; i < size; ++i) data[offset + i] = static_cast<char>((value >> (8 * (size - 1 - i))) & 0xFF); +} + +std::string ucp(const std::vector<std::pair<std::string, std::string>>& files) { + std::size_t offset = 0x60 + files.size() * 0x40; + std::size_t size = offset; + for (const auto& file : files) size += file.second.size(); + std::string data(size, '\0'); + putBigEndian(data, 0, 0xB228C60A, 4); + putBigEndian(data, 4, 1, 4); + putBigEndian(data, 8, size, 8); + putBigEndian(data, 0x10, files.size(), 4); + putBigEndian(data, 0x14, 0x40, 4); + std::size_t entry = 0x60; + for (const auto& [name, contents] : files) { + data.replace(entry, name.size(), name); + putBigEndian(data, entry + 0x20, offset, 8); + putBigEndian(data, entry + 0x28, contents.size(), 8); + data.replace(offset, contents.size(), contents); + offset += contents.size(); + entry += 0x40; + } + return data; +} + +class StderrCapture { +public: + StderrCapture() { + std::fflush(stderr); + file = std::tmpfile(); + check(file != nullptr, "cannot open a temporary file for stderr"); +#ifdef _WIN32 + saved = _dup(_fileno(stderr)); + check(saved >= 0 && _dup2(_fileno(file), _fileno(stderr)) == 0, "cannot capture stderr"); +#else + saved = dup(fileno(stderr)); + check(saved >= 0 && dup2(fileno(file), fileno(stderr)) == fileno(stderr), "cannot capture stderr"); +#endif + } + std::string Finish() { + std::fflush(stderr); +#ifdef _WIN32 + _dup2(saved, _fileno(stderr)); + _close(saved); +#else + dup2(saved, fileno(stderr)); + close(saved); +#endif + std::rewind(file); + std::string text; + char buffer[4096]; + for (std::size_t count; (count = std::fread(buffer, 1, sizeof(buffer), file)) != 0;) text.append(buffer, count); + std::fclose(file); + std::fwrite(text.data(), 1, text.size(), stderr); + return text; + } + +private: + std::FILE* file = nullptr; + int saved = -1; +}; + void testPersistence() { const std::filesystem::path directory("store"); { @@ -176,6 +246,44 @@ void testQueries() { check(sceNpTrophy2DestroyContext(context) == 0 && sceNpTrophy2DestroyContext(other) == 0, "contexts were not destroyed"); } + +void testPostedUnlockRules() { + std::filesystem::create_directories("app0/sce_sys/trophy2"); + std::filesystem::create_directories("app0/sce_sys/uds"); + write("app0/sce_sys/trophy2/trophy04.ucp", ucp({{"tropconf.json", R"({"defaultLanguage":"en-US","trophies":[ + {"id":"0","grade":"P"}, + {"id":"1","grade":"G","platinumTrophyId":"0"}, + {"id":"2","grade":"B","platinumTrophyId":"0","unlockCondition":{"udsStatId":"40","comparator":"ge","targetValue":"5","progressive":true}}, + {"id":"3","grade":"B"}]})"}, + {"tropmeta_en-US.json", R"({"metadata":{"titleMetadata":{"name":"Rules"},"trophyMetadata":[]}})"}})); + write("app0/sce_sys/uds/uds04.ucp", ucp({{"stats_definition.json", R"({"statDefinitionArray":[{"statId":40,"dataType":"uint32","aggregation":"latest"}]})"}, + {"stats_extraction.json", R"({"statsExtractionRuleArray":[{"condition":{"eventName":"STEPS"},"action":{"input":"$.steps","output":{"statId":40}}}]})"}})); + check(NpTrophy2::Package(4) != nullptr, "the trophy data of service label 4 was not loaded"); + Seen seen; + const auto listener = NpTrophy2AddUnlockListener_nid_no_patch(remember, &seen); + StderrCapture capture; + const bool unknown = NpTrophy2RecordUnlock_nid_no_patch(InitialUser, 4, 9); + const bool platinum = NpTrophy2RecordUnlock_nid_no_patch(InitialUser, 4, 0); + const bool progressive = NpTrophy2RecordUnlock_nid_no_patch(InitialUser, 4, 2); + const auto log = capture.Finish(); + check(!unknown && !platinum && !progressive, "an unknown, platinum or progressive trophy posted through UDS was recorded"); + check(NpTrophy2::TitleStore().UnlockCount(InitialUser, 4) == 0 && seen.unlocks.empty(), "a refused unlock reached the record or a listener"); + for (const char* line : {"[trophy] ignored the posted unlock of trophy 9 for user 268435456: the title's trophy data has no such trophy\n", + "[trophy] ignored the posted unlock of trophy 0 for user 268435456: it is the platinum, which unlocks with the last trophy that names it\n", + "[trophy] ignored the posted unlock of trophy 2 for user 268435456: it is progressive and unlocks through its statistic\n"}) { + check(log.find(line) != std::string::npos, "a refused unlock did not log its trophy and reason"); + } + check(NpTrophy2RecordUnlock_nid_no_patch(InitialUser, 4, 1) && NpTrophy2RecordUnlock_nid_no_patch(InitialUser, 4, 3), "an ordinary trophy posted through UDS was not recorded"); + check(!NpTrophy2::TitleStore().UnlockTick(InitialUser, 4, 0), "the platinum unlocked before every trophy that names it"); + NpTrophy2ApplyEvent_nid_no_patch(InitialUser, 4, NpTrophy2Event{"STEPS", {{"steps", 5, "5"}}}); + check(NpTrophy2::TitleStore().UnlockTick(InitialUser, 4, 2).has_value(), "the progressive trophy did not unlock through its statistic"); + check(NpTrophy2::TitleStore().UnlockTick(InitialUser, 4, 0).has_value(), "the platinum did not unlock with the last trophy that names it"); + NpTrophy2RemoveUnlockListener_nid_no_patch(listener); + std::vector<std::int32_t> seenIds; + for (const auto& unlock : seen.unlocks) seenIds.push_back(unlock.trophyId); + check(seenIds == std::vector<std::int32_t>{1, 3, 2, 0}, "the listener did not see the posted, statistic and platinum unlocks once, in order"); +} + } int main() { @@ -187,6 +295,7 @@ int main() { testPersistence(); testDamagedRecords(); testQueries(); + testPostedUnlockRules(); testListenerRemovalWaitsForItsCallback(); std::filesystem::current_path(previous); std::filesystem::remove_all(root); diff --git a/core/libs/tests/GuestNpTrophyData.cpp b/core/libs/tests/GuestNpTrophyData.cpp index eabbe68eae..5c4eaf025a 100644 --- a/core/libs/tests/GuestNpTrophyData.cpp +++ b/core/libs/tests/GuestNpTrophyData.cpp @@ -193,7 +193,7 @@ static void ReportTrophyData(int context) { "the group list does not hold the base game and group 1"); } -static void UnlockPlatinum(int context) { +static void PostUnlocks(int context) { Unlocks unlocks; const auto listener = NpTrophy2AddUnlockListener_nid_no_patch(Remember, &unlocks); for (const int trophyId : {1, 2, 3, 6}) PostUnlock(trophyId); @@ -203,12 +203,13 @@ static void UnlockPlatinum(int context) { "the game does not count the unlocks by grade"); PostUnlock(4); Require(!Trophy(context, 0).unlocked, "the platinum unlocked before every trophy that names it"); - PostUnlock(5); - const auto progressive = Trophy(context, 5); - Require(progressive.unlocked && progressive.progress.value == 5, "an unlocked progressive trophy does not report its target as progress"); - Require(Trophy(context, 0).unlocked && Game(context).unlocked_platinum == 1, "the platinum did not unlock with the last trophy that names it"); + for (const int trophyId : {5, 0, 42}) PostUnlock(trophyId); + const auto refused = Game(context); + Require(!Trophy(context, 5).unlocked && !Trophy(context, 0).unlocked && refused.unlocked_trophies == 5 && refused.unlocked_platinum == 0 && refused.unlocked_bronze == 3 && + refused.progress_percentage == (6 + 2 + 3) * 100 / (6 + 2 + 5), + "a progressive, platinum or unknown trophy posted through UDS unlocked, or changed the counts"); NpTrophy2RemoveUnlockListener_nid_no_patch(listener); - Require(unlocks.trophies == (std::vector<std::int32_t>{1, 2, 3, 6, 4, 5, 0}), "the listener did not see each unlock and the platinum once, in order"); + Require(unlocks.trophies == (std::vector<std::int32_t>{1, 2, 3, 6, 4}), "the listener did not see each posted unlock once, in order, or saw a refused one"); } static void ReportPlaceholders() { @@ -237,7 +238,7 @@ int main() { const auto context = TrophyContext(0); ReportTrophyData(context); - UnlockPlatinum(context); + PostUnlocks(context); ReportPlaceholders(); std::filesystem::current_path(previous); From be9ade0f01ac3970e61bb18164acf11922c43910 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Sat, 17 Oct 2026 09:38:18 +0000 Subject: [PATCH 0913/1000] docs: say which posted trophy unlocks are refused and what stops a title USAGE.md describes the refused posted unlocks and their log line, and says that the icon queries and a damaged or unwritable unlock or statistics file stop the game with an error. The debt entries of libSceNpTrophy2 and libSceNpUniversalDataSystem no longer say that any posted id is stored. Both USAGE.md and the libSceNpTrophy2 entry say that without trophy data the platinum is not unlocked when the other trophies are. --- docs/dev/debt/libSceNpTrophy2.md | 2 +- docs/dev/debt/libSceNpUniversalDataSystem.md | 2 +- docs/user/USAGE.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/dev/debt/libSceNpTrophy2.md b/docs/dev/debt/libSceNpTrophy2.md index dbf8da133f..7cb85f5310 100644 --- a/docs/dev/debt/libSceNpTrophy2.md +++ b/docs/dev/debt/libSceNpTrophy2.md @@ -15,4 +15,4 @@ - [Trophy queries](../../../core/libs/prx/libSceNpTrophy2/src/TrophyInfo.cpp) report the title's [trophy data](../../../core/libs/prx/libSceNpTrophy2/src/TrophyPackage.cpp) (`sce_sys/trophy2/trophyNN.ucp`) and the unlocks in the [trophy store](../../../core/libs/prx/libSceNpTrophy2/src/TrophyStore.cpp). Without the file, or when it is malformed, they report the old placeholders and `sceNpTrophy2GetTrophyInfoArray` lists only trophy 0. Icons are not read. Hidden trophies report their names and descriptions; whether the console blanks them before the unlock is not known. `progress_percentage` weighs gold 6, silver 2 and bronze 1 and leaves the platinum out, as KytyPS5 does (the ratio of the published PSN trophy points); the console's formula is not known. A trophy or group that is not in the file throws. A context that was not created or was destroyed reports placeholders and no unlocks and returns success; the console's errors for it are unknown. - [Trophy data limits](../../../core/libs/prx/libSceNpTrophy2/src/TrophyPackage.cpp): a `trophyNN.ucp` or `udsNN.ucp` larger than 256 MiB or listing more than 4096 files, and a JSON file in it that nests deeper than 64 levels or holds more than 1048576 values, are not used, and the title runs as without that file. The limits keep the memory the reader takes to the file and about 140 MiB of parsed JSON; the console's limits are not known. - [Text language](../../../core/libs/prx/libSceNpTrophy2/src/TrophyPackage.cpp): texts come from `tropmeta_<tag>.json` for the console language, then `en-US`, then the file's `defaultLanguage`. The tags for languages 0 to 29 are KytyPS5's; `uk-UA` for 30 (Ukrainian) is an assumption. -- [Platinum](../../../core/libs/prx/libSceNpTrophy2/src/TitleTrophies.cpp): an unlock through any path unlocks the platinum that the trophy names in `platinumTrophyId` once every trophy naming that platinum is unlocked, as KytyPS5 does; trophies without `platinumTrophyId` are not required. A platinum or a progressive trophy can still be unlocked directly with `_UnlockTrophy`, which KytyPS5 refuses; the console's behaviour is not known. Without trophy data any id of 0 or more is stored, and the platinum is not unlocked when the trophies it needs are. +- [Platinum](../../../core/libs/prx/libSceNpTrophy2/src/TitleTrophies.cpp): an unlock through any path unlocks the platinum that the trophy names in `platinumTrophyId` once every trophy naming that platinum is unlocked, as KytyPS5 does; trophies without `platinumTrophyId` are not required. With the title's trophy data loaded, a posted `_UnlockTrophy` of a trophy that is not in the data, of a platinum or of a progressive trophy is not stored and prints a `[trophy] ignored` line, as KytyPS5 refuses them, so a progressive trophy unlocks only through its statistic; without trophy data any id of 0 or more is stored, including ids the data would make the platinum, a progressive trophy or no trophy, and the platinum is not unlocked when the trophies it needs are. The console's behaviour is not known. diff --git a/docs/dev/debt/libSceNpUniversalDataSystem.md b/docs/dev/debt/libSceNpUniversalDataSystem.md index 5dc32b7964..6c8a996278 100644 --- a/docs/dev/debt/libSceNpUniversalDataSystem.md +++ b/docs/dev/debt/libSceNpUniversalDataSystem.md @@ -6,5 +6,5 @@ ### Functional -- [Trophy events](../../../core/libs/prx/libSceNpUniversalDataSystem/src/Event.cpp): `sceNpUniversalDataSystemPostEvent` stores an `_UnlockTrophy` event with an int32 `_trophy_id` of 0 or more as an unlock for the user and service label of its context, as KytyPS5 does. `_UpdateTrophyProgress`, named only by a third-party unlocker, is logged once per trophy and not interpreted. A context need not be registered, and every post of an event that `sceNpUniversalDataSystemCreateEvent` returned and that is not yet destroyed returns success (a null or any other event pointer stops the game): KytyPS5's errors for an unknown or unregistered context and for a malformed unlock are not verified. The storage statistics stay zero. +- [Trophy events](../../../core/libs/prx/libSceNpUniversalDataSystem/src/Event.cpp): `sceNpUniversalDataSystemPostEvent` stores an `_UnlockTrophy` event with an int32 `_trophy_id` of 0 or more as an unlock for the user and service label of its context, as KytyPS5 does, except an id that [libSceNpTrophy2](../../../core/libs/prx/libSceNpTrophy2/src/TrophyStore.cpp) refuses when the title's trophy data is loaded (one not in the data, a platinum or a progressive trophy). `_UpdateTrophyProgress`, named only by a third-party unlocker, is logged once per trophy and not interpreted. A context need not be registered, and every post of an event that `sceNpUniversalDataSystemCreateEvent` returned and that is not yet destroyed returns success (a null or any other event pointer stops the game): KytyPS5's errors for an unknown or unregistered context and for a malformed unlock are not verified. The storage statistics stay zero. - [Statistics](../../../core/libs/prx/libSceNpUniversalDataSystem/src/Event.cpp): every other event goes through the title's rules in `sce_sys/uds/udsNN.ucp`, which [libSceNpTrophy2](../../../core/libs/prx/libSceNpTrophy2/src/TitleTrophies.cpp) reads. Only statistics with `"aggregation":"latest"` and `dataType` `uint32` or `uint64` are kept, as in KytyPS5; no public source documents the other aggregations or types, so their definitions and rules are skipped. Only rules whose condition is an `eventName` alone and whose input is a `$.name` path (dotted for nested objects) are applied. A value is used when it is an integer of any width that is not negative and fits the statistic's type and `minValue`/`maxValue`; KytyPS5 requires the statistic's exact width, and whether the console converts, clamps or rejects other values is not known. Statistics are stored per user and service label, and every trophy's `unlockCondition` is compared after each event that sets one. diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index 2686db9e09..83c37d8b6a 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -159,7 +159,7 @@ Games ask for the console's language with `sceSystemServiceParamGetInt` and show ### Trophies -A game unlocks a trophy by posting a `_UnlockTrophy` event with an int32 `_trophy_id` property to `libSceNpUniversalDataSystem`, or through the statistics rules described below. The unlock is stored in `_trophy/<user id>_<service label>.txt` in the directory the game runs in (the one that holds `app0/`), one `<trophy id> <unlock time>` line per trophy; the time is an RTC tick, microseconds since 0001-01-01 UTC. Delete the file before starting the game to lock its trophies again. Each new unlock prints one line, such as `[trophy] PPSA01556 unlocked trophy 12 for user 268435456`; unlocking a trophy again prints nothing. `sceNpTrophy2GetTrophyInfo`, `sceNpTrophy2GetTrophyInfoArray` and `sceNpTrophy2GetGameInfo` report the stored unlocks and their times to the game. Every posted `_trophy_id` of 0 or more is stored, even one the trophy data has no trophy for or makes the platinum or a progressive trophy. +A game unlocks a trophy by posting a `_UnlockTrophy` event with an int32 `_trophy_id` property to `libSceNpUniversalDataSystem`, or through the statistics rules described below. The unlock is stored in `_trophy/<user id>_<service label>.txt` in the directory the game runs in (the one that holds `app0/`), one `<trophy id> <unlock time>` line per trophy; the time is an RTC tick, microseconds since 0001-01-01 UTC. Delete the file before starting the game to lock its trophies again. Each new unlock prints one line, such as `[trophy] PPSA01556 unlocked trophy 12 for user 268435456`; unlocking a trophy again prints nothing. `sceNpTrophy2GetTrophyInfo`, `sceNpTrophy2GetTrophyInfoArray` and `sceNpTrophy2GetGameInfo` report the stored unlocks and their times to the game. When the game's trophy data is loaded (see below), a posted unlock of a trophy that the data does not have, of the platinum, or of a progressive trophy is not stored and prints the reason, such as `[trophy] ignored the posted unlock of trophy 0 for user 268435456: it is the platinum, which unlocks with the last trophy that names it`; the platinum unlocks with the last trophy that names it, and a progressive trophy through its statistic. Without trophy data every posted id of 0 or more is stored, and the platinum is not unlocked when the other trophies are. The trophy data comes from the game's own `app0/sce_sys/trophy2/trophyNN.ucp`, where `NN` is the service label (`00` for most games). The trophy queries report its names, descriptions and rewards in the console language (`ANYPS5_LANGUAGE`, see above), or in English (United States) or the data's default language when it has no text in that language, and its grades, groups, hidden flags, number of trophies and grade counts. A platinum trophy unlocks as soon as every trophy that names it in `platinumTrophyId` is unlocked. The first trophy query or event prints what was read, such as `[trophy] /app0/sce_sys/trophy2/trophy00.ucp: 52 trophies, 1 group, texts in en-US`. From 8e927be27f54ce786217d1c0a46c4b85669c5cc0 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Sat, 17 Oct 2026 23:30:58 +0000 Subject: [PATCH 0914/1000] docs: say that a malformed trophy or stat record line fails the query --- docs/dev/debt/libSceNpTrophy2.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/dev/debt/libSceNpTrophy2.md b/docs/dev/debt/libSceNpTrophy2.md index 7cb85f5310..33aa1abb92 100644 --- a/docs/dev/debt/libSceNpTrophy2.md +++ b/docs/dev/debt/libSceNpTrophy2.md @@ -16,3 +16,4 @@ - [Trophy data limits](../../../core/libs/prx/libSceNpTrophy2/src/TrophyPackage.cpp): a `trophyNN.ucp` or `udsNN.ucp` larger than 256 MiB or listing more than 4096 files, and a JSON file in it that nests deeper than 64 levels or holds more than 1048576 values, are not used, and the title runs as without that file. The limits keep the memory the reader takes to the file and about 140 MiB of parsed JSON; the console's limits are not known. - [Text language](../../../core/libs/prx/libSceNpTrophy2/src/TrophyPackage.cpp): texts come from `tropmeta_<tag>.json` for the console language, then `en-US`, then the file's `defaultLanguage`. The tags for languages 0 to 29 are KytyPS5's; `uk-UA` for 30 (Ukrainian) is an assumption. - [Platinum](../../../core/libs/prx/libSceNpTrophy2/src/TitleTrophies.cpp): an unlock through any path unlocks the platinum that the trophy names in `platinumTrophyId` once every trophy naming that platinum is unlocked, as KytyPS5 does; trophies without `platinumTrophyId` are not required. With the title's trophy data loaded, a posted `_UnlockTrophy` of a trophy that is not in the data, of a platinum or of a progressive trophy is not stored and prints a `[trophy] ignored` line, as KytyPS5 refuses them, so a progressive trophy unlocks only through its statistic; without trophy data any id of 0 or more is stored, including ids the data would make the platinum, a progressive trophy or no trophy, and the platinum is not unlocked when the trophies it needs are. The console's behaviour is not known. +- [Damaged records](../../../core/libs/prx/libSceNpTrophy2/src/TrophyStore.cpp): a line in `_trophy/<user id>_<service label>.txt` or `.stats.txt` that is not a new `<id> <value>` pair (a blank line, a carriage return, a repeated or negative id, a value that is not a number) makes `readRecord` throw a `std::runtime_error` that names the file and the line. Every call that reads that record then throws it instead of returning, which stops the title as any throwing export does, so the title gets no return value; main returned 0 from these calls. The failed read is not kept, so each later call reads the file again and throws again until the file is fixed or deleted. The unlock record is read by `sceNpTrophy2GetTrophyInfo`, `sceNpTrophy2GetTrophyInfoArray` and `sceNpTrophy2GetGameInfo` for a created context, by `sceNpTrophy2GetGroupInfo` and `sceNpTrophy2GetGroupInfoArray` when the title's trophy data is loaded, by a `sceNpUniversalDataSystemPostEvent` of an `_UnlockTrophy` that the trophy data does not refuse, and by a statistic change when the trophy data is loaded; the stat record by a `sceNpUniversalDataSystemPostEvent` of an event that a statistics rule copies and by the trophy queries of a progressive trophy with a statistic. What the console does with a damaged record is not known. From a37797625cb7b2b2c96e44eb1fec0bc256c8858d Mon Sep 17 00:00:00 2001 From: Destroyers212x <65466724+Destroyers212x@users.noreply.github.com> Date: Sat, 17 Oct 2026 15:36:51 +0000 Subject: [PATCH 0915/1000] ci(relinker): build and test the relinker for x86-64 on macOS under Rosetta --- .github/workflows/build.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 200e825e3a..cab59565e5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -140,6 +140,7 @@ jobs: ...(full && linux ? [] : [{ os: 'ubuntu-latest', compiler: 'g++' }]), ...(full ? [] : [{ os: 'windows-latest', compiler: 'g++' }]), { os: 'macos-latest', compiler: 'clang++' }, + { os: 'macos-latest', compiler: 'clang++', architectures: 'x86_64' }, ]); if (context.eventName === 'push') { if (context.ref !== 'refs/heads/main') { @@ -314,7 +315,9 @@ jobs: 7z x "$RUNNER_TEMP/winlibs.zip" -oC:/winlibs > /dev/null - if: runner.os == 'Windows' run: echo 'C:\winlibs\mingw64\bin' >> "$GITHUB_PATH" - - run: cmake -S . -B build-relinker -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_COMPILER=${{ matrix.compiler }} -DANYPS5_RELINKER_ONLY=ON -DBUILD_TESTING=ON + - if: ${{ matrix.architectures == 'x86_64' }} + run: sudo softwareupdate --install-rosetta --agree-to-license + - run: cmake -S . -B build-relinker -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_COMPILER=${{ matrix.compiler }} -DANYPS5_RELINKER_ONLY=ON -DBUILD_TESTING=ON -DCMAKE_OSX_ARCHITECTURES=${{ matrix.architectures }} - run: cmake --build build-relinker --parallel 3 - run: ctest --test-dir build-relinker --output-on-failure --timeout 120 --parallel 4 From 4d741c26de4329c8391811dc76c3f7dadd9395ed Mon Sep 17 00:00:00 2001 From: MontyMcK <MontyMcK@users.noreply.github.com> Date: Sun, 18 Oct 2026 12:30:43 +1300 Subject: [PATCH 0916/1000] feat(libSceHmd2): report a console without a PS VR2 headset --- core/libs/prx/libSceHmd2/CMakeLists.txt | 1 + core/libs/prx/libSceHmd2/Export.cpp | 154 +++++++++++++------ core/libs/prx/libSceHmd2/Hmd2.hpp | 66 ++++++++ core/libs/tests/GuestHmd2.cpp | 124 +++++++++++++-- core/libs/tests/ProcessParametersFixture.hpp | 91 +++++++++++ docs/dev/debt/libSceHmd2.md | 12 +- docs/dev/debt/libScePad.md | 1 + 7 files changed, 382 insertions(+), 67 deletions(-) create mode 100644 core/libs/prx/libSceHmd2/Hmd2.hpp create mode 100644 core/libs/tests/ProcessParametersFixture.hpp diff --git a/core/libs/prx/libSceHmd2/CMakeLists.txt b/core/libs/prx/libSceHmd2/CMakeLists.txt index 84efff8feb..fc0c32cbf4 100644 --- a/core/libs/prx/libSceHmd2/CMakeLists.txt +++ b/core/libs/prx/libSceHmd2/CMakeLists.txt @@ -1,5 +1,6 @@ add_library(libSceHmd2 SHARED EXCLUDE_FROM_ALL Export.cpp) target_include_directories(libSceHmd2 PRIVATE ${LIBS_INCLUDE_DIR}) +target_link_libraries(libSceHmd2 PRIVATE libc) configure_windows_unwind(libSceHmd2) if(BUILD_TESTING) diff --git a/core/libs/prx/libSceHmd2/Export.cpp b/core/libs/prx/libSceHmd2/Export.cpp index e7361d9b35..b23019b0df 100644 --- a/core/libs/prx/libSceHmd2/Export.cpp +++ b/core/libs/prx/libSceHmd2/Export.cpp @@ -1,16 +1,117 @@ -#include <cstdint> -#include <cstddef> -#include "SceTypes.hpp" +#include "Hmd2.hpp" +#include "prx/libc/include/ApplicationHeap.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libSceUserService/UserService.hpp" +#include "SceTypes.hpp" + +#include <cstring> +#include <mutex> +#include <stdexcept> namespace { -constexpr std::int32_t SCE_HMD2_ERROR_UNSUPPORTED_FEATURE = static_cast<std::int32_t>(0x81110016); +constexpr std::int32_t DeviceHandle = 0x0e010000; +constexpr Hmd2::FieldOfView DefaultFieldOfView{1.84177089f, 0.947190642f, 1.32898104f, 1.32898104f}; + +std::mutex stateMutex; +bool initialized = false; +std::int32_t openHandle = 0; + +std::uint32_t CompiledSdkVersion() { + static const auto* processParameters = static_cast<const std::byte*>(ApplicationProcessParameters_nid_no_patch()); + std::uint32_t version; + std::memcpy(&version, processParameters + 0x10, sizeof(version)); + return version; +} + +std::int32_t GetDeviceInformation(Hmd2::DeviceInformation* info) { + if (!info) return Hmd2::ParameterNull; + *info = {}; + if (!initialized) return Hmd2::NotInitialized; + info->status = Hmd2::DeviceStatus::NotDetected; + if (CompiledSdkVersion() < Hmd2::UserIdSdkVersion) info->userId = -1; + info->panelWidth = 4000; + info->panelHeight = 2040; + info->latency90Hz = 5555; + info->latency120Hz = 4160; + info->virtualImageDistance = 2.0f; + return 0; +} } extern "C" { +std::int32_t APS5_VABI sceHmd2Initialize(const void* param) { + std::lock_guard lock(stateMutex); + if (initialized) return Hmd2::AlreadyInitialized; + if (!param) return Hmd2::ParameterNull; + initialized = true; + return 0; +} + +std::int32_t APS5_VABI sceHmd2Terminate() { + std::lock_guard lock(stateMutex); + if (!initialized) return Hmd2::NotInitialized; + initialized = false; + return 0; +} + +std::int32_t APS5_VABI sceHmd2Open(std::int32_t userId, std::int32_t type, std::int32_t index, const void* param) { + std::lock_guard lock(stateMutex); + if (!initialized) return Hmd2::NotInitialized; + if (type != 0 || index != 0 || param) return Hmd2::ParameterInvalid; + if (userId != USER_SERVICE_INITIAL_USER_ID) { + if (CompiledSdkVersion() >= Hmd2::UserIdSdkVersion) return Hmd2::ParameterInvalid; + throw std::runtime_error("sceHmd2Open: user is not the initial user"); + } + if (openHandle != 0) return Hmd2::AlreadyOpened; + openHandle = DeviceHandle; + return DeviceHandle; +} + +std::int32_t APS5_VABI sceHmd2Close(std::int32_t handle) { + std::lock_guard lock(stateMutex); + if (!initialized) return Hmd2::NotInitialized; + if (handle != openHandle) return Hmd2::InvalidHandle; + openHandle = 0; + return 0; +} + +std::int32_t APS5_VABI sceHmd2GetDeviceInformation(Hmd2::DeviceInformation* info) { + std::lock_guard lock(stateMutex); + return GetDeviceInformation(info); +} + +std::int32_t APS5_VABI sceHmd2GetDeviceInformationByHandle(std::int32_t handle, Hmd2::DeviceInformation* info) { + std::lock_guard lock(stateMutex); + if (handle != openHandle) return Hmd2::InvalidHandle; + return GetDeviceInformation(info); +} + +std::int32_t APS5_VABI sceHmd2GetFieldOfView(std::int32_t handle, Hmd2::FieldOfView* fieldOfView) { + std::lock_guard lock(stateMutex); + if (!fieldOfView) return Hmd2::ParameterNull; + if (handle == 0 || handle != openHandle) return Hmd2::InvalidHandle; + if (!initialized) return Hmd2::NotInitialized; + *fieldOfView = DefaultFieldOfView; + return 0; +} + +std::int32_t APS5_VABI sceHmd2GetFieldOfViewWithoutHandle(Hmd2::FieldOfView* fieldOfView) { + std::lock_guard lock(stateMutex); + if (!fieldOfView) return Hmd2::ParameterNull; + if (!initialized) return Hmd2::NotInitialized; + *fieldOfView = DefaultFieldOfView; + return 0; +} + +std::int32_t APS5_VABI sceHmd2SetVibration(std::int32_t handle, std::uint32_t level) { + (void)level; + std::lock_guard lock(stateMutex); + return handle == openHandle ? 0 : Hmd2::InvalidHandle; +} + int APS5_VABI sceHmd2ReprojectionGetStatus() { NotImplemented_nid_no_patch(__func__); return 0; @@ -21,31 +122,6 @@ int APS5_VABI sceHmd2ReprojectionQuerySeeThroughBufferSizeAlign() { return 0; } -std::int32_t APS5_VABI sceHmd2Initialize(const void* param) { - (void)param; - return SCE_HMD2_ERROR_UNSUPPORTED_FEATURE; -} - -int APS5_VABI sceHmd2GetFieldOfView() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2GetDeviceInformationByHandle() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2Terminate() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2Close() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceHmd2GazeGetResult() { NotImplemented_nid_no_patch(__func__); return 0; @@ -56,21 +132,6 @@ int APS5_VABI sceHmd2GazeGetResultForFoveatedRendering() { return 0; } -int APS5_VABI sceHmd2GetDeviceInformation() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2GetFieldOfViewWithoutHandle() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2Open() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceHmd2ReprojectionBeginFrame() { NotImplemented_nid_no_patch(__func__); return 0; @@ -161,9 +222,4 @@ int APS5_VABI sceHmd2ReprojectionTerminate() { return 0; } -int APS5_VABI sceHmd2SetVibration() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - } diff --git a/core/libs/prx/libSceHmd2/Hmd2.hpp b/core/libs/prx/libSceHmd2/Hmd2.hpp new file mode 100644 index 0000000000..3594b32bd1 --- /dev/null +++ b/core/libs/prx/libSceHmd2/Hmd2.hpp @@ -0,0 +1,66 @@ +#ifndef CORE_LIBS_PRX_LIBSCEHMD2_HMD2_HPP +#define CORE_LIBS_PRX_LIBSCEHMD2_HMD2_HPP + +#include "prx/libc/include/general/VabiMacros.hpp" + +#include <cstddef> +#include <cstdint> + +namespace Hmd2 { + +inline constexpr std::int32_t AlreadyInitialized = static_cast<std::int32_t>(0x8a720001); +inline constexpr std::int32_t NotInitialized = static_cast<std::int32_t>(0x8a720002); +inline constexpr std::int32_t InvalidHandle = static_cast<std::int32_t>(0x8a720003); +inline constexpr std::int32_t AlreadyOpened = static_cast<std::int32_t>(0x8a720006); +inline constexpr std::int32_t ParameterNull = static_cast<std::int32_t>(0x8a720008); +inline constexpr std::int32_t ParameterInvalid = static_cast<std::int32_t>(0x8a720009); + +inline constexpr std::uint32_t UserIdSdkVersion = 0x07000000; + +enum class DeviceStatus : std::uint32_t { + NotDetected = 2, +}; + +struct DeviceInformation { + DeviceStatus status; + std::int32_t userId; + std::uint32_t panelWidth; + std::uint32_t panelHeight; + std::uint16_t latency90Hz; + std::uint16_t latency120Hz; + std::uint8_t hmuMount; + std::uint8_t lensSeparationDistance; + std::uint8_t reserved[2]; + float virtualImageDistance; +}; + +struct FieldOfView { + float tanOut; + float tanIn; + float tanTop; + float tanBottom; +}; + +static_assert(sizeof(DeviceInformation) == 28); +static_assert(offsetof(DeviceInformation, userId) == 4); +static_assert(offsetof(DeviceInformation, panelWidth) == 8); +static_assert(offsetof(DeviceInformation, latency90Hz) == 16); +static_assert(offsetof(DeviceInformation, hmuMount) == 20); +static_assert(offsetof(DeviceInformation, virtualImageDistance) == 24); +static_assert(sizeof(FieldOfView) == 16); + +} + +extern "C" { +std::int32_t APS5_VABI sceHmd2Initialize(const void* param); +std::int32_t APS5_VABI sceHmd2Terminate(); +std::int32_t APS5_VABI sceHmd2Open(std::int32_t userId, std::int32_t type, std::int32_t index, const void* param); +std::int32_t APS5_VABI sceHmd2Close(std::int32_t handle); +std::int32_t APS5_VABI sceHmd2GetDeviceInformation(Hmd2::DeviceInformation* info); +std::int32_t APS5_VABI sceHmd2GetDeviceInformationByHandle(std::int32_t handle, Hmd2::DeviceInformation* info); +std::int32_t APS5_VABI sceHmd2GetFieldOfView(std::int32_t handle, Hmd2::FieldOfView* fieldOfView); +std::int32_t APS5_VABI sceHmd2GetFieldOfViewWithoutHandle(Hmd2::FieldOfView* fieldOfView); +std::int32_t APS5_VABI sceHmd2SetVibration(std::int32_t handle, std::uint32_t level); +} + +#endif diff --git a/core/libs/tests/GuestHmd2.cpp b/core/libs/tests/GuestHmd2.cpp index 85af8b0350..8ec9cbeb30 100644 --- a/core/libs/tests/GuestHmd2.cpp +++ b/core/libs/tests/GuestHmd2.cpp @@ -1,17 +1,18 @@ -#include "prx/libc/include/general/VabiMacros.hpp" +#include "ProcessParametersFixture.hpp" +#include "prx/libSceHmd2/Hmd2.hpp" +#include "prx/libSceUserService/UserService.hpp" +#include <array> #include <cstdint> #include <cstdio> #include <cstdlib> +#include <cstring> #include <stdexcept> -extern "C" { -std::int32_t APS5_VABI sceHmd2Initialize(const void* param); -int APS5_VABI sceHmd2Open(); -} - namespace { +alignas(16) std::array<std::byte, 0x40> process{}; + void Require(bool condition, const char* message) { if (!condition) { std::fprintf(stderr, "HMD2: %s\n", message); @@ -19,19 +20,120 @@ void Require(bool condition, const char* message) { } } +void SetSdkVersion(std::uint32_t version) { + std::memcpy(process.data() + 0x10, &version, sizeof(version)); } -int main() { +void InstallProcessParameters() { + const std::uint64_t size = process.size(); + const std::uint32_t magic = 0x4942524f; + std::memcpy(process.data(), &size, sizeof(size)); + std::memcpy(process.data() + 8, &magic, sizeof(magic)); + Require(ProcessParametersFixture::Install(process.data(), process.size()), "process parameters were not installed"); +} + +bool IsDefaultFieldOfView(const Hmd2::FieldOfView& fieldOfView) { + return fieldOfView.tanOut == 1.84177089f && fieldOfView.tanIn == 0.947190642f && + fieldOfView.tanTop == 1.32898104f && fieldOfView.tanBottom == 1.32898104f; +} + +void RequireNotDetected(const Hmd2::DeviceInformation& info, std::int32_t userId) { + Require(info.status == Hmd2::DeviceStatus::NotDetected, "device status is not NOT_DETECTED"); + Require(info.userId == userId, "device user does not match the SDK version"); + Require(info.panelWidth == 4000 && info.panelHeight == 2040, "panel resolution differs from the firmware"); + Require(info.latency90Hz == 5555 && info.latency120Hz == 4160, "flip latencies differ from the firmware"); + Require(info.hmuMount == 0 && info.lensSeparationDistance == 0, "a missing headset reported a mount or lens separation"); + Require(info.reserved[0] == 0 && info.reserved[1] == 0, "reserved bytes were not cleared"); + Require(info.virtualImageDistance == 2.0f, "virtual image distance differs from the firmware"); +} + +void TestBeforeInitialize() { + Hmd2::DeviceInformation info; + std::memset(&info, 0xcc, sizeof(info)); + Require(sceHmd2GetDeviceInformation(&info) == Hmd2::NotInitialized, "device information before initialization"); + const Hmd2::DeviceInformation cleared{}; + Require(std::memcmp(&info, &cleared, sizeof(info)) == 0, "device information was not cleared before the initialization check"); + Require(sceHmd2GetDeviceInformation(nullptr) == Hmd2::ParameterNull, "null device information before initialization"); + Hmd2::FieldOfView fieldOfView{}; + Require(sceHmd2GetFieldOfViewWithoutHandle(&fieldOfView) == Hmd2::NotInitialized, "field of view before initialization"); + Require(sceHmd2GetFieldOfView(0, &fieldOfView) == Hmd2::InvalidHandle, "field of view of handle 0 before initialization"); + Require(sceHmd2GetDeviceInformationByHandle(0, &info) == Hmd2::NotInitialized, "device information of handle 0 before initialization"); + Require(sceHmd2Open(USER_SERVICE_INITIAL_USER_ID, 0, 0, nullptr) == Hmd2::NotInitialized, "open before initialization"); + Require(sceHmd2Close(0) == Hmd2::NotInitialized, "close before initialization"); + Require(sceHmd2Terminate() == Hmd2::NotInitialized, "terminate before initialization"); + Require(sceHmd2SetVibration(0, 0) == 0, "vibration of the unopened handle 0 failed"); +} + +void TestInitialized() { const std::uint8_t param[16]{}; - Require(sceHmd2Initialize(param) == static_cast<std::int32_t>(0x81110016), "initialization did not report the unsupported feature"); - Require(sceHmd2Initialize(nullptr) == static_cast<std::int32_t>(0x81110016), "initialization without a param did not report the unsupported feature"); + Require(sceHmd2Initialize(nullptr) == Hmd2::ParameterNull, "initialization without a param"); + Require(sceHmd2Initialize(param) == 0, "initialization failed"); + Require(sceHmd2Initialize(param) == Hmd2::AlreadyInitialized, "second initialization"); + + Hmd2::DeviceInformation info; + std::memset(&info, 0xcc, sizeof(info)); + SetSdkVersion(0x06000000); + Require(sceHmd2GetDeviceInformation(&info) == 0, "device information failed"); + RequireNotDetected(info, -1); + SetSdkVersion(0x07590001); + std::memset(&info, 0xcc, sizeof(info)); + Require(sceHmd2GetDeviceInformation(&info) == 0, "device information for SDK 7.59 failed"); + RequireNotDetected(info, 0); + + Hmd2::FieldOfView fieldOfView{}; + Require(sceHmd2GetFieldOfViewWithoutHandle(&fieldOfView) == 0 && IsDefaultFieldOfView(fieldOfView), "default field of view"); + Require(sceHmd2GetFieldOfViewWithoutHandle(nullptr) == Hmd2::ParameterNull, "null field of view"); + Require(sceHmd2GetFieldOfView(12345, &fieldOfView) == Hmd2::InvalidHandle, "field of view of an unknown handle"); + Require(sceHmd2GetFieldOfView(12345, nullptr) == Hmd2::ParameterNull, "null field of view of an unknown handle"); + Require(sceHmd2GetDeviceInformationByHandle(12345, &info) == Hmd2::InvalidHandle, "device information of an unknown handle"); + Require(sceHmd2SetVibration(12345, 0) == Hmd2::InvalidHandle, "vibration of an unknown handle"); + Require(sceHmd2Close(12345) == Hmd2::InvalidHandle, "close of an unknown handle"); + + Require(sceHmd2Open(USER_SERVICE_INITIAL_USER_ID, 1, 0, nullptr) == Hmd2::ParameterInvalid, "open with a device type"); + Require(sceHmd2Open(USER_SERVICE_INITIAL_USER_ID, 0, 1, nullptr) == Hmd2::ParameterInvalid, "open with a device index"); + Require(sceHmd2Open(USER_SERVICE_INITIAL_USER_ID, 0, 0, param) == Hmd2::ParameterInvalid, "open with a param"); + Require(sceHmd2Open(USER_SERVICE_INITIAL_USER_ID + 1, 0, 0, nullptr) == Hmd2::ParameterInvalid, "open for another user on SDK 7.59"); + + const auto handle = sceHmd2Open(USER_SERVICE_INITIAL_USER_ID, 0, 0, nullptr); + Require(handle > 0, "open without a headset failed"); + Require(sceHmd2Open(USER_SERVICE_INITIAL_USER_ID, 0, 0, nullptr) == Hmd2::AlreadyOpened, "second open"); + std::memset(&info, 0xcc, sizeof(info)); + Require(sceHmd2GetDeviceInformationByHandle(handle, &info) == 0, "device information by handle failed"); + RequireNotDetected(info, 0); + fieldOfView = {}; + Require(sceHmd2GetFieldOfView(handle, &fieldOfView) == 0 && IsDefaultFieldOfView(fieldOfView), "field of view by handle"); + Require(sceHmd2SetVibration(handle, 26) == 0, "vibration of a disconnected headset failed"); + Require(sceHmd2Close(handle) == 0, "close failed"); + Require(sceHmd2Close(handle) == Hmd2::InvalidHandle, "second close"); + Require(sceHmd2Close(0) == 0, "close of the unopened handle 0 failed"); + + const auto kept = sceHmd2Open(USER_SERVICE_INITIAL_USER_ID, 0, 0, nullptr); + Require(kept > 0, "reopen failed"); + Require(sceHmd2Terminate() == 0, "terminate failed"); + Require(sceHmd2Terminate() == Hmd2::NotInitialized, "second terminate"); + Require(sceHmd2GetFieldOfView(kept, &fieldOfView) == Hmd2::NotInitialized, "field of view after terminate"); + Require(sceHmd2Close(kept) == Hmd2::NotInitialized, "close after terminate"); + Require(sceHmd2Initialize(param) == 0, "reinitialization failed"); + Require(sceHmd2Open(USER_SERVICE_INITIAL_USER_ID, 0, 0, nullptr) == Hmd2::AlreadyOpened, "terminate closed the open handle"); + Require(sceHmd2Close(kept) == 0, "close after reinitialization failed"); + + SetSdkVersion(0x06000000); bool threw = false; try { - sceHmd2Open(); + sceHmd2Open(USER_SERVICE_INITIAL_USER_ID + 1, 0, 0, nullptr); } catch (const std::runtime_error&) { threw = true; } - Require(threw, "open after failed initialization did not throw"); + Require(threw, "open for another user before SDK 7.00 did not throw"); + Require(sceHmd2Terminate() == 0, "final terminate failed"); +} + +} + +int main() { + InstallProcessParameters(); + TestBeforeInitialize(); + TestInitialized(); std::puts("HMD2 tests passed"); return 0; } diff --git a/core/libs/tests/ProcessParametersFixture.hpp b/core/libs/tests/ProcessParametersFixture.hpp new file mode 100644 index 0000000000..794ba40ac9 --- /dev/null +++ b/core/libs/tests/ProcessParametersFixture.hpp @@ -0,0 +1,91 @@ +#ifndef CORE_LIBS_TESTS_PROCESSPARAMETERSFIXTURE_HPP +#define CORE_LIBS_TESTS_PROCESSPARAMETERSFIXTURE_HPP + +#include <cstddef> +#include <cstdint> +#include <cstring> +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include <windows.h> +#elif defined(__linux__) +#include "prx/libc/include/specifics/linux/ElfTypes.hpp" +#include <sys/mman.h> +#include <unistd.h> +#endif + +namespace ProcessParametersFixture { +namespace { + +#ifdef _WIN32 +[[gnu::section(".proctst"), gnu::used]] alignas(4) std::uint32_t metadata[2]{}; +#endif + +bool Install(std::byte* process, std::size_t size) { +#ifdef _WIN32 + auto* image = reinterpret_cast<std::byte*>(GetModuleHandleW(nullptr)); + if (image == nullptr) return false; + auto* dos = reinterpret_cast<IMAGE_DOS_HEADER*>(image); + auto* nt = reinterpret_cast<IMAGE_NT_HEADERS64*>(image + dos->e_lfanew); + auto* sections = IMAGE_FIRST_SECTION(nt); + IMAGE_SECTION_HEADER* target = nullptr; + for (unsigned index = 0; index < nt->FileHeader.NumberOfSections; ++index) { + if (std::memcmp(sections[index].Name, ".proctst", 8) == 0) { + target = §ions[index]; + break; + } + } + if (target == nullptr) return false; + metadata[0] = static_cast<std::uint32_t>(process - image); + metadata[1] = static_cast<std::uint32_t>(size); + DWORD oldProtect = 0; + if (VirtualProtect(target, sizeof(*target), PAGE_READWRITE, &oldProtect) == 0) return false; + std::memcpy(target->Name, ".procpar", 8); + target->Misc.VirtualSize = sizeof(metadata); + DWORD restored = 0; + VirtualProtect(target, sizeof(*target), oldProtect, &restored); + return true; +#elif defined(__linux__) + struct Request { + std::byte* process; + std::size_t size; + bool installed; + } request{process, size, false}; + dl_iterate_phdr([](dl_phdr_info* image, std::size_t, void* data) { + auto& request = *static_cast<Request*>(data); + if (image->dlpi_name != nullptr && image->dlpi_name[0] != '\0') return 0; + for (std::uint16_t index = 0; index < image->dlpi_phnum; ++index) { + const auto& header = image->dlpi_phdr[index]; + if (header.p_type == PT_LOAD || header.p_type == PT_GNU_EH_FRAME || header.p_type < 0x60000000u) continue; + auto* mutableHeader = const_cast<Elf64_Phdr*>(&header); + const auto pageSize = static_cast<std::uintptr_t>(sysconf(_SC_PAGESIZE)); + const auto pageStart = reinterpret_cast<std::uintptr_t>(mutableHeader) & ~(pageSize - 1); + const auto pageEnd = (reinterpret_cast<std::uintptr_t>(mutableHeader + 1) + pageSize - 1) & ~(pageSize - 1); + int protection = PROT_READ; + for (std::uint16_t loadIndex = 0; loadIndex < image->dlpi_phnum; ++loadIndex) { + const auto& load = image->dlpi_phdr[loadIndex]; + const auto start = image->dlpi_addr + load.p_vaddr; + if (load.p_type == PT_LOAD && pageStart >= (start & ~(pageSize - 1)) && pageStart < start + load.p_memsz && (load.p_flags & PF_X)) protection |= PROT_EXEC; + } + if (mprotect(reinterpret_cast<void*>(pageStart), pageEnd - pageStart, protection | PROT_WRITE) != 0) return 1; + mutableHeader->p_type = 0x61000001; + mutableHeader->p_vaddr = reinterpret_cast<std::uintptr_t>(request.process) - image->dlpi_addr; + mutableHeader->p_paddr = mutableHeader->p_vaddr; + mutableHeader->p_filesz = request.size; + mutableHeader->p_memsz = request.size; + request.installed = mprotect(reinterpret_cast<void*>(pageStart), pageEnd - pageStart, protection) == 0; + return 1; + } + return 1; + }, &request); + return request.installed; +#else + return false; +#endif +} + +} +} + +#endif diff --git a/docs/dev/debt/libSceHmd2.md b/docs/dev/debt/libSceHmd2.md index 9b27109ad4..70033deefa 100644 --- a/docs/dev/debt/libSceHmd2.md +++ b/docs/dev/debt/libSceHmd2.md @@ -2,16 +2,14 @@ ### Silent stubs -- [libSceHmd2](../../../core/libs/prx/libSceHmd2/Export.cpp) has no PS VR2 runtime: `sceHmd2Initialize` fails, and every other export it has (and every [libSceVrTracker2](../../../core/libs/prx/libSceVrTracker2/Export.cpp) export) throws. PPSA01317 skips its whole VR set-up when initialization fails +- [libSceHmd2](../../../core/libs/prx/libSceHmd2/Export.cpp) reports a console without a PS VR2 headset, as firmware 13.20's libSceHmd2 does with the `/dev/hmd2_cmd` state measured on a retail PS5 with none connected (connected flag 0, status 2, user -1): initialization succeeds, `sceHmd2Open` returns a handle, device information reports `NotDetected` with the firmware's fixed panel, latency and image distance values, both field-of-view calls return the firmware's default tangents and `sceHmd2SetVibration` succeeds without vibrating. The reprojection and gaze exports and every [libSceVrTracker2](../../../core/libs/prx/libSceVrTracker2/Export.cpp) export throw. PPSA01317, PPSA07411, PPSA09030, PPSA15878 and PPSA29866, which skipped their VR set-up while initialization failed, have not been run with initialization succeeding ### Unknown function info - PPSA29866 imports declared without parameters, signatures unknown, added without an implementation because the title fails to load without them: [sceHmd2ReprojectionQuerySeeThroughBufferSizeAlign](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2), [sceVideoOutSetFlipMaster and sceVideoOutClearFlipMaster](../../../core/libs/prx/libSceVideoOut/Unimplemented.cpp) (libSceVideoOut) - PPSA09030 imports from its `Media/Plugins/psvr2.prx` declared without parameters, signatures unknown, added without an implementation because the title fails to load the plugin without them: [sceHmd2ReprojectionGetStatus](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2), [sceVrTracker2GetCoordinateSystem and sceVrTracker2GetPlayAreaBoundaryGeometry](../../../core/libs/prx/libSceVrTracker2/Export.cpp) (libSceVrTracker2) -- [sceHmd2Initialize](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - the error the console returns when the PS VR2 runtime is unavailable is unknown; it returns libSceHmd's `UNSUPPORTED_FEATURE` (0x81110016). PPSA01317 passes a pointer to 16 zeroed bytes and only tests the result for non-zero -- PPSA01317 imports declared without parameters, signatures unknown: the remaining [libSceHmd2](../../../core/libs/prx/libSceHmd2/Export.cpp) and [libSceVrTracker2](../../../core/libs/prx/libSceVrTracker2/Export.cpp) exports. Names from the aerolib NID list +- [sceHmd2Initialize](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - the firmware also fails with 0x8a72001f when `sceKernelGetAppInfo` fails and with 0x8a72000a for some applications, depending on app info fields at 0x2c, 0x4b and 0x4d whose meaning is unknown, and then calls `sceMbusInit`; none of this is modelled +- [sceHmd2Open](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - the handle is the value the console's driver returned to the first open; titles built with an SDK before 7.00 may open for any user, and the firmware's `sceMbusAddHandleByUserId` result for a user other than the initial one is unknown, so that case throws. 0x8a720006 for a second open is named `AlreadyOpened` from its use +- [sceHmd2SetVibration](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - the second parameter is checked against 25 only while a headset is connected; its name and unit are unknown. The field of view member names are assumed: of the firmware's default tangents the first is the widest and the last two are equal +- PPSA01317 imports declared without parameters, signatures unknown: the [libSceHmd2](../../../core/libs/prx/libSceHmd2/Export.cpp) exports that throw and the remaining [libSceVrTracker2](../../../core/libs/prx/libSceVrTracker2/Export.cpp) exports. Names from the aerolib NID list - PPSA15878 imports declared without parameters, signatures unknown: the [libSceHmd2](../../../core/libs/prx/libSceHmd2/Export.cpp) reprojection and gaze exports, `sceVrTracker2Finalize`, `sceVrTracker2GetPlayAreaOrientedBoundingBox` and `sceVrTracker2LocateCoordinateSystem` in [libSceVrTracker2](../../../core/libs/prx/libSceVrTracker2/Export.cpp), and the `scePadVrController` trigger effect exports in [libScePad](../../../core/libs/prx/libScePad/Export.cpp). Names from the aerolib NID list - -### Functional - -- PPSA07411 PS VR2 imports declared without parameters, signatures unknown, added without an implementation because the title fails to load without them: [sceHmd2GetDeviceInformationByHandle, sceHmd2GetFieldOfView and sceHmd2Terminate](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2), [scePadVrControllerSetVibration, scePadVrControllerSetVibrationMode and scePadVrControllerReadCombinedState](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) and [sceVrTracker2ResetLocalCoordinate](../../../core/libs/prx/libSceVrTracker2/Export.cpp) (libSceVrTracker2) diff --git a/docs/dev/debt/libScePad.md b/docs/dev/debt/libScePad.md index 573dc9d373..291c86d4e2 100644 --- a/docs/dev/debt/libScePad.md +++ b/docs/dev/debt/libScePad.md @@ -11,3 +11,4 @@ - [scePadSetAngularVelocityBiasCorrectionState](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - inferred signature `(int handle, bool enabled)` follows `scePadSetTiltCorrectionState` and `scePadSetAngularVelocityDeadbandState`; an invalid handle returns `PAD_ERROR_INVALID_HANDLE`, disabling is a no-op, and enabling throws because bias correction is not modelled - [scePadUnknown_zBsgF0q8DIM](../../../core/libs/prx/libScePad/Export.cpp) (libScePad, NID `zBsgF0q8DIM`) - PPSA03671 imports it and fails to load without it. The aerolib list has no name for the NID, and none of the pad names in SharpProspero, shadPS4 or OpenOrbis, nor the trigger effect, motion sensor, VR controller, HD and haptic names tried, hashes to it. It is declared without parameters and throws. The title's executable has an import stub for it but no call, jump or address reference to that stub, so the argument count and whether the result is used are unknown - [scePadUnknown_n3kSX62fgNo](../../../core/libs/prx/libScePad/Export.cpp) (libScePad, NID `n3kSX62fgNo`) - PPSA01530 and PPSA13356 import it and fail to load without it. The real name is unknown; it is declared without parameters and throws +- PPSA07411 PS VR2 imports declared without parameters, signatures unknown, added without an implementation because the title fails to load without them: [scePadVrControllerSetVibration, scePadVrControllerSetVibrationMode and scePadVrControllerReadCombinedState](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) and [sceVrTracker2ResetLocalCoordinate](../../../core/libs/prx/libSceVrTracker2/Export.cpp) (libSceVrTracker2) From bea4be8f9510450a3a164d7f348b052952ed8d6f Mon Sep 17 00:00:00 2001 From: thibaut <thibaut-leaux-angeli@live.fr> Date: Sun, 18 Oct 2026 01:27:59 +0200 Subject: [PATCH 0917/1000] perf(agc): let the resource cache bound come back down Halve the grown bound (not below 1024 entries) once a window of the bound's uses times a shrink window was served by at most a quarter of it, and reset the bound on Clear(). The shrink window starts at 8 and doubles (up to 1024) each time growth undoes the last shrink, so a set of keys that comes back over a longer span stops the bound from oscillating. The entries over the bound leave at the next insert, with the other evictions. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XBXM4pgkMHYEtqN1EQni2P --- .../Graphics/include/ShaderResources.hpp | 8 +++++ .../Graphics/src/ShaderResources.cpp | 29 ++++++++++++++++ .../tests/execution/ResourceCacheGrowth.cpp | 34 +++++++++++++++++-- 3 files changed, 69 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp index 30e1012671..ef70d27744 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp @@ -18,6 +18,7 @@ #include <mutex> #include <string> #include <unordered_map> +#include <unordered_set> #include <vector> namespace AgcDriver { class VulkanDevice; } @@ -640,6 +641,7 @@ private: void noteMiss(const Key& key); void noteEvicted(const Key& key); void noteReturn(const Key& key); + void noteUse(const Key& key); struct KeyHash { std::size_t operator()(const Key& key) const noexcept { std::uint64_t hash = 14695981039346656037ull; @@ -656,6 +658,12 @@ private: std::unordered_map<std::size_t, decltype(evictedOrder)::iterator> evictedKeys; std::size_t evictionsSinceCheck = 0; std::size_t returnsSinceCheck = 0; + std::unordered_set<std::size_t> usedKeys; + std::size_t usesSinceCheck = 0; + static constexpr std::size_t InitialShrinkWindow = 8; + static constexpr std::size_t MaximumShrinkWindow = 1024; + std::size_t shrinkWindow = InitialShrinkWindow; + bool shrankLast = false; }; // The process-wide cache the device and the draw path share (keys name the device; a device clears diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index fc09e2f367..907062d951 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -3150,6 +3150,7 @@ std::shared_ptr<ShaderResources> ResourceCache::Find(const Key& key) { return nullptr; } entries.splice(entries.begin(), entries, found->second); + noteUse(key); return found->second->second; } @@ -3170,9 +3171,30 @@ void ResourceCache::noteEvicted(const Key& key) { if (!churning || capacity >= MaximumEntries) return; const auto previous = capacity; capacity = std::min(capacity * 2u, MaximumEntries); + if (shrankLast) shrinkWindow = std::min(shrinkWindow * 2u, MaximumShrinkWindow); + shrankLast = false; + usedKeys.clear(); + usesSinceCheck = 0; std::fprintf(stderr, "[rescache] capacity %zu -> %zu entries: keys it evicted keep coming back\n", previous, capacity); } +void ResourceCache::noteUse(const Key& key) { + if (fixedCapacity || capacity <= InitialEntries) return; + usedKeys.insert(KeyHash{}(key)); + if (++usesSinceCheck < capacity * shrinkWindow) return; + const auto used = usedKeys.size(); + const auto uses = usesSinceCheck; + usedKeys.clear(); + usesSinceCheck = 0; + if (used * 4u > capacity) return; + const auto previous = capacity; + capacity = std::max(capacity / 2u, InitialEntries); + shrankLast = true; + evictionsSinceCheck = 0; + returnsSinceCheck = 0; + std::fprintf(stderr, "[rescache] capacity %zu -> %zu entries: %zu keys served the last %zu uses\n", previous, capacity, used, uses); +} + void ResourceCache::noteReturn(const Key& key) { if (fixedCapacity) return; const auto found = evictedKeys.find(KeyHash{}(key)); @@ -3315,6 +3337,7 @@ void ResourceCache::Insert(const Key& key, std::shared_ptr<ShaderResources> reso fixedCapacity = parsed != 0; capacity = fixedCapacity ? static_cast<std::size_t>(parsed) : InitialEntries; } + noteUse(key); while (entries.size() > capacity) { noteEvicted(entries.back().first); if (evicted != nullptr) evicted->push_back(std::move(entries.back().second)); @@ -3365,6 +3388,7 @@ bool ResourceCache::Touch(const Key& key) { const auto found = index.find(key); if (found == index.end()) return false; entries.splice(entries.begin(), entries, found->second); + noteUse(key); return true; } @@ -3384,6 +3408,11 @@ void ResourceCache::Clear() { evictedOrder.clear(); evictionsSinceCheck = 0; returnsSinceCheck = 0; + usedKeys.clear(); + usesSinceCheck = 0; + shrinkWindow = InitialShrinkWindow; + shrankLast = false; + if (!fixedCapacity) capacity = 0; } std::size_t ResourceCache::Size() const { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ResourceCacheGrowth.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ResourceCacheGrowth.cpp index fbb0ccec87..bae94f9150 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ResourceCacheGrowth.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ResourceCacheGrowth.cpp @@ -31,6 +31,8 @@ using AgcDriver::Graphics::Require; constexpr std::uint32_t Dispatches = 1536; constexpr std::uint32_t Passes = 4; +constexpr std::uint32_t HotDispatches = 64; +constexpr std::uint32_t HotRepeats = 640; constexpr std::size_t SrtBytes = 64; constexpr std::size_t OutputBytes = 256; constexpr std::size_t SrtOffset = 4096; @@ -88,10 +90,11 @@ void Prepare(const GuestBlock& block) { std::memset(block.Bytes(OutputOffset), 0, static_cast<std::size_t>(Dispatches) * OutputBytes); } -void RunPass(const GuestBlock& block) { +void RunPass(const GuestBlock& block, std::uint32_t count = Dispatches, std::uint32_t repeats = 1) { const auto kernel = block.At(0); std::vector<std::uint32_t> words; - for (std::uint32_t dispatch = 0; dispatch < Dispatches; ++dispatch) { + for (std::uint32_t use = 0; use < count * repeats; ++use) { + const auto dispatch = use % count; const auto srt = block.At(SrtOffset + static_cast<std::size_t>(dispatch) * SrtBytes); for (const auto& packet : {Pm4(0x76, {0x20c, static_cast<std::uint32_t>(kernel >> 8u)}), Pm4(0x76, {0x20d, static_cast<std::uint32_t>(kernel >> 40u) & 0xffu}), Pm4(0x76, {0x207, 64}), Pm4(0x76, {0x208, 1}), Pm4(0x76, {0x209, 1}), Pm4(0x76, {0x213, 2u << 1u}), Pm4(0x76, {0x240, static_cast<std::uint32_t>(srt)}), Pm4(0x76, {0x241, static_cast<std::uint32_t>(srt >> 32u)}), Pm4(0x15, {1, 1, 1, 0x41})}) { words.insert(words.end(), packet.begin(), packet.end()); @@ -117,6 +120,32 @@ void WorkingSetLargerThanTheInitialBound(const GuestBlock& block) { Require(kept >= Dispatches, "resource cache growth: " + std::to_string(Dispatches) + " dispatches with distinct resources repeated " + std::to_string(Passes) + " times in turn, but the resource cache kept only " + std::to_string(kept) + " entries (initial bound " + std::to_string(InitialEntries) + "), so every pass rebuilt every set"); } +void BoundComesBackDown(const GuestBlock& block) { + auto& cache = AgcDriver::Graphics::SharedResourceCache(); + const auto grown = cache.Capacity(); + Require(grown > InitialEntries, "resource cache growth: the bound stayed at " + std::to_string(grown) + " entries after the large working set"); + RunPass(block, HotDispatches, HotRepeats); + const auto shrunk = cache.Capacity(); + Require(shrunk < grown, "resource cache growth: " + std::to_string(HotDispatches) + " sets served " + std::to_string(HotDispatches * HotRepeats) + " dispatches, but the bound stayed at " + std::to_string(shrunk) + " entries"); + + const std::uint32_t records = OutputBytes / 2; + std::memcpy(block.Bytes(SrtOffset + 8), &records, sizeof(records)); + RunPass(block, 1); + Require(cache.Size() <= shrunk, "resource cache growth: the bound came down to " + std::to_string(shrunk) + " entries, but the next insert left " + std::to_string(cache.Size())); + + for (std::uint32_t pass = 0; pass < Passes; ++pass) RunPass(block); + const auto regrown = cache.Capacity(); + Require(regrown > shrunk, "resource cache growth: the bound did not grow again when the large working set came back"); + RunPass(block, HotDispatches, HotRepeats); + Require(cache.Capacity() == regrown, "resource cache growth: the bound came down again after the same hot pass, although the last shrink had been undone"); + + cache.Clear(); + Require(cache.Capacity() == InitialEntries, "resource cache growth: Clear() left the bound at " + std::to_string(cache.Capacity()) + " entries instead of " + std::to_string(InitialEntries)); + Prepare(block); + for (std::uint32_t pass = 0; pass < Passes; ++pass) RunPass(block); + Require(cache.Capacity() > InitialEntries, "resource cache growth: after Clear() the large working set did not grow the bound again"); +} + } int main() { @@ -128,6 +157,7 @@ int main() { GuestBlock block; Prepare(block); WorkingSetLargerThanTheInitialBound(block); + BoundComesBackDown(block); AgcDriverShutdown_nid_postfix(); std::puts("resource cache growth tests passed"); return 0; From 316ad40b46167e726c6c3619e5bfae3c6d426ce0 Mon Sep 17 00:00:00 2001 From: Kaua <kaua02249@gmail.com> Date: Sat, 17 Oct 2026 21:06:51 -0300 Subject: [PATCH 0918/1000] fix(tools): write check_compile output as UTF-8 --- tools/check_compile.py | 3 +++ tools/tests/test_check_compile.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/tools/check_compile.py b/tools/check_compile.py index cd8761de32..47076b046b 100644 --- a/tools/check_compile.py +++ b/tools/check_compile.py @@ -155,6 +155,9 @@ def check(root, build, files): def main(argv=None): + for stream in (sys.stdout, sys.stderr): + if hasattr(stream, "reconfigure"): + stream.reconfigure(encoding="utf-8", errors="replace") parser = argparse.ArgumentParser( description="compile the changed translation units only, to check a change builds without a full build") parser.add_argument("--build", type=Path, default=Path("build"), diff --git a/tools/tests/test_check_compile.py b/tools/tests/test_check_compile.py index 975094663a..3b83764f0a 100644 --- a/tools/tests/test_check_compile.py +++ b/tools/tests/test_check_compile.py @@ -61,7 +61,7 @@ class CheckCompileTests(unittest.TestCase): environment = dict(os.environ) environment.update(env or {}) return subprocess.run([sys.executable, "-I", str(TOOL), *arguments], cwd=self.root, - capture_output=True, text=True, env=environment) + capture_output=True, text=True, encoding="utf-8", env=environment) def source(self, relative, text="int x;\n"): path = self.root / relative From 051a61079830beb1784b131ce31a1da6435ef941 Mon Sep 17 00:00:00 2001 From: fma-rc <188743521+fma-rc@users.noreply.github.com> Date: Sun, 11 Oct 2026 02:31:51 +0200 Subject: [PATCH 0919/1000] feat(libSceJobManager): implement compute sequences and compute dispatches --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 2 + .../tests/execution/JobManagerCompute.cpp | 242 ++++++++++++++++ core/libs/prx/libSceJobManager/CMakeLists.txt | 3 +- core/libs/prx/libSceJobManager/Export.cpp | 261 +++++++++++++++--- docs/dev/debt/libSceJobManager.md | 2 +- 5 files changed, 474 insertions(+), 36 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/JobManagerCompute.cpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index fcd75d52e0..49d614a22a 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1564,6 +1564,8 @@ target_include_directories(agc_driver_swap_alt_blend_tests PRIVATE $<TARGET_PROP target_include_directories(agc_driver_packed_float_color_target_tests PRIVATE $<TARGET_PROPERTY:shader_recompiler_agc_driver,INTERFACE_INCLUDE_DIRECTORIES>) +target_link_libraries(agc_driver_job_manager_compute_tests PRIVATE libSceJobManager libSceAgc libkernel) + target_include_directories(agc_driver_packed_unorm_color_target_tests PRIVATE $<TARGET_PROPERTY:shader_recompiler_agc_driver,INTERFACE_INCLUDE_DIRECTORIES>) target_include_directories(agc_driver_color1555_target_tests PRIVATE $<TARGET_PROPERTY:shader_recompiler_agc_driver,INTERFACE_INCLUDE_DIRECTORIES>) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/JobManagerCompute.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/JobManagerCompute.cpp new file mode 100644 index 0000000000..79af7f5d54 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/JobManagerCompute.cpp @@ -0,0 +1,242 @@ +#include "VulkanTestDevice.hpp" +#include "SceTypes.hpp" +#include <array> +#include <atomic> +#include <chrono> +#include <cstdint> +#include <cstdlib> +#include <cstring> +#include <iostream> +#include <new> +#include <stdexcept> +#include <string> +#include <thread> + +extern "C" void AgcDriverShutdown_nid_postfix(); + +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 void CompleteDestructor() = 0; + APS5_VABI virtual void DeletingDestructor() = 0; + APS5_VABI virtual int DispatchCompute(const void*, void*, std::int32_t, std::int32_t, std::int32_t, const char*) = 0; + APS5_VABI virtual int DispatchComputeIndirect(const void*, void*, const void*, const char*) = 0; +}; + +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 void CompleteDestructor() = 0; + APS5_VABI virtual void DeletingDestructor() = 0; + 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*); +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 _ZN3sce3Job10JobManager8shutdownEv(void*); +SequenceFactory* APS5_VABI _ZN3sce3Job10JobManager34getUnifiedSequenceFactoryInterfaceENS1_8PriorityE(void*, std::uint32_t); +} + +#define Construct _ZN3sce3Job10JobManagerC1Ev +#define RequiredSize _ZN3sce3Job10JobManager27calculateRequiredMemorySizeEPKNS1_21MemorySizeQueryParamsE +#define Initialize _ZN3sce3Job10JobManager10initializeEPvmjjjmPKNS1_12ConfigParamsE +#define Shutdown _ZN3sce3Job10JobManager8shutdownEv +#define Factories _ZN3sce3Job10JobManager34getUnifiedSequenceFactoryInterfaceENS1_8PriorityE + +namespace { + +constexpr int InvalidArgument = static_cast<int>(0x81581000u); +constexpr int OutOfRange = static_cast<int>(0x81580000u); +constexpr int NullArgument = static_cast<int>(0x81580001u); +constexpr int NoNodes = static_cast<int>(0x81580002u); +constexpr int LimitExceeded = static_cast<int>(0x81580003u); +constexpr int NoneFree = static_cast<int>(0x81580006u); +constexpr int Invalid = static_cast<int>(0x81580007u); + +void Require(bool condition, const std::string& message) { + if (!condition) throw std::runtime_error(message); +} + +alignas(256) std::array<std::uint32_t, 12> ReaderCode{0xf4040100u, 0xfa000000u, 0xf4080201u, 0xfa000000u, 0xbf8cc07fu, 0xf4000182u, 0xfa000000u, 0xbf8cc07fu, 0x7e000206u, 0xe0700000u, 0x80020000u, 0xbf810000u}; +alignas(256) std::array<std::uint32_t, 4> Value{}; +alignas(256) std::array<std::uint32_t, 2> Table{}; +alignas(256) volatile std::uint32_t Output[4]{}; +alignas(256) std::array<std::uint32_t, 4> OutputDescriptor{}; +alignas(32) std::array<std::uint32_t, 8> IndirectArguments{1, 1, 1, 0, 0, 0, 0, 0}; +alignas(16) std::array<std::uint32_t, 8> Specials{}; +alignas(16) std::array<std::uint32_t, 8> SpecialsWithoutDispatch{}; +std::array<ShaderRegister, 9> Registers{}; +alignas(16) std::array<unsigned char, 0x80> Shader{}; +alignas(16) std::array<unsigned char, 0x80> ShaderWithoutDispatch{}; + +std::uint32_t Low(const volatile void* pointer) { + return static_cast<std::uint32_t>(reinterpret_cast<std::uintptr_t>(pointer)); +} + +std::uint32_t High(const volatile void* pointer) { + return static_cast<std::uint32_t>(static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(pointer)) >> 32u); +} + +void BuildShader() { + Table = {Low(Value.data()), High(Value.data())}; + OutputDescriptor = {Low(Output), High(Output) & 0xffffu, sizeof(Output), 0x31016facu}; + const auto code = static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(ReaderCode.data())); + Registers = {{{0x20c, static_cast<std::uint32_t>(code >> 8u)}, {0x20d, static_cast<std::uint32_t>(code >> 40u) & 0xffu}, {0x207, 1}, {0x208, 1}, {0x209, 1}, {0x212, 0}, {0x213, 4u << 1u}, + {0x242, Low(OutputDescriptor.data())}, {0x243, High(OutputDescriptor.data())}}}; + Specials[4] = 1; + const ShaderRegister* registers = Registers.data(); + for (auto* shader : {&Shader, &ShaderWithoutDispatch}) { + const std::uint32_t* specials = shader == &Shader ? Specials.data() : SpecialsWithoutDispatch.data(); + std::memcpy(shader->data() + 0x20, ®isters, sizeof(registers)); + std::memcpy(shader->data() + 0x28, &specials, sizeof(specials)); + (*shader)[0x5c] = static_cast<unsigned char>(Registers.size()); + } +} + +std::uint64_t SizeFor(std::uint32_t computeSequences, std::uint64_t mask, bool limits) { + const MemorySizeQueryParams query{1, 2, 1, limits ? 100u : 0u, limits ? 8u : 0u, limits ? 4u : 0u, limits ? 2u : 0u, computeSequences, mask}; + return RequiredSize(&query); +} + +std::atomic<bool> Released{false}; +std::atomic<std::uint32_t> SeenByJob{0}; + +void APS5_VABI BlockingJob(void*, std::uint32_t) { + while (!Released) std::this_thread::yield(); +} + +void APS5_VABI ReadOutputJob(void*, std::uint32_t) { + SeenByJob = Output[0]; +} + +void APS5_VABI Nothing(void*, std::uint32_t) { +} + +void SizeCase() { + Require(SizeFor(0, 0, false) == 0x10d0 && SizeFor(0, 0, true) == 0x14c0, "size without compute sequences"); + Require(SizeFor(1, 1, false) == 0x24fd8 && SizeFor(1, 1, true) == 0x253c8, "size with one compute sequence"); + Require(SizeFor(2, 3, false) == 0x48e20 && SizeFor(2, 3, true) == 0x49210, "size with two compute sequences"); + Require(SizeFor(3, 7, false) == 0x6cb68, "size with three compute sequences"); + Require(SizeFor(1, 3, true) == 0x491c8, "size with two queues for one compute sequence"); + Require(SizeFor(2, 1, true) == 0, "size with fewer queues than compute sequences"); +} + +void RunCase() { + const std::uint64_t size = SizeFor(1, 1, true); + void* memory = ::operator new((size + 255) & ~std::uint64_t{255}, std::align_val_t{256}); + Require(memory != nullptr, "allocation failed"); + alignas(64) static unsigned char manager[0x100]; + Construct(manager); + const ConfigParams badHandler{1, 0x00021fff, 0x00010000, 0x2bc, nullptr, 0, 0x1fff, 0x50, 0}; + Require(Initialize(manager, memory, size, 2, 1, 1, 1, &badHandler) == InvalidArgument, "a handler priority below 0x100 was accepted"); + Require(Initialize(manager, memory, size, 2, 1, 1, 1, nullptr) == 0, "initialize with one compute sequence failed"); + SequenceFactory* factory = Factories(manager, 0); + Sequence* sequence = nullptr; + Sequence* other = nullptr; + Require(factory->CreateCompute(&sequence, 100, 4, 4, 2, "compute") == 0, "the compute sequence was not created"); + Require(factory->CreateCompute(&other, 1, 1, 1, 1, "second") == NoneFree, "a second compute sequence was created"); + Require(factory->Create(&other, 1, 1, 1, 1, "cpu") == 0, "the CPU sequence was not created"); + Require(factory->DestroyCompute(other) == Invalid, "a CPU sequence was destroyed as a compute sequence"); + Require(factory->Destroy(sequence) == Invalid, "a compute sequence was destroyed as a CPU sequence"); + Require(factory->Destroy(other) == 0, "the CPU sequence was not destroyed"); + + Require(sequence->DispatchCompute(nullptr, Table.data(), 1, 1, 1, nullptr) == NullArgument, "a null shader was accepted"); + Require(sequence->DispatchCompute(Shader.data(), Table.data(), 0, 1, 1, nullptr) == OutOfRange, "a zero group count was accepted"); + Require(sequence->DispatchCompute(Shader.data(), Table.data(), 1, 1, -1, nullptr) == OutOfRange, "a negative group count was accepted"); + Require(sequence->DispatchComputeIndirect(Shader.data(), Table.data(), nullptr, nullptr) == NullArgument, "null indirect arguments were accepted"); + Require(sequence->DispatchComputeIndirect(Shader.data(), Table.data(), reinterpret_cast<const char*>(IndirectArguments.data()) + 4, nullptr) == NullArgument, "misaligned indirect arguments were accepted"); + + Value[0] = 0x11110001u; + Require(sequence->DispatchCompute(Shader.data(), Table.data(), 1, 1, 1, "direct") == 0, "the compute dispatch failed"); + Require(sequence->Wait() == 0, "the wait after a compute dispatch failed"); + Require(Output[0] == 0x11110001u, "the compute dispatch had not run when the wait returned: " + std::to_string(Output[0])); + + Value[0] = 0x22220002u; + Require(sequence->DispatchComputeIndirect(Shader.data(), Table.data(), IndirectArguments.data(), nullptr) == 0, "the indirect compute dispatch failed"); + Require(sequence->Wait() == 0, "the wait after an indirect compute dispatch failed"); + Require(Output[0] == 0x22220002u, "the indirect compute dispatch had not run when the wait returned: " + std::to_string(Output[0])); + + Value[0] = 0x33330003u; + Require(sequence->DispatchCompute(ShaderWithoutDispatch.data(), Table.data(), 1, 1, 1, nullptr) == 0, "the dispatch of a shader without the dispatch bit failed"); + Require(sequence->Wait() == 0, "the wait after a shader without the dispatch bit failed"); + Require(Output[0] == 0x22220002u, "a shader without the dispatch bit ran"); + + Require(sequence->Dispatch(BlockingJob, nullptr, 1, "blocking") == 0, "the blocking job was not dispatched"); + Require(sequence->SyncPoint() == 0, "the sync point failed"); + Require(sequence->DispatchCompute(Shader.data(), Table.data(), 1, 1, 1, nullptr) == 0, "the compute dispatch behind a sync point failed"); + std::this_thread::sleep_for(std::chrono::milliseconds(200)); + Require(Output[0] == 0x22220002u, "a compute dispatch ran before the CPU job ahead of its sync point finished"); + Released = true; + Require(sequence->SyncPoint() == 0, "the second sync point failed"); + Require(sequence->Dispatch(ReadOutputJob, nullptr, 1, "reader") == 0, "the reading job was not dispatched"); + Require(sequence->Wait() == 0, "the wait after the ordered dispatches failed"); + Require(Output[0] == 0x33330003u, "the compute dispatch behind a sync point did not run"); + Require(SeenByJob == 0x33330003u, "a CPU job behind a sync point ran before the compute dispatch ahead of it finished"); + + for (int index = 0; index < 4; ++index) Require(sequence->DispatchCompute(Shader.data(), Table.data(), 1, 1, 1, nullptr) == 0, "a compute dispatch within the limit failed"); + Require(sequence->DispatchCompute(Shader.data(), Table.data(), 1, 1, 1, nullptr) == LimitExceeded, "a compute dispatch past the dispatch limit was accepted"); + Require(sequence->DispatchCompute(Shader.data(), Table.data(), 1, 1, 1, nullptr) == Invalid, "a latched compute sequence accepted a dispatch"); + Require(factory->DestroyCompute(sequence) == 0, "the compute sequence was not destroyed"); + + Require(factory->Create(&other, 0x7fff, 0x3ff, 0xff, 1, "nodes") == 0, "the node counting sequence was not created"); + int accepted = 0; + int result = 0; + while (accepted < 0x3ff && (result = other->Dispatch(Nothing, nullptr, 1, nullptr)) == 0) ++accepted; + Require(result == NoNodes && accepted == 43, "the node pool of a compute configuration holds " + std::to_string(accepted) + " dispatches"); + Require(Shutdown(manager) == 0, "shutdown failed"); + ::operator delete(memory, std::align_val_t{256}); +} + +} + +int main() { + try { + auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + device.reset(); + BuildShader(); + SizeCase(); + RunCase(); + AgcDriverShutdown_nid_postfix(); + std::cout << "job manager compute test passed\n"; + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + try { + AgcDriverShutdown_nid_postfix(); + } catch (const std::exception& shutdownError) { + std::cerr << shutdownError.what() << '\n'; + } + return 1; + } +} diff --git a/core/libs/prx/libSceJobManager/CMakeLists.txt b/core/libs/prx/libSceJobManager/CMakeLists.txt index c029f72ce6..8539bbd0c2 100644 --- a/core/libs/prx/libSceJobManager/CMakeLists.txt +++ b/core/libs/prx/libSceJobManager/CMakeLists.txt @@ -1,11 +1,12 @@ add_library(libSceJobManager SHARED EXCLUDE_FROM_ALL Export.cpp) target_include_directories(libSceJobManager PRIVATE ${LIBS_INCLUDE_DIR}) +target_link_libraries(libSceJobManager PRIVATE libSceAgc libSceAgcDriver) 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) + target_link_libraries(guest_job_manager_tests PRIVATE libSceJobManager libSceAgc libSceAgcDriver libkernel libc) configure_windows_unwind(guest_job_manager_tests) foreach(testCase IN ITEMS memory validation dispatch ordering nested limits priority shutdown) add_test(NAME guest_job_manager_${testCase} COMMAND guest_job_manager_tests ${testCase}) diff --git a/core/libs/prx/libSceJobManager/Export.cpp b/core/libs/prx/libSceJobManager/Export.cpp index 94d53929a9..9ed6abee66 100644 --- a/core/libs/prx/libSceJobManager/Export.cpp +++ b/core/libs/prx/libSceJobManager/Export.cpp @@ -22,6 +22,24 @@ int APS5_VABI scePthreadAttrSetstacksize(PthreadAttr* attr, std::size_t stackSiz 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); +int APS5_VABI sceKernelCreateEqueue(KernelEqueue* eq, const char* name); +int APS5_VABI sceKernelDeleteEqueue(KernelEqueue eq); +int APS5_VABI sceKernelWaitEqueue(KernelEqueue eq, KernelEvent* ev, int num, int* out, const KernelUseconds* timo); +int APS5_VABI sceKernelAddUserEventEdge(KernelEqueue eq, int id); +int APS5_VABI sceKernelTriggerUserEvent(KernelEqueue eq, int id, void* udata); +int APS5_VABI sceKernelGetEventFilter(const KernelEvent* ev); +std::uintptr_t APS5_VABI sceKernelGetEventId(const KernelEvent* ev); +int APS5_VABI sceAgcDriverAddEqEvent(KernelEqueue eq, int id, void* udata); +int APS5_VABI sceAgcDriverDeleteEqEvent(KernelEqueue eq, int id); +int APS5_VABI sceAgcDriverSubmitAcb(std::uint32_t queue, const Packet* packet); +std::uint32_t* APS5_VABI sceAgcAcbAcquireMem(CommandBuffer* buf, std::uint32_t gcrControl, const volatile void* base, std::uint64_t sizeBytes, std::uint32_t pollCycles); +std::uint32_t* APS5_VABI sceAgcCbSetShRegistersDirect(CommandBuffer* buf, const volatile ShaderRegister* regs, std::uint32_t numRegs); +std::uint32_t* APS5_VABI sceAgcCbSetShRegisterRangeDirect(CommandBuffer* buf, std::uint32_t offset, const std::uint32_t* values, std::uint32_t numValues); +std::uint32_t* APS5_VABI sceAgcAcbPushMarker(CommandBuffer* buf, const char* str, std::uint32_t color); +std::uint32_t* APS5_VABI sceAgcAcbPopMarker(CommandBuffer* buf); +std::uint32_t* APS5_VABI sceAgcCbDispatch(CommandBuffer* buf, std::uint32_t threadGroupX, std::uint32_t threadGroupY, std::uint32_t threadGroupZ, std::uint32_t modifier); +std::uint32_t* APS5_VABI sceAgcAcbDispatchIndirect(CommandBuffer* buf, const volatile void* indirectArguments, std::uint32_t modifier); +std::uint32_t* APS5_VABI sceAgcCbReleaseMem(CommandBuffer* buf, std::uint8_t action, std::uint16_t gcrControl, std::uint8_t dst, std::uint8_t cachePolicy, const volatile Label* address, std::uint8_t dataSelect, std::uint64_t data, std::uint16_t gdsOffset, std::uint16_t gdsSize, std::uint8_t interrupt, std::uint32_t interruptContextId); } namespace { @@ -50,6 +68,11 @@ constexpr std::uint64_t StateEnd = 0x2d8; constexpr std::uint64_t ThreadRecordBytes = 0x48; constexpr std::uint64_t SequenceBytes = 0x240; constexpr std::uint64_t NodeBytes = 72; +constexpr std::uint64_t ComputeContextBytes = 0x168; +constexpr std::uint64_t ComputeCommandBytes = 0x3ff * 104 + 0xff * 148 + 0x3f * 32; +constexpr std::uint32_t FirstComputeQueue = 0x20; +constexpr std::uint32_t UserDataRegister = 0x240; +constexpr int UserEventFilter = -11; struct MemorySizeQueryParams { std::uint32_t version; @@ -94,7 +117,7 @@ std::uint64_t AlignUp(std::uint64_t value, std::uint64_t alignment) { struct Manager; class Sequence; -enum class ItemType { Dispatch, SyncPoint, Wait }; +enum class ItemType { Dispatch, SyncPoint, Wait, Compute, ComputeIndirect }; struct Item { ItemType type; @@ -105,6 +128,11 @@ struct Item { std::uint32_t handedOut; std::uint32_t completed; bool* passed; + const void* shader = nullptr; + std::array<std::uint32_t, 3> groups{}; + const void* indirectArguments = nullptr; + std::string name; + bool named = false; }; struct Counters { @@ -140,27 +168,26 @@ 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; - } + APS5_VABI int DispatchCompute(const void* shader, void* userData, std::int32_t x, std::int32_t y, std::int32_t z, const char* name) override; + APS5_VABI int DispatchComputeIndirect(const void* shader, void* userData, const void* indirectArguments, const char* name) override; bool Open(Manager* owner, const Counters& limit, std::uint32_t runQueue); int WaitAndClose(std::uint32_t index); void Advance(); void DispatchCompleted(std::uint32_t invocations); + void CollectCompute(); Manager* manager = nullptr; bool open = false; + bool compute = false; + std::uint32_t queue = 0; private: int Latch(int code); void RequireOpen() const; int WaitLocked(std::unique_lock<std::mutex>& guard); + int AppendCompute(Item item); + void SubmitCompute(const Item& item); Counters limits; Counters submitted; @@ -172,6 +199,11 @@ private: std::deque<Item> items; std::mutex waiters; std::condition_variable passed; + Label label{}; + std::uint64_t labelSeen = 0; + std::uint64_t labelTotal = 0; + bool batchStarted = false; + std::deque<std::vector<std::uint32_t>> commands; }; class Factory final : public ISequenceFactory { @@ -196,6 +228,11 @@ struct Manager { std::uint32_t persistentThreads = 0; std::array<std::deque<Item*>, 2> runQueues; std::vector<Sequence> sequences; + std::deque<Sequence> computeSequences; + std::uint32_t freeComputeSequences = 0; + std::vector<std::uint32_t> computeQueues; + KernelEqueue computeEvents = 0; + Pthread computeHandler = nullptr; std::array<Factory, 2> factories; std::array<Factory, 2> unifiedFactories; std::vector<Pthread> workers; @@ -231,6 +268,8 @@ bool Sequence::Open(Manager* owner, const Counters& limit, std::uint32_t runQueu outstanding = 0; cursor = 0; items.clear(); + commands.clear(); + batchStarted = false; if (!TakeNode(*owner)) return false; open = true; return true; @@ -250,6 +289,12 @@ void Sequence::Advance() { manager->work.notify_all(); continue; } + if (item.type == ItemType::Compute || item.type == ItemType::ComputeIndirect) { + ++outstanding; + ++cursor; + SubmitCompute(item); + continue; + } if (outstanding != 0) return; if (item.type == ItemType::SyncPoint) { ++completed.syncPoints; @@ -260,6 +305,8 @@ void Sequence::Advance() { const auto retired = static_cast<std::uint32_t>(cursor) + 1; items.erase(items.begin(), items.begin() + static_cast<std::ptrdiff_t>(retired)); cursor = 0; + commands.clear(); + batchStarted = false; if (error == 0) { submitted.invocations -= completed.invocations; submitted.dispatches -= completed.dispatches; @@ -296,6 +343,81 @@ int Sequence::Dispatch(JobEntry entry, void* argument, std::uint32_t count, cons return 0; } +void Sequence::SubmitCompute(const Item& item) { + const auto* bytes = static_cast<const std::uint8_t*>(item.shader); + const ShaderRegister* registers = nullptr; + const std::uint8_t* specials = nullptr; + std::memcpy(®isters, bytes + 0x20, sizeof(registers)); + std::memcpy(&specials, bytes + 0x28, sizeof(specials)); + const std::uint32_t registerCount = bytes[0x5c]; + std::uint32_t modifier = 0; + std::memcpy(&modifier, specials + 0x10, sizeof(modifier)); + const auto userData = reinterpret_cast<std::uint64_t>(item.argument); + const std::array<std::uint32_t, 2> userDataWords{static_cast<std::uint32_t>(userData), static_cast<std::uint32_t>(userData >> 32)}; + auto& words = commands.emplace_back(64 + 3 * registerCount + item.name.size() / 4); + CommandBuffer buffer{words.data(), words.data() + words.size(), words.data(), words.data() + words.size(), nullptr, nullptr, 0}; + bool encoded = sceAgcAcbAcquireMem(&buffer, 0x20380u | (batchStarted ? 0u : 1u), nullptr, 0, 0x190) != nullptr; + encoded = encoded && sceAgcCbSetShRegistersDirect(&buffer, registers, registerCount) != nullptr; + encoded = encoded && sceAgcCbSetShRegisterRangeDirect(&buffer, UserDataRegister, userDataWords.data(), 2) != nullptr; + if (item.named) encoded = encoded && sceAgcAcbPushMarker(&buffer, item.name.c_str(), 0) != nullptr; + if ((modifier & 1) != 0) { + if (item.type == ItemType::Compute) { + encoded = encoded && sceAgcCbDispatch(&buffer, item.groups[0], item.groups[1], item.groups[2], modifier) != nullptr; + } else { + encoded = encoded && sceAgcAcbDispatchIndirect(&buffer, item.indirectArguments, modifier) != nullptr; + } + } + if (item.named) encoded = encoded && sceAgcAcbPopMarker(&buffer) != nullptr; + encoded = encoded && sceAgcCbReleaseMem(&buffer, 0x2f, 0x620, 0, 0, &label, 1, ++labelTotal, 0, 0, 2, 0) != nullptr; + if (!encoded) throw std::runtime_error("libSceJobManager: a compute dispatch did not fit its command buffer"); + batchStarted = true; + const Packet packet{words.data(), static_cast<std::uint32_t>(buffer.cursor_up - words.data()), 0, {}}; + if (sceAgcDriverSubmitAcb(queue, &packet) != 0) throw std::runtime_error("libSceJobManager: sceAgcDriverSubmitAcb failed"); +} + +void Sequence::CollectCompute() { + const std::uint64_t value = label.value; + const auto finished = static_cast<std::uint32_t>(value - labelSeen); + labelSeen = value; + if (finished == 0) return; + completed.dispatches += finished; + outstanding -= finished; + if (outstanding == 0) Advance(); +} + +int Sequence::AppendCompute(Item item) { + const std::lock_guard guard(manager->lock); + RequireOpen(); + if (!compute) throw std::runtime_error("libSceJobManager: a compute dispatch was made on a CPU sequence"); + if (error != 0) return SequenceInvalid; + if (submitted.dispatches++ >= limits.dispatches) return Latch(SequenceLimitExceeded); + if (!TakeNode(*manager)) return Latch(SequenceNoNodes); + items.push_back(std::move(item)); + Advance(); + return 0; +} + +int Sequence::DispatchCompute(const void* shader, void* userData, std::int32_t x, std::int32_t y, std::int32_t z, const char* name) { + if (shader == nullptr) return SequenceNullArgument; + if (x <= 0 || y <= 0 || z <= 0) return SequenceOutOfRange; + Item item{ItemType::Compute, this, nullptr, userData, 0, 0, 0, nullptr}; + item.shader = shader; + item.groups = {static_cast<std::uint32_t>(x), static_cast<std::uint32_t>(y), static_cast<std::uint32_t>(z)}; + item.named = name != nullptr; + if (item.named) item.name = name; + return AppendCompute(std::move(item)); +} + +int Sequence::DispatchComputeIndirect(const void* shader, void* userData, const void* indirectArguments, const char* name) { + if (shader == nullptr || indirectArguments == nullptr || (reinterpret_cast<std::uint64_t>(indirectArguments) & 0x1f) != 0) return SequenceNullArgument; + Item item{ItemType::ComputeIndirect, this, nullptr, userData, 0, 0, 0, nullptr}; + item.shader = shader; + item.indirectArguments = indirectArguments; + item.named = name != nullptr; + if (item.named) item.name = name; + return AppendCompute(std::move(item)); +} + int Sequence::SyncPoint() { const std::lock_guard guard(manager->lock); RequireOpen(); @@ -318,10 +440,11 @@ int Sequence::WaitAndClose(std::uint32_t index) { const int result = WaitLocked(guard); ReturnNodes(*manager, static_cast<std::uint32_t>(items.size()) + 1); items.clear(); + commands.clear(); cursor = 0; outstanding = 0; open = false; - manager->freeSequences |= std::uint32_t{1} << index; + (compute ? manager->freeComputeSequences : manager->freeSequences) |= std::uint32_t{1} << index; return result; } @@ -353,42 +476,63 @@ bool ValidLimits(std::uint32_t invocations, std::uint32_t dispatches, std::uint3 return invocations != 0 || dispatches != 0 || syncPoints != 0 || waits != 0; } -int Factory::Create(ISequence** out, std::uint32_t invocations, std::uint32_t dispatches, std::uint32_t syncPoints, std::uint32_t waits, const char*) { +template <typename TSequences> +int CreateIn(Manager* manager, TSequences& sequences, std::uint32_t& free, std::uint32_t priority, ISequence** out, const Counters& limits) { if (out == nullptr) return SequenceNullArgument; - if (!ValidLimits(invocations, dispatches, syncPoints, waits)) return SequenceOutOfRange; + if (!ValidLimits(limits.invocations, limits.dispatches, limits.syncPoints, limits.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; + if (free == 0) return SequenceNoneFree; + const int index = std::countr_zero(free); + free &= ~(std::uint32_t{1} << index); + Sequence& sequence = sequences[static_cast<std::size_t>(index)]; + if (!sequence.Open(manager, limits, priority)) return SequenceNoNodes; *out = &sequence; return 0; } -int Factory::Destroy(ISequence* sequence) { +template <typename TSequences> +int DestroyIn(Manager* manager, TSequences& sequences, const std::uint32_t& free, ISequence* 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; + for (; index < sequences.size(); ++index) { + if (&sequences[index] == sequence) break; } - if (index == manager->sequences.size() || (manager->freeSequences & (std::uint32_t{1} << index)) != 0) return SequenceInvalid; - found = &manager->sequences[index]; + if (index == sequences.size() || (free & (std::uint32_t{1} << index)) != 0) return SequenceInvalid; + found = &sequences[index]; } static_cast<void>(found->WaitAndClose(index)); return 0; } -int Factory::CreateCompute(ISequence** 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::Create(ISequence** out, std::uint32_t invocations, std::uint32_t dispatches, std::uint32_t syncPoints, std::uint32_t waits, const char*) { + return CreateIn(manager, manager->sequences, manager->freeSequences, priority, out, {invocations, dispatches, syncPoints, waits}); } -int Factory::DestroyCompute(ISequence*) { - return SequenceInvalid; +int Factory::Destroy(ISequence* sequence) { + return DestroyIn(manager, manager->sequences, manager->freeSequences, sequence); +} + +int Factory::CreateCompute(ISequence** out, std::uint32_t invocations, std::uint32_t dispatches, std::uint32_t syncPoints, std::uint32_t waits, const char*) { + return CreateIn(manager, manager->computeSequences, manager->freeComputeSequences, priority, out, {invocations, dispatches, syncPoints, waits}); +} + +int Factory::DestroyCompute(ISequence* sequence) { + return DestroyIn(manager, manager->computeSequences, manager->freeComputeSequences, sequence); +} + +void* APS5_VABI ComputeHandlerEntry(void* argument) { + Manager& manager = *static_cast<Manager*>(argument); + for (;;) { + KernelEvent event{}; + int count = 0; + static_cast<void>(sceKernelWaitEqueue(manager.computeEvents, &event, 1, &count, nullptr)); + if (count <= 0) continue; + if (sceKernelGetEventFilter(&event) == UserEventFilter && sceKernelGetEventId(&event) == 0) return nullptr; + const std::lock_guard guard(manager.lock); + for (Sequence& sequence : manager.computeSequences) sequence.CollectCompute(); + } } bool RunOne(Manager& manager, std::unique_lock<std::mutex>& guard) { @@ -421,11 +565,16 @@ void* APS5_VABI WorkerEntry(void* argument) { return nullptr; } -std::uint32_t NodeCount(std::uint64_t memory, std::uint64_t size, std::uint32_t workers, std::uint32_t sequences, bool* fits) { +std::uint32_t NodeCount(std::uint64_t memory, std::uint64_t size, std::uint32_t workers, std::uint32_t sequences, std::uint32_t computeQueues, 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); + address = AlignUp(address, 64) + (sequences + computeQueues) * SequenceBytes; + if (computeQueues != 0) { + address = AlignUp(address + ComputeContextBytes, 0x100) + AlignUp(ComputeCommandBytes * computeQueues, 8); + address += AlignUp(4 * std::uint64_t{computeQueues}, 8) + 8 * std::uint64_t{computeQueues}; + } + address = AlignUp(address, 64); *fits = address <= end; return *fits ? static_cast<std::uint32_t>(((end - address) / NodeBytes) & 0xffff) : 0; } @@ -462,6 +611,30 @@ void StartWorkers(Manager& manager, const ConfigParams& config, std::uint32_t wo } } +void StartCompute(Manager& manager, std::uint32_t computeSequences, std::uint64_t computeQueueMask, std::uint32_t priority, std::uint32_t affinity) { + if (sceKernelCreateEqueue(&manager.computeEvents, "SceJobManagerComputeEventQueue") != 0) throw std::runtime_error("libSceJobManager: sceKernelCreateEqueue failed"); + for (std::uint64_t mask = computeQueueMask; mask != 0; mask &= mask - 1) { + const std::uint32_t queue = FirstComputeQueue + static_cast<std::uint32_t>(std::countr_zero(mask)); + if (sceAgcDriverAddEqEvent(manager.computeEvents, static_cast<int>(queue), nullptr) != 0) throw std::runtime_error("libSceJobManager: sceAgcDriverAddEqEvent failed"); + manager.computeQueues.push_back(queue); + } + manager.computeSequences.resize(computeSequences); + for (std::uint32_t index = 0; index < computeSequences; ++index) { + manager.computeSequences[index].compute = true; + manager.computeSequences[index].queue = manager.computeQueues[index]; + } + manager.freeComputeSequences = computeSequences == MaxSequences ? ~std::uint32_t{0} : (std::uint32_t{1} << computeSequences) - 1; + const KernelSchedParam schedule{static_cast<int>(priority)}; + PthreadAttr attributes = nullptr; + if (scePthreadAttrInit(&attributes) != 0 || scePthreadAttrSetinheritsched(&attributes, 0) != 0 || scePthreadAttrSetschedparam(&attributes, &schedule) != 0 || + scePthreadAttrSetstacksize(&attributes, 0x4000) != 0 || scePthreadAttrSetaffinity(&attributes, affinity) != 0) { + throw std::runtime_error("libSceJobManager: a compute handler thread attribute was rejected"); + } + const int created = scePthreadCreate(&manager.computeHandler, &attributes, ComputeHandlerEntry, &manager, "SceJobManagerComputeEventHdlr"); + scePthreadAttrDestroy(&attributes); + if (created != 0) throw std::runtime_error("libSceJobManager: scePthreadCreate failed for the compute handler"); +} + 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; @@ -479,12 +652,16 @@ int Initialize(void* self, void* memory, std::uint64_t size, std::uint32_t worke 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; + const auto computeQueues = computeSequences != 0 ? static_cast<std::uint32_t>(std::popcount(computeQueueMask)) : 0; bool fits = false; - const std::uint32_t nodes = NodeCount(base, size, workers, sequences, &fits); + const std::uint32_t nodes = NodeCount(base, size, workers, sequences, computeQueues, &fits); if (!fits || nodes == 0) return ManagerMemoryTooSmall; if (const int invalid = ValidateWorkers(settings, workers); invalid != 0) return invalid; + const bool ownHandlerSettings = (settings.version & 1) != 0; + const std::uint32_t handlerPriority = ownHandlerSettings ? settings.computePriority : 0x100; + const std::uint32_t handlerAffinity = ownHandlerSettings ? settings.computeAffinity : 0x1fff; + if (computeSequences != 0 && (handlerPriority < 0x100 || handlerPriority > 0x2ff || handlerAffinity == 0 || handlerAffinity > 0x1fff)) return ManagerInvalidArgument; auto* manager = new Manager; manager->running = true; manager->freeNodes = nodes - 1; @@ -496,6 +673,7 @@ int Initialize(void* self, void* memory, std::uint64_t size, std::uint32_t worke manager->unifiedFactories[priority].manager = manager; manager->unifiedFactories[priority].priority = priority; } + if (computeSequences != 0) StartCompute(*manager, computeSequences, computeQueueMask, handlerPriority, handlerAffinity); Implementation(self) = manager; StartWorkers(*manager, settings, workers); return 0; @@ -539,12 +717,19 @@ std::uint64_t APS5_VABI _ZN3sce3Job10JobManager27calculateRequiredMemorySizeEPKN } 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; + if (computeSequences != 0) { + const auto queues = static_cast<std::uint64_t>(std::popcount(params->computeQueueMask)); + bytes = (bytes + (params->sequences + queues) * SequenceBytes + 0x267) & ~std::uint64_t{0xff}; + bytes = (bytes + ComputeCommandBytes * queues + 3) & ~std::uint64_t{3}; + bytes = (bytes + 4 * queues + 7) & ~std::uint64_t{7}; + bytes += 8 * queues; + } else { + 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; } @@ -569,6 +754,14 @@ int APS5_VABI _ZN3sce3Job10JobManager8shutdownEv(void* self) { for (const Pthread worker : manager->workers) { if (scePthreadJoin(worker, nullptr) != 0) throw std::runtime_error("sce::Job::JobManager::shutdown: scePthreadJoin failed"); } + if (manager->computeHandler != nullptr) { + if (sceKernelAddUserEventEdge(manager->computeEvents, 0) != 0 || sceKernelTriggerUserEvent(manager->computeEvents, 0, nullptr) != 0) { + throw std::runtime_error("sce::Job::JobManager::shutdown: the compute handler could not be signalled"); + } + if (scePthreadJoin(manager->computeHandler, nullptr) != 0) throw std::runtime_error("sce::Job::JobManager::shutdown: scePthreadJoin failed"); + for (const std::uint32_t queue : manager->computeQueues) static_cast<void>(sceAgcDriverDeleteEqEvent(manager->computeEvents, static_cast<int>(queue))); + if (sceKernelDeleteEqueue(manager->computeEvents) != 0) throw std::runtime_error("sce::Job::JobManager::shutdown: sceKernelDeleteEqueue failed"); + } Implementation(self) = nullptr; delete manager; return 0; diff --git a/docs/dev/debt/libSceJobManager.md b/docs/dev/debt/libSceJobManager.md index 023da38c79..f1a6c84fee 100644 --- a/docs/dev/debt/libSceJobManager.md +++ b/docs/dev/debt/libSceJobManager.md @@ -2,4 +2,4 @@ ### Functional -- [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, `shutdown` throws while a thread is blocked in a sequence wait, and using a sequence after its `destroy` throws. `initialize` returns 0 where the original returns the result of its last import (`BG26hBGiNlw`, not resolved to a name), the `sceRazorCpuJobManager*` profiler calls are not made, and a worker thread that cannot be created or configured throws where the original does not check. 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 +- [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. Compute sequences submit through `sceAgcDriverSubmitAcb` on the queue their mask bit selects and are checked the same way by `agc_driver_job_manager_compute_tests`: results, ordering against CPU dispatches and sync points, limits and sizes match the two modules. The command stream is simpler than the original's: every compute dispatch is submitted with its own label write and interrupt, where the original batches the label write and rewinds commands it encoded ahead, and the command buffers and labels live on the host heap. The per-queue command size in `calculateRequiredMemorySize` is a constant (104, 148 and 32 bytes per dispatch, sync point and wait), which is what the original measures by running the builders of this project's libSceAgc. A compute dispatch on a CPU sequence throws (the original does not check), and a failing `sceAgcDriverAddEqEvent` throws. 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, `shutdown` throws while a thread is blocked in a sequence wait, and using a sequence after its `destroy` throws. `initialize` returns 0 where the original returns the result of its last import (`BG26hBGiNlw`, not resolved to a name), the `sceRazorCpuJobManager*` profiler calls are not made, and a worker thread that cannot be created or configured throws where the original does not check. 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 From 394ac0194688945c7c0774c81753d58c623efaf6 Mon Sep 17 00:00:00 2001 From: fma-rc <188743521+fma-rc@users.noreply.github.com> Date: Sun, 11 Oct 2026 02:37:10 +0200 Subject: [PATCH 0920/1000] fix(libSceJobManager): refuse shutdown with compute work in flight and check the handler wait --- .../tests/execution/JobManagerCompute.cpp | 18 +++++++++++++++--- core/libs/prx/libSceJobManager/Export.cpp | 8 ++++++-- docs/dev/debt/libSceJobManager.md | 2 +- 3 files changed, 22 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/JobManagerCompute.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/JobManagerCompute.cpp index 79af7f5d54..c149aa41a9 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/JobManagerCompute.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/JobManagerCompute.cpp @@ -152,14 +152,19 @@ void SizeCase() { } void RunCase() { - const std::uint64_t size = SizeFor(1, 1, true); + const std::uint64_t size = SizeFor(1, 2, true); void* memory = ::operator new((size + 255) & ~std::uint64_t{255}, std::align_val_t{256}); Require(memory != nullptr, "allocation failed"); alignas(64) static unsigned char manager[0x100]; Construct(manager); const ConfigParams badHandler{1, 0x00021fff, 0x00010000, 0x2bc, nullptr, 0, 0x1fff, 0x50, 0}; - Require(Initialize(manager, memory, size, 2, 1, 1, 1, &badHandler) == InvalidArgument, "a handler priority below 0x100 was accepted"); - Require(Initialize(manager, memory, size, 2, 1, 1, 1, nullptr) == 0, "initialize with one compute sequence failed"); + const ConfigParams noAffinity{1, 0x00021fff, 0x00010000, 0x2bc, nullptr, 0, 0, 0x100, 0}; + const ConfigParams wideAffinity{1, 0x00021fff, 0x00010000, 0x2bc, nullptr, 0, 0x2000, 0x100, 0}; + Require(Initialize(manager, memory, size, 2, 1, 1, 2, &badHandler) == InvalidArgument, "a handler priority below 0x100 was accepted"); + Require(Initialize(manager, memory, size, 2, 1, 1, 2, &noAffinity) == InvalidArgument, "an empty handler affinity was accepted"); + Require(Initialize(manager, memory, size, 2, 1, 1, 2, &wideAffinity) == InvalidArgument, "a handler affinity beyond 13 cores was accepted"); + Require(Initialize(manager, memory, size, 2, 1, 0xffffffffu, 2, nullptr) == InvalidArgument, "a sequence count that wraps was accepted"); + Require(Initialize(manager, memory, size, 2, 1, 1, 2, nullptr) == 0, "initialize with one compute sequence failed"); SequenceFactory* factory = Factories(manager, 0); Sequence* sequence = nullptr; Sequence* other = nullptr; @@ -203,6 +208,13 @@ void RunCase() { Require(Output[0] == 0x33330003u, "the compute dispatch behind a sync point did not run"); Require(SeenByJob == 0x33330003u, "a CPU job behind a sync point ran before the compute dispatch ahead of it finished"); + Value[0] = 0x44440004u; + Require(sequence->DispatchCompute(ShaderWithoutDispatch.data(), Table.data(), 1, 1, 1, nullptr) == 0, "the leading dispatch failed"); + Require(sequence->SyncPoint() == 0, "the sync point between compute dispatches failed"); + Require(sequence->DispatchCompute(Shader.data(), Table.data(), 1, 1, 1, "behind") == 0, "the compute dispatch behind a compute dispatch failed"); + Require(sequence->Wait() == 0, "the wait after chained compute dispatches failed"); + Require(Output[0] == 0x44440004u, "a compute dispatch behind a sync point and a compute dispatch did not run"); + for (int index = 0; index < 4; ++index) Require(sequence->DispatchCompute(Shader.data(), Table.data(), 1, 1, 1, nullptr) == 0, "a compute dispatch within the limit failed"); Require(sequence->DispatchCompute(Shader.data(), Table.data(), 1, 1, 1, nullptr) == LimitExceeded, "a compute dispatch past the dispatch limit was accepted"); Require(sequence->DispatchCompute(Shader.data(), Table.data(), 1, 1, 1, nullptr) == Invalid, "a latched compute sequence accepted a dispatch"); diff --git a/core/libs/prx/libSceJobManager/Export.cpp b/core/libs/prx/libSceJobManager/Export.cpp index 9ed6abee66..4c03e4254e 100644 --- a/core/libs/prx/libSceJobManager/Export.cpp +++ b/core/libs/prx/libSceJobManager/Export.cpp @@ -176,6 +176,7 @@ public: void Advance(); void DispatchCompleted(std::uint32_t invocations); void CollectCompute(); + bool Busy() const { return outstanding != 0; } Manager* manager = nullptr; bool open = false; @@ -527,7 +528,7 @@ void* APS5_VABI ComputeHandlerEntry(void* argument) { for (;;) { KernelEvent event{}; int count = 0; - static_cast<void>(sceKernelWaitEqueue(manager.computeEvents, &event, 1, &count, nullptr)); + if (sceKernelWaitEqueue(manager.computeEvents, &event, 1, &count, nullptr) != 0) throw std::runtime_error("libSceJobManager: sceKernelWaitEqueue failed in the compute handler"); if (count <= 0) continue; if (sceKernelGetEventFilter(&event) == UserEventFilter && sceKernelGetEventId(&event) == 0) return nullptr; const std::lock_guard guard(manager.lock); @@ -642,7 +643,7 @@ int Initialize(void* self, void* memory, std::uint64_t size, std::uint32_t worke 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; + if ((sequences == 0 && computeSequences == 0) || std::uint64_t{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; } @@ -748,6 +749,9 @@ int APS5_VABI _ZN3sce3Job10JobManager8shutdownEv(void* self) { { 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"); + for (const Sequence& sequence : manager->computeSequences) { + if (sequence.open && sequence.Busy()) throw std::runtime_error("sce::Job::JobManager::shutdown: a compute sequence still has work in flight"); + } manager->running = false; manager->work.notify_all(); } diff --git a/docs/dev/debt/libSceJobManager.md b/docs/dev/debt/libSceJobManager.md index f1a6c84fee..123977e935 100644 --- a/docs/dev/debt/libSceJobManager.md +++ b/docs/dev/debt/libSceJobManager.md @@ -2,4 +2,4 @@ ### Functional -- [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. Compute sequences submit through `sceAgcDriverSubmitAcb` on the queue their mask bit selects and are checked the same way by `agc_driver_job_manager_compute_tests`: results, ordering against CPU dispatches and sync points, limits and sizes match the two modules. The command stream is simpler than the original's: every compute dispatch is submitted with its own label write and interrupt, where the original batches the label write and rewinds commands it encoded ahead, and the command buffers and labels live on the host heap. The per-queue command size in `calculateRequiredMemorySize` is a constant (104, 148 and 32 bytes per dispatch, sync point and wait), which is what the original measures by running the builders of this project's libSceAgc. A compute dispatch on a CPU sequence throws (the original does not check), and a failing `sceAgcDriverAddEqEvent` throws. 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, `shutdown` throws while a thread is blocked in a sequence wait, and using a sequence after its `destroy` throws. `initialize` returns 0 where the original returns the result of its last import (`BG26hBGiNlw`, not resolved to a name), the `sceRazorCpuJobManager*` profiler calls are not made, and a worker thread that cannot be created or configured throws where the original does not check. 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 +- [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. Compute sequences submit through `sceAgcDriverSubmitAcb` on the queue their mask bit selects and are checked the same way by `agc_driver_job_manager_compute_tests`: results, ordering against CPU dispatches and sync points, limits and sizes match the two modules. The command stream is simpler than the original's: every compute dispatch is submitted with its own label write and interrupt, where the original batches the label write and rewinds commands it encoded ahead, and the command buffers and labels live on the host heap. The per-queue command size in `calculateRequiredMemorySize` is a constant (104, 148 and 32 bytes per dispatch, sync point and wait), which is what the original measures by running the builders of this project's libSceAgc. A compute dispatch on a CPU sequence throws (the original does not check), and a failing `sceAgcDriverAddEqEvent` throws. `shutdown` throws while a compute sequence has work in flight, a sequence's label total is written as 32 bits, and since the label is host memory no GPU import covers, the driver drains recorded work before each label write. 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, `shutdown` throws while a thread is blocked in a sequence wait, and using a sequence after its `destroy` throws. `initialize` returns 0 where the original returns the result of its last import (`BG26hBGiNlw`, not resolved to a name), the `sceRazorCpuJobManager*` profiler calls are not made, and a worker thread that cannot be created or configured throws where the original does not check. 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 From 68355e42698e8d9678eea8dfa836889db67e60da Mon Sep 17 00:00:00 2001 From: MontyMcK <MontyMcK@users.noreply.github.com> Date: Sun, 18 Oct 2026 13:20:55 +1300 Subject: [PATCH 0921/1000] refactor(libSceHmd2): implement the exports where their stubs were --- core/libs/prx/libSceHmd2/Export.cpp | 125 ++++++++++++++-------------- docs/dev/debt/libSceHmd2.md | 5 +- docs/dev/debt/libScePad.md | 2 +- 3 files changed, 68 insertions(+), 64 deletions(-) diff --git a/core/libs/prx/libSceHmd2/Export.cpp b/core/libs/prx/libSceHmd2/Export.cpp index b23019b0df..55d518e133 100644 --- a/core/libs/prx/libSceHmd2/Export.cpp +++ b/core/libs/prx/libSceHmd2/Export.cpp @@ -42,6 +42,16 @@ std::int32_t GetDeviceInformation(Hmd2::DeviceInformation* info) { extern "C" { +int APS5_VABI sceHmd2ReprojectionGetStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2ReprojectionQuerySeeThroughBufferSizeAlign() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + std::int32_t APS5_VABI sceHmd2Initialize(const void* param) { std::lock_guard lock(stateMutex); if (initialized) return Hmd2::AlreadyInitialized; @@ -50,6 +60,21 @@ std::int32_t APS5_VABI sceHmd2Initialize(const void* param) { return 0; } +std::int32_t APS5_VABI sceHmd2GetFieldOfView(std::int32_t handle, Hmd2::FieldOfView* fieldOfView) { + std::lock_guard lock(stateMutex); + if (!fieldOfView) return Hmd2::ParameterNull; + if (handle == 0 || handle != openHandle) return Hmd2::InvalidHandle; + if (!initialized) return Hmd2::NotInitialized; + *fieldOfView = DefaultFieldOfView; + return 0; +} + +std::int32_t APS5_VABI sceHmd2GetDeviceInformationByHandle(std::int32_t handle, Hmd2::DeviceInformation* info) { + std::lock_guard lock(stateMutex); + if (handle != openHandle) return Hmd2::InvalidHandle; + return GetDeviceInformation(info); +} + std::int32_t APS5_VABI sceHmd2Terminate() { std::lock_guard lock(stateMutex); if (!initialized) return Hmd2::NotInitialized; @@ -57,6 +82,37 @@ std::int32_t APS5_VABI sceHmd2Terminate() { return 0; } +std::int32_t APS5_VABI sceHmd2Close(std::int32_t handle) { + std::lock_guard lock(stateMutex); + if (!initialized) return Hmd2::NotInitialized; + if (handle != openHandle) return Hmd2::InvalidHandle; + openHandle = 0; + return 0; +} + +int APS5_VABI sceHmd2GazeGetResult() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2GazeGetResultForFoveatedRendering() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +std::int32_t APS5_VABI sceHmd2GetDeviceInformation(Hmd2::DeviceInformation* info) { + std::lock_guard lock(stateMutex); + return GetDeviceInformation(info); +} + +std::int32_t APS5_VABI sceHmd2GetFieldOfViewWithoutHandle(Hmd2::FieldOfView* fieldOfView) { + std::lock_guard lock(stateMutex); + if (!fieldOfView) return Hmd2::ParameterNull; + if (!initialized) return Hmd2::NotInitialized; + *fieldOfView = DefaultFieldOfView; + return 0; +} + std::int32_t APS5_VABI sceHmd2Open(std::int32_t userId, std::int32_t type, std::int32_t index, const void* param) { std::lock_guard lock(stateMutex); if (!initialized) return Hmd2::NotInitialized; @@ -70,68 +126,6 @@ std::int32_t APS5_VABI sceHmd2Open(std::int32_t userId, std::int32_t type, std:: return DeviceHandle; } -std::int32_t APS5_VABI sceHmd2Close(std::int32_t handle) { - std::lock_guard lock(stateMutex); - if (!initialized) return Hmd2::NotInitialized; - if (handle != openHandle) return Hmd2::InvalidHandle; - openHandle = 0; - return 0; -} - -std::int32_t APS5_VABI sceHmd2GetDeviceInformation(Hmd2::DeviceInformation* info) { - std::lock_guard lock(stateMutex); - return GetDeviceInformation(info); -} - -std::int32_t APS5_VABI sceHmd2GetDeviceInformationByHandle(std::int32_t handle, Hmd2::DeviceInformation* info) { - std::lock_guard lock(stateMutex); - if (handle != openHandle) return Hmd2::InvalidHandle; - return GetDeviceInformation(info); -} - -std::int32_t APS5_VABI sceHmd2GetFieldOfView(std::int32_t handle, Hmd2::FieldOfView* fieldOfView) { - std::lock_guard lock(stateMutex); - if (!fieldOfView) return Hmd2::ParameterNull; - if (handle == 0 || handle != openHandle) return Hmd2::InvalidHandle; - if (!initialized) return Hmd2::NotInitialized; - *fieldOfView = DefaultFieldOfView; - return 0; -} - -std::int32_t APS5_VABI sceHmd2GetFieldOfViewWithoutHandle(Hmd2::FieldOfView* fieldOfView) { - std::lock_guard lock(stateMutex); - if (!fieldOfView) return Hmd2::ParameterNull; - if (!initialized) return Hmd2::NotInitialized; - *fieldOfView = DefaultFieldOfView; - return 0; -} - -std::int32_t APS5_VABI sceHmd2SetVibration(std::int32_t handle, std::uint32_t level) { - (void)level; - std::lock_guard lock(stateMutex); - return handle == openHandle ? 0 : Hmd2::InvalidHandle; -} - -int APS5_VABI sceHmd2ReprojectionGetStatus() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2ReprojectionQuerySeeThroughBufferSizeAlign() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2GazeGetResult() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2GazeGetResultForFoveatedRendering() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceHmd2ReprojectionBeginFrame() { NotImplemented_nid_no_patch(__func__); return 0; @@ -222,4 +216,11 @@ int APS5_VABI sceHmd2ReprojectionTerminate() { return 0; } +std::int32_t APS5_VABI sceHmd2SetVibration(std::int32_t handle, std::uint32_t level) { + (void)level; + std::lock_guard lock(stateMutex); + if (handle != openHandle) return Hmd2::InvalidHandle; + return 0; +} + } diff --git a/docs/dev/debt/libSceHmd2.md b/docs/dev/debt/libSceHmd2.md index 70033deefa..050b0f7c9d 100644 --- a/docs/dev/debt/libSceHmd2.md +++ b/docs/dev/debt/libSceHmd2.md @@ -8,8 +8,11 @@ - PPSA29866 imports declared without parameters, signatures unknown, added without an implementation because the title fails to load without them: [sceHmd2ReprojectionQuerySeeThroughBufferSizeAlign](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2), [sceVideoOutSetFlipMaster and sceVideoOutClearFlipMaster](../../../core/libs/prx/libSceVideoOut/Unimplemented.cpp) (libSceVideoOut) - PPSA09030 imports from its `Media/Plugins/psvr2.prx` declared without parameters, signatures unknown, added without an implementation because the title fails to load the plugin without them: [sceHmd2ReprojectionGetStatus](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2), [sceVrTracker2GetCoordinateSystem and sceVrTracker2GetPlayAreaBoundaryGeometry](../../../core/libs/prx/libSceVrTracker2/Export.cpp) (libSceVrTracker2) -- [sceHmd2Initialize](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - the firmware also fails with 0x8a72001f when `sceKernelGetAppInfo` fails and with 0x8a72000a for some applications, depending on app info fields at 0x2c, 0x4b and 0x4d whose meaning is unknown, and then calls `sceMbusInit`; none of this is modelled - [sceHmd2Open](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - the handle is the value the console's driver returned to the first open; titles built with an SDK before 7.00 may open for any user, and the firmware's `sceMbusAddHandleByUserId` result for a user other than the initial one is unknown, so that case throws. 0x8a720006 for a second open is named `AlreadyOpened` from its use - [sceHmd2SetVibration](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - the second parameter is checked against 25 only while a headset is connected; its name and unit are unknown. The field of view member names are assumed: of the firmware's default tangents the first is the widest and the last two are equal - PPSA01317 imports declared without parameters, signatures unknown: the [libSceHmd2](../../../core/libs/prx/libSceHmd2/Export.cpp) exports that throw and the remaining [libSceVrTracker2](../../../core/libs/prx/libSceVrTracker2/Export.cpp) exports. Names from the aerolib NID list - PPSA15878 imports declared without parameters, signatures unknown: the [libSceHmd2](../../../core/libs/prx/libSceHmd2/Export.cpp) reprojection and gaze exports, `sceVrTracker2Finalize`, `sceVrTracker2GetPlayAreaOrientedBoundingBox` and `sceVrTracker2LocateCoordinateSystem` in [libSceVrTracker2](../../../core/libs/prx/libSceVrTracker2/Export.cpp), and the `scePadVrController` trigger effect exports in [libScePad](../../../core/libs/prx/libScePad/Export.cpp). Names from the aerolib NID list + +### Functional + +- [sceHmd2Initialize](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - the firmware also fails with 0x8a72001f when `sceKernelGetAppInfo` fails and with 0x8a72000a for some applications, depending on app info fields at 0x2c, 0x4b and 0x4d whose meaning is unknown, and then calls `sceMbusInit`; none of this is modelled diff --git a/docs/dev/debt/libScePad.md b/docs/dev/debt/libScePad.md index 291c86d4e2..6ec836b141 100644 --- a/docs/dev/debt/libScePad.md +++ b/docs/dev/debt/libScePad.md @@ -6,9 +6,9 @@ - [scePadSetAngularVelocityDeadbandState](../../../core/libs/prx/libScePad/src/PadState.cpp) (libScePad) - with the deadband on, each angular velocity axis whose magnitude is below 0.017453292 rad/s (1 degree per second) reads as 0, before the orientation is integrated; it starts off. Threshold, per-axis test and default follow [duaLib](https://github.com/WujekFoliarz/duaLib/blob/03bad1bea2a36561b520846776f2a07ced6773e0/src/source/duaLib.cpp#L44), which reimplements the PC `libScePad.dll`; the console's threshold is not measured, and the deadband is kept per library rather than per handle since one pad handle exists - [scePadOpen and scePadOpenExt](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - special ports (type 2) return `DEVICE_NOT_CONNECTED` (0x80920007) because the host has no special controller, and `scePadGetHandle` has no handle for them; whether the console opens a handle and reports the device as disconnected later is unknown. scePadOpenExt handles only special ports. PPSA01317 passes a 16-byte param that names the device by vendor and product id (`b7 0e 04 0e ...` for 0x0eb7:0x0e04) and skips the user on any negative result. Other port types throw - [scePadVrControllerReadState](../../../core/libs/prx/libScePad/Export.cpp) (libScePad, NID `iA-DdobUen8`) - the name hashes to the imported NID; the `(int handle, void* state)` signature follows [PS5VR](https://github.com/theghostonline/PS5VR/blob/0e408909ce7a39891a2e06eee3438d07a09e11e3/app/src/app_vr.c#L326-L369) (PPSA99177), which passes a PS VR2 Sense controller handle from `scePadOpen` type 3 or 4 and a 0x68-byte state it reads as buttons at 0, stick bytes at 4 and a connected byte at 0x40. The title calls it only after `sceHmd2Initialize` succeeds, which it does not here, so every call throws; the state layout is otherwise unknown +- PPSA07411 PS VR2 imports declared without parameters, signatures unknown, added without an implementation because the title fails to load without them: [scePadVrControllerSetVibration, scePadVrControllerSetVibrationMode and scePadVrControllerReadCombinedState](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) and [sceVrTracker2ResetLocalCoordinate](../../../core/libs/prx/libSceVrTracker2/Export.cpp) (libSceVrTracker2) - [scePadSetVibrationMode](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - modes 1 to 3 and 5 are accepted and stored. On a PS5 (firmware 13.60) the call returns `0x80920005` before `scePadInit`, then rejects a mode of 0 or below with `INVALID_ARG` before it checks the handle, and returns `INVALID_HANDLE` for a bad handle with any higher mode; whether an open handle accepts modes above 3 was not measured, since the payload could not open a pad; 5 is accepted because PPSA25286 passes it on its open handle at start-up; the meanings of modes 3 and 5 are unknown (PPSA21480 calls it with 3 right after `scePadOpen` and aborts on failure; [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Pad/Pad.cs) names only 1 and 2). The stored mode has no other effect - [scePadIsRemoteController](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - the name hashes to NID `fCWdlnmB1Ks`; the signature `(int handle, bool* remote)` comes from PPSA16064, which zeroes a byte, passes its address after `scePadGetControllerInformation` and ignores the result. The error codes follow the other pad calls. Controllers are local devices, so the answer is always false - [scePadSetAngularVelocityBiasCorrectionState](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - inferred signature `(int handle, bool enabled)` follows `scePadSetTiltCorrectionState` and `scePadSetAngularVelocityDeadbandState`; an invalid handle returns `PAD_ERROR_INVALID_HANDLE`, disabling is a no-op, and enabling throws because bias correction is not modelled - [scePadUnknown_zBsgF0q8DIM](../../../core/libs/prx/libScePad/Export.cpp) (libScePad, NID `zBsgF0q8DIM`) - PPSA03671 imports it and fails to load without it. The aerolib list has no name for the NID, and none of the pad names in SharpProspero, shadPS4 or OpenOrbis, nor the trigger effect, motion sensor, VR controller, HD and haptic names tried, hashes to it. It is declared without parameters and throws. The title's executable has an import stub for it but no call, jump or address reference to that stub, so the argument count and whether the result is used are unknown - [scePadUnknown_n3kSX62fgNo](../../../core/libs/prx/libScePad/Export.cpp) (libScePad, NID `n3kSX62fgNo`) - PPSA01530 and PPSA13356 import it and fail to load without it. The real name is unknown; it is declared without parameters and throws -- PPSA07411 PS VR2 imports declared without parameters, signatures unknown, added without an implementation because the title fails to load without them: [scePadVrControllerSetVibration, scePadVrControllerSetVibrationMode and scePadVrControllerReadCombinedState](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) and [sceVrTracker2ResetLocalCoordinate](../../../core/libs/prx/libSceVrTracker2/Export.cpp) (libSceVrTracker2) From 5e340fa54d44a55d6bfab7dc748008e2f60abe1d Mon Sep 17 00:00:00 2001 From: blu <131330772+ycbuzi@users.noreply.github.com> Date: Sun, 18 Oct 2026 13:37:21 +1300 Subject: [PATCH 0922/1000] fix(relinker): append the test temp directory instead of replacing the environment set_tests_properties replaced the ASAN_OPTIONS that add_asan_test_options appends to guest_module_args, guest_deferred_dependencies and guest_module_fini in ASan builds. Append the TMPDIR, TEMP and TMP settings instead. --- core/relinker/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index dab82857bf..11c7ea959a 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -739,7 +739,7 @@ if(BUILD_TESTING) set(relinkerTestTemp ${CMAKE_CURRENT_BINARY_DIR}/test-temp) file(MAKE_DIRECTORY ${relinkerTestTemp}) get_property(relinkerTests DIRECTORY PROPERTY TESTS) - set_tests_properties(${relinkerTests} PROPERTIES ENVIRONMENT_MODIFICATION + set_property(TEST ${relinkerTests} APPEND PROPERTY ENVIRONMENT_MODIFICATION "TMPDIR=set:${relinkerTestTemp};TEMP=set:${relinkerTestTemp};TMP=set:${relinkerTestTemp}") endif() From 06379ceac760effb19b113b4746df98daf8fce56 Mon Sep 17 00:00:00 2001 From: MarcZX100 <MarcZX100@users.noreply.github.com> Date: Sun, 18 Oct 2026 00:37:29 +0000 Subject: [PATCH 0923/1000] fix(shader): match barycentric tessellation clip interfaces --- .../prx/libSceAgcDriver/tests/Graphics.cpp | 13 +++++- .../src/BarycentricGeometryShader.cpp | 40 ++++++++++++++----- 2 files changed, 43 insertions(+), 10 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index c4d38792ee..62e224d7ee 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -5906,8 +5906,19 @@ void barycentricEmulationTests() { } return false; }; + const auto variableBuiltIn = [](const RecompileResult& result, spv::BuiltIn builtin) { + const auto& words = result.spirv.Words(); + std::set<std::uint32_t> variables; + for (std::size_t at = 5; at < words.size() && (words[at] >> 16u) != 0; at += words[at] >> 16u) { + if (static_cast<spv::Op>(words[at] & 0xffffu) == spv::OpVariable) variables.insert(words[at + 2]); + } + for (std::size_t at = 5; at < words.size() && (words[at] >> 16u) != 0; at += words[at] >> 16u) { + if (static_cast<spv::Op>(words[at] & 0xffffu) == spv::OpDecorate && words[at + 2] == spv::DecorationBuiltIn && words[at + 3] == builtin && variables.contains(words[at + 1])) return true; + } + return false; + }; Require(!memberBuiltIn(patches.control, spv::BuiltInClipDistance) && !memberBuiltIn(patches.evaluation, spv::BuiltInClipDistance), "a generated tessellation stage declares clip distances the vertex shader does not write"); - Require(memberBuiltIn(clippedPatches.control, spv::BuiltInClipDistance) && memberBuiltIn(clippedPatches.evaluation, spv::BuiltInClipDistance), "a generated tessellation stage does not forward the vertex clip distances"); + Require(!memberBuiltIn(clippedPatches.control, spv::BuiltInClipDistance) && !memberBuiltIn(clippedPatches.evaluation, spv::BuiltInClipDistance) && variableBuiltIn(clippedPatches.control, spv::BuiltInClipDistance) && variableBuiltIn(clippedPatches.evaluation, spv::BuiltInClipDistance), "generated tessellation stages do not forward clip distances as separate built-in variables"); const std::array<CompiledShader, 4> clippedShaders{{{ShaderStage::Vertex, &clipped, 0}, {ShaderStage::TessellationControl, &clippedPatches.control, 0}, {ShaderStage::TessellationEvaluation, &clippedPatches.evaluation, 0}, {ShaderStage::Fragment, &fragment, 0}}}; ValidateShaders(clippedShaders, state, subgroup, false, false, false, false, false, false, true); vertex.parameterExports.clear(); diff --git a/core/shader/recompiler/SpirvBackend/src/BarycentricGeometryShader.cpp b/core/shader/recompiler/SpirvBackend/src/BarycentricGeometryShader.cpp index 8b9b6d5710..a57a5d2454 100644 --- a/core/shader/recompiler/SpirvBackend/src/BarycentricGeometryShader.cpp +++ b/core/shader/recompiler/SpirvBackend/src/BarycentricGeometryShader.cpp @@ -188,12 +188,13 @@ public: functionType = builder.Type(spv::OpTypeFunction, voidType); if (clipDistances != 0u) { clipArrayType = builder.Type(spv::OpTypeArray, floatType, constant(uintType, clipDistances)); - perVertexType = builder.DecoratedType(spv::OpTypeStruct, {{spv::OpMemberDecorate, {0u, spv::DecorationBuiltIn, spv::BuiltInPosition}}, {spv::OpMemberDecorate, {1u, spv::DecorationBuiltIn, spv::BuiltInClipDistance}}, {spv::OpDecorate, {spv::DecorationBlock}}}, vec4FloatType, clipArrayType); - ptrInputClip = builder.Type(spv::OpTypePointer, spv::StorageClassInput, clipArrayType); - ptrOutputClip = builder.Type(spv::OpTypePointer, spv::StorageClassOutput, clipArrayType); - } else { - perVertexType = builder.DecoratedType(spv::OpTypeStruct, {{spv::OpMemberDecorate, {0u, spv::DecorationBuiltIn, spv::BuiltInPosition}}, {spv::OpDecorate, {spv::DecorationBlock}}}, vec4FloatType); + controlInputClipType = builder.Type(spv::OpTypeArray, clipArrayType, constant(uintType, 3u)); + ptrControlInputClip = builder.Type(spv::OpTypePointer, spv::StorageClassInput, controlInputClipType); + ptrControlOutputClip = builder.Type(spv::OpTypePointer, spv::StorageClassOutput, clipArrayType); + ptrEvaluationInputClip = builder.Type(spv::OpTypePointer, spv::StorageClassInput, controlInputClipType); + ptrEvaluationOutputClip = builder.Type(spv::OpTypePointer, spv::StorageClassOutput, clipArrayType); } + perVertexType = builder.DecoratedType(spv::OpTypeStruct, {{spv::OpMemberDecorate, {0u, spv::DecorationBuiltIn, spv::BuiltInPosition}}, {spv::OpDecorate, {spv::DecorationBlock}}}, vec4FloatType); perVertexArrayType = builder.Type(spv::OpTypeArray, perVertexType, constant(uintType, 3u)); ptrInputVec4 = builder.Type(spv::OpTypePointer, spv::StorageClassInput, vec4FloatType); ptrOutputVec4 = builder.Type(spv::OpTypePointer, spv::StorageClassOutput, vec4FloatType); @@ -207,6 +208,14 @@ public: decorate(invocationId, spv::DecorationBuiltIn, spv::BuiltInInvocationId); const auto glIn = addInterface(spv::StorageClassInput, perVertexArrayType); const auto glOut = addInterface(spv::StorageClassOutput, perVertexArrayType); + std::uint32_t clipIn = 0u; + std::uint32_t clipOut = 0u; + if (clipDistances != 0u) { + clipIn = addInterface(spv::StorageClassInput, controlInputClipType); + decorate(clipIn, spv::DecorationBuiltIn, spv::BuiltInClipDistance); + clipOut = addInterface(spv::StorageClassOutput, clipArrayType); + decorate(clipOut, spv::DecorationBuiltIn, spv::BuiltInClipDistance); + } const auto floatArray = [&](std::uint32_t size) { return builder.Type(spv::OpTypeArray, floatType, constant(uintType, size)); }; const auto outer = addInterface(spv::StorageClassOutput, floatArray(4u)); decorate(outer, spv::DecorationBuiltIn, spv::BuiltInTessLevelOuter); @@ -239,7 +248,7 @@ public: builder.AddFunction(spv::OpBranch, afterLevels); builder.AddFunction(spv::OpLabel, afterLevels); store(result(spv::OpAccessChain, ptrOutputVec4, glOut, invocation, constant(intType, 0u)), load(vec4FloatType, result(spv::OpAccessChain, ptrInputVec4, glIn, invocation, constant(intType, 0u)))); - if (clipDistances != 0u) store(result(spv::OpAccessChain, ptrOutputClip, glOut, invocation, constant(intType, 1u)), load(clipArrayType, result(spv::OpAccessChain, ptrInputClip, glIn, invocation, constant(intType, 1u)))); + if (clipDistances != 0u) store(result(spv::OpAccessChain, ptrControlOutputClip, clipOut, invocation), load(clipArrayType, result(spv::OpAccessChain, ptrControlInputClip, clipIn, invocation))); for (const auto& [input, output] : parameters) store(result(spv::OpAccessChain, ptrOutputVec4, output, invocation), load(vec4FloatType, result(spv::OpAccessChain, ptrInputVec4, input, invocation))); return finish(); } @@ -250,12 +259,21 @@ public: const auto tessCoord = addInterface(spv::StorageClassInput, vec3FloatType); decorate(tessCoord, spv::DecorationBuiltIn, spv::BuiltInTessCoord); const auto glIn = addInterface(spv::StorageClassInput, perVertexArrayType); + std::uint32_t clipIn = 0u; + if (clipDistances != 0u) { + clipIn = addInterface(spv::StorageClassInput, controlInputClipType); + decorate(clipIn, spv::DecorationBuiltIn, spv::BuiltInClipDistance); + } std::array<std::uint32_t, ParameterLocations> inputs {}; for (const auto location : sources) { inputs[location] = addInterface(spv::StorageClassInput, vertexArrayType); decorate(inputs[location], spv::DecorationLocation, location); } const auto glOut = addInterface(spv::StorageClassOutput, perVertexType); + if (clipDistances != 0u) { + evaluationClipOutput = addInterface(spv::StorageClassOutput, clipArrayType); + decorate(evaluationClipOutput, spv::DecorationBuiltIn, spv::BuiltInClipDistance); + } std::vector<std::uint32_t> passedOutputs; for (const auto location : passed) { passedOutputs.push_back(addInterface(spv::StorageClassOutput, vec4FloatType)); @@ -289,7 +307,7 @@ public: const auto weights = result(spv::OpCompositeConstruct, vec3FloatType, weight(0u), weight(1u), weight(2u)); store(result(spv::OpAccessChain, ptrOutputVec4, glOut, constant(intType, 0u)), load(vec4FloatType, result(spv::OpAccessChain, ptrInputVec4, glIn, index, constant(intType, 0u)))); - if (clipDistances != 0u) store(result(spv::OpAccessChain, ptrOutputClip, glOut, constant(intType, 1u)), load(clipArrayType, result(spv::OpAccessChain, ptrInputClip, glIn, index, constant(intType, 1u)))); + if (clipDistances != 0u) store(result(spv::OpAccessChain, ptrEvaluationOutputClip, evaluationClipOutput), load(clipArrayType, result(spv::OpAccessChain, ptrEvaluationInputClip, clipIn, index))); for (std::size_t i = 0; i < passed.size(); ++i) store(passedOutputs[i], load(vec4FloatType, result(spv::OpAccessChain, ptrInputVec4, inputs[passed[i]], passedFlat[i] ? constant(intType, 0u) : index))); if (smooth != 0u) store(smooth, weights); if (linear != 0u) store(linear, weights); @@ -316,10 +334,14 @@ private: std::uint32_t perVertexType = 0; std::uint32_t perVertexArrayType = 0; std::uint32_t clipArrayType = 0; + std::uint32_t controlInputClipType = 0; std::uint32_t ptrInputVec4 = 0; std::uint32_t ptrOutputVec4 = 0; - std::uint32_t ptrInputClip = 0; - std::uint32_t ptrOutputClip = 0; + std::uint32_t ptrControlInputClip = 0; + std::uint32_t ptrControlOutputClip = 0; + std::uint32_t ptrEvaluationInputClip = 0; + std::uint32_t ptrEvaluationOutputClip = 0; + std::uint32_t evaluationClipOutput = 0; void begin() { builder.EmitCapability(spv::CapabilityShader); From 6c47f92b52a70d8f39877b1261f564d9213a3ae8 Mon Sep 17 00:00:00 2001 From: milescoviello <kitslayer29@gmail.com> Date: Sun, 18 Oct 2026 00:43:33 +0000 Subject: [PATCH 0924/1000] fix(shader): pass clip distances through the rect-list stages as the vertex shader writes them The recompiled vertex shader writes gl_ClipDistance through a variable of its own beside its position block, but the rect-list control and evaluation stages read and wrote it as a second member of their per-vertex block, so the stage interfaces did not match (VUID-RuntimeSpirv-OpVariable-08746). They now take and forward the clip distances through ClipDistance variables of their own. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- .../prx/libSceAgcDriver/tests/Graphics.cpp | 32 +++++++++++++++++++ .../SpirvBackend/src/RectListShaders.cpp | 23 +++++++++---- 2 files changed, 48 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index c4d38792ee..720aa42f40 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -6042,6 +6042,23 @@ void registeredReadableCoverTests() { ::operator delete(memory, std::align_val_t{block}); } +std::size_t VariableBuiltIns(const ShaderRecompiler::RecompileResult& result, spv::BuiltIn builtin) { + std::size_t count = 0; + const auto& words = result.spirv.Words(); + for (std::size_t at = 5; at < words.size() && (words[at] >> 16u) != 0; at += words[at] >> 16u) { + if (static_cast<spv::Op>(words[at] & 0xffffu) == spv::OpDecorate && (words[at] >> 16u) == 4u && words[at + 2] == spv::DecorationBuiltIn && words[at + 3] == builtin) ++count; + } + return count; +} + +bool MemberBuiltIn(const ShaderRecompiler::RecompileResult& result, spv::BuiltIn builtin) { + const auto& words = result.spirv.Words(); + for (std::size_t at = 5; at < words.size() && (words[at] >> 16u) != 0; at += words[at] >> 16u) { + if (static_cast<spv::Op>(words[at] & 0xffffu) == spv::OpMemberDecorate && words[at + 3] == spv::DecorationBuiltIn && words[at + 4] == builtin) return true; + } + return false; +} + void rectListTests() { using namespace ShaderRecompiler; using namespace AgcDriver::Graphics; @@ -6075,6 +6092,21 @@ void rectListTests() { expectFailure([&] { ValidateShaders(shaders, state, VkPhysicalDeviceSubgroupProperties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_PROPERTIES}, false); }, "stage count"); } } + { + RecompileResult clipped; + clipped.spirv = makeModule({.clipDistanceLength = 2}); + const std::array<std::uint32_t, 3> clipCapabilities{spv::CapabilityShader, spv::CapabilityTessellation, spv::CapabilityClipDistance}; + auto clipTarget = target; + clipTarget.supportedCapabilities = clipCapabilities; + auto clippedRect = BuildRectListShaders(clipped, fragment, clipTarget); + for (const auto* stage : {&clippedRect.control, &clippedRect.lastVertexControl, &clippedRect.evaluation}) { + Require(!MemberBuiltIn(*stage, spv::BuiltInClipDistance) && VariableBuiltIns(*stage, spv::BuiltInClipDistance) == 2u, "a rect-list stage does not take and forward the vertex clip distances through variables of their own, as the vertex shader writes them"); + } + const std::array<CompiledShader, 4> clippedShaders{{{ShaderStage::Vertex, &clipped, 0}, {ShaderStage::TessellationControl, &clippedRect.control, 0}, {ShaderStage::TessellationEvaluation, &clippedRect.evaluation, 0}, {ShaderStage::Fragment, &fragment, 0}}}; + auto queue = makeState(); + queue.userConfig[0x242] = 17; + ValidateShaders(clippedShaders, DecodeState(queue), VkPhysicalDeviceSubgroupProperties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_PROPERTIES}, false, false, false, false, false, false, true); + } vertex.spirv = makeModule({.parameterOutput = true}); fragment.spirv = makeModule({.fragment = true, .rectParameters = true}); vertex.parameterExports = {0}; diff --git a/core/shader/recompiler/SpirvBackend/src/RectListShaders.cpp b/core/shader/recompiler/SpirvBackend/src/RectListShaders.cpp index 9be097f99f..053a298982 100644 --- a/core/shader/recompiler/SpirvBackend/src/RectListShaders.cpp +++ b/core/shader/recompiler/SpirvBackend/src/RectListShaders.cpp @@ -42,13 +42,11 @@ public: vec4FloatType = type(spv::OpTypeVector, floatType, 4u); functionType = type(spv::OpTypeFunction, voidType); + perVertexType = builder.DecoratedType(spv::OpTypeStruct, {{spv::OpMemberDecorate, {0u, spv::DecorationBuiltIn, spv::BuiltInPosition}}, {spv::OpMemberDecorate, {0u, spv::DecorationInvariant}}, {spv::OpDecorate, {spv::DecorationBlock}}}, vec4FloatType); if (clipDistances != 0u) { clipArrayType = array(floatType, clipDistances); - perVertexType = builder.DecoratedType(spv::OpTypeStruct, {{spv::OpMemberDecorate, {0u, spv::DecorationBuiltIn, spv::BuiltInPosition}}, {spv::OpMemberDecorate, {0u, spv::DecorationInvariant}}, {spv::OpMemberDecorate, {1u, spv::DecorationBuiltIn, spv::BuiltInClipDistance}}, {spv::OpDecorate, {spv::DecorationBlock}}}, vec4FloatType, clipArrayType); ptrInputClip = pointer(spv::StorageClassInput, clipArrayType); ptrOutputClip = pointer(spv::StorageClassOutput, clipArrayType); - } else { - perVertexType = builder.DecoratedType(spv::OpTypeStruct, {{spv::OpMemberDecorate, {0u, spv::DecorationBuiltIn, spv::BuiltInPosition}}, {spv::OpMemberDecorate, {0u, spv::DecorationInvariant}}, {spv::OpDecorate, {spv::DecorationBlock}}}, vec4FloatType); } ptrInputVec4Float = pointer(spv::StorageClassInput, vec4FloatType); @@ -126,8 +124,8 @@ public: store(access(ptrOutputVec4Float, glOut, invocation, intConstant(0)), position); if (clipDistances != 0u) { std::array<std::uint32_t, 3> clips {}; - for (std::uint32_t i = 0; i < clips.size(); i++) clips[i] = load(clipArrayType, access(ptrInputClip, glIn, intConstant(i), intConstant(1))); - const auto own = load(clipArrayType, access(ptrInputClip, glIn, index, intConstant(1))); + for (std::uint32_t i = 0; i < clips.size(); i++) clips[i] = load(clipArrayType, access(ptrInputClip, clipIn, intConstant(i))); + const auto own = load(clipArrayType, access(ptrInputClip, clipIn, index)); std::vector<std::uint32_t> values; for (std::uint32_t k = 0; k < clipDistances; k++) { auto sum = constant(floatType, 0); @@ -136,7 +134,7 @@ public: } const auto clip = builder.AllocateId(); builder.AddFunction(spv::OpCompositeConstruct, clipArrayType, clip, std::span<const std::uint32_t>(values)); - store(access(ptrOutputClip, glOut, invocation, intConstant(1)), clip); + store(access(ptrOutputClip, clipOut, invocation), clip); } for (std::uint32_t i = 0; i < parameters.size(); i++) { @@ -171,7 +169,7 @@ public: const auto position = load(vec4FloatType, access(ptrInputVec4Float, glIn, index, intConstant(0))); store(access(ptrOutputVec4Float, glOut, intConstant(0)), position); - if (clipDistances != 0u) store(access(ptrOutputClip, glOut, intConstant(1)), load(clipArrayType, access(ptrInputClip, glIn, index, intConstant(1)))); + if (clipDistances != 0u) store(clipOut, load(clipArrayType, access(ptrInputClip, clipIn, index))); for (std::uint32_t i = 0; i < parameters.size(); i++) { store(outputs[i], load(vec4FloatType, access(ptrInputVec4Float, inputs[i], index))); } @@ -287,6 +285,12 @@ private: return variable; } + std::uint32_t clipVariable(spv::StorageClass storage, std::uint32_t type) { + const auto variable = addInterface(storage, type); + decorate(variable, spv::DecorationBuiltIn, spv::BuiltInClipDistance); + return variable; + } + void decorate(std::uint32_t target, spv::Decoration decoration, std::uint32_t value) { builder.AddAnnotation(spv::OpDecorate, target, decoration, value); } @@ -320,6 +324,7 @@ private: decorate(tessCoord, spv::DecorationBuiltIn, spv::BuiltInTessCoord); } glIn = addInterface(spv::StorageClassInput, array(perVertexType, tessControl ? 3u : 4u)); + if (clipDistances != 0u) clipIn = clipVariable(spv::StorageClassInput, array(clipArrayType, tessControl ? 3u : 4u)); inputs.resize(parameters.size()); std::array<std::uint32_t, 32> locations {}; @@ -340,6 +345,7 @@ private: const auto tessControl = model == spv::ExecutionModelTessellationControl; if (tessControl) { glOut = addInterface(spv::StorageClassOutput, array(perVertexType, 4u)); + if (clipDistances != 0u) clipOut = clipVariable(spv::StorageClassOutput, array(clipArrayType, 4u)); tessInner = addInterface(spv::StorageClassOutput, array(floatType, 2u)); decorate(tessInner, spv::DecorationBuiltIn, spv::BuiltInTessLevelInner); builder.AddAnnotation(spv::OpDecorate, tessInner, spv::DecorationPatch); @@ -348,6 +354,7 @@ private: builder.AddAnnotation(spv::OpDecorate, tessOuter, spv::DecorationPatch); } else { glOut = addInterface(spv::StorageClassOutput, perVertexType); + if (clipDistances != 0u) clipOut = clipVariable(spv::StorageClassOutput, clipArrayType); } outputs.resize(parameters.size()); @@ -394,6 +401,8 @@ private: std::uint32_t ptrOutputVec4Float = 0; std::uint32_t glIn = 0; std::uint32_t glOut = 0; + std::uint32_t clipIn = 0; + std::uint32_t clipOut = 0; std::uint32_t tessInner = 0; std::uint32_t tessOuter = 0; std::uint32_t tessCoord = 0; From 087c57ac8419b2fe5835306d07c21eefbea8943a Mon Sep 17 00:00:00 2001 From: MarcZX100 <MarcZX100@users.noreply.github.com> Date: Sun, 18 Oct 2026 00:45:12 +0000 Subject: [PATCH 0925/1000] test: retry transient Windows filesystem cleanup --- core/libs/tests/GuestFilesystem.cpp | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/core/libs/tests/GuestFilesystem.cpp b/core/libs/tests/GuestFilesystem.cpp index f14e191c80..f474c03570 100644 --- a/core/libs/tests/GuestFilesystem.cpp +++ b/core/libs/tests/GuestFilesystem.cpp @@ -13,6 +13,7 @@ #include <cstring> #include <stdexcept> #include <string> +#include <thread> #include <vector> #ifdef _WIN32 #include <io.h> @@ -596,7 +597,13 @@ int main() { Require(rename_nid_postfix(name(replacement).c_str(), name(dangling).c_str()) == 0); Require(!std::filesystem::is_symlink(dangling) && std::filesystem::is_regular_file(dangling)); #endif - std::filesystem::remove_all(area); + std::error_code cleanupError; + for (int attempt = 0; attempt < 10; ++attempt) { + std::filesystem::remove_all(area, cleanupError); + if (!cleanupError) break; + std::this_thread::sleep_for(std::chrono::milliseconds(20)); + } + Require(!cleanupError); } Require(remove_nid_postfix(file.string().c_str()) == 0); Require(!std::filesystem::exists(file)); From 5c3d21023f924d9854054cfd156f9763e62c716a Mon Sep 17 00:00:00 2001 From: thuanquang <ghoulbulldog@gmail.com> Date: Sun, 18 Oct 2026 07:43:54 +0700 Subject: [PATCH 0926/1000] fix(agc): keep one trace thread pointer and recording flag across modules PerformanceTrace kept the recording flag and each thread's trace buffer as inline static members, so on Windows every module that includes the header had its own copies: agc_driver_registration_trace read an empty pointer, and libSceVideoOut's timers never saw the trace recording. They are now defined once in the driver. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 3 +++ .../Execution/include/PerformanceTrace.hpp | 18 ++++++++++++++---- .../Execution/src/PerformanceTraceThread.cpp | 16 ++++++++++++++++ 3 files changed, 33 insertions(+), 4 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/Execution/src/PerformanceTraceThread.cpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index fcd75d52e0..cd94455327 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -185,6 +185,7 @@ set(agcDriverSources ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/Driver/Synchronization/WaitStatistics.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/WorkerSampler.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/PerformanceTrace.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/PerformanceTraceThread.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/TraceFile.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/ThreadPriority.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/GuestMemory.cpp @@ -494,6 +495,7 @@ add_test_executable(agc_shader_memory_tests EXCLUDE_FROM_ALL tests/ShaderMemory.cpp Execution/src/ShaderMemory.cpp Execution/src/GuestMemory.cpp + Execution/src/PerformanceTraceThread.cpp ) target_include_directories(agc_shader_memory_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(agc_shader_memory_tests PRIVATE shader_recompiler_agc_driver libc) @@ -518,6 +520,7 @@ configure_windows_unwind(agc_shared_write_tests) add_test_executable(agc_write_tracking_tests EXCLUDE_FROM_ALL tests/WriteTracking.cpp Execution/src/GuestMemory.cpp + Execution/src/PerformanceTraceThread.cpp ) target_include_directories(agc_write_tracking_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(agc_write_tracking_tests PRIVATE shader_recompiler_agc_driver libc) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp index 2a5314f9ff..5c7fb7d2a6 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp @@ -21,6 +21,13 @@ #include <utility> #include <vector> +namespace AgcDriver { +class TraceThread; +} + +extern "C" bool AgcDriverTraceRecording_nid_postfix(); +extern "C" AgcDriver::TraceThread* AgcDriverTraceThread_nid_postfix(); + namespace AgcDriver { class VulkanDevice; @@ -532,23 +539,26 @@ public: } static bool Recording() { - return recording.load(std::memory_order_relaxed); + return AgcDriverTraceRecording_nid_postfix(); } static TraceThread* Thread() { - return local; + return AgcDriverTraceThread_nid_postfix(); } static void StopOutput(); private: + friend bool ::AgcDriverTraceRecording_nid_postfix(); + friend TraceThread* ::AgcDriverTraceThread_nid_postfix(); + static void OpenInterval(std::chrono::steady_clock::time_point now, std::uint64_t present); static void FlushInterval(std::chrono::steady_clock::time_point now, std::uint32_t width, std::uint32_t height, const VulkanDevice* device); - inline static std::atomic<bool> recording{false}; + static std::atomic<bool> recording; inline static std::atomic<bool> buffering{false}; inline static std::atomic<std::uint64_t> presentCount{0}; - inline static thread_local TraceThread* local = nullptr; + static thread_local TraceThread* local; inline static std::mutex gpuMutex; inline static std::vector<TraceGpuRange> gpuRanges; inline static std::uint64_t gpuLost = 0; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceTraceThread.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceTraceThread.cpp new file mode 100644 index 0000000000..9ce81e5294 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceTraceThread.cpp @@ -0,0 +1,16 @@ +#include "prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp" + +namespace AgcDriver { + +std::atomic<bool> PerformanceTrace::recording{false}; +thread_local TraceThread* PerformanceTrace::local = nullptr; + +} + +extern "C" bool AgcDriverTraceRecording_nid_postfix() { + return AgcDriver::PerformanceTrace::recording.load(std::memory_order_relaxed); +} + +extern "C" AgcDriver::TraceThread* AgcDriverTraceThread_nid_postfix() { + return AgcDriver::PerformanceTrace::local; +} From 563cdd6e99deb462eae7bca333b9af20783846c3 Mon Sep 17 00:00:00 2001 From: milescoviello <kitslayer29@gmail.com> Date: Sun, 18 Oct 2026 01:00:16 +0000 Subject: [PATCH 0927/1000] test(agc): create fixture command pools with the reset flag Seven test fixtures create their own command pool without VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT, while the driver's own pool sets it and Recorder begins a used command buffer again without an explicit reset. The Khronos validation layer reports VUID-vkBeginCommandBuffer-commandBuffer-00050 in depth_storage and partially_resident_snapshot --pending-stores for this. The fixtures now create the pool the way VulkanDevice does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- core/libs/prx/libSceAgcDriver/tests/BdaDevice.cpp | 1 + core/libs/prx/libSceAgcDriver/tests/execution/DepthStorage.cpp | 1 + .../libSceAgcDriver/tests/execution/OversizedDescriptorSets.cpp | 1 + .../tests/execution/PartiallyResidentSnapshot.cpp | 1 + .../prx/libSceAgcDriver/tests/execution/PooledBufferRange.cpp | 1 + .../prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp | 1 + .../prx/libSceAgcDriver/tests/execution/SparseBufferHoles.cpp | 1 + 7 files changed, 7 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaDevice.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaDevice.cpp index 860643bd27..61a1612f87 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaDevice.cpp @@ -102,6 +102,7 @@ public: context.bufferDeviceAddress = true; context.Function<PFN_vkGetDeviceQueue>("vkGetDeviceQueue")(context.device, family, 0, &context.queue); VkCommandPoolCreateInfo pool{VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO}; + pool.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT; pool.queueFamilyIndex = family; Check(context.Function<PFN_vkCreateCommandPool>("vkCreateCommandPool")(context.device, &pool, nullptr, &context.pool), "vkCreateCommandPool"); } catch (...) { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DepthStorage.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DepthStorage.cpp index 237d3f7646..3ab804d307 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DepthStorage.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DepthStorage.cpp @@ -109,6 +109,7 @@ public: if (hasExtension(VK_EXT_MEMORY_BUDGET_EXTENSION_NAME)) context.memoryProperties2 = function<PFN_vkGetPhysicalDeviceMemoryProperties2>("vkGetPhysicalDeviceMemoryProperties2"); context.Function<PFN_vkGetDeviceQueue>("vkGetDeviceQueue")(context.device, family, 0, &context.queue); VkCommandPoolCreateInfo pool{VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO}; + pool.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT; pool.queueFamilyIndex = family; Check(context.Function<PFN_vkCreateCommandPool>("vkCreateCommandPool")(context.device, &pool, nullptr, &context.pool), "vkCreateCommandPool"); } catch (...) { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/OversizedDescriptorSets.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/OversizedDescriptorSets.cpp index be56587bfc..8984302a91 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/OversizedDescriptorSets.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/OversizedDescriptorSets.cpp @@ -84,6 +84,7 @@ public: context.formatProperties = function<PFN_vkGetPhysicalDeviceFormatProperties>("vkGetPhysicalDeviceFormatProperties"); context.Function<PFN_vkGetDeviceQueue>("vkGetDeviceQueue")(context.device, family, 0, &context.queue); VkCommandPoolCreateInfo pool{VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO}; + pool.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT; pool.queueFamilyIndex = family; Check(context.Function<PFN_vkCreateCommandPool>("vkCreateCommandPool")(context.device, &pool, nullptr, &context.pool), "vkCreateCommandPool"); } catch (...) { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PartiallyResidentSnapshot.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PartiallyResidentSnapshot.cpp index 005a1e506f..5d747bf85a 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PartiallyResidentSnapshot.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PartiallyResidentSnapshot.cpp @@ -110,6 +110,7 @@ public: context.imageFormatProperties = function<PFN_vkGetPhysicalDeviceImageFormatProperties>("vkGetPhysicalDeviceImageFormatProperties"); context.Function<PFN_vkGetDeviceQueue>("vkGetDeviceQueue")(context.device, family, 0, &context.queue); VkCommandPoolCreateInfo pool{VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO}; + pool.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT; pool.queueFamilyIndex = family; Check(context.Function<PFN_vkCreateCommandPool>("vkCreateCommandPool")(context.device, &pool, nullptr, &context.pool), "vkCreateCommandPool"); } catch (...) { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PooledBufferRange.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PooledBufferRange.cpp index b8276c8bb1..13e19d7dcb 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PooledBufferRange.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PooledBufferRange.cpp @@ -214,6 +214,7 @@ public: context.formatProperties = function<PFN_vkGetPhysicalDeviceFormatProperties>("vkGetPhysicalDeviceFormatProperties"); context.Function<PFN_vkGetDeviceQueue>("vkGetDeviceQueue")(context.device, family, 0, &context.queue); VkCommandPoolCreateInfo pool{VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO}; + pool.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT; pool.queueFamilyIndex = family; Check(context.Function<PFN_vkCreateCommandPool>("vkCreateCommandPool")(context.device, &pool, nullptr, &context.pool), "vkCreateCommandPool"); } catch (...) { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp index a1b31188b8..fe436e6b22 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp @@ -130,6 +130,7 @@ public: if (hasExtension(VK_EXT_MEMORY_BUDGET_EXTENSION_NAME)) context.memoryProperties2 = function<PFN_vkGetPhysicalDeviceMemoryProperties2>("vkGetPhysicalDeviceMemoryProperties2"); context.Function<PFN_vkGetDeviceQueue>("vkGetDeviceQueue")(context.device, family, 0, &context.queue); VkCommandPoolCreateInfo pool{VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO}; + pool.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT; pool.queueFamilyIndex = family; Check(context.Function<PFN_vkCreateCommandPool>("vkCreateCommandPool")(context.device, &pool, nullptr, &context.pool), "vkCreateCommandPool"); } catch (...) { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SparseBufferHoles.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SparseBufferHoles.cpp index add6ad6737..a7a25f9f88 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/SparseBufferHoles.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SparseBufferHoles.cpp @@ -96,6 +96,7 @@ public: function<PFN_vkGetPhysicalDeviceMemoryProperties>("vkGetPhysicalDeviceMemoryProperties")(context.physical, &context.memory); context.Function<PFN_vkGetDeviceQueue>("vkGetDeviceQueue")(context.device, family, 0, &context.queue); VkCommandPoolCreateInfo pool{VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO}; + pool.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT; pool.queueFamilyIndex = family; Check(context.Function<PFN_vkCreateCommandPool>("vkCreateCommandPool")(context.device, &pool, nullptr, &context.pool), "vkCreateCommandPool"); } catch (...) { From ba9aec51194b8b4a4ea0e89e6b5a7b31451fa8eb Mon Sep 17 00:00:00 2001 From: Romans Pokrovskis <motivated.it@gmail.com> Date: Sun, 18 Oct 2026 03:44:01 +0300 Subject: [PATCH 0928/1000] fix(shader): preserve clip arrays for tessellation control points --- .../SpirvBackend/src/BarycentricGeometryShader.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/shader/recompiler/SpirvBackend/src/BarycentricGeometryShader.cpp b/core/shader/recompiler/SpirvBackend/src/BarycentricGeometryShader.cpp index a57a5d2454..daca4351b0 100644 --- a/core/shader/recompiler/SpirvBackend/src/BarycentricGeometryShader.cpp +++ b/core/shader/recompiler/SpirvBackend/src/BarycentricGeometryShader.cpp @@ -189,9 +189,9 @@ public: if (clipDistances != 0u) { clipArrayType = builder.Type(spv::OpTypeArray, floatType, constant(uintType, clipDistances)); controlInputClipType = builder.Type(spv::OpTypeArray, clipArrayType, constant(uintType, 3u)); - ptrControlInputClip = builder.Type(spv::OpTypePointer, spv::StorageClassInput, controlInputClipType); + ptrControlInputClip = builder.Type(spv::OpTypePointer, spv::StorageClassInput, clipArrayType); ptrControlOutputClip = builder.Type(spv::OpTypePointer, spv::StorageClassOutput, clipArrayType); - ptrEvaluationInputClip = builder.Type(spv::OpTypePointer, spv::StorageClassInput, controlInputClipType); + ptrEvaluationInputClip = builder.Type(spv::OpTypePointer, spv::StorageClassInput, clipArrayType); ptrEvaluationOutputClip = builder.Type(spv::OpTypePointer, spv::StorageClassOutput, clipArrayType); } perVertexType = builder.DecoratedType(spv::OpTypeStruct, {{spv::OpMemberDecorate, {0u, spv::DecorationBuiltIn, spv::BuiltInPosition}}, {spv::OpDecorate, {spv::DecorationBlock}}}, vec4FloatType); @@ -213,7 +213,7 @@ public: if (clipDistances != 0u) { clipIn = addInterface(spv::StorageClassInput, controlInputClipType); decorate(clipIn, spv::DecorationBuiltIn, spv::BuiltInClipDistance); - clipOut = addInterface(spv::StorageClassOutput, clipArrayType); + clipOut = addInterface(spv::StorageClassOutput, controlInputClipType); decorate(clipOut, spv::DecorationBuiltIn, spv::BuiltInClipDistance); } const auto floatArray = [&](std::uint32_t size) { return builder.Type(spv::OpTypeArray, floatType, constant(uintType, size)); }; From 859dcae2a1d0e0ae41777666270c75468171572f Mon Sep 17 00:00:00 2001 From: BiladeiroSupremo <ag.rio@hotmail.com> Date: Fri, 16 Oct 2026 22:23:36 -0300 Subject: [PATCH 0929/1000] fix(shader): reduce a DPP row scan read at a row's last lane over the active lanes A row scan (DPP row_shr 1, 2, 4 and 8 over keys that hold the identity on lanes the wave did not enter with) read with v_readlane at lane 15, 31, 47 or 63 shuffles from lanes that already left the surrounding loop, which are inactive on the host and whose values are undefined there. The read becomes a subgroup reduction over the active invocations of that row, per wave32 half when a wave64 host subgroup holds two waves. The saved exec mask set to every lane (`!x || x`) also counts as every lane. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- .../tests/execution/PixelRowReduction.cpp | 211 ++++++++++++++++++ .../src/SpirvAlu/SpirvAluEmitterMath.cpp | 68 +++++- 2 files changed, 278 insertions(+), 1 deletion(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/PixelRowReduction.cpp diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PixelRowReduction.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PixelRowReduction.cpp new file mode 100644 index 0000000000..d9b7114c0b --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PixelRowReduction.cpp @@ -0,0 +1,211 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include <algorithm> +#include <array> +#include <cstdio> +#include <functional> +#include <iostream> +#include <map> +#include <string> +#include <vector> + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Sentinel = 0xdeadbeefu; +constexpr std::uint32_t Width = 192; +constexpr std::uint32_t Height = 128; +alignas(256) std::array<std::byte, Width * Height * 4> Pixels{}; +alignas(256) std::array<std::uint32_t, Width * Height> Results{}; + +alignas(256) constexpr std::array<std::uint32_t, 6> VertexCode{ + 0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, +}; + +struct Reduction { + const char* name; + std::uint32_t identity; + std::uint32_t vector; + std::uint32_t scalar; + std::function<std::uint32_t(std::uint32_t, std::uint32_t)> combine; +}; + +std::uint32_t Key(std::uint32_t pixel) { + return (pixel * 0x9e3779b1u) >> 22u; +} + +std::vector<std::uint32_t> PixelCode(const Reduction& reduction) { + const std::uint32_t dpp = reduction.vector | 0x001a1afau; + return { + 0x7e040f00, 0x7e060f01, 0xd5430006, 0x0409ff03, 0x000000c0, 0x7e120506, 0xd569000b, 0x0001ff06, + 0x9e3779b1, 0x2c161696, 0xbe98047e, 0x879a187e, 0xbeea287e, 0xd501000d, reduction.identity == 0u ? 0x006a1680u : 0x006a16c1u, + dpp, 0xff01110d, dpp, 0xff01120d, dpp, 0xff01140d, dpp, 0xff01180d, 0xbefe046a, + 0xd760000a, 0x00011f0d, 0xd760000b, 0x00013f0d, reduction.scalar | 0x000c0b0au, + 0xd760000a, 0x00015f0d, reduction.scalar | 0x000c0a0cu, 0xd760000a, 0x00017f0d, reduction.scalar | 0x000c0a0cu, + 0x8f099009, 0x880c090c, 0x7e0a020c, 0xe0702000, 0x80010506, 0x7e0e02f2, 0xf800180f, 0x07070707, + 0xbf810000, + }; +} + +std::array<std::uint32_t, 4> BufferDescriptor(const void* data, std::uint32_t stride, std::uint32_t count) { + const auto address = reinterpret_cast<std::uintptr_t>(data); + return {static_cast<std::uint32_t>(address), static_cast<std::uint32_t>((address >> 32u) & 0xffffu) | (stride << 16u), count, 0x01016facu}; +} + +std::vector<std::array<float, 4>> Triangles() { + std::vector<std::array<float, 4>> vertices; + constexpr std::uint32_t columns = 6; + constexpr std::uint32_t rows = 4; + for (std::uint32_t cell = 0; cell < columns * rows; ++cell) { + const float width = 2.0f / columns; + const float height = 2.0f / rows; + const float x = -1.0f + width * static_cast<float>(cell % columns); + const float y = -1.0f + height * static_cast<float>(cell / columns); + const float skew = 0.07f * static_cast<float>(cell % 5u); + vertices.push_back({x + (0.05f + skew) * width, y + 0.05f * height, 0.5f, 1.0f}); + vertices.push_back({x + 0.93f * width, y + (0.11f + skew) * height, 0.5f, 1.0f}); + vertices.push_back({x + (0.37f - skew * 0.5f) * width, y + 0.91f * height, 0.5f, 1.0f}); + } + return vertices; +} + +void Draw(AgcDriver::VulkanDevice& device, std::span<const std::uint32_t> pixelCode, const ShaderRecompiler::SpirvTarget& target) { + static const auto triangles = Triangles(); + Pixels.fill(std::byte{0}); + Results.fill(Sentinel); + + std::vector<std::uint32_t> vertexUserData(4, 0u); + const auto vertexBuffer = BufferDescriptor(triangles.data(), 16u, static_cast<std::uint32_t>(triangles.size())); + std::copy(vertexBuffer.begin(), vertexBuffer.end(), vertexUserData.begin()); + const std::array<ShaderRecompiler::MemoryRegion, 1> vertexMemory{{{reinterpret_cast<std::uintptr_t>(VertexCode.data()), std::as_bytes(std::span(VertexCode))}}}; + ShaderRecompiler::RecompileRequest vertex{ + {ShaderStage::Vertex, reinterpret_cast<std::uintptr_t>(VertexCode.data()), VertexCode, 0, {}}, + {64u, 0, vertexUserData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, vertexMemory}, + target, + {0, 0, 0, 64} + }; + vertex.useCache = false; + const auto vertexResult = ShaderRecompiler::Recompile(vertex); + const auto vertexPush = static_cast<std::uint32_t>(vertexResult.pushConstants.size()); + + ShaderRecompiler::ShaderPixelStageInfo pixel{}; + pixel.inputAddr = ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionX) | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionY); + pixel.posX = true; + pixel.posY = true; + pixel.targetOutputMode[0] = 9; + pixel.targetExportMapping.fill(0xe4u); + std::vector<std::uint32_t> pixelUserData(8, 0u); + const auto results = BufferDescriptor(Results.data(), 4u, static_cast<std::uint32_t>(Results.size())); + std::copy(results.begin(), results.end(), pixelUserData.begin() + 4); + const std::array<ShaderRecompiler::MemoryRegion, 1> pixelMemory{{{reinterpret_cast<std::uintptr_t>(pixelCode.data()), std::as_bytes(pixelCode)}}}; + ShaderRecompiler::RecompileRequest fragment{ + {ShaderStage::Fragment, reinterpret_cast<std::uintptr_t>(pixelCode.data()), pixelCode, 0, {}}, + {64u, 0, pixelUserData, std::nullopt, pixel, std::nullopt, pixelMemory}, + target, + PixelPushLayout(vertexPush, target) + }; + fragment.useCache = false; + const auto pixelResult = ShaderRecompiler::Recompile(fragment); + const std::array<AgcDriver::Graphics::CompiledShader, 2> shaders{{ + {ShaderStage::Vertex, &vertexResult, 0}, + {ShaderStage::Fragment, &pixelResult, PixelPushOffset(vertexPush, target)} + }}; + + AgcDriver::Graphics::State state{}; + state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, 64u, 64u, std::nullopt, std::nullopt}; + state.color = {reinterpret_cast<std::uintptr_t>(Pixels.data()), {Width, Height}, VK_FORMAT_R8G8B8A8_UNORM, Pixels.size(), 0xe4u}; + state.colors = {state.color}; + state.hasColorTarget = true; + state.renderExtent = {Width, Height}; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.viewport = {0, static_cast<float>(Height), static_cast<float>(Width), -static_cast<float>(Height), 0, 1}; + state.negativeOneToOne = false; + state.scissor = {{0, 0}, {Width, Height}}; + state.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + state.blend.colorWriteMask = 15; + state.blends = {state.blend}; + state.blendConstants = {}; + const AgcDriver::Pm4::DrawParameters draw{0, static_cast<std::uint32_t>(triangles.size()), 0, 1, 0, false}; + device.Draw(state, draw, shaders); + device.WaitIdle(); +} + +std::uint32_t Check(const Reduction& reduction, std::uint32_t subgroupSize) { + const auto what = std::string("pixel row reduction ") + reduction.name; + std::map<std::uint32_t, std::vector<std::uint32_t>> waves; + for (std::uint32_t pixel = 0; pixel < Results.size(); ++pixel) { + if (std::to_integer<std::uint8_t>(Pixels[pixel * 4u]) != 255u) { + Require(Results[pixel] == Sentinel, what + ": uncovered pixel " + std::to_string(pixel) + " stored " + std::to_string(Results[pixel])); + continue; + } + Require(Results[pixel] != Sentinel, what + ": covered pixel " + std::to_string(pixel) + " stored nothing"); + waves[Results[pixel] >> 16u].push_back(pixel); + } + std::uint32_t partial = 0; + std::size_t covered = 0; + for (const auto& [first, pixels] : waves) { + Require(std::find(pixels.begin(), pixels.end(), first) != pixels.end(), what + ": the first lane of the wave of pixel " + std::to_string(pixels.front()) + ", pixel " + std::to_string(first) + ", is not a covered pixel of that wave"); + std::uint32_t expected = reduction.identity; + for (const auto pixel : pixels) expected = reduction.combine(expected, Key(pixel)); + for (const auto pixel : pixels) { + Require((Results[pixel] & 0xffffu) == expected, what + ": pixel " + std::to_string(pixel) + " of a wave of " + std::to_string(pixels.size()) + " covered pixels read " + std::to_string(Results[pixel] & 0xffffu) + ", the wave holds " + std::to_string(expected)); + } + if (pixels.size() < subgroupSize) ++partial; + covered += pixels.size(); + } + Require(covered > 1000, what + ": the triangles covered only " + std::to_string(covered) + " pixels"); + Require(partial != 0u, what + ": no wave had fewer covered pixels than the host subgroup"); + return partial; +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + const auto target = device->Target(); + if (!TargetHasCapability(target, spv::CapabilityGroupNonUniformArithmetic)) { + std::puts("skipped, the device has no subgroup arithmetic"); + return VulkanTestSkipped; + } + if (target.subgroupSize > 64u) { + std::printf("skipped, the %u-lane subgroup is wider than the wave\n", target.subgroupSize); + return VulkanTestSkipped; + } + auto wide = target; + wide.subgroupSize = 64u; + const std::array<ShaderRecompiler::SpirvTarget, 1> layouts{wide}; + const std::array<Reduction, 5> reductions{{ + {"UMin", 0xffffffffu, 0x26000000u, 0x83800000u, [](std::uint32_t left, std::uint32_t right) { return std::min(left, right); }}, + {"UMax", 0u, 0x28000000u, 0x84800000u, [](std::uint32_t left, std::uint32_t right) { return std::max(left, right); }}, + {"IAdd", 0u, 0x4a000000u, 0x80000000u, [](std::uint32_t left, std::uint32_t right) { return left + right; }}, + {"AND", 0xffffffffu, 0x36000000u, 0x87000000u, [](std::uint32_t left, std::uint32_t right) { return left & right; }}, + {"OR", 0u, 0x38000000u, 0x88000000u, [](std::uint32_t left, std::uint32_t right) { return left | right; }}, + }}; + for (const auto& layout : layouts) { + for (const auto& reduction : reductions) { + for (const bool windowed : {false, true}) { + auto code = PixelCode(reduction); + if (windowed) code.insert(std::find(code.begin(), code.end(), 0x7e0e02f2u), 0x7e008514u); + std::uint32_t partial = 0; + for (std::uint32_t pass = 0; pass < 4; ++pass) { + Draw(*device, code, layout); + partial += Check(reduction, target.subgroupSize); + } + std::printf("%s%s, laid out for %u lanes on %u-lane subgroups: %u partial waves over 4 draws\n", reduction.name, windowed ? " in a v_movreld_b32 window" : "", layout.subgroupSize, target.subgroupSize, partial); + } + } + } + std::puts("pixel row reduction tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp index 6ecc15534e..cbf8912a45 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp @@ -295,7 +295,9 @@ bool AllOnesWord(const IrValue* word) { } bool AllLanesBit(const IrValue* bit) { - const auto words = LaneBitWords(Resolved(bit)); + bit = Resolved(bit); + if (Is(bit, IrOpcode::LogicalOr) && EitherOrder(bit, [](const IrValue* inverted, const IrValue* other) { return Is(inverted, IrOpcode::LogicalNot) && Resolved(inverted->Argument(0)) == other; })) return true; + const auto words = LaneBitWords(bit); return words && AllOnesWord((*words)[0]) && AllOnesWord((*words)[1]); } @@ -596,6 +598,69 @@ std::optional<std::uint32_t> EmitHalfWaveReduction(SpirvValueEmitContext& ctx, c return result; } +bool ActiveMaskedWord(const IrValue* word, std::uint32_t depth = 0u) { + word = Resolved(word); + if (word == nullptr || depth > 8u) return false; + if (Is(word, IrOpcode::CompositeExtractU32x4)) { + const auto* ballot = Resolved(word->Argument(0)); + const auto* active = ballot != nullptr && Is(ballot, IrOpcode::Ballot) ? Resolved(ballot->Argument(0)) : nullptr; + return active != nullptr && active->HasImmediate() && active->Type() == IrType::Bool && active->ImmediateBool() && IsU32(word->Argument(1), 0u); + } + return Is(word, IrOpcode::BitwiseAnd32) && (ActiveMaskedWord(word->Argument(0), depth + 1u) || ActiveMaskedWord(word->Argument(1), depth + 1u)); +} + +bool ActiveMaskedBit(const IrValue* bit) { + const auto words = LaneBitWords(Resolved(bit)); + return words && ActiveMaskedWord((*words)[0]) && ActiveMaskedWord((*words)[1]); +} + +std::optional<HalfWaveScan> MatchRowScan(const IrValue* value, IrShaderStage stage, bool narrow) { + const auto* top = Unmasked(Resolved(value), narrow); + for (const auto& reduction : WaveReductions) { + if (reduction.floating) continue; + const IrValue* scan = top; + for (const auto control : {0x118u, 0x114u, 0x112u, 0x111u}) { + scan = RowScanStepInput(scan, reduction, control, narrow); + if (scan == nullptr) break; + } + if (scan == nullptr) continue; + const auto* source = Unmasked(scan, narrow); + if (Is(source, IrOpcode::SelectU32) && IsU32(source->Argument(2), reduction.identity) && (ActiveMaskedBit(source->Argument(0)) || EntryLaneWalk{stage}.ZeroBit(source->Argument(0)))) return HalfWaveScan{&reduction, source}; + } + return std::nullopt; +} + +std::optional<std::uint32_t> EmitRowReduction(SpirvValueEmitContext& ctx, const IrValue& inst, std::uint32_t lane) { + auto& state = ctx.state; + const bool split = state.splitSubgroup && state.hostSubgroupSize == 64u && state.program.WaveSize() == 32u; + if ((lane & 15u) != 15u || state.laneCount != 1u || HostSubgroupSingleLane(state) || (!split && (state.splitSubgroup || state.hostSubgroupSize != state.program.WaveSize()))) return std::nullopt; + const auto stage = state.program.Resources().stage; + auto scan = MatchRowScan(inst.Argument(0), stage, false); + if (!scan) scan = MatchRowScan(inst.Argument(0), stage, true); + if (!scan) return std::nullopt; + const auto& capabilities = state.supportedCapabilities; + if (std::find(capabilities.begin(), capabilities.end(), static_cast<std::uint32_t>(spv::CapabilityGroupNonUniformArithmetic)) == capabilities.end()) return std::nullopt; + const auto u32 = TypeU32(state); + const auto boolean = TypeBool(state); + const auto identity = ConstantU32(state, scan->reduction->identity); + const auto row = Binary(state, spv::OpShiftRightLogical, u32, EmitSubgroupLocalInvocationId(state), ConstantU32(state, 4u)); + const auto inRow = Binary(state, spv::OpIEqual, boolean, row, ConstantU32(state, lane >> 4u)); + const auto source = ctx.Def(scan->source); + state.module.EmitCapability(spv::CapabilityGroupNonUniformArithmetic); + const auto reduce = [&](std::uint32_t include) { + const auto keys = Select(state, u32, include, source, identity); + const auto result = state.module.AllocateId(); + state.module.AddFunction(scan->reduction->reduce, u32, result, ConstantU32(state, spv::ScopeSubgroup), static_cast<std::uint32_t>(spv::GroupOperationReduce), keys); + return result; + }; + if (!split) return reduce(inRow); + const auto upper = Binary(state, spv::OpUGreaterThanEqual, boolean, EmitHostSubgroupLane(state, ConstantU32(state, 0u)), ConstantU32(state, 32u)); + const auto lower = Unary(state, spv::OpLogicalNot, boolean, upper); + const auto lowerTotal = reduce(Binary(state, spv::OpLogicalAnd, boolean, inRow, lower)); + const auto upperTotal = reduce(Binary(state, spv::OpLogicalAnd, boolean, inRow, upper)); + return Select(state, u32, upper, upperTotal, lowerTotal); +} + } std::uint32_t EmitConvertU16U32(SpirvEmitterState& state, std::uint32_t arg0) { @@ -1032,6 +1097,7 @@ std::uint32_t EmitReadLane(SpirvValueEmitContext& ctx, const IrValue& inst) { if (selector != nullptr && selector->HasImmediate() && state.program.Resources().stage != IrShaderStage::Compute && state.laneCount == 1u) { const auto index = selector->ImmediateU32() & (state.program.WaveSize() - 1u); if (const auto reduced = EmitHalfWaveReduction(ctx, inst, index)) return *reduced; + if (const auto reduced = EmitRowReduction(ctx, inst, index)) return *reduced; if (HostSubgroupNarrowerThanWave(state) && index >= state.hostSubgroupSize) { if (index >= 32u && UpperLaneWalk{state.program.Resources().stage}.ZeroValue(inst.Argument(0))) return ConstantU32(state, 0u); FailOutsideHostSubgroup(ctx, inst, "v_readlane_b32 of lane " + std::to_string(index)); From f27ad681eec266f341d3847db1604fed2c0f6154 Mon Sep 17 00:00:00 2001 From: thibaut <thibaut-leaux-angeli@live.fr> Date: Sun, 18 Oct 2026 03:08:58 +0200 Subject: [PATCH 0930/1000] test(agc): keep the resource cache growth test short The test now sets a base bound of 32 entries and a first shrink window of 2 (ResourceCache::SetBase, applied at the next Clear()) and turns dispatch recipes off, so every dispatch goes through the cache: grow, shrink, regrow, backoff and the Clear() reset take about 1.2k dispatches instead of about 100k. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XBXM4pgkMHYEtqN1EQni2P --- .../Graphics/include/ShaderResources.hpp | 3 ++ .../Graphics/src/ShaderResources.cpp | 16 ++++++---- .../tests/execution/ResourceCacheGrowth.cpp | 29 ++++++++++++++----- 3 files changed, 35 insertions(+), 13 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp index ef70d27744..d1c446b69d 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp @@ -631,6 +631,7 @@ public: void Clear(); std::size_t Size() const; std::size_t Capacity() const; + void SetBase(std::size_t entries, std::size_t shrinkWindow); std::uint64_t TableElements() const; static std::uint64_t TableElementBudget(); static constexpr std::size_t InitialEntries = 1024; @@ -662,6 +663,8 @@ private: std::size_t usesSinceCheck = 0; static constexpr std::size_t InitialShrinkWindow = 8; static constexpr std::size_t MaximumShrinkWindow = 1024; + std::size_t baseEntries = InitialEntries; + std::size_t baseShrinkWindow = InitialShrinkWindow; std::size_t shrinkWindow = InitialShrinkWindow; bool shrankLast = false; }; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index 907062d951..4089e2702d 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -3179,7 +3179,7 @@ void ResourceCache::noteEvicted(const Key& key) { } void ResourceCache::noteUse(const Key& key) { - if (fixedCapacity || capacity <= InitialEntries) return; + if (fixedCapacity || capacity <= baseEntries) return; usedKeys.insert(KeyHash{}(key)); if (++usesSinceCheck < capacity * shrinkWindow) return; const auto used = usedKeys.size(); @@ -3188,7 +3188,7 @@ void ResourceCache::noteUse(const Key& key) { usesSinceCheck = 0; if (used * 4u > capacity) return; const auto previous = capacity; - capacity = std::max(capacity / 2u, InitialEntries); + capacity = std::max(capacity / 2u, baseEntries); shrankLast = true; evictionsSinceCheck = 0; returnsSinceCheck = 0; @@ -3335,7 +3335,7 @@ void ResourceCache::Insert(const Key& key, std::shared_ptr<ShaderResources> reso const char* value = std::getenv("APS5_RESOURCE_CACHE_ENTRIES"); const auto parsed = value ? std::strtoull(value, nullptr, 10) : 0ull; fixedCapacity = parsed != 0; - capacity = fixedCapacity ? static_cast<std::size_t>(parsed) : InitialEntries; + capacity = fixedCapacity ? static_cast<std::size_t>(parsed) : baseEntries; } noteUse(key); while (entries.size() > capacity) { @@ -3410,11 +3410,17 @@ void ResourceCache::Clear() { returnsSinceCheck = 0; usedKeys.clear(); usesSinceCheck = 0; - shrinkWindow = InitialShrinkWindow; + shrinkWindow = baseShrinkWindow; shrankLast = false; if (!fixedCapacity) capacity = 0; } +void ResourceCache::SetBase(std::size_t entries, std::size_t window) { + std::lock_guard lock(mutex); + baseEntries = entries; + baseShrinkWindow = window; +} + std::size_t ResourceCache::Size() const { std::lock_guard lock(mutex); return entries.size(); @@ -3422,7 +3428,7 @@ std::size_t ResourceCache::Size() const { std::size_t ResourceCache::Capacity() const { std::lock_guard lock(mutex); - return capacity == 0 ? InitialEntries : capacity; + return capacity == 0 ? baseEntries : capacity; } std::uint64_t ResourceCache::TableElementBudget() { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ResourceCacheGrowth.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ResourceCacheGrowth.cpp index bae94f9150..12242e569c 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ResourceCacheGrowth.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ResourceCacheGrowth.cpp @@ -29,16 +29,17 @@ namespace { using AgcDriver::Graphics::Require; -constexpr std::uint32_t Dispatches = 1536; +constexpr std::size_t BaseEntries = 32; +constexpr std::size_t BaseShrinkWindow = 2; +constexpr std::uint32_t Dispatches = 48; constexpr std::uint32_t Passes = 4; -constexpr std::uint32_t HotDispatches = 64; -constexpr std::uint32_t HotRepeats = 640; +constexpr std::uint32_t HotDispatches = 2; +constexpr std::uint32_t HotRepeats = 160; constexpr std::size_t SrtBytes = 64; constexpr std::size_t OutputBytes = 256; constexpr std::size_t SrtOffset = 4096; constexpr std::size_t OutputOffset = SrtOffset + static_cast<std::size_t>(Dispatches) * SrtBytes; constexpr std::size_t BlockBytes = (OutputOffset + static_cast<std::size_t>(Dispatches) * OutputBytes + 65535) & ~static_cast<std::size_t>(65535); -constexpr std::size_t InitialEntries = 1024; constexpr std::array<std::uint32_t, 7> StoreCode{0xf4080100u, 0xfa000000u, 0xbf8c0000u, 0x7e020281u, 0xe0700000u, 0x80010100u, 0xbf810000u}; @@ -117,13 +118,13 @@ void WorkingSetLargerThanTheInitialBound(const GuestBlock& block) { } } const auto kept = cache.Size(); - Require(kept >= Dispatches, "resource cache growth: " + std::to_string(Dispatches) + " dispatches with distinct resources repeated " + std::to_string(Passes) + " times in turn, but the resource cache kept only " + std::to_string(kept) + " entries (initial bound " + std::to_string(InitialEntries) + "), so every pass rebuilt every set"); + Require(kept >= Dispatches, "resource cache growth: " + std::to_string(Dispatches) + " dispatches with distinct resources repeated " + std::to_string(Passes) + " times in turn, but the resource cache kept only " + std::to_string(kept) + " entries (initial bound " + std::to_string(BaseEntries) + "), so every pass rebuilt every set"); } void BoundComesBackDown(const GuestBlock& block) { auto& cache = AgcDriver::Graphics::SharedResourceCache(); const auto grown = cache.Capacity(); - Require(grown > InitialEntries, "resource cache growth: the bound stayed at " + std::to_string(grown) + " entries after the large working set"); + Require(grown > BaseEntries, "resource cache growth: the bound stayed at " + std::to_string(grown) + " entries after the large working set"); RunPass(block, HotDispatches, HotRepeats); const auto shrunk = cache.Capacity(); Require(shrunk < grown, "resource cache growth: " + std::to_string(HotDispatches) + " sets served " + std::to_string(HotDispatches * HotRepeats) + " dispatches, but the bound stayed at " + std::to_string(shrunk) + " entries"); @@ -140,20 +141,32 @@ void BoundComesBackDown(const GuestBlock& block) { Require(cache.Capacity() == regrown, "resource cache growth: the bound came down again after the same hot pass, although the last shrink had been undone"); cache.Clear(); - Require(cache.Capacity() == InitialEntries, "resource cache growth: Clear() left the bound at " + std::to_string(cache.Capacity()) + " entries instead of " + std::to_string(InitialEntries)); + Require(cache.Capacity() == BaseEntries, "resource cache growth: Clear() left the bound at " + std::to_string(cache.Capacity()) + " entries instead of " + std::to_string(BaseEntries)); Prepare(block); for (std::uint32_t pass = 0; pass < Passes; ++pass) RunPass(block); - Require(cache.Capacity() > InitialEntries, "resource cache growth: after Clear() the large working set did not grow the bound again"); + Require(cache.Capacity() > BaseEntries, "resource cache growth: after Clear() the large working set did not grow the bound again"); +} + +void UseTheResourceCacheOnEveryDispatch() { +#ifdef _WIN32 + _putenv_s("APS5_NO_DISPATCH_RECIPE", "1"); +#else + setenv("APS5_NO_DISPATCH_RECIPE", "1", 1); +#endif } } int main() { + UseTheResourceCacheOnEveryDispatch(); try { { const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; } + auto& cache = AgcDriver::Graphics::SharedResourceCache(); + cache.SetBase(BaseEntries, BaseShrinkWindow); + cache.Clear(); GuestBlock block; Prepare(block); WorkingSetLargerThanTheInitialBound(block); From f96cdc1e7eb872466241b45554f069786677d443 Mon Sep 17 00:00:00 2001 From: MarcZX100 <MarcZX100@users.noreply.github.com> Date: Sun, 18 Oct 2026 01:10:16 +0000 Subject: [PATCH 0931/1000] fix(libc): mark unimplemented NID handler as non-returning --- core/libs/prx/libc/include/General.hpp | 2 +- core/libs/prx/libc/src/General.cpp | 2 +- core/libs/tests/WindowsExceptions.cpp | 2 +- core/libs/tests/shared_memory/Support.cpp | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libc/include/General.hpp b/core/libs/prx/libc/include/General.hpp index df34c7d52b..34cee2743f 100644 --- a/core/libs/prx/libc/include/General.hpp +++ b/core/libs/prx/libc/include/General.hpp @@ -9,7 +9,7 @@ #include "general/VabiMacros.hpp" #include "general/ExportMacros.hpp" -extern "C" void NotImplemented_nid_no_patch(const char* funcName); +extern "C" [[noreturn]] void NotImplemented_nid_no_patch(const char* funcName); extern "C" void CxaFinalize_nid_no_patch(void* dsoHandle); extern "C" bool StopAtPrefixLetter_nid_no_patch(const char* str, char** endptr, int base); #ifndef _WIN32 diff --git a/core/libs/prx/libc/src/General.cpp b/core/libs/prx/libc/src/General.cpp index 0f18419dfd..d96670de58 100644 --- a/core/libs/prx/libc/src/General.cpp +++ b/core/libs/prx/libc/src/General.cpp @@ -614,6 +614,6 @@ extern "C" void KeepStandardHandle_nid_no_patch(int descriptor, void* handle) { } #endif -extern "C" void NotImplemented_nid_no_patch(const char* funcName) { +extern "C" [[noreturn]] void NotImplemented_nid_no_patch(const char* funcName) { throw std::runtime_error(std::string(funcName) + " not implemented"); } diff --git a/core/libs/tests/WindowsExceptions.cpp b/core/libs/tests/WindowsExceptions.cpp index 99a3ca9282..19a4bb2e86 100644 --- a/core/libs/tests/WindowsExceptions.cpp +++ b/core/libs/tests/WindowsExceptions.cpp @@ -57,7 +57,7 @@ static void TestTypeInfoVtables() { if (catchType(&other, &single, &object, 0)) throw std::runtime_error("unrelated RTTI catch succeeded"); } -extern "C" void NotImplemented_nid_no_patch(const char*); +extern "C" [[noreturn]] void NotImplemented_nid_no_patch(const char*); static void RequireHandle(bool value) { if (!value) throw std::runtime_error("child process setup failed"); diff --git a/core/libs/tests/shared_memory/Support.cpp b/core/libs/tests/shared_memory/Support.cpp index 03f202e4ef..e9df457817 100644 --- a/core/libs/tests/shared_memory/Support.cpp +++ b/core/libs/tests/shared_memory/Support.cpp @@ -6,6 +6,6 @@ extern "C" int* APS5_VABI __error_nid_postfix() { return &error; } -extern "C" void NotImplemented_nid_no_patch(const char* name) { +extern "C" [[noreturn]] void NotImplemented_nid_no_patch(const char* name) { throw std::runtime_error(name); } From 304798015b0142e82ac40aab092a5bfe45edc317 Mon Sep 17 00:00:00 2001 From: Nikhil Panchal <37629345+nikhilpanchal1@users.noreply.github.com> Date: Sat, 10 Oct 2026 23:27:02 +0900 Subject: [PATCH 0932/1000] fix(agc): harden host import construction and mutation waits Guard incomplete imports and preserve mutation waiter errors and progress. Follow-up to #2575; refs #2352. --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 61 ++ .../Graphics/src/GuestBufferMemory.cpp | 183 ++-- .../tests/GuestMemoryLifetimeFixture.cpp | 82 ++ .../tests/GuestMemoryLifetimeSupport.hpp | 34 + .../tests/HostImportLifetime.cpp | 803 ++++++++++++++++++ .../tests/HostImportLifetime.hpp | 10 + .../prx/libSceAgcDriver/tests/Recorder.cpp | 3 + .../tests/execution/GuestMemoryLifetime.cpp | 259 ++++++ .../tests/test_guest_memory_lifetime.py | 143 ++++ core/libs/prx/libc/src/GuestAllocations.cpp | 9 +- 10 files changed, 1500 insertions(+), 87 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/GuestMemoryLifetimeFixture.cpp create mode 100644 core/libs/prx/libSceAgcDriver/tests/GuestMemoryLifetimeSupport.hpp create mode 100644 core/libs/prx/libSceAgcDriver/tests/HostImportLifetime.cpp create mode 100644 core/libs/prx/libSceAgcDriver/tests/HostImportLifetime.hpp create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/GuestMemoryLifetime.cpp create mode 100644 core/libs/prx/libSceAgcDriver/tests/test_guest_memory_lifetime.py diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 1927c61f6f..951a7c85ed 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1093,6 +1093,7 @@ add_custom_command( add_test_executable(agc_driver_recorder_tests EXCLUDE_FROM_ALL tests/Recorder.cpp + tests/HostImportLifetime.cpp tests/ResidentPresent.cpp ${agcTriangleHeaders} ${agcSampleLodShaderHeader} @@ -1273,6 +1274,22 @@ if(BUILD_TESTING) set_tests_properties(agc_pending_key_stores agc_shadowed_key_stores agc_sample_dumps agc_draw_descriptors PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>") endif() + add_test(NAME agc_driver_host_import_lifetime COMMAND agc_driver_recorder_tests --host-import-lifetime-only) + add_test(NAME agc_driver_host_import_mutation_race COMMAND agc_driver_recorder_tests --host-import-mutation-race-only) + set_tests_properties(agc_driver_host_import_lifetime agc_driver_host_import_mutation_race PROPERTIES + ENVIRONMENT "APS5_PIN_WAIT_MS=2000" SKIP_RETURN_CODE 77 TIMEOUT 30) + add_test(NAME agc_driver_unit_shadow COMMAND agc_driver_recorder_tests --unit-shadow-only) + add_test(NAME agc_driver_unit_shadow_budget COMMAND agc_driver_recorder_tests --unit-shadow-only) + set_tests_properties(agc_driver_unit_shadow PROPERTIES + ENVIRONMENT "APS5_PIN_WAIT_MS=2000" SKIP_RETURN_CODE 77 TIMEOUT 30) + set_tests_properties(agc_driver_unit_shadow_budget PROPERTIES + ENVIRONMENT "APS5_PIN_WAIT_MS=2000;APS5_UNIT_SHADOW_MIB=8" SKIP_RETURN_CODE 77 TIMEOUT 30) + if(WIN32) + set_tests_properties(agc_driver_host_import_lifetime agc_driver_host_import_mutation_race agc_driver_unit_shadow agc_driver_unit_shadow_budget PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>") + endif() + set_property(TEST agc_driver_host_import_lifetime APPEND PROPERTY ENVIRONMENT_MODIFICATION + "APS5_RELEASE_UNDER_LOCK=unset:" "APS5_RELEASE_ON_UNLOCK=unset:") endif() add_test_executable(agc_recorder_kept_bytes_tests EXCLUDE_FROM_ALL tests/RecorderKeptBytes.cpp) @@ -1635,6 +1652,8 @@ target_link_libraries(agc_driver_interpolation_modes_tests PRIVATE shader_recomp target_include_directories(agc_driver_prepared_unregistered_graphics_tests PRIVATE $<TARGET_PROPERTY:shader_recompiler_agc_driver,INTERFACE_INCLUDE_DIRECTORIES>) +target_link_libraries(agc_driver_guest_memory_lifetime_tests PRIVATE libkernel) + target_include_directories(agc_driver_prepared_shaders_tests PRIVATE $<TARGET_PROPERTY:shader_recompiler_agc_driver,INTERFACE_INCLUDE_DIRECTORIES> ) @@ -1849,6 +1868,48 @@ if(BUILD_TESTING) set_tests_properties(agc_driver_uint8x4_color_target_no_resident PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>") endif() + set(agcGuestMemoryLifetimeTests agc_driver_guest_memory_lifetime) + foreach(mode IN ITEMS copy direct locked heap heap_uncached) + set(testName agc_driver_guest_memory_lifetime_${mode}) + if(mode STREQUAL "heap_uncached") + add_test(NAME ${testName} COMMAND agc_driver_guest_memory_lifetime_tests heap) + else() + add_test(NAME ${testName} COMMAND agc_driver_guest_memory_lifetime_tests ${mode}) + endif() + list(APPEND agcGuestMemoryLifetimeTests ${testName}) + endforeach() + set_tests_properties(${agcGuestMemoryLifetimeTests} PROPERTIES + ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_PIN_WAIT_MS=1000;APS5_HEAP_CACHE_MIB=4" + SKIP_RETURN_CODE 77 TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + set_tests_properties(agc_driver_guest_memory_lifetime_heap_uncached PROPERTIES + ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_PIN_WAIT_MS=1000;APS5_HEAP_CACHE_MIB=0") + if(WIN32) + set_tests_properties(${agcGuestMemoryLifetimeTests} PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>") + endif() + if(CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64" AND Python3_Interpreter_FOUND) + add_library(agc_guest_memory_lifetime_fixture SHARED tests/GuestMemoryLifetimeFixture.cpp) + target_include_directories(agc_guest_memory_lifetime_fixture PRIVATE + ${LIBS_INCLUDE_DIR} + ${CMAKE_SOURCE_DIR}/core/shader/recompiler + ${CMAKE_SOURCE_DIR}/3rdparty/Vulkan-Headers/include + ${CMAKE_SOURCE_DIR}/3rdparty/SPIRV-Headers/include) + target_link_libraries(agc_guest_memory_lifetime_fixture PRIVATE libSceAgcDriver SDL2-static libc) + add_dependencies(agc_guest_memory_lifetime_fixture relinker libkernel) + set_target_properties(agc_guest_memory_lifetime_fixture PROPERTIES + OUTPUT_NAME GuestMemoryFixture SUFFIX ".prx" CXX_EXTENSIONS OFF + BUILD_WITH_INSTALL_RPATH TRUE INSTALL_RPATH "$ORIGIN") + add_test(NAME agc_driver_guest_memory_lifetime_converted COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tests/test_guest_memory_lifetime.py + ${CMAKE_SOURCE_DIR}/core/relinker/relinker/tests + $<TARGET_FILE:relinker> + $<TARGET_FILE:agc_guest_memory_lifetime_fixture> + $<TARGET_FILE:libkernel> + $<TARGET_FILE:libc> + $<TARGET_FILE:libSceAgcDriver>) + set_tests_properties(agc_driver_guest_memory_lifetime_converted PROPERTIES + ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_PIN_WAIT_MS=1000" SKIP_RETURN_CODE 77 TIMEOUT 30) + endif() set_tests_properties(agc_driver_flat_store PROPERTIES RUN_SERIAL TRUE) set_target_properties(agc_driver_flat_apertures_tests PROPERTIES EXCLUDE_FROM_ALL OFF) foreach(flatAperturesPart RANGE 13) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index a44b59cb26..a836b30afa 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -1036,7 +1036,30 @@ std::uint64_t evictableBytes(const HostImports& state, std::uint64_t begin, std: return bytes; } +struct ImportConstruction { + ImportConstruction(const Context& context, std::uint64_t base, std::uint64_t bytes) + : device(context.device), destroyBuffer(context.Function<PFN_vkDestroyBuffer>("vkDestroyBuffer")), freeMemory(context.Function<PFN_vkFreeMemory>("vkFreeMemory")), entry{base, bytes, VK_NULL_HANDLE, VK_NULL_HANDLE, 0} {} + ImportConstruction(const ImportConstruction&) = delete; + ImportConstruction& operator=(const ImportConstruction&) = delete; + ~ImportConstruction() { + if (!armed) return; + if (entry.buffer != VK_NULL_HANDLE) destroyBuffer(device, entry.buffer, nullptr); + if (entry.memory != VK_NULL_HANDLE) freeMemory(device, entry.memory, nullptr); +#ifdef _WIN32 + GuestArena::GuestArenaUnmapAlias_nid_postfix(entry.alias); +#endif + } + VkDevice device; + PFN_vkDestroyBuffer destroyBuffer; + PFN_vkFreeMemory freeMemory; + HostImport entry; + bool armed = true; +}; + +void ensurePinWaiter(); + const HostImport* importAllocation(const Context& context, HostImports& state, std::uint64_t address, const GuestAllocations::Lease& lease, std::uint64_t end = 0, bool evict = true) { + ensurePinWaiter(); const bool fresh = !importsStale(context, state); if (fresh && end != 0) { if (const auto* current = findImport(state, address, end)) return current; @@ -1113,7 +1136,8 @@ const HostImport* importRunAt(const Context& context, HostImports& state, Import it = next; } if (state.liveBytes + bytes > budget) retireLeastRecent(context, state, lease, state.liveBytes + bytes - budget); - HostImport entry{base, bytes, VK_NULL_HANDLE, VK_NULL_HANDLE, 0}; + ImportConstruction owner(context, base, bytes); + auto& entry = owner.entry; entry.range = run.ranges.front(); entry.ranges = std::move(run.ranges); #ifdef _WIN32 @@ -1208,9 +1232,6 @@ const HostImport* importRunAt(const Context& context, HostImports& state, Import #endif entry.unwatched = step == nullptr && !(entry.dmaBuf ? watchedDmaBuf : watchedHostPointer); if (step != nullptr) { -#ifdef _WIN32 - GuestArena::GuestArenaUnmapAlias_nid_postfix(entry.alias); -#endif failures.insert_or_assign(base, ImportFailure{base, bytes, GuestAllocations::Mapped(entry.ranges.begin(), entry.ranges.end())}); std::fprintf(stderr, "[gpu] host import of 0x%llx+0x%llx failed at %s (%d); falling back to copies\n", static_cast<unsigned long long>(base), static_cast<unsigned long long>(bytes), step, static_cast<int>(result)); #ifdef _WIN32 @@ -1244,7 +1265,9 @@ const HostImport* importRunAt(const Context& context, HostImports& state, Import if (trace) std::fprintf(stderr, "[gpu] host import of 0x%llx+0x%llx ok (%zu live, %.1f MiB live, %.1f MiB ever)%s\n", static_cast<unsigned long long>(base), static_cast<unsigned long long>(bytes), state.imports.size() + 1, state.liveBytes / 1048576.0, importedBytes / 1048576.0, joined ? ", joined at bind" : ""); entry.joined = joined; entry.lastUse = ++state.useCounter; - return &state.imports.emplace(base, entry).first->second; + const auto inserted = state.imports.emplace(base, entry); + owner.armed = false; + return &inserted.first->second; } // The readable registered range containing [begin, end), or null. @@ -2078,7 +2101,7 @@ SnapshotStats& Snapshots() { return stats; } -bool WaitForLeasesAndImports(std::uintptr_t address, std::size_t bytes) noexcept { +bool WaitForLeasesAndImports(std::uintptr_t address, std::size_t bytes) { const auto start = std::chrono::steady_clock::now(); bool synced = false; bool drained = false; @@ -2098,19 +2121,82 @@ bool WaitForLeasesAndImports(std::uintptr_t address, std::size_t bytes) noexcept state.stats.contentionMs += std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - start).count(); return true; } + if (Recorder::InCompletion()) throw std::runtime_error("guest allocation mutation cannot wait for an active GPU completion"); if (GuestMemory::GpuMutex().HeldByThisThread()) { - try { - auto* recorder = Recorder::Active(); + auto* recorder = Recorder::Active(); + if (recorder != nullptr && !recorder->Idle()) { + Recorder::CountSync(4); + recorder->Sync(); + synced = true; + drained = true; + } + if (recorder != nullptr) recorder->FlushDeferredReleases(); + const auto end = static_cast<std::uint64_t>(address) + bytes; + auto& imports = Imports(); + if (releaseParkedImports(imports, address, end)) retiredImport = true; + const auto lease = GuestAllocations::GuestAllocationsAcquire_nid_postfix(); + { + std::lock_guard importsLock(imports.mutex); + for (auto it = imports.imports.begin(); it != imports.imports.end();) { + const auto base = it->first; + if (base >= end || address >= base + it->second.bytes) { + ++it; + continue; + } + const auto next = std::next(it); + retireImport(imports.device, imports, it, lease, false); + it = next; + retiredImport = true; + } + } + if (retiredImport && recorder != nullptr && !recorder->Idle()) { + Recorder::CountSync(4); + recorder->Sync(); + synced = true; + drained = true; + } + if (retiredImport && recorder != nullptr) recorder->FlushDeferredReleases(); + } else { + std::lock_guard lock(GuestMemory::GpuMutex()); + auto& state = Leases(); + auto* recorder = Recorder::Active(); + const auto target = state.newestLeaseSerial; + if (recorder != nullptr && recorder == state.leaseRecorder && target > state.finishedLeaseSerial) { + Recorder::CountSync(4); + recorder->Submit(); + recorder->FinishUpTo(target); + state.finishedLeaseSerial = target; + synced = true; + } else if (recorder != nullptr && !recorder->Idle()) { + Recorder::CountSync(4); + recorder->Sync(); + synced = true; + drained = true; + } else { + std::this_thread::yield(); + } + if (recorder != nullptr) recorder->FlushDeferredReleases(); + + const auto end = static_cast<std::uint64_t>(address) + bytes; + auto& imports = Imports(); + if (releaseParkedImports(imports, address, end)) retiredImport = true; + bool overlapsImport = false; + { + std::lock_guard importsLock(imports.mutex); + for (const auto& [base, entry] : imports.imports) { + if (base < end && address < base + entry.bytes) { + overlapsImport = true; + break; + } + } + } + if (overlapsImport) { if (recorder != nullptr && !recorder->Idle()) { Recorder::CountSync(4); recorder->Sync(); synced = true; drained = true; } - if (recorder != nullptr) recorder->FlushDeferredReleases(); - const auto end = static_cast<std::uint64_t>(address) + bytes; - auto& imports = Imports(); - if (releaseParkedImports(imports, address, end)) retiredImport = true; const auto lease = GuestAllocations::GuestAllocationsAcquire_nid_postfix(); { std::lock_guard importsLock(imports.mutex); @@ -2132,79 +2218,6 @@ bool WaitForLeasesAndImports(std::uintptr_t address, std::size_t bytes) noexcept synced = true; drained = true; } - if (retiredImport && recorder != nullptr) recorder->FlushDeferredReleases(); - } catch (const std::exception& error) { - std::fprintf(stderr, "[gpu] lease wait failed: %s\n", error.what()); - } - } else { - try { - std::lock_guard lock(GuestMemory::GpuMutex()); - auto& state = Leases(); - auto* recorder = Recorder::Active(); - const auto target = state.newestLeaseSerial; - if (recorder != nullptr && recorder == state.leaseRecorder && target > state.finishedLeaseSerial) { - // The open batch may be the target; FinishUpTo waits front to back up to it only. - Recorder::CountSync(4); - recorder->Submit(); - recorder->FinishUpTo(target); - state.finishedLeaseSerial = target; - synced = true; - } else if (recorder != nullptr && !recorder->Idle()) { - Recorder::CountSync(4); - recorder->Sync(); - synced = true; - drained = true; - } else { - // Nothing recorded holds the lease: the holder is a lease-holding object that has - // yet to record (a test's, or a build whose worker gave up the lock); give it time. - std::this_thread::yield(); - } - if (recorder != nullptr) recorder->FlushDeferredReleases(); - - const auto end = static_cast<std::uint64_t>(address) + bytes; - auto& imports = Imports(); - if (releaseParkedImports(imports, address, end)) retiredImport = true; - bool overlapsImport = false; - { - std::lock_guard importsLock(imports.mutex); - for (const auto& [base, entry] : imports.imports) { - if (base < end && address < base + entry.bytes) { - overlapsImport = true; - break; - } - } - } - if (overlapsImport) { - if (recorder != nullptr && !recorder->Idle()) { - Recorder::CountSync(4); - recorder->Sync(); - synced = true; - drained = true; - } - const auto lease = GuestAllocations::GuestAllocationsAcquire_nid_postfix(); - { - std::lock_guard importsLock(imports.mutex); - for (auto it = imports.imports.begin(); it != imports.imports.end();) { - const auto base = it->first; - if (base >= end || address >= base + it->second.bytes) { - ++it; - continue; - } - const auto next = std::next(it); - retireImport(imports.device, imports, it, lease, false); - it = next; - retiredImport = true; - } - } - if (retiredImport && recorder != nullptr && !recorder->Idle()) { - Recorder::CountSync(4); - recorder->Sync(); - synced = true; - drained = true; - } - } - } catch (const std::exception& error) { - std::fprintf(stderr, "[gpu] lease wait failed: %s\n", error.what()); } } auto& state = Leases(); diff --git a/core/libs/prx/libSceAgcDriver/tests/GuestMemoryLifetimeFixture.cpp b/core/libs/prx/libSceAgcDriver/tests/GuestMemoryLifetimeFixture.cpp new file mode 100644 index 0000000000..465575f5dd --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/GuestMemoryLifetimeFixture.cpp @@ -0,0 +1,82 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/tests/execution/VulkanTestDevice.hpp" +#include "prx/libSceAgcDriver/tests/GuestMemoryLifetimeSupport.hpp" +#include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include <array> +#include <cstdint> +#include <cstdio> +#include <cstdlib> +#include <memory> +#include <mutex> + +namespace { +std::unique_ptr<AgcDriver::VulkanDevice> device; +constexpr std::array<std::uint32_t, 4> Pattern{0x12345678u, 0x89abcdefu, 0x24681357u, 0xfedcba98u}; +} + +extern "C" int APS5_VABI Begin_nid_no_patch() { + try { + device = OpenVulkanTestDevice(); + if (!device || !GuestMemoryLifetimeHasHostImports(device->Device())) { + device.reset(); + std::fflush(stdout); + std::exit(VulkanTestSkipped); + } + return 0; + } catch (const std::exception& error) { + std::fprintf(stderr, "converted guest Vulkan initialization failed: %s\n", error.what()); + std::exit(1); + } +} + +extern "C" int APS5_VABI Fill_nid_no_patch(void* pointer, int gpu) { + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + { + GuestAllocations::Mutation mutation; + const auto range = mutation.Find(pointer); + if (!range.readable || !range.writable || range.gpu != (gpu != 0)) { + std::fputs("converted guest mapping does not match its requested CPU and GPU access\n", stderr); + return 1; + } + } + if (!device->FillBuffer(reinterpret_cast<std::uintptr_t>(pointer), 4096, Pattern)) return 1; + AgcDriver::Graphics::Context probe{}; + probe.device = device->Device(); + probe.hostImportAlignment = 1; + return !AgcDriver::Graphics::HostImportCovers(probe, reinterpret_cast<std::uintptr_t>(pointer), 65536); +} + +extern "C" int APS5_VABI Retired_nid_no_patch(const void* pointer) { + AgcDriver::Graphics::Context probe{}; + probe.device = device->Device(); + probe.hostImportAlignment = 1; + const bool imported = AgcDriver::Graphics::HostImportCovers(probe, reinterpret_cast<std::uintptr_t>(pointer), 65536); + if (imported) std::fputs("converted guest observed a stale Vulkan host import\n", stderr); + return imported; +} + +extern "C" int APS5_VABI Verify_nid_no_patch(const void* pointer, int zero) { + const auto* bytes = static_cast<const std::uint8_t*>(pointer); + for (std::size_t offset = 0; offset < 4096; ++offset) { + const auto expected = zero ? std::uint8_t{0} : reinterpret_cast<const std::uint8_t*>(Pattern.data())[offset % sizeof(Pattern)]; + if (bytes[offset] != expected) { + std::fprintf(stderr, "converted guest memory mismatch at byte %zu: expected %u, observed %u\n", + offset, unsigned(expected), unsigned(bytes[offset])); + return 1; + } + } + return 0; +} + +extern "C" [[noreturn]] void APS5_VABI Finish_nid_no_patch(int result) { + { + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + device->WaitIdle(); + } + device.reset(); + std::printf("converted guest memory/Vulkan lifetime result=%d\n", result); + std::fflush(stdout); + std::exit(result); +} diff --git a/core/libs/prx/libSceAgcDriver/tests/GuestMemoryLifetimeSupport.hpp b/core/libs/prx/libSceAgcDriver/tests/GuestMemoryLifetimeSupport.hpp new file mode 100644 index 0000000000..863b8ca67f --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/GuestMemoryLifetimeSupport.hpp @@ -0,0 +1,34 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_TESTS_GUESTMEMORYLIFETIMESUPPORT_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_TESTS_GUESTMEMORYLIFETIMESUPPORT_HPP + +#include <SDL_error.h> +#include <SDL_loadso.h> +#include <vulkan/vulkan.h> +#include <cstdio> +#include <cstdlib> +#include <memory> +#include <stdexcept> +#include <string> + +static inline bool GuestMemoryLifetimeHasHostImports(VkDevice device) { + if (std::getenv("APS5_NO_HOST_IMPORT") != nullptr) { + std::puts("skipped, host imports are disabled by APS5_NO_HOST_IMPORT"); + return false; + } +#ifdef _WIN32 + constexpr const char* libraryName = "vulkan-1.dll"; +#else + constexpr const char* libraryName = "libvulkan.so.1"; +#endif + const std::unique_ptr<void, decltype(&SDL_UnloadObject)> library(SDL_LoadObject(libraryName), &SDL_UnloadObject); + if (!library) throw std::runtime_error(std::string("cannot load Vulkan for the host import capability query: ") + SDL_GetError()); + const auto deviceProc = reinterpret_cast<PFN_vkGetDeviceProcAddr>(SDL_LoadFunction(library.get(), "vkGetDeviceProcAddr")); + if (deviceProc == nullptr) throw std::runtime_error(std::string("cannot resolve vkGetDeviceProcAddr for the host import capability query: ") + SDL_GetError()); + if (deviceProc(device, "vkGetMemoryHostPointerPropertiesEXT") == nullptr) { + std::puts("skipped, the device has no VK_EXT_external_memory_host"); + return false; + } + return true; +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/tests/HostImportLifetime.cpp b/core/libs/prx/libSceAgcDriver/tests/HostImportLifetime.cpp new file mode 100644 index 0000000000..c507b67de3 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/HostImportLifetime.cpp @@ -0,0 +1,803 @@ +#include "HostImportLifetime.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include <algorithm> +#include <atomic> +#include <chrono> +#include <condition_variable> +#include <future> +#include <cstdio> +#include <cstring> +#include <exception> +#include <map> +#include <memory> +#include <mutex> +#include <new> +#include <stdexcept> +#include <string> +#include <thread> +#include <vector> + +namespace { + +using namespace AgcDriver::Graphics; +using AgcDriver::GuestMemory::GpuMutex; + +struct ImportState { + VkBuffer buffer; + VkDeviceMemory memory; + std::uint64_t address; + std::size_t bytes; + std::atomic<bool> destroyed{false}; + std::atomic<bool> freed{false}; + std::atomic<unsigned> frees{0}; + std::atomic<bool> outOfOrder{false}; + std::atomic<bool> inaccessible{false}; + std::mutex mutex; + std::condition_variable wake; + bool blockFree = false; + bool freeing = false; + bool allowFree = false; + std::thread::id freeingThread; +}; + +struct Tracking { + PFN_vkGetDeviceProcAddr resolve; + VkDevice device; + std::mutex mutex; + std::map<VkDeviceMemory, std::shared_ptr<ImportState>> imports; + std::atomic<bool> failAddressLookup{false}; + std::atomic<bool> failQueueSubmit{false}; + std::atomic<unsigned> rejectedSubmissions{0}; + std::atomic<unsigned> acceptedSubmissions{0}; + std::vector<VkFence> rejectedFences; + std::shared_ptr<ImportState> lastAllocated; + + void CleanupRejectedFences() { + std::vector<VkFence> fences; + { + std::lock_guard lock(mutex); + fences.swap(rejectedFences); + } + const auto destroy = reinterpret_cast<PFN_vkDestroyFence>(resolve(device, "vkDestroyFence")); + for (const auto fence : fences) destroy(device, fence, nullptr); + } + + std::shared_ptr<ImportState> Add(const HostImport& import) { + auto state = std::make_shared<ImportState>(); + state->buffer = import.buffer; + state->memory = import.memory; + state->address = import.base; + state->bytes = import.bytes; + std::lock_guard lock(mutex); + imports[import.memory] = state; + return state; + } +}; + +Tracking* tracking = nullptr; + +VKAPI_ATTR void VKAPI_CALL destroyBuffer(VkDevice device, VkBuffer buffer, const VkAllocationCallbacks* allocator) { + const auto original = reinterpret_cast<PFN_vkDestroyBuffer>(tracking->resolve(device, "vkDestroyBuffer")); + original(device, buffer, allocator); + std::lock_guard lock(tracking->mutex); + for (const auto& [memory, state] : tracking->imports) { + if (state->buffer == buffer && !state->freed.load()) state->destroyed.store(true); + } +} + +VKAPI_ATTR void VKAPI_CALL freeMemory(VkDevice device, VkDeviceMemory memory, const VkAllocationCallbacks* allocator) { + std::shared_ptr<ImportState> state; + { + std::lock_guard lock(tracking->mutex); + const auto found = tracking->imports.find(memory); + if (found != tracking->imports.end()) state = found->second; + } + if (state != nullptr) { + if (!state->destroyed.load()) state->outOfOrder.store(true); + if (!RegisteredReadableCovers(state->address, state->bytes) || !AgcDriver::GuestMemory::Accessible(reinterpret_cast<const void*>(state->address), state->bytes, false)) state->inaccessible.store(true); + std::unique_lock lock(state->mutex); + if (state->blockFree) { + state->freeing = true; + state->freeingThread = std::this_thread::get_id(); + state->wake.notify_all(); + if (!state->wake.wait_for(lock, std::chrono::seconds(2), [&] { return state->allowFree; })) state->outOfOrder.store(true); + } + } + const auto original = reinterpret_cast<PFN_vkFreeMemory>(tracking->resolve(device, "vkFreeMemory")); + original(device, memory, allocator); + if (state != nullptr) { + if (!RegisteredReadableCovers(state->address, state->bytes) || !AgcDriver::GuestMemory::Accessible(reinterpret_cast<const void*>(state->address), state->bytes, false)) state->inaccessible.store(true); + state->frees.fetch_add(1); + state->freed.store(true); + } +} + +VKAPI_ATTR VkResult VKAPI_CALL allocateMemory(VkDevice device, const VkMemoryAllocateInfo* allocation, const VkAllocationCallbacks* allocator, VkDeviceMemory* memory) { + const auto original = reinterpret_cast<PFN_vkAllocateMemory>(tracking->resolve(device, "vkAllocateMemory")); + const auto result = original(device, allocation, allocator, memory); + if (result != VK_SUCCESS) return result; + for (auto* next = static_cast<const VkBaseInStructure*>(allocation->pNext); next != nullptr; next = next->pNext) { + if (next->sType != VK_STRUCTURE_TYPE_IMPORT_MEMORY_HOST_POINTER_INFO_EXT) continue; + const auto* host = reinterpret_cast<const VkImportMemoryHostPointerInfoEXT*>(next); + auto state = std::make_shared<ImportState>(); + state->buffer = VK_NULL_HANDLE; + state->memory = *memory; + state->address = reinterpret_cast<std::uint64_t>(host->pHostPointer); + state->bytes = static_cast<std::size_t>(allocation->allocationSize); + std::lock_guard lock(tracking->mutex); + tracking->imports[*memory] = state; + tracking->lastAllocated = state; + } + return result; +} + +VKAPI_ATTR VkResult VKAPI_CALL bindBufferMemory(VkDevice device, VkBuffer buffer, VkDeviceMemory memory, VkDeviceSize offset) { + { + std::lock_guard lock(tracking->mutex); + if (const auto found = tracking->imports.find(memory); found != tracking->imports.end()) found->second->buffer = buffer; + } + const auto original = reinterpret_cast<PFN_vkBindBufferMemory>(tracking->resolve(device, "vkBindBufferMemory")); + return original(device, buffer, memory, offset); +} + +VKAPI_ATTR void VKAPI_CALL destroyFence(VkDevice device, VkFence fence, const VkAllocationCallbacks* allocator) { + const auto original = reinterpret_cast<PFN_vkDestroyFence>(tracking->resolve(device, "vkDestroyFence")); + original(device, fence, allocator); + std::lock_guard lock(tracking->mutex); + std::erase(tracking->rejectedFences, fence); +} + +VKAPI_ATTR VkResult VKAPI_CALL queueSubmit(VkQueue queue, std::uint32_t count, const VkSubmitInfo* submissions, VkFence fence) { + if (tracking->failQueueSubmit.exchange(false)) { + std::lock_guard lock(tracking->mutex); + tracking->rejectedFences.push_back(fence); + tracking->rejectedSubmissions.fetch_add(1); + return VK_ERROR_OUT_OF_HOST_MEMORY; + } + const auto original = reinterpret_cast<PFN_vkQueueSubmit>(tracking->resolve(tracking->device, "vkQueueSubmit")); + const auto result = original(queue, count, submissions, fence); + if (result == VK_SUCCESS) { + std::lock_guard lock(tracking->mutex); + std::erase(tracking->rejectedFences, fence); + tracking->acceptedSubmissions.fetch_add(1); + } + return result; +} + +PFN_vkVoidFunction VKAPI_CALL deviceProc(VkDevice device, const char* name) { + if (std::strcmp(name, "vkDestroyFence") == 0) return reinterpret_cast<PFN_vkVoidFunction>(destroyFence); + if (std::strcmp(name, "vkQueueSubmit") == 0) return reinterpret_cast<PFN_vkVoidFunction>(queueSubmit); + if (std::strcmp(name, "vkDestroyBuffer") == 0) return reinterpret_cast<PFN_vkVoidFunction>(destroyBuffer); + if (std::strcmp(name, "vkFreeMemory") == 0) return reinterpret_cast<PFN_vkVoidFunction>(freeMemory); + if (std::strcmp(name, "vkAllocateMemory") == 0) return reinterpret_cast<PFN_vkVoidFunction>(allocateMemory); + if (std::strcmp(name, "vkBindBufferMemory") == 0) return reinterpret_cast<PFN_vkVoidFunction>(bindBufferMemory); + if (std::strcmp(name, "vkGetBufferDeviceAddressKHR") == 0 && tracking->failAddressLookup.exchange(false)) return nullptr; + return tracking->resolve(device, name); +} + +void requireFreed(const std::shared_ptr<ImportState>& state) { + Require(state->freed.load(), "guest mutation reached host apply before vkFreeMemory completed"); + Require(state->destroyed.load() && !state->outOfOrder.load() && state->frees.load() == 1, "host import Vulkan objects were destroyed out of order or more than once"); + Require(!state->inaccessible.load(), "guest pages became inaccessible before vkFreeMemory returned"); +} + +class Fixture { +public: + explicit Fixture(const Context& original) : context(original), calls{original.deviceProc, original.device} { + Require(tracking == nullptr, "host import tracking is already active"); + tracking = &calls; + context.deviceProc = deviceProc; + context.functions = nullptr; + std::lock_guard gpu(GpuMutex()); + recorder = std::make_unique<Recorder>(context); + recorder->Activate(); + } + + ~Fixture() { + std::lock_guard gpu(GpuMutex()); + recorder.reset(); + calls.CleanupRejectedFences(); + ClearHostImports(context.device); + context.bufferPool.reset(); + tracking = nullptr; + } + + std::shared_ptr<ImportState> Import(std::uint64_t address, std::size_t bytes) { + std::lock_guard gpu(GpuMutex()); + const auto* imported = HostImportFor(context, address, bytes); + Require(imported != nullptr, "host pointer import was refused for the lifetime test"); + return calls.Add(*imported); + } + + void Cleanup() { + std::lock_guard gpu(GpuMutex()); + recorder->Sync(); + ClearHostImports(context.device); + } + + Context context; + Tracking calls; + std::unique_ptr<Recorder> recorder; +}; + +class Allocation { +public: + Allocation(Fixture& fixture, std::size_t units = 1) : fixture(fixture) { + alignment = static_cast<std::size_t>(std::max<VkDeviceSize>(65536, fixture.context.hostImportAlignment)); + bytes = alignment * units; + data = ::operator new(bytes, std::align_val_t{alignment}); + std::memset(data, 0x37, bytes); + GuestAllocations::Mutation mutation; + mutation.Add(data, bytes, true, true, true); + } + + ~Allocation() { + try { + if (registered) { + fixture.Cleanup(); + GuestAllocations::Mutation mutation; + mutation.Remove(data); + } + Release(); + } catch (const std::exception& error) { + std::fprintf(stderr, "host import test cleanup failed: %s\n", error.what()); + std::terminate(); + } + } + + std::uint64_t Address() const { return reinterpret_cast<std::uint64_t>(data); } + + void Release() { + if (released) return; + ::operator delete(data, std::align_val_t{alignment}); + released = true; + } + + Fixture& fixture; + void* data = nullptr; + std::size_t alignment = 0; + std::size_t bytes = 0; + bool registered = true; + bool released = false; +}; + +void descriptorFirstImport(Fixture& fixture) { + Require(fixture.calls.lastAllocated == nullptr, "descriptor-only lifetime test did not run before the first host import"); + Allocation allocation(fixture); + std::shared_ptr<ImportState> imported; + { + std::lock_guard gpu(GpuMutex()); + GuestBufferMemory memory(fixture.context); + memory.AddReadable(allocation.Address(), allocation.bytes); + memory.Upload(false); + imported = fixture.calls.lastAllocated; + Require(imported != nullptr && imported->address == allocation.Address(), "descriptor-only upload did not import its allocation"); + std::uint32_t adjustment = 0; + const auto descriptor = memory.Descriptor(allocation.Address(), allocation.bytes, adjustment); + Require(descriptor.buffer == imported->buffer && descriptor.offset == 0 && adjustment == 0, "descriptor-only upload did not bind the tracked import"); + } + Require(HostImportCovers(fixture.context, allocation.Address(), allocation.bytes), "descriptor-only import did not persist after its build ended"); + { + GuestAllocations::Mutation mutation; + mutation.Unmap(allocation.data, allocation.bytes, [&](const void*, std::size_t, const void*, bool) { + requireFreed(imported); + allocation.Release(); + }); + allocation.registered = false; + } + Require(!HostImportCovers(fixture.context, allocation.Address(), allocation.bytes), "descriptor-only import survived allocation removal"); +} + +class PendingRetirement { +public: + explicit PendingRetirement(Fixture& fixture) : fixture(fixture), first(fixture) { + std::lock_guard gpu(GpuMutex()); + build = std::make_unique<GuestBufferMemory>(fixture.context); + build->AcquireRegistered(); + build->UploadPrepare(true); + retry = std::make_unique<GuestBufferMemory>(fixture.context); + retry->AcquireRegistered(); + retry->UploadPrepare(true); + allocation = std::make_unique<Allocation>(fixture); + imported = fixture.Import(allocation->Address(), allocation->bytes); + result = std::make_unique<Buffer>(fixture.context, allocation->bytes, VK_BUFFER_USAGE_TRANSFER_DST_BIT); + const auto commands = fixture.recorder->Commands(); + RecordMemoryBarrier(fixture.context, commands, VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_HOST_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT); + CopyBuffer(fixture.context, commands, imported->buffer, 0, result->Handle(), 0, allocation->bytes); + RecordMemoryBarrier(fixture.context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_HOST_READ_BIT); + fixture.recorder->NotePendingRead(allocation->Address(), allocation->bytes, Recorder::ReadKind::AddressBased); + fixture.recorder->Submit(); + Check(fixture.context.Function<PFN_vkQueueWaitIdle>("vkQueueWaitIdle")(fixture.context.queue), "vkQueueWaitIdle retirement fixture"); + generation = std::make_unique<Allocation>(fixture); + Require(!fixture.recorder->Idle(), "retirement fixture has no submitted batch"); + } + + ~PendingRetirement() { + if (imported != nullptr) { + { + std::lock_guard lock(imported->mutex); + imported->allowFree = true; + } + imported->wake.notify_all(); + } + build.reset(); + retry.reset(); + fixture.Cleanup(); + } + + void VerifyCopy() { + result->Invalidate(); + const auto bytes = result->Bytes(); + Require(std::all_of(bytes.begin(), bytes.end(), [](std::byte value) { return value == std::byte{0x37}; }), "retirement lost the submitted import read"); + } + + Fixture& fixture; + Allocation first; + std::unique_ptr<Buffer> result; + std::unique_ptr<Allocation> allocation; + std::unique_ptr<Allocation> generation; + std::unique_ptr<GuestBufferMemory> build; + std::unique_ptr<GuestBufferMemory> retry; + std::shared_ptr<ImportState> imported; +}; + +void deferredRetirement(Fixture& fixture) { + PendingRetirement pending(fixture); + pending.imported->blockFree = true; + { + std::lock_guard gpu(GpuMutex()); + pending.build->UploadFinish(true); + Require(!HostImportCovers(fixture.context, pending.allocation->Address(), pending.allocation->bytes), "deferred retirement left its import cached"); + Require(!pending.imported->freed.load(), "retirement did not keep its import through the pending batch"); + fixture.recorder->Sync(); + fixture.recorder->FlushDeferredReleases(); + } + bool entered = false; + bool onReleaseThread = false; + { + std::unique_lock lock(pending.imported->mutex); + entered = pending.imported->wake.wait_for(lock, std::chrono::seconds(2), [&] { return pending.imported->freeing; }); + onReleaseThread = pending.imported->freeingThread != std::this_thread::get_id(); + } + std::atomic<bool> applied{false}; + const auto waitsBefore = LeaseCounters().contentionWaits; + std::promise<void> starting; + auto started = starting.get_future(); + auto mutation = std::async(std::launch::async, [&] { + starting.set_value(); + GuestAllocations::Mutation mutation; + mutation.Unmap(pending.allocation->data, pending.allocation->bytes, [&](const void*, std::size_t, const void*, bool) { + requireFreed(pending.imported); + applied.store(true); + pending.allocation->Release(); + }); + pending.allocation->registered = false; + }); + started.wait(); + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(1); + while (LeaseCounters().contentionWaits == waitsBefore && mutation.wait_for(std::chrono::milliseconds(0)) == std::future_status::timeout && std::chrono::steady_clock::now() < deadline) std::this_thread::yield(); + const bool reachedWaiter = LeaseCounters().contentionWaits > waitsBefore; + const bool blocked = mutation.wait_for(std::chrono::milliseconds(0)) == std::future_status::timeout; + const bool premature = applied.load(); + const bool registered = RegisteredReadableCovers(pending.allocation->Address(), pending.allocation->bytes); + { + std::lock_guard lock(pending.imported->mutex); + pending.imported->allowFree = true; + } + pending.imported->wake.notify_all(); + mutation.get(); + Require(entered && onReleaseThread, "retired import destruction did not run on the recorder release thread"); + Require(reachedWaiter && blocked && !premature && registered, "guest mutation passed a retired import whose Vulkan free was still running"); + Require(applied.load(), "guest mutation did not resume after deferred Vulkan free"); + requireFreed(pending.imported); + pending.VerifyCopy(); +} + +void idleUnmap(Fixture& fixture) { + Allocation allocation(fixture); + const auto imported = fixture.Import(allocation.Address(), allocation.bytes); + { + GuestAllocations::Mutation mutation; + mutation.Unmap(allocation.data, allocation.bytes, [&](const void*, std::size_t, const void*, bool) { + requireFreed(imported); + allocation.Release(); + }); + allocation.registered = false; + } + Require(!HostImportCovers(fixture.context, allocation.Address(), allocation.bytes), "an unmapped range still has a live host import"); +} + +void pendingRead(Fixture& fixture, bool submitted, bool locked) { + Allocation allocation(fixture); + const auto imported = fixture.Import(allocation.Address(), allocation.bytes); + Buffer result(fixture.context, allocation.bytes, VK_BUFFER_USAGE_TRANSFER_DST_BIT); + std::atomic<bool> completed{false}; + struct Finish { + Fixture& fixture; + ~Finish() { fixture.Cleanup(); } + } finish{fixture}; + std::unique_lock gpu(GpuMutex()); + const auto commands = fixture.recorder->Commands(); + RecordMemoryBarrier(fixture.context, commands, VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_HOST_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT); + CopyBuffer(fixture.context, commands, imported->buffer, 0, result.Handle(), 0, allocation.bytes); + RecordMemoryBarrier(fixture.context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_HOST_READ_BIT); + fixture.recorder->NotePendingRead(allocation.Address(), allocation.bytes, Recorder::ReadKind::AddressBased); + fixture.recorder->Keep(std::make_shared<GuestAllocations::Lease>(GuestAllocations::GuestAllocationsAcquire_nid_postfix())); + fixture.recorder->OnComplete([&] { completed.store(true); }); + if (submitted) fixture.recorder->Submit(); + if (!locked) gpu.unlock(); + { + GuestAllocations::Mutation mutation; + mutation.Unmap(allocation.data, allocation.bytes, [&](const void*, std::size_t, const void*, bool) { + requireFreed(imported); + Require(completed.load(), "guest unmap passed a pending GPU read"); + result.Invalidate(); + const auto copied = result.Bytes(); + Require(std::all_of(copied.begin(), copied.end(), [](std::byte value) { return value == std::byte{0x37}; }), "GPU read lost the source contents before guest unmap"); + allocation.Release(); + }); + allocation.registered = false; + } +} + +void productionWaiterFailure(Fixture& fixture) { + Allocation allocation(fixture); + const auto imported = fixture.Import(allocation.Address(), allocation.bytes); + Buffer result(fixture.context, allocation.bytes, VK_BUFFER_USAGE_TRANSFER_DST_BIT); + bool completed = false; + struct Finish { + Fixture& fixture; + ~Finish() { fixture.Cleanup(); } + } finish{fixture}; + const auto recordCopy = [&] { + const auto commands = fixture.recorder->Commands(); + RecordMemoryBarrier(fixture.context, commands, VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_HOST_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT); + CopyBuffer(fixture.context, commands, imported->buffer, 0, result.Handle(), 0, allocation.bytes); + RecordMemoryBarrier(fixture.context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_HOST_READ_BIT); + fixture.recorder->NotePendingRead(allocation.Address(), allocation.bytes, Recorder::ReadKind::AddressBased); + fixture.recorder->Keep(std::make_shared<GuestAllocations::Lease>(GuestAllocations::GuestAllocationsAcquire_nid_postfix())); + }; + { + std::lock_guard gpu(GpuMutex()); + recordCopy(); + } + const auto rejectedBefore = fixture.calls.rejectedSubmissions.load(); + const auto acceptedBefore = fixture.calls.acceptedSubmissions.load(); + fixture.calls.failQueueSubmit.store(true); + bool applied = false; + std::string failure; + { + GuestAllocations::Mutation mutation; + try { + mutation.Protect(allocation.data, allocation.bytes, true, true, true, [&] { applied = true; }); + } catch (const std::runtime_error& error) { + failure = error.what(); + } + const auto range = mutation.Find(allocation.data); + Require(range.readable && range.writable && range.gpu, "failed production waiter changed the guest registration"); + } + Require(!fixture.calls.failQueueSubmit.load() && fixture.calls.rejectedSubmissions.load() == rejectedBefore + 1, "production pin waiter did not reach the injected queue submission failure"); + Require(fixture.calls.acceptedSubmissions.load() == acceptedBefore, "failed production waiter accepted GPU work before reporting failure"); + Require(failure == "AGC graphics: vkQueueSubmit recorder: Vulkan result -1" && !applied, "production pin waiter did not propagate the original Vulkan error before host apply"); + Require(!imported->destroyed.load() && !imported->freed.load(), "failed production waiter retired its host import"); + Require(HostImportCovers(fixture.context, allocation.Address(), allocation.bytes), "failed production waiter removed the live import"); + { + std::lock_guard gpu(GpuMutex()); + recordCopy(); + fixture.recorder->OnComplete([&] { completed = true; }); + } + { + GuestAllocations::Mutation mutation; + mutation.Unmap(allocation.data, allocation.bytes, [&](const void*, std::size_t, const void*, bool) { + requireFreed(imported); + Require(completed, "production waiter retry passed a pending GPU read"); + result.Invalidate(); + const auto copied = result.Bytes(); + Require(std::all_of(copied.begin(), copied.end(), [](std::byte value) { return value == std::byte{0x37}; }), "production waiter retry lost its real GPU copy"); + allocation.Release(); + }); + allocation.registered = false; + } + Require(fixture.calls.acceptedSubmissions.load() == acceptedBefore + 1, "production waiter retry did not submit exactly its new copy"); + requireFreed(imported); +} + +void partialUnmap(Fixture& fixture) { + Allocation allocation(fixture, 3); + Allocation unrelated(fixture); + const auto imported = fixture.Import(allocation.Address(), allocation.bytes); + const auto other = fixture.Import(unrelated.Address(), unrelated.bytes); + auto* middle = static_cast<std::byte*>(allocation.data) + allocation.alignment; + { + GuestAllocations::Mutation mutation; + mutation.Unmap(middle, allocation.alignment, [&](const void* piece, std::size_t bytes, const void* original, bool last) { + requireFreed(imported); + Require(!other->freed.load(), "partial unmap retired a disjoint host import"); + Require(piece == middle && bytes == allocation.alignment && original == allocation.data && !last, "partial unmap changed the wrong guest extent"); + }); + } + Require(!RegisteredReadableCovers(reinterpret_cast<std::uint64_t>(middle), allocation.alignment), "the middle of a partial unmap remains registered"); + const auto low = fixture.Import(allocation.Address(), allocation.alignment); + const auto high = fixture.Import(allocation.Address() + 2 * allocation.alignment, allocation.alignment); + Require(!low->freed.load() && !high->freed.load() && !other->freed.load(), "remaining partial mappings could not be imported"); + { + GuestAllocations::Mutation mutation; + mutation.Remove(allocation.data); + allocation.registered = false; + } + requireFreed(low); + requireFreed(high); + Require(!other->freed.load(), "removing a split allocation retired a disjoint import"); +} + +void protectRollback(Fixture& fixture) { + Allocation allocation(fixture); + auto imported = fixture.Import(allocation.Address(), allocation.bytes); + bool called = false; + try { + GuestAllocations::Mutation mutation; + mutation.Protect(allocation.data, allocation.bytes, true, false, true, [&] { + requireFreed(imported); + called = true; + throw std::runtime_error("expected host protection failure"); + }); + throw std::runtime_error("host protection failure was ignored"); + } catch (const std::runtime_error& error) { + Require(called && std::string(error.what()) == "expected host protection failure", "unexpected protection failure"); + } + { + GuestAllocations::Mutation mutation; + const auto range = mutation.Find(allocation.data); + Require(range.readable && range.writable && range.gpu, "failed host protection changed the guest registration"); + } + imported = fixture.Import(allocation.Address(), allocation.bytes); + { + GuestAllocations::Mutation mutation; + mutation.Protect(allocation.data, allocation.bytes, true, false, true, [&] { requireFreed(imported); }); + } + { + GuestAllocations::Mutation mutation; + const auto range = mutation.Find(allocation.data); + Require(range.readable && !range.writable && range.gpu, "successful protection did not update the guest registration"); + mutation.Protect(allocation.data, allocation.bytes, true, true, true, [] {}); + } + imported = fixture.Import(allocation.Address(), allocation.bytes); + { + GuestAllocations::Mutation mutation; + mutation.Remove(allocation.data); + allocation.registered = false; + } + requireFreed(imported); +} + +void blockedFree(Fixture& fixture) { + Allocation allocation(fixture); + const auto imported = fixture.Import(allocation.Address(), allocation.bytes); + imported->blockFree = true; + std::atomic<bool> applied{false}; + auto mutation = std::async(std::launch::async, [&] { + GuestAllocations::Mutation mutation; + mutation.Unmap(allocation.data, allocation.bytes, [&](const void*, std::size_t, const void*, bool) { + requireFreed(imported); + applied.store(true); + allocation.Release(); + }); + allocation.registered = false; + }); + bool entered = false; + { + std::unique_lock lock(imported->mutex); + entered = imported->wake.wait_for(lock, std::chrono::seconds(2), [&] { return imported->freeing; }); + } + const bool premature = applied.load(); + const bool stillRegistered = RegisteredReadableCovers(allocation.Address(), allocation.bytes); + { + std::lock_guard lock(imported->mutex); + imported->allowFree = true; + } + imported->wake.notify_all(); + mutation.get(); + Require(entered, "guest unmap never reached Vulkan import destruction"); + Require(!premature && stillRegistered, "guest unmap released its registration while vkFreeMemory was blocked"); + Require(applied.load(), "guest unmap did not resume after vkFreeMemory returned"); +} + +void failedConstruction(Fixture& fixture) { + Allocation allocation(fixture); + fixture.calls.failAddressLookup.store(true); + bool failed = false; + try { + fixture.Import(allocation.Address(), allocation.bytes); + } catch (const std::runtime_error& error) { + failed = std::string(error.what()).find("vkGetBufferDeviceAddressKHR") != std::string::npos; + } + Require(failed && !fixture.calls.failAddressLookup.load(), "import construction did not reach the injected Vulkan resolver failure"); + const auto allocated = fixture.calls.lastAllocated; + Require(allocated != nullptr && allocated->address == allocation.Address(), "resolver failure happened before host memory was imported"); + if (!allocated->freed.load()) { + destroyBuffer(fixture.context.device, allocated->buffer, nullptr); + freeMemory(fixture.context.device, allocated->memory, nullptr); + throw std::runtime_error("failed host import construction leaked its Vulkan memory"); + } + requireFreed(allocated); + Require(!HostImportCovers(fixture.context, allocation.Address(), allocation.bytes), "failed host import construction remained cached"); + const auto retried = fixture.Import(allocation.Address(), allocation.bytes); + { + GuestAllocations::Mutation mutation; + mutation.Remove(allocation.data); + allocation.registered = false; + } + requireFreed(retried); + Require(allocated->frees.load() == 1, "retry destroyed the failed import a second time"); +} + +void mutationDuringCompletion(Fixture& fixture) { + Allocation allocation(fixture); + const auto imported = fixture.Import(allocation.Address(), allocation.bytes); + Buffer result(fixture.context, allocation.bytes, VK_BUFFER_USAGE_TRANSFER_DST_BIT); + bool rejected = false; + bool applied = false; + bool laterCompletion = false; + bool copied = false; + std::lock_guard gpu(GpuMutex()); + const auto commands = fixture.recorder->Commands(); + RecordMemoryBarrier(fixture.context, commands, VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_HOST_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT); + CopyBuffer(fixture.context, commands, imported->buffer, 0, result.Handle(), 0, allocation.bytes); + RecordMemoryBarrier(fixture.context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_HOST_READ_BIT); + fixture.recorder->OnComplete([&] { + try { + GuestAllocations::Mutation mutation; + mutation.Protect(allocation.data, allocation.bytes, false, false, false, [&] { applied = true; }); + } catch (const std::runtime_error& error) { + rejected = std::string(error.what()).find("active GPU completion") != std::string::npos; + } + }); + fixture.recorder->OnComplete([&] { + laterCompletion = true; + result.Invalidate(); + const auto bytes = result.Bytes(); + copied = std::all_of(bytes.begin(), bytes.end(), [](std::byte value) { return value == std::byte{0x37}; }); + }); + fixture.recorder->Sync(); + Require(rejected && !applied, "an active completion allowed guest memory mutation before later completions"); + Require(laterCompletion && copied, "rejecting a completion mutation lost later GPU results"); + Require(!imported->freed.load(), "rejecting a completion mutation retired its host import"); + { + GuestAllocations::Mutation mutation; + const auto range = mutation.Find(allocation.data); + Require(range.readable && range.writable && range.gpu, "rejecting a completion mutation changed its guest range"); + mutation.Remove(allocation.data); + allocation.registered = false; + } + requireFreed(imported); +} + +void completedLeaseUnderLock(Fixture& fixture) { + Allocation allocation(fixture); + const auto imported = fixture.Import(allocation.Address(), allocation.bytes); + std::lock_guard gpu(GpuMutex()); + fixture.recorder->Keep(std::make_shared<GuestAllocations::Lease>(GuestAllocations::GuestAllocationsAcquire_nid_postfix())); + fixture.recorder->Sync(); + Require(fixture.recorder->Idle(), "completed lease test recorder is not idle"); + { + GuestAllocations::Mutation mutation; + mutation.Remove(allocation.data); + allocation.registered = false; + } + requireFreed(imported); +} + + +struct MutationRace { + Fixture* fixture; + Allocation* allocation; + std::shared_ptr<ImportState> replacement; + unsigned calls = 0; +}; + +MutationRace* mutationRace = nullptr; + +bool raceWaiter(std::uintptr_t address, std::size_t bytes) { + auto& race = *mutationRace; + Require(address == race.allocation->Address() && bytes == race.allocation->bytes, "pin waiter received the wrong mutation extent"); + std::lock_guard gpu(GpuMutex()); + race.fixture->Cleanup(); + if (++race.calls == 1) race.replacement = race.fixture->Import(address, bytes); + return true; +} + +bool failingWaiter(std::uintptr_t, std::size_t) { + throw std::runtime_error("expected pin waiter failure"); +} + +void waiterFailure(Fixture& fixture) { + Allocation allocation(fixture); + const auto imported = fixture.Import(allocation.Address(), allocation.bytes); + GuestAllocations::GuestAllocationsSetPinWaiter_nid_postfix(&failingWaiter); + bool failed = false; + bool excluded = false; + std::future<void> reader; + { + GuestAllocations::Mutation mutation; + try { + mutation.Protect(allocation.data, allocation.bytes, false, false, false, [] { throw std::runtime_error("host apply ran after a pin waiter failure"); }); + } catch (const std::runtime_error& error) { + failed = std::string(error.what()) == "expected pin waiter failure"; + } + std::promise<void> starting; + auto ready = starting.get_future(); + reader = std::async(std::launch::async, [&] { + starting.set_value(); + static_cast<void>(GuestAllocations::GuestAllocationsAcquire_nid_postfix()); + }); + ready.wait(); + excluded = reader.wait_for(std::chrono::milliseconds(20)) == std::future_status::timeout; + const auto range = mutation.Find(allocation.data); + Require(range.readable && range.writable && range.gpu, "a pin waiter failure changed the registered permissions"); + } + reader.get(); + Require(failed, "a pin waiter error did not propagate"); + Require(excluded, "a failed pin waiter did not restore the registry lock"); + Require(!imported->freed.load(), "a failed pin waiter retired an import"); +} + +void reimportBeforeRelock(Fixture& fixture) { + Allocation allocation(fixture); + const auto imported = fixture.Import(allocation.Address(), allocation.bytes); + MutationRace race{&fixture, &allocation}; + mutationRace = ∽̱ + GuestAllocations::GuestAllocationsSetPinWaiter_nid_postfix(&raceWaiter); + { + GuestAllocations::Mutation mutation; + mutation.Unmap(allocation.data, allocation.bytes, [&](const void*, std::size_t, const void*, bool) { + Require(race.calls == 2 && race.replacement != nullptr, "guest mutation did not recheck an import acquired before registry relock"); + requireFreed(imported); + requireFreed(race.replacement); + allocation.Release(); + }); + allocation.registered = false; + } + mutationRace = nullptr; +} + +} + +int RunHostImportLifetimeTests(const Context& context) { + if (context.hostImportAlignment == 0) { + std::puts("skipped, the device has no VK_EXT_external_memory_host"); + return 77; + } + Fixture fixture(context); + descriptorFirstImport(fixture); + idleUnmap(fixture); + pendingRead(fixture, false, false); + pendingRead(fixture, true, false); + pendingRead(fixture, false, true); + pendingRead(fixture, true, true); + partialUnmap(fixture); + protectRollback(fixture); + completedLeaseUnderLock(fixture); + mutationDuringCompletion(fixture); + blockedFree(fixture); + failedConstruction(fixture); + productionWaiterFailure(fixture); + deferredRetirement(fixture); + std::puts("host import lifetime tests passed"); + return 0; +} + +int RunHostImportMutationRaceTests(const Context& context) { + if (context.hostImportAlignment == 0) { + std::puts("skipped, the device has no VK_EXT_external_memory_host"); + return 77; + } + Fixture fixture(context); + waiterFailure(fixture); + reimportBeforeRelock(fixture); + std::puts("host import mutation race tests passed"); + return 0; +} diff --git a/core/libs/prx/libSceAgcDriver/tests/HostImportLifetime.hpp b/core/libs/prx/libSceAgcDriver/tests/HostImportLifetime.hpp new file mode 100644 index 0000000000..9e28ea9da5 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/HostImportLifetime.hpp @@ -0,0 +1,10 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_TESTS_HOSTIMPORTLIFETIME_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_TESTS_HOSTIMPORTLIFETIME_HPP + +#include "prx/libSceAgcDriver/Graphics/include/Context.hpp" + +int RunHostImportLifetimeTests(const AgcDriver::Graphics::Context& context); + +int RunHostImportMutationRaceTests(const AgcDriver::Graphics::Context& context); + +#endif diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 64c2075bf5..b7adb1d449 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -29,6 +29,7 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include "ResidentPresent.hpp" #include "ImageTableAbi.hpp" +#include "HostImportLifetime.hpp" #include "SampleLod_spv.h" #include "BufferUpdate_spv.h" #include "SampleArray_spv.h" @@ -8300,6 +8301,8 @@ int main(int argc, char** argv) { return 77; } Device& device = *created; + if (argc == 2 && std::string_view(argv[1]) == "--host-import-lifetime-only") return RunHostImportLifetimeTests(device.GetContext()); + if (argc == 2 && std::string_view(argv[1]) == "--host-import-mutation-race-only") return RunHostImportMutationRaceTests(device.GetContext()); if (argc == 2 && std::string_view(argv[1]) == "--import-watch-only") { importWatchTests(device); return 0; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/GuestMemoryLifetime.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/GuestMemoryLifetime.cpp new file mode 100644 index 0000000000..9e9e22d814 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/GuestMemoryLifetime.cpp @@ -0,0 +1,259 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/ApplicationHeap.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libSceAgcDriver/tests/execution/VulkanTestDevice.hpp" +#include "prx/libSceAgcDriver/tests/GuestMemoryLifetimeSupport.hpp" +#include <array> +#include <cstddef> +#include <cstdint> +#include <cstdio> +#include <cstring> +#include <memory> +#include <mutex> +#include <stdexcept> +#include <string_view> + +extern "C" { +void* APS5_VABI mmap_nid_postfix(void*, std::size_t, int, int, int, std::int64_t) noexcept; +int APS5_VABI munmap_nid_postfix(void*, std::size_t) noexcept; +int APS5_VABI mprotect_nid_postfix(void*, std::size_t, int) noexcept; +void* APS5_VABI memalign_nid_postfix(std::size_t, std::size_t); +void* APS5_VABI realloc_nid_postfix(void*, std::size_t); +void APS5_VABI free_nid_postfix(void*); +int APS5_VABI sceKernelMapFlexibleMemory(void**, std::size_t, int, int); +int APS5_VABI sceKernelAllocateDirectMemory(std::int64_t, std::int64_t, std::size_t, std::size_t, int, std::int64_t*); +int APS5_VABI sceKernelMapDirectMemory(void**, std::size_t, int, int, std::int64_t, std::size_t); +int APS5_VABI sceKernelReleaseDirectMemory(std::int64_t, std::size_t); +} + +namespace { + +constexpr std::size_t BlockBytes = 65536; +constexpr std::size_t FillBytes = 4096; +constexpr std::array<std::uint32_t, 4> Pattern{0x12345678u, 0x89abcdefu, 0x24681357u, 0xfedcba98u}; + +void Require(bool condition, const char* message) { + if (!condition) throw std::runtime_error(message); +} + +std::uint64_t Address(const void* pointer) { + return reinterpret_cast<std::uintptr_t>(pointer); +} + +bool Imported(const AgcDriver::VulkanDevice& device, const void* pointer, std::size_t bytes = BlockBytes) { + AgcDriver::Graphics::Context probe{}; + probe.device = device.Device(); + probe.hostImportAlignment = 1; + return AgcDriver::Graphics::HostImportCovers(probe, Address(pointer), bytes); +} + +void* Map(std::size_t bytes = BlockBytes) { + auto* result = mmap_nid_postfix(nullptr, bytes, 3, 0x1002, -1, 0); + Require(result != reinterpret_cast<void*>(~std::uintptr_t{0}), "guest mmap failed"); + { + GuestAllocations::Mutation mutation; + const auto range = mutation.Find(result); + Require(range.readable && range.writable && !range.gpu, "guest mmap did not register CPU-only read/write access"); + } + std::memset(result, 0, bytes); + return result; +} + +void CheckPattern(const void* pointer) { + const auto* data = static_cast<const std::byte*>(pointer); + for (std::size_t offset = 0; offset < FillBytes; offset += sizeof(Pattern)) + Require(std::memcmp(data + offset, Pattern.data(), sizeof(Pattern)) == 0, "guest mutation lost the pending GPU fill"); +} + +void Fill(AgcDriver::VulkanDevice& device, void* pointer) { + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + Require(device.FillBuffer(Address(pointer), FillBytes, Pattern), "Vulkan device did not import the guest allocation"); + Require(Imported(device, pointer), "GPU fill did not create a host import"); +} + +void CheckMappings(AgcDriver::VulkanDevice& device) { + auto* mapping = Map(); + Fill(device, mapping); + Require(mprotect_nid_postfix(mapping, BlockBytes, 3) == 0, "guest writable protection refresh failed"); + Require(!Imported(device, mapping), "host import survived guest writable protection refresh"); + CheckPattern(mapping); + Fill(device, mapping); + Require(mprotect_nid_postfix(mapping, BlockBytes, 1) == 0, "guest read-only protection failed"); + Require(!Imported(device, mapping), "host import survived guest protection change"); + CheckPattern(mapping); + Require(mprotect_nid_postfix(mapping, BlockBytes, 0) == 0, "guest inaccessible protection failed"); + Require(mprotect_nid_postfix(mapping, BlockBytes, 3) == 0, "guest writable protection restore failed"); + CheckPattern(mapping); + Fill(device, mapping); + void* replaced = mapping; + Require(sceKernelMapFlexibleMemory(&replaced, BlockBytes, 0x33, 0x10) == 0 && replaced == mapping, "guest fixed mapping replacement failed"); + Require(!Imported(device, mapping), "host import survived guest fixed mapping replacement"); + { + GuestAllocations::Mutation mutation; + const auto range = mutation.Find(mapping); + Require(range.readable && range.writable && range.gpu, "guest fixed mapping did not register its requested CPU and GPU access"); + } + const auto* data = static_cast<const std::uint8_t*>(mapping); + for (std::size_t offset = 0; offset < BlockBytes; ++offset) + Require(data[offset] == 0, "GPU work reached replacement guest pages"); + Fill(device, mapping); + Require(munmap_nid_postfix(mapping, BlockBytes) == 0, "guest munmap failed"); + Require(!Imported(device, mapping), "host import survived guest munmap"); + + mapping = Map(3 * BlockBytes); + Fill(device, mapping); + auto* middle = static_cast<std::uint8_t*>(mapping) + BlockBytes; + Require(mprotect_nid_postfix(middle, BlockBytes, 1) == 0, "guest partial protection failed"); + Require(!Imported(device, mapping), "whole host import survived partial protection change"); + CheckPattern(mapping); + Fill(device, mapping); + Require(munmap_nid_postfix(middle, BlockBytes) == 0, "guest middle unmap failed"); + Require(Imported(device, mapping), "unrelated prefix import was retired by middle unmap"); + Require(munmap_nid_postfix(mapping, BlockBytes) == 0, "guest prefix unmap failed"); + Require(!Imported(device, mapping), "prefix host import survived guest munmap"); + Require(munmap_nid_postfix(middle + BlockBytes, BlockBytes) == 0, "guest suffix unmap failed"); +} + +void CheckHeap(AgcDriver::VulkanDevice& device) { + const std::array<void*, 10> api{}; + ApplicationHeapRegister_nid_no_patch(api.data()); + auto* allocation = memalign_nid_postfix(BlockBytes, BlockBytes); + Require(allocation != nullptr, "guest aligned allocation failed"); + std::memset(allocation, 0, BlockBytes); + Fill(device, allocation); + auto* replacement = realloc_nid_postfix(allocation, 2 * BlockBytes); + Require(replacement != nullptr, "guest realloc failed"); + Require(!Imported(device, allocation), "host import survived guest realloc"); + CheckPattern(replacement); + free_nid_postfix(replacement); + for (int iteration = 0; iteration < 3; ++iteration) { + allocation = memalign_nid_postfix(BlockBytes, BlockBytes); + Require(allocation != nullptr, "guest heap reuse allocation failed"); + std::memset(allocation, 0, BlockBytes); + Fill(device, allocation); + free_nid_postfix(allocation); + Require(!Imported(device, allocation), "host import survived guest free or heap cache eviction"); + } +} + +void CheckLockedMutation(AgcDriver::VulkanDevice& device) { + auto* mapping = Map(); + { + std::lock_guard outer(AgcDriver::GuestMemory::GpuMutex()); + Fill(device, mapping); + Require(mprotect_nid_postfix(mapping, BlockBytes, 3) == 0, "writable protection refresh under the GPU lock did not finish"); + Require(!Imported(device, mapping), "locked protection refresh retained the import"); + CheckPattern(mapping); + Fill(device, mapping); + Require(mprotect_nid_postfix(mapping, BlockBytes, 1) == 0, "protection change under the GPU lock did not finish"); + Require(!Imported(device, mapping), "locked protection change retained the import"); + CheckPattern(mapping); + Require(munmap_nid_postfix(mapping, BlockBytes) == 0, "unmap under the GPU lock did not finish"); + } + + mapping = Map(); + { + std::lock_guard outer(AgcDriver::GuestMemory::GpuMutex()); + auto lease = std::make_shared<GuestAllocations::Lease>(GuestAllocations::GuestAllocationsAcquire_nid_postfix()); + auto* recorder = AgcDriver::Graphics::Recorder::Active(); + Require(recorder != nullptr, "Vulkan device has no active recorder"); + recorder->Keep(std::move(lease)); + Require(munmap_nid_postfix(mapping, BlockBytes) == 0, "completed batch kept its lease until the outer GPU lock was released"); + } +} + +void CheckCopy(AgcDriver::VulkanDevice& device) { + auto* source = Map(); + auto* destination = Map(); + { + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + Fill(device, source); + const auto copied = device.CopyBuffer(Address(destination), Address(source), FillBytes, 0, BlockBytes, 0, 0, 0, [](std::span<const std::byte>, std::uint64_t) {}); + Require(copied.path == 1, "guest source was not read by a Vulkan host-import transfer"); + Require(Imported(device, source) && Imported(device, destination), "GPU copy did not import both guest ranges"); + } + Require(mprotect_nid_postfix(source, BlockBytes, 3) == 0, "copied source protection refresh failed"); + Require(!Imported(device, source), "GPU copy source remained imported after guest protection refresh"); + CheckPattern(destination); + Require(Imported(device, destination), "source retirement unnecessarily retired the copy destination"); + { + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + Fill(device, source); + const auto copied = device.CopyBuffer(Address(destination), Address(source), FillBytes, 0, BlockBytes, 0, 0, 0, [](std::span<const std::byte>, std::uint64_t) {}); + Require(copied.path == 1, "second source transfer did not use host imports"); + } + Require(munmap_nid_postfix(source, BlockBytes) == 0, "copied source unmap failed"); + Require(!Imported(device, source), "GPU copy source remained imported after guest unmap"); + CheckPattern(destination); + Require(Imported(device, destination), "source unmap retired the unrelated copy destination"); + Require(munmap_nid_postfix(destination, BlockBytes) == 0, "copy destination unmap failed"); + Require(!Imported(device, destination), "GPU copy destination remained imported after guest unmap"); +} + +void CheckDirectRelease(AgcDriver::VulkanDevice& device) { + std::int64_t physical = 0; + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, BlockBytes, BlockBytes, 0, &physical) == 0, "guest direct allocation failed"); + void* first = nullptr; + void* second = nullptr; + Require(sceKernelMapDirectMemory(&first, BlockBytes, 0x33, 0, physical, BlockBytes) == 0, "first direct view mapping failed"); + Require(sceKernelMapDirectMemory(&second, BlockBytes, 0x33, 0, physical, BlockBytes) == 0 && second != first, "second direct view mapping failed"); + { + GuestAllocations::Mutation mutation; + const auto firstRange = mutation.Find(first); + const auto secondRange = mutation.Find(second); + Require(firstRange.readable && firstRange.writable && firstRange.gpu && secondRange.readable && secondRange.writable && secondRange.gpu, "direct views did not register their requested CPU and GPU access"); + } + std::memset(first, 0, BlockBytes); + static_cast<std::uint8_t*>(first)[BlockBytes - 1] = 0x5a; + Require(static_cast<const std::uint8_t*>(second)[BlockBytes - 1] == 0x5a, "direct views do not share their physical backing"); + auto* destination = Map(); + { + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + Fill(device, first); + Require(device.FillBuffer(Address(second) + FillBytes, FillBytes, Pattern), "second direct view was not imported for its GPU fill"); + const auto firstCopy = device.CopyBuffer(Address(destination), Address(first), FillBytes, 0, BlockBytes, 0, 0, 0, [](std::span<const std::byte>, std::uint64_t) {}); + const auto secondCopy = device.CopyBuffer(Address(destination) + FillBytes, Address(second) + FillBytes, FillBytes, 0, BlockBytes, 0, 0, 0, [](std::span<const std::byte>, std::uint64_t) {}); + Require(firstCopy.path == 1 && secondCopy.path == 1, "direct views were not read through GPU import transfers"); + Require(Imported(device, first) && Imported(device, second), "both direct view imports were not retained before release"); + } + Require(sceKernelReleaseDirectMemory(physical, BlockBytes) == 0, "guest direct release failed"); + Require(!Imported(device, first) && !Imported(device, second), "direct release retained an imported virtual view"); + Require(!AgcDriver::Graphics::RegisteredReadableCovers(Address(first), BlockBytes) && !AgcDriver::Graphics::RegisteredReadableCovers(Address(second), BlockBytes), "direct release left a virtual view registered"); + CheckPattern(destination); + CheckPattern(static_cast<const std::byte*>(destination) + FillBytes); + Require(Imported(device, destination), "direct release retired the unrelated copy destination"); + Require(munmap_nid_postfix(destination, BlockBytes) == 0, "direct release copy destination unmap failed"); + Require(!Imported(device, destination), "direct release copy destination remained imported after unmap"); +} + +} + +int main(int argc, char** argv) { + try { + Require(argc <= 2, "expected mappings, heap, locked, copy or direct test mode"); + auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + if (!GuestMemoryLifetimeHasHostImports(device->Device())) return VulkanTestSkipped; + const std::string_view mode = argc == 2 ? argv[1] : "mappings"; + if (mode == "mappings") CheckMappings(*device); + else if (mode == "heap") CheckHeap(*device); + else if (mode == "locked") CheckLockedMutation(*device); + else if (mode == "copy") CheckCopy(*device); + else if (mode == "direct") CheckDirectRelease(*device); + else throw std::runtime_error("unknown guest memory lifetime test mode"); + { + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + device->WaitIdle(); + } + device.reset(); + std::puts("guest memory host import lifetime passed"); + return 0; + } catch (const std::exception& error) { + std::fprintf(stderr, "guest memory host import lifetime: %s\n", error.what()); + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/test_guest_memory_lifetime.py b/core/libs/prx/libSceAgcDriver/tests/test_guest_memory_lifetime.py new file mode 100644 index 0000000000..e5cc5d7467 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/test_guest_memory_lifetime.py @@ -0,0 +1,143 @@ +from pathlib import Path +import struct +import subprocess +import sys +import tempfile + +sys.path.insert(0, sys.argv[1]) +from test_guest_symbol_names import importing_executable, string_table, DYNAMIC, STRINGS, SYMBOLS, RELOCATIONS, GOT + +NAMES = ['Begin_nid_no_patch', 'mmap_nid_postfix', 'Fill_nid_no_patch', 'mprotect_nid_postfix', + 'Retired_nid_no_patch', 'Verify_nid_no_patch', 'sceKernelMapFlexibleMemory', + 'munmap_nid_postfix', 'Finish_nid_no_patch'] + + +def fixture(): + image = importing_executable(NAMES) + struct.pack_into('<I', image, 64 + 4, 7) + libraries = ['libGuestMemoryFixture.prx', 'libkernel.prx', 'libc.prx'] + strings, offsets = string_table(NAMES + libraries) + assert len(strings) < SYMBOLS - STRINGS + image[STRINGS:STRINGS + len(strings)] = strings + tags = [(5, STRINGS), (10, len(strings)), (6, SYMBOLS), (11, 24), + (7, RELOCATIONS), (8, len(NAMES) * 24), (9, 24)] + tags.extend((1, offset) for offset in offsets[len(NAMES):]) + tags.append((0, 0)) + for index, tag in enumerate(tags): + struct.pack_into('<qQ', image, DYNAMIC + index * 16, *tag) + struct.pack_into('<QQQQQ', image, 120 + 8, DYNAMIC, DYNAMIC, DYNAMIC, len(tags) * 16, len(tags) * 16) + code = bytearray(bytes.fromhex('4883ec18')) + checks = [] + + def emit(value): + code.extend(bytes.fromhex(value)) + + def call(name): + address = 0x4000 + len(code) + code.extend(b'\xff\x15' + struct.pack('<i', GOT + NAMES.index(name) * 8 - address - 6)) + + def check(): + emit('85c00f85') + checks.append(len(code)) + code.extend(bytes(4)) + + def pointer(): + emit('4c89e7') + + def fill(gpu): + pointer() + emit('be') + code.extend(struct.pack('<I', gpu)) + call('Fill_nid_no_patch') + check() + + def retired(): + pointer() + call('Retired_nid_no_patch') + check() + + def protect(value): + pointer() + emit('be00000100ba') + code.extend(struct.pack('<I', value)) + call('mprotect_nid_postfix') + check() + + def verify(zero): + pointer() + emit('be') + code.extend(struct.pack('<I', zero)) + call('Verify_nid_no_patch') + check() + + call('Begin_nid_no_patch') + check() + emit('31ffbe00000100ba03000000b90210000041b8ffffffff4531c9') + call('mmap_nid_postfix') + emit('4989c44883f8ff0f84') + checks.append(len(code)) + code.extend(bytes(4)) + fill(0) + protect(3) + retired() + verify(0) + fill(0) + protect(1) + retired() + verify(0) + protect(0) + protect(3) + verify(0) + fill(0) + emit('4c8924244889e7be00000100ba33000000b910000000') + call('sceKernelMapFlexibleMemory') + check() + retired() + verify(1) + fill(1) + pointer() + emit('be00000100') + call('munmap_nid_postfix') + check() + retired() + emit('31ff') + call('Finish_nid_no_patch') + emit('0f0b') + failed = len(code) + emit('bf01000000') + call('Finish_nid_no_patch') + emit('0f0b') + for position in checks: + struct.pack_into('<i', code, position, failed - position - 4) + assert len(code) < 0x600 + image[0x4000:0x4000 + len(code)] = code + return image + + +def main(): + relinker, fixture_library, kernel, libc, driver = (Path(value).resolve() for value in sys.argv[2:]) + with tempfile.TemporaryDirectory(prefix="anyps5-guest-memory-lifetime-") as directory: + work = Path(directory) + libraries = work / "libs" + libraries.mkdir() + for path in (fixture_library, kernel, libc, driver): + (libraries / path.name).symlink_to(path) + source = work / "guest.elf" + source.write_bytes(fixture()) + output = work / "native.elf" + converted = subprocess.run( + [str(relinker), "--skip-sce-module", "--registry", str(source), str(output)], + cwd=work, capture_output=True, text=True, timeout=10) + assert converted.returncode == 0, (converted.stdout, converted.stderr) + output.chmod(0o755) + result = subprocess.run([str(output)], cwd=work, capture_output=True, text=True, timeout=15) + if result.returncode == 77: + print(result.stdout, end="") + sys.exit(77) + assert result.returncode == 0, (result.returncode, result.stdout, result.stderr) + assert "converted guest memory/Vulkan lifetime result=0" in result.stdout, result.stdout + print("Converted guest memory/Vulkan lifetime tests passed") + + +if __name__ == "__main__": + main() diff --git a/core/libs/prx/libc/src/GuestAllocations.cpp b/core/libs/prx/libc/src/GuestAllocations.cpp index 438a819c8f..a931983d75 100644 --- a/core/libs/prx/libc/src/GuestAllocations.cpp +++ b/core/libs/prx/libc/src/GuestAllocations.cpp @@ -385,8 +385,13 @@ void GuestAllocationsRequireUnpinned_nid_postfix(void* mutation, const void* poi bool progressed = false; if (state != nullptr && state->lock.owns_lock()) { state->lock.unlock(); - if (waiter != nullptr) progressed = waiter(address, bytes); - else std::this_thread::yield(); + try { + if (waiter != nullptr) progressed = waiter(address, bytes); + else std::this_thread::yield(); + } catch (...) { + state->lock.lock(); + throw; + } state->lock.lock(); } else { std::this_thread::yield(); From 0563713626b79b1525f3b80a438de9fda8dc99c5 Mon Sep 17 00:00:00 2001 From: thuanquang <ghoulbulldog@gmail.com> Date: Sun, 18 Oct 2026 08:25:13 +0700 Subject: [PATCH 0933/1000] fix(agc): read a 32-bit depth-bits T# over guest memory through a float view A sampled 32_UINT or 32_SINT T# with a depth swizzle that no live depth surface matches threw, while the 16-bit case already falls back to guest memory through an R16_UNORM view. The 32-bit case now reads the guest bytes through an R32_SFLOAT view, which the shader casts back to the stored dword. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- .../Graphics/src/ShaderResources.cpp | 9 +++++---- .../tests/execution/DepthPlaneSampling.cpp | 17 +++++++++++++++++ docs/dev/debt/libSceAgcDriver.md | 1 + 3 files changed, 23 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index d7253bea3c..02d1da80d9 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -607,10 +607,11 @@ std::shared_ptr<Texture> cachedTexture(const Context& context, std::span<const s return depth; } const auto depthBitsWidth = words.size() >= 4 ? ShaderRecompiler::DepthBitsTextureWidth(words[1], words[3]) : 0u; - if (depthBitsWidth == 32u) { - char text[160]; - std::snprintf(text, sizeof(text), "AGC graphics: 32-bit integer read of the depth-layout texture 0x%llx, which is no depth surface drawn with, is not implemented", static_cast<unsigned long long>(resource.baseAddress)); - throw std::runtime_error(text); + constexpr auto float32 = static_cast<std::uint32_t>(ShaderRecompiler::IrBufferFormat::Format32Float); + if (depthBitsWidth == 32u && resource.format != float32) { + auto bits = resource; + bits.format = float32; + return cachedTexture(context, words, bits, components, guestBytes, depthCompare); } constexpr auto unorm16 = static_cast<std::uint32_t>(ShaderRecompiler::IrBufferFormat::Format16UNorm); if (depthBitsWidth == 16u && resource.format != unorm16) { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneSampling.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneSampling.cpp index f43484e980..1133b100b3 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneSampling.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneSampling.cpp @@ -50,6 +50,7 @@ using ShaderRecompiler::ShaderStage; constexpr std::uint32_t Threads = 32; constexpr std::uint32_t Format16Unorm = 7; constexpr std::uint32_t Format16Uint = 11; +constexpr std::uint32_t Format32Uint = 20; constexpr std::uint32_t Format32Float = 22; constexpr std::uint32_t Type2D = 9; constexpr std::uint32_t TypeCube = 11; @@ -68,6 +69,7 @@ constexpr std::size_t FirstSliceOffset = 0xe0000; constexpr std::size_t PaddedCubeOffset = 0x120000; constexpr std::size_t SmallCubeOffset = 0x180000; constexpr std::size_t LargeCubeOffset = 0x200000; +constexpr std::size_t GuestDepthBitsOffset = 0x480000; constexpr float DrawnDepth = 0.25f; constexpr float DrawnD16Depth = 0.2f; constexpr std::uint32_t DrawnD16Bits = 13107; @@ -324,6 +326,20 @@ void DepthBitsBaseSliceCase(AgcDriver::VulkanDevice& device, GuestBlock& block, Expect(output, 0x1234u, "D16 depth-bits array from base slice 1"); } +// A 32_UINT T# with a depth swizzle over guest memory that no depth surface was drawn into: the raw dwords, +// including float denormals, NaN payloads and the sign bit alone. +void GuestDepthBitsCase(AgcDriver::VulkanDevice& device, GuestBlock& block, std::span<std::uint32_t> output) { + constexpr std::array<std::uint32_t, 9> values{0x00000001u, 0x007fffffu, 0x80000000u, 0x80000001u, 0x7f800001u, 0x7fc00001u, 0xffffffffu, 0x3e800000u, 0x12345678u}; + for (std::size_t index = 0; index < values.size(); ++index) { + auto* base = block.At(GuestDepthBitsOffset + index * SliceBytes); + FillSlice(base, SliceBytes, values[index], 4u); + Load(device, base, {Format32Uint, 128, 128, Type2D, 0, Swizzle64KbZX}, 0, output, false, static_cast<std::uint32_t>(index * 13u)); + char what[96]; + std::snprintf(what, sizeof(what), "32-bit depth-bits read of guest memory holding 0x%08x", values[index]); + Expect(output, values[index], what); + } +} + void FirstSliceCase(AgcDriver::VulkanDevice& device, GuestBlock& block, std::span<std::uint32_t> output) { auto* one = block.At(OneSliceOffset); DrawDepth(device, reinterpret_cast<std::uintptr_t>(one), {128, 128}, VK_FORMAT_D32_SFLOAT, DrawnDepth); @@ -431,6 +447,7 @@ int main() { IntegerCase(*device, block, output); DepthBitsArrayCase(*device, block, output); DepthBitsBaseSliceCase(*device, block, output); + GuestDepthBitsCase(*device, block, output); FirstSliceCase(*device, block, output); PaddedCubeCase(*device, block, output); SmallD16CubeCase(*device, block, output); diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 86c2eba6e7..89be73d076 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -53,6 +53,7 @@ - [Multisampled color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) lose their guest-read guards and cannot be reused after a successful allocation removal or unmap covers their full guarded range. Retired Vulkan images remain allocated until device teardown to preserve outstanding references. Partial unmaps keep the guard; partial allocation reuse and guest access to live multisampled data remain unsupported. - [Stencil reference export replay](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) on devices without `VK_EXT_shader_stencil_export` supports at most sixteen values proved from constant, select and phi exports, with an export proved on every kept path. It records instances, then their triangles, then the reference classes of each triangle. One pipeline filters fragments at shader termination against a dynamic push value before late stencil and depth tests. The [Vulkan stencil-reference replacement rule](https://docs.vulkan.org/refpages/latest/refpages/source/FragStencilRefEXT.html) discards bits above the attachment's stencil width; supported stencil attachments have eight bits. Numeric subgroup observations throw except bounded own-lane mask plumbing and group control with kept-lane and derivative-helper coverage proved. WQM inputs must exclude helper contributions, since [non-quad subgroup operations may treat helpers as inactive](https://docs.vulkan.org/glslext/latest/glslext/khr/GL_KHR_shader_subgroup.html). Readonly guarded SRT reads may report internal host faults; these draws wait and check faults before publishing results, and loops throw. Indirect draws, restart, strips, fans, tessellation, mesh, multisampling, early tests, ordered pixel execution, guest shader memory writes, other fault-producing memory access, clocks, primitive ID inputs, attachment feedback and pipeline face/channel splits throw. An exhausted stage push range throws. Equivalence to console ordering has not been measured on console hardware. +- [32-bit depth-bits reads of guest memory](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver): a sampled 32_UINT or 32_SINT T# with a depth swizzle mode that no live depth surface matches reads the guest bytes through an R32_SFLOAT image, as the recompiler's depth-bits read expects, and the shader casts the fetched value back to the stored dword. On RX 6700 XT (gfx1031, AMD Windows driver 32.0.21045.5002) the read keeps float denormals, the sign bit alone and quiet and signaling NaN payloads bit for bit. A host that canonicalizes NaNs or flushes denormals on a 32-bit float fetch would change those dwords, and other drivers are not measured. A live depth surface whose extent or base level differs from the T# is not used; the read takes the guest bytes, as the 32-bit float and 16-bit views already do. - [Reused depth storage](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp): a storage view with another extent or a format that cannot hold the cached depth plane uses guest memory. Depth rendering is not written back to those guest bytes; this admission rule does not prove allocation reuse or preserve a depth writer through an incompatible view. A 2D storage view, or a one-layer 2D array view of layer 0, of level 0 with the plane's extent and format (32-bit float or the 32-bit depth-bits layout for the depth plane, 16-bit unorm for D16, 8-bit uint for the stencil plane) is a storage image that is [filled](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) from the depth image before every use and, when the shader writes it, copied back after the work: a whole-plane copy through a staging buffer each way. A written view is also left pending like any other storage image, so the plane's content, drawn depth included, reaches guest memory on the next flush of that range. A 2D array view of more than one slice of the depth plane, or of a later slice, is a storage image over the slices from 0 to its last (`LayeredDepthStorage`); only the slices from its base slice to its last are copied: a slice that has a depth surface at the plane's address plus a whole depth slice per layer (`DepthSliceBytes`, as for the depth plane copies below) is copied from that surface before every use and back to it after the work, every bound slice each time, through a staging buffer; the other slices hold guest memory like any storage image. These images are kept per address, extent, slice count, base slice, format and tile mode until the device goes away, outside the storage cache's budget. Such a view needs a layer pitch equal to that slice size, and another view of the same memory (the 2D view of its first slice, a view from another base slice, or a sampled copy) is a separate image, copied in before and out after each use, so views that are written before a flush and share a slice do not merge: the later copy-out wins, so their results are not merged. A matching view still throws when it is a cube, a 2D array of more than one layer of the stencil plane or from a later slice, or another level, when the surface is multisampled, when it names the depth plane of a 16-bit depth with stencil surface the device stores in a wider format, and when it was written after another view of the same plane (another format, tile mode, address or dimension) replaced it, as two such views bound in one dispatch are. Stored depth values outside 0..1 are not clamped, and Vulkan leaves that copy undefined without `VK_EXT_depth_range_unrestricted`. `agc_driver_depth_storage_dispatch` pins these paths; none is measured on the console. - [Collapsed color array views](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) preserve the selected slice in the XOR address equation when the view is represented as a shifted 2D surface. Native Vulkan coverage checks R_X draws, array reads, publication and re-upload for slices 1-3; Lost Soul Aside PPSA26358 validates the RGBA16F cube mip chain on Windows/NVIDIA. Other devices and console comparisons remain unverified. Full-array views retain their layer addressing. - A [depth surface](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver) is retired when a color target or a written storage image it does not serve overlaps it, or when it is sampled through a view it cannot serve and guest memory over it was written since it was last bound; lookups then skip it, so sampling reads the memory like any texture. Bound again, it keeps its own depth instead of the bytes written over it. Without write watching, a view it cannot serve still throws. A retired surface also gives up its depth and stencil planes, so another surface sharing them no longer copies from it. From 66247cef5cebd87468ba22090261954024cdacb1 Mon Sep 17 00:00:00 2001 From: T3cm0 <srodriguez7604@gmail.com> Date: Sat, 17 Oct 2026 20:40:58 -0500 Subject: [PATCH 0934/1000] feat(libc): implement the sceLibcPafMspace allocation exports Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- core/libs/prx/libc/src/Mspace.cpp | 42 ++++++++++++++++++++++++++++++ core/libs/tests/Mspace.cpp | 43 +++++++++++++++++++++++++++++++ docs/dev/debt/libc.md | 1 + 3 files changed, 86 insertions(+) diff --git a/core/libs/prx/libc/src/Mspace.cpp b/core/libs/prx/libc/src/Mspace.cpp index dbd74b1203..5292c9fe89 100644 --- a/core/libs/prx/libc/src/Mspace.cpp +++ b/core/libs/prx/libc/src/Mspace.cpp @@ -9,6 +9,7 @@ #include <memory> #include <mutex> #include <set> +#include <stdexcept> #include <utility> extern "C" int* APS5_VABI __error_nid_postfix(); @@ -309,4 +310,45 @@ std::size_t APS5_VABI sceLibcMspaceMallocUsableSize_nid_postfix(const void* poin return 0; } +void* APS5_VABI sceLibcPafMspaceCreate_nid_postfix(const char* name, void* base, std::size_t size, unsigned flags, const void* options) { + if (options) throw std::runtime_error("sceLibcPafMspaceCreate: creation options are not supported"); + return sceLibcMspaceCreate_nid_postfix(name, base, size, flags); +} + +int APS5_VABI sceLibcPafMspaceDestroy_nid_postfix(void* handle) { + return sceLibcMspaceDestroy_nid_postfix(handle); +} + +void* APS5_VABI sceLibcPafMspaceMalloc_nid_postfix(void* handle, std::size_t size, const void* owner) { + (void)owner; + return sceLibcMspaceMalloc_nid_postfix(handle, size); +} + +void APS5_VABI sceLibcPafMspaceFree_nid_postfix(void* handle, void* pointer, const void* owner) { + (void)owner; + sceLibcMspaceFree_nid_postfix(handle, pointer); +} + +void* APS5_VABI sceLibcPafMspaceCalloc_nid_postfix(void* handle, std::size_t count, std::size_t size, const void* owner) { + (void)owner; + return sceLibcMspaceCalloc_nid_postfix(handle, count, size); +} + +void* APS5_VABI sceLibcPafMspaceRealloc_nid_postfix(void* handle, void* pointer, std::size_t size, const void* owner) { + (void)owner; + return sceLibcMspaceRealloc_nid_postfix(handle, pointer, size); +} + +void* APS5_VABI sceLibcPafMspaceReallocalign_nid_postfix(void* handle, void* pointer, std::size_t alignment, std::size_t size, const void* owner) { + (void)owner; + return sceLibcMspaceReallocalign_nid_postfix(handle, pointer, alignment, size); +} + +int APS5_VABI sceLibcPafMspaceMallocStats_nid_postfix(void* handle, MallocManagedSize* stats) { + return sceLibcMspaceMallocStats_nid_postfix(handle, stats); +} + +int APS5_VABI sceLibcPafMspaceMallocStatsFast_nid_postfix(void* handle, MallocManagedSize* stats) { + return sceLibcMspaceMallocStatsFast_nid_postfix(handle, stats); +} } diff --git a/core/libs/tests/Mspace.cpp b/core/libs/tests/Mspace.cpp index 8a3571292f..2224e2b7a1 100644 --- a/core/libs/tests/Mspace.cpp +++ b/core/libs/tests/Mspace.cpp @@ -5,6 +5,7 @@ #include <cstdlib> #include <cstring> #include <limits> +#include <stdexcept> #include <thread> extern "C" { @@ -22,6 +23,15 @@ void* APS5_VABI sceLibcMspaceAlignedAlloc_nid_postfix(void*, std::size_t, std::s void* APS5_VABI sceLibcMspaceReallocalign_nid_postfix(void*, void*, std::size_t, std::size_t); int APS5_VABI sceLibcMspaceMallocStats_nid_postfix(void*, void*); int APS5_VABI sceLibcMspaceMallocStatsFast_nid_postfix(void*, void*); +void* APS5_VABI sceLibcPafMspaceCreate_nid_postfix(const char*, void*, std::size_t, unsigned, const void*); +int APS5_VABI sceLibcPafMspaceDestroy_nid_postfix(void*); +void* APS5_VABI sceLibcPafMspaceMalloc_nid_postfix(void*, std::size_t, const void*); +void APS5_VABI sceLibcPafMspaceFree_nid_postfix(void*, void*, const void*); +void* APS5_VABI sceLibcPafMspaceCalloc_nid_postfix(void*, std::size_t, std::size_t, const void*); +void* APS5_VABI sceLibcPafMspaceRealloc_nid_postfix(void*, void*, std::size_t, const void*); +void* APS5_VABI sceLibcPafMspaceReallocalign_nid_postfix(void*, void*, std::size_t, std::size_t, const void*); +int APS5_VABI sceLibcPafMspaceMallocStats_nid_postfix(void*, void*); +int APS5_VABI sceLibcPafMspaceMallocStatsFast_nid_postfix(void*, void*); } struct MallocManagedSize { std::uint16_t size; @@ -35,6 +45,38 @@ struct MallocManagedSize { static void Require(bool value) { if (!value) { std::fputs("Mspace check failed\n", stderr); std::abort(); } } +static void CheckPaf() { + alignas(64) static std::array<unsigned char, 65536> storage{}; + bool rejected = false; + int options = 0; + try { sceLibcPafMspaceCreate_nid_postfix("paf", storage.data(), storage.size(), 0, &options); } + catch (const std::runtime_error&) { rejected = true; } + Require(rejected); + void* arena = sceLibcPafMspaceCreate_nid_postfix("paf", storage.data(), storage.size(), 0, nullptr); + Require(arena == storage.data()); + const int owner = 0; + auto* block = static_cast<unsigned char*>(sceLibcPafMspaceMalloc_nid_postfix(arena, 40, &owner)); + Require(block != nullptr && sceLibcMspaceMallocUsableSize_nid_postfix(block) >= 40); + std::memset(block, 0x5a, 40); + auto* zeroed = static_cast<unsigned char*>(sceLibcPafMspaceCalloc_nid_postfix(arena, 4, 8, arena)); + Require(zeroed != nullptr && zeroed[0] == 0 && zeroed[31] == 0); + block = static_cast<unsigned char*>(sceLibcPafMspaceRealloc_nid_postfix(arena, block, 4000, &owner)); + Require(block != nullptr && block[0] == 0x5a && block[39] == 0x5a); + block = static_cast<unsigned char*>(sceLibcPafMspaceReallocalign_nid_postfix(arena, block, 256, 64, &owner)); + Require(block != nullptr && reinterpret_cast<std::uintptr_t>(block) % 256 == 0 && block[39] == 0x5a); + MallocManagedSize viaPaf{}, viaMspace{}, fastPaf{}, fastMspace{}; + viaPaf.size = viaMspace.size = fastPaf.size = fastMspace.size = sizeof(MallocManagedSize); + viaPaf.version = viaMspace.version = fastPaf.version = fastMspace.version = 1; + Require(sceLibcPafMspaceMallocStats_nid_postfix(arena, &viaPaf) == sceLibcMspaceMallocStats_nid_postfix(arena, &viaMspace)); + Require(sceLibcPafMspaceMallocStatsFast_nid_postfix(arena, &fastPaf) == sceLibcMspaceMallocStatsFast_nid_postfix(arena, &fastMspace)); + Require(viaPaf.currentInuseSize == viaMspace.currentInuseSize && viaPaf.currentInuseSize > 0); + Require(fastPaf.currentInuseSize == fastMspace.currentInuseSize); + sceLibcPafMspaceFree_nid_postfix(arena, block, &owner); + sceLibcPafMspaceFree_nid_postfix(arena, zeroed, arena); + sceLibcPafMspaceFree_nid_postfix(arena, nullptr, &owner); + Require(sceLibcPafMspaceDestroy_nid_postfix(arena) == 0); + Require(sceLibcPafMspaceDestroy_nid_postfix(arena) == -1); +} static void CheckAlignment() { alignas(32) static std::array<unsigned char, 65536 + 16> storage{}; void* arena = sceLibcMspaceCreate_nid_postfix("aligned", storage.data() + 16, 65536, 0); @@ -107,6 +149,7 @@ static void CheckMovedBlockKeepsUsableBytes() { Require(sceLibcMspaceDestroy_nid_postfix(arena) == 0); } int main() { + CheckPaf(); CheckAlignment(); CheckAdjacentBlocks(); CheckMovedBlockKeepsUsableBytes(); diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 497130f640..b077f88597 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -15,6 +15,7 @@ - [Guest arena](../../../core/libs/prx/libc/src/GuestArena.cpp) (libc) - the application map area is assumed to end at 0xFC_0000_0000, as `SCE_KERNEL_APP_MAP_AREA_END_ADDR` does on the PS4. On Windows, host allocations made in the arena before libc loads stay, and a fixed guest mapping over one throws - [sceLibcInternalBacktraceForGame](../../../core/libs/prx/libc/src/HeapDiagnostics.cpp) (libSceLibcInternal, implemented in libc) - `(const char* heapName)` and its return follow the `libc.prx` ASTRO BOT (PPSA21564 01.007.000) bundles: its four calls (0x6801, 0x7F46, 0xE75E, 0xEE02) are in the `malloc_stats` and `sceLibcMspaceMallocStats` paths, each after a `[%s] ...` line printed with the same heap name buffer, and the caller ignores the result and continues. The console's backtrace output is not reproduced: the heap name and the guest return address are written to the host standard error - [sceLibcInternalHeapErrorReportForGame](../../../core/libs/prx/libc/src/HeapDiagnostics.cpp) (libSceLibcInternal, implemented in libc) - `(void* heap, void* block, uint32 error)` follows the same `libc.prx` (call at 0xF634): it passes the heap, the block and the nonzero result of its block check, and then calls a function that never returns, so the report only has to return. The meaning of the error values is unknown; the three values and the guest return address are written to the host standard error +- [sceLibcPafMspaceCreate, Destroy, Malloc, Free, Calloc, Realloc, Reallocalign, MallocStats and MallocStatsFast](../../../core/libs/prx/libc/src/Mspace.cpp) (libSceLibcInternal, implemented in libc) follow the console's `libSceLibcInternal.sprx` (SDK 4.5x build of `J03950001`, 0x5D680 to 0x5D770): each calls the same internal routine as its `sceLibcMspace` counterpart without the malloc callback hook those add, and `Reallocalign` takes the alignment before the size as `sceLibcMspaceReallocalign` does. `Create` takes a fifth argument, a pointer to a creation options block (a version word, a type of 1 or 2 at offset 2, a field at 0x10) that the `Mspace` form passes as null; a non-null block throws, as its meaning is unknown. `Malloc`, `Free`, `Calloc`, `Realloc` and `Reallocalign` take a trailing owner value that the console XORs with its block magic and stores in each block's footer, where the `Mspace` forms pass the heap itself. Footers are not modelled here, so the owner is ignored. Measured on a PS5 (firmware 13.60) from a homebrew payload, the calls with a matching owner behave as here, but the console ends the process when `Reallocalign` is given a block whose owner differs and when it resizes a block in place with an owner other than the heap, and `MallocStats` returns 1 instead of 0 while the heap holds blocks owned by something other than the heap; none of this is reproduced. `PafMspaceMemalign`, `PosixMemalign`, `MallocUsableSize`, `Trim`, `IsHeapEmpty`, `CheckMemoryBounds`, `ReportMemoryBlocks` and `GetFooterValue` are not implemented - [malloc_usable_size](../../../core/libs/prx/libc/src/ApplicationHeap.cpp) (libc) - with the default allocator it returns the size the block was requested with (the range registered for it), not the rounded-up chunk size Sony's allocator reports; both satisfy "at least the requested size", and bytes past the request would not be write-watched. With an application allocator it calls entry 9 of the replacement table (`malloc_usable_size` in KytyPS5's `MallocReplace` order) and throws when the table leaves it null, like `malloc_stats_fast` - [malloc, calloc, realloc, memalign, aligned_alloc and reallocalign](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) - follow FreeBSD 11.4 `contrib/jemalloc/src/jemalloc.c` for failures, not measured on the console: a size the default heap rejects or the guest arena cannot place (including a `calloc` product that overflows) returns null with `errno` `ENOMEM`, and `memalign` and `aligned_alloc` return null with `EINVAL` for an alignment that is zero or not a power of two. FreeBSD's libc has no `memalign`, so setting `errno` there is assumed. A host commit failure for a block that fits in the arena (Windows `ERROR_NOT_ENOUGH_MEMORY`, `ERROR_OUTOFMEMORY` or `ERROR_COMMITMENT_LIMIT`, Linux `ENOMEM`) resets and releases the reserved range and returns null with `ENOMEM`; any other commit failure releases the range and throws. `reallocalign` returns null with `EINVAL` for an invalid alignment and `ENOMEM` like the others, which is an assumption as it is not in FreeBSD. `realloc(ptr, 0)` frees the block and returns null, where FreeBSD 11.4's jemalloc 4.5 resizes it to one byte and returns a block - [reallocf](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libSceLibcInternal, implemented in libc) - follows FreeBSD 11 `lib/libc/stdlib/reallocf.c` on top of this libc's `realloc`, not measured on the console: size 0 frees the block and returns null as `realloc` does here, so the block is not freed twice; when `realloc` fails because the application allocator returns null or the default heap rejects the size, the block is freed through the same allocator, `errno` is `ENOMEM` and null is returned. Other failures (a commit failure that is not out of memory, an unknown pointer) throw, as they do for `realloc` From f9b7b230fc5b7576eff55eeb355dfe83b6463a69 Mon Sep 17 00:00:00 2001 From: T3cm0 <srodriguez7604@gmail.com> Date: Sat, 17 Oct 2026 20:41:26 -0500 Subject: [PATCH 0935/1000] feat(libSceLibcInternal): implement the remaining posix_spawnattr accessors Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- core/libs/prx/libSceLibcInternal/Spawn.cpp | 66 ++++++++++++++++++++++ core/libs/tests/GuestPosixSpawn.cpp | 33 +++++++++++ docs/dev/debt/libSceLibcInternal.md | 1 + 3 files changed, 100 insertions(+) diff --git a/core/libs/prx/libSceLibcInternal/Spawn.cpp b/core/libs/prx/libSceLibcInternal/Spawn.cpp index c4571fb64e..c81a8fd0cb 100644 --- a/core/libs/prx/libSceLibcInternal/Spawn.cpp +++ b/core/libs/prx/libSceLibcInternal/Spawn.cpp @@ -60,6 +60,72 @@ int APS5_VABI posix_spawnattr_setsigdefault_nid_postfix(PosixSpawnAttributes** a return 0; } +int APS5_VABI posix_spawnattr_getflags_nid_postfix(PosixSpawnAttributes* const* attributes, std::int16_t* flags) { + const auto& source = Attributes(attributes, "posix_spawnattr_getflags"); + if (flags == nullptr) throw std::invalid_argument("posix_spawnattr_getflags: null flags"); + *flags = source.flags; + return 0; +} + +int APS5_VABI posix_spawnattr_getpgroup_nid_postfix(PosixSpawnAttributes* const* attributes, std::int32_t* processGroup) { + const auto& source = Attributes(attributes, "posix_spawnattr_getpgroup"); + if (processGroup == nullptr) throw std::invalid_argument("posix_spawnattr_getpgroup: null process group"); + *processGroup = source.processGroup; + return 0; +} + +int APS5_VABI posix_spawnattr_getschedparam_nid_postfix(PosixSpawnAttributes* const* attributes, std::int32_t* schedulingParameter) { + const auto& source = Attributes(attributes, "posix_spawnattr_getschedparam"); + if (schedulingParameter == nullptr) throw std::invalid_argument("posix_spawnattr_getschedparam: null scheduling parameter"); + *schedulingParameter = source.schedulingPriority; + return 0; +} + +int APS5_VABI posix_spawnattr_getschedpolicy_nid_postfix(PosixSpawnAttributes* const* attributes, std::int32_t* schedulingPolicy) { + const auto& source = Attributes(attributes, "posix_spawnattr_getschedpolicy"); + if (schedulingPolicy == nullptr) throw std::invalid_argument("posix_spawnattr_getschedpolicy: null scheduling policy"); + *schedulingPolicy = source.schedulingPolicy; + return 0; +} + +int APS5_VABI posix_spawnattr_getsigdefault_nid_postfix(PosixSpawnAttributes* const* attributes, GuestSignalSet* signalDefault) { + const auto& source = Attributes(attributes, "posix_spawnattr_getsigdefault"); + if (signalDefault == nullptr) throw std::invalid_argument("posix_spawnattr_getsigdefault: null signal set"); + *signalDefault = source.signalDefault; + return 0; +} + +int APS5_VABI posix_spawnattr_getsigmask_nid_postfix(PosixSpawnAttributes* const* attributes, GuestSignalSet* signalMask) { + const auto& source = Attributes(attributes, "posix_spawnattr_getsigmask"); + if (signalMask == nullptr) throw std::invalid_argument("posix_spawnattr_getsigmask: null signal set"); + *signalMask = source.signalMask; + return 0; +} + +int APS5_VABI posix_spawnattr_setpgroup_nid_postfix(PosixSpawnAttributes** attributes, std::int32_t processGroup) { + Attributes(attributes, "posix_spawnattr_setpgroup").processGroup = processGroup; + return 0; +} + +int APS5_VABI posix_spawnattr_setschedparam_nid_postfix(PosixSpawnAttributes** attributes, const std::int32_t* schedulingParameter) { + auto& target = Attributes(attributes, "posix_spawnattr_setschedparam"); + if (schedulingParameter == nullptr) throw std::invalid_argument("posix_spawnattr_setschedparam: null scheduling parameter"); + target.schedulingPriority = *schedulingParameter; + return 0; +} + +int APS5_VABI posix_spawnattr_setschedpolicy_nid_postfix(PosixSpawnAttributes** attributes, std::int32_t schedulingPolicy) { + Attributes(attributes, "posix_spawnattr_setschedpolicy").schedulingPolicy = schedulingPolicy; + return 0; +} + +int APS5_VABI posix_spawnattr_setsigmask_nid_postfix(PosixSpawnAttributes** attributes, const GuestSignalSet* signalMask) { + auto& target = Attributes(attributes, "posix_spawnattr_setsigmask"); + if (signalMask == nullptr) throw std::invalid_argument("posix_spawnattr_setsigmask: null signal set"); + target.signalMask = *signalMask; + return 0; +} + int APS5_VABI posix_spawn_file_actions_init_nid_postfix(PosixSpawnFileActions** actions) { if (actions == nullptr) throw std::invalid_argument("posix_spawn_file_actions_init: null file actions"); auto* created = new (std::nothrow) PosixSpawnFileActions{}; diff --git a/core/libs/tests/GuestPosixSpawn.cpp b/core/libs/tests/GuestPosixSpawn.cpp index 8fe49f711d..33ae4ebd0f 100644 --- a/core/libs/tests/GuestPosixSpawn.cpp +++ b/core/libs/tests/GuestPosixSpawn.cpp @@ -12,6 +12,16 @@ int APS5_VABI posix_spawnattr_init_nid_postfix(PosixSpawnAttributes**); int APS5_VABI posix_spawnattr_destroy_nid_postfix(PosixSpawnAttributes**); int APS5_VABI posix_spawnattr_setflags_nid_postfix(PosixSpawnAttributes**, std::int16_t); int APS5_VABI posix_spawnattr_setsigdefault_nid_postfix(PosixSpawnAttributes**, const GuestSignalSet*); +int APS5_VABI posix_spawnattr_getflags_nid_postfix(PosixSpawnAttributes* const*, std::int16_t*); +int APS5_VABI posix_spawnattr_getpgroup_nid_postfix(PosixSpawnAttributes* const*, std::int32_t*); +int APS5_VABI posix_spawnattr_getschedparam_nid_postfix(PosixSpawnAttributes* const*, std::int32_t*); +int APS5_VABI posix_spawnattr_getschedpolicy_nid_postfix(PosixSpawnAttributes* const*, std::int32_t*); +int APS5_VABI posix_spawnattr_getsigdefault_nid_postfix(PosixSpawnAttributes* const*, GuestSignalSet*); +int APS5_VABI posix_spawnattr_getsigmask_nid_postfix(PosixSpawnAttributes* const*, GuestSignalSet*); +int APS5_VABI posix_spawnattr_setpgroup_nid_postfix(PosixSpawnAttributes**, std::int32_t); +int APS5_VABI posix_spawnattr_setschedparam_nid_postfix(PosixSpawnAttributes**, const std::int32_t*); +int APS5_VABI posix_spawnattr_setschedpolicy_nid_postfix(PosixSpawnAttributes**, std::int32_t); +int APS5_VABI posix_spawnattr_setsigmask_nid_postfix(PosixSpawnAttributes**, const GuestSignalSet*); int APS5_VABI posix_spawn_file_actions_init_nid_postfix(PosixSpawnFileActions**); int APS5_VABI posix_spawn_file_actions_destroy_nid_postfix(PosixSpawnFileActions**); int APS5_VABI posix_spawn_file_actions_addopen_nid_postfix(PosixSpawnFileActions**, int, const char*, int, std::uint16_t); @@ -67,7 +77,30 @@ static void CheckAttributes() { Check(std::memcmp(&attributes->signalDefault, &defaults, sizeof(defaults)) == 0); Check(std::memcmp(&attributes->signalMask, &empty, sizeof(empty)) == 0); + std::int16_t flags = 1; + std::int32_t value = 1; + GuestSignalSet read{{9u, 9u, 9u, 9u}}; + Check(posix_spawnattr_getflags_nid_postfix(&attributes, &flags) == 0 && flags == 0); + Check(posix_spawnattr_getsigdefault_nid_postfix(&attributes, &read) == 0 && std::memcmp(&read, &defaults, sizeof(read)) == 0); + Check(posix_spawnattr_getsigmask_nid_postfix(&attributes, &read) == 0 && std::memcmp(&read, &empty, sizeof(read)) == 0); + Check(posix_spawnattr_setpgroup_nid_postfix(&attributes, 77) == 0 && attributes->processGroup == 77); + Check(posix_spawnattr_getpgroup_nid_postfix(&attributes, &value) == 0 && value == 77); + const std::int32_t priority = 450; + Check(posix_spawnattr_setschedparam_nid_postfix(&attributes, &priority) == 0 && attributes->schedulingPriority == 450); + Check(posix_spawnattr_getschedparam_nid_postfix(&attributes, &value) == 0 && value == 450); + Check(posix_spawnattr_setschedpolicy_nid_postfix(&attributes, 2) == 0 && attributes->schedulingPolicy == 2); + Check(posix_spawnattr_getschedpolicy_nid_postfix(&attributes, &value) == 0 && value == 2); + const GuestSignalSet mask{{1u, 2u, 3u, 4u}}; + Check(posix_spawnattr_setsigmask_nid_postfix(&attributes, &mask) == 0 && std::memcmp(&attributes->signalMask, &mask, sizeof(mask)) == 0); + Check(posix_spawnattr_getsigmask_nid_postfix(&attributes, &read) == 0 && std::memcmp(&read, &mask, sizeof(read)) == 0); + Check(std::memcmp(&attributes->signalDefault, &defaults, sizeof(defaults)) == 0); + PosixSpawnAttributes* uninitialized = nullptr; + Check(ThrowsInvalidArgument([&] { posix_spawnattr_getflags_nid_postfix(&uninitialized, &flags); })); + Check(ThrowsInvalidArgument([&] { posix_spawnattr_getpgroup_nid_postfix(&attributes, nullptr); })); + Check(ThrowsInvalidArgument([&] { posix_spawnattr_setpgroup_nid_postfix(&uninitialized, 1); })); + Check(ThrowsInvalidArgument([&] { posix_spawnattr_setschedparam_nid_postfix(&attributes, nullptr); })); + Check(ThrowsInvalidArgument([&] { posix_spawnattr_setsigmask_nid_postfix(&attributes, nullptr); })); Check(ThrowsInvalidArgument([&] { posix_spawnattr_setflags_nid_postfix(&uninitialized, 1); })); Check(ThrowsInvalidArgument([&] { posix_spawnattr_setsigdefault_nid_postfix(&uninitialized, &defaults); })); Check(ThrowsInvalidArgument([&] { posix_spawnattr_setsigdefault_nid_postfix(&attributes, nullptr); })); diff --git a/docs/dev/debt/libSceLibcInternal.md b/docs/dev/debt/libSceLibcInternal.md index 7cc67d0d95..be978908f4 100644 --- a/docs/dev/debt/libSceLibcInternal.md +++ b/docs/dev/debt/libSceLibcInternal.md @@ -14,4 +14,5 @@ - [fstatvfs](../../../core/libs/prx/libSceLibcInternal/FilesystemStatistics.cpp) (libSceLibcInternal) converts `_fstatfs` to the 88-byte `struct statvfs` as FreeBSD 9.2's `sfs2svfs` does (`f_bsize` from the transfer size, `f_frsize` from the block size, `f_favail` equal to `f_ffree`, `f_fsid` 0, `ST_RDONLY` and `ST_NOSUID` only). Differences: `f_namemax` is the `statfs` value, not `fpathconf(_PC_NAME_MAX)`; negative `f_bavail` and `f_ffree` wrap to unsigned without `EOVERFLOW`, as the same-width copy in FreeBSD does; inode counts are 0 on Windows, inherited from `_fstatfs`; a null output returns `EFAULT` after the descriptor is validated, where FreeBSD would fault. Layout and conversion are unverified against the PS5 library (PS5MC imports `fstatvfs` but was not run) - [posix_spawn and posix_spawnp](../../../core/libs/prx/libSceLibcInternal/Spawn.cpp) (libSceLibcInternal) throw: creating a child process is not supported. PPSA99305 (Dusklight) and PPSA99309 (Wind Waker) stop on `main` at unresolved import `44hlATrd47U` (`posix_spawnattr_init`); a run with this PR has not established how far either title gets, and process creation still throws. The attribute and file action calls keep FreeBSD 11.4's objects, so a real `posix_spawn` would have to apply them in the child. Null or uninitialized objects, which FreeBSD dereferences, throw. - [getopt, getopt_long](../../../core/libs/prx/libSceLibcInternal/Getopt.cpp) (libSceLibcInternal) follow FreeBSD 11 [getopt.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/stdlib/getopt.c) and [getopt_long.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/stdlib/getopt_long.c) with `GNU_COMPATIBLE`: `getopt` does not permute, `getopt_long` permutes unless `POSIXLY_CORRECT` is set in the guest environment or the option string starts with `+`, and the exported `opterr`, `optind`, `optopt`, `optreset` and `optarg` start at 1, 1, 0, 0 and null. Warnings go to the guest's current `stderr` prefixed with `__progname`, where FreeBSD's `warnx` keeps the `stderr` of its first call. A null `getopt` option string, a null argument vector or argument, a negative `optind` and an `optind` moved inside a pending permutation throw where FreeBSD faults. `getopt_long_only` is not exported. That the console's `libSceLibcInternal` uses this FreeBSD code is unverified +- [posix_spawnattr_getflags, getpgroup, getschedparam, getschedpolicy, getsigdefault, getsigmask, setpgroup, setschedparam, setschedpolicy and setsigmask](../../../core/libs/prx/libSceLibcInternal/Spawn.cpp) (libSceLibcInternal) read and write the 0x30-byte attribute object at the offsets of the console's `libSceLibcInternal.sprx` (SDK 4.5x build of `J03950001`, 0x8D860 to 0x8D910: flags at 0, process group at 4, scheduling parameter at 8, policy at 0xC, default and masked signal sets at 0x10 and 0x20), which never fail there and return 0; the layout matched on a PS5 (firmware 13.60). Uninitialized attributes and null pointers, which the console dereferences, throw as the existing setters do - [sys_siglist and sys_nsig](../../../core/libs/prx/libSceLibcInternal/SignalList.cpp) (libSceLibcInternal) hold the 32 descriptions and the count of [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/gen/siglist.c#L73-L107) `siglist.c` (`NSIG` 32, signal 0 to `SIGUSR2`); the console's table was not read From 48b394d1ecf8eb61dac140ba10cf263dff519432 Mon Sep 17 00:00:00 2001 From: thuanquang <ghoulbulldog@gmail.com> Date: Sun, 18 Oct 2026 08:42:00 +0700 Subject: [PATCH 0936/1000] fix(agc): keep robustBufferAccess2 off on AMD's proprietary driver Since #4048 enabled robustBufferAccess2, AMD's Windows driver draws nothing in agc_driver_layered_volume_draw and agc_driver_layered_volume_vertex, and the selected loads make the high half of agc_driver_subvector_loop's wave64 skip its loop. Until the cause is found, that driver does not get the feature, and its loads keep their selections. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 9 +++++ .../Execution/include/RobustBufferAccess.hpp | 14 ++++++++ .../Execution/src/VulkanDevice.cpp | 10 +++++- .../tests/RobustBufferAccess.cpp | 33 +++++++++++++++++++ docs/dev/debt/shader.md | 2 +- 5 files changed, 66 insertions(+), 2 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/Execution/include/RobustBufferAccess.hpp create mode 100644 core/libs/prx/libSceAgcDriver/tests/RobustBufferAccess.cpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index fdf3f37b5c..16c629d635 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -610,6 +610,15 @@ if(BUILD_TESTING) add_test(NAME agc_pipeline_link_optimization COMMAND agc_pipeline_link_optimization_tests) endif() +add_test_executable(agc_robust_buffer_access_tests EXCLUDE_FROM_ALL tests/RobustBufferAccess.cpp) +target_include_directories(agc_robust_buffer_access_tests PRIVATE ${LIBS_INCLUDE_DIR} ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR}) +set_target_properties(agc_robust_buffer_access_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_robust_buffer_access_tests) +if(BUILD_TESTING) + set_target_properties(agc_robust_buffer_access_tests PROPERTIES EXCLUDE_FROM_ALL OFF) + add_test(NAME agc_robust_buffer_access COMMAND agc_robust_buffer_access_tests) +endif() + add_test_executable(agc_device_fault_tests EXCLUDE_FROM_ALL tests/DeviceFault.cpp) target_include_directories(agc_device_fault_tests PRIVATE ${LIBS_INCLUDE_DIR} ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR}) set_target_properties(agc_device_fault_tests PROPERTIES CXX_EXTENSIONS OFF) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/RobustBufferAccess.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/RobustBufferAccess.hpp new file mode 100644 index 0000000000..a180b23104 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Execution/include/RobustBufferAccess.hpp @@ -0,0 +1,14 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_ROBUSTBUFFERACCESS_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_ROBUSTBUFFERACCESS_HPP + +#include <vulkan/vulkan.h> + +namespace AgcDriver { + +inline bool RobustBufferAccess2Usable(VkDriverId driver) { + return driver != VK_DRIVER_ID_AMD_PROPRIETARY; +} + +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index e1e3b027f6..8f1b3bc7db 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -15,6 +15,7 @@ #include "prx/libSceAgcDriver/Execution/include/DeviceFault.hpp" #include "prx/libSceAgcDriver/Execution/include/FragmentBarycentric.hpp" #include "prx/libSceAgcDriver/Execution/include/PipelineLinkOptimization.hpp" +#include "prx/libSceAgcDriver/Execution/include/RobustBufferAccess.hpp" #include "prx/libSceAgcDriver/Execution/include/SubgroupClock.hpp" #include "prx/libSceAgcDriver/Execution/include/ShaderDeviceProfile.hpp" #include "prx/libSceAgcDriver/Execution/include/PresentMode.hpp" @@ -1836,7 +1837,14 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : VkPhysicalDeviceFeatures2 queried{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2, &robustness2}; state->InstanceFunction<PFN_vkGetPhysicalDeviceFeatures2>("vkGetPhysicalDeviceFeatures2")(selected, &queried); } - state->robustBufferAccess2 = robustness2.robustBufferAccess2 == VK_TRUE && enabled.robustBufferAccess == VK_TRUE; + VkDriverId robustnessDriver = VK_DRIVER_ID_MAX_ENUM; + if (hasExtension(VK_KHR_DRIVER_PROPERTIES_EXTENSION_NAME)) { + VkPhysicalDeviceDriverProperties driver{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DRIVER_PROPERTIES}; + VkPhysicalDeviceProperties2 driverProperties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, &driver}; + state->InstanceFunction<PFN_vkGetPhysicalDeviceProperties2>("vkGetPhysicalDeviceProperties2")(selected, &driverProperties); + robustnessDriver = driver.driverID; + } + state->robustBufferAccess2 = robustness2.robustBufferAccess2 == VK_TRUE && enabled.robustBufferAccess == VK_TRUE && RobustBufferAccess2Usable(robustnessDriver); if (robustness2.nullDescriptor == VK_TRUE || state->robustBufferAccess2) { const bool nullDescriptor = robustness2.nullDescriptor == VK_TRUE; robustness2 = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ROBUSTNESS_2_FEATURES_EXT}; diff --git a/core/libs/prx/libSceAgcDriver/tests/RobustBufferAccess.cpp b/core/libs/prx/libSceAgcDriver/tests/RobustBufferAccess.cpp new file mode 100644 index 0000000000..6b0331109f --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/RobustBufferAccess.cpp @@ -0,0 +1,33 @@ +#include "prx/libSceAgcDriver/Execution/include/RobustBufferAccess.hpp" +#include <cstdio> + +namespace { + +struct Case { + VkDriverId driver; + bool usable; +}; + +constexpr Case Cases[]{ + {VK_DRIVER_ID_AMD_PROPRIETARY, false}, + {VK_DRIVER_ID_AMD_OPEN_SOURCE, true}, + {VK_DRIVER_ID_MESA_RADV, true}, + {VK_DRIVER_ID_NVIDIA_PROPRIETARY, true}, + {VK_DRIVER_ID_INTEL_PROPRIETARY_WINDOWS, true}, + {VK_DRIVER_ID_INTEL_OPEN_SOURCE_MESA, true}, + {VK_DRIVER_ID_MESA_LLVMPIPE, true}, + {VK_DRIVER_ID_MOLTENVK, true}, +}; + +} + +int main() { + int failures = 0; + for (const auto& item : Cases) { + if (AgcDriver::RobustBufferAccess2Usable(item.driver) != item.usable) { + std::fprintf(stderr, "driver %d: expected robustBufferAccess2 to be %s\n", static_cast<int>(item.driver), item.usable ? "usable" : "unusable"); + ++failures; + } + } + return failures == 0 ? 0 : 1; +} diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index d4129c7e2f..768c21eace 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -155,4 +155,4 @@ - [Flag structurization](../../../core/shader/recompiler/ControlFlow/src/Structurizer.cpp) (shader recompiler) - a graph that every structurizer attempt refuses, irreducible and non-terminating graphs included, is rebuilt with one reach flag per block and one again flag per loop (`structurizeWithFlags`): each block keeps one copy and runs behind a guard on its flag, and each strongly connected component becomes a loop whose single latch repeats it while a back edge set its flag. The output has about 8 times the input's blocks and one route variable per block, and ends in an empty Return that never runs. Only blocks that start the top-level order unconditionally run unguarded, so resource planning also sees the static paths that skip a guarded block and can plan extra descriptor candidates for them. The attempts still run first. Shared merge splitting refuses an attempt once the graph has grown past 16 times its block count at the start of splitting (at least 64 blocks); succeeding attempts grew at most 10.6 times on random graphs of up to 128 blocks, and an attempt that would succeed only beyond the budget leaves the graph to a later attempt or to this construction. Splitting that adds one block per round stays within that budget until its 4 rounds per block run out, so the attempts can still take more than 10 s (9 of 2,000 random graphs of up to 128 blocks, 63 s at most). `APS5_FORCE_STRUCTURIZER_FALLBACK=1` builds every graph this way, for tests - [Scalar buffer descriptors](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) use only the base address, stride and record count for resources accessed exclusively by scalar loads; their fourth word is normalized to zero, following AMD RDNA2 ISA section 7.2.2. Vector and mixed accesses retain descriptor validation. Native Vulkan readbacks cover ignored fourth-word fields on Windows/NVIDIA; their console behavior has not been measured. - [Spanned wave64 compute](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvSubgroup.cpp) (shader recompiler) - `APS5_SPANNED_WAVE=1`, off by default, runs a single-wave wave64 compute program at one lane per invocation on a 32-lane host subgroup and exchanges ballots, shuffles and reductions between the two host subgroups through workgroup memory and barriers. The driver turns it on only where compute subgroups can be pinned at 32 full lanes (`subgroupSizeControl` and `computeFullSubgroups`), passes it as `SpirvTarget::spannedWaves` (part of the cache key and the serialized request), and creates the pipelines of spanned programs with `REQUIRE_FULL_SUBGROUPS`, since the lane number `SubgroupId * 32 + SubgroupLocalInvocationId` needs two full subgroups. Those barriers are correct only where every invocation of the workgroup reaches them, which holds for the guest's wave-uniform control flow. The emulated 64-bit LDS atomics ([LockedLdsUpdate](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp)) run only on the lanes the exec mask keeps, so their lock loop ballots within its own host subgroup, as its elect already does. Any other cross-lane exchange emitted inside per-lane control flow would hang a spanned wave in the same way, and the emitter does not reject one: there is no uniformity check. The only check so far is manual: disassemble the module (`spirv-dis`), list the selection and loop constructs that enclose each `OpControlBarrier`, and read the condition of each enclosing selection. In Ghost of Yōtei's program 0x80003af600 (PPSA26344), 108 of 111 barriers sat only in loops on wave-uniform conditions and 3 inside per-invocation conditionals, all in the lock loop. Other programs and titles have not been scanned. An earlier, cheaper ballot (one barrier instead of three) also lost the device for a reason that is not understood, which may be another barrier placement problem. Measured on an RTX 4090 (Windows): an offline replay of that program and two Yōtei runs that previously lost the device (NVIDIA TDR) now run past it, and [LdsMaskWaterfall.cpp](../../../core/libs/prx/libSceAgcDriver/tests/execution/LdsMaskWaterfall.cpp) runs both modes and covers a half-inactive wave. Stray (PPSA02101) on Intel ANV and a Demon's Souls (PPSA01342) request replay showed no fault, before this fix -- [Guest memory load guards](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (shader recompiler) - a guest load whose lane is inactive or whose address is out of bounds is still performed and its result replaced by 0 with `OpSelect`, instead of a selection per guard, when the load can be pointed at a location nothing writes: scratch reads its own element 0; storage buffers (direct buffers, GDS) read element 0xfffffffe, which `robustBufferAccess2` turns into a zero load with no memory access, so the extra read cannot race with a write the guarded program would not have raced with ([Vulkan memory model](https://docs.vulkan.org/spec/latest/appendices/memorymodel.html#data-race)). Without `robustBufferAccess2` ([VulkanDevice.cpp](../../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) enables it when the device has it) those loads keep their selections; LDS loads, stores, atomics, formatted and physical-address (BDA) accesses always do. LDS keeps the branch because NVIDIA's compiler predicates a branched LDS load at no cost, while the select form costs about 2.5 instructions per dword and splits a `ds_read_b128` into four loads (RTX 4060 measurements by @karanvora2599 in #4048); Intel's static counts favour the select only for wide reads, and run time was not measured on either. The aligned high word of a misaligned-capable buffer read re-reads the low word's own element. A selection guard costs four blocks per lane half and the host compiler's time grows with the block count: PPSA11142's merged PSP vertex program (3176 ISA words, wave64) emitted 16,983 blocks and 1,710 conditional branches, 5,907 and 868 with the selects. `agc_guarded_memory_loads` traces the emitted loads in both device modes. +- [Guest memory load guards](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (shader recompiler) - a guest load whose lane is inactive or whose address is out of bounds is still performed and its result replaced by 0 with `OpSelect`, instead of a selection per guard, when the load can be pointed at a location nothing writes: scratch reads its own element 0; storage buffers (direct buffers, GDS) read element 0xfffffffe, which `robustBufferAccess2` turns into a zero load with no memory access, so the extra read cannot race with a write the guarded program would not have raced with ([Vulkan memory model](https://docs.vulkan.org/spec/latest/appendices/memorymodel.html#data-race)). Without `robustBufferAccess2` ([VulkanDevice.cpp](../../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) enables it when the device has it) those loads keep their selections; LDS loads, stores, atomics, formatted and physical-address (BDA) accesses always do. LDS keeps the branch because NVIDIA's compiler predicates a branched LDS load at no cost, while the select form costs about 2.5 instructions per dword and splits a `ds_read_b128` into four loads (RTX 4060 measurements by @karanvora2599 in #4048); Intel's static counts favour the select only for wide reads, and run time was not measured on either. The aligned high word of a misaligned-capable buffer read re-reads the low word's own element. A selection guard costs four blocks per lane half and the host compiler's time grows with the block count: PPSA11142's merged PSP vertex program (3176 ISA words, wave64) emitted 16,983 blocks and 1,710 conditional branches, 5,907 and 868 with the selects. `agc_guarded_memory_loads` traces the emitted loads in both device modes. AMD's proprietary driver does not get `robustBufferAccess2` ([RobustBufferAccess.hpp](../../../core/libs/prx/libSceAgcDriver/Execution/include/RobustBufferAccess.hpp)), so its loads keep their selections. On an RX 6700 XT with driver 32.0.21045.5002, enabling the feature broke `agc_driver_layered_volume_draw` and `agc_driver_layered_volume_vertex`: the vertex draw left the volume untouched even with the selections kept. `agc_driver_subvector_loop` failed too, with the selected loads: the high half of a wave64 skipped its loop. The cause was not found. From 300b7ff34c49896d51d17e45a9f1d1cb724bd884 Mon Sep 17 00:00:00 2001 From: T3cm0 <srodriguez7604@gmail.com> Date: Sat, 17 Oct 2026 20:42:20 -0500 Subject: [PATCH 0937/1000] feat(libSceLibcInternal): implement inet_aton, inet_addr and inet_ntoa Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- .../prx/libSceLibcInternal/CMakeLists.txt | 11 ++ core/libs/prx/libSceLibcInternal/Inet.cpp | 106 ++++++++++++++++++ core/libs/tests/GuestInetAddress.cpp | 68 +++++++++++ docs/dev/debt/libSceLibcInternal.md | 1 + 4 files changed, 186 insertions(+) create mode 100644 core/libs/prx/libSceLibcInternal/Inet.cpp create mode 100644 core/libs/tests/GuestInetAddress.cpp diff --git a/core/libs/prx/libSceLibcInternal/CMakeLists.txt b/core/libs/prx/libSceLibcInternal/CMakeLists.txt index b5dde2fe3f..1780e1d720 100644 --- a/core/libs/prx/libSceLibcInternal/CMakeLists.txt +++ b/core/libs/prx/libSceLibcInternal/CMakeLists.txt @@ -4,6 +4,7 @@ target_link_libraries(libSceLibcInternal PRIVATE ${CMAKE_DL_LIBS}) target_sources(libSceLibcInternal PRIVATE Directory.cpp) configure_windows_unwind(libSceLibcInternal) target_sources(libSceLibcInternal PRIVATE FilesystemStatistics.cpp PathStatistics.cpp) +target_sources(libSceLibcInternal PRIVATE Inet.cpp) if(BUILD_TESTING) add_test_executable(guest_statvfs_tests ${LIBS_INCLUDE_DIR}/tests/GuestStatvfs.cpp) @@ -95,6 +96,16 @@ if(BUILD_TESTING) set_tests_properties(guest_getopt PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceLibcInternal>") endif() + add_test_executable(guest_inet_address_tests ${LIBS_INCLUDE_DIR}/tests/GuestInetAddress.cpp) + target_include_directories(guest_inet_address_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_inet_address_tests PRIVATE libSceLibcInternal libkernel libc) + configure_windows_unwind(guest_inet_address_tests) + add_test(NAME guest_inet_address COMMAND guest_inet_address_tests) + set_tests_properties(guest_inet_address PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_inet_address PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceLibcInternal>") + endif() add_test_executable(guest_force_tls_destructor_tests ${LIBS_INCLUDE_DIR}/tests/GuestForceTlsDestructor.cpp) target_include_directories(guest_force_tls_destructor_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_force_tls_destructor_tests PRIVATE libSceLibcInternal libkernel libc ${CMAKE_DL_LIBS}) diff --git a/core/libs/prx/libSceLibcInternal/Inet.cpp b/core/libs/prx/libSceLibcInternal/Inet.cpp new file mode 100644 index 0000000000..90996aacf6 --- /dev/null +++ b/core/libs/prx/libSceLibcInternal/Inet.cpp @@ -0,0 +1,106 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include <cstdint> +#include <cstring> +#include <stdexcept> + +extern "C" { +const char* APS5_VABI __inet_ntop_nid_postfix(int family, const void* source, char* destination, std::uint32_t capacity); +} + +namespace { + +constexpr int AddressFamilyInet = 2; +char g_ntoaBuffer[18]; + +bool IsDigit(unsigned char c) { return c >= '0' && c <= '9'; } +bool IsHexDigit(unsigned char c) { return IsDigit(c) || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'); } +bool IsSpace(unsigned char c) { return c == ' ' || (c >= '\t' && c <= '\r'); } + +} + +extern "C" { + +int APS5_VABI inet_aton_nid_postfix(const char* text, std::uint32_t* address) { + if (text == nullptr) throw std::invalid_argument("inet_aton: null text"); + std::uint64_t value = 0; + std::uint8_t parts[4] = {}; + std::uint8_t* part = parts; + bool digit = false; + auto c = static_cast<unsigned char>(*text); + for (;;) { + if (!IsDigit(c)) return 0; + value = 0; + unsigned base = 10; + digit = false; + if (c == '0') { + c = static_cast<unsigned char>(*++text); + if (c == 'x' || c == 'X') { + base = 16; + c = static_cast<unsigned char>(*++text); + } else { + base = 8; + digit = true; + } + } + for (;;) { + if (IsDigit(c)) { + if (base == 8 && (c == '8' || c == '9')) return 0; + value = value * base + (c - '0'); + c = static_cast<unsigned char>(*++text); + digit = true; + } else if (base == 16 && IsHexDigit(c)) { + value = (value << 4) | static_cast<std::uint64_t>(c + 10 - (c >= 'a' ? 'a' : 'A')); + c = static_cast<unsigned char>(*++text); + digit = true; + } else { + break; + } + } + if (c != '.') break; + if (part >= parts + 3 || value > 0xff) return 0; + *part++ = static_cast<std::uint8_t>(value); + c = static_cast<unsigned char>(*++text); + } + if (c != 0 && (c >= 0x80 || !IsSpace(c))) return 0; + if (!digit) return 0; + switch (part - parts + 1) { + case 2: + if (value > 0xffffff) return 0; + value |= static_cast<std::uint64_t>(parts[0]) << 24; + break; + case 3: + if (value > 0xffff) return 0; + value |= (static_cast<std::uint64_t>(parts[0]) << 24) | (static_cast<std::uint64_t>(parts[1]) << 16); + break; + case 4: + if (value > 0xff) return 0; + value |= (static_cast<std::uint64_t>(parts[0]) << 24) | (static_cast<std::uint64_t>(parts[1]) << 16) | (static_cast<std::uint64_t>(parts[2]) << 8); + break; + default: + break; + } + if (address != nullptr) { + const auto host = static_cast<std::uint32_t>(value); + const std::uint8_t network[4] = {static_cast<std::uint8_t>(host >> 24), static_cast<std::uint8_t>(host >> 16), static_cast<std::uint8_t>(host >> 8), static_cast<std::uint8_t>(host)}; + std::memcpy(address, network, sizeof(network)); + } + return 1; +} + +std::uint32_t APS5_VABI inet_addr_nid_postfix(const char* text) { + std::uint32_t address = 0; + return inet_aton_nid_postfix(text, &address) ? address : 0xffffffffu; +} + +char* APS5_VABI inet_ntoa_r_nid_postfix(std::uint32_t address, char* buffer, std::uint32_t size) { + __inet_ntop_nid_postfix(AddressFamilyInet, &address, buffer, size); + return buffer; +} + +char* APS5_VABI inet_ntoa_nid_postfix(std::uint32_t address) { + std::memcpy(g_ntoaBuffer, "[inet_ntoa error]", sizeof(g_ntoaBuffer)); + __inet_ntop_nid_postfix(AddressFamilyInet, &address, g_ntoaBuffer, sizeof(g_ntoaBuffer)); + return g_ntoaBuffer; +} + +} diff --git a/core/libs/tests/GuestInetAddress.cpp b/core/libs/tests/GuestInetAddress.cpp new file mode 100644 index 0000000000..80447cad5d --- /dev/null +++ b/core/libs/tests/GuestInetAddress.cpp @@ -0,0 +1,68 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include <cstdint> +#include <cstdio> +#include <cstdlib> +#include <cstring> +#include <string> + +extern "C" { +int APS5_VABI inet_aton_nid_postfix(const char* text, std::uint32_t* address); +std::uint32_t APS5_VABI inet_addr_nid_postfix(const char* text); +char* APS5_VABI inet_ntoa_nid_postfix(std::uint32_t address); +char* APS5_VABI inet_ntoa_r_nid_postfix(std::uint32_t address, char* buffer, std::uint32_t size); +} + +namespace { + +void Require(bool value, const char* what) { + if (value) return; + std::fprintf(stderr, "failed: %s\n", what); + std::abort(); +} + +std::uint32_t Network(std::uint8_t a, std::uint8_t b, std::uint8_t c, std::uint8_t d) { + const std::uint8_t bytes[4] = {a, b, c, d}; + std::uint32_t value = 0; + std::memcpy(&value, bytes, sizeof(value)); + return value; +} + +bool Parses(const char* text, std::uint32_t expected) { + std::uint32_t address = 0x5a5a5a5a; + return inet_aton_nid_postfix(text, &address) == 1 && address == expected; +} + +bool Rejects(const char* text) { + std::uint32_t address = 0x5a5a5a5a; + return inet_aton_nid_postfix(text, &address) == 0 && address == 0x5a5a5a5a; +} + +} + +int main() { + Require(Parses("192.168.1.2", Network(192, 168, 1, 2)), "dotted quad"); + Require(Parses("10.1", Network(10, 0, 0, 1)), "two parts"); + Require(Parses("10.1.300", Network(10, 1, 1, 44)), "three parts"); + Require(Parses("0x7f.1", Network(127, 0, 0, 1)), "hex part"); + Require(Parses("017.0.0.1", Network(15, 0, 0, 1)), "octal part"); + Require(Parses("3232235777", Network(192, 168, 1, 1)), "single number"); + Require(Parses("1.2.3.4 trailing", Network(1, 2, 3, 4)), "trailing after space"); + Require(Parses("0", Network(0, 0, 0, 0)), "zero"); + Require(Parses("0xffffffff", Network(255, 255, 255, 255)), "broadcast"); + Require(Rejects("256.1.1.1") && Rejects("1.2.3.256") && Rejects("1.2.65536"), "part out of range"); + Require(Rejects("1.2.3.4.5") && Rejects("08") && Rejects("0x") && Rejects("1.2.3.4x"), "malformed"); + Require(Rejects("") && Rejects(".1") && Rejects("1..2") && Rejects("-1"), "missing digits"); + Require(inet_aton_nid_postfix("1.2.3.4", nullptr) == 1 && inet_aton_nid_postfix("x", nullptr) == 0, "null destination"); + + Require(inet_addr_nid_postfix("127.0.0.1") == Network(127, 0, 0, 1), "inet_addr"); + Require(inet_addr_nid_postfix("bad") == 0xffffffffu && inet_addr_nid_postfix("255.255.255.255") == 0xffffffffu, "inet_addr none"); + + char* const text = inet_ntoa_nid_postfix(Network(10, 20, 30, 40)); + Require(std::string(text) == "10.20.30.40", "inet_ntoa"); + Require(inet_ntoa_nid_postfix(Network(255, 255, 255, 255)) == text && std::string(text) == "255.255.255.255", "inet_ntoa static buffer"); + char buffer[16]; + Require(inet_ntoa_r_nid_postfix(Network(1, 2, 3, 4), buffer, sizeof(buffer)) == buffer && std::string(buffer) == "1.2.3.4", "inet_ntoa_r"); + char tiny[4] = {'k', 'e', 'e', 'p'}; + Require(inet_ntoa_r_nid_postfix(Network(1, 2, 3, 4), tiny, sizeof(tiny)) == tiny, "inet_ntoa_r small buffer returns it"); + return 0; +} diff --git a/docs/dev/debt/libSceLibcInternal.md b/docs/dev/debt/libSceLibcInternal.md index 7cc67d0d95..14f0726d0a 100644 --- a/docs/dev/debt/libSceLibcInternal.md +++ b/docs/dev/debt/libSceLibcInternal.md @@ -15,3 +15,4 @@ - [posix_spawn and posix_spawnp](../../../core/libs/prx/libSceLibcInternal/Spawn.cpp) (libSceLibcInternal) throw: creating a child process is not supported. PPSA99305 (Dusklight) and PPSA99309 (Wind Waker) stop on `main` at unresolved import `44hlATrd47U` (`posix_spawnattr_init`); a run with this PR has not established how far either title gets, and process creation still throws. The attribute and file action calls keep FreeBSD 11.4's objects, so a real `posix_spawn` would have to apply them in the child. Null or uninitialized objects, which FreeBSD dereferences, throw. - [getopt, getopt_long](../../../core/libs/prx/libSceLibcInternal/Getopt.cpp) (libSceLibcInternal) follow FreeBSD 11 [getopt.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/stdlib/getopt.c) and [getopt_long.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/stdlib/getopt_long.c) with `GNU_COMPATIBLE`: `getopt` does not permute, `getopt_long` permutes unless `POSIXLY_CORRECT` is set in the guest environment or the option string starts with `+`, and the exported `opterr`, `optind`, `optopt`, `optreset` and `optarg` start at 1, 1, 0, 0 and null. Warnings go to the guest's current `stderr` prefixed with `__progname`, where FreeBSD's `warnx` keeps the `stderr` of its first call. A null `getopt` option string, a null argument vector or argument, a negative `optind` and an `optind` moved inside a pending permutation throw where FreeBSD faults. `getopt_long_only` is not exported. That the console's `libSceLibcInternal` uses this FreeBSD code is unverified - [sys_siglist and sys_nsig](../../../core/libs/prx/libSceLibcInternal/SignalList.cpp) (libSceLibcInternal) hold the 32 descriptions and the count of [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/gen/siglist.c#L73-L107) `siglist.c` (`NSIG` 32, signal 0 to `SIGUSR2`); the console's table was not read +- [inet_aton, inet_addr, inet_ntoa and inet_ntoa_r](../../../core/libs/prx/libSceLibcInternal/Inet.cpp) (libSceLibcInternal) follow the FreeBSD code in the console's `libSceLibcInternal.sprx` (SDK 4.5x build of `J03950001`, 0x8ADE0 to 0x8B3D0) and matched it on a PS5 (firmware 13.60) for 20 `inet_aton` inputs; a null text, which the console dereferences, throws From 8283831a19b804cf26b70fee0d77c20d4f0dcf51 Mon Sep 17 00:00:00 2001 From: T3cm0 <srodriguez7604@gmail.com> Date: Sat, 17 Oct 2026 20:42:46 -0500 Subject: [PATCH 0938/1000] feat(libc): implement mergesort Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- core/libs/prx/libc/src/FileAndHeap.cpp | 33 ++++++++++++ core/libs/tests/GuestQsort.cpp | 69 ++++++++++++++++++++++++++ docs/dev/debt/libc.md | 1 + 3 files changed, 103 insertions(+) diff --git a/core/libs/prx/libc/src/FileAndHeap.cpp b/core/libs/prx/libc/src/FileAndHeap.cpp index 4f7330bc0c..fa8e028a98 100644 --- a/core/libs/prx/libc/src/FileAndHeap.cpp +++ b/core/libs/prx/libc/src/FileAndHeap.cpp @@ -553,6 +553,39 @@ void* APS5_VABI bsearch_nid_postfix(const void* key, const void* base, size_t co return nullptr; } +int APS5_VABI mergesort_nid_postfix(void* base, size_t count, size_t size, GuestCompare compare) { + if (size < 4) { + errno = 22; + return -1; + } + if (count == 0) return 0; + if (!compare) throw std::invalid_argument("mergesort: null comparator"); + if (!base) throw std::invalid_argument("mergesort: null base"); + if (count > (std::numeric_limits<size_t>::max() - 8) / size) throw std::overflow_error("mergesort: array size overflow"); + auto* const scratch = static_cast<unsigned char*>(malloc_nid_postfix(count * size + 8)); + if (!scratch) return -1; + auto* source = static_cast<unsigned char*>(base); + auto* target = scratch; + for (size_t width = 1; width < count; width *= 2) { + for (size_t low = 0; low < count; low += 2 * width) { + const size_t middle = std::min(low + width, count); + const size_t high = std::min(low + 2 * width, count); + size_t left = low, right = middle, out = low; + while (left < middle && right < high) { + const size_t taken = compare(source + left * size, source + right * size) <= 0 ? left++ : right++; + std::memcpy(target + out++ * size, source + taken * size, size); + } + std::memcpy(target + out * size, source + left * size, (middle - left) * size); + out += middle - left; + std::memcpy(target + out * size, source + right * size, (high - right) * size); + } + std::swap(source, target); + } + if (source != base) std::memcpy(base, source, count * size); + free_nid_postfix(scratch); + return 0; +} + void APS5_VABI qsort_nid_postfix(void* base, size_t count, size_t size, int (APS5_VABI *compare)(const void*, const void*)) { if (!compare) throw std::invalid_argument("qsort: null comparator"); if (size == 0) throw std::invalid_argument("qsort: zero element size"); diff --git a/core/libs/tests/GuestQsort.cpp b/core/libs/tests/GuestQsort.cpp index 4a75f5c01e..6124f2540f 100644 --- a/core/libs/tests/GuestQsort.cpp +++ b/core/libs/tests/GuestQsort.cpp @@ -1,15 +1,58 @@ +#include "prx/libc/include/ApplicationHeap.hpp" #include "prx/libc/include/general/VabiMacros.hpp" +#include <array> +#include <cstdint> +#include <cstdlib> #include <cstddef> #include <cstdio> #include <cstring> #include <vector> extern "C" { +int APS5_VABI mergesort_nid_postfix(void*, std::size_t, std::size_t, int (APS5_VABI*)(const void*, const void*)); +int* APS5_VABI __error_nid_postfix(); void APS5_VABI qsort_nid_postfix(void*, std::size_t, std::size_t, int (APS5_VABI*)(const void*, const void*)); } namespace { +bool failAllocation = false; + +void* APS5_VABI Allocate(std::size_t size) { return failAllocation ? nullptr : std::malloc(size); } +void APS5_VABI Free(void* ptr) { std::free(ptr); } +void* APS5_VABI Reallocate(void* ptr, std::size_t size) { return std::realloc(ptr, size); } +void* APS5_VABI Calloc(std::size_t count, std::size_t size) { return std::calloc(count, size); } +void* APS5_VABI Align(std::size_t, std::size_t size) { return std::malloc(size); } +void* APS5_VABI Realign(void* ptr, std::size_t size, std::size_t) { return std::realloc(ptr, size); } +int APS5_VABI PosixAlign(void** ptr, std::size_t, std::size_t size) { + *ptr = std::malloc(size); + return *ptr ? 0 : 12; +} + +void InitializeHeap() { + alignas(16) static std::array<std::byte, 0x40> process{}; + alignas(16) static std::array<std::byte, 0x38> libc{}; + alignas(16) static std::array<std::byte, 0x78> replacement{}; + const auto write = [](auto& data, std::size_t offset, auto value) { + std::memcpy(data.data() + offset, &value, sizeof(value)); + }; + write(process, 0, std::uint64_t{0x40}); + write(process, 8, std::uint32_t{0x4942524f}); + write(process, 0x38, libc.data()); + write(libc, 0, std::uint64_t{0x38}); + write(libc, 0x30, replacement.data()); + write(replacement, 0, std::uint64_t{0x78}); + write(replacement, 8, std::uint64_t{2}); + write(replacement, 0x20, reinterpret_cast<void*>(&Allocate)); + write(replacement, 0x28, reinterpret_cast<void*>(&Free)); + write(replacement, 0x30, reinterpret_cast<void*>(&Calloc)); + write(replacement, 0x38, reinterpret_cast<void*>(&Reallocate)); + write(replacement, 0x40, reinterpret_cast<void*>(&Align)); + write(replacement, 0x48, reinterpret_cast<void*>(&Realign)); + write(replacement, 0x50, reinterpret_cast<void*>(&PosixAlign)); + ApplicationHeapInitialize_nid_no_patch(process.data()); +} + struct Element { int key; int tag; @@ -66,6 +109,31 @@ bool ExpectSorted(std::size_t size, std::size_t count) { return true; } + +bool CheckMergesort() { + InitializeHeap(); + bool correct = true; + for (const std::size_t count : {0, 1, 2, 3, 7, 16, 33, 1000}) { + std::vector<Element> elements; + for (std::size_t index = 0; index < count; ++index) elements.push_back({static_cast<int>((index * 7919) % 5), static_cast<int>(index)}); + correct &= mergesort_nid_postfix(elements.data(), count, sizeof(Element), CompareKeys) == 0; + for (std::size_t index = 1; index < count; ++index) { + const auto& before = elements[index - 1]; + const auto& after = elements[index]; + correct &= before.key < after.key || (before.key == after.key && before.tag < after.tag); + } + } + unsigned char bytes[3] = {3, 1, 2}; + *__error_nid_postfix() = 0; + correct &= mergesort_nid_postfix(bytes, 3, 1, CompareFirstByte) == -1 && *__error_nid_postfix() == 22 && bytes[0] == 3; + correct &= mergesort_nid_postfix(nullptr, 0, 4, CompareKeys) == 0; + Element pair[2] = {{2, 0}, {1, 1}}; + failAllocation = true; + correct &= mergesort_nid_postfix(pair, 2, sizeof(Element), CompareKeys) == -1 && pair[0].key == 2; + failAllocation = false; + if (!correct) std::fprintf(stderr, "mergesort failed\n"); + return correct; +} } int main() { @@ -80,5 +148,6 @@ int main() { for (const std::size_t size : {1, 3, 8, 24}) for (const std::size_t count : {0, 1, 2, 6, 7, 8, 40, 41, 1000}) correct &= ExpectSorted(size, count); + correct &= CheckMergesort(); return correct ? 0 : 1; } diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 497130f640..f4624f1b3b 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -59,6 +59,7 @@ - [asinl](../../../core/libs/prx/libc/src/Math.cpp) (libc) follows the `asinl` of the `libc.prx` bundled with PPSA28319 (disassembly), FreeBSD msun's [`e_asinl.c`](https://github.com/freebsd/freebsd-src/blob/9f0432041b8fb69832f10c32da50a67f34feeb61/lib/msun/src/e_asinl.c) with the ld80 [`invtrig.c`](https://github.com/freebsd/freebsd-src/blob/9f0432041b8fb69832f10c32da50a67f34feeb61/lib/msun/ld80/invtrig.c) coefficients, which the binary holds as the same 80-bit constants. For |x| > 1, the infinities and NaN, Sony's error helper raises `FE_INVALID` and sets `errno` to `EDOM` before returning `(x - x) / (x - x)`, so a NaN argument also sets `EDOM`; msun itself sets no `errno`. 20 million random arguments in (-1, 1) give the same bits as that code run on a Windows host. The result needs the x87 precision control at 64 bits, as FreeBSD amd64 starts threads (control word `0x37f`), but guest threads do not set it: on Windows a thread started by `_beginthreadex` runs at 53 bits (`0x27f`), where `asinl(0.75)` returns mantissa `0xd91a98ae3406e000` instead of `0xd91a98ae3406e041`. The system `libSceLibcInternal` that RetroArch (PPSA99169) imports is assumed to have the same code - [atanhf](../../../core/libs/prx/libc/src/Math.cpp) (libc) follows the `atanhf` of the `libc.prx` bundled with PPSA28319 (disassembly): FreeBSD msun's [`e_atanhf.c`](https://github.com/freebsd/freebsd-src/blob/59bc2f095289a10b8cfa55cbb2b45c3bd6c2d4ae/lib/msun/src/e_atanhf.c) calling msun's [`log1pf`](https://github.com/freebsd/freebsd-src/blob/59bc2f095289a10b8cfa55cbb2b45c3bd6c2d4ae/lib/msun/src/s_log1pf.c), whose paths for the positive arguments it passes are inlined here; the exported `log1pf` is still the host's. For |x| > 1, the infinities and NaN, Sony's error helper raises `FE_INVALID` and sets `errno` to `EDOM` before returning `(x - x) / (x - x)`, and for |x| = 1 it raises `FE_DIVBYZERO` and sets `errno` to `ERANGE` before returning `x / 0`; msun itself sets no `errno`. That code, run on a Windows host, gives the same bits, `errno` and floating-point exceptions as this implementation for all 2^32 arguments; it was not run on Linux. That the system `libSceLibcInternal`, which Dusklight (PPSA99305) imports while bundling a zero-return `libc.prx`, has the same code is assumed - [getenv, setenv, unsetenv, putenv](../../../core/libs/prx/libc/src/Environment.cpp) (libc) keep one guest environment that starts empty, like the `environ` libkernel exports, and never reads or changes the host's. A title sees only the variables it sets itself; host variables such as `HOME` and `PATH` name host paths a guest cannot use (PS5MC, PPSA99420, exits with `XBMC_Run returned -1` when it gets the host's `HOME`). Whether the console starts a title with any variables set is not measured +- [mergesort](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libSceLibcInternal, implemented in libc) checks its arguments like the console's `libSceLibcInternal.sprx` (SDK 4.5x build of `J03950001`, 0x762D0): an element size under 4 sets `errno` to `EINVAL` (22) and returns -1, an empty array returns 0, and a failed `malloc` of the count times the size plus 8 bytes returns -1. The sort is a stable bottom-up merge, not FreeBSD's natural merge with its insertion sort for short runs, so the comparator is called in a different order and number of times; the result is the same for a consistent comparator. A null base or comparator and a size product that overflows, which the console does not check, throw - On macOS the [write watch](../../../core/libs/prx/libc/src/GuestWriteWatch.cpp) write-protects watched pages and records the first write to each from a SIGBUS/SIGSEGV handler, because macOS has no counterpart to `userfaultfd` write-protect or `MEM_WRITE_WATCH`. The protection is real, so: - a system call that writes into a protected page fails with `EFAULT` instead of faulting. `GuestArena::HostWrite` lifts the protection for the calls it brackets (file reads and socket receives); a host call that writes guest memory without it (`clock_gettime` and the like into a watched page) fails; - code that changes the protection of watched pages, or maps over them, has to call `GuestWriteWatchProtectionChanged`, and code that asks the kernel for a page's protection has to ask `GuestWriteWatchProtection` first, since a watched page is read-only between a collection and the next write. Nothing calls either yet: the macOS memory paths are not in the tree; From 24a999c49947cafe5ba14c29930bdb5d420a8653 Mon Sep 17 00:00:00 2001 From: T3cm0 <srodriguez7604@gmail.com> Date: Sat, 17 Oct 2026 20:43:13 -0500 Subject: [PATCH 0939/1000] feat(libc): implement long double, Bessel and integer math exports Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- core/libs/prx/libc/src/Math.cpp | 60 +++++++++++++++++++++ core/libs/tests/GuestMath.cpp | 92 +++++++++++++++++++++++++++++++++ docs/dev/debt/libc.md | 1 + 3 files changed, 153 insertions(+) diff --git a/core/libs/prx/libc/src/Math.cpp b/core/libs/prx/libc/src/Math.cpp index 029440fc53..ddeea98f8c 100644 --- a/core/libs/prx/libc/src/Math.cpp +++ b/core/libs/prx/libc/src/Math.cpp @@ -126,6 +126,23 @@ struct ExtendedParts { std::uint16_t signExponent; }; +template <typename TValue, typename THypot> +TValue Hypot3(TValue x, TValue y, TValue z, THypot hypot) { + if (std::isnan(x)) return x; + if (std::isnan(y)) return y; + if (std::isnan(z)) return z; + if (std::isinf(x) || std::isinf(y) || std::isinf(z)) return std::numeric_limits<TValue>::infinity(); + if (x == 0) return hypot(y, z); + if (y == 0) return hypot(x, z); + if (z == 0) return hypot(x, y); + x = std::fabs(x); + y = std::fabs(y); + z = std::fabs(z); + if (z >= x && z >= y) return hypot(z, hypot(x, y)); + if (y >= x && y >= z) return hypot(y, hypot(x, z)); + return hypot(x, hypot(y, z)); +} + ExtendedParts SplitExtended(long double value) { unsigned char bytes[sizeof(long double)]; std::memcpy(bytes, &value, sizeof(value)); @@ -199,6 +216,22 @@ std::lldiv_t APS5_VABI ldiv_nid_postfix(std::int64_t numerator, std::int64_t den return std::lldiv(numerator, denominator); } +std::lldiv_t APS5_VABI imaxdiv_nid_postfix(std::int64_t numerator, std::int64_t denominator) { + return std::lldiv(numerator, denominator); +} + +std::int64_t APS5_VABI labs_nid_postfix(std::int64_t x) { + return x < 0 ? static_cast<std::int64_t>(0u - static_cast<std::uint64_t>(x)) : x; +} + +std::int64_t APS5_VABI llabs_nid_postfix(std::int64_t x) { + return labs_nid_postfix(x); +} + +std::int64_t APS5_VABI imaxabs_nid_postfix(std::int64_t x) { + return labs_nid_postfix(x); +} + float APS5_VABI fmodf_nid_postfix(float x, float y) { return std::fmod(x, y); } float APS5_VABI asinf_nid_postfix(float x) { const int saved_errno = errno; @@ -242,6 +275,8 @@ double APS5_VABI exp2_nid_postfix(double x) { double APS5_VABI ldexp_nid_postfix(double x, int exponent) { return std::ldexp(x, exponent); } double APS5_VABI scalbn_nid_postfix(double x, int exponent) { return std::scalbn(x, exponent); } float APS5_VABI scalbnf_nid_postfix(float x, int exponent) { return std::scalbn(x, exponent); } +long double APS5_VABI scalbnl_nid_postfix(long double x, int exponent) { return std::scalbn(x, exponent); } +float APS5_VABI significandf_nid_postfix(float x) { return x == 0.0f || !std::isfinite(x) ? x : std::scalbn(x, -std::ilogb(x)); } double APS5_VABI frexp_nid_postfix(double x, int* exponent) { return std::frexp(x, exponent); } float APS5_VABI frexpf_nid_postfix(float x, int* exponent) { return std::frexp(x, exponent); } // Guest long is 64-bit, including on Windows where native long is 32-bit. @@ -285,6 +320,31 @@ double APS5_VABI fmax_nid_postfix(double x, double y) { return GuestFmax(x, y); float APS5_VABI fmaxf_nid_postfix(float x, float y) { return GuestFmax(x, y); } long double APS5_VABI fmaxl_nid_postfix(long double x, long double y) { return GuestFmax(x, y); } long double APS5_VABI acosl_nid_postfix(long double x) { return std::acos(x); } +long double APS5_VABI acoshl_nid_postfix(long double x) { return std::acosh(x); } +long double APS5_VABI asinhl_nid_postfix(long double x) { return std::asinh(x); } +long double APS5_VABI atanhl_nid_postfix(long double x) { return std::atanh(x); } +long double APS5_VABI atan2l_nid_postfix(long double y, long double x) { return std::atan2(y, x); } +long double APS5_VABI cbrtl_nid_postfix(long double x) { return std::cbrt(x); } +long double APS5_VABI erfl_nid_postfix(long double x) { return std::erf(x); } +long double APS5_VABI erfcl_nid_postfix(long double x) { return std::erfc(x); } +long double APS5_VABI expm1l_nid_postfix(long double x) { return std::expm1(x); } +long double APS5_VABI log1pl_nid_postfix(long double x) { return std::log1p(x); } +long double APS5_VABI log2l_nid_postfix(long double x) { return std::log2(x); } +long double APS5_VABI lgammal_nid_postfix(long double x) { return std::lgamma(x); } +long double APS5_VABI tgammal_nid_postfix(long double x) { return std::tgamma(x); } +long double APS5_VABI hypotl_nid_postfix(long double x, long double y) { + if (std::isinf(x) || std::isinf(y)) return std::numeric_limits<long double>::infinity(); + return std::hypot(x, y); +} +float APS5_VABI hypot3f_nid_postfix(float x, float y, float z) { return Hypot3(x, y, z, hypotf_nid_postfix); } +double APS5_VABI hypot3_nid_postfix(double x, double y, double z) { return Hypot3(x, y, z, hypot_nid_postfix); } +long double APS5_VABI hypot3l_nid_postfix(long double x, long double y, long double z) { return Hypot3(x, y, z, hypotl_nid_postfix); } +float APS5_VABI j0f_nid_postfix(float x) { return static_cast<float>(::j0(x)); } +float APS5_VABI j1f_nid_postfix(float x) { return static_cast<float>(::j1(x)); } +float APS5_VABI jnf_nid_postfix(int order, float x) { return static_cast<float>(::jn(order, x)); } +float APS5_VABI y0f_nid_postfix(float x) { return static_cast<float>(::y0(x)); } +float APS5_VABI y1f_nid_postfix(float x) { return static_cast<float>(::y1(x)); } +float APS5_VABI ynf_nid_postfix(int order, float x) { return static_cast<float>(::yn(order, x)); } long double APS5_VABI asinl_nid_postfix(long double x) { const ExtendedParts parts = SplitExtended(x); diff --git a/core/libs/tests/GuestMath.cpp b/core/libs/tests/GuestMath.cpp index 3ab7ee3685..d095eda0ff 100644 --- a/core/libs/tests/GuestMath.cpp +++ b/core/libs/tests/GuestMath.cpp @@ -117,6 +117,34 @@ int APS5_VABI __isinf_nid_postfix(double); int APS5_VABI isnan_nid_postfix(double); int APS5_VABI isinf_nid_postfix(double); std::lldiv_t APS5_VABI lldiv_nid_postfix(long long, long long); +std::lldiv_t APS5_VABI imaxdiv_nid_postfix(std::int64_t, std::int64_t); +std::int64_t APS5_VABI labs_nid_postfix(std::int64_t); +std::int64_t APS5_VABI llabs_nid_postfix(std::int64_t); +std::int64_t APS5_VABI imaxabs_nid_postfix(std::int64_t); +long double APS5_VABI scalbnl_nid_postfix(long double, int); +float APS5_VABI significandf_nid_postfix(float); +long double APS5_VABI acoshl_nid_postfix(long double); +long double APS5_VABI asinhl_nid_postfix(long double); +long double APS5_VABI atanhl_nid_postfix(long double); +long double APS5_VABI atan2l_nid_postfix(long double, long double); +long double APS5_VABI cbrtl_nid_postfix(long double); +long double APS5_VABI erfl_nid_postfix(long double); +long double APS5_VABI erfcl_nid_postfix(long double); +long double APS5_VABI expm1l_nid_postfix(long double); +long double APS5_VABI log1pl_nid_postfix(long double); +long double APS5_VABI log2l_nid_postfix(long double); +long double APS5_VABI lgammal_nid_postfix(long double); +long double APS5_VABI tgammal_nid_postfix(long double); +long double APS5_VABI hypotl_nid_postfix(long double, long double); +float APS5_VABI hypot3f_nid_postfix(float, float, float); +double APS5_VABI hypot3_nid_postfix(double, double, double); +long double APS5_VABI hypot3l_nid_postfix(long double, long double, long double); +float APS5_VABI j0f_nid_postfix(float); +float APS5_VABI j1f_nid_postfix(float); +float APS5_VABI jnf_nid_postfix(int, float); +float APS5_VABI y0f_nid_postfix(float); +float APS5_VABI y1f_nid_postfix(float); +float APS5_VABI ynf_nid_postfix(int, float); std::lldiv_t APS5_VABI ldiv_nid_postfix(std::int64_t, std::int64_t); float APS5_VABI nanf_nid_postfix(const char*); double APS5_VABI nan_nid_postfix(const char*); @@ -755,6 +783,67 @@ static void CheckFloatMathExports() { Require(isinf_nid_postfix(nan) == 0 && isinf_nid_postfix(1.0) == 0); } +static void CheckIntegerAbsAndDivision() { + for (auto function : {labs_nid_postfix, llabs_nid_postfix, imaxabs_nid_postfix}) { + Require(function(-5) == 5 && function(7) == 7 && function(0) == 0); + Require(function(-0x100000000ll) == 0x100000000ll); + Require(function(INT64_MAX) == INT64_MAX && function(INT64_MIN) == INT64_MIN); + } + const auto small = imaxdiv_nid_postfix(-7, 2); + Require(small.quot == -3 && small.rem == -1); + const auto large = imaxdiv_nid_postfix(0x123456789abll, -0x1000); + Require(large.quot == -0x12345678ll && large.rem == 0x9abll); +} + +static void CheckScaling() { + Require(scalbnl_nid_postfix(3.0L, 4) == 48.0L && scalbnl_nid_postfix(1.0L, -16445) == 0x1p-16445L); + Require(scalbnl_nid_postfix(1.0L, 16384) == std::numeric_limits<long double>::infinity()); + Require(significandf_nid_postfix(12.0f) == 1.5f && significandf_nid_postfix(-0.375f) == -1.5f); + Require(significandf_nid_postfix(0x1p-149f) == 1.0f); + Require(std::signbit(significandf_nid_postfix(-0.0f)) && significandf_nid_postfix(-0.0f) == 0.0f); + Require(significandf_nid_postfix(std::numeric_limits<float>::infinity()) == std::numeric_limits<float>::infinity()); + Require(std::isnan(significandf_nid_postfix(std::numeric_limits<float>::quiet_NaN()))); +} + +static bool Near(long double actual, long double expected) { + return std::fabs(actual - expected) <= std::fabs(expected) * 1e-17L; +} + +static void CheckLongDoubleFunctions() { + const long double infinity = std::numeric_limits<long double>::infinity(); + const long double nan = std::numeric_limits<long double>::quiet_NaN(); + Require(acoshl_nid_postfix(1.0L) == 0.0L && std::isnan(acoshl_nid_postfix(0.5L))); + Require(asinhl_nid_postfix(-0.0L) == 0.0L && std::signbit(asinhl_nid_postfix(-0.0L))); + Require(atanhl_nid_postfix(1.0L) == infinity && Near(atanhl_nid_postfix(0.5L), 0.5493061443340548456976226L)); + Require(Near(atan2l_nid_postfix(1.0L, 1.0L), 0.7853981633974483096156608L) && Near(atan2l_nid_postfix(0.0L, -1.0L), 3.1415926535897932384626434L)); + Require(cbrtl_nid_postfix(-27.0L) == -3.0L && Near(cbrtl_nid_postfix(0x1p-16443L) * cbrtl_nid_postfix(0x1p-16443L) * cbrtl_nid_postfix(0x1p-16443L), 0x1p-16443L)); + Require(erfl_nid_postfix(0.0L) == 0.0L && erfl_nid_postfix(infinity) == 1.0L && erfcl_nid_postfix(0.0L) == 1.0L); + Require(erfcl_nid_postfix(infinity) == 0.0L && erfcl_nid_postfix(-infinity) == 2.0L); + Require(expm1l_nid_postfix(0.0L) == 0.0L && expm1l_nid_postfix(-infinity) == -1.0L); + Require(log1pl_nid_postfix(0.0L) == 0.0L && log1pl_nid_postfix(-1.0L) == -infinity); + Require(log2l_nid_postfix(8.0L) == 3.0L && log2l_nid_postfix(0x1p-16445L) == -16445.0L); + Require(lgammal_nid_postfix(1.0L) == 0.0L && lgammal_nid_postfix(2.0L) == 0.0L && lgammal_nid_postfix(0.0L) == infinity); + Require(tgammal_nid_postfix(5.0L) == 24.0L && Near(tgammal_nid_postfix(0.5L), 1.7724538509055160272981675L)); + Require(hypotl_nid_postfix(3.0L, -4.0L) == 5.0L && hypotl_nid_postfix(nan, -infinity) == infinity); + + Require(hypot3f_nid_postfix(3.0f, -4.0f, 12.0f) == 13.0f && hypot3f_nid_postfix(0.0f, 3.0f, -4.0f) == 5.0f); + Require(hypot3f_nid_postfix(2.0f, 0.0f, 0.0f) == 2.0f && hypot3f_nid_postfix(-2.0f, 3.0f, 6.0f) == 7.0f); + Require(hypot3_nid_postfix(1.0, 2.0, 2.0) == 3.0 && hypot3_nid_postfix(12.0, 3.0, 4.0) == 13.0); + Require(hypot3l_nid_postfix(2.0L, -3.0L, 6.0L) == 7.0L); + const double payload = std::bit_cast<double>(0x7ff8000000000123ull); + Require(std::bit_cast<std::uint64_t>(hypot3_nid_postfix(1.0, payload, std::numeric_limits<double>::infinity())) == 0x7ff8000000000123ull); + Require(std::isnan(hypot3f_nid_postfix(std::numeric_limits<float>::infinity(), 1.0f, std::numeric_limits<float>::quiet_NaN()))); + Require(hypot3l_nid_postfix(-infinity, 1.0L, 2.0L) == infinity && hypot3_nid_postfix(1.0, -std::numeric_limits<double>::infinity(), 0.0) == std::numeric_limits<double>::infinity()); + Require(hypot3_nid_postfix(0x1p1000, 0x1p1000, 0x1p1000) == std::hypot(0x1p1000, std::hypot(0x1p1000, 0x1p1000))); + + Require(j0f_nid_postfix(0.0f) == 1.0f && j1f_nid_postfix(0.0f) == 0.0f && jnf_nid_postfix(2, 0.0f) == 0.0f); + Require(std::fabs(j0f_nid_postfix(2.5f) - static_cast<float>(std::cyl_bessel_j(0.0, 2.5))) < 1e-6f); + Require(std::fabs(jnf_nid_postfix(3, 4.0f) - static_cast<float>(std::cyl_bessel_j(3.0, 4.0))) < 1e-6f); + Require(y0f_nid_postfix(0.0f) == -std::numeric_limits<float>::infinity() && y1f_nid_postfix(0.0f) == -std::numeric_limits<float>::infinity()); + Require(std::fabs(ynf_nid_postfix(2, 3.0f) - static_cast<float>(std::cyl_neumann(2.0, 3.0))) < 1e-6f); + Require(std::isnan(y0f_nid_postfix(-1.0f))); +} + static void CheckDoubleClassification() { const struct { std::uint64_t bits; short code; } cases[] = { {0x0000000000000000ull, 0}, {0x8000000000000000ull, 0}, @@ -1093,4 +1182,7 @@ int main() { CheckHypotSpecialValue(); CheckFmodErrno(); CheckDoubleClassification(); + CheckIntegerAbsAndDivision(); + CheckScaling(); + CheckLongDoubleFunctions(); } diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 497130f640..c2dc13eb6d 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -56,6 +56,7 @@ - [nan and nanf](../../../core/libs/prx/libc/src/Math.cpp) (libc) return fixed quiet NaN values (`0x7fc00000` and `0x7ff8000000000000`), ignoring the tag argument as observed in the console's `libc.prx` (both exports load a global constant via `mov rax,[rip+..]; vmovsd xmm0,[rax]; ret`); VLC (PPSA85300) stops on nanf at start-up after strsep. - [lgamma and signgam](../../../core/libs/prx/libc/src/Math.cpp) (libc) - `lgamma` returns the host's `lgamma` and stores the sign of Γ(x) in the exported `signgam` (NID `5gOOC0kzW0c`, starting at 0 as in FreeBSD 11's [`s_signgam.c`](https://github.com/freebsd/freebsd-src/blob/stable/11/lib/msun/src/s_signgam.c)) by the rules of [`e_lgamma_r.c`](https://github.com/freebsd/freebsd-src/blob/stable/11/lib/msun/src/e_lgamma_r.c): -1 for -0 and for a negative non-integer whose floor is odd, 1 otherwise, including poles, infinities and NaN. Taken from the FreeBSD source, not measured on the console. The NID is in the aerolib symbol list; no title is known to read `signgam`. PPSA99004 imports `lgamma` - [sinhf](../../../core/libs/prx/libc/src/Math.cpp) (libc) follows the `sinhf` of the `libc.prx` bundled with PPSA28319 (disassembly): FreeBSD msun's [`e_sinhf.c`](https://github.com/freebsd/freebsd-src/blob/5aa554c7e5b6fd9042986df07f12ac5687e41392/lib/msun/src/e_sinhf.c) from before `__ldexp_expf`, which squares `expf(|x| / 2)` between `logf(FLT_MAX)` and the overflow threshold, calling msun's [`expm1f`](https://github.com/freebsd/freebsd-src/blob/59bc2f095289a10b8cfa55cbb2b45c3bd6c2d4ae/lib/msun/src/s_expm1f.c) and [`expf`](https://github.com/freebsd/freebsd-src/blob/59bc2f095289a10b8cfa55cbb2b45c3bd6c2d4ae/lib/msun/src/e_expf.c), whose paths for the arguments it passes are inlined here. Past the threshold Sony's error helper sets `errno` to `ERANGE` before returning `x * 1e37`; msun itself sets no `errno`. That code, run on a Windows host, gives the same bits as this implementation for every finite argument up to the threshold, of both signs; it was not run on Linux. That the system `libSceLibcInternal`, which Ruffle (PPSA68091) and RetroArch (PPSA99169) import while bundling a zero-return `libc.prx`, has the same code is assumed +- [acoshl, asinhl, atanhl, atan2l, cbrtl, erfl, erfcl, expm1l, hypotl, log1pl, log2l, lgammal, tgammal, j0f, j1f, jnf, y0f, y1f and ynf](../../../core/libs/prx/libc/src/Math.cpp) (libc) return the host C library's results (the float Bessel functions through its double ones), not those of the console's own code, so the last bits can differ. As in the console's `libSceLibcInternal.sprx` (SDK 4.5x build of `J03950001`, 0x56410), `lgammal` does not store the sign in `signgam`; `hypotl` returns infinity for an infinite argument even when the other is a NaN, as C11 F.10.4.3 requires. [hypot3, hypot3f and hypot3l](../../../core/libs/prx/libc/src/Math.cpp) follow that module (0x51D30, 0x520D0, 0x52420): the first NaN of x, y and z is returned unchanged, any infinity gives positive infinity, a zero argument gives the two-argument hypot of the other two, and otherwise the largest magnitude is combined with the hypot of the other two - [asinl](../../../core/libs/prx/libc/src/Math.cpp) (libc) follows the `asinl` of the `libc.prx` bundled with PPSA28319 (disassembly), FreeBSD msun's [`e_asinl.c`](https://github.com/freebsd/freebsd-src/blob/9f0432041b8fb69832f10c32da50a67f34feeb61/lib/msun/src/e_asinl.c) with the ld80 [`invtrig.c`](https://github.com/freebsd/freebsd-src/blob/9f0432041b8fb69832f10c32da50a67f34feeb61/lib/msun/ld80/invtrig.c) coefficients, which the binary holds as the same 80-bit constants. For |x| > 1, the infinities and NaN, Sony's error helper raises `FE_INVALID` and sets `errno` to `EDOM` before returning `(x - x) / (x - x)`, so a NaN argument also sets `EDOM`; msun itself sets no `errno`. 20 million random arguments in (-1, 1) give the same bits as that code run on a Windows host. The result needs the x87 precision control at 64 bits, as FreeBSD amd64 starts threads (control word `0x37f`), but guest threads do not set it: on Windows a thread started by `_beginthreadex` runs at 53 bits (`0x27f`), where `asinl(0.75)` returns mantissa `0xd91a98ae3406e000` instead of `0xd91a98ae3406e041`. The system `libSceLibcInternal` that RetroArch (PPSA99169) imports is assumed to have the same code - [atanhf](../../../core/libs/prx/libc/src/Math.cpp) (libc) follows the `atanhf` of the `libc.prx` bundled with PPSA28319 (disassembly): FreeBSD msun's [`e_atanhf.c`](https://github.com/freebsd/freebsd-src/blob/59bc2f095289a10b8cfa55cbb2b45c3bd6c2d4ae/lib/msun/src/e_atanhf.c) calling msun's [`log1pf`](https://github.com/freebsd/freebsd-src/blob/59bc2f095289a10b8cfa55cbb2b45c3bd6c2d4ae/lib/msun/src/s_log1pf.c), whose paths for the positive arguments it passes are inlined here; the exported `log1pf` is still the host's. For |x| > 1, the infinities and NaN, Sony's error helper raises `FE_INVALID` and sets `errno` to `EDOM` before returning `(x - x) / (x - x)`, and for |x| = 1 it raises `FE_DIVBYZERO` and sets `errno` to `ERANGE` before returning `x / 0`; msun itself sets no `errno`. That code, run on a Windows host, gives the same bits, `errno` and floating-point exceptions as this implementation for all 2^32 arguments; it was not run on Linux. That the system `libSceLibcInternal`, which Dusklight (PPSA99305) imports while bundling a zero-return `libc.prx`, has the same code is assumed - [getenv, setenv, unsetenv, putenv](../../../core/libs/prx/libc/src/Environment.cpp) (libc) keep one guest environment that starts empty, like the `environ` libkernel exports, and never reads or changes the host's. A title sees only the variables it sets itself; host variables such as `HOME` and `PATH` name host paths a guest cannot use (PS5MC, PPSA99420, exits with `XBMC_Run returned -1` when it gets the host's `HOME`). Whether the console starts a title with any variables set is not measured From 58697488a2760a9fddc2179ecbde8800d0413589 Mon Sep 17 00:00:00 2001 From: T3cm0 <srodriguez7604@gmail.com> Date: Sat, 17 Oct 2026 20:43:39 -0500 Subject: [PATCH 0940/1000] feat(libc): implement the uchar.h conversions, mblen and wctomb Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- core/libs/prx/libc/src/Multibyte.cpp | 160 +++++++++++++++++++++++++++ core/libs/tests/GuestWideStrings.cpp | 56 ++++++++++ docs/dev/debt/libc.md | 1 + 3 files changed, 217 insertions(+) diff --git a/core/libs/prx/libc/src/Multibyte.cpp b/core/libs/prx/libc/src/Multibyte.cpp index fee93d88d6..5865fe5933 100644 --- a/core/libs/prx/libc/src/Multibyte.cpp +++ b/core/libs/prx/libc/src/Multibyte.cpp @@ -5,6 +5,54 @@ #include <cstdint> #include <cstring> #include <climits> +#include <stdexcept> + +namespace { + +constexpr std::size_t PendingOffset = 0xa; +constexpr std::int8_t LowSurrogatePending = 0x10; + +void StorePending(unsigned char* state, std::int8_t pending) { + const auto stored = static_cast<std::int16_t>(pending); + std::memcpy(state + PendingOffset, &stored, sizeof(stored)); +} + +std::size_t EncodeUtf8(char* destination, std::uint64_t value) { + if (value < 0x80) { + destination[0] = static_cast<char>(value); + return 1; + } + int extra = 5; + unsigned lead = 0xfc; + int shift = 30; + if (value < 0x800) { extra = 1; lead = 0xc0; shift = 6; } + else if (value < 0x10000) { extra = 2; lead = 0xe0; shift = 12; } + else if (value < 0x200000) { extra = 3; lead = 0xf0; shift = 18; } + else if (value < 0x4000000) { extra = 4; lead = 0xf8; shift = 24; } + destination[0] = static_cast<char>((value >> shift) | lead); + for (int index = 1; index <= extra; ++index) + destination[index] = static_cast<char>(((value >> (shift - 6 * index)) & 0x3f) | 0x80); + return static_cast<std::size_t>(extra + 1); +} + +bool DecodeUtf8Byte(unsigned char byte, std::uint64_t& value, std::int8_t& pending) { + if (pending > 0) { + if ((byte & 0xc0) != 0x80) return false; + value = (value << 6) | (byte & 0x3f); + --pending; + return true; + } + if (byte < 0x80) value = byte; + else if ((byte & 0xe0) == 0xc0) { value = byte & 0x1f; pending = 1; } + else if ((byte & 0xf0) == 0xe0) { value = byte & 0x0f; pending = 2; } + else if ((byte & 0xf8) == 0xf0) { value = byte & 0x07; pending = 3; } + else if (byte >= 0xfc) { value = byte & 0x03; pending = 5; } + else if ((byte & 0xfc) == 0xf8) { value = byte & 0x03; pending = 4; } + else return false; + return true; +} + +} extern "C" { @@ -51,6 +99,118 @@ std::size_t APS5_VABI wcrtomb_nid_postfix(char* destination, std::uint16_t value return 1; } +int APS5_VABI mblen_nid_postfix(const char* source, std::size_t count) { + return mbtowc_nid_postfix(nullptr, source, count); +} + +int APS5_VABI wctomb_nid_postfix(char* destination, std::uint16_t value) { + if (!destination) return 0; + const auto result = wcrtomb_nid_postfix(destination, value, nullptr); + return result == static_cast<std::size_t>(-1) ? -1 : static_cast<int>(result); +} + +std::size_t APS5_VABI c32rtomb_nid_postfix(char* destination, std::uint32_t value, void*) { + char scratch[8]; + if (!destination) { + destination = scratch; + value = 0; + } + return EncodeUtf8(destination, value); +} + +std::size_t APS5_VABI c16rtomb_nid_postfix(char* destination, std::uint16_t unit, void* state) { + if (!state) throw std::invalid_argument("c16rtomb: null conversion state"); + char scratch[8]; + if (!destination) { + destination = scratch; + unit = 0; + } + auto* const bytes = static_cast<unsigned char*>(state); + std::uint64_t value = unit; + if (bytes[PendingOffset] != 0) { + if (static_cast<std::uint16_t>(unit + 0x2000) < 0xfc00) return static_cast<std::size_t>(-1); + std::uint64_t high = 0; + std::memcpy(&high, bytes, sizeof(high)); + value = high | (unit & 0x3ffu); + StorePending(bytes, 0); + } else if (static_cast<std::uint16_t>(unit + 0x2400) >= 0xfc00) { + const std::uint64_t high = ((static_cast<std::uint64_t>(unit) << 10) + 0x10000) & 0x1ffc00; + std::memcpy(bytes, &high, sizeof(high)); + StorePending(bytes, 1); + return 0; + } + return EncodeUtf8(destination, value); +} + +std::size_t APS5_VABI mbrtoc32_nid_postfix(std::uint32_t* destination, const char* source, std::size_t count, void* state) { + if (!state) throw std::invalid_argument("mbrtoc32: null conversion state"); + auto* const bytes = static_cast<unsigned char*>(state); + if (!source) { + destination = nullptr; + source = ""; + count = 1; + } + std::uint64_t value = 0; + std::memcpy(&value, bytes, sizeof(value)); + auto pending = static_cast<std::int8_t>(bytes[PendingOffset]); + for (std::size_t consumed = 0; consumed < count;) { + const auto byte = static_cast<unsigned char>(source[consumed++]); + if (!DecodeUtf8Byte(byte, value, pending)) { + errno = 86; + return static_cast<std::size_t>(-1); + } + if (pending == 0) { + if (destination) *destination = static_cast<std::uint32_t>(value); + StorePending(bytes, 0); + return value == 0 ? 0 : consumed; + } + } + std::memcpy(bytes, &value, sizeof(value)); + StorePending(bytes, pending); + return static_cast<std::size_t>(-2); +} + +std::size_t APS5_VABI mbrtoc16_nid_postfix(std::uint16_t* destination, const char* source, std::size_t count, void* state) { + if (!state) throw std::invalid_argument("mbrtoc16: null conversion state"); + auto* const bytes = static_cast<unsigned char*>(state); + if (!source) { + destination = nullptr; + source = ""; + count = 1; + } + std::uint64_t value = 0; + std::memcpy(&value, bytes, sizeof(value)); + auto pending = static_cast<std::int8_t>(bytes[PendingOffset]); + if (pending == LowSurrogatePending) { + if (destination) *destination = static_cast<std::uint16_t>((value & 0x3ff) | 0xdc00); + StorePending(bytes, 0); + return static_cast<std::size_t>(-3); + } + for (std::size_t consumed = 0; consumed < count;) { + const auto byte = static_cast<unsigned char>(source[consumed++]); + if (!DecodeUtf8Byte(byte, value, pending)) { + errno = 86; + return static_cast<std::size_t>(-1); + } + if (pending != 0) continue; + if (value >= 0x110000) { + errno = 86; + return static_cast<std::size_t>(-1); + } + if (value >= 0x10000) { + if (destination) *destination = static_cast<std::uint16_t>(((value >> 10) + 0x3fc0) | 0xd800); + StorePending(bytes, LowSurrogatePending); + return consumed; + } + if (destination) *destination = static_cast<std::uint16_t>(value); + StorePending(bytes, 0); + return value == 0 ? 0 : consumed; + } + std::memcpy(bytes, &value, sizeof(value)); + StorePending(bytes, pending); + return static_cast<std::size_t>(-2); +} + std::size_t APS5_VABI wcsrtombs_nid_postfix(char* destination, const std::uint16_t** source, std::size_t capacity, void* state) { const auto* wide = *source; std::size_t converted = 0; diff --git a/core/libs/tests/GuestWideStrings.cpp b/core/libs/tests/GuestWideStrings.cpp index 39b16ff31e..ceb09aaa92 100644 --- a/core/libs/tests/GuestWideStrings.cpp +++ b/core/libs/tests/GuestWideStrings.cpp @@ -8,8 +8,16 @@ #include <cmath> #include <cstdint> #include <cstring> +#include <string> extern "C" { +std::size_t APS5_VABI c32rtomb_nid_postfix(char* destination, std::uint32_t value, void* state); +std::size_t APS5_VABI c16rtomb_nid_postfix(char* destination, std::uint16_t unit, void* state); +std::size_t APS5_VABI mbrtoc32_nid_postfix(std::uint32_t* destination, const char* source, std::size_t count, void* state); +std::size_t APS5_VABI mbrtoc16_nid_postfix(std::uint16_t* destination, const char* source, std::size_t count, void* state); +int APS5_VABI mblen_nid_postfix(const char* source, std::size_t count); +int APS5_VABI wctomb_nid_postfix(char* destination, std::uint16_t value); +int* APS5_VABI __error_nid_postfix(); const char16_t* APS5_VABI wmemchr_nid_postfix(const char16_t* s, char16_t c, std::size_t n); int APS5_VABI wmemcmp_nid_postfix(const char16_t* s1, const char16_t* s2, std::size_t n); char16_t* APS5_VABI wmemcpy_nid_postfix(char16_t* dest, const char16_t* src, std::size_t n); @@ -55,6 +63,54 @@ bool same(const char16_t* left, const char16_t* right, std::size_t count) { } int main() { + { + const auto bytesOf = [](const char* text, std::size_t count) { return std::string(text, count); }; + char out[8] = {}; + require(c32rtomb_nid_postfix(out, 0x41, nullptr) == 1 && out[0] == 'A'); + require(c32rtomb_nid_postfix(out, 0xe9, nullptr) == 2 && bytesOf(out, 2) == "\xc3\xa9"); + require(c32rtomb_nid_postfix(out, 0x20ac, nullptr) == 3 && bytesOf(out, 3) == "\xe2\x82\xac"); + require(c32rtomb_nid_postfix(out, 0x1f600, nullptr) == 4 && bytesOf(out, 4) == "\xf0\x9f\x98\x80"); + require(c32rtomb_nid_postfix(out, 0x7fffffff, nullptr) == 6 && bytesOf(out, 6) == "\xfd\xbf\xbf\xbf\xbf\xbf"); + require(c32rtomb_nid_postfix(nullptr, 0x20ac, nullptr) == 1); + + alignas(8) unsigned char state[16] = {}; + require(c16rtomb_nid_postfix(out, 0xd83d, state) == 0 && state[10] == 1); + require(c16rtomb_nid_postfix(out, 0x41, state) == static_cast<std::size_t>(-1) && state[10] == 1); + require(c16rtomb_nid_postfix(nullptr, 0x41, state) == static_cast<std::size_t>(-1)); + require(c16rtomb_nid_postfix(out, 0xde00, state) == 4 && bytesOf(out, 4) == "\xf0\x9f\x98\x80" && state[10] == 0); + require(c16rtomb_nid_postfix(out, 0xdc00, state) == 3 && bytesOf(out, 3) == "\xed\xb0\x80"); + require(c16rtomb_nid_postfix(out, 0x7f, state) == 1 && out[0] == 0x7f); + + std::memset(state, 0, sizeof(state)); + std::uint32_t wide = 0; + require(mbrtoc32_nid_postfix(&wide, "\xf0\x9f\x98\x80z", 5, state) == 4 && wide == 0x1f600); + require(mbrtoc32_nid_postfix(&wide, "\xe2\x82", 2, state) == static_cast<std::size_t>(-2) && state[10] == 1); + require(mbrtoc32_nid_postfix(&wide, "\xac", 1, state) == 1 && wide == 0x20ac && state[10] == 0); + require(mbrtoc32_nid_postfix(&wide, "", 1, state) == 0 && wide == 0); + require(mbrtoc32_nid_postfix(&wide, "A", 0, state) == static_cast<std::size_t>(-2)); + wide = 7; + require(mbrtoc32_nid_postfix(&wide, nullptr, 9, state) == 0 && wide == 7); + *__error_nid_postfix() = 0; + require(mbrtoc32_nid_postfix(&wide, "\x80", 1, state) == static_cast<std::size_t>(-1) && *__error_nid_postfix() == 86); + require(mbrtoc32_nid_postfix(&wide, "\xfd\xbf\xbf\xbf\xbf\xbf", 6, state) == 6 && wide == 0x7fffffff); + + std::memset(state, 0, sizeof(state)); + std::uint16_t unit = 0; + require(mbrtoc16_nid_postfix(&unit, "\xc3\xa9", 2, state) == 2 && unit == 0xe9); + require(mbrtoc16_nid_postfix(&unit, "\xf0\x9f", 2, state) == static_cast<std::size_t>(-2)); + require(mbrtoc16_nid_postfix(&unit, "\x98\x80", 2, state) == 2 && unit == 0xd83d && state[10] == 0x10); + require(mbrtoc16_nid_postfix(&unit, "A", 1, state) == static_cast<std::size_t>(-3) && unit == 0xdc1f && state[10] == 0); + std::memset(state, 0, sizeof(state)); + require(mbrtoc16_nid_postfix(&unit, "\xf0\x9f\x98\x80", 4, state) == 4 && unit == 0xd83d); + require(mbrtoc16_nid_postfix(&unit, nullptr, 0, state) == static_cast<std::size_t>(-3) && unit == 0xd83d); + *__error_nid_postfix() = 0; + require(mbrtoc16_nid_postfix(&unit, "\xf4\x90\x80\x80", 4, state) == static_cast<std::size_t>(-1) && *__error_nid_postfix() == 86); + + require(mblen_nid_postfix(nullptr, 1) == 0 && mblen_nid_postfix("ab", 2) == 1 && mblen_nid_postfix("", 1) == 0); + require(wctomb_nid_postfix(nullptr, 0x41) == 0); + require(wctomb_nid_postfix(out, 0x42) == 1 && out[0] == 'B'); + require(wctomb_nid_postfix(out, 0x263a) == -1); + } const char16_t split[] = {u'a', u'b', 0, u'c', 0}; require(wcslen_nid_postfix(u"") == 0); require(wcslen_nid_postfix(u"héllo") == 5); diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 497130f640..c36b632a21 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -72,6 +72,7 @@ - [getifaddrs](../../../core/libs/prx/libc/src/Interfaces.cpp) (libc) lists the host's IPv4 and IPv6 addresses in FreeBSD's `ifaddrs` and sockaddr layouts. FreeBSD's per-interface AF_LINK entries (MAC, index, MTU) and their `if_data` are not produced, so interfaces without an address are omitted. Names are the host's (Linux `lo`, `eth0`; Windows NDIS names such as `ethernet_32769`, or `if<index>` when one does not fit 15 bytes), not the console's. A Linux point-to-point address without a peer loses `IFF_POINTOPOINT`, and an IPv4 broadcast address the host omits is computed from the netmask. On Windows the flags are only `IFF_UP`, `IFF_RUNNING`, `IFF_LOOPBACK`, `IFF_BROADCAST` and `IFF_MULTICAST`, derived from the adapter: PPP and tunnel adapters get neither `IFF_BROADCAST` nor `IFF_POINTOPOINT` because their peer address is not reported, and an address whose prefix length the host reports as unspecified gets a host mask. - [gethostname](../../../core/libs/prx/libc/src/Hostname.cpp) (libc) reports the fixed guest name `AnyPS5` without querying the host machine name. Buffer copying, null queries and `ENAMETOOLONG` follow [FreeBSD gethostname](https://github.com/freebsd/freebsd-src/blob/main/lib/libc/gen/gethostname.c); the console's configurable hostname and its integration with `sysctl` and name resolution are unverified and not implemented. - [wcsrtombs_s](../../../core/libs/prx/libc/src/Multibyte.cpp) (libc) follows the console's `libc.prx`, but a runtime-constraint violation only returns its error: the default handler there writes the message and a newline to `stderr` before returning, which is not done here. Conversion uses the C locale of `wcrtomb` (`MB_CUR_MAX` 1), so the path where a multibyte character does not fit the remaining space is not reached +- [c16rtomb, c32rtomb, mbrtoc16 and mbrtoc32](../../../core/libs/prx/libc/src/Multibyte.cpp) (libSceLibcInternal, implemented in libc) follow the console's `libSceLibcInternal.sprx` (SDK 4.5x build of `J03950001`, 0x7D480 to 0x7D920), which converts to and from UTF-8 whatever the locale, with the original sequences of up to six bytes and no check for surrogates or for values above 0x10FFFF when encoding. The conversion state keeps the value in its first eight bytes and the count of bytes still expected (or 0x10 for a pending low surrogate, 1 for a stored high surrogate) as 16 bits at offset 0xA; a null state, which the console dereferences, throws. As on the console, `mbrtoc16` does not store the complete code point when it returns a high surrogate, so the low surrogate of the next call is built from the value the state last held, which is the partial value of an earlier incomplete call or whatever the caller initialised it to, `c16rtomb` returns -1 without setting `errno` when a high surrogate is not followed by a low one, and `mbrtoc16` and `mbrtoc32` report `EILSEQ` as 86. Measured on a PS5 (firmware 13.60) from a homebrew payload, the system `libSceLibcInternal`'s default locale also converts UTF-8 in `wcrtomb`, `wctomb`, `mbrtowc` and `mblen` (`wctomb` of U+263A gives `E2 98 BA`, `mblen` of a lone 0xE9 gives -1); this libc keeps its single-byte C locale, so [wctomb and mblen](../../../core/libs/prx/libc/src/Multibyte.cpp), which call `wcrtomb` and `mbtowc` here as they call the same internal routines on the console, share that difference. Titles bundle their own `libc.prx`, whose locale was not measured - [The narrow printf family](../../../core/libs/prx/libc/src/Formatting.cpp) (libc) converts 16-bit guest wide arguments to UTF-8 on both platforms, independently of the host locale. It does not use the guest's single-byte `wcrtomb`; the console's treatment of invalid UTF-16 and locale-specific encoding remains unverified. Linux retains host formatting for formats without wide arguments or `%m`, so a narrow `%p` and a NaN there still follow the host (glibc prints a null pointer as `(nil)`, where FreeBSD prints `0x0`, and a NaN with its sign, `-nan`, and with the `+` and space flags, where FreeBSD prints `nan`); argument positions above 99 (FreeBSD's `NL_ARGMAX`) throw when a format requires the guest formatter - [collate<char> and collate<wchar_t> _Getcat](../../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc) build the facet for the C locale only and throw for any other locale name, the wide entry point following the narrow one it sits next to. The wide facet carries its own vtable, whose `compare`, `transform` and `hash` take the guest's 16-bit characters, so the length of a range is a count of characters: `compare` orders by 16-bit value (the C locale, as Dinkumware's `char_traits<wchar_t>::compare` does), `transform` copies the characters into the result and records size and capacity in 16-bit elements (inline capacity 7, heap capacity `length | 7`, `(capacity + 1) * 2` bytes including the terminator), following Dinkumware's [VC12 `basic_string`](https://github.com/ganboing/crt.vc12/blob/f1d47126bf13d1c9dccaa6c53b72217fd0474968/src/xstring#L500-L531), and `hash` mixes one whole character per round, so wide text below 0x100 hashes like its narrow spelling and a character above 0xff hashes as one value instead of the two bytes it holds. The console's `libc.prx` exports both `_Getcat` (NIDs BSVJqITGCyI and 2wz4rthdiy8) with identical 0x19d-byte bodies, believed to build the same 0x20-byte facet; the console bodies themselves are unreadable (obfuscated dynsym, and the text of the relinked `libc.prx` is zeroed), so the wide entries follow the narrow algorithm and the C locale instead of the console's wide code, and no console run checks the two. The console's `libc.prx` stores pointers to its collation state tables at offsets 0x10 and 0x18 of the facet; ours leaves them null, as only the facet's own functions read them. `do_transform` throws for input with embedded null characters, which the console drops depending on the output buffer size. The narrow result capacity (`length | 15`) and the over-aligned storage of results of 4080 characters or more follow the console's `libc.prx` code and are not checked on hardware. No module of the PPSA59322, PPSA76677 or PPSA97358 dumps in this tree imports either `_Getcat`, so neither has been exercised by a title. - [The scanf family](../../../core/libs/prx/libc/src/Formatting.cpp) (libc), the `_s` forms included, on Linux throws for the 16-bit wide conversions (`%ls`, `%lc`, `%l[`, `%S`, `%C`): glibc would store a 32-bit `wchar_t` per character into the guest's 16-bit buffer. Windows hands `%ls`, `%lc` and `%l[` to the host, whose `wchar_t` is 16 bits From f68ab450e2a793c177f786ddbe9644bb12ab03e2 Mon Sep 17 00:00:00 2001 From: T3cm0 <srodriguez7604@gmail.com> Date: Sat, 17 Oct 2026 20:44:06 -0500 Subject: [PATCH 0941/1000] feat(libc): implement wcscspn, wcstoimax, wcstoumax, strtouq, rindex and flsl Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- core/libs/prx/libc/src/Strings.cpp | 26 ++++++++++++++++++++++++++ core/libs/tests/GuestWideStrings.cpp | 23 +++++++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/core/libs/prx/libc/src/Strings.cpp b/core/libs/prx/libc/src/Strings.cpp index 1a4215cf35..225d81c4a7 100644 --- a/core/libs/prx/libc/src/Strings.cpp +++ b/core/libs/prx/libc/src/Strings.cpp @@ -152,6 +152,14 @@ char* APS5_VABI strrchr_nid_postfix(const char* s, int c) { return std::strrchr(const_cast<char*>(s), c); } +char* APS5_VABI rindex_nid_postfix(const char* s, int c) { + return strrchr_nid_postfix(s, c); +} + +int APS5_VABI flsl_nid_postfix(std::int64_t mask) { + return mask == 0 ? 0 : 64 - std::countl_zero(static_cast<std::uint64_t>(mask)); +} + char* APS5_VABI strstr_nid_postfix(const char* haystack, const char* needle) { return std::strstr(const_cast<char*>(haystack), needle); } @@ -200,6 +208,10 @@ unsigned long long APS5_VABI strtoull_nid_postfix(const char* str, char** endptr return std::strtoull(str, endptr, base); } +unsigned long long APS5_VABI strtouq_nid_postfix(const char* str, char** endptr, int base) { + return strtoull_nid_postfix(str, endptr, base); +} + double APS5_VABI strtod_nid_postfix(const char* str, char** endptr) { return ParseFloat<double>(str, endptr, std::strtod); } @@ -478,6 +490,12 @@ size_t APS5_VABI wcsspn_nid_postfix(const char16_t* s, const char16_t* accept) { return count; } +size_t APS5_VABI wcscspn_nid_postfix(const char16_t* s, const char16_t* reject) { + size_t count = 0; + while (s[count] != 0 && !Contains(reject, s[count])) ++count; + return count; +} + char16_t* APS5_VABI wcstok_nid_postfix(char16_t* text, const char16_t* delimiters, char16_t** last) { if (text == nullptr) text = *last; if (text == nullptr) return nullptr; @@ -532,6 +550,14 @@ unsigned long long APS5_VABI wcstoull_nid_postfix(const char16_t* str, char16_t* return ParseAsciiPrefix(str, endptr, [base](const char* text, char** end) { return StopAtPrefixLetter_nid_no_patch(text, end, base) ? 0ULL : std::strtoull(text, end, base); }); } +long long APS5_VABI wcstoimax_nid_postfix(const char16_t* str, char16_t** endptr, int base) { + return wcstoll_nid_postfix(str, endptr, base); +} + +unsigned long long APS5_VABI wcstoumax_nid_postfix(const char16_t* str, char16_t** endptr, int base) { + return wcstoull_nid_postfix(str, endptr, base); +} + std::uint64_t APS5_VABI _WStoul_nid_postfix(const char16_t* str, char16_t** endptr, int base) { if (base < 0 || base == 1 || base > 36) throw std::invalid_argument("_WStoul: unsupported base " + std::to_string(base)); return wcstoul_nid_postfix(str, endptr, base); diff --git a/core/libs/tests/GuestWideStrings.cpp b/core/libs/tests/GuestWideStrings.cpp index 39b16ff31e..4502e1fe81 100644 --- a/core/libs/tests/GuestWideStrings.cpp +++ b/core/libs/tests/GuestWideStrings.cpp @@ -26,6 +26,7 @@ const char16_t* APS5_VABI wcsrchr_nid_postfix(const char16_t* s, char16_t c); const char16_t* APS5_VABI wcsstr_nid_postfix(const char16_t* haystack, const char16_t* needle); const char16_t* APS5_VABI wcspbrk_nid_postfix(const char16_t* s, const char16_t* accept); std::size_t APS5_VABI wcsspn_nid_postfix(const char16_t* s, const char16_t* accept); +std::size_t APS5_VABI wcscspn_nid_postfix(const char16_t* s, const char16_t* reject); char16_t* APS5_VABI wcstok_nid_postfix(char16_t* text, const char16_t* delimiters, char16_t** last); char16_t* APS5_VABI wmemset_nid_postfix(char16_t* s, char16_t c, std::size_t n); double APS5_VABI wcstod_nid_postfix(const char16_t* str, char16_t** endptr); @@ -33,6 +34,11 @@ float APS5_VABI wcstof_nid_postfix(const char16_t* str, char16_t** endptr); long double APS5_VABI wcstold_nid_postfix(const char16_t* str, char16_t** endptr); long long APS5_VABI wcstol_nid_postfix(const char16_t* str, char16_t** endptr, int base); long long APS5_VABI wcstoll_nid_postfix(const char16_t* str, char16_t** endptr, int base); +long long APS5_VABI wcstoimax_nid_postfix(const char16_t* str, char16_t** endptr, int base); +unsigned long long APS5_VABI wcstoumax_nid_postfix(const char16_t* str, char16_t** endptr, int base); +unsigned long long APS5_VABI strtouq_nid_postfix(const char* str, char** endptr, int base); +char* APS5_VABI rindex_nid_postfix(const char* s, int c); +int APS5_VABI flsl_nid_postfix(std::int64_t mask); unsigned long long APS5_VABI wcstoul_nid_postfix(const char16_t* str, char16_t** endptr, int base); unsigned long long APS5_VABI wcstoull_nid_postfix(const char16_t* str, char16_t** endptr, int base); int APS5_VABI wcscoll_nid_postfix(const char16_t* first, const char16_t* second); @@ -118,6 +124,7 @@ int main() { require(wcspbrk_nid_postfix(haystack, u"xyz") == nullptr); require(wcsspn_nid_postfix(u"aabbc", u"ab") == 4); require(wcsspn_nid_postfix(u"abc", u"") == 0); + require(wcscspn_nid_postfix(u"abcde", u"dx") == 3 && wcscspn_nid_postfix(u"abc", u"") == 3 && wcscspn_nid_postfix(u"", u"a") == 0 && wcscspn_nid_postfix(u"abc", u"a") == 0); char16_t tokens[] = u",,one,,two;three,"; char16_t* last = tokens; @@ -238,5 +245,21 @@ int main() { require(same(transformed, test.expected, test.written)); for (std::size_t index = test.written; index < 8; ++index) require(transformed[index] == 0xaaaa); } + { + const char16_t number[] = u" -0x7fffffffffffffff9"; + char16_t* wideEnd = nullptr; + require(wcstoimax_nid_postfix(number, &wideEnd, 0) == INT64_MIN && wideEnd == number + 22); + const char16_t positive[] = u"+42z"; + require(wcstoumax_nid_postfix(positive, &wideEnd, 10) == 42 && wideEnd == positive + 3); + require(wcstoumax_nid_postfix(u"-1", nullptr, 10) == UINT64_MAX); + char* narrowEnd = nullptr; + const char text[] = "0x1ffffffffffffffffg"; + require(strtouq_nid_postfix(text, &narrowEnd, 16) == UINT64_MAX && narrowEnd == text + 19); + const char path[] = "a/b/c"; + require(rindex_nid_postfix(path, '/') == path + 3 && rindex_nid_postfix(path, 'x') == nullptr); + require(rindex_nid_postfix(path, 0) == path + 5); + require(flsl_nid_postfix(0) == 0 && flsl_nid_postfix(1) == 1 && flsl_nid_postfix(0x80) == 8); + require(flsl_nid_postfix(0x100000000ll) == 33 && flsl_nid_postfix(INT64_MIN) == 64 && flsl_nid_postfix(-1) == 64); + } return 0; } From 26898755e5c62e0da6a283fb6fccb0b2f98c1814 Mon Sep 17 00:00:00 2001 From: T3cm0 <srodriguez7604@gmail.com> Date: Sat, 17 Oct 2026 20:44:32 -0500 Subject: [PATCH 0942/1000] feat(libc): implement seed48 and lcong48 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- core/libs/prx/libc/src/Random.cpp | 28 +++++++++++++++++++++++++--- core/libs/tests/GuestRandomState.cpp | 19 +++++++++++++++++++ docs/dev/debt/libc.md | 1 + 3 files changed, 45 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libc/src/Random.cpp b/core/libs/prx/libc/src/Random.cpp index 49b48e16ca..787fe00b2b 100644 --- a/core/libs/prx/libc/src/Random.cpp +++ b/core/libs/prx/libc/src/Random.cpp @@ -96,14 +96,17 @@ void SaveTypeWordLocked() { } std::uint16_t g_rand48Seed[3]{0x330e, 0xabcd, 0x1234}; +constexpr std::uint64_t DefaultRand48Multiplier = 0x5deece66dull; +constexpr std::uint16_t DefaultRand48Addend = 0xb; +std::uint64_t g_rand48Multiplier = DefaultRand48Multiplier; +std::uint16_t g_rand48Addend = DefaultRand48Addend; +std::uint16_t g_seed48Result[3]{}; std::uint64_t NextRand48(std::uint16_t* seed, const char* function) { if (seed == nullptr) throw std::invalid_argument(std::string(function) + ": null seed"); - constexpr std::uint64_t Multiplier = 0x5deece66dull; - constexpr std::uint64_t Addend = 0xb; constexpr std::uint64_t Mask = (1ull << 48) - 1; std::uint64_t value = (static_cast<std::uint64_t>(seed[2]) << 32) | (static_cast<std::uint64_t>(seed[1]) << 16) | seed[0]; - value = (value * Multiplier + Addend) & Mask; + value = (value * g_rand48Multiplier + g_rand48Addend) & Mask; seed[0] = static_cast<std::uint16_t>(value); seed[1] = static_cast<std::uint16_t>(value >> 16); seed[2] = static_cast<std::uint16_t>(value >> 32); @@ -146,6 +149,25 @@ void APS5_VABI srand48_nid_postfix(std::int64_t seedval) { g_rand48Seed[0] = 0x330e; g_rand48Seed[1] = static_cast<std::uint16_t>(static_cast<std::uint32_t>(seedval)); g_rand48Seed[2] = static_cast<std::uint16_t>(static_cast<std::uint32_t>(seedval) >> 16); + g_rand48Multiplier = DefaultRand48Multiplier; + g_rand48Addend = DefaultRand48Addend; +} + +std::uint16_t* APS5_VABI seed48_nid_postfix(const std::uint16_t* seed) { + if (seed == nullptr) throw std::invalid_argument("seed48: null seed"); + std::lock_guard lock(g_randomLock); + for (int index = 0; index < 3; ++index) g_rand48Seed[index] = g_seed48Result[index] = seed[index]; + g_rand48Multiplier = DefaultRand48Multiplier; + g_rand48Addend = DefaultRand48Addend; + return g_seed48Result; +} + +void APS5_VABI lcong48_nid_postfix(const std::uint16_t* parameters) { + if (parameters == nullptr) throw std::invalid_argument("lcong48: null parameters"); + std::lock_guard lock(g_randomLock); + for (int index = 0; index < 3; ++index) g_rand48Seed[index] = parameters[index]; + g_rand48Multiplier = parameters[3] | (static_cast<std::uint64_t>(parameters[4]) << 16) | (static_cast<std::uint64_t>(parameters[5]) << 32); + g_rand48Addend = parameters[6]; } std::int64_t APS5_VABI random_nid_postfix() { diff --git a/core/libs/tests/GuestRandomState.cpp b/core/libs/tests/GuestRandomState.cpp index 59b336db38..6f2ab74c7d 100644 --- a/core/libs/tests/GuestRandomState.cpp +++ b/core/libs/tests/GuestRandomState.cpp @@ -23,6 +23,8 @@ double APS5_VABI drand48_nid_postfix(); std::int64_t APS5_VABI lrand48_nid_postfix(); std::int64_t APS5_VABI mrand48_nid_postfix(); void APS5_VABI srand48_nid_postfix(std::int64_t); +std::uint16_t* APS5_VABI seed48_nid_postfix(const std::uint16_t*); +void APS5_VABI lcong48_nid_postfix(const std::uint16_t*); } static int failures = 0; @@ -74,6 +76,23 @@ static void CheckRand48() { Require(mrand48_nid_postfix() == jrand48_nid_postfix(expectedSeed), "mrand48 matches jrand48 on initialized seed"); Require(drand48_nid_postfix() == erand48_nid_postfix(expectedSeed), "drand48 fourth value matches erand48"); + const std::uint16_t parameters[7] = {1, 0, 0, 3, 0, 0, 5}; + lcong48_nid_postfix(parameters); + Require(lrand48_nid_postfix() == 0 && mrand48_nid_postfix() == 0, "lcong48 sets seed, multiplier and addend"); + std::uint16_t high[3] = {0, 0, 1}; + Require(jrand48_nid_postfix(high) == 196608 && SeedIs(high, 5, 0, 3), "lcong48 multiplier scales the seed"); + std::uint16_t custom[3] = {2, 0, 0}; + Require(nrand48_nid_postfix(custom) == 0 && SeedIs(custom, 11, 0, 0), "lcong48 parameters apply to nrand48"); + const std::uint16_t newSeed[3] = {0x330e, 0xabcd, 0x1234}; + std::uint16_t* const echoed = seed48_nid_postfix(newSeed); + Require(SeedIs(echoed, 0x330e, 0xabcd, 0x1234), "seed48 returns a copy of the new seed"); + std::uint16_t again[3] = {0x330e, 0xabcd, 0x1234}; + Require(drand48_nid_postfix() == erand48_nid_postfix(again), "seed48 restores the default multiplier and addend"); + lcong48_nid_postfix(parameters); + srand48_nid_postfix(0x1234abcd); + std::uint16_t reset[3] = {0x330e, 0xabcd, 0x1234}; + Require(lrand48_nid_postfix() == nrand48_nid_postfix(reset), "srand48 restores the default multiplier and addend"); + #ifndef _WIN32 std::uint16_t guest[3] = {0x1234, 0x5678, 0x9abc}; unsigned short host[3] = {0x1234, 0x5678, 0x9abc}; diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 497130f640..4d0addbf70 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -54,6 +54,7 @@ - [realpath](../../../core/libs/prx/libc/src/RealPath.cpp) (libc) follows FreeBSD 11's [`realpath.c`](https://github.com/freebsd/freebsd-src/blob/stable/11/lib/libc/stdlib/realpath.c) except for symlinks. The console's guest filesystem has none, so a host symlink under the guest root is followed for existence and type but not expanded, and `..` drops the previous component lexically, as `Resolve` does for every guest path. Through a host link `linkdir -> sub/deep`, `linkdir/../x` gives `x` beside the link, not beside its target. - [strtod, strtof, strtold](../../../core/libs/prx/libc/src/Strings.cpp) (libc) - `NAN(n-char-sequence)` converts to the quiet NaN that `nan` and `nanf` return, negated after a minus sign, and ignores the sequence as they ignore their tag. This is inferred from C17 7.12.11.2p2, which makes `nan("n")` equivalent to `strtod("NAN(n)", NULL)`, and not observed on the console. No console `nanl` was observed, so `strtold` returns the x87 default quiet NaN (exponent `0x7fff`, significand `0xc000000000000000`) - [nan and nanf](../../../core/libs/prx/libc/src/Math.cpp) (libc) return fixed quiet NaN values (`0x7fc00000` and `0x7ff8000000000000`), ignoring the tag argument as observed in the console's `libc.prx` (both exports load a global constant via `mov rax,[rip+..]; vmovsd xmm0,[rax]; ret`); VLC (PPSA85300) stops on nanf at start-up after strsep. +- [seed48 and lcong48](../../../core/libs/prx/libc/src/Random.cpp) (libSceLibcInternal, implemented in libc) follow the console's `libSceLibcInternal.sprx` (SDK 4.5x build of `J03950001`, 0x6F460 and 0x6F420): the multiplier and addend are shared by the whole `rand48` family, including `erand48`, `nrand48` and `jrand48` on a caller's seed; `lcong48` sets them with the seed, and `srand48` and `seed48` restore 0x5DEECE66D and 0xB. Unlike FreeBSD, `seed48` returns a static copy of the new seed rather than the previous one. A null argument, which the console dereferences, throws - [lgamma and signgam](../../../core/libs/prx/libc/src/Math.cpp) (libc) - `lgamma` returns the host's `lgamma` and stores the sign of Γ(x) in the exported `signgam` (NID `5gOOC0kzW0c`, starting at 0 as in FreeBSD 11's [`s_signgam.c`](https://github.com/freebsd/freebsd-src/blob/stable/11/lib/msun/src/s_signgam.c)) by the rules of [`e_lgamma_r.c`](https://github.com/freebsd/freebsd-src/blob/stable/11/lib/msun/src/e_lgamma_r.c): -1 for -0 and for a negative non-integer whose floor is odd, 1 otherwise, including poles, infinities and NaN. Taken from the FreeBSD source, not measured on the console. The NID is in the aerolib symbol list; no title is known to read `signgam`. PPSA99004 imports `lgamma` - [sinhf](../../../core/libs/prx/libc/src/Math.cpp) (libc) follows the `sinhf` of the `libc.prx` bundled with PPSA28319 (disassembly): FreeBSD msun's [`e_sinhf.c`](https://github.com/freebsd/freebsd-src/blob/5aa554c7e5b6fd9042986df07f12ac5687e41392/lib/msun/src/e_sinhf.c) from before `__ldexp_expf`, which squares `expf(|x| / 2)` between `logf(FLT_MAX)` and the overflow threshold, calling msun's [`expm1f`](https://github.com/freebsd/freebsd-src/blob/59bc2f095289a10b8cfa55cbb2b45c3bd6c2d4ae/lib/msun/src/s_expm1f.c) and [`expf`](https://github.com/freebsd/freebsd-src/blob/59bc2f095289a10b8cfa55cbb2b45c3bd6c2d4ae/lib/msun/src/e_expf.c), whose paths for the arguments it passes are inlined here. Past the threshold Sony's error helper sets `errno` to `ERANGE` before returning `x * 1e37`; msun itself sets no `errno`. That code, run on a Windows host, gives the same bits as this implementation for every finite argument up to the threshold, of both signs; it was not run on Linux. That the system `libSceLibcInternal`, which Ruffle (PPSA68091) and RetroArch (PPSA99169) import while bundling a zero-return `libc.prx`, has the same code is assumed - [asinl](../../../core/libs/prx/libc/src/Math.cpp) (libc) follows the `asinl` of the `libc.prx` bundled with PPSA28319 (disassembly), FreeBSD msun's [`e_asinl.c`](https://github.com/freebsd/freebsd-src/blob/9f0432041b8fb69832f10c32da50a67f34feeb61/lib/msun/src/e_asinl.c) with the ld80 [`invtrig.c`](https://github.com/freebsd/freebsd-src/blob/9f0432041b8fb69832f10c32da50a67f34feeb61/lib/msun/ld80/invtrig.c) coefficients, which the binary holds as the same 80-bit constants. For |x| > 1, the infinities and NaN, Sony's error helper raises `FE_INVALID` and sets `errno` to `EDOM` before returning `(x - x) / (x - x)`, so a NaN argument also sets `EDOM`; msun itself sets no `errno`. 20 million random arguments in (-1, 1) give the same bits as that code run on a Windows host. The result needs the x87 precision control at 64 bits, as FreeBSD amd64 starts threads (control word `0x37f`), but guest threads do not set it: on Windows a thread started by `_beginthreadex` runs at 53 bits (`0x27f`), where `asinl(0.75)` returns mantissa `0xd91a98ae3406e000` instead of `0xd91a98ae3406e041`. The system `libSceLibcInternal` that RetroArch (PPSA99169) imports is assumed to have the same code From e273a65352f5f4caedde0f34e0f81394da606414 Mon Sep 17 00:00:00 2001 From: milescoviello <kitslayer29@gmail.com> Date: Sun, 18 Oct 2026 01:22:30 +0000 Subject: [PATCH 0943/1000] perf(shader): initialize the sRGB decode table once per module DecodeSrgbTexel looked up the 256 table constants and stored the whole table into its Private array at every decode site. The constants are interned, but once a module has more than the declaration cache's 512 entries, each of those lookups that misses walks every slot before it reaches the map. The table is now built on first use as the Private array's initializer, as the buffer format table already is, and later sites only index it. The sRGB image load test checks that the recompiled module holds one table, initializes one variable with it and never stores it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- .../tests/execution/ImageSrgbLoad.cpp | 24 +++++++++++++++++-- .../SpirvBackend/src/SpirvImageEmitter.cpp | 16 +++++++++---- 2 files changed, 33 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageSrgbLoad.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageSrgbLoad.cpp index ec759a3f7c..eafabe0d7c 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ImageSrgbLoad.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageSrgbLoad.cpp @@ -156,7 +156,26 @@ void Fill(std::uint32_t format) { } } -void Run(AgcDriver::VulkanDevice& device, std::span<const std::uint32_t> code, const void* texels, std::uint32_t format, std::uint32_t swizzle, std::uint32_t groups, bool useCache = false) { +void RequireInitializedTable(std::span<const std::uint32_t> words, const std::string& what) { + std::vector<std::uint32_t> tables; + std::size_t stores = 0; + std::size_t initialized = 0; + for (std::size_t at = 5; at < words.size();) { + const auto count = words[at] >> 16u; + const auto opcode = words[at] & 0xffffu; + Require(count != 0u && at + count <= words.size(), what + ": malformed SPIR-V"); + const auto operands = words.subspan(at + 1u, count - 1u); + const auto table = [&](std::uint32_t id) { return std::find(tables.begin(), tables.end(), id) != tables.end(); }; + if (opcode == 44u && operands.size() == 2u + 256u) tables.push_back(operands[1]); + if (opcode == 59u && operands.size() == 4u && table(operands[3])) ++initialized; + if (opcode == 62u && operands.size() >= 2u && table(operands[1])) ++stores; + at += count; + } + Require(tables.size() == 1u, what + ": the shader has " + std::to_string(tables.size()) + " sRGB tables, expected 1"); + Require(stores == 0u && initialized == 1u, what + ": the sRGB table is stored " + std::to_string(stores) + " times and initializes " + std::to_string(initialized) + " variables, expected no store and one variable"); +} + +std::vector<std::uint32_t> Run(AgcDriver::VulkanDevice& device, std::span<const std::uint32_t> code, const void* texels, std::uint32_t format, std::uint32_t swizzle, std::uint32_t groups, bool useCache = false) { Output.fill(0xdeadbeefu); std::vector<std::uint32_t> userData(24, 0u); const auto output = BufferDescriptor(Output.data(), static_cast<std::uint32_t>(Output.size() * 4u)); @@ -176,6 +195,7 @@ void Run(AgcDriver::VulkanDevice& device, std::span<const std::uint32_t> code, c const auto result = ShaderRecompiler::Recompile(request); device.Dispatch(result, groups, 1, 1, {}, reinterpret_cast<std::uintptr_t>(code.data())); device.WaitIdle(); + return result.spirv.Copy(); } void Verify(std::uint32_t dmask, std::uint32_t format, std::uint32_t swizzle, const std::string& what) { @@ -194,7 +214,7 @@ void Verify(std::uint32_t dmask, std::uint32_t format, std::uint32_t swizzle, co void CheckLoad(AgcDriver::VulkanDevice& device, std::span<const std::uint32_t> code, std::uint32_t dmask, std::uint32_t format, std::uint32_t swizzle, const std::string& what) { Fill(format); - Run(device, code, Texels.data(), format, swizzle, Groups); + RequireInitializedTable(Run(device, code, Texels.data(), format, swizzle, Groups), what); Verify(dmask, format, swizzle, what); } diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp index 6b9f77a57e..c6d59e1b90 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp @@ -391,13 +391,19 @@ constexpr std::array<std::uint32_t, 256> SrgbToLinear{ 0x3f700000u, 0x3f730000u, 0x3f750000u, 0x3f770000u, 0x3f790000u, 0x3f7b0000u, 0x3f7e0000u, 0x3f800000u, }; -std::uint32_t DecodeSrgbTexel(SpirvEmitterState& state, std::uint32_t color) { - const auto pointerType = TypePointer(state, spv::StorageClassPrivate, TypeU32(state)); +std::uint32_t SrgbTable(SpirvEmitterState& state) { + if (state.srgbTableVariable != 0u) return state.srgbTableVariable; std::array<std::uint32_t, SrgbToLinear.size()> words{}; for (std::size_t index = 0; index < words.size(); ++index) words[index] = ConstantU32(state, SrgbToLinear[index]); const auto arrayType = state.module.Type(spv::OpTypeArray, TypeU32(state), ConstantU32(state, static_cast<std::uint32_t>(words.size()))); - if (state.srgbTableVariable == 0) state.srgbTableVariable = state.module.DefineGlobalVariable(TypePointer(state, spv::StorageClassPrivate, arrayType), spv::StorageClassPrivate); - state.module.AddFunction(spv::OpStore, state.srgbTableVariable, state.module.Constant(spv::OpConstantComposite, arrayType, std::span<const std::uint32_t>(words))); + const auto table = state.module.Constant(spv::OpConstantComposite, arrayType, std::span<const std::uint32_t>(words)); + state.srgbTableVariable = state.module.DefineInitializedGlobalVariable(TypePointer(state, spv::StorageClassPrivate, arrayType), spv::StorageClassPrivate, table); + return state.srgbTableVariable; +} + +std::uint32_t DecodeSrgbTexel(SpirvEmitterState& state, std::uint32_t color) { + const auto pointerType = TypePointer(state, spv::StorageClassPrivate, TypeU32(state)); + const auto table = SrgbTable(state); std::uint32_t components[4] = {}; for (std::uint32_t index = 0; index < 4u; index++) { const auto value = state.module.AllocateId(); @@ -406,7 +412,7 @@ std::uint32_t DecodeSrgbTexel(SpirvEmitterState& state, std::uint32_t color) { const auto code = state.module.AllocateId(); state.module.AddFunction(spv::OpExtInst, TypeU32(state), code, GlslStd450(state), GLSLstd450UMin, Unary(state, spv::OpConvertFToU, TypeU32(state), scaled), ConstantU32(state, 255u)); const auto pointer = state.module.AllocateId(); - state.module.AddFunction(spv::OpAccessChain, pointerType, pointer, state.srgbTableVariable, code); + state.module.AddFunction(spv::OpAccessChain, pointerType, pointer, table, code); const auto bits = state.module.AllocateId(); state.module.AddFunction(spv::OpLoad, TypeU32(state), bits, pointer); components[index] = Unary(state, spv::OpBitcast, TypeF32(state), bits); From 7f9427a7636a23cad65a6bf3528420e911ad695d Mon Sep 17 00:00:00 2001 From: SandstromPL <paritoshlahre@gmail.com> Date: Fri, 16 Oct 2026 08:40:11 +0530 Subject: [PATCH 0944/1000] fix(test): sample the anisotropic min/max rows without a mip filter The two anisotropic rows of agc_driver_image_sample_reduction sampled a two-level texture at explicit LOD 0.25 and 0.75 through a point mip filter, and expected only the nearest level in the min or max. Vulkan leaves the anisotropic filtering scheme to the implementation, and only recommends that it consider mipmapMode, minLod and maxLod. On an RTX 4090 (NVIDIA 616.92) the min row returns 5, a level-1 texel, where level 0's bilinear minimum is 17, so the test fails on main there. The rows now use no mip filter, which the decoder turns into a host sampler with minLod and maxLod 0, so they read level 0 only, as the RDNA2 measurements without derivatives did (gfx1030 with no mip filter over a single-level texture, gfx1035 with image_sample_lz). They still throw if the min or max reduction with anisotropy is refused again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- .../libSceAgcDriver/tests/execution/ImageSampleReduction.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleReduction.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleReduction.cpp index a6536f8c9a..e2f8d2dc66 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleReduction.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleReduction.cpp @@ -304,8 +304,8 @@ int main() { Run(*device, {ReductionMax, FilterPoint, MipPoint}, "max, point, point mip"); Run(*device, {ReductionMin, FilterBilinear, MipNone}, "min, bilinear, no mip"); Run(*device, {ReductionMax, FilterPoint, MipNone}, "max, point, no mip"); - Run(*device, {ReductionMin, FilterAnisoBilinear, MipPoint, Aniso2x}, "min, 2x anisotropic bilinear, point mip"); - Run(*device, {ReductionMax, FilterAnisoBilinear, MipPoint, Aniso2x}, "max, 2x anisotropic bilinear, point mip"); + Run(*device, {ReductionMin, FilterAnisoBilinear, MipNone, Aniso2x}, "min, 2x anisotropic bilinear, no mip"); + Run(*device, {ReductionMax, FilterAnisoBilinear, MipNone, Aniso2x}, "max, 2x anisotropic bilinear, no mip"); Gather(*device, {ReductionMin, FilterBilinear, MipPoint}, "gather4_lz, min"); Gather(*device, {ReductionMax, FilterBilinear, MipPoint}, "gather4_lz, max"); RunWithColors(*device, loaded, LoadedCode, false, "min 32_FLOAT, image_load 8_8_8_8"); From 991df771fc7e210f5eaac61810417ae051e34bfd Mon Sep 17 00:00:00 2001 From: pushkarreddyy <212133856+pushkarreddyy@users.noreply.github.com> Date: Sat, 17 Oct 2026 21:26:09 +0530 Subject: [PATCH 0945/1000] feat(libSceAgcDriver): support CMASK fast clears on base mip 0 of mipmapped 2D targets --- .../prx/libSceAgcDriver/Graphics/src/Draw.cpp | 2 +- .../libSceAgcDriver/Graphics/src/State.cpp | 36 ++++++++++-- .../prx/libSceAgcDriver/tests/Graphics.cpp | 58 ++++++++++++++++--- docs/dev/debt/libSceAgcDriver.md | 2 +- 4 files changed, 84 insertions(+), 14 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index d23793371f..92ecc8e112 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -289,7 +289,7 @@ bool materializeCmaskClear(const Context& context, const ColorTarget& color, con Require(color.dccAddress == 0, "CMASK fast clear of a DCC color target, whose clear belongs in its DCC keys"); const auto texel = clearTexel(color, DccKeys::ClearRegister); const char* refusal = nullptr; - const bool filled = state == CmaskState::Cleared && resident != nullptr && clearToTexel(*resident, texel, color.elementBytes, refusal); + const bool filled = state == CmaskState::Cleared && color.mipCount == 1 && resident != nullptr && clearToTexel(*resident, texel, color.elementBytes, refusal); if (!filled) { storeCmaskClearTexels(color, layout, state == CmaskState::Cleared ? nullptr : &cleared, texel); if (resident != nullptr) resident->Refresh(); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index 77b8c2595b..66c444520c 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -1528,13 +1528,19 @@ ColorTarget DecodeColorBuffer(const Registers& cx, std::uint32_t slot, bool allo color.surfaceExtent = color.extent; color.mipCount = maxMip + 1u; color.mip = viewMip; + std::uint32_t firstTailLevel = maxMip + 1u; if (maxMip != 0) { - // A mipmapped surface is addressed like a texture; the view renders into one mip of it. const auto mips = ComputeElementMipLayout(ColorTextureTileMode(color.tileMode), color.elementBytes, color.extent.width, color.extent.height, maxMip + 1u); const auto& mip = mips.at(viewMip); mipOffset = mip.tiledOffset; color.mipTail = mip.tail; color.extent = {mip.width, mip.height}; + for (std::uint32_t level = 0; level < mips.size(); ++level) { + if (mips[level].tail) { + firstTailLevel = level; + break; + } + } } const ColorTargetLayout colorLayout(color.extent.width, color.extent.height, color.tileMode, color.elementBytes); const auto high = read(cx, 0x390 + slot); @@ -1571,16 +1577,36 @@ ColorTarget DecodeColorBuffer(const Registers& cx, std::uint32_t slot, bool allo color.clearWords[word] = clear == cx.end() ? 0u : clear->second; } if (((info & 0x2000u) != 0 || (color.samples > 1 && (info & 0x4000u) != 0)) && (color.samples > 1 || viewMip == 0)) { - Require(color.samples > 1 || (maxMip == 0 && !volume && slice == 0), "CMASK fast clears of a mipmapped, 3D or array color target are unsupported"); + Require(color.samples > 1 || (!volume && slice == 0 && color.arrayLastSlice == 0), "CMASK fast clears of a 3D or array color target are unsupported"); Require(((attrib3 >> 19u) & 0x1fu) == 0x18u && (attrib3 & 0x4000000u) != 0, "CMASK fast clears need pipe-aligned SW_64KB_Z_X metadata (CB_COLOR_ATTRIB3 FMASK_SW_MODE 24, CMASK_PIPE_ALIGNED)"); - if (color.samples == 1) Require(color.elementBytes <= 8, "CMASK fast clears of texels over 64 bits are unsupported"); + if (color.samples == 1) { + Require(color.elementBytes <= 8, "CMASK fast clears of texels over 64 bits are unsupported"); + if (maxMip != 0) Require(color.elementBytes > 2, "CMASK fast clears of mipmapped color targets with 8 or 16 bpp are unsupported"); + } const auto cmaskHigh = find(cx, 0x398 + slot); const auto cmask = ((cmaskHigh == cx.end() ? 0ull : static_cast<std::uint64_t>(cmaskHigh->second & 0xffu)) << 40u) | (static_cast<std::uint64_t>(read(cx, 0x31f + stride)) << 8u); Require((info & 0x2000u) == 0 || cmask != 0, "CMASK fast clears without a CMASK address are unsupported"); if (cmask != 0) { - color.cmaskAddress = cmask; + std::size_t precedingBlocks = 0; + std::size_t totalBlocks = static_cast<std::size_t>((color.surfaceExtent.width + 1023u) / 1024u) * ((color.surfaceExtent.height + 511u) / 512u); + if (maxMip != 0) { + if (firstTailLevel > 0) { + precedingBlocks = firstTailLevel < color.mipCount ? 1u : 0u; + for (auto level = firstTailLevel; level-- > 1u;) { + const auto w = std::max((color.surfaceExtent.width + (1u << level) - 1u) >> level, 1u); + const auto h = std::max((color.surfaceExtent.height + (1u << level) - 1u) >> level, 1u); + precedingBlocks += static_cast<std::size_t>((w + 1023u) / 1024u) * ((h + 511u) / 512u); + } + totalBlocks += precedingBlocks; + } else { + totalBlocks = 1u; + } + } + const auto cmaskMip0Offset = precedingBlocks * CmaskLayout::Alignment; + const auto totalCmaskBytes = totalBlocks * CmaskLayout::Alignment; + color.cmaskAddress = cmask + cmaskMip0Offset; color.cmaskBytes = CmaskBytes(color.extent.width, color.extent.height); - if (color.samples == 1) GuestMemory::CheckRange(reinterpret_cast<const void*>(color.cmaskAddress), color.cmaskBytes, CmaskLayout::Alignment, true); + if (color.samples == 1) GuestMemory::CheckRange(reinterpret_cast<const void*>(cmask), totalCmaskBytes, CmaskLayout::Alignment, true); } color.cmaskFastClear = (info & 0x2000u) != 0; } diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 720aa42f40..845c26c5e9 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -2569,6 +2569,7 @@ alignas(4096) std::array<std::uint8_t, 4096> cmaskMemory{}; void cmaskTests() { using AgcDriver::Graphics::DecodeColorBuffer; + using AgcDriver::Graphics::CmaskLayout; auto queue = makeState(); queue.context[0x31c] |= 0x2000; const auto cmaskAddress = reinterpret_cast<std::uintptr_t>(cmaskMemory.data()); @@ -2589,16 +2590,59 @@ void cmaskTests() { Require(DecodeColorBuffer(makeState().context, 0).cmaskAddress == 0, "a target without FAST_CLEAR got a CMASK"); using AgcDriver::Graphics::CmaskBytes; Require(CmaskBytes(1920, 1080) == 0x6000 && CmaskBytes(960, 544) == 0x2000 && CmaskBytes(800, 450) == 0x1000 && CmaskBytes(1024, 512) == 0x1000 && CmaskBytes(3840, 2160) == 0x14000, "a CMASK does not span whole 1024x512 metablocks"); - auto mipmapped = queue; - mipmapped.context[0x3b0] |= 1u << 28u; - expectFailure([&] { DecodeColorBuffer(mipmapped.context, 0); }, "mipmapped, 3D or array"); constexpr std::size_t blockBytes = 65536u; static std::vector<std::byte> mipStorage(16u * blockBytes); const auto mipBase = (reinterpret_cast<std::uintptr_t>(mipStorage.data()) + blockBytes - 1u) / blockBytes * blockBytes; - auto higherMip = queue; - higherMip.context[0x318] = static_cast<std::uint32_t>(mipBase >> 8u); - higherMip.context[0x390] = static_cast<std::uint32_t>(mipBase >> 40u); - higherMip.context[0x3b0] = (4u << 28u) | (255u << 14u) | 127u; + auto tailMipmapped = queue; + tailMipmapped.context[0x3b8] = 0x0dc6c000u; + tailMipmapped.context[0x318] = static_cast<std::uint32_t>(mipBase >> 8u); + tailMipmapped.context[0x390] = static_cast<std::uint32_t>(mipBase >> 40u); + tailMipmapped.context[0x3b0] |= 1u << 28u; + const auto tailBaseMipTarget = DecodeColorBuffer(tailMipmapped.context, 0); + Require(tailBaseMipTarget.cmaskFastClear && tailBaseMipTarget.cmaskAddress == cmaskAddress && tailBaseMipTarget.cmaskBytes == cmaskMemory.size(), "tail-bound mip 0 of a mipmapped fast-clear target did not decode at CMASK base"); + auto arrayTarget = queue; + arrayTarget.context[0x3b8] |= 1u; + expectFailure([&] { DecodeColorBuffer(arrayTarget.context, 0); }, "3D or array"); + auto volumeTarget = queue; + volumeTarget.context[0x3b8] = (queue.context.at(0x3b8) & ~0x03000000u) | 0x02000000u; + expectFailure([&] { DecodeColorBuffer(volumeTarget.context, 0); }, "3D or array"); + static std::vector<std::byte> cmaskChainStorage(16u * CmaskLayout::Alignment); + const auto cmaskChainBase = (reinterpret_cast<std::uintptr_t>(cmaskChainStorage.data()) + CmaskLayout::Alignment - 1u) / CmaskLayout::Alignment * CmaskLayout::Alignment; + auto mipmapped = queue; + mipmapped.context[0x3b8] = 0x0dc6c000u; + mipmapped.context[0x318] = static_cast<std::uint32_t>(mipBase >> 8u); + mipmapped.context[0x390] = static_cast<std::uint32_t>(mipBase >> 40u); + mipmapped.context[0x31f] = static_cast<std::uint32_t>(cmaskChainBase >> 8u); + mipmapped.context[0x398] = static_cast<std::uint32_t>(cmaskChainBase >> 40u); + mipmapped.context[0x3b0] = (4u << 28u) | (255u << 14u) | 127u; + const auto baseMipTarget = DecodeColorBuffer(mipmapped.context, 0); + Require(baseMipTarget.cmaskFastClear && baseMipTarget.cmaskAddress == cmaskChainBase + 0x2000 && baseMipTarget.cmaskBytes == 4096, "base mip 0 of a mipmapped fast-clear target did not match addrlib CMASK offset"); + static std::vector<std::byte> largeMipStorage(50331648u + blockBytes); + const auto largeMipBase = (reinterpret_cast<std::uintptr_t>(largeMipStorage.data()) + blockBytes - 1u) / blockBytes * blockBytes; + static std::vector<std::byte> largeCmaskChainStorage(26u * CmaskLayout::Alignment); + const auto largeCmaskChainBase = (reinterpret_cast<std::uintptr_t>(largeCmaskChainStorage.data()) + CmaskLayout::Alignment - 1u) / CmaskLayout::Alignment * CmaskLayout::Alignment; + auto largeMipmapped = queue; + largeMipmapped.context[0x3b8] = 0x0dc6c000u; + largeMipmapped.context[0x318] = static_cast<std::uint32_t>(largeMipBase >> 8u); + largeMipmapped.context[0x390] = static_cast<std::uint32_t>(largeMipBase >> 40u); + largeMipmapped.context[0x31f] = static_cast<std::uint32_t>(largeCmaskChainBase >> 8u); + largeMipmapped.context[0x398] = static_cast<std::uint32_t>(largeCmaskChainBase >> 40u); + largeMipmapped.context[0x3b0] = (7u << 28u) | (4095u << 14u) | 2047u; + const auto largeBaseMipTarget = DecodeColorBuffer(largeMipmapped.context, 0); + Require(largeBaseMipTarget.cmaskFastClear && largeBaseMipTarget.cmaskAddress == largeCmaskChainBase + 0x9000 && largeBaseMipTarget.cmaskBytes == 16u * CmaskLayout::Alignment, "multiblock base mip 0 did not match addrlib CMASK offset"); + auto nonPowerOfTwo = queue; + nonPowerOfTwo.context[0x3b8] = 0x0dc6c000u; + nonPowerOfTwo.context[0x318] = static_cast<std::uint32_t>(largeMipBase >> 8u); + nonPowerOfTwo.context[0x390] = static_cast<std::uint32_t>(largeMipBase >> 40u); + nonPowerOfTwo.context[0x31f] = static_cast<std::uint32_t>(largeCmaskChainBase >> 8u); + nonPowerOfTwo.context[0x398] = static_cast<std::uint32_t>(largeCmaskChainBase >> 40u); + nonPowerOfTwo.context[0x3b0] = (1u << 28u) | (2048u << 14u) | 1023u; + const auto nonPowerOfTwoTarget = DecodeColorBuffer(nonPowerOfTwo.context, 0); + Require(nonPowerOfTwoTarget.cmaskFastClear && nonPowerOfTwoTarget.cmaskAddress == largeCmaskChainBase + 0x2000 && nonPowerOfTwoTarget.cmaskBytes == 6u * CmaskLayout::Alignment, "non-power-of-two base mip 0 did not round up mip dimensions"); + auto smallBppMipmapped = mipmapped; + smallBppMipmapped.context[0x31c] = (mipmapped.context[0x31c] & ~0x7cu) | 4u; + expectFailure([&] { DecodeColorBuffer(smallBppMipmapped.context, 0); }, "8 or 16 bpp"); + auto higherMip = mipmapped; higherMip.context[0x31b] = 1u << 26u; const auto higherTarget = DecodeColorBuffer(higherMip.context, 0); Require(!higherTarget.cmaskFastClear && higherTarget.cmaskAddress == 0, "a higher mip of a fast-clear target decoded a CMASK"); diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index d456a047e5..779722631d 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -111,7 +111,7 @@ - [DB_SHADER_CONTROL.DEPTH_BEFORE_SHADER](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp) (libSceAgcDriver) becomes `EarlyFragmentTests`, and so does `Z_ORDER` `EARLY_Z_THEN_LATE_Z` or `EARLY_Z_THEN_RE_Z` without kill, Z export, mask export or stencil export; with alpha-to-coverage the tests stay late. The meaning of the bit and of `Z_ORDER` 3 follows Mesa (`src/amd/registers/gfx10.json`; RADV sets the bit for `early_fragment_tests` in `src/amd/vulkan/radv_shader.c`). What the console does with `DEPTH_BEFORE_SHADER` and a Z export, a mask export, a stencil export or alpha-to-coverage together was not measured. - [PA_SC_CONSERVATIVE_RASTERIZATION_CNTL](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - 0x6001 uses Vulkan overestimation on devices that add at most 1/256 pixel and keep degenerate triangles; the console's uncertainty region and degenerate handling are unmeasured. A value with `OVER_RAST_ENABLE` and `UNDER_RAST_ENABLE` clear is decoded as disabled even when `UNCERTAINTY_REGION_MODE` or the `OUTER_`/`INNER_UNCERTAINTY_EDGERULE_OVERRIDE` bits (19:16) are set, as PPSA01474 writes 0xe6000; that these fields have no effect without over- or underestimation is assumed from their names, not measured. - [Descriptor pool chain](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - each pool holds two storage buffer descriptors more than a set can take, as a workaround: RADV (Mesa 26.0 and later) lets a `FREE_DESCRIPTOR_SET` pool place a set up to 32 bytes past the pool's buffer ([`util_vma_heap_init`](https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/src/amd/vulkan/radv_descriptor_pool.c) is given `bo_size + RADV_POOL_HEAP_OFFSET` as a size), and with a page-multiple pool size those bytes are an unmapped page -- [CMASK fast clears](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver) - each fast-cleared CMASK tile (code 0) is stored as `CB_COLOR_CLEAR_WORD` into its 8x8 texels at the first draw into the target or at its fast-clear eliminate or DCC decompress pass, not as the color block writes the tile, and the CMASK is then stored as expanded (0xF): a texture or copy read of the target after that draw and before the title's eliminate sees the clear value in tiles no draw wrote, where the console keeps their previous bytes. Tile addresses follow the pipe-aligned SW_64KB_Z_X CMASK equation for 16 pipes; other CMASK swizzle modes, a cleared tile of a DCC target and a fast clear of a mipmapped (base mip 0), 3D or array color target throw. A tile with a code other than 0 and 0xF keeps its texels like an expanded tile: PPSA18391 (engine: Clickteam Fusion) fast clears a target whose CMASK it never initialized, so some of its nibbles hold leftover values such as 1 and 10; what the console does with such codes on a single-sample target is unverified on hardware. Single-sample views with `viewMip > 0` skip CMASK fast-clear decoding since CMASK metadata only covers base mip 0 on AMD hardware (Mesa `si_state.c` similarly clears `FAST_CLEAR` for level > 0). +- [CMASK fast clears](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver) - each fast-cleared CMASK tile (code 0) is stored as `CB_COLOR_CLEAR_WORD` into its 8x8 texels at the first draw into the target or at its fast-clear eliminate or DCC decompress pass, not as the color block writes the tile, and the CMASK is then stored as expanded (0xF): a texture or copy read of the target after that draw and before the title's eliminate sees the clear value in tiles no draw wrote, where the console keeps their previous bytes. Tile addresses follow the pipe-aligned SW_64KB_Z_X CMASK equation for 16 pipes; other CMASK swizzle modes, a cleared tile of a DCC target and a fast clear of a 3D or array color target throw; base mip 0 of a 2D mipmapped color target is supported (offset follows AMD Addrlib `Gfx10Lib::HwlComputeCmaskInfo` reverse mip-chain ordering preceding base mip 0, unverified on PS5 hardware; mipmapped targets with 8 or 16 bpp throw until `dsMipmapHtileFix` mip-tail shrinkage is modelled). A tile with a code other than 0 and 0xF keeps its texels like an expanded tile: PPSA18391 (engine: Clickteam Fusion) fast clears a target whose CMASK it never initialized, so some of its nibbles hold leftover values such as 1 and 10; what the console does with such codes on a single-sample target is unverified on hardware. Single-sample views with `viewMip > 0` skip CMASK fast-clear decoding since CMASK metadata only covers base mip 0 on AMD hardware (Mesa `si_state.c` similarly clears `FAST_CLEAR` for level > 0). - [HTILE depth fast clears](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver) - HTILE is not modeled. Only RADV's `clear_htile_mask` compute kernel (17 GFX10 words, 64x1x1 groups) is recognized, and only when it leaves ZMASK at 0 (`user[4]` and `user[5]` low nibbles both 0); the dispatch still runs, and the host depth image is cleared to `DB_DEPTH_CLEAR` the next time the surface with that `DB_HTILE_DATA_BASE` address is bound. The address match is exact, so a clear of a different HTILE range does not hit. HTILE initialization, the stencil-only update, and fast clears made by the SDK's own paths, CP DMA fills or other kernels are not covered and leave the depth image as it was. The kernel words are the ones the PR author saw RADV submit; they are not compared with Mesa's source. A second kernel, the 9-word dword fill PPSA01802 (Kena: Bridge of Spirits) clears its depth HTILE with (64x1x1 groups, one `buffer_store_format_x` per thread through the stride-4 V# in `user[0..3]`, the value read by `s_buffer_load_dword` through the V# in `user[4..7]`), is taken as a fill of that range when the store format is 32_UINT, 32_SINT or 32_FLOAT and the value V# covers the dword (NUM_RECORDS times its stride, or NUM_RECORDS bytes when the stride is 0) ([`MatchMemoryValueFill`](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp), then the whole-HTILE and ZMask rules of `NoteDepthMetadataFill`). The value is read from guest memory when the dispatch is decoded, so a value that earlier GPU work in the same submission has not written back yet is not seen. When the [fill HLE](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferFill.cpp) takes the dispatch instead (an output descriptor of one 32-bit value per element, a value descriptor that covers the loaded dword, a 16-byte aligned range), the value is read again when the fill is recorded, after the labels queued before it, and the HTILE fill is noted with that value. - [HTILE written by other kernels](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver) - when a depth target is bound and its HTILE was written since it was last read, the words over its whole pipe-aligned metadata (`HtileMetadataBytes`, the extent `HtileFillCovers` takes) are read, outside the surfaces mutex since the read waits for recorded work: when every one marks the depth (ZMask 0) or the stencil (SMem 0) cleared, the surface takes that fast clear and the words are stored back as expanded (depth only: ZMask 15 over the full Z range; with stencil: ZMask 15, Z range and SMem, SR1 and SR0 3), so only the title's next clear clears it again. PPSA29141 clears its depth every frame with a compute kernel the fill hooks do not recognize (`InvokeHTileClearComputeShader` in its `ClearMRT` pass); without this its depth kept every earlier frame and moving objects left black trails. PPSA25344 (Unreal Engine 5) reads its HTILE after the depth prepass in its Nanite depth export, which takes a tile with ZMask 0 as holding the clear value; without the expanded words the export put Nanite depth over the prepass and the Nanite material pass painted over the meshes in front. The word layouts are Mesa radv's for GFX10. Metadata with some tiles cleared is not modelled, and the compression state draws leave in HTILE is not tracked, so a title reading its HTILE after a draw sees the stored expanded words. A recognized HTILE fill or clear makes the next bind read the words again. A slice other than 0 of a depth array is read at its own HTILE (`DB_HTILE_DATA_BASE` plus the slice times `HtileMetadataBytes`) but the HTILE word layout of slices 1 and up is not modelled (PPSA05684's tile masks need X8 and Y8 to take Z0 and Z1 as well, slices 4 and up unmeasured), so such a slice takes the clear only when all its words are equal and otherwise keeps the fill-time fast clears. A depth-only HTILE whose tiles all have ZMask 0 but different Min/Max Z (per-tile clear values, PPSA05684's cascades) is taken as a whole-surface clear to `DB_DEPTH_CLEAR`. A multisampled depth target, and any target whose HTILE overlaps a resident native depth guard, skips this read and store-back and goes to the native multisample depth owner, which keeps its own refusal for a fast-clear transition. - [Host imports](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver) on Linux skip ranges that are not mapped read-write, which are copied instead. The NVIDIA driver (595.91.07) refuses `VK_EXT_external_memory_host` imports of read-only or executable mappings (and shared mappings of regular files) with `VK_ERROR_OUT_OF_DEVICE_MEMORY`, after which every `vkAllocateMemory` on the device fails the same way. Read-only ranges are never imported, even on drivers that accept them. Shared mappings of regular files are not checked for. On Windows, an alias of adjacent registered ranges is refused if any page is not a shared view; a range made entirely of shared views is aliased run by run, including across sections or nonconsecutive section offsets. Adjacent registered ranges are joined into one import where they fill a whole 8 MiB aligned window; ranges of a partly filled window, and ranges larger than the window, are imported one at a time. A buffer bound across adjacent ranges that are each larger than the window, or bound exactly over adjacent ranges of any size (from the first one's start to the last one's end), joins them into one import when it is bound, and their own imports are retired; a buffer that crosses any other two imports is copied. A heap grown in pieces re-imports its joined run only when a buffer is bound exactly over all of its pieces, or when a sparse descriptor heap over it is bound: a heap a shader binds whole while only the pieces from its base are mapped is bound in place from the import of that run, its views ending at the import's end as the copy's do, once the previous bind of that address saw the same last mapped byte (a heap that grows between every two of its binds stays copied; one bound more often than it grows is joined again after each growth) and at most 64 times per joined run base; past that, with a message, it is not joined again: it is bound in place while an existing import covers its run and copied otherwise, until no registered range starts at that base or the device changes. Ranges larger than the window are taken to be registered whole and never grown in pieces: a title that grows a heap in pieces larger than 8 MiB re-imports the joined run each time a bound buffer crosses into a new piece. The 8 MiB window is a choice: unbounded runs were re-imported whole on every growth of a heap mapped in small pieces, and the NVIDIA driver (610.57.04, 615.78.08) then refused imports or lost the device. Not measured on RADV. From 8cd9c1d31057520965389071f276bf845f742fa7 Mon Sep 17 00:00:00 2001 From: milescoviello <kitslayer29@gmail.com> Date: Sun, 18 Oct 2026 02:37:39 +0000 Subject: [PATCH 0946/1000] fix(libkernel): fail sigprocmask with EINVAL for an unknown how sigprocmask and _sigprocmask threw std::invalid_argument for a how other than SIG_BLOCK, SIG_UNBLOCK or SIG_SETMASK, which ends the title. FreeBSD returns -1 with EINVAL and leaves the old mask uncopied, and ignores how when set is null; pthread_sigmask here already returns EINVAL. The check now comes first, before the previous mask is written. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- .../libs/prx/libkernel/System/src/Signals.cpp | 2 +- core/libs/tests/GuestSignalSets.cpp | 20 +++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libkernel/System/src/Signals.cpp b/core/libs/prx/libkernel/System/src/Signals.cpp index c9216736c2..36678ba196 100644 --- a/core/libs/prx/libkernel/System/src/Signals.cpp +++ b/core/libs/prx/libkernel/System/src/Signals.cpp @@ -540,6 +540,7 @@ int APS5_VABI sigaltstack_nid_postfix(const GuestStack* stack, GuestStack* previ return 0; } int APS5_VABI _sigprocmask_nid_postfix(int how, const GuestSignalSet* set, GuestSignalSet* previousSet) { + if (set != nullptr && (how < 1 || how > 3)) { *__error_nid_postfix() = 22; return -1; } { std::lock_guard lock(registration); if (previousSet != nullptr) { @@ -560,7 +561,6 @@ int APS5_VABI _sigprocmask_nid_postfix(int how, const GuestSignalSet* set, Guest blockedMask.store(set->bits[0]); for (int word = 1; word < 4; ++word) blockedMaskExtra[word - 1].store(set->bits[word]); break; - default: throw std::invalid_argument("_sigprocmask: invalid how"); } } } diff --git a/core/libs/tests/GuestSignalSets.cpp b/core/libs/tests/GuestSignalSets.cpp index ddbea3ad02..786ddae94b 100644 --- a/core/libs/tests/GuestSignalSets.cpp +++ b/core/libs/tests/GuestSignalSets.cpp @@ -19,6 +19,7 @@ int APS5_VABI sigdelset_nid_postfix(GuestSignalSet*, int); int APS5_VABI sigismember_nid_postfix(const GuestSignalSet*, int); int* APS5_VABI __error_nid_postfix(); int APS5_VABI _sigprocmask_nid_postfix(int, const GuestSignalSet*, GuestSignalSet*); +int APS5_VABI sigprocmask_nid_postfix(int, const void*, void*); [[gnu::returns_twice]] int APS5_VABI sigsetjmp_nid_postfix(std::uint64_t* buffer, int saveMask); [[noreturn]] void APS5_VABI siglongjmp_nid_postfix(std::uint64_t* buffer, int value); int APS5_VABI JumpKeepingCalleeSaved(std::uint64_t* buffer); @@ -164,9 +165,28 @@ static void CheckSigsetjmp() { SetBlockedMask(0); } +static void CheckInvalidHow() { + SetBlockedMask(0x5u); + const GuestSignalSet blocked{{0x30u, 0, 0, 0}}; + for (const int how : {0, 4, -1, 1000}) { + GuestSignalSet previous{{0xa5a5a5a5u, 0xa5a5a5a5u, 0xa5a5a5a5u, 0xa5a5a5a5u}}; + *__error_nid_postfix() = 0; + Require(_sigprocmask_nid_postfix(how, &blocked, &previous) == -1 && *__error_nid_postfix() == 22); + Require(Equals(previous, 0xa5a5a5a5u, 0xa5a5a5a5u, 0xa5a5a5a5u, 0xa5a5a5a5u)); + *__error_nid_postfix() = 0; + Require(sigprocmask_nid_postfix(how, &blocked, &previous) == -1 && *__error_nid_postfix() == 22); + Require(Equals(previous, 0xa5a5a5a5u, 0xa5a5a5a5u, 0xa5a5a5a5u, 0xa5a5a5a5u)); + Require(BlockedMask() == 0x5u); + Require(_sigprocmask_nid_postfix(how, nullptr, &previous) == 0); + Require(Equals(previous, 0x5u, 0, 0, 0)); + } + SetBlockedMask(0); +} + int main() { CheckSigsetjmp(); CheckSiglongjmp(); + CheckInvalidHow(); static constexpr int Untouched = 5; GuestSignalSet set{{0x12345678u, 0x9abcdef0u, 0xffffffffu, 1u}}; *__error_nid_postfix() = Untouched; From 4ebc7d7d4352df30647a5e2b32fd71fe3ddffcf2 Mon Sep 17 00:00:00 2001 From: milescoviello <kitslayer29@gmail.com> Date: Sun, 18 Oct 2026 02:39:34 +0000 Subject: [PATCH 0947/1000] fix(libkernel): answer a join of the calling thread before its detach state scePthreadJoin checked whether the thread was detached before whether it was the calling thread. So a detached thread joining itself got EINVAL instead of EDEADLK, and so did the main thread, which scePthreadSelf adopts as detached. FreeBSD's join checks the calling thread right after the null check and before it looks at the detach state. The self check now comes first; the null, detached and already-joined answers are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- .../libs/prx/libkernel/Pthread/src/Thread.cpp | 2 +- core/libs/tests/GuestPthreadJoinSelf.cpp | 26 +++++++++++++++++++ core/libs/tests/GuestThreadSelf.cpp | 3 ++- 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libkernel/Pthread/src/Thread.cpp b/core/libs/prx/libkernel/Pthread/src/Thread.cpp index b27a79bb5e..cba281fd82 100644 --- a/core/libs/prx/libkernel/Pthread/src/Thread.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Thread.cpp @@ -830,9 +830,9 @@ int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, Pthread int APS5_VABI scePthreadJoin(Pthread thread, void** retval) { if (!thread) return SCE_KERNEL_ERROR_EINVAL; + if (thread == CurrentGuestState().current) return SCE_KERNEL_ERROR_EDEADLK; if (thread->_detached) return SCE_KERNEL_ERROR_EINVAL; if (thread->_joined.load(std::memory_order_acquire)) return SCE_KERNEL_ERROR_ESRCH; - if (thread == CurrentGuestState().current) return SCE_KERNEL_ERROR_EDEADLK; ThreadCancel::Check(); { std::unique_lock lock(thread->_join_mtx); diff --git a/core/libs/tests/GuestPthreadJoinSelf.cpp b/core/libs/tests/GuestPthreadJoinSelf.cpp index 742fa9d734..2fec26bef7 100644 --- a/core/libs/tests/GuestPthreadJoinSelf.cpp +++ b/core/libs/tests/GuestPthreadJoinSelf.cpp @@ -1,12 +1,16 @@ #include "SceTypes.hpp" +#include <atomic> +#include <chrono> #include <cstdint> #include <cstdlib> +#include <thread> extern "C" { int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, PthreadEntry entry, void* arg, const char* name); int APS5_VABI scePthreadJoin(Pthread thread, void** retval); Pthread APS5_VABI scePthreadSelf(void); int APS5_VABI pthread_join_nid_postfix(Pthread thread, void** retval); +int APS5_VABI scePthreadDetach(Pthread thread); } static constexpr int SCE_OK = 0; @@ -23,6 +27,18 @@ static void* APS5_VABI JoinSelf(void*) { return reinterpret_cast<void*>(std::uintptr_t{0x1234}); } +static std::atomic<int> detachedChecks{0}; + +static void* APS5_VABI DetachThenJoinSelf(void*) { + void* result = reinterpret_cast<void*>(std::uintptr_t{0x66}); + Require(scePthreadDetach(scePthreadSelf()) == SCE_OK); + Require(scePthreadJoin(scePthreadSelf(), &result) == SCE_KERNEL_ERROR_EDEADLK); + Require(pthread_join_nid_postfix(scePthreadSelf(), &result) == GUEST_EDEADLK); + Require(result == reinterpret_cast<void*>(std::uintptr_t{0x66})); + detachedChecks.store(1); + return nullptr; +} + int main() { Pthread thread = nullptr; void* result = nullptr; @@ -36,4 +52,14 @@ int main() { Require(scePthreadCreate(&thread, &unset, JoinSelf, nullptr, "unset-attr") == SCE_OK); Require(scePthreadJoin(thread, &result) == SCE_OK); Require(result == reinterpret_cast<void*>(std::uintptr_t{0x1234})); + + thread = nullptr; + Require(scePthreadCreate(&thread, nullptr, DetachThenJoinSelf, nullptr, "detach-join-self") == SCE_OK); + for (int wait = 0; wait < 5000 && detachedChecks.load() == 0; ++wait) std::this_thread::sleep_for(std::chrono::milliseconds(1)); + Require(detachedChecks.load() == 1); + + result = reinterpret_cast<void*>(std::uintptr_t{0x77}); + Require(scePthreadJoin(scePthreadSelf(), &result) == SCE_KERNEL_ERROR_EDEADLK); + Require(pthread_join_nid_postfix(scePthreadSelf(), &result) == GUEST_EDEADLK); + Require(result == reinterpret_cast<void*>(std::uintptr_t{0x77})); } diff --git a/core/libs/tests/GuestThreadSelf.cpp b/core/libs/tests/GuestThreadSelf.cpp index 9c59471e91..c94f557187 100644 --- a/core/libs/tests/GuestThreadSelf.cpp +++ b/core/libs/tests/GuestThreadSelf.cpp @@ -110,7 +110,8 @@ int main() { Require(pthread_getthreadid_np_nid_postfix() == static_cast<int>(mainTid)); int local = 0; Require(StackContains(mainSelf, &local)); - Require(scePthreadJoin(mainSelf, nullptr) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadJoin(mainSelf, nullptr) == SCE_KERNEL_ERROR_EDEADLK); + Require(pthread_join_nid_postfix(mainSelf, nullptr) == GUEST_EDEADLK); Require(scePthreadDetach(mainSelf) == SCE_KERNEL_ERROR_EINVAL); Require(scePthreadJoin(nullptr, nullptr) == SCE_KERNEL_ERROR_EINVAL); Require(scePthreadDetach(nullptr) == SCE_KERNEL_ERROR_EINVAL); From af0af833c112bb1c0640cf752f4e2a6700420d2a Mon Sep 17 00:00:00 2001 From: Crimson <144462348+CrimsonMac@users.noreply.github.com> Date: Sun, 18 Oct 2026 05:30:45 +0300 Subject: [PATCH 0948/1000] fix(shader): use 64-bit buffer atomics for device-memory LDS Compute LDS beyond the device's shared-memory limit lives in a storage buffer, and its 64-bit atomics took a lock in that buffer. AMD's Windows driver compiles the lock's acquire and release to s_barrier, which inside the divergent lock loop pairs with the program's own barriers, so the device-memory case of agc_driver_lds_lock_budget read half its sum. With shaderBufferInt64Atomics the device-memory LDS now uses a 64-bit view of the buffer instead: 64-bit atomics are compare-exchange loops on it and 32-bit atomics stay native, so no lock is taken. Without the feature the lock is kept. The per-workgroup slice is rounded up to an even dword count so that the 64-bit view stays aligned. --- .../libSceAgcDriver/tests/LdsLockBudget.cpp | 50 +++++++++++++------ .../tests/execution/LdsBeyondDeviceLimit.cpp | 4 +- .../tests/execution/LdsMixedWidthAtomics.cpp | 2 +- .../IrMetadata/ShaderInfo.hpp | 2 +- .../include/SpirvBackend/SpirvAnalysis.hpp | 1 + .../SpirvBackend/SpirvEmitterHelpers.hpp | 1 + .../SpirvBackend/SpirvEmitterState.hpp | 1 + .../SpirvMemory/SpirvBufferAccess.hpp | 1 + .../SpirvBackend/src/SpirvEmitter.cpp | 4 ++ .../src/SpirvMemory/SpirvBufferAccess.cpp | 9 ++++ .../src/SpirvMemory/SpirvEmitterHelpers.cpp | 8 +++ .../SpirvMemory/SpirvMemoryInstructions.cpp | 12 ++++- .../src/SpirvMemory/SpirvModuleSetup.cpp | 4 ++ docs/dev/debt/libSceAgcDriver.md | 2 +- docs/dev/debt/shader.md | 2 +- 15 files changed, 82 insertions(+), 21 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/LdsLockBudget.cpp b/core/libs/prx/libSceAgcDriver/tests/LdsLockBudget.cpp index 8901e04a35..e69551e405 100644 --- a/core/libs/prx/libSceAgcDriver/tests/LdsLockBudget.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/LdsLockBudget.cpp @@ -5,6 +5,7 @@ #include <functional> #include <iostream> #include <map> +#include <set> #include <span> #include <stdexcept> #include <string> @@ -56,12 +57,27 @@ std::uint64_t workgroupBytes(std::span<const std::uint32_t> words) { return bytes; } -RecompileResult compile(std::uint32_t waveSize, std::uint32_t limit) { +std::set<std::uint32_t> compareExchangeWidths(std::span<const std::uint32_t> words) { + std::map<std::uint32_t, std::uint32_t> widths; + std::set<std::uint32_t> found; + for (std::size_t offset = 5; offset < words.size();) { + const auto count = words[offset] >> 16u; + require(count != 0u && count <= words.size() - offset, "malformed SPIR-V instruction"); + const auto instruction = words.subspan(offset, count); + const auto opcode = static_cast<spv::Op>(instruction[0] & 0xffffu); + if (opcode == spv::OpTypeInt) widths.emplace(instruction[1], instruction[2]); + if (opcode == spv::OpAtomicCompareExchange) found.insert(widths.at(instruction[1])); + offset += count; + } + return found; +} + +RecompileResult compile(std::uint32_t waveSize, std::uint32_t limit, bool int64Atomics) { static constexpr std::array<std::uint32_t, 8> code{ 0x7e020280u, 0x7e0402c1u, 0x7e060280u, 0x7e080281u, 0xd9000000u, 0x00000201u, 0xbf8cc07fu, 0xbf810000u }; - static constexpr std::array<std::uint32_t, 7> capabilities{spv::CapabilityShader, spv::CapabilityGroupNonUniform, spv::CapabilityGroupNonUniformBallot, spv::CapabilityGroupNonUniformShuffle, spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess}; + static constexpr std::array<std::uint32_t, 8> capabilities{spv::CapabilityShader, spv::CapabilityGroupNonUniform, spv::CapabilityGroupNonUniformBallot, spv::CapabilityGroupNonUniformShuffle, spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess, spv::CapabilityInt64Atomics}; static constexpr std::array<std::string_view, 2> extensions{"SPV_KHR_physical_storage_buffer", "SPV_KHR_8bit_storage"}; static constexpr std::array<std::uint32_t, 4> userData{}; RecompileRequest request{}; @@ -73,7 +89,7 @@ RecompileResult compile(std::uint32_t waveSize, std::uint32_t limit) { request.target.spirvVersion = 0x00010600u; request.target.subgroupSize = 32; request.target.bdaAbiVersion = 1; - request.target.supportedCapabilities = capabilities; + request.target.supportedCapabilities = std::span(capabilities).first(int64Atomics ? 8u : 7u); request.target.supportedExtensions = extensions; request.target.maxWorkgroupSize = {1024u, 1024u, 64u}; request.target.maxWorkgroupInvocations = 1024; @@ -83,13 +99,17 @@ RecompileResult compile(std::uint32_t waveSize, std::uint32_t limit) { return Recompile(request); } -void verify(std::uint32_t waveSize, std::uint32_t limit) { - const auto result = compile(waveSize, limit); +void verify(std::uint32_t waveSize, std::uint32_t limit, bool int64Atomics) { + const auto result = compile(waveSize, limit, int64Atomics); require(!result.spirv.empty(), "LDS atomic program did not compile"); const auto declared = workgroupBytes(result.spirv.Words()); require(declared <= limit, "declared " + std::to_string(declared) + " Workgroup bytes exceeds limit " + std::to_string(limit)); - require(result.workgroupMemoryDwords == (limit < 68u ? 17u : 0u), "incorrect device-memory LDS stride at limit " + std::to_string(limit)); + require(result.workgroupMemoryDwords == (limit < 68u ? 18u : 0u), "incorrect device-memory LDS stride at limit " + std::to_string(limit)); if (limit >= 68u) require(declared != 0u, "in-budget LDS unexpectedly lost its Workgroup allocation"); + const auto widths = compareExchangeWidths(result.spirv.Words()); + const bool native = int64Atomics && limit < 68u; + require(widths.contains(64u) == native, native ? "device-memory LDS did not use a 64-bit compare exchange" : "a 64-bit compare exchange appeared without device-memory LDS and shaderBufferInt64Atomics"); + require(widths.contains(32u) != native, native ? "device-memory LDS with shaderBufferInt64Atomics kept the workgroup lock" : "the 64-bit LDS atomic lost its workgroup lock"); } } @@ -98,14 +118,16 @@ int main() { unsigned failures = 0; for (const auto waveSize : {32u, 64u}) { for (const auto limit : {64u, 68u, 72u}) { - const auto name = "wave" + std::to_string(waveSize) + " limit" + std::to_string(limit); - try { - verify(waveSize, limit); - std::cout << "LDS lock budget: " << name << " passed\n"; - } catch (const std::exception& error) { - ++failures; - const std::string message(error.what()); - std::cerr << "LDS lock budget: " << name << ": " << message.substr(0, message.find("RecompileRequest:")) << '\n'; + for (const bool int64Atomics : {false, true}) { + const auto name = "wave" + std::to_string(waveSize) + " limit" + std::to_string(limit) + (int64Atomics ? " int64 atomics" : ""); + try { + verify(waveSize, limit, int64Atomics); + std::cout << "LDS lock budget: " << name << " passed\n"; + } catch (const std::exception& error) { + ++failures; + const std::string message(error.what()); + std::cerr << "LDS lock budget: " << name << ": " << message.substr(0, message.find("RecompileRequest:")) << '\n'; + } } } } diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/LdsBeyondDeviceLimit.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/LdsBeyondDeviceLimit.cpp index 8b37c4591f..fb6fd2d583 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/LdsBeyondDeviceLimit.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/LdsBeyondDeviceLimit.cpp @@ -148,7 +148,7 @@ void Run(AgcDriver::VulkanDevice& device, std::uint32_t waveSize, const ShaderRe Require(within.workgroupMemoryDwords == 0u, "lds beyond the device limit " + run + ": LDS below the device limit left workgroup memory"); fits(within, "LDS below the device limit"); const auto atLimit = Compile(waveSize, limit, target); - Require(atLimit.workgroupMemoryDwords == limit / 4u + 1u, "lds beyond the device limit " + run + ": LDS at the device limit gave " + std::to_string(atLimit.workgroupMemoryDwords) + " device memory dwords per workgroup"); + Require(atLimit.workgroupMemoryDwords == limit / 4u + 2u, "lds beyond the device limit " + run + ": LDS at the device limit gave " + std::to_string(atLimit.workgroupMemoryDwords) + " device memory dwords per workgroup"); fits(atLimit, "LDS at the device limit"); const auto bytes = limit + AboveLimitBytes; for (std::uint32_t tid = 0; tid < Threads; ++tid) { @@ -159,7 +159,7 @@ void Run(AgcDriver::VulkanDevice& device, std::uint32_t waveSize, const ShaderRe } Output.fill(Untouched); const auto result = Compile(waveSize, bytes, target); - Require(result.workgroupMemoryDwords == bytes / 4u + 1u, "lds beyond the device limit " + run + ": " + std::to_string(bytes) + " bytes of LDS gave " + std::to_string(result.workgroupMemoryDwords) + " device memory dwords per workgroup"); + Require(result.workgroupMemoryDwords == bytes / 4u + 2u, "lds beyond the device limit " + run + ": " + std::to_string(bytes) + " bytes of LDS gave " + std::to_string(result.workgroupMemoryDwords) + " device memory dwords per workgroup"); fits(result, "LDS above the device limit"); if (indirect) { IndirectGroups = {groupsX, groupsY, groupsZ, 0u}; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/LdsMixedWidthAtomics.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/LdsMixedWidthAtomics.cpp index 7c35530d9f..f63631ee61 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/LdsMixedWidthAtomics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/LdsMixedWidthAtomics.cpp @@ -253,7 +253,7 @@ void LockBudget(AgcDriver::VulkanDevice& device) { if (std::getenv("ANYPS5_REQUIRE_ROBUST2") != nullptr) Require(target.robustBufferAccess2, "lds lock budget: robustBufferAccess2 was not enabled"); Require(limit >= 32u && limit % 4u == 0u, "lds lock budget: unexpected device shared-memory limit"); const auto operations = static_cast<std::uint64_t>(Threads) * Iterations; - Run(device, UniformCode, "limit", operations, 1, limit / 4u, limit / 4u + 1u); + Run(device, UniformCode, "limit", operations, 1, limit / 4u, (limit / 4u + 2u) & ~1u); Run(device, UniformCode, "limit-4", operations, 1, (limit - 4u) / 4u, 0u); std::puts("lds lock budget tests passed"); } diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ShaderInfo.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ShaderInfo.hpp index 8879b166be..1440effcb2 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ShaderInfo.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ShaderInfo.hpp @@ -42,7 +42,7 @@ struct ShaderInfo { }; [[nodiscard]] inline std::uint32_t WorkgroupMemoryStrideDwords(const ShaderInfo& info) { - return info.sharedMemoryBytes == 0u ? 0u : info.sharedMemoryBytes / 4u + 1u; + return info.sharedMemoryBytes == 0u ? 0u : (info.sharedMemoryBytes / 4u + 2u) & ~1u; } } diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvAnalysis.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvAnalysis.hpp index 1b1c53242b..ebfd8888e3 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvAnalysis.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvAnalysis.hpp @@ -19,6 +19,7 @@ struct SpirvRequirements { bool imageGatherExtended = false; bool functionLds = false; bool ldsLock = false; + bool ldsBufferInt64Atomics = false; std::uint32_t functionLdsDwords = 0; std::unordered_map<const IrValue*, std::uint32_t> functionLdsAddresses; bool functionScratch = false; diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterHelpers.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterHelpers.hpp index 836b58a04c..e2101177cf 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterHelpers.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterHelpers.hpp @@ -112,6 +112,7 @@ bool LdsInDeviceMemory(const SpirvEmitterState& state); std::uint32_t LdsMemorySemantics(const SpirvEmitterState& state); void EmitLdsBufferBase(SpirvEmitterState& state); std::uint32_t EmitLdsLockPointer(SpirvEmitterState& state); +std::uint32_t EmitLdsBufferU64Pointer(SpirvEmitterState& state, std::uint32_t index); MemoryResourceAccess PrepareMemoryResourceAccess(SpirvEmitterState& state, const MemoryInfo& mem); std::uint32_t EmitMemoryElementIndex(SpirvEmitterState& state, const MemoryResourceAccess& access, std::uint32_t rawIndex); std::uint32_t EmitMemoryElementInBounds(SpirvEmitterState& state, const MemoryResourceAccess& access, std::uint32_t index); diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp index e293dd87c7..efafc0b8b3 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp @@ -164,6 +164,7 @@ struct SpirvEmitterState { std::uint32_t flattenedSrtVariable = 0; std::uint32_t ldsVariable = 0; std::uint32_t ldsBufferVariable = 0; + std::uint32_t ldsBufferU64Variable = 0; std::uint32_t ldsBufferBase = 0; std::uint32_t numWorkgroupsVariable = 0; std::array<std::uint32_t, 2> scratchVariable {}; diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvMemory/SpirvBufferAccess.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvMemory/SpirvBufferAccess.hpp index 03bd0330d6..53a851625b 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvMemory/SpirvBufferAccess.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvMemory/SpirvBufferAccess.hpp @@ -15,6 +15,7 @@ bool LdsInDeviceMemory(const SpirvEmitterState& state); std::uint32_t LdsMemorySemantics(const SpirvEmitterState& state); void EmitLdsBufferBase(SpirvEmitterState& state); std::uint32_t EmitLdsLockPointer(SpirvEmitterState& state); +std::uint32_t EmitLdsBufferU64Pointer(SpirvEmitterState& state, std::uint32_t index); MemoryResourceAccess PrepareMemoryResourceAccess(SpirvEmitterState& state, const MemoryInfo& mem); std::uint32_t EmitMemoryElementIndex(SpirvEmitterState& state, const MemoryResourceAccess& access, std::uint32_t rawIndex); std::uint32_t EmitMemoryElementInBounds(SpirvEmitterState& state, const MemoryResourceAccess& access, std::uint32_t index); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp index f30656653d..050316a6f7 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp @@ -262,6 +262,10 @@ std::vector<std::uint32_t> SpirvEmitter::Emit(const IrProgram& program, const Sh state.float16DenormPreserve = target.float16DenormPreserve; state.robustBufferAccess2 = target.robustBufferAccess2; state.hostSubgroupSize = target.subgroupSize; + if (state.requirements.ldsLock && LdsInDeviceMemory(state) && std::find(target.supportedCapabilities.begin(), target.supportedCapabilities.end(), static_cast<std::uint32_t>(spv::CapabilityInt64Atomics)) != target.supportedCapabilities.end()) { + state.requirements.ldsLock = false; + state.requirements.ldsBufferInt64Atomics = true; + } if (HostSubgroupSingleLane(state)) { state.requirements.subgroupBallot = false; state.requirements.subgroupShuffle = false; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvBufferAccess.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvBufferAccess.cpp index d208290cac..192b60cc27 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvBufferAccess.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvBufferAccess.cpp @@ -111,6 +111,15 @@ std::uint32_t EmitLdsLockPointer(SpirvEmitterState& state) { return pointer; } +std::uint32_t EmitLdsBufferU64Pointer(SpirvEmitterState& state, std::uint32_t index) { + if (!state.requirements.ldsBufferInt64Atomics || state.ldsBufferU64Variable == 0u || state.ldsBufferBase == 0u) { + FailEmit("64-bit device memory LDS atomics were not prepared at function entry"); + } + const auto pointer = state.module.AllocateId(); + state.module.AddFunction(spv::OpAccessChain, TypePointer(state, spv::StorageClassStorageBuffer, TypeScalarU64(state)), pointer, state.ldsBufferU64Variable, ConstantU32(state, 0u), EmitBinaryU32(state, spv::OpShiftRightLogical, EmitAddU32(state, state.ldsBufferBase, index), ConstantU32(state, 1u))); + return pointer; +} + MemoryResourceAccess PrepareMemoryResourceAccess(SpirvEmitterState& state, const MemoryInfo& mem) { MemoryResourceAccess access; access.kind = mem.kind; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp index 2f44307dc3..7a704ea52d 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp @@ -518,6 +518,14 @@ void DefineDescriptors(SpirvEmitterState& state) { state.module.AddAnnotation(spv::OpDecorate, state.ldsBufferVariable, spv::DecorationDescriptorSet, WorkgroupMemoryDescriptorSet); state.module.AddAnnotation(spv::OpDecorate, state.ldsBufferVariable, spv::DecorationBinding, 0u); state.module.AddAnnotation(spv::OpDecorate, state.ldsBufferVariable, spv::DecorationCoherent); + if (state.requirements.ldsBufferInt64Atomics) { + state.ldsBufferU64Variable = state.module.DefineGlobalVariable(TypePointer(state, spv::StorageClassStorageBuffer, StorageBufferU64BlockType(state)), spv::StorageClassStorageBuffer); + state.module.AddName(state.ldsBufferU64Variable, "workgroup_memory_u64"); + state.module.AddAnnotation(spv::OpDecorate, state.ldsBufferU64Variable, spv::DecorationDescriptorSet, WorkgroupMemoryDescriptorSet); + state.module.AddAnnotation(spv::OpDecorate, state.ldsBufferU64Variable, spv::DecorationBinding, 0u); + state.module.AddAnnotation(spv::OpDecorate, state.ldsBufferU64Variable, spv::DecorationCoherent); + for (const auto variable : {state.ldsBufferVariable, state.ldsBufferU64Variable}) state.module.AddAnnotation(spv::OpDecorate, variable, spv::DecorationAliased); + } } } diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp index d561dea75c..417ce2c9de 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp @@ -761,6 +761,11 @@ std::uint32_t UpdateFunctionElement(SpirvEmitterState& state, std::uint32_t poin template<typename TUpdate> std::uint32_t LockedLdsUpdate(SpirvEmitterState& state, std::uint32_t type, std::uint32_t zero, TUpdate&& update); +template<typename TNext> +std::uint32_t LdsBufferAtomic64(SpirvEmitterState& state, std::uint32_t low, TNext&& next) { + return Unary(state, spv::OpBitcast, TypeU64(state), AtomicUpdateTyped(state, EmitLdsBufferU64Pointer(state, low), ResourceKind::Lds, TypeScalarU64(state), next)); +} + template<typename TOperation> std::uint32_t EmitLdsAtomic32(SpirvEmitterState& state, const MemoryInfo& mem, TOperation&& operation) { if (mem.kind != ResourceKind::Lds || !state.requirements.ldsLock) return operation(); @@ -1145,7 +1150,7 @@ std::uint32_t ApertureAtomic64(SpirvValueEmitContext& ctx, const IrValue& inst, const auto unaligned = Binary(state, spv::OpINotEqual, TypeBool(state), Binary(state, spv::OpBitwiseAnd, TypeU32(state), byteOffset, ConstantU32(state, 7u)), ConstantU32(state, 0u)); EmitIfCondition(state, unaligned, [&] { RecordBdaFault(state, address, ConstantU32(state, 8u), instruction, BdaAbi::FaultReason::Unaligned); }); if (kind == ResourceKind::Scratch && state.program.Info().scratchDwords == 0u) return ConstantU64(state, 0u); - if (kind == ResourceKind::Lds && !state.requirements.ldsLock) ctx.Fail(inst, "64-bit LDS atomic reached through a flat address without the workgroup lock"); + if (kind == ResourceKind::Lds && !state.requirements.ldsLock && !state.requirements.ldsBufferInt64Atomics) ctx.Fail(inst, "64-bit LDS atomic reached through a flat address without the workgroup lock"); MemoryInfo storage{}; storage.kind = kind; const auto resource = PrepareMemoryResourceAccess(state, storage); @@ -1155,6 +1160,7 @@ std::uint32_t ApertureAtomic64(SpirvValueEmitContext& ctx, const IrValue& inst, const auto opcode = inst.Opcode(); return EmitValueOrDefaultIfCondition(state, aligned, TypeU64(state), ConstantU64(state, 0u), [&] { return EmitValueOrDefaultIfCondition(state, EmitMemoryElementInBounds(state, resource, high), TypeU64(state), ConstantU64(state, 0u), [&] { + if (kind == ResourceKind::Lds && state.requirements.ldsBufferInt64Atomics) return LdsBufferAtomic64(state, low, [&](std::uint32_t old) { return FlatAtomicNext64(state, opcode, old, value, comparator); }); const auto update = [&]() { const auto lowPointer = EmitMemoryElementPointer(state, resource, low); const auto highPointer = EmitMemoryElementPointer(state, resource, high); @@ -1387,6 +1393,10 @@ std::uint32_t SharedAtomic64(SpirvValueEmitContext& ctx, const IrValue& inst, TR const auto low = EmitMemoryElementIndex(state, resource, rawIndex); const auto high = EmitMemoryElementIndex(state, resource, Binary(state, spv::OpIAdd, TypeU32(state), rawIndex, ConstantU32(state, 1u))); return EmitValueOrDefaultIfCondition(state, EmitMemoryElementInBounds(state, resource, high), TypeU64(state), ConstantU64(state, 0u), [&]() { + if (state.requirements.ldsBufferInt64Atomics) { + const auto aligned = Binary(state, spv::OpIEqual, TypeBool(state), Binary(state, spv::OpBitwiseAnd, TypeU32(state), low, ConstantU32(state, 1u)), ConstantU32(state, 0u)); + return EmitValueOrDefaultIfCondition(state, aligned, TypeU64(state), ConstantU64(state, 0u), [&]() { return LdsBufferAtomic64(state, low, [&](std::uint32_t old) { return replacement(state, old); }); }); + } const auto update = [&]() { const auto lowPointer = EmitMemoryElementPointer(state, resource, low); const auto highPointer = EmitMemoryElementPointer(state, resource, high); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp index 763f1a1fa8..ef3e1ac42d 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp @@ -265,6 +265,10 @@ void DefineModule(SpirvEmitterState& state) { if (state.requirements.sharedInt64Atomics) { state.module.EmitCapability(spv::CapabilityInt64); } + if (state.requirements.ldsBufferInt64Atomics) { + state.module.EmitCapability(spv::CapabilityInt64); + state.module.EmitCapability(spv::CapabilityInt64Atomics); + } if (state.requirements.imageInt64Atomics) { if (std::find(state.supportedCapabilities.begin(), state.supportedCapabilities.end(), static_cast<std::uint32_t>(spv::CapabilityInt64ImageEXT)) == state.supportedCapabilities.end()) { FailEmit("64-bit image atomics need VK_EXT_shader_image_atomic_int64"); diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index d456a047e5..4107aa6246 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -68,7 +68,7 @@ - [Unused pixel programs](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver): a bound pixel program is skipped when color masks have no intersection and DB export and execution enables are clear, regardless of retained SPI_SHADER_Z_FORMAT. This follows [AMD PAL DbShaderControl](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/hw/gfxip/gfx9/gfx9AbiToPipelineRegisters.h) and its [GFX10 register definitions](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/hw/gfxip/gfx9/chip/gfx9_plus_merged_registers.h), not a hardware measurement. Enabled DB_ALPHA_TO_MASK conservatively retains the program even with ALPHA_TO_MASK_DISABLE, because DecodeState currently enables Vulkan alpha-to-coverage without applying that override. Console behavior for these combinations has not been measured. - [Sample masks](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - `PA_SC_AA_MASK_X0Y0_X1Y0` and `PA_SC_AA_MASK_X0Y1_X1Y1` hold one 16-bit sample mask for each pixel of the aligned 2x2 quad ([Mesa gfx10.json](https://gitlab.freedesktop.org/mesa/mesa/-/blob/7b87f8ad226e/src/amd/registers/gfx10.json#L13512)); only the bits of the `MSAA_NUM_SAMPLES` rasterized samples count, and at one sample bit 0 of a pixel's field covers every sample of the target the draw is raised to. A mask equal for the four pixels is the Vulkan `pSampleMask`, the case [radv](https://gitlab.freedesktop.org/mesa/mesa/-/blob/7b87f8ad226e/src/amd/vulkan/radv_cmd_buffer.c#L13633) and [radeonsi](https://gitlab.freedesktop.org/mesa/mesa/-/blob/7b87f8ad226e/src/gallium/drivers/radeonsi/si_state.c#L4218) program; with early fragment tests it needs `earlyFragmentSampleMaskTestBeforeSampleCounting` ([radv](https://gitlab.freedesktop.org/mesa/mesa/-/blob/7b87f8ad226e/src/amd/vulkan/radv_physical_device.c#L2079) reports it). A mask that differs between the pixels is applied in the pixel shader without early fragment tests: a pixel whose mask covers all or none of its samples is demoted to a helper invocation by `gl_FragCoord` parity, and otherwise the pixel's mask is ANDed into `gl_SampleMask` (with alpha-to-coverage and the mask export) after demoting invocations whose `SampleMask` input it leaves empty. A pixel program with early Z that writes memory under depth, stencil or depth-bounds tests is therefore refused. That XnYm is the pixel at (x mod 2, y mod 2) = (n, m) is read from the field names; that the mask removes samples before the depth and stencil tests and occlusion counting is inferred from radv implementing the Vulkan sample mask test with it; and that it takes no part in the centroid (`AA_MASK_CENTROID_DTMN` 0) is inferred from radv setting that bit only for conservative rasterization. None of it is measured on gfx1013. A mask equal for the four pixels that removes some but not all samples of a pixel is refused with active centroid inputs, since where Vulkan puts the centroid relative to `pSampleMask` is implementation-defined, and any partial mask is refused with conservative rasterization (radv sets `PREZ_AA_MASK_ENABLE` and `POSTZ_AA_MASK_ENABLE` [there](https://gitlab.freedesktop.org/mesa/mesa/-/blob/7b87f8ad226e/src/amd/vulkan/radv_cmd_buffer.c#L13653), which 0x6001 leaves clear). - [Covered-centroid raster state](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) accepts PA_SC_AA_CONFIG.COVERED_CENTROID_IS_CENTER only when the pixel program is skipped or SPI_PS_INPUT_ENA and SPI_PS_INPUT_ADDR have no active perspective or linear centroid input. Bit 29 follows [PS5_Mesa GFX10.3 registers](https://github.com/mihawk-99/PS5_Mesa/blob/0b2d6d1a61d9bbf89cf8beb88a696144f67c61f8/src/amd/registers/gfx103.json); [RADV](https://github.com/mihawk-99/PS5_Mesa/blob/0b2d6d1a61d9bbf89cf8beb88a696144f67c61f8/src/amd/vulkan/radv_cmd_buffer.c) sets it on GFX10.3 and later. Active centroid interpolation and unspecified input registers still throw. Console covered-pixel centroid results were not measured. -- [Compute LDS beyond the device's shared-memory limit](../../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) (libSceAgcDriver) lives in a coherent storage buffer with one slice of `LDS bytes / 4 + 1` dwords per workgroup (the last dword is the atomic lock), shared by all such dispatches and grown in 1 MiB steps up to `maxStorageBufferRange`; a dispatch whose slices do not fit throws. It is slower than shared memory and its cost was not measured. An indirect dispatch of such a program always reads its group counts on the CPU. Its results are pinned against the in-limit semantics in `agc_driver_lds_beyond_device_limit`, not measured on hardware. +- [Compute LDS beyond the device's shared-memory limit](../../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) (libSceAgcDriver) lives in a coherent storage buffer with one slice of `LDS bytes / 4 + 1` dwords, rounded up to an even count so that 64-bit atomics stay 8-byte aligned, per workgroup (the dword after the LDS is the atomic lock on a device without `shaderBufferInt64Atomics`), shared by all such dispatches and grown in 1 MiB steps up to `maxStorageBufferRange`; a dispatch whose slices do not fit throws. It is slower than shared memory and its cost was not measured. An indirect dispatch of such a program always reads its group counts on the CPU. Its results are pinned against the in-limit semantics in `agc_driver_lds_beyond_device_limit`, not measured on hardware. - [AGC buffer allocations](../../../core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp) retry host/device out-of-memory once after reclaiming idle pool buffers and fully unused slab blocks. Live buffers and blocks with live slots are retained. Live images and other retained resources have no global memory bound; allocation failure still throws when reclaim cannot satisfy the request. - [Pipeline journal](../../../core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp) (libSceAgcDriver): the journal is append-only and never compacted, so a module a later run no longer builds pipelines from stays in the file until it is deleted, and the whole file is read into memory while the replay runs (its modules reference that buffer until the last replayed pipeline was built). Only the first `APS5_PIPELINE_JOURNAL_ENTRIES` pipelines recorded are replayed (a record is never rewritten, so a pipeline past the bound stays past it), and the pipeline store evicts the least recently used entry past `APS5_PIPELINE_STORE_ENTRIES` (1024 each): a title with more configurations rebuilds the rest at the draw as before. A compute pipeline is replayed over the set layout `DescribeSetLayouts` derives from the result's bindings, which a dispatch that finds it resident checks against its `ShaderResources` layout only under `APS5_VERIFY_SET_LAYOUTS=1`. Recording and replay are best effort: a record whose write fails is lost, an entry whose build fails is skipped, and both are counted in the `[pipeline-journal]` line rather than stopping the run. A replayed pipeline that the title never asks for again costs its build time and its driver memory (its optimized relink is deferred to its first draw, so a run whose first frames bind many replayed pipelines queues their relinks then); nothing ages recipes out of the file. Two runs sharing the file take turns under its lock file, but each only knows the modules it loaded or wrote itself, so a module the other run appended meanwhile may be appended again (the loader keeps the first copy). Records a run appends after another run's load are replayed by that run only after its next start. The file header's format number is the only guard against a change of the recipe codec, `CanonicalPipelineState` or the store key that keeps the structures' sizes (the recompiler's source version does not cover the driver's sources). - [Graphics pipeline libraries](../../../core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp) (libSceAgcDriver): a pipeline is fast-linked at the draw and relinked with `VK_PIPELINE_CREATE_LINK_TIME_OPTIMIZATION_BIT_EXT` on a background thread, and binds the optimized one from the next draw on. Draws recorded before that use the fast link, whose vertex stage keeps the outputs the fragment stage does not read: on RADV (Radeon 660M, Mesa 26.2.4) 346 of 483 Astro's Playroom vertex shaders paired with a fragment stage that reads none of them used more VGPRs (median 8 more, up to 2.5 times), and on an RTX 4090 13 of 15 PPSA36082 vertex shaders up to 68 instead of 38 (#2242). The optimized link compiles to the same code as a whole pipeline diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index c567f99417..3f58b62d0a 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -129,7 +129,7 @@ - [Negative color-texture comparison wrap indices](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) (libSceAgcDriver) use explicit `OpSRem` normalization to implement modulo; the negative-offset point-wrap regression fails with direct `OpSMod` and passes with normalization on Mesa 22.3.6 llvmpipe (LLVM 15.0.6), but negative-index wrap has not been measured on PS5 hardware. - [Converted unorm images](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) (10_11_11_UNORM, read and written through an R32_UINT view): samples, gathers, LOD queries, 16-bit data and a DST_SEL of the missing W channel throw. - [Converted unorm image loads](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) divide by 2^n - 1 with the buffer format path's `OpFDiv`, which Vulkan allows to be up to 2.5 ULP off, so some drivers are not bit-exact. -- [64-bit LDS atomics](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (`ds_*_u64`, `ds_*_b64`, `ds_*_f64`) read and write the two dwords under a workgroup lock stored after the guest LDS, as LDS is a 32-bit array without 64-bit atomics. On RDNA2 (gfx1036) the LDS serialises a 32-bit atomic on a slot's high dword and a 64-bit atomic on that slot: 128 lanes of `ds_add_u32` and 128 lanes of `ds_add_u64` on one slot sum exactly. Only that pair was measured. Every 32-bit LDS atomic of a program that contains a 64-bit LDS atomic therefore takes the same lock, including a 32-bit `flat_atomic_*` whose address lands in the LDS aperture (a 64-bit one takes it already); the flat forms were not measured against a 64-bit atomic either (the other 32-bit LDS atomics, `ds_min/max`, `ds_cmpst`, the float forms, `ds_inc/dec`, `ds_mskor` and `ds_wrxchg`, are extrapolated from `ds_add_u32` and were not measured against a 64-bit atomic); a program without a 64-bit LDS atomic keeps its native 32-bit atomics. `ds_condxchg32_rtn_b64` is two `ds_mskor` halves, each under the lock but not atomic as a pair, so a 64-bit atomic between the two halves is not excluded. Sub-dword writes (`ds_write_b8`, `ds_write_b16` and their `_d16_hi` forms) and plain `ds_read_*`, `ds_write_*`, `ds_append_u32` and `ds_consume_u32` are not taken under the lock, so a plain access racing a 64-bit atomic on the same dwords can see or overwrite one half of it; whether the hardware orders them was not measured. The lock serialises the workgroup's LDS atomics of such a program; the throughput cost was not measured. `VK_KHR_workgroup_memory_explicit_layout` with `shaderSharedInt64Atomics` would make both widths native and is not used. A 64-bit atomic at an address that is 4-byte but not 8-byte aligned faults the wave on gfx1036, as on gfx1035. +- [64-bit LDS atomics](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (`ds_*_u64`, `ds_*_b64`, `ds_*_f64`) read and write the two dwords under a workgroup lock stored after the guest LDS, as LDS is a 32-bit array without 64-bit atomics. On RDNA2 (gfx1036) the LDS serialises a 32-bit atomic on a slot's high dword and a 64-bit atomic on that slot: 128 lanes of `ds_add_u32` and 128 lanes of `ds_add_u64` on one slot sum exactly. Only that pair was measured. Every 32-bit LDS atomic of a program that contains a 64-bit LDS atomic therefore takes the same lock, including a 32-bit `flat_atomic_*` whose address lands in the LDS aperture (a 64-bit one takes it already); the flat forms were not measured against a 64-bit atomic either (the other 32-bit LDS atomics, `ds_min/max`, `ds_cmpst`, the float forms, `ds_inc/dec`, `ds_mskor` and `ds_wrxchg`, are extrapolated from `ds_add_u32` and were not measured against a 64-bit atomic); a program without a 64-bit LDS atomic keeps its native 32-bit atomics. `ds_condxchg32_rtn_b64` is two `ds_mskor` halves, each under the lock but not atomic as a pair, so a 64-bit atomic between the two halves is not excluded. Sub-dword writes (`ds_write_b8`, `ds_write_b16` and their `_d16_hi` forms) and plain `ds_read_*`, `ds_write_*`, `ds_append_u32` and `ds_consume_u32` are not taken under the lock, so a plain access racing a 64-bit atomic on the same dwords can see or overwrite one half of it; whether the hardware orders them was not measured. The lock serialises the workgroup's LDS atomics of such a program; the throughput cost was not measured. `VK_KHR_workgroup_memory_explicit_layout` with `shaderSharedInt64Atomics` would make both widths native and is not used. A 64-bit atomic at an address that is 4-byte but not 8-byte aligned faults the wave on gfx1036, as on gfx1035. When the LDS lives in device memory (compute LDS beyond the device's shared-memory limit) and the device has `shaderBufferInt64Atomics`, no lock is used: a 64-bit atomic is a compare-exchange loop on a 64-bit view of the storage buffer and a 32-bit atomic is a native buffer atomic, so a mixed-width pair on one slot relies on the device ordering both widths on one buffer address; only `ds_add_u32` against `ds_add_u64` was measured, on gfx1036 (AMD Windows driver 32.0.21042.62) and an RTX 4070 SUPER. The lock is not used there because AMD's Windows driver compiles an acquire or release on a storage buffer, at workgroup scope or wider, to `s_barrier`, and those barriers inside the lock's divergent loop pair with the program's own `s_barrier`s. The fence after each LDS atomic compiles to `s_barrier` on that driver as well, so a wave that skips an LDS atomic that other waves of its workgroup execute misses a barrier they reach; its effect was not measured. On that path a 64-bit atomic at an address that is 4-byte but not 8-byte aligned is skipped and returns 0. - [LDS float atomics](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (`ds_min/max/cmpst_f32`, `ds_min/max/cmpst_f64` and their `_rtn` forms) follow the hardware measured on RDNA2 (gfx1036) over 14 x 14 operand sets (zeros, ones, infinities, denormals, quiet and signalling NaNs of both signs and with payloads) in IEEE 0 and 1 with every value 0-3 of the type's denormal field: both operands are flushed to a signed zero when bit 0 of the field is clear (0 and 2; 1 and 3 keep them; the f32 field for the f32 forms, the f16/f64 field for the f64 forms; a shader without a known float mode flushes f32 and keeps f64), then a signalling-NaN data operand is stored quieted, else a signalling-NaN old value is stored quieted, else a quiet-NaN old value is replaced by the data, else a quiet-NaN data operand loses, else the ordered compare with -0 below +0 decides, and the flushed value is what is written, also when the old value wins. `cmpst` compares the flushed bits (both zeros equal, NaN never equal) and leaves memory unchanged when they differ. The returned value is always the old value as it was in memory. `IEEE_MODE` has no effect. The same bit rule serves the buffer float atomics (`AtomicFloatBits`). Not measured: a denormal or signalling-NaN `data1` of `cmpst` (stored as given). `ds_add_f32` and `ds_add_rtn_f32` follow RDNA1 (gfx1010, RX 5700 XT), measured with the [hardware oracle](../HW_ORACLE.md) template run through the Windows driver's comgr and HIP runtime over 1574 operand pairs (signed zeros, denormals, the smallest normals, infinities, quiet and signalling NaNs with payloads, the largest normals, random pairs near the denormal range and pairs that nearly cancel) in all 16 combinations of the f32 denormal field and the f32 round mode, and at IEEE 1 and DX10 clamp 0: both operands are flushed to a signed zero when bit 0 of the field is clear and the result when bit 1 is clear, the sum is rounded to nearest even whatever the round mode, a NaN data operand is stored quieted, else a NaN old value is stored quieted, `+inf + -inf` stores 0xffc00000, and `IEEE_MODE` and `DX10_CLAMP` have no effect. The sum is the host's `OpFAdd` between those flushes, so where the field keeps f32 denormals the result also depends on the host preserving them, which the module does not request. thuanquang measured the same rule on RDNA2 (gfx1031, RX 6700 XT) in #5492, with the same operand classes and 18 modes in wave32 and wave64; the PS5's gfx1013 is not measured. - [LDS in pixel, vertex-path and fetch programs](../../../core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp) (shader recompiler) - is an array private to each host invocation, in `Function` storage, as only compute and mesh programs get workgroup memory; on the console one LDS region is shared by the wave (RDNA 2 ISA sections 3.6.6 and 10.4), so a lane does not see another lane's LDS writes. Sub-dword writes (`ds_write_b8`, `ds_write_b16` and their `_d16_hi` forms), the 32-bit LDS atomics, `ds_append` and `ds_consume` load, update and store the invocation's own dword without an atomic, as Vulkan allows atomics only on `Uniform`, `Workgroup`, `Image`, `StorageBuffer`, `PhysicalStorageBuffer` and `TaskPayloadWorkgroupEXT` pointers (VUID-StandaloneSpirv-None-04686). `ds_append` and `ds_consume` update only the first active lane's copy and return its old value to every lane, so a second append after the first active lane changes, or a read of the counter by another lane, differs from the hardware. Not measured on hardware. - [V# bases off a DWORD boundary](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (shader recompiler) - raw accesses use the exact bytes and `s_buffer_load` reads `(base & ~3) + (offset & ~3)`, as measured on gfx1035 (Linux, alignment mode UNALIGNED; the console's mode is unknown). Formatted accesses off their element's alignment throw. Atomics use the byte address `base + offset`, base's low two bits included: measured on RDNA2 (gfx1031, Radeon RX 6700 XT, AMD Windows driver 32.0.21045.5002 through HIP) with `buffer_atomic_add`, `buffer_atomic_swap` and `buffer_atomic_cmpswap` and bases 0 to 3 bytes past a DWORD boundary with offsets 0 to 7, an address on a DWORD boundary updates that DWORD and returns its old value, and any other address stops the dispatch, which the recompiler reports as an unaligned access fault. From 4aaa1252796782eec176a3c24b8caa2a5c97e831 Mon Sep 17 00:00:00 2001 From: thuanquang <ghoulbulldog@gmail.com> Date: Sun, 18 Oct 2026 09:55:38 +0700 Subject: [PATCH 0949/1000] test(agc): drop the comment above the guest depth-bits case Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- .../prx/libSceAgcDriver/tests/execution/DepthPlaneSampling.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneSampling.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneSampling.cpp index 1133b100b3..33c6cfdb6b 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneSampling.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneSampling.cpp @@ -326,8 +326,6 @@ void DepthBitsBaseSliceCase(AgcDriver::VulkanDevice& device, GuestBlock& block, Expect(output, 0x1234u, "D16 depth-bits array from base slice 1"); } -// A 32_UINT T# with a depth swizzle over guest memory that no depth surface was drawn into: the raw dwords, -// including float denormals, NaN payloads and the sign bit alone. void GuestDepthBitsCase(AgcDriver::VulkanDevice& device, GuestBlock& block, std::span<std::uint32_t> output) { constexpr std::array<std::uint32_t, 9> values{0x00000001u, 0x007fffffu, 0x80000000u, 0x80000001u, 0x7f800001u, 0x7fc00001u, 0xffffffffu, 0x3e800000u, 0x12345678u}; for (std::size_t index = 0; index < values.size(); ++index) { From 8f03282f5eaa50ec1a8f39f9c530961487fe9c49 Mon Sep 17 00:00:00 2001 From: thuanquang <ghoulbulldog@gmail.com> Date: Sun, 18 Oct 2026 09:01:19 +0700 Subject: [PATCH 0950/1000] fix(agc): draw geometry programs with the last provoking vertex Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- .../libSceAgcDriver/Graphics/src/Pipeline.cpp | 4 +- .../libSceAgcDriver/Graphics/src/State.cpp | 4 +- .../prx/libSceAgcDriver/tests/Graphics.cpp | 28 +++ .../tests/execution/MeshProvokingVertex.cpp | 181 ++++++++++++++++++ docs/dev/debt/libSceAgcDriver.md | 2 +- 5 files changed, 216 insertions(+), 3 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/MeshProvokingVertex.cpp diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp index e718cee2f4..c404531663 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp @@ -110,7 +110,9 @@ void ValidateProvokingVertex(const Context& context, const State& state, std::sp return; } Require(context.provokingVertexLast, "last provoking vertex requires VK_EXT_provoking_vertex with provokingVertexLast enabled"); - Require(!BarycentricPatches(state, shaders) && state.stages.path == ShaderPath::Vertex && !state.stages.mesh && !state.stages.tessellation, "last provoking vertex is unsupported for generated primitive pipelines"); + const bool meshPrimitives = state.stages.path == ShaderPath::Geometry && state.stages.mesh; + Require(!BarycentricPatches(state, shaders) && !state.stages.tessellation && (meshPrimitives || (state.stages.path == ShaderPath::Vertex && !state.stages.mesh)), "last provoking vertex is unsupported for generated primitive pipelines"); + Require(!meshPrimitives || (state.topology != VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP && state.topology != VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN), "last provoking vertex is unsupported for triangle strips or fans assembled for a geometry program"); Require(state.rectList || state.topology == VK_PRIMITIVE_TOPOLOGY_POINT_LIST || state.topology == VK_PRIMITIVE_TOPOLOGY_LINE_LIST || state.topology == VK_PRIMITIVE_TOPOLOGY_LINE_STRIP || state.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST || state.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP || state.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN, "last provoking vertex is unsupported for this primitive topology"); for (const auto& shader : shaders) { if (shader.stage != ShaderRecompiler::ShaderStage::Fragment) continue; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index 77b8c2595b..92e0850160 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -945,7 +945,9 @@ ShaderStages DecodeShaderStages(const QueueState& queue, bool vertexWorkgroup) { validate(amplification != 0, "fast launch subgroup amplifies to no primitives"); const auto maxPrimitives = outputSize == 2u && !fastLaunch ? std::max(primitives * amplification, maxVertices) : primitives * amplification; const auto threads = std::max({(groupPrimitives - 1u) * inputStep + inputSize, primitives, maxVertices, maxPrimitives}); - result.mesh = ShaderRecompiler::MeshConfiguration{primitive, groupPrimitives, (groupPrimitives - 1u) * inputStep + inputSize, maxVertices, maxPrimitives, ((threads + result.vertexWaveSize - 1u) / result.vertexWaveSize) * result.vertexWaveSize, ((resources >> 19u) & 0xffu) * 128u, 0, esgsItemSize, outputSize}; + const auto raster = find(queue.context, 0x205); + const auto provokingVertex = raster != queue.context.end() && (raster->second & (1u << 19u)) != 0 ? outputSize - 1u : 0u; + result.mesh = ShaderRecompiler::MeshConfiguration{primitive, groupPrimitives, (groupPrimitives - 1u) * inputStep + inputSize, maxVertices, maxPrimitives, ((threads + result.vertexWaveSize - 1u) / result.vertexWaveSize) * result.vertexWaveSize, ((resources >> 19u) & 0xffu) * 128u, provokingVertex, esgsItemSize, outputSize}; } return result; } diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 720aa42f40..89dbaece1f 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -867,6 +867,13 @@ void ShaderStageTests() { state = AgcDriver::Graphics::DecodeState(lines); mesh = *state.stages.mesh; Require(mesh.maxPrimitives == 153 && mesh.threadsPerGroup == 256 && mesh.outputPrimitiveSize == 3 && AgcDriver::Graphics::RasterPrimitiveOf(state) == ShaderRecompiler::RasterPrimitive::Triangle, "the triangle-strip subgroup of the same draw changed"); + Require(mesh.provokingVertex == 0, "a first-vertex geometry draw does not take its flat inputs and layer from the first vertex of each output primitive"); + const auto raster = lines.context.at(0x205); + lines.context[0x205] = raster | (1u << 19u); + Require(AgcDriver::Graphics::DecodeState(lines).stages.mesh->provokingVertex == 2 && AgcDriver::Graphics::DecodeShaderStages(lines).mesh->provokingVertex == 2, "a last-vertex geometry draw does not take its flat inputs and layer from the third vertex of each output triangle"); + lines.context[0x29b] = 1; + Require(AgcDriver::Graphics::DecodeState(lines).stages.mesh->provokingVertex == 1, "a last-vertex geometry draw does not take its flat inputs and layer from the second vertex of each output line"); + lines.context[0x205] = raster; lines.context[0x29b] = 1; lines.context[0x2ce] = 1; expectFailure([&] { AgcDriver::Graphics::DecodeState(lines); }, "unsupported geometry input or output assembly"); @@ -6991,6 +6998,27 @@ void ProvokingVertexTests() { if (generated == 1) state.stages.tessellation.emplace(); expectFailure([&] { ValidateProvokingVertex(supported, state); }, "last provoking vertex is unsupported for generated primitive pipelines"); } + auto meshDraw = last; + meshDraw.stages.path = ShaderPath::Geometry; + meshDraw.stages.mesh.emplace(); + meshDraw.topology = VK_PRIMITIVE_TOPOLOGY_LINE_LIST; + for (const auto topology : {VK_PRIMITIVE_TOPOLOGY_POINT_LIST, VK_PRIMITIVE_TOPOLOGY_LINE_LIST, VK_PRIMITIVE_TOPOLOGY_LINE_STRIP, VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST}) { + meshDraw.topology = topology; + for (const auto& drawn : {std::span<const CompiledShader>{}, std::span<const CompiledShader>(flatShaders), std::span<const CompiledShader>(flatP10Shaders), std::span<const CompiledShader>(smoothShaders), std::span<const CompiledShader>(slotShaders)}) ValidateProvokingVertex(supported, meshDraw, drawn); + } + for (const auto topology : {VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP, VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN}) { + meshDraw.topology = topology; + expectFailure([&] { ValidateProvokingVertex(supported, meshDraw, flatShaders); }, "last provoking vertex is unsupported for triangle strips or fans assembled for a geometry program"); + auto firstDraw = meshDraw; + firstDraw.provokingVertexMode = VK_PROVOKING_VERTEX_MODE_FIRST_VERTEX_EXT; + ValidateProvokingVertex(supported, firstDraw, flatShaders); + } + meshDraw.topology = VK_PRIMITIVE_TOPOLOGY_LINE_LIST; + expectFailure([&] { ValidateProvokingVertex(unsupported, meshDraw, flatShaders); }, "last provoking vertex requires VK_EXT_provoking_vertex with provokingVertexLast enabled"); + fragment.fragmentParameters = {{0, 0, true, true, false, false, false}}; + expectFailure([&] { ValidateProvokingVertex(supported, meshDraw, shaders); }, "last provoking vertex with explicit per-vertex flat interpolation is unsupported"); + meshDraw.stages.tessellation.emplace(); + expectFailure([&] { ValidateProvokingVertex(supported, meshDraw, flatShaders); }, "last provoking vertex is unsupported for generated primitive pipelines"); auto rectangle = last; rectangle.rectList = true; rectangle.topology = VK_PRIMITIVE_TOPOLOGY_PATCH_LIST; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/MeshProvokingVertex.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/MeshProvokingVertex.cpp new file mode 100644 index 0000000000..3c7b2a9333 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/MeshProvokingVertex.cpp @@ -0,0 +1,181 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" +#include "../MeshGeometryFixture.hpp" +#include "VulkanTestDevice.hpp" +#include <algorithm> +#include <array> +#include <bit> +#include <cstddef> +#include <cstdint> +#include <cstdio> +#include <exception> +#include <iostream> +#include <string> +#include <utility> +#include <vector> + +namespace { + +using AgcDriver::Graphics::Require; + +constexpr std::uint32_t Width = 64; +constexpr std::uint32_t Height = 64; +using Image = std::array<std::byte, Width * Height * 4>; +alignas(256) Image Pixels{}; +alignas(256) constexpr std::array<std::uint16_t, 4> Indices{0, 1, 2, 0}; +alignas(256) constexpr std::array<std::array<float, 8>, 3> Vertices{{ + {-0.9f, -0.8f, 0, 1, 1, 0, 0, 1}, {-0.1f, -0.8f, 0, 1, 0, 1, 0, 1}, {-0.5f, 0.8f, 0, 1, 0, 0, 1, 1} +}}; +alignas(256) constexpr std::array<std::uint32_t, 7> FlatPixelCode{0xc8120002u, 0xc8160102u, 0xc81a0202u, 0xc81e0302u, 0xf800180fu, 0x07060504u, 0xbf810000u}; +alignas(256) constexpr std::array<std::uint32_t, 11> SmoothPixelCode{0xc8100000u, 0xc8110001u, 0xc8140100u, 0xc8150101u, 0xc8180200u, 0xc8190201u, 0xc81c0300u, 0xc81d0301u, 0xf800180fu, 0x07060504u, 0xbf810000u}; + +constexpr std::array<std::uint32_t, 3> LoadBufferDescriptor{0xf4080204u, 0xfa000000u, 0xbf8cc07fu}; +alignas(256) constexpr auto GeometryCode = [] { + std::array<std::uint32_t, LoadBufferDescriptor.size() + AgcDriver::Test::MeshGeometryCode.size()> code{}; + std::copy(LoadBufferDescriptor.begin(), LoadBufferDescriptor.end(), code.begin()); + std::copy(AgcDriver::Test::MeshGeometryCode.begin(), AgcDriver::Test::MeshGeometryCode.end(), code.begin() + LoadBufferDescriptor.size()); + return code; +}(); + +alignas(16) std::array<std::uint32_t, 4> Descriptor{}; + +struct Header { + Shader shader{}; + ShaderUserData users{}; +}; + +Header GeometryHeader{}; +Header FlatPixelHeader{}; +Header SmoothPixelHeader{}; + +void Register(Header& header, const std::uint32_t* code, std::size_t size, std::uint8_t type) { + header.shader.file_header = 0x34333231u; + header.shader.version = 0x18u; + header.shader.header_size = sizeof(header); + header.shader.shader_size = static_cast<std::uint32_t>(size); + header.shader.code = code; + header.shader.type = type; + header.shader.user_data = &header.users; + AgcDriverRegisterShader_nid_postfix(&header.shader); +} + +void Write(std::vector<std::uint32_t>& commands, std::uint32_t opcode, std::uint32_t offset, std::uint32_t value) { + commands.insert(commands.end(), {0xc0010000u | (opcode << 8u), offset, value}); +} + +Image Draw(const std::uint32_t* pixelCode, bool flat, bool last) { + Pixels.fill(std::byte{0}); + const auto color = reinterpret_cast<std::uintptr_t>(Pixels.data()); + const auto geometry = reinterpret_cast<std::uintptr_t>(GeometryCode.data()); + const auto pixel = reinterpret_cast<std::uintptr_t>(pixelCode); + const auto table = reinterpret_cast<std::uintptr_t>(Descriptor.data()); + const std::vector<std::pair<std::uint32_t, std::uint32_t>> context{ + {0x2d5, 0x20}, {0x1ff, 12}, {0x2ce, 3}, {0x29b, 2}, {0x2ab, 4}, {0x260, 0}, + {0x1b6, 1}, {0x1b3, 2}, {0x1b4, 2}, {0x191, flat ? 0x400u : 0u}, + {0x207, 0}, {0x200, 0}, {0x203, 0x800}, + {0x2dc, 0xaa00}, {0x2f8, 0}, {0x292, 2}, {0x293, 0}, + {0x80, 0}, {0x8d, 0}, {0x83, 0xffff}, {0x8c, 0xa}, + {0x2f9, 0x2d}, {0x313, 0x6000}, {0x30e, 0xffffffff}, {0x30f, 0xffffffff}, + {0x206, 0x43f}, {0x204, 0x80000}, {0x205, 0x240u | (last ? 1u << 19u : 0u)}, + {0x8e, 0xf}, {0x8f, 0xf}, {0x202, 0xcc0010}, + {0x1c4, 0}, {0x1c5, 9}, {0x1c3, 4}, {0x31c, 0x28028}, + {0x31b, 0}, {0x31d, 0}, {0x3b0, ((Height - 1u) << 14u) | (Width - 1u)}, + {0x3b8, 0x9000000}, {0x1e0, 0}, {0xc, 0}, {0xd, (Height << 16u) | Width}, + {0x81, 0x80000000}, {0x82, (Height << 16u) | Width}, + {0x90, 0x80000000}, {0x91, (Height << 16u) | Width}, + {0x94, 0x80000000}, {0x95, (Height << 16u) | Width}, + {0x318, static_cast<std::uint32_t>(color >> 8u)}, {0x390, static_cast<std::uint32_t>(color >> 40u)}, + {0x10f, std::bit_cast<std::uint32_t>(32.0f)}, {0x110, std::bit_cast<std::uint32_t>(32.0f)}, + {0x111, std::bit_cast<std::uint32_t>(32.0f)}, {0x112, std::bit_cast<std::uint32_t>(32.0f)}, + {0x113, std::bit_cast<std::uint32_t>(1.0f)}, {0x114, 0}, + {0xb4, 0}, {0xb5, std::bit_cast<std::uint32_t>(1.0f)} + }; + const std::vector<std::pair<std::uint32_t, std::uint32_t>> shaders{ + {0xc8, static_cast<std::uint32_t>(geometry >> 8u)}, {0xc9, static_cast<std::uint32_t>(geometry >> 40u)}, + {0x8a, 0x600c0000u}, {0x8b, (8u << 19u) | (3u << 16u) | (2u << 1u)}, + {0x8c, static_cast<std::uint32_t>(table)}, {0x8d, static_cast<std::uint32_t>(table >> 32u)}, + {0x8, static_cast<std::uint32_t>(pixel >> 8u)}, {0x9, static_cast<std::uint32_t>(pixel >> 40u)}, {0xb, 0} + }; + std::vector<std::uint32_t> commands; + Write(commands, 0x79, 0x242, 4); + Write(commands, 0x79, 0x25b, (12u << 9u) | 4u); + for (const auto& [offset, value] : context) Write(commands, 0x69, offset, value); + for (const auto& [offset, value] : shaders) Write(commands, 0x76, offset, value); + commands.insert(commands.end(), {0xc0002a00u, 0u}); + const auto address = reinterpret_cast<std::uintptr_t>(Indices.data()); + commands.insert(commands.end(), {0xc0042700u, 3u, static_cast<std::uint32_t>(address), static_cast<std::uint32_t>(address >> 32u), 3u, 0u}); + Packet packet{commands.data(), static_cast<std::uint32_t>(commands.size()), 0, {}}; + Require(sceAgcDriverSubmitDcb(&packet) == 0, "mesh submission failed"); + AgcDriverWaitIdle_nid_postfix(); + AgcDriver::GuestMemory::FlushGpuWrites(color, Pixels.size()); + return Pixels; +} + +unsigned Channel(const Image& image, std::uint32_t x, std::uint32_t y, std::uint32_t channel) { + return std::to_integer<unsigned>(image[(y * Width + x) * 4u + channel]); +} + +constexpr std::array<std::pair<std::uint32_t, std::uint32_t>, 5> Inside{{{12, 32}, {16, 32}, {20, 32}, {16, 28}, {16, 36}}}; + +void FlatInputs() { + for (const auto last : {false, true, false}) { + const auto image = Draw(FlatPixelCode.data(), true, last); + const std::array<unsigned, 4> expected{last ? 0u : 255u, 0u, last ? 255u : 0u, 255u}; + const std::string mode = last ? "last" : "first"; + for (const auto& [x, y] : Inside) { + for (std::uint32_t channel = 0; channel < 4; ++channel) { + Require(Channel(image, x, y, channel) == expected[channel], "flat input, " + mode + " provoking vertex: pixel (" + std::to_string(x) + ", " + std::to_string(y) + ") channel " + std::to_string(channel) + " is " + std::to_string(Channel(image, x, y, channel)) + ", expected " + std::to_string(expected[channel]) + " (the color of vertex " + (last ? "2" : "0") + ")"); + } + } + Require(Channel(image, 48, 48, 3) == 0, "flat input, " + mode + " provoking vertex: the draw wrote outside its triangle"); + } +} + +void SmoothInputs() { + const auto first = Draw(SmoothPixelCode.data(), false, false); + for (const auto& [x, y] : Inside) { + for (std::uint32_t channel = 0; channel < 3; ++channel) Require(Channel(first, x, y, channel) > 16u, "smooth input: pixel (" + std::to_string(x) + ", " + std::to_string(y) + ") channel " + std::to_string(channel) + " is " + std::to_string(Channel(first, x, y, channel)) + ", not a blend of the three vertex colors"); + } + const auto last = Draw(SmoothPixelCode.data(), false, true); + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + for (std::uint32_t channel = 0; channel < 4; ++channel) Require(Channel(last, x, y, channel) == Channel(first, x, y, channel), "smooth input: pixel (" + std::to_string(x) + ", " + std::to_string(y) + ") channel " + std::to_string(channel) + " is " + std::to_string(Channel(last, x, y, channel)) + " with the last provoking vertex and " + std::to_string(Channel(first, x, y, channel)) + " with the first"); + } + } +} + +} + +int main() { + try { + bool smooth = false; + { + const auto device = OpenVulkanTestDevice(); + if (!device || !TargetHasCapability(device->Target(), spv::CapabilityMeshShadingEXT)) return VulkanTestSkipped; + if (device->DeviceName().starts_with("llvmpipe")) { + std::puts("skipped, the device has no mesh shader compiler that handles these programs (llvmpipe)"); + return VulkanTestSkipped; + } + if (!device->ProvokingVertexLast()) { + std::puts("skipped, the device has no last provoking vertex mode"); + return VulkanTestSkipped; + } + smooth = TargetHasCapability(device->Target(), spv::CapabilityFragmentBarycentricKHR); + } + const auto vertices = reinterpret_cast<std::uintptr_t>(Vertices.data()); + Descriptor = {static_cast<std::uint32_t>(vertices), static_cast<std::uint32_t>(vertices >> 32u) | (32u << 16u), static_cast<std::uint32_t>(Vertices.size()), 0x01016facu}; + Register(GeometryHeader, GeometryCode.data(), sizeof(GeometryCode), 2); + Register(FlatPixelHeader, FlatPixelCode.data(), sizeof(FlatPixelCode), 1); + Register(SmoothPixelHeader, SmoothPixelCode.data(), sizeof(SmoothPixelCode), 1); + FlatInputs(); + if (smooth) SmoothInputs(); + AgcDriverShutdown_nid_postfix(); + std::cout << "geometry-program triangles take flat inputs from vertex 0 with the first provoking vertex and from vertex 2 with the last" << (smooth ? ", and interpolate smooth inputs the same in both modes\n" : "; smooth inputs skipped without fragment shader barycentrics\n"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + AgcDriverShutdown_nid_postfix(); + return 1; + } +} diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index d456a047e5..9d59de6bc0 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -103,7 +103,7 @@ - [PA_CL_CLIP_CNTL](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - `DX_LINEAR_ATTR_CLIP_ENA` (bit 24) clear is drawn like bit 24 set, with Vulkan's clipping of `NoPerspective` outputs; what a clear bit changes on the console is undocumented. - [10_11_11 unorm color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) render into R32_UINT attachments: the pixel shader packs each export, rounded to nearest even like format-30 image stores, which is assumed to match the CB's conversion. - [11_11_10 float color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) render into R32_UINT attachments: the pixel shader packs each export toward zero like format-43 image stores (overflow to the largest finite value, negatives to zero, NaN kept), which is assumed to match the CB's conversion; only the reversed component order (COMP_SWAP 2) is accepted. -- [PA_SU_SC_MODE_CNTL.PROVOKING_VTX_LAST](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp) (libSceAgcDriver) uses Vulkan's last-vertex mode for native primitives. Bit 19 follows [opengnm's `flatshade_first` mapping](https://github.com/blackbearreloaded/ps5-opengl/blob/db752da4c89380af228497b5b6852163a4e61069/src/gallium/ps5/ps5_screen.c#L2244); vertex selection follows [Vulkan](https://docs.vulkan.org/spec/latest/chapters/drawing.html#drawing-primitive-topologies), without console measurements. A [rect-list](../../../core/shader/recompiler/SpirvBackend/src/RectListShaders.cpp) draw in last mode gives all four corners of each rectangle the flat inputs of its third vertex through the generated control stage, as first mode gives them those of its first, so Vulkan's choice of provoking vertex for the tessellated triangles does not matter; which vertex the console takes for a rectangle is not measured either. Last mode throws without `VK_EXT_provoking_vertex`, for generated mesh or tessellation primitives, explicit per-vertex flat inputs, and triangle strips whose pixel shader reads a vertex slot itself (`v_interp_mov_f32`, or 16-bit interpolation, which is not checked yet), since the vertex order of odd strip triangles [depends on the device](https://docs.vulkan.org/refpages/latest/refpages/source/VkPhysicalDeviceFragmentShaderBarycentricPropertiesKHR.html). `v_interp_p1_f32` and `v_interp_p2_f32` read P10 and P20 against the guest's I and J, which come from `gl_BaryCoordKHR` in the same vertex order as the per-vertex values, so the order cancels out and strips with only those reads draw. A FLAT_SHADE input without pass-through that is read through `v_interp_mov_f32` P10 and not P20 [returns the provoking vertex's value](../../../core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp), like P0. ps5-opengl reads every flat input that way, and its driver passes the GL 4.6 CTS on a PS5 (#1413). The lane itself was not measured on the console, so with the first provoking vertex such a shader throws. +- [PA_SU_SC_MODE_CNTL.PROVOKING_VTX_LAST](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp) (libSceAgcDriver) uses Vulkan's last-vertex mode for native primitives. Bit 19 follows [opengnm's `flatshade_first` mapping](https://github.com/blackbearreloaded/ps5-opengl/blob/db752da4c89380af228497b5b6852163a4e61069/src/gallium/ps5/ps5_screen.c#L2244); vertex selection follows [Vulkan](https://docs.vulkan.org/spec/latest/chapters/drawing.html#drawing-primitive-topologies), without console measurements. A [rect-list](../../../core/shader/recompiler/SpirvBackend/src/RectListShaders.cpp) draw in last mode gives all four corners of each rectangle the flat inputs of its third vertex through the generated control stage, as first mode gives them those of its first, so Vulkan's choice of provoking vertex for the tessellated triangles does not matter; which vertex the console takes for a rectangle is not measured either. A geometry program's primitives keep the vertex indices of its own primitive export on the [mesh path](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp), so last mode takes their flat inputs (from [Vulkan](https://docs.vulkan.org/spec/latest/chapters/vertexpostproc.html#vertexpostproc-flatshading)) and their [layer](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) from the last index of each primitive, while their per-vertex inputs keep the order of first mode ([Vulkan](https://docs.vulkan.org/spec/latest/chapters/primsrast.html#primsrast-barycentric)); that the console does the same for an exported primitive is not measured. Last mode throws without `VK_EXT_provoking_vertex`, for tessellation primitives, for triangle strips or fans assembled for a geometry program (whose odd-triangle order on the console is not measured), explicit per-vertex flat inputs, and triangle strips whose pixel shader reads a vertex slot itself (`v_interp_mov_f32`, or 16-bit interpolation, which is not checked yet), since the vertex order of odd strip triangles [depends on the device](https://docs.vulkan.org/refpages/latest/refpages/source/VkPhysicalDeviceFragmentShaderBarycentricPropertiesKHR.html). `v_interp_p1_f32` and `v_interp_p2_f32` read P10 and P20 against the guest's I and J, which come from `gl_BaryCoordKHR` in the same vertex order as the per-vertex values, so the order cancels out and strips with only those reads draw. A FLAT_SHADE input without pass-through that is read through `v_interp_mov_f32` P10 and not P20 [returns the provoking vertex's value](../../../core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp), like P0. ps5-opengl reads every flat input that way, and its driver passes the GL 4.6 CTS on a PS5 (#1413). The lane itself was not measured on the console, so with the first provoking vertex such a shader throws. - [Dual-source blending](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) takes the MRT1 export as the second source of the MRT0 blend, as RADV and radeonsi program it; what the CB does with other export or target layouts is unknown, so they throw. - [Blending into reversed-order color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - 8_8_8_8 and 16_16_16_16 targets with `COMP_SWAP` 2 or 3 keep the exported alpha in their first channel and red or blue in their fourth, which Vulkan would blend with the color and the alpha equation respectively. The color block is taken to blend in shader export order and apply `COMP_SWAP` afterwards: the alpha equation and every source-alpha factor use the exported alpha, and the destination alpha is the first channel. This follows the `CB_TARGET_MASK` text of the AMD [SI](https://xorg.freedesktop.org/docs/AMD/old/SI_3D_registers.pdf) (p. 229) and [CIK](https://xorg.freedesktop.org/docs/AMD/old/CIK_3D_registers_v2.pdf) (p. 238) register references ("the order of components after blending and before COMP_SWAP is applied"). RADV builds `CB_BLENDn_CONTROL` without regard to the swap ([radv_pipeline_graphics.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-26.2.2/src/amd/vulkan/radv_pipeline_graphics.c#L650-L709)) and renders R4G4B4A4, B4G4R4A4, R5G5B5A1 and B5G5R5A1 with `SWAP_STD_REV` and `SWAP_ALT_REV` ([vk_format.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-26.2.2/src/vulkan/util/vk_format.c#L43-L48), [ac_formats.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-26.2.2/src/amd/common/ac_formats.c#L609-L624)). Mesa CI's Navi10 (gfx1010) CTS run ([job 112381553](https://gitlab.freedesktop.org/mesa/mesa/-/jobs/112381553), [112381554](https://gitlab.freedesktop.org/mesa/mesa/-/jobs/112381554)) passes all 400 `blend.format.<format>.states` cases of each of those formats, including separate alpha and destination-alpha factors. It is not measured on gfx1013 or on 8_8_8_8 and 16_16_16_16 targets. Factor pairs Vulkan applies per channel are used directly. Otherwise the pixel shader only copies values: it writes the exported alpha, each channel's own export or the constants 0 and 1 into a second color at location 0 index 1 (and 1 into a channel of the first color whose source factor is ONE, moving its export into the factor), and the blend forms every factor, `1 -` and product from those copies through `SRC1_COLOR`/`SRC1_ALPHA` factors. The blend unit therefore rounds them as it rounds a plain Vulkan blend of the same export; Vulkan does not require a second-source color to be converted like the first, but on an RTX 5080 and lavapipe every reversed-order case of the execution test is bit-identical to the same draw into a `SWAP_STD` target, which the test checks. Factor combinations that copies cannot express (for example `ONE_MINUS_SRC_ALPHA` in the alpha equation with `SRC_COLOR` in the color equation), sRGB, SNORM and integer targets, constant and second-source factors, destination-alpha and `SRC_ALPHA_SATURATE` factors in the color equation, different color and alpha operations, MIN or MAX with factors other than ONE, other color exports when the blend needs the second color, and devices without `dualSrcBlend` throw. Gran Turismo 7 (PPSA01317) blends `CB_BLEND0_CONTROL` 0x65010504 into such a target (#4782). - [DB_SHADER_CONTROL.CONSERVATIVE_Z_EXPORT](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp) (libSceAgcDriver) becomes `DepthLess`/`DepthGreater`. For a pixel shader that breaks the promise, the host driver decides whether its interpolated Z is depth-tested before shading; what the console does then was not measured. From 326104974a51fc1e024c2cd6e1244b57e29f39fc Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Sat, 17 Oct 2026 20:19:49 -0700 Subject: [PATCH 0951/1000] test(agc): read the depth bits of a SW_64KB_Z_X surface from guest memory A compute shader loads every texel of a 256x192 32_UINT, 32_SINT and 16_UINT texture in SW_64KB_Z_X whose memory holds a known pattern and no depth surface, from untracked memory and from a guest allocation, and stores what it read. Every texel must keep its stored bits, NaN, infinity and denormal patterns included; without the 32-bit fix the 32_UINT read throws. --- .../tests/execution/DepthBitsGuestMemory.cpp | 252 ++++++++++++++++++ 1 file changed, 252 insertions(+) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/DepthBitsGuestMemory.cpp diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DepthBitsGuestMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DepthBitsGuestMemory.cpp new file mode 100644 index 0000000000..4732c5c876 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DepthBitsGuestMemory.cpp @@ -0,0 +1,252 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureSwizzleEquations.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include <windows.h> +#endif +#include <algorithm> +#include <array> +#include <bit> +#include <cstddef> +#include <cstdint> +#include <cstdio> +#include <cstdlib> +#include <cstring> +#include <iostream> +#include <span> +#include <string> +#include <vector> + +namespace { + +using namespace AgcDriver::Graphics; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Width = 256; +constexpr std::uint32_t Height = 192; +constexpr std::uint32_t Texels = Width * Height; +constexpr std::uint32_t Format16UInt = 11; +constexpr std::uint32_t Format32UInt = 20; +constexpr std::uint32_t Format32SInt = 21; +constexpr std::uint32_t Type2D = 9; +constexpr std::uint32_t Swizzle64KbZX = 24; +constexpr std::uint32_t IdentitySwizzle = 0xfacu; +constexpr std::uint32_t Unwritten = 0xa5a5a5a5u; +constexpr std::size_t SlotBytes = 256u << 10u; +constexpr std::size_t BlockBytes = 1u << 20u; + +static_assert(Width == 256u, "the row shader scales the row by 256 texels"); + +alignas(256) constexpr std::array<std::uint32_t, 10> RowCode{ + 0x7e020210u, 0x34040288u, 0x4a040500u, 0x34040482u, 0xf0001108u, 0x00010300u, 0xbf8c3f70u, 0xe0701000u, + 0x80000302u, 0xbf810000u, +}; + +constexpr std::array<std::uint32_t, 16> Edges32{ + 0x00000000u, 0x00000001u, 0x12345678u, 0x7f7fffffu, 0x3f800000u, 0x80000000u, 0xbf800000u, 0x00800000u, + 0x007fffffu, 0x80000001u, 0x7f800000u, 0xff800000u, 0x7fc00000u, 0x7f800001u, 0xffffffffu, 0x7fbfffffu, +}; + +constexpr std::array<std::uint32_t, 8> Edges16{ + 0x0000u, 0x0001u, 0x1234u, 0x7fffu, 0x8000u, 0x8001u, 0xfffeu, 0xffffu, +}; + +alignas(4096) std::array<std::uint8_t, 3u * SlotBytes + 65536u> UntrackedStorage{}; +alignas(256) std::array<std::uint32_t, Texels> UntrackedOutput{}; + +class GuestBlock { +public: + GuestBlock() { +#ifdef _WIN32 + block = static_cast<std::uint8_t*>(VirtualAlloc(nullptr, BlockBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); +#else + block = static_cast<std::uint8_t*>(std::aligned_alloc(BlockBytes, BlockBytes)); +#endif + Require(block != nullptr, "depth bits guest memory: cannot allocate the guest block"); + std::memset(block, 0, BlockBytes); + GuestAllocations::Mutation().Add(block, BlockBytes, true, true, true); + } + + ~GuestBlock() { + GuestAllocations::Mutation().Remove(block); +#ifdef _WIN32 + VirtualFree(block, 0, MEM_RELEASE); +#else + std::free(block); +#endif + } + + GuestBlock(const GuestBlock&) = delete; + GuestBlock& operator=(const GuestBlock&) = delete; + + std::uint8_t* Slots() { return block; } + std::span<std::uint32_t> Output() { return {reinterpret_cast<std::uint32_t*>(block + 3u * SlotBytes), Texels}; } + +private: + std::uint8_t* block = nullptr; +}; + +struct Memory { + const char* name; + std::uint8_t* slots; + std::span<std::uint32_t> output; +}; + +struct Case { + const char* name; + std::uint32_t format; + std::uint32_t elementBytes; + std::uint32_t slot; +}; + +constexpr std::array<Case, 3> Cases{{ + {"32_UINT", Format32UInt, 4u, 0u}, + {"32_SINT", Format32SInt, 4u, 1u}, + {"16_UINT", Format16UInt, 2u, 2u}, +}}; + +static_assert(Texels * 4u <= SlotBytes && 4u * SlotBytes <= BlockBytes, "the slots and the output must fit the guest block"); + +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + +std::uint32_t Mix(std::uint32_t x, std::uint32_t y) { + auto hash = (x * 0x9e3779b1u) ^ (y * 0x85ebca77u); + hash ^= hash >> 15u; + hash *= 0x2c1b3c6du; + hash ^= hash >> 12u; + return hash; +} + +std::uint32_t Pattern(const Case& test, std::uint32_t x, std::uint32_t y) { + const bool edge = (x & 15u) == (y & 15u); + if (test.elementBytes == 2u) return edge ? Edges16[(x / 16u + y) % Edges16.size()] : Mix(x, y) & 0xffffu; + return edge ? Edges32[(x / 16u + y) % Edges32.size()] : Mix(x, y); +} + +std::array<std::uint32_t, 4> BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast<std::uintptr_t>(data); + return {static_cast<std::uint32_t>(address), static_cast<std::uint32_t>((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +std::array<std::uint32_t, 8> TextureDescriptor(const void* data, std::uint32_t format) { + const auto address = static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(data)); + return { + static_cast<std::uint32_t>(address >> 8u), + static_cast<std::uint32_t>((address >> 40u) & 0xffu) | (format << 20u) | (((Width - 1u) & 3u) << 30u), + ((Width - 1u) >> 2u) | ((Height - 1u) << 14u), + IdentitySwizzle | (Swizzle64KbZX << 20u) | (Type2D << 28u), + 0u, 0u, 0u, 0u, + }; +} + +std::uint64_t EquationOffset(const TextureSwizzleEquation& equation, std::uint32_t x, std::uint32_t y) { + std::uint64_t offset = 0; + for (std::uint32_t bit = 0; bit < 16; ++bit) { + const auto mask = equation.bits[bit]; + const auto selected = (x & (mask & 0xfffu)) ^ ((y << 12) & (mask & 0xfff000u)); + offset |= static_cast<std::uint64_t>(std::popcount(selected) & 1u) << bit; + } + return offset; +} + +std::uint8_t* SlotOf(const Memory& memory, const Case& test) { + return memory.slots + test.slot * SlotBytes; +} + +void Fill(const Memory& memory, const Case& test) { + auto* texels = SlotOf(memory, test); + const auto descriptor = DecodeTextureResource(TextureDescriptor(texels, test.format)); + Require(descriptor.baseAddress == reinterpret_cast<std::uintptr_t>(texels) && descriptor.pipeBankXor == 0u, std::string(test.name) + ": the texels are not 64 KiB aligned"); + const auto geometry = DescribeSurface(descriptor); + Require(geometry.guestBytes <= SlotBytes, std::string(test.name) + ": the surface does not fit its slot"); + const auto* equation = FindTextureSwizzleEquation(XorSwizzleMode(descriptor.tileMode), test.elementBytes); + Require(equation != nullptr, std::string(test.name) + ": the SW_64KB_Z_X equation is missing"); + const auto& mip = geometry.mips.at(0); + Require(!mip.tail, std::string(test.name) + ": the base level is a tail mip"); + const auto layout = ThinBlockLayout(descriptor.tileMode, test.elementBytes); + std::fill(texels, texels + SlotBytes, std::uint8_t{0xee}); + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + const auto offset = mip.tiledOffset + (static_cast<std::uint64_t>(y / layout[2]) * mip.blocksPerRow + x / layout[1]) * layout[0] + EquationOffset(*equation, x, y); + Require(offset + test.elementBytes <= geometry.guestBytes, std::string(test.name) + ": a texel lies outside the surface"); + const auto value = Pattern(test, x, y); + std::memcpy(texels + offset, &value, test.elementBytes); + } + } +} + +void Read(AgcDriver::VulkanDevice& device, const Memory& memory, const Case& test) { + std::fill(memory.output.begin(), memory.output.end(), Unwritten); + std::vector<std::uint32_t> userData(16, 0u); + const auto output = BufferDescriptor(memory.output.data(), static_cast<std::uint32_t>(memory.output.size_bytes())); + const auto texture = TextureDescriptor(SlotOf(memory, test), test.format); + std::copy(output.begin(), output.end(), userData.begin()); + std::copy(texture.begin(), texture.end(), userData.begin() + 4); + const std::span<const std::uint32_t> code(RowCode); + const std::array<ShaderRecompiler::MemoryRegion, 1> regions{{{reinterpret_cast<std::uintptr_t>(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Width, 1, 1}, 0u, {true, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast<std::uintptr_t>(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, regions}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, Height, 1, 1, {}, reinterpret_cast<std::uintptr_t>(code.data())); + device.WaitIdle(); +} + +void Check(const Memory& memory, const Case& test) { + std::uint32_t wrong = 0; + std::string first; + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + const auto expected = Pattern(test, x, y); + const auto actual = memory.output[y * Width + x]; + if (actual == expected) continue; + if (wrong == 0) first = "(" + std::to_string(x) + ", " + std::to_string(y) + ") read " + Hex(actual) + ", expected " + Hex(expected); + ++wrong; + } + } + Require(wrong == 0, std::string(test.name) + " SW_64KB_Z_X depth bits from " + memory.name + ": " + std::to_string(wrong) + " of " + std::to_string(Texels) + " texels differ, first " + first); +} + +} + +int main() { + try { + GuestBlock block; + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + auto* untracked = UntrackedStorage.data() + ((0u - reinterpret_cast<std::uintptr_t>(UntrackedStorage.data())) & 0xffffu); + const std::array<Memory, 2> memories{{ + {"untracked memory", untracked, UntrackedOutput}, + {"a guest allocation", block.Slots(), block.Output()}, + }}; + for (const auto& memory : memories) { + for (const auto& test : Cases) { + Fill(memory, test); + Read(*device, memory, test); + Check(memory, test); + } + } + std::puts("depth bits guest memory tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} From a7499e072f1e796e8d0172dcc76bc488174f4586 Mon Sep 17 00:00:00 2001 From: thuanquang <ghoulbulldog@gmail.com> Date: Sun, 18 Oct 2026 10:21:01 +0700 Subject: [PATCH 0952/1000] fix(relinker): skip a dumper's backup copy of a module beside the module Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- core/relinker/cli/src/NativeModules.cpp | 26 +++++++++++++ .../tests/test_guest_module_directories.py | 39 +++++++++++++++++++ docs/user/USAGE.md | 2 +- 3 files changed, 66 insertions(+), 1 deletion(-) diff --git a/core/relinker/cli/src/NativeModules.cpp b/core/relinker/cli/src/NativeModules.cpp index f3839033c8..f6e5bc3100 100644 --- a/core/relinker/cli/src/NativeModules.cpp +++ b/core/relinker/cli/src/NativeModules.cpp @@ -4,6 +4,7 @@ #include <algorithm> #include <fstream> #include <iostream> +#include <map> namespace Cli { @@ -77,6 +78,31 @@ NativeModules ReadModules(const std::filesystem::path& inputPath, const std::fil result.Root = root; result.RuntimeDirectories = std::move(runtimeDirectories); for (const auto& path : paths) result.Modules.push_back({path, Io::FileReader().Read(Io::Utf8Path(path))}); + std::map<std::size_t, std::string> sonames; + const auto soname = [&](std::size_t index) -> const std::string& { + const auto [found, inserted] = sonames.try_emplace(index); + if (inserted) found->second = GuestImageReader().Read(result.Modules[index].Path, result.Modules[index].Bytes).Soname; + return found->second; + }; + std::vector<bool> copies(result.Modules.size()); + for (std::size_t copy = 0; copy < result.Modules.size(); ++copy) { + const auto& path = result.Modules[copy].Path; + const auto name = Io::Utf8Path(path.filename()); + for (std::size_t original = 0; original < result.Modules.size(); ++original) { + const auto& kept = result.Modules[original].Path; + const auto prefix = Io::Utf8Path(kept.filename()); + if (kept.parent_path() != path.parent_path() || prefix.size() >= name.size() || !name.starts_with(prefix)) continue; + if (soname(copy).empty() || soname(copy) != soname(original)) continue; + copies[copy] = true; + std::cout << "Skipping backup copy: " << path.lexically_relative(root).generic_string() << " (same SONAME as " + << kept.lexically_relative(root).generic_string() << ")\n"; + break; + } + } + std::vector<GuestModuleInput> modules; + for (std::size_t index = 0; index < result.Modules.size(); ++index) + if (!copies[index]) modules.push_back(std::move(result.Modules[index])); + result.Modules = std::move(modules); return result; } diff --git a/core/relinker/relinker/tests/test_guest_module_directories.py b/core/relinker/relinker/tests/test_guest_module_directories.py index f263965ffe..fcbf2375e1 100644 --- a/core/relinker/relinker/tests/test_guest_module_directories.py +++ b/core/relinker/relinker/tests/test_guest_module_directories.py @@ -5,6 +5,7 @@ import subprocess import sys import tempfile +from guest_import_fixtures import dynamic from test_guest_intel_trampolines import PLAIN_SITE, elf_loads, guest_fixture, main_fixture @@ -23,6 +24,15 @@ def module_with_symbol(exported): return image +def module_with_soname(soname): + image = module_with_symbol(True) + strings = b"\0shared#A#B\0" + soname.encode() + b"\0" + image[0x800:0x800 + len(strings)] = strings + dynamic(image, 0x600, 176, [(5, 0x2200), (10, len(strings)), (6, 0x2280), (11, 24), + (4, 0x2240), (7, 0x2300), (8, 0), (9, 24), (14, 12)]) + return image + + def needed_libraries(image): loads = elf_loads(image) @@ -128,6 +138,35 @@ def main(): assert warning not in result.stdout, result.stdout assert {path.name for path in (case / "app0").rglob("*.guest.prx")} == {"provider.prx.guest.prx"} + case = work / f"{windows}-backup-copies" + (case / "sce_module").mkdir(parents=True) + for name in ("libSceFace.prx", "libSceFace.prx.esbak", "libSceFace.prxbak"): + (case / "sce_module" / name).write_bytes(module_with_soname("libSceFace.prx")) + result, output = convert(case, windows) + assert result.returncode == 0 and output.exists(), (result.stdout, result.stderr) + for name in ("libSceFace.prx.esbak", "libSceFace.prxbak"): + skipped = f"Skipping backup copy: sce_module/{name} (same SONAME as sce_module/libSceFace.prx)" + assert result.stdout.count(skipped) == 1, result.stdout + assert {path.name for path in (case / "app0").rglob("*.guest.prx")} == {"libSceFace.prx.guest.prx"} + + case = work / f"{windows}-prefixed-libraries" + (case / "sce_module").mkdir(parents=True) + for name in ("libfoo.so.1", "libfoo.so.10"): + (case / "sce_module" / name).write_bytes(module_with_soname(name)) + result, output = convert(case, windows) + assert result.returncode == 0 and output.exists(), (result.stdout, result.stderr) + assert "Skipping backup copy" not in result.stdout, result.stdout + assert {path.name for path in (case / "app0").rglob("*.guest.prx")} == {"libfoo.so.1.guest.prx", "libfoo.so.10.guest.prx"} + + case = work / f"{windows}-backup-of-self" + (case / "sce_module").mkdir(parents=True) + (case / "sce_module" / "libSceFace.prx").write_bytes(b"\x4f\x15\x3d\x1d" + bytes(0x1000)) + (case / "sce_module" / "libSceFace.prx.esbak").write_bytes(module_with_soname("libSceFace.prx")) + result, output = convert(case, windows) + assert result.returncode == 0 and output.exists(), (result.stdout, result.stderr) + assert "Skipping backup copy" not in result.stdout, result.stdout + assert {path.name for path in (case / "app0").rglob("*.guest.prx")} == {"libSceFace.prx.esbak.guest.prx"} + case = work / f"{windows}-ambiguous" for name in ("sce_module", "sce_modules", "prx"): (case / name).mkdir(parents=True) diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index 068662e2ce..663fe29811 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -8,7 +8,7 @@ The online converter can save Linux output as an uncompressed folder in supporte ## Input and conversion -Use a clean ELF executable. Place its bundled ELF modules in `sce_module/`, `sce_modules/`, or `prx/` beside the input executable, or under the parent directory selected with `--sce-module-path <path>`. The option accepts absolute paths and paths relative to the current working directory; do not include the `sce_module` directory itself. Only files directly inside the module directories are processed. SELF/fSELF containers are skipped with a warning; unwrap them before relinking if they need to be included. `tools/unwrap_self.py <input.self> <output.elf>` does that for a decrypted container: it places the segments at the offsets of the SELF segment records and restores `p_offset % p_align == p_vaddr % p_align`, and it refuses a container whose load segments all look encrypted. `prx/` can coexist with either `sce_module/` or `sce_modules/`. Both `sce_module/` and `sce_modules/` present, or all three absent, is an error. +Use a clean ELF executable. Place its bundled ELF modules in `sce_module/`, `sce_modules/`, or `prx/` beside the input executable, or under the parent directory selected with `--sce-module-path <path>`. The option accepts absolute paths and paths relative to the current working directory; do not include the `sce_module` directory itself. Only files directly inside the module directories are processed. SELF/fSELF containers are skipped with a warning; unwrap them before relinking if they need to be included. `tools/unwrap_self.py <input.self> <output.elf>` does that for a decrypted container: it places the segments at the offsets of the SELF segment records and restores `p_offset % p_align == p_vaddr % p_align`, and it refuses a container whose load segments all look encrypted. The command-line relinker skips an ELF module as a backup copy, with a message, when its file name extends the name of another ELF module in the same directory and both have the same SONAME, such as a dumper's `libX.prx.esbak` or `libX.prxbak` beside an ELF `libX.prx`; a single backup beside a SELF container is still relinked. `prx/` can coexist with either `sce_module/` or `sce_modules/`. Both `sce_module/` and `sce_modules/` present, or all three absent, is an error. ```text source/ From fd301c78a5d625d6eba8b8a32e5746a044fc235e Mon Sep 17 00:00:00 2001 From: Dean Galvin <dean.galvin@live.com> Date: Sun, 18 Oct 2026 16:47:05 +1300 Subject: [PATCH 0953/1000] perf(agc): release settle-retired imports after the overlap block of a pin wait The Windows release of imports retired during a settle read belongs after the whole overlapsImport block in the not-holding-the-lock branch of WaitForLeasesAndImports, as in the original change, not inside it. It was placed inside while main was merged in. --- .../prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index 396a821b16..4f14db8c4a 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -2570,9 +2570,6 @@ bool WaitForLeasesAndImports(std::uintptr_t address, std::size_t bytes) { retiredImport = true; } } -#ifdef _WIN32 - if (releaseSettleRetired(imports, address, end)) retiredImport = true; -#endif if (retiredImport && recorder != nullptr && !recorder->Idle()) { Recorder::CountSync(4); recorder->Sync(); @@ -2580,6 +2577,9 @@ bool WaitForLeasesAndImports(std::uintptr_t address, std::size_t bytes) { drained = true; } } +#ifdef _WIN32 + if (releaseSettleRetired(imports, address, end)) retiredImport = true; +#endif } auto& state = Leases(); std::lock_guard lock(state.mutex); From 432ad483937ad434117891b94063d6a759fc4af4 Mon Sep 17 00:00:00 2001 From: Nithishvelan M <mnithishvelan@gmail.com> Date: Sun, 18 Oct 2026 09:23:56 +0530 Subject: [PATCH 0954/1000] test(libSceUserService): unwind guest_user_service_tests on Windows Register unwind tables on Windows via configure_windows_unwind so that throwing stubs tested in GuestUserService can be caught without unhandled runtime_error aborting the test runner. --- core/libs/prx/libSceUserService/CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/core/libs/prx/libSceUserService/CMakeLists.txt b/core/libs/prx/libSceUserService/CMakeLists.txt index 449131a43a..6d606a67e8 100644 --- a/core/libs/prx/libSceUserService/CMakeLists.txt +++ b/core/libs/prx/libSceUserService/CMakeLists.txt @@ -6,6 +6,7 @@ if(BUILD_TESTING) add_test_executable(guest_user_service_tests ${LIBS_INCLUDE_DIR}/tests/GuestUserService.cpp) target_include_directories(guest_user_service_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_user_service_tests PRIVATE libSceUserService) + configure_windows_unwind(guest_user_service_tests) add_test(NAME guest_user_service COMMAND guest_user_service_tests) if(WIN32) set_tests_properties(guest_user_service PROPERTIES ENVIRONMENT_MODIFICATION From 9cc8e2c081fac2b2e4c88a20dc07e10e3284db86 Mon Sep 17 00:00:00 2001 From: Alex Callisaya <alex.callisaya@venturatravel.org> Date: Sun, 18 Oct 2026 00:11:15 -0400 Subject: [PATCH 0955/1000] fix(tools): clip trace digest totals to the frame window --- tools/tests/test_trace_digest.py | 37 ++++++++++++++++++++++++++++++++ tools/trace_digest.py | 16 +++++++++----- 2 files changed, 48 insertions(+), 5 deletions(-) diff --git a/tools/tests/test_trace_digest.py b/tools/tests/test_trace_digest.py index c46ba26290..0a52be7ab6 100644 --- a/tools/tests/test_trace_digest.py +++ b/tools/tests/test_trace_digest.py @@ -232,6 +232,43 @@ class TraceDigestTests(unittest.TestCase): self.assertAlmostEqual(presenter["wait_ms_per_frame"], 15.5 / 3) self.assertAlmostEqual(presenter["busy_ms_per_frame"], 2.5 / 3) + def test_thread_totals_only_count_time_inside_the_frame_window(self): + events = [ + complete(1, "Driver.Packet", -5000, 20000), + complete(1, "GpuMutex.wait.draw", -2000, 4000), + complete(1, "Driver.Packet.after", 12000, 1000), + complete(2, "Driver.WorkerWait", -10000, 2000), + {"name": "present", "ph": "i", "ts": 0, "tid": 1, "args": {"present": 600}}, + {"name": "present", "ph": "i", "ts": 10000, "tid": 1, "args": {"present": 601}}, + ] + result = trace_digest.digest(trace_digest.Trace(events)) + active, outside = result["threads"] + self.assertEqual(active["busy_ms_per_frame"], 8.0) + self.assertEqual(active["wait_ms_per_frame"], 2.0) + self.assertEqual(active["busy_pct"], 80.0) + self.assertEqual(active["wait_pct"], 20.0) + self.assertEqual(active["top"], [{"name": "Driver.Packet", "ms_per_frame": 10.0}]) + self.assertEqual(outside["busy_ms_per_frame"], 0.0) + self.assertEqual(outside["wait_ms_per_frame"], 0.0) + self.assertEqual(outside["untraced_ms_per_frame"], 10.0) + self.assertEqual(outside["top"], []) + self.assertEqual(result["top_slices_ms_per_frame"], [ + {"name": "Driver.Packet", "ms": 10.0}, + {"name": "GpuMutex.wait.draw", "ms": 2.0}, + ]) + + def test_thread_totals_use_the_summary_end_after_the_last_present(self): + trace = trace_digest.Trace([ + complete(1, "Driver.Packet", 1000, 20000), + {"name": "present", "ph": "i", "ts": 0, "tid": 1, "args": {"present": 600}}, + ]) + result = trace_digest.digest(trace, {"window": {"ms": 10.0}}) + thread = result["threads"][0] + self.assertEqual(result["frames"], 1) + self.assertEqual(thread["busy_ms_per_frame"], 9.0) + self.assertEqual(thread["busy_pct"], 90.0) + self.assertEqual(thread["untraced_ms_per_frame"], 1.0) + def test_largest_slices_per_frame(self): largest = self.result()["top_slices_ms_per_frame"] self.assertEqual(largest[0]["name"], "Driver.Packet") diff --git a/tools/trace_digest.py b/tools/trace_digest.py index 9753a91d8a..7cd2fe3d85 100644 --- a/tools/trace_digest.py +++ b/tools/trace_digest.py @@ -130,13 +130,19 @@ def digest(trace, summary=None, stacks=None, worst=5, top=10): waits = [] for tid in sorted(trace.slices.keys() | trace.folded.keys()): slices = trace.slices.get(tid, []) - traced = sum(dur for _, dur, _, depth, _ in slices if depth == 0) - waited = sum(dur for _, dur, name, _, nested in slices if is_wait(name) and not nested) + traced = 0.0 + waited = 0.0 leaders = collections.Counter() for ts, dur, name, depth, nested in slices: - totals[name] += dur - if depth == 0 and not is_wait(name): - leaders[name] += dur + overlap = max(min(ts + dur, frames[-1][2]) - max(ts, frames[0][1]), 0.0) if frames else dur + if overlap > 0: + totals[name] += overlap + if depth == 0: + traced += overlap + if not is_wait(name): + leaders[name] += overlap + if is_wait(name) and not nested: + waited += overlap if is_wait(name) and not nested: waits.append((dur, name, trace.name(tid), ts)) threads.append({"thread": trace.name(tid), "busy_ms_per_frame": (traced - waited) / 1000.0 / count, From 813fdda24dfc671a0aab2d1f61061b06efb12c0d Mon Sep 17 00:00:00 2001 From: pushkarreddyy <212133856+pushkarreddyy@users.noreply.github.com> Date: Sun, 18 Oct 2026 08:42:28 +0530 Subject: [PATCH 0956/1000] fix(json2): return parse error on null text or zero size --- core/libs/prx/libSceJson2/Export.cpp | 2 +- core/libs/tests/GuestJson.cpp | 5 +++++ docs/dev/debt/libSceJson2.md | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceJson2/Export.cpp b/core/libs/prx/libSceJson2/Export.cpp index adb79106c5..4399dcfb47 100644 --- a/core/libs/prx/libSceJson2/Export.cpp +++ b/core/libs/prx/libSceJson2/Export.cpp @@ -809,7 +809,7 @@ int APS5_VABI _ZN3sce4Json6Parser5parseERNS0_5ValueEPFiRcPvES5_(Value* out, Pars } int APS5_VABI _ZN3sce4Json6Parser5parseERNS0_5ValueEPKcm(Value* out, const char* text, std::size_t size) { - if (text == nullptr) throw std::invalid_argument("sce::Json::Parser::parse: null text"); + if (text == nullptr || size == 0) return JsonErrorParse; Node parsed{}; Parser parser(text, size); bool parsedOk = false; diff --git a/core/libs/tests/GuestJson.cpp b/core/libs/tests/GuestJson.cpp index a520e90225..29761c381e 100644 --- a/core/libs/tests/GuestJson.cpp +++ b/core/libs/tests/GuestJson.cpp @@ -124,6 +124,7 @@ static void Check(bool value, int line) { #define Require(value) Check((value), __LINE__) enum : std::int32_t { TypeNull, TypeBoolean, TypeInteger, TypeUInteger, TypeReal, TypeString, TypeArray, TypeObject }; +constexpr int JsonErrorParse = static_cast<int>(0x80920101); static Value fallback{}; static int callbackCalls = 0; @@ -209,6 +210,10 @@ static void ParseAndRoundTrip() { Require(_ZN3sce4Json6Parser5parseERNS0_5ValueEPKcm(&root, invalid, std::strlen(invalid)) < 0); Require(_ZNK3sce4Json5Value7getTypeEv(&root) == TypeObject && _ZNK3sce4Json5Value5countEv(&root) == 8); } + Require(_ZN3sce4Json6Parser5parseERNS0_5ValueEPKcm(&root, nullptr, 0) == JsonErrorParse); + Require(_ZN3sce4Json6Parser5parseERNS0_5ValueEPKcm(&root, nullptr, 10) == JsonErrorParse); + Require(_ZN3sce4Json6Parser5parseERNS0_5ValueEPKcm(&root, "", 0) == JsonErrorParse); + Require(_ZNK3sce4Json5Value7getTypeEv(&root) == TypeObject && _ZNK3sce4Json5Value5countEv(&root) == 8); for (const char* overflow : {"1e309", "[-1e309]", "{\"a\":[1,1e400]}"}) { bool unverified = false; try { diff --git a/docs/dev/debt/libSceJson2.md b/docs/dev/debt/libSceJson2.md index f723880a61..b6023a5f29 100644 --- a/docs/dev/debt/libSceJson2.md +++ b/docs/dev/debt/libSceJson2.md @@ -3,7 +3,7 @@ ### Unknown function info - [sce::Json::Parser::parse with a read callback](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - the callback's contract is taken from PPSA04540's: it returns 0 with the next character, and at the end of its data writes `'\0'` and returns -1. Characters are read until the callback returns non-zero or gives `'\0'`, and the text read is then parsed as by the `(text, size)` overload, so trailing characters after the value are rejected as there. Whether the console stops reading at the end of the top-level value, and what it returns when the callback fails before the end, are unknown -- [sce::Json::Parser::parse](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - a number that overflows a double (e.g. `1e309`) throws: whether the console rejects the document or stores an infinite real is unknown +- [sce::Json::Parser::parse](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - a null text pointer or zero size returns `JsonErrorParse` (0x80920101) without modifying the destination Value; whether the console distinguishes a null pointer from a syntax error with a separate parameter error code is unknown. A number that overflows a double (e.g. `1e309`) throws: whether the console rejects the document or stores an infinite real is unknown - [sce::Json::InitParameter2, InitParameterRtti and InitParameterRtti2](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - layouts inferred, not known. `InitParameter2` is taken as the 40-byte object of [SharpEmu](https://github.com/sharpemu/sharpemu/blob/1811cef94435db7655209fb39f65ddf258b93fd6/src/SharpEmu.Libs/Json/JsonExports.cs) and [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/eda6ba37ece127ab0396378404d99182e3e8a530/src/libs/libJson2.cpp): the allocator, its user data, the file buffer size, the 32-bit special float format type at 0x18 and 12 bytes both zero, and both constructors zero all of it. `InitParameterRtti2::setAllocatorRtti` writes the allocation parameters and user data where `setAllocator` writes the allocator, as both references do; whether the console also marks the object as using RTTI allocation is unknown. `InitParameterRtti`'s constructor writes the allocation parameters, user data and file buffer size to the first 24 bytes, an order inferred from its mangled arguments. The values of `SpecialFloatFormatType` and what they select are unknown, so `setSpecialFloatFormatType` stores any value and nothing reads it: `Initializer::initialize(InitParameter2)` ignores every field as `initialize(InitParameter)` does, serializing a non-finite real throws whatever the type, and whether the type lets the parser accept NaN or infinity is unknown. The setters return nothing; whether the console's return `this` is unknown - [sce::Json::Object::Pair and sce::Json::Array::iterator members](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) imported by PPSA30601 - the signatures follow the mangled names, with `Array::iterator::operator++(int)` returning its copy through a result pointer before `this` as `Array::begin` does, since the iterator has a non-trivial destructor. `Pair(const String&, const Value&)` deep-copies both into the layout `Object` iteration already hands out (key at 0, value at 0x10) and zeroes the 8 bytes between them; what the console keeps there is unknown. The default-constructed iterator is singular - [sce::Json::MemAllocator::notifyError](../../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - the base class's virtual does nothing. A title's allocator that does not override it calls it through its own vtable; the library never calls the title's allocator, so it never reports an allocation failure. Whether the console's base implementation logs anything is unknown From dec0e2013c66eccd9278bd832b4a92dc5256474c Mon Sep 17 00:00:00 2001 From: Nithishvelan M <mnithishvelan@gmail.com> Date: Sun, 18 Oct 2026 10:01:34 +0530 Subject: [PATCH 0957/1000] feat(libScePlayerSelectionDialog): export scePlayerSelectionDialogGetStatus and scePlayerSelectionDialogClose Implement scePlayerSelectionDialogGetStatus and scePlayerSelectionDialogClose in libScePlayerSelectionDialog, matching the common dialog lifecycle convention across player dialogs, and add unit test coverage in GuestPlayerSelectionDialog. --- core/libs/prx/libScePlayerSelectionDialog/Export.cpp | 10 ++++++++++ core/libs/tests/GuestPlayerSelectionDialog.cpp | 11 +++++++++++ docs/dev/debt/libScePlayerSelectionDialog.md | 2 +- 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libScePlayerSelectionDialog/Export.cpp b/core/libs/prx/libScePlayerSelectionDialog/Export.cpp index 6093f69d67..d866a19fe2 100644 --- a/core/libs/prx/libScePlayerSelectionDialog/Export.cpp +++ b/core/libs/prx/libScePlayerSelectionDialog/Export.cpp @@ -39,10 +39,20 @@ int APS5_VABI scePlayerSelectionDialogOpen(const void* param) { return 0; } +int APS5_VABI scePlayerSelectionDialogClose(void) { + if (g_status.load() == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + g_status = COMMON_DIALOG_STATUS_FINISHED; + return 0; +} + int APS5_VABI scePlayerSelectionDialogUpdateStatus(void) { return g_status.load(); } +int APS5_VABI scePlayerSelectionDialogGetStatus(void) { + return g_status.load(); +} + int APS5_VABI scePlayerSelectionDialogGetResult(void* result) { const int status = g_status.load(); if (status == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; diff --git a/core/libs/tests/GuestPlayerSelectionDialog.cpp b/core/libs/tests/GuestPlayerSelectionDialog.cpp index cd5d62693e..639a8297fe 100644 --- a/core/libs/tests/GuestPlayerSelectionDialog.cpp +++ b/core/libs/tests/GuestPlayerSelectionDialog.cpp @@ -6,7 +6,9 @@ extern "C" { int APS5_VABI scePlayerSelectionDialogInitialize(void); int APS5_VABI scePlayerSelectionDialogOpen(const void* param); int APS5_VABI scePlayerSelectionDialogUpdateStatus(void); +int APS5_VABI scePlayerSelectionDialogGetStatus(void); int APS5_VABI scePlayerSelectionDialogGetResult(void* result); +int APS5_VABI scePlayerSelectionDialogClose(void); int APS5_VABI scePlayerSelectionDialogTerminate(void); } @@ -29,15 +31,21 @@ int main() { Require(scePlayerSelectionDialogTerminate() == kErrNotInitialized); Require(scePlayerSelectionDialogOpen(¶m) == kErrNotInitialized); + Require(scePlayerSelectionDialogClose() == kErrNotInitialized); Require(scePlayerSelectionDialogGetResult(result) == kErrNotInitialized); + Require(scePlayerSelectionDialogGetStatus() == 0); + Require(scePlayerSelectionDialogUpdateStatus() == 0); Require(scePlayerSelectionDialogInitialize() == 0); Require(scePlayerSelectionDialogInitialize() == kErrAlreadyInitialized); + Require(scePlayerSelectionDialogGetStatus() == 1); + Require(scePlayerSelectionDialogUpdateStatus() == 1); Require(scePlayerSelectionDialogOpen(nullptr) == kErrArgNull); Require(scePlayerSelectionDialogGetResult(result) == kErrNotFinished); Require(scePlayerSelectionDialogOpen(¶m) == 0); + Require(scePlayerSelectionDialogGetStatus() == kStatusFinished); Require(scePlayerSelectionDialogUpdateStatus() == kStatusFinished); Require(scePlayerSelectionDialogGetResult(nullptr) == kErrArgNull); @@ -45,6 +53,9 @@ int main() { Require(result[0] == kResultUserCanceled); Require(result[1] == -1); + Require(scePlayerSelectionDialogClose() == 0); Require(scePlayerSelectionDialogTerminate() == 0); Require(scePlayerSelectionDialogTerminate() == kErrNotInitialized); + Require(scePlayerSelectionDialogGetStatus() == 0); + Require(scePlayerSelectionDialogClose() == kErrNotInitialized); } diff --git a/docs/dev/debt/libScePlayerSelectionDialog.md b/docs/dev/debt/libScePlayerSelectionDialog.md index 602e740063..c8920961b9 100644 --- a/docs/dev/debt/libScePlayerSelectionDialog.md +++ b/docs/dev/debt/libScePlayerSelectionDialog.md @@ -2,4 +2,4 @@ ### Silent stubs -- [libScePlayerSelectionDialog](../../../core/libs/prx/libScePlayerSelectionDialog/Export.cpp) - the player-selection dialog runs the common-dialog state machine but shows nothing and reports the user as having cancelled. `GetResult` writes only the first `int32` (`USER_CANCELED`); the rest of `ScePlayerSelectionDialogResult` (player list pointer and length) is not filled. EA SPORTS FC 27 (PPSA34015) fails to load `libScePlayerSelectionDialog.prx` without `Initialize`, `Open`, `UpdateStatus` and `GetResult` (`unresolved ELF import` / `GetLastError: 127`) +- [libScePlayerSelectionDialog](../../../core/libs/prx/libScePlayerSelectionDialog/Export.cpp) - the player-selection dialog runs the common-dialog state machine but shows nothing and reports the user as having cancelled. `GetResult` writes only the first `int32` (`USER_CANCELED`); the rest of `ScePlayerSelectionDialogResult` (player list pointer and length) is not filled. EA SPORTS FC 27 (PPSA34015) fails to load `libScePlayerSelectionDialog.prx` without `Initialize`, `Open`, `Close`, `GetStatus`, `UpdateStatus` and `GetResult` (`unresolved ELF import` / `GetLastError: 127`) From 5a0fbcd9ddeac94f61bcac1866cbc6f562beda0f Mon Sep 17 00:00:00 2001 From: "husari.orozco" <husari09.oro@gmail.com> Date: Tue, 13 Oct 2026 13:43:30 -0600 Subject: [PATCH 0958/1000] feat(tools): list the APS5_* environment names the code reads --- CMakeLists.txt | 2 + tools/env_reference.py | 109 ++++++++++++++++++++++++++++++ tools/tests/test_env_reference.py | 85 +++++++++++++++++++++++ 3 files changed, 196 insertions(+) create mode 100644 tools/env_reference.py create mode 100644 tools/tests/test_env_reference.py diff --git a/CMakeLists.txt b/CMakeLists.txt index 48ecc49c93..8b7705614b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -195,6 +195,8 @@ if(BUILD_TESTING) add_test(NAME conventions_bom_shebang COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_bom_shebang.py) set_tests_properties(conventions_bom_shebang PROPERTIES TIMEOUT 20) + add_test(NAME env_reference COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_env_reference.py) add_test(NAME conventions_body_bom COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_body_bom.py) add_test(NAME conventions_blob_reads COMMAND ${Python3_EXECUTABLE} diff --git a/tools/env_reference.py b/tools/env_reference.py new file mode 100644 index 0000000000..3091ebadc1 --- /dev/null +++ b/tools/env_reference.py @@ -0,0 +1,109 @@ +import argparse +import re +import sys +from pathlib import Path + +PREFIXES = ("APS5_",) +DOC = "docs/dev/ENVIRONMENT.md" +ROOTS = ("core",) +EXTENSIONS = {".c", ".cc", ".cpp", ".cxx", ".h", ".hh", ".hpp", ".hxx", ".inl", ".ipp", ".py"} +SKIP = {"build", "build-linux", "build-relinker", "3rdparty", ".git"} + + +def pattern(prefixes): + name = "(?:" + "|".join(re.escape(prefix) for prefix in prefixes) + ")[A-Za-z0-9_]+" + return re.compile( + r"(?:std::)?getenv\(\s*\"(" + name + r")\"\s*\)" + r"|IsEnvironmentSet\(\s*\"(" + name + r")\"\s*\)" + r"|kibSetting\(\s*\"(" + name + r")\"" + r"|MaskFromEnvironment\(\s*\"(" + name + r")\"\s*\)" + ) + + +def documented_pattern(prefixes): + name = "(?:" + "|".join(re.escape(prefix) for prefix in prefixes) + ")[A-Za-z0-9_]+" + return re.compile(r"`(" + name + r")`") + + +def source_files(root): + root = Path(root) + if not root.is_dir(): + return + for path in sorted(root.rglob("*")): + if path.suffix not in EXTENSIONS or any(part in SKIP for part in path.parts): + continue + yield path + + +def reads(roots, prefixes=PREFIXES): + read = pattern(prefixes) + found = {} + for root in roots: + for path in source_files(root): + try: + text = path.read_text(encoding="utf-8") + except (OSError, UnicodeDecodeError): + continue + for number, line in enumerate(text.splitlines(), 1): + for match in read.finditer(line): + name = next(group for group in match.groups() if group) + found.setdefault(name, []).append((path.as_posix(), number)) + for sites in found.values(): + sites.sort() + return found + + +def documented(path, prefixes=PREFIXES): + try: + return set(documented_pattern(prefixes).findall(Path(path).read_text(encoding="utf-8"))) + except (OSError, UnicodeDecodeError): + return None + + +def list_names(found, out): + for name in sorted(found): + for path, number in found[name]: + out.write(f"{name}\t{path}:{number}\n") + + +def check(found, doc, prefixes, out): + names = documented(doc, prefixes) + if names is None: + out.write(f"error: cannot read {doc}\n") + return 1 + missing = sorted(set(found) - names) + stale = sorted(names - set(found)) + for name in missing: + path, number = found[name][0] + out.write(f"error: {name} is read at {path}:{number} but has no row in {doc}\n") + for name in stale: + out.write(f"error: {name} has a row in {doc} but the code no longer reads it\n") + if missing or stale: + out.write(f"{len(missing)} name(s) undocumented, {len(stale)} stale\n") + return 1 + out.write(f"{len(found)} name(s), all documented\n") + return 0 + + +def main(argv=None): + parser = argparse.ArgumentParser( + description="list the environment names the code reads, or check them against a reference document") + parser.add_argument("--check", action="store_true", + help="compare the names the code reads with the document and fail on a mismatch") + parser.add_argument("--doc", default=DOC, help="reference document to check (default: %(default)s)") + parser.add_argument("--root", action="append", default=None, + help="directory to scan (default: %s); repeatable" % ", ".join(ROOTS)) + parser.add_argument("--prefix", action="append", default=None, + help="name prefix to scan (default: %s); repeatable" % ", ".join(PREFIXES)) + args = parser.parse_args(argv) + roots = args.root if args.root else ROOTS + prefixes = tuple(args.prefix) if args.prefix else PREFIXES + found = reads(roots, prefixes) + if args.check: + return check(found, args.doc, prefixes, sys.stdout) + list_names(found, sys.stdout) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/tests/test_env_reference.py b/tools/tests/test_env_reference.py new file mode 100644 index 0000000000..adff46647a --- /dev/null +++ b/tools/tests/test_env_reference.py @@ -0,0 +1,85 @@ +import io +import sys +import tempfile +import unittest +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +import env_reference + +BOTH = ("APS5_", "ANYPS5_") + +CODE = """static bool a = std::getenv("APS5_PROFILE_DRAW") != nullptr; +static bool b = IsEnvironmentSet("APS5_TRACE_GPU"); +static unsigned long c = kibSetting("APS5_HOST_IMPORT_MIB", 6144); +static unsigned long d = MaskFromEnvironment("APS5_WORKER_AFFINITY_MASK"); +static const char* e = std::getenv("ANYPS5_GPU"); +static const char* f = std::getenv("PATH"); +static const char* g = std::getenv("TZ"); +""" + + +class EnvReferenceTests(unittest.TestCase): + def fixture(self): + directory = tempfile.TemporaryDirectory() + self.addCleanup(directory.cleanup) + root = Path(directory.name) + (root / "core").mkdir() + (root / "core" / "Sample.cpp").write_text(CODE, encoding="utf-8") + return root + + def test_reads_find_every_reader_form_and_ignore_other_names(self): + root = self.fixture() + found = env_reference.reads([root / "core"], BOTH) + self.assertEqual( + set(found), + {"APS5_PROFILE_DRAW", "APS5_TRACE_GPU", "APS5_HOST_IMPORT_MIB", + "APS5_WORKER_AFFINITY_MASK", "ANYPS5_GPU"}) + self.assertEqual(found["APS5_PROFILE_DRAW"][0][1], 1) + + def test_the_default_prefix_scans_only_aps5(self): + root = self.fixture() + found = env_reference.reads([root / "core"]) + self.assertTrue(found) + self.assertFalse([name for name in found if name.startswith("ANYPS5_")]) + + def test_check_passes_when_every_read_name_has_a_row(self): + root = self.fixture() + found = env_reference.reads([root / "core"]) + doc = root / "ENVIRONMENT.md" + doc.write_text("\n".join("| `%s` |" % name for name in found), encoding="utf-8") + out = io.StringIO() + self.assertEqual(env_reference.check(found, doc, env_reference.PREFIXES, out), 0) + + def test_check_reports_a_read_name_without_a_row(self): + root = self.fixture() + found = env_reference.reads([root / "core"]) + doc = root / "ENVIRONMENT.md" + doc.write_text("| `APS5_PROFILE_DRAW` |", encoding="utf-8") + out = io.StringIO() + self.assertEqual(env_reference.check(found, doc, env_reference.PREFIXES, out), 1) + self.assertIn("APS5_TRACE_GPU", out.getvalue()) + self.assertIn("has no row", out.getvalue()) + + def test_check_reports_a_row_the_code_no_longer_reads(self): + root = self.fixture() + found = env_reference.reads([root / "core"]) + doc = root / "ENVIRONMENT.md" + names = ["| `%s` |" % name for name in found] + ["| `APS5_REMOVED` |"] + doc.write_text("\n".join(names), encoding="utf-8") + out = io.StringIO() + self.assertEqual(env_reference.check(found, doc, env_reference.PREFIXES, out), 1) + self.assertIn("APS5_REMOVED", out.getvalue()) + self.assertIn("no longer reads", out.getvalue()) + + def test_check_fails_when_the_document_is_missing(self): + root = self.fixture() + found = env_reference.reads([root / "core"]) + out = io.StringIO() + self.assertEqual( + env_reference.check(found, root / "missing.md", env_reference.PREFIXES, out), 1) + self.assertIn("cannot read", out.getvalue()) + + +if __name__ == "__main__": + unittest.main() From a084b22f23bbaf9cd4a91fb793822ebd2de4e0a5 Mon Sep 17 00:00:00 2001 From: "husari.orozco" <husari09.oro@gmail.com> Date: Wed, 14 Oct 2026 13:54:55 -0600 Subject: [PATCH 0959/1000] feat(tools): cover both prefixes and report the kind and default --- tools/env_reference.py | 92 +++++++++++++++++++++++++++---- tools/tests/test_env_reference.py | 5 +- 2 files changed, 83 insertions(+), 14 deletions(-) diff --git a/tools/env_reference.py b/tools/env_reference.py index 3091ebadc1..045f56746a 100644 --- a/tools/env_reference.py +++ b/tools/env_reference.py @@ -3,20 +3,28 @@ import re import sys from pathlib import Path -PREFIXES = ("APS5_",) +PREFIXES = ("APS5_", "ANYPS5_") DOC = "docs/dev/ENVIRONMENT.md" ROOTS = ("core",) EXTENSIONS = {".c", ".cc", ".cpp", ".cxx", ".h", ".hh", ".hpp", ".hxx", ".inl", ".ipp", ".py"} SKIP = {"build", "build-linux", "build-relinker", "3rdparty", ".git"} +READERS = { + "IsEnvironmentSet": "presence", + "kibSetting": "integer", + "MaskFromEnvironment": "integer", +} +NULL_TEST = re.compile(r"[!=]=\s*(nullptr|NULL|0)\b|!\s*(?:std::)?getenv\(") +INTEGER_TEST = re.compile(r"\b(atoi|strtol|strtoul|strtoll|strtoull|stoi|stol|stoul|from_chars|atoi64)\s*\(") + def pattern(prefixes): name = "(?:" + "|".join(re.escape(prefix) for prefix in prefixes) + ")[A-Za-z0-9_]+" return re.compile( - r"(?:std::)?getenv\(\s*\"(" + name + r")\"\s*\)" - r"|IsEnvironmentSet\(\s*\"(" + name + r")\"\s*\)" - r"|kibSetting\(\s*\"(" + name + r")\"" - r"|MaskFromEnvironment\(\s*\"(" + name + r")\"\s*\)" + r"(?P<getenv>(?:std::)?getenv)\(\s*\"(?P<getenv_name>" + name + r")\"\s*\)" + r"|IsEnvironmentSet\(\s*\"(?P<presence_name>" + name + r")\"\s*\)" + r"|MaskFromEnvironment\(\s*\"(?P<mask_name>" + name + r")\"\s*\)" + r"|kibSetting\(\s*\"(?P<kib_name>" + name + r")\"\s*,\s*(?P<kib_default>[0-9]+)" ) @@ -25,6 +33,25 @@ def documented_pattern(prefixes): return re.compile(r"`(" + name + r")`") +def kind_of(reader, window): + if reader == "IsEnvironmentSet": + return "presence" + if reader in ("kibSetting", "MaskFromEnvironment"): + return "integer" + if NULL_TEST.search(window): + return "presence" + if INTEGER_TEST.search(window): + return "integer" + return "string" + + +def default_of(match, window): + if match.group("kib_default"): + return match.group("kib_default") + found = re.search(r"\?[\s\S]*?:\s*([0-9]+)", window) + return found.group(1) if found else "" + + def source_files(root): root = Path(root) if not root.is_dir(): @@ -44,10 +71,24 @@ def reads(roots, prefixes=PREFIXES): text = path.read_text(encoding="utf-8") except (OSError, UnicodeDecodeError): continue - for number, line in enumerate(text.splitlines(), 1): + lines = text.splitlines() + for number, line in enumerate(lines, 1): + if not read.search(line): + continue + window = "\n".join(lines[number - 1:number + 2]) for match in read.finditer(line): - name = next(group for group in match.groups() if group) - found.setdefault(name, []).append((path.as_posix(), number)) + name = (match.group("getenv_name") or match.group("presence_name") or + match.group("mask_name") or match.group("kib_name")) + if match.group("getenv"): + reader = "getenv" + elif match.group("presence_name"): + reader = "IsEnvironmentSet" + elif match.group("mask_name"): + reader = "MaskFromEnvironment" + else: + reader = "kibSetting" + found.setdefault(name, []).append( + (path.as_posix(), number, kind_of(reader, window), default_of(match, window))) for sites in found.values(): sites.sort() return found @@ -60,10 +101,32 @@ def documented(path, prefixes=PREFIXES): return None +def library_of(source): + parts = Path(source).parts + if len(parts) >= 4 and parts[0] == "core" and parts[1] == "libs" and parts[2] == "prx": + return parts[3] + if len(parts) >= 2 and parts[0] == "core": + return "core/" + parts[1] + return parts[0] if parts else source + + def list_names(found, out): for name in sorted(found): - for path, number in found[name]: - out.write(f"{name}\t{path}:{number}\n") + for source, number, kind, default in found[name]: + out.write(f"{name}\t{kind}\t{source}:{number}\t{default}\n") + + +def table(found, out): + libraries = {} + for name, sites in found.items(): + libraries.setdefault(library_of(sites[0][0]), []).append((name, sites[0])) + for library in sorted(libraries): + out.write(f"### {library}\n\n") + out.write("| Name | Kind | Source | Default | Description |\n") + out.write("|------|------|--------|---------|-------------|\n") + for name, (source, number, kind, default) in sorted(libraries[library]): + out.write(f"| `{name}` | {kind} | `{source}` | {default} | |\n") + out.write("\n") def check(found, doc, prefixes, out): @@ -74,8 +137,8 @@ def check(found, doc, prefixes, out): missing = sorted(set(found) - names) stale = sorted(names - set(found)) for name in missing: - path, number = found[name][0] - out.write(f"error: {name} is read at {path}:{number} but has no row in {doc}\n") + source, number, _, _ = found[name][0] + out.write(f"error: {name} is read at {source}:{number} but has no row in {doc}\n") for name in stale: out.write(f"error: {name} has a row in {doc} but the code no longer reads it\n") if missing or stale: @@ -90,6 +153,8 @@ def main(argv=None): description="list the environment names the code reads, or check them against a reference document") parser.add_argument("--check", action="store_true", help="compare the names the code reads with the document and fail on a mismatch") + parser.add_argument("--table", action="store_true", + help="emit the reference table rows, grouped by library") parser.add_argument("--doc", default=DOC, help="reference document to check (default: %(default)s)") parser.add_argument("--root", action="append", default=None, help="directory to scan (default: %s); repeatable" % ", ".join(ROOTS)) @@ -101,6 +166,9 @@ def main(argv=None): found = reads(roots, prefixes) if args.check: return check(found, args.doc, prefixes, sys.stdout) + if args.table: + table(found, sys.stdout) + return 0 list_names(found, sys.stdout) return 0 diff --git a/tools/tests/test_env_reference.py b/tools/tests/test_env_reference.py index adff46647a..14cc0d3d5f 100644 --- a/tools/tests/test_env_reference.py +++ b/tools/tests/test_env_reference.py @@ -37,11 +37,12 @@ class EnvReferenceTests(unittest.TestCase): "APS5_WORKER_AFFINITY_MASK", "ANYPS5_GPU"}) self.assertEqual(found["APS5_PROFILE_DRAW"][0][1], 1) - def test_the_default_prefix_scans_only_aps5(self): + def test_the_default_prefixes_cover_both(self): root = self.fixture() found = env_reference.reads([root / "core"]) self.assertTrue(found) - self.assertFalse([name for name in found if name.startswith("ANYPS5_")]) + self.assertTrue([name for name in found if name.startswith("APS5_")]) + self.assertTrue([name for name in found if name.startswith("ANYPS5_")]) def test_check_passes_when_every_read_name_has_a_row(self): root = self.fixture() From d49b0fecf53ff4e4a54b542b8cadb3f23e6f2ab8 Mon Sep 17 00:00:00 2001 From: "husari.orozco" <husari09.oro@gmail.com> Date: Wed, 14 Oct 2026 14:04:04 -0600 Subject: [PATCH 0960/1000] docs(environment): add the APS5_* and ANYPS5_* reference and check it --- docs/dev/ENVIRONMENT.md | 472 +++++++++++++++++++++++++++++++++++++ tools/check_conventions.py | 24 ++ 2 files changed, 496 insertions(+) create mode 100644 docs/dev/ENVIRONMENT.md diff --git a/docs/dev/ENVIRONMENT.md b/docs/dev/ENVIRONMENT.md new file mode 100644 index 0000000000..00470832da --- /dev/null +++ b/docs/dev/ENVIRONMENT.md @@ -0,0 +1,472 @@ +# Environment variables + +The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetting` and `MaskFromEnvironment`. This table lists every `APS5_*` and `ANYPS5_*` name it reads, its kind, the value used when it is unset and what it does, one table per library. Regenerate the names with `python3 tools/env_reference.py --table`; the Conventions check fails when a name the code reads has no row here, or a row's name is no longer read. + +### core/libs + +| Name | Kind | Source | Default | Description | +|------|------|--------|---------|-------------| +| `ANYPS5_LOADEXEC_TEST_ROOT` | string | `core/libs/tests/GuestLoadExecHost.cpp` | | directory the load-exec host test reads its guest executable from | +| `ANYPS5_TEST_QUOTED_ARGV0` | presence | `core/libs/tests/GuestLoadExecHost.cpp` | | makes the load-exec test's first argument one with quotes, a backslash and non-ASCII text | +| `APS5_EBOOT` | string | `core/libs/tests/GuestSystemExit.cpp` | | host path of the executable launched for /app0/eboot.bin, defaulting to the running executable | +| `APS5_USBD_DEVICE` | string | `core/libs/tests/GuestUsbd.cpp` | | selects the USB device a test drives, as vendor:product in hex | + +### core/relinker + +| Name | Kind | Source | Default | Description | +|------|------|--------|---------|-------------| +| `ANYPS5_GUEST_MODULE_ARGS` | string | `core/relinker/elfpatcher/tests/GuestModuleArgsHost.cpp` | | path of the file the module-argument fixture appends its events to | + +### core/shader + +| Name | Kind | Source | Default | Description | +|------|------|--------|---------|-------------| +| `ANYPS5_NO_SHADER_CACHE` | presence | `core/shader/recompiler/ShaderDiskCache.cpp` | | disables the on-disk shader cache when set to a non-empty value other than 0 | +| `ANYPS5_SHADER_CACHE_DIR` | string | `core/shader/recompiler/ShaderDiskCache.cpp` | | directory for the on-disk shader cache, overriding the one beside the executable | +| `APS5_BDA_BYTE_READS` | presence | `core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp` | | forces one address lookup and byte load per byte for BDA reads instead of a wide dword load | +| `APS5_BDA_DWORD_STORES` | presence | `core/shader/recompiler/Translation/src/MemoryInstructions/FlatInstructions.cpp` | | reverts BDA writes to one address store per dword instead of a wide multi-dword store | +| `APS5_DUMP_IR` | string | `core/shader/recompiler/Recompiler.cpp` | | hex code address (or all) of the programs whose IR is printed after each front-end pass | +| `APS5_GATHER_CONST` | integer | `core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp` | | integer every non-depth gather returns in all four texels instead of the sampled value | +| `APS5_IMAGE_TABLE_KEY_SCAN` | integer | `core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp` | 256 | number of image-table entries the key scan examines | +| `APS5_IMAGE_TABLE_STRICT` | presence | `core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp` | | sets image-table materialization to its strict form; 0 leaves it off | +| `APS5_NO_ATOMIC_BARRIER` | presence | `core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvFormatConvert.cpp` | | drops the device-scope acquire/release memory barrier emitted after atomics | +| `APS5_NO_ATOMIC_ZERO_SKIP` | presence | `core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp` | | restores plain buffer atomics instead of skipping identity-operand atomics | +| `APS5_NO_CODE_HASH_KEY` | presence | `core/shader/recompiler/CacheKey.hpp` | | appends every code word to the cache key instead of hashing the code | +| `APS5_NO_FAILURE_MEMO` | presence | `core/shader/recompiler/Recompiler.cpp` | | rebuilds a program whose compilation failed instead of reusing the memoized failure | +| `APS5_NO_IMAGE_TABLES` | presence | `core/shader/recompiler/Optimization/src/ResourceTracker.cpp` | | disables image tables, leaving table-loaded descriptors unplanned | +| `APS5_NO_PERF_DISK_PLANS` | presence | `core/shader/recompiler/Recompiler.cpp` | | disables the disk cache of planning results | +| `APS5_NO_PERF_DOM_INTERSECTION` | presence | `core/shader/recompiler/ControlFlow/src/Structurizer.cpp` | | computes dominator intersections into a fresh vector instead of intersecting in place | +| `APS5_NO_PERF_ITERATION_SELECTION` | presence | `core/shader/recompiler/ControlFlow/src/Structurizer.cpp` | | recomputes the post-dominator sets when finding a loop's iteration-selection merge instead of reusing them | +| `APS5_NO_RESULT_MEMO` | presence | `core/shader/recompiler/Recompiler.cpp` | | materializes a fresh result for every recompile instead of reusing the result memo | +| `APS5_PROBE` | string | `core/shader/recompiler/Translation/src/DispatchInstructions.cpp` | | pc:vgpr[:shift] probe that copies that VGPR into v255 after the instruction so image stores write it as a per-lane trace | +| `APS5_RAYTRACING` | string | `core/shader/recompiler/Translation/src/DispatchInstructions.cpp` | | selects the image_bvh_intersect_ray behaviour: strict throws and miss writes 0xffffffff | +| `APS5_SAVEEXEC_WRITE_FIRST` | presence | `core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp` | | writes the saved exec to the destination before reading the source in s_*_saveexec | +| `APS5_SINGLE_LANE` | string | `core/shader/recompiler/Recompiler.cpp` | | hex code addresses (comma separated, or all) of wave64 programs kept at one guest lane per invocation | +| `APS5_SRT_DEBUG` | presence | `core/shader/recompiler/Optimization/src/RequestMemoryView.cpp` | | prints extra SRT descriptor-walk debug output, such as guest reads outside the request memory | +| `APS5_TRACE_BDA` | presence | `core/shader/recompiler/Optimization/src/ResourceTracker.cpp` | | prints the first few accesses that make a program take the address-based path | +| `APS5_TRACE_BUFFER_WRITTEN` | presence | `core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp` | | prints how many guest buffer elements a program may store to and how many it only loads | +| `APS5_TRACE_IMAGE_TABLE` | presence | `core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp` | | traces image-table resolution | +| `APS5_WAVE_LDS_SUBGROUP` | presence | `core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp` | | restores subgroup-scope LDS barriers for single-wave workgroups instead of workgroup scope | + +### libSceAgc + +| Name | Kind | Source | Default | Description | +|------|------|--------|---------|-------------| +| `APS5_TRACE_LABEL` | integer | `core/libs/prx/libSceAgc/Cb/src/Sync.cpp` | 0 | hex guest address of a RELEASE_MEM label whose packet is logged when recorded | + +### libSceAgcDriver + +| Name | Kind | Source | Default | Description | +|------|------|--------|---------|-------------| +| `ANYPS5_GPU` | string | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | restricts physical device selection to device names containing this case-insensitive substring | +| `ANYPS5_PRESENT_MODE` | string | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | fifo | selects the swapchain present mode: fifo, fifo_latest_ready, mailbox or immediate | +| `ANYPS5_REQUIRE_DESCRIPTOR_BINDING_FLAGS` | presence | `core/libs/prx/libSceAgcDriver/tests/ComputePipelineReplay.cpp` | | fails the replay test instead of accepting a device that refuses descriptor binding flags | +| `ANYPS5_REQUIRE_VULKAN` | presence | `core/libs/prx/libSceAgcDriver/tests/BdaDevice.cpp` | | fails a test instead of skipping when no usable Vulkan device is present | +| `APS5_ADDRESS_COPY_MAX_MIB` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | 64 | upper bound in MiB on the guest memory one address-based build copies; exceeding it aborts | +| `APS5_ALL_BUFFERS_WRITTEN` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | | notes every shader buffer element as written, as before bufferWritten existed | +| `APS5_ASSERT_GPU_LOCK` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp` | | aborts when a guarded path is reached without holding GuestMemory::GpuMutex | +| `APS5_ATOMIC_STAGE_MAX_KIB` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | 1024 | size cap in KiB on an atomic shader element covered by device-local staging | +| `APS5_BARRIER_VALIDATE` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | counts the barriers a hazard-tracking validator would emit and skip, by class and hazard kind | +| `APS5_BATCH_CAP` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/DeferredLabels.cpp` | 64 | recorded work since the last submit at which a label batch, flush or reap is forced (0 disables the cap) | +| `APS5_BDA_FLUSH` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | | also flushes pending storage results over address-based shader regions, which are otherwise exempt | +| `APS5_BDA_TABLE_CACHE_ENTRIES` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp` | 8 | number of recently used BDA page tables kept in the cache (1 is the previous behaviour) | +| `APS5_BLOCK_WRITEBACK_EACH` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | | stores the touched units only on every write-back instead of widening repeated partial stores | +| `APS5_BLOCK_WRITEBACK_FRAMES` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | 8 | window of presents within which repeated partial stores trigger the whole-image widening | +| `APS5_BLOCK_WRITEBACK_PIECES` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | 4 | number of partial stores within the window that triggers the whole-image widening | +| `APS5_BLOCK_WRITEBACK_WIDEN` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | | enables the widening that stores a whole image when it keeps being asked for in pieces | +| `APS5_BUFFER_POOL_SHARED` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp` | | uses one buffer tier for every allocation size, as before the tier split | +| `APS5_BUFFER_POOL_SLOTS` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp` | 4096 | bounds the buffers each pool tier retains (0 keeps the default of 4096) | +| `APS5_CAPTURE_GPU_COPIES` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | | records GPU buffer copies in the capture trace (requires APS5_CAPTURE_TRACE) | +| `APS5_CAPTURE_INPUTS` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp` | | records each draw's inputs in the capture trace (requires APS5_CAPTURE_TRACE) | +| `APS5_CAPTURE_TRACE` | presence | `core/libs/prx/libSceAgcDriver/Execution/include/CaptureTrace.hpp` | | writes capture events to capture-trace.log | +| `APS5_CHECK_INDIRECT_ARGS` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp` | | reads GPU-side indirect draw records back on the CPU and prints the first ones | +| `APS5_COMPLETION_STORE_SYNC` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | makes a store made by a completion action wait for later batches over its range | +| `APS5_COPY_DESTINATION_STRICT` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | refuses a CPU copy whose destination has any pending writer, not only an unsettled one | +| `APS5_COPY_HLE_CPU_MAX` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferCopy.cpp` | 262144 | largest copy in bytes the CPU side of the copy HLE handles | +| `APS5_COPY_HLE_GPU_MAX` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferCopy.cpp` | | largest copy in bytes the GPU side of the copy HLE handles; unset means no limit | +| `APS5_COPY_READ_TRACKING` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | notes in-place reads of a range; set to 0 to disable, making the CPU copy path require an idle recorder | +| `APS5_COPY_SYNC` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | treats any pending writer over a copy's range as needing a wait | +| `APS5_COPY_VERIFY` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | verifies a CPU copy's source and destination bytes against the pre-copy values after a sync | +| `APS5_COPY_WAIT_SOURCE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | waits for the source's producer batch when it is the only one in flight, on non-zero queues | +| `APS5_COUNT_ALL_COMPLETION_LABELS` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | counts a completion label also stored on the GPU as pending from its registration | +| `APS5_CPU_COPIES` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | | copies misaligned descriptor sub-ranges on the CPU instead of with a GPU buffer copy | +| `APS5_CPU_DCC_KEYS` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp` | | always stores DCC keys on the CPU | +| `APS5_CPU_STORES` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp` | | writes packet stores on the CPU instead of recording them on the GPU | +| `APS5_DCC_KEYS_EACH` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | records every DCC key store at once instead of batching them | +| `APS5_DEPTH_COPY_MIN_KIB` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | 1024 | minimum surface size in KiB for a depth target to take the depth-copy path | +| `APS5_DISPATCH_CACHE_BYTES` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchCache.cpp` | 268435456 | size of the dispatch cache in bytes (0 keeps the default of 256 MiB) | +| `APS5_DISPATCH_CACHE_ENTRIES` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchCache.cpp` | 8192 | number of dispatch cache entries; 0 picks a default from the variant count | +| `APS5_DISPATCH_STAMP_VALIDATE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchCache.cpp` | | validates dispatch cache hits by write stamps, forcing one variant and a 4096-entry cache | +| `APS5_DISPATCH_VARIANTS` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchCache.cpp` | 4 | number of dispatch variants kept per cache entry, clamped to the maximum | +| `APS5_DRAIN_ALL` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp` | | drains every packet, disabling the batched recording path | +| `APS5_DRAIN_COMPLETION_LABELS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/DeferredLabels.cpp` | | records completion labels immediately instead of deferring them with the batch | +| `APS5_DRAW_CACHE_ENTRIES` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCache.cpp` | 4096 | number of entries in the draw cache; 0 keeps the default 4096 | +| `APS5_DRAW_DRAIN` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp` | | treats draw packets as drains, waiting for them instead of recording ahead | +| `APS5_DRAW_KEY_ALL_USER_WORDS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp` | | includes all 32 user words in a draw's register key instead of the used count | +| `APS5_DRAW_SNAPSHOT_MAX_MIB` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | 64 | upper bound in MiB on the snapshot bytes a draw's bindings may take | +| `APS5_DRAW_TRANSITIONS` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp` | | keeps the per-draw upload and download barriers and layout transitions instead of the lean recorded path | +| `APS5_DUMP_COMPUTE_MIN_WORDS` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | only captures compute pipelines whose SPIR-V has at least this many words | +| `APS5_DUMP_COMPUTE_PIPELINES` | string | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | directory in which captured compute pipelines are written | +| `APS5_DUMP_DRAW_SHADERS` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp` | 0 | hex color-target or slot-0 address whose draw's shaders are dumped | +| `APS5_DUMP_DRAW_SLOT1` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp` | 0 | hex slot-1 address whose draw's shaders and register state are dumped | +| `APS5_DUMP_FRAMES` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | 0 | saves the first n presented frames as frame_<index>.bmp | +| `APS5_DUMP_FRAMES_EVERY` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | 1 | dumps every n-th presented frame | +| `APS5_DUMP_QUEUE` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp` | | dumps the first submissions of the queue with this hex id | +| `APS5_DUMP_REJECTED` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/CommandValidation.cpp` | | writes a rejected submission to rejected_submission_<n>.bin | +| `APS5_DUMP_SCALE` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | 4 | keeps every n-th pixel in each direction of a dumped frame | +| `APS5_DUMP_SHADERS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp` | | writes each prepared shader's request to shader_<address>.req | +| `APS5_DUMP_STORAGE` | string | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | | selects storage textures to dump by hex address or WxHxL[:format] extent as storage_<address>_<n>.raw | +| `APS5_DUMP_SUBMITS` | string | `core/libs/prx/libSceAgcDriver/Execution/src/SubmissionCapture.cpp` | | directory in which every submission is captured for replay | +| `APS5_DUMP_TARGETS` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp` | 0 | saves the first n renders of every color target as a raw file | +| `APS5_DUMP_TEXTURE` | string | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | | hex addresses or WxH extents whose sampled textures are dumped on first upload as texture_<address>_<n>.raw | +| `APS5_EAGER_WRITEBACK` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | | writes back pending storage layers eagerly | +| `APS5_FILL_CHAIN` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | seeds and doubles the fill pattern in place in the import | +| `APS5_FILL_CLEAR_EXACT_ONLY` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferFill.cpp` | | turns a fill into a clear only when it covers the target exactly and nothing else | +| `APS5_FILL_SYNC` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | waits for every recorded store over the fill's range | +| `APS5_FLIP_INFLIGHT` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | 1 | number of presentations that may trail on the GPU (0, 1 or 2) | +| `APS5_FLIP_READ_CHECK` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | records the in-place reads of completed batches and checks them before the present blit's fence | +| `APS5_FORCE_STRUCTURIZER_FALLBACK` | presence | `core/libs/prx/libSceAgcDriver/tests/ControlFlow.cpp` | | makes the control-flow test expect the reach-flag fallback instead of no cloning | +| `APS5_FULL_BARRIERS` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | records every barrier instead of merging redundant ones | +| `APS5_GPU_CHECKPOINTS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | attaches device diagnostic checkpoints through VK_NV_device_diagnostic_checkpoints | +| `APS5_GPU_COPY_MAX_KIB` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | 1024 | size cap in KiB on a descriptor sub-range copied through the GPU path, larger ones staying on the CPU | +| `APS5_GPU_NO_WRITEBACK` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | | skips writing GPU results back into guest memory | +| `APS5_GPU_WAIT_TIMEOUT_MS` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/WaitMemory.cpp` | 1000 | milliseconds a GPU wait may run before a warning is printed | +| `APS5_GROUP_CAPTURE_ALL_QUEUES` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchAhead.cpp` | | resolves group-ahead capture on queue 0 as well | +| `APS5_HEAP_MIRROR_MIB` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | 24576 | budget in MiB for the heap mirrors held for registered allocations | +| `APS5_HOOK_FLUSH_CPU_BLOCKS` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | set to 0 flushes pending images whose every 64 KiB block the CPU wrote, instead of skipping them | +| `APS5_HOOK_FULL_SYNC` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | makes the flush hook's recorded-store wait a full Sync under the GpuMutex | +| `APS5_HOOK_LOCKED_WAIT` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | keeps the flush hook's pending-write wait under the GpuMutex | +| `APS5_HOST_IMPORT_MIB` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | | budget in MiB for pinned host imports; unset uses the larger of 6 GiB and half the physical memory, capped by the host heap | +| `APS5_IGNORE_DEPTH_TEST` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp` | | renders depth- and stencil-tested draws without a depth target as if the tests always passed | +| `APS5_INDIRECT_VERTEX_MIB` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/IndirectDraw.cpp` | 64 | cap in MiB on a vertex attribute extent, above which a GPU-side indirect draw resolves on the CPU | +| `APS5_INSERT_COMPARE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchCache.cpp` | | compares a fresh capture's regions against the recorder before inserting it into the cache | +| `APS5_KEYFLIP_DROP` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | | drops a pending unit's results on a clear-code to uncompressed key flip, as before | +| `APS5_KEYS_FILL_CLEAR` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | | clears an image from DCC keys whose fill pattern it matches (only when set to 1) | +| `APS5_LABEL_FLUSH_US` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/DeferredLabels.cpp` | 1000 | age in microseconds at which a pending label batch is recorded or submitted | +| `APS5_LABEL_GATE_ALL` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | gates a label store on any pending completion, not only one overlapping the range | +| `APS5_LABEL_LOCK_EACH` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/DeferredLabels.cpp` | | records each label under the GpuMutex instead of deferring it | +| `APS5_LABEL_OWN_LOCK` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketFlush.cpp` | | makes a label flush take the GpuMutex itself instead of relying on a self-locking packet | +| `APS5_LABEL_REAP_ALL_QUEUES` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | reaps completion labels on every queue instead of only queue 0 | +| `APS5_LABEL_RUNS_INLINE` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | records store runs at once and closes each run at the next command | +| `APS5_LABEL_STORE_ALWAYS` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | writes back every label store even when a later store already covered its bytes | +| `APS5_LABEL_SUBMIT_NOW` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | submits every label immediately instead of waiting for the flush deadline | +| `APS5_LABEL_TRUST_LATE` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | trusts cache entries with a stamp at or below the wait's stamp under the late rule (off unless set to a non-zero value) | +| `APS5_LABEL_TRY_EACH_PACKET` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/DeferredLabels.cpp` | | attempts to record deferred labels at every self-locking packet instead of deferring to its lock | +| `APS5_LAND_WAITS_ALL_QUEUES` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/WaitMemory.cpp` | | notes pending waits on queue 0 as well | +| `APS5_LDS_SLACK` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp` | 0 | dwords added to every dispatch's LDS allocation | +| `APS5_LOCKED_BUILD` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | builds the whole dispatch preparation under the GpuMutex, skipping the split path | +| `APS5_LOCKED_DRAW_PREPARE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp` | | takes the GpuMutex before preparing a draw | +| `APS5_LOOP_GUARD` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | | enables the fault buffer that guards shader loops running past their bound | +| `APS5_MIRROR_REFRESH_THREADS` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | 3 | number of helper threads that refresh heap mirrors, clamped to 0 through 16 | +| `APS5_NO_ADDRESS_SPACE_CACHE` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | | disables the address-space cache so every build rebuilds its space | +| `APS5_NO_ADJACENT_GENERATION` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | | leaves adjacent pending images at their generation during a write-back, as before | +| `APS5_NO_ALIAS_BORROW` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | | disables borrowing a pending aliased storage image | +| `APS5_NO_ATOMIC_STAGING` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | | binds atomic shader elements in place instead of staging them through a device-local buffer | +| `APS5_NO_BARRIER_ELISION` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | records every leading barrier instead of merging it into the following submission | +| `APS5_NO_BDA_TABLE_CACHE` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp` | | builds every BDA descriptor table instead of reusing the recent-table cache | +| `APS5_NO_BLOCK_TRACKING` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | | tracks array layers, or the whole surface, instead of 64 KiB blocks | +| `APS5_NO_BUFFER_CLASSES` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp` | | matches exact sizes in the buffer pool instead of rounding up to size classes | +| `APS5_NO_CACHED_PRECOLLECT` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | leaves the write-watch walks of a cached object's Revalidate under the GPU mutex | +| `APS5_NO_CLEARED_VIEW` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | | keeps fast-cleared surfaces as CPU snapshots instead of viewing the cleared storage image | +| `APS5_NO_COLLECT_MEMO` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp` | | disables the per-thread memo of write-watch collects | +| `APS5_NO_CONTEXT_CACHE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | rebuilds the Graphics::Context on every call instead of returning the cached one | +| `APS5_NO_COPY_ALIAS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | keeps every device copy a transfer instead of aliasing the source storage image | +| `APS5_NO_COPY_HLE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferCopy.cpp` | | disables the copy-kernel HLE, so matching copies run as compute | +| `APS5_NO_COPY_KNOWN_VALUES` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferCopy.cpp` | | disables serving a copy's destination from source values the driver already knows | +| `APS5_NO_CPU_COPY` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferCopy.cpp` | | disables the CPU copy path of the copy HLE | +| `APS5_NO_DATA_HITS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchCache.cpp` | | disables dispatch-cache hits that differ only in ShaderData or FlattenedSrt words | +| `APS5_NO_DCC_WORD_SCAN` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp` | | restores the byte-by-byte DCC key scan instead of the word-at-a-time compare | +| `APS5_NO_DEFERRED_EVICTION` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | destroys displaced resource-cache entries under the mutex instead of deferring them to the open batch | +| `APS5_NO_DISPATCH_CACHE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp` | | disables the dispatch cache so every dispatch takes the uncached path | +| `APS5_NO_DISPATCH_KEY_HYGIENE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp` | | mixes every shader register into the dispatch key instead of only the registers that affect the dispatch | +| `APS5_NO_DISPATCH_LRU` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchInsert.cpp` | | clears the whole dispatch cache on overflow instead of evicting the least recently used entry | +| `APS5_NO_DISPATCH_RECIPE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | builds no dispatch recipes, so dispatch-cache hits repeat the recorded work | +| `APS5_NO_DRAW_KEY` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCache.cpp` | | registers no draw key, so every draw decodes its programs instead of hitting the draw cache | +| `APS5_NO_DRAW_KEY_TRIM` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp` | | keys the render target and index ranges into the draw resource key instead of repeating the alias checks on a hit | +| `APS5_NO_DRAW_PRECHECK` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPrecheck.cpp` | | disables the draw precheck that rejects empty or invalid draws early | +| `APS5_NO_DRAW_RECIPE` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp` | | builds no draw recipes, so draw-cache hits repeat the recorded stages | +| `APS5_NO_DRAW_RESOURCE_CACHE` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp` | | builds every draw's shader resources instead of reusing a cached identical build | +| `APS5_NO_DRAW_SRT_ENTRIES` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCache.cpp` | | disables the draw cache's SRT entries, so every draw decodes its programs | +| `APS5_NO_EPOCH_REVALIDATE` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | | runs every Revalidate's per-element and per-region checks instead of the epoch gate | +| `APS5_NO_FAST_REVALIDATE` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | | always repeats the resource lookups instead of using the fast proof | +| `APS5_NO_FILL_CLEAR` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferFill.cpp` | | disables the clear-through-DCC-keys path in a fill | +| `APS5_NO_FILL_HLE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferFill.cpp` | | disables the fill-kernel HLE, so matching fills run as compute | +| `APS5_NO_FLUSH_PRETEST` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | | lists every pending image on FlushPending instead of testing whether a pending unit lies inside the range | +| `APS5_NO_FOREIGN_WRITERS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/WriteEvidence.cpp` | | records no draw-written ranges as foreign writers for capture validation | +| `APS5_NO_GATE_RETRY` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/WriteEvidence.cpp` | | retries no validation whose entry moved, leaving the dispatch or draw unvalidated | +| `APS5_NO_GPL` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | disables graphics pipeline libraries | +| `APS5_NO_GPU_DUMP` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | decodes dump frames from the tiled guest buffer on the CPU | +| `APS5_NO_GPU_INDIRECT` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/IndirectDispatch.cpp` | | reads indirect dispatch arguments on the CPU instead of letting the GPU count them | +| `APS5_NO_GPU_INDIRECT_DRAW` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp` | | reads indirect draw arguments through the flush hook on the CPU instead of from the GPU | +| `APS5_NO_HOLD_PROFILE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp` | | turns off GpuMutex hold-time profiling, keeping only the wait timing | +| `APS5_NO_HOOKSYNC_PROFILE` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | leaves only the plain hook-sync counts under APS5_PROFILE_DRAW | +| `APS5_NO_HOOK_COMPLETION_GUARD` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | lets the flush hook record the accessing worker's queued labels even from inside a completion action | +| `APS5_NO_HOOK_SNAPSHOT` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | takes the GpuMutex on every access instead of using the pending-write snapshot | +| `APS5_NO_HOST_IMPORT` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | disables importing guest memory, so address-based shaders copy instead of using it in place | +| `APS5_NO_IMAGE_PAGE_CACHE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp` | | leaves the main guest image's page-protection table uncached | +| `APS5_NO_INDIRECT_DRAW` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp` | | rejects indirect draw packets at submission validation | +| `APS5_NO_JOIN_WAW_CHECK` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | lets a contiguous store join the pending one without the write-after-write check | +| `APS5_NO_KEEP_NEW_STORAGE` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | | does not keep a newly created storage image alive with the open batch | +| `APS5_NO_KEYS_FILL_MEMO` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp` | | records no memo of GPU-side DCC key fills | +| `APS5_NO_KEYS_REFILL` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | | keeps a covered image's uploaded keys as they were instead of marking them uncompressed after a matching key fill | +| `APS5_NO_KEYS_RESCAN` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | | reuses the DCC keys scanned before the lookup's refresh instead of discarding them for a rescan | +| `APS5_NO_KEY_FAST_PATH` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp` | | scans a surface's DCC keys on every call instead of using and storing a proof | +| `APS5_NO_LABEL_BATCHING` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/DeferredLabels.cpp` | | turns off every label-batching behaviour, the label runs, queued labels and batched submit | +| `APS5_NO_LABEL_BATCH_SUBMIT` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/DeferredLabels.cpp` | | disables batched label submission | +| `APS5_NO_LABEL_PACKET_FLUSH` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketFlush.cpp` | | disables the label flush between packets | +| `APS5_NO_LABEL_RUNS` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | records every store in a run of its own between a barrier pair | +| `APS5_NO_LABEL_SHORTCUT` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/WaitMemory.cpp` | | disables the pending-label table shortcut in a memory wait | +| `APS5_NO_LAYER_CLEAR` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | | tracks every surface as one layer instead of per array layer | +| `APS5_NO_LAYER_REFUSAL_MEMO` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | | re-checks a layer whose surface check failed at its current generation on every fill | +| `APS5_NO_LAYOUT_CACHE` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | | builds descriptor set layouts fresh instead of through the descriptor cache | +| `APS5_NO_LEASE_MIRROR` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | | restores copies instead of image mirrors for address-based builds | +| `APS5_NO_LIBRARY_PREBUILD` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp` | | disables prebuilding registration-form shader libraries (only 1 disables it) | +| `APS5_NO_LOCK_PROFILE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp` | | turns off GpuMutex wait profiling under APS5_PROFILE_DRAW | +| `APS5_NO_MOVED_BUFFER_TEMPLATES` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp` | | keys the full descriptor words of read-only guest buffers instead of allowing a hit when they move | +| `APS5_NO_OBSERVATION_GUARD` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/WriteEvidence.cpp` | | observes every validated range, not only the ranges whose flush hook waited | +| `APS5_NO_OPPORTUNISTIC_REAP` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | skips retiring already-finished batches before a submit | +| `APS5_NO_OWN_IMAGE_REFRESH` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | | lets a Pending failure of the fast proof take the full walk instead of the own-object refresh | +| `APS5_NO_PARALLEL_MIRROR_REFRESH` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | | disables the mirror-refresh thread pool, so mirror refreshes run on the calling thread | +| `APS5_NO_PATTERN_FILL_HLE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferFill.cpp` | | disables the dword-pattern fill HLE | +| `APS5_NO_PAUSE_SPIN` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/WaitMemory.cpp` | | disables the CPU pause-spin in a blocked memory wait | +| `APS5_NO_PERF_FRONTEND_PAIR` | presence | `core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp` | | disables frontend-pair reuse in the shader-memory test | +| `APS5_NO_PERF_LAYOUT_PAIR` | presence | `core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp` | | disables layout-pair reuse in the shader-memory test | +| `APS5_NO_PIPELINE_CACHE` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp` | | builds a pipeline per draw instead of reusing cached ones | +| `APS5_NO_PIPELINE_LTO` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp` | | disables background link-time optimization of graphics pipeline libraries | +| `APS5_NO_POOL_CACHE` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | | allocates descriptor pools fresh instead of through the descriptor cache | +| `APS5_NO_PRECOLLECT` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | | disables the stage-A precollect pass | +| `APS5_NO_PREPARED_FIND` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | looks the resource-cache key up again instead of using the object stage A found | +| `APS5_NO_PRESYNC` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | disables the stage-A pre-sync of pending writes | +| `APS5_NO_PROC_TABLE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | resolves the device's Vulkan entry points per call instead of filling the function table once | +| `APS5_NO_QUEUED_LABELS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/DeferredLabels.cpp` | | disables the queued-label table | +| `APS5_NO_RECORDED_CLEAR` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | | waits for a storage image's fast clear in a batch of its own instead of recording it | +| `APS5_NO_RECORDER_SYNC_DRAWS` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp` | | gives the draws the recorder waits for their own batch instead of recording them with the wait | +| `APS5_NO_RECORD_COPIED_DRAWS` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp` | | waits for copied-write draws at once instead of listing their write-back for later | +| `APS5_NO_RESIDENT_PRESENT` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | always presents through guest memory instead of the resident image | +| `APS5_NO_RESIDENT_TARGETS` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp` | | copies every render target in and out again instead of keeping it resident | +| `APS5_NO_RESOURCE_CACHE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | disables the shared resource cache, so every dispatch builds its resources fresh | +| `APS5_NO_SAMPLED_FROM_STORAGE` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | | keeps every sampled texture a CPU snapshot instead of viewing the surface's storage image | +| `APS5_NO_SAMPLER_CACHE` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | | creates a sampler per binding instead of caching them | +| `APS5_NO_SEPARATE_QUEUED_LABELS` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | lets a queued label enter the recorded table and replace a recorded entry of its dword | +| `APS5_NO_SINGLE_PASS_COLLECT` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp` | | collects written pages in one cleared pass instead of a second confirming walk | +| `APS5_NO_SITE_SAMPLING` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/WriteEvidence.cpp` | | disables per-site sampling of write evidence | +| `APS5_NO_SNAPSHOT_CHECK` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | | skips the live compare that proves a captured snapshot still equals the guest bytes | +| `APS5_NO_SORTED_SNAPSHOT_LOOKUP` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | | scans the snapshot's regions in AddSnapshot instead of searching them | +| `APS5_NO_SPARSE_HEAP_MIRROR` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | | disables sparse heap mirrors, mirroring only whole heap ranges | +| `APS5_NO_STAGE_A_IMAGES` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | | leaves every image element to the stage-B full lookup instead of a stage-A record | +| `APS5_NO_STORAGE_DEDUPE` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | | looks each identical consecutive storage descriptor up, and refreshes its image, separately | +| `APS5_NO_SYNC_SITES` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | leaves only the per-source and per-thread sync counts, with no per-site attribution | +| `APS5_NO_SYNC_THROUGH` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | makes every pending-write sync a full Sync instead of the targeted unlocked wait | +| `APS5_NO_TABLE_REUSE` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | | disables reusing a materialized image table | +| `APS5_NO_TARGET_WRITEBACK` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp` | | skips writing render targets back to guest memory after the draw | +| `APS5_NO_TEMPLATE_DATA_REFRESH` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | keys the ShaderData and FlattenedSrt words into compute keys instead of refreshing a template's data buffers | +| `APS5_NO_TEXTURE_CACHE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | builds every texture and storage-image object fresh instead of caching it | +| `APS5_NO_TEXTURE_HASH` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | | finds texture-cache entries by scanning the list instead of the hash index | +| `APS5_NO_TIMELINE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | leaves timeline semaphores off, so drains wait under the mutex | +| `APS5_NO_UNIT_SHADOW` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/UnitShadow.cpp` | | makes every unit shadow primitive inert | +| `APS5_NO_UNLOCKED_DEVICE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp` | | loads the device and runs a dispatch under the GPU mutex instead of using an unlocked device | +| `APS5_NO_UNLOCKED_DRAIN` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp` | | keeps the GPU mutex held while draining a packet instead of the unlocked drain | +| `APS5_NO_UNLOCKED_VALIDATE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchLookup.cpp` | | keeps the dispatch-cache mutex held while validating a cache entry | +| `APS5_NO_UNREGISTERED_DROP` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | | stores a storage image's pending results even when its memory is no longer registered, instead of dropping them | +| `APS5_NO_VALIDATE_CACHE` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp` | | validates every draw instead of serving the validation memo | +| `APS5_NO_VALIDATE_SKIP` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/WriteEvidence.cpp` | | syncs every pending write instead of skipping validation on written-element evidence | +| `APS5_NO_WAIT_OVERLAP_SUBMIT` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/WaitMemory.cpp` | | disables the unlocked wait that submits recorded work while overlapping writes are open | +| `APS5_NO_WORDWISE_CAPTURE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/ShaderMemory.cpp` | | fetches every captured page whole instead of reading pending pages word by word | +| `APS5_NO_WORKER_AFFINITY` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/WorkerAffinity.cpp` | | leaves the queue workers and presenter unpinned (affinity mask 0) | +| `APS5_NO_WRITER_KEYED_EVIDENCE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/WriteEvidence.cpp` | | records written-element evidence without keying it by writer | +| `APS5_NO_WRITE_EVIDENCE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/WriteEvidence.cpp` | | disables the written-element evidence that lets a read skip the flush hook | +| `APS5_NO_WRITTEN_SHADOW` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | | binds written staging elements in place instead of through the written shadow | +| `APS5_PACKET_EPOCH` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/WaitStatistics.cpp` | | makes queue workers bump the collect epoch before every packet | +| `APS5_PIPELINE_STATS` | string | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | captures and prints driver statistics for each new pipeline, requiring `VK_KHR_pipeline_executable_properties`; `1` enables | +| `APS5_POLL_REAP` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/WaitMemory.cpp` | | reaps completions on every poll tick | +| `APS5_POLL_TRY_EACH` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/WaitMemory.cpp` | | tries the poll service on every tick instead of at the poll interval | +| `APS5_PREBUILD_THREADS` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp` | 1 | number of threads in the one-thread pool that prebuilds registration-form shader libraries | +| `APS5_PRESENT_POLL_FENCE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | polls the present fence every 50 us instead of blocking on it | +| `APS5_PROBE_DISPATCH` | string | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp` | | probes one dispatch at a hex program address (optionally `:index`) with debug output | +| `APS5_PROFILE_DRAW` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp` | | prints the per-phase driver and draw timing totals | +| `APS5_PROFILE_GPU` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | records the GPU time of recorded work by key from batch timestamps | +| `APS5_REAP_EACH_PACKET` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/DeferredLabels.cpp` | | reaps deferred labels after every packet | +| `APS5_RELEASE_ON_UNLOCK` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | destroys deferred batches on the unlocking thread instead of a release thread | +| `APS5_RELEASE_QUEUE_MAX` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | 256 | maximum deferred batches handed to the release thread before a thread destroys its own | +| `APS5_RELEASE_UNDER_LOCK` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | destroys deferred batches in finish() under the lock instead of on a release thread | +| `APS5_RESOURCE_CACHE_ENTRIES` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | 1024 | maximum entries in the per-device shader-resource cache | +| `APS5_RESOURCE_CACHE_TABLE_ELEMENTS` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | 65536 | number of image-table elements the shader-resource cache keeps (0 keeps the default) | +| `APS5_SAMPLE_THREADS` | string | `core/libs/prx/libSceAgcDriver/Execution/src/WorkerSampler.cpp` | | samples every thread of the process every 2 ms and writes each busy thread's hottest frames to a file (Windows only) | +| `APS5_SAMPLE_WORKER` | string | `core/libs/prx/libSceAgcDriver/Execution/src/WorkerSampler.cpp` | | samples the calling queue worker every millisecond and writes its stacks to a file (Windows only) | +| `APS5_SHADOW_VERIFY` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/UnitShadow.cpp` | | verifies that the shadow-published bytes equal the guest bytes | +| `APS5_SHARED_COLLECT_MEMO` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp` | | uses the tracker's shared collect-memo ring under its mutex instead of the per-thread ring | +| `APS5_SHARED_MEMORY_COUNTERS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp` | | accumulates the memory counters into one shared set with atomic RMWs | +| `APS5_SKIP_REQUEST_DUMPS` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderDiagnostics.cpp` | 8 | caps RecompileRequest dumps per shader-skip reason | +| `APS5_SKIP_SHADER` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Diagnostics.cpp` | 0 | hex program address of a shader to skip preparing | +| `APS5_SPIRV_OPT` | string | `core/libs/prx/libSceAgcDriver/tests/SpirvOptimizer.cpp` | | selects the SPIR-V optimization pipeline: `full` restores the performance pipeline, `none` skips optimization | +| `APS5_SRGB_ATTACHMENT_PROXY` | string | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | | forces the sRGB color-attachment proxy format; `1` forces it | +| `APS5_SRGB_SHADER_DECODE` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp` | | forces shader sRGB decode for the sRGB 8-bit formats | +| `APS5_STAGING_POOL_MIB` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp` | | size in MiB of the device-local staging buffer pool; `0` disables the device tier | +| `APS5_STALE_EVICT` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | | also evicts a proved-present dead image's pending layers on an access | +| `APS5_STORE_AT_FLIP` | string | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | stores every pending storage-image result at each display flip; only `1` is accepted | +| `APS5_STORE_AT_LABEL` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | flushes every pending storage-image result when a label is written on the GPU | +| `APS5_SUBMIT_AT_END` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp` | | submits recorded work at the end of a submission even when a worker is queued | +| `APS5_SUSPEND_DRAIN` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp` | | drains the device at a suspend point | +| `APS5_SYNC_COMPLETION_DRAWS` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp` | | keeps every draw with completion work synchronous | +| `APS5_SYNC_DISPATCH` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | waits for every dispatch instead of batching it | +| `APS5_SYNC_DRAWS` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp` | | makes every draw synchronous | +| `APS5_SYNC_FLIP` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp` | | makes flips synchronous (no frames in flight) | +| `APS5_SYNC_LEASE_DISPATCH` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | | waits for leased draws and dispatches at once instead of deferring them | +| `APS5_SYNC_SHADER_PREPARE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp` | | prepares registered shaders on the registering thread instead of in the background | +| `APS5_SYNC_TEXTURE_UPLOAD` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | | restores the waiting batch for every texture upload | +| `APS5_TEXTURE_CACHE_MIB` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | 0 | forced byte budget in MiB for the storage and sampled texture caches; `0` selects the budget automatically | +| `APS5_THREAD_PRIORITY` | string | `core/libs/prx/libSceAgcDriver/Execution/src/ThreadPriority.cpp` | off | raises the queue workers and presenter above the title's threads: `off`, `high` or `rt` | +| `APS5_TRACE_BARRIERS` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | prints that many simulated hazard barriers before stopping | +| `APS5_TRACE_BIGBUF` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | | lists every copied guest region of at least 1 MiB | +| `APS5_TRACE_CAPSYNC` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/WriteEvidence.cpp` | | prints every unlocked hook wait | +| `APS5_TRACE_CONTEXT_STATE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp` | | logs context-state operations and the context registers written | +| `APS5_TRACE_COPY_HLE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferCopy.cpp` | | traces the CPU copy HLE's refusals | +| `APS5_TRACE_DCC_KEYS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/DisplayBuffer.cpp` | | traces stores of uncompressed DCC keys | +| `APS5_TRACE_DIR` | string | `core/libs/prx/libSceAgcDriver/Execution/src/PerformanceTrace.cpp` | . | directory for the trace and summary files | +| `APS5_TRACE_DISPATCH_CACHE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp` | | reports dispatch-cache entries whose key changed | +| `APS5_TRACE_DISPATCH_IO` | string | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp` | | prints every dispatch's resources; `2` also lists the small-buffer words | +| `APS5_TRACE_DRAWS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp` | | prints one line per draw with its phases and inputs | +| `APS5_TRACE_EVENTS` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/PerformanceTrace.cpp` | | number of events a windowed trace keeps in its buffer | +| `APS5_TRACE_FILL_COVER` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | | prints the first fills that meet images without covering them | +| `APS5_TRACE_FLUSH` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | | names what forces pending storage-image results into guest memory | +| `APS5_TRACE_FRAMES` | string | `core/libs/prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp` | | records `<count>` presents from present `<start>` (whole numbers, `start:count`) | +| `APS5_TRACE_GPU` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/DisplayBuffer.cpp` | | prints GPU wait, submission and scanout events | +| `APS5_TRACE_HOST_IMPORT` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | | traces host-import mapping failures | +| `APS5_TRACE_INDIRECT_DRAWS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPrecheck.cpp` | | prints indirect draw packets that reach the precheck | +| `APS5_TRACE_LATE_LABELS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/WaitMemory.cpp` | | prints how late labels were trusted or refused | +| `APS5_TRACE_PREPARATION_WAIT` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp` | | prints where a demanded shader preparation sat in the queue | +| `APS5_TRACE_QUERY` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp` | | prints every 16th page query with its cost | +| `APS5_TRACE_RESOURCE_KEY_MISSES` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | | reports shader-resource cache key misses | +| `APS5_TRACE_SECONDS` | string | `core/libs/prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp` | | records from the first present `<start>` seconds after the first present for `<length>` seconds (whole numbers, `start:length`) | +| `APS5_TRACE_SHADER_REGS` | string | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp` | | prints a shader's registers for one hex program address, or `all` | +| `APS5_TRACE_SHADOW` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/UnitShadow.cpp` | | prints the first n shadow publishes, the first 64 seeds, every evict and retire | +| `APS5_TRACE_SKIPS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderDiagnostics.cpp` | | reports each distinct shader skip once | +| `APS5_TRACE_STAGING` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | | prints every distinct staged range once, with its size and whether an atomic element lies in it | +| `APS5_TRACE_SUMMARY_EVERY` | integer | `core/libs/prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp` | | number of presents between the trace summary lines | +| `APS5_TRACE_TEXTURES` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | | prints a sampled texture's bytes and how many are nonzero | +| `APS5_TRACE_UNREADABLE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp` | | names the code that checked an inaccessible range | +| `APS5_TRACE_UPLOAD` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | | names why a storage image is uploaded again | +| `APS5_UNIT_SHADOW_MIB` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/UnitShadow.cpp` | 1024 | budget in MiB of the unit shadow's slabs | +| `APS5_UNIT_SHADOW_SLAB_MIB` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/UnitShadow.cpp` | 8 | size in MiB of one unit-shadow slab | +| `APS5_VALIDATE_LEGACY` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/WriteEvidence.cpp` | | uses the legacy snapshot and full-compare path for pending-write validation | +| `APS5_VALIDATE_SYNC_SKIP` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/WriteEvidence.cpp` | | reads skipped dwords through the hook too and counts a mismatch | +| `APS5_VERIFY_DATA_HITS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchCache.cpp` | | recomputes a dispatch data hit and aborts on a disagreement | +| `APS5_VERIFY_DRAW_ENTRIES` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCache.cpp` | | re-decodes every draw-cache hit through the full path and checks it | +| `APS5_VERIFY_DRAW_RECIPE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCache.cpp` | | re-decodes a draw recipe instead of using the cache and checks the result | +| `APS5_VERIFY_KNOWN_VALUES` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferCopy.cpp` | | serves known values through the hook too and counts mismatches | +| `APS5_VERIFY_PROOFS` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | | runs the full walk beside every own-object refresh proof and aborts on a different decision | +| `APS5_VERIFY_RECIPE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | makes the ordinary prepare path beside a recipe hit and compares the answers | +| `APS5_VERIFY_REGISTER_KEYS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp` | | checks the incremental register fingerprint against the full state | +| `APS5_VERIFY_SET_LAYOUTS` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp` | | checks the draw's set layouts against those described from the shaders | +| `APS5_WAIT_LOCK` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/WaitMemory.cpp` | | always takes the GPU mutex on a wait (no unlocked label lookup) | +| `APS5_WAIT_LOCK_ENTRY` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/WaitMemory.cpp` | | takes the GPU mutex blocking on the entry instead of trying it | +| `APS5_WATCH_MEMORY` | string | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | watches named guest ranges (`<hex address>:<hex bytes>[,...]`) across dispatches; needs `APS5_SYNC_DISPATCH=1` | +| `APS5_WORKER_AFFINITY` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/WorkerAffinity.cpp` | | pins the queue workers and presenter to the performant cores (needed to enable it on non-Linux hosts) | +| `APS5_WORKER_AFFINITY_MASK` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/WorkerAffinity.cpp` | | explicit CPU affinity mask for the queue workers and presenter | +| `APS5_WORKING_SET_MIB` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | sets the process working-set minimum to this many MiB (Windows only) | +| `APS5_WRITE_EVIDENCE_AFTER` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/WriteEvidence.cpp` | 4 | consecutive unchanged observations before a dword's written-element evidence is trusted | +| `APS5_WRITE_EVIDENCE_MAX_KIB` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/WriteEvidence.cpp` | 16 | largest written range in KiB that the evidence may serve | +| `APS5_WRITE_EVIDENCE_SAMPLE` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/WriteEvidence.cpp` | 16 | reads one in this many pending writes to sample them | +| `APS5_WRITE_WATCH_IMPORTS` | string | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | | how host imports are write-tracked: `probe`, `watch` or `unwatch` | +| `APS5_WRITTEN_SHADOW_MAX_KIB` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | 2048 | upper bound in KiB of the written staging window | +| `APS5_WRITTEN_SHADOW_MIN_KIB` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | 16 | lower bound in KiB of the written staging window | + +### libSceAjm.native + +| Name | Kind | Source | Default | Description | +|------|------|--------|---------|-------------| +| `APS5_TRACE_AJM` | presence | `core/libs/prx/libSceAjm.native/src/Ajm.cpp` | | logs every job the title submits and what the library writes back | + +### libSceAudioOut + +| Name | Kind | Source | Default | Description | +|------|------|--------|---------|-------------| +| `APS5_TRACE_AUDIOOUT` | presence | `core/libs/prx/libSceAudioOut/src/AudioOut.cpp` | | logs each audio-out port opened with its type, sample count, frequency and format | +| `APS5_TRACE_AUDIOOUT2` | presence | `core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp` | | prints the title's AudioOut2 context and port parameters and stream activity | + +### libSceFiber + +| Name | Kind | Source | Default | Description | +|------|------|--------|---------|-------------| +| `APS5_TRACE_FIBER` | presence | `core/libs/prx/libSceFiber/Export.cpp` | | logs fiber initialization, switches and returns to the thread | + +### libSceFont + +| Name | Kind | Source | Default | Description | +|------|------|--------|---------|-------------| +| `ANYPS5_SYSTEM_FONTS` | string | `core/libs/prx/libSceFont/src/SystemFont.cpp` | | directory holding the system fonts, defaulting to anyps5-fonts beside the executable | + +### libSceNpEntitlementAccess + +| Name | Kind | Source | Default | Description | +|------|------|--------|---------|-------------| +| `ANYPS5_ENTITLEMENTS` | string | `core/libs/prx/libSceNpEntitlementAccess/Export.cpp` | | path of the entitlements ini listing owned add-ons, defaulting to anyps5-entitlements.ini beside the executable | + +### libScePad + +| Name | Kind | Source | Default | Description | +|------|------|--------|---------|-------------| +| `APS5_PAD_INPUT_FILE` | string | `core/libs/prx/libScePad/src/PadState.cpp` | | path of the file polled for injected pad button and stick commands | + +### libSceSaveData.native + +| Name | Kind | Source | Default | Description | +|------|------|--------|---------|-------------| +| `APS5_SAVEDATA_TRACE` | presence | `core/libs/prx/libSceSaveData.native/Export.cpp` | 3 | logs every save-data operation instead of the first three | + +### libSceSystemService + +| Name | Kind | Source | Default | Description | +|------|------|--------|---------|-------------| +| `ANYPS5_LANGUAGE` | integer | `core/libs/prx/libSceSystemService/Export.cpp` | 1 | console language number returned by sceSystemServiceParamGetInt, defaulting to English US | + +### libSceVideoOut + +| Name | Kind | Source | Default | Description | +|------|------|--------|---------|-------------| +| `ANYPS5_INPUT_CONFIG` | string | `core/libs/prx/libSceVideoOut/src/InputMapping.cpp` | | path of the input mapping file that binds host keys and mouse to guest pad actions | +| `ANYPS5_REQUIRE_DISPLAY` | presence | `core/libs/prx/libSceVideoOut/tests/Flip.cpp` | | makes video-out tests fail instead of skipping when no display or Vulkan device is available | +| `APS5_NO_PAD_INPUT` | presence | `core/libs/prx/libSceVideoOut/src/PadInput.cpp` | | ignores keyboard and mouse input so a measurement run is not advanced by a stray press | + +### libSceVideodec2 + +| Name | Kind | Source | Default | Description | +|------|------|--------|---------|-------------| +| `APS5_TRACE_VIDEODEC` | presence | `core/libs/prx/libSceVideodec2/Export.cpp` | | logs decoder memory queries, queue allocation and the first few decodes | + +### libc + +| Name | Kind | Source | Default | Description | +|------|------|--------|---------|-------------| +| `APS5_HANG_DUMP_SECS` | integer | `core/libs/prx/libc/src/specifics/windows/CrashReport.cpp` | | seconds after which a watchdog dumps all threads and aborts | +| `APS5_HEAP_CACHE_MIB` | integer | `core/libs/prx/libc/src/GuestHeap.cpp` | 4096 | size in MiB of the cache of freed large blocks before the oldest are decommitted | +| `APS5_HELPER_AFFINITY_MASK` | integer | `core/libs/prx/libc/include/CpuTopology.hpp` | | hex core mask that helper threads (deferred release, mirror refresh) are pinned to | +| `APS5_LOG_TIMESTAMPS` | presence | `core/libs/prx/libc/include/general/LogMacros.hpp` | | prefixes each log line with the milliseconds since the first log | +| `APS5_NO_PROCMAP_QUERY` | presence | `core/libs/prx/libc/include/specifics/linux/HostMapping.hpp` | | skips the /proc/self/maps PROCMAP_QUERY ioctl and scans the maps file instead | +| `APS5_NO_SSE4A_EMULATION` | presence | `core/libs/prx/libc/src/specifics/windows/CrashReport.cpp` | | disables emulation of AMD-only SSE4a instructions, making such a fault fatal | +| `APS5_NO_WRITE_WATCH` | presence | `core/libs/prx/libc/src/GuestArena.cpp` | | disables write watching so guest buffers are fully re-read on every use | +| `APS5_PIN_WAIT_MS` | integer | `core/libs/prx/libc/src/GuestAllocations.cpp` | 60000 | milliseconds to wait for a pinned allocation lease before failing | +| `APS5_TRACE_AFFINITY` | presence | `core/libs/prx/libc/include/CpuTopology.hpp` | | prints a line naming the role, tid and mask of every thread pinning | +| `APS5_TRACE_CALLS` | string | `core/libs/prx/libc/src/specifics/windows/CallTrace.cpp` | | comma-separated libraries (or all) whose guest calls are traced | +| `APS5_TRACE_EXIT` | presence | `core/libs/prx/libc/src/Process.cpp` | | prints a backtrace of the caller of exit, _Exit and the process-exit path | +| `APS5_TRACE_SSE4A` | presence | `core/libs/prx/libc/src/specifics/windows/CrashReport.cpp` | | prints each emulated SSE4a instruction once per rip | +| `APS5_WATCH_ADDR` | integer | `core/libs/prx/libc/src/specifics/windows/CrashReport.cpp` | | absolute hex address whose writes are reported after its page is committed | +| `APS5_WATCH_WRITE` | integer | `core/libs/prx/libc/src/specifics/windows/CrashReport.cpp` | | guest ELF vaddr whose 8-byte writes are reported by single-stepping the protected page | + +### libkernel + +| Name | Kind | Source | Default | Description | +|------|------|--------|---------|-------------| +| `APS5_COARSE_TIMED_WAITS` | presence | `core/libs/prx/libkernel/Time/TimedWait.cpp` | | on Windows, rounds timed waits up to the scheduler tick | +| `APS5_JOB_AFFINITY` | presence | `core/libs/prx/libkernel/Pthread/src/Thread.cpp` | | pins the title's job worker threads to the efficiency cores of a hybrid CPU | +| `APS5_JOB_AFFINITY_MASK` | integer | `core/libs/prx/libkernel/Pthread/src/Thread.cpp` | | hex core mask that job worker threads are pinned to | +| `APS5_NO_JOB_AFFINITY` | presence | `core/libs/prx/libkernel/Pthread/src/Thread.cpp` | | disables pinning of job worker threads | +| `APS5_NO_TIMER_RESOLUTION` | presence | `core/libs/prx/libkernel/Time/Time.cpp` | | keeps the default 15.6 ms scheduler tick instead of raising it to 0.5 ms on Windows | +| `APS5_TIME_SCALE` | integer | `core/libs/prx/libkernel/Time/Time.cpp` | 1 | factor applied to the guest monotonic clocks and TSC; sleeps still take real time | +| `APS5_TRACE_APR` | presence | `core/libs/prx/libkernel/Apr/src/Apr.cpp` | | logs every APR file read with its path, offset, size and destination | +| `APS5_TRACE_MEMORY` | presence | `core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp` | | logs direct-memory map, remap, protect, unmap and physical allocation operations | +| `APS5_TRACE_SYNC` | presence | `core/libs/prx/libkernel/EventFlag/src/EventFlag.cpp` | | logs event-flag create, set and wait operations | +| `APS5_TRACE_TIMERS` | presence | `core/libs/prx/libkernel/Time/Time.cpp` | | runs a timed-wait selftest at startup and prints mean, p50 and max latencies | +| `APS5_TRACE_USLEEP` | presence | `core/libs/prx/libkernel/Time/Time.cpp` | | reports every 2000 usleep calls the guest call sites that sleep most | +| `APS5_TRACE_WAITS` | presence | `core/libs/prx/libkernel/Time/Time.cpp` | | aggregates blocking waits per guest call site and prints the longest-waiting sites every 3 s | + diff --git a/tools/check_conventions.py b/tools/check_conventions.py index a5dade7483..cd0cb0f05e 100644 --- a/tools/check_conventions.py +++ b/tools/check_conventions.py @@ -29,6 +29,7 @@ RULES = { "commit-subject": ("error", "docs/dev/CONVENTIONS.md", "Commit subject is not Conventional Commits"), "pr-title": ("error", "docs/dev/CONVENTIONS.md", "Pull request title is not Conventional Commits"), "pr-template": ("error", ".github/pull_request_template.md", "Fill in the pull request template"), + "env-reference": ("error", "docs/dev/ENVIRONMENT.md", "Every APS5_* or ANYPS5_* name the code reads has a row in docs/dev/ENVIRONMENT.md, and every row is a name the code still reads"), } CPP = {".c", ".cc", ".cpp", ".cxx", ".h", ".hh", ".hpp", ".hxx", ".inl", ".ipp"} @@ -57,6 +58,13 @@ FENCE = re.compile(r"^ {0,3}(```|~~~).*?^ {0,3}\1[ \t]*\r?$|^ {0,3}(?:```|~~~).* INLINE = re.compile(r"`[^`\n]*`") SECTION = re.compile(r"^###(?!#)\s*(.+?)\s*$", re.M) KEY = re.compile(r"^( *)([A-Za-z0-9_-]+|'[^']*'|\"[^\"]*\"):(?:\s+(.*?))?\s*$") +ENV_PREFIX = r"(?:APS5|ANYPS5)_[A-Za-z0-9_]+" +ENV_GREP = (r'(std::)?getenv\([[:space:]]*"(APS5|ANYPS5)_[A-Za-z0-9_]+"' + r'|IsEnvironmentSet\([[:space:]]*"(APS5|ANYPS5)_[A-Za-z0-9_]+"' + r'|kibSetting\([[:space:]]*"(APS5|ANYPS5)_[A-Za-z0-9_]+"' + r'|MaskFromEnvironment\([[:space:]]*"(APS5|ANYPS5)_[A-Za-z0-9_]+"') +ENV_READER = re.compile(r'(?:(?:std::)?getenv|IsEnvironmentSet|kibSetting|MaskFromEnvironment)\(\s*"(' + ENV_PREFIX + r')"') +ENV_NAME = re.compile(r"`(" + ENV_PREFIX + r")`") def git(*args, text=True): @@ -275,6 +283,21 @@ class Check: if not CONVENTIONAL.match(subject): self.report("commit-subject", "", 0, f"{sha} {subject}") + def env_reference(self): + doc = "docs/dev/ENVIRONMENT.md" + if doc not in self.tree: + return + result = subprocess.run(["git", "grep", "-nE", ENV_GREP, self.head, "--", "core"], + capture_output=True, encoding="utf-8", errors="replace") + reads = set() + for line in result.stdout.splitlines(): + reads.update(match.group(1) for match in ENV_READER.finditer(line)) + documented = set(ENV_NAME.findall(self.show(doc))) + for name in sorted(reads - documented): + self.report("env-reference", doc, 0, f"{name} is read by the code but has no row") + for name in sorted(documented - reads): + self.report("env-reference", doc, 0, f"{name} has a row but the code no longer reads it") + def pull_request(self, title, body): if not CONVENTIONAL.match(title): self.report("pr-title", "", 0, title) @@ -503,6 +526,7 @@ if __name__ == "__main__": check.docs() check.workflows() check.commits() + check.env_reference() pr = json.load(open(args.event, encoding="utf-8")).get("pull_request") if args.event else None if pr: check.pull_request(pr["title"], pr["body"]) From abec10fe2d298ec71af797202469c182587b9409 Mon Sep 17 00:00:00 2001 From: "husari.orozco" <husari09.oro@gmail.com> Date: Wed, 14 Oct 2026 16:54:57 -0600 Subject: [PATCH 0961/1000] fix(tools): catch the EnvInt and GetEnvironmentVariableA readers and take the review rows --- CMakeLists.txt | 1 + docs/dev/ENVIRONMENT.md | 17 ++++++++++++++--- tools/check_conventions.py | 6 ++++-- tools/env_reference.py | 18 ++++++++++++++---- tools/tests/test_env_reference.py | 7 ++++++- 5 files changed, 39 insertions(+), 10 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 8b7705614b..91efaf77e3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -197,6 +197,7 @@ if(BUILD_TESTING) set_tests_properties(conventions_bom_shebang PROPERTIES TIMEOUT 20) add_test(NAME env_reference COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_env_reference.py) + set_tests_properties(env_reference PROPERTIES TIMEOUT 20) add_test(NAME conventions_body_bom COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_body_bom.py) add_test(NAME conventions_blob_reads COMMAND ${Python3_EXECUTABLE} diff --git a/docs/dev/ENVIRONMENT.md b/docs/dev/ENVIRONMENT.md index 00470832da..5fd30ae620 100644 --- a/docs/dev/ENVIRONMENT.md +++ b/docs/dev/ENVIRONMENT.md @@ -117,7 +117,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_DUMP_SHADERS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp` | | writes each prepared shader's request to shader_<address>.req | | `APS5_DUMP_STORAGE` | string | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | | selects storage textures to dump by hex address or WxHxL[:format] extent as storage_<address>_<n>.raw | | `APS5_DUMP_SUBMITS` | string | `core/libs/prx/libSceAgcDriver/Execution/src/SubmissionCapture.cpp` | | directory in which every submission is captured for replay | -| `APS5_DUMP_TARGETS` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp` | 0 | saves the first n renders of every color target as a raw file | +| `APS5_DUMP_TARGETS` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp` | 0 | dumps the first n renders of each non-resident color target as target_<address>_<n>.raw in the working directory; a non-zero value also runs every draw synchronously, and in practice it needs APS5_NO_RESIDENT_TARGETS=1 | | `APS5_DUMP_TEXTURE` | string | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | | hex addresses or WxH extents whose sampled textures are dumped on first upload as texture_<address>_<n>.raw | | `APS5_EAGER_WRITEBACK` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | | writes back pending storage layers eagerly | | `APS5_FILL_CHAIN` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | seeds and doubles the fill pattern in place in the import | @@ -136,7 +136,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_HOOK_FLUSH_CPU_BLOCKS` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | set to 0 flushes pending images whose every 64 KiB block the CPU wrote, instead of skipping them | | `APS5_HOOK_FULL_SYNC` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | makes the flush hook's recorded-store wait a full Sync under the GpuMutex | | `APS5_HOOK_LOCKED_WAIT` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | keeps the flush hook's pending-write wait under the GpuMutex | -| `APS5_HOST_IMPORT_MIB` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | | budget in MiB for pinned host imports; unset uses the larger of 6 GiB and half the physical memory, capped by the host heap | +| `APS5_HOST_IMPORT_MIB` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | 6144 | budget in MiB for pinned host imports; 6144 is the floor, and an unset or 0 value uses the largest host heap minus 2 GiB | | `APS5_IGNORE_DEPTH_TEST` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp` | | renders depth- and stencil-tested draws without a depth target as if the tests always passed | | `APS5_INDIRECT_VERTEX_MIB` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/IndirectDraw.cpp` | 64 | cap in MiB on a vertex attribute extent, above which a GPU-side indirect draw resolves on the CPU | | `APS5_INSERT_COMPARE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchCache.cpp` | | compares a fresh capture's regions against the recorder before inserting it into the cache | @@ -181,12 +181,14 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_NO_DISPATCH_KEY_HYGIENE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp` | | mixes every shader register into the dispatch key instead of only the registers that affect the dispatch | | `APS5_NO_DISPATCH_LRU` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchInsert.cpp` | | clears the whole dispatch cache on overflow instead of evicting the least recently used entry | | `APS5_NO_DISPATCH_RECIPE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | builds no dispatch recipes, so dispatch-cache hits repeat the recorded work | +| `APS5_NO_DRAW_DATA_HITS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCache.cpp` | | disables draw-cache hits that differ only in ShaderData words | | `APS5_NO_DRAW_KEY` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCache.cpp` | | registers no draw key, so every draw decodes its programs instead of hitting the draw cache | | `APS5_NO_DRAW_KEY_TRIM` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp` | | keys the render target and index ranges into the draw resource key instead of repeating the alias checks on a hit | | `APS5_NO_DRAW_PRECHECK` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPrecheck.cpp` | | disables the draw precheck that rejects empty or invalid draws early | | `APS5_NO_DRAW_RECIPE` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp` | | builds no draw recipes, so draw-cache hits repeat the recorded stages | | `APS5_NO_DRAW_RESOURCE_CACHE` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp` | | builds every draw's shader resources instead of reusing a cached identical build | | `APS5_NO_DRAW_SRT_ENTRIES` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCache.cpp` | | disables the draw cache's SRT entries, so every draw decodes its programs | +| `APS5_NO_DRAW_STAGE_DATA_HITS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCache.cpp` | | disables draw-cache hits that differ only in a stage's ShaderData words | | `APS5_NO_EPOCH_REVALIDATE` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | | runs every Revalidate's per-element and per-region checks instead of the epoch gate | | `APS5_NO_FAST_REVALIDATE` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | | always repeats the resource lookups instead of using the fast proof | | `APS5_NO_FILL_CLEAR` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferFill.cpp` | | disables the clear-through-DCC-keys path in a fill | @@ -298,6 +300,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_SHARED_MEMORY_COUNTERS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp` | | accumulates the memory counters into one shared set with atomic RMWs | | `APS5_SKIP_REQUEST_DUMPS` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderDiagnostics.cpp` | 8 | caps RecompileRequest dumps per shader-skip reason | | `APS5_SKIP_SHADER` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Diagnostics.cpp` | 0 | hex program address of a shader to skip preparing | +| `APS5_SNAPSHOT_CHECK_FATAL` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | | throws on the first snapshot that differs from registered memory instead of only counting them | | `APS5_SPIRV_OPT` | string | `core/libs/prx/libSceAgcDriver/tests/SpirvOptimizer.cpp` | | selects the SPIR-V optimization pipeline: `full` restores the performance pipeline, `none` skips optimization | | `APS5_SRGB_ATTACHMENT_PROXY` | string | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | | forces the sRGB color-attachment proxy format; `1` forces it | | `APS5_SRGB_SHADER_DECODE` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp` | | forces shader sRGB decode for the sRGB 8-bit formats | @@ -315,7 +318,10 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_SYNC_SHADER_PREPARE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp` | | prepares registered shaders on the registering thread instead of in the background | | `APS5_SYNC_TEXTURE_UPLOAD` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | | restores the waiting batch for every texture upload | | `APS5_TEXTURE_CACHE_MIB` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | 0 | forced byte budget in MiB for the storage and sampled texture caches; `0` selects the budget automatically | +| `APS5_THREAD_NICE` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/ThreadPriority.cpp` | -10 | nice value the raised queue workers and presenter ask rtkit for (clamped to -20..19) | | `APS5_THREAD_PRIORITY` | string | `core/libs/prx/libSceAgcDriver/Execution/src/ThreadPriority.cpp` | off | raises the queue workers and presenter above the title's threads: `off`, `high` or `rt` | +| `APS5_THREAD_RT` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/ThreadPriority.cpp` | 10 | realtime priority the raised threads ask rtkit for (clamped to 1..99) | +| `APS5_THREAD_RTTIME_US` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/ThreadPriority.cpp` | 200000 | hard RLIMIT_RTTIME the realtime plan lowers to, in microseconds | | `APS5_TRACE_BARRIERS` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | prints that many simulated hazard barriers before stopping | | `APS5_TRACE_BIGBUF` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | | lists every copied guest region of at least 1 MiB | | `APS5_TRACE_CAPSYNC` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/WriteEvidence.cpp` | | prints every unlocked hook wait | @@ -326,11 +332,12 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_TRACE_DISPATCH_CACHE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp` | | reports dispatch-cache entries whose key changed | | `APS5_TRACE_DISPATCH_IO` | string | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp` | | prints every dispatch's resources; `2` also lists the small-buffer words | | `APS5_TRACE_DRAWS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp` | | prints one line per draw with its phases and inputs | +| `APS5_TRACE_DRAW_CACHE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCacheStatistics.cpp` | | reports draw-cache entries whose key changed | | `APS5_TRACE_EVENTS` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/PerformanceTrace.cpp` | | number of events a windowed trace keeps in its buffer | | `APS5_TRACE_FILL_COVER` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | | prints the first fills that meet images without covering them | | `APS5_TRACE_FLUSH` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | | names what forces pending storage-image results into guest memory | | `APS5_TRACE_FRAMES` | string | `core/libs/prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp` | | records `<count>` presents from present `<start>` (whole numbers, `start:count`) | -| `APS5_TRACE_GPU` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/DisplayBuffer.cpp` | | prints GPU wait, submission and scanout events | +| `APS5_TRACE_GPU` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/DisplayBuffer.cpp` | | prints GPU wait, submission and scanout events, the execute lines, and waits longer than 200 ms (read in DisplayBuffer, PacketExecution, QueueWorker, Submission and WaitMemory) | | `APS5_TRACE_HOST_IMPORT` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | | traces host-import mapping failures | | `APS5_TRACE_INDIRECT_DRAWS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPrecheck.cpp` | | prints indirect draw packets that reach the precheck | | `APS5_TRACE_LATE_LABELS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/WaitMemory.cpp` | | prints how late labels were trusted or refused | @@ -350,9 +357,12 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_UNIT_SHADOW_SLAB_MIB` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/UnitShadow.cpp` | 8 | size in MiB of one unit-shadow slab | | `APS5_VALIDATE_LEGACY` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/WriteEvidence.cpp` | | uses the legacy snapshot and full-compare path for pending-write validation | | `APS5_VALIDATE_SYNC_SKIP` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/WriteEvidence.cpp` | | reads skipped dwords through the hook too and counts a mismatch | +| `APS5_VERIFY_BDA_TABLES` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | | checks a reused BDA page table against the one just built | | `APS5_VERIFY_DATA_HITS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchCache.cpp` | | recomputes a dispatch data hit and aborts on a disagreement | +| `APS5_VERIFY_DRAW_DATA_HITS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCache.cpp` | | recomputes a draw data hit and aborts on a disagreement | | `APS5_VERIFY_DRAW_ENTRIES` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCache.cpp` | | re-decodes every draw-cache hit through the full path and checks it | | `APS5_VERIFY_DRAW_RECIPE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCache.cpp` | | re-decodes a draw recipe instead of using the cache and checks the result | +| `APS5_VERIFY_DRAW_SNAPSHOTS` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | | re-reads a reused draw snapshot and counts the ones that differ | | `APS5_VERIFY_KNOWN_VALUES` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferCopy.cpp` | | serves known values through the hook too and counts mismatches | | `APS5_VERIFY_PROOFS` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | | runs the full walk beside every own-object refresh proof and aborts on a different decision | | `APS5_VERIFY_RECIPE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | makes the ordinary prepare path beside a recipe hit and compares the answers | @@ -438,6 +448,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | Name | Kind | Source | Default | Description | |------|------|--------|---------|-------------| +| `APS5_CRASH_INDIRECT_MEMORY` | presence | `core/libs/prx/libc/src/specifics/windows/CrashReport.cpp` | | dumps the words the faulting registers point at in the Windows crash report | | `APS5_HANG_DUMP_SECS` | integer | `core/libs/prx/libc/src/specifics/windows/CrashReport.cpp` | | seconds after which a watchdog dumps all threads and aborts | | `APS5_HEAP_CACHE_MIB` | integer | `core/libs/prx/libc/src/GuestHeap.cpp` | 4096 | size in MiB of the cache of freed large blocks before the oldest are decommitted | | `APS5_HELPER_AFFINITY_MASK` | integer | `core/libs/prx/libc/include/CpuTopology.hpp` | | hex core mask that helper threads (deferred release, mirror refresh) are pinned to | diff --git a/tools/check_conventions.py b/tools/check_conventions.py index cd0cb0f05e..af2d083056 100644 --- a/tools/check_conventions.py +++ b/tools/check_conventions.py @@ -62,8 +62,10 @@ ENV_PREFIX = r"(?:APS5|ANYPS5)_[A-Za-z0-9_]+" ENV_GREP = (r'(std::)?getenv\([[:space:]]*"(APS5|ANYPS5)_[A-Za-z0-9_]+"' r'|IsEnvironmentSet\([[:space:]]*"(APS5|ANYPS5)_[A-Za-z0-9_]+"' r'|kibSetting\([[:space:]]*"(APS5|ANYPS5)_[A-Za-z0-9_]+"' - r'|MaskFromEnvironment\([[:space:]]*"(APS5|ANYPS5)_[A-Za-z0-9_]+"') -ENV_READER = re.compile(r'(?:(?:std::)?getenv|IsEnvironmentSet|kibSetting|MaskFromEnvironment)\(\s*"(' + ENV_PREFIX + r')"') + r'|MaskFromEnvironment\([[:space:]]*"(APS5|ANYPS5)_[A-Za-z0-9_]+"' + r'|EnvInt\([[:space:]]*"(APS5|ANYPS5)_[A-Za-z0-9_]+"' + r'|GetEnvironmentVariableA\([[:space:]]*"(APS5|ANYPS5)_[A-Za-z0-9_]+"') +ENV_READER = re.compile(r'(?:(?:std::)?getenv|IsEnvironmentSet|kibSetting|MaskFromEnvironment|EnvInt|GetEnvironmentVariableA)\(\s*"(' + ENV_PREFIX + r')"') ENV_NAME = re.compile(r"`(" + ENV_PREFIX + r")`") diff --git a/tools/env_reference.py b/tools/env_reference.py index 045f56746a..986947f42d 100644 --- a/tools/env_reference.py +++ b/tools/env_reference.py @@ -25,6 +25,8 @@ def pattern(prefixes): r"|IsEnvironmentSet\(\s*\"(?P<presence_name>" + name + r")\"\s*\)" r"|MaskFromEnvironment\(\s*\"(?P<mask_name>" + name + r")\"\s*\)" r"|kibSetting\(\s*\"(?P<kib_name>" + name + r")\"\s*,\s*(?P<kib_default>[0-9]+)" + r"|EnvInt\(\s*\"(?P<envint_name>" + name + r")\"\s*,\s*(?P<envint_default>-?[0-9]+)" + r"|GetEnvironmentVariableA\(\s*\"(?P<win_name>" + name + r")\"" ) @@ -36,8 +38,10 @@ def documented_pattern(prefixes): def kind_of(reader, window): if reader == "IsEnvironmentSet": return "presence" - if reader in ("kibSetting", "MaskFromEnvironment"): + if reader in ("kibSetting", "MaskFromEnvironment", "EnvInt"): return "integer" + if reader == "GetEnvironmentVariableA": + return "string" if NULL_TEST.search(window): return "presence" if INTEGER_TEST.search(window): @@ -46,8 +50,9 @@ def kind_of(reader, window): def default_of(match, window): - if match.group("kib_default"): - return match.group("kib_default") + for group in ("kib_default", "envint_default"): + if match.group(group): + return match.group(group) found = re.search(r"\?[\s\S]*?:\s*([0-9]+)", window) return found.group(1) if found else "" @@ -78,13 +83,18 @@ def reads(roots, prefixes=PREFIXES): window = "\n".join(lines[number - 1:number + 2]) for match in read.finditer(line): name = (match.group("getenv_name") or match.group("presence_name") or - match.group("mask_name") or match.group("kib_name")) + match.group("mask_name") or match.group("kib_name") or + match.group("envint_name") or match.group("win_name")) if match.group("getenv"): reader = "getenv" elif match.group("presence_name"): reader = "IsEnvironmentSet" elif match.group("mask_name"): reader = "MaskFromEnvironment" + elif match.group("envint_name"): + reader = "EnvInt" + elif match.group("win_name"): + reader = "GetEnvironmentVariableA" else: reader = "kibSetting" found.setdefault(name, []).append( diff --git a/tools/tests/test_env_reference.py b/tools/tests/test_env_reference.py index 14cc0d3d5f..87d5a961d2 100644 --- a/tools/tests/test_env_reference.py +++ b/tools/tests/test_env_reference.py @@ -16,6 +16,8 @@ static unsigned long d = MaskFromEnvironment("APS5_WORKER_AFFINITY_MASK"); static const char* e = std::getenv("ANYPS5_GPU"); static const char* f = std::getenv("PATH"); static const char* g = std::getenv("TZ"); +static int h = EnvInt("APS5_THREAD_NICE", -10); +static int i = GetEnvironmentVariableA("APS5_CRASH_INDIRECT_MEMORY", nullptr, 0); """ @@ -34,8 +36,11 @@ class EnvReferenceTests(unittest.TestCase): self.assertEqual( set(found), {"APS5_PROFILE_DRAW", "APS5_TRACE_GPU", "APS5_HOST_IMPORT_MIB", - "APS5_WORKER_AFFINITY_MASK", "ANYPS5_GPU"}) + "APS5_WORKER_AFFINITY_MASK", "ANYPS5_GPU", "APS5_THREAD_NICE", + "APS5_CRASH_INDIRECT_MEMORY"}) self.assertEqual(found["APS5_PROFILE_DRAW"][0][1], 1) + self.assertEqual(found["APS5_THREAD_NICE"][0][2:], ("integer", "-10")) + self.assertEqual(found["APS5_HOST_IMPORT_MIB"][0][2:], ("integer", "6144")) def test_the_default_prefixes_cover_both(self): root = self.fixture() From fc14940647c049d18e0307ffbb977a74b23209ee Mon Sep 17 00:00:00 2001 From: "husari.orozco" <husari09.oro@gmail.com> Date: Sat, 17 Oct 2026 10:03:34 -0600 Subject: [PATCH 0962/1000] docs(environment): add the rows the readers on main need --- docs/dev/ENVIRONMENT.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/docs/dev/ENVIRONMENT.md b/docs/dev/ENVIRONMENT.md index 5fd30ae620..c46bec9fe1 100644 --- a/docs/dev/ENVIRONMENT.md +++ b/docs/dev/ENVIRONMENT.md @@ -9,6 +9,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `ANYPS5_LOADEXEC_TEST_ROOT` | string | `core/libs/tests/GuestLoadExecHost.cpp` | | directory the load-exec host test reads its guest executable from | | `ANYPS5_TEST_QUOTED_ARGV0` | presence | `core/libs/tests/GuestLoadExecHost.cpp` | | makes the load-exec test's first argument one with quotes, a backslash and non-ASCII text | | `APS5_EBOOT` | string | `core/libs/tests/GuestSystemExit.cpp` | | host path of the executable launched for /app0/eboot.bin, defaulting to the running executable | +| `APS5_SKIP_TIMING_TESTS` | presence | `core/libs/tests/PreciseWait.cpp` | | treats the timing tests' runner as preempted, so they skip without probing the clock | | `APS5_USBD_DEVICE` | string | `core/libs/tests/GuestUsbd.cpp` | | selects the USB device a test drives, as vendor:product in hex | ### core/relinker @@ -38,6 +39,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_NO_PERF_DOM_INTERSECTION` | presence | `core/shader/recompiler/ControlFlow/src/Structurizer.cpp` | | computes dominator intersections into a fresh vector instead of intersecting in place | | `APS5_NO_PERF_ITERATION_SELECTION` | presence | `core/shader/recompiler/ControlFlow/src/Structurizer.cpp` | | recomputes the post-dominator sets when finding a loop's iteration-selection merge instead of reusing them | | `APS5_NO_RESULT_MEMO` | presence | `core/shader/recompiler/Recompiler.cpp` | | materializes a fresh result for every recompile instead of reusing the result memo | +| `APS5_NO_WINDOW_NARROWING` | presence | `core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp` | | indexes every slot of the shader's vector window instead of only the slots the program uses | | `APS5_PROBE` | string | `core/shader/recompiler/Translation/src/DispatchInstructions.cpp` | | pc:vgpr[:shift] probe that copies that VGPR into v255 after the instruction so image stores write it as a per-lane trace | | `APS5_RAYTRACING` | string | `core/shader/recompiler/Translation/src/DispatchInstructions.cpp` | | selects the image_bvh_intersect_ray behaviour: strict throws and miss writes 0xffffffff | | `APS5_SAVEEXEC_WRITE_FIRST` | presence | `core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp` | | writes the saved exec to the destination before reading the source in s_*_saveexec | @@ -46,6 +48,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_TRACE_BDA` | presence | `core/shader/recompiler/Optimization/src/ResourceTracker.cpp` | | prints the first few accesses that make a program take the address-based path | | `APS5_TRACE_BUFFER_WRITTEN` | presence | `core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp` | | prints how many guest buffer elements a program may store to and how many it only loads | | `APS5_TRACE_IMAGE_TABLE` | presence | `core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp` | | traces image-table resolution | +| `APS5_VERIFY_SRT_COMPACT` | presence | `core/shader/recompiler/Optimization/src/SrtWalker/SrtDescriptorEvaluation.cpp` | | walks every SRT descriptor a second time through the IR evaluator and throws when the two walks differ | | `APS5_WAVE_LDS_SUBGROUP` | presence | `core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp` | | restores subgroup-scope LDS barriers for single-wave workgroups instead of workgroup scope | ### libSceAgc @@ -120,6 +123,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_DUMP_TARGETS` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp` | 0 | dumps the first n renders of each non-resident color target as target_<address>_<n>.raw in the working directory; a non-zero value also runs every draw synchronously, and in practice it needs APS5_NO_RESIDENT_TARGETS=1 | | `APS5_DUMP_TEXTURE` | string | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | | hex addresses or WxH extents whose sampled textures are dumped on first upload as texture_<address>_<n>.raw | | `APS5_EAGER_WRITEBACK` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | | writes back pending storage layers eagerly | +| `APS5_EXACT_DRAW_WRITES` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/WrittenBuffers.cpp` | | sizes a color or depth target's draw write ranges from its own bytes and slices instead of the slack and unknown-byte approximation; 0 leaves the approximation on | | `APS5_FILL_CHAIN` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | seeds and doubles the fill pattern in place in the import | | `APS5_FILL_CLEAR_EXACT_ONLY` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferFill.cpp` | | turns a fill into a clear only when it covers the target exactly and nothing else | | `APS5_FILL_SYNC` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | waits for every recorded store over the fill's range | @@ -153,9 +157,12 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_LABEL_TRUST_LATE` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | trusts cache entries with a stamp at or below the wait's stamp under the late rule (off unless set to a non-zero value) | | `APS5_LABEL_TRY_EACH_PACKET` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/DeferredLabels.cpp` | | attempts to record deferred labels at every self-locking packet instead of deferring to its lock | | `APS5_LAND_WAITS_ALL_QUEUES` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/WaitMemory.cpp` | | notes pending waits on queue 0 as well | +| `APS5_LAZY_SHADER_PREPARE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp` | | prepares a registered shader on its first use instead of while registering it | | `APS5_LDS_SLACK` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp` | 0 | dwords added to every dispatch's LDS allocation | | `APS5_LOCKED_BUILD` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | builds the whole dispatch preparation under the GpuMutex, skipping the split path | | `APS5_LOCKED_DRAW_PREPARE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp` | | takes the GpuMutex before preparing a draw | +| `APS5_LOG_STATS` | presence | `core/libs/prx/libSceAgcDriver/Execution/include/AsyncLog.hpp` | | prints the AGC log's line, byte and coalescing statistics every window; 0 turns them off | +| `APS5_LOG_SYNC` | presence | `core/libs/prx/libSceAgcDriver/Execution/include/AsyncLog.hpp` | | writes AGC log lines on the calling thread instead of through the asynchronous sink thread | | `APS5_LOOP_GUARD` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | | enables the fault buffer that guards shader loops running past their bound | | `APS5_MIRROR_REFRESH_THREADS` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | 3 | number of helper threads that refresh heap mirrors, clamped to 0 through 16 | | `APS5_NO_ADDRESS_SPACE_CACHE` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | | disables the address-space cache so every build rebuilds its space | @@ -186,9 +193,11 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_NO_DRAW_KEY_TRIM` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp` | | keys the render target and index ranges into the draw resource key instead of repeating the alias checks on a hit | | `APS5_NO_DRAW_PRECHECK` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPrecheck.cpp` | | disables the draw precheck that rejects empty or invalid draws early | | `APS5_NO_DRAW_RECIPE` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp` | | builds no draw recipes, so draw-cache hits repeat the recorded stages | +| `APS5_NO_DRAW_RELOCATION` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRelocate.cpp` | | disables draw relocation, which rewrites a draw's moved user words from a learned variant instead of preparing the draw again | | `APS5_NO_DRAW_RESOURCE_CACHE` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp` | | builds every draw's shader resources instead of reusing a cached identical build | | `APS5_NO_DRAW_SRT_ENTRIES` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCache.cpp` | | disables the draw cache's SRT entries, so every draw decodes its programs | | `APS5_NO_DRAW_STAGE_DATA_HITS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCache.cpp` | | disables draw-cache hits that differ only in a stage's ShaderData words | +| `APS5_NO_EPOCH_COLLECT_MAP` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp` | | drops the per-epoch map of completed collects, leaving only the fixed-size thread memo to answer a repeated collect | | `APS5_NO_EPOCH_REVALIDATE` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | | runs every Revalidate's per-element and per-region checks instead of the epoch gate | | `APS5_NO_FAST_REVALIDATE` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | | always repeats the resource lookups instead of using the fast proof | | `APS5_NO_FILL_CLEAR` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferFill.cpp` | | disables the clear-through-DCC-keys path in a fill | @@ -200,6 +209,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_NO_GPU_DUMP` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | decodes dump frames from the tiled guest buffer on the CPU | | `APS5_NO_GPU_INDIRECT` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/IndirectDispatch.cpp` | | reads indirect dispatch arguments on the CPU instead of letting the GPU count them | | `APS5_NO_GPU_INDIRECT_DRAW` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp` | | reads indirect draw arguments through the flush hook on the CPU instead of from the GPU | +| `APS5_NO_GPU_SAMPLE_DUMPS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | dumps occlusion-counter samples on the CPU instead of through the GPU copy path | | `APS5_NO_HOLD_PROFILE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp` | | turns off GpuMutex hold-time profiling, keeping only the wait timing | | `APS5_NO_HOOKSYNC_PROFILE` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | leaves only the plain hook-sync counts under APS5_PROFILE_DRAW | | `APS5_NO_HOOK_COMPLETION_GUARD` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | lets the flush hook record the accessing worker's queued labels even from inside a completion action | @@ -234,6 +244,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_NO_PERF_FRONTEND_PAIR` | presence | `core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp` | | disables frontend-pair reuse in the shader-memory test | | `APS5_NO_PERF_LAYOUT_PAIR` | presence | `core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp` | | disables layout-pair reuse in the shader-memory test | | `APS5_NO_PIPELINE_CACHE` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp` | | builds a pipeline per draw instead of reusing cached ones | +| `APS5_NO_PIPELINE_JOURNAL` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp` | | keeps the pipeline journal off; 0 leaves it on, and it is also off while the shader disk cache is disabled | | `APS5_NO_PIPELINE_LTO` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp` | | disables background link-time optimization of graphics pipeline libraries | | `APS5_NO_POOL_CACHE` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | | allocates descriptor pools fresh instead of through the descriptor cache | | `APS5_NO_PRECOLLECT` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | | disables the stage-A precollect pass | @@ -244,6 +255,8 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_NO_RECORDED_CLEAR` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | | waits for a storage image's fast clear in a batch of its own instead of recording it | | `APS5_NO_RECORDER_SYNC_DRAWS` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp` | | gives the draws the recorder waits for their own batch instead of recording them with the wait | | `APS5_NO_RECORD_COPIED_DRAWS` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp` | | waits for copied-write draws at once instead of listing their write-back for later | +| `APS5_NO_RECT_INDIRECT` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/IndirectDraw.cpp` | | sends an indirect rectangle-list draw down the CPU path instead of the GPU-side path | +| `APS5_NO_RESIDENT_LINEAR` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp` | | drops the residency of a linear color target whose address already has a host import, so it is copied in and out again | | `APS5_NO_RESIDENT_PRESENT` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | always presents through guest memory instead of the resident image | | `APS5_NO_RESIDENT_TARGETS` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp` | | copies every render target in and out again instead of keeping it resident | | `APS5_NO_RESOURCE_CACHE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | disables the shared resource cache, so every dispatch builds its resources fresh | @@ -279,6 +292,12 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_NO_WRITE_EVIDENCE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/WriteEvidence.cpp` | | disables the written-element evidence that lets a read skip the flush hook | | `APS5_NO_WRITTEN_SHADOW` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | | binds written staging elements in place instead of through the written shadow | | `APS5_PACKET_EPOCH` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/WaitStatistics.cpp` | | makes queue workers bump the collect epoch before every packet | +| `APS5_PIPELINED_DRAWS` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp` | 8 | depth of the queue-0 draw pipeline: 0 records draws inline, 1 selects the default depth of 8, and any other value is the depth capped at 256 | +| `APS5_PIPELINE_DISPATCH` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp` | | records an eligible dispatch as an item in the queue-0 pipeline instead of running it inline | +| `APS5_PIPELINE_DMA` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp` | | records a queue-0 DMA data packet in the draw pipeline instead of executing it inline | +| `APS5_PIPELINE_DRAIN_FILLS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferFill.cpp` | | runs queue-0 buffer fills through the draw pipeline to keep their order; any value stores them in place instead | +| `APS5_PIPELINE_DRAIN_LABELS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp` | | answers a queue-0 wait from the draw pipeline's own pending labels while it is busy; any value drains the pipeline at the wait | +| `APS5_PIPELINE_DRAIN_SUBMISSIONS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp` | | lets a submission end without submitting its recorded work, so it joins a later submit with the pipeline's queue; any value drains the pipeline and submits at the end of the submission | | `APS5_PIPELINE_STATS` | string | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | captures and prints driver statistics for each new pipeline, requiring `VK_KHR_pipeline_executable_properties`; `1` enables | | `APS5_POLL_REAP` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/WaitMemory.cpp` | | reaps completions on every poll tick | | `APS5_POLL_TRY_EACH` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/WaitMemory.cpp` | | tries the poll service on every tick instead of at the poll interval | @@ -301,6 +320,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_SKIP_REQUEST_DUMPS` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderDiagnostics.cpp` | 8 | caps RecompileRequest dumps per shader-skip reason | | `APS5_SKIP_SHADER` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Diagnostics.cpp` | 0 | hex program address of a shader to skip preparing | | `APS5_SNAPSHOT_CHECK_FATAL` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | | throws on the first snapshot that differs from registered memory instead of only counting them | +| `APS5_SPANNED_WAVE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | spans a single-wave wave64 compute program over two full 32-lane subgroups | | `APS5_SPIRV_OPT` | string | `core/libs/prx/libSceAgcDriver/tests/SpirvOptimizer.cpp` | | selects the SPIR-V optimization pipeline: `full` restores the performance pipeline, `none` skips optimization | | `APS5_SRGB_ATTACHMENT_PROXY` | string | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | | forces the sRGB color-attachment proxy format; `1` forces it | | `APS5_SRGB_SHADER_DECODE` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp` | | forces shader sRGB decode for the sRGB 8-bit formats | @@ -308,6 +328,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_STALE_EVICT` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | | also evicts a proved-present dead image's pending layers on an access | | `APS5_STORE_AT_FLIP` | string | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | stores every pending storage-image result at each display flip; only `1` is accepted | | `APS5_STORE_AT_LABEL` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | flushes every pending storage-image result when a label is written on the GPU | +| `APS5_SUBMISSION_EPOCH_EACH` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp` | | bumps the guest-memory collect epoch on every submission instead of only when the submission is newer than the last bump | | `APS5_SUBMIT_AT_END` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp` | | submits recorded work at the end of a submission even when a worker is queued | | `APS5_SUSPEND_DRAIN` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp` | | drains the device at a suspend point | | `APS5_SYNC_COMPLETION_DRAWS` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp` | | keeps every draw with completion work synchronous | @@ -315,6 +336,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_SYNC_DRAWS` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp` | | makes every draw synchronous | | `APS5_SYNC_FLIP` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp` | | makes flips synchronous (no frames in flight) | | `APS5_SYNC_LEASE_DISPATCH` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | | waits for leased draws and dispatches at once instead of deferring them | +| `APS5_SYNC_SAMPLE_DUMPS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | takes the CPU path for occlusion-counter sample dumps, as `APS5_NO_GPU_SAMPLE_DUMPS` does | | `APS5_SYNC_SHADER_PREPARE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp` | | prepares registered shaders on the registering thread instead of in the background | | `APS5_SYNC_TEXTURE_UPLOAD` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | | restores the waiting batch for every texture upload | | `APS5_TEXTURE_CACHE_MIB` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | 0 | forced byte budget in MiB for the storage and sampled texture caches; `0` selects the budget automatically | @@ -333,6 +355,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_TRACE_DISPATCH_IO` | string | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp` | | prints every dispatch's resources; `2` also lists the small-buffer words | | `APS5_TRACE_DRAWS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp` | | prints one line per draw with its phases and inputs | | `APS5_TRACE_DRAW_CACHE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCacheStatistics.cpp` | | reports draw-cache entries whose key changed | +| `APS5_TRACE_DRAW_PROGRAMS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp` | | prints one line per recorded draw with the stage, role and code address of each program | | `APS5_TRACE_EVENTS` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/PerformanceTrace.cpp` | | number of events a windowed trace keeps in its buffer | | `APS5_TRACE_FILL_COVER` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | | prints the first fills that meet images without covering them | | `APS5_TRACE_FLUSH` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | | names what forces pending storage-image results into guest memory | @@ -418,6 +441,12 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti |------|------|--------|---------|-------------| | `APS5_PAD_INPUT_FILE` | string | `core/libs/prx/libScePad/src/PadState.cpp` | | path of the file polled for injected pad button and stick commands | +### libScePlayGo + +| Name | Kind | Source | Default | Description | +|------|------|--------|---------|-------------| +| `APS5_PLAYGO_UNLISTED_CHUNKS` | string | `core/libs/prx/libScePlayGo/Export.cpp` | | answer for a chunk ID the playgo manifest does not list: installed or absent; unset throws instead of answering | + ### libSceSaveData.native | Name | Kind | Source | Default | Description | From 5f10ea8fbfea95997cec39d4413aad7496a8ceb5 Mon Sep 17 00:00:00 2001 From: 011010 <husari09.oro@gmail.com> Date: Sun, 18 Oct 2026 01:20:56 -0600 Subject: [PATCH 0963/1000] docs(environment): add the rows for the names main added since the last rebase --- docs/dev/ENVIRONMENT.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/docs/dev/ENVIRONMENT.md b/docs/dev/ENVIRONMENT.md index c46bec9fe1..eff1f00066 100644 --- a/docs/dev/ENVIRONMENT.md +++ b/docs/dev/ENVIRONMENT.md @@ -39,6 +39,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_NO_PERF_DOM_INTERSECTION` | presence | `core/shader/recompiler/ControlFlow/src/Structurizer.cpp` | | computes dominator intersections into a fresh vector instead of intersecting in place | | `APS5_NO_PERF_ITERATION_SELECTION` | presence | `core/shader/recompiler/ControlFlow/src/Structurizer.cpp` | | recomputes the post-dominator sets when finding a loop's iteration-selection merge instead of reusing them | | `APS5_NO_RESULT_MEMO` | presence | `core/shader/recompiler/Recompiler.cpp` | | materializes a fresh result for every recompile instead of reusing the result memo | +| `APS5_NO_SRT_TAPE` | presence | `core/shader/recompiler/Optimization/src/SrtWalker/SrtTape.cpp` | | runs the runtime-source walk of a plan in the IR evaluator instead of the compiled tape | | `APS5_NO_WINDOW_NARROWING` | presence | `core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp` | | indexes every slot of the shader's vector window instead of only the slots the program uses | | `APS5_PROBE` | string | `core/shader/recompiler/Translation/src/DispatchInstructions.cpp` | | pc:vgpr[:shift] probe that copies that VGPR into v255 after the instruction so image stores write it as a per-lane trace | | `APS5_RAYTRACING` | string | `core/shader/recompiler/Translation/src/DispatchInstructions.cpp` | | selects the image_bvh_intersect_ray behaviour: strict throws and miss writes 0xffffffff | @@ -49,6 +50,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_TRACE_BUFFER_WRITTEN` | presence | `core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp` | | prints how many guest buffer elements a program may store to and how many it only loads | | `APS5_TRACE_IMAGE_TABLE` | presence | `core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp` | | traces image-table resolution | | `APS5_VERIFY_SRT_COMPACT` | presence | `core/shader/recompiler/Optimization/src/SrtWalker/SrtDescriptorEvaluation.cpp` | | walks every SRT descriptor a second time through the IR evaluator and throws when the two walks differ | +| `APS5_VERIFY_SRT_TAPE` | presence | `core/shader/recompiler/Optimization/src/SrtWalker/SrtTape.cpp` | | walks a plan's runtime sources a second time through the evaluator, adopts its answers where the two differ and prints the first twenty differences | | `APS5_WAVE_LDS_SUBGROUP` | presence | `core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp` | | restores subgroup-scope LDS barriers for single-wave workgroups instead of workgroup scope | ### libSceAgc @@ -64,6 +66,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `ANYPS5_GPU` | string | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | restricts physical device selection to device names containing this case-insensitive substring | | `ANYPS5_PRESENT_MODE` | string | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | fifo | selects the swapchain present mode: fifo, fifo_latest_ready, mailbox or immediate | | `ANYPS5_REQUIRE_DESCRIPTOR_BINDING_FLAGS` | presence | `core/libs/prx/libSceAgcDriver/tests/ComputePipelineReplay.cpp` | | fails the replay test instead of accepting a device that refuses descriptor binding flags | +| `ANYPS5_REQUIRE_ROBUST2` | presence | `core/libs/prx/libSceAgcDriver/tests/execution/LdsMixedWidthAtomics.cpp` | | fails the LDS lock-budget test when the device does not enable robustBufferAccess2, instead of only printing it | | `ANYPS5_REQUIRE_VULKAN` | presence | `core/libs/prx/libSceAgcDriver/tests/BdaDevice.cpp` | | fails a test instead of skipping when no usable Vulkan device is present | | `APS5_ADDRESS_COPY_MAX_MIB` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | 64 | upper bound in MiB on the guest memory one address-based build copies; exceeding it aborts | | `APS5_ALL_BUFFERS_WRITTEN` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | | notes every shader buffer element as written, as before bufferWritten existed | @@ -101,6 +104,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_DISPATCH_CACHE_ENTRIES` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchCache.cpp` | 8192 | number of dispatch cache entries; 0 picks a default from the variant count | | `APS5_DISPATCH_STAMP_VALIDATE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchCache.cpp` | | validates dispatch cache hits by write stamps, forcing one variant and a 4096-entry cache | | `APS5_DISPATCH_VARIANTS` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchCache.cpp` | 4 | number of dispatch variants kept per cache entry, clamped to the maximum | +| `APS5_DISPMOVE_MAX_KIB` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | 1024 | upper bound in KiB on the read-only buffers a dispatch-cache hit moves into its template, above which the dispatch is prepared again | | `APS5_DRAIN_ALL` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp` | | drains every packet, disabling the batched recording path | | `APS5_DRAIN_COMPLETION_LABELS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/DeferredLabels.cpp` | | records completion labels immediately instead of deferring them with the batch | | `APS5_DRAW_CACHE_ENTRIES` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCache.cpp` | 4096 | number of entries in the draw cache; 0 keeps the default 4096 | @@ -136,6 +140,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_GPU_NO_WRITEBACK` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | | skips writing GPU results back into guest memory | | `APS5_GPU_WAIT_TIMEOUT_MS` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/WaitMemory.cpp` | 1000 | milliseconds a GPU wait may run before a warning is printed | | `APS5_GROUP_CAPTURE_ALL_QUEUES` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchAhead.cpp` | | resolves group-ahead capture on queue 0 as well | +| `APS5_HANDOFF_SPIN_US` | integer | `core/libs/prx/libSceAgcDriver/Execution/include/HandoffSpin.hpp` | | microseconds a thread spins for a handed-off draw's work or for the GPU mutex before blocking, clamped to 0..10000; unset spins 150 us when the process has more than 8 usable processors and not at all otherwise | | `APS5_HEAP_MIRROR_MIB` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | 24576 | budget in MiB for the heap mirrors held for registered allocations | | `APS5_HOOK_FLUSH_CPU_BLOCKS` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | set to 0 flushes pending images whose every 64 KiB block the CPU wrote, instead of skipping them | | `APS5_HOOK_FULL_SYNC` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | makes the flush hook's recorded-store wait a full Sync under the GpuMutex | @@ -165,6 +170,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_LOG_SYNC` | presence | `core/libs/prx/libSceAgcDriver/Execution/include/AsyncLog.hpp` | | writes AGC log lines on the calling thread instead of through the asynchronous sink thread | | `APS5_LOOP_GUARD` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | | enables the fault buffer that guards shader loops running past their bound | | `APS5_MIRROR_REFRESH_THREADS` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | 3 | number of helper threads that refresh heap mirrors, clamped to 0 through 16 | +| `APS5_NO_ADDRESS_READ_PASSES` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp` | | stops a draw that only reads through its buffer descriptors from counting as read-only, so it breaks the pass instead of continuing the producer's | | `APS5_NO_ADDRESS_SPACE_CACHE` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | | disables the address-space cache so every build rebuilds its space | | `APS5_NO_ADJACENT_GENERATION` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | | leaves adjacent pending images at their generation during a write-back, as before | | `APS5_NO_ALIAS_BORROW` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | | disables borrowing a pending aliased storage image | @@ -182,11 +188,14 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_NO_COPY_KNOWN_VALUES` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferCopy.cpp` | | disables serving a copy's destination from source values the driver already knows | | `APS5_NO_CPU_COPY` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferCopy.cpp` | | disables the CPU copy path of the copy HLE | | `APS5_NO_DATA_HITS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchCache.cpp` | | disables dispatch-cache hits that differ only in ShaderData or FlattenedSrt words | +| `APS5_NO_DCC_PRESYNC` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | | leaves the DCC key ranges out of the surfaces a build pre-syncs, so only the surface itself waits for its producer | | `APS5_NO_DCC_WORD_SCAN` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp` | | restores the byte-by-byte DCC key scan instead of the word-at-a-time compare | | `APS5_NO_DEFERRED_EVICTION` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | destroys displaced resource-cache entries under the mutex instead of deferring them to the open batch | +| `APS5_NO_DEFERRED_SETTLES` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | | settles watched private host imports as they open instead of through the deferred block-settling path (Windows only) | | `APS5_NO_DISPATCH_CACHE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp` | | disables the dispatch cache so every dispatch takes the uncached path | | `APS5_NO_DISPATCH_KEY_HYGIENE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp` | | mixes every shader register into the dispatch key instead of only the registers that affect the dispatch | | `APS5_NO_DISPATCH_LRU` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchInsert.cpp` | | clears the whole dispatch cache on overflow instead of evicting the least recently used entry | +| `APS5_NO_DISPATCH_MOVE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | leaves a dispatch-cache hit from moving its read-only buffers into the template, so it prepares the dispatch again; also off when APS5_NO_TEMPLATE_DATA_REFRESH is set | | `APS5_NO_DISPATCH_RECIPE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | builds no dispatch recipes, so dispatch-cache hits repeat the recorded work | | `APS5_NO_DRAW_DATA_HITS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCache.cpp` | | disables draw-cache hits that differ only in ShaderData words | | `APS5_NO_DRAW_KEY` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCache.cpp` | | registers no draw key, so every draw decodes its programs instead of hitting the draw cache | @@ -435,6 +444,12 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti |------|------|--------|---------|-------------| | `ANYPS5_ENTITLEMENTS` | string | `core/libs/prx/libSceNpEntitlementAccess/Export.cpp` | | path of the entitlements ini listing owned add-ons, defaulting to anyps5-entitlements.ini beside the executable | +### libSceNpTrophy2 + +| Name | Kind | Source | Default | Description | +|------|------|--------|---------|-------------| +| `APS5_TRACE_UDS` | presence | `core/libs/prx/libSceNpTrophy2/src/TitleTrophies.cpp` | | prints the UDS events contexts post and the stat changes trophy rules apply, and the trophy files a lookup does not find (read in NpUniversalDataSystem/Event and NpTrophy2/TitleTrophies) | + ### libScePad | Name | Kind | Source | Default | Description | From 479847104aaa545430cf78160316159db4b147df Mon Sep 17 00:00:00 2001 From: 011010 <husari09.oro@gmail.com> Date: Sun, 18 Oct 2026 01:24:50 -0600 Subject: [PATCH 0964/1000] docs(environment): name the readers the scanner recognises --- docs/dev/ENVIRONMENT.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/ENVIRONMENT.md b/docs/dev/ENVIRONMENT.md index eff1f00066..c3ba2637c9 100644 --- a/docs/dev/ENVIRONMENT.md +++ b/docs/dev/ENVIRONMENT.md @@ -1,6 +1,6 @@ # Environment variables -The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetting` and `MaskFromEnvironment`. This table lists every `APS5_*` and `ANYPS5_*` name it reads, its kind, the value used when it is unset and what it does, one table per library. Regenerate the names with `python3 tools/env_reference.py --table`; the Conventions check fails when a name the code reads has no row here, or a row's name is no longer read. +The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetting`, `MaskFromEnvironment`, `EnvInt` and `GetEnvironmentVariableA`. This table lists every `APS5_*` and `ANYPS5_*` name it reads, its kind, the value used when it is unset and what it does, one table per library. Regenerate the names with `python3 tools/env_reference.py --table`; the Conventions check fails when a name the code reads has no row here, or a row's name is no longer read. ### core/libs From 062326351293cb4c454284ca2c6a9f3eb35e14ed Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Sun, 18 Oct 2026 00:47:31 -0700 Subject: [PATCH 0965/1000] fix(agc): keep a settled import compared while a storage image overlaps it Since #6678, a Windows host import under 1 MiB returns to the write watch once its deferred settle read has completed, and GuestMemory::Rewatch stamps every block of it as a CPU store. The return waited only while storage results were pending over the import. A storage image can take its generation before the stamp and have results pending only after it: an image's first upload takes its generation and then imports its block, a recorded draw marks its targets pending once it is recorded, and the settle is polled at the import lookups between the two. The image's next refresh then takes the stamp for a CPU store over its results, drops them and uploads the guest bytes again, so the GPU's output is lost. On an RTX 3070 this also splits the first two draws of agc_driver_address_passes into two passes. A settled import that a live storage image overlaps now stays compared, as every import under 1 MiB was before #6678. Imports with no image over them still return to the write watch. Keeping the import compared, rather than waiting for the image to go, checks each settled import against the live images once instead of at every poll. The [buffers] line counts the imports kept compared. agc_import_settle_storage_image makes an image whose first upload imports its block, waits for the settle read, clears the image on the GPU, then refreshes and stores it. On the old code the refresh drops the clear. --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 5 +- .../Graphics/include/GuestBufferMemory.hpp | 1 + .../Graphics/include/Texture.hpp | 1 + .../Graphics/src/GuestBufferMemory.cpp | 19 ++-- .../libSceAgcDriver/Graphics/src/Texture.cpp | 13 +++ .../prx/libSceAgcDriver/tests/Recorder.cpp | 102 ++++++++++++++++++ 6 files changed, 128 insertions(+), 13 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index a6de94a52f..e8ebdb5310 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1164,8 +1164,9 @@ if(BUILD_TESTING) add_test(NAME agc_import_settle_first_use COMMAND agc_driver_recorder_tests --settle-first-use-only) add_test(NAME agc_import_settle_earlier_import COMMAND agc_driver_recorder_tests --settle-earlier-import-only) add_test(NAME agc_import_settle_retire COMMAND agc_driver_recorder_tests --settle-retire-only) - set_tests_properties(agc_import_settle_deferred agc_import_settle_deferred_other_family agc_import_settle_first_use agc_import_settle_earlier_import agc_import_settle_retire PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 60) - set_tests_properties(agc_import_watch agc_import_watch_other_family agc_import_settle_deferred agc_import_settle_deferred_other_family agc_import_settle_first_use agc_import_settle_earlier_import agc_import_settle_retire PROPERTIES ENVIRONMENT_MODIFICATION + add_test(NAME agc_import_settle_storage_image COMMAND agc_driver_recorder_tests --settle-storage-image-only) + set_tests_properties(agc_import_settle_deferred agc_import_settle_deferred_other_family agc_import_settle_first_use agc_import_settle_earlier_import agc_import_settle_retire agc_import_settle_storage_image PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 60) + set_tests_properties(agc_import_watch agc_import_watch_other_family agc_import_settle_deferred agc_import_settle_deferred_other_family agc_import_settle_first_use agc_import_settle_earlier_import agc_import_settle_retire agc_import_settle_storage_image PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>") endif() add_test(NAME agc_unchanged_cpu_stamps COMMAND agc_driver_recorder_tests --unchanged-cpu-stamps-only) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp index 827b85e746..7f811d75b5 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp @@ -79,6 +79,7 @@ struct HostImportSettleCounts { std::uint64_t rewatched = 0; std::uint64_t parked = 0; std::uint64_t unmapWaits = 0; + std::uint64_t keptCompared = 0; }; HostImportSettleCounts HostImportSettles(); // Tests: how many imports settleImport has used on the settle queue. diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp index 5a70e2f069..cad1c2bf36 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp @@ -209,6 +209,7 @@ public: // Whether an image with results pending, or one a FlushPending is storing right now, overlaps // any of the [begin, end) ranges: one acquisition of the registry's mutex. static bool AnyPendingOverlaps(std::span<const std::pair<std::uint64_t, std::uint64_t>> ranges); + static bool AnyLiveOverlaps(std::span<const std::pair<std::uint64_t, std::uint64_t>> ranges); // Moves with every change of the pending registry (an image marked dirty, stored, flushed or // dropped; after the store for a flush), bumped under the registry's mutex with the change, so // a reader that loads it before and after its own reads knows the registry it consulted stayed diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index 4f14db8c4a..f2bd6dd3c4 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -231,6 +231,7 @@ struct HostImports { std::uint64_t settlesSubmitted = 0; std::uint64_t settlesWaited = 0; std::uint64_t settlesRewatched = 0; + std::uint64_t settlesKeptCompared = 0; std::uint64_t settlesParked = 0; std::uint64_t settleUnmapWaits = 0; #endif @@ -1051,24 +1052,20 @@ void reapSettleBatches(const Context& context, HostImports& state) { void rewatchSettledImports(HostImports& state) { if (state.settleReady.empty()) return; std::vector<std::pair<std::uint64_t, std::uint64_t>> ranges; - std::vector<std::uint64_t> deferred; for (const auto base : state.settleReady) { const auto settle = state.pendingSettles.find(base); if (settle == state.pendingSettles.end() || settle->second.phase != SettlePhase::Settled) continue; + state.pendingSettles.erase(settle); const auto import = state.imports.find(base); - if (import == state.imports.end() || !import->second.unwatched) { - state.pendingSettles.erase(settle); - continue; - } + if (import == state.imports.end() || !import->second.unwatched) continue; const std::array<std::pair<std::uint64_t, std::uint64_t>, 1> span{{{base, base + import->second.bytes}}}; - if (StorageTexture::AnyPendingOverlaps(span)) { - deferred.push_back(base); + if (StorageTexture::AnyLiveOverlaps(span)) { + ++state.settlesKeptCompared; continue; } ranges.push_back(span.front()); - state.pendingSettles.erase(settle); } - state.settleReady = std::move(deferred); + state.settleReady.clear(); if (ranges.empty()) return; GuestMemory::Rewatch(ranges); for (const auto& [begin, end] : ranges) { @@ -2409,7 +2406,7 @@ void reportMirrors() { #ifdef _WIN32 auto& imports = Imports(); std::lock_guard importsLock(imports.mutex); - if (imports.settlesSubmitted != 0 || imports.settlesWaited != 0 || !imports.settles.empty()) AgcDriver::ProfilePrint_nid_no_patch("[buffers] host imports under %llu KiB: %llu settle reads submitted, %llu back in the write watch, %llu retired during their read (%llu unmaps waited for it), %zu pending; %llu larger imports settled inside their window\n", static_cast<unsigned long long>(settleFloorBytes >> 10u), static_cast<unsigned long long>(imports.settlesSubmitted), static_cast<unsigned long long>(imports.settlesRewatched), static_cast<unsigned long long>(imports.settlesParked), static_cast<unsigned long long>(imports.settleUnmapWaits), imports.settles.size(), static_cast<unsigned long long>(imports.settlesWaited)); + if (imports.settlesSubmitted != 0 || imports.settlesWaited != 0 || !imports.settles.empty()) AgcDriver::ProfilePrint_nid_no_patch("[buffers] host imports under %llu KiB: %llu settle reads submitted, %llu back in the write watch, %llu kept compared under a storage image, %llu retired during their read (%llu unmaps waited for it), %zu pending; %llu larger imports settled inside their window\n", static_cast<unsigned long long>(settleFloorBytes >> 10u), static_cast<unsigned long long>(imports.settlesSubmitted), static_cast<unsigned long long>(imports.settlesRewatched), static_cast<unsigned long long>(imports.settlesKeptCompared), static_cast<unsigned long long>(imports.settlesParked), static_cast<unsigned long long>(imports.settleUnmapWaits), imports.settles.size(), static_cast<unsigned long long>(imports.settlesWaited)); #endif } @@ -2985,7 +2982,7 @@ HostImportSettleCounts HostImportSettles() { #ifdef _WIN32 auto& state = Imports(); std::lock_guard lock(state.mutex); - return {state.settlesSubmitted, state.settlesWaited, state.settlesRewatched, state.settlesParked, state.settleUnmapWaits}; + return {state.settlesSubmitted, state.settlesWaited, state.settlesRewatched, state.settlesParked, state.settleUnmapWaits, state.settlesKeptCompared}; #else return {}; #endif diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index ba62bdcde6..f903ccebba 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -3602,6 +3602,19 @@ bool StorageTexture::AnyPendingOverlaps(std::span<const std::pair<std::uint64_t, return false; } +bool StorageTexture::AnyLiveOverlaps(std::span<const std::pair<std::uint64_t, std::uint64_t>> ranges) { + auto& live = Live(); + std::lock_guard lock(live.mutex); + for (const auto& range : ranges) { + if (range.second <= range.first) continue; + const auto bytes = static_cast<std::size_t>(range.second - range.first); + for (const auto* texture : live.textures) { + if (texture->overlapsLive(range.first, bytes)) return true; + } + } + return false; +} + std::uint64_t StorageTexture::PendingSerial() { return pendingSerial.load(std::memory_order_acquire); } diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 7c4bd1b701..2e9cd49f46 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -1972,6 +1972,107 @@ bool settleRetireTests(const Device& device) { std::cout << "imports retired during their settle read tests passed\n"; return true; } + +bool settleStorageImageTests(const Device& device) { + using namespace AgcDriver::GuestMemory; + const auto& base = device.GetContext(); + constexpr std::size_t unit = 65536; + constexpr std::array<std::uint8_t, 4> red{255, 0, 0, 255}; + if (base.hostImportAlignment == 0 || base.hostImportAlignment > unit || base.settleQueue == VK_NULL_HANDLE || !WriteWatched()) { + std::cout << "host imports within 64 KiB, a settle queue or write watching unavailable: a settled import under a storage image not tested\n"; + return false; + } + std::unique_lock gpu(GpuMutex()); + PrepareImportWatch(base); + if (!settledImportsWatchPrivate(base)) { + std::cout << "this device compares every Windows import: a settled import under a storage image not tested\n"; + return false; + } + void* block = AllocateWatched(unit, unit); + Require(block != nullptr, "(i) no watched block although write watching is on"); + struct Release { + void* block; + ~Release() { ReleaseWatched(block, unit); } + } release{block}; + const auto address = reinterpret_cast<std::uint64_t>(block); + std::memset(block, 0x5a, unit); + { + GuestAllocations::Mutation mutation; + mutation.Add(block, unit, true, true, true); + } + TextureDetiler detiler(base); + auto context = base; + context.detiler = &detiler; + GuestTextureResource resource{}; + resource.baseAddress = address; + resource.width = 128; + resource.height = 128; + resource.mipCount = 1; + resource.tileMode = TextureTileMode::kR64KBX; + resource.dimension = TextureDimension::k2D; + resource.format = 56; + resource.dstSelX = 4; + resource.dstSelY = 5; + resource.dstSelZ = 6; + resource.dstSelW = 7; + Require(DescribeSurface(resource).guestBytes == unit, "(i) unexpected surface size"); + Require(Watched(address, unit) && ExistingHostImportFor(context, address, unit) == nullptr, "(i) the block was imported or out of the write watch before the image was made"); + Recorder recorder(context); + recorder.Activate(); + const auto before = HostImportSettles(); + { + auto image = StorageTexture::Create(context, detiler, resource, 0); + const auto* import = ExistingHostImportFor(context, address, unit); + Require(import != nullptr && import->unwatched && !Watched(address, unit), "(i) the image's first upload did not import its block compared"); + const auto decided = [&] { + const auto now = HostImportSettles(); + return now.rewatched != before.rewatched || now.keptCompared != before.keptCompared; + }; + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5); + while (!decided() && std::chrono::steady_clock::now() < deadline) { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + HostImportFor(context, address, unit); + } + Require(decided(), "(i) the settle read of the image's import did not finish"); + const auto after = HostImportSettles(); + const bool returned = Watched(address, unit); + const VkImageSubresourceRange range{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + const VkClearColorValue value{{1.0f, 0.0f, 0.0f, 1.0f}}; + const auto commands = recorder.Commands(); + recorder.Keep(image); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, VK_ACCESS_TRANSFER_WRITE_BIT); + context.Function<PFN_vkCmdClearColorImage>("vkCmdClearColorImage")(commands, image->Image(), VK_IMAGE_LAYOUT_GENERAL, &value, 1, &range); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT); + image->MarkDirty(); + image->Refresh(); + image->WriteBack(); + recorder.Submit(); + device.WaitQueue(); + recorder.Sync(); + std::vector<std::byte> read(unit); + AgcDriver::GuestMemory::Read(address, read); + std::size_t lost = 0; + for (std::size_t at = 0; at < unit; at += 4) { + for (std::size_t c = 0; c < 4; ++c) { + if (std::to_integer<std::uint8_t>(read[at + c]) == red[c]) continue; + ++lost; + break; + } + } + std::cout << "(i) the image's import " << (returned ? "returned to the write watch" : "stayed compared") << " after its settle read (" << after.rewatched - before.rewatched << " back in the write watch, " << after.keptCompared - before.keptCompared << " kept compared); the refresh after the image's GPU results lost " << lost << " of " << unit / 4 << " texels\n"; + Require(lost == 0, "(i) the refresh after the image's GPU results dropped " + std::to_string(lost) + " of " + std::to_string(unit / 4) + " texels: the import's return to the write watch stamped its block as a CPU store after the image took its generation"); + Require(!returned && after.keptCompared == before.keptCompared + 1, "(i) the settled import under a live storage image returned to the write watch"); + } + recorder.Sync(); + recorder.FlushDeferredReleases(); + { + GuestAllocations::Mutation mutation; + mutation.Remove(block); + } + HostImportFor(context, address, unit); + std::cout << "a settled import under a storage image tests passed\n"; + return true; +} #endif void remappedImportTests(const Device& device) { @@ -8694,6 +8795,7 @@ int main(int argc, char** argv) { if (argc == 2 && std::string_view(argv[1]) == "--settle-first-use-only") return settleFirstUseTests(device) ? 0 : 77; if (argc == 2 && std::string_view(argv[1]) == "--settle-earlier-import-only") return settleEarlierImportTests(device) ? 0 : 77; if (argc == 2 && std::string_view(argv[1]) == "--settle-retire-only") return settleRetireTests(device) ? 0 : 77; + if (argc == 2 && std::string_view(argv[1]) == "--settle-storage-image-only") return settleStorageImageTests(device) ? 0 : 77; #endif if (argc == 2 && std::string_view(argv[1]) == "--host-import-retirement-only") { const auto alignment = device.GetContext().hostImportAlignment; From 3ea0be6488a98f443f8fe3dc546315b4ae37c250 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Sun, 18 Oct 2026 00:47:31 -0700 Subject: [PATCH 0966/1000] docs(agc): say a settled import under a storage image stays compared ARCHITECTURE.md and the libSceAgcDriver debt entry now say that a settled Windows import under 1 MiB returns to the write watch only when no storage image overlaps it, and why. --- docs/dev/ARCHITECTURE.md | 2 +- docs/dev/debt/libSceAgcDriver.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/dev/ARCHITECTURE.md b/docs/dev/ARCHITECTURE.md index 60e568cd98..9aff6ab25b 100644 --- a/docs/dev/ARCHITECTURE.md +++ b/docs/dev/ARCHITECTURE.md @@ -111,7 +111,7 @@ flowchart LR - A draw whose `PA_SC_AA_CONFIG` selects 2, 4 or 8 samples renders into host-only multisampled images. [`MultisampleTarget.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) keeps one image per device, surface address, format, extent and sample count (`MultisampleTargetView`); the pipeline, render pass and library keys carry `State::samples`, and a multisampled depth target is a multisampled depth surface. The guest sample planes and FMASK are never read or written, so the registry also guards the target's guest range: a CPU read (`GuestMemory::SetReadGuard`), a DMA copy, and a sampled or storage image read of it throw. A `CB_COLOR_CONTROL` resolve mode over a multisampled CB0 runs as a render pass whose resolve attachment is CB1's resident image (`ResolveMultisampleTarget`). A zero fill of the target's CMASK (`NoteColorMetadataFill`, from fill kernels and CP DMA fills) clears the image at its next use. - A texture over a depth surface samples the depth image itself when it views one 2D slice of the plane's own format (`DepthSurfaceTexture` in [`DepthSurface.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp)). A view of several slices (a 2D array or a cube) or an integer view of a D16 plane is served by a `DepthPlaneCopy`, an image the slices are copied into at every use: from the depth surface bound at each slice's address, else from guest memory. Such a texture is never kept in the resource templates (`RefreshedPerUse`). - Guest memory reaches the GPU through host imports (`VK_EXT_external_memory_host`) where the driver accepts them, and through copies otherwise ([`GuestBufferMemory.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp)). A registered range larger than the import window (64 MiB) is imported per window for buffer and fill accesses and whole for an address-based build. Live imports stay under a budget, with the least recently used import retired first (see TechnicalDebt, Host import windows and budget). -- On Windows, a host import of private `MEM_WRITE_WATCH` memory (singly mapped direct memory) stays on `GetWriteWatch` when `ProbeImportWriteProtection` shows the driver reports nothing written after the second GPU read and a GPU write, and a CPU store is collected (`decideImportWatch`). The driver marks every page of such an import written, asynchronously, until a GPU use of it completes (a read on the settle queue ends it even while main-queue work that used the import first is still running, and the first use of an earlier import of the same pages after that read marks nothing), so `settleImport` in [`GuestBufferMemory.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) copies 4 bytes out of the new import on a second queue of the graphics family, or on a queue of another copy-capable family when the graphics family has only one (`Context::settleQueue`, own pool and mutex) inside `ImportWatched`'s window and waits for it, for imports of 1 MiB or more; the main queue can't be used there because `importAllocation` holds the imports lock and `GpuMutex` comes before it in the lock order. A smaller import is compared when it is made, so a title that maps, imports and unmaps a small buffer around every submission does not block its mapping thread. Its 4-byte read is queued instead: the next import lookup that holds `GpuMutex` while the imports match the registry (`settleUnwatchedImports`) records the queued reads into one batch on the settle queue, and once the batch's fence has signalled a later lookup returns the import's whole 64 KiB blocks to the write watch (`GuestMemory::Rewatch` drops their marks and stamps every block as a CPU store), except while storage results are pending over them. A settled import whose range had already left the watch returns the same way, and an import retired while its read is in flight is kept with that batch and freed once the batch has completed, without waiting for it; a guest unmap of its memory waits for the batch instead. `APS5_NO_DEFERRED_SETTLES` (any value) keeps every import under 1 MiB compared. Imports through a section alias keep tracking on the guest views, and every other Windows host import is compared. A device without such a queue keeps the comparisons. On Linux the settle queue is made when the device imports dma-buf memory, and every dma-buf import of 1 MiB or more is settled right after it is made, for another reason: the kernel maps an import's pages for the GPU at the first submission that uses it, which took about 2 s for a 3 GiB import on RADV and held up the title's frame. The import's buffer is shared with the settle queue's family instead of aliased, since a sparse import is bound to many memory objects. +- On Windows, a host import of private `MEM_WRITE_WATCH` memory (singly mapped direct memory) stays on `GetWriteWatch` when `ProbeImportWriteProtection` shows the driver reports nothing written after the second GPU read and a GPU write, and a CPU store is collected (`decideImportWatch`). The driver marks every page of such an import written, asynchronously, until a GPU use of it completes (a read on the settle queue ends it even while main-queue work that used the import first is still running, and the first use of an earlier import of the same pages after that read marks nothing), so `settleImport` in [`GuestBufferMemory.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) copies 4 bytes out of the new import on a second queue of the graphics family, or on a queue of another copy-capable family when the graphics family has only one (`Context::settleQueue`, own pool and mutex) inside `ImportWatched`'s window and waits for it, for imports of 1 MiB or more; the main queue can't be used there because `importAllocation` holds the imports lock and `GpuMutex` comes before it in the lock order. A smaller import is compared when it is made, so a title that maps, imports and unmaps a small buffer around every submission does not block its mapping thread. Its 4-byte read is queued instead: the next import lookup that holds `GpuMutex` while the imports match the registry (`settleUnwatchedImports`) records the queued reads into one batch on the settle queue, and once the batch's fence has signalled a later lookup returns the import's whole 64 KiB blocks to the write watch (`GuestMemory::Rewatch` drops their marks and stamps every block as a CPU store), unless a live storage image overlaps them. Such an import stays compared: an image that took its generation before the stamp would take the stamp for a CPU store over the results it produces after it, and drop them. A settled import whose range had already left the watch returns the same way, and an import retired while its read is in flight is kept with that batch and freed once the batch has completed, without waiting for it; a guest unmap of its memory waits for the batch instead. `APS5_NO_DEFERRED_SETTLES` (any value) keeps every import under 1 MiB compared. Imports through a section alias keep tracking on the guest views, and every other Windows host import is compared. A device without such a queue keeps the comparisons. On Linux the settle queue is made when the device imports dma-buf memory, and every dma-buf import of 1 MiB or more is settled right after it is made, for another reason: the kernel maps an import's pages for the GPU at the first submission that uses it, which took about 2 s for a 3 GiB import on RADV and held up the title's frame. The import's buffer is shared with the settle queue's family instead of aliased, since a sparse import is bound to many memory objects. ### Log output diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 01fbd5d3e5..b9eaa66ec5 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -80,7 +80,7 @@ - [Draw input snapshots](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver): `PrepareDrawBindings` caps read-only input buffer snapshots at 64 MiB (overridable via `APS5_DRAW_SNAPSHOT_MAX_MIB`). Larger inputs (streaming heaps, bindless pools) or draws exceeding the cumulative budget skip the per-draw snapshot and bind in place, avoiding multi-gigabyte CPU copy stalls (such as Ratchet & Clank: Rift Apart PPSA01474 stalling at frame ~380 copying 36 GB every 5 s). If the title rewrites those large ranges on the CPU before GPU completion, the in-place binding observes the modification, as verified by `drawSnapshotCapTests`. - [Pending storage-image results](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) are stored when something reads their memory, not when the GPU work that wrote them completes. A title that waits for that work, frees the image and reuses its memory can have the new data overwritten by the late store (#1970). A title's own CPU reads are not seen either, so a title that reads the results after waiting for a label reads stale memory. `APS5_STORE_AT_FLIP=1` stores every pending result at each flip and `APS5_STORE_AT_LABEL=1` before each label the GPU writes; both store every pending image, not only the ones the waited-for work wrote, and neither is on by default. - [Pending storage-image results over memory outside the write watch](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) leave to the CPU the 64 KiB blocks whose bytes differ from the ones saved at the upload, the fill, the image copy, the previous store or the moment their range left the watch (`Unwatch`, or a host import that does not stay watched), and the blocks the watch saw the CPU write before that moment. A store made on the GPU saves its bytes through a host-visible copy that becomes the saved bytes when its batch completes; a store or a refresh that needs them earlier waits for that batch. A unit without a write generation saves the guest bytes when results land in it outside the watch. One whose range leaves the watch while its results are pending, and ranges that leave the watch through a failed collect, have no saved bytes; storing such results throws. -- Windows host-pointer imports of the guest mapping itself are compared instead of write-watched, except writable imports of private `MEM_WRITE_WATCH` memory when the import probe keeps the write watch and a settle queue exists: those of 1 MiB or more stay watched, and smaller ones return to the watch once their settle has completed (see Pages under an import window): a driver can mark imported pages dirty after the synchronous import window. Shared memory imported through a separate writable alias retains protection-based tracking on the guest views; driver access to the import alias does not disarm those views. [GuestMemory](../../../core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp) excludes every overlapping 64 KiB tracker block while its backing is retained. A full block replaced by fresh private backing regains tracking and gets a new generation; protection changes, partial replacements and shared remappings do not restore it. Excluded blocks keep their write-watch marks as a hint only (`UntrackedHint`): a storage image or a sampled texture snapshot over them compares its bytes again after a mark in its range, a driver store, a mapping change or a recorded GPU write instead of at every collect epoch or lookup, and a mark the driver makes without a CPU store costs one comparison. This preserves CPU edits through byte comparisons but limits snapshot reuse on excluded blocks to draws of one collect epoch and disables unit shadows for them; restoring tracking on import retirement without replacing the backing remains unimplemented. +- Windows host-pointer imports of the guest mapping itself are compared instead of write-watched, except writable imports of private `MEM_WRITE_WATCH` memory when the import probe keeps the write watch and a settle queue exists: those of 1 MiB or more stay watched, and smaller ones return to the watch once their settle has completed, unless a storage image overlaps them (see Pages under an import window): a driver can mark imported pages dirty after the synchronous import window. Shared memory imported through a separate writable alias retains protection-based tracking on the guest views; driver access to the import alias does not disarm those views. [GuestMemory](../../../core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp) excludes every overlapping 64 KiB tracker block while its backing is retained. A full block replaced by fresh private backing regains tracking and gets a new generation; protection changes, partial replacements and shared remappings do not restore it. Excluded blocks keep their write-watch marks as a hint only (`UntrackedHint`): a storage image or a sampled texture snapshot over them compares its bytes again after a mark in its range, a driver store, a mapping change or a recorded GPU write instead of at every collect epoch or lookup, and a mark the driver makes without a CPU store costs one comparison. This preserves CPU edits through byte comparisons but limits snapshot reuse on excluded blocks to draws of one collect epoch and disables unit shadows for them; restoring tracking on import retirement without replacing the backing remains unimplemented. - [Pages under an import window](../../../core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp) (libSceAgcDriver) whose write protection was resolved while a host import that leaves its range watched was made are compared with the bytes they held before the import and count as stored over only when those differ. A CPU store of the bytes already there within the window cannot be told apart from an import that resolves the write protection without storing, as the NVIDIA driver (610.57.04) does for every imported page, so such a store does not win over pending results. Where host-pointer and dma-buf imports differ in whether they stay watched, a failed host-pointer import and the dma-buf import after it are two windows, and the pages the failed one resolved count as stored over. On Windows, the NVIDIA driver (617.42) marks every page of a host import of private `MEM_WRITE_WATCH` memory written, asynchronously, until a GPU use of the import completes; a 4-byte read of it on the settle queue ends that even while main-queue work that used the import first is still running, and an earlier import of the same pages whose first use runs after that read marks none of them (`agc_import_settle_first_use`, `agc_import_settle_earlier_import`). Such an import stays watched only after a 4-byte read of it on the settle queue has completed inside the window. Each one waits for that read, which keeps its window open for one round trip on the settle queue, so a same-value CPU store from another thread in that time counts as unchanged too. An import made where no settle queue exists is compared instead. An import of under 1 MiB is not settled inside its window, because the settle blocks the importing thread and a title that imports a 64 KiB buffer around every submission lost about 19 % of its frame rate to that wait (Dripl1x, #6060). It is compared until its read, submitted in a batch by a later import lookup under `GpuMutex` while the imports match the registry, has completed; a later lookup then returns its whole blocks to the write watch, each stamped as a CPU store, so whatever was derived from them while they were compared reads as changed once. Its blocks stay compared while storage results are pending over them, and those of an import retired before that stay compared. - [Settled dma-buf imports](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver): on Linux a dma-buf import of 1 MiB or more is read by a 4-byte copy on the settle queue right after it is made (`settleImport`), so the kernel maps its pages for the GPU there and not in the first submission that uses it (about 2 s for a 3 GiB import on RADV). The import's buffer is shared with the settle queue's family. An import made at map time settles on the guest thread that maps the range, inside its map call, so that thread waits while the kernel maps the pages: 0.47 to 2.2 s for the 3 GiB range of Hades (PPSA03355) and 0.49 to 4.3 s for the 4.5 GiB range of ASTRO BOT (PPSA21567) on RADV. Both titles map these ranges at start-up; a title that maps large ranges during gameplay stalls its mapping thread for as long. An import made from a submission settles under the GPU mutex, so its frame still pays for it; a smaller import, and every import on a device with no settle queue (one without a copy-capable queue besides the main one), pays in its first submission. A range imported through a host pointer, as lavapipe imports shared memory, is not settled - [Sparse dma-buf imports](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver): on Linux a shared-backed range larger than one 64 MiB udmabuf buffer is imported as one sparse buffer bound to the 64 MiB buffers, on a queue of its own when the device has a second family that binds sparse memory. The path is decided before anything is imported, and a failure after that throws. A range takes the single udmabuf import instead, which udmabuf's `size_limit_mb` refuses above 64 MiB by default so the range is copied, when its offset in the backing file is not a multiple of the buffer's sparse block size (64 KiB on RADV), when the sparse buffer does not accept every memory type a chunk import can have, when the backing file ends inside its last chunk, or when it is imported at map time on a device with no second family (the bind then needs the GPU mutex); the NVIDIA proprietary driver imports no dma-buf memory From 53a1fe47d6c703c26068e08e63a2c768a801e6d4 Mon Sep 17 00:00:00 2001 From: T3cm0 <srodriguez7604@gmail.com> Date: Sun, 18 Oct 2026 03:02:13 -0500 Subject: [PATCH 0967/1000] feat(libSceLibcInternal): implement err, warn, psignal and sys_signame Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- .../prx/libSceLibcInternal/CMakeLists.txt | 11 + core/libs/prx/libSceLibcInternal/Err.cpp | 137 ++++++++++++ .../prx/libSceLibcInternal/SignalList.cpp | 52 +++++ core/libs/tests/GuestErr.cpp | 197 ++++++++++++++++++ 4 files changed, 397 insertions(+) create mode 100644 core/libs/prx/libSceLibcInternal/Err.cpp create mode 100644 core/libs/tests/GuestErr.cpp diff --git a/core/libs/prx/libSceLibcInternal/CMakeLists.txt b/core/libs/prx/libSceLibcInternal/CMakeLists.txt index 1780e1d720..8f665f5631 100644 --- a/core/libs/prx/libSceLibcInternal/CMakeLists.txt +++ b/core/libs/prx/libSceLibcInternal/CMakeLists.txt @@ -2,6 +2,7 @@ add_library(libSceLibcInternal SHARED EXCLUDE_FROM_ALL Export.cpp FileTime.cpp G target_include_directories(libSceLibcInternal PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(libSceLibcInternal PRIVATE ${CMAKE_DL_LIBS}) target_sources(libSceLibcInternal PRIVATE Directory.cpp) +target_sources(libSceLibcInternal PRIVATE Err.cpp) configure_windows_unwind(libSceLibcInternal) target_sources(libSceLibcInternal PRIVATE FilesystemStatistics.cpp PathStatistics.cpp) target_sources(libSceLibcInternal PRIVATE Inet.cpp) @@ -86,6 +87,16 @@ if(BUILD_TESTING) set_tests_properties(guest_thread_atexit PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceLibcInternal>") endif() + add_test_executable(guest_err_tests ${LIBS_INCLUDE_DIR}/tests/GuestErr.cpp) + target_include_directories(guest_err_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_err_tests PRIVATE libSceLibcInternal libkernel libc) + configure_windows_unwind(guest_err_tests) + add_test(NAME guest_err COMMAND guest_err_tests) + set_tests_properties(guest_err PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_err PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceLibcInternal>") + endif() add_test_executable(guest_getopt_tests ${LIBS_INCLUDE_DIR}/tests/GuestGetopt.cpp) target_include_directories(guest_getopt_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_getopt_tests PRIVATE libSceLibcInternal libkernel libc) diff --git a/core/libs/prx/libSceLibcInternal/Err.cpp b/core/libs/prx/libSceLibcInternal/Err.cpp new file mode 100644 index 0000000000..f16dd24ca1 --- /dev/null +++ b/core/libs/prx/libSceLibcInternal/Err.cpp @@ -0,0 +1,137 @@ +#include "prx/libc/include/FileStream.hpp" +#include "prx/libc/include/Shutdown.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include "SceTypes.hpp" +#include <cstdarg> + +using ErrExit = void (APS5_VABI*)(int); + +extern "C" { +extern const char* __progname_nid_postfix; +int APS5_VABI vfprintf_nid_postfix(FileStream* stream, const char* format, VaList* args); +int APS5_VABI fprintf_nid_postfix(FileStream* stream, const char* format, ...); +int APS5_VABI fputs_nid_postfix(const char* str, FileStream* stream); +char* APS5_VABI strerror_nid_postfix(int error); +int* APS5_VABI __error_nid_postfix(); +} + +namespace { + +FileStream* g_errFile = nullptr; +ErrExit g_errExit = nullptr; + +FileStream* ErrFile() { + if (g_errFile == nullptr) g_errFile = &_Stderr_nid_postfix; + return g_errFile; +} + +const char* ProgramName() { + return __progname_nid_postfix ? __progname_nid_postfix : "(null)"; +} + +void Report(const char* format, VaList* args, const int* code) { + FileStream* const stream = ErrFile(); + fprintf_nid_postfix(stream, "%s: ", ProgramName()); + if (format != nullptr) { + vfprintf_nid_postfix(stream, format, args); + if (code != nullptr) fputs_nid_postfix(": ", stream); + } + if (code != nullptr) fprintf_nid_postfix(stream, "%s\n", strerror_nid_postfix(*code)); + else fputs_nid_postfix("\n", stream); +} + +[[noreturn]] void Exit(int status) { + if (g_errExit != nullptr) g_errExit(status); + LibcExit_nid_no_patch(status); +} + +} + +#ifdef _WIN32 +#define APS5_ERR_VA_LIST __builtin_sysv_va_list +#define APS5_ERR_VA_START(list, last) __builtin_sysv_va_start(list, last) +#define APS5_ERR_VA_END(list) __builtin_sysv_va_end(list) +#else +#define APS5_ERR_VA_LIST std::va_list +#define APS5_ERR_VA_START(list, last) va_start(list, last) +#define APS5_ERR_VA_END(list) va_end(list) +#endif + +extern "C" { + +void APS5_VABI err_set_file_nid_postfix(FileStream* stream) { + g_errFile = stream != nullptr ? stream : &_Stderr_nid_postfix; +} + +void APS5_VABI err_set_exit_nid_postfix(ErrExit function) { + g_errExit = function; +} + +void APS5_VABI vwarnc_nid_postfix(int code, const char* format, VaList* args) { + Report(format, args, &code); +} + +void APS5_VABI vwarn_nid_postfix(const char* format, VaList* args) { + vwarnc_nid_postfix(*__error_nid_postfix(), format, args); +} + +void APS5_VABI vwarnx_nid_postfix(const char* format, VaList* args) { + Report(format, args, nullptr); +} + +[[noreturn]] void APS5_VABI verrc_nid_postfix(int status, int code, const char* format, VaList* args) { + Report(format, args, &code); + Exit(status); +} + +[[noreturn]] void APS5_VABI verr_nid_postfix(int status, const char* format, VaList* args) { + verrc_nid_postfix(status, *__error_nid_postfix(), format, args); +} + +[[noreturn]] void APS5_VABI verrx_nid_postfix(int status, const char* format, VaList* args) { + Report(format, args, nullptr); + Exit(status); +} + +void APS5_VABI warnc_nid_postfix(int code, const char* format, ...) { + APS5_ERR_VA_LIST args; + APS5_ERR_VA_START(args, format); + vwarnc_nid_postfix(code, format, reinterpret_cast<VaList*>(args)); + APS5_ERR_VA_END(args); +} + +void APS5_VABI warn_nid_postfix(const char* format, ...) { + const int code = *__error_nid_postfix(); + APS5_ERR_VA_LIST args; + APS5_ERR_VA_START(args, format); + vwarnc_nid_postfix(code, format, reinterpret_cast<VaList*>(args)); + APS5_ERR_VA_END(args); +} + +void APS5_VABI warnx_nid_postfix(const char* format, ...) { + APS5_ERR_VA_LIST args; + APS5_ERR_VA_START(args, format); + vwarnx_nid_postfix(format, reinterpret_cast<VaList*>(args)); + APS5_ERR_VA_END(args); +} + +[[noreturn]] void APS5_VABI errc_nid_postfix(int status, int code, const char* format, ...) { + APS5_ERR_VA_LIST args; + APS5_ERR_VA_START(args, format); + verrc_nid_postfix(status, code, format, reinterpret_cast<VaList*>(args)); +} + +[[noreturn]] void APS5_VABI err_nid_postfix(int status, const char* format, ...) { + const int code = *__error_nid_postfix(); + APS5_ERR_VA_LIST args; + APS5_ERR_VA_START(args, format); + verrc_nid_postfix(status, code, format, reinterpret_cast<VaList*>(args)); +} + +[[noreturn]] void APS5_VABI errx_nid_postfix(int status, const char* format, ...) { + APS5_ERR_VA_LIST args; + APS5_ERR_VA_START(args, format); + verrx_nid_postfix(status, format, reinterpret_cast<VaList*>(args)); +} + +} diff --git a/core/libs/prx/libSceLibcInternal/SignalList.cpp b/core/libs/prx/libSceLibcInternal/SignalList.cpp index c51fa70fe8..d27a5531c4 100644 --- a/core/libs/prx/libSceLibcInternal/SignalList.cpp +++ b/core/libs/prx/libSceLibcInternal/SignalList.cpp @@ -1,3 +1,8 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include <cstddef> +#include <cstdint> +#include <cstring> + extern "C" { extern const char* const sys_siglist_nid_postfix[32] = { @@ -37,4 +42,51 @@ extern const char* const sys_siglist_nid_postfix[32] = { extern const int sys_nsig_nid_postfix = sizeof(sys_siglist_nid_postfix) / sizeof(sys_siglist_nid_postfix[0]); +extern const char* const sys_signame_nid_postfix[32] = { + "Signal 0", + "HUP", + "INT", + "QUIT", + "ILL", + "TRAP", + "ABRT", + "EMT", + "FPE", + "KILL", + "BUS", + "SEGV", + "SYS", + "PIPE", + "ALRM", + "TERM", + "URG", + "STOP", + "TSTP", + "CONT", + "CHLD", + "TTIN", + "TTOU", + "IO", + "XCPU", + "XFSZ", + "VTALRM", + "PROF", + "WINCH", + "INFO", + "USR1", + "USR2", +}; + +std::int64_t APS5_VABI _write_nid_postfix(int descriptor, const void* buffer, std::size_t count); + +void APS5_VABI psignal_nid_postfix(unsigned int signal, const char* prefix) { + const char* const message = signal < 32 ? sys_siglist_nid_postfix[signal] : "Unknown signal"; + if (prefix != nullptr && prefix[0] != 0) { + _write_nid_postfix(2, prefix, std::strlen(prefix)); + _write_nid_postfix(2, ": ", 2); + } + _write_nid_postfix(2, message, std::strlen(message)); + _write_nid_postfix(2, "\n", 1); +} + } diff --git a/core/libs/tests/GuestErr.cpp b/core/libs/tests/GuestErr.cpp new file mode 100644 index 0000000000..942936c8f7 --- /dev/null +++ b/core/libs/tests/GuestErr.cpp @@ -0,0 +1,197 @@ +#include "prx/libc/include/FileStream.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include "SceTypes.hpp" +#include <cstdarg> +#include <cstdio> +#include <cstdlib> +#include <string> +#ifdef _WIN32 +#include <io.h> +#else +#include <unistd.h> +#endif + +using ErrExit = void (APS5_VABI*)(int); + +extern "C" { +void APS5_VABI err_set_file_nid_postfix(FileStream* stream); +void APS5_VABI err_set_exit_nid_postfix(ErrExit function); +void APS5_VABI warn_nid_postfix(const char* format, ...); +void APS5_VABI warnc_nid_postfix(int code, const char* format, ...); +void APS5_VABI warnx_nid_postfix(const char* format, ...); +[[noreturn]] void APS5_VABI err_nid_postfix(int status, const char* format, ...); +[[noreturn]] void APS5_VABI errc_nid_postfix(int status, int code, const char* format, ...); +[[noreturn]] void APS5_VABI errx_nid_postfix(int status, const char* format, ...); +void APS5_VABI vwarn_nid_postfix(const char* format, VaList* args); +void APS5_VABI vwarnc_nid_postfix(int code, const char* format, VaList* args); +void APS5_VABI vwarnx_nid_postfix(const char* format, VaList* args); +[[noreturn]] void APS5_VABI verr_nid_postfix(int status, const char* format, VaList* args); +[[noreturn]] void APS5_VABI verrc_nid_postfix(int status, int code, const char* format, VaList* args); +[[noreturn]] void APS5_VABI verrx_nid_postfix(int status, const char* format, VaList* args); +char* APS5_VABI strerror_nid_postfix(int error); +int* APS5_VABI __error_nid_postfix(); +extern const char* __progname_nid_postfix; +void APS5_VABI psignal_nid_postfix(unsigned int signal, const char* prefix); +extern const char* const sys_signame_nid_postfix[32]; +extern const char* const sys_siglist_nid_postfix[32]; +} + +namespace { + +struct ExitRequested { + int status; +}; + +int g_hookStatus = -1; + +void APS5_VABI Hook(int status) { + g_hookStatus = status; + throw ExitRequested{status}; +} + +void Require(bool value, const char* what) { + if (value) return; + std::fprintf(stderr, "failed: %s\n", what); + std::abort(); +} + +class Capture { + FileStream stream; + long consumed = 0; + +public: + Capture() : stream(std::tmpfile()) { err_set_file_nid_postfix(&stream); } + ~Capture() { + err_set_file_nid_postfix(nullptr); + stream.Close(); + } + std::string Take() { + std::FILE* handle = stream.GetHandle(); + std::fflush(handle); + const long end = std::ftell(handle); + std::fseek(handle, consumed, SEEK_SET); + std::string text; + for (int character; (character = std::fgetc(handle)) != EOF;) text.push_back(static_cast<char>(character)); + consumed = end; + std::fseek(handle, end, SEEK_SET); + return text; + } +}; + +template <typename TCall> +std::string DescriptorTwoOutput(TCall call) { + std::fflush(stderr); + std::FILE* capture = std::tmpfile(); + const int saved = dup(2); + dup2(fileno(capture), 2); + call(); + dup2(saved, 2); + close(saved); + std::rewind(capture); + std::string text; + for (int character; (character = std::fgetc(capture)) != EOF;) text.push_back(static_cast<char>(character)); + std::fclose(capture); + return text; +} + +template <typename TCall> +int ExitStatus(TCall call) { + try { + call(); + } catch (const ExitRequested& request) { + return request.status; + } + return -1; +} + +enum class Variant { Warn, Warnc, Warnx, Err, Errc, Errx, WarnxSkipping }; + +void APS5_VABI CallVariant(Variant variant, int status, int code, const char* format, ...) { +#ifdef _WIN32 + __builtin_sysv_va_list args; + __builtin_sysv_va_start(args, format); + if (variant == Variant::WarnxSkipping) (void)__builtin_va_arg(args, int); +#else + std::va_list args; + va_start(args, format); + if (variant == Variant::WarnxSkipping) (void)va_arg(args, int); +#endif + auto* list = reinterpret_cast<VaList*>(args); + switch (variant) { + case Variant::Warn: vwarn_nid_postfix(format, list); break; + case Variant::Warnc: vwarnc_nid_postfix(code, format, list); break; + case Variant::Warnx: + case Variant::WarnxSkipping: vwarnx_nid_postfix(format, list); break; + case Variant::Err: verr_nid_postfix(status, format, list); + case Variant::Errc: verrc_nid_postfix(status, code, format, list); + case Variant::Errx: verrx_nid_postfix(status, format, list); + } +#ifdef _WIN32 + __builtin_sysv_va_end(args); +#else + va_end(args); +#endif +} + +} + +int main() { + const char* const savedName = __progname_nid_postfix; + __progname_nid_postfix = "prog"; + const std::string noEntry = strerror_nid_postfix(2); + const std::string denied = strerror_nid_postfix(13); + { + Capture capture; + *__error_nid_postfix() = 2; + warn_nid_postfix("open %s", "a.txt"); + Require(capture.Take() == "prog: open a.txt: " + noEntry + "\n", "warn"); + *__error_nid_postfix() = 2; + warn_nid_postfix(nullptr); + Require(capture.Take() == "prog: " + noEntry + "\n", "warn without format"); + warnc_nid_postfix(13, "%d-%s", 7, "x"); + Require(capture.Take() == "prog: 7-x: " + denied + "\n", "warnc"); + warnx_nid_postfix("plain %c", 'q'); + Require(capture.Take() == "prog: plain q\n", "warnx"); + warnx_nid_postfix(nullptr); + Require(capture.Take() == "prog: \n", "warnx without format"); + + err_set_exit_nid_postfix(Hook); + Require(ExitStatus([] { errx_nid_postfix(3, "bad %d", 1); }) == 3 && g_hookStatus == 3, "errx status"); + Require(capture.Take() == "prog: bad 1\n", "errx text"); + Require(ExitStatus([] { errc_nid_postfix(4, 13, "c"); }) == 4, "errc status"); + Require(capture.Take() == "prog: c: " + denied + "\n", "errc text"); + *__error_nid_postfix() = 2; + Require(ExitStatus([] { err_nid_postfix(5, "e"); }) == 5, "err status"); + Require(capture.Take() == "prog: e: " + noEntry + "\n", "err text"); + + *__error_nid_postfix() = 2; + CallVariant(Variant::Warn, 0, 0, "open %s", "a.txt"); + Require(capture.Take() == "prog: open a.txt: " + noEntry + "\n", "vwarn"); + *__error_nid_postfix() = 2; + CallVariant(Variant::Warnc, 0, 13, "%d-%s", 7, "x"); + Require(capture.Take() == "prog: 7-x: " + denied + "\n", "vwarnc uses its code"); + CallVariant(Variant::Warnc, 0, 13, nullptr); + Require(capture.Take() == "prog: " + denied + "\n", "vwarnc without format"); + CallVariant(Variant::Warnx, 0, 0, "%d %d %d %d %d %d %d %d %.2f %s %ld", 1, 2, 3, 4, 5, 6, 7, 8, 2.5, "s", 123456789L); + Require(capture.Take() == "prog: 1 2 3 4 5 6 7 8 2.50 s 123456789\n", "vwarnx stack and floating arguments"); + CallVariant(Variant::WarnxSkipping, 0, 0, "after %d", 1, 2); + Require(capture.Take() == "prog: after 2\n", "vwarnx continues an advanced list"); + *__error_nid_postfix() = 16; + Require(ExitStatus([] { CallVariant(Variant::Err, 6, 0, nullptr); }) == 6, "verr status"); + Require(capture.Take() == "prog: " + std::string(strerror_nid_postfix(16)) + "\n", "verr without format"); + *__error_nid_postfix() = 2; + Require(ExitStatus([] { CallVariant(Variant::Errc, 4, 13, "c%s", "!"); }) == 4, "verrc status"); + Require(capture.Take() == "prog: c!: " + denied + "\n", "verrc text"); + Require(ExitStatus([] { CallVariant(Variant::Errx, 0, 0, nullptr); }) == 0, "verrx status 0"); + Require(capture.Take() == "prog: \n", "verrx without format"); + err_set_exit_nid_postfix(nullptr); + } + __progname_nid_postfix = savedName; + + Require(std::string(sys_signame_nid_postfix[0]) == "Signal 0" && std::string(sys_signame_nid_postfix[15]) == "TERM", "sys_signame"); + Require(std::string(sys_signame_nid_postfix[26]) == "VTALRM" && std::string(sys_signame_nid_postfix[31]) == "USR2", "sys_signame tail"); + Require(DescriptorTwoOutput([] { psignal_nid_postfix(2, "tag"); }) == std::string("tag: ") + sys_siglist_nid_postfix[2] + "\n", "psignal with prefix"); + Require(DescriptorTwoOutput([] { psignal_nid_postfix(11, ""); }) == std::string(sys_siglist_nid_postfix[11]) + "\n", "psignal empty prefix"); + Require(DescriptorTwoOutput([] { psignal_nid_postfix(32, nullptr); }) == "Unknown signal\n", "psignal unknown signal"); + return 0; +} From ef3cb7c9ef3a893b52b73ef9cf0cd988f7724bb1 Mon Sep 17 00:00:00 2001 From: Alex Collini <pipodi93@gmail.com> Date: Sun, 18 Oct 2026 09:57:24 +0200 Subject: [PATCH 0968/1000] docs(agc): bound the released-texture sweep by the grown resource cache Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- docs/dev/debt/libSceAgcDriver.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index e67564161c..6c7838b646 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -172,7 +172,7 @@ - [Flip packets raise the graphics end-of-pipe event](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp) (libSceAgcDriver) - a flip delivers the queue's end-of-pipe event (`sceAgcDriverAddEqEvent`) once the work recorded before it completes, as a `RELEASE_MEM` with `INT_SEL` does. Minecraft (PPSA17221) registers the graphics event, never requests an interrupt in its releases, and waits on the event after every flip while holding its render lock, so without this it never presents a second frame. Whether the console raises the event from the flip itself or from an interrupt its flip sequence contains is not measured; a title that waits only on release interrupts sees one extra event per flip, with `data` counting it. - [Host import windows and budget](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver): a registered range larger than 64 MiB is imported one 64 MiB window at a time for buffer and fill accesses; an access across window edges imports the cells it touches, grown to the union of the range's imports it overlaps. An address-based build (`AcquireRegistered`) imports the range whole and replaces its windows. Live imports stay under a budget: `APS5_HOST_IMPORT_MIB`, else the larger of 6 GiB and half the physical memory, at most the largest host heap less 2 GiB (imports and type-3 mirrors share it; #167's rule). A new import beyond it retires the least recently used imports not used in the last 256 lookups; one that would not fit even then is refused before anything is retired, and imports made when a range is mapped never evict. A refused import is tried again at its next use. The 64 MiB window, the 256-lookup protection and the 2 GiB reserve are choices, not measured. - [Registration preparation at submit](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp) (libSceAgcDriver) - `Driver::Submit` waits for every registration-time shader preparation queued before the call, not only those of the shaders the commands use, so a submission behind a long queue of unrelated preparations waits for all of them on the submitting thread. Waiting there instead of in the queue worker keeps the GPU side of a submission short: a title that sees a submission take seconds treats the GPU as hung (Goat Simulator 3, PPSA04159, then stops submitting and its garbage collector fails a 10 s timeout). On the console shaders are compiled ahead of time, so neither wait exists; how long a title tolerates a slow submit call is not known. -- [Resource cache entries holding released textures](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - a sampled texture the texture cache drops while something else holds it, or makes without caching (`admitTexture` refusing a first use), is marked released, and `ResourceCache::Insert` refuses descriptor sets that hold one and removes the entries that do. The removal waits for the next insert: until then `Find` still returns those entries and their textures stay alive. One process-wide flag starts the sweep, so while the sampled cache is over its budget every insert scans all entries (up to 1,024, each with its textures); that cost is not measured. A draw or dispatch whose textures the cache refused builds its descriptor sets again on each use for as long as the cache stays full; the count bounds (`APS5_RESOURCE_CACHE_ENTRIES`, `APS5_RESOURCE_CACHE_TABLE_ELEMENTS`) still bound nothing by bytes, and sampled textures made outside the texture cache (the disabled cache, depth-surface copies) are never marked. Measured only on Ghost of Yōtei (PPSA26344) on one RTX 4090: with the default bounds, sampled textures held outside the cache peaked at 2,836 MiB (7,830 MiB before) and device-local heap 0 stayed under its 22.9 GB budget for the whole run (26,357 MiB before, which aborted with `VK_ERROR_OUT_OF_DEVICE_MEMORY` at the wolf scene). Whether the extra descriptor-set builds cost frame time elsewhere is not measured. +- [Resource cache entries holding released textures](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - a sampled texture the texture cache drops while something else holds it, or makes without caching (`admitTexture` refusing a first use), is marked released, and `ResourceCache::Insert` refuses descriptor sets that hold one and removes the entries that do. The removal waits for the next insert: until then `Find` still returns those entries and their textures stay alive. One process-wide flag starts the sweep, so while the sampled cache is over its budget every insert scans all entries (1,024 at start, up to 16,384 after growth, or `APS5_RESOURCE_CACHE_ENTRIES`, each with its textures); that cost is not measured. When `ReleaseTextureCaches` runs on device memory exhaustion, the sampled textures these entries hold are only marked released and are freed at the next insert, so the immediate allocation retry gets no memory back from them. A draw or dispatch whose textures the cache refused builds its descriptor sets again on each use for as long as the cache stays full; the count bounds (`APS5_RESOURCE_CACHE_ENTRIES`, `APS5_RESOURCE_CACHE_TABLE_ELEMENTS`) still bound nothing by bytes, and sampled textures made outside the texture cache (the disabled cache, depth-surface copies) are never marked. Measured only on Ghost of Yōtei (PPSA26344) on one RTX 4090: with the default bounds, sampled textures held outside the cache peaked at 2,836 MiB (7,830 MiB before) and device-local heap 0 stayed under its 22.9 GB budget for the whole run (26,357 MiB before, which aborted with `VK_ERROR_OUT_OF_DEVICE_MEMORY` at the wolf scene). Whether the extra descriptor-set builds cost frame time elsewhere is not measured. - [Unclamped normalized color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - CB_COLOR_INFO targets with BLEND_CLAMP clear (bit 15) on UNORM and SNORM formats are accepted when blending is disabled (ENABLE clear or BLEND_BYPASS set). Vulkan converts unblended shader values and clamps to the normalized range automatically; blending into an unclamped normalized target remains unmodeled because Vulkan always clamps inputs prior to blending. - [Thick SW_4KB_S_X volumes](../../../core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp) use AMD addrlib's GFX10 non-RB+ 16-pipe `GFX10_SW_4K_S3_X_PATINFO` equations for 1, 2, 4, 8 and 16-byte elements, including packed mip tails. The layout follows [AMD PAL c5e80007](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/gfx10/gfx10addrlib.cpp); it has not been measured on console hardware. Other thick swizzle modes outside the supported families still throw. Mipmapped 3D color targets remain refused by the color-register decoder. From bb109df676fcb66e1b7fe658976ff9d85a1cfe83 Mon Sep 17 00:00:00 2001 From: 011010 <husari09.oro@gmail.com> Date: Sun, 18 Oct 2026 02:13:39 -0600 Subject: [PATCH 0969/1000] fix(tools): read the helper and wide-string environment readers --- tools/check_conventions.py | 16 ++----- tools/env_reference.py | 78 +++++++++++++++---------------- tools/tests/test_env_reference.py | 17 ++++++- 3 files changed, 58 insertions(+), 53 deletions(-) diff --git a/tools/check_conventions.py b/tools/check_conventions.py index af2d083056..f9fb2ad69e 100644 --- a/tools/check_conventions.py +++ b/tools/check_conventions.py @@ -13,6 +13,7 @@ from urllib.parse import unquote sys.path.insert(0, str(Path(__file__).resolve().parent)) from nid_names import CPP_TOKEN +import env_reference RULES = { "comment": ("error", "docs/dev/CONVENTIONS.md", "Comments are only for technical debt, so they need a change to docs/dev/TechnicalDebt.md or docs/dev/debt/ in the same pull request; #endif and namespace ends are always allowed"), @@ -58,15 +59,6 @@ FENCE = re.compile(r"^ {0,3}(```|~~~).*?^ {0,3}\1[ \t]*\r?$|^ {0,3}(?:```|~~~).* INLINE = re.compile(r"`[^`\n]*`") SECTION = re.compile(r"^###(?!#)\s*(.+?)\s*$", re.M) KEY = re.compile(r"^( *)([A-Za-z0-9_-]+|'[^']*'|\"[^\"]*\"):(?:\s+(.*?))?\s*$") -ENV_PREFIX = r"(?:APS5|ANYPS5)_[A-Za-z0-9_]+" -ENV_GREP = (r'(std::)?getenv\([[:space:]]*"(APS5|ANYPS5)_[A-Za-z0-9_]+"' - r'|IsEnvironmentSet\([[:space:]]*"(APS5|ANYPS5)_[A-Za-z0-9_]+"' - r'|kibSetting\([[:space:]]*"(APS5|ANYPS5)_[A-Za-z0-9_]+"' - r'|MaskFromEnvironment\([[:space:]]*"(APS5|ANYPS5)_[A-Za-z0-9_]+"' - r'|EnvInt\([[:space:]]*"(APS5|ANYPS5)_[A-Za-z0-9_]+"' - r'|GetEnvironmentVariableA\([[:space:]]*"(APS5|ANYPS5)_[A-Za-z0-9_]+"') -ENV_READER = re.compile(r'(?:(?:std::)?getenv|IsEnvironmentSet|kibSetting|MaskFromEnvironment|EnvInt|GetEnvironmentVariableA)\(\s*"(' + ENV_PREFIX + r')"') -ENV_NAME = re.compile(r"`(" + ENV_PREFIX + r")`") def git(*args, text=True): @@ -289,12 +281,12 @@ class Check: doc = "docs/dev/ENVIRONMENT.md" if doc not in self.tree: return - result = subprocess.run(["git", "grep", "-nE", ENV_GREP, self.head, "--", "core"], + result = subprocess.run(["git", "grep", "-nE", env_reference.GREP, self.head, "--", "core"], capture_output=True, encoding="utf-8", errors="replace") reads = set() for line in result.stdout.splitlines(): - reads.update(match.group(1) for match in ENV_READER.finditer(line)) - documented = set(ENV_NAME.findall(self.show(doc))) + reads.update(match.group("name") for _, match in env_reference.line_reads(line)) + documented = set(env_reference.documented_pattern(env_reference.PREFIXES).findall(self.show(doc))) for name in sorted(reads - documented): self.report("env-reference", doc, 0, f"{name} is read by the code but has no row") for name in sorted(documented - reads): diff --git a/tools/env_reference.py b/tools/env_reference.py index 986947f42d..55aa42af6b 100644 --- a/tools/env_reference.py +++ b/tools/env_reference.py @@ -1,4 +1,5 @@ import argparse +import functools import re import sys from pathlib import Path @@ -9,38 +10,51 @@ ROOTS = ("core",) EXTENSIONS = {".c", ".cc", ".cpp", ".cxx", ".h", ".hh", ".hpp", ".hxx", ".inl", ".ipp", ".py"} SKIP = {"build", "build-linux", "build-relinker", "3rdparty", ".git"} -READERS = { - "IsEnvironmentSet": "presence", - "kibSetting": "integer", - "MaskFromEnvironment": "integer", -} +INTEGER_READERS = {"kibSetting", "MaskFromEnvironment", "EnvInt", "configured", "environmentCount"} +STRING_READERS = {"GetEnvironmentVariableA", "GetEnvironmentVariableW", "addWideString"} NULL_TEST = re.compile(r"[!=]=\s*(nullptr|NULL|0)\b|!\s*(?:std::)?getenv\(") INTEGER_TEST = re.compile(r"\b(atoi|strtol|strtoul|strtoll|strtoull|stoi|stol|stoul|from_chars|atoi64)\s*\(") +GREP = (r'(getenv|IsEnvironmentSet|kibSetting|MaskFromEnvironment|EnvInt|GetEnvironmentVariable[AW]' + r'|configured|PipelineSwitch|environmentCount|addWideString)' + r'\([[:space:]]*(u|L)?"(APS5|ANYPS5)_[A-Za-z0-9_]+"') -def pattern(prefixes): - name = "(?:" + "|".join(re.escape(prefix) for prefix in prefixes) + ")[A-Za-z0-9_]+" - return re.compile( - r"(?P<getenv>(?:std::)?getenv)\(\s*\"(?P<getenv_name>" + name + r")\"\s*\)" - r"|IsEnvironmentSet\(\s*\"(?P<presence_name>" + name + r")\"\s*\)" - r"|MaskFromEnvironment\(\s*\"(?P<mask_name>" + name + r")\"\s*\)" - r"|kibSetting\(\s*\"(?P<kib_name>" + name + r")\"\s*,\s*(?P<kib_default>[0-9]+)" - r"|EnvInt\(\s*\"(?P<envint_name>" + name + r")\"\s*,\s*(?P<envint_default>-?[0-9]+)" - r"|GetEnvironmentVariableA\(\s*\"(?P<win_name>" + name + r")\"" +def _name(prefixes): + return "(?:" + "|".join(re.escape(prefix) for prefix in prefixes) + ")[A-Za-z0-9_]+" + + +@functools.lru_cache(maxsize=None) +def forms(prefixes=PREFIXES): + name = _name(prefixes) + return ( + ("getenv", re.compile(r'(?:std::)?getenv\(\s*"(?P<name>' + name + r')"\s*\)')), + ("IsEnvironmentSet", re.compile(r'IsEnvironmentSet\(\s*"(?P<name>' + name + r')"\s*\)')), + ("MaskFromEnvironment", re.compile(r'MaskFromEnvironment\(\s*"(?P<name>' + name + r')"\s*\)')), + ("kibSetting", re.compile(r'kibSetting\(\s*"(?P<name>' + name + r')"\s*,\s*(?P<default>[0-9]+)')), + ("EnvInt", re.compile(r'EnvInt\(\s*"(?P<name>' + name + r')"\s*,\s*(?P<default>-?[0-9]+)')), + ("configured", re.compile(r'\b(?:configured|PipelineSwitch)\(\s*"(?P<name>' + name + r')"' + r'(?:\s*,\s*(?P<default>[0-9]+)[uUlL]*)?')), + ("environmentCount", re.compile(r'\benvironmentCount\(\s*"(?P<name>' + name + r')"\s*,\s*(?P<default>[0-9]+)')), + ("GetEnvironmentVariableA", re.compile(r'GetEnvironmentVariableA\(\s*"(?P<name>' + name + r')"')), + ("GetEnvironmentVariableW", re.compile(r'GetEnvironmentVariableW\(\s*L"(?P<name>' + name + r')"')), + ("addWideString", re.compile(r'\baddWideString\(\s*u"(?P<name>' + name + r')"\s*\)')), ) +def line_reads(line, prefixes=PREFIXES): + return [(reader, match) for reader, form in forms(tuple(prefixes)) for match in form.finditer(line)] + + def documented_pattern(prefixes): - name = "(?:" + "|".join(re.escape(prefix) for prefix in prefixes) + ")[A-Za-z0-9_]+" - return re.compile(r"`(" + name + r")`") + return re.compile(r"`(" + _name(prefixes) + r")`") def kind_of(reader, window): if reader == "IsEnvironmentSet": return "presence" - if reader in ("kibSetting", "MaskFromEnvironment", "EnvInt"): + if reader in INTEGER_READERS: return "integer" - if reader == "GetEnvironmentVariableA": + if reader in STRING_READERS: return "string" if NULL_TEST.search(window): return "presence" @@ -50,9 +64,9 @@ def kind_of(reader, window): def default_of(match, window): - for group in ("kib_default", "envint_default"): - if match.group(group): - return match.group(group) + default = match.groupdict().get("default") + if default: + return default found = re.search(r"\?[\s\S]*?:\s*([0-9]+)", window) return found.group(1) if found else "" @@ -68,7 +82,6 @@ def source_files(root): def reads(roots, prefixes=PREFIXES): - read = pattern(prefixes) found = {} for root in roots: for path in source_files(root): @@ -78,26 +91,11 @@ def reads(roots, prefixes=PREFIXES): continue lines = text.splitlines() for number, line in enumerate(lines, 1): - if not read.search(line): + if not any(prefix in line for prefix in prefixes): continue window = "\n".join(lines[number - 1:number + 2]) - for match in read.finditer(line): - name = (match.group("getenv_name") or match.group("presence_name") or - match.group("mask_name") or match.group("kib_name") or - match.group("envint_name") or match.group("win_name")) - if match.group("getenv"): - reader = "getenv" - elif match.group("presence_name"): - reader = "IsEnvironmentSet" - elif match.group("mask_name"): - reader = "MaskFromEnvironment" - elif match.group("envint_name"): - reader = "EnvInt" - elif match.group("win_name"): - reader = "GetEnvironmentVariableA" - else: - reader = "kibSetting" - found.setdefault(name, []).append( + for reader, match in line_reads(line, prefixes): + found.setdefault(match.group("name"), []).append( (path.as_posix(), number, kind_of(reader, window), default_of(match, window))) for sites in found.values(): sites.sort() diff --git a/tools/tests/test_env_reference.py b/tools/tests/test_env_reference.py index 87d5a961d2..ae390c5e9d 100644 --- a/tools/tests/test_env_reference.py +++ b/tools/tests/test_env_reference.py @@ -18,6 +18,13 @@ static const char* f = std::getenv("PATH"); static const char* g = std::getenv("TZ"); static int h = EnvInt("APS5_THREAD_NICE", -10); static int i = GetEnvironmentVariableA("APS5_CRASH_INDIRECT_MEMORY", nullptr, 0); +static unsigned long j = configured("APS5_PIPELINE_JOURNAL_MAX_MIB", 1024u) << 20u; +static unsigned long k = PipelineSwitch("APS5_PIPELINE_STORE_ENTRIES", 1024u); +static unsigned long l = configured("APS5_PIPELINE_JOURNAL_THREADS", std::max(1u, hardware / 2u)); +static std::size_t m = environmentCount("APS5_PM4_RING", 16, MaximumCapacity, "APS5_PM4_RING must be a number"); +static int n = GetEnvironmentVariableW(L"ANYPS5_LOADEXEC_ARGV_SKIP", skip, 2); +static const auto* o = addWideString(u"ANYPS5_LOADEXEC_PARENT"); +static unsigned long p = reconfigured("APS5_NOT_A_READ", 1); """ @@ -37,10 +44,18 @@ class EnvReferenceTests(unittest.TestCase): set(found), {"APS5_PROFILE_DRAW", "APS5_TRACE_GPU", "APS5_HOST_IMPORT_MIB", "APS5_WORKER_AFFINITY_MASK", "ANYPS5_GPU", "APS5_THREAD_NICE", - "APS5_CRASH_INDIRECT_MEMORY"}) + "APS5_CRASH_INDIRECT_MEMORY", "APS5_PIPELINE_JOURNAL_MAX_MIB", + "APS5_PIPELINE_STORE_ENTRIES", "APS5_PIPELINE_JOURNAL_THREADS", "APS5_PM4_RING", + "ANYPS5_LOADEXEC_ARGV_SKIP", "ANYPS5_LOADEXEC_PARENT"}) self.assertEqual(found["APS5_PROFILE_DRAW"][0][1], 1) self.assertEqual(found["APS5_THREAD_NICE"][0][2:], ("integer", "-10")) self.assertEqual(found["APS5_HOST_IMPORT_MIB"][0][2:], ("integer", "6144")) + self.assertEqual(found["APS5_PIPELINE_JOURNAL_MAX_MIB"][0][2:], ("integer", "1024")) + self.assertEqual(found["APS5_PIPELINE_STORE_ENTRIES"][0][2:], ("integer", "1024")) + self.assertEqual(found["APS5_PIPELINE_JOURNAL_THREADS"][0][2:], ("integer", "")) + self.assertEqual(found["APS5_PM4_RING"][0][2:], ("integer", "16")) + self.assertEqual(found["ANYPS5_LOADEXEC_ARGV_SKIP"][0][2:], ("string", "")) + self.assertEqual(found["ANYPS5_LOADEXEC_PARENT"][0][2:], ("string", "")) def test_the_default_prefixes_cover_both(self): root = self.fixture() From 47710d80133b573659e54809446d4e3b7ee84095 Mon Sep 17 00:00:00 2001 From: 011010 <husari09.oro@gmail.com> Date: Sun, 18 Oct 2026 02:13:39 -0600 Subject: [PATCH 0970/1000] docs(environment): add the rows for the helper and wide-string readers --- docs/dev/ENVIRONMENT.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/docs/dev/ENVIRONMENT.md b/docs/dev/ENVIRONMENT.md index c3ba2637c9..27bf42846e 100644 --- a/docs/dev/ENVIRONMENT.md +++ b/docs/dev/ENVIRONMENT.md @@ -1,11 +1,12 @@ # Environment variables -The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetting`, `MaskFromEnvironment`, `EnvInt` and `GetEnvironmentVariableA`. This table lists every `APS5_*` and `ANYPS5_*` name it reads, its kind, the value used when it is unset and what it does, one table per library. Regenerate the names with `python3 tools/env_reference.py --table`; the Conventions check fails when a name the code reads has no row here, or a row's name is no longer read. +The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetting`, `MaskFromEnvironment`, `EnvInt`, `GetEnvironmentVariableA`, `GetEnvironmentVariableW` with a wide literal, `configured`, `PipelineSwitch`, `environmentCount` and `addWideString` (the wide names the Windows entry stub reads). This table lists every `APS5_*` and `ANYPS5_*` name it reads, its kind, the value used when it is unset and what it does, one table per library. Regenerate the names with `python3 tools/env_reference.py --table`; the Conventions check fails when a name the code reads has no row here, or a row's name is no longer read. ### core/libs | Name | Kind | Source | Default | Description | |------|------|--------|---------|-------------| +| `ANYPS5_LOADEXEC_PARENT` | string | `core/libs/tests/GuestLoadExecHost.cpp` | | variable the Windows entry stub reads; its wide name is built in `core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp` | | `ANYPS5_LOADEXEC_TEST_ROOT` | string | `core/libs/tests/GuestLoadExecHost.cpp` | | directory the load-exec host test reads its guest executable from | | `ANYPS5_TEST_QUOTED_ARGV0` | presence | `core/libs/tests/GuestLoadExecHost.cpp` | | makes the load-exec test's first argument one with quotes, a backslash and non-ASCII text | | `APS5_EBOOT` | string | `core/libs/tests/GuestSystemExit.cpp` | | host path of the executable launched for /app0/eboot.bin, defaulting to the running executable | @@ -307,7 +308,12 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_PIPELINE_DRAIN_FILLS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferFill.cpp` | | runs queue-0 buffer fills through the draw pipeline to keep their order; any value stores them in place instead | | `APS5_PIPELINE_DRAIN_LABELS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp` | | answers a queue-0 wait from the draw pipeline's own pending labels while it is busy; any value drains the pipeline at the wait | | `APS5_PIPELINE_DRAIN_SUBMISSIONS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp` | | lets a submission end without submitting its recorded work, so it joins a later submit with the pipeline's queue; any value drains the pipeline and submits at the end of the submission | +| `APS5_PIPELINE_JOURNAL_ENTRIES` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp` | 1024 | number of journal entries loaded at most; a longer journal is truncated to this count | +| `APS5_PIPELINE_JOURNAL_MAX_MIB` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp` | 1024 | size cap in MiB on the pipeline journal; later pipelines are not recorded once it is reached | +| `APS5_PIPELINE_JOURNAL_THREADS` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp` | | threads that replay the journal's pipelines, 0 disables the replay; unset uses half the hardware threads, at least one | | `APS5_PIPELINE_STATS` | string | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | captures and prints driver statistics for each new pipeline, requiring `VK_KHR_pipeline_executable_properties`; `1` enables | +| `APS5_PIPELINE_STORE_ENTRIES` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp` | 1024 | LRU bound on the entries of the pipeline store; an entry is evicted only once no recorded draw holds it | +| `APS5_PM4_RING` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/SubmissionRing.cpp` | 16 | number of submissions the PM4 ring keeps for dumps, from 0 to 4096; 0 disables the ring | | `APS5_POLL_REAP` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/WaitMemory.cpp` | | reaps completions on every poll tick | | `APS5_POLL_TRY_EACH` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/WaitMemory.cpp` | | tries the poll service on every tick instead of at the poll interval | | `APS5_PREBUILD_THREADS` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp` | 1 | number of threads in the one-thread pool that prebuilds registration-form shader libraries | @@ -335,6 +341,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_SRGB_SHADER_DECODE` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp` | | forces shader sRGB decode for the sRGB 8-bit formats | | `APS5_STAGING_POOL_MIB` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp` | | size in MiB of the device-local staging buffer pool; `0` disables the device tier | | `APS5_STALE_EVICT` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | | also evicts a proved-present dead image's pending layers on an access | +| `APS5_STALL_WATCHDOG_SECS` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/SubmissionRing.cpp` | 0 | seconds a queue may go without progress before the submission rings are dumped; 0 disables the watchdog, at most 86400 | | `APS5_STORE_AT_FLIP` | string | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | stores every pending storage-image result at each display flip; only `1` is accepted | | `APS5_STORE_AT_LABEL` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | flushes every pending storage-image result when a label is written on the GPU | | `APS5_SUBMISSION_EPOCH_EACH` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp` | | bumps the guest-memory collect epoch on every submission instead of only when the submission is newer than the last bump | @@ -512,6 +519,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | Name | Kind | Source | Default | Description | |------|------|--------|---------|-------------| +| `ANYPS5_LOADEXEC_ARGV_SKIP` | string | `core/libs/prx/libkernel/System/src/Process.cpp` | | on Windows, set to `1` to drop the first process argument and clear the variable | | `APS5_COARSE_TIMED_WAITS` | presence | `core/libs/prx/libkernel/Time/TimedWait.cpp` | | on Windows, rounds timed waits up to the scheduler tick | | `APS5_JOB_AFFINITY` | presence | `core/libs/prx/libkernel/Pthread/src/Thread.cpp` | | pins the title's job worker threads to the efficiency cores of a hybrid CPU | | `APS5_JOB_AFFINITY_MASK` | integer | `core/libs/prx/libkernel/Pthread/src/Thread.cpp` | | hex core mask that job worker threads are pinned to | From ea01ee89650eabce885f3b48c6e2637a58cb8559 Mon Sep 17 00:00:00 2001 From: 011010 <husari09.oro@gmail.com> Date: Sun, 18 Oct 2026 02:26:27 -0600 Subject: [PATCH 0971/1000] fix(tools): check the environment names a pull request adds --- tools/check_conventions.py | 31 +++-- .../test_check_conventions_env_reference.py | 111 ++++++++++++++++++ 2 files changed, 134 insertions(+), 8 deletions(-) create mode 100644 tools/tests/test_check_conventions_env_reference.py diff --git a/tools/check_conventions.py b/tools/check_conventions.py index f9fb2ad69e..6d5edeeab8 100644 --- a/tools/check_conventions.py +++ b/tools/check_conventions.py @@ -30,7 +30,7 @@ RULES = { "commit-subject": ("error", "docs/dev/CONVENTIONS.md", "Commit subject is not Conventional Commits"), "pr-title": ("error", "docs/dev/CONVENTIONS.md", "Pull request title is not Conventional Commits"), "pr-template": ("error", ".github/pull_request_template.md", "Fill in the pull request template"), - "env-reference": ("error", "docs/dev/ENVIRONMENT.md", "Every APS5_* or ANYPS5_* name the code reads has a row in docs/dev/ENVIRONMENT.md, and every row is a name the code still reads"), + "env-reference": ("error", "docs/dev/ENVIRONMENT.md", "Every APS5_* or ANYPS5_* name a pull request adds to the code has a row in docs/dev/ENVIRONMENT.md, and a row whose name the pull request stops reading is removed"), } CPP = {".c", ".cc", ".cpp", ".cxx", ".h", ".hh", ".hpp", ".hxx", ".inl", ".ipp"} @@ -279,18 +279,33 @@ class Check: def env_reference(self): doc = "docs/dev/ENVIRONMENT.md" - if doc not in self.tree: - return + added, removed = {}, set() + path, number = "", 0 + for line in git("diff", "-U0", self.base, self.head, "--", "core").splitlines(): + if line.startswith("+++ b/"): + path = line[6:] + elif line.startswith("+++") or line.startswith("---"): + continue + elif line.startswith("@@"): + number = int(re.search(r"\+(\d+)", line).group(1)) - 1 + elif line.startswith("+"): + number += 1 + for _, match in env_reference.line_reads(line[1:]): + added.setdefault(match.group("name"), (path, number)) + elif line.startswith("-"): + removed.update(match.group("name") for _, match in env_reference.line_reads(line[1:])) result = subprocess.run(["git", "grep", "-nE", env_reference.GREP, self.head, "--", "core"], capture_output=True, encoding="utf-8", errors="replace") reads = set() for line in result.stdout.splitlines(): reads.update(match.group("name") for _, match in env_reference.line_reads(line)) - documented = set(env_reference.documented_pattern(env_reference.PREFIXES).findall(self.show(doc))) - for name in sorted(reads - documented): - self.report("env-reference", doc, 0, f"{name} is read by the code but has no row") - for name in sorted(documented - reads): - self.report("env-reference", doc, 0, f"{name} has a row but the code no longer reads it") + documented = set(env_reference.documented_pattern(env_reference.PREFIXES).findall(self.show(doc))) if doc in self.tree else set() + for name, (source, line) in sorted(added.items()): + if name not in documented: + self.report("env-reference", source, line, f"{name} is read here but has no row in {doc}") + for name in sorted(removed - reads): + if name in documented: + self.report("env-reference", doc, 0, f"{name} has a row but the code no longer reads it") def pull_request(self, title, body): if not CONVENTIONAL.match(title): diff --git a/tools/tests/test_check_conventions_env_reference.py b/tools/tests/test_check_conventions_env_reference.py new file mode 100644 index 0000000000..5794513b90 --- /dev/null +++ b/tools/tests/test_check_conventions_env_reference.py @@ -0,0 +1,111 @@ +import os +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +import check_conventions +from check_conventions import Check + +DOC = "docs/dev/ENVIRONMENT.md" +SAMPLE = "core/libs/prx/libSceExample/Sample.cpp" +DOC_TEXT = "| `APS5_DOCUMENTED` | presence |\n| `APS5_GONE` | presence |\n" + + +def repository(root, base, head): + def git(*args): + subprocess.run(["git", "-c", f"safe.directory={root}", "-c", "user.name=Conventions Test", + "-c", "user.email=test@example.com", "-c", "commit.gpgsign=false", *args], + cwd=root, capture_output=True, check=True) + + def apply(files): + for path, text in files.items(): + target = root / path + if text is None: + target.unlink() + continue + target.parent.mkdir(parents=True, exist_ok=True) + target.write_text(text, encoding="utf-8") + + git("init", "-q") + apply(base) + git("add", ".") + git("commit", "-qm", "docs: add base") + git("tag", "base") + apply(head) + git("add", "-A") + git("commit", "-qm", "feat: read a new name") + + +def findings_of(root, base, head): + previous = os.getcwd() + os.chdir(root) + try: + check = Check("base", "HEAD") + check.env_reference() + return check.findings + finally: + os.chdir(previous) + + +class EnvReferenceDiffTests(unittest.TestCase): + def run_case(self, base, head): + directory = tempfile.TemporaryDirectory() + self.addCleanup(directory.cleanup) + root = Path(directory.name) + repository(root, base, head) + return findings_of(root, base, head) + + def test_only_a_name_the_pull_request_adds_is_reported_at_its_line(self): + base = { + DOC: DOC_TEXT, + SAMPLE: 'static bool a = std::getenv("APS5_DOCUMENTED") != nullptr;\n' + 'static bool b = std::getenv("APS5_OLD_UNDOCUMENTED") != nullptr;\n', + } + added = 'static bool c = std::getenv("APS5_NEW_READ") != nullptr;\n' + head = {SAMPLE: base[SAMPLE] + added} + findings = self.run_case(base, head) + self.assertEqual(findings, [("env-reference", SAMPLE, 3, + f"APS5_NEW_READ is read here but has no row in {DOC}")]) + + def test_a_branch_without_the_document_still_reports_its_new_reads(self): + base = {SAMPLE: "int unrelated;\n"} + head = {SAMPLE: 'static bool a = std::getenv("APS5_NEW_READ") != nullptr;\n'} + findings = self.run_case(base, head) + self.assertEqual([(rule, detail) for rule, _, _, detail in findings], + [("env-reference", f"APS5_NEW_READ is read here but has no row in {DOC}")]) + + def test_a_row_is_reported_when_the_pull_request_removes_its_last_read(self): + base = { + DOC: DOC_TEXT, + SAMPLE: 'static bool a = std::getenv("APS5_DOCUMENTED") != nullptr;\n' + 'static bool b = std::getenv("APS5_GONE") != nullptr;\n', + } + head = {SAMPLE: 'static bool a = std::getenv("APS5_DOCUMENTED") != nullptr;\n'} + findings = self.run_case(base, head) + self.assertEqual(findings, [("env-reference", DOC, 0, + "APS5_GONE has a row but the code no longer reads it")]) + + def test_removing_one_of_two_reads_keeps_the_row(self): + other = "core/libs/prx/libSceExample/Other.cpp" + base = { + DOC: DOC_TEXT, + SAMPLE: 'static bool a = std::getenv("APS5_DOCUMENTED") != nullptr;\n', + other: 'static bool b = std::getenv("APS5_DOCUMENTED") != nullptr;\n', + } + head = {other: None} + self.assertEqual(self.run_case(base, head), []) + + def test_a_name_the_pull_request_does_not_touch_is_not_reported(self): + base = { + DOC: DOC_TEXT, + SAMPLE: 'static bool b = std::getenv("APS5_OLD_UNDOCUMENTED") != nullptr;\n', + } + head = {SAMPLE: base[SAMPLE] + "int unrelated;\n"} + self.assertEqual(self.run_case(base, head), []) + + +if __name__ == "__main__": + unittest.main() From 7212dae71a26eb12c617137f5376fd5f2ac9d778 Mon Sep 17 00:00:00 2001 From: Maz <mazenshaar@hotmail.com> Date: Sun, 18 Oct 2026 12:53:48 +0400 Subject: [PATCH 0972/1000] fix(libkernel): reject Windows volume mount points in junction reads A mounted folder uses the junction reparse tag and would otherwise be returned as a relative Volume GUID path. Only a drive path is reported, through the absolute-link text helpers. --- .../File/include/WindowsSymbolicLink.hpp | 33 ++++++++++--------- core/libs/tests/GuestKernelSys.cpp | 1 + docs/dev/debt/libkernel.md | 2 +- docs/dev/debt/libkernel_sys.md | 2 +- 4 files changed, 20 insertions(+), 18 deletions(-) diff --git a/core/libs/prx/libkernel/File/include/WindowsSymbolicLink.hpp b/core/libs/prx/libkernel/File/include/WindowsSymbolicLink.hpp index cfe60f586d..cdc089e731 100644 --- a/core/libs/prx/libkernel/File/include/WindowsSymbolicLink.hpp +++ b/core/libs/prx/libkernel/File/include/WindowsSymbolicLink.hpp @@ -26,10 +26,10 @@ class WindowsSymbolicLink { }; static constexpr ULONG relativeFlag = 1; - static std::wstring TextAt(const std::byte* data, DWORD size, USHORT offset, USHORT length, const char* operation) { - const std::size_t begin = sizeof(ReparseData) + offset; + static std::wstring TextAt(const std::byte* data, DWORD size, std::size_t origin, USHORT offset, USHORT length, const char* operation, const char* malformed) { + const std::size_t begin = origin + offset; if (length % sizeof(wchar_t) != 0 || begin + length > size) - throw std::runtime_error(std::string(operation) + ": malformed symbolic link reparse data"); + throw std::runtime_error(std::string(operation) + ": " + malformed); std::wstring name(length / sizeof(wchar_t), L'\0'); if (!name.empty()) std::memcpy(name.data(), data + begin, name.size() * sizeof(wchar_t)); return name; @@ -40,6 +40,12 @@ class WindowsSymbolicLink { return name; } + static bool IsDriveAbsolute(const std::wstring& name) { + if (name.size() < 3 || name[1] != L':' || (name[2] != L'\\' && name[2] != L'/')) return false; + const wchar_t letter = name[0]; + return (letter >= L'A' && letter <= L'Z') || (letter >= L'a' && letter <= L'z'); + } + public: static bool Read(const std::filesystem::path& native, std::string& target, const char* operation) { const HANDLE handle = CreateFileW(native.c_str(), 0, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, @@ -59,10 +65,10 @@ public: if (header.tag != IO_REPARSE_TAG_SYMLINK) return false; const bool relative = (header.flags & relativeFlag) != 0; std::wstring name = relative - ? TextAt(data.data(), size, header.substituteNameOffset, header.substituteNameLength, operation) - : TextAt(data.data(), size, header.printNameOffset, header.printNameLength, operation); + ? TextAt(data.data(), size, sizeof(ReparseData), header.substituteNameOffset, header.substituteNameLength, operation, "malformed symbolic link reparse data") + : TextAt(data.data(), size, sizeof(ReparseData), header.printNameOffset, header.printNameLength, operation, "malformed symbolic link reparse data"); if (!relative && name.empty()) - name = WithoutNtPrefix(TextAt(data.data(), size, header.substituteNameOffset, header.substituteNameLength, operation)); + name = WithoutNtPrefix(TextAt(data.data(), size, sizeof(ReparseData), header.substituteNameOffset, header.substituteNameLength, operation, "malformed symbolic link reparse data")); target = std::filesystem::path(name).generic_string(); return true; } @@ -91,17 +97,12 @@ public: if (size < sizeof(header)) return false; std::memcpy(&header, data.data(), sizeof(header)); if (header.tag != IO_REPARSE_TAG_MOUNT_POINT) return false; - const auto textAt = [&](USHORT offset, USHORT length) { - const std::size_t begin = sizeof(header) + offset; - if (length % sizeof(wchar_t) != 0 || begin + length > size) - throw std::runtime_error(std::string(operation) + ": malformed junction reparse data"); - std::wstring name(length / sizeof(wchar_t), L'\0'); - if (!name.empty()) std::memcpy(name.data(), data.data() + begin, name.size() * sizeof(wchar_t)); - return name; + const auto nameAt = [&](USHORT offset, USHORT length) { + return WithoutNtPrefix(TextAt(data.data(), size, sizeof(header), offset, length, operation, "malformed junction reparse data")); }; - std::wstring name = textAt(header.printNameOffset, header.printNameLength); - if (name.empty()) name = textAt(header.substituteNameOffset, header.substituteNameLength); - if (name.rfind(L"\\??\\", 0) == 0 || name.rfind(L"\\\\?\\", 0) == 0) name.erase(0, 4); + std::wstring name = nameAt(header.printNameOffset, header.printNameLength); + if (!IsDriveAbsolute(name)) name = nameAt(header.substituteNameOffset, header.substituteNameLength); + if (!IsDriveAbsolute(name)) return false; target = std::filesystem::path(name).generic_string(); return true; } diff --git a/core/libs/tests/GuestKernelSys.cpp b/core/libs/tests/GuestKernelSys.cpp index d005fbc525..9041bbc22a 100644 --- a/core/libs/tests/GuestKernelSys.cpp +++ b/core/libs/tests/GuestKernelSys.cpp @@ -483,6 +483,7 @@ int main() { const std::string junctionText(junctionBuffer, static_cast<std::size_t>(junctionLength)); std::error_code junctionError; Require(junctionText.find('?') == std::string::npos && std::filesystem::equivalent(junctionText, junctionTarget, junctionError) && !junctionError); + Require(std::filesystem::remove(junction) && std::filesystem::remove(junctionTarget)); #endif #ifndef _WIN32 if (created == 0) { diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 7089662ef2..c3989d4641 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -116,7 +116,7 @@ - [Windows file timestamps](../../../core/libs/prx/libkernel/File/include/WindowsFileTime.hpp) use the host filesystem's timestamp range and resolution. `utimes`, `futimes`, and `lutimes` reject timestamps outside nonnegative signed 64-bit Windows file-time ticks with `EINVAL`, including exactly 1601-01-01 00:00:00 UTC, whose zero value means "leave unchanged" to `SetFileTime`. Guest directory descriptors still resolve through their recorded path. Filesystems with coarser timestamp resolution cannot retain every guest microsecond - [EVFILT_VNODE on macOS](../../../core/libs/prx/libkernel/Equeue/src/FileEvent.cpp) (libkernel) registers `EVFILT_VNODE` on a duplicate of the descriptor in a host kqueue, whose notes have FreeBSD's values, and drops the `NOTE_LINK` that XNU adds to `NOTE_DELETE` when the watched file or directory is removed, since FreeBSD's `vop_remove_post` and `vop_rmdir_post` report `NOTE_DELETE` alone. As on Windows, a directory registered with `NOTE_EXTEND` is accepted but never receives it: XNU reports a move between directories as `NOTE_WRITE` alone, with `NOTE_LINK` for a subdirectory - [Windows file identity](../../../core/libs/prx/libkernel/File/src/NativeStat.cpp) (libkernel) - `stat` and `fstat` report the volume serial number as `st_dev` and the 64-bit file index folded into the guest's 32-bit `st_ino` (never 0), so two different files can in principle share an `st_ino`; the fold is not taken from a reference. `st_nlink` is clamped to 65535, the width of the guest field. The call keeps the `_wstat64` values when `GetFileInformationByHandle` fails -- [lstat of a Windows symbolic link](../../../core/libs/prx/libkernel/File/src/NativeStat.cpp) (libkernel) reports a link whose target is relative, as `symlink` creates them, with the link's own times, `st_dev` and `st_ino`, mode `S_IFLNK | 0777` and the length of the target `readlink` returns as `st_size`. FreeBSD gives a new link `0777` less the creating process's umask, which the host does not keep, so the permission bits are not the console's. A directory junction is reported the same way, using its print name, or its substitute name when the print name is empty, without a `\??\` or `\\?\` prefix. A link with an absolute target and any other reparse point throw, as for `readlink` +- [lstat of a Windows symbolic link](../../../core/libs/prx/libkernel/File/src/NativeStat.cpp) (libkernel) reports a link whose target is relative, as `symlink` creates them, with the link's own times, `st_dev` and `st_ino`, mode `S_IFLNK | 0777` and the length of the target `readlink` returns as `st_size`. FreeBSD gives a new link `0777` less the creating process's umask, which the host does not keep, so the permission bits are not the console's. A link with an absolute target is reported the same way, using its print name, or its substitute name when the print name is empty, without a `\??\` or `\\?\` prefix. A directory junction is reported the same way when the name left after that prefix is a drive path (`X:\...`): the print name when that name is a drive path, otherwise the substitute name. A mounted volume, whose substitute name is `\??\Volume{GUID}\`, and any other reparse point throw, as for `readlink` - [setitimer](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) follows FreeBSD's `sys_setitimer` and `kern_setitimer`: `EINVAL` for timers other than `ITIMER_REAL`, `ITIMER_VIRTUAL` and `ITIMER_PROF`, and for a value, or the interval of a non-zero value, with a negative field, `tv_usec` of 1000000 or more, or more than `INT32_MAX / 2` seconds; a null value reads the timer as `getitimer` does, with `EFAULT` when the output is null too. `ITIMER_REAL` shares the timer thread of `alarm`, which clears its interval: a zero value disarms it, the previous value is returned, and each expiry re-arms it by `it_interval`. An expiry delivers nothing: with `SIG_IGN` it is dropped, with the default disposition the process exits with status 142, and with a handler, or with `SIGALRM` blocked, it throws, since guest `SIGALRM` handlers are never run and signals are never left pending. The value is not raised to `tick / 16` as `itimerfix` does, and an interval below 1 ms throws, since the console's `hz`, and so that rounding, is unknown (1 ms covers any `hz` of 63 or more). `ITIMER_VIRTUAL` and `ITIMER_PROF` throw when armed, as process CPU time is not accounted, and read and disarm as zero. PPSA99420 imports it and fails to load without it - On Linux, [direct memory](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) lives in one sparse memfd at file offset = physical address, so the [GPU imports](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) can share one udmabuf buffer object per 64 MiB of it. Released direct memory is not punched out of the file, because a live import of its chunk pins the pages and the GPU would keep reading the old ones: its pages stay committed (the process keeps the high-water mark of the direct memory it used) and are zeroed when the range is allocated again. - [system](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) runs no command. It follows FreeBSD 11's `lib/libc/stdlib/system.c` for a sandbox without `/bin/sh`: a null command returns 1 unconditionally, as that version does, and any command returns the wait status of a child that could not exec the shell and called `_exit(127)` (`0x7f00`). Whether the console's application sandbox has no shell, and whether `vfork` succeeds there (FreeBSD returns -1 when it fails), is unverified diff --git a/docs/dev/debt/libkernel_sys.md b/docs/dev/debt/libkernel_sys.md index ec4578c5e3..0995c0cf8c 100644 --- a/docs/dev/debt/libkernel_sys.md +++ b/docs/dev/debt/libkernel_sys.md @@ -10,7 +10,7 @@ - [fchmodat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `chmod`; any other relative path is joined to the descriptor's host directory the same way as for `openat`, so guest path aliases and case-insensitive name matching do not apply to it and a `..` component is not kept inside the guest root. A flag other than `0` or `AT_SYMLINK_NOFOLLOW` (0x200) returns `EINVAL`; the mode is masked to `07777`. With `AT_SYMLINK_NOFOLLOW` a symbolic link returns success silently on Windows and Linux, since the host cannot change a link's own mode, while a regular file is changed either way. The mode maps to the host file mode, which on Windows is only the CRT's read-only bit. PPSA99621 calls it through `std::filesystem::permissions`. Console behaviour is unverified - [pathconf](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) looks the path up as FreeBSD 11's `kern_pathconf` does (links followed, `ENAMETOOLONG` for a path of 1024 bytes or more or a component over 255) and returns the constants of FreeBSD 11's UFS `ufs_pathconf` for files and directories alike: `_PC_LINK_MAX` 32767, `_PC_NAME_MAX` 255, `_PC_PATH_MAX` and `_PC_SYMLINK_MAX` 1024, `_PC_PIPE_BUF` 512, `_PC_CHOWN_RESTRICTED` and `_PC_NO_TRUNC` 1, `_PC_FILESIZEBITS` 64, `_PC_REC_MAX_XFER_SIZE` -1, `_PC_PRIO_IO` and `_PC_SYNC_IO` 0. Whether the console's filesystems report the same is not verified. It throws for the names whose UFS value depends on the mount (`_PC_ALLOC_SIZE_MIN`, `_PC_REC_INCR_XFER_SIZE`, `_PC_REC_MIN_XFER_SIZE`, `_PC_MIN_HOLE_SIZE`), the kernel options (`_PC_ACL_EXTENDED`, `_PC_ACL_PATH_MAX`, `_PC_ACL_NFS4`, `_PC_MAC_PRESENT`) or the page size (`_PC_REC_XFER_ALIGN`), and on which UFS and `vop_stdpathconf` disagree (`_PC_MAX_CANON`, `_PC_MAX_INPUT`, `_PC_VDISABLE`, `_PC_ASYNC_IO`); other names fail with `EINVAL` - [mkdirat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `mkdir`. Any other relative path is joined to the descriptor's host directory, found the same way as for `fchdir`, so guest path aliases and case-insensitive name matching do not apply to it, and a `..` component is not kept inside the guest root. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory. On Windows, `mode` is ignored. Console behaviour is unverified -- [readlink](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) on Windows reads a symbolic link's target from its reparse data and returns it with `/` separators, only when the target is relative, as `symlink` creates them. A symbolic link with an absolute target throws, as a host path has no guest form. A directory junction returns its print name, or its substitute name when the print name is empty, without a `\??\` or `\\?\` prefix. Any other reparse point throws too, where FreeBSD reports `EINVAL` for a file that is not a link +- [readlink](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) on Windows reads a symbolic link's target from its reparse data and returns it with `/` separators, only when the target is relative, as `symlink` creates them. A symbolic link with an absolute target returns its print name, or its substitute name when the print name is empty, without a `\??\` or `\\?\` prefix. A directory junction returns the same kind of name when it is a drive path (`X:\...`), using the print name when that is a drive path and the substitute name otherwise. A mounted volume, whose substitute name is `\??\Volume{GUID}\`, stays a throw, because the name left after the prefix is not a drive path. Any other reparse point throws too, where FreeBSD reports `EINVAL` for a file that is not a link - [readlinkat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `readlink`. Any other relative path is joined to the descriptor's host directory, found the same way as for `fchdir`, so guest path aliases and case-insensitive name matching do not apply to it, and a `..` component is not kept inside the guest root. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory. Console behaviour is unverified - [unlinkat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `rmdir` (with `AT_REMOVEDIR`, 0x800) or `unlink`. Any other relative path is joined to the descriptor's host directory, found the same way as for `fchdir`, so guest path aliases and case-insensitive name matching do not apply to it, and a `..` component is not kept inside the guest root. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory. Removing a directory without `AT_REMOVEDIR` fails with `EPERM`, removing `.` or `..` with `AT_REMOVEDIR` fails with `EINVAL`, and flags other than `AT_REMOVEDIR` fail with `EINVAL`. Console behaviour is unverified - [renameat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `rename`. Any other relative path is joined to its descriptor's host directory, found the same way as for `fchdir`, so guest path aliases and case-insensitive name matching do not apply to relative descriptors, and a `..` component is not kept inside the guest root. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory. Console behaviour is unverified From 6491e12466617135b43e26cb955e3cb064e5f68d Mon Sep 17 00:00:00 2001 From: David <David.Samir.WD@gmail.com> Date: Sun, 18 Oct 2026 13:08:53 +0400 Subject: [PATCH 0973/1000] fix(shader): keep clip distances in the blocks between the barycentric tessellation stages #7330 passes the vertex clip distances as separate ClipDistance arrays so the control stage's input matches the vertex shader's output. SPIRV-Cross, which MoltenVK uses, cannot translate a separate ClipDistance array in a tessellation evaluation stage, so on MoltenVK every pipeline of the barycentric tessellation route failed to compile. The control stage now reads the separate array the vertex shader writes and passes it on inside its per-vertex block, which the evaluation stage reads and writes. --- .../src/BarycentricGeometryShader.cpp | 28 +++++++------------ 1 file changed, 10 insertions(+), 18 deletions(-) diff --git a/core/shader/recompiler/SpirvBackend/src/BarycentricGeometryShader.cpp b/core/shader/recompiler/SpirvBackend/src/BarycentricGeometryShader.cpp index daca4351b0..dffc7835fa 100644 --- a/core/shader/recompiler/SpirvBackend/src/BarycentricGeometryShader.cpp +++ b/core/shader/recompiler/SpirvBackend/src/BarycentricGeometryShader.cpp @@ -196,6 +196,9 @@ public: } perVertexType = builder.DecoratedType(spv::OpTypeStruct, {{spv::OpMemberDecorate, {0u, spv::DecorationBuiltIn, spv::BuiltInPosition}}, {spv::OpDecorate, {spv::DecorationBlock}}}, vec4FloatType); perVertexArrayType = builder.Type(spv::OpTypeArray, perVertexType, constant(uintType, 3u)); + stageVertexType = perVertexType; + if (clipDistances != 0u) stageVertexType = builder.DecoratedType(spv::OpTypeStruct, {{spv::OpMemberDecorate, {0u, spv::DecorationBuiltIn, spv::BuiltInPosition}}, {spv::OpMemberDecorate, {1u, spv::DecorationBuiltIn, spv::BuiltInClipDistance}}, {spv::OpDecorate, {spv::DecorationBlock}}}, vec4FloatType, clipArrayType); + stageVertexArrayType = builder.Type(spv::OpTypeArray, stageVertexType, constant(uintType, 3u)); ptrInputVec4 = builder.Type(spv::OpTypePointer, spv::StorageClassInput, vec4FloatType); ptrOutputVec4 = builder.Type(spv::OpTypePointer, spv::StorageClassOutput, vec4FloatType); } @@ -207,14 +210,11 @@ public: const auto invocationId = addInterface(spv::StorageClassInput, intType); decorate(invocationId, spv::DecorationBuiltIn, spv::BuiltInInvocationId); const auto glIn = addInterface(spv::StorageClassInput, perVertexArrayType); - const auto glOut = addInterface(spv::StorageClassOutput, perVertexArrayType); + const auto glOut = addInterface(spv::StorageClassOutput, stageVertexArrayType); std::uint32_t clipIn = 0u; - std::uint32_t clipOut = 0u; if (clipDistances != 0u) { clipIn = addInterface(spv::StorageClassInput, controlInputClipType); decorate(clipIn, spv::DecorationBuiltIn, spv::BuiltInClipDistance); - clipOut = addInterface(spv::StorageClassOutput, controlInputClipType); - decorate(clipOut, spv::DecorationBuiltIn, spv::BuiltInClipDistance); } const auto floatArray = [&](std::uint32_t size) { return builder.Type(spv::OpTypeArray, floatType, constant(uintType, size)); }; const auto outer = addInterface(spv::StorageClassOutput, floatArray(4u)); @@ -248,7 +248,7 @@ public: builder.AddFunction(spv::OpBranch, afterLevels); builder.AddFunction(spv::OpLabel, afterLevels); store(result(spv::OpAccessChain, ptrOutputVec4, glOut, invocation, constant(intType, 0u)), load(vec4FloatType, result(spv::OpAccessChain, ptrInputVec4, glIn, invocation, constant(intType, 0u)))); - if (clipDistances != 0u) store(result(spv::OpAccessChain, ptrControlOutputClip, clipOut, invocation), load(clipArrayType, result(spv::OpAccessChain, ptrControlInputClip, clipIn, invocation))); + if (clipDistances != 0u) store(result(spv::OpAccessChain, ptrControlOutputClip, glOut, invocation, constant(intType, 1u)), load(clipArrayType, result(spv::OpAccessChain, ptrControlInputClip, clipIn, invocation))); for (const auto& [input, output] : parameters) store(result(spv::OpAccessChain, ptrOutputVec4, output, invocation), load(vec4FloatType, result(spv::OpAccessChain, ptrInputVec4, input, invocation))); return finish(); } @@ -258,22 +258,13 @@ public: addDomain(); const auto tessCoord = addInterface(spv::StorageClassInput, vec3FloatType); decorate(tessCoord, spv::DecorationBuiltIn, spv::BuiltInTessCoord); - const auto glIn = addInterface(spv::StorageClassInput, perVertexArrayType); - std::uint32_t clipIn = 0u; - if (clipDistances != 0u) { - clipIn = addInterface(spv::StorageClassInput, controlInputClipType); - decorate(clipIn, spv::DecorationBuiltIn, spv::BuiltInClipDistance); - } + const auto glIn = addInterface(spv::StorageClassInput, stageVertexArrayType); std::array<std::uint32_t, ParameterLocations> inputs {}; for (const auto location : sources) { inputs[location] = addInterface(spv::StorageClassInput, vertexArrayType); decorate(inputs[location], spv::DecorationLocation, location); } - const auto glOut = addInterface(spv::StorageClassOutput, perVertexType); - if (clipDistances != 0u) { - evaluationClipOutput = addInterface(spv::StorageClassOutput, clipArrayType); - decorate(evaluationClipOutput, spv::DecorationBuiltIn, spv::BuiltInClipDistance); - } + const auto glOut = addInterface(spv::StorageClassOutput, stageVertexType); std::vector<std::uint32_t> passedOutputs; for (const auto location : passed) { passedOutputs.push_back(addInterface(spv::StorageClassOutput, vec4FloatType)); @@ -307,7 +298,7 @@ public: const auto weights = result(spv::OpCompositeConstruct, vec3FloatType, weight(0u), weight(1u), weight(2u)); store(result(spv::OpAccessChain, ptrOutputVec4, glOut, constant(intType, 0u)), load(vec4FloatType, result(spv::OpAccessChain, ptrInputVec4, glIn, index, constant(intType, 0u)))); - if (clipDistances != 0u) store(result(spv::OpAccessChain, ptrEvaluationOutputClip, evaluationClipOutput), load(clipArrayType, result(spv::OpAccessChain, ptrEvaluationInputClip, clipIn, index))); + if (clipDistances != 0u) store(result(spv::OpAccessChain, ptrEvaluationOutputClip, glOut, constant(intType, 1u)), load(clipArrayType, result(spv::OpAccessChain, ptrEvaluationInputClip, glIn, index, constant(intType, 1u)))); for (std::size_t i = 0; i < passed.size(); ++i) store(passedOutputs[i], load(vec4FloatType, result(spv::OpAccessChain, ptrInputVec4, inputs[passed[i]], passedFlat[i] ? constant(intType, 0u) : index))); if (smooth != 0u) store(smooth, weights); if (linear != 0u) store(linear, weights); @@ -341,7 +332,8 @@ private: std::uint32_t ptrControlOutputClip = 0; std::uint32_t ptrEvaluationInputClip = 0; std::uint32_t ptrEvaluationOutputClip = 0; - std::uint32_t evaluationClipOutput = 0; + std::uint32_t stageVertexType = 0; + std::uint32_t stageVertexArrayType = 0; void begin() { builder.EmitCapability(spv::CapabilityShader); From f982c1c5bdbae7b3d64ea7a9dcb2bc8acf7d30e2 Mon Sep 17 00:00:00 2001 From: David <David.Samir.WD@gmail.com> Date: Sun, 18 Oct 2026 13:08:53 +0400 Subject: [PATCH 0974/1000] test(agc): check where the barycentric tessellation stages keep clip distances --- core/libs/prx/libSceAgcDriver/tests/Graphics.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 1f3594c1c8..688567f66c 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -5962,7 +5962,8 @@ void barycentricEmulationTests() { return false; }; Require(!memberBuiltIn(patches.control, spv::BuiltInClipDistance) && !memberBuiltIn(patches.evaluation, spv::BuiltInClipDistance), "a generated tessellation stage declares clip distances the vertex shader does not write"); - Require(!memberBuiltIn(clippedPatches.control, spv::BuiltInClipDistance) && !memberBuiltIn(clippedPatches.evaluation, spv::BuiltInClipDistance) && variableBuiltIn(clippedPatches.control, spv::BuiltInClipDistance) && variableBuiltIn(clippedPatches.evaluation, spv::BuiltInClipDistance), "generated tessellation stages do not forward clip distances as separate built-in variables"); + Require(variableBuiltIn(clippedPatches.control, spv::BuiltInClipDistance) && memberBuiltIn(clippedPatches.control, spv::BuiltInClipDistance), "the generated control stage does not read the vertex clip distances as a separate built-in variable and pass them on in its per-vertex block"); + Require(memberBuiltIn(clippedPatches.evaluation, spv::BuiltInClipDistance) && !variableBuiltIn(clippedPatches.evaluation, spv::BuiltInClipDistance), "the generated evaluation stage does not keep the clip distances in its per-vertex blocks"); const std::array<CompiledShader, 4> clippedShaders{{{ShaderStage::Vertex, &clipped, 0}, {ShaderStage::TessellationControl, &clippedPatches.control, 0}, {ShaderStage::TessellationEvaluation, &clippedPatches.evaluation, 0}, {ShaderStage::Fragment, &fragment, 0}}}; ValidateShaders(clippedShaders, state, subgroup, false, false, false, false, false, false, true); vertex.parameterExports.clear(); From debf6ee684805fab1fb3dca9b18d2e8a2fa2d2e5 Mon Sep 17 00:00:00 2001 From: David <David.Samir.WD@gmail.com> Date: Sun, 18 Oct 2026 13:08:53 +0400 Subject: [PATCH 0975/1000] docs(shader): say how the barycentric tessellation stages pass clip distances --- docs/dev/debt/shader.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index c567f99417..12e6133a6d 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -85,7 +85,7 @@ - [Mesh shader grid size](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) (shader recompiler) - every mesh shader loads `NumWorkgroups.x` and declares no vertices when it is 0, which no dispatch produces. The read is a workaround for RADV (Mesa 26.2.4, seen on an RX 9070 XT, GFX12): binding a mesh pipeline whose shader reads neither `NumWorkgroups` nor `DrawIndex` keeps the user SGPR count and draw-id flag of the vertex pipeline bound before it in the same command buffer (`radv_bind_pre_rast_shader` updates them only when the shader has the `AC_UD_VS_BASE_VERTEX_START_INSTANCE` slot), so `radv_emit_userdata_mesh` opens a 3-register `SET_SH_REG` and writes one value, the CP takes the following `DISPATCH_MESH_DIRECT` as register data and the GPU hangs (gfx ring timeout, MODE1 reset). Kena: Bridge of Spirits (PPSA01802) hits it on its layered volume draws. Reading `NumWorkgroups` gives every mesh shader the grid-size slot, so the count and the values match; drop the read once a fixed Mesa is the minimum - [Shader recompilation](../../../core/shader/recompiler/Recompiler.cpp) currently occurs right before it was transferred to Vulkan with caching, but should be moved to the [relinker](../../../core/relinker/main.cpp) stage. For this purpose, [shader/recompiler](../../../core/shader/recompiler) was written completely independently from [libs/prx](../../../core/libs/prx). - [v_interp_p1ll_f16, v_interp_p1lv_f16 and v_interp_p2_f16](../../../core/shader/recompiler/Translation/src/AttributeInstructions.cpp) read the 16-bit halves of each vertex's parameter through `VK_KHR_fragment_shader_barycentric`, as fixed-function interpolation cannot split a 32-bit attribute. Without the extension the pixel shader takes the [barycentric emulation](../../../core/shader/recompiler/SpirvBackend/src/BarycentricGeometryShader.cpp), whose generated geometry stage passes each vertex's parameter to it as a flat input, so the halves arrive bit for bit; a device without geometry shaders takes the tessellation stages of the same emulation instead, which no 16-bit interpolation test has run through yet. P10 and P20 are the vertex differences rounded to f16 to nearest even, which is not confirmed on hardware. v_interp_p1ll_f16 and v_interp_p1lv_f16 round the exact `P10 * I + P0` once to f32 and v_interp_p2_f16 rounds the exact `P20 * J + S2` once to f16, as measured on RDNA2 (#697); the exact sums are computed in f64, so these instructions need the Float64 capability. They follow the shader's float mode, measured on RDNA2 (gfx1035) in all eight combinations of `IEEE_MODE` and kept/flushed f32 and f16 denormals over 768 special and 1,024 random rows. With IEEE=1 a NaN operand is returned quieted; with IEEE=0 it is returned unquieted, except that v_interp_p2_f16's f16 result is always quiet. Flushed f32 denormals flush I/J, v_interp_p2_f16's f32 S2 and the f32 result; flushed f16 denormals flush P0, P10/P20, v_interp_p1lv_f16's f16 S2 and the f16 result. Denormal modes 1 and 2 (flush only inputs or only outputs) and rounding other than to nearest even are not measured and throw. An unknown mode follows IEEE=0 with denormals kept. Lavapipe has no barycentrics, so the draw test is skipped in CI; `agc_driver_interpolation_modes` checks the arithmetic in every mode against the hardware rows on any device with Float64. -- Without `VK_KHR_fragment_shader_barycentric`, a pixel shader whose I/J VGPRs [only feed `v_interp_p1_f32`/`v_interp_p2_f32` of their own pair](../../../core/shader/recompiler/Optimization/src/HostInterpolationChecker.cpp) reads the attribute Vulkan interpolated. Any other read of them or of a raw vertex parameter takes the weights and the three vertex values from a [generated geometry stage](../../../core/shader/recompiler/SpirvBackend/src/BarycentricGeometryShader.cpp), which needs `geometryShader` and a vertex shader before it. On a device with `tessellationShader` but no `geometryShader` (MoltenVK), a triangle list without primitive restart takes them from [generated tessellation stages](../../../core/shader/recompiler/SpirvBackend/src/BarycentricGeometryShader.cpp) instead: each triangle is one three-point patch at tessellation level 1, the evaluation stage writes the corner `gl_TessCoord` points at as the weights, and the vertex clip distances are forwarded. A flat parameter takes the first vertex's value there, as the geometry stage's strip gives it; the last provoking vertex throws. The patch keeps the triangle's facing with `VertexOrderCw`, checked with face culling on MoltenVK 1.4.2 (Apple M2) only. Tessellation, mesh and rect-list pipelines, primitives other than triangles, triangle strips and fans without `geometryShader`, more than 32 fragment input locations and interfaces beyond the device's geometry or tessellation limits throw. +- Without `VK_KHR_fragment_shader_barycentric`, a pixel shader whose I/J VGPRs [only feed `v_interp_p1_f32`/`v_interp_p2_f32` of their own pair](../../../core/shader/recompiler/Optimization/src/HostInterpolationChecker.cpp) reads the attribute Vulkan interpolated. Any other read of them or of a raw vertex parameter takes the weights and the three vertex values from a [generated geometry stage](../../../core/shader/recompiler/SpirvBackend/src/BarycentricGeometryShader.cpp), which needs `geometryShader` and a vertex shader before it. On a device with `tessellationShader` but no `geometryShader` (MoltenVK), a triangle list without primitive restart takes them from [generated tessellation stages](../../../core/shader/recompiler/SpirvBackend/src/BarycentricGeometryShader.cpp) instead: each triangle is one three-point patch at tessellation level 1, the evaluation stage writes the corner `gl_TessCoord` points at as the weights, and the vertex clip distances are forwarded: the control stage reads them as the separate `ClipDistance` array the vertex shader writes, so the vertex-to-control interfaces match, and passes them on inside its per-vertex block, which the evaluation stage reads and writes. MoltenVK 1.4.2 (SPIRV-Cross) cannot translate a separate `ClipDistance` array in a tessellation evaluation stage (`no member named 'gl_ClipDistance' in 'main0_out'`), so the two generated stages keep it in their blocks. A flat parameter takes the first vertex's value there, as the geometry stage's strip gives it; the last provoking vertex throws. The patch keeps the triangle's facing with `VertexOrderCw`, checked with face culling on MoltenVK 1.4.2 (Apple M2) only. Tessellation, mesh and rect-list pipelines, primitives other than triangles, triangle strips and fans without `geometryShader`, more than 32 fragment input locations and interfaces beyond the device's geometry or tessellation limits throw. - [v_fma_f64 and v_div_fmas_f64](../../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterFloat64.cpp) with a subnormal operand or result round once, as IEEE 754 fusedMultiplyAdd does with f64 denormals kept, measured on RDNA2 (gfx1035) at IEEE=0 and IEEE=1, `v_div_fmas_f64` with VCC clear and set. The f16/f64 denormal field of `FLOAT_MODE` (bits 7:6) is not read: when it flushes, the hardware flushes the operands and the result of `v_fma_f64` (tininess before rounding) and only the result of `v_div_fmas_f64`, while these keep them. Shaders with f64 instructions declare `DenormPreserve` for 64-bit floats only when `shaderDenormPreserveFloat64` is true and Vulkan reports `denormBehaviorIndependence` as `ALL`; other independence modes are not handled by the current backend because they require coupled execution modes. See [VkShaderFloatControlsIndependence](https://docs.vulkan.org/refpages/latest/refpages/source/VkShaderFloatControlsIndependence.html). Elsewhere (including lavapipe), the driver may flush f64 subnormals, and `agc_driver_f64_fma_subnormals` is skipped. - [SINT storage images](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) of 8- and 16-bit components saturate each stored 32-bit value to the component range, as adriadam10 measured for image stores on RDNA2 (gfx1035) in #944 (448 rows match); the PS5's gfx1013 is not measured. A D16 store sign-extends each 16-bit half before that saturation, as measured on RDNA2 (gfx1035) for `image_store d16` to all eight SINT formats with DMASK 0xf, 0x7, 0x5, 0x3, 0x2, 0xa and 0x1 (1792 rows match). - [`StoreTexel`](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) writes channel 1 of a two-channel format from the data slot whose DST_SEL is Y, for SINT and UINT alike. With DST_SEL X,Y,Z,W, gfx1035 writes it from the W slot, as adriadam10 measured in #944. From cb28abeb89b5260f3824932f265c3b1cc9801e63 Mon Sep 17 00:00:00 2001 From: Crimson <144462348+CrimsonMac@users.noreply.github.com> Date: Sun, 18 Oct 2026 12:31:11 +0300 Subject: [PATCH 0976/1000] fix(shader): fault on misaligned 64-bit device-memory LDS atomics --- .../libSceAgcDriver/tests/LdsLockBudget.cpp | 7 +- .../tests/execution/LdsMixedWidthAtomics.cpp | 102 +++++++++++++++++- .../Optimization/src/ShaderInfoCollector.cpp | 6 ++ .../SpirvMemory/SpirvMemoryInstructions.cpp | 4 +- docs/dev/debt/shader.md | 2 +- 5 files changed, 115 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/LdsLockBudget.cpp b/core/libs/prx/libSceAgcDriver/tests/LdsLockBudget.cpp index e69551e405..d409fc1d07 100644 --- a/core/libs/prx/libSceAgcDriver/tests/LdsLockBudget.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/LdsLockBudget.cpp @@ -60,13 +60,17 @@ std::uint64_t workgroupBytes(std::span<const std::uint32_t> words) { std::set<std::uint32_t> compareExchangeWidths(std::span<const std::uint32_t> words) { std::map<std::uint32_t, std::uint32_t> widths; std::set<std::uint32_t> found; + std::uint32_t faultFunction = 0; + bool inFaultFunction = false; for (std::size_t offset = 5; offset < words.size();) { const auto count = words[offset] >> 16u; require(count != 0u && count <= words.size() - offset, "malformed SPIR-V instruction"); const auto instruction = words.subspan(offset, count); const auto opcode = static_cast<spv::Op>(instruction[0] & 0xffffu); + if (opcode == spv::OpName && std::string_view(reinterpret_cast<const char*>(instruction.data() + 2)) == "record_bda_fault") faultFunction = instruction[1]; + if (opcode == spv::OpFunction) inFaultFunction = instruction[2] == faultFunction; if (opcode == spv::OpTypeInt) widths.emplace(instruction[1], instruction[2]); - if (opcode == spv::OpAtomicCompareExchange) found.insert(widths.at(instruction[1])); + if (opcode == spv::OpAtomicCompareExchange && !inFaultFunction) found.insert(widths.at(instruction[1])); offset += count; } return found; @@ -110,6 +114,7 @@ void verify(std::uint32_t waveSize, std::uint32_t limit, bool int64Atomics) { const bool native = int64Atomics && limit < 68u; require(widths.contains(64u) == native, native ? "device-memory LDS did not use a 64-bit compare exchange" : "a 64-bit compare exchange appeared without device-memory LDS and shaderBufferInt64Atomics"); require(widths.contains(32u) != native, native ? "device-memory LDS with shaderBufferInt64Atomics kept the workgroup lock" : "the 64-bit LDS atomic lost its workgroup lock"); + require(result.bdaAbiVersion == (limit < 68u ? 1u : 0u), "the fault buffer for a misaligned 64-bit LDS atomic does not follow device-memory LDS at limit " + std::to_string(limit)); } } diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/LdsMixedWidthAtomics.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/LdsMixedWidthAtomics.cpp index f63631ee61..1f8032e93c 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/LdsMixedWidthAtomics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/LdsMixedWidthAtomics.cpp @@ -14,12 +14,61 @@ #include <string_view> #include <unordered_map> #include <vector> +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#include <windows.h> +#include <io.h> +#else +#include <unistd.h> +#endif namespace { using AgcDriver::Graphics::Require; using ShaderRecompiler::ShaderStage; +class StderrCapture { +public: + StderrCapture() { + std::fflush(stderr); + file = std::tmpfile(); + Require(file != nullptr, "cannot open a temporary file for stderr"); +#ifdef _WIN32 + saved = _dup(_fileno(stderr)); + _dup2(_fileno(file), _fileno(stderr)); +#else + saved = dup(fileno(stderr)); + dup2(fileno(file), fileno(stderr)); +#endif + } + std::string Finish() { + std::fflush(stderr); +#ifdef _WIN32 + _dup2(saved, _fileno(stderr)); + _close(saved); +#else + dup2(saved, fileno(stderr)); + close(saved); +#endif + std::rewind(file); + std::string text; + char buffer[4096]; + for (std::size_t read = 0; (read = std::fread(buffer, 1, sizeof(buffer), file)) != 0;) text.append(buffer, read); + std::fclose(file); + std::fwrite(text.data(), 1, text.size(), stderr); + return text; + } + +private: + std::FILE* file = nullptr; + int saved = -1; +}; + constexpr std::uint32_t Threads = 256; constexpr std::uint32_t Iterations = 16; constexpr std::uint32_t Repeats = 32; @@ -167,6 +216,32 @@ alignas(256) constexpr auto LoadedParityCode = Unrolled<1>( 0xbf810000, })); +alignas(256) constexpr auto MisalignedCode = Unrolled<1>( + std::to_array<std::uint32_t>({ + 0x34100083, + 0x7e020280, + 0x7e0402c1, + 0x7e060280, + 0x7e080281, + 0x7e0c0280, + 0x7e0e0280, + 0xd9340000, 0x00000601, + 0xbf8cc07f, + 0xbf8a0000, + }), + std::to_array<std::uint32_t>({ + 0xd9000004, 0x00000201, + 0xbf8cc07f, + }), + std::to_array<std::uint32_t>({ + 0xbf8cc07f, + 0xbf8a0000, + 0xd9d80000, 0x0a000001, + 0xbf8cc07f, + 0xe0741000, 0x80010a08, + 0xbf810000, + })); + std::array<std::uint32_t, 4> BufferDescriptor(const void* data, std::uint32_t bytes) { const auto address = reinterpret_cast<std::uintptr_t>(data); return {static_cast<std::uint32_t>(address), static_cast<std::uint32_t>((address >> 32u) & 0xffffu), bytes, 0x31016facu}; @@ -213,7 +288,7 @@ std::uint64_t WorkgroupBytes(std::span<const std::uint32_t> words) { return total; } -void Run(AgcDriver::VulkanDevice& device, std::span<const std::uint32_t> code, const char* name, std::uint64_t operations, std::uint32_t repeats, std::uint32_t ldsDwords = LdsDwords, std::optional<std::uint32_t> expectedDeviceDwords = std::nullopt) { +ShaderRecompiler::RecompileResult Compile(AgcDriver::VulkanDevice& device, std::span<const std::uint32_t> code, std::uint32_t ldsDwords) { std::vector<std::uint32_t> userData(8, 0u); const auto input = BufferDescriptor(Input.data(), static_cast<std::uint32_t>(Input.size() * 4u)); const auto output = BufferDescriptor(Output.data(), static_cast<std::uint32_t>(Output.size() * 4u)); @@ -228,10 +303,14 @@ void Run(AgcDriver::VulkanDevice& device, std::span<const std::uint32_t> code, c {0, 0, 0, 128} }; request.useCache = false; - const auto result = ShaderRecompiler::Recompile(request); + return ShaderRecompiler::Recompile(request); +} + +void Run(AgcDriver::VulkanDevice& device, std::span<const std::uint32_t> code, const char* name, std::uint64_t operations, std::uint32_t repeats, std::uint32_t ldsDwords = LdsDwords, std::optional<std::uint32_t> expectedDeviceDwords = std::nullopt) { + const auto result = Compile(device, code, ldsDwords); if (expectedDeviceDwords) { const auto bytes = WorkgroupBytes(result.spirv.Words()); - const auto limit = request.target.maxWorkgroupSharedMemoryBytes; + const auto limit = device.Target().maxWorkgroupSharedMemoryBytes; Require(bytes <= limit, "lds lock budget: guest LDS and lock exceed the device limit before dispatch"); Require(result.workgroupMemoryDwords == *expectedDeviceDwords, "lds lock budget: unexpected device-backed LDS allocation before dispatch"); if (*expectedDeviceDwords == 0u) Require(bytes == limit, "lds lock budget: exact-fit case did not retain its lock"); @@ -246,6 +325,21 @@ void Run(AgcDriver::VulkanDevice& device, std::span<const std::uint32_t> code, c if (expectedDeviceDwords) std::printf("lds lock budget: %s GPU readback verified across 256 lanes\n", name); } +void Misaligned(AgcDriver::VulkanDevice& device, std::uint32_t ldsDwords) { + const auto result = Compile(device, MisalignedCode, ldsDwords); + StderrCapture capture; + try { + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast<std::uintptr_t>(MisalignedCode.data())); + device.WaitIdle(); + } catch (...) { + static_cast<void>(capture.Finish()); + throw; + } + const auto log = capture.Finish(); + Require(log.find("BDA access failed: address=0x4 instruction=0x2c bytes=8 ") != std::string::npos && log.find("reason=7") != std::string::npos, "lds lock budget: misaligned ds_add_u64 on device-backed LDS did not fault"); + std::puts("lds lock budget: misaligned ds_add_u64 faulted"); +} + void LockBudget(AgcDriver::VulkanDevice& device) { const auto target = device.Target(); const auto limit = target.maxWorkgroupSharedMemoryBytes; @@ -254,6 +348,8 @@ void LockBudget(AgcDriver::VulkanDevice& device) { Require(limit >= 32u && limit % 4u == 0u, "lds lock budget: unexpected device shared-memory limit"); const auto operations = static_cast<std::uint64_t>(Threads) * Iterations; Run(device, UniformCode, "limit", operations, 1, limit / 4u, (limit / 4u + 2u) & ~1u); + const auto& capabilities = target.supportedCapabilities; + if (std::find(capabilities.begin(), capabilities.end(), static_cast<std::uint32_t>(spv::CapabilityInt64Atomics)) != capabilities.end()) Misaligned(device, limit / 4u); Run(device, UniformCode, "limit-4", operations, 1, (limit - 4u) / 4u, 0u); std::puts("lds lock budget tests passed"); } diff --git a/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp b/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp index fac95cb5d4..80871f8ea9 100644 --- a/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp +++ b/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp @@ -535,6 +535,12 @@ void ShaderInfoCollector::Collect(IrProgram& program, const ShaderStageInputInfo return inst->Opcode() == IrOpcode::BitwiseXor32; }); }); + const bool ldsAtomic64 = std::ranges::any_of(program.Blocks(), [](const auto& block) { + return std::ranges::any_of(block->Instructions(), [](const IrValue* inst) { + return SharedAccessOf(inst->Opcode()) == SharedAccess::Atomic && inst->Type() == IrType::U64; + }); + }); + next.usesFaultBuffer = next.usesFaultBuffer || (next.sharedMemoryBytes != 0u && ldsAtomic64); switch (program.Resources().stage) { case IrShaderStage::Vertex: case IrShaderStage::Local: diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp index 417ce2c9de..85cb5aa621 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp @@ -1394,7 +1394,9 @@ std::uint32_t SharedAtomic64(SpirvValueEmitContext& ctx, const IrValue& inst, TR const auto high = EmitMemoryElementIndex(state, resource, Binary(state, spv::OpIAdd, TypeU32(state), rawIndex, ConstantU32(state, 1u))); return EmitValueOrDefaultIfCondition(state, EmitMemoryElementInBounds(state, resource, high), TypeU64(state), ConstantU64(state, 0u), [&]() { if (state.requirements.ldsBufferInt64Atomics) { - const auto aligned = Binary(state, spv::OpIEqual, TypeBool(state), Binary(state, spv::OpBitwiseAnd, TypeU32(state), low, ConstantU32(state, 1u)), ConstantU32(state, 0u)); + const auto unaligned = Binary(state, spv::OpINotEqual, TypeBool(state), Binary(state, spv::OpBitwiseAnd, TypeU32(state), low, ConstantU32(state, 1u)), ConstantU32(state, 0u)); + EmitIfCondition(state, unaligned, [&] { RecordBdaFaultWords(state, Binary(state, spv::OpShiftLeftLogical, TypeU32(state), rawIndex, ConstantU32(state, 2u)), ConstantU32(state, 0u), ConstantU32(state, 8u), ConstantU32(state, inst.Flags<MemoryFlags>().pc), BdaAbi::FaultReason::Unaligned); }); + const auto aligned = Unary(state, spv::OpLogicalNot, TypeBool(state), unaligned); return EmitValueOrDefaultIfCondition(state, aligned, TypeU64(state), ConstantU64(state, 0u), [&]() { return LdsBufferAtomic64(state, low, [&](std::uint32_t old) { return replacement(state, old); }); }); } const auto update = [&]() { diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index 3f58b62d0a..85a3ad935c 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -129,7 +129,7 @@ - [Negative color-texture comparison wrap indices](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) (libSceAgcDriver) use explicit `OpSRem` normalization to implement modulo; the negative-offset point-wrap regression fails with direct `OpSMod` and passes with normalization on Mesa 22.3.6 llvmpipe (LLVM 15.0.6), but negative-index wrap has not been measured on PS5 hardware. - [Converted unorm images](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) (10_11_11_UNORM, read and written through an R32_UINT view): samples, gathers, LOD queries, 16-bit data and a DST_SEL of the missing W channel throw. - [Converted unorm image loads](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) divide by 2^n - 1 with the buffer format path's `OpFDiv`, which Vulkan allows to be up to 2.5 ULP off, so some drivers are not bit-exact. -- [64-bit LDS atomics](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (`ds_*_u64`, `ds_*_b64`, `ds_*_f64`) read and write the two dwords under a workgroup lock stored after the guest LDS, as LDS is a 32-bit array without 64-bit atomics. On RDNA2 (gfx1036) the LDS serialises a 32-bit atomic on a slot's high dword and a 64-bit atomic on that slot: 128 lanes of `ds_add_u32` and 128 lanes of `ds_add_u64` on one slot sum exactly. Only that pair was measured. Every 32-bit LDS atomic of a program that contains a 64-bit LDS atomic therefore takes the same lock, including a 32-bit `flat_atomic_*` whose address lands in the LDS aperture (a 64-bit one takes it already); the flat forms were not measured against a 64-bit atomic either (the other 32-bit LDS atomics, `ds_min/max`, `ds_cmpst`, the float forms, `ds_inc/dec`, `ds_mskor` and `ds_wrxchg`, are extrapolated from `ds_add_u32` and were not measured against a 64-bit atomic); a program without a 64-bit LDS atomic keeps its native 32-bit atomics. `ds_condxchg32_rtn_b64` is two `ds_mskor` halves, each under the lock but not atomic as a pair, so a 64-bit atomic between the two halves is not excluded. Sub-dword writes (`ds_write_b8`, `ds_write_b16` and their `_d16_hi` forms) and plain `ds_read_*`, `ds_write_*`, `ds_append_u32` and `ds_consume_u32` are not taken under the lock, so a plain access racing a 64-bit atomic on the same dwords can see or overwrite one half of it; whether the hardware orders them was not measured. The lock serialises the workgroup's LDS atomics of such a program; the throughput cost was not measured. `VK_KHR_workgroup_memory_explicit_layout` with `shaderSharedInt64Atomics` would make both widths native and is not used. A 64-bit atomic at an address that is 4-byte but not 8-byte aligned faults the wave on gfx1036, as on gfx1035. When the LDS lives in device memory (compute LDS beyond the device's shared-memory limit) and the device has `shaderBufferInt64Atomics`, no lock is used: a 64-bit atomic is a compare-exchange loop on a 64-bit view of the storage buffer and a 32-bit atomic is a native buffer atomic, so a mixed-width pair on one slot relies on the device ordering both widths on one buffer address; only `ds_add_u32` against `ds_add_u64` was measured, on gfx1036 (AMD Windows driver 32.0.21042.62) and an RTX 4070 SUPER. The lock is not used there because AMD's Windows driver compiles an acquire or release on a storage buffer, at workgroup scope or wider, to `s_barrier`, and those barriers inside the lock's divergent loop pair with the program's own `s_barrier`s. The fence after each LDS atomic compiles to `s_barrier` on that driver as well, so a wave that skips an LDS atomic that other waves of its workgroup execute misses a barrier they reach; its effect was not measured. On that path a 64-bit atomic at an address that is 4-byte but not 8-byte aligned is skipped and returns 0. +- [64-bit LDS atomics](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (`ds_*_u64`, `ds_*_b64`, `ds_*_f64`) read and write the two dwords under a workgroup lock stored after the guest LDS, as LDS is a 32-bit array without 64-bit atomics. On RDNA2 (gfx1036) the LDS serialises a 32-bit atomic on a slot's high dword and a 64-bit atomic on that slot: 128 lanes of `ds_add_u32` and 128 lanes of `ds_add_u64` on one slot sum exactly. Only that pair was measured. Every 32-bit LDS atomic of a program that contains a 64-bit LDS atomic therefore takes the same lock, including a 32-bit `flat_atomic_*` whose address lands in the LDS aperture (a 64-bit one takes it already); the flat forms were not measured against a 64-bit atomic either (the other 32-bit LDS atomics, `ds_min/max`, `ds_cmpst`, the float forms, `ds_inc/dec`, `ds_mskor` and `ds_wrxchg`, are extrapolated from `ds_add_u32` and were not measured against a 64-bit atomic); a program without a 64-bit LDS atomic keeps its native 32-bit atomics. `ds_condxchg32_rtn_b64` is two `ds_mskor` halves, each under the lock but not atomic as a pair, so a 64-bit atomic between the two halves is not excluded. Sub-dword writes (`ds_write_b8`, `ds_write_b16` and their `_d16_hi` forms) and plain `ds_read_*`, `ds_write_*`, `ds_append_u32` and `ds_consume_u32` are not taken under the lock, so a plain access racing a 64-bit atomic on the same dwords can see or overwrite one half of it; whether the hardware orders them was not measured. The lock serialises the workgroup's LDS atomics of such a program; the throughput cost was not measured. `VK_KHR_workgroup_memory_explicit_layout` with `shaderSharedInt64Atomics` would make both widths native and is not used. A 64-bit atomic at an address that is 4-byte but not 8-byte aligned faults the wave on gfx1036, as on gfx1035. When the LDS lives in device memory (compute LDS beyond the device's shared-memory limit) and the device has `shaderBufferInt64Atomics`, no lock is used: a 64-bit atomic is a compare-exchange loop on a 64-bit view of the storage buffer and a 32-bit atomic is a native buffer atomic, a misaligned 64-bit atomic records the `Unaligned` shader fault with the LDS byte address instead of updating, so a mixed-width pair on one slot relies on the device ordering both widths on one buffer address; only `ds_add_u32` against `ds_add_u64` was measured, on gfx1036 (AMD Windows driver 32.0.21042.62) and an RTX 4070 SUPER. The lock is not used there because AMD's Windows driver compiles an acquire or release on a storage buffer, at workgroup scope or wider, to `s_barrier`, and those barriers inside the lock's divergent loop pair with the program's own `s_barrier`s. The fence after each LDS atomic compiles to `s_barrier` on that driver as well, so a wave that skips an LDS atomic that other waves of its workgroup execute misses a barrier they reach; its effect was not measured. On that path a 64-bit atomic at an address that is 4-byte but not 8-byte aligned is skipped and returns 0. - [LDS float atomics](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (`ds_min/max/cmpst_f32`, `ds_min/max/cmpst_f64` and their `_rtn` forms) follow the hardware measured on RDNA2 (gfx1036) over 14 x 14 operand sets (zeros, ones, infinities, denormals, quiet and signalling NaNs of both signs and with payloads) in IEEE 0 and 1 with every value 0-3 of the type's denormal field: both operands are flushed to a signed zero when bit 0 of the field is clear (0 and 2; 1 and 3 keep them; the f32 field for the f32 forms, the f16/f64 field for the f64 forms; a shader without a known float mode flushes f32 and keeps f64), then a signalling-NaN data operand is stored quieted, else a signalling-NaN old value is stored quieted, else a quiet-NaN old value is replaced by the data, else a quiet-NaN data operand loses, else the ordered compare with -0 below +0 decides, and the flushed value is what is written, also when the old value wins. `cmpst` compares the flushed bits (both zeros equal, NaN never equal) and leaves memory unchanged when they differ. The returned value is always the old value as it was in memory. `IEEE_MODE` has no effect. The same bit rule serves the buffer float atomics (`AtomicFloatBits`). Not measured: a denormal or signalling-NaN `data1` of `cmpst` (stored as given). `ds_add_f32` and `ds_add_rtn_f32` follow RDNA1 (gfx1010, RX 5700 XT), measured with the [hardware oracle](../HW_ORACLE.md) template run through the Windows driver's comgr and HIP runtime over 1574 operand pairs (signed zeros, denormals, the smallest normals, infinities, quiet and signalling NaNs with payloads, the largest normals, random pairs near the denormal range and pairs that nearly cancel) in all 16 combinations of the f32 denormal field and the f32 round mode, and at IEEE 1 and DX10 clamp 0: both operands are flushed to a signed zero when bit 0 of the field is clear and the result when bit 1 is clear, the sum is rounded to nearest even whatever the round mode, a NaN data operand is stored quieted, else a NaN old value is stored quieted, `+inf + -inf` stores 0xffc00000, and `IEEE_MODE` and `DX10_CLAMP` have no effect. The sum is the host's `OpFAdd` between those flushes, so where the field keeps f32 denormals the result also depends on the host preserving them, which the module does not request. thuanquang measured the same rule on RDNA2 (gfx1031, RX 6700 XT) in #5492, with the same operand classes and 18 modes in wave32 and wave64; the PS5's gfx1013 is not measured. - [LDS in pixel, vertex-path and fetch programs](../../../core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp) (shader recompiler) - is an array private to each host invocation, in `Function` storage, as only compute and mesh programs get workgroup memory; on the console one LDS region is shared by the wave (RDNA 2 ISA sections 3.6.6 and 10.4), so a lane does not see another lane's LDS writes. Sub-dword writes (`ds_write_b8`, `ds_write_b16` and their `_d16_hi` forms), the 32-bit LDS atomics, `ds_append` and `ds_consume` load, update and store the invocation's own dword without an atomic, as Vulkan allows atomics only on `Uniform`, `Workgroup`, `Image`, `StorageBuffer`, `PhysicalStorageBuffer` and `TaskPayloadWorkgroupEXT` pointers (VUID-StandaloneSpirv-None-04686). `ds_append` and `ds_consume` update only the first active lane's copy and return its old value to every lane, so a second append after the first active lane changes, or a read of the counter by another lane, differs from the hardware. Not measured on hardware. - [V# bases off a DWORD boundary](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (shader recompiler) - raw accesses use the exact bytes and `s_buffer_load` reads `(base & ~3) + (offset & ~3)`, as measured on gfx1035 (Linux, alignment mode UNALIGNED; the console's mode is unknown). Formatted accesses off their element's alignment throw. Atomics use the byte address `base + offset`, base's low two bits included: measured on RDNA2 (gfx1031, Radeon RX 6700 XT, AMD Windows driver 32.0.21045.5002 through HIP) with `buffer_atomic_add`, `buffer_atomic_swap` and `buffer_atomic_cmpswap` and bases 0 to 3 bytes past a DWORD boundary with offsets 0 to 7, an address on a DWORD boundary updates that DWORD and returns its old value, and any other address stops the dispatch, which the recompiler reports as an unaligned access fault. From 061101a3e88241ce2c05247319fa3ad966cf7875 Mon Sep 17 00:00:00 2001 From: 011010 <husari09.oro@gmail.com> Date: Sun, 18 Oct 2026 04:16:04 -0600 Subject: [PATCH 0977/1000] fix(tools): report a row the pull request removes --- tools/check_conventions.py | 35 +++++++++++++++++++++-------------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/tools/check_conventions.py b/tools/check_conventions.py index 6d5edeeab8..5bb45523c1 100644 --- a/tools/check_conventions.py +++ b/tools/check_conventions.py @@ -30,7 +30,7 @@ RULES = { "commit-subject": ("error", "docs/dev/CONVENTIONS.md", "Commit subject is not Conventional Commits"), "pr-title": ("error", "docs/dev/CONVENTIONS.md", "Pull request title is not Conventional Commits"), "pr-template": ("error", ".github/pull_request_template.md", "Fill in the pull request template"), - "env-reference": ("error", "docs/dev/ENVIRONMENT.md", "Every APS5_* or ANYPS5_* name a pull request adds to the code has a row in docs/dev/ENVIRONMENT.md, and a row whose name the pull request stops reading is removed"), + "env-reference": ("error", "docs/dev/ENVIRONMENT.md", "Every APS5_* or ANYPS5_* name a pull request adds to the code has a row in docs/dev/ENVIRONMENT.md, a row whose name the pull request stops reading is removed, and a row the pull request removes is no longer read"), } CPP = {".c", ".cc", ".cpp", ".cxx", ".h", ".hh", ".hpp", ".hxx", ".inl", ".ipp"} @@ -279,21 +279,26 @@ class Check: def env_reference(self): doc = "docs/dev/ENVIRONMENT.md" - added, removed = {}, set() - path, number = "", 0 - for line in git("diff", "-U0", self.base, self.head, "--", "core").splitlines(): - if line.startswith("+++ b/"): - path = line[6:] - elif line.startswith("+++") or line.startswith("---"): - continue + added, removed, dropped = {}, set(), set() + path, number, in_hunk = "", 0, False + for line in git("diff", "-U0", self.base, self.head, "--", "core", doc).splitlines(): + if line.startswith("diff --git"): + path, in_hunk = "", False elif line.startswith("@@"): number = int(re.search(r"\+(\d+)", line).group(1)) - 1 - elif line.startswith("+"): - number += 1 - for _, match in env_reference.line_reads(line[1:]): - added.setdefault(match.group("name"), (path, number)) - elif line.startswith("-"): - removed.update(match.group("name") for _, match in env_reference.line_reads(line[1:])) + in_hunk = True + elif in_hunk: + if path == doc: + if line.startswith("-"): + dropped.update(env_reference.documented_pattern(env_reference.PREFIXES).findall(line[1:])) + elif line.startswith("+"): + number += 1 + for _, match in env_reference.line_reads(line[1:]): + added.setdefault(match.group("name"), (path, number)) + elif line.startswith("-"): + removed.update(match.group("name") for _, match in env_reference.line_reads(line[1:])) + elif line.startswith("+++ b/"): + path = line[6:] result = subprocess.run(["git", "grep", "-nE", env_reference.GREP, self.head, "--", "core"], capture_output=True, encoding="utf-8", errors="replace") reads = set() @@ -306,6 +311,8 @@ class Check: for name in sorted(removed - reads): if name in documented: self.report("env-reference", doc, 0, f"{name} has a row but the code no longer reads it") + for name in sorted(dropped & reads - documented): + self.report("env-reference", doc, 0, f"{name} is still read by the code but this pull request removes its row") def pull_request(self, title, body): if not CONVENTIONAL.match(title): From 99e295a7bab56fb21429894662dbdded83cf2f2b Mon Sep 17 00:00:00 2001 From: 011010 <husari09.oro@gmail.com> Date: Sun, 18 Oct 2026 04:16:05 -0600 Subject: [PATCH 0978/1000] test(tools): register and extend the environment rule tests --- CMakeLists.txt | 3 ++ .../test_check_conventions_env_reference.py | 37 +++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 91efaf77e3..e652e28cff 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -198,6 +198,9 @@ if(BUILD_TESTING) add_test(NAME env_reference COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_env_reference.py) set_tests_properties(env_reference PROPERTIES TIMEOUT 20) + add_test(NAME conventions_env_reference COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_env_reference.py) + set_tests_properties(conventions_env_reference PROPERTIES TIMEOUT 60) add_test(NAME conventions_body_bom COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_body_bom.py) add_test(NAME conventions_blob_reads COMMAND ${Python3_EXECUTABLE} diff --git a/tools/tests/test_check_conventions_env_reference.py b/tools/tests/test_check_conventions_env_reference.py index 5794513b90..0ccd9bf93c 100644 --- a/tools/tests/test_check_conventions_env_reference.py +++ b/tools/tests/test_check_conventions_env_reference.py @@ -106,6 +106,43 @@ class EnvReferenceDiffTests(unittest.TestCase): head = {SAMPLE: base[SAMPLE] + "int unrelated;\n"} self.assertEqual(self.run_case(base, head), []) + def test_a_row_the_pull_request_removes_is_reported_while_the_code_still_reads_it(self): + base = { + DOC: DOC_TEXT, + SAMPLE: 'static bool a = std::getenv("APS5_DOCUMENTED") != nullptr;\n', + } + head = {DOC: "| `APS5_GONE` | presence |\n"} + self.assertEqual(self.run_case(base, head), [("env-reference", DOC, 0, + "APS5_DOCUMENTED is still read by the code but this pull request removes its row")]) + + def test_a_removed_row_is_kept_quiet_when_the_code_stops_reading_it_too(self): + base = { + DOC: DOC_TEXT, + SAMPLE: 'static bool a = std::getenv("APS5_DOCUMENTED") != nullptr;\n' + 'static bool b = std::getenv("APS5_GONE") != nullptr;\n', + } + head = { + DOC: "| `APS5_DOCUMENTED` | presence |\n", + SAMPLE: 'static bool a = std::getenv("APS5_DOCUMENTED") != nullptr;\n', + } + self.assertEqual(self.run_case(base, head), []) + + def test_an_added_line_starting_with_plus_does_not_shift_the_reported_line(self): + base = {DOC: DOC_TEXT, SAMPLE: "int a;\n"} + head = {SAMPLE: "int a;\n+++x;\n" 'static bool b = std::getenv("APS5_NEW_READ") != nullptr;\n'} + findings = self.run_case(base, head) + self.assertEqual(findings, [("env-reference", SAMPLE, 3, + f"APS5_NEW_READ is read here but has no row in {DOC}")]) + + def test_a_removed_line_starting_with_minus_still_counts_as_a_removed_read(self): + base = { + DOC: DOC_TEXT, + SAMPLE: '--x; std::getenv("APS5_GONE") != nullptr;\n', + } + head = {SAMPLE: "int unrelated;\n"} + self.assertEqual(self.run_case(base, head), [("env-reference", DOC, 0, + "APS5_GONE has a row but the code no longer reads it")]) + if __name__ == "__main__": unittest.main() From f07df07969e5dd93ec381ffb57f455b8c965dd10 Mon Sep 17 00:00:00 2001 From: pushkarreddyy <212133856+pushkarreddyy@users.noreply.github.com> Date: Sat, 17 Oct 2026 20:25:01 +0530 Subject: [PATCH 0979/1000] feat(libSceAgcDriver): support polygon primitive type 21 --- .../src/Driver/Draw/DrawPrecheck.cpp | 2 +- .../libSceAgcDriver/Graphics/src/State.cpp | 12 +++++--- .../prx/libSceAgcDriver/tests/Graphics.cpp | 29 ++++++++++++++++--- docs/dev/debt/libSceAgcDriver.md | 1 + 4 files changed, 35 insertions(+), 9 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPrecheck.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPrecheck.cpp index ebee83d0d4..a6787296d4 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPrecheck.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPrecheck.cpp @@ -47,7 +47,7 @@ std::optional<DrawVerdict> Driver::precheckDraw(const QueueState& queue, const S const auto primitive = queue.userConfig.find(0x242); const std::uint32_t allOnes = drawParameters.indexSize == 2 ? 0xffffu : 0xffffffffu; const auto type = primitive == queue.userConfig.end() ? 0u : primitive->second & 0x3fu; - const bool strip = type == 3 || type == 5 || type == 6; + const bool strip = type == 3 || type == 5 || type == 6 || type == 21; const bool list = type == 1 || type == 2 || type == 4; const auto restartDevice = device.Load(); if (!strip && !(list && restartDevice != nullptr && restartDevice->PrimitiveListRestart())) { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index 66c444520c..440a0f22bf 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -92,7 +92,7 @@ VkConservativeRasterizationModeEXT decodeConservativeRasterization(const QueueSt requireConservativeTriangles((domain == 1u || domain == 2u) && (topology == 2u || topology == 3u), topology == 0u ? "points" : topology == 1u || domain == 0u ? "lines" : "other primitive types", "VGT_TF_PARAM", parameters); } else { const auto primitive = read(queue.userConfig, 0x242, RegisterBank::UserConfig); - requireConservativeTriangles(primitive == 4u || primitive == 5u || primitive == 6u, primitive == 1u ? "points" : primitive == 2u || primitive == 3u ? "lines" : primitive == 7u || primitive == 17u ? "rectangles" : "other primitive types", "VGT_PRIMITIVE_TYPE", primitive); + requireConservativeTriangles(primitive == 4u || primitive == 5u || primitive == 6u || primitive == 21u, primitive == 1u ? "points" : primitive == 2u || primitive == 3u ? "lines" : primitive == 7u || primitive == 17u ? "rectangles" : "other primitive types", "VGT_PRIMITIVE_TYPE", primitive); } Require((read(cx, 0x1b3) & 0x44u) == 0, "conservative rasterization with centroid interpolation is unsupported"); return VK_CONSERVATIVE_RASTERIZATION_MODE_OVERESTIMATE_EXT; @@ -118,6 +118,7 @@ std::uint32_t inactiveVertexPointSize(const QueueState& queue) { case 6: case 7: case 17: + case 21: return 1u << 16u; default: return 0; @@ -401,7 +402,7 @@ std::string nonTriangleRejection(const QueueState& queue, const char* message, b const auto topology = (*parameters >> 5u) & 7u; if ((domain != 1u && domain != 2u) || (topology != 2u && topology != 3u)) return zeroMessage(0x2db, *parameters, message); } - } else if (const auto primitive = word(queue.userConfig, 0x242, RegisterBank::UserConfig); primitive && *primitive != 4u && *primitive != 5u && *primitive != 6u && !(rectangles && (*primitive == 7u || *primitive == 17u))) { + } else if (const auto primitive = word(queue.userConfig, 0x242, RegisterBank::UserConfig); primitive && *primitive != 4u && *primitive != 5u && *primitive != 6u && *primitive != 21u && !(rectangles && (*primitive == 7u || *primitive == 17u))) { return zeroMessage(0x242, *primitive, message); } return {}; @@ -1039,7 +1040,10 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) { break; case 9: result.topology = VK_PRIMITIVE_TOPOLOGY_PATCH_LIST; break; case 4: result.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; break; - case 5: result.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN; break; + case 5: + case 21: + result.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN; + break; case 6: result.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP; break; default: throw std::runtime_error("AGC graphics: unsupported primitive type " + std::to_string(primitive)); } @@ -1733,7 +1737,7 @@ std::string DrawRejection(const QueueState& queue, bool indexed) { if (indexed && value(queue.userConfig, 0x24b, word) && word != 0) { std::uint32_t primitive = 0; std::uint32_t resetIndex = 0; - if (value(queue.userConfig, 0x242, primitive) && (primitive & 0x3fu) != 1 && (primitive & 0x3fu) != 2 && (primitive & 0x3fu) != 3 && (primitive & 0x3fu) != 4 && (primitive & 0x3fu) != 5 && (primitive & 0x3fu) != 6) return "AGC graphics: primitive restart is only supported for point, line and triangle topologies"; + if (value(queue.userConfig, 0x242, primitive) && (primitive & 0x3fu) != 1 && (primitive & 0x3fu) != 2 && (primitive & 0x3fu) != 3 && (primitive & 0x3fu) != 4 && (primitive & 0x3fu) != 5 && (primitive & 0x3fu) != 6 && (primitive & 0x3fu) != 21) return "AGC graphics: primitive restart is only supported for point, line and triangle topologies"; if (value(cx, 0x103, resetIndex) && (resetIndex & 0xffffu) != 0xffffu) return "AGC graphics: primitive restart index other than all ones is unsupported"; } if (auto reason = nonzero(cx, 0x207, ~(LayerExports | ClipCullExports | inactiveVertexPointSize(queue)), "layer, viewport or auxiliary vertex exports"); !reason.empty()) return reason; diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 1f3594c1c8..54ac32b25a 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -117,7 +117,7 @@ void inactiveVertexPointSizeTests() { queue.context[0x207] = 0x01210000; return queue; }; - for (const auto primitive : {2u, 4u, 5u, 6u, 7u, 17u}) { + for (const auto primitive : {2u, 4u, 5u, 6u, 7u, 17u, 21u}) { auto queue = pointSizeState(); queue.userConfig[0x242] = primitive; for (const auto raster : {0u, 0x240u, 0x248u}) { @@ -194,6 +194,23 @@ void inactiveVertexPointSizeTests() { rejected(queue); } +void PolygonPrimitiveTests() { + auto queue = makeState(); + queue.userConfig[0x242] = 21; + queue.context[0x1b3] = 2; + queue.context[0x1b4] = 2; + const auto state = AgcDriver::Graphics::DecodeState(queue); + Require(state.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN, "a polygon did not decode to a triangle fan"); + Require(!state.primitiveRestart, "polygon decoding enabled primitive restart by default"); + Require(AgcDriver::Graphics::RasterPrimitiveOf(state) == ShaderRecompiler::RasterPrimitive::Triangle, "a polygon was not classified as a triangle primitive"); + Require(AgcDriver::Graphics::DrawRejection(queue, false).empty(), "the precheck rejected a direct polygon draw"); + queue.userConfig[0x24b] = 1; + queue.context[0x103] = 0xffffffffu; + Require(AgcDriver::Graphics::DrawRejection(queue, true).empty(), "the precheck rejected an indexed polygon draw with restart"); + const auto indexedState = AgcDriver::Graphics::DecodeState(queue); + Require(indexedState.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN && indexedState.primitiveRestart, "indexed polygon decoding did not keep primitive restart"); +} + void retainedPixelStageTests() { using namespace AgcDriver::Graphics; auto queue = makeState(); @@ -315,6 +332,9 @@ void stateTests() { queue.context[0x103] = 0xffffffffu; Require(AgcDriver::Graphics::DrawRejection(queue, true).empty(), "primitive restart was rejected for an indexed strip"); Require(AgcDriver::Graphics::DecodeState(queue).primitiveRestart, "primitive restart was not decoded for a strip"); + queue.userConfig[0x242] = 21; + Require(AgcDriver::Graphics::DrawRejection(queue, true).empty(), "primitive restart was rejected for an indexed polygon"); + Require(AgcDriver::Graphics::DecodeState(queue).primitiveRestart, "primitive restart was not decoded for a polygon"); queue.context[0x103] = 5; Require(AgcDriver::Graphics::DrawRejection(queue, true).find("all ones") != std::string::npos, "a restart index other than all ones was accepted"); queue = makeState(); @@ -726,10 +746,10 @@ void MsaaEnableSingleSampleTests() { queue = enabled(); queue.userConfig[0x242] = 1; Require(DrawRejection(queue, false).find("primitives other than triangles") != std::string::npos, "the precheck accepted MSAA_ENABLE on a single-sample target for points"); - for (const auto primitive : {5u, 6u}) { + for (const auto primitive : {5u, 6u, 21u}) { queue = enabled(); queue.userConfig[0x242] = primitive; - Require(DecodeState(queue).samples == 1 && DrawRejection(queue, false).empty(), "MSAA_ENABLE on a single-sample target was refused for a triangle fan or strip"); + Require(DecodeState(queue).samples == 1 && DrawRejection(queue, false).empty(), "MSAA_ENABLE on a single-sample target was refused for a triangle fan, strip or polygon"); } queue = enabled(); queue.context[0x292] = 7; @@ -7095,7 +7115,7 @@ void ConservativeRasterizationTests() { queue.context[0x1b4] = 2; Require(AgcDriver::Graphics::DecodeState(queue).conservativeRasterization == VK_CONSERVATIVE_RASTERIZATION_MODE_DISABLED_EXT, "PA_SC_CONSERVATIVE_RASTERIZATION_CNTL 0x6000 enabled conservative rasterization"); queue.context[0x313] = 0x6001; - for (const auto primitive : {4u, 5u, 6u}) { + for (const auto primitive : {4u, 5u, 6u, 21u}) { queue.userConfig[0x242] = primitive; Require(AgcDriver::Graphics::DecodeState(queue).conservativeRasterization == VK_CONSERVATIVE_RASTERIZATION_MODE_OVERESTIMATE_EXT, "OVER_RAST_ENABLE did not decode to overestimation for primitive type " + std::to_string(primitive)); Require(AgcDriver::Graphics::DrawRejection(queue, false).empty(), "the precheck rejected OVER_RAST_ENABLE"); @@ -8745,6 +8765,7 @@ int main(int argc, char** argv) { ShaderStageTests(); NggVertexWorkgroupTests(); inactiveVertexPointSizeTests(); + PolygonPrimitiveTests(); TuningFieldTests(); PixelInputLayoutTests(); UnexportedPixelInputTests(); diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 49c7ebc7f3..bc49a0d84a 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -46,6 +46,7 @@ ### Functional - [Texture cache budgets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - the storage image cache's budget is what the reported heap budget leaves after the heap's use other than the cached storage images, the unused part of the sampled textures' 2 GiB floor and an eighth of the budget, capped at a quarter of the budget and at `TextureCacheBudget` (a quarter of the heap's size, at least 2 GiB), never below 512 MiB; the sampled cache's budget reserves `TextureCacheBudget` for the storage cache. So the sampled budget falls first while the sampled textures alive stay within their floor, and both fall together above it; each cache trims only at its next insert, a storage image is allocated before its insert reads the budget, and both budgets read the heap at most once a second, so a burst between two reads can still exceed the reported budget. The 2 GiB floor and the eighth are the sampled formula's values reused and the 512 MiB floor and the caps the earlier ones; none was measured beyond one title (PPSA11386) on one 10 GB GPU, and a title whose storage working set exceeds the shrunk budget re-creates storage images at each use. +- [Polygon primitive type](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - `VGT_PRIMITIVE_TYPE` 21 (`PrimitiveType::Polygon`) decodes to `VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN` and is treated as inactive for vertex point size, like triangle fans and strips. PPSA01317 (Gran Turismo 7) submits polygon draws; whether the console performs polygon-mode decomposition or custom vertex routing is unverified. - [Native multisampled DCC fast clears](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) import uniform 0000/0001/1110/1111 keys into a single-layer, level-zero SW_64KB_R_X color image with 2, 4 or 8 fragments. Color and DCC/CMASK ranges must stay write-watched and unchanged; expanded or mixed initial DCC, register CMASK clears, compressed payloads, color transforms, lossy modes, metadata changes, guest publication and writes over a live owner throw. Driver-mediated reads of the color and metadata stay guarded. The metadata extent follows [AMD Addrlib](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/gfx10/gfx10addrlib.cpp#L347-L454) with the [PS5 Mesa model](https://github.com/mihawk-99/PS5_Mesa/blob/0b2d6d1a61d9bbf89cf8beb88a696144f67c61f8/src/amd/vulkan/winsys/ps5/radv_ps5_winsys.c): GFX10.1, 16 pipes, 256-byte interleave, two maximum compressed fragments. Console geometry, concurrent guest mutation and transitions outside this subset are unverified. - [Initial stencil state](../../../core/libs/prx/libSceAgcDriver/Execution/include/QueueState.hpp) uses zero for `DB_STENCIL_CONTROL`, `DB_STENCILREFMASK` and `DB_STENCILREFMASK_BF` (context `0x10b`-`0x10d`), including after `CLEAR_STATE`. The public V0, V4, V5 and V7-V11 [register-default tables](../../../core/libs/prx/libSceAgc/Command/include/RegisterDefaultsData.hpp) supply these values. Linux's [GFX10 clear-state table](https://github.com/torvalds/linux/blob/master/drivers/gpu/drm/amd/amdgpu/clearstate_gfx10.h) agrees on the control register but uses `0x01000000` for the reference masks; the console reset values have not been measured. Planet Coaster (PPSA01736 v01.017) enables stencil tests without writing the control register. From c3c400a6f8cf4d608f1bc439cd4cf218266fd94a Mon Sep 17 00:00:00 2001 From: pushkarreddyy <212133856+pushkarreddyy@users.noreply.github.com> Date: Sat, 17 Oct 2026 16:30:15 +0530 Subject: [PATCH 0980/1000] fix(libSceAgcDriver): normalize reserved bit 30 in guest buffer descriptors --- .../Graphics/src/ShaderResources.cpp | 5 ++--- .../libs/prx/libSceAgcDriver/tests/Graphics.cpp | 2 +- .../tests/execution/BufferDescriptorRuntime.cpp | 17 ++++++++++------- docs/dev/debt/libSceAgcDriver.md | 1 + 4 files changed, 14 insertions(+), 11 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index 36d2c9aa87..a6e271eb2d 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -3800,7 +3800,7 @@ DescriptorCache::Stats DescriptorCache::Counters() const { } std::uint64_t ShaderResources::guestBufferBytes(std::span<const std::uint32_t> words) const { - const ShaderRecompiler::ShaderBufferResource descriptor{{words[0], words[1], words[2], words[3]}}; + const ShaderRecompiler::ShaderBufferResource descriptor{{words[0], words[1] & ~0x40000000u, words[2], words[3]}}; const auto address = descriptor.Base48(); auto byteSize = descriptor.GetSize(); if (byteSize > context.limits.maxStorageBufferRange) { @@ -3823,8 +3823,7 @@ std::uint64_t ShaderResources::guestBufferBytes(std::span<const std::uint32_t> w std::size_t ShaderResources::addGuestBuffer(std::span<const std::uint32_t> words, const ColorTarget* target, std::uint64_t indexAddress, std::size_t indexBytes, bool written, bool atomic, bool read) { Require(words.size() == 4, "buffer descriptor must contain four DWORDs"); - Require((words[1] & 0x40000000u) == 0, "buffer descriptor has reserved bits set"); - const ShaderRecompiler::ShaderBufferResource descriptor{{words[0], words[1], words[2], words[3]}}; + const ShaderRecompiler::ShaderBufferResource descriptor{{words[0], words[1] & ~0x40000000u, words[2], words[3]}}; Require(descriptor.Type() == 0u, "buffer descriptor uses an unsupported type"); const auto address = descriptor.Base48(); const auto byteSize = guestBufferBytes(words); diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 54ac32b25a..fc3f825871 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -4889,7 +4889,7 @@ void resourceTests() { expectSingleFailure(changed([](auto& binding) { binding.role = Role::ShaderData; }), "invalid compact shader data size"); expectSingleFailure(changed([](auto& binding) { binding.role = Role::ShaderData; binding.guestDescriptor = ShaderDataWords({}); binding.guestDescriptor.pop_back(); }), "invalid compact shader data size"); expectSingleFailure(changed([](auto& binding) { binding.role = Role::FlattenedSrt; binding.guestDescriptor.clear(); }), "empty shader data descriptor"); - expectSingleFailure(changed([](auto& binding) { binding.guestDescriptor[1] |= 0x40000000u; }), "reserved bits"); + expectSingleAccepted(changed([](auto& binding) { binding.guestDescriptor[1] |= 0x40000000u; }), "a V# with reserved bit 30"); expectSingleFailure(changed([](auto& binding) { binding.guestDescriptor[3] |= 0x40000000u; }), "unsupported type"); expectSingleAccepted(changed([](auto& binding) { binding.guestDescriptor[1] |= 0x3fffu << 16u; binding.guestDescriptor[2] = 0xffffffffu; }), "an unregistered V# past the range limit"); expectSingleAccepted(changed([](auto& binding) { binding.guestDescriptor[2] = 32768; }), "an unregistered V# past the range limit"); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/BufferDescriptorRuntime.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/BufferDescriptorRuntime.cpp index 7ca17147aa..51fa4f8630 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/BufferDescriptorRuntime.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/BufferDescriptorRuntime.cpp @@ -136,16 +136,19 @@ void ScalarReads(AgcDriver::VulkanDevice& device) { request.shader.code = code; request.shader.codeAddress = reinterpret_cast<std::uintptr_t>(code.data()); auto input = BufferDescriptor(Input.data(), InputBytes); + std::copy(input.begin(), input.end(), userData.begin()); + Output.fill(Fill); + const auto baselineResult = ShaderRecompiler::Recompile(request); + device.Dispatch(baselineResult, 1, 1, 1); + device.WaitIdle(); + const auto baseline = Output; input[1] |= 0x40000000u; std::copy(input.begin(), input.end(), userData.begin()); + Output.fill(Fill); const auto result = ShaderRecompiler::Recompile(request); - try { - device.Dispatch(result, 1, 1, 1); - } catch (const std::runtime_error& error) { - Require(std::string(error.what()).find("buffer descriptor has reserved bits set") != std::string::npos, "vector or mixed buffer descriptor failed for another reason"); - continue; - } - throw std::runtime_error("a vector or mixed buffer access accepted reserved descriptor bits"); + device.Dispatch(result, 1, 1, 1); + device.WaitIdle(); + Require(Output == baseline, "vector or mixed buffer read changed with reserved bit 30 set"); } } diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index bc49a0d84a..a891c5e68e 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -27,6 +27,7 @@ - [NGG vertex programs on the mesh path](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - an unregistered vertex program with `PRIMGEN_EN` that uses LDS or `s_barrier` runs on the geometry (mesh) path. Each input primitive is assumed to yield at most one output primitive, the subgroup is bounded by `GE_MAX_OUTPUT_PER_SUBGROUP` as a vertex budget, and the GS/ES VGPR count checks are skipped, because the merged inputs keep their positions. Taken from the code Mesa's ACO emits for NGG culling and from PPSA59322 and PPSA99010 draws, not measured on hardware. Registered programs keep the vertex path - [VGT_SHADER_STAGES_EN 0x02002008](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - this one value, `ES_EN` = 1 without `HS_EN` on a non-patch draw, is drawn as the vertex path. AMD's CIK register reference calls `ES_EN` = 1 `ES_STAGE_DS` (the ES stage runs the tessellation evaluation shader), which does not explain it on a non-patch draw. Seen only on PPSA29413 and not verified on the console; other vertex-path values with `ES_EN` set still throw. - [Strided buffer overhang](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - a strided buffer descriptor that ends less than one stride past the end of the readable registered allocation holding its base is bound only up to that allocation's end, even when another readable allocation starts right there. This assumes the bytes past the end are the rounded-up `num_records` of the last record (RADV rounds it up), which no fetch inside the buffer reads; a guest that really keeps a vertex split across two adjacent allocations would read zeros for the cut tail. Taken from the RetroArch Dolphin core case in #2912, not measured on hardware. +- [Buffer descriptor CACHE_SWIZZLE hint](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - bit 30 of word 1 in a buffer resource descriptor (`words[1] & 0x40000000u`) is normalized to zero when binding guest buffers in `ShaderResources::addGuestBuffer`. On GFX10 (gfx1013), bit 30 of V# word 1 is `CACHE_SWIZZLE` (Mesa's `gfx10-rsrc.json`: STRIDE [29:16], CACHE_SWIZZLE [30], SWIZZLE_ENABLE [31]), a cache-bank hint with no effect on the address or data, which is dropped here. - [Buffer descriptors past the range limit](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - a buffer V# whose size is over `maxStorageBufferRange` is bound only up to the end of the readable registered allocation holding its base when that allocation ends before the limit, and as the empty placeholder buffer, like a null V#, when no registered allocation holds its base; a V# over an allocation that reaches the limit still throws. Titles leave such words in descriptor slots their shaders do not reach on the path taken (PPSA10528 binds `c8010002 2c6bec20 c0053c00 04000013`, about 33 TB from an address above the user address space). What the console returns for an access past the bound range is not known; here the device's robust buffer access decides. Follows the review of #3202, not measured on hardware. - [Zero-stride vertex fetch range](../../../core/libs/prx/libSceAgcDriver/Graphics/include/VertexInput.hpp) (libSceAgcDriver) - for a stride-0 V# with OOB_SELECT 2 the whole fetch reads memory unless NUM_RECORDS is 0; with OOB_SELECT 3 it reads zero when NUM_RECORDS is 0 and throws when the fetch only partly fits. Rules taken from the GFX10 OOB_SELECT table in Mesa [ac_descriptors.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/src/amd/common/ac_descriptors.c), not measured on hardware. Whether OOB_SELECT 3 checks each dword of a multi-dword fetch separately is unknown. OOB_SELECT 0 and 1 keep the structured record checks. - [Resource registration](../../../core/libs/prx/libSceAgcDriver/Resource/src/Registration.cpp) (libSceAgcDriver) - registration always fails with `RESOURCE_REGISTRATION_UNAVAILABLE`, so the owner and resource queries, `sceAgcDriverSetResourceUserData`, `sceAgcDriverFindResourcesPublic` and `sceAgcDriverUnregisterAllResourcesForOwner` return the same error without reading their arguments. Their parameter lists follow the libSceGnmDriver counterparts and are unverified. `sceAgcDriverGetResourceRegistrationMaxNameLength` writes 0xfc and returns 0, as [prosper](https://github.com/mattias800/prosper) does from a title's registration configuration; the `uint32_t*` parameter matches the SDK wrapper `sce::Agc::ResourceRegistration::getMaxNameLength(unsigned int*)`. sharpemu writes 256 and KytyPS5 returns `RESOURCE_REGISTRATION_UNAVAILABLE`, so the value is unverified; a null output throws From 49ad10b771213a69ed4a816fd22d0f07a8af0248 Mon Sep 17 00:00:00 2001 From: Dextroid17 <217507631+Dextroid17@users.noreply.github.com> Date: Wed, 14 Oct 2026 16:15:26 -0400 Subject: [PATCH 0981/1000] feat(agc): sample 8 and 8_8 sRGB textures through an RGBA8 sRGB copy A device that cannot sample R8_SRGB or R8G8_SRGB had its sRGB textures decoded in the shader, which cannot serve filtered samples: the decode must come before the filter. The texture is now an RGBA8 UNORM image viewed as RGBA8 sRGB, filled by a one-to-one blit of the R8 or RG8 texels (a snapshot's upload, or a storage image's texels refreshed when its Version changes), so the sampler decodes and then filters, and every sample, gather and load of these images is an ordinary sRGB one. The recompiler is given the decode mask only for formats the device cannot blit, or when APS5_SRGB_SHADER_DECODE=1 forces the shader decode. Tests run on lavapipe only. --- .../Execution/src/VulkanDevice.cpp | 5 +- .../Graphics/include/Context.hpp | 1 + .../Graphics/include/Texture.hpp | 6 + .../Graphics/include/TextureFormat.hpp | 2 + .../Graphics/src/ShaderResources.cpp | 14 +- .../libSceAgcDriver/Graphics/src/Texture.cpp | 167 ++++++++- .../Graphics/src/TextureFormat.cpp | 20 ++ .../tests/execution/ImageSrgbSampleCopy.cpp | 319 ++++++++++++++++++ 8 files changed, 526 insertions(+), 8 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/ImageSrgbSampleCopy.cpp diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index bdbb94ebe8..2fc64d1995 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -324,6 +324,7 @@ struct VulkanDevice::State { bool pipelineExecutableInfo = false; bool maintenance8 = false; std::uint32_t srgbDecodeFormats = 0; + std::uint32_t srgbCopyFormats = 0; bool storageImageWithoutFormat = false; VkSampleCountFlags sampleLocationSampleCounts = 0; VkSampleCountFlags sampleLocationGridSampleCounts = 0; @@ -1111,6 +1112,7 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : state->properties = properties.properties; state->physical = selected; state->srgbDecodeFormats = Graphics::SrgbDecodeFormats(state->InstanceFunction<PFN_vkGetPhysicalDeviceFormatProperties>("vkGetPhysicalDeviceFormatProperties"), selected); + state->srgbCopyFormats = Graphics::SrgbCopyFormats(state->InstanceFunction<PFN_vkGetPhysicalDeviceFormatProperties>("vkGetPhysicalDeviceFormatProperties"), selected, state->srgbDecodeFormats); if ((state->subgroup.supportedOperations & VK_SUBGROUP_FEATURE_BASIC_BIT) != 0) { state->capabilities.push_back(spv::CapabilityGroupNonUniform); if ((state->subgroup.supportedOperations & VK_SUBGROUP_FEATURE_BALLOT_BIT) != 0) state->capabilities.push_back(spv::CapabilityGroupNonUniformBallot); @@ -3506,7 +3508,7 @@ ShaderRecompiler::SpirvTarget VulkanDevice::buildTarget() const { target.fixedPushSlots = state->graphicsPipelineLibrary; target.registrationModules = state->graphicsPipelineLibrary; target.robustBufferAccess2 = state->robustBufferAccess2; - target.srgbDecodeFormats = state->srgbDecodeFormats; + target.srgbDecodeFormats = state->srgbDecodeFormats & ~state->srgbCopyFormats; target.vertexSubgroups = (state->subgroup.supportedStages & VK_SHADER_STAGE_VERTEX_BIT) != 0; if (state->meshShader) { const auto& mesh = state->meshLimits; @@ -3647,6 +3649,7 @@ Graphics::Context VulkanDevice::buildContext() const { context.imageViewMinLod = state->imageViewMinLod; context.pipelineExecutableInfo = state->pipelineExecutableInfo; context.srgbDecodeFormats = state->srgbDecodeFormats; + context.srgbCopyFormats = state->srgbCopyFormats; context.sampleLocationSampleCounts = state->sampleLocationSampleCounts; context.sampleLocationGridSampleCounts = state->sampleLocationGridSampleCounts; context.memoryProperties2 = state->memoryBudget ? state->InstanceFunction<PFN_vkGetPhysicalDeviceMemoryProperties2>("vkGetPhysicalDeviceMemoryProperties2") : nullptr; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp index 8eaada9e84..d5f3cd7fee 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp @@ -229,6 +229,7 @@ struct Context { bool graphicsPipelineLibrary = false; bool pipelineLinkTimeOptimization = true; std::uint32_t srgbDecodeFormats = 0; + std::uint32_t srgbCopyFormats = 0; bool provokingVertexLast = false; bool provokingVertexModePerPipeline = false; bool singlePassStorage = false; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp index 344d5ee592..68869367e8 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp @@ -66,6 +66,9 @@ public: Texture(const Context& context, VkImage depthImage, VkFormat depthFormat, VkImageAspectFlags aspect, VkComponentMapping components, VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_2D, std::uint32_t baseLayer = 0, std::shared_ptr<OwnedImage> owner = {}); struct DepthCopyTag {}; Texture(const Context& context, const std::shared_ptr<StorageTexture>& source, const GuestTextureResource& descriptor, VkComponentMapping components, DepthCopyTag); + struct SrgbExpandTag {}; + Texture(const Context& context, const std::shared_ptr<StorageTexture>& source, const GuestTextureResource& descriptor, VkComponentMapping components, SrgbExpandTag); + static bool CanSrgbExpandFrom(const Context& context, const StorageTexture& source, const GuestTextureResource& descriptor); static VkFormat DepthCopyFormat(VkFormat colorFormat); static bool CanDepthCopyFrom(const StorageTexture& source, const GuestTextureResource& descriptor); struct BlockCopyTag {}; @@ -117,6 +120,7 @@ public: private: void release() noexcept; bool refreshBlockCopy(); + bool refreshSrgbExpand(); void createFirstLayerView(const GuestTextureResource& descriptor, VkImageViewCreateInfo viewInfo); void prepareBaseLevelView(const GuestTextureResource& descriptor, VkImageViewCreateInfo viewInfo); void prepareSrgbView(const GuestTextureResource& descriptor, VkImageViewCreateInfo viewInfo); @@ -147,6 +151,8 @@ private: std::unique_ptr<CommandBatch> upload; PerUseCopy perUseCopy = PerUseCopy::None; bool copiedOnce = false; + bool srgbExpand = false; + GuestTextureResource sourceDescriptor{}; std::uint64_t copiedVersion = 0; VkFormat depthFormat = VK_FORMAT_UNDEFINED; VkFormat blockFormat = VK_FORMAT_UNDEFINED; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp index ff9eaf11df..9271395ec1 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp @@ -16,6 +16,8 @@ VkFormat ResolveTextureFormat(std::uint32_t guestFormat); VkComponentSwizzle TextureComponentChannel(std::uint32_t guestFormat, VkComponentSwizzle component); std::uint32_t SrgbDecodeFormats(PFN_vkGetPhysicalDeviceFormatProperties formatProperties, VkPhysicalDevice physical); VkFormat SampledTextureFormat(const Context& context, std::uint32_t guestFormat); +std::uint32_t SrgbCopyFormats(PFN_vkGetPhysicalDeviceFormatProperties formatProperties, VkPhysicalDevice physical, std::uint32_t decodeFormats); +bool SrgbExpandsToRgba8(const Context& context, std::uint32_t guestFormat); bool IsSrgbTextureFormat(std::uint32_t guestFormat); std::optional<std::uint32_t> FindGuestTextureFormat(VkFormat format, std::uint32_t elementBytes); std::optional<std::uint32_t> FindGuestColorTargetFormat(VkFormat format, std::uint32_t elementBytes); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index 36d2c9aa87..a08d68a71b 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -636,10 +636,11 @@ std::shared_ptr<Texture> cachedTexture(const Context& context, std::span<const s // the GPU supplies the texture by a view of it; anything else needs those results in guest // memory first. const bool depthCopy = depthCompare && guestBytes >= DepthCopyMinBytes() && Texture::DepthCopyFormat(ResolveTextureFormat(resource.format)) != VK_FORMAT_UNDEFINED && resource.dimension != TextureDimension::k3D; + const bool srgbExpand = !depthCompare && SrgbExpandsToRgba8(context, resource.format); auto source = StorageTexture::FindPending(address, guestBytes); - const bool blockCopy = source != nullptr && !depthCompare && Texture::CanBlockCopyFrom(*source, resource); + const bool blockCopy = source != nullptr && !depthCompare && !srgbExpand && Texture::CanBlockCopyFrom(*source, resource); const bool repackCopy = source != nullptr && !depthCompare && Texture::CanRepackCopyFrom(*source, resource); - if (source != nullptr && !blockCopy && !repackCopy && (depthCompare ? !depthCopy || !Texture::CanDepthCopyFrom(*source, resource) : !Texture::CanCopyFrom(*source, resource))) source.reset(); + if (source != nullptr && !blockCopy && !repackCopy && (depthCompare ? !depthCopy || !Texture::CanDepthCopyFrom(*source, resource) : srgbExpand ? !Texture::CanSrgbExpandFrom(context, *source, resource) : !Texture::CanCopyFrom(*source, resource))) source.reset(); if (source != nullptr && MetadataMoved(*source, resource)) source.reset(); std::optional<DccKeys> keys; bool clearThroughKeys = false; @@ -671,6 +672,7 @@ std::shared_ptr<Texture> cachedTexture(const Context& context, std::span<const s if (*keys == DccKeys::Uncompressed) { source = sampledStorageSource(context, resource, guestBytes); if (source != nullptr && depthCompare && !Texture::CanDepthCopyFrom(*source, resource)) source.reset(); + if (source != nullptr && srgbExpand && !Texture::CanSrgbExpandFrom(context, *source, resource)) source.reset(); } else if (!depthCompare && ClearedViewEnabled() && StorageClearAvailable(context, resource.format, *keys)) { // A fast-cleared surface is viewed as well, through its image cleared on the GPU (the // lookup's Refresh, see StorageTexture::upload), when the image's own descriptor names @@ -731,6 +733,11 @@ std::shared_ptr<Texture> cachedTexture(const Context& context, std::span<const s texture->RefreshDepthCopy(); return texture; } + if (source != nullptr && srgbExpand) { + auto texture = std::make_shared<Texture>(context, source, resource, components, Texture::SrgbExpandTag{}); + texture->RefreshDepthCopy(); + return texture; + } if (source != nullptr) return std::make_shared<Texture>(context, source, resource, components); std::vector<std::byte> snapshot(bytes); ReadTextureSurface(resource, *keys, snapshot); @@ -813,6 +820,9 @@ std::shared_ptr<Texture> cachedTexture(const Context& context, std::span<const s } else if (IsRepackedTextureFormat(resource.format)) { entry.texture = std::make_shared<Texture>(context, source, resource, components, Texture::RepackCopyTag{}); entry.texture->RefreshDepthCopy(); + } else if (srgbExpand) { + entry.texture = std::make_shared<Texture>(context, source, resource, components, Texture::SrgbExpandTag{}); + entry.texture->RefreshDepthCopy(); } else { entry.texture = std::make_shared<Texture>(context, source, resource, components); } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index 270d9ca0f8..b2870d8232 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -232,6 +232,53 @@ void ChainMinLod(const Context& context, const GuestTextureResource& descriptor, viewInfo.pNext = &minLod; } +struct ExpandImage { + VkImage image = VK_NULL_HANDLE; + std::shared_ptr<OwnedImage> owned; + VkDeviceSize bytes = 0; +}; + +ExpandImage CreateExpandImage(const Context& context, const GuestTextureResource& descriptor, std::uint32_t depth, std::uint32_t layers) { + ExpandImage result; + VkImageCreateInfo imageInfo{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO}; + imageInfo.flags = VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT | (descriptor.dimension == TextureDimension::kCube ? VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT : 0u); + imageInfo.imageType = ImageTypeFor(descriptor.dimension); + imageInfo.format = VK_FORMAT_R8G8B8A8_UNORM; + imageInfo.extent = {descriptor.width, descriptor.height, depth}; + imageInfo.mipLevels = descriptor.mipCount; + imageInfo.arrayLayers = layers; + imageInfo.samples = VK_SAMPLE_COUNT_1_BIT; + imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL; + imageInfo.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT; + imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + Check(context.Function<PFN_vkCreateImage>("vkCreateImage")(context.device, &imageInfo, nullptr, &result.image), "vkCreateImage sRGB copy"); + result.owned = std::make_shared<OwnedImage>(context, result.image, VK_NULL_HANDLE); + VkMemoryRequirements requirements{}; + context.Function<PFN_vkGetImageMemoryRequirements>("vkGetImageMemoryRequirements")(context.device, result.image, &requirements); + VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; + allocation.allocationSize = requirements.size; + allocation.memoryTypeIndex = context.DeviceMemoryType(requirements.memoryTypeBits, requirements.size, "texture"); + Check(context.Function<PFN_vkAllocateMemory>("vkAllocateMemory")(context.device, &allocation, nullptr, &result.owned->memory), "vkAllocateMemory sRGB copy"); + Check(context.Function<PFN_vkBindImageMemory>("vkBindImageMemory")(context.device, result.image, result.owned->memory, 0), "vkBindImageMemory sRGB copy"); + result.bytes = requirements.size; + return result; +} + +void RecordExpandBlit(const Context& context, VkCommandBuffer commands, VkImage source, VkImageLayout sourceLayout, VkImage target, const GuestTextureResource& descriptor, std::uint32_t depth, std::uint32_t layers) { + std::vector<VkImageBlit> regions; + for (std::uint32_t level = 0; level < descriptor.mipCount; ++level) { + const VkOffset3D end{static_cast<std::int32_t>(std::max(descriptor.width >> level, 1u)), static_cast<std::int32_t>(std::max(descriptor.height >> level, 1u)), static_cast<std::int32_t>(descriptor.dimension == TextureDimension::k3D ? std::max(depth >> level, 1u) : 1u)}; + VkImageBlit region{}; + region.srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, level, 0, layers}; + region.srcOffsets[1] = end; + region.dstSubresource = region.srcSubresource; + region.dstOffsets[1] = end; + regions.push_back(region); + } + context.Function<PFN_vkCmdBlitImage>("vkCmdBlitImage")(commands, source, sourceLayout, target, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, static_cast<std::uint32_t>(regions.size()), regions.data(), VK_FILTER_NEAREST); +} + std::atomic<std::uint64_t>& SampledMemoryCounter() { static std::atomic<std::uint64_t> bytes{0}; return bytes; @@ -249,6 +296,7 @@ Texture::Texture(const Context& context, TextureDetiler& detiler, const GuestTex PhaseTimer timer; try { const auto colorFormat = SampledTextureFormat(context, descriptor.format); + const bool srgbExpand = !depthCompare && SrgbExpandsToRgba8(context, descriptor.format); Require(!depthCompare || colorFormat == VK_FORMAT_R32_SFLOAT || colorFormat == VK_FORMAT_R16_UNORM, "comparison sampling requires an R32 float or R16 unorm depth texture"); Require(!depthCompare || descriptor.dimension != TextureDimension::k3D, "comparison sampling does not support 3D depth textures"); const auto vkFormat = depthCompare ? (colorFormat == VK_FORMAT_R32_SFLOAT ? VK_FORMAT_D32_SFLOAT : VK_FORMAT_D16_UNORM) : colorFormat; @@ -281,7 +329,7 @@ Texture::Texture(const Context& context, TextureDetiler& detiler, const GuestTex imageInfo.arrayLayers = geometry.imageLayers; imageInfo.samples = VK_SAMPLE_COUNT_1_BIT; imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL; - imageInfo.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT; + imageInfo.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT | (srgbExpand ? VK_IMAGE_USAGE_TRANSFER_SRC_BIT : 0u); imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; Check(context.Function<PFN_vkCreateImage>("vkCreateImage")(context.device, &imageInfo, nullptr, &image), "vkCreateImage"); @@ -296,6 +344,16 @@ Texture::Texture(const Context& context, TextureDetiler& detiler, const GuestTex owned->memory = GetBufferPool(context)->AllocateMemory(allocation, "vkAllocateMemory texture"); Check(context.Function<PFN_vkBindImageMemory>("vkBindImageMemory")(context.device, image, owned->memory, 0), "vkBindImageMemory"); + const VkImage uploadImage = image; + std::shared_ptr<OwnedImage> uploadOwned; + if (srgbExpand) { + auto expanded = CreateExpandImage(context, descriptor, geometry.imageDepth, geometry.imageLayers); + uploadOwned = std::move(owned); + owned = std::move(expanded.owned); + image = expanded.image; + allocationBytes = expanded.bytes; + } + { // Debug aid: APS5_DUMP_TEXTURE=<hex addresses, comma separated> saves the detiled first mip // of those sampled textures on their first 8 uploads as texture_<address>_<n>.raw (u32 @@ -334,6 +392,7 @@ Texture::Texture(const Context& context, TextureDetiler& detiler, const GuestTex recorder->Keep(tiled, static_cast<std::size_t>(guestBytes)); recorder->Keep(linear, static_cast<std::size_t>(linearBytes)); recorder->Keep(owned); + if (uploadOwned != nullptr) recorder->Keep(uploadOwned); } else { batch = std::make_unique<CommandBatch>(context); commands = batch->Handle(); @@ -379,7 +438,7 @@ Texture::Texture(const Context& context, TextureDetiler& detiler, const GuestTex toTransferDst.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; toTransferDst.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; toTransferDst.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; - toTransferDst.image = image; + toTransferDst.image = uploadImage; toTransferDst.subresourceRange = {aspect, 0, descriptor.mipCount, 0, geometry.imageLayers}; context.Function<PFN_vkCmdPipelineBarrier>("vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 1, &linearReadBarrier, 1, &toTransferDst); @@ -400,7 +459,19 @@ Texture::Texture(const Context& context, TextureDetiler& detiler, const GuestTex regions.push_back(region); } } - context.Function<PFN_vkCmdCopyBufferToImage>("vkCmdCopyBufferToImage")(commands, linear->Handle(), image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, static_cast<std::uint32_t>(regions.size()), regions.data()); + context.Function<PFN_vkCmdCopyBufferToImage>("vkCmdCopyBufferToImage")(commands, linear->Handle(), uploadImage, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, static_cast<std::uint32_t>(regions.size()), regions.data()); + if (srgbExpand) { + std::array<VkImageMemoryBarrier, 2> toBlit{}; + toBlit[0] = toTransferDst; + toBlit[0].srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + toBlit[0].dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT; + toBlit[0].oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + toBlit[0].newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; + toBlit[1] = toTransferDst; + toBlit[1].image = image; + context.Function<PFN_vkCmdPipelineBarrier>("vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 2, toBlit.data()); + RecordExpandBlit(context, commands, uploadImage, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, image, descriptor, geometry.imageDepth, geometry.imageLayers); + } std::unique_ptr<Buffer> dump; if (dumpWanted) { dump = std::make_unique<Buffer>(context, static_cast<std::size_t>(mips[0].linearSize), VK_BUFFER_USAGE_TRANSFER_DST_BIT); @@ -451,8 +522,8 @@ Texture::Texture(const Context& context, TextureDetiler& detiler, const GuestTex VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; viewInfo.image = image; viewInfo.viewType = ViewTypeFor(descriptor.dimension, viewLayerCount); - viewInfo.format = vkFormat; - viewFormat = vkFormat; + viewInfo.format = srgbExpand ? VK_FORMAT_R8G8B8A8_SRGB : vkFormat; + viewFormat = viewInfo.format; viewInfo.components = depthCompare ? VkComponentMapping{} : components; viewInfo.subresourceRange = {aspect, descriptor.baseLevel, viewLevelCount, descriptor.baseArray, viewLayerCount}; VkImageViewMinLodCreateInfoEXT minLod{VK_STRUCTURE_TYPE_IMAGE_VIEW_MIN_LOD_CREATE_INFO_EXT}; @@ -687,10 +758,96 @@ Texture::Texture(const Context& context, const std::shared_ptr<StorageTexture>& } } +bool Texture::CanSrgbExpandFrom(const Context& context, const StorageTexture& source, const GuestTextureResource& descriptor) { + if (!SrgbExpandsToRgba8(context, descriptor.format) || !CanCopyFrom(source, descriptor) || descriptor.dimension == TextureDimension::k3D) return false; + if (!StorageFormatAvailable(context, source.Descriptor().format) || StorageFormatForGuest(context, source.Descriptor().format) != SampledTextureFormat(context, descriptor.format)) return false; + const auto geometry = DescribeSurface(descriptor); + return geometry.imageDepth == 1 && source.ImageDepth() == 1 && source.ImageLayers() >= geometry.imageLayers && source.Descriptor().mipCount >= descriptor.mipCount; +} + +Texture::Texture(const Context& context, const std::shared_ptr<StorageTexture>& source, const GuestTextureResource& descriptor, VkComponentMapping components, SrgbExpandTag) : context(context), storageSource(source), perUseCopy(PerUseCopy::RefreshedOnReuse), srgbExpand(true) { + try { + Require(source != nullptr && CanSrgbExpandFrom(context, *source, descriptor), "storage image cannot serve an sRGB copy of the sampled texture"); + const auto geometry = DescribeSurface(descriptor); + copyWidth = descriptor.width; + copyHeight = descriptor.height; + copyLevels = descriptor.mipCount; + copyLayers = geometry.imageLayers; + APS5_LOG_OUT("Texture address=0x%llx %ux%u mips=%u layers=%u sRGB copy of storage image (guest format %u)", static_cast<unsigned long long>(descriptor.baseAddress), descriptor.width, descriptor.height, descriptor.mipCount, copyLayers, descriptor.format); + auto expanded = CreateExpandImage(context, descriptor, 1u, copyLayers); + image = expanded.image; + owned = std::move(expanded.owned); + allocationBytes = expanded.bytes; + + const auto viewLevelCount = std::min(descriptor.lastLevel, descriptor.mipCount - 1u) - descriptor.baseLevel + 1u; + const auto viewLayerCount = geometry.imageLayers - descriptor.baseArray; + if (descriptor.dimension == TextureDimension::kCube) { + Require(viewLayerCount % 6u == 0, "guest cube texture view does not contain a multiple of 6 array slices"); + } + VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; + viewInfo.image = image; + viewInfo.viewType = ViewTypeFor(descriptor.dimension, viewLayerCount); + viewInfo.format = VK_FORMAT_R8G8B8A8_SRGB; + viewFormat = viewInfo.format; + viewInfo.components = components; + viewInfo.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, descriptor.baseLevel, viewLevelCount, descriptor.baseArray, viewLayerCount}; + VkImageViewMinLodCreateInfoEXT minLod{VK_STRUCTURE_TYPE_IMAGE_VIEW_MIN_LOD_CREATE_INFO_EXT}; + ChainMinLod(context, descriptor, viewInfo, minLod); + Check(context.Function<PFN_vkCreateImageView>("vkCreateImageView")(context.device, &viewInfo, nullptr, &view), "vkCreateImageView sRGB copy"); + viewRange = {viewInfo.viewType, viewInfo.subresourceRange.levelCount, viewInfo.subresourceRange.layerCount}; + createFirstLayerView(descriptor, viewInfo); + prepareBaseLevelView(descriptor, viewInfo); + sourceDescriptor = descriptor; + countedBytes = allocationBytes; + SampledMemoryCounter().fetch_add(countedBytes, std::memory_order_relaxed); + } catch (...) { + release(); + throw; + } +} + +bool Texture::refreshSrgbExpand() { + auto* recorder = Recorder::Active(); + std::unique_ptr<CommandBatch> batch; + VkCommandBuffer commands = VK_NULL_HANDLE; + if (recorder != nullptr) { + commands = recorder->Commands(); + recorder->Keep(owned); + recorder->Keep(storageSource); + } else { + batch = std::make_unique<CommandBatch>(context); + commands = batch->Handle(); + } + const auto barrier = context.Function<PFN_vkCmdPipelineBarrier>("vkCmdPipelineBarrier"); + const VkMemoryBarrier produced{VK_STRUCTURE_TYPE_MEMORY_BARRIER, nullptr, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT}; + VkImageMemoryBarrier toTransferDst{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; + toTransferDst.srcAccessMask = VK_ACCESS_SHADER_READ_BIT; + toTransferDst.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + toTransferDst.oldLayout = copiedOnce ? VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL : VK_IMAGE_LAYOUT_UNDEFINED; + toTransferDst.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + toTransferDst.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + toTransferDst.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + toTransferDst.image = image; + toTransferDst.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, copyLevels, 0, copyLayers}; + barrier(commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 1, &produced, 0, nullptr, 1, &toTransferDst); + RecordExpandBlit(context, commands, storageSource->Image(), VK_IMAGE_LAYOUT_GENERAL, image, sourceDescriptor, 1u, copyLayers); + VkImageMemoryBarrier toShaderRead = toTransferDst; + toShaderRead.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + toShaderRead.dstAccessMask = VK_ACCESS_SHADER_READ_BIT; + toShaderRead.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + toShaderRead.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + barrier(commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 0, nullptr, 0, nullptr, 1, &toShaderRead); + if (batch) batch->SubmitAndWait(); + copiedOnce = true; + copiedVersion = storageSource->Version(); + return true; +} + bool Texture::RefreshDepthCopy() { if (perUseCopy != PerUseCopy::RefreshedOnReuse || storageSource == nullptr) return false; if (copiedOnce && storageSource->Version() == copiedVersion) return false; if (blockFormat != VK_FORMAT_UNDEFINED) return refreshBlockCopy(); + if (srgbExpand) return refreshSrgbExpand(); std::uint64_t linearBytes = 0; for (std::uint32_t level = 0; level < copyLevels; ++level) linearBytes += static_cast<std::uint64_t>(std::max(copyWidth >> level, 1u)) * std::max(copyHeight >> level, 1u) * copyElementBytes * copyLayers; auto linear = std::make_shared<DeviceBuffer>(context, static_cast<std::size_t>(linearBytes), VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT | (repackCopy ? VK_BUFFER_USAGE_STORAGE_BUFFER_BIT : 0u)); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp index 59dd54b906..e72bae96fc 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp @@ -137,6 +137,26 @@ VkFormat SampledTextureFormat(const Context& context, std::uint32_t guestFormat) return ResolveTextureFormat(static_cast<std::uint32_t>(ShaderRecompiler::SrgbUnormFormat(format))); } +std::uint32_t SrgbCopyFormats(PFN_vkGetPhysicalDeviceFormatProperties formatProperties, VkPhysicalDevice physical, std::uint32_t decodeFormats) { + const char* forced = std::getenv("APS5_SRGB_SHADER_DECODE"); + if (forced != nullptr && std::strcmp(forced, "1") == 0) return 0u; + VkFormatProperties target{}; + formatProperties(physical, VK_FORMAT_R8G8B8A8_UNORM, &target); + if ((target.optimalTilingFeatures & VK_FORMAT_FEATURE_BLIT_DST_BIT) == 0) return 0u; + std::uint32_t formats = 0; + for (const auto format : {ShaderRecompiler::IrBufferFormat::Format8Srgb, ShaderRecompiler::IrBufferFormat::Format8_8Srgb}) { + if ((decodeFormats & ShaderRecompiler::SrgbDecodeBit(format)) == 0u) continue; + VkFormatProperties source{}; + formatProperties(physical, ResolveTextureFormat(static_cast<std::uint32_t>(ShaderRecompiler::SrgbUnormFormat(format))), &source); + if ((source.optimalTilingFeatures & VK_FORMAT_FEATURE_BLIT_SRC_BIT) != 0) formats |= ShaderRecompiler::SrgbDecodeBit(format); + } + return formats; +} + +bool SrgbExpandsToRgba8(const Context& context, std::uint32_t guestFormat) { + return (context.srgbCopyFormats & ShaderRecompiler::SrgbDecodeBit(static_cast<ShaderRecompiler::IrBufferFormat>(guestFormat))) != 0u; +} + bool IsSrgbTextureFormat(std::uint32_t guestFormat) { switch (findFormatEntry(guestFormat).vkFormat) { case VK_FORMAT_R8_SRGB: diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageSrgbSampleCopy.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageSrgbSampleCopy.cpp new file mode 100644 index 0000000000..ed83b35bd8 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageSrgbSampleCopy.cpp @@ -0,0 +1,319 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include <windows.h> +#else +#include <sys/mman.h> +#endif +#include <algorithm> +#include <array> +#include <cmath> +#include <cstdint> +#include <cstdio> +#include <cstring> +#include <iostream> +#include <span> +#include <string> +#include <vector> + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 32; +constexpr std::uint32_t Size = 4; +constexpr std::uint32_t Type2D = 9; +constexpr std::uint32_t Srgb8 = 128; +constexpr std::uint32_t Srgb8_8 = 129; +constexpr std::uint32_t SwizzleX = 0xfacu; +constexpr std::uint32_t SwizzleYAsX = 0xfadu; +constexpr std::uint32_t FilterPoint = 0; +constexpr std::uint32_t FilterBilinear = 1; +constexpr std::array<std::uint8_t, Size> Codes{0, 255, 128, 64}; +constexpr std::size_t BlockBytes = 2u << 20u; +constexpr std::uint32_t StoreSize = 1024; + +alignas(256) std::array<float, Threads * 3> Input{}; +alignas(256) std::array<float, Threads * 4> Output{}; +alignas(4096) std::array<std::uint8_t, 4096> Texels{}; + +alignas(256) constexpr std::array<std::uint32_t, 13> SampleCode{ + 0x1614008c, 0xe03c1000, 0x8000010a, 0xbf8c3f70, 0xf0900108, 0x00820401, 0xbf8c3f70, + 0x34160082, 0xe0701000, 0x8001040b, 0xbf810000, 0xbf810000, 0xbf810000, +}; + +alignas(256) std::array<std::uint32_t, 11> StoreCode{ + 0x7e280d00, 0x102828ff, 0x3d000000, 0x062a28ff, 0x41000000, 0x7e3c0300, 0x7e3e0280, 0xf0201108, 0x0001141e, 0xbf810000, 0xbf810000, +}; + +alignas(256) constexpr std::array<std::uint32_t, 13> GatherCode{ + 0x1614008c, 0xe03c1000, 0x8000010a, 0xbf8c3f70, 0xf11c0108, 0x00820401, 0xbf8c3f70, + 0x34160084, 0xe0781000, 0x8001040b, 0xbf810000, 0xbf810000, 0xbf810000, +}; + + +class GuestBlock { +public: + GuestBlock() { +#ifdef _WIN32 + block = static_cast<std::uint8_t*>(GuestArena::GuestArenaAllocate_nid_postfix(BlockBytes, 65536)); + if (block != nullptr) GuestArena::GuestArenaCommit_nid_postfix(block, BlockBytes, PAGE_READWRITE, BlockBytes); +#else + constexpr std::uintptr_t alignment = 65536; + void* mapped = mmap(nullptr, BlockBytes + alignment, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (mapped != MAP_FAILED) { + const auto begin = reinterpret_cast<std::uintptr_t>(mapped); + const auto aligned = (begin + alignment - 1) & ~(alignment - 1); + if (aligned != begin) munmap(mapped, aligned - begin); + if (aligned + BlockBytes != begin + BlockBytes + alignment) munmap(reinterpret_cast<void*>(aligned + BlockBytes), begin + alignment - aligned); + block = reinterpret_cast<std::uint8_t*>(aligned); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(block, BlockBytes); + } +#endif + Require(block != nullptr, "image sRGB sample copy: cannot allocate the guest block"); + Require(AgcDriver::GuestMemory::Watched(reinterpret_cast<std::uintptr_t>(block), BlockBytes), "image sRGB sample copy: the guest block is not write-watched"); + std::memset(block, 0, BlockBytes); + GuestAllocations::Mutation().Add(block, BlockBytes, true, true, true); + AgcDriver::GuestMemory::CollectWritesUncached(reinterpret_cast<std::uintptr_t>(block), BlockBytes); + } + + ~GuestBlock() { + GuestAllocations::Mutation().Remove(block); +#ifdef _WIN32 + GuestArena::GuestArenaReset_nid_postfix(block, BlockBytes); + GuestArena::GuestArenaRelease_nid_postfix(block, BlockBytes); +#else + munmap(block, BlockBytes); + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(block, BlockBytes); +#endif + } + + GuestBlock(const GuestBlock&) = delete; + GuestBlock& operator=(const GuestBlock&) = delete; + + std::uint8_t* Data() { return block; } + +private: + std::uint8_t* block = nullptr; +}; + +std::array<std::uint32_t, 4> BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast<std::uintptr_t>(data); + return {static_cast<std::uint32_t>(address), static_cast<std::uint32_t>((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +std::array<std::uint32_t, 8> TextureDescriptor(std::uint32_t format, std::uint32_t swizzle, const void* data = Texels.data(), std::uint32_t extent = Size) { + const auto address = static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(data)); + return { + static_cast<std::uint32_t>(address >> 8u), + static_cast<std::uint32_t>((address >> 40u) & 0xffu) | (format << 20u) | (((extent - 1u) & 3u) << 30u), + ((extent - 1u) >> 2u) | ((extent - 1u) << 14u), + swizzle | (Type2D << 28u), + 0u, + 0u, + 0u, + 0u, + }; +} + +std::array<std::uint32_t, 4> SamplerDescriptor(std::uint32_t filter) { + return {0x92u, 0x00fff000u, (filter << 20u) | (filter << 22u) | (1u << 24u), 0u}; +} + +double Decode(std::uint32_t code) { + const double encoded = code / 255.0; + return encoded <= 0.04045 ? encoded / 12.92 : std::pow((encoded + 0.055) / 1.055, 2.4); +} + +std::uint32_t Code(std::uint32_t format, bool second, std::uint32_t x) { + const std::uint32_t code = Codes.at(std::min(x, Size - 1u)); + return second && format == Srgb8_8 ? 255u - code : code; +} + +void FillTexels(std::uint32_t format) { + const std::uint32_t bytes = format == Srgb8 ? 1u : 2u; + const auto mips = AgcDriver::Graphics::ComputeMipLayout(AgcDriver::Graphics::TextureTileMode::kLinear, format, Size, Size, 1); + Require(AgcDriver::Graphics::ComputeSurfaceSize(mips, 1) <= Texels.size(), "image sRGB sample copy: the texture does not fit the texel storage"); + Texels.fill(0); + for (std::uint32_t y = 0; y < Size; ++y) { + for (std::uint32_t x = 0; x < Size; ++x) { + auto* texel = &Texels[mips[0].tiledOffset + static_cast<std::uint64_t>(y) * mips[0].pitchBytes + x * bytes]; + texel[0] = static_cast<std::uint8_t>(Code(format, false, x)); + if (bytes == 2u) texel[1] = static_cast<std::uint8_t>(Code(format, true, x)); + } + } +} + +void FillInput(bool gather, bool between) { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const std::uint32_t base = tid % (Size - 1u); + float u = (static_cast<float>(tid % Size) + 0.5f) / static_cast<float>(Size); + if (gather) u = (static_cast<float>(base) + 0.6f) / static_cast<float>(Size); + else if (between) u = static_cast<float>(base + 1u) / static_cast<float>(Size); + Input[tid * 3u + 0u] = u; + Input[tid * 3u + 1u] = 0.6f / static_cast<float>(Size); + Input[tid * 3u + 2u] = 0.0f; + } +} + +bool Close(double actual, double expected) { + return std::fabs(actual - expected) <= 0.01 + std::fabs(expected) * 0.04; +} + +void Store(AgcDriver::VulkanDevice& device, std::uint8_t* texels, float scale) { + std::vector<std::uint32_t> userData(16, 0u); + const auto texture = TextureDescriptor(Srgb8, SwizzleX, texels, StoreSize); + std::copy(texture.begin(), texture.end(), userData.begin() + 4); + std::uint32_t bits = 0; + std::memcpy(&bits, &scale, sizeof(bits)); + StoreCode[2] = bits; + const std::array<ShaderRecompiler::MemoryRegion, 1> memory{{{reinterpret_cast<std::uintptr_t>(StoreCode.data()), std::as_bytes(std::span<const std::uint32_t>(StoreCode))}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast<std::uintptr_t>(StoreCode.data()), std::span<const std::uint32_t>(StoreCode), 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast<std::uintptr_t>(StoreCode.data())); + device.WaitIdle(); +} + +void CheckStored(AgcDriver::VulkanDevice& device, std::uint8_t* texels, float scale, const std::string& what) { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + Input[tid * 3u + 0u] = (static_cast<float>(tid) + 0.5f) / static_cast<float>(StoreSize); + Input[tid * 3u + 1u] = 0.5f / static_cast<float>(StoreSize); + Input[tid * 3u + 2u] = 0.0f; + } + std::vector<std::uint32_t> userData(28, 0u); + const auto input = BufferDescriptor(Input.data(), static_cast<std::uint32_t>(sizeof(Input))); + const auto output = BufferDescriptor(Output.data(), static_cast<std::uint32_t>(sizeof(Output))); + const auto texture = TextureDescriptor(Srgb8, SwizzleX, texels, StoreSize); + const auto sampler = SamplerDescriptor(FilterPoint); + std::copy(input.begin(), input.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4); + std::copy(texture.begin(), texture.end(), userData.begin() + 8); + std::copy(sampler.begin(), sampler.end(), userData.begin() + 16); + const std::array<ShaderRecompiler::MemoryRegion, 1> memory{{{reinterpret_cast<std::uintptr_t>(SampleCode.data()), std::as_bytes(std::span<const std::uint32_t>(SampleCode))}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast<std::uintptr_t>(SampleCode.data()), std::span<const std::uint32_t>(SampleCode), 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + const auto result = ShaderRecompiler::Recompile(request); + Output.fill(-1.0f); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast<std::uintptr_t>(SampleCode.data())); + device.WaitIdle(); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const double code = std::round(std::min(1.0, static_cast<double>(tid) * scale) * 255.0); + const double expected = Decode(static_cast<std::uint32_t>(code)); + Require(Close(Output[tid], expected), what + ": thread " + std::to_string(tid) + " sampled " + std::to_string(Output[tid]) + ", expected " + std::to_string(expected)); + } +} + +void CheckStorageSource(AgcDriver::VulkanDevice& device) { + GuestBlock block; + auto* texels = block.Data(); + const auto surface = AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::DecodeTextureResource(TextureDescriptor(Srgb8, SwizzleX, texels, StoreSize))); + Require(surface.guestBytes >= (1u << 20u) && surface.guestBytes <= BlockBytes, "8_SRGB storage source: the surface does not fit the guest block or is below the storage image floor"); + AgcDriver::Graphics::StorageTexture::FlushPending(reinterpret_cast<std::uintptr_t>(texels), surface.guestBytes, nullptr, "test"); + AgcDriver::Graphics::FlushCachedTextures(device.Device()); + Store(device, texels, 1.0f / 32.0f); + CheckStored(device, texels, 1.0f / 32.0f, "8_SRGB sample of a pending storage image"); + const auto before = AgcDriver::Graphics::StorageTexture::FindPending(reinterpret_cast<std::uintptr_t>(texels), surface.guestBytes); + Require(before != nullptr, "8_SRGB storage source: no storage image holds the surface's results"); + const auto version = before->Version(); + Store(device, texels, 1.0f / 8.0f); + const auto after = AgcDriver::Graphics::StorageTexture::FindPending(reinterpret_cast<std::uintptr_t>(texels), surface.guestBytes); + Require(after != nullptr && after->Version() > version, "8_SRGB storage source: the second store did not advance the storage image"); + CheckStored(device, texels, 1.0f / 8.0f, "8_SRGB sample of a pending storage image after a second store"); +} + +void Check(AgcDriver::VulkanDevice& device, std::uint32_t format, bool second, std::uint32_t filter, bool gather, const std::string& what) { + FillTexels(format); + const bool between = filter == FilterBilinear; + FillInput(gather, between); + const auto& code = gather ? GatherCode : SampleCode; + std::vector<std::uint32_t> userData(28, 0u); + const auto input = BufferDescriptor(Input.data(), static_cast<std::uint32_t>(sizeof(Input))); + const auto output = BufferDescriptor(Output.data(), static_cast<std::uint32_t>(sizeof(Output))); + const auto texture = TextureDescriptor(format, second ? SwizzleYAsX : SwizzleX); + const auto sampler = SamplerDescriptor(filter); + std::copy(input.begin(), input.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4); + std::copy(texture.begin(), texture.end(), userData.begin() + 8); + std::copy(sampler.begin(), sampler.end(), userData.begin() + 16); + const std::array<ShaderRecompiler::MemoryRegion, 1> memory{{{reinterpret_cast<std::uintptr_t>(code.data()), std::as_bytes(std::span<const std::uint32_t>(code))}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast<std::uintptr_t>(code.data()), std::span<const std::uint32_t>(code), 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + const auto result = ShaderRecompiler::Recompile(request); + Output.fill(-1.0f); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast<std::uintptr_t>(code.data())); + device.WaitIdle(); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const std::uint32_t base = tid % (Size - 1u); + if (gather) { + const double near = Decode(Code(format, second, base)); + const double far = Decode(Code(format, second, base + 1u)); + const std::array<double, 4> expected{near, far, far, near}; + for (std::uint32_t component = 0; component < 4u; ++component) { + const double actual = Output[tid * 4u + component]; + Require(Close(actual, expected[component]), what + ": thread " + std::to_string(tid) + " component " + std::to_string(component) + " gathered " + std::to_string(actual) + ", expected " + std::to_string(expected[component])); + } + continue; + } + double expected = Decode(Code(format, second, tid % Size)); + if (between) expected = (Decode(Code(format, second, base)) + Decode(Code(format, second, base + 1u))) / 2.0; + const double actual = Output[tid]; + Require(Close(actual, expected), what + ": thread " + std::to_string(tid) + " sampled " + std::to_string(actual) + ", expected " + std::to_string(expected)); + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + for (const auto format : {Srgb8, Srgb8_8}) { + const std::string name = format == Srgb8 ? "8_SRGB" : "8_8_SRGB"; + Check(*device, format, false, FilterPoint, false, name + " point sample"); + Check(*device, format, false, FilterBilinear, false, name + " bilinear sample, decoded before filtering"); + Check(*device, format, false, FilterPoint, true, name + " gather4"); + if (format == Srgb8_8) { + Check(*device, format, true, FilterPoint, false, name + " point sample of the second channel"); + Check(*device, format, true, FilterBilinear, false, name + " bilinear sample of the second channel, decoded before filtering"); + Check(*device, format, true, FilterPoint, true, name + " gather4 of the second channel"); + } + } + CheckStorageSource(*device); + std::puts("image sRGB sample copy tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} From 97354b7fd15fe4105b67b0f864094ff20c38fb00 Mon Sep 17 00:00:00 2001 From: Dextroid17 <217507631+Dextroid17@users.noreply.github.com> Date: Wed, 14 Oct 2026 16:15:26 -0400 Subject: [PATCH 0982/1000] docs(debt): sRGB 8 and 8_8 textures read an RGBA8 sRGB copy --- docs/dev/debt/libSceAgcDriver.md | 2 +- docs/dev/debt/shader.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index f85f514f26..61cd50cbd7 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -89,7 +89,7 @@ - [Unnormalized S# coordinates](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp) (libSceAgcDriver) - `FORCE_UNNORMALIZED` uses a Vulkan `unnormalizedCoordinates` sampler, so the S# LOD range, LOD bias and mip filter are not applied, which is exact only on a one-level view. - An [unnormalized S#](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp) (libSceAgcDriver) maps clamp mode 0 (repeat) on X and Y to clamp-to-edge as required by Vulkan unnormalized coordinates, and assigns the magnification filter when minification and magnification filters differ (since unnormalized coordinates sample at LOD 0). Anisotropy, TRUNC_COORD, MC_COORD_TRUNC and other clamp modes still throw. Secondary LOD bias (LOD_BIAS_SEC, bits [19:14]) is accepted and ignored, matching RDNA2 hardware measurements across explicit and implicit samples. High on Life (PPSA15388, #4736) uses secondary LOD bias and unnormalized coordinates with clamp mode 0 and differing min/mag filters during home base and city rendering. - [Background link-time-optimized relink of library pipelines](../../../core/libs/prx/libSceAgcDriver/Execution/include/PipelineLinkOptimization.hpp) (libSceAgcDriver) is skipped on every GPU under NVIDIA's proprietary Vulkan driver (`VK_DRIVER_ID_NVIDIA_PROPRIETARY`), so those pipelines keep their fast-linked form. The optimized link loses the device when bound on an RTX 3080 (Ampere, #2866) and an RTX 4060 (Ada, #3434); an RTX 5060 and an RTX 5080 (Blackwell) on the same driver do not, so the scope is wider than the evidence. No standard Vulkan query tells the architecture apart, and the driver version that fixes it is unknown. `APS5_NO_PIPELINE_LTO` disables the relink on every driver. -- [8_SRGB and 8_8_SRGB textures](../../../core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp) (libSceAgcDriver) that the device samples as R8_SRGB or R8G8_SRGB use its own decode, which need not match gfx1035: NVIDIA's R8_SRGB is one bf16 step off in 29 of 256 codes. +- [8_SRGB and 8_8_SRGB textures](../../../core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp) (libSceAgcDriver) that the device samples as R8_SRGB or R8G8_SRGB use its own decode, which need not match gfx1035: NVIDIA's R8_SRGB is one bf16 step off in 29 of 256 codes. A device that cannot sample them gets an RGBA8 sRGB copy instead (a blit of the R8 or RG8 texels, refreshed with the storage image's version), whose decode is likewise the device's; 3D textures and storage images that are not single-depth and format-matched are copied from their snapshot instead. - [PM4 submission rings](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/SubmissionRing.cpp) (libSceAgcDriver) keep only the first 512 dwords of each submission, so the packets past them are not printed. The [failure report](../../../core/libs/prx/libc/src/FailureReport.cpp) dump does not allocate from the C runtime heap: packet names come from static strings, each line is formatted with integer and string conversions into a 512-byte stack buffer and written with one `WriteFile`. It still runs on whatever state the crash left. It waits about 100 yields for each ring's lock and otherwise prints that queue as busy, so a crash while recording a submission loses that queue's ring. It reads the rings without checking them, so a crash that corrupted them can fault inside the dump; such a fault during the report of an unhandled exception stops in that report's `Sleep(INFINITE)` and the process hangs instead of ending. The reports run at most once per process. Linux has no crash report: there the rings are printed only by `APS5_STALL_WATCHDOG_SECS`. - [WAIT_REG_MEM](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/WaitMemory.cpp) reports a diagnostic after `APS5_GPU_WAIT_TIMEOUT_MS` without treating the timeout as completion. An unsatisfied wait keeps polling and checking driver failures; a missing producer can therefore leave its queue waiting indefinitely. Existing recorded-label shortcuts remain in use. - [COND_EXEC](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp) reads its condition after the earlier packets of its queue complete. Without PFP_SYNC_ME the console's graphics PFP reads it ahead of the ME, so a title that races the read sees a later value here. diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index 12e6133a6d..8387d4982f 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -62,8 +62,8 @@ - [`ds_wrxchg2_rtn_b32`, `ds_wrxchg2st64_rtn_b32`, `ds_wrxchg2_rtn_b64` and `ds_wrxchg2st64_rtn_b64`](../../../core/shader/recompiler/Translation/src/MemoryInstructions/SharedMemoryInstructions.cpp) are translated as two separate 32-bit or 64-bit atomic swaps per invocation. RDNA2 hardware (measured on gfx1035) performs every lane's first exchange before any lane's second one, so when lanes of the same wave or of different waves overlap addresses, the result can differ from hardware. `offset0 == offset1` throws, although on hardware it is two exchanges in order. - [`ds_condxchg32_rtn_b64`](../../../core/shader/recompiler/Translation/src/MemoryInstructions/SharedMemoryInstructions.cpp) uses two 32-bit atomic conditional exchanges. RDNA2 (gfx1035) measurements confirmed aligned return values, conditional bit clearing and 16-bit offsets; out-of-range accesses return zero without modifying LDS. Misaligned accesses fault the wave on gfx1035; the recompiler does not model this fault. Radeon 780M (gfx1103, RDNA3) measurements confirmed aligned return values, conditional bit clearing, EXEC masking and operand overlap in wave32 and wave64; contended cases passed per-dword conservation checks. Contended lane ordering and gfx1013 remain unmeasured; pair-wide indivisibility is not assumed, and concurrent 64-bit LDS atomics use a separate lock. GDS throws. VGPR pairs extending beyond v255 throw; allocated-VGPR EXEC nullification is not emulated. - [64-bit image atomics](../../../core/shader/recompiler/Translation/src/MemoryInstructions/ImageInstructions.cpp) (`image_atomic_*` with a two-dword data mask) need `VK_EXT_shader_image_atomic_int64` and act on 32_32 UINT, SINT and FLOAT surfaces through an R64_UINT storage view. 64-bit `image_atomic_inc`, `image_atomic_dec` and the float atomics throw, as does any other access in the same shader to an image it updates with 64-bit atomics. -- [8_SRGB and 8_8_SRGB loads](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) on a device that cannot sample R8_SRGB or R8G8_SRGB return the sRGB EOTF rounded to nearest even at bf16 precision, as adriadam10 measured on RDNA2 (gfx1035) in #866; the PS5's gfx1013 is not measured. -- [Samples and gathers](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) of 8_SRGB and 8_8_SRGB textures throw on a device that cannot sample R8_SRGB or R8G8_SRGB. +- [8_SRGB and 8_8_SRGB loads](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) on a device that cannot sample R8_SRGB or R8G8_SRGB return the sRGB EOTF rounded to nearest even at bf16 precision, as adriadam10 measured on RDNA2 (gfx1035) in #866; the PS5's gfx1013 is not measured. This is the path on a device that cannot blit R8 or RG8 to RGBA8, and with `APS5_SRGB_SHADER_DECODE=1`; otherwise a load of a sampled texture reads the RGBA8 sRGB copy and gets the device's own decode. +- [Samples and gathers](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) of 8_SRGB and 8_8_SRGB textures read an RGBA8 sRGB copy of the texture (libSceAgcDriver, [Texture.cpp](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp)) on a device that cannot sample R8_SRGB or R8G8_SRGB but can blit R8 or RG8 to RGBA8, so they use that device's own decode. They still throw on a device that cannot blit, and with `APS5_SRGB_SHADER_DECODE=1`. A multisampled or depth view of these formats gets no copy. - [Bindless image table](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) entries of 8_SRGB and 8_8_SRGB textures read zeros, like converted formats, on a device that cannot sample R8_SRGB or R8G8_SRGB. - [Shader MODE register access](../../../core/shader/recompiler/Translation/src/RoundingMode.cpp) starts from the rounding fields of `GuestContext::floatMode` and follows immediate rounding writes (`s_round_mode`, `s_setreg_imm32_b32` on MODE bits 0-3) through the control flow graph; `s_getreg_b32` of those bits returns the tracked value. Only `v_cvt_f32_i32`, `v_cvt_f32_u32`, `v_ldexp_f32`, the f16 conversions and `v_ldexp_f16` (with f16 denormals kept) and the instructions that already read `FLOAT_MODE` follow a rounding mode set at run time; any other float instruction that runs while a directed rounding mode differs from the initial one throws at that instruction. Rounding bits written from an SGPR (`s_setreg_b32`) or that differ between the paths reaching an instruction throw where they are read or used. A shader without `FLOAT_MODE` metadata throws on any rounding change. Non-rounding register reads still throw; `s_setreg_b32` writes to other hardware registers are still ignored. Tests follow the public RDNA2/LLVM contract, not console measurements. - A [flat SRT read](../../../core/shader/recompiler/Optimization/src/SrtWalker/SrtDescriptorEvaluation.cpp) through a null or unmapped pointer loaded from memory raises a [BDA fault](../../../core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp) only if the program executes it, then stops or reads zero like a BDA read. From b5502ee4a101c491cbfb35662844180db004f6ae Mon Sep 17 00:00:00 2001 From: Dextroid17 <217507631+Dextroid17@users.noreply.github.com> Date: Thu, 15 Oct 2026 14:20:04 -0400 Subject: [PATCH 0983/1000] fix(agc): rename the near and far locals that windows.h defines Co-authored-by: Jacmd <59657462+Jacmd@users.noreply.github.com> --- .../libSceAgcDriver/tests/execution/ImageSrgbSampleCopy.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageSrgbSampleCopy.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageSrgbSampleCopy.cpp index ed83b35bd8..15311eba5e 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ImageSrgbSampleCopy.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageSrgbSampleCopy.cpp @@ -276,9 +276,9 @@ void Check(AgcDriver::VulkanDevice& device, std::uint32_t format, bool second, s for (std::uint32_t tid = 0; tid < Threads; ++tid) { const std::uint32_t base = tid % (Size - 1u); if (gather) { - const double near = Decode(Code(format, second, base)); - const double far = Decode(Code(format, second, base + 1u)); - const std::array<double, 4> expected{near, far, far, near}; + const double low = Decode(Code(format, second, base)); + const double high = Decode(Code(format, second, base + 1u)); + const std::array<double, 4> expected{low, high, high, low}; for (std::uint32_t component = 0; component < 4u; ++component) { const double actual = Output[tid * 4u + component]; Require(Close(actual, expected[component]), what + ": thread " + std::to_string(tid) + " component " + std::to_string(component) + " gathered " + std::to_string(actual) + ", expected " + std::to_string(expected[component])); From 192a37d026fd14621174116a941df5c645028ba2 Mon Sep 17 00:00:00 2001 From: Dextroid17 <217507631+Dextroid17@users.noreply.github.com> Date: Thu, 15 Oct 2026 14:20:45 -0400 Subject: [PATCH 0984/1000] test(agc): force the sRGB sample copy with APS5_SRGB_SHADER_DECODE=copy --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 8 +++++ .../Graphics/src/TextureFormat.cpp | 2 +- .../tests/execution/ImageSrgbSampleCopy.cpp | 33 +++++++++++++++++++ docs/dev/debt/shader.md | 2 +- 4 files changed, 43 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index fdca183551..03292a4e12 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1828,6 +1828,14 @@ if(BUILD_TESTING) set_tests_properties(agc_driver_emulated_color_compare_srgb_decode PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>") endif() + add_test(NAME agc_driver_image_srgb_sample_copy_forced COMMAND agc_driver_image_srgb_sample_copy_tests) + set_tests_properties(agc_driver_image_srgb_sample_copy_forced PROPERTIES + ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_SRGB_SHADER_DECODE=copy" + SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + if(WIN32) + set_tests_properties(agc_driver_image_srgb_sample_copy_forced PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>") + endif() add_test(NAME agc_shared_write_partial_collect COMMAND agc_shared_write_tests --partial-collect) set_tests_properties(agc_shared_write_partial_collect PROPERTIES SKIP_RETURN_CODE 77 ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" TIMEOUT 30) if(WIN32) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp index e72bae96fc..c9617b896f 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp @@ -117,7 +117,7 @@ VkFormat ResolveTextureFormat(std::uint32_t guestFormat) { std::uint32_t SrgbDecodeFormats(PFN_vkGetPhysicalDeviceFormatProperties formatProperties, VkPhysicalDevice physical) { const char* forced = std::getenv("APS5_SRGB_SHADER_DECODE"); - const bool always = forced != nullptr && std::strcmp(forced, "1") == 0; + const bool always = forced != nullptr && (std::strcmp(forced, "1") == 0 || std::strcmp(forced, "copy") == 0); const auto sampled = [&](ShaderRecompiler::IrBufferFormat format) { VkFormatProperties properties{}; formatProperties(physical, ResolveTextureFormat(static_cast<std::uint32_t>(format)), &properties); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageSrgbSampleCopy.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageSrgbSampleCopy.cpp index 15311eba5e..4ea7cc7525 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ImageSrgbSampleCopy.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageSrgbSampleCopy.cpp @@ -2,6 +2,7 @@ #include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" #include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp" #include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" #include "prx/libc/include/GuestAllocations.hpp" #include "prx/libc/include/GuestArena.hpp" @@ -21,6 +22,7 @@ #include <cmath> #include <cstdint> #include <cstdio> +#include <cstdlib> #include <cstring> #include <iostream> #include <span> @@ -246,6 +248,35 @@ void CheckStorageSource(AgcDriver::VulkanDevice& device) { CheckStored(device, texels, 1.0f / 8.0f, "8_SRGB sample of a pending storage image after a second store"); } +std::string ForcedMode() { + const char* forced = std::getenv("APS5_SRGB_SHADER_DECODE"); + return forced == nullptr ? std::string{} : std::string{forced}; +} + +void VKAPI_PTR NativeSrgbFormatProperties(VkPhysicalDevice, VkFormat, VkFormatProperties* properties) { + *properties = {}; + properties->optimalTilingFeatures = VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT | VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_LINEAR_BIT | VK_FORMAT_FEATURE_BLIT_SRC_BIT | VK_FORMAT_FEATURE_BLIT_DST_BIT; +} + +void VKAPI_PTR NoSrgbFormatProperties(VkPhysicalDevice, VkFormat format, VkFormatProperties* properties) { + *properties = {}; + if (format == VK_FORMAT_R8_SRGB || format == VK_FORMAT_R8G8_SRGB) return; + properties->optimalTilingFeatures = VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT | VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_LINEAR_BIT | VK_FORMAT_FEATURE_BLIT_SRC_BIT | VK_FORMAT_FEATURE_BLIT_DST_BIT; +} + +std::uint32_t SrgbBits() { + return AgcDriver::Graphics::SrgbDecodeFormats(NoSrgbFormatProperties, VK_NULL_HANDLE); +} + +void CheckSelection() { + const auto mode = ForcedMode(); + Require(SrgbBits() != 0u, "a device that cannot sample R8_SRGB or R8G8_SRGB selected no sRGB decode formats"); + const auto decode = AgcDriver::Graphics::SrgbDecodeFormats(NativeSrgbFormatProperties, VK_NULL_HANDLE); + const auto copy = AgcDriver::Graphics::SrgbCopyFormats(NativeSrgbFormatProperties, VK_NULL_HANDLE, decode); + const std::uint32_t expected = mode == "copy" ? SrgbBits() : 0u; + Require(copy == expected, "a device that samples R8_SRGB and R8G8_SRGB selected sRGB copy formats " + std::to_string(copy) + " with APS5_SRGB_SHADER_DECODE='" + mode + "', expected " + std::to_string(expected)); +} + void Check(AgcDriver::VulkanDevice& device, std::uint32_t format, bool second, std::uint32_t filter, bool gather, const std::string& what) { FillTexels(format); const bool between = filter == FilterBilinear; @@ -296,8 +327,10 @@ void Check(AgcDriver::VulkanDevice& device, std::uint32_t format, bool second, s int main() { try { + CheckSelection(); const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; + if (ForcedMode() == "copy") Require((device->Target().srgbDecodeFormats & SrgbBits()) == 0u, "APS5_SRGB_SHADER_DECODE=copy: the device cannot blit R8 or RG8 to RGBA8, so 8 and 8_8 sRGB samples have no copy"); for (const auto format : {Srgb8, Srgb8_8}) { const std::string name = format == Srgb8 ? "8_SRGB" : "8_8_SRGB"; Check(*device, format, false, FilterPoint, false, name + " point sample"); diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index 8387d4982f..00fa36d332 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -63,7 +63,7 @@ - [`ds_condxchg32_rtn_b64`](../../../core/shader/recompiler/Translation/src/MemoryInstructions/SharedMemoryInstructions.cpp) uses two 32-bit atomic conditional exchanges. RDNA2 (gfx1035) measurements confirmed aligned return values, conditional bit clearing and 16-bit offsets; out-of-range accesses return zero without modifying LDS. Misaligned accesses fault the wave on gfx1035; the recompiler does not model this fault. Radeon 780M (gfx1103, RDNA3) measurements confirmed aligned return values, conditional bit clearing, EXEC masking and operand overlap in wave32 and wave64; contended cases passed per-dword conservation checks. Contended lane ordering and gfx1013 remain unmeasured; pair-wide indivisibility is not assumed, and concurrent 64-bit LDS atomics use a separate lock. GDS throws. VGPR pairs extending beyond v255 throw; allocated-VGPR EXEC nullification is not emulated. - [64-bit image atomics](../../../core/shader/recompiler/Translation/src/MemoryInstructions/ImageInstructions.cpp) (`image_atomic_*` with a two-dword data mask) need `VK_EXT_shader_image_atomic_int64` and act on 32_32 UINT, SINT and FLOAT surfaces through an R64_UINT storage view. 64-bit `image_atomic_inc`, `image_atomic_dec` and the float atomics throw, as does any other access in the same shader to an image it updates with 64-bit atomics. - [8_SRGB and 8_8_SRGB loads](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) on a device that cannot sample R8_SRGB or R8G8_SRGB return the sRGB EOTF rounded to nearest even at bf16 precision, as adriadam10 measured on RDNA2 (gfx1035) in #866; the PS5's gfx1013 is not measured. This is the path on a device that cannot blit R8 or RG8 to RGBA8, and with `APS5_SRGB_SHADER_DECODE=1`; otherwise a load of a sampled texture reads the RGBA8 sRGB copy and gets the device's own decode. -- [Samples and gathers](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) of 8_SRGB and 8_8_SRGB textures read an RGBA8 sRGB copy of the texture (libSceAgcDriver, [Texture.cpp](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp)) on a device that cannot sample R8_SRGB or R8G8_SRGB but can blit R8 or RG8 to RGBA8, so they use that device's own decode. They still throw on a device that cannot blit, and with `APS5_SRGB_SHADER_DECODE=1`. A multisampled or depth view of these formats gets no copy. +- [Samples and gathers](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) of 8_SRGB and 8_8_SRGB textures read an RGBA8 sRGB copy of the texture (libSceAgcDriver, [Texture.cpp](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp)) on a device that cannot sample R8_SRGB or R8G8_SRGB but can blit R8 or RG8 to RGBA8, so they use that device's own decode. They still throw on a device that cannot blit, and with `APS5_SRGB_SHADER_DECODE=1`. `APS5_SRGB_SHADER_DECODE=copy` also takes the copy on a device that samples these formats natively, so the copy can be tested on any device that can blit R8 or RG8 to RGBA8; on a device that cannot blit, it behaves like `APS5_SRGB_SHADER_DECODE=1`. A multisampled or depth view of these formats gets no copy. - [Bindless image table](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) entries of 8_SRGB and 8_8_SRGB textures read zeros, like converted formats, on a device that cannot sample R8_SRGB or R8G8_SRGB. - [Shader MODE register access](../../../core/shader/recompiler/Translation/src/RoundingMode.cpp) starts from the rounding fields of `GuestContext::floatMode` and follows immediate rounding writes (`s_round_mode`, `s_setreg_imm32_b32` on MODE bits 0-3) through the control flow graph; `s_getreg_b32` of those bits returns the tracked value. Only `v_cvt_f32_i32`, `v_cvt_f32_u32`, `v_ldexp_f32`, the f16 conversions and `v_ldexp_f16` (with f16 denormals kept) and the instructions that already read `FLOAT_MODE` follow a rounding mode set at run time; any other float instruction that runs while a directed rounding mode differs from the initial one throws at that instruction. Rounding bits written from an SGPR (`s_setreg_b32`) or that differ between the paths reaching an instruction throw where they are read or used. A shader without `FLOAT_MODE` metadata throws on any rounding change. Non-rounding register reads still throw; `s_setreg_b32` writes to other hardware registers are still ignored. Tests follow the public RDNA2/LLVM contract, not console measurements. - A [flat SRT read](../../../core/shader/recompiler/Optimization/src/SrtWalker/SrtDescriptorEvaluation.cpp) through a null or unmapped pointer loaded from memory raises a [BDA fault](../../../core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp) only if the program executes it, then stops or reads zero like a BDA read. From c81cfce7cf336eacdc3ee1a5d41348bde55c49f8 Mon Sep 17 00:00:00 2001 From: Dextroid17 <217507631+Dextroid17@users.noreply.github.com> Date: Sun, 18 Oct 2026 07:27:33 -0400 Subject: [PATCH 0985/1000] docs(environment): say APS5_SRGB_SHADER_DECODE=copy samples 8 and 8_8 sRGB through the copy --- docs/dev/ENVIRONMENT.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/ENVIRONMENT.md b/docs/dev/ENVIRONMENT.md index 27bf42846e..b615c34335 100644 --- a/docs/dev/ENVIRONMENT.md +++ b/docs/dev/ENVIRONMENT.md @@ -338,7 +338,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_SPANNED_WAVE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | spans a single-wave wave64 compute program over two full 32-lane subgroups | | `APS5_SPIRV_OPT` | string | `core/libs/prx/libSceAgcDriver/tests/SpirvOptimizer.cpp` | | selects the SPIR-V optimization pipeline: `full` restores the performance pipeline, `none` skips optimization | | `APS5_SRGB_ATTACHMENT_PROXY` | string | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | | forces the sRGB color-attachment proxy format; `1` forces it | -| `APS5_SRGB_SHADER_DECODE` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp` | | forces shader sRGB decode for the sRGB 8-bit formats | +| `APS5_SRGB_SHADER_DECODE` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp` | | `1` forces shader sRGB decode for the sRGB 8-bit formats; `copy` does the same, except that 8 and 8_8 sRGB sample an RGBA8 sRGB copy when the device can blit to RGBA8 | | `APS5_STAGING_POOL_MIB` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp` | | size in MiB of the device-local staging buffer pool; `0` disables the device tier | | `APS5_STALE_EVICT` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | | also evicts a proved-present dead image's pending layers on an access | | `APS5_STALL_WATCHDOG_SECS` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/SubmissionRing.cpp` | 0 | seconds a queue may go without progress before the submission rings are dumped; 0 disables the watchdog, at most 86400 | From b40e0bfb80906ecea2f074be16f40d0990ca394f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexej=20Szpev=C3=A1k?= <szpevakalexej@gmail.com> Date: Sat, 17 Oct 2026 15:26:05 +0200 Subject: [PATCH 0986/1000] fix(libkernel): report the accept queue length for a listening socket's read event A read event on a listening socket took its data from FIONREAD, which a listening socket refuses on Linux, so kevent and sceKernelAddReadEvent reported 0 connections while saying one was ready. FreeBSD 11.4 gives a listening socket filt_solisten, which reports so_qlen, the completed connections waiting for accept. Ready now checks SO_ACCEPTCONN and on Linux reads a TCP listener's accept backlog from TCP_INFO, where tcp_get_info puts it in tcpi_unacked; with no host call for the count (Windows, macOS, a listener that is not TCP) it reports 1, the lower bound the readiness already implies. --- core/libs/prx/libkernel/Socket/src/Udp.cpp | 17 +++++++++++++++++ docs/dev/debt/libkernel.md | 2 +- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index 5b79792d75..799c7b8b9b 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -14,6 +14,7 @@ #include <fcntl.h> #include <time.h> #include <netinet/in.h> +#include <netinet/tcp.h> #include <poll.h> #include <unistd.h> #include <cerrno> @@ -740,6 +741,20 @@ int GuestSockets::Stat(int descriptor, FileStat* status) { return 0; } +namespace { +std::int64_t ListenQueueLength(NativeSocket socket) { +#ifdef __linux__ + tcp_info info{}; + socklen_t length = sizeof(info); + if (getsockopt(socket, IPPROTO_TCP, TCP_INFO, &info, &length) == 0 && length >= offsetof(tcp_info, tcpi_unacked) + sizeof(info.tcpi_unacked)) + return info.tcpi_unacked; +#else + (void)socket; +#endif + return 1; +} +} + bool GuestSockets::Ready(int descriptor, bool write, std::int64_t* data, bool* eof) { std::shared_ptr<Socket> socket; { @@ -766,6 +781,8 @@ bool GuestSockets::Ready(int descriptor, bool write, std::int64_t* data, bool* e int queued = 0; if (*data > 0 && ::ioctl(socket->value, SIOCOUTQ, &queued) == 0) *data = std::max<std::int64_t>(0, *data - queued); #endif + } else if (Listening(*socket)) { + *data = ListenQueueLength(socket->value); } else { #ifdef _WIN32 unsigned long available = 0; diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 2eb1a7613f..baef0291c7 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -125,7 +125,7 @@ - [/dev/random and /dev/urandom](../../../core/libs/prx/libkernel/File/src/RandomDevice.cpp) (libkernel) opened for reading return a descriptor backed by the host null device whose reads are filled with host OS entropy (`getentropy`, `rand_s` on Windows); both paths behave the same, never block, and positioned reads ignore the offset (FreeBSD `random(4)`). Only the exact paths are recognised, `fstat`, `lseek` and `mmap` see the null device, and opening them for writing throws - [pthread_attr_getinheritsched, pthread_attr_setinheritsched, pthread_attr_getscope, pthread_attr_setscope, pthread_attr_getstackaddr](../../../core/libs/prx/libkernel/Pthread/Posix/Attr.cpp) (libkernel) follow FreeBSD 11 libthr ([thr_attr.c](https://github.com/freebsd/freebsd-src/blob/5ed7eb0d97ba4436218e810f61bd059acba984c2/lib/libthr/thread/thr_attr.c), [thr_create.c](https://github.com/freebsd/freebsd-src/blob/5ed7eb0d97ba4436218e810f61bd059acba984c2/lib/libthr/thread/thr_create.c)), not measured on the console: the scope defaults to `PTHREAD_SCOPE_SYSTEM` (2), only it and `PTHREAD_SCOPE_PROCESS` (0) are accepted and other values return `EINVAL`, and an inherit value other than `PTHREAD_INHERIT_SCHED` (4) or `PTHREAD_EXPLICIT_SCHED` (0) returns `ENOTSUP`. `pthread_attr_getstackaddr` returns the configured stack address pointer (null by default). A null output of `pthread_attr_getinheritsched` throws, since libthr writes through it unchecked. A thread keeps the scope it was created with, taken from the creating thread under `PTHREAD_INHERIT_SCHED`, and `pthread_attr_get_np` and `scePthreadAttrGet` report it, but every guest thread is a host thread at system scope, so process scope changes nothing - [Shared memory](../../../core/libs/prx/libkernel/File/src/SharedMemory.cpp) (libkernel) on Linux implements anonymous and named objects, bounded descriptor writes and seeks, and `shm_unlink` with effective read/write permission checks from FreeBSD 11. Named paths up to 1023 bytes use `/anyps5-shm-` plus SHA-1; collisions are not detected, and the namespace is shared across processes in the host mount namespace. Named creation applies the guest umask without changing the host mask. Linux procfs and `/dev/shm` are required. Operations use an advisory directory lock; external host operations bypass it, unrelated objects serialize, and descriptor operations require two spare host descriptors and compare device/inode with the initial descriptor identity on both the pinned-object and ordinary-file fallback paths. [Guest mappings](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) support shared and private mappings of these descriptors through the allocation tracker and pin a duplicate descriptor during validation and mapping; mapping requires one spare host descriptor; file-backed mappings use the write-watch scan fallback because aliases and descriptor writes bypass per-mapping tracking. Named objects and file-backed mappings on Windows remain unsupported. Invalid non-null name pointers, console behaviour and title compatibility remain unverified. -- [sceKernelAddReadEvent and sceKernelAddWriteEvent](../../../core/libs/prx/libkernel/Equeue/Equeue.cpp) (libkernel): a write event reports the free space of the socket's send buffer in `data`, as FreeBSD 11.4's [`filt_sowrite`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c#L3358-L3382) reports `sbspace`: on Linux the host's `SO_SNDBUF` less the bytes `SIOCOUTQ` counts as queued, which include the host's per-packet overhead, so the space shrinks by somewhat more than the bytes sent. Windows has no query for that space and macOS is not wired up, so both report the whole `SO_SNDBUF`, which does not shrink as data is queued. Only guest sockets are supported; other descriptors and a low watermark above 1 throw +- [sceKernelAddReadEvent and sceKernelAddWriteEvent](../../../core/libs/prx/libkernel/Equeue/Equeue.cpp) (libkernel): a read event on a listening socket, here and in `kevent`, reports the number of completed connections waiting to be accepted in `data`, as FreeBSD 11.4's [`filt_solisten`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c#L3384-L3392) reports `so_qlen`: on Linux a TCP listener's accept queue from `TCP_INFO` (`tcpi_unacked`, which Linux fills with the accept backlog for a listening socket), and elsewhere, or for a listener that is not TCP, 1 while a connection is waiting, as no host call reports that count. A write event reports the free space of the socket's send buffer in `data`, as FreeBSD 11.4's [`filt_sowrite`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c#L3358-L3382) reports `sbspace`: on Linux the host's `SO_SNDBUF` less the bytes `SIOCOUTQ` counts as queued, which include the host's per-packet overhead, so the space shrinks by somewhat more than the bytes sent. Windows has no query for that space and macOS is not wired up, so both report the whole `SO_SNDBUF`, which does not shrink as data is queued. Only guest sockets are supported; other descriptors and a low watermark above 1 throw - [close](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) and [setsockopt](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) reach a [libSceNet](../../../core/libs/prx/libSceNet/Export.cpp) socket through hooks libSceNet registers, as `select` polls them; the other libkernel socket calls do not. libSceNet numbers its sockets from 32 in its own table, so a CRT file descriptor and a socket can share a number; `close` then closes the socket. On the console both are one kernel descriptor space - [sceKernelPread, sceKernelPwrite, sceKernelPreadv, sceKernelPwritev](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) on Windows transfer at the given offset with an overlapped `ReadFile`/`WriteFile` and restore the descriptor's file pointer afterwards. Guest scalar transfers, positioned transfers, vectored transfers and seeks share a position lock across duplicated descriptors; a vector holds it for the whole operation. Independently opened descriptors and other processes do not share that lock, so vector transfers over their overlapping file ranges are not coordinated. Handles that are not disk files return `ESPIPE`. A pipe or console `readv` can return a short read when no more input is available; each iovec is transferred separately, so a failure after the first transfer returns the bytes already done - [sceKernelAioSubmitReadCommands, sceKernelAioSubmitWriteCommands and their Multiple variants](../../../core/libs/prx/libkernel/Aio/src/Aio.cpp) (libkernel) on Windows transfer through the same positioned path as `sceKernelPread`/`sceKernelPwrite` and restore the descriptor's file pointer afterwards; positioned transfers, plain transfers and seeks of one descriptor are serialized per descriptor (shared across duplicated descriptors), so a concurrent transfer observes a stable file pointer From 93ea8f2684ba4a93c7c4b75dbe609f52c8c21176 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexej=20Szpev=C3=A1k?= <szpevakalexej@gmail.com> Date: Sat, 17 Oct 2026 15:26:05 +0200 Subject: [PATCH 0987/1000] test(libkernel): check the queued connection count on a listening socket The listener read event now reports one connection after the first connect, and on Linux two once a second client is waiting, before both are accepted. --- core/libs/tests/GuestSocketEvents.cpp | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/core/libs/tests/GuestSocketEvents.cpp b/core/libs/tests/GuestSocketEvents.cpp index 94bf0656db..92048bd727 100644 --- a/core/libs/tests/GuestSocketEvents.cpp +++ b/core/libs/tests/GuestSocketEvents.cpp @@ -73,8 +73,22 @@ int main() { Require(events[0].ident == static_cast<std::uintptr_t>(listener)); Require(events[0].filter == EVFILT_READ); Require(events[0].udata == &marker); + Require(events[0].data == 1); + const int second = socket_nid_postfix(2, 1, 0); + Require(second >= 0 && connect_nid_postfix(second, address.data(), address.size()) == 0); +#ifdef __linux__ + const auto queued = std::chrono::steady_clock::now() + std::chrono::seconds(1); + do { + Require(sceKernelWaitEqueue(eq, events.data(), events.size(), &count, &timeout) == SCE_OK); + Require(count == 1 && events[0].ident == static_cast<std::uintptr_t>(listener)); + } while (events[0].data != 2 && std::chrono::steady_clock::now() < queued); + Require(events[0].data == 2); +#endif const int accepted = accept_nid_postfix(listener, nullptr, nullptr); Require(accepted >= 0); + const int acceptedSecond = accept_nid_postfix(listener, nullptr, nullptr); + Require(acceptedSecond >= 0); + Require(close_nid_postfix(acceptedSecond) == 0 && close_nid_postfix(second) == 0); Require(sceKernelDeleteReadEvent(eq, listener) == SCE_OK); Require(sceKernelDeleteReadEvent(eq, listener) == SCE_KERNEL_ERROR_ENOENT); From 7b3098714ea9aca276e414e364987e838263fd7d Mon Sep 17 00:00:00 2001 From: Alex Collini <pipodi93@gmail.com> Date: Sun, 18 Oct 2026 13:42:27 +0200 Subject: [PATCH 0988/1000] docs(agc): keep the title name out of the released-textures debt entry Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> --- docs/dev/debt/libSceAgcDriver.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 6c7838b646..70e0cc2887 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -172,7 +172,7 @@ - [Flip packets raise the graphics end-of-pipe event](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp) (libSceAgcDriver) - a flip delivers the queue's end-of-pipe event (`sceAgcDriverAddEqEvent`) once the work recorded before it completes, as a `RELEASE_MEM` with `INT_SEL` does. Minecraft (PPSA17221) registers the graphics event, never requests an interrupt in its releases, and waits on the event after every flip while holding its render lock, so without this it never presents a second frame. Whether the console raises the event from the flip itself or from an interrupt its flip sequence contains is not measured; a title that waits only on release interrupts sees one extra event per flip, with `data` counting it. - [Host import windows and budget](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver): a registered range larger than 64 MiB is imported one 64 MiB window at a time for buffer and fill accesses; an access across window edges imports the cells it touches, grown to the union of the range's imports it overlaps. An address-based build (`AcquireRegistered`) imports the range whole and replaces its windows. Live imports stay under a budget: `APS5_HOST_IMPORT_MIB`, else the larger of 6 GiB and half the physical memory, at most the largest host heap less 2 GiB (imports and type-3 mirrors share it; #167's rule). A new import beyond it retires the least recently used imports not used in the last 256 lookups; one that would not fit even then is refused before anything is retired, and imports made when a range is mapped never evict. A refused import is tried again at its next use. The 64 MiB window, the 256-lookup protection and the 2 GiB reserve are choices, not measured. - [Registration preparation at submit](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp) (libSceAgcDriver) - `Driver::Submit` waits for every registration-time shader preparation queued before the call, not only those of the shaders the commands use, so a submission behind a long queue of unrelated preparations waits for all of them on the submitting thread. Waiting there instead of in the queue worker keeps the GPU side of a submission short: a title that sees a submission take seconds treats the GPU as hung (Goat Simulator 3, PPSA04159, then stops submitting and its garbage collector fails a 10 s timeout). On the console shaders are compiled ahead of time, so neither wait exists; how long a title tolerates a slow submit call is not known. -- [Resource cache entries holding released textures](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - a sampled texture the texture cache drops while something else holds it, or makes without caching (`admitTexture` refusing a first use), is marked released, and `ResourceCache::Insert` refuses descriptor sets that hold one and removes the entries that do. The removal waits for the next insert: until then `Find` still returns those entries and their textures stay alive. One process-wide flag starts the sweep, so while the sampled cache is over its budget every insert scans all entries (1,024 at start, up to 16,384 after growth, or `APS5_RESOURCE_CACHE_ENTRIES`, each with its textures); that cost is not measured. When `ReleaseTextureCaches` runs on device memory exhaustion, the sampled textures these entries hold are only marked released and are freed at the next insert, so the immediate allocation retry gets no memory back from them. A draw or dispatch whose textures the cache refused builds its descriptor sets again on each use for as long as the cache stays full; the count bounds (`APS5_RESOURCE_CACHE_ENTRIES`, `APS5_RESOURCE_CACHE_TABLE_ELEMENTS`) still bound nothing by bytes, and sampled textures made outside the texture cache (the disabled cache, depth-surface copies) are never marked. Measured only on Ghost of Yōtei (PPSA26344) on one RTX 4090: with the default bounds, sampled textures held outside the cache peaked at 2,836 MiB (7,830 MiB before) and device-local heap 0 stayed under its 22.9 GB budget for the whole run (26,357 MiB before, which aborted with `VK_ERROR_OUT_OF_DEVICE_MEMORY` at the wolf scene). Whether the extra descriptor-set builds cost frame time elsewhere is not measured. +- [Resource cache entries holding released textures](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - a sampled texture the texture cache drops while something else holds it, or makes without caching (`admitTexture` refusing a first use), is marked released, and `ResourceCache::Insert` refuses descriptor sets that hold one and removes the entries that do. The removal waits for the next insert: until then `Find` still returns those entries and their textures stay alive. One process-wide flag starts the sweep, so while the sampled cache is over its budget every insert scans all entries (1,024 at start, up to 16,384 after growth, or `APS5_RESOURCE_CACHE_ENTRIES`, each with its textures); that cost is not measured. When `ReleaseTextureCaches` runs on device memory exhaustion, the sampled textures these entries hold are only marked released and are freed at the next insert, so the immediate allocation retry gets no memory back from them. A draw or dispatch whose textures the cache refused builds its descriptor sets again on each use for as long as the cache stays full; the count bounds (`APS5_RESOURCE_CACHE_ENTRIES`, `APS5_RESOURCE_CACHE_TABLE_ELEMENTS`) still bound nothing by bytes, and sampled textures made outside the texture cache (the disabled cache, depth-surface copies) are never marked. Measured on one title on one RTX 4090: with the default bounds, sampled textures held outside the cache peaked at 2,836 MiB (7,830 MiB before) and device-local heap 0 stayed under its 22.9 GB budget for the whole run (26,357 MiB before, which aborted with `VK_ERROR_OUT_OF_DEVICE_MEMORY` at the wolf scene). Whether the extra descriptor-set builds cost frame time elsewhere is not measured. - [Unclamped normalized color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - CB_COLOR_INFO targets with BLEND_CLAMP clear (bit 15) on UNORM and SNORM formats are accepted when blending is disabled (ENABLE clear or BLEND_BYPASS set). Vulkan converts unblended shader values and clamps to the normalized range automatically; blending into an unclamped normalized target remains unmodeled because Vulkan always clamps inputs prior to blending. - [Thick SW_4KB_S_X volumes](../../../core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp) use AMD addrlib's GFX10 non-RB+ 16-pipe `GFX10_SW_4K_S3_X_PATINFO` equations for 1, 2, 4, 8 and 16-byte elements, including packed mip tails. The layout follows [AMD PAL c5e80007](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/gfx10/gfx10addrlib.cpp); it has not been measured on console hardware. Other thick swizzle modes outside the supported families still throw. Mipmapped 3D color targets remain refused by the color-register decoder. From ca7ea10d30ee8bf1e883dc9a59a6b497d163b10a Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Sun, 18 Oct 2026 05:09:14 -0700 Subject: [PATCH 0989/1000] docs(environment): add the rows for the performance overlay switches --- docs/dev/ENVIRONMENT.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/dev/ENVIRONMENT.md b/docs/dev/ENVIRONMENT.md index 27bf42846e..70584350e4 100644 --- a/docs/dev/ENVIRONMENT.md +++ b/docs/dev/ENVIRONMENT.md @@ -65,6 +65,8 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | Name | Kind | Source | Default | Description | |------|------|--------|---------|-------------| | `ANYPS5_GPU` | string | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | restricts physical device selection to device names containing this case-insensitive substring | +| `ANYPS5_OVERLAY` | string | `core/libs/prx/libSceAgcDriver/Execution/src/PerformanceOverlay.cpp` | 0 | 1 shows the performance overlay; 0 or unset hides it, and any other value is refused | +| `ANYPS5_OVERLAY_POS` | string | `core/libs/prx/libSceAgcDriver/Execution/src/PerformanceOverlay.cpp` | tl | the overlay's corner: tl, tr, bl or br | | `ANYPS5_PRESENT_MODE` | string | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | fifo | selects the swapchain present mode: fifo, fifo_latest_ready, mailbox or immediate | | `ANYPS5_REQUIRE_DESCRIPTOR_BINDING_FLAGS` | presence | `core/libs/prx/libSceAgcDriver/tests/ComputePipelineReplay.cpp` | | fails the replay test instead of accepting a device that refuses descriptor binding flags | | `ANYPS5_REQUIRE_ROBUST2` | presence | `core/libs/prx/libSceAgcDriver/tests/execution/LdsMixedWidthAtomics.cpp` | | fails the LDS lock-budget test when the device does not enable robustBufferAccess2, instead of only printing it | From afe2141a7cca4d85ca0a062203eb7599db4b8781 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= <adriadam10@gmail.com> Date: Sun, 18 Oct 2026 14:40:02 +0200 Subject: [PATCH 0990/1000] ci(pages): deploy only when the site or the progress changes Co-Authored-By: Claude <noreply@anthropic.com> --- .github/workflows/pages-progress.yml | 40 ++++++++++++++++++++++++++++ .github/workflows/pages.yml | 11 ++++++++ docs/dev/PROGRESS.md | 2 +- 3 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/pages-progress.yml diff --git a/.github/workflows/pages-progress.yml b/.github/workflows/pages-progress.yml new file mode 100644 index 0000000000..88c1f81d5a --- /dev/null +++ b/.github/workflows/pages-progress.yml @@ -0,0 +1,40 @@ +name: Pages progress + +on: + push: + branches: [main] + paths: + - core/libs/prx/** + - core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaOpcode.hpp + +permissions: + contents: read + actions: write + pages: read + +concurrency: + group: pages-progress + cancel-in-progress: true + +jobs: + progress: + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@v7 + with: + persist-credentials: false + - name: Deploy Pages if the progress changed + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + run: | + python3 tools/progress.py "$RUNNER_TEMP/progress" + site=$(gh api "repos/$GH_REPO/pages" --jq .html_url) + if curl -fsSL "${site%/}/progress.json?$GITHUB_SHA" -o "$RUNNER_TEMP/deployed.json" && + cmp -s "$RUNNER_TEMP/deployed.json" "$RUNNER_TEMP/progress/progress.json"; then + echo "Progress unchanged, Pages not deployed" >> "$GITHUB_STEP_SUMMARY" + else + gh workflow run pages.yml --ref main + echo "Progress changed, Pages deployment dispatched" >> "$GITHUB_STEP_SUMMARY" + fi diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index d789d4de62..29891c5c96 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -3,6 +3,17 @@ name: Pages on: push: branches: [main] + paths: + - .github/workflows/pages.yml + - CMakeLists.txt + - core/relinker/** + - demo/web/** + - tools/build_web.py + - tools/nid_names.py + - tools/progress.py + - tools/progress_history.py + - tools/rdna_isa.txt + - tools/test_web.py workflow_dispatch: release: types: [published] diff --git a/docs/dev/PROGRESS.md b/docs/dev/PROGRESS.md index e66bf9fcad..b337ab4c98 100644 --- a/docs/dev/PROGRESS.md +++ b/docs/dev/PROGRESS.md @@ -1,6 +1,6 @@ # Progress reporting -[`tools/progress.py`](../../tools/progress.py) generates `progress.json`, HTML, SVG maps and badges. The [Progress workflow](../../.github/workflows/progress.yml) publishes them after pushes to `main`. The [Progress report workflow](../../.github/workflows/progress-report.yml) compares the base and head trees of pull requests. +[`tools/progress.py`](../../tools/progress.py) generates `progress.json`, HTML, SVG maps and badges. The [Pages workflow](../../.github/workflows/pages.yml) publishes them with the web converter after pushes to `main` that change the converter, the web page or these tools. The [Pages progress workflow](../../.github/workflows/pages-progress.yml) runs after pushes that change `core/libs/prx` or the opcode list, compares the generated `progress.json` with the deployed one and dispatches the Pages workflow only when they differ, so the history chart gains new days only when the progress changes. The [Progress report workflow](../../.github/workflows/progress-report.yml) compares the base and head trees of pull requests. ## Library counts From d10c1b43ba938bd9a4d22243d22057b47491543e Mon Sep 17 00:00:00 2001 From: mugurc <mceker@gmail.com> Date: Sat, 17 Oct 2026 18:01:16 +0300 Subject: [PATCH 0991/1000] fix(libc): keep the app0.zar state through exit handlers Every path resolution reaches the app0.zar state, which was a function-local static: it is destroyed during exit before the handlers registered ahead of its first use run, so a path resolved in such a handler locked a destroyed mutex. glibc lets that pass; macOS fails it with EINVAL, and guest_path_lifetime failed there. The state is now allocated once and never destroyed, as the working directory state beside it already is. --- core/libs/prx/libc/src/AppArchive.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libc/src/AppArchive.cpp b/core/libs/prx/libc/src/AppArchive.cpp index be3916a765..ba32feae07 100644 --- a/core/libs/prx/libc/src/AppArchive.cpp +++ b/core/libs/prx/libc/src/AppArchive.cpp @@ -22,8 +22,8 @@ struct Archive { }; Archive& State() { - static Archive archive; - return archive; + static auto* archive = new Archive; + return *archive; } bool SameComponent(std::string_view left, std::string_view right) { From 2d8d3b9dc15f28c6b1a7494e1b969fca9ff59003 Mon Sep 17 00:00:00 2001 From: babyhuey <7751607+babyhuey@users.noreply.github.com> Date: Wed, 14 Oct 2026 01:20:41 -0400 Subject: [PATCH 0992/1000] fix(libc): keep the host standard stream when a guest freopen of it fails --- core/libs/prx/libc/CMakeLists.txt | 3 +- core/libs/prx/libc/include/FileStream.hpp | 35 +++++++++++++++++++++++ core/libs/prx/libc/include/General.hpp | 1 + core/libs/prx/libc/src/FileAndHeap.cpp | 3 +- core/libs/prx/libc/src/General.cpp | 10 +++++++ core/libs/tests/GuestStandardStreams.cpp | 14 +++++++++ 6 files changed, 64 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libc/CMakeLists.txt b/core/libs/prx/libc/CMakeLists.txt index 7344519b44..23f2780f7f 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -463,8 +463,9 @@ if(BUILD_TESTING) configure_windows_unwind(guest_standard_stream_tests) add_test(NAME guest_standard_streams COMMAND guest_standard_stream_tests) add_test(NAME guest_standard_streams_closed_stdout COMMAND guest_standard_stream_tests --closed-stdout) + add_test(NAME guest_standard_streams_exclusive_stdout COMMAND guest_standard_stream_tests --exclusive-stdout) if(WIN32) - set_tests_properties(guest_standard_streams guest_standard_streams_closed_stdout PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(guest_standard_streams guest_standard_streams_closed_stdout guest_standard_streams_exclusive_stdout PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>") endif() add_test_executable(file_streams_tests ${LIBS_INCLUDE_DIR}/tests/FileStreams.cpp) diff --git a/core/libs/prx/libc/include/FileStream.hpp b/core/libs/prx/libc/include/FileStream.hpp index 0f51bc7d6d..a72d143805 100644 --- a/core/libs/prx/libc/include/FileStream.hpp +++ b/core/libs/prx/libc/include/FileStream.hpp @@ -1,6 +1,7 @@ #ifndef CORE_LIBS_PRX_LIBC_INCLUDE_FILESTREAM_HPP #define CORE_LIBS_PRX_LIBC_INCLUDE_FILESTREAM_HPP +#include <cerrno> #include <cstdio> #include <filesystem> #include <stdexcept> @@ -15,7 +16,10 @@ #include "prx/libc/include/FileLock.hpp" extern "C" void* HostStandardHandle_nid_no_patch(int descriptor); extern "C" void KeepStandardHandle_nid_no_patch(int descriptor, void* handle); +#else +#include <unistd.h> #endif +extern "C" int DuplicateHostDescriptor_nid_no_patch(int descriptor); struct GuestFilePrefix { unsigned char* position = nullptr; @@ -80,6 +84,31 @@ public: } GuestFilePrefix& GuestState() { return _guest; } + bool IsHostStandard() const { + return _handle == stdin || _handle == stdout || _handle == stderr; + } + + void Detach() { + _handle = nullptr; + _guest.flags = 0; + _guest.descriptor = -1; + } + + void RestoreHostStandard(std::FILE* stream, int kept) { + const int error = errno; +#ifdef _WIN32 + const bool restored = _handle || (::_wfreopen(L"NUL", stream == stdin ? L"r" : L"w", stream) && ::_dup2(kept, ::_fileno(stream)) == 0); + ::_unlock_file(stream); + ::_close(kept); +#else + const bool restored = _handle || (std::freopen("/dev/null", stream == stdin ? "r" : "w", stream) && ::dup2(kept, ::fileno(stream)) >= 0); + ::funlockfile(stream); + ::close(kept); +#endif + if (!restored) throw std::runtime_error("FileStream: cannot restore the host standard stream"); + errno = error; + } + bool Reopen(const std::filesystem::path& filename, const char* mode) { auto* previous = GetHandle(); _guest = {}; @@ -90,11 +119,17 @@ public: const int replaced = previous ? _fileno(previous) : -1; auto* const standard = HostStandardHandle_nid_no_patch(replaced); GuestFileLockForget_nid_no_patch(replaced); + const int kept = IsHostStandard() ? DuplicateHostDescriptor_nid_no_patch(replaced) : -1; + if (kept >= 0) ::_lock_file(previous); _handle = ::_wfreopen(filename.c_str(), wideMode.c_str(), previous); + if (kept >= 0) RestoreHostStandard(previous, kept); KeepStandardHandle_nid_no_patch(replaced, standard); #else if (::fileno(previous) == 2) KeepHostStandardError_nid_no_patch(); + const int kept = IsHostStandard() ? DuplicateHostDescriptor_nid_no_patch(::fileno(previous)) : -1; + if (kept >= 0) ::flockfile(previous); _handle = std::freopen(filename.c_str(), mode, previous); + if (kept >= 0) RestoreHostStandard(previous, kept); #endif if (!_handle) return false; _guest.flags = 0x10; diff --git a/core/libs/prx/libc/include/General.hpp b/core/libs/prx/libc/include/General.hpp index 34cee2743f..c60a1704fa 100644 --- a/core/libs/prx/libc/include/General.hpp +++ b/core/libs/prx/libc/include/General.hpp @@ -35,6 +35,7 @@ extern "C" void RecordWrittenPath_nid_no_patch(const std::filesystem::path& path extern "C" std::vector<std::filesystem::path> WrittenPaths_nid_no_patch(); extern "C" void SyncWrittenPaths_nid_no_patch(); extern "C" void BlockPathAlias_nid_no_patch(const char* guestPrefix); +extern "C" int DuplicateHostDescriptor_nid_no_patch(int descriptor); #ifdef _WIN32 extern "C" int WindowsOpenError_nid_no_patch(unsigned long error); extern "C" void* HostStandardHandle_nid_no_patch(int descriptor); diff --git a/core/libs/prx/libc/src/FileAndHeap.cpp b/core/libs/prx/libc/src/FileAndHeap.cpp index fa8e028a98..bd221c5d2b 100644 --- a/core/libs/prx/libc/src/FileAndHeap.cpp +++ b/core/libs/prx/libc/src/FileAndHeap.cpp @@ -283,7 +283,8 @@ FileStream* APS5_VABI freopen_nid_postfix(const char* filename, const char* mode std::filesystem::path path; if (*filename) { if (const int error = ResolvePathError_nid_no_patch(filename, path)) { - stream->Close(); + if (stream->IsHostStandard()) stream->Detach(); + else stream->Close(); if (stream->IsDynamic()) delete stream; errno = error; return nullptr; diff --git a/core/libs/prx/libc/src/General.cpp b/core/libs/prx/libc/src/General.cpp index d96670de58..75c5e8b50c 100644 --- a/core/libs/prx/libc/src/General.cpp +++ b/core/libs/prx/libc/src/General.cpp @@ -356,6 +356,10 @@ extern "C" int KeepHostStandardError_nid_no_patch() { extern "C" int HostStandardError_nid_no_patch() { return g_hostStandardError.load(); } + +extern "C" int DuplicateHostDescriptor_nid_no_patch(int descriptor) { + return ::fcntl(descriptor, F_DUPFD_CLOEXEC, 3); +} #endif extern "C" void AddPathAlias_nid_no_patch(const char* guestPrefix, const char* hostPath) { @@ -612,6 +616,12 @@ extern "C" void KeepStandardHandle_nid_no_patch(int descriptor, void* handle) { if (handle != nullptr && !SetStdHandle(StandardHandleId(descriptor), handle)) throw std::runtime_error("Cannot keep the host standard handle of descriptor " + std::to_string(descriptor)); } +extern "C" int DuplicateHostDescriptor_nid_no_patch(int descriptor) { + const auto previous = _set_thread_local_invalid_parameter_handler([](const wchar_t*, const wchar_t*, const wchar_t*, unsigned int, std::uintptr_t) {}); + const int kept = ::_dup(descriptor); + _set_thread_local_invalid_parameter_handler(previous); + return kept; +} #endif extern "C" [[noreturn]] void NotImplemented_nid_no_patch(const char* funcName) { diff --git a/core/libs/tests/GuestStandardStreams.cpp b/core/libs/tests/GuestStandardStreams.cpp index dca63ca158..0ba21b86b1 100644 --- a/core/libs/tests/GuestStandardStreams.cpp +++ b/core/libs/tests/GuestStandardStreams.cpp @@ -566,11 +566,25 @@ static int CheckClosedStandardOutput() { Require(puts_nid_postfix("text") == -1 && *__error_nid_postfix() == 9); Require(PrintStandard("%d", 1) == -1 && *__error_nid_postfix() == 9); Require(fputs_nid_postfix("text", __stdoutp_nid_postfix) == -1 && *__error_nid_postfix() == 9); + Require(std::fputs("host output\n", stdout) >= 0 && std::fflush(stdout) == 0 && !std::ferror(stdout)); + Require(freopen_nid_postfix("missing-closed-stream-dir/err.log", "a", __stderrp_nid_postfix) == nullptr); + Require(std::fputs("host error\n", stderr) >= 0 && std::fflush(stderr) == 0 && !std::ferror(stderr)); + return 0; +} + +static int CheckExclusiveStandardOutput() { + const auto filename = "anyps5-exclusive-stdout-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()); + Require(freopen_nid_postfix(filename.c_str(), "wx", __stdoutp_nid_postfix) == __stdoutp_nid_postfix); + Require(fputs_nid_postfix("exclusive\n", __stdoutp_nid_postfix) >= 0 && fflush_nid_postfix(__stdoutp_nid_postfix) == 0); + Require(fclose_nid_postfix(__stdoutp_nid_postfix) == 0); + Require(CheckFileBytes(filename, "exclusive\n", "wx", true)); + Require(std::filesystem::remove(filename)); return 0; } int main(int argc, char** argv) { if (argc > 1 && std::string_view(argv[1]) == "--closed-stdout") return CheckClosedStandardOutput(); + if (argc > 1 && std::string_view(argv[1]) == "--exclusive-stdout") return CheckExclusiveStandardOutput(); CheckClosedStreamWrites(); CheckDescriptorAccessModes(); Require(_Getmbcurmax_nid_postfix() == 1 && _Getmbcurmax_nid_postfix() == ___mb_cur_max_nid_postfix()); From 267cb56a0de35bf4050f14214494843740040e73 Mon Sep 17 00:00:00 2001 From: Karim Ibrahim <40270714+karimibrahim07@users.noreply.github.com> Date: Thu, 15 Oct 2026 15:46:36 -0400 Subject: [PATCH 0993/1000] fix(libkernel): report the precommitted stack of an adopted thread on Windows An adopted thread (the main thread, or a host thread that calls scePthreadSelf) reported its whole Windows stack reservation, and so did sceKernelIsStack on a host thread before adoption. A relinked executable reserves 0x210000 bytes for its main thread and commits the top 0x200000, so the lowest 64 KiB of the reported stack hold only reserved pages and Windows' guard pages. mprotect fails there, so HotSpot in Java 25 LAN / DNS (PPSA99869) could not create the main thread's stack guard pages ("Attempt to allocate stack guard pages failed"). When everything above the bottom allocation granule is committed read-write, the host stack is now reported from there, as created guest threads report only their committed stack. Other host stacks keep the whole range. The committed-pages walk is shared with StartNativeThread and throws when VirtualQuery fails. --- core/libs/prx/libkernel/CMakeLists.txt | 4 + .../libs/prx/libkernel/Pthread/src/Thread.cpp | 30 +++++-- core/libs/tests/GuestThreadSelf.cpp | 79 +++++++++++++++++++ docs/dev/debt/libkernel.md | 1 + 4 files changed, 107 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index f3627ea391..18593eada7 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -420,6 +420,10 @@ if(BUILD_TESTING) add_test_executable(guest_thread_self_tests ${LIBS_INCLUDE_DIR}/tests/GuestThreadSelf.cpp) target_include_directories(guest_thread_self_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_thread_self_tests PRIVATE libkernel libc) + if(WIN32) + target_compile_definitions(guest_thread_self_tests PRIVATE _WIN32_WINNT=0x0602) + target_link_options(guest_thread_self_tests PRIVATE "SHELL:-Xlinker --stack=0x210000,0x200000") + endif() configure_windows_unwind(guest_thread_self_tests) add_test(NAME guest_thread_self COMMAND guest_thread_self_tests) set_tests_properties(guest_thread_self PROPERTIES TIMEOUT 20) diff --git a/core/libs/prx/libkernel/Pthread/src/Thread.cpp b/core/libs/prx/libkernel/Pthread/src/Thread.cpp index fdbdb8a765..fe9aa84863 100644 --- a/core/libs/prx/libkernel/Pthread/src/Thread.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Thread.cpp @@ -172,12 +172,32 @@ static void FreeGuestStack(PthreadPrivate*) {} static void RetireGuestStack(PthreadPrivate*) {} #endif +#ifdef _WIN32 +static bool CommittedReadWrite(std::uintptr_t begin, std::uintptr_t end) { + for (auto cursor = begin; cursor < end;) { + MEMORY_BASIC_INFORMATION memory{}; + if (VirtualQuery(reinterpret_cast<const void*>(cursor), &memory, sizeof(memory)) != sizeof(memory) || memory.RegionSize == 0) + throw std::system_error(static_cast<int>(GetLastError()), std::system_category(), "Querying the host thread stack"); + if (memory.State != MEM_COMMIT || memory.Protect != PAGE_READWRITE) return false; + cursor = reinterpret_cast<std::uintptr_t>(memory.BaseAddress) + memory.RegionSize; + } + return true; +} + +static std::uintptr_t PrecommittedStackStart(std::uintptr_t low, std::uintptr_t high) { + SYSTEM_INFO system{}; + GetSystemInfo(&system); + const std::uintptr_t start = low + system.dwAllocationGranularity; + return start < high && CommittedReadWrite(start, high) ? start : low; +} +#endif + static bool HostStackLimits(std::uintptr_t* low, std::uintptr_t* high) { #ifdef _WIN32 ULONG_PTR lowLimit = 0; ULONG_PTR highLimit = 0; GetCurrentThreadStackLimits(&lowLimit, &highLimit); - *low = lowLimit; + *low = PrecommittedStackStart(lowLimit, highLimit); *high = highLimit; #else pthread_attr_t attr; @@ -674,12 +694,8 @@ static unsigned __stdcall StartNativeThread(void* opaque) { if (high <= low || high - low < self->stackSize) throw std::runtime_error("Cannot query guest thread stack"); self->stackAddress = reinterpret_cast<void*>(high - self->stackSize); - for (auto cursor = high - self->stackSize; cursor < high;) { - MEMORY_BASIC_INFORMATION memory{}; - if (VirtualQuery(reinterpret_cast<void*>(cursor), &memory, sizeof(memory)) != sizeof(memory) || memory.State != MEM_COMMIT || memory.Protect != PAGE_READWRITE || memory.RegionSize == 0) - throw std::runtime_error("Guest thread stack is not fully committed"); - cursor = reinterpret_cast<std::uintptr_t>(memory.BaseAddress) + memory.RegionSize; - } + if (!CommittedReadWrite(high - self->stackSize, high)) + throw std::runtime_error("Guest thread stack is not fully committed"); self->threadId = std::this_thread::get_id(); CurrentGuestState().current = self; if (!self->name.empty()) { diff --git a/core/libs/tests/GuestThreadSelf.cpp b/core/libs/tests/GuestThreadSelf.cpp index c94f557187..87e38f6c0a 100644 --- a/core/libs/tests/GuestThreadSelf.cpp +++ b/core/libs/tests/GuestThreadSelf.cpp @@ -2,6 +2,12 @@ #include <cstdint> #include <cstdlib> #include <cstring> +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include <windows.h> +#endif extern "C" { int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, PthreadEntry entry, void* arg, const char* name); @@ -31,6 +37,8 @@ int APS5_VABI scePthreadAttrDestroy(PthreadAttr* attr); int APS5_VABI scePthreadAttrGet(Pthread thread, PthreadAttr* attr); int APS5_VABI scePthreadAttrGetstackaddr(const PthreadAttr* attr, void** stack_addr); int APS5_VABI scePthreadAttrGetstacksize(const PthreadAttr* attr, size_t* stack_size); +int APS5_VABI mprotect_nid_postfix(void* address, std::size_t length, int protection) noexcept; +int APS5_VABI sceKernelIsStack(void* addr, void** start, void** end); } static bool StackContains(Pthread thread, const void* address) { @@ -58,6 +66,72 @@ static constexpr std::intptr_t WorkerRetval = 0x1234; static void Require(bool value) { if (!value) std::abort(); } +#ifdef _WIN32 +static constexpr std::uintptr_t GuestPage = 0x4000; +static constexpr int GuestProtNone = 0; +static constexpr int GuestProtReadWrite = 3; + +static void ReportedStack(Pthread thread, std::uintptr_t* begin, std::uintptr_t* end) { + PthreadAttr attr = nullptr; + Require(scePthreadAttrInit(&attr) == SCE_OK && scePthreadAttrGet(thread, &attr) == SCE_OK); + void* stack = nullptr; + size_t size = 0; + Require(scePthreadAttrGetstackaddr(&attr, &stack) == SCE_OK && scePthreadAttrGetstacksize(&attr, &size) == SCE_OK); + scePthreadAttrDestroy(&attr); + *begin = reinterpret_cast<std::uintptr_t>(stack); + *end = *begin + size; +} + +static void RequireGuardable(std::uintptr_t begin, std::uintptr_t end) { + Require(begin % GuestPage == 0 && end - begin >= GuestPage); + for (auto cursor = begin; cursor < end;) { + MEMORY_BASIC_INFORMATION memory{}; + Require(VirtualQuery(reinterpret_cast<const void*>(cursor), &memory, sizeof(memory)) == sizeof(memory)); + Require(memory.State == MEM_COMMIT && memory.Protect == PAGE_READWRITE); + cursor = reinterpret_cast<std::uintptr_t>(memory.BaseAddress) + memory.RegionSize; + } + auto* lowest = reinterpret_cast<void*>(begin); + Require(mprotect_nid_postfix(lowest, GuestPage, GuestProtReadWrite) == 0); + Require(mprotect_nid_postfix(lowest, GuestPage, GuestProtNone) == 0); + Require(mprotect_nid_postfix(lowest, GuestPage, GuestProtReadWrite) == 0); +} + +static void RequireGuardableStack(Pthread thread) { + std::uintptr_t begin = 0; + std::uintptr_t end = 0; + ReportedStack(thread, &begin, &end); + RequireGuardable(begin, end); +} + +static DWORD WINAPI DefaultSizeHostThread(void*) { + int local = 0; + void* start = nullptr; + void* end = nullptr; + Require(sceKernelIsStack(&local, &start, &end) == SCE_OK); + RequireGuardable(reinterpret_cast<std::uintptr_t>(start), reinterpret_cast<std::uintptr_t>(end)); + RequireGuardableStack(scePthreadSelf()); + return 0; +} + +static DWORD WINAPI PartlyCommittedHostThread(void*) { + ULONG_PTR low = 0; + ULONG_PTR high = 0; + GetCurrentThreadStackLimits(&low, &high); + std::uintptr_t begin = 0; + std::uintptr_t end = 0; + ReportedStack(scePthreadSelf(), &begin, &end); + Require(begin == low && end == high); + return 0; +} + +static void RunHostThread(LPTHREAD_START_ROUTINE routine, SIZE_T committed) { + const HANDLE thread = CreateThread(nullptr, committed, routine, nullptr, 0, nullptr); + Require(thread != nullptr); + Require(WaitForSingleObject(thread, INFINITE) == WAIT_OBJECT_0); + Require(CloseHandle(thread) != 0); +} +#endif + static std::int64_t ThreadIdField(Pthread thread) { std::int64_t tid = 0; std::memcpy(&tid, static_cast<const void*>(thread), sizeof(tid)); @@ -110,6 +184,11 @@ int main() { Require(pthread_getthreadid_np_nid_postfix() == static_cast<int>(mainTid)); int local = 0; Require(StackContains(mainSelf, &local)); +#ifdef _WIN32 + RequireGuardableStack(mainSelf); + RunHostThread(DefaultSizeHostThread, 0); + RunHostThread(PartlyCommittedHostThread, 0x10000); +#endif Require(scePthreadJoin(mainSelf, nullptr) == SCE_KERNEL_ERROR_EDEADLK); Require(pthread_join_nid_postfix(mainSelf, nullptr) == GUEST_EDEADLK); Require(scePthreadDetach(mainSelf) == SCE_KERNEL_ERROR_EINVAL); diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index baef0291c7..c9f4961568 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -135,6 +135,7 @@ - [getcontext and setcontext](../../../core/libs/prx/libkernel/System/src/UserContext.cpp) (libkernel) and [makecontext](../../../core/libs/prx/libSceLibcInternal/UserContext.cpp) (libSceLibcInternal) use FreeBSD 11's amd64 `ucontext_t` (`mcontext` at 0x10, `mc_len` 800, `uc_link` at 0x330, `uc_stack` at 0x338, 0x370 bytes), the layout homebrew built against FreeBSD's headers passes: PPSA99039's OpenSSL `ASYNC` (`crypto/async/arch/async_posix.c`) captures into a 0x370-byte stack buffer. `getcontext` writes the 0x330 bytes before `uc_link`, as FreeBSD's `UC_COPY_SIZE`. The console's own layout for these functions is not verified; the exception handler's context is a different one (see `sceKernelRaiseException`) - [getcontext and setcontext](../../../core/libs/prx/libkernel/System/src/UserContext.cpp) (libkernel) keep the 512-byte FXSAVE image in `mc_fpstate` (x87, MXCSR, XMM0-15), so the upper halves of YMM0-15 are neither saved nor restored, and `getcontext` leaves `mc_xfpustate` and `mc_xfpustate_len` 0. `getcontext` sets `mc_fpformat` and `mc_ownedfp` to FreeBSD's `_MC_FPFMT_XMM` and `_MC_FPOWNED_FPU`, `mc_cs` and `mc_ss` to the host selectors, and `mc_flags`, `mc_fsbase` and `mc_gsbase` to 0. `setcontext` loads `mc_fpstate` whatever `mc_fpformat` and `mc_ownedfp` say, and restores neither the segment selectors, `mc_fsbase` and `mc_gsbase` nor the extended state, which FreeBSD restores when `mc_flags` asks for them - [setcontext](../../../core/libs/prx/libkernel/System/src/UserContext.cpp) (libkernel) and [longjmp](../../../core/libs/prx/libc/src/JumpBuffer.cpp) (libc) on Windows set the thread's stack bounds in the TEB to the stack they switch to: a `makecontext` stack, or else the committed region around the target stack pointer. A switch to any other guest stack, such as a context built by hand, gets the bounds of that region only +- [scePthreadAttrGet, pthread_attr_get_np and sceKernelIsStack](../../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) report the stack of a host thread the library did not create, on Windows, from one allocation granule (64 KiB) above the bottom of its reservation when everything above that is committed read-write, as in a relinked executable's main thread and its default-size host threads (0x200000 committed of 0x210000 reserved), so the guest can protect the lowest reported pages, as HotSpot does for its stack guard pages in Java 25 LAN / DNS (PPSA99869). Any other such thread, and every such thread on Linux, gets its whole host stack: on Windows `mprotect` commits its reserved lower pages before protecting them and drops the guard flag of Windows' guard pages, and on Linux it cannot change the unmapped lower pages - [sceKernelInstallExceptionHandler](../../../core/libs/prx/libkernel/System/src/Exception.cpp) records the handler, but a host fault still ends the process: only signals raised with `sceKernelRaiseException` are delivered. Unity's PS5Util module installs a SIGUSR1 handler that reads `uc_mcontext.mc_rsp` (offset 0xf8 of the context) and raises SIGUSR1 on other threads to stop them for garbage collection - [sceKernelRaiseException](../../../core/libs/prx/libkernel/System/src/Exception.cpp) on Linux sends the host SIGUSR1, which the library takes over, to the target thread, so the handler runs wherever the thread was interrupted, emulator code included. On the console kernel objects are system calls and libthr defers signals while a thread holds its internal locks; here a handler that needs a host lock the interrupted code holds deadlocks, and a thread that the handler keeps waiting keeps its host locks. Host waits resume after the handler. The host calls Linux does not restart end early: sleeps that do not retry return before their time, and the poll of `sceNetEpollWait` and socket calls with a send or receive timeout return `EINTR` to the guest - [sceKernelRaiseException](../../../core/libs/prx/libkernel/System/src/Exception.cpp) builds the handler's `ucontext` with FreeBSD's amd64 layout (16-byte sigmask, `mcontext` at 0x40, `mc_rsp` at 0xf8); the console's layout is not verified beyond the fields PS5Util reads From 902b0515a2b8edfbb8ba9ccbcf2bac17916c6f6e Mon Sep 17 00:00:00 2001 From: MarcZX100 <marczxrr@gmail.com> Date: Wed, 14 Oct 2026 18:44:46 +0000 Subject: [PATCH 0994/1000] fix(agc): account recorded storage images in keep budget --- .../Execution/src/VulkanDevice.cpp | 9 +++++- .../Graphics/include/ShaderResources.hpp | 1 + .../Graphics/include/Texture.hpp | 1 + .../prx/libSceAgcDriver/Graphics/src/Draw.cpp | 6 ++++ .../Graphics/src/ShaderResources.cpp | 22 +++++++++++-- .../libSceAgcDriver/Graphics/src/Texture.cpp | 32 ++++++++++++------- 6 files changed, 56 insertions(+), 15 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index bdbb94ebe8..58e893e4cb 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -4554,6 +4554,7 @@ void VulkanDevice::recordDispatch(RecordedDispatch& record) { const auto commands = recorder.Commands(&covered); recordStep(PhaseRecordCommands); recorder.Keep(record.objects); + if (record.resources != nullptr) record.resources->KeepStorageImageBytes(recorder); recorder.Keep(record.resources); if (record.resources != nullptr) recorder.KeepBytes(record.resources.get(), record.resources->CopiedBytes()); if (record.bindings != nullptr) { @@ -4861,7 +4862,10 @@ VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::Rec static const bool deferEviction = std::getenv("APS5_NO_DEFERRED_EVICTION") == nullptr; std::vector<std::shared_ptr<Graphics::ShaderResources>> evicted; state->resourceCache.Insert(contentKey, resources, deferEviction ? &evicted : nullptr); - for (auto& object : evicted) state->recorder->Keep(std::move(object)); + for (auto& object : evicted) { + object->KeepStorageImageBytes(*state->recorder); + state->recorder->Keep(std::move(object)); + } if (profile) { timer.add(PhaseResourcesInsert, std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - insertStart).count()); if (arguments != 0) d.indirectHold.phaseCounts[PhaseResourcesInsert] += evicted.size(); @@ -4959,6 +4963,7 @@ VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::Rec state->resourceCache.Remove(contentKey, cached.get()); ++d.cacheInvalidated; // Off the mutex with its batch, as an evicted entry (insert()). + cached->KeepStorageImageBytes(*state->recorder); state->recorder->Keep(std::move(cached)); } if (profile) { @@ -5137,6 +5142,7 @@ RecipeOutcome VulkanDevice::DispatchRecipe(const ShaderRecompiler::RecompileResu // ordinary path runs with the prepared find (C10: the template goes to the batch). if (verify->cached.get() != hit->resources.get()) { counters.verifyReplaced.fetch_add(1, std::memory_order_relaxed); + hit->resources->KeepStorageImageBytes(*state->recorder); state->recorder->Keep(std::move(hit->resources)); return RecipeOutcome::Rebuild; } @@ -5173,6 +5179,7 @@ RecipeOutcome VulkanDevice::DispatchRecipe(const ShaderRecompiler::RecompileResu state->resourceCache.Remove(recipe.key, hit->resources.get()); ++d.cacheInvalidated; // Off the mutex with its batch, as an evicted entry. + hit->resources->KeepStorageImageBytes(*state->recorder); state->recorder->Keep(std::move(hit->resources)); counters.rebuilds[static_cast<std::size_t>(report.failure)].fetch_add(1, std::memory_order_relaxed); return RecipeOutcome::Rebuild; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp index a2a248e09f..9982792eb7 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp @@ -245,6 +245,7 @@ public: // ranges are fixed by the build), so a Revalidate's collects on the same worker are memo hits. void PrecollectSurfaces() const; bool Reusable() const { return reusable; } + void KeepStorageImageBytes(Recorder& recorder) const; const std::vector<std::uint64_t>& MultisampleSources() const { return multisampleSources; } static bool NeverReusable(std::span<const CompiledShader> shaders); static std::uint64_t DepthGuardRefusals(); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp index 13dc4cadca..df86e88f1a 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp @@ -359,6 +359,7 @@ public: static std::uint64_t RefreshesProved(); static std::uint64_t UntrackedCompares(); std::uint64_t GuestBytes() const; + std::uint64_t BudgetBytes() const; std::size_t OriginalSnapshotBytes() const { return original.capacity(); } VkDeviceSize AllocationBytes() const { return memoryBytes; } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index 8ef1050892..e7d0b91ff9 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -257,6 +257,7 @@ void recordCmaskClear(const Context& context, Recorder& recorder, const HostImpo if (AnyShadowedOverlaps(address, bytes)) PublishShadow(address, bytes, PublishScope::Whole, PublishReason::Keys); recorder.FlushKeyStoresOverlapping(address, bytes); const auto commands = recorder.Commands(); + if (resident != nullptr && !resident->Cached()) recorder.KeepBytes(resident.get(), static_cast<std::size_t>(resident->BudgetBytes())); recorder.Keep(resident); context.detiler->DispatchCmaskClear(commands, import.buffer, HostImportOffset(import, address), bytes, view, color.extent.width, color.extent.height, color.elementBytes, color.clearWords, color.dccAddress == 0); recorder.NotePendingWrite(address, bytes); @@ -1544,6 +1545,10 @@ void keepRecordedDraw(Recorder& recorder, const std::shared_ptr<ShaderResources> kept->scratch = std::move(scratch); kept->targets = std::move(targets); const auto& residents = kept->targets; + resources->KeepStorageImageBytes(recorder); + for (const auto& target : residents) { + if (target != nullptr && !target->Cached()) recorder.KeepBytes(target.get(), static_cast<std::size_t>(target->BudgetBytes())); + } recorder.Keep(kept); if (resources != nullptr) recorder.KeepBytes(resources.get(), resources->CopiedBytes()); if (checkRecords) recorder.OnComplete(std::move(checkRecords)); @@ -2678,6 +2683,7 @@ DrawRecipeOutcome DrawWithRecipe(const Context& context, const State& state, con if (!proved) { SharedResourceCache().Remove(recipe.key, resources.get()); countCache(&DrawProfile::cacheInvalidated); + resources->KeepStorageImageBytes(*recorder); recorder->Keep(std::move(resources)); timer.phase(PhaseLookup); return miss(DrawRecipeMiss::Proof); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index b8c914b8f4..19a7fb9545 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -432,7 +432,7 @@ void logLookup(const LookupRecord& record) { std::shared_ptr<StorageTexture> cachedStorageTexture(const Context& context, std::span<const std::uint32_t> words, const GuestTextureResource& resource, std::uint32_t mip, std::uint64_t guestBytes = 0, bool written = false); std::uint64_t heldBytes(const StorageTexture& image) { - return std::max<std::uint64_t>(image.AllocationBytes(), image.GuestBytes()); + return image.BudgetBytes(); } bool MetadataMoved(const StorageTexture& image, const GuestTextureResource& resource) { @@ -1045,7 +1045,10 @@ std::shared_ptr<StorageTexture> cachedStorageTexture(const Context& context, std std::vector<std::shared_ptr<ShaderResources>> resources; SharedResourceCache().RemoveImage(image.get(), &resources); if (auto* recorder = Recorder::Active(); recorder != nullptr && recorder->Recording()) { - for (auto& resource : resources) recorder->Keep(std::move(resource)); + for (auto& resource : resources) { + resource->KeepStorageImageBytes(*recorder); + recorder->Keep(std::move(resource)); + } } } if (!evicted.empty()) { @@ -1165,7 +1168,10 @@ void ClearCachedTextures(VkDevice device) { std::vector<std::shared_ptr<ShaderResources>> resources; SharedResourceCache().RemoveImage(image.get(), &resources); if (auto* recorder = Recorder::Active(); recorder != nullptr && recorder->Recording()) { - for (auto& resource : resources) recorder->Keep(std::move(resource)); + for (auto& resource : resources) { + resource->KeepStorageImageBytes(*recorder); + recorder->Keep(std::move(resource)); + } } } } @@ -1342,6 +1348,16 @@ bool SameAsPreviousStorageElement(const ShaderRecompiler::DescriptorBinding& bin ShaderResources::ShaderResources(const Context& context, const ShaderRecompiler::RecompileResult& vertex, const ShaderRecompiler::RecompileResult& fragment, const ColorTarget& target, std::uint64_t indexAddress, std::size_t indexBytes) : ShaderResources(context, std::array<CompiledShader, 2>{{{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, {ShaderRecompiler::ShaderStage::Fragment, &fragment, static_cast<std::uint32_t>(vertex.pushConstants.size())}}}, target, 1, indexAddress, indexBytes) {} +void ShaderResources::KeepStorageImageBytes(Recorder& recorder) const { + const auto keep = [&](const std::shared_ptr<StorageTexture>& image) { + if (image != nullptr && !image->Cached()) recorder.KeepBytes(image.get(), static_cast<std::size_t>(image->BudgetBytes())); + }; + for (const auto& image : storageTextures) keep(image); + for (const auto& texture : textures) { + if (texture != nullptr) keep(texture->SharedStorageSource()); + } +} + ShaderResources::ShaderResources(const Context& context, std::span<const CompiledShader> shaders, const ColorTarget& target, std::uint32_t colorAttachments, std::uint64_t indexAddress, std::size_t indexBytes, std::span<const GuestMemorySnapshot> snapshots) : context(context), guestMemory(context) { drawBuild = true; prepareAddressBindings(shaders, snapshots); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index 270d9ca0f8..9bc64c599a 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -64,6 +64,12 @@ TextureProfile& Profile() { return profile; } +void keepStorageTexture(Recorder& recorder, std::shared_ptr<StorageTexture> texture) { + if (texture == nullptr) return; + if (!texture->Cached()) recorder.KeepBytes(texture.get(), static_cast<std::size_t>(texture->BudgetBytes())); + recorder.Keep(std::move(texture)); +} + // APS5_PROFILE_DRAW, every 10 s on the [storage] line: storage image bytes uploaded by path // (recorded DCC clear, direct detile from the import, CPU) and written back by the reason of the // FlushPending that forced the write-back (the other callers name themselves: "refresh", "cache @@ -2305,7 +2311,7 @@ void StorageTexture::upload(const std::vector<bool>* layers) { if (recorder != nullptr) { commands = recorder->Commands(); timing = recorder->BeginGpuTiming(Recorder::CommandClass::DccClear); - if (auto self = weak_from_this().lock()) recorder->Keep(std::move(self)); + if (auto self = weak_from_this().lock()) keepStorageTexture(*recorder, std::move(self)); ++Profile().storageRecordedClears; Recorder::CountBarriers(Recorder::CommandClass::DccClear, 2); if (Recorder::BarrierValidate()) { @@ -2397,7 +2403,7 @@ void StorageTexture::upload(const std::vector<bool>* layers) { timing = recorder->BeginGpuTiming(Recorder::CommandClass::StorageUpload); if (linear) recorder->Keep(linear, linear->Size()); // The image itself must outlive the recorded copy: the cache may evict it right after. - if (auto self = weak_from_this().lock()) recorder->Keep(std::move(self)); + if (auto self = weak_from_this().lock()) keepStorageTexture(*recorder, std::move(self)); // The detile reads the tiled bytes from the import when the batch runs. recorder->NotePendingRead(descriptor.baseAddress, static_cast<std::size_t>(guestBytes), Recorder::ReadKind::StorageUpload); Recorder::CountBarriers(Recorder::CommandClass::StorageUpload, direct ? 2 : 3); @@ -2825,7 +2831,7 @@ std::uint64_t StorageTexture::uploadWindows(const HostImport& import, std::span< for (const auto& source : sources) { if (source.slab != nullptr) recorder->Keep(source.slab); } - if (auto self = weak_from_this().lock()) recorder->Keep(std::move(self)); + if (auto self = weak_from_this().lock()) keepStorageTexture(*recorder, std::move(self)); if (!reads.empty()) recorder->NotePendingReads(reads, Recorder::ReadKind::StorageUpload); Recorder::CountBarriers(Recorder::CommandClass::StorageUpload, direct ? 2 : 3); if (Recorder::BarrierValidate()) { @@ -3029,7 +3035,7 @@ std::uint64_t StorageTexture::writeBackWindows(std::span<const ImportSegment> se recorder->Keep(tiledScratch, tiledScratch->Size()); for (const auto& pieces : slabPieces) recorder->Keep(pieces.slab); for (const auto& slab : padding.slabs) recorder->Keep(slab); - if (auto self = weak_from_this().lock()) recorder->Keep(std::move(self)); + if (auto self = weak_from_this().lock()) keepStorageTexture(*recorder, std::move(self)); Recorder::CountBarriers(Recorder::CommandClass::StorageWriteBack, direct ? 3 : 4); if (Recorder::BarrierValidate()) { const std::pair<VkImage, bool> read{image, false}; @@ -4103,7 +4109,7 @@ bool StorageTexture::clearByKeysFill(DccKeys keys, std::uint8_t key) { } const auto commands = recorder->Commands(); const auto timing = recorder->BeginGpuTiming(Recorder::CommandClass::DccClear); - if (auto self = weak_from_this().lock()) recorder->Keep(std::move(self)); + if (auto self = weak_from_this().lock()) keepStorageTexture(*recorder, std::move(self)); Recorder::CountBarriers(Recorder::CommandClass::DccClear, 2); if (Recorder::BarrierValidate()) { const std::pair<VkImage, bool> clearedImage{image, true}; @@ -4238,7 +4244,7 @@ bool StorageTexture::FillClear(std::span<const std::uint32_t, 4> pattern, std::u const auto commands = recorder->Commands(); const auto timing = recorder->BeginGpuTiming(Recorder::CommandClass::FillClear); Recorder::CountBarriers(Recorder::CommandClass::FillClear, 2); - if (auto self = weak_from_this().lock()) recorder->Keep(std::move(self)); + if (auto self = weak_from_this().lock()) keepStorageTexture(*recorder, std::move(self)); if (Recorder::BarrierValidate()) { const std::pair<VkImage, bool> cleared{image, true}; recorder->NoteAccess(Recorder::CommandClass::FillClear, Recorder::Access{{}, {}, std::span(&cleared, 1), VK_PIPELINE_STAGE_TRANSFER_BIT}); @@ -4365,8 +4371,8 @@ bool StorageTexture::CopyFrom(StorageTexture& source, const char*& refusal) { const auto commands = recorder->Commands(); const auto timing = recorder->BeginGpuTiming(Recorder::CommandClass::Copy); Recorder::CountBarriers(Recorder::CommandClass::Copy, 2); - if (auto self = weak_from_this().lock()) recorder->Keep(std::move(self)); - if (auto other = source.weak_from_this().lock()) recorder->Keep(std::move(other)); + if (auto self = weak_from_this().lock()) keepStorageTexture(*recorder, std::move(self)); + if (auto other = source.weak_from_this().lock()) keepStorageTexture(*recorder, std::move(other)); if (Recorder::BarrierValidate()) { const std::array<std::pair<VkImage, bool>, 2> images{{{source.image, false}, {image, true}}}; recorder->NoteAccess(Recorder::CommandClass::Copy, Recorder::Access{{}, {}, images, VK_PIPELINE_STAGE_TRANSFER_BIT}); @@ -4543,8 +4549,8 @@ std::uint64_t StorageTexture::borrowUnits(StorageTexture& source, const std::vec if (recorder != nullptr) { commands = recorder->Commands(); timing = recorder->BeginGpuTiming(Recorder::CommandClass::StorageUpload); - if (auto self = weak_from_this().lock()) recorder->Keep(std::move(self)); - if (auto other = source.weak_from_this().lock()) recorder->Keep(std::move(other)); + if (auto self = weak_from_this().lock()) keepStorageTexture(*recorder, std::move(self)); + if (auto other = source.weak_from_this().lock()) keepStorageTexture(*recorder, std::move(other)); Recorder::CountBarriers(Recorder::CommandClass::StorageUpload, 2); if (Recorder::BarrierValidate()) { const std::array<std::pair<VkImage, bool>, 2> images{{{source.image, false}, {image, true}}}; @@ -4845,7 +4851,7 @@ void StorageTexture::writeBackLayers(const std::vector<bool>& layers) { recorder->Keep(tiledScratch, tiledScratch->Size()); for (const auto& slab : padding.slabs) recorder->Keep(slab); // The image itself must outlive the recorded retile: the cache may evict it right after. - if (auto self = weak_from_this().lock()) recorder->Keep(std::move(self)); + if (auto self = weak_from_this().lock()) keepStorageTexture(*recorder, std::move(self)); Recorder::CountBarriers(Recorder::CommandClass::StorageWriteBack, padding.copies.empty() ? 4 : 5); if (Recorder::BarrierValidate()) { const std::pair<VkImage, bool> read{image, false}; @@ -5124,6 +5130,10 @@ std::uint64_t StorageTexture::GuestBytes() const { return guestBytes; } +std::uint64_t StorageTexture::BudgetBytes() const { + return std::max<std::uint64_t>(AllocationBytes(), GuestBytes()); +} + VkImageView StorageTexture::View() const { return view; } From 27497f0474fc8f5e9df57ea031bf6b15b250ed00 Mon Sep 17 00:00:00 2001 From: MarcZX100 <marczxrr@gmail.com> Date: Wed, 14 Oct 2026 19:45:43 +0000 Subject: [PATCH 0995/1000] fix(agc): account for batch-retained evicted storage images --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 6 ++ .../Graphics/src/ShaderResources.cpp | 1 + .../tests/execution/StorageCacheBudget.cpp | 62 ++++++++++++++++++- 3 files changed, 66 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index fdca183551..2a0c0bf92d 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1958,6 +1958,12 @@ if(BUILD_TESTING) endif() endforeach() set_tests_properties(agc_driver_storage_cache_budget PROPERTIES TIMEOUT 30) + add_test(NAME agc_driver_storage_cache_retained_batch COMMAND agc_driver_storage_cache_budget_tests --retained-batch) + set_tests_properties(agc_driver_storage_cache_retained_batch PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + if(WIN32) + set_tests_properties(agc_driver_storage_cache_retained_batch PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>") + endif() add_test(NAME agc_driver_sampled_cache_budget_memory COMMAND agc_driver_sampled_cache_budget_tests memory) set_tests_properties(agc_driver_sampled_cache_budget_memory PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77) add_test(NAME agc_driver_resource_cache_storage_admission COMMAND agc_driver_sampled_cache_budget_tests resource-admission) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index 19a7fb9545..8e3a12d880 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -1045,6 +1045,7 @@ std::shared_ptr<StorageTexture> cachedStorageTexture(const Context& context, std std::vector<std::shared_ptr<ShaderResources>> resources; SharedResourceCache().RemoveImage(image.get(), &resources); if (auto* recorder = Recorder::Active(); recorder != nullptr && recorder->Recording()) { + if (image.use_count() > 1) recorder->KeepBytes(image.get(), static_cast<std::size_t>(heldBytes(*image))); for (auto& resource : resources) { resource->KeepStorageImageBytes(*recorder); recorder->Keep(std::move(resource)); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/StorageCacheBudget.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/StorageCacheBudget.cpp index 09c9c0b2da..a3a273ff50 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/StorageCacheBudget.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/StorageCacheBudget.cpp @@ -1,4 +1,6 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" #include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp" #include "prx/libc/include/GuestAllocations.hpp" #include "Recompiler.hpp" @@ -10,6 +12,7 @@ #include <cstdlib> #include <iostream> #include <memory> +#include <mutex> #include <span> #include <string> #include <vector> @@ -17,6 +20,7 @@ namespace { using AgcDriver::Graphics::Require; +using AgcDriver::Graphics::Recorder; using AgcDriver::Graphics::StorageTexture; using ShaderRecompiler::ShaderStage; @@ -51,8 +55,7 @@ std::array<std::uint32_t, 8> TextureDescriptor(std::uint64_t address) { }; } -std::shared_ptr<StorageTexture> Store(AgcDriver::VulkanDevice& device, std::uint64_t base, std::uint32_t surface, bool flush = true) { - const auto address = base + surface * Stride; +void Record(AgcDriver::VulkanDevice& device, std::uint64_t address) { std::vector<std::uint32_t> userData(16, 0u); const auto texture = TextureDescriptor(address); std::copy(texture.begin(), texture.end(), userData.begin() + 4); @@ -67,7 +70,13 @@ std::shared_ptr<StorageTexture> Store(AgcDriver::VulkanDevice& device, std::uint }; request.useCache = true; const auto result = ShaderRecompiler::Recompile(request); + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast<std::uintptr_t>(code.data())); +} + +std::shared_ptr<StorageTexture> Store(AgcDriver::VulkanDevice& device, std::uint64_t base, std::uint32_t surface, bool flush = true) { + const auto address = base + surface * Stride; + Record(device, address); device.WaitIdle(); if (!flush) return StorageTexture::FindLive(address, SurfaceBytes); StorageTexture::FlushPending(address, static_cast<std::size_t>(SurfaceBytes), nullptr, "test"); @@ -75,10 +84,57 @@ std::shared_ptr<StorageTexture> Store(AgcDriver::VulkanDevice& device, std::uint return StorageTexture::FindLive(address, SurfaceBytes); } +int RetainedBatch() { +#ifdef _WIN32 + Require(_putenv_s("APS5_TEXTURE_CACHE_MIB", "32") == 0, "cannot set the test cache budget"); + Require(_putenv_s("APS5_NO_RESOURCE_CACHE", "1") == 0, "cannot disable the resource cache for the retained-batch test"); +#else + Require(setenv("APS5_TEXTURE_CACHE_MIB", "32", 1) == 0, "cannot set the test cache budget"); + Require(setenv("APS5_NO_RESOURCE_CACHE", "1", 1) == 0, "cannot disable the resource cache for the retained-batch test"); +#endif + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + std::vector<std::uint32_t> storage((SurfaceBytes + MaxSurfaces * Stride + Stride) / 4u, 0u); + auto* texels = reinterpret_cast<std::uint32_t*>((reinterpret_cast<std::uintptr_t>(storage.data()) + Stride - 1u) & ~std::uintptr_t{Stride - 1u}); + const auto base = static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(texels)); + const auto blockBytes = static_cast<std::size_t>(SurfaceBytes + (MaxSurfaces - 1u) * Stride); + { + GuestAllocations::Mutation mutation; + mutation.Add(texels, blockBytes, true, true, true); + } + const auto release = [&] { + device->WaitIdle(); + AgcDriver::Graphics::ClearCachedTextures(device->Device()); + GuestAllocations::Mutation mutation; + mutation.Remove(texels); + }; + try { + Record(*device, base); + auto retained = StorageTexture::FindLive(base, SurfaceBytes); + Require(retained != nullptr, "surface 0 has no cached storage image"); + const auto retainedBytes = retained->BudgetBytes(); + auto* recorder = Recorder::Active(); + Require(recorder != nullptr && recorder->Recording(), "the surface dispatch was not left in an open batch"); + const auto beforeEviction = recorder->OpenKeptBytes(); + const auto capacity = static_cast<std::uint32_t>(TestBudget / retainedBytes); + Require(capacity + 1u < MaxSurfaces, "the eviction set exceeds the test storage"); + retained.reset(); + for (std::uint32_t surface = 1; surface <= capacity; ++surface) Record(*device, base + surface * Stride); + Require(StorageTexture::FindLive(base, SurfaceBytes) == nullptr, "surface 0 remained cached after the storage budget was exceeded"); + Require(recorder->OpenKeptBytes() >= beforeEviction + retainedBytes, "an evicted storage image held by a batch without a shared resource-cache entry was not charged to that batch"); + } catch (...) { + release(); + throw; + } + release(); + return 0; } -int main() { +} + +int main(int argc, char** argv) { try { + if (argc == 2 && std::string(argv[1]) == "--retained-batch") return RetainedBatch(); #ifdef _WIN32 Require(_putenv_s("APS5_TEXTURE_CACHE_MIB", "32") == 0, "cannot set the test cache budget"); #else From 75d0e9a7f4ca3b9ddb937ba84177f96cbfd81b07 Mon Sep 17 00:00:00 2001 From: MarcZX100 <MarcZX100@users.noreply.github.com> Date: Sun, 18 Oct 2026 15:46:12 +0000 Subject: [PATCH 0996/1000] test(videoout): correct first flip pacing assertion --- core/libs/prx/libSceVideoOut/tests/Flip.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libSceVideoOut/tests/Flip.cpp b/core/libs/prx/libSceVideoOut/tests/Flip.cpp index 0f991c0c3c..a7072e5e51 100644 --- a/core/libs/prx/libSceVideoOut/tests/Flip.cpp +++ b/core/libs/prx/libSceVideoOut/tests/Flip.cpp @@ -681,7 +681,7 @@ void testReleaseVblank() { const bool done = cfg->vblankCond.wait_for(lock, std::chrono::seconds(15), [&] { return cfg->failure || cfg->flipStatus.count == 1; }); if (cfg->failure) std::rethrow_exception(cfg->failure); check(done, "the flip did not complete"); - check(cfg->lastFlipVblank >= 3 && cfg->vblankStatus.count >= cfg->lastFlipVblank + 3, "the next flip is not paced from the vblank this flip was released at"); + check(cfg->lastFlipVblank >= 3, "the first flip was released before its configured interval"); } sceVideoOutUnregisterBuffers(handle, 0); sceVideoOutClose(handle); From 7e4ce1ab3d922ca27873f9eea583bfebec3aff21 Mon Sep 17 00:00:00 2001 From: Nahid Delic <nahe.delic84@gmail.com> Date: Wed, 14 Oct 2026 23:44:17 +0200 Subject: [PATCH 0997/1000] fix(shader): act on the containing dword for unaligned 32-bit buffer atomics The RDNA2 ISA (8.1.7) ignores the two low address bits of dword or wider non-formatted accesses in alignment mode DWORD. MXGP 26 (PPSA30839) sets bits with buffer_atomic_or at odd byte offsets, which recorded an unaligned BDA fault and dropped the atomic. A 32-bit buffer atomic now acts on the dword that holds its address; 64-bit atomics off an 8-byte boundary still fault. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- .../libSceAgcDriver/tests/ShaderMemory.cpp | 2 +- .../tests/execution/BufferAtomicUnaligned.cpp | 92 +++++++++++++++++++ .../SpirvBackend/src/SpirvRuntimeBuffer.cpp | 34 ++++--- docs/dev/debt/shader.md | 1 + 4 files changed, 114 insertions(+), 15 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/BufferAtomicUnaligned.cpp diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 818211166c..0527b07d62 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -4507,7 +4507,7 @@ void verifySpecializedBufferAtomicBindings() { } require(!physical, "specialized buffer atomic: the module addresses physical storage without an address access"); require(int64, "specialized buffer atomic: the fault address is 64-bit and the module does not declare Int64"); - require(faultFunction != 0 && faultCalls == 1, "specialized buffer atomic: an unaligned atomic does not record exactly one fault"); + require(faultCalls == 0, "specialized buffer atomic: a 32-bit atomic records an unaligned fault instead of acting on its dword"); const std::array<std::uint32_t, 1> shaderOnly{spv::CapabilityShader}; expectFailure([&] { static_cast<void>(compile(shaderOnly)); }, "buffer atomic fault addresses require shaderInt64", "specialized buffer atomic: the program compiled without shaderInt64"); } diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/BufferAtomicUnaligned.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/BufferAtomicUnaligned.cpp new file mode 100644 index 0000000000..0cfcfe9c2f --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/BufferAtomicUnaligned.cpp @@ -0,0 +1,92 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include <algorithm> +#include <array> +#include <cstdint> +#include <cstdio> +#include <iostream> +#include <string> +#include <vector> + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 32; +constexpr std::uint32_t Sentinel = 0x5eed0001u; +constexpr std::size_t OffsetWord = 2; +alignas(256) std::array<std::uint32_t, Threads + 1> Buffer{}; +alignas(256) std::array<std::uint32_t, Threads> Output{}; + +alignas(256) auto Code = std::to_array<std::uint32_t>({ + 0x34020082, + 0x7e040281, + 0xe0c85000, 0x80000201, + 0xbf8c3f70, + 0xe0701000, 0x80010201, + 0xbf810000, +}); + +std::array<std::uint32_t, 4> BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast<std::uintptr_t>(data); + return {static_cast<std::uint32_t>(address), static_cast<std::uint32_t>((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +void Run(AgcDriver::VulkanDevice& device, std::uint32_t offset) { + for (std::uint32_t lane = 0; lane < Threads; ++lane) Buffer[lane] = lane * 100u; + Buffer[Threads] = Sentinel; + Output.fill(0xdeadbeefu); + Code[OffsetWord] = 0xe0c85000u | offset; + std::vector<std::uint32_t> userData(8, 0u); + const auto buffer = BufferDescriptor(Buffer.data(), static_cast<std::uint32_t>(Buffer.size() * 4u)); + const auto output = BufferDescriptor(Output.data(), static_cast<std::uint32_t>(Output.size() * 4u)); + std::copy(buffer.begin(), buffer.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4); + const std::span<const std::uint32_t> code(Code); + const std::array<ShaderRecompiler::MemoryRegion, 1> memory{{{reinterpret_cast<std::uintptr_t>(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast<std::uintptr_t>(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast<std::uintptr_t>(code.data())); + device.WaitIdle(); +} + +void Check(std::uint32_t offset) { + const auto what = "buffer_atomic_add at byte offset " + std::to_string(offset) + ": lane "; + for (std::uint32_t lane = 0; lane < Threads; ++lane) { + Require(Output[lane] == lane * 100u, what + std::to_string(lane) + " returned " + std::to_string(Output[lane]) + ", expected " + std::to_string(lane * 100u)); + Require(Buffer[lane] == lane * 100u + 1u, what + std::to_string(lane) + " left dword " + std::to_string(Buffer[lane]) + ", expected " + std::to_string(lane * 100u + 1u)); + } + Require(Buffer[Threads] == Sentinel, what + "the dword after the last lane changed"); +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + if (!TargetHasCapability(device->Target(), spv::CapabilityInt64)) { + std::puts("skipped, the device has no shaderInt64"); + return VulkanTestSkipped; + } + for (std::uint32_t offset = 0; offset < 4u; ++offset) { + Run(*device, offset); + Check(offset); + } + std::puts("buffer atomic unaligned tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp index 7820a803c3..fa019cafce 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp @@ -811,33 +811,39 @@ std::uint32_t EmitRuntimeBufferAtomic(SpirvValueEmitContext& context, const IrVa if (!context.Memory(instruction).gpuDescriptor) { const auto storage = directAccess(context, instruction, bits == 64u); const auto byte = EmitAddU32(state, access.guest, storage.byteOffset); - const auto unaligned = nonzero(state, EmitAndConstant(state, byte, bits / 8u - 1u)); - const auto guest = Binary(state, spv::OpIAdd, TypeScalarU64(state), resource.base, Unary(state, spv::OpUConvert, TypeScalarU64(state), access.guest)); - EmitIfCondition(state, unaligned, [&] { - const auto high = Unary(state, spv::OpUConvert, TypeU32(state), Binary(state, spv::OpShiftRightLogical, TypeScalarU64(state), guest, BdaConstant(state, 32u))); - RecordBdaFaultWords(state, Unary(state, spv::OpUConvert, TypeU32(state), guest), high, ConstantU32(state, bits / 8u), BdaInstructionPc(state, instruction), BdaAbi::FaultReason::Unaligned); - }); - StopBdaInvocationIf(state, unaligned); const auto index = Binary(state, spv::OpShiftRightLogical, TypeU32(state), byte, ConstantU32(state, bits == 64u ? 3u : 2u)); - return EmitValueOrDefaultIfCondition(state, both(state, Unary(state, spv::OpLogicalNot, TypeBool(state), unaligned), EmitMemoryElementInBounds(state, storage, index)), type, zero, [&] { + auto valid = EmitMemoryElementInBounds(state, storage, index); + if (bits == 64u) { + const auto unaligned = nonzero(state, EmitAndConstant(state, byte, bits / 8u - 1u)); + const auto guest = Binary(state, spv::OpIAdd, TypeScalarU64(state), resource.base, Unary(state, spv::OpUConvert, TypeScalarU64(state), access.guest)); + EmitIfCondition(state, unaligned, [&] { + const auto high = Unary(state, spv::OpUConvert, TypeU32(state), Binary(state, spv::OpShiftRightLogical, TypeScalarU64(state), guest, BdaConstant(state, 32u))); + RecordBdaFaultWords(state, Unary(state, spv::OpUConvert, TypeU32(state), guest), high, ConstantU32(state, bits / 8u), BdaInstructionPc(state, instruction), BdaAbi::FaultReason::Unaligned); + }); + StopBdaInvocationIf(state, unaligned); + valid = both(state, Unary(state, spv::OpLogicalNot, TypeBool(state), unaligned), valid); + } + return EmitValueOrDefaultIfCondition(state, valid, type, zero, [&] { const auto pointer = EmitStorageBufferElementPointer(state, storage, index, bits == 64u ? TypeStorageBufferU64ElementPointer(state) : TypeStorageBufferElementPointer(state)); return operation(pointer); }); } - const auto unaligned = Binary(state, spv::OpINotEqual, TypeBool(state), Binary(state, spv::OpBitwiseAnd, TypeScalarU64(state), access.guest, BdaConstant(state, bits / 8u - 1u)), BdaConstant(state, 0u)); - faultIf(context, instruction, unaligned, access.guest, bits / 8u, BdaAbi::FaultReason::Unaligned); - return EmitValueOrDefaultIfCondition(state, Unary(state, spv::OpLogicalNot, TypeBool(state), unaligned), type, zero, [&] { + const auto atomicAt = [&](std::uint32_t guest) { const auto physical = state.module.AllocateId(); - state.module.AddFunction(spv::OpFunctionCall, TypeScalarU64(state), physical, state.bdaAtomicPointerFunction, access.guest, ConstantU32(state, bits / 8u), BdaInstructionPc(state, instruction)); + state.module.AddFunction(spv::OpFunctionCall, TypeScalarU64(state), physical, state.bdaAtomicPointerFunction, guest, ConstantU32(state, bits / 8u), BdaInstructionPc(state, instruction)); const auto mapped = Binary(state, spv::OpINotEqual, TypeBool(state), physical, BdaConstant(state, 0u)); return EmitValueOrDefaultIfCondition(state, mapped, type, zero, [&] { const auto pointer = state.module.AllocateId(); state.module.AddFunction(spv::OpConvertUToPtr, TypePointer(state, spv::StorageClassPhysicalStorageBuffer, scalar), pointer, physical); const auto result = operation(pointer); - state.module.AddFunction(spv::OpFunctionCall, state.module.Type(spv::OpTypeVoid), state.module.AllocateId(), state.bdaNoteWriteFunction, access.guest); + state.module.AddFunction(spv::OpFunctionCall, state.module.Type(spv::OpTypeVoid), state.module.AllocateId(), state.bdaNoteWriteFunction, guest); return result; }); - }); + }; + if (bits == 32u) return atomicAt(Binary(state, spv::OpBitwiseAnd, TypeScalarU64(state), access.guest, BdaConstant(state, ~std::uint64_t{3u}))); + const auto unaligned = Binary(state, spv::OpINotEqual, TypeBool(state), Binary(state, spv::OpBitwiseAnd, TypeScalarU64(state), access.guest, BdaConstant(state, bits / 8u - 1u)), BdaConstant(state, 0u)); + faultIf(context, instruction, unaligned, access.guest, bits / 8u, BdaAbi::FaultReason::Unaligned); + return EmitValueOrDefaultIfCondition(state, Unary(state, spv::OpLogicalNot, TypeBool(state), unaligned), type, zero, [&] { return atomicAt(access.guest); }); }); }); } diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index 28cd206d18..e65c058fc0 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -133,6 +133,7 @@ - [LDS float atomics](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (`ds_min/max/cmpst_f32`, `ds_min/max/cmpst_f64` and their `_rtn` forms) follow the hardware measured on RDNA2 (gfx1036) over 14 x 14 operand sets (zeros, ones, infinities, denormals, quiet and signalling NaNs of both signs and with payloads) in IEEE 0 and 1 with every value 0-3 of the type's denormal field: both operands are flushed to a signed zero when bit 0 of the field is clear (0 and 2; 1 and 3 keep them; the f32 field for the f32 forms, the f16/f64 field for the f64 forms; a shader without a known float mode flushes f32 and keeps f64), then a signalling-NaN data operand is stored quieted, else a signalling-NaN old value is stored quieted, else a quiet-NaN old value is replaced by the data, else a quiet-NaN data operand loses, else the ordered compare with -0 below +0 decides, and the flushed value is what is written, also when the old value wins. `cmpst` compares the flushed bits (both zeros equal, NaN never equal) and leaves memory unchanged when they differ. The returned value is always the old value as it was in memory. `IEEE_MODE` has no effect. The same bit rule serves the buffer float atomics (`AtomicFloatBits`). Not measured: a denormal or signalling-NaN `data1` of `cmpst` (stored as given). `ds_add_f32` and `ds_add_rtn_f32` follow RDNA1 (gfx1010, RX 5700 XT), measured with the [hardware oracle](../HW_ORACLE.md) template run through the Windows driver's comgr and HIP runtime over 1574 operand pairs (signed zeros, denormals, the smallest normals, infinities, quiet and signalling NaNs with payloads, the largest normals, random pairs near the denormal range and pairs that nearly cancel) in all 16 combinations of the f32 denormal field and the f32 round mode, and at IEEE 1 and DX10 clamp 0: both operands are flushed to a signed zero when bit 0 of the field is clear and the result when bit 1 is clear, the sum is rounded to nearest even whatever the round mode, a NaN data operand is stored quieted, else a NaN old value is stored quieted, `+inf + -inf` stores 0xffc00000, and `IEEE_MODE` and `DX10_CLAMP` have no effect. The sum is the host's `OpFAdd` between those flushes, so where the field keeps f32 denormals the result also depends on the host preserving them, which the module does not request. thuanquang measured the same rule on RDNA2 (gfx1031, RX 6700 XT) in #5492, with the same operand classes and 18 modes in wave32 and wave64; the PS5's gfx1013 is not measured. - [LDS in pixel, vertex-path and fetch programs](../../../core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp) (shader recompiler) - is an array private to each host invocation, in `Function` storage, as only compute and mesh programs get workgroup memory; on the console one LDS region is shared by the wave (RDNA 2 ISA sections 3.6.6 and 10.4), so a lane does not see another lane's LDS writes. Sub-dword writes (`ds_write_b8`, `ds_write_b16` and their `_d16_hi` forms), the 32-bit LDS atomics, `ds_append` and `ds_consume` load, update and store the invocation's own dword without an atomic, as Vulkan allows atomics only on `Uniform`, `Workgroup`, `Image`, `StorageBuffer`, `PhysicalStorageBuffer` and `TaskPayloadWorkgroupEXT` pointers (VUID-StandaloneSpirv-None-04686). `ds_append` and `ds_consume` update only the first active lane's copy and return its old value to every lane, so a second append after the first active lane changes, or a read of the counter by another lane, differs from the hardware. Not measured on hardware. - [V# bases off a DWORD boundary](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (shader recompiler) - raw accesses use the exact bytes and `s_buffer_load` reads `(base & ~3) + (offset & ~3)`, as measured on gfx1035 (Linux, alignment mode UNALIGNED; the console's mode is unknown). Formatted accesses off their element's alignment throw. Atomics use the byte address `base + offset`, base's low two bits included: measured on RDNA2 (gfx1031, Radeon RX 6700 XT, AMD Windows driver 32.0.21045.5002 through HIP) with `buffer_atomic_add`, `buffer_atomic_swap` and `buffer_atomic_cmpswap` and bases 0 to 3 bytes past a DWORD boundary with offsets 0 to 7, an address on a DWORD boundary updates that DWORD and returns its old value, and any other address stops the dispatch, which the recompiler reports as an unaligned access fault. +- [Unaligned 32-bit buffer atomics](../../../core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp) (shader recompiler) act on the dword that holds their address, as section 8.1.7 of the [RDNA2 ISA](https://gpuopen.com/download/rdna2-shader-instruction-set-architecture.pdf) describes for alignment mode DWORD ("the two LSBs of the byte-address are ignored, thus forcing Dword alignment"). The mode comes from `SH_MEM_CONFIG.alignment_mode`, and the console's mode is unknown. MXGP 26 (PPSA30839) sets bits with `buffer_atomic_or` at odd byte offsets, so the console is not in a strict mode, where section 3.11 reports a memory violation for a misaligned atomic. In mode UNALIGNED, which Linux KFD sets, a misaligned atomic stops the dispatch (measured on gfx1031 and gfx1036), so the console is not in that mode either. A 64-bit buffer atomic off an 8-byte boundary still records an unaligned BDA fault: mode DWORD would align it only to 4 bytes, which a host 64-bit atomic cannot address. Covered by [BufferAtomicUnaligned.cpp](../../../core/libs/prx/libSceAgcDriver/tests/execution/BufferAtomicUnaligned.cpp). - [32-bit bitfield masks](../../../core/shader/recompiler/Translation/src/IntegerInstructions.cpp) (`S_BFM_B32`, `V_BFM_B32`) use the truncating 32-bit shift specified by the [RDNA 1.0 ISA](https://www.amd.com/content/dam/amd/en/documents/radeon-tech-docs/instruction-set-architectures/rdna-shader-instruction-set-architecture.pdf), sections 12.1 (opcode 36) and 12.12 (opcode 867). Width and offset use their low five bits. This matches gfx1035 (Linux) for every width/offset pair, with and without upper operand bits set, and both leave SCC unchanged; not measured on the console. - [Vector buffer offsets that are not a multiple of 4](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (shader recompiler) - dword accesses count whole dwords from the V# base, where the hardware reads the exact bytes at base + offset. - [Raw `buffer_load_dwordx2`](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) is one 64-bit access on the hardware (with or without glc), so it never sees half of a `buffer_atomic_swap_x2`. Only a glc load that is 8-byte aligned and in bounds, in a program that has the 64-bit storage view (it uses 64-bit buffer atomics), is read with one 64-bit atomic load; every other dwordx2 load is two dword loads and can tear against a concurrent 64-bit write: loads without glc, and loads in a program without 64-bit buffer atomics while another shader writes the pair with `swap_x2`. From 96c692c887338e3e3ab02061845557e829372661 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= <adriadam10@gmail.com> Date: Sun, 18 Oct 2026 17:36:42 +0200 Subject: [PATCH 0998/1000] test(agc): expect 32-bit buffer atomics off a DWORD boundary to act on their containing dword Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> --- .../tests/execution/BufferUnalignedBase.cpp | 10 +++++++--- docs/dev/debt/shader.md | 2 +- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/BufferUnalignedBase.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/BufferUnalignedBase.cpp index c3520e8346..bc07a0cdbf 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/BufferUnalignedBase.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/BufferUnalignedBase.cpp @@ -244,9 +244,13 @@ void RunAtomic(AgcDriver::VulkanDevice& device, std::uint32_t misalignment) { for (std::uint32_t lane = 0; lane < Lanes; ++lane) Require(Output[128u + lane] == Fill, run + ": lane " + std::to_string(lane) + " returned " + Hex(Output[128u + lane]) + ", expected " + Hex(Fill)); } -void RunAtomicFault(AgcDriver::VulkanDevice& device) { +void RunAtomicContainingDword(AgcDriver::VulkanDevice& device) { DispatchAtomic(device, 2u, 0u); - for (std::uint32_t word = 0; word < Output.size(); ++word) Require(Output[word] == Fill, "buffer unaligned base atomic, view +2, offset lane * 4: dword " + std::to_string(word) + " is " + Hex(Output[word]) + ", but an atomic off a DWORD boundary faults the wave on RDNA2"); + for (std::uint32_t word = 0; word < 128u; ++word) { + const auto expected = word >= 1u && word < Lanes ? Fill + word + 1u : Fill; + Require(Output[word] == expected, "buffer unaligned base atomic, view +2, offset lane * 4: dword " + std::to_string(word) + " is " + Hex(Output[word]) + ", expected " + Hex(expected) + ": a 32-bit atomic off a DWORD boundary acts on its containing dword"); + } + for (std::uint32_t lane = 0; lane < Lanes; ++lane) Require(Output[128u + lane] == Fill, "buffer unaligned base atomic, view +2, offset lane * 4: lane " + std::to_string(lane) + " returned " + Hex(Output[128u + lane]) + ", expected " + Hex(Fill)); } } @@ -272,7 +276,7 @@ int main() { RunFormattedFault(*device, "tbuffer_load_format_x 32_UINT", Load32Code, view); RunFormattedFault(*device, "tbuffer_load_format_x 16_16_UINT", Load16_16Code, view); } - RunAtomicFault(*device); + RunAtomicContainingDword(*device); std::puts("buffer unaligned base tests passed"); return 0; } catch (const std::exception& error) { diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index e65c058fc0..24d4948928 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -133,7 +133,7 @@ - [LDS float atomics](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (`ds_min/max/cmpst_f32`, `ds_min/max/cmpst_f64` and their `_rtn` forms) follow the hardware measured on RDNA2 (gfx1036) over 14 x 14 operand sets (zeros, ones, infinities, denormals, quiet and signalling NaNs of both signs and with payloads) in IEEE 0 and 1 with every value 0-3 of the type's denormal field: both operands are flushed to a signed zero when bit 0 of the field is clear (0 and 2; 1 and 3 keep them; the f32 field for the f32 forms, the f16/f64 field for the f64 forms; a shader without a known float mode flushes f32 and keeps f64), then a signalling-NaN data operand is stored quieted, else a signalling-NaN old value is stored quieted, else a quiet-NaN old value is replaced by the data, else a quiet-NaN data operand loses, else the ordered compare with -0 below +0 decides, and the flushed value is what is written, also when the old value wins. `cmpst` compares the flushed bits (both zeros equal, NaN never equal) and leaves memory unchanged when they differ. The returned value is always the old value as it was in memory. `IEEE_MODE` has no effect. The same bit rule serves the buffer float atomics (`AtomicFloatBits`). Not measured: a denormal or signalling-NaN `data1` of `cmpst` (stored as given). `ds_add_f32` and `ds_add_rtn_f32` follow RDNA1 (gfx1010, RX 5700 XT), measured with the [hardware oracle](../HW_ORACLE.md) template run through the Windows driver's comgr and HIP runtime over 1574 operand pairs (signed zeros, denormals, the smallest normals, infinities, quiet and signalling NaNs with payloads, the largest normals, random pairs near the denormal range and pairs that nearly cancel) in all 16 combinations of the f32 denormal field and the f32 round mode, and at IEEE 1 and DX10 clamp 0: both operands are flushed to a signed zero when bit 0 of the field is clear and the result when bit 1 is clear, the sum is rounded to nearest even whatever the round mode, a NaN data operand is stored quieted, else a NaN old value is stored quieted, `+inf + -inf` stores 0xffc00000, and `IEEE_MODE` and `DX10_CLAMP` have no effect. The sum is the host's `OpFAdd` between those flushes, so where the field keeps f32 denormals the result also depends on the host preserving them, which the module does not request. thuanquang measured the same rule on RDNA2 (gfx1031, RX 6700 XT) in #5492, with the same operand classes and 18 modes in wave32 and wave64; the PS5's gfx1013 is not measured. - [LDS in pixel, vertex-path and fetch programs](../../../core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp) (shader recompiler) - is an array private to each host invocation, in `Function` storage, as only compute and mesh programs get workgroup memory; on the console one LDS region is shared by the wave (RDNA 2 ISA sections 3.6.6 and 10.4), so a lane does not see another lane's LDS writes. Sub-dword writes (`ds_write_b8`, `ds_write_b16` and their `_d16_hi` forms), the 32-bit LDS atomics, `ds_append` and `ds_consume` load, update and store the invocation's own dword without an atomic, as Vulkan allows atomics only on `Uniform`, `Workgroup`, `Image`, `StorageBuffer`, `PhysicalStorageBuffer` and `TaskPayloadWorkgroupEXT` pointers (VUID-StandaloneSpirv-None-04686). `ds_append` and `ds_consume` update only the first active lane's copy and return its old value to every lane, so a second append after the first active lane changes, or a read of the counter by another lane, differs from the hardware. Not measured on hardware. - [V# bases off a DWORD boundary](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (shader recompiler) - raw accesses use the exact bytes and `s_buffer_load` reads `(base & ~3) + (offset & ~3)`, as measured on gfx1035 (Linux, alignment mode UNALIGNED; the console's mode is unknown). Formatted accesses off their element's alignment throw. Atomics use the byte address `base + offset`, base's low two bits included: measured on RDNA2 (gfx1031, Radeon RX 6700 XT, AMD Windows driver 32.0.21045.5002 through HIP) with `buffer_atomic_add`, `buffer_atomic_swap` and `buffer_atomic_cmpswap` and bases 0 to 3 bytes past a DWORD boundary with offsets 0 to 7, an address on a DWORD boundary updates that DWORD and returns its old value, and any other address stops the dispatch, which the recompiler reports as an unaligned access fault. -- [Unaligned 32-bit buffer atomics](../../../core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp) (shader recompiler) act on the dword that holds their address, as section 8.1.7 of the [RDNA2 ISA](https://gpuopen.com/download/rdna2-shader-instruction-set-architecture.pdf) describes for alignment mode DWORD ("the two LSBs of the byte-address are ignored, thus forcing Dword alignment"). The mode comes from `SH_MEM_CONFIG.alignment_mode`, and the console's mode is unknown. MXGP 26 (PPSA30839) sets bits with `buffer_atomic_or` at odd byte offsets, so the console is not in a strict mode, where section 3.11 reports a memory violation for a misaligned atomic. In mode UNALIGNED, which Linux KFD sets, a misaligned atomic stops the dispatch (measured on gfx1031 and gfx1036), so the console is not in that mode either. A 64-bit buffer atomic off an 8-byte boundary still records an unaligned BDA fault: mode DWORD would align it only to 4 bytes, which a host 64-bit atomic cannot address. Covered by [BufferAtomicUnaligned.cpp](../../../core/libs/prx/libSceAgcDriver/tests/execution/BufferAtomicUnaligned.cpp). +- [Unaligned 32-bit buffer atomics](../../../core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp) (shader recompiler) act on the dword that holds their address, as section 8.1.7 of the [RDNA2 ISA](https://gpuopen.com/download/rdna2-shader-instruction-set-architecture.pdf) describes for alignment mode DWORD ("the two LSBs of the byte-address are ignored, thus forcing Dword alignment"). The mode comes from `SH_MEM_CONFIG.alignment_mode`, and the console's mode is unknown. MXGP 26 (PPSA30839) sets bits with `buffer_atomic_or` at odd byte offsets, so the console is not in a strict mode, where section 3.11 reports a memory violation for a misaligned atomic. In mode UNALIGNED, which Linux KFD sets, a misaligned atomic stops the dispatch (measured on gfx1031 and gfx1036), so the console is not in that mode either. A 64-bit buffer atomic off an 8-byte boundary still records an unaligned BDA fault: mode DWORD would align it only to 4 bytes, which a host 64-bit atomic cannot address. When the V# base itself is off a DWORD boundary and the containing dword lies before the first aligned dword the buffer view binds to, that atomic is out of bounds and returns 0 without writing. Covered by [BufferAtomicUnaligned.cpp](../../../core/libs/prx/libSceAgcDriver/tests/execution/BufferAtomicUnaligned.cpp). - [32-bit bitfield masks](../../../core/shader/recompiler/Translation/src/IntegerInstructions.cpp) (`S_BFM_B32`, `V_BFM_B32`) use the truncating 32-bit shift specified by the [RDNA 1.0 ISA](https://www.amd.com/content/dam/amd/en/documents/radeon-tech-docs/instruction-set-architectures/rdna-shader-instruction-set-architecture.pdf), sections 12.1 (opcode 36) and 12.12 (opcode 867). Width and offset use their low five bits. This matches gfx1035 (Linux) for every width/offset pair, with and without upper operand bits set, and both leave SCC unchanged; not measured on the console. - [Vector buffer offsets that are not a multiple of 4](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (shader recompiler) - dword accesses count whole dwords from the V# base, where the hardware reads the exact bytes at base + offset. - [Raw `buffer_load_dwordx2`](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) is one 64-bit access on the hardware (with or without glc), so it never sees half of a `buffer_atomic_swap_x2`. Only a glc load that is 8-byte aligned and in bounds, in a program that has the 64-bit storage view (it uses 64-bit buffer atomics), is read with one 64-bit atomic load; every other dwordx2 load is two dword loads and can tear against a concurrent 64-bit write: loads without glc, and loads in a program without 64-bit buffer atomics while another shader writes the pair with `swap_x2`. From bb39eba819c0e4e2ccd0467ab2a47378ebe9075b Mon Sep 17 00:00:00 2001 From: Maz <mazenshaar@hotmail.com> Date: Wed, 14 Oct 2026 14:55:34 +0400 Subject: [PATCH 0999/1000] fix(agc): decode COLOR_8_8 UINT as R8G8_UINT --- core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp | 1 + core/libs/prx/libSceAgcDriver/tests/Graphics.cpp | 12 ++++++++++++ docs/dev/debt/libSceAgcDriver.md | 1 + 3 files changed, 14 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index 38d058aff3..aa903ba8fa 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -797,6 +797,7 @@ DecodedColorFormat DecodeColorFormat(std::uint32_t format, std::uint32_t number, const auto mapping = static_cast<std::uint8_t>(alternate ? 0xecu : 0xe4u); if (number == unorm) return {VK_FORMAT_R8G8_UNORM, 2, mapping}; if (number == snorm) return {VK_FORMAT_R8G8_SNORM, 2, mapping}; + if (number == uint) return {VK_FORMAT_R8G8_UINT, 2, mapping}; return fail(); } case 4: diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index a047c301a7..efbc2854b8 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -2787,6 +2787,17 @@ void cmaskTests() { Require(texels(0x5a5a5a5au), "a refused pass changed the texels of a DCC target"); } +void uint8x2TargetTests() { + auto queue = makeState(); + queue.context[0x1c5] = 7; + queue.context[0x31c] = (queue.context[0x31c] & ~0x1ffcu) | (3u << 2u) | (4u << 8u); + const auto state = AgcDriver::Graphics::DecodeState(queue); + Require(state.color.format == VK_FORMAT_R8G8_UINT && state.color.elementBytes == 2 && state.color.componentMapping == 0xe4u && state.color.uintExport, "an 8_8 UINT color target was rejected"); + queue.context[0x31c] = (queue.context[0x31c] & ~0x1800u) | (1u << 11u); + const auto alt = AgcDriver::Graphics::DecodeState(queue); + Require(alt.color.format == VK_FORMAT_R8G8_UINT && alt.color.componentMapping == 0xecu, "an 8_8 UINT SWAP_ALT color target was rejected"); +} + void uint8x4TargetTests() { auto queue = makeState(); queue.context[0x1c5] = 7; @@ -8877,6 +8888,7 @@ int main(int argc, char** argv) { metadataPassTests(); cmaskTests(); uint16ExportTests(); + uint8x2TargetTests(); uint8x4TargetTests(); msaaEnableClearDepthTests(); multisampleTests(); diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 2c2cdc7f2c..0cf731ad3a 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -66,6 +66,7 @@ - [Per-face depth bias](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver): Vulkan has one depth bias per draw, so a draw that rasterizes both faces with different front and back bias (PA_SU_SC_MODE_CNTL, PA_SU_POLY_OFFSET_FRONT/BACK_*) is recorded twice, front faces with the back faces culled and then back faces with the front faces culled. The vertex stages run twice, so a vertex shader that writes memory writes twice, and all front faces land before all back faces instead of in primitive order, which differs only for blending or stencil updates that depend on the order. - [Draw recipes with depth targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver): direct draw recipes record and replay with active depth targets, matching the bound depth surface view by its target view identity; altered depth surface addresses or incompatible view handles trigger a recipe miss and pipeline rebuild. - [Partial rect-list primitives](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver): a rect-list draw whose vertex or index count is not a multiple of 3 draws its complete rectangles and leaves out the 1 or 2 vertices after them, because rect lists are drawn as Vulkan patch lists of 3 control points, which generate [⌊vertexCount/3⌋ patches](https://registry.khronos.org/vulkan/specs/latest/html/vkspec.html#drawing-patch-lists). What the console draws for the incomplete rectangle is not measured; Palworld (PPSA23126) draws 4 vertices × 9 instances as a rect list when its character creation opens. +- [8_8 UINT color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) decode as `R8G8_UINT` with the same component-swap mapping as 8_8 UNORM/SNORM (`0xe4` / `0xec`). Follows the existing 8 and 8_8_8_8 UINT paths; not measured on the console. RetroArch (PPSA99169) uses it during menu startup. - [Separate alpha blend into 8_8 SWAP_ALT targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp) (libSceAgcDriver): the second channel of a two-channel target with `COMP_SWAP` 1 holds the alpha export, but Vulkan blends it with the color equation of the R8G8 attachment, so a draw whose alpha equation differs from its color equation is recorded twice, the first channel with the color equation and the second with the alpha factors and operation moved into the color equation. The vertex stages run twice, so a vertex shader that writes memory writes twice, and occlusion counts include both passes. Draws that write depth or stencil, pixel shaders that write memory and factors of the destination alpha throw. That the hardware blends the second channel with the alpha equation is taken from Mesa's `si_translate_colorswap` and radeonsi's use of it for luminance-alpha formats, not measured on hardware. - [Unexported pixel inputs](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp) (libSceAgcDriver): a pixel input whose `SPI_PS_INPUT_CNTL` parameter the previous stage does not export is programmed as a default-value input (`OFFSET` bit 5, `DEFAULT_VAL` 0, FP16 high half too) and reads (0, 0, 0, 0), as radeonsi's `SI_PS_INPUT_CNTL_UNUSED` does and as rect-list draws already do. The console's value for an unwritten parameter cache entry is not measured on hardware. - [Per-sample pixel shading](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver): `PA_SC_MODE_CNTL_1.PS_ITER_SAMPLE` (bit 16, [PS5_Mesa GFX10.3 registers](https://github.com/mihawk-99/PS5_Mesa/blob/0b2d6d1a61d9bbf89cf8beb88a696144f67c61f8/src/amd/registers/gfx103.json)) on a multisampled draw enables Vulkan sample shading with a minimum rate of 1.0, so the pixel program runs once per sample. The draw throws when the device lacks `sampleRateShading`. On a single-sample draw the bit has no effect and is dropped. That the console runs the pixel program once per covered sample, and what it does with the other multisample registers in that mode, is taken from the register name and Vulkan's definition, not measured on hardware. From 57e52c1f0353a963f2e5b1a605357872bcfa021b Mon Sep 17 00:00:00 2001 From: Karim Masmoudi <125830216+Kernel-Hunter@users.noreply.github.com> Date: Wed, 14 Oct 2026 23:29:01 +0100 Subject: [PATCH 1000/1000] fix(relinker): report processor 0 from RDTSCP under --to-rosetta Rosetta sets RDTSCP's ECX to its constant TSC_AUX, 0xAAAAAAAB, which is not a processor index. --to-rosetta already makes RDPID return processor 0 for the same reason, so code reading the processor through RDTSCP saw a different value from code reading it through RDPID. --to-rosetta now moves RDTSCP into an optional stub that runs the instruction unchanged and then loads 0 into ECX with a MOV, which leaves the counter in EDX:EAX and the flags as they were. A site with no movable instruction after it for the jump is kept, as for RDPID. The Intel target and a plain relink are unchanged. --- .../x86/Amd64OnlySubstitutionTable.hpp | 1 + .../codegen/x86/DecodedInstruction.hpp | 1 + .../src/x86/Amd64OnlyInstructionMatcher.cpp | 15 ++++++- .../codegen/src/x86/DecodedInstruction.cpp | 17 +++++++ .../codegen/tests/Amd64OnlyConverterTests.cpp | 44 ++++++++++++++++++- docs/dev/debt/relinker.md | 2 +- docs/user/USAGE.md | 2 +- 7 files changed, 77 insertions(+), 5 deletions(-) diff --git a/core/relinker/codegen/include/codegen/x86/Amd64OnlySubstitutionTable.hpp b/core/relinker/codegen/include/codegen/x86/Amd64OnlySubstitutionTable.hpp index 41948aec85..f82d8a19f9 100644 --- a/core/relinker/codegen/include/codegen/x86/Amd64OnlySubstitutionTable.hpp +++ b/core/relinker/codegen/include/codegen/x86/Amd64OnlySubstitutionTable.hpp @@ -40,6 +40,7 @@ inline constexpr Entry kRdpru = {"RDPRU", nullptr, 0}; inline constexpr Entry kMcommit = {"MCOMMIT", nullptr, 0}; inline constexpr Entry kRdseed = {"RDSEED", nullptr, 0}; inline constexpr Entry kRdpid = {"RDPID", nullptr, 0}; +inline constexpr Entry kRdtscp = {"RDTSCP", nullptr, 0}; inline constexpr Entry kClwb = {"CLWB", nullptr, 0}; inline constexpr Entry kWideStore = {"256-bit store", nullptr, 0}; inline constexpr Entry kScalarCompare = {"scalar compare", nullptr, 0}; diff --git a/core/relinker/codegen/include/codegen/x86/DecodedInstruction.hpp b/core/relinker/codegen/include/codegen/x86/DecodedInstruction.hpp index 798128e36c..4176cc4cff 100644 --- a/core/relinker/codegen/include/codegen/x86/DecodedInstruction.hpp +++ b/core/relinker/codegen/include/codegen/x86/DecodedInstruction.hpp @@ -25,6 +25,7 @@ struct DecodedInstruction { [[nodiscard]] bool IsMovntsd() const; [[nodiscard]] bool IsRdseed() const; [[nodiscard]] bool IsRdpid() const; + [[nodiscard]] bool IsRdtscp() const; [[nodiscard]] bool IsClwb() const; private: diff --git a/core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp b/core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp index 7b6175bdff..c9128ecbaf 100644 --- a/core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp +++ b/core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp @@ -134,6 +134,13 @@ std::vector<std::uint8_t> _rdpidReplacement(const DecodedInstruction& instr) { return bytes; } +std::vector<std::uint8_t> _rdtscpReplacement(const DecodedInstruction& instr) { + std::vector<std::uint8_t> bytes(instr.Data, instr.Data + instr.Length); + bytes.push_back(0xB9); + bytes.insert(bytes.end(), 4, 0); + return bytes; +} + bool _validWait(const DecodedInstruction& instr) { const auto opcode = instr.Data + instr.OpcodeOffset(); return instr.Length <= kMaxInstructionLength && std::find(instr.Data, opcode, X64OpcodeConstants::PrefixLock) == opcode; @@ -229,6 +236,8 @@ std::optional<Amd64OnlyMatch> Amd64OnlyInstructionMatcher::_matchRosetta(const D return _inPlace(kClwb, instr.Length, {}); if (instr.IsRdpid()) return _trampoline(kRdpid.Name, instr.Length, _outOfLine(instr.Length, trailing, [&](StubBodyBuilder& body) { body.Raw(_rdpidReplacement(instr)); }), true); + if (instr.IsRdtscp()) + return _trampoline(kRdtscp.Name, instr.Length, _outOfLine(instr.Length, trailing, [&](StubBodyBuilder& body) { body.Raw(_rdtscpReplacement(instr)); }), true); if (const auto wide = DecodeVexWideStore(instr.Data, instr.Length)) return _trampoline(kWideStore.Name, instr.Length, _outOfLine(instr.Length, trailing, [&](StubBodyBuilder& body) { _wideStoreLowering.EmitOutOfLine(body, *wide); }), false, true); if (const auto compare = DecodeScalarCompare(instr.Data, instr.Length)) @@ -268,6 +277,10 @@ const char* Amd64OnlyInstructionMatcher::_emitOutOfLine(StubBodyBuilder& body, c EmitAndn(body, *andn); return "ANDN"; } + if (_target == Amd64OnlyTarget::Rosetta && instr.IsRdtscp()) { + body.Raw(_rdtscpReplacement(instr)); + return kRdtscp.Name; + } if (const auto wide = _target == Amd64OnlyTarget::Rosetta ? DecodeVexWideStore(instr.Data, instr.Length) : std::nullopt) { _wideStoreLowering.EmitOutOfLine(body, *wide); return kWideStore.Name; @@ -304,7 +317,7 @@ std::optional<Amd64OnlyMatch> Amd64OnlyInstructionMatcher::MatchSequence( } _moveTrailing(body, trailing); const DecodedInstruction first{instructions.front().data(), instructions.front().size()}; - const bool optional = DecodeReciprocal(first.Data, first.Length).has_value() || (_target == Amd64OnlyTarget::Rosetta && first.IsRdpid()); + const bool optional = DecodeReciprocal(first.Data, first.Length).has_value() || (_target == Amd64OnlyTarget::Rosetta && (first.IsRdpid() || first.IsRdtscp())); return _trampoline(name, instructions.front().size(), body.Finish(), optional); } diff --git a/core/relinker/codegen/src/x86/DecodedInstruction.cpp b/core/relinker/codegen/src/x86/DecodedInstruction.cpp index 3bdb906efe..d54ce541bc 100644 --- a/core/relinker/codegen/src/x86/DecodedInstruction.cpp +++ b/core/relinker/codegen/src/x86/DecodedInstruction.cpp @@ -292,6 +292,23 @@ bool DecodedInstruction::IsRdpid() const { ((modrm >> ModRmModShift) & ModRmModMask) == ModRmModRegister && ((modrm >> ModRmRegShift) & ModRmRegMask) == 7; } +bool DecodedInstruction::IsRdtscp() const { + bool operandSizeOverride = false; + bool repnePrefix = false; + bool repPrefix = false; + const std::size_t pos = _skipPrefixesAndRex(&operandSizeOverride, &repnePrefix, &repPrefix); + + if (operandSizeOverride || repnePrefix || repPrefix) { + return false; + } + + if (pos + 2 >= Length) { + return false; + } + + return Data[pos] == TwoByteOpcodeEscape && Data[pos + 1] == TwoByteGrp7 && Data[pos + 2] == 0xF9; +} + bool DecodedInstruction::IsClwb() const { bool operandSizeOverride = false; bool repnePrefix = false; diff --git a/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp b/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp index 6fbecaf06b..89bc80f07d 100644 --- a/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp +++ b/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp @@ -655,8 +655,8 @@ void rosettaSubstitutions() { const auto intel = Codegen::MakeAmd64OnlyInstructionMatcher(); const auto rosetta = Codegen::MakeAmd64OnlyInstructionMatcher(Codegen::Amd64OnlyTarget::Rosetta); const auto match = [](const auto& matcher, const Bytes& bytes) { return matcher->Match(bytes.data(), bytes.size()); }; - for (const auto& bytes : {Bytes{0x0F, 0xC7, 0xF8}, Bytes{0xF3, 0x0F, 0xC7, 0xF8}, Bytes{0x66, 0x0F, 0xAE, 0x30}}) - require(!match(intel, bytes).has_value(), "RDSEED, RDPID or CLWB was converted for an Intel host"); + for (const auto& bytes : {Bytes{0x0F, 0xC7, 0xF8}, Bytes{0xF3, 0x0F, 0xC7, 0xF8}, Bytes{0x66, 0x0F, 0xAE, 0x30}, Bytes{0x0F, 0x01, 0xF9}}) + require(!match(intel, bytes).has_value(), "RDSEED, RDPID, CLWB or RDTSCP was converted for an Intel host"); const auto rdseed = match(rosetta, {0x48, 0x0F, 0xC7, 0xF9}); require(rdseed && rdseed->Lowering == Amd64OnlyLowering::InPlace && rdseed->ReplacementBytes == Bytes{0x48, 0x0F, 0xC7, 0xF1} && rdseed->InstructionName == "RDSEED", "RDSEED was not rewritten to RDRAND"); const auto rdseed16 = match(rosetta, {0x66, 0x41, 0x0F, 0xC7, 0xFA}); @@ -669,6 +669,11 @@ void rosettaSubstitutions() { const auto rdpid = match(rosetta, {0xF3, 0x41, 0x0F, 0xC7, 0xFB}); require(rdpid && rdpid->Lowering == Amd64OnlyLowering::Trampoline && rdpid->Optional && rdpid->InstructionName == "RDPID", "RDPID was not lowered through an optional stub"); require(rdpid->ReturnBranchOffset == 6 && Bytes(rdpid->StubBody.begin(), rdpid->StubBody.begin() + 7) == Bytes{0x41, 0xBB, 0, 0, 0, 0, 0xE9}, "RDPID r11 stub does not load 0 before returning"); + const auto rdtscp = match(rosetta, {0x0F, 0x01, 0xF9}); + require(rdtscp && rdtscp->Lowering == Amd64OnlyLowering::Trampoline && rdtscp->Optional && rdtscp->InstructionName == "RDTSCP", "RDTSCP was not lowered through an optional stub"); + require(rdtscp->ReturnBranchOffset == 8 && Bytes(rdtscp->StubBody.begin(), rdtscp->StubBody.begin() + 9) == Bytes{0x0F, 0x01, 0xF9, 0xB9, 0, 0, 0, 0, 0xE9}, "RDTSCP stub does not load 0 into ECX after reading the counter"); + require(!match(rosetta, {0x0F, 0x01, 0xF8}).has_value(), "SWAPGS (0F 01 F8) was taken for RDTSCP"); + require(!match(rosetta, {0x66, 0x0F, 0x01, 0xF9}).has_value(), "RDTSCP with an operand size prefix was lowered"); } void wideStoreSplit() { @@ -1745,6 +1750,40 @@ void immediateFormExecution() {} void ripRelativeExecution() {} #endif +#if defined(__x86_64__) && (defined(__linux__) || defined(__APPLE__)) +void rdtscpExecution() { + const auto matcher = Codegen::MakeAmd64OnlyInstructionMatcher(Codegen::Amd64OnlyTarget::Rosetta); + const Bytes site = {0x0F, 0x01, 0xF9}; + const auto match = matcher->Match(site.data(), site.size()); + require(match && match->Lowering == Codegen::Amd64OnlyLowering::Trampoline, "RDTSCP stub was not produced"); + auto body = match->StubBody; + body[match->ReturnBranchOffset] = 0xC3; + void* code = mmap(nullptr, 4096, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + require(code != MAP_FAILED, "cannot map executable memory for the stub"); + std::memcpy(code, body.data(), body.size()); + std::uint64_t low = 0; + std::uint64_t high = 0; + std::uint64_t processor = 0; + std::uint8_t carry = 0; + asm volatile( + "mov $-1, %%rcx\n\t" + "sub $128, %%rsp\n\t" + "stc\n\t" + "call *%[code]\n\t" + "setc %[carry]\n\t" + "add $128, %%rsp\n\t" + : "=&a"(low), "=&d"(high), "=&c"(processor), [carry] "=&q"(carry) + : [code] "r"(code) + : "memory", "cc"); + munmap(code, 4096); + require(((high << 32) | low) != 0, "RDTSCP stub did not read the time stamp counter"); + require(processor == 0, "RDTSCP stub left a value other than processor 0 in RCX"); + require(carry == 1, "RDTSCP stub changed the flags"); +} +#else +void rdtscpExecution() {} +#endif + void scannerZeroTail() { const auto scanner = Codegen::MakeInstructionScanner(); const Bytes code{0xC3, 0x00, 0x00, 0x00}; @@ -1786,6 +1825,7 @@ int main() { converterRosettaScalarCompare(); rosettaLegacyScalarCompares(); converterRosettaLegacyScalarCompare(); + rdtscpExecution(); wideStoreSplit(); converterWideStoreWindows(); converterJumpTableTargets(); diff --git a/docs/dev/debt/relinker.md b/docs/dev/debt/relinker.md index 06478ff3d9..cd07d6bf0d 100644 --- a/docs/dev/debt/relinker.md +++ b/docs/dev/debt/relinker.md @@ -18,7 +18,7 @@ - In executables and guest modules with a `PT_SCE_DYNLIBDATA` segment, the [code analysis](../../../core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp) of the Windows and macOS outputs follows code only from the end of `PT_INTERP`, or of the `/libexec/ld-elf.so.1` string that begins the segment when no `PT_INTERP` covers it, as in OpenOrbis modules, to the end of the PLT after the last function of the unwind table, the end the pipeline's scans above use; a guest module with PLT relocations and no PLT after its last unwind function fails as the executable does. Code after that PLT is not analyzed: a TLS access in it is not rewritten, an entry point or pointer into it is ignored, and a function symbol or init/fini array entry pointing into it fails the conversion. - A relocation against a dynamic symbol entry that is all zero, like the null symbol, takes the value 0 plus its addend, as the gABI defines for `STN_UNDEF` ([pipeline](../../../core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp)). The Linux output imports such an entry as an unnamed weak symbol, which the host loader resolves to 0; the Windows and macOS outputs write the value into the slot ([relocation builder](../../../core/relinker/elfpatcher/src/windows/WindowsRelocationBuilder.cpp)). What the console's loader does with such an entry is not measured. OliSe Player (PPSA01153) has seven of them, and its code calls through those slots only when they are not 0 ([#4437](https://github.com/boykopovar/AnyPS5/issues/4437)). - The Linux placement of the `--to-intel` stubs in the [ELF patcher](../../../core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp) is covered only by a synthetic test. -- `--to-rosetta` lowers what Rosetta lacks on top of `--to-intel` ([matcher](../../../core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp)). AVX2, FMA, BMI1/2, F16C and MOVBE were observed to execute on an M5 Max without being reported through CPUID; this is not a guarantee for other Rosetta hosts. ANDN and RDRAND fault on an M1 running macOS 15.3.1 and execute on an M1 running macOS 26.6.2, so on the M1 the difference follows the macOS version; on that host BEXTR, BLSI, BLSMSK, BLSR, TZCNT, LZCNT, POPCNT, BZHI, MULX, PDEP, PEXT, RORX, SARX, SHLX, SHRX, MOVBE, FMA, AVX2 and F16C execute as well, its CPUID leaf 1 reports none of MOVBE, FMA, AVX or F16C, and RDSEED, RDPID, CLWB, ADCX and ADOX raise SIGILL. They also execute on an M5 running macOS 26.4, where the rest of BMI1 and BMI2, LZCNT, PREFETCHW, CLFLUSHOPT, AVX, AVX2, FMA, F16C and MOVBE execute as well although CPUID leaf 7 reads zero and leaves 1 and 0x80000001 report none of them, and where RDSEED, CLWB, RDPID, ADCX and ADOX raise an illegal instruction. ANDN is lowered, but RDRAND and other BMI instructions still depend on the host. RDSEED becomes RDRAND and CLWB a NOP. RDPID returns processor 0, because Rosetta's TSC_AUX is the constant 0xAAAAAAAB; the same value reaches RDTSCP's ECX, which is not converted. ADCX/ADOX fault under Rosetta and are not converted: compilers do not emit them, and code that checks CPUID sees no ADX. The conversions are covered by the converter tests. ANDN lowering is also executed by an x86-64 test build under Rosetta on the M1; the other conversions were originally checked on an M5 Max, outside this repository's CI. +- `--to-rosetta` lowers what Rosetta lacks on top of `--to-intel` ([matcher](../../../core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp)). AVX2, FMA, BMI1/2, F16C and MOVBE were observed to execute on an M5 Max without being reported through CPUID; this is not a guarantee for other Rosetta hosts. ANDN and RDRAND fault on an M1 running macOS 15.3.1 and execute on an M1 running macOS 26.6.2, so on the M1 the difference follows the macOS version; on that host BEXTR, BLSI, BLSMSK, BLSR, TZCNT, LZCNT, POPCNT, BZHI, MULX, PDEP, PEXT, RORX, SARX, SHLX, SHRX, MOVBE, FMA, AVX2 and F16C execute as well, its CPUID leaf 1 reports none of MOVBE, FMA, AVX or F16C, and RDSEED, RDPID, CLWB, ADCX and ADOX raise SIGILL. They also execute on an M5 running macOS 26.4, where the rest of BMI1 and BMI2, LZCNT, PREFETCHW, CLFLUSHOPT, AVX, AVX2, FMA, F16C and MOVBE execute as well although CPUID leaf 7 reads zero and leaves 1 and 0x80000001 report none of them, and where RDSEED, CLWB, RDPID, ADCX and ADOX raise an illegal instruction. ANDN is lowered, but RDRAND and other BMI instructions still depend on the host. RDSEED becomes RDRAND and CLWB a NOP. RDPID returns processor 0, and RDTSCP runs unchanged with ECX set to 0 after it, because Rosetta's TSC_AUX is the constant 0xAAAAAAAB (also measured on an M5, macOS 27.0.1). An RDPID or RDTSCP site with no movable instruction after it for the jump is kept; a kept RDTSCP reports 0xAAAAAAAB. ADCX/ADOX fault under Rosetta and are not converted: compilers do not emit them, and code that checks CPUID sees no ADX. The conversions are covered by the converter tests. ANDN lowering is also executed by an x86-64 test build under Rosetta on the M1; the other conversions were originally checked on an M5 Max, outside this repository's CI. - `--to-intel` replaces the approximate legacy SSE and VEX.128 register forms of RSQRTPS/RCPPS and VRSQRTPS/VRCPPS with [correctly rounded](../../../core/relinker/codegen/src/x86/ReciprocalLowering.cpp) 1/sqrt(x) and 1/x, not with the AMD approximation: its tables are not public. Intel's approximation of 1/sqrt(1.0) is 0x3F7FF000, and two Newton-Raphson steps on it settle on 0.99999994 instead of 1.0; a renormalised identity quaternion then turns into a NaN axis in PPSA21564, which the console does not show. Refined results now reach the exact value; unrefined ones can still differ from the console in the low bits. Each site costs an out-of-line stub (two jumps, a spill below the red zone, SQRTPS/DIVPS). Legacy SSE forms preserve the destination's upper YMM bits; VEX forms clear them. The 256-bit, scalar and memory forms keep the native instruction, as does a site shorter than 5 bytes with no movable instruction after it for the jump (5 sites in PPSA21564, counted before a following RIP-relative instruction could move into the stub; not recounted). - `--trace-imports` ([entry stub](../../../core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp)) exists for Windows output only. Only imports bound through a PLT slot, or through a GOT slot whose symbol is a function, are traced; a function reached through any other relocation, or looked up at run time with `sceKernelDlsym`, is not, and neither are weak imports or the imports of modules loaded at run time diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index 2138b71355..1a36a518f1 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -62,7 +62,7 @@ All switches are disabled by default. `unused-filter` defaults to `0`; `--rpath` | `--trace-imports` | Pass each function import through the libc.prx call tracer, see [tracing system library calls](#tracing-system-library-calls). Requires `--windows`; conflicts with `--lazy-binding`. | | `--linux-fixed-base <address>` | Load the Linux executable at this fixed address, given in decimal or hexadecimal (`0x`): every virtual address of the image is offset by it, so virtual address 0 is placed at the address, as the console places an executable. Requires a 64 KiB-aligned address from `0x10000` through `0x7fffffff0000` that is a multiple of every `PT_LOAD` alignment, and an image that ends in the user address space. The executable is written as `ET_EXEC`, without ASLR. Bundled modules keep their normal placement. Conflicts with `--windows`, `--macos` and `--lazy-binding`. | | `--to-intel` | Convert supported AMD-only instructions in the executable and bundled modules. Unsupported instructions or unreachable conversion stubs cause an error. | -| `--to-rosetta` | Convert as `--to-intel` does and also lower what Rosetta lacks: ANDN becomes scalar NOT/AND, RDSEED becomes RDRAND, CLWB a NOP, and RDPID returns processor 0. Implies `--to-intel`. Independent of `--macos`: `--macos` alone leaves these instructions as they are. Conflicts with `--windows`. | +| `--to-rosetta` | Convert as `--to-intel` does and also lower what Rosetta lacks: ANDN becomes scalar NOT/AND, RDSEED becomes RDRAND, CLWB a NOP, and RDPID and the ECX of RDTSCP return processor 0. Implies `--to-intel`. Independent of `--macos`: `--macos` alone leaves these instructions as they are. Conflicts with `--windows`. | | `unused-filter=0` | Keep all imported NID references. | | `unused-filter=1` | Filter unused non-PLT imports using control-flow and GOT access analysis, treating address-taken code as reachable; preserve PLT imports. | | `unused-filter=2` | Apply strict unused-import analysis and compact the PLT. Unsupported analysis cases cause an error. |